From 54d8f4872aed67ec8e19ea46a94be6d94d1f0070 Mon Sep 17 00:00:00 2001 From: Tyler French <66684063+tyler-french@users.noreply.github.com> Date: Tue, 23 Apr 2024 10:33:18 -0400 Subject: [PATCH] prepare rules_go v 0.47 (#3923) This PR prepares the `rules_go` release `v0.47.0`. A few tests are no longer applicable in the `x/tools` repo. --- MODULE.bazel | 2 +- go/def.bzl | 2 +- go/private/repositories.bzl | 68 +- tests/integration/popular_repos/BUILD.bazel | 4 - .../com_github_golang_mock-gazelle.patch | 77 +- .../org_golang_google_genproto-gazelle.patch | 46 +- ...g_google_grpc_cmd_protoc_gen_go_grpc.patch | 8 +- .../org_golang_google_protobuf-gazelle.patch | 322 +- third_party/org_golang_x_sys-gazelle.patch | 3 +- .../org_golang_x_tools-deletegopls.patch | 233534 ++++++++------- third_party/org_golang_x_tools-gazelle.patch | 1361 +- 11 files changed, 122076 insertions(+), 113351 deletions(-) diff --git a/MODULE.bazel b/MODULE.bazel index 92e5968aff..fddd15c428 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,6 +1,6 @@ module( name = "rules_go", - version = "0.46.0", + version = "0.47.0", compatibility_level = 0, repo_name = "io_bazel_rules_go", ) diff --git a/go/def.bzl b/go/def.bzl index bbf6a83ca8..0e6f5ed0ce 100644 --- a/go/def.bzl +++ b/go/def.bzl @@ -121,7 +121,7 @@ TOOLS_NOGO = [str(Label(l)) for l in _TOOLS_NOGO] # Current version or next version to be tagged. Gazelle and other tools may # check this to determine compatibility. -RULES_GO_VERSION = "0.46.0" +RULES_GO_VERSION = "0.47.0" go_context = _go_context gomock = _gomock diff --git a/go/private/repositories.bzl b/go/private/repositories.bzl index 094c103c06..934bf517de 100644 --- a/go/private/repositories.bzl +++ b/go/private/repositories.bzl @@ -51,7 +51,7 @@ def go_rules_dependencies(force = False): wrapper( http_archive, name = "bazel_skylib", - # 1.5.0, latest as of 2023-12-15 + # 1.5.0, latest as of 2024-04-19 urls = [ "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.5.0/bazel-skylib-1.5.0.tar.gz", "https://github.com/bazelbuild/bazel-skylib/releases/download/1.5.0/bazel-skylib-1.5.0.tar.gz", @@ -65,13 +65,13 @@ def go_rules_dependencies(force = False): wrapper( http_archive, name = "org_golang_x_tools", - # v0.15.0, latest as of 2023-11-16 + # v0.20.0, latest as of 2024-04-19 urls = [ - "https://mirror.bazel.build/github.com/golang/tools/archive/refs/tags/v0.15.0.zip", - "https://github.com/golang/tools/archive/refs/tags/v0.15.0.zip", + "https://mirror.bazel.build/github.com/golang/tools/archive/refs/tags/v0.20.0.zip", + "https://github.com/golang/tools/archive/refs/tags/v0.20.0.zip", ], - sha256 = "e76a03b11719138502c7fef44d5e1dc4469f8c2fcb2ee4a1d96fb09aaea13362", - strip_prefix = "tools-0.15.0", + sha256 = "06d29c1c1844c668bad5d57c755f5a11b99242f7a8ecd09c7b5f66aabeab32bc", + strip_prefix = "tools-0.20.0", patches = [ # deletegopls removes the gopls subdirectory. It contains a nested # module with additional dependencies. It's not needed by rules_go. @@ -106,13 +106,13 @@ def go_rules_dependencies(force = False): wrapper( http_archive, name = "org_golang_x_sys", - # v0.15.0, latest as of 2023-12-15 + # v0.19.0, latest as of 2024-04-19 urls = [ - "https://mirror.bazel.build/github.com/golang/sys/archive/refs/tags/v0.15.0.zip", - "https://github.com/golang/sys/archive/refs/tags/v0.15.0.zip", + "https://mirror.bazel.build/github.com/golang/sys/archive/refs/tags/v0.19.0.zip", + "https://github.com/golang/sys/archive/refs/tags/v0.19.0.zip", ], - sha256 = "36e7b6587b60eabebcd5102211ef5fabc6c6f40d93dd0db83dcefd13cdeb1b71", - strip_prefix = "sys-0.15.0", + sha256 = "a2fa1126030bf928b0ab559d2e985ea99fc974bf58a2d512f4e2e1a5303a57ae", + strip_prefix = "sys-0.19.0", patches = [ # releaser:patch-cmd gazelle -repo_root . -go_prefix golang.org/x/sys -go_naming_convention import_alias Label("//third_party:org_golang_x_sys-gazelle.patch"), @@ -125,7 +125,7 @@ def go_rules_dependencies(force = False): wrapper( http_archive, name = "org_golang_x_xerrors", - # master, as of 2023-12-15 + # master, as of 2024-04-19 urls = [ "https://mirror.bazel.build/github.com/golang/xerrors/archive/104605ab7028f4af38a8aff92ac848a51bd53c5d.zip", "https://github.com/golang/xerrors/archive/104605ab7028f4af38a8aff92ac848a51bd53c5d.zip", @@ -160,13 +160,13 @@ def go_rules_dependencies(force = False): wrapper( http_archive, name = "org_golang_google_protobuf", - sha256 = "f5d1f6d0e9b836aceb715f1df2dc065083a55b07ecec3b01b5e89d039b14da02", - # v1.31.0, latest as of 2023-12-15 + sha256 = "39a8bbfadaa3e71f9d7741d67ee60d69db40422dc531708a777259e594d923e3", + # v1.33.0, latest as of 2024-04-19 urls = [ - "https://mirror.bazel.build/github.com/protocolbuffers/protobuf-go/archive/refs/tags/v1.31.0.zip", - "https://github.com/protocolbuffers/protobuf-go/archive/refs/tags/v1.31.0.zip", + "https://mirror.bazel.build/github.com/protocolbuffers/protobuf-go/archive/refs/tags/v1.33.0.zip", + "https://github.com/protocolbuffers/protobuf-go/archive/refs/tags/v1.33.0.zip", ], - strip_prefix = "protobuf-go-1.31.0", + strip_prefix = "protobuf-go-1.33.0", patches = [ # releaser:patch-cmd gazelle -repo_root . -go_prefix google.golang.org/protobuf -go_naming_convention import_alias -proto disable_global Label("//third_party:org_golang_google_protobuf-gazelle.patch"), @@ -180,7 +180,7 @@ def go_rules_dependencies(force = False): http_archive, name = "org_golang_google_grpc_cmd_protoc_gen_go_grpc", sha256 = "1e84df03c94d1cded8e94da7a2df162463f3be4c7a94289d85c0871f14c7b8e3", - # cmd/protoc-gen-go-grpc/v1.3.0, latest as of 2023-12-13 + # cmd/protoc-gen-go-grpc/v1.3.0, latest as of 2024-04-19 urls = [ "https://mirror.bazel.build/github.com/grpc/grpc-go/archive/refs/tags/cmd/protoc-gen-go-grpc/v1.3.0.zip", "https://github.com/grpc/grpc-go/archive/refs/tags/cmd/protoc-gen-go-grpc/v1.3.0.zip", @@ -200,13 +200,13 @@ def go_rules_dependencies(force = False): wrapper( http_archive, name = "com_github_golang_protobuf", - # v1.5.3, latest as of 2023-12-15 + # v1.5.4, latest as of 2024-04-19 urls = [ - "https://mirror.bazel.build/github.com/golang/protobuf/archive/refs/tags/v1.5.3.zip", - "https://github.com/golang/protobuf/archive/refs/tags/v1.5.3.zip", + "https://mirror.bazel.build/github.com/golang/protobuf/archive/refs/tags/v1.5.4.zip", + "https://github.com/golang/protobuf/archive/refs/tags/v1.5.4.zip", ], - sha256 = "2dced4544ae5372281e20f1e48ca76368355a01b31353724718c4d6e3dcbb430", - strip_prefix = "protobuf-1.5.3", + sha256 = "9efeb4561ed4fbb9cefe97da407bb7b6247d4ed3dee4bfc2c24fc03dd4b5596d", + strip_prefix = "protobuf-1.5.4", patches = [ # releaser:patch-cmd gazelle -repo_root . -go_prefix github.com/golang/protobuf -go_naming_convention import_alias -proto disable_global Label("//third_party:com_github_golang_protobuf-gazelle.patch"), @@ -218,7 +218,7 @@ def go_rules_dependencies(force = False): wrapper( http_archive, name = "com_github_gogo_protobuf", - # v1.3.2, latest as of 2023-12-15 + # v1.3.2, latest as of 2024-04-19 urls = [ "https://mirror.bazel.build/github.com/gogo/protobuf/archive/refs/tags/v1.3.2.zip", "https://github.com/gogo/protobuf/archive/refs/tags/v1.3.2.zip", @@ -244,13 +244,13 @@ def go_rules_dependencies(force = False): wrapper( http_archive, name = "org_golang_google_genproto", - # main, as of 2023-12-15 + # main, as of 2024-04-19 urls = [ - "https://mirror.bazel.build/github.com/googleapis/go-genproto/archive/995d672761c0c5b9ac6127b488b48825f9a2e5fb.zip", - "https://github.com/googleapis/go-genproto/archive/995d672761c0c5b9ac6127b488b48825f9a2e5fb.zip", + "https://mirror.bazel.build/github.com/googleapis/go-genproto/archive/8c6c420018be7d99c6336d84554d856523d514bf.zip", + "https://github.com/googleapis/go-genproto/archive/8c6c420018be7d99c6336d84554d856523d514bf.zip", ], - sha256 = "14164722fe3c601a0515a911b319a4d6a397f96ee74d9c12b57e5b5501f8cb48", - strip_prefix = "go-genproto-995d672761c0c5b9ac6127b488b48825f9a2e5fb", + sha256 = "eddb238f5a61df730989baa6e0303666af3adf2835f226185e6c64f2958855c9", + strip_prefix = "go-genproto-8c6c420018be7d99c6336d84554d856523d514bf", patches = [ # releaser:patch-cmd gazelle -repo_root . -go_prefix google.golang.org/genproto -go_naming_convention import_alias -proto disable_global Label("//third_party:org_golang_google_genproto-gazelle.patch"), @@ -262,18 +262,18 @@ def go_rules_dependencies(force = False): _maybe( http_archive, name = "com_github_golang_mock", - # v1.7.0-rc.1, latest as of 2023-12-18 + # v1.6.0, latest as of 2024-04-19 urls = [ - "https://mirror.bazel.build/github.com/golang/mock/archive/refs/tags/v1.7.0-rc.1.zip", - "https://github.com/golang/mock/archive/refs/tags/v1.7.0-rc.1.zip", + "https://mirror.bazel.build/github.com/golang/mock/archive/refs/tags/v1.6.0.zip", + "https://github.com/golang/mock/archive/refs/tags/v1.6.0.zip", ], patches = [ # releaser:patch-cmd gazelle -repo_root . -go_prefix github.com/golang/mock -go_naming_convention import_alias Label("//third_party:com_github_golang_mock-gazelle.patch"), ], patch_args = ["-p1"], - sha256 = "5359c78b0c1649cf7beb3b48ff8b1d1aaf0243b22ea4789aba94805280075d8e", - strip_prefix = "mock-1.7.0-rc.1", + sha256 = "604d9ab25b07d60c1b8ba6d3ea2e66873138edeed2e561c5358de804ea421a0e", + strip_prefix = "mock-1.6.0", ) # This may be overridden by go_register_toolchains, but it's not mandatory diff --git a/tests/integration/popular_repos/BUILD.bazel b/tests/integration/popular_repos/BUILD.bazel index 93f4f94747..34db3c79b6 100644 --- a/tests/integration/popular_repos/BUILD.bazel +++ b/tests/integration/popular_repos/BUILD.bazel @@ -162,7 +162,6 @@ test_suite( "@org_golang_x_tools//benchmark/parse:parse_test", "@org_golang_x_tools//cmd/benchcmp:benchcmp_test", "@org_golang_x_tools//cmd/digraph:digraph_test", - "@org_golang_x_tools//cmd/getgo:getgo_test", "@org_golang_x_tools//cmd/go-contrib-init:go-contrib-init_test", "@org_golang_x_tools//cmd/splitdwarf/internal/macho:macho_test", "@org_golang_x_tools//cover:cover_test", @@ -173,7 +172,6 @@ test_suite( "@org_golang_x_tools//go/callgraph:callgraph_test", "@org_golang_x_tools//go/callgraph/static:static_test", "@org_golang_x_tools//go/callgraph/vta/internal/trie:trie_test", - "@org_golang_x_tools//go/cfg:cfg_test", "@org_golang_x_tools//godoc/redirect:redirect_test", "@org_golang_x_tools//godoc/vfs:vfs_test", "@org_golang_x_tools//godoc/vfs/gatefs:gatefs_test", @@ -192,7 +190,6 @@ test_suite( "@org_golang_x_tools//internal/jsonrpc2/servertest:servertest_test", "@org_golang_x_tools//internal/jsonrpc2_v2:jsonrpc2_v2_test", "@org_golang_x_tools//internal/memoize:memoize_test", - "@org_golang_x_tools//internal/persistent:persistent_test", "@org_golang_x_tools//internal/proxydir:proxydir_test", "@org_golang_x_tools//internal/robustio:robustio_test", "@org_golang_x_tools//internal/stack:stack_test", @@ -260,7 +257,6 @@ build_test( "@org_golang_x_tools//cmd/file2fuzz:file2fuzz", "@org_golang_x_tools//cmd/fiximports:fiximports", "@org_golang_x_tools//cmd/gorename:gorename", - "@org_golang_x_tools//cmd/guru:guru", "@org_golang_x_tools//cmd/signature-fuzzer/fuzz-driver:fuzz-driver", "@org_golang_x_tools//cmd/signature-fuzzer/fuzz-runner:fuzz-runner", "@org_golang_x_tools//cmd/stringer:stringer", diff --git a/third_party/com_github_golang_mock-gazelle.patch b/third_party/com_github_golang_mock-gazelle.patch index 6cf261577f..ea7a433495 100644 --- a/third_party/com_github_golang_mock-gazelle.patch +++ b/third_party/com_github_golang_mock-gazelle.patch @@ -1,7 +1,7 @@ diff -urN a/gomock/BUILD.bazel b/gomock/BUILD.bazel --- a/gomock/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/gomock/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,34 @@ +@@ -0,0 +1,35 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -10,7 +10,6 @@ diff -urN a/gomock/BUILD.bazel b/gomock/BUILD.bazel + "call.go", + "callset.go", + "controller.go", -+ "doc.go", + "matchers.go", + ], + importpath = "github.com/golang/mock/gomock", @@ -28,6 +27,8 @@ diff -urN a/gomock/BUILD.bazel b/gomock/BUILD.bazel + srcs = [ + "call_test.go", + "callset_test.go", ++ "controller_113_test.go", ++ "controller_114_test.go", + "controller_test.go", + "example_test.go", + "matchers_test.go", @@ -58,18 +59,17 @@ diff -urN a/gomock/internal/mock_gomock/BUILD.bazel b/gomock/internal/mock_gomoc diff -urN a/mockgen/BUILD.bazel b/mockgen/BUILD.bazel --- a/mockgen/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/mockgen/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,36 @@ +@@ -0,0 +1,35 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test") + +go_library( + name = "mockgen_lib", + srcs = [ -+ "generic_go118.go", -+ "generic_notgo118.go", + "mockgen.go", + "parse.go", + "reflect.go", -+ "version.go", ++ "version.1.11.go", ++ "version.1.12.go", + ], + importpath = "github.com/golang/mock/mockgen", + visibility = ["//visibility:private"], @@ -355,72 +355,10 @@ diff -urN a/mockgen/internal/tests/generated_identifier_conflict/BUILD.bazel b/m + embed = [":generated_identifier_conflict"], + deps = ["//gomock"], +) -diff -urN a/mockgen/internal/tests/generics/BUILD.bazel b/mockgen/internal/tests/generics/BUILD.bazel ---- a/mockgen/internal/tests/generics/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ b/mockgen/internal/tests/generics/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,21 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_library") -+ -+go_library( -+ name = "generics", -+ srcs = [ -+ "external.go", -+ "generics.go", -+ ], -+ importpath = "github.com/golang/mock/mockgen/internal/tests/generics", -+ visibility = ["//mockgen:__subpackages__"], -+ deps = [ -+ "//mockgen/internal/tests/generics/other", -+ "@org_golang_x_exp//constraints:go_default_library", -+ ], -+) -+ -+alias( -+ name = "go_default_library", -+ actual = ":generics", -+ visibility = ["//mockgen:__subpackages__"], -+) -diff -urN a/mockgen/internal/tests/generics/other/BUILD.bazel b/mockgen/internal/tests/generics/other/BUILD.bazel ---- a/mockgen/internal/tests/generics/other/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ b/mockgen/internal/tests/generics/other/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,14 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_library") -+ -+go_library( -+ name = "other", -+ srcs = ["other.go"], -+ importpath = "github.com/golang/mock/mockgen/internal/tests/generics/other", -+ visibility = ["//mockgen:__subpackages__"], -+) -+ -+alias( -+ name = "go_default_library", -+ actual = ":other", -+ visibility = ["//mockgen:__subpackages__"], -+) -diff -urN a/mockgen/internal/tests/generics/source/BUILD.bazel b/mockgen/internal/tests/generics/source/BUILD.bazel ---- a/mockgen/internal/tests/generics/source/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ b/mockgen/internal/tests/generics/source/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,15 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_test") -+ -+go_test( -+ name = "source_test", -+ srcs = [ -+ "mock_external_test.go", -+ "mock_generics_test.go", -+ ], -+ deps = [ -+ "//gomock", -+ "//mockgen/internal/tests/generics", -+ "//mockgen/internal/tests/generics/other", -+ "@org_golang_x_exp//constraints:go_default_library", -+ ], -+) diff -urN a/mockgen/internal/tests/import_embedded_interface/BUILD.bazel b/mockgen/internal/tests/import_embedded_interface/BUILD.bazel --- a/mockgen/internal/tests/import_embedded_interface/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/mockgen/internal/tests/import_embedded_interface/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,36 @@ +@@ -0,0 +1,35 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -428,7 +366,6 @@ diff -urN a/mockgen/internal/tests/import_embedded_interface/BUILD.bazel b/mockg + srcs = [ + "bugreport.go", + "bugreport_mock.go", -+ "foo.go", + "net.go", + "net_mock.go", + ], diff --git a/third_party/org_golang_google_genproto-gazelle.patch b/third_party/org_golang_google_genproto-gazelle.patch index eafcf2280d..c857bbc63c 100644 --- a/third_party/org_golang_google_genproto-gazelle.patch +++ b/third_party/org_golang_google_genproto-gazelle.patch @@ -1051,6 +1051,29 @@ diff -urN a/googleapis/apps/alertcenter/v1beta1/BUILD.bazel b/googleapis/apps/al + actual = ":v1beta1", + visibility = ["//visibility:public"], +) +diff -urN a/googleapis/apps/card/v1/BUILD.bazel b/googleapis/apps/card/v1/BUILD.bazel +--- a/googleapis/apps/card/v1/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 ++++ b/googleapis/apps/card/v1/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 +@@ -0,0 +1,19 @@ ++load("@io_bazel_rules_go//go:def.bzl", "go_library") ++ ++go_library( ++ name = "card", ++ srcs = ["card.pb.go"], ++ importpath = "google.golang.org/genproto/googleapis/apps/card/v1", ++ visibility = ["//visibility:public"], ++ deps = [ ++ "//googleapis/type/color", ++ "@org_golang_google_protobuf//reflect/protoreflect:go_default_library", ++ "@org_golang_google_protobuf//runtime/protoimpl:go_default_library", ++ ], ++) ++ ++alias( ++ name = "go_default_library", ++ actual = ":card", ++ visibility = ["//visibility:public"], ++) diff -urN a/googleapis/apps/drive/activity/v2/BUILD.bazel b/googleapis/apps/drive/activity/v2/BUILD.bazel --- a/googleapis/apps/drive/activity/v2/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/googleapis/apps/drive/activity/v2/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 @@ -1089,7 +1112,7 @@ diff -urN a/googleapis/apps/drive/activity/v2/BUILD.bazel b/googleapis/apps/driv diff -urN a/googleapis/apps/drive/labels/v2/BUILD.bazel b/googleapis/apps/drive/labels/v2/BUILD.bazel --- a/googleapis/apps/drive/labels/v2/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/googleapis/apps/drive/labels/v2/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,34 @@ +@@ -0,0 +1,39 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( @@ -1100,9 +1123,12 @@ diff -urN a/googleapis/apps/drive/labels/v2/BUILD.bazel b/googleapis/apps/drive/ + "exception_detail.pb.go", + "field.pb.go", + "label.pb.go", ++ "label_limits.pb.go", ++ "label_lock.pb.go", + "label_permission.pb.go", + "label_service.pb.go", + "requests.pb.go", ++ "user_capabilities.pb.go", + ], + importpath = "google.golang.org/genproto/googleapis/apps/drive/labels/v2", + visibility = ["//visibility:public"], @@ -1115,6 +1141,8 @@ diff -urN a/googleapis/apps/drive/labels/v2/BUILD.bazel b/googleapis/apps/drive/ + "@org_golang_google_grpc//status:go_default_library", + "@org_golang_google_protobuf//reflect/protoreflect:go_default_library", + "@org_golang_google_protobuf//runtime/protoimpl:go_default_library", ++ "@org_golang_google_protobuf//types/known/emptypb:go_default_library", ++ "@org_golang_google_protobuf//types/known/fieldmaskpb:go_default_library", + "@org_golang_google_protobuf//types/known/timestamppb:go_default_library", + ], +) @@ -1484,7 +1512,7 @@ diff -urN a/googleapis/bigtable/admin/table/v1/BUILD.bazel b/googleapis/bigtable diff -urN a/googleapis/bigtable/admin/v2/BUILD.bazel b/googleapis/bigtable/admin/v2/BUILD.bazel --- a/googleapis/bigtable/admin/v2/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/googleapis/bigtable/admin/v2/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,35 @@ +@@ -0,0 +1,36 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( @@ -1495,6 +1523,7 @@ diff -urN a/googleapis/bigtable/admin/v2/BUILD.bazel b/googleapis/bigtable/admin + "common.pb.go", + "instance.pb.go", + "table.pb.go", ++ "types.pb.go", + ], + importpath = "google.golang.org/genproto/googleapis/bigtable/admin/v2", + visibility = ["//visibility:public"], @@ -1592,7 +1621,7 @@ diff -urN a/googleapis/bigtable/v2/BUILD.bazel b/googleapis/bigtable/v2/BUILD.ba diff -urN a/googleapis/bytestream/BUILD.bazel b/googleapis/bytestream/BUILD.bazel --- a/googleapis/bytestream/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/googleapis/bytestream/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,23 @@ +@@ -0,0 +1,21 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( @@ -1601,13 +1630,11 @@ diff -urN a/googleapis/bytestream/BUILD.bazel b/googleapis/bytestream/BUILD.baze + importpath = "google.golang.org/genproto/googleapis/bytestream", + visibility = ["//visibility:public"], + deps = [ -+ "//googleapis/api/annotations", + "@org_golang_google_grpc//:go_default_library", + "@org_golang_google_grpc//codes:go_default_library", + "@org_golang_google_grpc//status:go_default_library", + "@org_golang_google_protobuf//reflect/protoreflect:go_default_library", + "@org_golang_google_protobuf//runtime/protoimpl:go_default_library", -+ "@org_golang_google_protobuf//types/known/wrapperspb:go_default_library", + ], +) + @@ -9126,7 +9153,7 @@ diff -urN a/googleapis/datastore/admin/v1beta1/BUILD.bazel b/googleapis/datastor diff -urN a/googleapis/datastore/v1/BUILD.bazel b/googleapis/datastore/v1/BUILD.bazel --- a/googleapis/datastore/v1/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/googleapis/datastore/v1/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,31 @@ +@@ -0,0 +1,33 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( @@ -9136,6 +9163,7 @@ diff -urN a/googleapis/datastore/v1/BUILD.bazel b/googleapis/datastore/v1/BUILD. + "datastore.pb.go", + "entity.pb.go", + "query.pb.go", ++ "query_profile.pb.go", + ], + importpath = "google.golang.org/genproto/googleapis/datastore/v1", + visibility = ["//visibility:public"], @@ -9147,6 +9175,7 @@ diff -urN a/googleapis/datastore/v1/BUILD.bazel b/googleapis/datastore/v1/BUILD. + "@org_golang_google_grpc//status:go_default_library", + "@org_golang_google_protobuf//reflect/protoreflect:go_default_library", + "@org_golang_google_protobuf//runtime/protoimpl:go_default_library", ++ "@org_golang_google_protobuf//types/known/durationpb:go_default_library", + "@org_golang_google_protobuf//types/known/structpb:go_default_library", + "@org_golang_google_protobuf//types/known/timestamppb:go_default_library", + "@org_golang_google_protobuf//types/known/wrapperspb:go_default_library", @@ -10200,7 +10229,7 @@ diff -urN a/googleapis/geo/type/viewport/BUILD.bazel b/googleapis/geo/type/viewp diff -urN a/googleapis/grafeas/v1/BUILD.bazel b/googleapis/grafeas/v1/BUILD.bazel --- a/googleapis/grafeas/v1/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/googleapis/grafeas/v1/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,49 @@ +@@ -0,0 +1,50 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( @@ -10220,6 +10249,7 @@ diff -urN a/googleapis/grafeas/v1/BUILD.bazel b/googleapis/grafeas/v1/BUILD.baze + "intoto_statement.pb.go", + "package.pb.go", + "provenance.pb.go", ++ "sbom.pb.go", + "severity.pb.go", + "slsa_provenance.pb.go", + "slsa_provenance_zero_two.pb.go", @@ -11505,9 +11535,9 @@ diff -urN a/googleapis/streetview/publish/v1/BUILD.bazel b/googleapis/streetview + visibility = ["//visibility:public"], + deps = [ + "//googleapis/api/annotations", -+ "//googleapis/longrunning", + "//googleapis/rpc/status", + "//googleapis/type/latlng", ++ "@com_google_cloud_go_longrunning//autogen/longrunningpb:go_default_library", + "@org_golang_google_grpc//:go_default_library", + "@org_golang_google_grpc//codes:go_default_library", + "@org_golang_google_grpc//status:go_default_library", diff --git a/third_party/org_golang_google_grpc_cmd_protoc_gen_go_grpc.patch b/third_party/org_golang_google_grpc_cmd_protoc_gen_go_grpc.patch index 7232784c99..9cee02aa5d 100644 --- a/third_party/org_golang_google_grpc_cmd_protoc_gen_go_grpc.patch +++ b/third_party/org_golang_google_grpc_cmd_protoc_gen_go_grpc.patch @@ -1,8 +1,6 @@ -diff --git a/BUILD.bazel b/BUILD.bazel -new file mode 100644 -index 00000000..c87b2ab8 ---- /dev/null -+++ b/BUILD.bazel +diff -urN a/BUILD.bazel b/BUILD.bazel +--- a/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 ++++ b/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 @@ -0,0 +1,23 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + diff --git a/third_party/org_golang_google_protobuf-gazelle.patch b/third_party/org_golang_google_protobuf-gazelle.patch index 27f75c572f..a43d9d5fd7 100644 --- a/third_party/org_golang_google_protobuf-gazelle.patch +++ b/third_party/org_golang_google_protobuf-gazelle.patch @@ -1,3 +1,14 @@ +diff -urN a/BUILD.bazel b/BUILD.bazel +--- a/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 ++++ b/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 +@@ -0,0 +1,7 @@ ++load("@io_bazel_rules_go//go:def.bzl", "go_test") ++ ++go_test( ++ name = "protobuf_test", ++ srcs = ["integration_test.go"], ++ deps = ["//internal/version"], ++) diff -urN a/cmd/protoc-gen-go/BUILD.bazel b/cmd/protoc-gen-go/BUILD.bazel --- a/cmd/protoc-gen-go/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/cmd/protoc-gen-go/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 @@ -43,7 +54,7 @@ diff -urN a/cmd/protoc-gen-go/BUILD.bazel b/cmd/protoc-gen-go/BUILD.bazel diff -urN a/cmd/protoc-gen-go/internal_gengo/BUILD.bazel b/cmd/protoc-gen-go/internal_gengo/BUILD.bazel --- a/cmd/protoc-gen-go/internal_gengo/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/cmd/protoc-gen-go/internal_gengo/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,33 @@ +@@ -0,0 +1,34 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( @@ -60,6 +71,7 @@ diff -urN a/cmd/protoc-gen-go/internal_gengo/BUILD.bazel b/cmd/protoc-gen-go/int + "//compiler/protogen", + "//encoding/protowire", + "//internal/encoding/tag", ++ "//internal/filedesc", + "//internal/genid", + "//internal/version", + "//proto", @@ -103,7 +115,7 @@ diff -urN a/cmd/protoc-gen-go/testdata/annotations/BUILD.bazel b/cmd/protoc-gen- diff -urN a/cmd/protoc-gen-go/testdata/BUILD.bazel b/cmd/protoc-gen-go/testdata/BUILD.bazel --- a/cmd/protoc-gen-go/testdata/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/cmd/protoc-gen-go/testdata/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,34 @@ +@@ -0,0 +1,35 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_test") + +go_test( @@ -132,6 +144,7 @@ diff -urN a/cmd/protoc-gen-go/testdata/BUILD.bazel b/cmd/protoc-gen-go/testdata/ + "//cmd/protoc-gen-go/testdata/nopackage", + "//cmd/protoc-gen-go/testdata/proto2", + "//cmd/protoc-gen-go/testdata/proto3", ++ "//cmd/protoc-gen-go/testdata/protoeditions", + "//cmd/protoc-gen-go/testdata/retention", + "//internal/filedesc", + "//reflect/protoreflect", @@ -575,6 +588,33 @@ diff -urN a/cmd/protoc-gen-go/testdata/proto3/BUILD.bazel b/cmd/protoc-gen-go/te + actual = ":proto3", + visibility = ["//visibility:public"], +) +diff -urN a/cmd/protoc-gen-go/testdata/protoeditions/BUILD.bazel b/cmd/protoc-gen-go/testdata/protoeditions/BUILD.bazel +--- a/cmd/protoc-gen-go/testdata/protoeditions/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 ++++ b/cmd/protoc-gen-go/testdata/protoeditions/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 +@@ -0,0 +1,23 @@ ++load("@io_bazel_rules_go//go:def.bzl", "go_library") ++ ++go_library( ++ name = "protoeditions", ++ srcs = [ ++ "enum.pb.go", ++ "fields.pb.go", ++ "nested_messages.pb.go", ++ ], ++ importpath = "google.golang.org/protobuf/cmd/protoc-gen-go/testdata/protoeditions", ++ visibility = ["//visibility:public"], ++ deps = [ ++ "//reflect/protoreflect", ++ "//runtime/protoimpl", ++ "//types/gofeaturespb", ++ ], ++) ++ ++alias( ++ name = "go_default_library", ++ actual = ":protoeditions", ++ visibility = ["//visibility:public"], ++) diff -urN a/cmd/protoc-gen-go/testdata/retention/BUILD.bazel b/cmd/protoc-gen-go/testdata/retention/BUILD.bazel --- a/cmd/protoc-gen-go/testdata/retention/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/cmd/protoc-gen-go/testdata/retention/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 @@ -700,7 +740,7 @@ diff -urN a/encoding/protodelim/BUILD.bazel b/encoding/protodelim/BUILD.bazel diff -urN a/encoding/protojson/BUILD.bazel b/encoding/protojson/BUILD.bazel --- a/encoding/protojson/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/encoding/protojson/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,64 @@ +@@ -0,0 +1,70 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -714,6 +754,7 @@ diff -urN a/encoding/protojson/BUILD.bazel b/encoding/protojson/BUILD.bazel + importpath = "google.golang.org/protobuf/encoding/protojson", + visibility = ["//visibility:public"], + deps = [ ++ "//encoding/protowire", + "//internal/encoding/json", + "//internal/encoding/messageset", + "//internal/errors", @@ -742,18 +783,23 @@ diff -urN a/encoding/protojson/BUILD.bazel b/encoding/protojson/BUILD.bazel + "bench_test.go", + "decode_test.go", + "encode_test.go", ++ "fuzz_test.go", + ], + deps = [ + ":protojson", + "//internal/detrand", + "//internal/errors", + "//internal/flags", ++ "//internal/testprotos/editionsfuzztest", + "//internal/testprotos/test", + "//internal/testprotos/test/weak1", + "//internal/testprotos/textpb2", + "//internal/testprotos/textpb3", ++ "//internal/testprotos/textpbeditions", + "//proto", ++ "//reflect/protoreflect", + "//reflect/protoregistry", ++ "//testing/protocmp", + "//testing/protopack", + "//types/known/anypb", + "//types/known/durationpb", @@ -768,7 +814,7 @@ diff -urN a/encoding/protojson/BUILD.bazel b/encoding/protojson/BUILD.bazel diff -urN a/encoding/prototext/BUILD.bazel b/encoding/prototext/BUILD.bazel --- a/encoding/prototext/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/encoding/prototext/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,61 @@ +@@ -0,0 +1,66 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -808,18 +854,23 @@ diff -urN a/encoding/prototext/BUILD.bazel b/encoding/prototext/BUILD.bazel + srcs = [ + "decode_test.go", + "encode_test.go", ++ "fuzz_test.go", + "other_test.go", + ], + deps = [ + ":prototext", + "//internal/detrand", + "//internal/flags", ++ "//internal/testprotos/editionsfuzztest", + "//internal/testprotos/test", + "//internal/testprotos/test/weak1", + "//internal/testprotos/textpb2", + "//internal/testprotos/textpb3", ++ "//internal/testprotos/textpbeditions", + "//proto", ++ "//reflect/protoreflect", + "//reflect/protoregistry", ++ "//testing/protocmp", + "//testing/protopack", + "//types/known/anypb", + "//types/known/durationpb", @@ -924,7 +975,7 @@ diff -urN a/internal/cmd/generate-corpus/BUILD.bazel b/internal/cmd/generate-cor diff -urN a/internal/cmd/generate-protos/BUILD.bazel b/internal/cmd/generate-protos/BUILD.bazel --- a/internal/cmd/generate-protos/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/internal/cmd/generate-protos/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,19 @@ +@@ -0,0 +1,20 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( @@ -936,6 +987,7 @@ diff -urN a/internal/cmd/generate-protos/BUILD.bazel b/internal/cmd/generate-pro + "//cmd/protoc-gen-go/internal_gengo", + "//compiler/protogen", + "//internal/detrand", ++ "//reflect/protodesc", + ], +) + @@ -1008,7 +1060,7 @@ diff -urN a/internal/cmd/pbdump/BUILD.bazel b/internal/cmd/pbdump/BUILD.bazel diff -urN a/internal/conformance/BUILD.bazel b/internal/conformance/BUILD.bazel --- a/internal/conformance/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/internal/conformance/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,12 @@ +@@ -0,0 +1,13 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_test") + +go_test( @@ -1018,13 +1070,14 @@ diff -urN a/internal/conformance/BUILD.bazel b/internal/conformance/BUILD.bazel + "//encoding/protojson", + "//encoding/prototext", + "//internal/testprotos/conformance", ++ "//internal/testprotos/conformance/editions", + "//proto", + ], +) diff -urN a/internal/descfmt/BUILD.bazel b/internal/descfmt/BUILD.bazel --- a/internal/descfmt/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/internal/descfmt/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,25 @@ +@@ -0,0 +1,29 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -1048,7 +1101,11 @@ diff -urN a/internal/descfmt/BUILD.bazel b/internal/descfmt/BUILD.bazel +go_test( + name = "descfmt_test", + srcs = ["desc_test.go"], -+ embed = [":descfmt"], ++ deps = [ ++ ":descfmt", ++ "//internal/testprotos/test", ++ "//reflect/protoreflect", ++ ], +) diff -urN a/internal/descopts/BUILD.bazel b/internal/descopts/BUILD.bazel --- a/internal/descopts/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 @@ -1093,6 +1150,25 @@ diff -urN a/internal/detrand/BUILD.bazel b/internal/detrand/BUILD.bazel + srcs = ["rand_test.go"], + embed = [":detrand"], +) +diff -urN a/internal/editiondefaults/BUILD.bazel b/internal/editiondefaults/BUILD.bazel +--- a/internal/editiondefaults/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 ++++ b/internal/editiondefaults/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 +@@ -0,0 +1,15 @@ ++load("@io_bazel_rules_go//go:def.bzl", "go_library") ++ ++go_library( ++ name = "editiondefaults", ++ srcs = ["defaults.go"], ++ embedsrcs = ["editions_defaults.binpb"], ++ importpath = "google.golang.org/protobuf/internal/editiondefaults", ++ visibility = ["//:__subpackages__"], ++) ++ ++alias( ++ name = "go_default_library", ++ actual = ":editiondefaults", ++ visibility = ["//:__subpackages__"], ++) diff -urN a/internal/encoding/defval/BUILD.bazel b/internal/encoding/defval/BUILD.bazel --- a/internal/encoding/defval/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/internal/encoding/defval/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 @@ -1306,7 +1382,7 @@ diff -urN a/internal/errors/BUILD.bazel b/internal/errors/BUILD.bazel diff -urN a/internal/filedesc/BUILD.bazel b/internal/filedesc/BUILD.bazel --- a/internal/filedesc/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/internal/filedesc/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,55 @@ +@@ -0,0 +1,57 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -1318,6 +1394,7 @@ diff -urN a/internal/filedesc/BUILD.bazel b/internal/filedesc/BUILD.bazel + "desc_lazy.go", + "desc_list.go", + "desc_list_gen.go", ++ "editions.go", + "placeholder.go", + ], + importpath = "google.golang.org/protobuf/internal/filedesc", @@ -1326,6 +1403,7 @@ diff -urN a/internal/filedesc/BUILD.bazel b/internal/filedesc/BUILD.bazel + "//encoding/protowire", + "//internal/descfmt", + "//internal/descopts", ++ "//internal/editiondefaults", + "//internal/encoding/defval", + "//internal/encoding/messageset", + "//internal/errors", @@ -1521,7 +1599,7 @@ diff -urN a/internal/fuzztest/BUILD.bazel b/internal/fuzztest/BUILD.bazel diff -urN a/internal/genid/BUILD.bazel b/internal/genid/BUILD.bazel --- a/internal/genid/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/internal/genid/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,31 @@ +@@ -0,0 +1,32 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( @@ -1534,6 +1612,7 @@ diff -urN a/internal/genid/BUILD.bazel b/internal/genid/BUILD.bazel + "duration_gen.go", + "empty_gen.go", + "field_mask_gen.go", ++ "go_features_gen.go", + "goname.go", + "map_entry.go", + "source_context_gen.go", @@ -1838,14 +1917,15 @@ diff -urN a/internal/set/BUILD.bazel b/internal/set/BUILD.bazel diff -urN a/internal/strs/BUILD.bazel b/internal/strs/BUILD.bazel --- a/internal/strs/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/internal/strs/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,27 @@ +@@ -0,0 +1,28 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "strs", + srcs = [ + "strings.go", -+ "strings_unsafe.go", ++ "strings_unsafe_go120.go", ++ "strings_unsafe_go121.go", + ], + importpath = "google.golang.org/protobuf/internal/strs", + visibility = ["//:__subpackages__"], @@ -2090,6 +2170,65 @@ diff -urN a/internal/testprotos/conformance/BUILD.bazel b/internal/testprotos/co + actual = ":conformance", + visibility = ["//:__subpackages__"], +) +diff -urN a/internal/testprotos/conformance/editions/BUILD.bazel b/internal/testprotos/conformance/editions/BUILD.bazel +--- a/internal/testprotos/conformance/editions/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 ++++ b/internal/testprotos/conformance/editions/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 +@@ -0,0 +1,27 @@ ++load("@io_bazel_rules_go//go:def.bzl", "go_library") ++ ++go_library( ++ name = "editions", ++ srcs = [ ++ "test_messages_proto2_editions.pb.go", ++ "test_messages_proto3_editions.pb.go", ++ ], ++ importpath = "google.golang.org/protobuf/internal/testprotos/conformance/editions", ++ visibility = ["//:__subpackages__"], ++ deps = [ ++ "//reflect/protoreflect", ++ "//runtime/protoimpl", ++ "//types/known/anypb", ++ "//types/known/durationpb", ++ "//types/known/fieldmaskpb", ++ "//types/known/structpb", ++ "//types/known/timestamppb", ++ "//types/known/wrapperspb", ++ ], ++) ++ ++alias( ++ name = "go_default_library", ++ actual = ":editions", ++ visibility = ["//:__subpackages__"], ++) +diff -urN a/internal/testprotos/editionsfuzztest/BUILD.bazel b/internal/testprotos/editionsfuzztest/BUILD.bazel +--- a/internal/testprotos/editionsfuzztest/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 ++++ b/internal/testprotos/editionsfuzztest/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 +@@ -0,0 +1,24 @@ ++load("@io_bazel_rules_go//go:def.bzl", "go_library") ++ ++go_library( ++ name = "editionsfuzztest", ++ srcs = [ ++ "test2.pb.go", ++ "test2editions.pb.go", ++ "test3.pb.go", ++ "test3editions.pb.go", ++ ], ++ importpath = "google.golang.org/protobuf/internal/testprotos/editionsfuzztest", ++ visibility = ["//:__subpackages__"], ++ deps = [ ++ "//reflect/protoreflect", ++ "//runtime/protoimpl", ++ "//types/gofeaturespb", ++ ], ++) ++ ++alias( ++ name = "go_default_library", ++ actual = ":editionsfuzztest", ++ visibility = ["//:__subpackages__"], ++) diff -urN a/internal/testprotos/enums/BUILD.bazel b/internal/testprotos/enums/BUILD.bazel --- a/internal/testprotos/enums/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/internal/testprotos/enums/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 @@ -2608,6 +2747,66 @@ diff -urN a/internal/testprotos/order/BUILD.bazel b/internal/testprotos/order/BU + actual = ":order", + visibility = ["//:__subpackages__"], +) +diff -urN a/internal/testprotos/race/BUILD.bazel b/internal/testprotos/race/BUILD.bazel +--- a/internal/testprotos/race/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 ++++ b/internal/testprotos/race/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 +@@ -0,0 +1,11 @@ ++load("@io_bazel_rules_go//go:def.bzl", "go_test") ++ ++go_test( ++ name = "race_test", ++ srcs = ["race_test.go"], ++ deps = [ ++ "//internal/testprotos/race/extender", ++ "//internal/testprotos/race/message", ++ "//proto", ++ ], ++) +diff -urN a/internal/testprotos/race/extender/BUILD.bazel b/internal/testprotos/race/extender/BUILD.bazel +--- a/internal/testprotos/race/extender/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 ++++ b/internal/testprotos/race/extender/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 +@@ -0,0 +1,19 @@ ++load("@io_bazel_rules_go//go:def.bzl", "go_library") ++ ++go_library( ++ name = "extender", ++ srcs = ["test.pb.go"], ++ importpath = "google.golang.org/protobuf/internal/testprotos/race/extender", ++ visibility = ["//:__subpackages__"], ++ deps = [ ++ "//internal/testprotos/race/message", ++ "//reflect/protoreflect", ++ "//runtime/protoimpl", ++ ], ++) ++ ++alias( ++ name = "go_default_library", ++ actual = ":extender", ++ visibility = ["//:__subpackages__"], ++) +diff -urN a/internal/testprotos/race/message/BUILD.bazel b/internal/testprotos/race/message/BUILD.bazel +--- a/internal/testprotos/race/message/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 ++++ b/internal/testprotos/race/message/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 +@@ -0,0 +1,18 @@ ++load("@io_bazel_rules_go//go:def.bzl", "go_library") ++ ++go_library( ++ name = "message", ++ srcs = ["test.pb.go"], ++ importpath = "google.golang.org/protobuf/internal/testprotos/race/message", ++ visibility = ["//:__subpackages__"], ++ deps = [ ++ "//reflect/protoreflect", ++ "//runtime/protoimpl", ++ ], ++) ++ ++alias( ++ name = "go_default_library", ++ actual = ":message", ++ visibility = ["//:__subpackages__"], ++) diff -urN a/internal/testprotos/registry/BUILD.bazel b/internal/testprotos/registry/BUILD.bazel --- a/internal/testprotos/registry/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/internal/testprotos/registry/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 @@ -2752,6 +2951,31 @@ diff -urN a/internal/testprotos/test3/BUILD.bazel b/internal/testprotos/test3/BU + actual = ":test3", + visibility = ["//:__subpackages__"], +) +diff -urN a/internal/testprotos/testeditions/BUILD.bazel b/internal/testprotos/testeditions/BUILD.bazel +--- a/internal/testprotos/testeditions/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 ++++ b/internal/testprotos/testeditions/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 +@@ -0,0 +1,21 @@ ++load("@io_bazel_rules_go//go:def.bzl", "go_library") ++ ++go_library( ++ name = "testeditions", ++ srcs = [ ++ "test.pb.go", ++ "test_extension.pb.go", ++ ], ++ importpath = "google.golang.org/protobuf/internal/testprotos/testeditions", ++ visibility = ["//:__subpackages__"], ++ deps = [ ++ "//reflect/protoreflect", ++ "//runtime/protoimpl", ++ ], ++) ++ ++alias( ++ name = "go_default_library", ++ actual = ":testeditions", ++ visibility = ["//:__subpackages__"], ++) diff -urN a/internal/testprotos/textpb2/BUILD.bazel b/internal/testprotos/textpb2/BUILD.bazel --- a/internal/testprotos/textpb2/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/internal/testprotos/textpb2/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 @@ -2803,6 +3027,35 @@ diff -urN a/internal/testprotos/textpb3/BUILD.bazel b/internal/testprotos/textpb + actual = ":textpb3", + visibility = ["//:__subpackages__"], +) +diff -urN a/internal/testprotos/textpbeditions/BUILD.bazel b/internal/testprotos/textpbeditions/BUILD.bazel +--- a/internal/testprotos/textpbeditions/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 ++++ b/internal/testprotos/textpbeditions/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 +@@ -0,0 +1,25 @@ ++load("@io_bazel_rules_go//go:def.bzl", "go_library") ++ ++go_library( ++ name = "textpbeditions", ++ srcs = ["test2.pb.go"], ++ importpath = "google.golang.org/protobuf/internal/testprotos/textpbeditions", ++ visibility = ["//:__subpackages__"], ++ deps = [ ++ "//reflect/protoreflect", ++ "//runtime/protoimpl", ++ "//types/known/anypb", ++ "//types/known/durationpb", ++ "//types/known/emptypb", ++ "//types/known/fieldmaskpb", ++ "//types/known/structpb", ++ "//types/known/timestamppb", ++ "//types/known/wrapperspb", ++ ], ++) ++ ++alias( ++ name = "go_default_library", ++ actual = ":textpbeditions", ++ visibility = ["//:__subpackages__"], ++) diff -urN a/internal/version/BUILD.bazel b/internal/version/BUILD.bazel --- a/internal/version/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/internal/version/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 @@ -2842,7 +3095,7 @@ diff -urN a/internal/weakdeps/BUILD.bazel b/internal/weakdeps/BUILD.bazel diff -urN a/proto/BUILD.bazel b/proto/BUILD.bazel --- a/proto/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/proto/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,95 @@ +@@ -0,0 +1,98 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -2897,6 +3150,7 @@ diff -urN a/proto/BUILD.bazel b/proto/BUILD.bazel + "encode_test.go", + "equal_test.go", + "extension_test.go", ++ "fuzz_test.go", + "merge_test.go", + "messageset_test.go", + "methods_test.go", @@ -2917,6 +3171,7 @@ diff -urN a/proto/BUILD.bazel b/proto/BUILD.bazel + "//internal/impl", + "//internal/pragma", + "//internal/protobuild", ++ "//internal/testprotos/editionsfuzztest", + "//internal/testprotos/legacy", + "//internal/testprotos/legacy/proto2_20160225_2fc053c5", + "//internal/testprotos/messageset/messagesetpb", @@ -2926,6 +3181,7 @@ diff -urN a/proto/BUILD.bazel b/proto/BUILD.bazel + "//internal/testprotos/test", + "//internal/testprotos/test/weak1", + "//internal/testprotos/test3", ++ "//internal/testprotos/testeditions", + "//reflect/protodesc", + "//reflect/protoreflect", + "//reflect/protoregistry", @@ -2964,7 +3220,7 @@ diff -urN a/protoadapt/BUILD.bazel b/protoadapt/BUILD.bazel diff -urN a/reflect/protodesc/BUILD.bazel b/reflect/protodesc/BUILD.bazel --- a/reflect/protodesc/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/reflect/protodesc/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,48 @@ +@@ -0,0 +1,52 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -2974,12 +3230,14 @@ diff -urN a/reflect/protodesc/BUILD.bazel b/reflect/protodesc/BUILD.bazel + "desc_init.go", + "desc_resolve.go", + "desc_validate.go", ++ "editions.go", + "proto.go", + ], + importpath = "google.golang.org/protobuf/reflect/protodesc", + visibility = ["//visibility:public"], + deps = [ + "//encoding/protowire", ++ "//internal/editiondefaults", + "//internal/encoding/defval", + "//internal/errors", + "//internal/filedesc", @@ -2991,6 +3249,7 @@ diff -urN a/reflect/protodesc/BUILD.bazel b/reflect/protodesc/BUILD.bazel + "//reflect/protoreflect", + "//reflect/protoregistry", + "//types/descriptorpb", ++ "//types/gofeaturespb", + ], +) + @@ -3006,6 +3265,7 @@ diff -urN a/reflect/protodesc/BUILD.bazel b/reflect/protodesc/BUILD.bazel + embed = [":protodesc"], + deps = [ + "//encoding/prototext", ++ "//internal/filedesc", + "//internal/flags", + "//proto", + "//reflect/protoreflect", @@ -3092,7 +3352,7 @@ diff -urN a/reflect/protorange/BUILD.bazel b/reflect/protorange/BUILD.bazel diff -urN a/reflect/protoreflect/BUILD.bazel b/reflect/protoreflect/BUILD.bazel --- a/reflect/protoreflect/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/reflect/protoreflect/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,38 @@ +@@ -0,0 +1,39 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -3106,7 +3366,8 @@ diff -urN a/reflect/protoreflect/BUILD.bazel b/reflect/protoreflect/BUILD.bazel + "value.go", + "value_equal.go", + "value_union.go", -+ "value_unsafe.go", ++ "value_unsafe_go120.go", ++ "value_unsafe_go121.go", + ], + importpath = "google.golang.org/protobuf/reflect/protoreflect", + visibility = ["//visibility:public"], @@ -3226,7 +3487,7 @@ diff -urN a/runtime/protoimpl/BUILD.bazel b/runtime/protoimpl/BUILD.bazel diff -urN a/testing/protocmp/BUILD.bazel b/testing/protocmp/BUILD.bazel --- a/testing/protocmp/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/testing/protocmp/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,52 @@ +@@ -0,0 +1,53 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -3272,6 +3533,7 @@ diff -urN a/testing/protocmp/BUILD.bazel b/testing/protocmp/BUILD.bazel + "//internal/testprotos/textpb2", + "//proto", + "//reflect/protoreflect", ++ "//reflect/protoregistry", + "//testing/protopack", + "//types/dynamicpb", + "//types/known/anypb", @@ -3317,7 +3579,7 @@ diff -urN a/testing/protopack/BUILD.bazel b/testing/protopack/BUILD.bazel diff -urN a/testing/prototest/BUILD.bazel b/testing/prototest/BUILD.bazel --- a/testing/prototest/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/testing/prototest/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,42 @@ +@@ -0,0 +1,43 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -3356,6 +3618,7 @@ diff -urN a/testing/prototest/BUILD.bazel b/testing/prototest/BUILD.bazel + "//internal/testprotos/test/weak1", + "//internal/testprotos/test/weak2", + "//internal/testprotos/test3", ++ "//internal/testprotos/testeditions", + "//proto", + "//runtime/protoimpl", + ], @@ -3429,6 +3692,29 @@ diff -urN a/types/dynamicpb/BUILD.bazel b/types/dynamicpb/BUILD.bazel + "//types/descriptorpb", + ], +) +diff -urN a/types/gofeaturespb/BUILD.bazel b/types/gofeaturespb/BUILD.bazel +--- a/types/gofeaturespb/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 ++++ b/types/gofeaturespb/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 +@@ -0,0 +1,19 @@ ++load("@io_bazel_rules_go//go:def.bzl", "go_library") ++ ++go_library( ++ name = "gofeaturespb", ++ srcs = ["go_features.pb.go"], ++ importpath = "google.golang.org/protobuf/types/gofeaturespb", ++ visibility = ["//visibility:public"], ++ deps = [ ++ "//reflect/protoreflect", ++ "//runtime/protoimpl", ++ "//types/descriptorpb", ++ ], ++) ++ ++alias( ++ name = "go_default_library", ++ actual = ":gofeaturespb", ++ visibility = ["//visibility:public"], ++) diff -urN a/types/known/anypb/BUILD.bazel b/types/known/anypb/BUILD.bazel --- a/types/known/anypb/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/types/known/anypb/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 diff --git a/third_party/org_golang_x_sys-gazelle.patch b/third_party/org_golang_x_sys-gazelle.patch index 39f738808a..4caedb682c 100644 --- a/third_party/org_golang_x_sys-gazelle.patch +++ b/third_party/org_golang_x_sys-gazelle.patch @@ -479,7 +479,7 @@ diff -urN a/unix/internal/mkmerge/BUILD.bazel b/unix/internal/mkmerge/BUILD.baze diff -urN a/windows/BUILD.bazel b/windows/BUILD.bazel --- a/windows/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ b/windows/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,53 @@ +@@ -0,0 +1,54 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -522,6 +522,7 @@ diff -urN a/windows/BUILD.bazel b/windows/BUILD.bazel +go_test( + name = "windows_test", + srcs = [ ++ "env_windows_test.go", + "syscall_test.go", + "syscall_windows_test.go", + ], diff --git a/third_party/org_golang_x_tools-deletegopls.patch b/third_party/org_golang_x_tools-deletegopls.patch index 2d740d7116..238e7146d1 100644 --- a/third_party/org_golang_x_tools-deletegopls.patch +++ b/third_party/org_golang_x_tools-deletegopls.patch @@ -1,19 +1,15 @@ diff -urN a/gopls/api-diff/api_diff.go b/gopls/api-diff/api_diff.go --- a/gopls/api-diff/api_diff.go 2000-01-01 00:00:00.000000000 -0000 +++ b/gopls/api-diff/api_diff.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,89 +0,0 @@ +@@ -1,84 +0,0 @@ -// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --//go:build go1.18 --// +build go1.18 -- -package main - -import ( - "bytes" -- "context" - "encoding/json" - "flag" - "fmt" @@ -22,7 +18,7 @@ diff -urN a/gopls/api-diff/api_diff.go b/gopls/api-diff/api_diff.go - "os/exec" - - "github.com/google/go-cmp/cmp" -- "golang.org/x/tools/gopls/internal/lsp/source" +- "golang.org/x/tools/gopls/internal/settings" -) - -const usage = `api-diff [] @@ -54,17 +50,16 @@ diff -urN a/gopls/api-diff/api_diff.go b/gopls/api-diff/api_diff.go -} - -func diffAPI(oldVer, newVer string) (string, error) { -- ctx := context.Background() -- previousAPI, err := loadAPI(ctx, oldVer) +- previousAPI, err := loadAPI(oldVer) - if err != nil { - return "", fmt.Errorf("loading %s: %v", oldVer, err) - } -- var currentAPI *source.APIJSON +- var currentAPI *settings.APIJSON - if newVer == "" { -- currentAPI = source.GeneratedAPIJSON +- currentAPI = settings.GeneratedAPIJSON - } else { - var err error -- currentAPI, err = loadAPI(ctx, newVer) +- currentAPI, err = loadAPI(newVer) - if err != nil { - return "", fmt.Errorf("loading %s: %v", newVer, err) - } @@ -73,7 +68,7 @@ diff -urN a/gopls/api-diff/api_diff.go b/gopls/api-diff/api_diff.go - return cmp.Diff(previousAPI, currentAPI), nil -} - --func loadAPI(ctx context.Context, version string) (*source.APIJSON, error) { +-func loadAPI(version string) (*settings.APIJSON, error) { - ver := fmt.Sprintf("golang.org/x/tools/gopls@%s", version) - cmd := exec.Command("go", "run", ver, "api-json") - @@ -85,7 +80,7 @@ diff -urN a/gopls/api-diff/api_diff.go b/gopls/api-diff/api_diff.go - if err := cmd.Run(); err != nil { - return nil, fmt.Errorf("go run failed: %v; stderr:\n%s", err, stderr) - } -- apiJson := &source.APIJSON{} +- apiJson := &settings.APIJSON{} - if err := json.Unmarshal(stdout.Bytes(), apiJson); err != nil { - return nil, fmt.Errorf("unmarshal: %v", err) - } @@ -178,7 +173,7 @@ diff -urN a/gopls/doc/advanced.md b/gopls/doc/advanced.md diff -urN a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md --- a/gopls/doc/analyzers.md 2000-01-01 00:00:00.000000000 -0000 +++ b/gopls/doc/analyzers.md 1970-01-01 00:00:00.000000000 +0000 -@@ -1,837 +0,0 @@ +@@ -1,1035 +0,0 @@ -# Analyzers - -This document describes the analyzers that `gopls` uses inside the editor. @@ -189,7 +184,7 @@ diff -urN a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md - -## **appends** - --check for missing values after append +-appends: check for missing values after append - -This checker reports calls to append that pass -no values to be appended to the slice. @@ -200,27 +195,33 @@ diff -urN a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md -Such calls are always no-ops and often indicate an -underlying mistake. - +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/appends) +- -**Enabled by default.** - -## **asmdecl** - --report mismatches between assembly files and Go declarations +-asmdecl: report mismatches between assembly files and Go declarations +- +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/asmdecl) - -**Enabled by default.** - -## **assign** - --check for useless assignments +-assign: check for useless assignments - -This checker reports assignments of the form x = x or a[i] = a[i]. -These are almost always useless, and even when they aren't they are -usually a mistake. - +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/assign) +- -**Enabled by default.** - -## **atomic** - --check for common mistakes using the sync/atomic package +-atomic: check for common mistakes using the sync/atomic package - -The atomic checker looks for assignment statements of the form: - @@ -228,29 +229,37 @@ diff -urN a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md - -which are not atomic. - +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/atomic) +- -**Enabled by default.** - -## **atomicalign** - --check for non-64-bits-aligned arguments to sync/atomic functions +-atomicalign: check for non-64-bits-aligned arguments to sync/atomic functions +- +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/atomicalign) - -**Enabled by default.** - -## **bools** - --check for common mistakes involving boolean operators +-bools: check for common mistakes involving boolean operators +- +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/bools) - -**Enabled by default.** - -## **buildtag** - --check //go:build and // +build directives +-buildtag: check //go:build and // +build directives +- +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/buildtag) - -**Enabled by default.** - -## **cgocall** - --detect some violations of the cgo pointer passing rules +-cgocall: detect some violations of the cgo pointer passing rules - -Check for invalid cgo pointer passing. -This looks for code that uses cgo to call C code passing values @@ -259,11 +268,13 @@ diff -urN a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md -Specifically, it warns about attempts to pass a Go chan, map, func, -or slice to C, either directly, or via a pointer, array, or struct. - +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/cgocall) +- -**Enabled by default.** - -## **composites** - --check for unkeyed composite literals +-composites: check for unkeyed composite literals - -This analyzer reports a diagnostic for composite literals of struct -types imported from another package that do not use the field-keyed @@ -279,21 +290,25 @@ diff -urN a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md - err = &net.DNSConfigError{Err: err} - - +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/composite) +- -**Enabled by default.** - -## **copylocks** - --check for locks erroneously passed by value +-copylocks: check for locks erroneously passed by value - -Inadvertently copying a value containing a lock, such as sync.Mutex or -sync.WaitGroup, may cause both copies to malfunction. Generally such -values should be referred to through a pointer. - +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/copylocks) +- -**Enabled by default.** - -## **deepequalerrors** - --check for calls of reflect.DeepEqual on error values +-deepequalerrors: check for calls of reflect.DeepEqual on error values - -The deepequalerrors checker looks for calls of the form: - @@ -302,11 +317,13 @@ diff -urN a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md -where err1 and err2 are errors. Using reflect.DeepEqual to compare -errors is discouraged. - +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/deepequalerrors) +- -**Enabled by default.** - -## **defers** - --report common mistakes in defer statements +-defers: report common mistakes in defer statements - -The defers analyzer reports a diagnostic when a defer statement would -result in a non-deferred call to time.Since, as experience has shown @@ -322,22 +339,27 @@ diff -urN a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md - - defer func() { recordLatency(time.Since(start)) }() - +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/defers) +- -**Enabled by default.** - -## **deprecated** - --check for use of deprecated identifiers +-deprecated: check for use of deprecated identifiers - --The deprecated analyzer looks for deprecated symbols and package imports. +-The deprecated analyzer looks for deprecated symbols and package +-imports. - -See https://go.dev/wiki/Deprecated to learn about Go's convention -for documenting and signaling deprecated identifiers. - +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/deprecated) +- -**Enabled by default.** - -## **directive** - --check Go toolchain directives such as //go:debug +-directive: check Go toolchain directives such as //go:debug - -This analyzer checks for problems with known Go toolchain directives -in all Go source files in a package directory, even those excluded by @@ -353,11 +375,13 @@ diff -urN a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md -buildtag analyzer. - - +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/directive) +- -**Enabled by default.** - -## **embed** - --check //go:embed directive usage +-embed: check //go:embed directive usage - -This analyzer checks that the embed package is imported if //go:embed -directives are present, providing a suggested fix to add the import if @@ -366,20 +390,24 @@ diff -urN a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md -This analyzer also checks that //go:embed directives precede the -declaration of a single variable. - +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/embeddirective) +- -**Enabled by default.** - -## **errorsas** - --report passing non-pointer or non-error values to errors.As +-errorsas: report passing non-pointer or non-error values to errors.As - -The errorsas analysis reports calls to errors.As where the type -of the second argument is not a pointer to a type implementing error. - +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/errorsas) +- -**Enabled by default.** - -## **fieldalignment** - --find structs that would use less memory if their fields were sorted +-fieldalignment: find structs that would use less memory if their fields were sorted - -This analyzer find structs that can be rearranged to use less memory, and provides -a suggested edit with the most compact order. @@ -407,11 +435,36 @@ diff -urN a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md -known as "false sharing" that slows down both goroutines. - - +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/fieldalignment) +- -**Disabled by default. Enable it by setting `"analyses": {"fieldalignment": true}`.** - +-## **fillreturns** +- +-fillreturns: suggest fixes for errors due to an incorrect number of return values +- +-This checker provides suggested fixes for type errors of the +-type "wrong number of return values (want %d, got %d)". For example: +- +- func m() (int, string, *bool, error) { +- return +- } +- +-will turn into +- +- func m() (int, string, *bool, error) { +- return 0, "", nil, nil +- } +- +-This functionality is similar to https://github.com/sqs/goreturns. +- +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/fillreturns) +- +-**Enabled by default.** +- -## **httpresponse** - --check for mistakes using HTTP responses +-httpresponse: check for mistakes using HTTP responses - -A common mistake when using the net/http package is to defer a function -call to close the http.Response Body before checking the error that @@ -427,11 +480,13 @@ diff -urN a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md -This checker helps uncover latent nil dereference bugs by reporting a -diagnostic for such mistakes. - +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/httpresponse) +- -**Enabled by default.** - -## **ifaceassert** - --detect impossible interface-to-interface type assertions +-ifaceassert: detect impossible interface-to-interface type assertions - -This checker flags type assertions v.(T) and corresponding type-switch cases -in which the static type V of v is an interface that cannot possibly implement @@ -446,19 +501,43 @@ diff -urN a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md -The Read method in v has a different signature than the Read method in -io.Reader, so this assertion cannot succeed. - +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/ifaceassert) +- +-**Enabled by default.** +- +-## **infertypeargs** +- +-infertypeargs: check for unnecessary type arguments in call expressions +- +-Explicit type arguments may be omitted from call expressions if they can be +-inferred from function arguments, or from other type arguments: +- +- func f[T any](T) {} +- +- func _() { +- f[string]("foo") // string could be inferred +- } +- +- +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/infertypeargs) +- -**Enabled by default.** - -## **loopclosure** - --check references to loop variables from within nested functions +-loopclosure: check references to loop variables from within nested functions - -This analyzer reports places where a function literal references the -iteration variable of an enclosing loop, and the loop calls the function -in such a way (e.g. with go or defer) that it may outlive the loop -iteration and possibly observe the wrong value of the variable. - +-Note: An iteration variable can only outlive a loop iteration in Go versions <=1.21. +-In Go 1.22 and later, the loop variable lifetimes changed to create a new +-iteration variable per loop iteration. (See go.dev/issue/60078.) +- -In this example, all the deferred functions run after the loop has --completed, so all observe the final value of v. +-completed, so all observe the final value of v [" +- +-This checker provides suggested fixes for type errors of the +-type "undeclared name: <>". It will either insert a new statement, +-such as: +- +- <> := +- +-or a new function declaration, such as: +- +- func <>(inferred parameters) { +- panic("implement me!") +- } +- +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/undeclaredname) +- -**Enabled by default.** - -## **unmarshal** - --report passing non-pointer or non-interface values to unmarshal +-unmarshal: report passing non-pointer or non-interface values to unmarshal - -The unmarshal analysis reports calls to functions such as json.Unmarshal -in which the argument type is not a pointer or an interface. - +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unmarshal) +- -**Enabled by default.** - -## **unreachable** - --check for unreachable code +-unreachable: check for unreachable code - -The unreachable analyzer finds statements that execution can never reach -because they are preceded by an return statement, a call to panic, an -infinite loop, or similar constructs. - +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unreachable) +- -**Enabled by default.** - -## **unsafeptr** - --check for invalid conversions of uintptr to unsafe.Pointer +-unsafeptr: check for invalid conversions of uintptr to unsafe.Pointer - -The unsafeptr analyzer reports likely incorrect uses of unsafe.Pointer -to convert integers to pointers. A conversion from uintptr to @@ -835,26 +1109,44 @@ diff -urN a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md -word in memory that holds a pointer value, because that word will be -invisible to stack copying and to the garbage collector. - +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unsafeptr) +- -**Enabled by default.** - -## **unusedparams** - --check for unused parameters of functions +-unusedparams: check for unused parameters of functions - -The unusedparams analyzer checks functions to see if there are -any parameters that are not being used. - --To reduce false positives it ignores: --- methods --- parameters that do not have a name or have the name '_' (the blank identifier) --- functions in test files --- functions with empty bodies or those with just a return stmt +-To ensure soundness, it ignores: +- - "address-taken" functions, that is, functions that are used as +- a value rather than being called directly; their signatures may +- be required to conform to a func type. +- - exported functions or methods, since they may be address-taken +- in another package. +- - unexported methods whose name matches an interface method +- declared in the same package, since the method's signature +- may be required to conform to the interface type. +- - functions with empty bodies, or containing just a call to panic. +- - parameters that are unnamed, or named "_", the blank identifier. +- +-The analyzer suggests a fix of replacing the parameter name by "_", +-but in such cases a deeper fix can be obtained by invoking the +-"Refactor: remove unused parameter" code action, which will +-eliminate the parameter entirely, along with all corresponding +-arguments at call sites, while taking care to preserve any side +-effects in the argument expressions; see +-https://github.com/golang/tools/releases/tag/gopls%2Fv0.14. +- +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/unusedparams) - --**Disabled by default. Enable it by setting `"analyses": {"unusedparams": true}`.** +-**Enabled by default.** - -## **unusedresult** - --check for unused results of calls to some functions +-unusedresult: check for unused results of calls to some functions - -Some functions like fmt.Errorf return a result and have no side -effects, so it is always a mistake to discard the result. Other @@ -864,11 +1156,21 @@ diff -urN a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md - -The set of functions may be controlled using flags. - +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unusedresult) +- -**Enabled by default.** - +-## **unusedvariable** +- +-unusedvariable: check for unused variables and suggest fixes +- +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/unusedvariable) +- +-**Disabled by default. Enable it by setting `"analyses": {"unusedvariable": true}`.** +- -## **unusedwrite** - --checks for unused writes +-unusedwrite: checks for unused writes - -The analyzer reports instances of writes to struct fields and -arrays that are never read. Specifically, when a struct object @@ -894,126 +1196,17 @@ diff -urN a/gopls/doc/analyzers.md b/gopls/doc/analyzers.md - t.x = i // unused write to field x - } - --**Disabled by default. Enable it by setting `"analyses": {"unusedwrite": true}`.** -- --## **useany** -- --check for constraints that could be simplified to "any" -- --**Disabled by default. Enable it by setting `"analyses": {"useany": true}`.** -- --## **fillreturns** -- --suggest fixes for errors due to an incorrect number of return values -- --This checker provides suggested fixes for type errors of the --type "wrong number of return values (want %d, got %d)". For example: -- func m() (int, string, *bool, error) { -- return -- } --will turn into -- func m() (int, string, *bool, error) { -- return 0, "", nil, nil -- } -- --This functionality is similar to https://github.com/sqs/goreturns. -- +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unusedwrite) - -**Enabled by default.** - --## **nonewvars** -- --suggested fixes for "no new vars on left side of :=" -- --This checker provides suggested fixes for type errors of the --type "no new vars on left side of :=". For example: -- z := 1 -- z := 2 --will turn into -- z := 1 -- z = 2 -- -- --**Enabled by default.** -- --## **noresultvalues** -- --suggested fixes for unexpected return values -- --This checker provides suggested fixes for type errors of the --type "no result values expected" or "too many return values". --For example: -- func z() { return nil } --will turn into -- func z() { return } -- -- --**Enabled by default.** -- --## **undeclaredname** -- --suggested fixes for "undeclared name: <>" -- --This checker provides suggested fixes for type errors of the --type "undeclared name: <>". It will either insert a new statement, --such as: -- --"<> := " -- --or a new function declaration, such as: -- --func <>(inferred parameters) { -- panic("implement me!") --} -- -- --**Enabled by default.** -- --## **unusedvariable** -- --check for unused variables -- --The unusedvariable analyzer suggests fixes for unused variables errors. -- -- --**Disabled by default. Enable it by setting `"analyses": {"unusedvariable": true}`.** -- --## **fillstruct** -- --note incomplete struct initializations -- --This analyzer provides diagnostics for any struct literals that do not have --any fields initialized. Because the suggested fix for this analysis is --expensive to compute, callers should compute it separately, using the --SuggestedFix function below. -- -- --**Enabled by default.** -- --## **infertypeargs** -- --check for unnecessary type arguments in call expressions -- --Explicit type arguments may be omitted from call expressions if they can be --inferred from function arguments, or from other type arguments: -- -- func f[T any](T) {} -- -- func _() { -- f[string]("foo") // string could be inferred -- } -- -- --**Enabled by default.** -- --## **stubmethods** +-## **useany** - --stub methods analyzer +-useany: check for constraints that could be simplified to "any" - --This analyzer generates method stubs for concrete types --in order to implement a target interface +-[Full documentation](https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/useany) - --**Enabled by default.** +-**Disabled by default. Enable it by setting `"analyses": {"useany": true}`.** - - diff -urN a/gopls/doc/command-line.md b/gopls/doc/command-line.md @@ -1038,7 +1231,7 @@ diff -urN a/gopls/doc/command-line.md b/gopls/doc/command-line.md diff -urN a/gopls/doc/commands.md b/gopls/doc/commands.md --- a/gopls/doc/commands.md 2000-01-01 00:00:00.000000000 -0000 +++ b/gopls/doc/commands.md 1970-01-01 00:00:00.000000000 +0000 -@@ -1,606 +0,0 @@ +@@ -1,749 +0,0 @@ -# Commands - -This document describes the LSP-level commands supported by `gopls`. They cannot be invoked directly by users, and all the details are subject to change, so nobody should rely on this information. @@ -1082,7 +1275,7 @@ diff -urN a/gopls/doc/commands.md b/gopls/doc/commands.md -} -``` - --### **update the given telemetry counters.** +-### **Update the given telemetry counters** -Identifier: `gopls.add_telemetry_counters` - -Gopls will prepend "fwd/" to all the counters updated using this command @@ -1107,7 +1300,14 @@ diff -urN a/gopls/doc/commands.md b/gopls/doc/commands.md - -``` -{ -- // The fix to apply. +- // The name of the fix to apply. +- // +- // For fixes suggested by analyzers, this is a string constant +- // advertised by the analyzer that matches the Category of +- // the analysis.Diagnostic with a SuggestedFix containing no edits. +- // +- // For fixes suggested by code actions, this is a string agreed +- // upon by the code action and golang.ApplyFix. - "Fix": string, - // The file URI for the document to fix. - "URI": string, @@ -1122,10 +1322,51 @@ diff -urN a/gopls/doc/commands.md b/gopls/doc/commands.md - "character": uint32, - }, - }, +- // Whether to resolve and return the edits. +- "ResolveEdits": bool, +-} +-``` +- +-Result: +- +-``` +-{ +- // Holds changes to existing resources. +- "changes": map[golang.org/x/tools/gopls/internal/protocol.DocumentURI][]golang.org/x/tools/gopls/internal/protocol.TextEdit, +- // Depending on the client capability `workspace.workspaceEdit.resourceOperations` document changes +- // are either an array of `TextDocumentEdit`s to express changes to n different text documents +- // where each text document edit addresses a specific version of a text document. Or it can contain +- // above `TextDocumentEdit`s mixed with create, rename and delete file / folder operations. +- // +- // Whether a client supports versioned document edits is expressed via +- // `workspace.workspaceEdit.documentChanges` client capability. +- // +- // If a client neither supports `documentChanges` nor `workspace.workspaceEdit.resourceOperations` then +- // only plain `TextEdit`s using the `changes` property are supported. +- "documentChanges": []{ +- "TextDocumentEdit": { +- "textDocument": { ... }, +- "edits": { ... }, +- }, +- "RenameFile": { +- "kind": string, +- "oldUri": string, +- "newUri": string, +- "options": { ... }, +- "ResourceOperation": { ... }, +- }, +- }, +- // A map of change annotations that can be referenced in `AnnotatedTextEdit`s or create, rename and +- // delete file / folder operations. +- // +- // Whether clients honor this property depends on the client capability `workspace.changeAnnotationSupport`. +- // +- // @since 3.16.0 +- "changeAnnotations": map[string]golang.org/x/tools/gopls/internal/protocol.ChangeAnnotation, -} -``` - --### **performs a "change signature" refactoring.** +-### **Perform a "change signature" refactoring** -Identifier: `gopls.change_signature` - -This command is experimental, currently only supporting parameter removal. @@ -1142,6 +1383,47 @@ diff -urN a/gopls/doc/commands.md b/gopls/doc/commands.md - "end": { ... }, - }, - }, +- // Whether to resolve and return the edits. +- "ResolveEdits": bool, +-} +-``` +- +-Result: +- +-``` +-{ +- // Holds changes to existing resources. +- "changes": map[golang.org/x/tools/gopls/internal/protocol.DocumentURI][]golang.org/x/tools/gopls/internal/protocol.TextEdit, +- // Depending on the client capability `workspace.workspaceEdit.resourceOperations` document changes +- // are either an array of `TextDocumentEdit`s to express changes to n different text documents +- // where each text document edit addresses a specific version of a text document. Or it can contain +- // above `TextDocumentEdit`s mixed with create, rename and delete file / folder operations. +- // +- // Whether a client supports versioned document edits is expressed via +- // `workspace.workspaceEdit.documentChanges` client capability. +- // +- // If a client neither supports `documentChanges` nor `workspace.workspaceEdit.resourceOperations` then +- // only plain `TextEdit`s using the `changes` property are supported. +- "documentChanges": []{ +- "TextDocumentEdit": { +- "textDocument": { ... }, +- "edits": { ... }, +- }, +- "RenameFile": { +- "kind": string, +- "oldUri": string, +- "newUri": string, +- "options": { ... }, +- "ResourceOperation": { ... }, +- }, +- }, +- // A map of change annotations that can be referenced in `AnnotatedTextEdit`s or create, rename and +- // delete file / folder operations. +- // +- // Whether clients honor this property depends on the client capability `workspace.changeAnnotationSupport`. +- // +- // @since 3.16.0 +- "changeAnnotations": map[string]golang.org/x/tools/gopls/internal/protocol.ChangeAnnotation, -} -``` - @@ -1161,6 +1443,43 @@ diff -urN a/gopls/doc/commands.md b/gopls/doc/commands.md -} -``` - +-### **Cause server to publish diagnostics for the specified files.** +-Identifier: `gopls.diagnose_files` +- +-This command is needed by the 'gopls {check,fix}' CLI subcommands. +- +-Args: +- +-``` +-{ +- "Files": []string, +-} +-``` +- +-### **View package documentation.** +-Identifier: `gopls.doc` +- +-Opens the Go package documentation page for the current +-package in a browser. +- +-Args: +- +-``` +-{ +- "uri": string, +- "range": { +- "start": { +- "line": uint32, +- "character": uint32, +- }, +- "end": { +- "line": uint32, +- "character": uint32, +- }, +- }, +-} +-``` +- -### **Run go mod edit -go=version** -Identifier: `gopls.edit_go_directive` - @@ -1194,7 +1513,7 @@ diff -urN a/gopls/doc/commands.md b/gopls/doc/commands.md -Result: - -``` --map[golang.org/x/tools/gopls/internal/lsp/protocol.DocumentURI]*golang.org/x/tools/gopls/internal/vulncheck.Result +-map[golang.org/x/tools/gopls/internal/protocol.DocumentURI]*golang.org/x/tools/gopls/internal/vulncheck.Result -``` - -### **Toggle gc_details** @@ -1224,7 +1543,7 @@ diff -urN a/gopls/doc/commands.md b/gopls/doc/commands.md -} -``` - --### **go get a package** +-### **'go get' a package** -Identifier: `gopls.go_get_package` - -Runs `go get` to fetch a package. @@ -1299,13 +1618,14 @@ diff -urN a/gopls/doc/commands.md b/gopls/doc/commands.md -} -``` - --### **checks for the right conditions, and then prompts** +-### **Prompt user to enable telemetry** -Identifier: `gopls.maybe_prompt_for_telemetry` - --the user to ask if they want to enable Go telemetry uploading. If the user --responds 'Yes', the telemetry mode is set to "on". +-Checks for the right conditions, and then prompts the user +-to ask if they want to enable Go telemetry uploading. If +-the user responds 'Yes', the telemetry mode is set to "on". - --### **fetch memory statistics** +-### **Fetch memory statistics** -Identifier: `gopls.mem_stats` - -Call runtime.GC multiple times and return memory statistics as reported by @@ -1375,10 +1695,10 @@ diff -urN a/gopls/doc/commands.md b/gopls/doc/commands.md -} -``` - --### **run `go work [args...]`, and apply the resulting go.work** +-### **Run `go work [args...]`, and apply the resulting go.work** -Identifier: `gopls.run_go_work_command` - --edits to the current go.work file. +-edits to the current go.work file - -Args: - @@ -1390,7 +1710,7 @@ diff -urN a/gopls/doc/commands.md b/gopls/doc/commands.md -} -``` - --### **Run vulncheck.** +-### **Run vulncheck** -Identifier: `gopls.run_govulncheck` - -Run vulnerability check (`govulncheck`). @@ -1479,7 +1799,7 @@ diff -urN a/gopls/doc/commands.md b/gopls/doc/commands.md -} -``` - --### **start capturing a profile of gopls' execution.** +-### **Start capturing a profile of gopls' execution** -Identifier: `gopls.start_profile` - -Start a new pprof profile. Before using the resulting file, profiling must @@ -1500,7 +1820,7 @@ diff -urN a/gopls/doc/commands.md b/gopls/doc/commands.md -struct{} -``` - --### **stop an ongoing profile.** +-### **Stop an ongoing profile** -Identifier: `gopls.stop_profile` - -This command is intended for internal use only, by the gopls benchmark @@ -1608,7 +1928,23 @@ diff -urN a/gopls/doc/commands.md b/gopls/doc/commands.md -} -``` - --### **fetch workspace statistics** +-### **List current Views on the server.** +-Identifier: `gopls.views` +- +-This command is intended for use by gopls tests only. +- +-Result: +- +-``` +-[]{ +- "Type": string, +- "Root": string, +- "Folder": string, +- "EnvOverlay": []string, +-} +-``` +- +-### **Fetch workspace statistics** -Identifier: `gopls.workspace_stats` - -Query statistics about workspace builds, modules, packages, and files. @@ -1648,7 +1984,7 @@ diff -urN a/gopls/doc/commands.md b/gopls/doc/commands.md diff -urN a/gopls/doc/contributing.md b/gopls/doc/contributing.md --- a/gopls/doc/contributing.md 2000-01-01 00:00:00.000000000 -0000 +++ b/gopls/doc/contributing.md 1970-01-01 00:00:00.000000000 +0000 -@@ -1,165 +0,0 @@ +@@ -1,167 +0,0 @@ -# Documentation for contributors - -This documentation augments the general documentation for contributing to the @@ -1669,8 +2005,8 @@ diff -urN a/gopls/doc/contributing.md b/gopls/doc/contributing.md - -## Getting started - --Most of the `gopls` logic is in the `golang.org/x/tools/gopls/internal/lsp` --directory. +-Most of the `gopls` logic is in the `golang.org/x/tools/gopls/internal` +-directory. See [design/implementation.md] for an overview of the code organization. - -## Build - @@ -1745,41 +2081,43 @@ diff -urN a/gopls/doc/contributing.md b/gopls/doc/contributing.md - -## Testing - --To run tests for just `gopls/`, run, -- --```bash --cd /path/to/tools/gopls --go test ./... --``` -- --But, much of the gopls work involves `internal/lsp` too, so you will want to --run both: +-The normal command you should use to run the tests after a change is: - -```bash --cd /path/to/tools --cd gopls && go test ./... --cd .. --go test ./internal/lsp/... +-gopls$ go test -short ./... -``` - --There is additional information about the `internal/lsp` tests in the --[internal/lsp/tests `README`](https://github.com/golang/tools/blob/master/internal/lsp/tests/README.md). -- --### Regtests -- --gopls has a suite of regression tests defined in the `./gopls/internal/regtest` --directory. Each of these tests writes files to a temporary directory, starts a --separate gopls session, and scripts interactions using an editor-like API. As a --result of this overhead they can be quite slow, particularly on systems where --file operations are costly. -- --Due to the asynchronous nature of the LSP, regtests assertions are written --as 'expectations' that the editor state must achieve _eventually_. This can --make debugging the regtests difficult. To aid with debugging, the regtests --output their LSP logs on any failure. If your CL gets a test failure while --running the regtests, please do take a look at the description of the error and --the LSP logs, but don't hesitate to [reach out](#getting-help) to the gopls --team if you need help. +-(The `-short` flag skips some slow-running ones. The trybot builders +-run the complete set, on a wide range of platforms.) +- +-Gopls tests are a mix of two kinds. +- +-- [Marker tests](../internal/test/marker) express each test scenario +- in a standalone text file that contains the target .go, go.mod, and +- go.work files, in which special annotations embedded in comments +- drive the test. These tests are generally easy to write and fast +- to iterate, but have limitations on what they can express. +- +-- [Integration tests](../internal/test/integration) are regular Go +- `func Test(*testing.T)` functions that make a series of calls to an +- API for a fake LSP-enabled client editor. The API allows you to open +- and edit a file, navigate to a definition, invoke other LSP +- operations, and assert properties about the state. +- +- Due to the asynchronous nature of the LSP, integration tests make +- assertions about states that the editor must achieve eventually, +- even when the program goes wrong quickly, it may take a while before +- the error is reported as a failure to achieve the desired state +- within several minutes. We recommend that you set +- `GOPLS_INTEGRATION_TEST_TIMEOUT=10s` to reduce the timeout for +- integration tests when debugging. +- +- When they fail, the integration tests print the log of the LSP +- session between client and server. Though verbose, they are very +- helpful for debugging once you know how to read them. +- +-Don't hesitate to [reach out](#getting-help) to the gopls team if you +-need help. - -### CI - @@ -2001,12 +2339,60 @@ diff -urN a/gopls/doc/daemon.md b/gopls/doc/daemon.md -Note that once the daemon is already running, setting these flags will not -change its configuration. These flags only matter for the forwarder process -that actually starts the daemon. +diff -urN a/gopls/doc/design/architecture.svg b/gopls/doc/design/architecture.svg +--- a/gopls/doc/design/architecture.svg 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/doc/design/architecture.svg 1970-01-01 00:00:00.000000000 +0000 +@@ -1 +0,0 @@ +- +\ No newline at end of file diff -urN a/gopls/doc/design/design.md b/gopls/doc/design/design.md --- a/gopls/doc/design/design.md 2000-01-01 00:00:00.000000000 -0000 +++ b/gopls/doc/design/design.md 1970-01-01 00:00:00.000000000 +0000 -@@ -1,394 +0,0 @@ +@@ -1,436 +0,0 @@ -# `gopls` design documentation - +-## _A note from the future_ +- +-What follows below is the original design document for gopls, aggregated from +-various sources spanning 2018 and 2019. Since then, all of the features listed +-below have been implemented, along with many others. The first two goals have +-been achieved: gopls is a full implementation of the LSP, and the default +-backend for VS Code Go and many other editors. The third goal has only been +-partially realized: while gopls has gained many features, it is not extensible +-in the sense used in this document: the only way to extend gopls is to modify +-gopls. The fourth goal is not achieved: while some notable companies are able +-to use gopls with Bazel, the experience is subpar, and the Go command is the +-only officially supported build system. +- +-On the other hand, two of the explicit non-goals have been reconsidered. One is +-minor: syntax highlighting is now supported in the LSP by way of semantic +-tokens. The other is major: as gopls gained popularity, it became apparent that +-its memory footprint was a problem. The size of developer workspaces was +-increasing faster than the RAM available in typically development environments +-(particularly with containerized development). Gopls now uses a hybrid of +-on-disk indexes and in-memory caches, described in more detail in our +-[blog post on scalability](https://go.dev/blog/gopls-scalability). +- +-Notably, in anticipating difficulties this doc turned out to be prescient. +-Gopls has indeed struggled against the core standary library packages upon +-which it is built, and its user experience is still limited by the LSP. +-Nevertheless, sticking with the standard library and LSP was the right +-approach, as despite our small team these decisions have helped gopls keep up +-with the evolving Go language (i.e. generics), and to integrate with many new +-text editors. +- +-Gopls development continues, more than four years later, with a focus on +-simplicity, reliability, and extensibility. The new, opt-in +-[Go telemetry](https://github.com/golang/tools/releases/tag/gopls%2Fv0.14.0) +-will help us attain a higher standard of stability in our releases than we've +-been able to achieve through Github issues alone. Furthermore, telemetry will +-allow us to focus on high-priority features, and deprecate historical +-workarounds that burden the codebase. With greater velocity, we look forward +-to working with the community on improved refactoring, static analysis, and +-whatever else the future brings. +- +-- _Rob Findley (rfindley@google.com), 2023_ +- -## Goals - -* `gopls` should **become the default editor backend** for the major editors used by Go programmers, fully supported by the Go team. @@ -2402,59 +2788,183 @@ diff -urN a/gopls/doc/design/design.md b/gopls/doc/design/design.md diff -urN a/gopls/doc/design/implementation.md b/gopls/doc/design/implementation.md --- a/gopls/doc/design/implementation.md 2000-01-01 00:00:00.000000000 -0000 +++ b/gopls/doc/design/implementation.md 1970-01-01 00:00:00.000000000 +0000 -@@ -1,48 +0,0 @@ --# gopls implementation documentation -- --This is not intended as a complete description of the implementation, for the most the part the package godoc, code comments and the code itself hold that. --Instead this is meant to be a guide into finding parts of the implementation, and understanding some core concepts used throughout the implementation. -- --## View/Session/Cache -- --Throughout the code there are references to these three concepts, and they build on each other. -- --At the base is the *Cache*. This is the level at which we hold information that is global in nature, for instance information about the file system and its contents. -- --Above that is the *Session*, which holds information for a connection to an editor. This layer hold things like the edited files (referred to as overlays). -- --The top layer is called the *View*. This holds the configuration, and the mapping to configured packages. -- --The purpose of this layering is to allow a single editor session to have multiple views active whilst still sharing as much information as possible for efficiency. --In theory if only the View layer existed, the results would be identical, but slower and using more memory. -- --## Code location -- --gopls will be developed in the [x/tools] Go repository; the core packages are in [internal/lsp], and the binary and integration tests are located in [gopls]. -- --Below is a list of the core packages of gopls, and their primary purpose: +@@ -1,172 +0,0 @@ - --Package | Description ----- | --- --[gopls] | the main binary, plugins and integration tests --[internal/lsp] | the core message handling package --[internal/lsp/cache] | the cache layer --[internal/lsp/cmd] | the gopls command line layer --[internal/lsp/debug] | features to aid in debugging gopls --[internal/lsp/protocol] | the types of LSP request and response messages --[internal/lsp/source] | the core feature implementations --[internal/span] | a package for dealing with source file locations --[internal/memoize] | a function invocation cache used to reduce the work done --[internal/jsonrpc2] | an implementation of the JSON RPC2 specification +-# Gopls architecture +- +-Last major update: Jan 16 2024 +- +-This doc presents a high-level overview of the structure of gopls to +-help new contributors find their way. It is not intended to be a +-complete description of the implementation, nor even of any key +-components; for that, the package documentation (linked below) and +-other comments within the code are a better guide. +- +-The diagram below shows selected components of the gopls module and +-their relationship to each other according to the Go import graph. +-Tests and test infrastructure are not shown, nor are utility packages, +-nor packages from the [x/tools] module. For brevity, packages are +-referred to by their last segment, which is usually unambiguous. +- +-The height of each blob corresponds loosely to its technical depth. +-Some blocks are wide and shallow, such as [protocol], which declares +-Go types for the entire LSP protocol. Others are deep, such as [cache] +-and [golang], as they contain a lot of dense logic and algorithms. +- +- +-![Gopls architecture](architecture.svg) +- +-Starting from the bottom, we'll describe the various components. +- +-The lowest layer defines the request and response types of the +-Language Server Protocol: +- +-- The [protocol] package defines the standard protocol; it is mostly +- generated mechanically from the schema definition provided by +- Microsoft. +- The most important type is DocumentURI, which represents a `file:` +- URL that identifies a client editor document. It also provides +- `Mapper`, which maps between the different coordinate systems used +- for source positions: UTF-8, UTF-16, and token.Pos. +- +-- The [command] package defines Gopls's non-standard commands, which +- are all invoked through the `workspace/executeCommand` extension +- mechanism. These commands are typically returned by the server as +- continuations of Code Actions or Code Lenses; most clients do not +- construct calls to them directly. +- +-The next layer defines a number of important and very widely used data structures: +- +-- The [file] package defines the primary abstractions of a client +- file: its `Identity` (URI and content hash), and its `Handle` (which +- additionally provides the version and content of a particular +- snapshot of the file. +- +-- The [parsego] package defines `File`, the parsed form of a Go source +- file, including its content, syntax tree, and coordinary mappings +- (Mapper and token.File). The package performs various kinds of tree +- repair to work around error-recovery shortcomings of the Go parser. +- +-- The [metadata] package defines `Package`, an abstraction of the +- metadata of a Go package, similar to the output of `go list -json`. +- Metadata is produced from [go/packages], which takes +- care of invoking `go list`. (Users report that it works to some extent +- with a GOPACKAGESDRIVER for Bazel, though we maintain no tests for this +- scenario.) +- +- The package also provides `Graph`, the complete import graph for a +- workspace; each graph node is a `Package`. +- +-The [settings] layer defines the data structure (effectively a large +-tree) for gopls configuration options, along with its JSON encoding. +- +-The [cache] layer is the largest and most complex component of gopls. +-It is concerned with state management, dependency analysis, and invalidation: +-the `Session` of communication with the client; +-the `Folder`s that the client has opened; +-the `View` of a particular workspace tree with particular build +-options; +-the `Snapshot` of the state of all files in the workspace after a +-particular edit operation; +-the contents of all files, whether saved to disk (`DiskFile`) or +-edited and unsaved (`Overlay`); +-the `Cache` of in-memory memoized computations, +-such as parsing go.mod files or build the symbol index; +-and the `Package`, which holds the results of type checking a package +-from Go syntax. +- +-The cache layer depends on various auxiliary packages, including: +- +-- The [filecache] package, which manages gopls' persistent, transactional, +- file-based key/value store. +- +-- The [xrefs], [methodsets], and [typerefs] packages define algorithms +- for constructing indexes of information derived from type-checking, +- and for encoding and decoding these serializable indexes in the file +- cache. +- +- Together these packages enable the fast restart, reduced memory +- consumption, and synergy across processes that were delivered by the +- v0.12 redesign and described in ["Scaling gopls for the growing Go +- ecosystem"](https://go.dev/blog/gopls-scalability). +- +-The cache also defines gopls's [go/analysis] driver, which runs +-modular analysis (similar to `go vet`) across the workspace. +-Gopls also includes a number of analysis passes that are not part of vet. +- +-The next layer defines four packages, each for handling files in a +-particular language: +-[mod] for go.mod files; +-[work] for go.work files; +-[template] for files in `text/template` syntax; and +-[golang], for files in Go itself. +-This package, by far the largest, provides the main features of gopls: +-navigation, analysis, and refactoring of Go code. +-As most users imagine it, this package _is_ gopls. +- +-The [server] package defines the LSP service implementation, with one +-handler method per LSP request type. Each handler switches on the type +-of the file and dispatches to one of the four language-specific +-packages. +- +-The [lsprpc] package connects the service interface to our [JSON RPC](jsonrpc2) +-server. +- +-Bear in mind that the diagram is a dependency graph, a "static" +-viewpoint of the program's structure. A more dynamic viewpoint would +-order the packages based on the sequence in which they are encountered +-during processing of a particular request; in such a view, the bottom +-layer would represent the "wire" (protocol and command), the next +-layer up would hold the RPC-related packages (lsprpc and server), and +-features (e.g. golang, mod, work, template) would be at the top. +- +- - --[gopls]: https://github.com/golang/tools/tree/master/gopls --[internal/jsonrpc2]: https://github.com/golang/tools/tree/master/internal/jsonrpc2 --[internal/lsp]: https://github.com/golang/tools/tree/master/gopls/internal/lsp --[internal/lsp/cache]: https://github.com/golang/tools/tree/master/gopls/internal/lsp/cache --[internal/lsp/cmd]: https://github.com/golang/tools/tree/master/gopls/internal/lsp/cmd --[internal/lsp/debug]: https://github.com/golang/tools/tree/master/gopls/internal/lsp/debug --[internal/lsp/protocol]: https://github.com/golang/tools/tree/master/gopls/internal/lsp/protocol --[internal/lsp/source]: https://github.com/golang/tools/tree/master/gopls/internal/lsp/source --[internal/memoize]: https://github.com/golang/tools/tree/master/internal/memoize --[internal/span]: https://github.com/golang/tools/tree/master/gopls/internal/span --[x/tools]: https://github.com/golang/tools +-The [cmd] package defines the command-line interface of the `gopls` +-command, around which gopls's main package is just a trivial wrapper. +-It is usually run without arguments, causing it to start a server and +-listen indefinitely. +-It also provides a number of subcommands that start a server, make a +-single request to it, and exit, providing traditional batch-command +-access to server functionality. These subcommands are primarily +-provided as a debugging aid (but see +-[#63693](https://github.com/golang/go/issues/63693)). +- +-[cache]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/cache +-[cmd]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/cmd +-[command]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/protocol/command +-[debug]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/debug +-[file]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/file +-[filecache]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/filecache +-[go/analysis]: https://pkg.go.dev/golang.org/x/tools@master/go/analysis +-[go/packages]: https://pkg.go.dev/golang.org/x/tools@master/go/packages +-[gopls]: https://pkg.go.dev/golang.org/x/tools/gopls@master +-[jsonrpc2]: https://pkg.go.dev/golang.org/x/tools@master/internal/jsonrpc2 +-[lsprpc]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/lsprpc +-[memoize]: https://github.com/golang/tools/tree/master/internal/memoize +-[metadata]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/cache/metadata +-[methodsets]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/cache/methodsets +-[mod]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/mod +-[parsego]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/cache/parsego +-[protocol]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/protocol +-[server]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/server +-[settings]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/settings +-[golang]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/golang +-[template]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/template +-[typerefs]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/cache/typerefs +-[work]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/work +-[x/tools]: https://github.com/golang/tools@master +-[xrefs]: https://pkg.go.dev/golang.org/x/tools/gopls@master/internal/cache/xrefs diff -urN a/gopls/doc/design/integrating.md b/gopls/doc/design/integrating.md --- a/gopls/doc/design/integrating.md 2000-01-01 00:00:00.000000000 -0000 +++ b/gopls/doc/design/integrating.md 1970-01-01 00:00:00.000000000 +0000 -@@ -1,91 +0,0 @@ +@@ -1,89 +0,0 @@ -# Documentation for plugin authors - -If you are integrating `gopls` into an editor by writing an editor plugin, there are quite a few semantics of the communication between the editor and `gopls` that are not specified by the [LSP specification]. @@ -2476,9 +2986,7 @@ diff -urN a/gopls/doc/design/integrating.md b/gopls/doc/design/integrating.md -> A position inside a document (see Position definition below) is expressed as a zero-based line and character offset. The offsets are based on a UTF-16 string representation. So a string of the form a𐐀b the character offset of the character a is 0, the character offset of 𐐀 is 1 and the character offset of b is 3 since 𐐀 is represented using two code units in UTF-16. - -This means that integrators will need to calculate UTF-16 based column offsets. -- --[`golang.org/x/tools/gopls/internal/span`] has the code to do this in go. --[#31080] tracks making `span` and other useful packages non-internal. +-Use `protocol.Mapper` for all the conversions. - -## Edits - @@ -2518,9 +3026,9 @@ diff -urN a/gopls/doc/design/integrating.md b/gopls/doc/design/integrating.md -Monitoring files inside gopls directly has a lot of awkward problems, but the [LSP specification] has methods that allow gopls to request that the client notify it of file system changes, specifically [`workspace/didChangeWatchedFiles`]. -This is currently being added to gopls by a community member, and tracked in [#31553] - --[InitializeResult]: https://pkg.go.dev/golang.org/x/tools/gopls/internal/lsp/protocol#InitializeResult --[ServerCapabilities]: https://pkg.go.dev/golang.org/x/tools/gopls/internal/lsp/protocol#ServerCapabilities --[`golang.org/x/tools/gopls/internal/span`]: https://pkg.go.dev/golang.org/x/tools/internal/span#NewPoint +-[InitializeResult]: https://pkg.go.dev/golang.org/x/tools/gopls/internal/protocol#InitializeResult +-[ServerCapabilities]: https://pkg.go.dev/golang.org/x/tools/gopls/internal/protocol#ServerCapabilities +-[`golang.org/x/tools/gopls/internal/protocol`]: https://pkg.go.dev/golang.org/x/tools/internal/protocol#NewPoint - -[LSP specification]: https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/ -[lsp-response]: https://github.com/Microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-14.md#response-message @@ -2549,7 +3057,7 @@ diff -urN a/gopls/doc/design/integrating.md b/gopls/doc/design/integrating.md diff -urN a/gopls/doc/emacs.md b/gopls/doc/emacs.md --- a/gopls/doc/emacs.md 2000-01-01 00:00:00.000000000 -0000 +++ b/gopls/doc/emacs.md 1970-01-01 00:00:00.000000000 +0000 -@@ -1,183 +0,0 @@ +@@ -1,185 +0,0 @@ -# Emacs - -## Installing `gopls` @@ -2696,12 +3204,14 @@ diff -urN a/gopls/doc/emacs.md b/gopls/doc/emacs.md -(or a key of your choice bound to the `eglot-code-actions` function) and -selecting `Organize Imports` at the prompt. - --Eglot does not currently support a standalone function to execute a specific --code action (see --[joaotavora/eglot#411](https://github.com/joaotavora/eglot/issues/411)), nor an --option to organize imports as a `before-save-hook` (see --[joaotavora/eglot#574](https://github.com/joaotavora/eglot/issues/574)). In the --meantime, see those issues for discussion and possible workarounds. +-To automatically organize imports before saving, add a hook: +- +-```elisp +-(add-hook 'before-save-hook +- (lambda () +- (call-interactively 'eglot-code-action-organize-imports)) +- nil t) +-``` - -## Troubleshooting - @@ -2795,7 +3305,7 @@ diff -urN a/gopls/doc/features.md b/gopls/doc/features.md diff -urN a/gopls/doc/generate.go b/gopls/doc/generate.go --- a/gopls/doc/generate.go 2000-01-01 00:00:00.000000000 -0000 +++ b/gopls/doc/generate.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,786 +0,0 @@ +@@ -1,783 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. @@ -2830,11 +3340,12 @@ diff -urN a/gopls/doc/generate.go b/gopls/doc/generate.go - "github.com/jba/printsrc" - "golang.org/x/tools/go/ast/astutil" - "golang.org/x/tools/go/packages" -- "golang.org/x/tools/gopls/internal/lsp/command" -- "golang.org/x/tools/gopls/internal/lsp/command/commandmeta" -- "golang.org/x/tools/gopls/internal/lsp/mod" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/gopls/internal/lsp/source" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/mod" +- "golang.org/x/tools/gopls/internal/protocol/command" +- "golang.org/x/tools/gopls/internal/protocol/command/commandmeta" +- "golang.org/x/tools/gopls/internal/settings" +- "golang.org/x/tools/gopls/internal/util/safetoken" -) - -func main() { @@ -2850,12 +3361,12 @@ diff -urN a/gopls/doc/generate.go b/gopls/doc/generate.go - return false, err - } - -- sourceDir, err := pkgDir("golang.org/x/tools/gopls/internal/lsp/source") +- settingsDir, err := pkgDir("golang.org/x/tools/gopls/internal/settings") - if err != nil { - return false, err - } - -- if ok, err := rewriteFile(filepath.Join(sourceDir, "api_json.go"), api, write, rewriteAPI); !ok || err != nil { +- if ok, err := rewriteFile(filepath.Join(settingsDir, "api_json.go"), api, write, rewriteAPI); !ok || err != nil { - return ok, err - } - @@ -2893,24 +3404,25 @@ diff -urN a/gopls/doc/generate.go b/gopls/doc/generate.go - return strings.TrimSpace(string(out)), nil -} - --func loadAPI() (*source.APIJSON, error) { +-func loadAPI() (*settings.APIJSON, error) { - pkgs, err := packages.Load( - &packages.Config{ - Mode: packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedDeps, - }, -- "golang.org/x/tools/gopls/internal/lsp/source", +- "golang.org/x/tools/gopls/internal/settings", - ) - if err != nil { - return nil, err - } - pkg := pkgs[0] - -- api := &source.APIJSON{ -- Options: map[string][]*source.OptionJSON{}, +- defaults := settings.DefaultOptions() +- api := &settings.APIJSON{ +- Options: map[string][]*settings.OptionJSON{}, +- Analyzers: loadAnalyzers(defaults.DefaultAnalyzers), // no staticcheck analyzers - } -- defaults := source.DefaultOptions() - -- api.Commands, err = loadCommands(pkg) +- api.Commands, err = loadCommands() - if err != nil { - return nil, err - } @@ -2920,15 +3432,7 @@ diff -urN a/gopls/doc/generate.go b/gopls/doc/generate.go - for _, c := range api.Commands { - c.Command = command.ID(c.Command) - } -- for _, m := range []map[string]*source.Analyzer{ -- defaults.DefaultAnalyzers, -- defaults.TypeErrorAnalyzers, -- defaults.ConvenienceAnalyzers, -- // Don't yet add staticcheck analyzers. -- } { -- api.Analyzers = append(api.Analyzers, loadAnalyzers(m)...) -- } -- api.Hints = loadHints(source.AllInlayHints) +- api.Hints = loadHints(golang.AllInlayHints) - for _, category := range []reflect.Value{ - reflect.ValueOf(defaults.UserOptions), - } { @@ -2950,7 +3454,7 @@ diff -urN a/gopls/doc/generate.go b/gopls/doc/generate.go - switch opt.Name { - case "analyses": - for _, a := range api.Analyzers { -- opt.EnumKeys.Keys = append(opt.EnumKeys.Keys, source.EnumKey{ +- opt.EnumKeys.Keys = append(opt.EnumKeys.Keys, settings.EnumKey{ - Name: fmt.Sprintf("%q", a.Name), - Doc: a.Doc, - Default: strconv.FormatBool(a.Default), @@ -2967,7 +3471,7 @@ diff -urN a/gopls/doc/generate.go b/gopls/doc/generate.go - if err != nil { - return nil, err - } -- opt.EnumKeys.Keys = append(opt.EnumKeys.Keys, source.EnumKey{ +- opt.EnumKeys.Keys = append(opt.EnumKeys.Keys, settings.EnumKey{ - Name: fmt.Sprintf("%q", l.Lens), - Doc: l.Doc, - Default: def, @@ -2975,7 +3479,7 @@ diff -urN a/gopls/doc/generate.go b/gopls/doc/generate.go - } - case "hints": - for _, a := range api.Hints { -- opt.EnumKeys.Keys = append(opt.EnumKeys.Keys, source.EnumKey{ +- opt.EnumKeys.Keys = append(opt.EnumKeys.Keys, settings.EnumKey{ - Name: fmt.Sprintf("%q", a.Name), - Doc: a.Doc, - Default: strconv.FormatBool(a.Default), @@ -2987,7 +3491,7 @@ diff -urN a/gopls/doc/generate.go b/gopls/doc/generate.go - return api, nil -} - --func loadOptions(category reflect.Value, optsType types.Object, pkg *packages.Package, hierarchy string) ([]*source.OptionJSON, error) { +-func loadOptions(category reflect.Value, optsType types.Object, pkg *packages.Package, hierarchy string) ([]*settings.OptionJSON, error) { - file, err := fileForPos(pkg, optsType.Pos()) - if err != nil { - return nil, err @@ -2998,7 +3502,7 @@ diff -urN a/gopls/doc/generate.go b/gopls/doc/generate.go - return nil, err - } - -- var opts []*source.OptionJSON +- var opts []*settings.OptionJSON - optsStruct := optsType.Type().Underlying().(*types.Struct) - for i := 0; i < optsStruct.NumFields(); i++ { - // The types field gives us the type. @@ -3045,8 +3549,8 @@ diff -urN a/gopls/doc/generate.go b/gopls/doc/generate.go - } - name := lowerFirst(typesField.Name()) - -- var enumKeys source.EnumKeys -- if m, ok := typesField.Type().(*types.Map); ok { +- var enumKeys settings.EnumKeys +- if m, ok := typesField.Type().Underlying().(*types.Map); ok { - e, ok := enums[m.Key()] - if ok { - typ = strings.Replace(typ, m.Key().String(), m.Key().Underlying().String(), 1) @@ -3067,7 +3571,7 @@ diff -urN a/gopls/doc/generate.go b/gopls/doc/generate.go - } - status := reflectStructField.Tag.Get("status") - -- opts = append(opts, &source.OptionJSON{ +- opts = append(opts, &settings.OptionJSON{ - Name: name, - Type: typ, - Doc: lowerFirst(astField.Doc.Text()), @@ -3081,8 +3585,8 @@ diff -urN a/gopls/doc/generate.go b/gopls/doc/generate.go - return opts, nil -} - --func loadEnums(pkg *packages.Package) (map[types.Type][]source.EnumValue, error) { -- enums := map[types.Type][]source.EnumValue{} +-func loadEnums(pkg *packages.Package) (map[types.Type][]settings.EnumValue, error) { +- enums := map[types.Type][]settings.EnumValue{} - for _, name := range pkg.Types.Scope().Names() { - obj := pkg.Types.Scope().Lookup(name) - cnst, ok := obj.(*types.Const) @@ -3097,7 +3601,7 @@ diff -urN a/gopls/doc/generate.go b/gopls/doc/generate.go - spec := path[1].(*ast.ValueSpec) - value := cnst.Val().ExactString() - doc := valueDoc(cnst.Name(), value, spec.Doc.Text()) -- v := source.EnumValue{ +- v := settings.EnumValue{ - Value: value, - Doc: doc, - } @@ -3106,18 +3610,18 @@ diff -urN a/gopls/doc/generate.go b/gopls/doc/generate.go - return enums, nil -} - --func collectEnumKeys(name string, m *types.Map, reflectField reflect.Value, enumValues []source.EnumValue) (*source.EnumKeys, error) { +-func collectEnumKeys(name string, m *types.Map, reflectField reflect.Value, enumValues []settings.EnumValue) (*settings.EnumKeys, error) { - // Make sure the value type gets set for analyses and codelenses - // too. - if len(enumValues) == 0 && !hardcodedEnumKeys(name) { - return nil, nil - } -- keys := &source.EnumKeys{ +- keys := &settings.EnumKeys{ - ValueType: m.Elem().String(), - } - // We can get default values for enum -> bool maps. - var isEnumBoolMap bool -- if basic, ok := m.Elem().(*types.Basic); ok && basic.Kind() == types.Bool { +- if basic, ok := m.Elem().Underlying().(*types.Basic); ok && basic.Kind() == types.Bool { - isEnumBoolMap = true - } - for _, v := range enumValues { @@ -3129,7 +3633,7 @@ diff -urN a/gopls/doc/generate.go b/gopls/doc/generate.go - return nil, err - } - } -- keys.Keys = append(keys.Keys, source.EnumKey{ +- keys.Keys = append(keys.Keys, settings.EnumKey{ - Name: v.Value, - Doc: v.Doc, - Default: def, @@ -3207,8 +3711,8 @@ diff -urN a/gopls/doc/generate.go b/gopls/doc/generate.go - return fmt.Sprintf("`%s`: %s", value, doc) -} - --func loadCommands(pkg *packages.Package) ([]*source.CommandJSON, error) { -- var commands []*source.CommandJSON +-func loadCommands() ([]*settings.CommandJSON, error) { +- var commands []*settings.CommandJSON - - _, cmds, err := commandmeta.Load() - if err != nil { @@ -3216,7 +3720,7 @@ diff -urN a/gopls/doc/generate.go b/gopls/doc/generate.go - } - // Parse the objects it contains. - for _, cmd := range cmds { -- cmdjson := &source.CommandJSON{ +- cmdjson := &settings.CommandJSON{ - Command: cmd.Name, - Title: cmd.Title, - Doc: cmd.Doc, @@ -3283,9 +3787,9 @@ diff -urN a/gopls/doc/generate.go b/gopls/doc/generate.go - return b.String() -} - --func loadLenses(commands []*source.CommandJSON) []*source.LensJSON { +-func loadLenses(commands []*settings.CommandJSON) []*settings.LensJSON { - all := map[command.Command]struct{}{} -- for k := range source.LensFuncs() { +- for k := range golang.LensFuncs() { - all[k] = struct{}{} - } - for k := range mod.LensFuncs() { @@ -3295,11 +3799,11 @@ diff -urN a/gopls/doc/generate.go b/gopls/doc/generate.go - all[k] = struct{}{} - } - -- var lenses []*source.LensJSON +- var lenses []*settings.LensJSON - - for _, cmd := range commands { - if _, ok := all[command.Command(cmd.Command)]; ok { -- lenses = append(lenses, &source.LensJSON{ +- lenses = append(lenses, &settings.LensJSON{ - Lens: cmd.Command, - Title: cmd.Title, - Doc: cmd.Doc, @@ -3309,16 +3813,16 @@ diff -urN a/gopls/doc/generate.go b/gopls/doc/generate.go - return lenses -} - --func loadAnalyzers(m map[string]*source.Analyzer) []*source.AnalyzerJSON { +-func loadAnalyzers(m map[string]*settings.Analyzer) []*settings.AnalyzerJSON { - var sorted []string - for _, a := range m { - sorted = append(sorted, a.Analyzer.Name) - } - sort.Strings(sorted) -- var json []*source.AnalyzerJSON +- var json []*settings.AnalyzerJSON - for _, name := range sorted { - a := m[name] -- json = append(json, &source.AnalyzerJSON{ +- json = append(json, &settings.AnalyzerJSON{ - Name: a.Analyzer.Name, - Doc: a.Analyzer.Doc, - URL: a.Analyzer.URL, @@ -3328,16 +3832,16 @@ diff -urN a/gopls/doc/generate.go b/gopls/doc/generate.go - return json -} - --func loadHints(m map[string]*source.Hint) []*source.HintJSON { +-func loadHints(m map[string]*golang.Hint) []*settings.HintJSON { - var sorted []string - for _, h := range m { - sorted = append(sorted, h.Name) - } - sort.Strings(sorted) -- var json []*source.HintJSON +- var json []*settings.HintJSON - for _, name := range sorted { - h := m[name] -- json = append(json, &source.HintJSON{ +- json = append(json, &settings.HintJSON{ - Name: h.Name, - Doc: h.Doc, - }) @@ -3369,7 +3873,7 @@ diff -urN a/gopls/doc/generate.go b/gopls/doc/generate.go - return nil, fmt.Errorf("no file for pos %v", pos) -} - --func rewriteFile(file string, api *source.APIJSON, write bool, rewrite func([]byte, *source.APIJSON) ([]byte, error)) (bool, error) { +-func rewriteFile(file string, api *settings.APIJSON, write bool, rewrite func([]byte, *settings.APIJSON) ([]byte, error)) (bool, error) { - old, err := os.ReadFile(file) - if err != nil { - return false, err @@ -3391,10 +3895,10 @@ diff -urN a/gopls/doc/generate.go b/gopls/doc/generate.go - return true, nil -} - --func rewriteAPI(_ []byte, api *source.APIJSON) ([]byte, error) { +-func rewriteAPI(_ []byte, api *settings.APIJSON) ([]byte, error) { - var buf bytes.Buffer -- fmt.Fprintf(&buf, "// Code generated by \"golang.org/x/tools/gopls/doc/generate\"; DO NOT EDIT.\n\npackage source\n\nvar GeneratedAPIJSON = ") -- if err := printsrc.NewPrinter("golang.org/x/tools/gopls/internal/lsp/source").Fprint(&buf, api); err != nil { +- fmt.Fprintf(&buf, "// Code generated by \"golang.org/x/tools/gopls/doc/generate\"; DO NOT EDIT.\n\npackage settings\n\nvar GeneratedAPIJSON = ") +- if err := printsrc.NewPrinter("golang.org/x/tools/gopls/internal/settings").Fprint(&buf, api); err != nil { - return nil, err - } - return format.Source(buf.Bytes()) @@ -3404,10 +3908,10 @@ diff -urN a/gopls/doc/generate.go b/gopls/doc/generate.go - title string - final string - level int -- options []*source.OptionJSON +- options []*settings.OptionJSON -} - --func rewriteSettings(doc []byte, api *source.APIJSON) ([]byte, error) { +-func rewriteSettings(doc []byte, api *settings.APIJSON) ([]byte, error) { - result := doc - for category, opts := range api.Options { - groups := collectGroups(opts) @@ -3446,8 +3950,8 @@ diff -urN a/gopls/doc/generate.go b/gopls/doc/generate.go - return replaceSection(result, "Lenses", section.Bytes()) -} - --func collectGroups(opts []*source.OptionJSON) []optionsGroup { -- optsByHierarchy := map[string][]*source.OptionJSON{} +-func collectGroups(opts []*settings.OptionJSON) []optionsGroup { +- optsByHierarchy := map[string][]*settings.OptionJSON{} - for _, opt := range opts { - optsByHierarchy[opt.Hierarchy] = append(optsByHierarchy[opt.Hierarchy], opt) - } @@ -3528,12 +4032,12 @@ diff -urN a/gopls/doc/generate.go b/gopls/doc/generate.go -func strMultiply(str string, count int) string { - var result string - for i := 0; i < count; i++ { -- result += string(str) +- result += str - } - return result -} - --func rewriteCommands(doc []byte, api *source.APIJSON) ([]byte, error) { +-func rewriteCommands(doc []byte, api *settings.APIJSON) ([]byte, error) { - section := bytes.NewBuffer(nil) - for _, command := range api.Commands { - command.Write(section) @@ -3541,11 +4045,14 @@ diff -urN a/gopls/doc/generate.go b/gopls/doc/generate.go - return replaceSection(doc, "Commands", section.Bytes()) -} - --func rewriteAnalyzers(doc []byte, api *source.APIJSON) ([]byte, error) { +-func rewriteAnalyzers(doc []byte, api *settings.APIJSON) ([]byte, error) { - section := bytes.NewBuffer(nil) - for _, analyzer := range api.Analyzers { - fmt.Fprintf(section, "## **%v**\n\n", analyzer.Name) -- fmt.Fprintf(section, "%s\n\n", analyzer.Doc) +- fmt.Fprintf(section, "%s: %s\n\n", analyzer.Name, analyzer.Doc) +- if analyzer.URL != "" { +- fmt.Fprintf(section, "[Full documentation](%s)\n\n", analyzer.URL) +- } - switch analyzer.Default { - case true: - fmt.Fprintf(section, "**Enabled by default.**\n\n") @@ -3556,7 +4063,7 @@ diff -urN a/gopls/doc/generate.go b/gopls/doc/generate.go - return replaceSection(doc, "Analyzers", section.Bytes()) -} - --func rewriteInlayHints(doc []byte, api *source.APIJSON) ([]byte, error) { +-func rewriteInlayHints(doc []byte, api *settings.APIJSON) ([]byte, error) { - section := bytes.NewBuffer(nil) - for _, hint := range api.Hints { - fmt.Fprintf(section, "## **%v**\n\n", hint.Name) @@ -3614,6 +4121,61 @@ diff -urN a/gopls/doc/generate_test.go b/gopls/doc/generate_test.go - t.Error("documentation needs updating. Run: cd gopls && go generate") - } -} +diff -urN a/gopls/doc/helix.md b/gopls/doc/helix.md +--- a/gopls/doc/helix.md 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/doc/helix.md 1970-01-01 00:00:00.000000000 +0000 +@@ -1,51 +0,0 @@ +-# Helix +- +-Configuring `gopls` to work with Helix is rather straightforward. Install `gopls`, and then add it to the `PATH` variable. If it is in the `PATH` variable, Helix will be able to detect it automatically. +- +-The documentation explaining how to install the default language servers for Helix can be found [here](https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers) +- +-## Installing `gopls` +- +-The first step is to install `gopls` on your machine. +-You can follow installation instructions [here](https://github.com/golang/tools/tree/master/gopls#installation). +- +-## Setting your path to include `gopls` +- +-Set your `PATH` environment variable to point to `gopls`. +-If you used `go install` to download `gopls`, it should be in `$GOPATH/bin`. +-If you don't have `GOPATH` set, you can use `go env GOPATH` to find it. +- +-## Additional information +- +-You can find more information about how to set up the LSP formatter [here](https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers#autoformatting). +- +-It is possible to use `hx --health go` to see that the language server is properly set up. +- +-### Configuration +- +-The settings for `gopls` can be configured in the `languages.toml` file. +-The official Helix documentation for this can be found [here](https://docs.helix-editor.com/languages.html) +- +-Configuration pertaining to `gopls` should be in the table `language-server.gopls`. +- +-#### How to set flags +- +-To set flags, add them to the `args` array in the `language-server.gopls` section of the `languages.toml` file. +- +-#### How to set LSP configuration +- +-Configuration options can be set in the `language-server.gopls.config` section of the `languages.toml` file, or in the `config` key of the `language-server.gopls` section of the `languages.toml` file. +- +-#### A minimal config example +- +-In the `~/.config/helix/languages.toml` file, the following snippet would set up `gopls` with a logfile located at `/tmp/gopls.log` and enable staticcheck. +- +-```toml +-[language-server.gopls] +-command = "gopls" +-args = ["-logfile=/tmp/gopls.log", "serve"] +-[language-server.gopls.config] +-"ui.diagnostic.staticcheck" = true +-``` +- +- diff -urN a/gopls/doc/inlayHints.md b/gopls/doc/inlayHints.md --- a/gopls/doc/inlayHints.md 2000-01-01 00:00:00.000000000 -0000 +++ b/gopls/doc/inlayHints.md 1970-01-01 00:00:00.000000000 +0000 @@ -3865,6 +4427,118 @@ diff -urN a/gopls/doc/refactor-inline.md b/gopls/doc/refactor-inline.md -Please give the inliner a try, and if you find any bugs (where the -transformation is incorrect), please do report them. We'd also like to -hear what "optimizations" you'd like to see next. +diff -urN a/gopls/doc/release/README b/gopls/doc/release/README +--- a/gopls/doc/release/README 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/doc/release/README 1970-01-01 00:00:00.000000000 +0000 +@@ -1,10 +0,0 @@ +-This directory contains the draft release notes for each upcoming release. +- +-Be sure to update the file for the forthcoming release in the same CL +-that you add new features or fix noteworthy bugs. +- +-See https://github.com/golang/tools/releases for all past releases. +- +-Tip: when reviewing edits to markdown files in Gerrit, to see the +-rendered form, click the "Open in Code Search" link (magnifying glass +-in blue square) then click "View in > gitiles" (shortcut: `v g`). +diff -urN a/gopls/doc/release/v0.16.0.md b/gopls/doc/release/v0.16.0.md +--- a/gopls/doc/release/v0.16.0.md 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/doc/release/v0.16.0.md 1970-01-01 00:00:00.000000000 +0000 +@@ -1,93 +0,0 @@ +-gopls/v0.16.0 +- +-``` +-go install golang.org/x/tools/gopls@v0.16.0 +-``` +- +-## New features +- +-### Integrated documentation viewer +- +-Gopls now offers a "View package documentation" code action that opens +-a local web page displaying the generated documentation for the +-current Go package in a form similar to https://pkg.go.dev. +-The page will be initially scrolled to the documentation for the +-declaration containing the cursor. +-Use this feature to preview the marked-up documentation as you prepare API +-changes, or to read the documentation for locally edited packages, +-even ones that have not yet been saved. Reload the page after an edit +-to see updated documentation. +- +-TODO: demo in VS Code. +- +-Clicking on the source-code link associated with a declaration will +-cause your editor to navigate to the declaration. +- +-TODO: demo of source linking. +- +-Editor support: +- +-- VS Code: use the `Source action > View package documentation` menu item. +- Note: source links navigate the editor but don't yet raise the window yet. +- Please upvote https://github.com/microsoft/vscode/issues/208093 and +- https://github.com/microsoft/vscode/issues/207634 (temporarily closed). +- +-- Emacs: requires eglot v1.17. You may find this `go-doc` function a +- useful shortcut: +- +-```lisp +-(eglot--code-action eglot-code-action-doc "source.doc") +- +-(defalias 'go-doc #'eglot-code-action-doc +- "View documentation for the current Go package.") +-``` +- +-- TODO: test in vim, neovim, sublime, helix. +- +-### `unusedwrite` analyzer +- +-The new +-[unusedwrite](https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unusedwrite) +-analyzer reports assignments, often to fields of structs, that have no +-effect because, for example, the struct is never used again: +- +-```go +-func scheme(host string) string { +- u := &url.URL{ +- Host: host, // "unused write to field Host" (no need to construct a URL) +- Scheme: "https:", +- } +- return u.Scheme +-} +-``` +- +-This is at best an indication that the code is unnecessarily complex +-(for instance, some dead code could be removed), but often indicates a +-bug, as in this example: +- +-```go +-type S struct { x int } +- +-func (s S) set(x int) { +- s.x = x // "unused write to field x" (s should be a *S pointer) +-} +-``` +- +- +-### Hover shows size/offset info +- +-Hovering over the identifier that declares a type or struct field now +-displays the size information for the type, and the offset information +-for the field. In addition, it reports the percentage of wasted space +-due to suboptimal ordering of struct fields, if this figure is 20% or +-higher. This information may be helpful when making space +-optimizations to your data structures, or when reading assembly code. +- +-TODO: example hover image. +- +-## Bugs fixed +- +-## Thank you to our contributors! +- +-@guodongli-google for the `unusedwrite` analyzer. +-TODO: they're a xoogler; is there a more current GH account? +\ No newline at end of file diff -urN a/gopls/doc/releases.md b/gopls/doc/releases.md --- a/gopls/doc/releases.md 2000-01-01 00:00:00.000000000 -0000 +++ b/gopls/doc/releases.md 1970-01-01 00:00:00.000000000 +0000 @@ -4023,10 +4697,10 @@ diff -urN a/gopls/doc/semantictokens.md b/gopls/doc/semantictokens.md diff -urN a/gopls/doc/settings.md b/gopls/doc/settings.md --- a/gopls/doc/settings.md 2000-01-01 00:00:00.000000000 -0000 +++ b/gopls/doc/settings.md 1970-01-01 00:00:00.000000000 +0000 -@@ -1,581 +0,0 @@ +@@ -1,573 +0,0 @@ -# Settings - -- +- - -This document describes the global settings for `gopls` inside the editor. -The settings block will be called `"gopls"` and contains a collection of @@ -4111,34 +4785,26 @@ diff -urN a/gopls/doc/settings.md b/gopls/doc/settings.md - -Default: `[]`. - --#### **memoryMode** *enum* +-#### **memoryMode** *string* - -**This setting is experimental and may be deleted.** - --memoryMode controls the tradeoff `gopls` makes between memory usage and --correctness. -- --Values other than `Normal` are untested and may break in surprising ways. -- --Must be one of: -- --* `"DegradeClosed"`: In DegradeClosed mode, `gopls` will collect less information about --packages without open files. As a result, features like Find --References and Rename will miss results in such packages. --* `"Normal"` +-obsolete, no effect - --Default: `"Normal"`. +-Default: `""`. - -#### **expandWorkspaceToModule** *bool* - -**This setting is experimental and may be deleted.** - --expandWorkspaceToModule instructs `gopls` to adjust the scope of the --workspace to find the best available module root. `gopls` first looks for --a go.mod file in any parent directory of the workspace folder, expanding --the scope to that directory if it exists. If no viable parent directory is --found, gopls will check if there is exactly one child directory containing --a go.mod file, narrowing the scope to that directory if it exists. +-expandWorkspaceToModule determines which packages are considered +-"workspace packages" when the workspace is using modules. +- +-Workspace packages affect the scope of workspace-wide operations. Notably, +-gopls diagnoses all packages considered to be part of the workspace after +-every keystroke, so by setting "ExpandWorkspaceToModule" to false, and +-opening a nested workspace directory, you can reduce the amount of work +-gopls has to do to keep your workspace up to date. - -Default: `true`. - @@ -4320,7 +4986,7 @@ diff -urN a/gopls/doc/settings.md b/gopls/doc/settings.md -... -"analyses": { - "unreachable": false, // Disable the unreachable analyzer. -- "unusedparams": true // Enable the unusedparams analyzer. +- "unusedvariable": true // Enable the unusedvariable analyzer. -} -... -``` @@ -4579,7 +5245,7 @@ diff -urN a/gopls/doc/settings.md b/gopls/doc/settings.md -Identifier: `regenerate_cgo` - -Regenerates cgo definitions. --### **Run vulncheck.** +-### **Run vulncheck** - -Identifier: `run_govulncheck` - @@ -4983,138 +5649,177 @@ diff -urN a/gopls/doc/vim.md b/gopls/doc/vim.md diff -urN a/gopls/doc/workspace.md b/gopls/doc/workspace.md --- a/gopls/doc/workspace.md 2000-01-01 00:00:00.000000000 -0000 +++ b/gopls/doc/workspace.md 1970-01-01 00:00:00.000000000 +0000 -@@ -1,101 +0,0 @@ +@@ -1,139 +0,0 @@ -# Setting up your workspace - --`gopls` supports both Go module and GOPATH modes. However, it needs a defined --scope in which language features like references, rename, and implementation --should operate. -- --The following options are available for configuring this scope: -- --## Module mode -- --### One module -- --If you are working with a single module, you can open the module root (the --directory containing the `go.mod` file), a subdirectory within the module, --or a parent directory containing the module. -- --**Note**: If you open a parent directory containing a module, it must **only** --contain that single module. Otherwise, you are working with multiple modules. -- --### Multiple modules -- --Gopls has several alternatives for working on multiple modules simultaneously, --described below. Starting with Go 1.18, Go workspaces are the preferred solution. -- --#### Go workspaces (Go 1.18+) -- --Starting with Go 1.18, the `go` command has native support for multi-module --workspaces, via [`go.work`](https://go.dev/ref/mod#workspaces) files. These --files are recognized by gopls starting with `gopls@v0.8.0`. -- --The easiest way to work on multiple modules in Go 1.18 and later is therefore --to create a `go.work` file containing the modules you wish to work on, and set --your workspace root to the directory containing the `go.work` file. -- --For example, suppose this repo is checked out into the `$WORK/tools` directory. --We can work on both `golang.org/x/tools` and `golang.org/x/tools/gopls` --simultaneously by creating a `go.work` file using `go work init`, followed by --`go work use MODULE_DIRECTORIES...` to add directories containing `go.mod` files to the --workspace: +-In the language server protocol, a "workspace" consists of a folder along with +-per-folder configuration. Some LSP clients such as VS Code allow configuring +-workspaces explicitly, while others do so automatically by looking for special +-files defining a workspace root (such as a `.git` directory or `go.mod` file). +- +-In order to function, gopls needs a defined scope in which language features +-like references, rename, and implementation should operate. Put differently, +-gopls needs to infer from the LSP workspace which `go build` invocations you +-would use to build your workspace, including the working directory, +-environment, and build flags. +- +-In the past, it could be tricky to set up your workspace so that gopls would +-infer the correct build information. It required opening the correct directory +-or using a `go.work` file to tell gopls about the modules you're working on, +-and configuring the correct operating system and architecture in advance. +-When this didn't work as expected, gopls would often fail in mysterious +-ways--the dreaded "No packages found" error. +- +-Starting with gopls v0.15.0, workspace configuration is much simpler, and gopls +-will typically work when you open a Go file anywhere in your workspace. If it +-isn't working for you, or if you want to better understand how gopls models +-your workspace, please read on. +- +-## Workspace builds +- +-Starting with gopls v0.15.0, gopls will guess the builds you are working on +-based on the set of open files. When you open a file in a workspace folder, +-gopls checks whether the file is contained in a module, `go.work` workspace, or +-GOPATH directory, and configures the build accordingly. Additionally, if you +-open a file that is constrained to a different operating system or +-architecture, for example opening `foo_windows.go` when working on Linux, gopls +-will create a scope with `GOOS` and `GOARCH` set to a value that matches the +-file. +- +-For example, suppose we had a repository with three modules: `moda`, `modb`, +-and `modc`, and a `go.work` file using modules `moda` and `modb`. If we open +-the files `moda/a.go`, `modb/b.go`, `moda/a_windows.go`, and `modc/c.go`, gopls +-will automatically create three builds: +- +-![Zero Config gopls](zeroconfig.png) +- +-This allows gopls to _just work_ when you open a Go file, but it does come with +-several caveats: +- +-- It causes gopls to do more work, since it is now tracking three builds +- instead of one. However, the recent +- [scalability redesign](https://go.dev/blog/gopls-scalability) +- allows much of this work to be avoided through efficient caching. +-- For operations invoked from a given file, such as "References" +- or "Implementations", gopls executes the operation in +- _the default build for that file_. For example, finding references to +- a symbol `S` from `foo_linux.go` will return references from the Linux build, +- and finding references to the same symbol `S` from `foo_windows.go` will +- return references from the Windows build. Gopls searches the default build +- for the file, but it doesn't search all the other possible builds (even +- though that would be nice) because it is liable to be too expensive. +- Issues [#65757](https://go.dev/issue/65757) and +- [#65755](https://go.dev/issue/65755) propose improvements to this behavior. +-- When selecting a `GOOS/GOARCH` combination to match a build-constrained file, +- gopls will choose the first matching combination from +- [this list](https://cs.opensource.google/go/x/tools/+/master:gopls/internal/cache/port.go;l=30;drc=f872b3d6f05822d290bc7bdd29db090fd9d89f5c). +- In some cases, that may be surprising. +-- When working in a `GOOS/GOARCH` constrained file that does not match your +- default toolchain, `CGO_ENABLED=0` is implicitly set, since a C toolchain for +- that target is unlikely to be available. This means that gopls will not +- work in files including `import "C"`. Issue +- [#65758](https://go.dev/issue/65758) may lead to improvements in this +- behavior. +-- Gopls is currently unable to guess build flags that include arbitrary +- user-defined build constraints, such as a file with the build directive +- `//go:build mytag`. Issue [#65089](https://go.dev/issue/65089) proposes +- a heuristic by which gopls could handle this automatically. +- +-Please provide feedback on this behavior by upvoting or commenting the issues +-mentioned above, or opening a [new issue](https://go.dev/issue/new) for other +-improvements you'd like to see. +- +-## When to use a `go.work` file for development +- +-Starting with Go 1.18, the `go` command has built-in support for multi-module +-workspaces specified by [`go.work`](https://go.dev/ref/mod#workspaces) files. +-Gopls will recognize these files if they are present in your workspace. +- +-Use a `go.work` file when: +- +-- you want to work on multiple modules simultaneously in a single logical +- build, for example if you want changes to one module to be reflected in +- another. +-- you want to improve gopls' memory usage or performance by reducing the number +- of builds it must track. +-- you want gopls to know which modules you are working on in a multi-module +- workspace, without opening any files. For example, it may be convenient to use +- `workspace/symbol` queries before any files are open. +-- you are using gopls v0.14.2 or earlier, and want to work on multiple +- modules. +- +-For example, suppose this repo is checked out into the `$WORK/tools` directory, +-and [`x/mod`](https://pkg.go.dev/golang.org/x/mod) is checked out into +-`$WORK/mod`, and you are working on a new `x/mod` API for editing `go.mod` +-files that you want to simultaneously integrate into gopls. +- +-You can work on both `golang.org/x/tools/gopls` and `golang.org/x/mod` +-simultaneously by creating a `go.work` file: - -```sh -cd $WORK -go work init --go work use ./tools/ ./tools/gopls/ +-go work use tools/gopls mod -``` - --...followed by opening the `$WORK` directory in our editor. -- --#### DEPRECATED: Experimental workspace module (Go 1.17 and earlier) -- --**This feature is deprecated and will be removed in future versions of gopls. --Please see [issue #52897](https://go.dev/issue/52897) for additional --information.** -- --With earlier versions of Go, `gopls` can simulate multi-module workspaces by --creating a synthetic module requiring the modules in the workspace root. --See [the design document](https://github.com/golang/proposal/blob/master/design/37720-gopls-workspaces.md) --for more information. -- --This feature is experimental, and will eventually be removed once `go.work` --files are accepted by all supported Go versions. -- --You can enable this feature by configuring the --[experimentalWorkspaceModule](settings.md#experimentalworkspacemodule-bool) --setting. -- --#### Multiple workspace folders -- --If neither of the above solutions work, and your editor allows configuring the --set of --["workspace folders"](https://microsoft.github.io/language-server-protocol/specifications/specification-3-17/#workspaceFolder) --used during your LSP session, you can still work on multiple modules by adding --a workspace folder at each module root (the locations of `go.mod` files). This --means that each module has its own scope, and features will not work across --modules. +-then opening the `$WORK` directory in your editor. - --In VS Code, you can create a workspace folder by setting up a --[multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces). --View the [documentation for your editor plugin](../README.md#editor) to learn how to --configure a workspace folder in your editor. +-## When to manually configure `GOOS`, `GOARCH`, or `-tags` - --### GOPATH mode +-As described in the first section, gopls v0.15.0 and later will try to +-configure a new build scope automatically when you open a file that doesn't +-match the system default operating system (`GOOS`) or architecture (`GOARCH`). - --When opening a directory within your GOPATH, the workspace scope will be just --that directory. +-However, per the caveats listed in that section, this automatic behavior comes +-with limitations. Customize your gopls environment by setting `GOOS` or +-`GOARCH` in your +-[`"build.env"`](https://github.com/golang/tools/blob/master/gopls/doc/settings.md#env-mapstringstring) +-or `-tags=...` in your" +-["build.buildFlags"](https://github.com/golang/tools/blob/master/gopls/doc/settings.md#buildflags-string) +-when: - --### At your own risk +-- You want to modify the default build environment. +-- Gopls is not guessing the `GOOS/GOARCH` combination you want to use for +- cross platform development. +-- You need to work on a file that is constrained by a user-defined build tags, +- such as the build directive `//go:build mytag`. - --Some users or companies may have projects that encompass one `$GOPATH`. If you --open your entire `$GOPATH` or `$GOPATH/src` folder, the workspace scope will be --your entire `GOPATH`. If your GOPATH is large, `gopls` to be very slow to start --because it will try to find all of the Go files in the directory you have --opened. It will then load all of the files it has found. +-## GOPATH mode - --To work around this case, you can create a new `$GOPATH` that contains only the --packages you want to work on. -- ----- -- --If you have additional use cases that are not mentioned above, please --[file a new issue](https://github.com/golang/go/issues/new). +-When opening a directory within a `GOPATH` directory, the workspace scope will +-be just that directory and all directories contained within it. Note that +-opening a large GOPATH directory can make gopls very slow to start. +Binary files a/gopls/doc/zeroconfig.png and b/gopls/doc/zeroconfig.png differ diff -urN a/gopls/go.mod b/gopls/go.mod --- a/gopls/go.mod 2000-01-01 00:00:00.000000000 -0000 +++ b/gopls/go.mod 1970-01-01 00:00:00.000000000 +0000 @@ -1,30 +0,0 @@ -module golang.org/x/tools/gopls - --go 1.18 +-go 1.19 - -require ( -- github.com/google/go-cmp v0.5.9 +- github.com/google/go-cmp v0.6.0 - github.com/jba/printsrc v0.2.2 -- github.com/jba/templatecheck v0.6.0 -- github.com/sergi/go-diff v1.1.0 -- golang.org/x/mod v0.14.0 -- golang.org/x/sync v0.5.0 -- golang.org/x/sys v0.14.0 -- golang.org/x/telemetry v0.0.0-20231011160506-788d5629a052 +- github.com/jba/templatecheck v0.7.0 +- golang.org/x/mod v0.17.0 +- golang.org/x/sync v0.7.0 +- golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 - golang.org/x/text v0.14.0 -- golang.org/x/tools v0.13.1-0.20230920233436-f9b8da7b22be -- golang.org/x/vuln v1.0.1 +- golang.org/x/tools v0.18.0 +- golang.org/x/vuln v1.0.4 - gopkg.in/yaml.v3 v3.0.1 -- honnef.co/go/tools v0.4.5 -- mvdan.cc/gofumpt v0.4.0 -- mvdan.cc/xurls/v2 v2.4.0 +- honnef.co/go/tools v0.4.7 +- mvdan.cc/gofumpt v0.6.0 +- mvdan.cc/xurls/v2 v2.5.0 -) - -require ( - github.com/BurntSushi/toml v1.2.1 // indirect - github.com/google/safehtml v0.1.0 // indirect - golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338 // indirect +- golang.org/x/sys v0.19.0 // indirect +- gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect - -) - @@ -5122,79 +5827,63 @@ diff -urN a/gopls/go.mod b/gopls/go.mod diff -urN a/gopls/go.sum b/gopls/go.sum --- a/gopls/go.sum 2000-01-01 00:00:00.000000000 -0000 +++ b/gopls/go.sum 1970-01-01 00:00:00.000000000 +0000 -@@ -1,72 +0,0 @@ +@@ -1,56 +0,0 @@ -github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= -github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= --github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= --github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= --github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= --github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= --github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= --github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= --github.com/google/safehtml v0.0.2/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU= +-github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +-github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/safehtml v0.1.0 h1:EwLKo8qawTKfsi0orxcQAZzu07cICaBeFMegAU9eaT8= -github.com/google/safehtml v0.1.0/go.mod h1:L4KWwDsUJdECRAEpZoBn3O64bQaywRscowZjJAzjHnU= -github.com/jba/printsrc v0.2.2 h1:9OHK51UT+/iMAEBlQIIXW04qvKyF3/vvLuwW/hL8tDU= -github.com/jba/printsrc v0.2.2/go.mod h1:1xULjw59sL0dPdWpDoVU06TIEO/Wnfv6AHRpiElTwYM= --github.com/jba/templatecheck v0.6.0 h1:SwM8C4hlK/YNLsdcXStfnHWE2HKkuTVwy5FKQHt5ro8= --github.com/jba/templatecheck v0.6.0/go.mod h1:/1k7EajoSErFI9GLHAsiIJEaNLt3ALKNw2TV7z2SYv4= --github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= --github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= --github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= --github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +-github.com/jba/templatecheck v0.7.0 h1:wjTb/VhGgSFeim5zjWVePBdaMo28X74bGLSABZV+zIA= +-github.com/jba/templatecheck v0.7.0/go.mod h1:n1Etw+Rrw1mDDD8dDRsEKTwMZsJ98EkktgNJC6wLUGo= +-github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= --github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= --github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= --github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= --github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= --github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= --github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= --github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= --github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= --github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= --github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +-github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= --golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g= +-golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +-golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338 h1:2O2DON6y3XMJiQRAS1UWU+54aec2uopH3x7MAiqGW6Y= -golang.org/x/exp/typeparams v0.0.0-20221212164502-fae10dda9338/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= --golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= --golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +-golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +-golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +-golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= --golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= --golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= --golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= --golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +-golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +-golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +-golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +-golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +-golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= --golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= --golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= --golang.org/x/telemetry v0.0.0-20231011160506-788d5629a052 h1:1baVNneD/IRxmu8JQdBuki78zUqBtZxq8smZXQj0X2Y= --golang.org/x/telemetry v0.0.0-20231011160506-788d5629a052/go.mod h1:6p4ScoNeC2dhpQ1nSSMmkZ7mEj5JQUSCyc0uExBp5T4= +-golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +-golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +-golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +-golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY= +-golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= --golang.org/x/term v0.14.0/go.mod h1:TySc+nGkYR6qt8km8wUhuFRTVSMIX3XPR58y2lC8vww= +-golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +-golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= --golang.org/x/vuln v1.0.1 h1:KUas02EjQK5LTuIx1OylBQdKKZ9jeugs+HiqO5HormU= --golang.org/x/vuln v1.0.1/go.mod h1:bb2hMwln/tqxg32BNY4CcxHWtHXuYa3SbIBmtsyjxtM= +-golang.org/x/vuln v1.0.4 h1:SP0mPeg2PmGCu03V+61EcQiOjmpri2XijexKdzv8Z1I= +-golang.org/x/vuln v1.0.4/go.mod h1:NbJdUQhX8jY++FtuhrXs2Eyx0yePo9pF7nPlIjo9aaQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= --gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= --gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= --gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= --gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= --gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= --honnef.co/go/tools v0.4.5 h1:YGD4H+SuIOOqsyoLOpZDWcieM28W47/zRO7f+9V3nvo= --honnef.co/go/tools v0.4.5/go.mod h1:GUV+uIBCLpdf0/v6UhHHG/yzI/z6qPskBeQCjcNB96k= --mvdan.cc/gofumpt v0.4.0 h1:JVf4NN1mIpHogBj7ABpgOyZc65/UUOkKQFkoURsz4MM= --mvdan.cc/gofumpt v0.4.0/go.mod h1:PljLOHDeZqgS8opHRKLzp2It2VBuSdteAgqUfzMTxlQ= --mvdan.cc/xurls/v2 v2.4.0 h1:tzxjVAj+wSBmDcF6zBB7/myTy3gX9xvi8Tyr28AuQgc= --mvdan.cc/xurls/v2 v2.4.0/go.mod h1:+GEjq9uNjqs8LQfM9nVnM8rff0OQ5Iash5rzX+N1CSg= +-honnef.co/go/tools v0.4.7 h1:9MDAWxMoSnB6QoSqiVr7P5mtkT9pOc1kSxchzPCnqJs= +-honnef.co/go/tools v0.4.7/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0= +-mvdan.cc/gofumpt v0.6.0 h1:G3QvahNDmpD+Aek/bNOLrFR2XC6ZAdo62dZu65gmwGo= +-mvdan.cc/gofumpt v0.6.0/go.mod h1:4L0wf+kgIPZtcCWXynNS2e6bhmj73umwnuXSZarixzA= +-mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8= +-mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE= diff -urN a/gopls/integration/govim/artifacts.go b/gopls/integration/govim/artifacts.go --- a/gopls/integration/govim/artifacts.go 2000-01-01 00:00:00.000000000 -0000 +++ b/gopls/integration/govim/artifacts.go 1970-01-01 00:00:00.000000000 +0000 @@ -5549,3950 +6238,4111 @@ diff -urN a/gopls/integration/govim/run_tests_for_cloudbuild.sh b/gopls/integrat - # Remove directories we don't care about. - find "$GOVIM_TESTSCRIPT_WORKDIR_ROOT" -type d \( -name .vim -o -name gopath \) -prune -exec rm -rf '{}' \; -fi -diff -urN a/gopls/internal/astutil/purge.go b/gopls/internal/astutil/purge.go ---- a/gopls/internal/astutil/purge.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/astutil/purge.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,74 +0,0 @@ +diff -urN a/gopls/internal/analysis/deprecated/deprecated.go b/gopls/internal/analysis/deprecated/deprecated.go +--- a/gopls/internal/analysis/deprecated/deprecated.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/deprecated/deprecated.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,267 +0,0 @@ -// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --// Package astutil provides various AST utility functions for gopls. --package astutil +-package deprecated - -import ( - "bytes" -- "go/scanner" +- "go/ast" +- "go/format" - "go/token" +- "go/types" +- "strconv" +- "strings" +- +- _ "embed" - -- "golang.org/x/tools/gopls/internal/lsp/safetoken" +- "golang.org/x/tools/go/analysis" +- "golang.org/x/tools/go/analysis/passes/inspect" +- "golang.org/x/tools/go/ast/inspector" +- "golang.org/x/tools/internal/analysisinternal" -) - --// PurgeFuncBodies returns a copy of src in which the contents of each --// outermost {...} region except struct and interface types have been --// deleted. This reduces the amount of work required to parse the --// top-level declarations. --// --// PurgeFuncBodies does not preserve newlines or position information. --// Also, if the input is invalid, parsing the output of --// PurgeFuncBodies may result in a different tree due to its effects --// on parser error recovery. --func PurgeFuncBodies(src []byte) []byte { -- // Destroy the content of any {...}-bracketed regions that are -- // not immediately preceded by a "struct" or "interface" -- // token. That includes function bodies, composite literals, -- // switch/select bodies, and all blocks of statements. -- // This will lead to non-void functions that don't have return -- // statements, which of course is a type error, but that's ok. +-//go:embed doc.go +-var doc string - -- var out bytes.Buffer -- file := token.NewFileSet().AddFile("", -1, len(src)) -- var sc scanner.Scanner -- sc.Init(file, src, nil, 0) -- var prev token.Token -- var cursor int // last consumed src offset -- var braces []token.Pos // stack of unclosed braces or -1 for struct/interface type -- for { -- pos, tok, _ := sc.Scan() -- if tok == token.EOF { -- break -- } -- switch tok { -- case token.COMMENT: -- // TODO(adonovan): opt: skip, to save an estimated 20% of time. +-var Analyzer = &analysis.Analyzer{ +- Name: "deprecated", +- Doc: analysisinternal.MustExtractDoc(doc, "deprecated"), +- Requires: []*analysis.Analyzer{inspect.Analyzer}, +- Run: checkDeprecated, +- FactTypes: []analysis.Fact{(*deprecationFact)(nil)}, +- RunDespiteErrors: true, +- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/deprecated", +-} - -- case token.LBRACE: -- if prev == token.STRUCT || prev == token.INTERFACE { -- pos = -1 -- } -- braces = append(braces, pos) +-// checkDeprecated is a simplified copy of staticcheck.CheckDeprecated. +-func checkDeprecated(pass *analysis.Pass) (interface{}, error) { +- inspector := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - -- case token.RBRACE: -- if last := len(braces) - 1; last >= 0 { -- top := braces[last] -- braces = braces[:last] -- if top < 0 { -- // struct/interface type: leave alone -- } else if len(braces) == 0 { // toplevel only -- // Delete {...} body. -- start, _ := safetoken.Offset(file, top) -- end, _ := safetoken.Offset(file, pos) -- out.Write(src[cursor : start+len("{")]) -- cursor = end -- } -- } +- deprs, err := collectDeprecatedNames(pass, inspector) +- if err != nil || (len(deprs.packages) == 0 && len(deprs.objects) == 0) { +- return nil, err +- } +- +- reportDeprecation := func(depr *deprecationFact, node ast.Node) { +- // TODO(hyangah): staticcheck.CheckDeprecated has more complex logic. Do we need it here? +- // TODO(hyangah): Scrub depr.Msg. depr.Msg may contain Go comments +- // markdown syntaxes but LSP diagnostics do not support markdown syntax. +- +- buf := new(bytes.Buffer) +- if err := format.Node(buf, pass.Fset, node); err != nil { +- // This shouldn't happen but let's be conservative. +- buf.Reset() +- buf.WriteString("declaration") - } -- prev = tok +- pass.ReportRangef(node, "%s is deprecated: %s", buf, depr.Msg) - } -- out.Write(src[cursor:]) -- return out.Bytes() --} -diff -urN a/gopls/internal/astutil/purge_test.go b/gopls/internal/astutil/purge_test.go ---- a/gopls/internal/astutil/purge_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/astutil/purge_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,89 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package astutil_test +- nodeFilter := []ast.Node{(*ast.SelectorExpr)(nil)} +- inspector.Preorder(nodeFilter, func(node ast.Node) { +- // Caveat: this misses dot-imported objects +- sel, ok := node.(*ast.SelectorExpr) +- if !ok { +- return +- } - --import ( -- "go/ast" -- "go/parser" -- "go/token" -- "os" -- "reflect" -- "testing" +- obj := pass.TypesInfo.ObjectOf(sel.Sel) +- if fn, ok := obj.(*types.Func); ok { +- obj = fn.Origin() +- } +- if obj == nil || obj.Pkg() == nil { +- // skip invalid sel.Sel. +- return +- } - -- "golang.org/x/tools/go/packages" -- "golang.org/x/tools/gopls/internal/astutil" -- "golang.org/x/tools/internal/testenv" --) +- if obj.Pkg() == pass.Pkg { +- // A package is allowed to use its own deprecated objects +- return +- } - --// TestPurgeFuncBodies tests PurgeFuncBodies by comparing it against a --// (less efficient) reference implementation that purges after parsing. --func TestPurgeFuncBodies(t *testing.T) { -- testenv.NeedsGoBuild(t) // we need the source code for std +- // A package "foo" has two related packages "foo_test" and "foo.test", for external tests and the package main +- // generated by 'go test' respectively. "foo_test" can import and use "foo", "foo.test" imports and uses "foo" +- // and "foo_test". - -- // Load a few standard packages. -- config := packages.Config{Mode: packages.NeedCompiledGoFiles} -- pkgs, err := packages.Load(&config, "encoding/...") -- if err != nil { -- t.Fatal(err) -- } +- if strings.TrimSuffix(pass.Pkg.Path(), "_test") == obj.Pkg().Path() { +- // foo_test (the external tests of foo) can use objects from foo. +- return +- } +- if strings.TrimSuffix(pass.Pkg.Path(), ".test") == obj.Pkg().Path() { +- // foo.test (the main package of foo's tests) can use objects from foo. +- return +- } +- if strings.TrimSuffix(pass.Pkg.Path(), ".test") == strings.TrimSuffix(obj.Pkg().Path(), "_test") { +- // foo.test (the main package of foo's tests) can use objects from foo's external tests. +- return +- } - -- // preorder returns the nodes of tree f in preorder. -- preorder := func(f *ast.File) (nodes []ast.Node) { -- ast.Inspect(f, func(n ast.Node) bool { -- if n != nil { -- nodes = append(nodes, n) +- if depr, ok := deprs.objects[obj]; ok { +- reportDeprecation(depr, sel) +- } +- }) +- +- for _, f := range pass.Files { +- for _, spec := range f.Imports { +- var imp *types.Package +- var obj types.Object +- if spec.Name != nil { +- obj = pass.TypesInfo.ObjectOf(spec.Name) +- } else { +- obj = pass.TypesInfo.Implicits[spec] - } -- return true -- }) -- return nodes -- } +- pkgName, ok := obj.(*types.PkgName) +- if !ok { +- continue +- } +- imp = pkgName.Imported() - -- packages.Visit(pkgs, nil, func(p *packages.Package) { -- for _, filename := range p.CompiledGoFiles { -- content, err := os.ReadFile(filename) +- path, err := strconv.Unquote(spec.Path.Value) - if err != nil { -- t.Fatal(err) +- continue +- } +- pkgPath := pass.Pkg.Path() +- if strings.TrimSuffix(pkgPath, "_test") == path { +- // foo_test can import foo +- continue +- } +- if strings.TrimSuffix(pkgPath, ".test") == path { +- // foo.test can import foo +- continue +- } +- if strings.TrimSuffix(pkgPath, ".test") == strings.TrimSuffix(path, "_test") { +- // foo.test can import foo_test +- continue +- } +- if depr, ok := deprs.packages[imp]; ok { +- reportDeprecation(depr, spec.Path) - } +- } +- } +- return nil, nil +-} - -- fset := token.NewFileSet() +-type deprecationFact struct{ Msg string } - -- // Parse then purge (reference implementation). -- f1, _ := parser.ParseFile(fset, filename, content, 0) -- ast.Inspect(f1, func(n ast.Node) bool { -- switch n := n.(type) { -- case *ast.FuncDecl: -- if n.Body != nil { -- n.Body.List = nil -- } -- case *ast.FuncLit: -- n.Body.List = nil -- case *ast.CompositeLit: -- n.Elts = nil -- } -- return true -- }) +-func (*deprecationFact) AFact() {} +-func (d *deprecationFact) String() string { return "Deprecated: " + d.Msg } - -- // Purge before parse (logic under test). -- f2, _ := parser.ParseFile(fset, filename, astutil.PurgeFuncBodies(content), 0) +-type deprecatedNames struct { +- objects map[types.Object]*deprecationFact +- packages map[*types.Package]*deprecationFact +-} - -- // Compare sequence of node types. -- nodes1 := preorder(f1) -- nodes2 := preorder(f2) -- if len(nodes2) < len(nodes1) { -- t.Errorf("purged file has fewer nodes: %d vs %d", -- len(nodes2), len(nodes1)) -- nodes1 = nodes1[:len(nodes2)] // truncate +-// collectDeprecatedNames collects deprecated identifiers and publishes +-// them both as Facts and the return value. This is a simplified copy +-// of staticcheck's fact_deprecated analyzer. +-func collectDeprecatedNames(pass *analysis.Pass, ins *inspector.Inspector) (deprecatedNames, error) { +- extractDeprecatedMessage := func(docs []*ast.CommentGroup) string { +- for _, doc := range docs { +- if doc == nil { +- continue - } -- for i := range nodes1 { -- x, y := nodes1[i], nodes2[i] -- if reflect.TypeOf(x) != reflect.TypeOf(y) { -- t.Errorf("%s: got %T, want %T", -- fset.Position(x.Pos()), y, x) -- break +- parts := strings.Split(doc.Text(), "\n\n") +- for _, part := range parts { +- if !strings.HasPrefix(part, "Deprecated: ") { +- continue - } +- alt := part[len("Deprecated: "):] +- alt = strings.Replace(alt, "\n", " ", -1) +- return strings.TrimSpace(alt) - } - } -- }) --} -diff -urN a/gopls/internal/astutil/util.go b/gopls/internal/astutil/util.go ---- a/gopls/internal/astutil/util.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/astutil/util.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,61 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +- return "" +- } - --package astutil +- doDocs := func(names []*ast.Ident, docs *ast.CommentGroup) { +- alt := extractDeprecatedMessage([]*ast.CommentGroup{docs}) +- if alt == "" { +- return +- } - --import ( -- "go/ast" +- for _, name := range names { +- obj := pass.TypesInfo.ObjectOf(name) +- pass.ExportObjectFact(obj, &deprecationFact{alt}) +- } +- } - -- "golang.org/x/tools/internal/typeparams" --) -- --// UnpackRecv unpacks a receiver type expression, reporting whether it is a --// pointer recever, along with the type name identifier and any receiver type --// parameter identifiers. --// --// Copied (with modifications) from go/types. --func UnpackRecv(rtyp ast.Expr) (ptr bool, rname *ast.Ident, tparams []*ast.Ident) { --L: // unpack receiver type -- // This accepts invalid receivers such as ***T and does not -- // work for other invalid receivers, but we don't care. The -- // validity of receiver expressions is checked elsewhere. -- for { -- switch t := rtyp.(type) { -- case *ast.ParenExpr: -- rtyp = t.X -- case *ast.StarExpr: -- ptr = true -- rtyp = t.X -- default: -- break L +- var docs []*ast.CommentGroup +- for _, f := range pass.Files { +- docs = append(docs, f.Doc) +- } +- if alt := extractDeprecatedMessage(docs); alt != "" { +- // Don't mark package syscall as deprecated, even though +- // it is. A lot of people still use it for simple +- // constants like SIGKILL, and I am not comfortable +- // telling them to use x/sys for that. +- if pass.Pkg.Path() != "syscall" { +- pass.ExportPackageFact(&deprecationFact{alt}) - } - } -- -- // unpack type parameters, if any -- switch rtyp.(type) { -- case *ast.IndexExpr, *typeparams.IndexListExpr: -- var indices []ast.Expr -- rtyp, _, indices, _ = typeparams.UnpackIndexExpr(rtyp) -- for _, arg := range indices { -- var par *ast.Ident -- switch arg := arg.(type) { -- case *ast.Ident: -- par = arg +- nodeFilter := []ast.Node{ +- (*ast.GenDecl)(nil), +- (*ast.FuncDecl)(nil), +- (*ast.TypeSpec)(nil), +- (*ast.ValueSpec)(nil), +- (*ast.File)(nil), +- (*ast.StructType)(nil), +- (*ast.InterfaceType)(nil), +- } +- ins.Preorder(nodeFilter, func(node ast.Node) { +- var names []*ast.Ident +- var docs *ast.CommentGroup +- switch node := node.(type) { +- case *ast.GenDecl: +- switch node.Tok { +- case token.TYPE, token.CONST, token.VAR: +- docs = node.Doc +- for i := range node.Specs { +- switch n := node.Specs[i].(type) { +- case *ast.ValueSpec: +- names = append(names, n.Names...) +- case *ast.TypeSpec: +- names = append(names, n.Name) +- } +- } - default: -- // ignore errors +- return - } -- if par == nil { -- par = &ast.Ident{NamePos: arg.Pos(), Name: "_"} +- case *ast.FuncDecl: +- docs = node.Doc +- names = []*ast.Ident{node.Name} +- case *ast.TypeSpec: +- docs = node.Doc +- names = []*ast.Ident{node.Name} +- case *ast.ValueSpec: +- docs = node.Doc +- names = node.Names +- case *ast.StructType: +- for _, field := range node.Fields.List { +- doDocs(field.Names, field.Doc) +- } +- case *ast.InterfaceType: +- for _, field := range node.Methods.List { +- doDocs(field.Names, field.Doc) - } -- tparams = append(tparams, par) - } -- } +- if docs != nil && len(names) > 0 { +- doDocs(names, docs) +- } +- }) - -- // unpack receiver name -- if name, _ := rtyp.(*ast.Ident); name != nil { -- rname = name +- // Every identifier is potentially deprecated, so we will need +- // to look up facts a lot. Construct maps of all facts propagated +- // to this pass for fast lookup. +- out := deprecatedNames{ +- objects: map[types.Object]*deprecationFact{}, +- packages: map[*types.Package]*deprecationFact{}, +- } +- for _, fact := range pass.AllObjectFacts() { +- out.objects[fact.Object] = fact.Fact.(*deprecationFact) +- } +- for _, fact := range pass.AllPackageFacts() { +- out.packages[fact.Package] = fact.Fact.(*deprecationFact) - } - -- return +- return out, nil -} -diff -urN a/gopls/internal/bug/bug.go b/gopls/internal/bug/bug.go ---- a/gopls/internal/bug/bug.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/bug/bug.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,142 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/analysis/deprecated/deprecated_test.go b/gopls/internal/analysis/deprecated/deprecated_test.go +--- a/gopls/internal/analysis/deprecated/deprecated_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/deprecated/deprecated_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,16 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --// Package bug provides utilities for reporting internal bugs, and being --// notified when they occur. --// --// Philosophically, because gopls runs as a sidecar process that the user does --// not directly control, sometimes it keeps going on broken invariants rather --// than panicking. In those cases, bug reports provide a mechanism to alert --// developers and capture relevant metadata. --package bug +-package deprecated - -import ( -- "fmt" -- "runtime" -- "runtime/debug" -- "sort" -- "sync" -- "time" +- "testing" - -- "golang.org/x/telemetry/counter" +- "golang.org/x/tools/go/analysis/analysistest" -) - --// PanicOnBugs controls whether to panic when bugs are reported. +-func Test(t *testing.T) { +- testdata := analysistest.TestData() +- analysistest.Run(t, testdata, Analyzer, "a") +-} +diff -urN a/gopls/internal/analysis/deprecated/doc.go b/gopls/internal/analysis/deprecated/doc.go +--- a/gopls/internal/analysis/deprecated/doc.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/deprecated/doc.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,16 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-// Package deprecated defines an Analyzer that marks deprecated symbols and package imports. -// --// It may be set to true during testing. --var PanicOnBugs = false +-// # Analyzer deprecated +-// +-// deprecated: check for use of deprecated identifiers +-// +-// The deprecated analyzer looks for deprecated symbols and package +-// imports. +-// +-// See https://go.dev/wiki/Deprecated to learn about Go's convention +-// for documenting and signaling deprecated identifiers. +-package deprecated +diff -urN a/gopls/internal/analysis/deprecated/testdata/src/a/a.go b/gopls/internal/analysis/deprecated/testdata/src/a/a.go +--- a/gopls/internal/analysis/deprecated/testdata/src/a/a.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/deprecated/testdata/src/a/a.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,17 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --var ( -- mu sync.Mutex -- exemplars map[string]Bug -- handlers []func(Bug) --) +-package usedeprecated - --// A Bug represents an unexpected event or broken invariant. They are used for --// capturing metadata that helps us understand the event. --// --// Bugs are JSON-serializable. --type Bug struct { -- File string // file containing the call to bug.Report -- Line int // line containing the call to bug.Report -- Description string // description of the bug -- Key string // key identifying the bug (file:line if available) -- Stack string // call stack -- AtTime time.Time // time the bug was reported --} +-import "io/ioutil" // want "\"io/ioutil\" is deprecated: .*" - --// Reportf reports a formatted bug message. --func Reportf(format string, args ...interface{}) { -- report(fmt.Sprintf(format, args...)) +-func x() { +- _, _ = ioutil.ReadFile("") // want "ioutil.ReadFile is deprecated: As of Go 1.16, .*" +- Legacy() // expect no deprecation notice. -} - --// Errorf calls fmt.Errorf for the given arguments, and reports the resulting --// error message as a bug. --func Errorf(format string, args ...interface{}) error { -- err := fmt.Errorf(format, args...) -- report(err.Error()) -- return err +-// Legacy is deprecated. +-// +-// Deprecated: use X instead. +-func Legacy() {} // want Legacy:"Deprecated: use X instead." +diff -urN a/gopls/internal/analysis/deprecated/testdata/src/a/a_test.go b/gopls/internal/analysis/deprecated/testdata/src/a/a_test.go +--- a/gopls/internal/analysis/deprecated/testdata/src/a/a_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/deprecated/testdata/src/a/a_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,12 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package usedeprecated +- +-import "testing" +- +-func TestF(t *testing.T) { +- Legacy() // expect no deprecation notice. +- x() -} +diff -urN a/gopls/internal/analysis/embeddirective/doc.go b/gopls/internal/analysis/embeddirective/doc.go +--- a/gopls/internal/analysis/embeddirective/doc.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/embeddirective/doc.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,18 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// Report records a new bug encountered on the server. --// It uses reflection to report the position of the immediate caller. --func Report(description string) { -- report(description) +-// Package embeddirective defines an Analyzer that validates //go:embed directives. +-// The analyzer defers fixes to its parent golang.Analyzer. +-// +-// # Analyzer embed +-// +-// embed: check //go:embed directive usage +-// +-// This analyzer checks that the embed package is imported if //go:embed +-// directives are present, providing a suggested fix to add the import if +-// it is missing. +-// +-// This analyzer also checks that //go:embed directives precede the +-// declaration of a single variable. +-package embeddirective +diff -urN a/gopls/internal/analysis/embeddirective/embeddirective.go b/gopls/internal/analysis/embeddirective/embeddirective.go +--- a/gopls/internal/analysis/embeddirective/embeddirective.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/embeddirective/embeddirective.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,166 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package embeddirective +- +-import ( +- _ "embed" +- "go/ast" +- "go/token" +- "go/types" +- "strings" +- +- "golang.org/x/tools/go/analysis" +- "golang.org/x/tools/internal/aliases" +- "golang.org/x/tools/internal/analysisinternal" +-) +- +-//go:embed doc.go +-var doc string +- +-var Analyzer = &analysis.Analyzer{ +- Name: "embed", +- Doc: analysisinternal.MustExtractDoc(doc, "embed"), +- Run: run, +- RunDespiteErrors: true, +- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/embeddirective", -} - --// BugReportCount is a telemetry counter that tracks # of bug reports. --var BugReportCount = counter.NewStack("gopls/bug", 16) +-const FixCategory = "addembedimport" // recognized by gopls ApplyFix - --func report(description string) { -- _, file, line, ok := runtime.Caller(2) // all exported reporting functions call report directly +-func run(pass *analysis.Pass) (interface{}, error) { +- for _, f := range pass.Files { +- comments := embedDirectiveComments(f) +- if len(comments) == 0 { +- continue // nothing to check +- } - -- key := "" -- if ok { -- key = fmt.Sprintf("%s:%d", file, line) -- } +- hasEmbedImport := false +- for _, imp := range f.Imports { +- if imp.Path.Value == `"embed"` { +- hasEmbedImport = true +- break +- } +- } - -- if PanicOnBugs { -- panic(fmt.Sprintf("%s: %s", key, description)) -- } +- for _, c := range comments { +- pos, end := c.Pos(), c.Pos()+token.Pos(len("//go:embed")) - -- bug := Bug{ -- File: file, -- Line: line, -- Description: description, -- Key: key, -- Stack: string(debug.Stack()), -- AtTime: time.Now(), +- if !hasEmbedImport { +- pass.Report(analysis.Diagnostic{ +- Pos: pos, +- End: end, +- Message: `must import "embed" when using go:embed directives`, +- Category: FixCategory, +- SuggestedFixes: []analysis.SuggestedFix{{ +- Message: `Add missing "embed" import`, +- // No TextEdits => computed by a gopls command. +- }}, +- }) +- } +- +- var msg string +- spec := nextVarSpec(c, f) +- switch { +- case spec == nil: +- msg = `go:embed directives must precede a "var" declaration` +- case len(spec.Names) != 1: +- msg = "declarations following go:embed directives must define a single variable" +- case len(spec.Values) > 0: +- msg = "declarations following go:embed directives must not specify a value" +- case !embeddableType(pass.TypesInfo.Defs[spec.Names[0]]): +- msg = "declarations following go:embed directives must be of type string, []byte or embed.FS" +- } +- if msg != "" { +- pass.Report(analysis.Diagnostic{ +- Pos: pos, +- End: end, +- Message: msg, +- }) +- } +- } - } +- return nil, nil +-} - -- newBug := false -- mu.Lock() -- if _, ok := exemplars[key]; !ok { -- if exemplars == nil { -- exemplars = make(map[string]Bug) +-// embedDirectiveComments returns all comments in f that contains a //go:embed directive. +-func embedDirectiveComments(f *ast.File) []*ast.Comment { +- comments := []*ast.Comment{} +- for _, cg := range f.Comments { +- for _, c := range cg.List { +- if strings.HasPrefix(c.Text, "//go:embed ") { +- comments = append(comments, c) +- } - } -- exemplars[key] = bug // capture one exemplar per key -- newBug = true - } -- hh := handlers -- handlers = nil -- mu.Unlock() +- return comments +-} - -- if newBug { -- BugReportCount.Inc() +-// nextVarSpec returns the ValueSpec for the variable declaration immediately following +-// the go:embed comment, or nil if the next declaration is not a variable declaration. +-func nextVarSpec(com *ast.Comment, f *ast.File) *ast.ValueSpec { +- // Embed directives must be followed by a declaration of one variable with no value. +- // There may be comments and empty lines between the directive and the declaration. +- var nextDecl ast.Decl +- for _, d := range f.Decls { +- if com.End() < d.End() { +- nextDecl = d +- break +- } - } -- // Call the handlers outside the critical section since a -- // handler may itself fail and call bug.Report. Since handlers -- // are one-shot, the inner call should be trivial. -- for _, handle := range hh { -- handle(bug) +- if nextDecl == nil || nextDecl.Pos() == token.NoPos { +- return nil +- } +- decl, ok := nextDecl.(*ast.GenDecl) +- if !ok { +- return nil +- } +- if decl.Tok != token.VAR { +- return nil - } --} - --// Handle adds a handler function that will be called with the next --// bug to occur on the server. The handler only ever receives one bug. --// It is called synchronously, and should return in a timely manner. --func Handle(h func(Bug)) { -- mu.Lock() -- defer mu.Unlock() -- handlers = append(handlers, h) +- // var declarations can be both freestanding and blocks (with parenthesis). +- // Only the first variable spec following the directive is interesting. +- var nextSpec ast.Spec +- for _, s := range decl.Specs { +- if com.End() < s.End() { +- nextSpec = s +- break +- } +- } +- if nextSpec == nil { +- return nil +- } +- spec, ok := nextSpec.(*ast.ValueSpec) +- if !ok { +- // Invalid AST, but keep going. +- return nil +- } +- return spec -} - --// List returns a slice of bug exemplars -- the first bugs to occur at each --// callsite. --func List() []Bug { -- mu.Lock() -- defer mu.Unlock() -- -- var bugs []Bug +-// embeddableType in go:embed directives are string, []byte or embed.FS. +-func embeddableType(o types.Object) bool { +- if o == nil { +- return false +- } - -- for _, bug := range exemplars { -- bugs = append(bugs, bug) +- // For embed.FS the underlying type is an implementation detail. +- // As long as the named type resolves to embed.FS, it is OK. +- if named, ok := aliases.Unalias(o.Type()).(*types.Named); ok { +- obj := named.Obj() +- if obj.Pkg() != nil && obj.Pkg().Path() == "embed" && obj.Name() == "FS" { +- return true +- } - } - -- sort.Slice(bugs, func(i, j int) bool { -- return bugs[i].Key < bugs[j].Key -- }) +- switch v := o.Type().Underlying().(type) { +- case *types.Basic: +- return types.Identical(v, types.Typ[types.String]) +- case *types.Slice: +- return types.Identical(v.Elem(), types.Typ[types.Byte]) +- } - -- return bugs +- return false -} -diff -urN a/gopls/internal/bug/bug_test.go b/gopls/internal/bug/bug_test.go ---- a/gopls/internal/bug/bug_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/bug/bug_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,91 +0,0 @@ +diff -urN a/gopls/internal/analysis/embeddirective/embeddirective_test.go b/gopls/internal/analysis/embeddirective/embeddirective_test.go +--- a/gopls/internal/analysis/embeddirective/embeddirective_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/embeddirective/embeddirective_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,16 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package bug +-package embeddirective - -import ( -- "encoding/json" -- "fmt" - "testing" -- "time" - -- "github.com/google/go-cmp/cmp" +- "golang.org/x/tools/go/analysis/analysistest" -) - --func resetForTesting() { -- exemplars = nil -- handlers = nil +-func Test(t *testing.T) { +- testdata := analysistest.TestData() +- analysistest.RunWithSuggestedFixes(t, testdata, Analyzer, "a") -} +diff -urN a/gopls/internal/analysis/embeddirective/testdata/src/a/embedText b/gopls/internal/analysis/embeddirective/testdata/src/a/embedText +--- a/gopls/internal/analysis/embeddirective/testdata/src/a/embedText 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/embeddirective/testdata/src/a/embedText 1970-01-01 00:00:00.000000000 +0000 +@@ -1 +0,0 @@ +-Hello World +\ No newline at end of file +diff -urN a/gopls/internal/analysis/embeddirective/testdata/src/a/import_missing.go b/gopls/internal/analysis/embeddirective/testdata/src/a/import_missing.go +--- a/gopls/internal/analysis/embeddirective/testdata/src/a/import_missing.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/embeddirective/testdata/src/a/import_missing.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,17 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func TestListBugs(t *testing.T) { -- defer resetForTesting() -- -- Report("bad") +-package a - -- wantBugs(t, "bad") +-import ( +- "fmt" +-) - -- for i := 0; i < 3; i++ { -- Report(fmt.Sprintf("index:%d", i)) -- } +-//go:embed embedtext // want "must import \"embed\" when using go:embed directives" +-var s string - -- wantBugs(t, "bad", "index:0") +-// This is main function +-func main() { +- fmt.Println(s) -} +diff -urN a/gopls/internal/analysis/embeddirective/testdata/src/a/import_present.go b/gopls/internal/analysis/embeddirective/testdata/src/a/import_present.go +--- a/gopls/internal/analysis/embeddirective/testdata/src/a/import_present.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/embeddirective/testdata/src/a/import_present.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,129 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func wantBugs(t *testing.T, want ...string) { -- t.Helper() +-package a - -- bugs := List() -- if got, want := len(bugs), len(want); got != want { -- t.Errorf("List(): got %d bugs, want %d", got, want) -- return -- } +-// Misplaced, above imports. +-//go:embed embedText // want "go:embed directives must precede a \"var\" declaration" - -- for i, b := range bugs { -- if got, want := b.Description, want[i]; got != want { -- t.Errorf("bug.List()[%d] = %q, want %q", i, got, want) -- } -- } --} +-import ( +- "embed" +- embedPkg "embed" +- "fmt" - --func TestBugHandler(t *testing.T) { -- defer resetForTesting() +- _ "embed" +-) - -- Report("unseen") +-//go:embed embedText // ok +-var e1 string - -- // Both handlers are called, in order of registration, only once. -- var got string -- Handle(func(b Bug) { got += "1:" + b.Description }) -- Handle(func(b Bug) { got += "2:" + b.Description }) +-// The analyzer does not check for many directives using the same var. +-// +-//go:embed embedText // ok +-//go:embed embedText // ok +-var e2 string - -- Report("seen") +-// Comments and blank lines between are OK. All types OK. +-// +-//go:embed embedText // ok +-// +-// foo - -- Report("again") +-var e3 string - -- if want := "1:seen2:seen"; got != want { -- t.Errorf("got %q, want %q", got, want) -- } --} +-//go:embed embedText //ok +-var e4 []byte - --func TestBugJSON(t *testing.T) { -- b1 := Bug{ -- File: "foo.go", -- Line: 1, -- Description: "a bug", -- Key: "foo.go:1", -- Stack: "", -- AtTime: time.Now(), -- } +-//go:embed embedText //ok +-var e5 embed.FS - -- data, err := json.Marshal(b1) -- if err != nil { -- t.Fatal(err) -- } -- var b2 Bug -- if err := json.Unmarshal(data, &b2); err != nil { -- t.Fatal(err) -- } -- if diff := cmp.Diff(b1, b2); diff != "" { -- t.Errorf("bugs differ after JSON Marshal/Unmarshal (-b1 +b2):\n%s", diff) -- } --} -diff -urN a/gopls/internal/coverage/coverage.go b/gopls/internal/coverage/coverage.go ---- a/gopls/internal/coverage/coverage.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/coverage/coverage.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,266 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-// Followed by wrong kind of decl. +-// +-//go:embed embedText // want "go:embed directives must precede a \"var\" declaration" +-func fooFunc() {} +- +-// Multiple variable specs. +-// +-//go:embed embedText // want "declarations following go:embed directives must define a single variable" +-var e6, e7 []byte +- +-// Specifying a value is not allowed. +-// +-//go:embed embedText // want "declarations following go:embed directives must not specify a value" +-var e8 string = "foo" +- +-// TODO: This should not be OK, misplaced according to compiler. +-// +-//go:embed embedText // ok +-var ( +- e9 string +- e10 string +-) +- +-// Type definition. +-type fooType []byte +- +-//go:embed embedText //ok +-var e11 fooType +- +-// Type alias. +-type barType = string +- +-//go:embed embedText //ok +-var e12 barType +- +-// Renamed embed package. +- +-//go:embed embedText //ok +-var e13 embedPkg.FS +- +-// Renamed embed package alias. +-type embedAlias = embedPkg.FS +- +-//go:embed embedText //ok +-var e14 embedAlias +- +-// var blocks are OK as long as the variable following the directive is OK. +-var ( +- x, y, z string +- //go:embed embedText // ok +- e20 string +- q, r, t string +-) +- +-//go:embed embedText // want "go:embed directives must precede a \"var\" declaration" +-var () +- +-// Incorrect types. +- +-//go:embed embedText // want `declarations following go:embed directives must be of type string, \[\]byte or embed.FS` +-var e16 byte +- +-//go:embed embedText // want `declarations following go:embed directives must be of type string, \[\]byte or embed.FS` +-var e17 []string - --//go:build go.1.16 --// +build go.1.16 +-//go:embed embedText // want `declarations following go:embed directives must be of type string, \[\]byte or embed.FS` +-var e18 embed.Foo +- +-//go:embed embedText // want `declarations following go:embed directives must be of type string, \[\]byte or embed.FS` +-var e19 foo.FS +- +-type byteAlias byte +- +-//go:embed embedText // want `declarations following go:embed directives must be of type string, \[\]byte or embed.FS` +-var e15 byteAlias +- +-// A type declaration of embed.FS is not accepted by the compiler, in contrast to an alias. +-type embedDecl embed.FS +- +-//go:embed embedText // want `declarations following go:embed directives must be of type string, \[\]byte or embed.FS` +-var e16 embedDecl +- +-// This is main function +-func main() { +- fmt.Println(s) +-} +- +-// No declaration following. +-//go:embed embedText // want "go:embed directives must precede a \"var\" declaration" +diff -urN a/gopls/internal/analysis/embeddirective/testdata/src/a/import_present_go120.go b/gopls/internal/analysis/embeddirective/testdata/src/a/import_present_go120.go +--- a/gopls/internal/analysis/embeddirective/testdata/src/a/import_present_go120.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/embeddirective/testdata/src/a/import_present_go120.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,26 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-//go:build go1.20 +-// +build go1.20 +- +-package a +- +-var ( +- // Okay directive wise but the compiler will complain that +- // imports must appear before other declarations. +- //go:embed embedText // ok +- foo string +-) +- +-import ( +- "fmt" +- +- _ "embed" +-) +- +-// This is main function +-func main() { +- fmt.Println(s) +-} +diff -urN a/gopls/internal/analysis/fillreturns/doc.go b/gopls/internal/analysis/fillreturns/doc.go +--- a/gopls/internal/analysis/fillreturns/doc.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/fillreturns/doc.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,27 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// Running this program in the tools directory will produce a coverage file /tmp/cover.out --// and a coverage report for all the packages under internal/lsp, accumulated by all the tests --// under gopls. +-// Package fillreturns defines an Analyzer that will attempt to +-// automatically fill in a return statement that has missing +-// values with zero value elements. -// --// -o controls where the coverage file is written, defaulting to /tmp/cover.out --// -i coverage-file will generate the report from an existing coverage file --// -v controls verbosity (0: only report coverage, 1: report as each directory is finished, +-// # Analyzer fillreturns -// --// 2: report on each test, 3: more details, 4: too much) +-// fillreturns: suggest fixes for errors due to an incorrect number of return values -// --// -t tests only tests packages in the given comma-separated list of directories in gopls. +-// This checker provides suggested fixes for type errors of the +-// type "wrong number of return values (want %d, got %d)". For example: -// --// The names should start with ., as in ./internal/regtest/bench +-// func m() (int, string, *bool, error) { +-// return +-// } -// --// -run tests. If set, -run tests is passed on to the go test command. +-// will turn into -// --// Despite gopls' use of goroutines, the counts are almost deterministic. --package main +-// func m() (int, string, *bool, error) { +-// return 0, "", nil, nil +-// } +-// +-// This functionality is similar to https://github.com/sqs/goreturns. +-package fillreturns +diff -urN a/gopls/internal/analysis/fillreturns/fillreturns.go b/gopls/internal/analysis/fillreturns/fillreturns.go +--- a/gopls/internal/analysis/fillreturns/fillreturns.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/fillreturns/fillreturns.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,264 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package fillreturns - -import ( - "bytes" -- "encoding/json" -- "flag" +- _ "embed" - "fmt" -- "log" -- "os" -- "os/exec" -- "path/filepath" -- "sort" +- "go/ast" +- "go/format" +- "go/types" +- "regexp" - "strings" -- "time" - -- "golang.org/x/tools/cover" +- "golang.org/x/tools/go/analysis" +- "golang.org/x/tools/go/ast/astutil" +- "golang.org/x/tools/internal/analysisinternal" +- "golang.org/x/tools/internal/fuzzy" -) - --var ( -- proFile = flag.String("i", "", "existing profile file") -- outFile = flag.String("o", "/tmp/cover.out", "where to write the coverage file") -- verbose = flag.Int("v", 0, "how much detail to print as tests are running") -- tests = flag.String("t", "", "list of tests to run") -- run = flag.String("run", "", "value of -run to pass to go test") --) +-//go:embed doc.go +-var doc string - --func main() { -- log.SetFlags(log.Lshortfile) -- flag.Parse() +-var Analyzer = &analysis.Analyzer{ +- Name: "fillreturns", +- Doc: analysisinternal.MustExtractDoc(doc, "fillreturns"), +- Run: run, +- RunDespiteErrors: true, +- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/fillreturns", +-} - -- if *proFile != "" { -- report(*proFile) -- return +-func run(pass *analysis.Pass) (interface{}, error) { +- info := pass.TypesInfo +- if info == nil { +- return nil, fmt.Errorf("nil TypeInfo") - } - -- checkCwd() -- // find the packages under gopls containing tests -- tests := listDirs("gopls") -- tests = onlyTests(tests) -- tests = realTestName(tests) -- -- // report coverage for packages under internal/lsp -- parg := "golang.org/x/tools/gopls/internal/lsp/..." +-outer: +- for _, typeErr := range pass.TypeErrors { +- // Filter out the errors that are not relevant to this analyzer. +- if !FixesError(typeErr) { +- continue +- } +- var file *ast.File +- for _, f := range pass.Files { +- if f.Pos() <= typeErr.Pos && typeErr.Pos <= f.End() { +- file = f +- break +- } +- } +- if file == nil { +- continue +- } - -- accum := []string{} -- seen := make(map[string]bool) -- now := time.Now() -- for _, toRun := range tests { -- if excluded(toRun) { +- // Get the end position of the error. +- // (This heuristic assumes that the buffer is formatted, +- // at least up to the end position of the error.) +- var buf bytes.Buffer +- if err := format.Node(&buf, pass.Fset, file); err != nil { - continue - } -- x := runTest(toRun, parg) -- if *verbose > 0 { -- fmt.Printf("finished %s %.1fs\n", toRun, time.Since(now).Seconds()) +- typeErrEndPos := analysisinternal.TypeErrorEndPos(pass.Fset, buf.Bytes(), typeErr.Pos) +- +- // TODO(rfindley): much of the error handling code below returns, when it +- // should probably continue. +- +- // Get the path for the relevant range. +- path, _ := astutil.PathEnclosingInterval(file, typeErr.Pos, typeErrEndPos) +- if len(path) == 0 { +- return nil, nil - } -- lines := bytes.Split(x, []byte{'\n'}) -- for _, l := range lines { -- if len(l) == 0 { -- continue -- } -- if !seen[string(l)] { -- // not accumulating counts, so only works for mode:set -- seen[string(l)] = true -- accum = append(accum, string(l)) +- +- // Find the enclosing return statement. +- var ret *ast.ReturnStmt +- var retIdx int +- for i, n := range path { +- if r, ok := n.(*ast.ReturnStmt); ok { +- ret = r +- retIdx = i +- break - } - } -- } -- sort.Strings(accum[1:]) -- if err := os.WriteFile(*outFile, []byte(strings.Join(accum, "\n")), 0644); err != nil { -- log.Print(err) -- } -- report(*outFile) --} +- if ret == nil { +- return nil, nil +- } - --type result struct { -- Time time.Time -- Test string -- Action string -- Package string -- Output string -- Elapsed float64 --} -- --func runTest(tName, parg string) []byte { -- args := []string{"test", "-short", "-coverpkg", parg, "-coverprofile", *outFile, -- "-json"} -- if *run != "" { -- args = append(args, fmt.Sprintf("-run=%s", *run)) -- } -- args = append(args, tName) -- cmd := exec.Command("go", args...) -- cmd.Dir = "./gopls" -- ans, err := cmd.Output() -- if *verbose > 1 { -- got := strings.Split(string(ans), "\n") -- for _, g := range got { -- if g == "" { -- continue +- // Get the function type that encloses the ReturnStmt. +- var enclosingFunc *ast.FuncType +- for _, n := range path[retIdx+1:] { +- switch node := n.(type) { +- case *ast.FuncLit: +- enclosingFunc = node.Type +- case *ast.FuncDecl: +- enclosingFunc = node.Type - } -- var m result -- if err := json.Unmarshal([]byte(g), &m); err != nil { -- log.Printf("%T/%v", err, err) // shouldn't happen -- continue +- if enclosingFunc != nil { +- break - } -- maybePrint(m) - } -- } -- if err != nil { -- log.Printf("%s: %q, cmd=%s", tName, ans, cmd.String()) -- } -- buf, err := os.ReadFile(*outFile) -- if err != nil { -- log.Fatal(err) -- } -- return buf --} +- if enclosingFunc == nil || enclosingFunc.Results == nil { +- continue +- } - --func report(fn string) { -- profs, err := cover.ParseProfiles(fn) -- if err != nil { -- log.Fatal(err) -- } -- for _, p := range profs { -- statements, counts := 0, 0 -- for _, x := range p.Blocks { -- statements += x.NumStmt -- if x.Count != 0 { -- counts += x.NumStmt // sic: if any were executed, all were -- } +- // Skip any generic enclosing functions, since type parameters don't +- // have 0 values. +- // TODO(rfindley): We should be able to handle this if the return +- // values are all concrete types. +- if tparams := enclosingFunc.TypeParams; tparams != nil && tparams.NumFields() > 0 { +- return nil, nil - } -- pc := 100 * float64(counts) / float64(statements) -- fmt.Printf("%3.0f%% %3d/%3d %s\n", pc, counts, statements, p.FileName) -- } --} - --var todo []string // tests to run +- // Find the function declaration that encloses the ReturnStmt. +- var outer *ast.FuncDecl +- for _, p := range path { +- if p, ok := p.(*ast.FuncDecl); ok { +- outer = p +- break +- } +- } +- if outer == nil { +- return nil, nil +- } - --func excluded(tname string) bool { -- if *tests == "" { // run all tests -- return false -- } -- if todo == nil { -- todo = strings.Split(*tests, ",") -- } -- for _, nm := range todo { -- if tname == nm { // run this test -- return false +- // Skip any return statements that contain function calls with multiple +- // return values. +- for _, expr := range ret.Results { +- e, ok := expr.(*ast.CallExpr) +- if !ok { +- continue +- } +- if tup, ok := info.TypeOf(e).(*types.Tuple); ok && tup.Len() > 1 { +- continue outer +- } - } -- } -- // not in list, skip it -- return true --} - --// should m.Package be printed sometime? --func maybePrint(m result) { -- switch m.Action { -- case "pass", "fail", "skip": -- fmt.Printf("%s %s %.3f\n", m.Action, m.Test, m.Elapsed) -- case "run": -- if *verbose > 2 { -- fmt.Printf("%s %s %.3f\n", m.Action, m.Test, m.Elapsed) +- // Duplicate the return values to track which values have been matched. +- remaining := make([]ast.Expr, len(ret.Results)) +- copy(remaining, ret.Results) +- +- fixed := make([]ast.Expr, len(enclosingFunc.Results.List)) +- +- // For each value in the return function declaration, find the leftmost element +- // in the return statement that has the desired type. If no such element exists, +- // fill in the missing value with the appropriate "zero" value. +- // Beware that type information may be incomplete. +- var retTyps []types.Type +- for _, ret := range enclosingFunc.Results.List { +- retTyp := info.TypeOf(ret.Type) +- if retTyp == nil { +- return nil, nil +- } +- retTyps = append(retTyps, retTyp) - } -- case "output": -- if *verbose > 3 { -- fmt.Printf("%s %s %q %.3f\n", m.Action, m.Test, m.Output, m.Elapsed) +- matches := analysisinternal.MatchingIdents(retTyps, file, ret.Pos(), info, pass.Pkg) +- for i, retTyp := range retTyps { +- var match ast.Expr +- var idx int +- for j, val := range remaining { +- if t := info.TypeOf(val); t == nil || !matchingTypes(t, retTyp) { +- continue +- } +- if !analysisinternal.IsZeroValue(val) { +- match, idx = val, j +- break +- } +- // If the current match is a "zero" value, we keep searching in +- // case we find a non-"zero" value match. If we do not find a +- // non-"zero" value, we will use the "zero" value. +- match, idx = val, j +- } +- +- if match != nil { +- fixed[i] = match +- remaining = append(remaining[:idx], remaining[idx+1:]...) +- } else { +- names, ok := matches[retTyp] +- if !ok { +- return nil, fmt.Errorf("invalid return type: %v", retTyp) +- } +- // Find the identifier most similar to the return type. +- // If no identifier matches the pattern, generate a zero value. +- if best := fuzzy.BestMatch(retTyp.String(), names); best != "" { +- fixed[i] = ast.NewIdent(best) +- } else if zero := analysisinternal.ZeroValue(file, pass.Pkg, retTyp); zero != nil { +- fixed[i] = zero +- } else { +- return nil, nil +- } +- } - } -- case "pause", "cont": -- if *verbose > 2 { -- fmt.Printf("%s %s %.3f\n", m.Action, m.Test, m.Elapsed) +- +- // Remove any non-matching "zero values" from the leftover values. +- var nonZeroRemaining []ast.Expr +- for _, expr := range remaining { +- if !analysisinternal.IsZeroValue(expr) { +- nonZeroRemaining = append(nonZeroRemaining, expr) +- } - } -- default: -- fmt.Printf("%#v\n", m) -- log.Fatalf("unknown action %s\n", m.Action) -- } --} +- // Append leftover return values to end of new return statement. +- fixed = append(fixed, nonZeroRemaining...) - --// return only the directories that contain tests --func onlyTests(s []string) []string { -- ans := []string{} --outer: -- for _, d := range s { -- files, err := os.ReadDir(d) -- if err != nil { -- log.Fatalf("%s: %v", d, err) +- newRet := &ast.ReturnStmt{ +- Return: ret.Pos(), +- Results: fixed, - } -- for _, de := range files { -- if strings.Contains(de.Name(), "_test.go") { -- ans = append(ans, d) -- continue outer -- } +- +- // Convert the new return statement AST to text. +- var newBuf bytes.Buffer +- if err := format.Node(&newBuf, pass.Fset, newRet); err != nil { +- return nil, err - } -- } -- return ans --} - --// replace the prefix gopls/ with ./ as the tests are run in the gopls directory --func realTestName(p []string) []string { -- ans := []string{} -- for _, x := range p { -- x = x[len("gopls/"):] -- ans = append(ans, "./"+x) +- pass.Report(analysis.Diagnostic{ +- Pos: typeErr.Pos, +- End: typeErrEndPos, +- Message: typeErr.Msg, +- SuggestedFixes: []analysis.SuggestedFix{{ +- Message: "Fill in return values", +- TextEdits: []analysis.TextEdit{{ +- Pos: ret.Pos(), +- End: ret.End(), +- NewText: newBuf.Bytes(), +- }}, +- }}, +- }) - } -- return ans +- return nil, nil -} - --// make sure we start in a tools directory --func checkCwd() { -- dir, err := os.Getwd() -- if err != nil { -- log.Fatal(err) -- } -- // we expect to be at the root of golang.org/x/tools -- cmd := exec.Command("go", "list", "-m", "-f", "{{.Dir}}", "golang.org/x/tools") -- buf, err := cmd.Output() -- buf = bytes.Trim(buf, "\n \t") // remove \n at end -- if err != nil { -- log.Fatal(err) -- } -- if string(buf) != dir { -- log.Fatalf("wrong directory: in %q, should be in %q", dir, string(buf)) +-func matchingTypes(want, got types.Type) bool { +- if want == got || types.Identical(want, got) { +- return true - } -- // and we expect gopls and internal/lsp as subdirectories -- _, err = os.Stat("gopls") -- if err != nil { -- log.Fatalf("expected a gopls directory, %v", err) +- // Code segment to help check for untyped equality from (golang/go#32146). +- if rhs, ok := want.(*types.Basic); ok && rhs.Info()&types.IsUntyped > 0 { +- if lhs, ok := got.Underlying().(*types.Basic); ok { +- return rhs.Info()&types.IsConstType == lhs.Info()&types.IsConstType +- } - } +- return types.AssignableTo(want, got) || types.ConvertibleTo(want, got) -} - --func listDirs(dir string) []string { -- ans := []string{} -- f := func(path string, dirEntry os.DirEntry, err error) error { -- if strings.HasSuffix(path, "/testdata") || strings.HasSuffix(path, "/typescript") { -- return filepath.SkipDir -- } -- if dirEntry.IsDir() { -- ans = append(ans, path) +-// Error messages have changed across Go versions. These regexps capture recent +-// incarnations. +-// +-// TODO(rfindley): once error codes are exported and exposed via go/packages, +-// use error codes rather than string matching here. +-var wrongReturnNumRegexes = []*regexp.Regexp{ +- regexp.MustCompile(`wrong number of return values \(want (\d+), got (\d+)\)`), +- regexp.MustCompile(`too many return values`), +- regexp.MustCompile(`not enough return values`), +-} +- +-func FixesError(err types.Error) bool { +- msg := strings.TrimSpace(err.Msg) +- for _, rx := range wrongReturnNumRegexes { +- if rx.MatchString(msg) { +- return true - } -- return nil - } -- filepath.WalkDir(dir, f) -- return ans +- return false -} -diff -urN a/gopls/internal/hooks/analysis_116.go b/gopls/internal/hooks/analysis_116.go ---- a/gopls/internal/hooks/analysis_116.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/hooks/analysis_116.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,14 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/analysis/fillreturns/fillreturns_test.go b/gopls/internal/analysis/fillreturns/fillreturns_test.go +--- a/gopls/internal/analysis/fillreturns/fillreturns_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/fillreturns/fillreturns_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,24 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --//go:build !go1.19 --// +build !go1.19 +-package fillreturns_test - --package hooks +-import ( +- "os" +- "strings" +- "testing" - --import "golang.org/x/tools/gopls/internal/lsp/source" +- "golang.org/x/tools/go/analysis/analysistest" +- "golang.org/x/tools/gopls/internal/analysis/fillreturns" +-) - --func updateAnalyzers(options *source.Options) { -- options.StaticcheckSupported = false +-func Test(t *testing.T) { +- // TODO(golang/go#65294): delete once gotypesalias=1 is the default. +- if strings.Contains(os.Getenv("GODEBUG"), "gotypesalias=1") { +- t.Skip("skipping due to gotypesalias=1, which changes (improves) the result; reenable and update the expectations once it is the default") +- } +- +- testdata := analysistest.TestData() +- analysistest.RunWithSuggestedFixes(t, testdata, fillreturns.Analyzer, "a", "typeparams") -} -diff -urN a/gopls/internal/hooks/analysis_119.go b/gopls/internal/hooks/analysis_119.go ---- a/gopls/internal/hooks/analysis_119.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/hooks/analysis_119.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,62 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/analysis/fillreturns/testdata/src/a/a.go b/gopls/internal/analysis/fillreturns/testdata/src/a/a.go +--- a/gopls/internal/analysis/fillreturns/testdata/src/a/a.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/fillreturns/testdata/src/a/a.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,139 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --//go:build go1.19 --// +build go1.19 -- --package hooks +-package fillreturns - -import ( -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "honnef.co/go/tools/analysis/lint" -- "honnef.co/go/tools/quickfix" -- "honnef.co/go/tools/simple" -- "honnef.co/go/tools/staticcheck" -- "honnef.co/go/tools/stylecheck" +- "errors" +- "go/ast" +- ast2 "go/ast" +- "io" +- "net/http" +- . "net/http" +- "net/url" +- "strconv" -) - --func updateAnalyzers(options *source.Options) { -- options.StaticcheckSupported = true +-type T struct{} +-type T1 = T +-type I interface{} +-type I1 = I +-type z func(string, http.Handler) error - -- mapSeverity := func(severity lint.Severity) protocol.DiagnosticSeverity { -- switch severity { -- case lint.SeverityError: -- return protocol.SeverityError -- case lint.SeverityDeprecated: -- // TODO(dh): in LSP, deprecated is a tag, not a severity. -- // We'll want to support this once we enable SA5011. -- return protocol.SeverityWarning -- case lint.SeverityWarning: -- return protocol.SeverityWarning -- case lint.SeverityInfo: -- return protocol.SeverityInformation -- case lint.SeverityHint: -- return protocol.SeverityHint -- default: -- return protocol.SeverityWarning -- } -- } -- add := func(analyzers []*lint.Analyzer, skip map[string]struct{}) { -- for _, a := range analyzers { -- if _, ok := skip[a.Analyzer.Name]; ok { -- continue -- } +-func x() error { +- return errors.New("foo") +-} - -- enabled := !a.Doc.NonDefault -- options.AddStaticcheckAnalyzer(a.Analyzer, enabled, mapSeverity(a.Doc.Severity)) -- } -- } +-// The error messages below changed in 1.18; "return values" covers both forms. - -- add(simple.Analyzers, nil) -- add(staticcheck.Analyzers, map[string]struct{}{ -- // This check conflicts with the vet printf check (golang/go#34494). -- "SA5009": {}, -- // This check relies on facts from dependencies, which -- // we don't currently compute. -- "SA5011": {}, -- }) -- add(stylecheck.Analyzers, nil) -- add(quickfix.Analyzers, nil) +-func b() (string, int, error) { +- return "", errors.New("foo") // want "return values" -} -diff -urN a/gopls/internal/hooks/diff.go b/gopls/internal/hooks/diff.go ---- a/gopls/internal/hooks/diff.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/hooks/diff.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,168 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package hooks +-func c() (string, int, error) { +- return 7, errors.New("foo") // want "return values" +-} - --import ( -- "encoding/json" -- "fmt" -- "log" -- "os" -- "path/filepath" -- "runtime" -- "sync" -- "time" -- -- "github.com/sergi/go-diff/diffmatchpatch" -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/internal/diff" --) +-func d() (string, int, error) { +- return "", 7 // want "return values" +-} - --// structure for saving information about diffs --// while the new code is being rolled out --type diffstat struct { -- Before, After int -- Oldedits, Newedits int -- Oldtime, Newtime time.Duration -- Stack string -- Msg string `json:",omitempty"` // for errors -- Ignored int `json:",omitempty"` // numbr of skipped records with 0 edits +-func e() (T, error, *bool) { +- return (z(http.ListenAndServe))("", nil) // want "return values" -} - --var ( -- ignoredMu sync.Mutex -- ignored int // counter of diff requests on equal strings +-func preserveLeft() (int, int, error) { +- return 1, errors.New("foo") // want "return values" +-} - -- diffStatsOnce sync.Once -- diffStats *os.File // never closed --) +-func matchValues() (int, error, string) { +- return errors.New("foo"), 3 // want "return values" +-} - --// save writes a JSON record of statistics about diff requests to a temporary file. --func (s *diffstat) save() { -- diffStatsOnce.Do(func() { -- f, err := os.CreateTemp("", "gopls-diff-stats-*") -- if err != nil { -- log.Printf("can't create diff stats temp file: %v", err) // e.g. disk full -- return -- } -- diffStats = f -- }) -- if diffStats == nil { -- return -- } +-func preventDataOverwrite() (int, string) { +- return errors.New("foo") // want "return values" +-} - -- // diff is frequently called with equal strings, -- // so we count repeated instances but only print every 15th. -- ignoredMu.Lock() -- if s.Oldedits == 0 && s.Newedits == 0 { -- ignored++ -- if ignored < 15 { -- ignoredMu.Unlock() -- return -- } +-func closure() (string, error) { +- _ = func() (int, error) { +- return // want "return values" - } -- s.Ignored = ignored -- ignored = 0 -- ignoredMu.Unlock() +- return // want "return values" +-} - -- // Record the name of the file in which diff was called. -- // There aren't many calls, so only the base name is needed. -- if _, file, line, ok := runtime.Caller(2); ok { -- s.Stack = fmt.Sprintf("%s:%d", filepath.Base(file), line) -- } -- x, err := json.Marshal(s) -- if err != nil { -- log.Fatalf("internal error marshalling JSON: %v", err) -- } -- fmt.Fprintf(diffStats, "%s\n", x) +-func basic() (uint8, uint16, uint32, uint64, int8, int16, int32, int64, float32, float64, complex64, complex128, byte, rune, uint, int, uintptr, string, bool, error) { +- return // want "return values" -} - --// disaster is called when the diff algorithm panics or produces a --// diff that cannot be applied. It saves the broken input in a --// new temporary file and logs the file name, which is returned. --func disaster(before, after string) string { -- // We use the pid to salt the name, not os.TempFile, -- // so that each process creates at most one file. -- // One is sufficient for a bug report. -- filename := fmt.Sprintf("%s/gopls-diff-bug-%x", os.TempDir(), os.Getpid()) +-func complex() (*int, []int, [2]int, map[int]int) { +- return // want "return values" +-} - -- // We use NUL as a separator: it should never appear in Go source. -- data := before + "\x00" + after +-func structsAndInterfaces() (T, url.URL, T1, I, I1, io.Reader, Client, ast2.Stmt) { +- return // want "return values" +-} - -- if err := os.WriteFile(filename, []byte(data), 0600); err != nil { -- log.Printf("failed to write diff bug report: %v", err) -- return "" +-func m() (int, error) { +- if 1 == 2 { +- return // want "return values" +- } else if 1 == 3 { +- return errors.New("foo") // want "return values" +- } else { +- return 1 // want "return values" - } +- return // want "return values" +-} - -- bug.Reportf("Bug detected in diff algorithm! Please send file %s to the maintainers of gopls if you are comfortable sharing its contents.", filename) +-func convertibleTypes() (ast2.Expr, int) { +- return &ast2.ArrayType{} // want "return values" +-} - -- return filename +-func assignableTypes() (map[string]int, int) { +- type X map[string]int +- var x X +- return x // want "return values" -} - --// BothDiffs edits calls both the new and old diffs, checks that the new diffs --// change before into after, and attempts to preserve some statistics. --func BothDiffs(before, after string) (edits []diff.Edit) { -- // The new diff code contains a lot of internal checks that panic when they -- // fail. This code catches the panics, or other failures, tries to save -- // the failing example (and it would ask the user to send it back to us, and -- // changes options.newDiff to 'old', if only we could figure out how.) -- stat := diffstat{Before: len(before), After: len(after)} -- now := time.Now() -- oldedits := ComputeEdits(before, after) -- stat.Oldedits = len(oldedits) -- stat.Oldtime = time.Since(now) -- defer func() { -- if r := recover(); r != nil { -- disaster(before, after) -- edits = oldedits -- } -- }() -- now = time.Now() -- newedits := diff.Strings(before, after) -- stat.Newedits = len(newedits) -- stat.Newtime = time.Now().Sub(now) -- got, err := diff.Apply(before, newedits) -- if err != nil || got != after { -- stat.Msg += "FAIL" -- disaster(before, after) -- stat.save() -- return oldedits -- } -- stat.save() -- return newedits --} -- --// ComputeEdits computes a diff using the github.com/sergi/go-diff implementation. --func ComputeEdits(before, after string) (edits []diff.Edit) { -- // The go-diff library has an unresolved panic (see golang/go#278774). -- // TODO(rstambler): Remove the recover once the issue has been fixed -- // upstream. -- defer func() { -- if r := recover(); r != nil { -- bug.Reportf("unable to compute edits: %s", r) -- // Report one big edit for the whole file. -- edits = []diff.Edit{{ -- Start: 0, -- End: len(before), -- New: after, -- }} -- } -- }() -- diffs := diffmatchpatch.New().DiffMain(before, after, true) -- edits = make([]diff.Edit, 0, len(diffs)) -- offset := 0 -- for _, d := range diffs { -- start := offset -- switch d.Type { -- case diffmatchpatch.DiffDelete: -- offset += len(d.Text) -- edits = append(edits, diff.Edit{Start: start, End: offset}) -- case diffmatchpatch.DiffEqual: -- offset += len(d.Text) -- case diffmatchpatch.DiffInsert: -- edits = append(edits, diff.Edit{Start: start, End: start, New: d.Text}) -- } -- } -- return edits +-func interfaceAndError() (I, int) { +- return errors.New("foo") // want "return values" -} -diff -urN a/gopls/internal/hooks/diff_test.go b/gopls/internal/hooks/diff_test.go ---- a/gopls/internal/hooks/diff_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/hooks/diff_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,32 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package hooks +-func funcOneReturn() (string, error) { +- return strconv.Itoa(1) // want "return values" +-} - --import ( -- "os" -- "testing" +-func funcMultipleReturn() (int, error, string) { +- return strconv.Atoi("1") +-} - -- "golang.org/x/tools/internal/diff/difftest" --) +-func localFuncMultipleReturn() (string, int, error, string) { +- return b() +-} - --func TestDiff(t *testing.T) { -- difftest.DiffTest(t, ComputeEdits) +-func multipleUnused() (int, string, string, string) { +- return 3, 4, 5 // want "return values" -} - --func TestDisaster(t *testing.T) { -- a := "This is a string,(\u0995) just for basic\nfunctionality" -- b := "This is another string, (\u0996) to see if disaster will store stuff correctly" -- fname := disaster(a, b) -- buf, err := os.ReadFile(fname) -- if err != nil { -- t.Fatal(err) -- } -- if string(buf) != a+"\x00"+b { -- t.Error("failed to record original strings") -- } -- if err := os.Remove(fname); err != nil { -- t.Error(err) +-func gotTooMany() int { +- if true { +- return 0, "" // want "return values" +- } else { +- return 1, 0, nil // want "return values" - } +- return 0, 5, false // want "return values" -} -diff -urN a/gopls/internal/hooks/gen-licenses.sh b/gopls/internal/hooks/gen-licenses.sh ---- a/gopls/internal/hooks/gen-licenses.sh 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/hooks/gen-licenses.sh 1970-01-01 00:00:00.000000000 +0000 -@@ -1,38 +0,0 @@ --#!/bin/bash -eu -- --# Copyright 2020 The Go Authors. All rights reserved. --# Use of this source code is governed by a BSD-style --# license that can be found in the LICENSE file. -- --set -o pipefail -- --output=$1 --tempfile=$(mktemp) --cd $(dirname $0) - --cat > $tempfile <> $tempfile -- echo >> $tempfile -- sed 's/^-- / &/' $dir/$license >> $tempfile -- echo >> $tempfile --done +-type T struct{} +-type T1 = T +-type I interface{} +-type I1 = I +-type z func(string, http.Handler) error - --echo "\`" >> $tempfile --mv $tempfile $output -\ No newline at end of file -diff -urN a/gopls/internal/hooks/gofumpt_117.go b/gopls/internal/hooks/gofumpt_117.go ---- a/gopls/internal/hooks/gofumpt_117.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/hooks/gofumpt_117.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,13 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-func x() error { +- return errors.New("foo") +-} - --//go:build !go1.18 --// +build !go1.18 +-// The error messages below changed in 1.18; "return values" covers both forms. - --package hooks +-func b() (string, int, error) { +- return "", 0, errors.New("foo") // want "return values" +-} - --import "golang.org/x/tools/gopls/internal/lsp/source" +-func c() (string, int, error) { +- return "", 7, errors.New("foo") // want "return values" +-} - --func updateGofumpt(options *source.Options) { +-func d() (string, int, error) { +- return "", 7, nil // want "return values" -} -diff -urN a/gopls/internal/hooks/gofumpt_118.go b/gopls/internal/hooks/gofumpt_118.go ---- a/gopls/internal/hooks/gofumpt_118.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/hooks/gofumpt_118.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,78 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --//go:build go1.18 --// +build go1.18 +-func e() (T, error, *bool) { +- return T{}, (z(http.ListenAndServe))("", nil), nil // want "return values" +-} - --package hooks +-func preserveLeft() (int, int, error) { +- return 1, 0, errors.New("foo") // want "return values" +-} - --import ( -- "context" -- "fmt" +-func matchValues() (int, error, string) { +- return 3, errors.New("foo"), "" // want "return values" +-} - -- "golang.org/x/tools/gopls/internal/lsp/source" -- "mvdan.cc/gofumpt/format" --) +-func preventDataOverwrite() (int, string) { +- return 0, "", errors.New("foo") // want "return values" +-} - --func updateGofumpt(options *source.Options) { -- options.GofumptFormat = func(ctx context.Context, langVersion, modulePath string, src []byte) ([]byte, error) { -- fixedVersion, err := fixLangVersion(langVersion) -- if err != nil { -- return nil, err -- } -- return format.Source(src, format.Options{ -- LangVersion: fixedVersion, -- ModulePath: modulePath, -- }) +-func closure() (string, error) { +- _ = func() (int, error) { +- return 0, nil // want "return values" - } +- return "", nil // want "return values" -} - --// fixLangVersion function cleans the input so that gofumpt doesn't panic. It is --// rather permissive, and accepts version strings that aren't technically valid --// in a go.mod file. --// --// More specifically, it looks for an optional 'v' followed by 1-3 --// '.'-separated numbers. The resulting string is stripped of any suffix beyond --// this expected version number pattern. --// --// See also golang/go#61692: gofumpt does not accept the new language versions --// appearing in go.mod files (e.g. go1.21rc3). --func fixLangVersion(input string) (string, error) { -- bad := func() (string, error) { -- return "", fmt.Errorf("invalid language version syntax %q", input) -- } -- if input == "" { -- return input, nil -- } -- i := 0 -- if input[0] == 'v' { // be flexible about 'v' -- i++ -- } -- // takeDigits consumes ascii numerals 0-9 and reports if at least one was -- // consumed. -- takeDigits := func() bool { -- found := false -- for ; i < len(input) && '0' <= input[i] && input[i] <= '9'; i++ { -- found = true -- } -- return found -- } -- if !takeDigits() { // versions must start with at least one number -- return bad() -- } +-func basic() (uint8, uint16, uint32, uint64, int8, int16, int32, int64, float32, float64, complex64, complex128, byte, rune, uint, int, uintptr, string, bool, error) { +- return 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, "", false, nil // want "return values" +-} - -- // Accept optional minor and patch versions. -- for n := 0; n < 2; n++ { -- if i < len(input) && input[i] == '.' { -- // Look for minor/patch version. -- i++ -- if !takeDigits() { -- i-- -- break -- } -- } +-func complex() (*int, []int, [2]int, map[int]int) { +- return nil, nil, nil, nil // want "return values" +-} +- +-func structsAndInterfaces() (T, url.URL, T1, I, I1, io.Reader, Client, ast2.Stmt) { +- return T{}, url.URL{}, T{}, nil, nil, nil, Client{}, nil // want "return values" +-} +- +-func m() (int, error) { +- if 1 == 2 { +- return 0, nil // want "return values" +- } else if 1 == 3 { +- return 0, errors.New("foo") // want "return values" +- } else { +- return 1, nil // want "return values" - } -- // Accept any suffix. -- return input[:i], nil +- return 0, nil // want "return values" -} -diff -urN a/gopls/internal/hooks/gofumpt_118_test.go b/gopls/internal/hooks/gofumpt_118_test.go ---- a/gopls/internal/hooks/gofumpt_118_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/hooks/gofumpt_118_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,53 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --//go:build go1.18 --// +build go1.18 +-func convertibleTypes() (ast2.Expr, int) { +- return &ast2.ArrayType{}, 0 // want "return values" +-} - --package hooks +-func assignableTypes() (map[string]int, int) { +- type X map[string]int +- var x X +- return x, 0 // want "return values" +-} - --import "testing" +-func interfaceAndError() (I, int) { +- return errors.New("foo"), 0 // want "return values" +-} - --func TestFixLangVersion(t *testing.T) { -- tests := []struct { -- input, want string -- wantErr bool -- }{ -- {"", "", false}, -- {"1.18", "1.18", false}, -- {"v1.18", "v1.18", false}, -- {"1.21", "1.21", false}, -- {"1.21rc3", "1.21", false}, -- {"1.21.0", "1.21.0", false}, -- {"1.21.1", "1.21.1", false}, -- {"v1.21.1", "v1.21.1", false}, -- {"v1.21.0rc1", "v1.21.0", false}, // not technically valid, but we're flexible -- {"v1.21.0.0", "v1.21.0", false}, // also technically invalid -- {"1.1", "1.1", false}, -- {"v1", "v1", false}, -- {"1", "1", false}, -- {"v1.21.", "v1.21", false}, // also invalid -- {"1.21.", "1.21", false}, +-func funcOneReturn() (string, error) { +- return strconv.Itoa(1), nil // want "return values" +-} - -- // Error cases. -- {"rc1", "", true}, -- {"x1.2.3", "", true}, -- } +-func funcMultipleReturn() (int, error, string) { +- return strconv.Atoi("1") +-} - -- for _, test := range tests { -- got, err := fixLangVersion(test.input) -- if test.wantErr { -- if err == nil { -- t.Errorf("fixLangVersion(%q) succeeded unexpectedly", test.input) -- } -- continue -- } -- if err != nil { -- t.Fatalf("fixLangVersion(%q) failed: %v", test.input, err) -- } -- if got != test.want { -- t.Errorf("fixLangVersion(%q) = %s, want %s", test.input, got, test.want) -- } -- } +-func localFuncMultipleReturn() (string, int, error, string) { +- return b() -} -diff -urN a/gopls/internal/hooks/hooks.go b/gopls/internal/hooks/hooks.go ---- a/gopls/internal/hooks/hooks.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/hooks/hooks.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,31 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --// Package hooks adds all the standard gopls implementations. --// This can be used in tests without needing to use the gopls main, and is --// also the place to edit for custom builds of gopls. --package hooks // import "golang.org/x/tools/gopls/internal/hooks" +-func multipleUnused() (int, string, string, string) { +- return 3, "", "", "", 4, 5 // want "return values" +-} - --import ( -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/internal/diff" -- "mvdan.cc/xurls/v2" --) +-func gotTooMany() int { +- if true { +- return 0 // want "return values" +- } else { +- return 1 // want "return values" +- } +- return 5 // want "return values" +-} - --func Options(options *source.Options) { -- options.LicensesText = licensesText -- if options.GoDiff { -- switch options.NewDiff { -- case "old": -- options.ComputeEdits = ComputeEdits -- case "new": -- options.ComputeEdits = diff.Strings -- default: -- options.ComputeEdits = BothDiffs -- } +-func fillVars() (int, string, ast.Node, bool, error) { +- eint := 0 +- s := "a" +- var t bool +- if true { +- err := errors.New("fail") +- return eint, s, nil, false, err // want "return values" - } -- options.URLRegexp = xurls.Relaxed() -- updateAnalyzers(options) -- updateGofumpt(options) +- n := ast.NewIdent("ident") +- int := 3 +- var b bool +- return int, "", n, b, nil // want "return values" -} -diff -urN a/gopls/internal/hooks/licenses.go b/gopls/internal/hooks/licenses.go ---- a/gopls/internal/hooks/licenses.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/hooks/licenses.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,169 +0,0 @@ +diff -urN a/gopls/internal/analysis/fillreturns/testdata/src/a/typeparams/a.go b/gopls/internal/analysis/fillreturns/testdata/src/a/typeparams/a.go +--- a/gopls/internal/analysis/fillreturns/testdata/src/a/typeparams/a.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/fillreturns/testdata/src/a/typeparams/a.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,5 +0,0 @@ +-package fillreturns +- +-func hello[T any]() int { +- return +-} +diff -urN a/gopls/internal/analysis/fillreturns/testdata/src/a/typeparams/a.go.golden b/gopls/internal/analysis/fillreturns/testdata/src/a/typeparams/a.go.golden +--- a/gopls/internal/analysis/fillreturns/testdata/src/a/typeparams/a.go.golden 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/fillreturns/testdata/src/a/typeparams/a.go.golden 1970-01-01 00:00:00.000000000 +0000 +@@ -1,5 +0,0 @@ +-package fillreturns +- +-func hello[T any]() int { +- return +-} +diff -urN a/gopls/internal/analysis/fillstruct/fillstruct.go b/gopls/internal/analysis/fillstruct/fillstruct.go +--- a/gopls/internal/analysis/fillstruct/fillstruct.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/fillstruct/fillstruct.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,494 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --//go:generate ./gen-licenses.sh licenses.go --package hooks +-// Package fillstruct defines an Analyzer that automatically +-// fills in a struct declaration with zero value elements for each field. +-// +-// The analyzer's diagnostic is merely a prompt. +-// The actual fix is created by a separate direct call from gopls to +-// the SuggestedFixes function. +-// Tests of Analyzer.Run can be found in ./testdata/src. +-// Tests of the SuggestedFixes logic live in ../../testdata/fillstruct. +-package fillstruct - --const licensesText = ` ---- github.com/BurntSushi/toml COPYING -- +-import ( +- "bytes" +- "fmt" +- "go/ast" +- "go/format" +- "go/token" +- "go/types" +- "strings" +- "unicode" - --The MIT License (MIT) +- "golang.org/x/tools/go/analysis" +- "golang.org/x/tools/go/ast/astutil" +- "golang.org/x/tools/go/ast/inspector" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/internal/aliases" +- "golang.org/x/tools/internal/analysisinternal" +- "golang.org/x/tools/internal/fuzzy" +- "golang.org/x/tools/internal/typeparams" +-) - --Copyright (c) 2013 TOML authors +-// Diagnose computes diagnostics for fillable struct literals overlapping with +-// the provided start and end position. +-// +-// The diagnostic contains a lazy fix; the actual patch is computed +-// (via the ApplyFix command) by a call to [SuggestedFix]. +-// +-// If either start or end is invalid, the entire package is inspected. +-func Diagnose(inspect *inspector.Inspector, start, end token.Pos, pkg *types.Package, info *types.Info) []analysis.Diagnostic { +- var diags []analysis.Diagnostic +- nodeFilter := []ast.Node{(*ast.CompositeLit)(nil)} +- inspect.Preorder(nodeFilter, func(n ast.Node) { +- expr := n.(*ast.CompositeLit) - --Permission is hereby granted, free of charge, to any person obtaining a copy --of this software and associated documentation files (the "Software"), to deal --in the Software without restriction, including without limitation the rights --to use, copy, modify, merge, publish, distribute, sublicense, and/or sell --copies of the Software, and to permit persons to whom the Software is --furnished to do so, subject to the following conditions: +- if (start.IsValid() && expr.End() < start) || (end.IsValid() && expr.Pos() > end) { +- return // non-overlapping +- } - --The above copyright notice and this permission notice shall be included in --all copies or substantial portions of the Software. +- typ := info.TypeOf(expr) +- if typ == nil { +- return +- } - --THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR --IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, --FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE --AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER --LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, --OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN --THE SOFTWARE. +- // Find reference to the type declaration of the struct being initialized. +- typ = typeparams.Deref(typ) +- tStruct, ok := typeparams.CoreType(typ).(*types.Struct) +- if !ok { +- return +- } +- // Inv: typ is the possibly-named struct type. - ---- github.com/google/go-cmp LICENSE -- +- fieldCount := tStruct.NumFields() - --Copyright (c) 2017 The Go Authors. All rights reserved. +- // Skip any struct that is already populated or that has no fields. +- if fieldCount == 0 || fieldCount == len(expr.Elts) { +- return +- } - --Redistribution and use in source and binary forms, with or without --modification, are permitted provided that the following conditions are --met: +- // Are any fields in need of filling? +- var fillableFields []string +- for i := 0; i < fieldCount; i++ { +- field := tStruct.Field(i) +- // Ignore fields that are not accessible in the current package. +- if field.Pkg() != nil && field.Pkg() != pkg && !field.Exported() { +- continue +- } +- fillableFields = append(fillableFields, fmt.Sprintf("%s: %s", field.Name(), field.Type().String())) +- } +- if len(fillableFields) == 0 { +- return +- } - -- * Redistributions of source code must retain the above copyright --notice, this list of conditions and the following disclaimer. -- * Redistributions in binary form must reproduce the above --copyright notice, this list of conditions and the following disclaimer --in the documentation and/or other materials provided with the --distribution. -- * Neither the name of Google Inc. nor the names of its --contributors may be used to endorse or promote products derived from --this software without specific prior written permission. +- // Derive a name for the struct type. +- var name string +- if typ != tStruct { +- // named struct type (e.g. pkg.S[T]) +- name = types.TypeString(typ, types.RelativeTo(pkg)) +- } else { +- // anonymous struct type +- totalFields := len(fillableFields) +- const maxLen = 20 +- // Find the index to cut off printing of fields. +- var i, fieldLen int +- for i = range fillableFields { +- if fieldLen > maxLen { +- break +- } +- fieldLen += len(fillableFields[i]) +- } +- fillableFields = fillableFields[:i] +- if i < totalFields { +- fillableFields = append(fillableFields, "...") +- } +- name = fmt.Sprintf("anonymous struct{ %s }", strings.Join(fillableFields, ", ")) +- } +- diags = append(diags, analysis.Diagnostic{ +- Message: fmt.Sprintf("%s literal has missing fields", name), +- Pos: expr.Pos(), +- End: expr.End(), +- Category: FixCategory, +- SuggestedFixes: []analysis.SuggestedFix{{ +- Message: fmt.Sprintf("Fill %s", name), +- // No TextEdits => computed later by gopls. +- }}, +- }) +- }) - --THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS --"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT --LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR --A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT --OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, --SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT --LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, --DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY --THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT --(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE --OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +- return diags +-} - ---- github.com/sergi/go-diff LICENSE -- +-const FixCategory = "fillstruct" // recognized by gopls ApplyFix - --Copyright (c) 2012-2016 The go-diff Authors. All rights reserved. +-// SuggestedFix computes the suggested fix for the kinds of +-// diagnostics produced by the Analyzer above. +-func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, file *ast.File, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { +- if info == nil { +- return nil, nil, fmt.Errorf("nil types.Info") +- } - --Permission is hereby granted, free of charge, to any person obtaining a --copy of this software and associated documentation files (the "Software"), --to deal in the Software without restriction, including without limitation --the rights to use, copy, modify, merge, publish, distribute, sublicense, --and/or sell copies of the Software, and to permit persons to whom the --Software is furnished to do so, subject to the following conditions: +- pos := start // don't use the end - --The above copyright notice and this permission notice shall be included --in all copies or substantial portions of the Software. +- // TODO(rstambler): Using ast.Inspect would probably be more efficient than +- // calling PathEnclosingInterval. Switch this approach. +- path, _ := astutil.PathEnclosingInterval(file, pos, pos) +- if len(path) == 0 { +- return nil, nil, fmt.Errorf("no enclosing ast.Node") +- } +- var expr *ast.CompositeLit +- for _, n := range path { +- if node, ok := n.(*ast.CompositeLit); ok { +- expr = node +- break +- } +- } - --THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS --OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, --FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE --AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER --LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING --FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER --DEALINGS IN THE SOFTWARE. +- typ := info.TypeOf(expr) +- if typ == nil { +- return nil, nil, fmt.Errorf("no composite literal") +- } - +- // Find reference to the type declaration of the struct being initialized. +- typ = typeparams.Deref(typ) +- tStruct, ok := typ.Underlying().(*types.Struct) +- if !ok { +- return nil, nil, fmt.Errorf("%s is not a (pointer to) struct type", +- types.TypeString(typ, types.RelativeTo(pkg))) +- } +- // Inv: typ is the possibly-named struct type. - ---- honnef.co/go/tools LICENSE -- +- fieldCount := tStruct.NumFields() - --Copyright (c) 2016 Dominik Honnef +- // Check which types have already been filled in. (we only want to fill in +- // the unfilled types, or else we'll blat user-supplied details) +- prefilledFields := map[string]ast.Expr{} +- for _, e := range expr.Elts { +- if kv, ok := e.(*ast.KeyValueExpr); ok { +- if key, ok := kv.Key.(*ast.Ident); ok { +- prefilledFields[key.Name] = kv.Value +- } +- } +- } - --Permission is hereby granted, free of charge, to any person obtaining --a copy of this software and associated documentation files (the --"Software"), to deal in the Software without restriction, including --without limitation the rights to use, copy, modify, merge, publish, --distribute, sublicense, and/or sell copies of the Software, and to --permit persons to whom the Software is furnished to do so, subject to --the following conditions: +- // Use a new fileset to build up a token.File for the new composite +- // literal. We need one line for foo{, one line for }, and one line for +- // each field we're going to set. format.Node only cares about line +- // numbers, so we don't need to set columns, and each line can be +- // 1 byte long. +- // TODO(adonovan): why is this necessary? The position information +- // is going to be wrong for the existing trees in prefilledFields. +- // Can't the formatter just do its best with an empty fileset? +- fakeFset := token.NewFileSet() +- tok := fakeFset.AddFile("", -1, fieldCount+2) - --The above copyright notice and this permission notice shall be --included in all copies or substantial portions of the Software. +- line := 2 // account for 1-based lines and the left brace +- var fieldTyps []types.Type +- for i := 0; i < fieldCount; i++ { +- field := tStruct.Field(i) +- // Ignore fields that are not accessible in the current package. +- if field.Pkg() != nil && field.Pkg() != pkg && !field.Exported() { +- fieldTyps = append(fieldTyps, nil) +- continue +- } +- fieldTyps = append(fieldTyps, field.Type()) +- } +- matches := analysisinternal.MatchingIdents(fieldTyps, file, start, info, pkg) +- var elts []ast.Expr +- for i, fieldTyp := range fieldTyps { +- if fieldTyp == nil { +- continue // TODO(adonovan): is this reachable? +- } +- fieldName := tStruct.Field(i).Name() - --THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, --EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF --MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND --NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE --LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION --OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION --WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +- tok.AddLine(line - 1) // add 1 byte per line +- if line > tok.LineCount() { +- panic(fmt.Sprintf("invalid line number %v (of %v) for fillstruct", line, tok.LineCount())) +- } +- pos := tok.LineStart(line) - ---- mvdan.cc/gofumpt LICENSE -- +- kv := &ast.KeyValueExpr{ +- Key: &ast.Ident{ +- NamePos: pos, +- Name: fieldName, +- }, +- Colon: pos, +- } +- if expr, ok := prefilledFields[fieldName]; ok { +- kv.Value = expr +- } else { +- names, ok := matches[fieldTyp] +- if !ok { +- return nil, nil, fmt.Errorf("invalid struct field type: %v", fieldTyp) +- } - --Copyright (c) 2019, Daniel Martí. All rights reserved. +- // Find the name most similar to the field name. +- // If no name matches the pattern, generate a zero value. +- // NOTE: We currently match on the name of the field key rather than the field type. +- if best := fuzzy.BestMatch(fieldName, names); best != "" { +- kv.Value = ast.NewIdent(best) +- } else if v := populateValue(file, pkg, fieldTyp); v != nil { +- kv.Value = v +- } else { +- return nil, nil, nil // no fix to suggest +- } +- } +- elts = append(elts, kv) +- line++ +- } - --Redistribution and use in source and binary forms, with or without --modification, are permitted provided that the following conditions are --met: +- // If all of the struct's fields are unexported, we have nothing to do. +- if len(elts) == 0 { +- return nil, nil, fmt.Errorf("no elements to fill") +- } - -- * Redistributions of source code must retain the above copyright --notice, this list of conditions and the following disclaimer. -- * Redistributions in binary form must reproduce the above --copyright notice, this list of conditions and the following disclaimer --in the documentation and/or other materials provided with the --distribution. -- * Neither the name of the copyright holder nor the names of its --contributors may be used to endorse or promote products derived from --this software without specific prior written permission. +- // Add the final line for the right brace. Offset is the number of +- // bytes already added plus 1. +- tok.AddLine(len(elts) + 1) +- line = len(elts) + 2 +- if line > tok.LineCount() { +- panic(fmt.Sprintf("invalid line number %v (of %v) for fillstruct", line, tok.LineCount())) +- } - --THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS --"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT --LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR --A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT --OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, --SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT --LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, --DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY --THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT --(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE --OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +- cl := &ast.CompositeLit{ +- Type: expr.Type, +- Lbrace: tok.LineStart(1), +- Elts: elts, +- Rbrace: tok.LineStart(line), +- } - ---- mvdan.cc/xurls/v2 LICENSE -- +- // Find the line on which the composite literal is declared. +- split := bytes.Split(content, []byte("\n")) +- lineNumber := safetoken.StartPosition(fset, expr.Lbrace).Line +- firstLine := split[lineNumber-1] // lines are 1-indexed - --Copyright (c) 2015, Daniel Martí. All rights reserved. +- // Trim the whitespace from the left of the line, and use the index +- // to get the amount of whitespace on the left. +- trimmed := bytes.TrimLeftFunc(firstLine, unicode.IsSpace) +- index := bytes.Index(firstLine, trimmed) +- whitespace := firstLine[:index] - --Redistribution and use in source and binary forms, with or without --modification, are permitted provided that the following conditions are --met: +- // First pass through the formatter: turn the expr into a string. +- var formatBuf bytes.Buffer +- if err := format.Node(&formatBuf, fakeFset, cl); err != nil { +- return nil, nil, fmt.Errorf("failed to run first format on:\n%s\ngot err: %v", cl.Type, err) +- } +- sug := indent(formatBuf.Bytes(), whitespace) - -- * Redistributions of source code must retain the above copyright --notice, this list of conditions and the following disclaimer. -- * Redistributions in binary form must reproduce the above --copyright notice, this list of conditions and the following disclaimer --in the documentation and/or other materials provided with the --distribution. -- * Neither the name of the copyright holder nor the names of its --contributors may be used to endorse or promote products derived from --this software without specific prior written permission. +- if len(prefilledFields) > 0 { +- // Attempt a second pass through the formatter to line up columns. +- sourced, err := format.Source(sug) +- if err == nil { +- sug = indent(sourced, whitespace) +- } +- } - --THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS --"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT --LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR --A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT --OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, --SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT --LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, --DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY --THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT --(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE --OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +- return fset, &analysis.SuggestedFix{ +- TextEdits: []analysis.TextEdit{ +- { +- Pos: expr.Pos(), +- End: expr.End(), +- NewText: sug, +- }, +- }, +- }, nil +-} - --` -diff -urN a/gopls/internal/hooks/licenses_test.go b/gopls/internal/hooks/licenses_test.go ---- a/gopls/internal/hooks/licenses_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/hooks/licenses_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,47 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-// indent works line by line through str, indenting (prefixing) each line with +-// ind. +-func indent(str, ind []byte) []byte { +- split := bytes.Split(str, []byte("\n")) +- newText := bytes.NewBuffer(nil) +- for i, s := range split { +- if len(s) == 0 { +- continue +- } +- // Don't add the extra indentation to the first line. +- if i != 0 { +- newText.Write(ind) +- } +- newText.Write(s) +- if i < len(split)-1 { +- newText.WriteByte('\n') +- } +- } +- return newText.Bytes() +-} - --package hooks +-// populateValue constructs an expression to fill the value of a struct field. +-// +-// When the type of a struct field is a basic literal or interface, we return +-// default values. For other types, such as maps, slices, and channels, we create +-// empty expressions such as []T{} or make(chan T) rather than using default values. +-// +-// The reasoning here is that users will call fillstruct with the intention of +-// initializing the struct, in which case setting these fields to nil has no effect. +-// +-// populateValue returns nil if the value cannot be filled. +-func populateValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { +- switch u := typ.Underlying().(type) { +- case *types.Basic: +- switch { +- case u.Info()&types.IsNumeric != 0: +- return &ast.BasicLit{Kind: token.INT, Value: "0"} +- case u.Info()&types.IsBoolean != 0: +- return &ast.Ident{Name: "false"} +- case u.Info()&types.IsString != 0: +- return &ast.BasicLit{Kind: token.STRING, Value: `""`} +- case u.Kind() == types.UnsafePointer: +- return ast.NewIdent("nil") +- case u.Kind() == types.Invalid: +- return nil +- default: +- panic(fmt.Sprintf("unknown basic type %v", u)) +- } - --import ( -- "bytes" -- "os" -- "os/exec" -- "runtime" -- "testing" +- case *types.Map: +- k := analysisinternal.TypeExpr(f, pkg, u.Key()) +- v := analysisinternal.TypeExpr(f, pkg, u.Elem()) +- if k == nil || v == nil { +- return nil +- } +- return &ast.CompositeLit{ +- Type: &ast.MapType{ +- Key: k, +- Value: v, +- }, +- } +- case *types.Slice: +- s := analysisinternal.TypeExpr(f, pkg, u.Elem()) +- if s == nil { +- return nil +- } +- return &ast.CompositeLit{ +- Type: &ast.ArrayType{ +- Elt: s, +- }, +- } - -- "golang.org/x/tools/internal/testenv" --) +- case *types.Array: +- a := analysisinternal.TypeExpr(f, pkg, u.Elem()) +- if a == nil { +- return nil +- } +- return &ast.CompositeLit{ +- Type: &ast.ArrayType{ +- Elt: a, +- Len: &ast.BasicLit{ +- Kind: token.INT, Value: fmt.Sprintf("%v", u.Len()), +- }, +- }, +- } - --func TestLicenses(t *testing.T) { -- // License text differs for older Go versions because staticcheck or gofumpt -- // isn't supported for those versions, and this fails for unknown, unrelated -- // reasons on Kokoro legacy CI. -- testenv.NeedsGo1Point(t, 21) +- case *types.Chan: +- v := analysisinternal.TypeExpr(f, pkg, u.Elem()) +- if v == nil { +- return nil +- } +- dir := ast.ChanDir(u.Dir()) +- if u.Dir() == types.SendRecv { +- dir = ast.SEND | ast.RECV +- } +- return &ast.CallExpr{ +- Fun: ast.NewIdent("make"), +- Args: []ast.Expr{ +- &ast.ChanType{ +- Dir: dir, +- Value: v, +- }, +- }, +- } - -- if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { -- t.Skip("generating licenses only works on Unixes") -- } -- tmp, err := os.CreateTemp("", "") -- if err != nil { -- t.Fatal(err) -- } -- tmp.Close() +- case *types.Struct: +- s := analysisinternal.TypeExpr(f, pkg, typ) +- if s == nil { +- return nil +- } +- return &ast.CompositeLit{ +- Type: s, +- } - -- if out, err := exec.Command("./gen-licenses.sh", tmp.Name()).CombinedOutput(); err != nil { -- t.Fatalf("generating licenses failed: %q, %v", out, err) -- } +- case *types.Signature: +- var params []*ast.Field +- for i := 0; i < u.Params().Len(); i++ { +- p := analysisinternal.TypeExpr(f, pkg, u.Params().At(i).Type()) +- if p == nil { +- return nil +- } +- params = append(params, &ast.Field{ +- Type: p, +- Names: []*ast.Ident{ +- { +- Name: u.Params().At(i).Name(), +- }, +- }, +- }) +- } +- var returns []*ast.Field +- for i := 0; i < u.Results().Len(); i++ { +- r := analysisinternal.TypeExpr(f, pkg, u.Results().At(i).Type()) +- if r == nil { +- return nil +- } +- returns = append(returns, &ast.Field{ +- Type: r, +- }) +- } +- return &ast.FuncLit{ +- Type: &ast.FuncType{ +- Params: &ast.FieldList{ +- List: params, +- }, +- Results: &ast.FieldList{ +- List: returns, +- }, +- }, +- Body: &ast.BlockStmt{}, +- } - -- got, err := os.ReadFile(tmp.Name()) -- if err != nil { -- t.Fatal(err) -- } -- want, err := os.ReadFile("licenses.go") -- if err != nil { -- t.Fatal(err) -- } -- if !bytes.Equal(got, want) { -- t.Error("combined license text needs updating. Run: `go generate ./internal/hooks` from the gopls module.") +- case *types.Pointer: +- switch aliases.Unalias(u.Elem()).(type) { +- case *types.Basic: +- return &ast.CallExpr{ +- Fun: &ast.Ident{ +- Name: "new", +- }, +- Args: []ast.Expr{ +- &ast.Ident{ +- Name: u.Elem().String(), +- }, +- }, +- } +- default: +- x := populateValue(f, pkg, u.Elem()) +- if x == nil { +- return nil +- } +- return &ast.UnaryExpr{ +- Op: token.AND, +- X: x, +- } +- } +- +- case *types.Interface: +- if param, ok := aliases.Unalias(typ).(*types.TypeParam); ok { +- // *new(T) is the zero value of a type parameter T. +- // TODO(adonovan): one could give a more specific zero +- // value if the type has a core type that is, say, +- // always a number or a pointer. See go/ssa for details. +- return &ast.StarExpr{ +- X: &ast.CallExpr{ +- Fun: ast.NewIdent("new"), +- Args: []ast.Expr{ +- ast.NewIdent(param.Obj().Name()), +- }, +- }, +- } +- } +- +- return ast.NewIdent("nil") - } +- return nil -} -diff -urN a/gopls/internal/lsp/analysis/deprecated/deprecated.go b/gopls/internal/lsp/analysis/deprecated/deprecated.go ---- a/gopls/internal/lsp/analysis/deprecated/deprecated.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/deprecated/deprecated.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,270 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/analysis/fillstruct/fillstruct_test.go b/gopls/internal/analysis/fillstruct/fillstruct_test.go +--- a/gopls/internal/analysis/fillstruct/fillstruct_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/fillstruct/fillstruct_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,38 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --// Package deprecated defines an Analyzer that marks deprecated symbols and package imports. --package deprecated +-package fillstruct_test - -import ( -- "bytes" -- "go/ast" -- "go/format" - "go/token" -- "go/types" -- "strconv" -- "strings" +- "testing" - - "golang.org/x/tools/go/analysis" +- "golang.org/x/tools/go/analysis/analysistest" - "golang.org/x/tools/go/analysis/passes/inspect" - "golang.org/x/tools/go/ast/inspector" -- "golang.org/x/tools/internal/typeparams" +- "golang.org/x/tools/gopls/internal/analysis/fillstruct" -) - --// TODO(hyangah): use analysisutil.MustExtractDoc. --var doc = `check for use of deprecated identifiers -- --The deprecated analyzer looks for deprecated symbols and package imports. -- --See https://go.dev/wiki/Deprecated to learn about Go's convention --for documenting and signaling deprecated identifiers.` -- --var Analyzer = &analysis.Analyzer{ -- Name: "deprecated", -- Doc: doc, -- Requires: []*analysis.Analyzer{inspect.Analyzer}, -- Run: checkDeprecated, -- FactTypes: []analysis.Fact{(*deprecationFact)(nil)}, +-// analyzer allows us to test the fillstruct code action using the analysistest +-// harness. (fillstruct used to be a gopls analyzer.) +-var analyzer = &analysis.Analyzer{ +- Name: "fillstruct", +- Doc: "test only", +- Requires: []*analysis.Analyzer{inspect.Analyzer}, +- Run: func(pass *analysis.Pass) (any, error) { +- inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) +- for _, d := range fillstruct.Diagnose(inspect, token.NoPos, token.NoPos, pass.Pkg, pass.TypesInfo) { +- pass.Report(d) +- } +- return nil, nil +- }, +- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/fillstruct", - RunDespiteErrors: true, -} - --// checkDeprecated is a simplified copy of staticcheck.CheckDeprecated. --func checkDeprecated(pass *analysis.Pass) (interface{}, error) { -- inspector := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) -- -- deprs, err := collectDeprecatedNames(pass, inspector) -- if err != nil || (len(deprs.packages) == 0 && len(deprs.objects) == 0) { -- return nil, err -- } +-func Test(t *testing.T) { +- testdata := analysistest.TestData() +- analysistest.Run(t, testdata, analyzer, "a", "typeparams") +-} +diff -urN a/gopls/internal/analysis/fillstruct/testdata/src/a/a.go b/gopls/internal/analysis/fillstruct/testdata/src/a/a.go +--- a/gopls/internal/analysis/fillstruct/testdata/src/a/a.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/fillstruct/testdata/src/a/a.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,112 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- reportDeprecation := func(depr *deprecationFact, node ast.Node) { -- // TODO(hyangah): staticcheck.CheckDeprecated has more complex logic. Do we need it here? -- // TODO(hyangah): Scrub depr.Msg. depr.Msg may contain Go comments -- // markdown syntaxes but LSP diagnostics do not support markdown syntax. +-package fillstruct - -- buf := new(bytes.Buffer) -- if err := format.Node(buf, pass.Fset, node); err != nil { -- // This shouldn't happen but let's be conservative. -- buf.Reset() -- buf.WriteString("declaration") -- } -- pass.ReportRangef(node, "%s is deprecated: %s", buf, depr.Msg) -- } +-import ( +- data "b" +- "go/ast" +- "go/token" +- "unsafe" +-) - -- nodeFilter := []ast.Node{(*ast.SelectorExpr)(nil)} -- inspector.Preorder(nodeFilter, func(node ast.Node) { -- // Caveat: this misses dot-imported objects -- sel, ok := node.(*ast.SelectorExpr) -- if !ok { -- return -- } +-type emptyStruct struct{} - -- obj := pass.TypesInfo.ObjectOf(sel.Sel) -- if obj_, ok := obj.(*types.Func); ok { -- obj = typeparams.OriginMethod(obj_) -- } -- if obj == nil || obj.Pkg() == nil { -- // skip invalid sel.Sel. -- return -- } +-var _ = emptyStruct{} - -- if obj.Pkg() == pass.Pkg { -- // A package is allowed to use its own deprecated objects -- return -- } +-type basicStruct struct { +- foo int +-} - -- // A package "foo" has two related packages "foo_test" and "foo.test", for external tests and the package main -- // generated by 'go test' respectively. "foo_test" can import and use "foo", "foo.test" imports and uses "foo" -- // and "foo_test". +-var _ = basicStruct{} // want `basicStruct literal has missing fields` - -- if strings.TrimSuffix(pass.Pkg.Path(), "_test") == obj.Pkg().Path() { -- // foo_test (the external tests of foo) can use objects from foo. -- return -- } -- if strings.TrimSuffix(pass.Pkg.Path(), ".test") == obj.Pkg().Path() { -- // foo.test (the main package of foo's tests) can use objects from foo. -- return -- } -- if strings.TrimSuffix(pass.Pkg.Path(), ".test") == strings.TrimSuffix(obj.Pkg().Path(), "_test") { -- // foo.test (the main package of foo's tests) can use objects from foo's external tests. -- return -- } +-type twoArgStruct struct { +- foo int +- bar string +-} - -- if depr, ok := deprs.objects[obj]; ok { -- reportDeprecation(depr, sel) -- } -- }) +-var _ = twoArgStruct{} // want `twoArgStruct literal has missing fields` - -- for _, f := range pass.Files { -- for _, spec := range f.Imports { -- var imp *types.Package -- var obj types.Object -- if spec.Name != nil { -- obj = pass.TypesInfo.ObjectOf(spec.Name) -- } else { -- obj = pass.TypesInfo.Implicits[spec] -- } -- pkgName, ok := obj.(*types.PkgName) -- if !ok { -- continue -- } -- imp = pkgName.Imported() +-var _ = twoArgStruct{ // want `twoArgStruct literal has missing fields` +- bar: "bar", +-} - -- path, err := strconv.Unquote(spec.Path.Value) -- if err != nil { -- continue -- } -- pkgPath := pass.Pkg.Path() -- if strings.TrimSuffix(pkgPath, "_test") == path { -- // foo_test can import foo -- continue -- } -- if strings.TrimSuffix(pkgPath, ".test") == path { -- // foo.test can import foo -- continue -- } -- if strings.TrimSuffix(pkgPath, ".test") == strings.TrimSuffix(path, "_test") { -- // foo.test can import foo_test -- continue -- } -- if depr, ok := deprs.packages[imp]; ok { -- reportDeprecation(depr, spec.Path) -- } -- } -- } -- return nil, nil +-type nestedStruct struct { +- bar string +- basic basicStruct -} - --type deprecationFact struct{ Msg string } +-var _ = nestedStruct{} // want `nestedStruct literal has missing fields` - --func (*deprecationFact) AFact() {} --func (d *deprecationFact) String() string { return "Deprecated: " + d.Msg } +-var _ = data.B{} // want `b.B literal has missing fields` - --type deprecatedNames struct { -- objects map[types.Object]*deprecationFact -- packages map[*types.Package]*deprecationFact +-type typedStruct struct { +- m map[string]int +- s []int +- c chan int +- c1 <-chan int +- a [2]string -} - --// collectDeprecatedNames collects deprecated identifiers and publishes --// them both as Facts and the return value. This is a simplified copy --// of staticcheck's fact_deprecated analyzer. --func collectDeprecatedNames(pass *analysis.Pass, ins *inspector.Inspector) (deprecatedNames, error) { -- extractDeprecatedMessage := func(docs []*ast.CommentGroup) string { -- for _, doc := range docs { -- if doc == nil { -- continue -- } -- parts := strings.Split(doc.Text(), "\n\n") -- for _, part := range parts { -- if !strings.HasPrefix(part, "Deprecated: ") { -- continue -- } -- alt := part[len("Deprecated: "):] -- alt = strings.Replace(alt, "\n", " ", -1) -- return strings.TrimSpace(alt) -- } -- } -- return "" -- } +-var _ = typedStruct{} // want `typedStruct literal has missing fields` - -- doDocs := func(names []*ast.Ident, docs *ast.CommentGroup) { -- alt := extractDeprecatedMessage([]*ast.CommentGroup{docs}) -- if alt == "" { -- return -- } +-type funStruct struct { +- fn func(i int) int +-} - -- for _, name := range names { -- obj := pass.TypesInfo.ObjectOf(name) -- pass.ExportObjectFact(obj, &deprecationFact{alt}) -- } -- } +-var _ = funStruct{} // want `funStruct literal has missing fields` - -- var docs []*ast.CommentGroup -- for _, f := range pass.Files { -- docs = append(docs, f.Doc) -- } -- if alt := extractDeprecatedMessage(docs); alt != "" { -- // Don't mark package syscall as deprecated, even though -- // it is. A lot of people still use it for simple -- // constants like SIGKILL, and I am not comfortable -- // telling them to use x/sys for that. -- if pass.Pkg.Path() != "syscall" { -- pass.ExportPackageFact(&deprecationFact{alt}) -- } -- } -- nodeFilter := []ast.Node{ -- (*ast.GenDecl)(nil), -- (*ast.FuncDecl)(nil), -- (*ast.TypeSpec)(nil), -- (*ast.ValueSpec)(nil), -- (*ast.File)(nil), -- (*ast.StructType)(nil), -- (*ast.InterfaceType)(nil), -- } -- ins.Preorder(nodeFilter, func(node ast.Node) { -- var names []*ast.Ident -- var docs *ast.CommentGroup -- switch node := node.(type) { -- case *ast.GenDecl: -- switch node.Tok { -- case token.TYPE, token.CONST, token.VAR: -- docs = node.Doc -- for i := range node.Specs { -- switch n := node.Specs[i].(type) { -- case *ast.ValueSpec: -- names = append(names, n.Names...) -- case *ast.TypeSpec: -- names = append(names, n.Name) -- } -- } -- default: -- return -- } -- case *ast.FuncDecl: -- docs = node.Doc -- names = []*ast.Ident{node.Name} -- case *ast.TypeSpec: -- docs = node.Doc -- names = []*ast.Ident{node.Name} -- case *ast.ValueSpec: -- docs = node.Doc -- names = node.Names -- case *ast.StructType: -- for _, field := range node.Fields.List { -- doDocs(field.Names, field.Doc) -- } -- case *ast.InterfaceType: -- for _, field := range node.Methods.List { -- doDocs(field.Names, field.Doc) -- } -- } -- if docs != nil && len(names) > 0 { -- doDocs(names, docs) -- } -- }) +-type funStructComplex struct { +- fn func(i int, s string) (string, int) +-} - -- // Every identifier is potentially deprecated, so we will need -- // to look up facts a lot. Construct maps of all facts propagated -- // to this pass for fast lookup. -- out := deprecatedNames{ -- objects: map[types.Object]*deprecationFact{}, -- packages: map[*types.Package]*deprecationFact{}, -- } -- for _, fact := range pass.AllObjectFacts() { -- out.objects[fact.Object] = fact.Fact.(*deprecationFact) -- } -- for _, fact := range pass.AllPackageFacts() { -- out.packages[fact.Package] = fact.Fact.(*deprecationFact) -- } +-var _ = funStructComplex{} // want `funStructComplex literal has missing fields` - -- return out, nil +-type funStructEmpty struct { +- fn func() -} -diff -urN a/gopls/internal/lsp/analysis/deprecated/deprecated_test.go b/gopls/internal/lsp/analysis/deprecated/deprecated_test.go ---- a/gopls/internal/lsp/analysis/deprecated/deprecated_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/deprecated/deprecated_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,18 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package deprecated +-var _ = funStructEmpty{} // want `funStructEmpty literal has missing fields` - --import ( -- "testing" +-type Foo struct { +- A int +-} - -- "golang.org/x/tools/go/analysis/analysistest" -- "golang.org/x/tools/internal/testenv" --) +-type Bar struct { +- X *Foo +- Y *Foo +-} - --func Test(t *testing.T) { -- testenv.NeedsGo1Point(t, 19) -- testdata := analysistest.TestData() -- analysistest.Run(t, testdata, Analyzer, "a") +-var _ = Bar{} // want `Bar literal has missing fields` +- +-type importedStruct struct { +- m map[*ast.CompositeLit]ast.Field +- s []ast.BadExpr +- a [3]token.Token +- c chan ast.EmptyStmt +- fn func(ast_decl ast.DeclStmt) ast.Ellipsis +- st ast.CompositeLit -} -diff -urN a/gopls/internal/lsp/analysis/deprecated/testdata/src/a/a.go b/gopls/internal/lsp/analysis/deprecated/testdata/src/a/a.go ---- a/gopls/internal/lsp/analysis/deprecated/testdata/src/a/a.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/deprecated/testdata/src/a/a.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,17 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package usedeprecated +-var _ = importedStruct{} // want `importedStruct literal has missing fields` - --import "io/ioutil" // want "\"io/ioutil\" is deprecated: .*" +-type pointerBuiltinStruct struct { +- b *bool +- s *string +- i *int +-} - --func x() { -- _, _ = ioutil.ReadFile("") // want "ioutil.ReadFile is deprecated: As of Go 1.16, .*" -- Legacy() // expect no deprecation notice. +-var _ = pointerBuiltinStruct{} // want `pointerBuiltinStruct literal has missing fields` +- +-var _ = []ast.BasicLit{ +- {}, // want `go/ast.BasicLit literal has missing fields` -} - --// Legacy is deprecated. --// --// Deprecated: use X instead. --func Legacy() {} // want Legacy:"Deprecated: use X instead." -diff -urN a/gopls/internal/lsp/analysis/deprecated/testdata/src/a/a_test.go b/gopls/internal/lsp/analysis/deprecated/testdata/src/a/a_test.go ---- a/gopls/internal/lsp/analysis/deprecated/testdata/src/a/a_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/deprecated/testdata/src/a/a_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,12 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. +-var _ = []ast.BasicLit{{}} // want "go/ast.BasicLit literal has missing fields" +- +-type unsafeStruct struct { +- foo unsafe.Pointer +-} +- +-var _ = unsafeStruct{} // want `unsafeStruct literal has missing fields` +diff -urN a/gopls/internal/analysis/fillstruct/testdata/src/b/b.go b/gopls/internal/analysis/fillstruct/testdata/src/b/b.go +--- a/gopls/internal/analysis/fillstruct/testdata/src/b/b.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/fillstruct/testdata/src/b/b.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,6 +0,0 @@ +-package fillstruct +- +-type B struct { +- ExportedInt int +- unexportedInt int +-} +diff -urN a/gopls/internal/analysis/fillstruct/testdata/src/typeparams/typeparams.go b/gopls/internal/analysis/fillstruct/testdata/src/typeparams/typeparams.go +--- a/gopls/internal/analysis/fillstruct/testdata/src/typeparams/typeparams.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/fillstruct/testdata/src/typeparams/typeparams.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,54 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package usedeprecated +-package fillstruct - --import "testing" +-type emptyStruct[A any] struct{} - --func TestF(t *testing.T) { -- Legacy() // expect no deprecation notice. -- x() +-var _ = emptyStruct[int]{} +- +-type basicStruct[T any] struct { +- foo T -} -diff -urN a/gopls/internal/lsp/analysis/embeddirective/embeddirective.go b/gopls/internal/lsp/analysis/embeddirective/embeddirective.go ---- a/gopls/internal/lsp/analysis/embeddirective/embeddirective.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/embeddirective/embeddirective.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,162 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. +- +-var _ = basicStruct[int]{} // want `basicStruct\[int\] literal has missing fields` +- +-type twoArgStruct[F, B any] struct { +- foo F +- bar B +-} +- +-var _ = twoArgStruct[string, int]{} // want `twoArgStruct\[string, int\] literal has missing fields` +- +-var _ = twoArgStruct[int, string]{ // want `twoArgStruct\[int, string\] literal has missing fields` +- bar: "bar", +-} +- +-type nestedStruct struct { +- bar string +- basic basicStruct[int] +-} +- +-var _ = nestedStruct{} // want "nestedStruct literal has missing fields" +- +-func _[T any]() { +- type S struct{ t T } +- x := S{} // want "S" +- _ = x +-} +- +-func Test() { +- var tests = []struct { +- a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p string +- }{ +- {}, // want "anonymous struct{ a: string, b: string, c: string, ... } literal has missing fields" +- } +- for _, test := range tests { +- _ = test +- } +-} +- +-func _[T twoArgStruct[int, int]]() { +- _ = T{} // want "T literal has missing fields" +-} +diff -urN a/gopls/internal/analysis/fillswitch/doc.go b/gopls/internal/analysis/fillswitch/doc.go +--- a/gopls/internal/analysis/fillswitch/doc.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/fillswitch/doc.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,66 +0,0 @@ +-// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --// Package embeddirective defines an Analyzer that validates //go:embed directives. --// The analyzer defers fixes to its parent source.Analyzer. --package embeddirective +-// Package fillswitch identifies switches with missing cases. +-// +-// It reports a diagnostic for each type switch or 'enum' switch that +-// has missing cases, and suggests a fix to fill them in. +-// +-// The possible cases are: for a type switch, each accessible named +-// type T or pointer *T that is assignable to the interface type; and +-// for an 'enum' switch, each accessible named constant of the same +-// type as the switch value. +-// +-// For an 'enum' switch, it will suggest cases for all possible values of the +-// type. +-// +-// type Suit int8 +-// const ( +-// Spades Suit = iota +-// Hearts +-// Diamonds +-// Clubs +-// ) +-// +-// var s Suit +-// switch s { +-// case Spades: +-// } +-// +-// It will report a diagnostic with a suggested fix to fill in the remaining +-// cases: +-// +-// var s Suit +-// switch s { +-// case Spades: +-// case Hearts: +-// case Diamonds: +-// case Clubs: +-// default: +-// panic(fmt.Sprintf("unexpected Suit: %v", s)) +-// } +-// +-// For a type switch, it will suggest cases for all types that implement the +-// interface. +-// +-// var stmt ast.Stmt +-// switch stmt.(type) { +-// case *ast.IfStmt: +-// } +-// +-// It will report a diagnostic with a suggested fix to fill in the remaining +-// cases: +-// +-// var stmt ast.Stmt +-// switch stmt.(type) { +-// case *ast.IfStmt: +-// case *ast.ForStmt: +-// case *ast.RangeStmt: +-// case *ast.AssignStmt: +-// case *ast.GoStmt: +-// ... +-// default: +-// panic(fmt.Sprintf("unexpected ast.Stmt: %T", stmt)) +-// } +-package fillswitch +diff -urN a/gopls/internal/analysis/fillswitch/fillswitch.go b/gopls/internal/analysis/fillswitch/fillswitch.go +--- a/gopls/internal/analysis/fillswitch/fillswitch.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/fillswitch/fillswitch.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,301 +0,0 @@ +-// Copyright 2024 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package fillswitch - -import ( +- "bytes" +- "fmt" - "go/ast" - "go/token" - "go/types" -- "strings" - - "golang.org/x/tools/go/analysis" +- "golang.org/x/tools/go/ast/inspector" -) - --const Doc = `check //go:embed directive usage +-// Diagnose computes diagnostics for switch statements with missing cases +-// overlapping with the provided start and end position. +-// +-// If either start or end is invalid, the entire package is inspected. +-func Diagnose(inspect *inspector.Inspector, start, end token.Pos, pkg *types.Package, info *types.Info) []analysis.Diagnostic { +- var diags []analysis.Diagnostic +- nodeFilter := []ast.Node{(*ast.SwitchStmt)(nil), (*ast.TypeSwitchStmt)(nil)} +- inspect.Preorder(nodeFilter, func(n ast.Node) { +- if start.IsValid() && n.End() < start || +- end.IsValid() && n.Pos() > end { +- return // non-overlapping +- } +- +- var fix *analysis.SuggestedFix +- switch n := n.(type) { +- case *ast.SwitchStmt: +- fix = suggestedFixSwitch(n, pkg, info) +- case *ast.TypeSwitchStmt: +- fix = suggestedFixTypeSwitch(n, pkg, info) +- } - --This analyzer checks that the embed package is imported if //go:embed --directives are present, providing a suggested fix to add the import if --it is missing. +- if fix == nil { +- return +- } - --This analyzer also checks that //go:embed directives precede the --declaration of a single variable.` +- diags = append(diags, analysis.Diagnostic{ +- Message: fix.Message, +- Pos: n.Pos(), +- End: n.Pos() + token.Pos(len("switch")), +- SuggestedFixes: []analysis.SuggestedFix{*fix}, +- }) +- }) - --var Analyzer = &analysis.Analyzer{ -- Name: "embed", -- Doc: Doc, -- Requires: []*analysis.Analyzer{}, -- Run: run, -- RunDespiteErrors: true, +- return diags -} - --// source.fixedByImportingEmbed relies on this message to filter --// out fixable diagnostics from this Analyzer. --const MissingImportMessage = `must import "embed" when using go:embed directives` +-func suggestedFixTypeSwitch(stmt *ast.TypeSwitchStmt, pkg *types.Package, info *types.Info) *analysis.SuggestedFix { +- if hasDefaultCase(stmt.Body) { +- return nil +- } - --func run(pass *analysis.Pass) (interface{}, error) { -- for _, f := range pass.Files { -- comments := embedDirectiveComments(f) -- if len(comments) == 0 { -- continue // nothing to check +- namedType := namedTypeFromTypeSwitch(stmt, info) +- if namedType == nil { +- return nil +- } +- +- existingCases := caseTypes(stmt.Body, info) +- // Gather accessible package-level concrete types +- // that implement the switch interface type. +- scope := namedType.Obj().Pkg().Scope() +- var buf bytes.Buffer +- for _, name := range scope.Names() { +- obj := scope.Lookup(name) +- if tname, ok := obj.(*types.TypeName); !ok || tname.IsAlias() { +- continue // not a defined type - } - -- hasEmbedImport := false -- for _, imp := range f.Imports { -- if imp.Path.Value == `"embed"` { -- hasEmbedImport = true -- break -- } +- if types.IsInterface(obj.Type()) { +- continue - } - -- for _, c := range comments { -- report := func(msg string) { -- pass.Report(analysis.Diagnostic{ -- Pos: c.Pos(), -- End: c.Pos() + token.Pos(len("//go:embed")), -- Message: msg, -- }) +- samePkg := obj.Pkg() == pkg +- if !samePkg && !obj.Exported() { +- continue // inaccessible +- } +- +- var key caseType +- if types.AssignableTo(obj.Type(), namedType.Obj().Type()) { +- key.named = obj.Type().(*types.Named) +- } else if ptr := types.NewPointer(obj.Type()); types.AssignableTo(ptr, namedType.Obj().Type()) { +- key.named = obj.Type().(*types.Named) +- key.ptr = true +- } +- +- if key.named != nil { +- if existingCases[key] { +- continue - } - -- if !hasEmbedImport { -- report(MissingImportMessage) +- if buf.Len() > 0 { +- buf.WriteString("\t") - } - -- spec := nextVarSpec(c, f) -- switch { -- case spec == nil: -- report(`go:embed directives must precede a "var" declaration`) -- case len(spec.Names) != 1: -- report("declarations following go:embed directives must define a single variable") -- case len(spec.Values) > 0: -- report("declarations following go:embed directives must not specify a value") -- case !embeddableType(pass.TypesInfo.Defs[spec.Names[0]]): -- report("declarations following go:embed directives must be of type string, []byte or embed.FS") +- buf.WriteString("case ") +- if key.ptr { +- buf.WriteByte('*') - } -- } -- } -- return nil, nil --} - --// embedDirectiveComments returns all comments in f that contains a //go:embed directive. --func embedDirectiveComments(f *ast.File) []*ast.Comment { -- comments := []*ast.Comment{} -- for _, cg := range f.Comments { -- for _, c := range cg.List { -- if strings.HasPrefix(c.Text, "//go:embed ") { -- comments = append(comments, c) +- if p := key.named.Obj().Pkg(); p != pkg { +- // TODO: use the correct package name when the import is renamed +- buf.WriteString(p.Name()) +- buf.WriteByte('.') - } +- buf.WriteString(key.named.Obj().Name()) +- buf.WriteString(":\n") - } - } -- return comments --} - --// nextVarSpec returns the ValueSpec for the variable declaration immediately following --// the go:embed comment, or nil if the next declaration is not a variable declaration. --func nextVarSpec(com *ast.Comment, f *ast.File) *ast.ValueSpec { -- // Embed directives must be followed by a declaration of one variable with no value. -- // There may be comments and empty lines between the directive and the declaration. -- var nextDecl ast.Decl -- for _, d := range f.Decls { -- if com.End() < d.End() { -- nextDecl = d -- break +- if buf.Len() == 0 { +- return nil +- } +- +- switch assign := stmt.Assign.(type) { +- case *ast.AssignStmt: +- addDefaultCase(&buf, namedType, assign.Lhs[0]) +- case *ast.ExprStmt: +- if assert, ok := assign.X.(*ast.TypeAssertExpr); ok { +- addDefaultCase(&buf, namedType, assert.X) - } - } -- if nextDecl == nil || nextDecl.Pos() == token.NoPos { -- return nil +- +- return &analysis.SuggestedFix{ +- Message: fmt.Sprintf("Add cases for %s", namedType.Obj().Name()), +- TextEdits: []analysis.TextEdit{{ +- Pos: stmt.End() - token.Pos(len("}")), +- End: stmt.End() - token.Pos(len("}")), +- NewText: buf.Bytes(), +- }}, - } -- decl, ok := nextDecl.(*ast.GenDecl) -- if !ok { +-} +- +-func suggestedFixSwitch(stmt *ast.SwitchStmt, pkg *types.Package, info *types.Info) *analysis.SuggestedFix { +- if hasDefaultCase(stmt.Body) { - return nil - } -- if decl.Tok != token.VAR { +- +- namedType, ok := info.TypeOf(stmt.Tag).(*types.Named) +- if !ok { - return nil - } - -- // var declarations can be both freestanding and blocks (with parenthesis). -- // Only the first variable spec following the directive is interesting. -- var nextSpec ast.Spec -- for _, s := range decl.Specs { -- if com.End() < s.End() { -- nextSpec = s -- break +- existingCases := caseConsts(stmt.Body, info) +- // Gather accessible named constants of the same type as the switch value. +- scope := namedType.Obj().Pkg().Scope() +- var buf bytes.Buffer +- for _, name := range scope.Names() { +- obj := scope.Lookup(name) +- if c, ok := obj.(*types.Const); ok && +- (obj.Pkg() == pkg || obj.Exported()) && // accessible +- types.Identical(obj.Type(), namedType.Obj().Type()) && +- !existingCases[c] { +- +- if buf.Len() > 0 { +- buf.WriteString("\t") +- } +- +- buf.WriteString("case ") +- if c.Pkg() != pkg { +- buf.WriteString(c.Pkg().Name()) +- buf.WriteByte('.') +- } +- buf.WriteString(c.Name()) +- buf.WriteString(":\n") - } - } -- if nextSpec == nil { -- return nil -- } -- spec, ok := nextSpec.(*ast.ValueSpec) -- if !ok { -- // Invalid AST, but keep going. +- +- if buf.Len() == 0 { - return nil - } -- return spec --} - --// embeddableType in go:embed directives are string, []byte or embed.FS. --func embeddableType(o types.Object) bool { -- if o == nil { -- return false +- addDefaultCase(&buf, namedType, stmt.Tag) +- +- return &analysis.SuggestedFix{ +- Message: fmt.Sprintf("Add cases for %s", namedType.Obj().Name()), +- TextEdits: []analysis.TextEdit{{ +- Pos: stmt.End() - token.Pos(len("}")), +- End: stmt.End() - token.Pos(len("}")), +- NewText: buf.Bytes(), +- }}, - } +-} - -- // For embed.FS the underlying type is an implementation detail. -- // As long as the named type resolves to embed.FS, it is OK. -- if named, ok := o.Type().(*types.Named); ok { -- obj := named.Obj() -- if obj.Pkg() != nil && obj.Pkg().Path() == "embed" && obj.Name() == "FS" { +-func addDefaultCase(buf *bytes.Buffer, named *types.Named, expr ast.Expr) { +- var dottedBuf bytes.Buffer +- // writeDotted emits a dotted path a.b.c. +- var writeDotted func(e ast.Expr) bool +- writeDotted = func(e ast.Expr) bool { +- switch e := e.(type) { +- case *ast.SelectorExpr: +- if !writeDotted(e.X) { +- return false +- } +- dottedBuf.WriteByte('.') +- dottedBuf.WriteString(e.Sel.Name) +- return true +- case *ast.Ident: +- dottedBuf.WriteString(e.Name) - return true - } +- return false - } - -- switch v := o.Type().Underlying().(type) { -- case *types.Basic: -- return types.Identical(v, types.Typ[types.String]) -- case *types.Slice: -- return types.Identical(v.Elem(), types.Typ[types.Byte]) +- buf.WriteString("\tdefault:\n") +- typeName := fmt.Sprintf("%s.%s", named.Obj().Pkg().Name(), named.Obj().Name()) +- if writeDotted(expr) { +- // Switch tag expression is a dotted path. +- // It is safe to re-evaluate it in the default case. +- format := fmt.Sprintf("unexpected %s: %%#v", typeName) +- fmt.Fprintf(buf, "\t\tpanic(fmt.Sprintf(%q, %s))\n\t", format, dottedBuf.String()) +- } else { +- // Emit simpler message, without re-evaluating tag expression. +- fmt.Fprintf(buf, "\t\tpanic(%q)\n\t", "unexpected "+typeName) - } +-} - -- return false +-func namedTypeFromTypeSwitch(stmt *ast.TypeSwitchStmt, info *types.Info) *types.Named { +- switch assign := stmt.Assign.(type) { +- case *ast.ExprStmt: +- if typ, ok := assign.X.(*ast.TypeAssertExpr); ok { +- if named, ok := info.TypeOf(typ.X).(*types.Named); ok { +- return named +- } +- } +- +- case *ast.AssignStmt: +- if typ, ok := assign.Rhs[0].(*ast.TypeAssertExpr); ok { +- if named, ok := info.TypeOf(typ.X).(*types.Named); ok { +- return named +- } +- } +- } +- +- return nil -} -diff -urN a/gopls/internal/lsp/analysis/embeddirective/embeddirective_test.go b/gopls/internal/lsp/analysis/embeddirective/embeddirective_test.go ---- a/gopls/internal/lsp/analysis/embeddirective/embeddirective_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/embeddirective/embeddirective_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,22 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package embeddirective +-func hasDefaultCase(body *ast.BlockStmt) bool { +- for _, clause := range body.List { +- if len(clause.(*ast.CaseClause).List) == 0 { +- return true +- } +- } - --import ( -- "testing" +- return false +-} - -- "golang.org/x/tools/go/analysis/analysistest" -- "golang.org/x/tools/internal/typeparams" --) +-func caseConsts(body *ast.BlockStmt, info *types.Info) map[*types.Const]bool { +- out := map[*types.Const]bool{} +- for _, stmt := range body.List { +- for _, e := range stmt.(*ast.CaseClause).List { +- if info.Types[e].Value == nil { +- continue // not a constant +- } - --func Test(t *testing.T) { -- testdata := analysistest.TestData() -- tests := []string{"a"} -- if typeparams.Enabled { -- tests = append(tests) +- if sel, ok := e.(*ast.SelectorExpr); ok { +- e = sel.Sel // replace pkg.C with C +- } +- +- if e, ok := e.(*ast.Ident); ok { +- if c, ok := info.Uses[e].(*types.Const); ok { +- out[c] = true +- } +- } +- } - } - -- analysistest.RunWithSuggestedFixes(t, testdata, Analyzer, tests...) +- return out -} -diff -urN a/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/embedText b/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/embedText ---- a/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/embedText 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/embedText 1970-01-01 00:00:00.000000000 +0000 -@@ -1 +0,0 @@ --Hello World -\ No newline at end of file -diff -urN a/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/import_missing.go b/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/import_missing.go ---- a/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/import_missing.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/import_missing.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,17 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package a +-type caseType struct { +- named *types.Named +- ptr bool +-} - --import ( -- "fmt" --) +-func caseTypes(body *ast.BlockStmt, info *types.Info) map[caseType]bool { +- out := map[caseType]bool{} +- for _, stmt := range body.List { +- for _, e := range stmt.(*ast.CaseClause).List { +- if tv, ok := info.Types[e]; ok && tv.IsType() { +- t := tv.Type +- ptr := false +- if p, ok := t.(*types.Pointer); ok { +- t = p.Elem() +- ptr = true +- } - --//go:embed embedtext // want "must import \"embed\" when using go:embed directives" --var s string +- if named, ok := t.(*types.Named); ok { +- out[caseType{named, ptr}] = true +- } +- } +- } +- } - --// This is main function --func main() { -- fmt.Println(s) +- return out -} -diff -urN a/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/import_present.go b/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/import_present.go ---- a/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/import_present.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/import_present.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,129 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/analysis/fillswitch/fillswitch_test.go b/gopls/internal/analysis/fillswitch/fillswitch_test.go +--- a/gopls/internal/analysis/fillswitch/fillswitch_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/fillswitch/fillswitch_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,38 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package a -- --// Misplaced, above imports. --//go:embed embedText // want "go:embed directives must precede a \"var\" declaration" +-package fillswitch_test - -import ( -- "embed" -- embedPkg "embed" -- "fmt" +- "go/token" +- "testing" - -- _ "embed" +- "golang.org/x/tools/go/analysis" +- "golang.org/x/tools/go/analysis/analysistest" +- "golang.org/x/tools/go/analysis/passes/inspect" +- "golang.org/x/tools/go/ast/inspector" +- "golang.org/x/tools/gopls/internal/analysis/fillswitch" -) - --//go:embed embedText // ok --var e1 string -- --// The analyzer does not check for many directives using the same var. --// --//go:embed embedText // ok --//go:embed embedText // ok --var e2 string -- --// Comments and blank lines between are OK. All types OK. --// --//go:embed embedText // ok --// --// foo -- --var e3 string -- --//go:embed embedText //ok --var e4 []byte -- --//go:embed embedText //ok --var e5 embed.FS -- --// Followed by wrong kind of decl. --// --//go:embed embedText // want "go:embed directives must precede a \"var\" declaration" --func fooFunc() {} +-// analyzer allows us to test the fillswitch code action using the analysistest +-// harness. +-var analyzer = &analysis.Analyzer{ +- Name: "fillswitch", +- Doc: "test only", +- Requires: []*analysis.Analyzer{inspect.Analyzer}, +- Run: func(pass *analysis.Pass) (any, error) { +- inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) +- for _, d := range fillswitch.Diagnose(inspect, token.NoPos, token.NoPos, pass.Pkg, pass.TypesInfo) { +- pass.Report(d) +- } +- return nil, nil +- }, +- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/fillswitch", +- RunDespiteErrors: true, +-} - --// Multiple variable specs. --// --//go:embed embedText // want "declarations following go:embed directives must define a single variable" --var e6, e7 []byte +-func Test(t *testing.T) { +- testdata := analysistest.TestData() +- analysistest.Run(t, testdata, analyzer, "a") +-} +diff -urN a/gopls/internal/analysis/fillswitch/testdata/src/a/a.go b/gopls/internal/analysis/fillswitch/testdata/src/a/a.go +--- a/gopls/internal/analysis/fillswitch/testdata/src/a/a.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/fillswitch/testdata/src/a/a.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,78 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// Specifying a value is not allowed. --// --//go:embed embedText // want "declarations following go:embed directives must not specify a value" --var e8 string = "foo" +-package fillswitch - --// TODO: This should not be OK, misplaced according to compiler. --// --//go:embed embedText // ok --var ( -- e9 string -- e10 string +-import ( +- data "b" -) - --// Type definition. --type fooType []byte -- --//go:embed embedText //ok --var e11 fooType -- --// Type alias. --type barType = string -- --//go:embed embedText //ok --var e12 barType +-type typeA int - --// Renamed embed package. +-const ( +- typeAOne typeA = iota +- typeATwo +- typeAThree +-) - --//go:embed embedText //ok --var e13 embedPkg.FS +-func doSwitch() { +- var a typeA +- switch a { // want `Add cases for typeA` +- } - --// Renamed embed package alias. --type embedAlias = embedPkg.FS +- switch a { // want `Add cases for typeA` +- case typeAOne: +- } - --//go:embed embedText //ok --var e14 embedAlias +- switch a { +- case typeAOne: +- default: +- } - --// var blocks are OK as long as the variable following the directive is OK. --var ( -- x, y, z string -- //go:embed embedText // ok -- e20 string -- q, r, t string --) +- switch a { +- case typeAOne: +- case typeATwo: +- case typeAThree: +- } - --//go:embed embedText // want "go:embed directives must precede a \"var\" declaration" --var () +- var b data.TypeB +- switch b { // want `Add cases for TypeB` +- case data.TypeBOne: +- } +-} - --// Incorrect types. +-type notification interface { +- isNotification() +-} - --//go:embed embedText // want `declarations following go:embed directives must be of type string, \[\]byte or embed.FS` --var e16 byte +-type notificationOne struct{} - --//go:embed embedText // want `declarations following go:embed directives must be of type string, \[\]byte or embed.FS` --var e17 []string +-func (notificationOne) isNotification() {} - --//go:embed embedText // want `declarations following go:embed directives must be of type string, \[\]byte or embed.FS` --var e18 embed.Foo +-type notificationTwo struct{} - --//go:embed embedText // want `declarations following go:embed directives must be of type string, \[\]byte or embed.FS` --var e19 foo.FS +-func (notificationTwo) isNotification() {} - --type byteAlias byte +-func doTypeSwitch() { +- var not notification +- switch not.(type) { // want `Add cases for notification` +- } - --//go:embed embedText // want `declarations following go:embed directives must be of type string, \[\]byte or embed.FS` --var e15 byteAlias +- switch not.(type) { // want `Add cases for notification` +- case notificationOne: +- } - --// A type declaration of embed.FS is not accepted by the compiler, in contrast to an alias. --type embedDecl embed.FS +- switch not.(type) { +- case notificationOne: +- case notificationTwo: +- } - --//go:embed embedText // want `declarations following go:embed directives must be of type string, \[\]byte or embed.FS` --var e16 embedDecl +- switch not.(type) { +- default: +- } - --// This is main function --func main() { -- fmt.Println(s) +- var t data.ExportedInterface +- switch t { +- } -} -- --// No declaration following. --//go:embed embedText // want "go:embed directives must precede a \"var\" declaration" -diff -urN a/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/import_present_go120.go b/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/import_present_go120.go ---- a/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/import_present_go120.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/embeddirective/testdata/src/a/import_present_go120.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,26 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/analysis/fillswitch/testdata/src/b/b.go b/gopls/internal/analysis/fillswitch/testdata/src/b/b.go +--- a/gopls/internal/analysis/fillswitch/testdata/src/b/b.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/fillswitch/testdata/src/b/b.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,21 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --//go:build go1.20 --// +build go1.20 +-package fillswitch - --package a +-type TypeB int - --var ( -- // Okay directive wise but the compiler will complain that -- // imports must appear before other declarations. -- //go:embed embedText // ok -- foo string +-const ( +- TypeBOne TypeB = iota +- TypeBTwo +- TypeBThree -) - --import ( -- "fmt" +-type ExportedInterface interface { +- isExportedInterface() +-} - -- _ "embed" --) +-type notExportedType struct{} - --// This is main function --func main() { -- fmt.Println(s) --} -diff -urN a/gopls/internal/lsp/analysis/fillreturns/fillreturns.go b/gopls/internal/lsp/analysis/fillreturns/fillreturns.go ---- a/gopls/internal/lsp/analysis/fillreturns/fillreturns.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/fillreturns/fillreturns.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,279 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +-func (notExportedType) isExportedInterface() {} +diff -urN a/gopls/internal/analysis/infertypeargs/infertypeargs.go b/gopls/internal/analysis/infertypeargs/infertypeargs.go +--- a/gopls/internal/analysis/infertypeargs/infertypeargs.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/infertypeargs/infertypeargs.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,149 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --// Package fillreturns defines an Analyzer that will attempt to --// automatically fill in a return statement that has missing --// values with zero value elements. --package fillreturns +-package infertypeargs - -import ( -- "bytes" -- "fmt" - "go/ast" -- "go/format" +- "go/token" - "go/types" -- "regexp" -- "strings" - - "golang.org/x/tools/go/analysis" -- "golang.org/x/tools/go/ast/astutil" -- "golang.org/x/tools/internal/analysisinternal" -- "golang.org/x/tools/internal/fuzzy" +- "golang.org/x/tools/go/analysis/passes/inspect" +- "golang.org/x/tools/go/ast/inspector" - "golang.org/x/tools/internal/typeparams" +- "golang.org/x/tools/internal/versions" -) - --const Doc = `suggest fixes for errors due to an incorrect number of return values +-const Doc = `check for unnecessary type arguments in call expressions - --This checker provides suggested fixes for type errors of the --type "wrong number of return values (want %d, got %d)". For example: -- func m() (int, string, *bool, error) { -- return -- } --will turn into -- func m() (int, string, *bool, error) { -- return 0, "", nil, nil -- } +-Explicit type arguments may be omitted from call expressions if they can be +-inferred from function arguments, or from other type arguments: - --This functionality is similar to https://github.com/sqs/goreturns. +- func f[T any](T) {} +- +- func _() { +- f[string]("foo") // string could be inferred +- } -` - -var Analyzer = &analysis.Analyzer{ -- Name: "fillreturns", -- Doc: Doc, -- Requires: []*analysis.Analyzer{}, -- Run: run, -- RunDespiteErrors: true, +- Name: "infertypeargs", +- Doc: Doc, +- Requires: []*analysis.Analyzer{inspect.Analyzer}, +- Run: run, +- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/infertypeargs", -} - --func run(pass *analysis.Pass) (interface{}, error) { -- info := pass.TypesInfo -- if info == nil { -- return nil, fmt.Errorf("nil TypeInfo") +-func run(pass *analysis.Pass) (any, error) { +- inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) +- for _, diag := range diagnose(pass.Fset, inspect, token.NoPos, token.NoPos, pass.Pkg, pass.TypesInfo) { +- pass.Report(diag) - } +- return nil, nil +-} - --outer: -- for _, typeErr := range pass.TypeErrors { -- // Filter out the errors that are not relevant to this analyzer. -- if !FixesError(typeErr) { -- continue -- } -- var file *ast.File -- for _, f := range pass.Files { -- if f.Pos() <= typeErr.Pos && typeErr.Pos <= f.End() { -- file = f -- break -- } -- } -- if file == nil { -- continue -- } +-// Diagnose reports diagnostics describing simplifications to type +-// arguments overlapping with the provided start and end position. +-// +-// If start or end is token.NoPos, the corresponding bound is not checked +-// (i.e. if both start and end are NoPos, all call expressions are considered). +-func diagnose(fset *token.FileSet, inspect *inspector.Inspector, start, end token.Pos, pkg *types.Package, info *types.Info) []analysis.Diagnostic { +- var diags []analysis.Diagnostic - -- // Get the end position of the error. -- // (This heuristic assumes that the buffer is formatted, -- // at least up to the end position of the error.) -- var buf bytes.Buffer -- if err := format.Node(&buf, pass.Fset, file); err != nil { -- continue +- nodeFilter := []ast.Node{(*ast.CallExpr)(nil)} +- inspect.Preorder(nodeFilter, func(node ast.Node) { +- call := node.(*ast.CallExpr) +- x, lbrack, indices, rbrack := typeparams.UnpackIndexExpr(call.Fun) +- ident := calledIdent(x) +- if ident == nil || len(indices) == 0 { +- return // no explicit args, nothing to do - } -- typeErrEndPos := analysisinternal.TypeErrorEndPos(pass.Fset, buf.Bytes(), typeErr.Pos) -- -- // TODO(rfindley): much of the error handling code below returns, when it -- // should probably continue. - -- // Get the path for the relevant range. -- path, _ := astutil.PathEnclosingInterval(file, typeErr.Pos, typeErrEndPos) -- if len(path) == 0 { -- return nil, nil +- if (start.IsValid() && call.End() < start) || (end.IsValid() && call.Pos() > end) { +- return // non-overlapping - } - -- // Find the enclosing return statement. -- var ret *ast.ReturnStmt -- var retIdx int -- for i, n := range path { -- if r, ok := n.(*ast.ReturnStmt); ok { -- ret = r -- retIdx = i -- break -- } -- } -- if ret == nil { -- return nil, nil +- // Confirm that instantiation actually occurred at this ident. +- idata, ok := info.Instances[ident] +- if !ok { +- return // something went wrong, but fail open - } +- instance := idata.Type - -- // Get the function type that encloses the ReturnStmt. -- var enclosingFunc *ast.FuncType -- for _, n := range path[retIdx+1:] { -- switch node := n.(type) { -- case *ast.FuncLit: -- enclosingFunc = node.Type -- case *ast.FuncDecl: -- enclosingFunc = node.Type +- // Start removing argument expressions from the right, and check if we can +- // still infer the call expression. +- required := len(indices) // number of type expressions that are required +- for i := len(indices) - 1; i >= 0; i-- { +- var fun ast.Expr +- if i == 0 { +- // No longer an index expression: just use the parameterized operand. +- fun = x +- } else { +- fun = typeparams.PackIndexExpr(x, lbrack, indices[:i], indices[i-1].End()) - } -- if enclosingFunc != nil { +- newCall := &ast.CallExpr{ +- Fun: fun, +- Lparen: call.Lparen, +- Args: call.Args, +- Ellipsis: call.Ellipsis, +- Rparen: call.Rparen, +- } +- info := &types.Info{ +- Instances: make(map[*ast.Ident]types.Instance), +- } +- versions.InitFileVersions(info) +- if err := types.CheckExpr(fset, pkg, call.Pos(), newCall, info); err != nil { +- // Most likely inference failed. - break - } -- } -- if enclosingFunc == nil || enclosingFunc.Results == nil { -- continue -- } -- -- // Skip any generic enclosing functions, since type parameters don't -- // have 0 values. -- // TODO(rfindley): We should be able to handle this if the return -- // values are all concrete types. -- if tparams := typeparams.ForFuncType(enclosingFunc); tparams != nil && tparams.NumFields() > 0 { -- return nil, nil -- } -- -- // Find the function declaration that encloses the ReturnStmt. -- var outer *ast.FuncDecl -- for _, p := range path { -- if p, ok := p.(*ast.FuncDecl); ok { -- outer = p +- newIData := info.Instances[ident] +- newInstance := newIData.Type +- if !types.Identical(instance, newInstance) { +- // The inferred result type does not match the original result type, so +- // this simplification is not valid. - break - } +- required = i - } -- if outer == nil { -- return nil, nil -- } -- -- // Skip any return statements that contain function calls with multiple -- // return values. -- for _, expr := range ret.Results { -- e, ok := expr.(*ast.CallExpr) -- if !ok { -- continue +- if required < len(indices) { +- var s, e token.Pos +- var edit analysis.TextEdit +- if required == 0 { +- s, e = lbrack, rbrack+1 // erase the entire index +- edit = analysis.TextEdit{Pos: s, End: e} +- } else { +- s = indices[required].Pos() +- e = rbrack +- // erase from end of last arg to include last comma & white-spaces +- edit = analysis.TextEdit{Pos: indices[required-1].End(), End: e} - } -- if tup, ok := info.TypeOf(e).(*types.Tuple); ok && tup.Len() > 1 { -- continue outer +- // Recheck that our (narrower) fixes overlap with the requested range. +- if (start.IsValid() && e < start) || (end.IsValid() && s > end) { +- return // non-overlapping - } +- diags = append(diags, analysis.Diagnostic{ +- Pos: s, +- End: e, +- Message: "unnecessary type arguments", +- SuggestedFixes: []analysis.SuggestedFix{{ +- Message: "Simplify type arguments", +- TextEdits: []analysis.TextEdit{edit}, +- }}, +- }) - } +- }) - -- // Duplicate the return values to track which values have been matched. -- remaining := make([]ast.Expr, len(ret.Results)) -- copy(remaining, ret.Results) +- return diags +-} - -- fixed := make([]ast.Expr, len(enclosingFunc.Results.List)) +-func calledIdent(x ast.Expr) *ast.Ident { +- switch x := x.(type) { +- case *ast.Ident: +- return x +- case *ast.SelectorExpr: +- return x.Sel +- } +- return nil +-} +diff -urN a/gopls/internal/analysis/infertypeargs/infertypeargs_test.go b/gopls/internal/analysis/infertypeargs/infertypeargs_test.go +--- a/gopls/internal/analysis/infertypeargs/infertypeargs_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/infertypeargs/infertypeargs_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,17 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- // For each value in the return function declaration, find the leftmost element -- // in the return statement that has the desired type. If no such element exists, -- // fill in the missing value with the appropriate "zero" value. -- // Beware that type information may be incomplete. -- var retTyps []types.Type -- for _, ret := range enclosingFunc.Results.List { -- retTyp := info.TypeOf(ret.Type) -- if retTyp == nil { -- return nil, nil -- } -- retTyps = append(retTyps, retTyp) -- } -- matches := analysisinternal.MatchingIdents(retTyps, file, ret.Pos(), info, pass.Pkg) -- for i, retTyp := range retTyps { -- var match ast.Expr -- var idx int -- for j, val := range remaining { -- if t := info.TypeOf(val); t == nil || !matchingTypes(t, retTyp) { -- continue -- } -- if !analysisinternal.IsZeroValue(val) { -- match, idx = val, j -- break -- } -- // If the current match is a "zero" value, we keep searching in -- // case we find a non-"zero" value match. If we do not find a -- // non-"zero" value, we will use the "zero" value. -- match, idx = val, j -- } -- -- if match != nil { -- fixed[i] = match -- remaining = append(remaining[:idx], remaining[idx+1:]...) -- } else { -- names, ok := matches[retTyp] -- if !ok { -- return nil, fmt.Errorf("invalid return type: %v", retTyp) -- } -- // Find the identifier most similar to the return type. -- // If no identifier matches the pattern, generate a zero value. -- if best := fuzzy.BestMatch(retTyp.String(), names); best != "" { -- fixed[i] = ast.NewIdent(best) -- } else if zero := analysisinternal.ZeroValue(file, pass.Pkg, retTyp); zero != nil { -- fixed[i] = zero -- } else { -- return nil, nil -- } -- } -- } -- -- // Remove any non-matching "zero values" from the leftover values. -- var nonZeroRemaining []ast.Expr -- for _, expr := range remaining { -- if !analysisinternal.IsZeroValue(expr) { -- nonZeroRemaining = append(nonZeroRemaining, expr) -- } -- } -- // Append leftover return values to end of new return statement. -- fixed = append(fixed, nonZeroRemaining...) -- -- newRet := &ast.ReturnStmt{ -- Return: ret.Pos(), -- Results: fixed, -- } -- -- // Convert the new return statement AST to text. -- var newBuf bytes.Buffer -- if err := format.Node(&newBuf, pass.Fset, newRet); err != nil { -- return nil, err -- } -- -- pass.Report(analysis.Diagnostic{ -- Pos: typeErr.Pos, -- End: typeErrEndPos, -- Message: typeErr.Msg, -- SuggestedFixes: []analysis.SuggestedFix{{ -- Message: "Fill in return values", -- TextEdits: []analysis.TextEdit{{ -- Pos: ret.Pos(), -- End: ret.End(), -- NewText: newBuf.Bytes(), -- }}, -- }}, -- }) -- } -- return nil, nil --} -- --func matchingTypes(want, got types.Type) bool { -- if want == got || types.Identical(want, got) { -- return true -- } -- // Code segment to help check for untyped equality from (golang/go#32146). -- if rhs, ok := want.(*types.Basic); ok && rhs.Info()&types.IsUntyped > 0 { -- if lhs, ok := got.Underlying().(*types.Basic); ok { -- return rhs.Info()&types.IsConstType == lhs.Info()&types.IsConstType -- } -- } -- return types.AssignableTo(want, got) || types.ConvertibleTo(want, got) --} -- --// Error messages have changed across Go versions. These regexps capture recent --// incarnations. --// --// TODO(rfindley): once error codes are exported and exposed via go/packages, --// use error codes rather than string matching here. --var wrongReturnNumRegexes = []*regexp.Regexp{ -- regexp.MustCompile(`wrong number of return values \(want (\d+), got (\d+)\)`), -- regexp.MustCompile(`too many return values`), -- regexp.MustCompile(`not enough return values`), --} -- --func FixesError(err types.Error) bool { -- msg := strings.TrimSpace(err.Msg) -- for _, rx := range wrongReturnNumRegexes { -- if rx.MatchString(msg) { -- return true -- } -- } -- return false --} -diff -urN a/gopls/internal/lsp/analysis/fillreturns/fillreturns_test.go b/gopls/internal/lsp/analysis/fillreturns/fillreturns_test.go ---- a/gopls/internal/lsp/analysis/fillreturns/fillreturns_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/fillreturns/fillreturns_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,22 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package fillreturns_test +-package infertypeargs_test - -import ( - "testing" - - "golang.org/x/tools/go/analysis/analysistest" -- "golang.org/x/tools/gopls/internal/lsp/analysis/fillreturns" -- "golang.org/x/tools/internal/typeparams" +- "golang.org/x/tools/gopls/internal/analysis/infertypeargs" -) - -func Test(t *testing.T) { - testdata := analysistest.TestData() -- tests := []string{"a"} -- if typeparams.Enabled { -- tests = append(tests, "typeparams") -- } -- analysistest.RunWithSuggestedFixes(t, testdata, fillreturns.Analyzer, tests...) +- analysistest.RunWithSuggestedFixes(t, testdata, infertypeargs.Analyzer, "a") -} -diff -urN a/gopls/internal/lsp/analysis/fillreturns/testdata/src/a/a.go b/gopls/internal/lsp/analysis/fillreturns/testdata/src/a/a.go ---- a/gopls/internal/lsp/analysis/fillreturns/testdata/src/a/a.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/fillreturns/testdata/src/a/a.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,139 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/analysis/infertypeargs/testdata/src/a/basic.go b/gopls/internal/analysis/infertypeargs/testdata/src/a/basic.go +--- a/gopls/internal/analysis/infertypeargs/testdata/src/a/basic.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/infertypeargs/testdata/src/a/basic.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,20 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package fillreturns -- --import ( -- "errors" -- "go/ast" -- ast2 "go/ast" -- "io" -- "net/http" -- . "net/http" -- "net/url" -- "strconv" --) -- --type T struct{} --type T1 = T --type I interface{} --type I1 = I --type z func(string, http.Handler) error -- --func x() error { -- return errors.New("foo") --} -- --// The error messages below changed in 1.18; "return values" covers both forms. -- --func b() (string, int, error) { -- return "", errors.New("foo") // want "return values" --} -- --func c() (string, int, error) { -- return 7, errors.New("foo") // want "return values" --} -- --func d() (string, int, error) { -- return "", 7 // want "return values" --} -- --func e() (T, error, *bool) { -- return (z(http.ListenAndServe))("", nil) // want "return values" --} -- --func preserveLeft() (int, int, error) { -- return 1, errors.New("foo") // want "return values" --} -- --func matchValues() (int, error, string) { -- return errors.New("foo"), 3 // want "return values" --} -- --func preventDataOverwrite() (int, string) { -- return errors.New("foo") // want "return values" --} -- --func closure() (string, error) { -- _ = func() (int, error) { -- return // want "return values" -- } -- return // want "return values" --} -- --func basic() (uint8, uint16, uint32, uint64, int8, int16, int32, int64, float32, float64, complex64, complex128, byte, rune, uint, int, uintptr, string, bool, error) { -- return // want "return values" --} -- --func complex() (*int, []int, [2]int, map[int]int) { -- return // want "return values" --} +-// This file contains tests for the infertyepargs checker. - --func structsAndInterfaces() (T, url.URL, T1, I, I1, io.Reader, Client, ast2.Stmt) { -- return // want "return values" --} +-package a - --func m() (int, error) { -- if 1 == 2 { -- return // want "return values" -- } else if 1 == 3 { -- return errors.New("foo") // want "return values" -- } else { -- return 1 // want "return values" -- } -- return // want "return values" --} +-func f[T any](T) {} - --func convertibleTypes() (ast2.Expr, int) { -- return &ast2.ArrayType{} // want "return values" --} +-func g[T any]() T { var x T; return x } - --func assignableTypes() (map[string]int, int) { -- type X map[string]int -- var x X -- return x // want "return values" --} +-func h[P interface{ ~*T }, T any]() {} - --func interfaceAndError() (I, int) { -- return errors.New("foo") // want "return values" +-func _() { +- f[string]("hello") // want "unnecessary type arguments" +- f[int](2) // want "unnecessary type arguments" +- _ = g[int]() +- h[*int, int]() // want "unnecessary type arguments" -} +diff -urN a/gopls/internal/analysis/infertypeargs/testdata/src/a/basic.go.golden b/gopls/internal/analysis/infertypeargs/testdata/src/a/basic.go.golden +--- a/gopls/internal/analysis/infertypeargs/testdata/src/a/basic.go.golden 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/infertypeargs/testdata/src/a/basic.go.golden 1970-01-01 00:00:00.000000000 +0000 +@@ -1,20 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func funcOneReturn() (string, error) { -- return strconv.Itoa(1) // want "return values" --} +-// This file contains tests for the infertyepargs checker. - --func funcMultipleReturn() (int, error, string) { -- return strconv.Atoi("1") --} +-package a - --func localFuncMultipleReturn() (string, int, error, string) { -- return b() --} +-func f[T any](T) {} - --func multipleUnused() (int, string, string, string) { -- return 3, 4, 5 // want "return values" --} +-func g[T any]() T { var x T; return x } - --func gotTooMany() int { -- if true { -- return 0, "" // want "return values" -- } else { -- return 1, 0, nil // want "return values" -- } -- return 0, 5, false // want "return values" --} +-func h[P interface{ ~*T }, T any]() {} - --func fillVars() (int, string, ast.Node, bool, error) { -- eint := 0 -- s := "a" -- var t bool -- if true { -- err := errors.New("fail") -- return // want "return values" -- } -- n := ast.NewIdent("ident") -- int := 3 -- var b bool -- return "" // want "return values" +-func _() { +- f("hello") // want "unnecessary type arguments" +- f(2) // want "unnecessary type arguments" +- _ = g[int]() +- h[*int]() // want "unnecessary type arguments" -} -diff -urN a/gopls/internal/lsp/analysis/fillreturns/testdata/src/a/a.go.golden b/gopls/internal/lsp/analysis/fillreturns/testdata/src/a/a.go.golden ---- a/gopls/internal/lsp/analysis/fillreturns/testdata/src/a/a.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/fillreturns/testdata/src/a/a.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,139 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/analysis/infertypeargs/testdata/src/a/imported/imported.go b/gopls/internal/analysis/infertypeargs/testdata/src/a/imported/imported.go +--- a/gopls/internal/analysis/infertypeargs/testdata/src/a/imported/imported.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/infertypeargs/testdata/src/a/imported/imported.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,7 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package fillreturns -- --import ( -- "errors" -- "go/ast" -- ast2 "go/ast" -- "io" -- "net/http" -- . "net/http" -- "net/url" -- "strconv" --) -- --type T struct{} --type T1 = T --type I interface{} --type I1 = I --type z func(string, http.Handler) error -- --func x() error { -- return errors.New("foo") --} -- --// The error messages below changed in 1.18; "return values" covers both forms. +-package imported - --func b() (string, int, error) { -- return "", 0, errors.New("foo") // want "return values" --} +-func F[T any](T) {} +diff -urN a/gopls/internal/analysis/infertypeargs/testdata/src/a/imported.go b/gopls/internal/analysis/infertypeargs/testdata/src/a/imported.go +--- a/gopls/internal/analysis/infertypeargs/testdata/src/a/imported.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/infertypeargs/testdata/src/a/imported.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,12 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func c() (string, int, error) { -- return "", 7, errors.New("foo") // want "return values" --} +-package a - --func d() (string, int, error) { -- return "", 7, nil // want "return values" --} +-import "a/imported" - --func e() (T, error, *bool) { -- return T{}, (z(http.ListenAndServe))("", nil), nil // want "return values" +-func _() { +- var x int +- imported.F[int](x) // want "unnecessary type arguments" -} +diff -urN a/gopls/internal/analysis/infertypeargs/testdata/src/a/imported.go.golden b/gopls/internal/analysis/infertypeargs/testdata/src/a/imported.go.golden +--- a/gopls/internal/analysis/infertypeargs/testdata/src/a/imported.go.golden 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/infertypeargs/testdata/src/a/imported.go.golden 1970-01-01 00:00:00.000000000 +0000 +@@ -1,12 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func preserveLeft() (int, int, error) { -- return 1, 0, errors.New("foo") // want "return values" --} +-package a - --func matchValues() (int, error, string) { -- return 3, errors.New("foo"), "" // want "return values" --} +-import "a/imported" - --func preventDataOverwrite() (int, string) { -- return 0, "", errors.New("foo") // want "return values" +-func _() { +- var x int +- imported.F(x) // want "unnecessary type arguments" -} +diff -urN a/gopls/internal/analysis/infertypeargs/testdata/src/a/notypechange.go b/gopls/internal/analysis/infertypeargs/testdata/src/a/notypechange.go +--- a/gopls/internal/analysis/infertypeargs/testdata/src/a/notypechange.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/infertypeargs/testdata/src/a/notypechange.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,26 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func closure() (string, error) { -- _ = func() (int, error) { -- return 0, nil // want "return values" -- } -- return "", nil // want "return values" --} +-// We should not suggest removing type arguments if doing so would change the +-// resulting type. - --func basic() (uint8, uint16, uint32, uint64, int8, int16, int32, int64, float32, float64, complex64, complex128, byte, rune, uint, int, uintptr, string, bool, error) { -- return 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, "", false, nil // want "return values" --} +-package a - --func complex() (*int, []int, [2]int, map[int]int) { -- return nil, nil, nil, nil // want "return values" --} +-func id[T any](t T) T { return t } - --func structsAndInterfaces() (T, url.URL, T1, I, I1, io.Reader, Client, ast2.Stmt) { -- return T{}, url.URL{}, T{}, nil, nil, nil, Client{}, nil // want "return values" --} +-var _ = id[int](1) // want "unnecessary type arguments" +-var _ = id[string]("foo") // want "unnecessary type arguments" +-var _ = id[int64](2) - --func m() (int, error) { -- if 1 == 2 { -- return 0, nil // want "return values" -- } else if 1 == 3 { -- return 0, errors.New("foo") // want "return values" -- } else { -- return 1, nil // want "return values" -- } -- return 0, nil // want "return values" --} +-func pair[T any](t T) (T, T) { return t, t } - --func convertibleTypes() (ast2.Expr, int) { -- return &ast2.ArrayType{}, 0 // want "return values" --} +-var _, _ = pair[int](3) // want "unnecessary type arguments" +-var _, _ = pair[int64](3) - --func assignableTypes() (map[string]int, int) { -- type X map[string]int -- var x X -- return x, 0 // want "return values" --} +-func noreturn[T any](t T) {} - --func interfaceAndError() (I, int) { -- return errors.New("foo"), 0 // want "return values" +-func _() { +- noreturn[int64](4) +- noreturn[int](4) // want "unnecessary type arguments" -} +diff -urN a/gopls/internal/analysis/infertypeargs/testdata/src/a/notypechange.go.golden b/gopls/internal/analysis/infertypeargs/testdata/src/a/notypechange.go.golden +--- a/gopls/internal/analysis/infertypeargs/testdata/src/a/notypechange.go.golden 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/infertypeargs/testdata/src/a/notypechange.go.golden 1970-01-01 00:00:00.000000000 +0000 +@@ -1,26 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func funcOneReturn() (string, error) { -- return strconv.Itoa(1), nil // want "return values" --} +-// We should not suggest removing type arguments if doing so would change the +-// resulting type. - --func funcMultipleReturn() (int, error, string) { -- return strconv.Atoi("1") --} +-package a - --func localFuncMultipleReturn() (string, int, error, string) { -- return b() --} +-func id[T any](t T) T { return t } - --func multipleUnused() (int, string, string, string) { -- return 3, "", "", "", 4, 5 // want "return values" --} +-var _ = id(1) // want "unnecessary type arguments" +-var _ = id("foo") // want "unnecessary type arguments" +-var _ = id[int64](2) - --func gotTooMany() int { -- if true { -- return 0 // want "return values" -- } else { -- return 1 // want "return values" -- } -- return 5 // want "return values" --} +-func pair[T any](t T) (T, T) { return t, t } - --func fillVars() (int, string, ast.Node, bool, error) { -- eint := 0 -- s := "a" -- var t bool -- if true { -- err := errors.New("fail") -- return eint, s, nil, false, err // want "return values" -- } -- n := ast.NewIdent("ident") -- int := 3 -- var b bool -- return int, "", n, b, nil // want "return values" --} -diff -urN a/gopls/internal/lsp/analysis/fillreturns/testdata/src/a/typeparams/a.go b/gopls/internal/lsp/analysis/fillreturns/testdata/src/a/typeparams/a.go ---- a/gopls/internal/lsp/analysis/fillreturns/testdata/src/a/typeparams/a.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/fillreturns/testdata/src/a/typeparams/a.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,5 +0,0 @@ --package fillreturns +-var _, _ = pair(3) // want "unnecessary type arguments" +-var _, _ = pair[int64](3) - --func hello[T any]() int { -- return --} -diff -urN a/gopls/internal/lsp/analysis/fillreturns/testdata/src/a/typeparams/a.go.golden b/gopls/internal/lsp/analysis/fillreturns/testdata/src/a/typeparams/a.go.golden ---- a/gopls/internal/lsp/analysis/fillreturns/testdata/src/a/typeparams/a.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/fillreturns/testdata/src/a/typeparams/a.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,5 +0,0 @@ --package fillreturns +-func noreturn[T any](t T) {} - --func hello[T any]() int { -- return +-func _() { +- noreturn[int64](4) +- noreturn(4) // want "unnecessary type arguments" -} -diff -urN a/gopls/internal/lsp/analysis/fillstruct/fillstruct.go b/gopls/internal/lsp/analysis/fillstruct/fillstruct.go ---- a/gopls/internal/lsp/analysis/fillstruct/fillstruct.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/fillstruct/fillstruct.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,515 +0,0 @@ +diff -urN a/gopls/internal/analysis/nonewvars/doc.go b/gopls/internal/analysis/nonewvars/doc.go +--- a/gopls/internal/analysis/nonewvars/doc.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/nonewvars/doc.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,22 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --// Package fillstruct defines an Analyzer that automatically --// fills in a struct declaration with zero value elements for each field. +-// Package nonewvars defines an Analyzer that applies suggested fixes +-// to errors of the type "no new variables on left side of :=". -// --// The analyzer's diagnostic is merely a prompt. --// The actual fix is created by a separate direct call from gopls to --// the SuggestedFixes function. --// Tests of Analyzer.Run can be found in ./testdata/src. --// Tests of the SuggestedFixes logic live in ../../testdata/fillstruct. --package fillstruct +-// # Analyzer nonewvars +-// +-// nonewvars: suggested fixes for "no new vars on left side of :=" +-// +-// This checker provides suggested fixes for type errors of the +-// type "no new vars on left side of :=". For example: +-// +-// z := 1 +-// z := 2 +-// +-// will turn into +-// +-// z := 1 +-// z = 2 +-package nonewvars +diff -urN a/gopls/internal/analysis/nonewvars/nonewvars.go b/gopls/internal/analysis/nonewvars/nonewvars.go +--- a/gopls/internal/analysis/nonewvars/nonewvars.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/nonewvars/nonewvars.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,89 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-// Package nonewvars defines an Analyzer that applies suggested fixes +-// to errors of the type "no new variables on left side of :=". +-package nonewvars - -import ( - "bytes" -- "fmt" +- _ "embed" - "go/ast" - "go/format" - "go/token" -- "go/types" -- "strings" -- "unicode" - - "golang.org/x/tools/go/analysis" - "golang.org/x/tools/go/analysis/passes/inspect" -- "golang.org/x/tools/go/ast/astutil" - "golang.org/x/tools/go/ast/inspector" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" - "golang.org/x/tools/internal/analysisinternal" -- "golang.org/x/tools/internal/fuzzy" -- "golang.org/x/tools/internal/typeparams" -) - --const Doc = `note incomplete struct initializations -- --This analyzer provides diagnostics for any struct literals that do not have --any fields initialized. Because the suggested fix for this analysis is --expensive to compute, callers should compute it separately, using the --SuggestedFix function below. --` +-//go:embed doc.go +-var doc string - -var Analyzer = &analysis.Analyzer{ -- Name: "fillstruct", -- Doc: Doc, +- Name: "nonewvars", +- Doc: analysisinternal.MustExtractDoc(doc, "nonewvars"), - Requires: []*analysis.Analyzer{inspect.Analyzer}, - Run: run, - RunDespiteErrors: true, +- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/nonewvars", -} - --// TODO(rfindley): remove this thin wrapper around the fillstruct refactoring, --// and eliminate the fillstruct analyzer. --// --// Previous iterations used the analysis framework for computing refactorings, --// which proved inefficient. -func run(pass *analysis.Pass) (interface{}, error) { - inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) -- for _, d := range DiagnoseFillableStructs(inspect, token.NoPos, token.NoPos, pass.Pkg, pass.TypesInfo) { -- pass.Report(d) +- if len(pass.TypeErrors) == 0 { +- return nil, nil - } -- return nil, nil --} - --// DiagnoseFillableStructs computes diagnostics for fillable struct composite --// literals overlapping with the provided start and end position. --// --// If either start or end is invalid, it is considered an unbounded condition. --func DiagnoseFillableStructs(inspect *inspector.Inspector, start, end token.Pos, pkg *types.Package, info *types.Info) []analysis.Diagnostic { -- var diags []analysis.Diagnostic -- nodeFilter := []ast.Node{(*ast.CompositeLit)(nil)} +- nodeFilter := []ast.Node{(*ast.AssignStmt)(nil)} - inspect.Preorder(nodeFilter, func(n ast.Node) { -- expr := n.(*ast.CompositeLit) -- -- if (start.IsValid() && expr.End() < start) || (end.IsValid() && expr.Pos() > end) { -- return // non-overlapping -- } -- -- typ := info.TypeOf(expr) -- if typ == nil { +- assignStmt, _ := n.(*ast.AssignStmt) +- // We only care about ":=". +- if assignStmt.Tok != token.DEFINE { - return - } - -- // Find reference to the type declaration of the struct being initialized. -- typ = deref(typ) -- tStruct, ok := typ.Underlying().(*types.Struct) -- if !ok { -- return +- var file *ast.File +- for _, f := range pass.Files { +- if f.Pos() <= assignStmt.Pos() && assignStmt.Pos() < f.End() { +- file = f +- break +- } - } -- // Inv: typ is the possibly-named struct type. -- -- fieldCount := tStruct.NumFields() -- -- // Skip any struct that is already populated or that has no fields. -- if fieldCount == 0 || fieldCount == len(expr.Elts) { +- if file == nil { - return - } - -- // Are any fields in need of filling? -- var fillableFields []string -- for i := 0; i < fieldCount; i++ { -- field := tStruct.Field(i) -- // Ignore fields that are not accessible in the current package. -- if field.Pkg() != nil && field.Pkg() != pkg && !field.Exported() { +- for _, err := range pass.TypeErrors { +- if !FixesError(err.Msg) { - continue - } -- fillableFields = append(fillableFields, fmt.Sprintf("%s: %s", field.Name(), field.Type().String())) -- } -- if len(fillableFields) == 0 { -- return -- } -- -- // Derive a name for the struct type. -- var name string -- if typ != tStruct { -- // named struct type (e.g. pkg.S[T]) -- name = types.TypeString(typ, types.RelativeTo(pkg)) -- } else { -- // anonymous struct type -- totalFields := len(fillableFields) -- const maxLen = 20 -- // Find the index to cut off printing of fields. -- var i, fieldLen int -- for i = range fillableFields { -- if fieldLen > maxLen { -- break -- } -- fieldLen += len(fillableFields[i]) +- if assignStmt.Pos() > err.Pos || err.Pos >= assignStmt.End() { +- continue - } -- fillableFields = fillableFields[:i] -- if i < totalFields { -- fillableFields = append(fillableFields, "...") +- var buf bytes.Buffer +- if err := format.Node(&buf, pass.Fset, file); err != nil { +- continue - } -- name = fmt.Sprintf("anonymous struct { %s }", strings.Join(fillableFields, ", ")) +- pass.Report(analysis.Diagnostic{ +- Pos: err.Pos, +- End: analysisinternal.TypeErrorEndPos(pass.Fset, buf.Bytes(), err.Pos), +- Message: err.Msg, +- SuggestedFixes: []analysis.SuggestedFix{{ +- Message: "Change ':=' to '='", +- TextEdits: []analysis.TextEdit{{ +- Pos: err.Pos, +- End: err.Pos + 1, +- }}, +- }}, +- }) - } -- diags = append(diags, analysis.Diagnostic{ -- Message: fmt.Sprintf("Fill %s", name), -- Pos: expr.Pos(), -- End: expr.End(), -- }) - }) -- -- return diags +- return nil, nil -} - --// SuggestedFix computes the suggested fix for the kinds of --// diagnostics produced by the Analyzer above. --func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { -- if info == nil { -- return nil, fmt.Errorf("nil types.Info") -- } -- -- pos := start // don't use the end -- -- // TODO(rstambler): Using ast.Inspect would probably be more efficient than -- // calling PathEnclosingInterval. Switch this approach. -- path, _ := astutil.PathEnclosingInterval(file, pos, pos) -- if len(path) == 0 { -- return nil, fmt.Errorf("no enclosing ast.Node") -- } -- var expr *ast.CompositeLit -- for _, n := range path { -- if node, ok := n.(*ast.CompositeLit); ok { -- expr = node -- break -- } -- } -- -- typ := info.TypeOf(expr) -- if typ == nil { -- return nil, fmt.Errorf("no composite literal") -- } -- -- // Find reference to the type declaration of the struct being initialized. -- typ = deref(typ) -- tStruct, ok := typ.Underlying().(*types.Struct) -- if !ok { -- return nil, fmt.Errorf("%s is not a (pointer to) struct type", -- types.TypeString(typ, types.RelativeTo(pkg))) -- } -- // Inv: typ is the possibly-named struct type. +-func FixesError(msg string) bool { +- return msg == "no new variables on left side of :=" +-} +diff -urN a/gopls/internal/analysis/nonewvars/nonewvars_test.go b/gopls/internal/analysis/nonewvars/nonewvars_test.go +--- a/gopls/internal/analysis/nonewvars/nonewvars_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/nonewvars/nonewvars_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,17 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- fieldCount := tStruct.NumFields() +-package nonewvars_test - -- // Check which types have already been filled in. (we only want to fill in -- // the unfilled types, or else we'll blat user-supplied details) -- prefilledFields := map[string]ast.Expr{} -- for _, e := range expr.Elts { -- if kv, ok := e.(*ast.KeyValueExpr); ok { -- if key, ok := kv.Key.(*ast.Ident); ok { -- prefilledFields[key.Name] = kv.Value -- } -- } -- } +-import ( +- "testing" - -- // Use a new fileset to build up a token.File for the new composite -- // literal. We need one line for foo{, one line for }, and one line for -- // each field we're going to set. format.Node only cares about line -- // numbers, so we don't need to set columns, and each line can be -- // 1 byte long. -- // TODO(adonovan): why is this necessary? The position information -- // is going to be wrong for the existing trees in prefilledFields. -- // Can't the formatter just do its best with an empty fileset? -- fakeFset := token.NewFileSet() -- tok := fakeFset.AddFile("", -1, fieldCount+2) +- "golang.org/x/tools/go/analysis/analysistest" +- "golang.org/x/tools/gopls/internal/analysis/nonewvars" +-) - -- line := 2 // account for 1-based lines and the left brace -- var fieldTyps []types.Type -- for i := 0; i < fieldCount; i++ { -- field := tStruct.Field(i) -- // Ignore fields that are not accessible in the current package. -- if field.Pkg() != nil && field.Pkg() != pkg && !field.Exported() { -- fieldTyps = append(fieldTyps, nil) -- continue -- } -- fieldTyps = append(fieldTyps, field.Type()) -- } -- matches := analysisinternal.MatchingIdents(fieldTyps, file, start, info, pkg) -- var elts []ast.Expr -- for i, fieldTyp := range fieldTyps { -- if fieldTyp == nil { -- continue // TODO(adonovan): is this reachable? -- } -- fieldName := tStruct.Field(i).Name() +-func Test(t *testing.T) { +- testdata := analysistest.TestData() +- analysistest.RunWithSuggestedFixes(t, testdata, nonewvars.Analyzer, "a", "typeparams") +-} +diff -urN a/gopls/internal/analysis/nonewvars/testdata/src/a/a.go b/gopls/internal/analysis/nonewvars/testdata/src/a/a.go +--- a/gopls/internal/analysis/nonewvars/testdata/src/a/a.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/nonewvars/testdata/src/a/a.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,16 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- tok.AddLine(line - 1) // add 1 byte per line -- if line > tok.LineCount() { -- panic(fmt.Sprintf("invalid line number %v (of %v) for fillstruct", line, tok.LineCount())) -- } -- pos := tok.LineStart(line) +-package nonewvars - -- kv := &ast.KeyValueExpr{ -- Key: &ast.Ident{ -- NamePos: pos, -- Name: fieldName, -- }, -- Colon: pos, -- } -- if expr, ok := prefilledFields[fieldName]; ok { -- kv.Value = expr -- } else { -- names, ok := matches[fieldTyp] -- if !ok { -- return nil, fmt.Errorf("invalid struct field type: %v", fieldTyp) -- } +-import "log" - -- // Find the name most similar to the field name. -- // If no name matches the pattern, generate a zero value. -- // NOTE: We currently match on the name of the field key rather than the field type. -- if best := fuzzy.BestMatch(fieldName, names); best != "" { -- kv.Value = ast.NewIdent(best) -- } else if v := populateValue(file, pkg, fieldTyp); v != nil { -- kv.Value = v -- } else { -- return nil, nil -- } -- } -- elts = append(elts, kv) -- line++ -- } +-func x() { +- z := 1 +- z := 2 // want "no new variables on left side of :=" - -- // If all of the struct's fields are unexported, we have nothing to do. -- if len(elts) == 0 { -- return nil, fmt.Errorf("no elements to fill") -- } +- _, z := 3, 100 // want "no new variables on left side of :=" - -- // Add the final line for the right brace. Offset is the number of -- // bytes already added plus 1. -- tok.AddLine(len(elts) + 1) -- line = len(elts) + 2 -- if line > tok.LineCount() { -- panic(fmt.Sprintf("invalid line number %v (of %v) for fillstruct", line, tok.LineCount())) -- } +- log.Println(z) +-} +diff -urN a/gopls/internal/analysis/nonewvars/testdata/src/a/a.go.golden b/gopls/internal/analysis/nonewvars/testdata/src/a/a.go.golden +--- a/gopls/internal/analysis/nonewvars/testdata/src/a/a.go.golden 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/nonewvars/testdata/src/a/a.go.golden 1970-01-01 00:00:00.000000000 +0000 +@@ -1,16 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- cl := &ast.CompositeLit{ -- Type: expr.Type, -- Lbrace: tok.LineStart(1), -- Elts: elts, -- Rbrace: tok.LineStart(line), -- } +-package nonewvars - -- // Find the line on which the composite literal is declared. -- split := bytes.Split(content, []byte("\n")) -- lineNumber := safetoken.StartPosition(fset, expr.Lbrace).Line -- firstLine := split[lineNumber-1] // lines are 1-indexed +-import "log" - -- // Trim the whitespace from the left of the line, and use the index -- // to get the amount of whitespace on the left. -- trimmed := bytes.TrimLeftFunc(firstLine, unicode.IsSpace) -- index := bytes.Index(firstLine, trimmed) -- whitespace := firstLine[:index] +-func x() { +- z := 1 +- z = 2 // want "no new variables on left side of :=" - -- // First pass through the formatter: turn the expr into a string. -- var formatBuf bytes.Buffer -- if err := format.Node(&formatBuf, fakeFset, cl); err != nil { -- return nil, fmt.Errorf("failed to run first format on:\n%s\ngot err: %v", cl.Type, err) -- } -- sug := indent(formatBuf.Bytes(), whitespace) +- _, z = 3, 100 // want "no new variables on left side of :=" - -- if len(prefilledFields) > 0 { -- // Attempt a second pass through the formatter to line up columns. -- sourced, err := format.Source(sug) -- if err == nil { -- sug = indent(sourced, whitespace) -- } -- } +- log.Println(z) +-} +diff -urN a/gopls/internal/analysis/nonewvars/testdata/src/typeparams/a.go b/gopls/internal/analysis/nonewvars/testdata/src/typeparams/a.go +--- a/gopls/internal/analysis/nonewvars/testdata/src/typeparams/a.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/nonewvars/testdata/src/typeparams/a.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,6 +0,0 @@ +-package nonewvars - -- return &analysis.SuggestedFix{ -- TextEdits: []analysis.TextEdit{ -- { -- Pos: expr.Pos(), -- End: expr.End(), -- NewText: sug, -- }, -- }, -- }, nil +-func hello[T any]() int { +- var z T +- z := 1 // want "no new variables on left side of :=" -} +diff -urN a/gopls/internal/analysis/nonewvars/testdata/src/typeparams/a.go.golden b/gopls/internal/analysis/nonewvars/testdata/src/typeparams/a.go.golden +--- a/gopls/internal/analysis/nonewvars/testdata/src/typeparams/a.go.golden 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/nonewvars/testdata/src/typeparams/a.go.golden 1970-01-01 00:00:00.000000000 +0000 +@@ -1,6 +0,0 @@ +-package nonewvars - --// indent works line by line through str, indenting (prefixing) each line with --// ind. --func indent(str, ind []byte) []byte { -- split := bytes.Split(str, []byte("\n")) -- newText := bytes.NewBuffer(nil) -- for i, s := range split { -- if len(s) == 0 { -- continue -- } -- // Don't add the extra indentation to the first line. -- if i != 0 { -- newText.Write(ind) -- } -- newText.Write(s) -- if i < len(split)-1 { -- newText.WriteByte('\n') -- } -- } -- return newText.Bytes() +-func hello[T any]() int { +- var z T +- z = 1 // want "no new variables on left side of :=" -} +diff -urN a/gopls/internal/analysis/noresultvalues/doc.go b/gopls/internal/analysis/noresultvalues/doc.go +--- a/gopls/internal/analysis/noresultvalues/doc.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/noresultvalues/doc.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,21 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// populateValue constructs an expression to fill the value of a struct field. +-// Package noresultvalues defines an Analyzer that applies suggested fixes +-// to errors of the type "no result values expected". -// --// When the type of a struct field is a basic literal or interface, we return --// default values. For other types, such as maps, slices, and channels, we create --// empty expressions such as []T{} or make(chan T) rather than using default values. +-// # Analyzer noresultvalues -// --// The reasoning here is that users will call fillstruct with the intention of --// initializing the struct, in which case setting these fields to nil has no effect. --func populateValue(f *ast.File, pkg *types.Package, typ types.Type) ast.Expr { -- switch u := typ.Underlying().(type) { -- case *types.Basic: -- switch { -- case u.Info()&types.IsNumeric != 0: -- return &ast.BasicLit{Kind: token.INT, Value: "0"} -- case u.Info()&types.IsBoolean != 0: -- return &ast.Ident{Name: "false"} -- case u.Info()&types.IsString != 0: -- return &ast.BasicLit{Kind: token.STRING, Value: `""`} -- case u.Kind() == types.UnsafePointer: -- return ast.NewIdent("nil") -- default: -- panic(fmt.Sprintf("unknown basic type %v", u)) -- } +-// noresultvalues: suggested fixes for unexpected return values +-// +-// This checker provides suggested fixes for type errors of the +-// type "no result values expected" or "too many return values". +-// For example: +-// +-// func z() { return nil } +-// +-// will turn into +-// +-// func z() { return } +-package noresultvalues +diff -urN a/gopls/internal/analysis/noresultvalues/noresultvalues.go b/gopls/internal/analysis/noresultvalues/noresultvalues.go +--- a/gopls/internal/analysis/noresultvalues/noresultvalues.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/noresultvalues/noresultvalues.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,86 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- case *types.Map: -- k := analysisinternal.TypeExpr(f, pkg, u.Key()) -- v := analysisinternal.TypeExpr(f, pkg, u.Elem()) -- if k == nil || v == nil { -- return nil -- } -- return &ast.CompositeLit{ -- Type: &ast.MapType{ -- Key: k, -- Value: v, -- }, -- } -- case *types.Slice: -- s := analysisinternal.TypeExpr(f, pkg, u.Elem()) -- if s == nil { -- return nil -- } -- return &ast.CompositeLit{ -- Type: &ast.ArrayType{ -- Elt: s, -- }, -- } +-package noresultvalues - -- case *types.Array: -- a := analysisinternal.TypeExpr(f, pkg, u.Elem()) -- if a == nil { -- return nil -- } -- return &ast.CompositeLit{ -- Type: &ast.ArrayType{ -- Elt: a, -- Len: &ast.BasicLit{ -- Kind: token.INT, Value: fmt.Sprintf("%v", u.Len()), -- }, -- }, -- } +-import ( +- "bytes" +- "go/ast" +- "go/format" +- "strings" - -- case *types.Chan: -- v := analysisinternal.TypeExpr(f, pkg, u.Elem()) -- if v == nil { -- return nil -- } -- dir := ast.ChanDir(u.Dir()) -- if u.Dir() == types.SendRecv { -- dir = ast.SEND | ast.RECV -- } -- return &ast.CallExpr{ -- Fun: ast.NewIdent("make"), -- Args: []ast.Expr{ -- &ast.ChanType{ -- Dir: dir, -- Value: v, -- }, -- }, -- } +- _ "embed" - -- case *types.Struct: -- s := analysisinternal.TypeExpr(f, pkg, typ) -- if s == nil { -- return nil -- } -- return &ast.CompositeLit{ -- Type: s, -- } +- "golang.org/x/tools/go/analysis" +- "golang.org/x/tools/go/analysis/passes/inspect" +- "golang.org/x/tools/go/ast/inspector" +- "golang.org/x/tools/internal/analysisinternal" +-) - -- case *types.Signature: -- var params []*ast.Field -- for i := 0; i < u.Params().Len(); i++ { -- p := analysisinternal.TypeExpr(f, pkg, u.Params().At(i).Type()) -- if p == nil { -- return nil -- } -- params = append(params, &ast.Field{ -- Type: p, -- Names: []*ast.Ident{ -- { -- Name: u.Params().At(i).Name(), -- }, -- }, -- }) -- } -- var returns []*ast.Field -- for i := 0; i < u.Results().Len(); i++ { -- r := analysisinternal.TypeExpr(f, pkg, u.Results().At(i).Type()) -- if r == nil { -- return nil +-//go:embed doc.go +-var doc string +- +-var Analyzer = &analysis.Analyzer{ +- Name: "noresultvalues", +- Doc: analysisinternal.MustExtractDoc(doc, "noresultvalues"), +- Requires: []*analysis.Analyzer{inspect.Analyzer}, +- Run: run, +- RunDespiteErrors: true, +- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/noresultvars", +-} +- +-func run(pass *analysis.Pass) (interface{}, error) { +- inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) +- if len(pass.TypeErrors) == 0 { +- return nil, nil +- } +- +- nodeFilter := []ast.Node{(*ast.ReturnStmt)(nil)} +- inspect.Preorder(nodeFilter, func(n ast.Node) { +- retStmt, _ := n.(*ast.ReturnStmt) +- +- var file *ast.File +- for _, f := range pass.Files { +- if f.Pos() <= retStmt.Pos() && retStmt.Pos() < f.End() { +- file = f +- break - } -- returns = append(returns, &ast.Field{ -- Type: r, -- }) - } -- return &ast.FuncLit{ -- Type: &ast.FuncType{ -- Params: &ast.FieldList{ -- List: params, -- }, -- Results: &ast.FieldList{ -- List: returns, -- }, -- }, -- Body: &ast.BlockStmt{}, +- if file == nil { +- return - } - -- case *types.Pointer: -- switch u.Elem().(type) { -- case *types.Basic: -- return &ast.CallExpr{ -- Fun: &ast.Ident{ -- Name: "new", -- }, -- Args: []ast.Expr{ -- &ast.Ident{ -- Name: u.Elem().String(), -- }, -- }, +- for _, err := range pass.TypeErrors { +- if !FixesError(err.Msg) { +- continue - } -- default: -- return &ast.UnaryExpr{ -- Op: token.AND, -- X: populateValue(f, pkg, u.Elem()), +- if retStmt.Pos() >= err.Pos || err.Pos >= retStmt.End() { +- continue - } -- } -- -- case *types.Interface: -- if param, ok := typ.(*typeparams.TypeParam); ok { -- // *new(T) is the zero value of a type parameter T. -- // TODO(adonovan): one could give a more specific zero -- // value if the type has a core type that is, say, -- // always a number or a pointer. See go/ssa for details. -- return &ast.StarExpr{ -- X: &ast.CallExpr{ -- Fun: ast.NewIdent("new"), -- Args: []ast.Expr{ -- ast.NewIdent(param.Obj().Name()), -- }, -- }, +- var buf bytes.Buffer +- if err := format.Node(&buf, pass.Fset, file); err != nil { +- continue - } +- pass.Report(analysis.Diagnostic{ +- Pos: err.Pos, +- End: analysisinternal.TypeErrorEndPos(pass.Fset, buf.Bytes(), err.Pos), +- Message: err.Msg, +- SuggestedFixes: []analysis.SuggestedFix{{ +- Message: "Delete return values", +- TextEdits: []analysis.TextEdit{{ +- Pos: retStmt.Pos(), +- End: retStmt.End(), +- NewText: []byte("return"), +- }}, +- }}, +- }) - } -- -- return ast.NewIdent("nil") -- } -- return nil +- }) +- return nil, nil -} - --func deref(t types.Type) types.Type { -- for { -- ptr, ok := t.Underlying().(*types.Pointer) -- if !ok { -- return t -- } -- t = ptr.Elem() -- } +-func FixesError(msg string) bool { +- return msg == "no result values expected" || +- strings.HasPrefix(msg, "too many return values") && strings.Contains(msg, "want ()") -} -diff -urN a/gopls/internal/lsp/analysis/fillstruct/fillstruct_test.go b/gopls/internal/lsp/analysis/fillstruct/fillstruct_test.go ---- a/gopls/internal/lsp/analysis/fillstruct/fillstruct_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/fillstruct/fillstruct_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,22 +0,0 @@ +diff -urN a/gopls/internal/analysis/noresultvalues/noresultvalues_test.go b/gopls/internal/analysis/noresultvalues/noresultvalues_test.go +--- a/gopls/internal/analysis/noresultvalues/noresultvalues_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/noresultvalues/noresultvalues_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,17 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package fillstruct_test +-package noresultvalues_test - -import ( - "testing" - - "golang.org/x/tools/go/analysis/analysistest" -- "golang.org/x/tools/gopls/internal/lsp/analysis/fillstruct" -- "golang.org/x/tools/internal/typeparams" +- "golang.org/x/tools/gopls/internal/analysis/noresultvalues" -) - -func Test(t *testing.T) { - testdata := analysistest.TestData() -- tests := []string{"a"} -- if typeparams.Enabled { -- tests = append(tests, "typeparams") -- } -- analysistest.Run(t, testdata, fillstruct.Analyzer, tests...) +- analysistest.RunWithSuggestedFixes(t, testdata, noresultvalues.Analyzer, "a", "typeparams") -} -diff -urN a/gopls/internal/lsp/analysis/fillstruct/testdata/src/a/a.go b/gopls/internal/lsp/analysis/fillstruct/testdata/src/a/a.go ---- a/gopls/internal/lsp/analysis/fillstruct/testdata/src/a/a.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/fillstruct/testdata/src/a/a.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,113 +0,0 @@ +diff -urN a/gopls/internal/analysis/noresultvalues/testdata/src/a/a.go b/gopls/internal/analysis/noresultvalues/testdata/src/a/a.go +--- a/gopls/internal/analysis/noresultvalues/testdata/src/a/a.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/noresultvalues/testdata/src/a/a.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,9 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package fillstruct +-package noresultvalues - --import ( -- data "b" -- "go/ast" -- "go/token" -- "unsafe" --) +-func x() { return nil } // want `no result values expected|too many return values` - --type emptyStruct struct{} +-func y() { return nil, "hello" } // want `no result values expected|too many return values` +diff -urN a/gopls/internal/analysis/noresultvalues/testdata/src/a/a.go.golden b/gopls/internal/analysis/noresultvalues/testdata/src/a/a.go.golden +--- a/gopls/internal/analysis/noresultvalues/testdata/src/a/a.go.golden 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/noresultvalues/testdata/src/a/a.go.golden 1970-01-01 00:00:00.000000000 +0000 +@@ -1,9 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --var _ = emptyStruct{} +-package noresultvalues - --type basicStruct struct { -- foo int --} +-func x() { return } // want `no result values expected|too many return values` - --var _ = basicStruct{} // want `Fill basicStruct` +-func y() { return } // want `no result values expected|too many return values` +diff -urN a/gopls/internal/analysis/noresultvalues/testdata/src/typeparams/a.go b/gopls/internal/analysis/noresultvalues/testdata/src/typeparams/a.go +--- a/gopls/internal/analysis/noresultvalues/testdata/src/typeparams/a.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/noresultvalues/testdata/src/typeparams/a.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,6 +0,0 @@ +-package noresult - --type twoArgStruct struct { -- foo int -- bar string --} -- --var _ = twoArgStruct{} // want `Fill twoArgStruct` -- --var _ = twoArgStruct{ // want `Fill twoArgStruct` -- bar: "bar", +-func hello[T any]() { +- var z T +- return z // want `no result values expected|too many return values` -} +diff -urN a/gopls/internal/analysis/noresultvalues/testdata/src/typeparams/a.go.golden b/gopls/internal/analysis/noresultvalues/testdata/src/typeparams/a.go.golden +--- a/gopls/internal/analysis/noresultvalues/testdata/src/typeparams/a.go.golden 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/noresultvalues/testdata/src/typeparams/a.go.golden 1970-01-01 00:00:00.000000000 +0000 +@@ -1,6 +0,0 @@ +-package noresult - --type nestedStruct struct { -- bar string -- basic basicStruct +-func hello[T any]() { +- var z T +- return // want `no result values expected|too many return values` -} +diff -urN a/gopls/internal/analysis/simplifycompositelit/doc.go b/gopls/internal/analysis/simplifycompositelit/doc.go +--- a/gopls/internal/analysis/simplifycompositelit/doc.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/simplifycompositelit/doc.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,22 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --var _ = nestedStruct{} // want `Fill nestedStruct` -- --var _ = data.B{} // want `Fill b.B` +-// Package simplifycompositelit defines an Analyzer that simplifies composite literals. +-// https://github.com/golang/go/blob/master/src/cmd/gofmt/simplify.go +-// https://golang.org/cmd/gofmt/#hdr-The_simplify_command +-// +-// # Analyzer simplifycompositelit +-// +-// simplifycompositelit: check for composite literal simplifications +-// +-// An array, slice, or map composite literal of the form: +-// +-// []T{T{}, T{}} +-// +-// will be simplified to: +-// +-// []T{{}, {}} +-// +-// This is one of the simplifications that "gofmt -s" applies. +-package simplifycompositelit +diff -urN a/gopls/internal/analysis/simplifycompositelit/simplifycompositelit.go b/gopls/internal/analysis/simplifycompositelit/simplifycompositelit.go +--- a/gopls/internal/analysis/simplifycompositelit/simplifycompositelit.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/simplifycompositelit/simplifycompositelit.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,193 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --type typedStruct struct { -- m map[string]int -- s []int -- c chan int -- c1 <-chan int -- a [2]string --} +-// Package simplifycompositelit defines an Analyzer that simplifies composite literals. +-// https://github.com/golang/go/blob/master/src/cmd/gofmt/simplify.go +-// https://golang.org/cmd/gofmt/#hdr-The_simplify_command +-package simplifycompositelit - --var _ = typedStruct{} // want `Fill typedStruct` +-import ( +- "bytes" +- _ "embed" +- "fmt" +- "go/ast" +- "go/printer" +- "go/token" +- "reflect" - --type funStruct struct { -- fn func(i int) int --} +- "golang.org/x/tools/go/analysis" +- "golang.org/x/tools/go/analysis/passes/inspect" +- "golang.org/x/tools/go/ast/inspector" +- "golang.org/x/tools/internal/analysisinternal" +-) - --var _ = funStruct{} // want `Fill funStruct` +-//go:embed doc.go +-var doc string - --type funStructComplex struct { -- fn func(i int, s string) (string, int) +-var Analyzer = &analysis.Analyzer{ +- Name: "simplifycompositelit", +- Doc: analysisinternal.MustExtractDoc(doc, "simplifycompositelit"), +- Requires: []*analysis.Analyzer{inspect.Analyzer}, +- Run: run, +- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/simplifycompositelit", -} - --var _ = funStructComplex{} // want `Fill funStructComplex` +-func run(pass *analysis.Pass) (interface{}, error) { +- inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) +- nodeFilter := []ast.Node{(*ast.CompositeLit)(nil)} +- inspect.Preorder(nodeFilter, func(n ast.Node) { +- expr := n.(*ast.CompositeLit) - --type funStructEmpty struct { -- fn func() --} +- outer := expr +- var keyType, eltType ast.Expr +- switch typ := outer.Type.(type) { +- case *ast.ArrayType: +- eltType = typ.Elt +- case *ast.MapType: +- keyType = typ.Key +- eltType = typ.Value +- } - --var _ = funStructEmpty{} // want `Fill funStructEmpty` +- if eltType == nil { +- return +- } +- var ktyp reflect.Value +- if keyType != nil { +- ktyp = reflect.ValueOf(keyType) +- } +- typ := reflect.ValueOf(eltType) +- for _, x := range outer.Elts { +- // look at value of indexed/named elements +- if t, ok := x.(*ast.KeyValueExpr); ok { +- if keyType != nil { +- simplifyLiteral(pass, ktyp, keyType, t.Key) +- } +- x = t.Value +- } +- simplifyLiteral(pass, typ, eltType, x) +- } +- }) +- return nil, nil +-} - --type Foo struct { -- A int +-func simplifyLiteral(pass *analysis.Pass, typ reflect.Value, astType, x ast.Expr) { +- // if the element is a composite literal and its literal type +- // matches the outer literal's element type exactly, the inner +- // literal type may be omitted +- if inner, ok := x.(*ast.CompositeLit); ok && match(typ, reflect.ValueOf(inner.Type)) { +- var b bytes.Buffer +- printer.Fprint(&b, pass.Fset, inner.Type) +- createDiagnostic(pass, inner.Type.Pos(), inner.Type.End(), b.String()) +- } +- // if the outer literal's element type is a pointer type *T +- // and the element is & of a composite literal of type T, +- // the inner &T may be omitted. +- if ptr, ok := astType.(*ast.StarExpr); ok { +- if addr, ok := x.(*ast.UnaryExpr); ok && addr.Op == token.AND { +- if inner, ok := addr.X.(*ast.CompositeLit); ok { +- if match(reflect.ValueOf(ptr.X), reflect.ValueOf(inner.Type)) { +- var b bytes.Buffer +- printer.Fprint(&b, pass.Fset, inner.Type) +- // Account for the & by subtracting 1 from typ.Pos(). +- createDiagnostic(pass, inner.Type.Pos()-1, inner.Type.End(), "&"+b.String()) +- } +- } +- } +- } -} - --type Bar struct { -- X *Foo -- Y *Foo +-func createDiagnostic(pass *analysis.Pass, start, end token.Pos, typ string) { +- pass.Report(analysis.Diagnostic{ +- Pos: start, +- End: end, +- Message: "redundant type from array, slice, or map composite literal", +- SuggestedFixes: []analysis.SuggestedFix{{ +- Message: fmt.Sprintf("Remove '%s'", typ), +- TextEdits: []analysis.TextEdit{{ +- Pos: start, +- End: end, +- NewText: []byte{}, +- }}, +- }}, +- }) -} - --var _ = Bar{} // want `Fill Bar` +-// match reports whether pattern matches val, +-// recording wildcard submatches in m. +-// If m == nil, match checks whether pattern == val. +-// from https://github.com/golang/go/blob/26154f31ad6c801d8bad5ef58df1e9263c6beec7/src/cmd/gofmt/rewrite.go#L160 +-func match(pattern, val reflect.Value) bool { +- // Otherwise, pattern and val must match recursively. +- if !pattern.IsValid() || !val.IsValid() { +- return !pattern.IsValid() && !val.IsValid() +- } +- if pattern.Type() != val.Type() { +- return false +- } +- +- // Special cases. +- switch pattern.Type() { +- case identType: +- // For identifiers, only the names need to match +- // (and none of the other *ast.Object information). +- // This is a common case, handle it all here instead +- // of recursing down any further via reflection. +- p := pattern.Interface().(*ast.Ident) +- v := val.Interface().(*ast.Ident) +- return p == nil && v == nil || p != nil && v != nil && p.Name == v.Name +- case objectPtrType, positionType: +- // object pointers and token positions always match +- return true +- case callExprType: +- // For calls, the Ellipsis fields (token.Position) must +- // match since that is how f(x) and f(x...) are different. +- // Check them here but fall through for the remaining fields. +- p := pattern.Interface().(*ast.CallExpr) +- v := val.Interface().(*ast.CallExpr) +- if p.Ellipsis.IsValid() != v.Ellipsis.IsValid() { +- return false +- } +- } - --type importedStruct struct { -- m map[*ast.CompositeLit]ast.Field -- s []ast.BadExpr -- a [3]token.Token -- c chan ast.EmptyStmt -- fn func(ast_decl ast.DeclStmt) ast.Ellipsis -- st ast.CompositeLit --} +- p := reflect.Indirect(pattern) +- v := reflect.Indirect(val) +- if !p.IsValid() || !v.IsValid() { +- return !p.IsValid() && !v.IsValid() +- } - --var _ = importedStruct{} // want `Fill importedStruct` +- switch p.Kind() { +- case reflect.Slice: +- if p.Len() != v.Len() { +- return false +- } +- for i := 0; i < p.Len(); i++ { +- if !match(p.Index(i), v.Index(i)) { +- return false +- } +- } +- return true - --type pointerBuiltinStruct struct { -- b *bool -- s *string -- i *int --} +- case reflect.Struct: +- for i := 0; i < p.NumField(); i++ { +- if !match(p.Field(i), v.Field(i)) { +- return false +- } +- } +- return true - --var _ = pointerBuiltinStruct{} // want `Fill pointerBuiltinStruct` +- case reflect.Interface: +- return match(p.Elem(), v.Elem()) +- } - --var _ = []ast.BasicLit{ -- {}, // want `Fill go/ast.BasicLit` +- // Handle token integers, etc. +- return p.Interface() == v.Interface() -} - --var _ = []ast.BasicLit{{}, // want "go/ast.BasicLit" --} +-// Values/types for special cases. +-var ( +- identType = reflect.TypeOf((*ast.Ident)(nil)) +- objectPtrType = reflect.TypeOf((*ast.Object)(nil)) +- positionType = reflect.TypeOf(token.NoPos) +- callExprType = reflect.TypeOf((*ast.CallExpr)(nil)) +-) +diff -urN a/gopls/internal/analysis/simplifycompositelit/simplifycompositelit_test.go b/gopls/internal/analysis/simplifycompositelit/simplifycompositelit_test.go +--- a/gopls/internal/analysis/simplifycompositelit/simplifycompositelit_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/simplifycompositelit/simplifycompositelit_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,17 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --type unsafeStruct struct { -- foo unsafe.Pointer --} +-package simplifycompositelit_test - --var _ = unsafeStruct{} // want `Fill unsafeStruct` -diff -urN a/gopls/internal/lsp/analysis/fillstruct/testdata/src/b/b.go b/gopls/internal/lsp/analysis/fillstruct/testdata/src/b/b.go ---- a/gopls/internal/lsp/analysis/fillstruct/testdata/src/b/b.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/fillstruct/testdata/src/b/b.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,6 +0,0 @@ --package fillstruct +-import ( +- "testing" - --type B struct { -- ExportedInt int -- unexportedInt int +- "golang.org/x/tools/go/analysis/analysistest" +- "golang.org/x/tools/gopls/internal/analysis/simplifycompositelit" +-) +- +-func Test(t *testing.T) { +- testdata := analysistest.TestData() +- analysistest.RunWithSuggestedFixes(t, testdata, simplifycompositelit.Analyzer, "a") -} -diff -urN a/gopls/internal/lsp/analysis/fillstruct/testdata/src/typeparams/typeparams.go b/gopls/internal/lsp/analysis/fillstruct/testdata/src/typeparams/typeparams.go ---- a/gopls/internal/lsp/analysis/fillstruct/testdata/src/typeparams/typeparams.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/fillstruct/testdata/src/typeparams/typeparams.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,50 +0,0 @@ +diff -urN a/gopls/internal/analysis/simplifycompositelit/testdata/src/a/a.go b/gopls/internal/analysis/simplifycompositelit/testdata/src/a/a.go +--- a/gopls/internal/analysis/simplifycompositelit/testdata/src/a/a.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/simplifycompositelit/testdata/src/a/a.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,234 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package fillstruct -- --type emptyStruct[A any] struct{} +-package testdata - --var _ = emptyStruct[int]{} +-type T struct { +- x, y int +-} - --type basicStruct[T any] struct { -- foo T +-type T2 struct { +- w, z int -} - --var _ = basicStruct[int]{} // want `Fill basicStruct\[int\]` +-var _ = [42]T{ +- T{}, // want "redundant type from array, slice, or map composite literal" +- T{1, 2}, // want "redundant type from array, slice, or map composite literal" +- T{3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - --type twoArgStruct[F, B any] struct { -- foo F -- bar B +-var _ = [...]T{ +- T{}, // want "redundant type from array, slice, or map composite literal" +- T{1, 2}, // want "redundant type from array, slice, or map composite literal" +- T{3, 4}, // want "redundant type from array, slice, or map composite literal" -} - --var _ = twoArgStruct[string, int]{} // want `Fill twoArgStruct\[string, int\]` +-var _ = []T{ +- T{}, // want "redundant type from array, slice, or map composite literal" +- T{1, 2}, // want "redundant type from array, slice, or map composite literal" +- T{3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - --var _ = twoArgStruct[int, string]{ // want `Fill twoArgStruct\[int, string\]` -- bar: "bar", +-var _ = []T{ +- T{}, // want "redundant type from array, slice, or map composite literal" +- 10: T{1, 2}, // want "redundant type from array, slice, or map composite literal" +- 20: T{3, 4}, // want "redundant type from array, slice, or map composite literal" -} - --type nestedStruct struct { -- bar string -- basic basicStruct[int] +-var _ = []struct { +- x, y int +-}{ +- struct{ x, y int }{}, // want "redundant type from array, slice, or map composite literal" +- 10: struct{ x, y int }{1, 2}, // want "redundant type from array, slice, or map composite literal" +- 20: struct{ x, y int }{3, 4}, // want "redundant type from array, slice, or map composite literal" -} - --var _ = nestedStruct{} // want "Fill nestedStruct" +-var _ = []interface{}{ +- T{}, +- 10: T{1, 2}, +- 20: T{3, 4}, +-} - --func _[T any]() { -- type S struct{ t T } -- x := S{} // want "Fill S" -- _ = x +-var _ = [][]int{ +- []int{}, // want "redundant type from array, slice, or map composite literal" +- []int{1, 2}, // want "redundant type from array, slice, or map composite literal" +- []int{3, 4}, // want "redundant type from array, slice, or map composite literal" -} - --func Test() { -- var tests = []struct { -- a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p string -- }{ -- {}, // want "Fill anonymous struct { a: string, b: string, c: string, ... }" -- } -- for _, test := range tests { -- _ = test -- } +-var _ = [][]int{ +- ([]int{}), +- ([]int{1, 2}), +- []int{3, 4}, // want "redundant type from array, slice, or map composite literal" -} -diff -urN a/gopls/internal/lsp/analysis/infertypeargs/infertypeargs.go b/gopls/internal/lsp/analysis/infertypeargs/infertypeargs.go ---- a/gopls/internal/lsp/analysis/infertypeargs/infertypeargs.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/infertypeargs/infertypeargs.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,47 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --// Package infertypeargs defines an analyzer that checks for explicit function --// arguments that could be inferred. --package infertypeargs +-var _ = [][][]int{ +- [][]int{}, // want "redundant type from array, slice, or map composite literal" +- [][]int{ // want "redundant type from array, slice, or map composite literal" +- []int{}, // want "redundant type from array, slice, or map composite literal" +- []int{0, 1, 2, 3}, // want "redundant type from array, slice, or map composite literal" +- []int{4, 5}, // want "redundant type from array, slice, or map composite literal" +- }, +-} - --import ( -- "go/token" +-var _ = map[string]T{ +- "foo": T{}, // want "redundant type from array, slice, or map composite literal" +- "bar": T{1, 2}, // want "redundant type from array, slice, or map composite literal" +- "bal": T{3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - -- "golang.org/x/tools/go/analysis" -- "golang.org/x/tools/go/analysis/passes/inspect" -- "golang.org/x/tools/go/ast/inspector" --) +-var _ = map[string]struct { +- x, y int +-}{ +- "foo": struct{ x, y int }{}, // want "redundant type from array, slice, or map composite literal" +- "bar": struct{ x, y int }{1, 2}, // want "redundant type from array, slice, or map composite literal" +- "bal": struct{ x, y int }{3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - --const Doc = `check for unnecessary type arguments in call expressions +-var _ = map[string]interface{}{ +- "foo": T{}, +- "bar": T{1, 2}, +- "bal": T{3, 4}, +-} - --Explicit type arguments may be omitted from call expressions if they can be --inferred from function arguments, or from other type arguments: +-var _ = map[string][]int{ +- "foo": []int{}, // want "redundant type from array, slice, or map composite literal" +- "bar": []int{1, 2}, // want "redundant type from array, slice, or map composite literal" +- "bal": []int{3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - -- func f[T any](T) {} -- -- func _() { -- f[string]("foo") // string could be inferred -- } --` +-var _ = map[string][]int{ +- "foo": ([]int{}), +- "bar": ([]int{1, 2}), +- "bal": []int{3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - --var Analyzer = &analysis.Analyzer{ -- Name: "infertypeargs", -- Doc: Doc, -- Requires: []*analysis.Analyzer{inspect.Analyzer}, -- Run: run, +-type Point struct { +- a int +- b int -} - --// TODO(rfindley): remove this thin wrapper around the infertypeargs refactoring, --// and eliminate the infertypeargs analyzer. --// --// Previous iterations used the analysis framework for computing refactorings, --// which proved inefficient. --func run(pass *analysis.Pass) (interface{}, error) { -- inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) -- for _, diag := range DiagnoseInferableTypeArgs(pass.Fset, inspect, token.NoPos, token.NoPos, pass.Pkg, pass.TypesInfo) { -- pass.Report(diag) -- } -- return nil, nil +-type Piece struct { +- a int +- b int +- c Point +- d []Point +- e *Point +- f *Point -} -diff -urN a/gopls/internal/lsp/analysis/infertypeargs/infertypeargs_test.go b/gopls/internal/lsp/analysis/infertypeargs/infertypeargs_test.go ---- a/gopls/internal/lsp/analysis/infertypeargs/infertypeargs_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/infertypeargs/infertypeargs_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,21 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package infertypeargs_test +-// from exp/4s/data.go +-var pieces3 = []Piece{ +- Piece{0, 0, Point{4, 1}, []Point{Point{0, 0}, Point{1, 0}, Point{1, 0}, Point{1, 0}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" +- Piece{1, 0, Point{1, 4}, []Point{Point{0, 0}, Point{0, 1}, Point{0, 1}, Point{0, 1}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" +- Piece{2, 0, Point{4, 1}, []Point{Point{0, 0}, Point{1, 0}, Point{1, 0}, Point{1, 0}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" +- Piece{3, 0, Point{1, 4}, []Point{Point{0, 0}, Point{0, 1}, Point{0, 1}, Point{0, 1}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" +-} - --import ( -- "testing" +-var _ = [42]*T{ +- &T{}, // want "redundant type from array, slice, or map composite literal" +- &T{1, 2}, // want "redundant type from array, slice, or map composite literal" +- &T{3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - -- "golang.org/x/tools/go/analysis/analysistest" -- "golang.org/x/tools/gopls/internal/lsp/analysis/infertypeargs" -- "golang.org/x/tools/internal/typeparams" --) +-var _ = [...]*T{ +- &T{}, // want "redundant type from array, slice, or map composite literal" +- &T{1, 2}, // want "redundant type from array, slice, or map composite literal" +- &T{3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - --func Test(t *testing.T) { -- if !typeparams.Enabled { -- t.Skip("type params are not enabled") -- } -- testdata := analysistest.TestData() -- analysistest.RunWithSuggestedFixes(t, testdata, infertypeargs.Analyzer, "a") +-var _ = []*T{ +- &T{}, // want "redundant type from array, slice, or map composite literal" +- &T{1, 2}, // want "redundant type from array, slice, or map composite literal" +- &T{3, 4}, // want "redundant type from array, slice, or map composite literal" -} -diff -urN a/gopls/internal/lsp/analysis/infertypeargs/run_go117.go b/gopls/internal/lsp/analysis/infertypeargs/run_go117.go ---- a/gopls/internal/lsp/analysis/infertypeargs/run_go117.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/infertypeargs/run_go117.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,22 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --//go:build !go1.18 --// +build !go1.18 +-var _ = []*T{ +- &T{}, // want "redundant type from array, slice, or map composite literal" +- 10: &T{1, 2}, // want "redundant type from array, slice, or map composite literal" +- 20: &T{3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - --package infertypeargs +-var _ = []*struct { +- x, y int +-}{ +- &struct{ x, y int }{}, // want "redundant type from array, slice, or map composite literal" +- 10: &struct{ x, y int }{1, 2}, // want "redundant type from array, slice, or map composite literal" +- 20: &struct{ x, y int }{3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - --import ( -- "go/token" -- "go/types" +-var _ = []interface{}{ +- &T{}, +- 10: &T{1, 2}, +- 20: &T{3, 4}, +-} - -- "golang.org/x/tools/go/analysis" -- "golang.org/x/tools/go/ast/inspector" --) +-var _ = []*[]int{ +- &[]int{}, // want "redundant type from array, slice, or map composite literal" +- &[]int{1, 2}, // want "redundant type from array, slice, or map composite literal" +- &[]int{3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - --// DiagnoseInferableTypeArgs returns an empty slice, as generics are not supported at --// this go version. --func DiagnoseInferableTypeArgs(fset *token.FileSet, inspect *inspector.Inspector, start, end token.Pos, pkg *types.Package, info *types.Info) []analysis.Diagnostic { -- return nil +-var _ = []*[]int{ +- (&[]int{}), +- (&[]int{1, 2}), +- &[]int{3, 4}, // want "redundant type from array, slice, or map composite literal" -} -diff -urN a/gopls/internal/lsp/analysis/infertypeargs/run_go118.go b/gopls/internal/lsp/analysis/infertypeargs/run_go118.go ---- a/gopls/internal/lsp/analysis/infertypeargs/run_go118.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/infertypeargs/run_go118.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,120 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --//go:build go1.18 --// +build go1.18 +-var _ = []*[]*[]int{ +- &[]*[]int{}, // want "redundant type from array, slice, or map composite literal" +- &[]*[]int{ // want "redundant type from array, slice, or map composite literal" +- &[]int{}, // want "redundant type from array, slice, or map composite literal" +- &[]int{0, 1, 2, 3}, // want "redundant type from array, slice, or map composite literal" +- &[]int{4, 5}, // want "redundant type from array, slice, or map composite literal" +- }, +-} - --package infertypeargs +-var _ = map[string]*T{ +- "foo": &T{}, // want "redundant type from array, slice, or map composite literal" +- "bar": &T{1, 2}, // want "redundant type from array, slice, or map composite literal" +- "bal": &T{3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - --import ( -- "go/ast" -- "go/token" -- "go/types" +-var _ = map[string]*struct { +- x, y int +-}{ +- "foo": &struct{ x, y int }{}, // want "redundant type from array, slice, or map composite literal" +- "bar": &struct{ x, y int }{1, 2}, // want "redundant type from array, slice, or map composite literal" +- "bal": &struct{ x, y int }{3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - -- "golang.org/x/tools/go/analysis" -- "golang.org/x/tools/go/ast/inspector" -- "golang.org/x/tools/internal/typeparams" --) +-var _ = map[string]interface{}{ +- "foo": &T{}, +- "bar": &T{1, 2}, +- "bal": &T{3, 4}, +-} - --// DiagnoseInferableTypeArgs reports diagnostics describing simplifications to type --// arguments overlapping with the provided start and end position. --// --// If start or end is token.NoPos, the corresponding bound is not checked --// (i.e. if both start and end are NoPos, all call expressions are considered). --func DiagnoseInferableTypeArgs(fset *token.FileSet, inspect *inspector.Inspector, start, end token.Pos, pkg *types.Package, info *types.Info) []analysis.Diagnostic { -- var diags []analysis.Diagnostic -- -- nodeFilter := []ast.Node{(*ast.CallExpr)(nil)} -- inspect.Preorder(nodeFilter, func(node ast.Node) { -- call := node.(*ast.CallExpr) -- x, lbrack, indices, rbrack := typeparams.UnpackIndexExpr(call.Fun) -- ident := calledIdent(x) -- if ident == nil || len(indices) == 0 { -- return // no explicit args, nothing to do -- } -- -- if (start.IsValid() && call.End() < start) || (end.IsValid() && call.Pos() > end) { -- return // non-overlapping -- } +-var _ = map[string]*[]int{ +- "foo": &[]int{}, // want "redundant type from array, slice, or map composite literal" +- "bar": &[]int{1, 2}, // want "redundant type from array, slice, or map composite literal" +- "bal": &[]int{3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - -- // Confirm that instantiation actually occurred at this ident. -- idata, ok := typeparams.GetInstances(info)[ident] -- if !ok { -- return // something went wrong, but fail open -- } -- instance := idata.Type +-var _ = map[string]*[]int{ +- "foo": (&[]int{}), +- "bar": (&[]int{1, 2}), +- "bal": &[]int{3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - -- // Start removing argument expressions from the right, and check if we can -- // still infer the call expression. -- required := len(indices) // number of type expressions that are required -- for i := len(indices) - 1; i >= 0; i-- { -- var fun ast.Expr -- if i == 0 { -- // No longer an index expression: just use the parameterized operand. -- fun = x -- } else { -- fun = typeparams.PackIndexExpr(x, lbrack, indices[:i], indices[i-1].End()) -- } -- newCall := &ast.CallExpr{ -- Fun: fun, -- Lparen: call.Lparen, -- Args: call.Args, -- Ellipsis: call.Ellipsis, -- Rparen: call.Rparen, -- } -- info := new(types.Info) -- typeparams.InitInstanceInfo(info) -- if err := types.CheckExpr(fset, pkg, call.Pos(), newCall, info); err != nil { -- // Most likely inference failed. -- break -- } -- newIData := typeparams.GetInstances(info)[ident] -- newInstance := newIData.Type -- if !types.Identical(instance, newInstance) { -- // The inferred result type does not match the original result type, so -- // this simplification is not valid. -- break -- } -- required = i -- } -- if required < len(indices) { -- var s, e token.Pos -- var edit analysis.TextEdit -- if required == 0 { -- s, e = lbrack, rbrack+1 // erase the entire index -- edit = analysis.TextEdit{Pos: s, End: e} -- } else { -- s = indices[required].Pos() -- e = rbrack -- // erase from end of last arg to include last comma & white-spaces -- edit = analysis.TextEdit{Pos: indices[required-1].End(), End: e} -- } -- // Recheck that our (narrower) fixes overlap with the requested range. -- if (start.IsValid() && e < start) || (end.IsValid() && s > end) { -- return // non-overlapping -- } -- diags = append(diags, analysis.Diagnostic{ -- Pos: s, -- End: e, -- Message: "unnecessary type arguments", -- SuggestedFixes: []analysis.SuggestedFix{{ -- Message: "simplify type arguments", -- TextEdits: []analysis.TextEdit{edit}, -- }}, -- }) -- } -- }) +-var pieces4 = []*Piece{ +- &Piece{0, 0, Point{4, 1}, []Point{Point{0, 0}, Point{1, 0}, Point{1, 0}, Point{1, 0}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" +- &Piece{1, 0, Point{1, 4}, []Point{Point{0, 0}, Point{0, 1}, Point{0, 1}, Point{0, 1}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" +- &Piece{2, 0, Point{4, 1}, []Point{Point{0, 0}, Point{1, 0}, Point{1, 0}, Point{1, 0}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" +- &Piece{3, 0, Point{1, 4}, []Point{Point{0, 0}, Point{0, 1}, Point{0, 1}, Point{0, 1}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" +-} - -- return diags +-var _ = map[T]T2{ +- T{1, 2}: T2{3, 4}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" +- T{5, 6}: T2{7, 8}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" -} - --func calledIdent(x ast.Expr) *ast.Ident { -- switch x := x.(type) { -- case *ast.Ident: -- return x -- case *ast.SelectorExpr: -- return x.Sel -- } -- return nil +-var _ = map[*T]*T2{ +- &T{1, 2}: &T2{3, 4}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" +- &T{5, 6}: &T2{7, 8}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" -} -diff -urN a/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/basic.go b/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/basic.go ---- a/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/basic.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/basic.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,20 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/analysis/simplifycompositelit/testdata/src/a/a.go.golden b/gopls/internal/analysis/simplifycompositelit/testdata/src/a/a.go.golden +--- a/gopls/internal/analysis/simplifycompositelit/testdata/src/a/a.go.golden 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/simplifycompositelit/testdata/src/a/a.go.golden 1970-01-01 00:00:00.000000000 +0000 +@@ -1,234 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --// This file contains tests for the infertyepargs checker. +-package testdata - --package a +-type T struct { +- x, y int +-} - --func f[T any](T) {} +-type T2 struct { +- w, z int +-} - --func g[T any]() T { var x T; return x } +-var _ = [42]T{ +- {}, // want "redundant type from array, slice, or map composite literal" +- {1, 2}, // want "redundant type from array, slice, or map composite literal" +- {3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - --func h[P interface{ ~*T }, T any]() {} +-var _ = [...]T{ +- {}, // want "redundant type from array, slice, or map composite literal" +- {1, 2}, // want "redundant type from array, slice, or map composite literal" +- {3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - --func _() { -- f[string]("hello") // want "unnecessary type arguments" -- f[int](2) // want "unnecessary type arguments" -- _ = g[int]() -- h[*int, int]() // want "unnecessary type arguments" +-var _ = []T{ +- {}, // want "redundant type from array, slice, or map composite literal" +- {1, 2}, // want "redundant type from array, slice, or map composite literal" +- {3, 4}, // want "redundant type from array, slice, or map composite literal" -} -diff -urN a/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/basic.go.golden b/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/basic.go.golden ---- a/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/basic.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/basic.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,20 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --// This file contains tests for the infertyepargs checker. +-var _ = []T{ +- {}, // want "redundant type from array, slice, or map composite literal" +- 10: {1, 2}, // want "redundant type from array, slice, or map composite literal" +- 20: {3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - --package a +-var _ = []struct { +- x, y int +-}{ +- {}, // want "redundant type from array, slice, or map composite literal" +- 10: {1, 2}, // want "redundant type from array, slice, or map composite literal" +- 20: {3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - --func f[T any](T) {} +-var _ = []interface{}{ +- T{}, +- 10: T{1, 2}, +- 20: T{3, 4}, +-} - --func g[T any]() T { var x T; return x } +-var _ = [][]int{ +- {}, // want "redundant type from array, slice, or map composite literal" +- {1, 2}, // want "redundant type from array, slice, or map composite literal" +- {3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - --func h[P interface{ ~*T }, T any]() {} +-var _ = [][]int{ +- ([]int{}), +- ([]int{1, 2}), +- {3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - --func _() { -- f("hello") // want "unnecessary type arguments" -- f(2) // want "unnecessary type arguments" -- _ = g[int]() -- h[*int]() // want "unnecessary type arguments" +-var _ = [][][]int{ +- {}, // want "redundant type from array, slice, or map composite literal" +- { // want "redundant type from array, slice, or map composite literal" +- {}, // want "redundant type from array, slice, or map composite literal" +- {0, 1, 2, 3}, // want "redundant type from array, slice, or map composite literal" +- {4, 5}, // want "redundant type from array, slice, or map composite literal" +- }, -} -diff -urN a/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/imported/imported.go b/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/imported/imported.go ---- a/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/imported/imported.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/imported/imported.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,7 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package imported +-var _ = map[string]T{ +- "foo": {}, // want "redundant type from array, slice, or map composite literal" +- "bar": {1, 2}, // want "redundant type from array, slice, or map composite literal" +- "bal": {3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - --func F[T any](T) {} -diff -urN a/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/imported.go b/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/imported.go ---- a/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/imported.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/imported.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,12 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-var _ = map[string]struct { +- x, y int +-}{ +- "foo": {}, // want "redundant type from array, slice, or map composite literal" +- "bar": {1, 2}, // want "redundant type from array, slice, or map composite literal" +- "bal": {3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - --package a +-var _ = map[string]interface{}{ +- "foo": T{}, +- "bar": T{1, 2}, +- "bal": T{3, 4}, +-} - --import "a/imported" +-var _ = map[string][]int{ +- "foo": {}, // want "redundant type from array, slice, or map composite literal" +- "bar": {1, 2}, // want "redundant type from array, slice, or map composite literal" +- "bal": {3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - --func _() { -- var x int -- imported.F[int](x) // want "unnecessary type arguments" +-var _ = map[string][]int{ +- "foo": ([]int{}), +- "bar": ([]int{1, 2}), +- "bal": {3, 4}, // want "redundant type from array, slice, or map composite literal" -} -diff -urN a/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/imported.go.golden b/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/imported.go.golden ---- a/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/imported.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/imported.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,12 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package a +-type Point struct { +- a int +- b int +-} - --import "a/imported" +-type Piece struct { +- a int +- b int +- c Point +- d []Point +- e *Point +- f *Point +-} - --func _() { -- var x int -- imported.F(x) // want "unnecessary type arguments" +-// from exp/4s/data.go +-var pieces3 = []Piece{ +- {0, 0, Point{4, 1}, []Point{{0, 0}, {1, 0}, {1, 0}, {1, 0}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" +- {1, 0, Point{1, 4}, []Point{{0, 0}, {0, 1}, {0, 1}, {0, 1}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" +- {2, 0, Point{4, 1}, []Point{{0, 0}, {1, 0}, {1, 0}, {1, 0}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" +- {3, 0, Point{1, 4}, []Point{{0, 0}, {0, 1}, {0, 1}, {0, 1}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" -} -diff -urN a/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/notypechange.go b/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/notypechange.go ---- a/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/notypechange.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/notypechange.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,26 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --// We should not suggest removing type arguments if doing so would change the --// resulting type. +-var _ = [42]*T{ +- {}, // want "redundant type from array, slice, or map composite literal" +- {1, 2}, // want "redundant type from array, slice, or map composite literal" +- {3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - --package a +-var _ = [...]*T{ +- {}, // want "redundant type from array, slice, or map composite literal" +- {1, 2}, // want "redundant type from array, slice, or map composite literal" +- {3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - --func id[T any](t T) T { return t } +-var _ = []*T{ +- {}, // want "redundant type from array, slice, or map composite literal" +- {1, 2}, // want "redundant type from array, slice, or map composite literal" +- {3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - --var _ = id[int](1) // want "unnecessary type arguments" --var _ = id[string]("foo") // want "unnecessary type arguments" --var _ = id[int64](2) +-var _ = []*T{ +- {}, // want "redundant type from array, slice, or map composite literal" +- 10: {1, 2}, // want "redundant type from array, slice, or map composite literal" +- 20: {3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - --func pair[T any](t T) (T, T) { return t, t } +-var _ = []*struct { +- x, y int +-}{ +- {}, // want "redundant type from array, slice, or map composite literal" +- 10: {1, 2}, // want "redundant type from array, slice, or map composite literal" +- 20: {3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - --var _, _ = pair[int](3) // want "unnecessary type arguments" --var _, _ = pair[int64](3) +-var _ = []interface{}{ +- &T{}, +- 10: &T{1, 2}, +- 20: &T{3, 4}, +-} - --func noreturn[T any](t T) {} +-var _ = []*[]int{ +- {}, // want "redundant type from array, slice, or map composite literal" +- {1, 2}, // want "redundant type from array, slice, or map composite literal" +- {3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - --func _() { -- noreturn[int64](4) -- noreturn[int](4) // want "unnecessary type arguments" +-var _ = []*[]int{ +- (&[]int{}), +- (&[]int{1, 2}), +- {3, 4}, // want "redundant type from array, slice, or map composite literal" -} -diff -urN a/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/notypechange.go.golden b/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/notypechange.go.golden ---- a/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/notypechange.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/infertypeargs/testdata/src/a/notypechange.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,26 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --// We should not suggest removing type arguments if doing so would change the --// resulting type. +-var _ = []*[]*[]int{ +- {}, // want "redundant type from array, slice, or map composite literal" +- { // want "redundant type from array, slice, or map composite literal" +- {}, // want "redundant type from array, slice, or map composite literal" +- {0, 1, 2, 3}, // want "redundant type from array, slice, or map composite literal" +- {4, 5}, // want "redundant type from array, slice, or map composite literal" +- }, +-} - --package a +-var _ = map[string]*T{ +- "foo": {}, // want "redundant type from array, slice, or map composite literal" +- "bar": {1, 2}, // want "redundant type from array, slice, or map composite literal" +- "bal": {3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - --func id[T any](t T) T { return t } +-var _ = map[string]*struct { +- x, y int +-}{ +- "foo": {}, // want "redundant type from array, slice, or map composite literal" +- "bar": {1, 2}, // want "redundant type from array, slice, or map composite literal" +- "bal": {3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - --var _ = id(1) // want "unnecessary type arguments" --var _ = id("foo") // want "unnecessary type arguments" --var _ = id[int64](2) +-var _ = map[string]interface{}{ +- "foo": &T{}, +- "bar": &T{1, 2}, +- "bal": &T{3, 4}, +-} - --func pair[T any](t T) (T, T) { return t, t } +-var _ = map[string]*[]int{ +- "foo": {}, // want "redundant type from array, slice, or map composite literal" +- "bar": {1, 2}, // want "redundant type from array, slice, or map composite literal" +- "bal": {3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - --var _, _ = pair(3) // want "unnecessary type arguments" --var _, _ = pair[int64](3) +-var _ = map[string]*[]int{ +- "foo": (&[]int{}), +- "bar": (&[]int{1, 2}), +- "bal": {3, 4}, // want "redundant type from array, slice, or map composite literal" +-} - --func noreturn[T any](t T) {} +-var pieces4 = []*Piece{ +- {0, 0, Point{4, 1}, []Point{{0, 0}, {1, 0}, {1, 0}, {1, 0}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" +- {1, 0, Point{1, 4}, []Point{{0, 0}, {0, 1}, {0, 1}, {0, 1}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" +- {2, 0, Point{4, 1}, []Point{{0, 0}, {1, 0}, {1, 0}, {1, 0}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" +- {3, 0, Point{1, 4}, []Point{{0, 0}, {0, 1}, {0, 1}, {0, 1}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" +-} - --func _() { -- noreturn[int64](4) -- noreturn(4) // want "unnecessary type arguments" +-var _ = map[T]T2{ +- {1, 2}: {3, 4}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" +- {5, 6}: {7, 8}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" +-} +- +-var _ = map[*T]*T2{ +- {1, 2}: {3, 4}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" +- {5, 6}: {7, 8}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" -} -diff -urN a/gopls/internal/lsp/analysis/nonewvars/nonewvars.go b/gopls/internal/lsp/analysis/nonewvars/nonewvars.go ---- a/gopls/internal/lsp/analysis/nonewvars/nonewvars.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/nonewvars/nonewvars.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,95 +0,0 @@ +diff -urN a/gopls/internal/analysis/simplifyrange/doc.go b/gopls/internal/analysis/simplifyrange/doc.go +--- a/gopls/internal/analysis/simplifyrange/doc.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/simplifyrange/doc.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,30 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-// Package simplifyrange defines an Analyzer that simplifies range statements. +-// https://golang.org/cmd/gofmt/#hdr-The_simplify_command +-// https://github.com/golang/go/blob/master/src/cmd/gofmt/simplify.go +-// +-// # Analyzer simplifyrange +-// +-// simplifyrange: check for range statement simplifications +-// +-// A range of the form: +-// +-// for x, _ = range v {...} +-// +-// will be simplified to: +-// +-// for x = range v {...} +-// +-// A range of the form: +-// +-// for _ = range v {...} +-// +-// will be simplified to: +-// +-// for range v {...} +-// +-// This is one of the simplifications that "gofmt -s" applies. +-package simplifyrange +diff -urN a/gopls/internal/analysis/simplifyrange/simplifyrange.go b/gopls/internal/analysis/simplifyrange/simplifyrange.go +--- a/gopls/internal/analysis/simplifyrange/simplifyrange.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/simplifyrange/simplifyrange.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,105 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --// Package nonewvars defines an Analyzer that applies suggested fixes --// to errors of the type "no new variables on left side of :=". --package nonewvars +-package simplifyrange - -import ( - "bytes" +- _ "embed" - "go/ast" -- "go/format" +- "go/printer" - "go/token" - - "golang.org/x/tools/go/analysis" @@ -9501,184 +10351,195 @@ diff -urN a/gopls/internal/lsp/analysis/nonewvars/nonewvars.go b/gopls/internal/ - "golang.org/x/tools/internal/analysisinternal" -) - --const Doc = `suggested fixes for "no new vars on left side of :=" -- --This checker provides suggested fixes for type errors of the --type "no new vars on left side of :=". For example: -- z := 1 -- z := 2 --will turn into -- z := 1 -- z = 2 --` +-//go:embed doc.go +-var doc string - -var Analyzer = &analysis.Analyzer{ -- Name: "nonewvars", -- Doc: Doc, -- Requires: []*analysis.Analyzer{inspect.Analyzer}, -- Run: run, -- RunDespiteErrors: true, +- Name: "simplifyrange", +- Doc: analysisinternal.MustExtractDoc(doc, "simplifyrange"), +- Requires: []*analysis.Analyzer{inspect.Analyzer}, +- Run: run, +- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/simplifyrange", -} - -func run(pass *analysis.Pass) (interface{}, error) { - inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) -- if len(pass.TypeErrors) == 0 { -- return nil, nil +- nodeFilter := []ast.Node{ +- (*ast.RangeStmt)(nil), - } -- -- nodeFilter := []ast.Node{(*ast.AssignStmt)(nil)} - inspect.Preorder(nodeFilter, func(n ast.Node) { -- assignStmt, _ := n.(*ast.AssignStmt) -- // We only care about ":=". -- if assignStmt.Tok != token.DEFINE { +- var copy *ast.RangeStmt +- if stmt, ok := n.(*ast.RangeStmt); ok { +- x := *stmt +- copy = &x +- } +- if copy == nil { - return - } +- end := newlineIndex(pass.Fset, copy) - -- var file *ast.File -- for _, f := range pass.Files { -- if f.Pos() <= assignStmt.Pos() && assignStmt.Pos() < f.End() { -- file = f -- break -- } +- // Range statements of the form: for i, _ := range x {} +- var old ast.Expr +- if isBlank(copy.Value) { +- old = copy.Value +- copy.Value = nil - } -- if file == nil { -- return +- // Range statements of the form: for _ := range x {} +- if isBlank(copy.Key) && copy.Value == nil { +- old = copy.Key +- copy.Key = nil - } -- -- for _, err := range pass.TypeErrors { -- if !FixesError(err.Msg) { -- continue -- } -- if assignStmt.Pos() > err.Pos || err.Pos >= assignStmt.End() { -- continue -- } -- var buf bytes.Buffer -- if err := format.Node(&buf, pass.Fset, file); err != nil { -- continue -- } -- pass.Report(analysis.Diagnostic{ -- Pos: err.Pos, -- End: analysisinternal.TypeErrorEndPos(pass.Fset, buf.Bytes(), err.Pos), -- Message: err.Msg, -- SuggestedFixes: []analysis.SuggestedFix{{ -- Message: "Change ':=' to '='", -- TextEdits: []analysis.TextEdit{{ -- Pos: err.Pos, -- End: err.Pos + 1, -- }}, -- }}, -- }) +- // Return early if neither if condition is met. +- if old == nil { +- return - } +- pass.Report(analysis.Diagnostic{ +- Pos: old.Pos(), +- End: old.End(), +- Message: "simplify range expression", +- SuggestedFixes: suggestedFixes(pass.Fset, copy, end), +- }) - }) - return nil, nil -} - --func FixesError(msg string) bool { -- return msg == "no new variables on left side of :=" --} -diff -urN a/gopls/internal/lsp/analysis/nonewvars/nonewvars_test.go b/gopls/internal/lsp/analysis/nonewvars/nonewvars_test.go ---- a/gopls/internal/lsp/analysis/nonewvars/nonewvars_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/nonewvars/nonewvars_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,22 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package nonewvars_test -- --import ( -- "testing" -- -- "golang.org/x/tools/go/analysis/analysistest" -- "golang.org/x/tools/gopls/internal/lsp/analysis/nonewvars" -- "golang.org/x/tools/internal/typeparams" --) +-func suggestedFixes(fset *token.FileSet, rng *ast.RangeStmt, end token.Pos) []analysis.SuggestedFix { +- var b bytes.Buffer +- printer.Fprint(&b, fset, rng) +- stmt := b.Bytes() +- index := bytes.Index(stmt, []byte("\n")) +- // If there is a new line character, then don't replace the body. +- if index != -1 { +- stmt = stmt[:index] +- } +- return []analysis.SuggestedFix{{ +- Message: "Remove empty value", +- TextEdits: []analysis.TextEdit{{ +- Pos: rng.Pos(), +- End: end, +- NewText: stmt[:index], +- }}, +- }} +-} - --func Test(t *testing.T) { -- testdata := analysistest.TestData() -- tests := []string{"a"} -- if typeparams.Enabled { -- tests = append(tests, "typeparams") +-func newlineIndex(fset *token.FileSet, rng *ast.RangeStmt) token.Pos { +- var b bytes.Buffer +- printer.Fprint(&b, fset, rng) +- contents := b.Bytes() +- index := bytes.Index(contents, []byte("\n")) +- if index == -1 { +- return rng.End() - } -- analysistest.RunWithSuggestedFixes(t, testdata, nonewvars.Analyzer, tests...) +- return rng.Pos() + token.Pos(index) -} -diff -urN a/gopls/internal/lsp/analysis/nonewvars/testdata/src/a/a.go b/gopls/internal/lsp/analysis/nonewvars/testdata/src/a/a.go ---- a/gopls/internal/lsp/analysis/nonewvars/testdata/src/a/a.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/nonewvars/testdata/src/a/a.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,16 +0,0 @@ +- +-func isBlank(x ast.Expr) bool { +- ident, ok := x.(*ast.Ident) +- return ok && ident.Name == "_" +-} +diff -urN a/gopls/internal/analysis/simplifyrange/simplifyrange_test.go b/gopls/internal/analysis/simplifyrange/simplifyrange_test.go +--- a/gopls/internal/analysis/simplifyrange/simplifyrange_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/simplifyrange/simplifyrange_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,17 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package nonewvars -- --import "log" +-package simplifyrange_test - --func x() { -- z := 1 -- z := 2 // want "no new variables on left side of :=" +-import ( +- "testing" - -- _, z := 3, 100 // want "no new variables on left side of :=" +- "golang.org/x/tools/go/analysis/analysistest" +- "golang.org/x/tools/gopls/internal/analysis/simplifyrange" +-) - -- log.Println(z) +-func Test(t *testing.T) { +- testdata := analysistest.TestData() +- analysistest.RunWithSuggestedFixes(t, testdata, simplifyrange.Analyzer, "a") -} -diff -urN a/gopls/internal/lsp/analysis/nonewvars/testdata/src/a/a.go.golden b/gopls/internal/lsp/analysis/nonewvars/testdata/src/a/a.go.golden ---- a/gopls/internal/lsp/analysis/nonewvars/testdata/src/a/a.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/nonewvars/testdata/src/a/a.go.golden 1970-01-01 00:00:00.000000000 +0000 +diff -urN a/gopls/internal/analysis/simplifyrange/testdata/src/a/a.go b/gopls/internal/analysis/simplifyrange/testdata/src/a/a.go +--- a/gopls/internal/analysis/simplifyrange/testdata/src/a/a.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/simplifyrange/testdata/src/a/a.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,16 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package nonewvars +-package testdata - -import "log" - --func x() { -- z := 1 -- z = 2 // want "no new variables on left side of :=" +-func m() { +- maps := make(map[string]string) +- for k, _ := range maps { // want "simplify range expression" +- log.Println(k) +- } +- for _ = range maps { // want "simplify range expression" +- } +-} +diff -urN a/gopls/internal/analysis/simplifyrange/testdata/src/a/a.go.golden b/gopls/internal/analysis/simplifyrange/testdata/src/a/a.go.golden +--- a/gopls/internal/analysis/simplifyrange/testdata/src/a/a.go.golden 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/simplifyrange/testdata/src/a/a.go.golden 1970-01-01 00:00:00.000000000 +0000 +@@ -1,16 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- _, z = 3, 100 // want "no new variables on left side of :=" +-package testdata - -- log.Println(z) --} -diff -urN a/gopls/internal/lsp/analysis/nonewvars/testdata/src/typeparams/a.go b/gopls/internal/lsp/analysis/nonewvars/testdata/src/typeparams/a.go ---- a/gopls/internal/lsp/analysis/nonewvars/testdata/src/typeparams/a.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/nonewvars/testdata/src/typeparams/a.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,6 +0,0 @@ --package nonewvars +-import "log" - --func hello[T any]() int { -- var z T -- z := 1 // want "no new variables on left side of :=" +-func m() { +- maps := make(map[string]string) +- for k := range maps { // want "simplify range expression" +- log.Println(k) +- } +- for range maps { // want "simplify range expression" +- } -} -diff -urN a/gopls/internal/lsp/analysis/nonewvars/testdata/src/typeparams/a.go.golden b/gopls/internal/lsp/analysis/nonewvars/testdata/src/typeparams/a.go.golden ---- a/gopls/internal/lsp/analysis/nonewvars/testdata/src/typeparams/a.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/nonewvars/testdata/src/typeparams/a.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,6 +0,0 @@ --package nonewvars +diff -urN a/gopls/internal/analysis/simplifyslice/doc.go b/gopls/internal/analysis/simplifyslice/doc.go +--- a/gopls/internal/analysis/simplifyslice/doc.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/simplifyslice/doc.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,22 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func hello[T any]() int { -- var z T -- z = 1 // want "no new variables on left side of :=" --} -diff -urN a/gopls/internal/lsp/analysis/noresultvalues/noresultvalues.go b/gopls/internal/lsp/analysis/noresultvalues/noresultvalues.go ---- a/gopls/internal/lsp/analysis/noresultvalues/noresultvalues.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/noresultvalues/noresultvalues.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,92 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +-// Package simplifyslice defines an Analyzer that simplifies slice statements. +-// https://github.com/golang/go/blob/master/src/cmd/gofmt/simplify.go +-// https://golang.org/cmd/gofmt/#hdr-The_simplify_command +-// +-// # Analyzer simplifyslice +-// +-// simplifyslice: check for slice simplifications +-// +-// A slice expression of the form: +-// +-// s[a:len(s)] +-// +-// will be simplified to: +-// +-// s[a:] +-// +-// This is one of the simplifications that "gofmt -s" applies. +-package simplifyslice +diff -urN a/gopls/internal/analysis/simplifyslice/simplifyslice.go b/gopls/internal/analysis/simplifyslice/simplifyslice.go +--- a/gopls/internal/analysis/simplifyslice/simplifyslice.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/simplifyslice/simplifyslice.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,88 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --// Package noresultvalues defines an Analyzer that applies suggested fixes --// to errors of the type "no result values expected". --package noresultvalues +-package simplifyslice - -import ( - "bytes" +- _ "embed" +- "fmt" - "go/ast" -- "go/format" -- "strings" +- "go/printer" - - "golang.org/x/tools/go/analysis" - "golang.org/x/tools/go/analysis/passes/inspect" @@ -9686,2016 +10547,2292 @@ diff -urN a/gopls/internal/lsp/analysis/noresultvalues/noresultvalues.go b/gopls - "golang.org/x/tools/internal/analysisinternal" -) - --const Doc = `suggested fixes for unexpected return values -- --This checker provides suggested fixes for type errors of the --type "no result values expected" or "too many return values". --For example: -- func z() { return nil } --will turn into -- func z() { return } --` +-//go:embed doc.go +-var doc string - -var Analyzer = &analysis.Analyzer{ -- Name: "noresultvalues", -- Doc: Doc, -- Requires: []*analysis.Analyzer{inspect.Analyzer}, -- Run: run, -- RunDespiteErrors: true, +- Name: "simplifyslice", +- Doc: analysisinternal.MustExtractDoc(doc, "simplifyslice"), +- Requires: []*analysis.Analyzer{inspect.Analyzer}, +- Run: run, +- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/simplifyslice", -} - +-// Note: We could also simplify slice expressions of the form s[0:b] to s[:b] +-// but we leave them as is since sometimes we want to be very explicit +-// about the lower bound. +-// An example where the 0 helps: +-// x, y, z := b[0:2], b[2:4], b[4:6] +-// An example where it does not: +-// x, y := b[:n], b[n:] +- -func run(pass *analysis.Pass) (interface{}, error) { - inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) -- if len(pass.TypeErrors) == 0 { -- return nil, nil +- nodeFilter := []ast.Node{ +- (*ast.SliceExpr)(nil), - } -- -- nodeFilter := []ast.Node{(*ast.ReturnStmt)(nil)} - inspect.Preorder(nodeFilter, func(n ast.Node) { -- retStmt, _ := n.(*ast.ReturnStmt) -- -- var file *ast.File -- for _, f := range pass.Files { -- if f.Pos() <= retStmt.Pos() && retStmt.Pos() < f.End() { -- file = f -- break -- } +- expr := n.(*ast.SliceExpr) +- // - 3-index slices always require the 2nd and 3rd index +- if expr.Max != nil { +- return - } -- if file == nil { +- s, ok := expr.X.(*ast.Ident) +- // the array/slice object is a single, resolved identifier +- if !ok || s.Obj == nil { - return - } -- -- for _, err := range pass.TypeErrors { -- if !FixesError(err.Msg) { -- continue -- } -- if retStmt.Pos() >= err.Pos || err.Pos >= retStmt.End() { -- continue -- } -- var buf bytes.Buffer -- if err := format.Node(&buf, pass.Fset, file); err != nil { -- continue -- } -- pass.Report(analysis.Diagnostic{ -- Pos: err.Pos, -- End: analysisinternal.TypeErrorEndPos(pass.Fset, buf.Bytes(), err.Pos), -- Message: err.Msg, -- SuggestedFixes: []analysis.SuggestedFix{{ -- Message: "Delete return values", -- TextEdits: []analysis.TextEdit{{ -- Pos: retStmt.Pos(), -- End: retStmt.End(), -- NewText: []byte("return"), -- }}, -- }}, -- }) +- call, ok := expr.High.(*ast.CallExpr) +- // the high expression is a function call with a single argument +- if !ok || len(call.Args) != 1 || call.Ellipsis.IsValid() { +- return +- } +- fun, ok := call.Fun.(*ast.Ident) +- // the function called is "len" and it is not locally defined; and +- // because we don't have dot imports, it must be the predefined len() +- if !ok || fun.Name != "len" || fun.Obj != nil { +- return +- } +- arg, ok := call.Args[0].(*ast.Ident) +- // the len argument is the array/slice object +- if !ok || arg.Obj != s.Obj { +- return - } +- var b bytes.Buffer +- printer.Fprint(&b, pass.Fset, expr.High) +- pass.Report(analysis.Diagnostic{ +- Pos: expr.High.Pos(), +- End: expr.High.End(), +- Message: fmt.Sprintf("unneeded: %s", b.String()), +- SuggestedFixes: []analysis.SuggestedFix{{ +- Message: fmt.Sprintf("Remove '%s'", b.String()), +- TextEdits: []analysis.TextEdit{{ +- Pos: expr.High.Pos(), +- End: expr.High.End(), +- NewText: []byte{}, +- }}, +- }}, +- }) - }) - return nil, nil -} -- --func FixesError(msg string) bool { -- return msg == "no result values expected" || -- strings.HasPrefix(msg, "too many return values") && strings.Contains(msg, "want ()") --} -diff -urN a/gopls/internal/lsp/analysis/noresultvalues/noresultvalues_test.go b/gopls/internal/lsp/analysis/noresultvalues/noresultvalues_test.go ---- a/gopls/internal/lsp/analysis/noresultvalues/noresultvalues_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/noresultvalues/noresultvalues_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,22 +0,0 @@ +diff -urN a/gopls/internal/analysis/simplifyslice/simplifyslice_test.go b/gopls/internal/analysis/simplifyslice/simplifyslice_test.go +--- a/gopls/internal/analysis/simplifyslice/simplifyslice_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/simplifyslice/simplifyslice_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,17 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package noresultvalues_test +-package simplifyslice_test - -import ( - "testing" - - "golang.org/x/tools/go/analysis/analysistest" -- "golang.org/x/tools/gopls/internal/lsp/analysis/noresultvalues" -- "golang.org/x/tools/internal/typeparams" +- "golang.org/x/tools/gopls/internal/analysis/simplifyslice" -) - -func Test(t *testing.T) { - testdata := analysistest.TestData() -- tests := []string{"a"} -- if typeparams.Enabled { -- tests = append(tests, "typeparams") -- } -- analysistest.RunWithSuggestedFixes(t, testdata, noresultvalues.Analyzer, tests...) +- analysistest.RunWithSuggestedFixes(t, testdata, simplifyslice.Analyzer, "a", "typeparams") -} -diff -urN a/gopls/internal/lsp/analysis/noresultvalues/testdata/src/a/a.go b/gopls/internal/lsp/analysis/noresultvalues/testdata/src/a/a.go ---- a/gopls/internal/lsp/analysis/noresultvalues/testdata/src/a/a.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/noresultvalues/testdata/src/a/a.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,9 +0,0 @@ +diff -urN a/gopls/internal/analysis/simplifyslice/testdata/src/a/a.go b/gopls/internal/analysis/simplifyslice/testdata/src/a/a.go +--- a/gopls/internal/analysis/simplifyslice/testdata/src/a/a.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/simplifyslice/testdata/src/a/a.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,70 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package noresultvalues +-package testdata - --func x() { return nil } // want `no result values expected|too many return values` +-var ( +- a [10]byte +- b [20]float32 +- s []int +- t struct { +- s []byte +- } - --func y() { return nil, "hello" } // want `no result values expected|too many return values` -diff -urN a/gopls/internal/lsp/analysis/noresultvalues/testdata/src/a/a.go.golden b/gopls/internal/lsp/analysis/noresultvalues/testdata/src/a/a.go.golden ---- a/gopls/internal/lsp/analysis/noresultvalues/testdata/src/a/a.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/noresultvalues/testdata/src/a/a.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,9 +0,0 @@ +- _ = a[0:] +- _ = a[1:10] +- _ = a[2:len(a)] // want "unneeded: len\\(a\\)" +- _ = a[3:(len(a))] +- _ = a[len(a)-1 : len(a)] // want "unneeded: len\\(a\\)" +- _ = a[2:len(a):len(a)] +- +- _ = a[:] +- _ = a[:10] +- _ = a[:len(a)] // want "unneeded: len\\(a\\)" +- _ = a[:(len(a))] +- _ = a[:len(a)-1] +- _ = a[:len(a):len(a)] +- +- _ = s[0:] +- _ = s[1:10] +- _ = s[2:len(s)] // want "unneeded: len\\(s\\)" +- _ = s[3:(len(s))] +- _ = s[len(a) : len(s)-1] +- _ = s[0:len(b)] +- _ = s[2:len(s):len(s)] +- +- _ = s[:] +- _ = s[:10] +- _ = s[:len(s)] // want "unneeded: len\\(s\\)" +- _ = s[:(len(s))] +- _ = s[:len(s)-1] +- _ = s[:len(b)] +- _ = s[:len(s):len(s)] +- +- _ = t.s[0:] +- _ = t.s[1:10] +- _ = t.s[2:len(t.s)] +- _ = t.s[3:(len(t.s))] +- _ = t.s[len(a) : len(t.s)-1] +- _ = t.s[0:len(b)] +- _ = t.s[2:len(t.s):len(t.s)] +- +- _ = t.s[:] +- _ = t.s[:10] +- _ = t.s[:len(t.s)] +- _ = t.s[:(len(t.s))] +- _ = t.s[:len(t.s)-1] +- _ = t.s[:len(b)] +- _ = t.s[:len(t.s):len(t.s)] +-) +- +-func _() { +- s := s[0:len(s)] // want "unneeded: len\\(s\\)" +- _ = s +-} +- +-func m() { +- maps := []int{} +- _ = maps[1:len(maps)] // want "unneeded: len\\(maps\\)" +-} +diff -urN a/gopls/internal/analysis/simplifyslice/testdata/src/a/a.go.golden b/gopls/internal/analysis/simplifyslice/testdata/src/a/a.go.golden +--- a/gopls/internal/analysis/simplifyslice/testdata/src/a/a.go.golden 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/simplifyslice/testdata/src/a/a.go.golden 1970-01-01 00:00:00.000000000 +0000 +@@ -1,70 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package noresultvalues +-package testdata - --func x() { return } // want `no result values expected|too many return values` +-var ( +- a [10]byte +- b [20]float32 +- s []int +- t struct { +- s []byte +- } - --func y() { return } // want `no result values expected|too many return values` -diff -urN a/gopls/internal/lsp/analysis/noresultvalues/testdata/src/typeparams/a.go b/gopls/internal/lsp/analysis/noresultvalues/testdata/src/typeparams/a.go ---- a/gopls/internal/lsp/analysis/noresultvalues/testdata/src/typeparams/a.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/noresultvalues/testdata/src/typeparams/a.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,6 +0,0 @@ --package noresult +- _ = a[0:] +- _ = a[1:10] +- _ = a[2:] // want "unneeded: len\\(a\\)" +- _ = a[3:(len(a))] +- _ = a[len(a)-1:] // want "unneeded: len\\(a\\)" +- _ = a[2:len(a):len(a)] - --func hello[T any]() { -- var z T -- return z // want `no result values expected|too many return values` +- _ = a[:] +- _ = a[:10] +- _ = a[:] // want "unneeded: len\\(a\\)" +- _ = a[:(len(a))] +- _ = a[:len(a)-1] +- _ = a[:len(a):len(a)] +- +- _ = s[0:] +- _ = s[1:10] +- _ = s[2:] // want "unneeded: len\\(s\\)" +- _ = s[3:(len(s))] +- _ = s[len(a) : len(s)-1] +- _ = s[0:len(b)] +- _ = s[2:len(s):len(s)] +- +- _ = s[:] +- _ = s[:10] +- _ = s[:] // want "unneeded: len\\(s\\)" +- _ = s[:(len(s))] +- _ = s[:len(s)-1] +- _ = s[:len(b)] +- _ = s[:len(s):len(s)] +- +- _ = t.s[0:] +- _ = t.s[1:10] +- _ = t.s[2:len(t.s)] +- _ = t.s[3:(len(t.s))] +- _ = t.s[len(a) : len(t.s)-1] +- _ = t.s[0:len(b)] +- _ = t.s[2:len(t.s):len(t.s)] +- +- _ = t.s[:] +- _ = t.s[:10] +- _ = t.s[:len(t.s)] +- _ = t.s[:(len(t.s))] +- _ = t.s[:len(t.s)-1] +- _ = t.s[:len(b)] +- _ = t.s[:len(t.s):len(t.s)] +-) +- +-func _() { +- s := s[0:] // want "unneeded: len\\(s\\)" +- _ = s -} -diff -urN a/gopls/internal/lsp/analysis/noresultvalues/testdata/src/typeparams/a.go.golden b/gopls/internal/lsp/analysis/noresultvalues/testdata/src/typeparams/a.go.golden ---- a/gopls/internal/lsp/analysis/noresultvalues/testdata/src/typeparams/a.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/noresultvalues/testdata/src/typeparams/a.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,6 +0,0 @@ --package noresult - --func hello[T any]() { -- var z T -- return // want `no result values expected|too many return values` +-func m() { +- maps := []int{} +- _ = maps[1:] // want "unneeded: len\\(maps\\)" -} -diff -urN a/gopls/internal/lsp/analysis/simplifycompositelit/simplifycompositelit.go b/gopls/internal/lsp/analysis/simplifycompositelit/simplifycompositelit.go ---- a/gopls/internal/lsp/analysis/simplifycompositelit/simplifycompositelit.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/simplifycompositelit/simplifycompositelit.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,196 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/analysis/simplifyslice/testdata/src/typeparams/typeparams.go b/gopls/internal/analysis/simplifyslice/testdata/src/typeparams/typeparams.go +--- a/gopls/internal/analysis/simplifyslice/testdata/src/typeparams/typeparams.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/simplifyslice/testdata/src/typeparams/typeparams.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,36 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --// Package simplifycompositelit defines an Analyzer that simplifies composite literals. --// https://github.com/golang/go/blob/master/src/cmd/gofmt/simplify.go --// https://golang.org/cmd/gofmt/#hdr-The_simplify_command --package simplifycompositelit +-package testdata +- +-type List[E any] []E +- +-// TODO(suzmue): add a test for generic slice expressions when https://github.com/golang/go/issues/48618 is closed. +-// type S interface{ ~[]int } +- +-var ( +- a [10]byte +- b [20]float32 +- p List[int] +- +- _ = p[0:] +- _ = p[1:10] +- _ = p[2:len(p)] // want "unneeded: len\\(p\\)" +- _ = p[3:(len(p))] +- _ = p[len(a) : len(p)-1] +- _ = p[0:len(b)] +- _ = p[2:len(p):len(p)] +- +- _ = p[:] +- _ = p[:10] +- _ = p[:len(p)] // want "unneeded: len\\(p\\)" +- _ = p[:(len(p))] +- _ = p[:len(p)-1] +- _ = p[:len(b)] +- _ = p[:len(p):len(p)] +-) +- +-func foo[E any](a List[E]) { +- _ = a[0:len(a)] // want "unneeded: len\\(a\\)" +-} +diff -urN a/gopls/internal/analysis/simplifyslice/testdata/src/typeparams/typeparams.go.golden b/gopls/internal/analysis/simplifyslice/testdata/src/typeparams/typeparams.go.golden +--- a/gopls/internal/analysis/simplifyslice/testdata/src/typeparams/typeparams.go.golden 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/simplifyslice/testdata/src/typeparams/typeparams.go.golden 1970-01-01 00:00:00.000000000 +0000 +@@ -1,36 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package testdata +- +-type List[E any] []E +- +-// TODO(suzmue): add a test for generic slice expressions when https://github.com/golang/go/issues/48618 is closed. +-// type S interface{ ~[]int } +- +-var ( +- a [10]byte +- b [20]float32 +- p List[int] +- +- _ = p[0:] +- _ = p[1:10] +- _ = p[2:] // want "unneeded: len\\(p\\)" +- _ = p[3:(len(p))] +- _ = p[len(a) : len(p)-1] +- _ = p[0:len(b)] +- _ = p[2:len(p):len(p)] +- +- _ = p[:] +- _ = p[:10] +- _ = p[:] // want "unneeded: len\\(p\\)" +- _ = p[:(len(p))] +- _ = p[:len(p)-1] +- _ = p[:len(b)] +- _ = p[:len(p):len(p)] +-) +- +-func foo[E any](a List[E]) { +- _ = a[0:] // want "unneeded: len\\(a\\)" +-} +diff -urN a/gopls/internal/analysis/stubmethods/doc.go b/gopls/internal/analysis/stubmethods/doc.go +--- a/gopls/internal/analysis/stubmethods/doc.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/stubmethods/doc.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,38 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-// Package stubmethods defines a code action for missing interface methods. +-// +-// # Analyzer stubmethods +-// +-// stubmethods: detect missing methods and fix with stub implementations +-// +-// This analyzer detects type-checking errors due to missing methods +-// in assignments from concrete types to interface types, and offers +-// a suggested fix that will create a set of stub methods so that +-// the concrete type satisfies the interface. +-// +-// For example, this function will not compile because the value +-// NegativeErr{} does not implement the "error" interface: +-// +-// func sqrt(x float64) (float64, error) { +-// if x < 0 { +-// return 0, NegativeErr{} // error: missing method +-// } +-// ... +-// } +-// +-// type NegativeErr struct{} +-// +-// This analyzer will suggest a fix to declare this method: +-// +-// // Error implements error.Error. +-// func (NegativeErr) Error() string { +-// panic("unimplemented") +-// } +-// +-// (At least, it appears to behave that way, but technically it +-// doesn't use the SuggestedFix mechanism and the stub is created by +-// logic in gopls's golang.stub function.) +-package stubmethods +diff -urN a/gopls/internal/analysis/stubmethods/stubmethods.go b/gopls/internal/analysis/stubmethods/stubmethods.go +--- a/gopls/internal/analysis/stubmethods/stubmethods.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/stubmethods/stubmethods.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,403 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package stubmethods - -import ( - "bytes" +- _ "embed" - "fmt" - "go/ast" -- "go/printer" +- "go/format" - "go/token" -- "reflect" +- "go/types" +- "strings" - - "golang.org/x/tools/go/analysis" -- "golang.org/x/tools/go/analysis/passes/inspect" -- "golang.org/x/tools/go/ast/inspector" +- "golang.org/x/tools/go/ast/astutil" +- "golang.org/x/tools/gopls/internal/util/typesutil" +- "golang.org/x/tools/internal/aliases" +- "golang.org/x/tools/internal/analysisinternal" +- "golang.org/x/tools/internal/typesinternal" -) - --const Doc = `check for composite literal simplifications -- --An array, slice, or map composite literal of the form: -- []T{T{}, T{}} --will be simplified to: -- []T{{}, {}} -- --This is one of the simplifications that "gofmt -s" applies.` +-//go:embed doc.go +-var doc string - -var Analyzer = &analysis.Analyzer{ -- Name: "simplifycompositelit", -- Doc: Doc, -- Requires: []*analysis.Analyzer{inspect.Analyzer}, -- Run: run, +- Name: "stubmethods", +- Doc: analysisinternal.MustExtractDoc(doc, "stubmethods"), +- Run: run, +- RunDespiteErrors: true, +- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/stubmethods", -} - +-// TODO(rfindley): remove this thin wrapper around the stubmethods refactoring, +-// and eliminate the stubmethods analyzer. +-// +-// Previous iterations used the analysis framework for computing refactorings, +-// which proved inefficient. -func run(pass *analysis.Pass) (interface{}, error) { -- inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) -- nodeFilter := []ast.Node{(*ast.CompositeLit)(nil)} -- inspect.Preorder(nodeFilter, func(n ast.Node) { -- expr := n.(*ast.CompositeLit) -- -- outer := expr -- var keyType, eltType ast.Expr -- switch typ := outer.Type.(type) { -- case *ast.ArrayType: -- eltType = typ.Elt -- case *ast.MapType: -- keyType = typ.Key -- eltType = typ.Value -- } -- -- if eltType == nil { -- return -- } -- var ktyp reflect.Value -- if keyType != nil { -- ktyp = reflect.ValueOf(keyType) +- for _, err := range pass.TypeErrors { +- var file *ast.File +- for _, f := range pass.Files { +- if f.Pos() <= err.Pos && err.Pos < f.End() { +- file = f +- break +- } - } -- typ := reflect.ValueOf(eltType) -- for _, x := range outer.Elts { -- // look at value of indexed/named elements -- if t, ok := x.(*ast.KeyValueExpr); ok { -- if keyType != nil { -- simplifyLiteral(pass, ktyp, keyType, t.Key) -- } -- x = t.Value +- // Get the end position of the error. +- _, _, end, ok := typesinternal.ReadGo116ErrorData(err) +- if !ok { +- var buf bytes.Buffer +- if err := format.Node(&buf, pass.Fset, file); err != nil { +- continue - } -- simplifyLiteral(pass, typ, eltType, x) +- end = analysisinternal.TypeErrorEndPos(pass.Fset, buf.Bytes(), err.Pos) - } -- }) +- if diag, ok := DiagnosticForError(pass.Fset, file, err.Pos, end, err.Msg, pass.TypesInfo); ok { +- pass.Report(diag) +- } +- } +- - return nil, nil -} - --func simplifyLiteral(pass *analysis.Pass, typ reflect.Value, astType, x ast.Expr) { -- // if the element is a composite literal and its literal type -- // matches the outer literal's element type exactly, the inner -- // literal type may be omitted -- if inner, ok := x.(*ast.CompositeLit); ok && match(typ, reflect.ValueOf(inner.Type)) { -- var b bytes.Buffer -- printer.Fprint(&b, pass.Fset, inner.Type) -- createDiagnostic(pass, inner.Type.Pos(), inner.Type.End(), b.String()) +-// MatchesMessage reports whether msg matches the error message sought after by +-// the stubmethods fix. +-func MatchesMessage(msg string) bool { +- return strings.Contains(msg, "missing method") || strings.HasPrefix(msg, "cannot convert") || strings.Contains(msg, "not implement") +-} +- +-// DiagnosticForError computes a diagnostic suggesting to implement an +-// interface to fix the type checking error defined by (start, end, msg). +-// +-// If no such fix is possible, the second result is false. +-func DiagnosticForError(fset *token.FileSet, file *ast.File, start, end token.Pos, msg string, info *types.Info) (analysis.Diagnostic, bool) { +- if !MatchesMessage(msg) { +- return analysis.Diagnostic{}, false - } -- // if the outer literal's element type is a pointer type *T -- // and the element is & of a composite literal of type T, -- // the inner &T may be omitted. -- if ptr, ok := astType.(*ast.StarExpr); ok { -- if addr, ok := x.(*ast.UnaryExpr); ok && addr.Op == token.AND { -- if inner, ok := addr.X.(*ast.CompositeLit); ok { -- if match(reflect.ValueOf(ptr.X), reflect.ValueOf(inner.Type)) { -- var b bytes.Buffer -- printer.Fprint(&b, pass.Fset, inner.Type) -- // Account for the & by subtracting 1 from typ.Pos(). -- createDiagnostic(pass, inner.Type.Pos()-1, inner.Type.End(), "&"+b.String()) -- } +- +- path, _ := astutil.PathEnclosingInterval(file, start, end) +- si := GetStubInfo(fset, info, path, start) +- if si == nil { +- return analysis.Diagnostic{}, false +- } +- qf := typesutil.FileQualifier(file, si.Concrete.Obj().Pkg(), info) +- iface := types.TypeString(si.Interface.Type(), qf) +- return analysis.Diagnostic{ +- Pos: start, +- End: end, +- Message: msg, +- Category: FixCategory, +- SuggestedFixes: []analysis.SuggestedFix{{ +- Message: fmt.Sprintf("Declare missing methods of %s", iface), +- // No TextEdits => computed later by gopls. +- }}, +- }, true +-} +- +-const FixCategory = "stubmethods" // recognized by gopls ApplyFix +- +-// StubInfo represents a concrete type +-// that wants to stub out an interface type +-type StubInfo struct { +- // Interface is the interface that the client wants to implement. +- // When the interface is defined, the underlying object will be a TypeName. +- // Note that we keep track of types.Object instead of types.Type in order +- // to keep a reference to the declaring object's package and the ast file +- // in the case where the concrete type file requires a new import that happens to be renamed +- // in the interface file. +- // TODO(marwan-at-work): implement interface literals. +- Fset *token.FileSet // the FileSet used to type-check the types below +- Interface *types.TypeName +- Concrete *types.Named +- Pointer bool +-} +- +-// GetStubInfo determines whether the "missing method error" +-// can be used to deduced what the concrete and interface types are. +-// +-// TODO(adonovan): this function (and its following 5 helpers) tries +-// to deduce a pair of (concrete, interface) types that are related by +-// an assignment, either explicitly or through a return statement or +-// function call. This is essentially what the refactor/satisfy does, +-// more generally. Refactor to share logic, after auditing 'satisfy' +-// for safety on ill-typed code. +-func GetStubInfo(fset *token.FileSet, info *types.Info, path []ast.Node, pos token.Pos) *StubInfo { +- for _, n := range path { +- switch n := n.(type) { +- case *ast.ValueSpec: +- return fromValueSpec(fset, info, n, pos) +- case *ast.ReturnStmt: +- // An error here may not indicate a real error the user should know about, but it may. +- // Therefore, it would be best to log it out for debugging/reporting purposes instead of ignoring +- // it. However, event.Log takes a context which is not passed via the analysis package. +- // TODO(marwan-at-work): properly log this error. +- si, _ := fromReturnStmt(fset, info, pos, path, n) +- return si +- case *ast.AssignStmt: +- return fromAssignStmt(fset, info, n, pos) +- case *ast.CallExpr: +- // Note that some call expressions don't carry the interface type +- // because they don't point to a function or method declaration elsewhere. +- // For eaxmple, "var Interface = (*Concrete)(nil)". In that case, continue +- // this loop to encounter other possibilities such as *ast.ValueSpec or others. +- si := fromCallExpr(fset, info, pos, n) +- if si != nil { +- return si - } - } - } +- return nil -} - --func createDiagnostic(pass *analysis.Pass, start, end token.Pos, typ string) { -- pass.Report(analysis.Diagnostic{ -- Pos: start, -- End: end, -- Message: "redundant type from array, slice, or map composite literal", -- SuggestedFixes: []analysis.SuggestedFix{{ -- Message: fmt.Sprintf("Remove '%s'", typ), -- TextEdits: []analysis.TextEdit{{ -- Pos: start, -- End: end, -- NewText: []byte{}, -- }}, -- }}, -- }) --} +-// fromCallExpr tries to find an *ast.CallExpr's function declaration and +-// analyzes a function call's signature against the passed in parameter to deduce +-// the concrete and interface types. +-func fromCallExpr(fset *token.FileSet, info *types.Info, pos token.Pos, call *ast.CallExpr) *StubInfo { +- // Find argument containing pos. +- argIdx := -1 +- var arg ast.Expr +- for i, callArg := range call.Args { +- if callArg.Pos() <= pos && pos <= callArg.End() { +- argIdx = i +- arg = callArg +- break +- } +- } +- if arg == nil { +- return nil +- } - --// match reports whether pattern matches val, --// recording wildcard submatches in m. --// If m == nil, match checks whether pattern == val. --// from https://github.com/golang/go/blob/26154f31ad6c801d8bad5ef58df1e9263c6beec7/src/cmd/gofmt/rewrite.go#L160 --func match(pattern, val reflect.Value) bool { -- // Otherwise, pattern and val must match recursively. -- if !pattern.IsValid() || !val.IsValid() { -- return !pattern.IsValid() && !val.IsValid() +- concType, pointer := concreteType(arg, info) +- if concType == nil || concType.Obj().Pkg() == nil { +- return nil - } -- if pattern.Type() != val.Type() { -- return false +- tv, ok := info.Types[call.Fun] +- if !ok { +- return nil +- } +- sig, ok := aliases.Unalias(tv.Type).(*types.Signature) +- if !ok { +- return nil +- } +- var paramType types.Type +- if sig.Variadic() && argIdx >= sig.Params().Len()-1 { +- v := sig.Params().At(sig.Params().Len() - 1) +- if s, _ := v.Type().(*types.Slice); s != nil { +- paramType = s.Elem() +- } +- } else if argIdx < sig.Params().Len() { +- paramType = sig.Params().At(argIdx).Type() - } +- if paramType == nil { +- return nil // A type error prevents us from determining the param type. +- } +- iface := ifaceObjFromType(paramType) +- if iface == nil { +- return nil +- } +- return &StubInfo{ +- Fset: fset, +- Concrete: concType, +- Pointer: pointer, +- Interface: iface, +- } +-} - -- // Special cases. -- switch pattern.Type() { -- case identType: -- // For identifiers, only the names need to match -- // (and none of the other *ast.Object information). -- // This is a common case, handle it all here instead -- // of recursing down any further via reflection. -- p := pattern.Interface().(*ast.Ident) -- v := val.Interface().(*ast.Ident) -- return p == nil && v == nil || p != nil && v != nil && p.Name == v.Name -- case objectPtrType, positionType: -- // object pointers and token positions always match -- return true -- case callExprType: -- // For calls, the Ellipsis fields (token.Position) must -- // match since that is how f(x) and f(x...) are different. -- // Check them here but fall through for the remaining fields. -- p := pattern.Interface().(*ast.CallExpr) -- v := val.Interface().(*ast.CallExpr) -- if p.Ellipsis.IsValid() != v.Ellipsis.IsValid() { -- return false +-// fromReturnStmt analyzes a "return" statement to extract +-// a concrete type that is trying to be returned as an interface type. +-// +-// For example, func() io.Writer { return myType{} } +-// would return StubInfo with the interface being io.Writer and the concrete type being myType{}. +-func fromReturnStmt(fset *token.FileSet, info *types.Info, pos token.Pos, path []ast.Node, ret *ast.ReturnStmt) (*StubInfo, error) { +- // Find return operand containing pos. +- returnIdx := -1 +- for i, r := range ret.Results { +- if r.Pos() <= pos && pos <= r.End() { +- returnIdx = i +- break - } - } +- if returnIdx == -1 { +- return nil, fmt.Errorf("pos %d not within return statement bounds: [%d-%d]", pos, ret.Pos(), ret.End()) +- } - -- p := reflect.Indirect(pattern) -- v := reflect.Indirect(val) -- if !p.IsValid() || !v.IsValid() { -- return !p.IsValid() && !v.IsValid() +- concType, pointer := concreteType(ret.Results[returnIdx], info) +- if concType == nil || concType.Obj().Pkg() == nil { +- return nil, nil +- } +- funcType := enclosingFunction(path, info) +- if funcType == nil { +- return nil, fmt.Errorf("could not find the enclosing function of the return statement") +- } +- if len(funcType.Results.List) != len(ret.Results) { +- return nil, fmt.Errorf("%d-operand return statement in %d-result function", +- len(ret.Results), +- len(funcType.Results.List)) +- } +- iface := ifaceType(funcType.Results.List[returnIdx].Type, info) +- if iface == nil { +- return nil, nil - } +- return &StubInfo{ +- Fset: fset, +- Concrete: concType, +- Pointer: pointer, +- Interface: iface, +- }, nil +-} - -- switch p.Kind() { -- case reflect.Slice: -- if p.Len() != v.Len() { -- return false -- } -- for i := 0; i < p.Len(); i++ { -- if !match(p.Index(i), v.Index(i)) { -- return false -- } +-// fromValueSpec returns *StubInfo from a variable declaration such as +-// var x io.Writer = &T{} +-func fromValueSpec(fset *token.FileSet, info *types.Info, spec *ast.ValueSpec, pos token.Pos) *StubInfo { +- // Find RHS element containing pos. +- var rhs ast.Expr +- for _, r := range spec.Values { +- if r.Pos() <= pos && pos <= r.End() { +- rhs = r +- break - } -- return true +- } +- if rhs == nil { +- return nil // e.g. pos was on the LHS (#64545) +- } - -- case reflect.Struct: -- for i := 0; i < p.NumField(); i++ { -- if !match(p.Field(i), v.Field(i)) { -- return false +- // Possible implicit/explicit conversion to interface type? +- ifaceNode := spec.Type // var _ myInterface = ... +- if call, ok := rhs.(*ast.CallExpr); ok && ifaceNode == nil && len(call.Args) == 1 { +- // var _ = myInterface(v) +- ifaceNode = call.Fun +- rhs = call.Args[0] +- } +- concType, pointer := concreteType(rhs, info) +- if concType == nil || concType.Obj().Pkg() == nil { +- return nil +- } +- ifaceObj := ifaceType(ifaceNode, info) +- if ifaceObj == nil { +- return nil +- } +- return &StubInfo{ +- Fset: fset, +- Concrete: concType, +- Interface: ifaceObj, +- Pointer: pointer, +- } +-} +- +-// fromAssignStmt returns *StubInfo from a variable assignment such as +-// var x io.Writer +-// x = &T{} +-func fromAssignStmt(fset *token.FileSet, info *types.Info, assign *ast.AssignStmt, pos token.Pos) *StubInfo { +- // The interface conversion error in an assignment is against the RHS: +- // +- // var x io.Writer +- // x = &T{} // error: missing method +- // ^^^^ +- // +- // Find RHS element containing pos. +- var lhs, rhs ast.Expr +- for i, r := range assign.Rhs { +- if r.Pos() <= pos && pos <= r.End() { +- if i >= len(assign.Lhs) { +- // This should never happen as we would get a +- // "cannot assign N values to M variables" +- // before we get an interface conversion error. +- // But be defensive. +- return nil - } +- lhs = assign.Lhs[i] +- rhs = r +- break - } -- return true +- } +- if lhs == nil || rhs == nil { +- return nil +- } - -- case reflect.Interface: -- return match(p.Elem(), v.Elem()) +- ifaceObj := ifaceType(lhs, info) +- if ifaceObj == nil { +- return nil +- } +- concType, pointer := concreteType(rhs, info) +- if concType == nil || concType.Obj().Pkg() == nil { +- return nil +- } +- return &StubInfo{ +- Fset: fset, +- Concrete: concType, +- Interface: ifaceObj, +- Pointer: pointer, - } +-} - -- // Handle token integers, etc. -- return p.Interface() == v.Interface() +-// ifaceType returns the named interface type to which e refers, if any. +-func ifaceType(e ast.Expr, info *types.Info) *types.TypeName { +- tv, ok := info.Types[e] +- if !ok { +- return nil +- } +- return ifaceObjFromType(tv.Type) -} - --// Values/types for special cases. --var ( -- identType = reflect.TypeOf((*ast.Ident)(nil)) -- objectPtrType = reflect.TypeOf((*ast.Object)(nil)) -- positionType = reflect.TypeOf(token.NoPos) -- callExprType = reflect.TypeOf((*ast.CallExpr)(nil)) --) -diff -urN a/gopls/internal/lsp/analysis/simplifycompositelit/simplifycompositelit_test.go b/gopls/internal/lsp/analysis/simplifycompositelit/simplifycompositelit_test.go ---- a/gopls/internal/lsp/analysis/simplifycompositelit/simplifycompositelit_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/simplifycompositelit/simplifycompositelit_test.go 1970-01-01 00:00:00.000000000 +0000 +-func ifaceObjFromType(t types.Type) *types.TypeName { +- named, ok := aliases.Unalias(t).(*types.Named) +- if !ok { +- return nil +- } +- if !types.IsInterface(named) { +- return nil +- } +- // Interfaces defined in the "builtin" package return nil a Pkg(). +- // But they are still real interfaces that we need to make a special case for. +- // Therefore, protect gopls from panicking if a new interface type was added in the future. +- if named.Obj().Pkg() == nil && named.Obj().Name() != "error" { +- return nil +- } +- return named.Obj() +-} +- +-// concreteType tries to extract the *types.Named that defines +-// the concrete type given the ast.Expr where the "missing method" +-// or "conversion" errors happened. If the concrete type is something +-// that cannot have methods defined on it (such as basic types), this +-// method will return a nil *types.Named. The second return parameter +-// is a boolean that indicates whether the concreteType was defined as a +-// pointer or value. +-func concreteType(e ast.Expr, info *types.Info) (*types.Named, bool) { +- tv, ok := info.Types[e] +- if !ok { +- return nil, false +- } +- typ := tv.Type +- ptr, isPtr := aliases.Unalias(typ).(*types.Pointer) +- if isPtr { +- typ = ptr.Elem() +- } +- named, ok := aliases.Unalias(typ).(*types.Named) +- if !ok { +- return nil, false +- } +- return named, isPtr +-} +- +-// enclosingFunction returns the signature and type of the function +-// enclosing the given position. +-func enclosingFunction(path []ast.Node, info *types.Info) *ast.FuncType { +- for _, node := range path { +- switch t := node.(type) { +- case *ast.FuncDecl: +- if _, ok := info.Defs[t.Name]; ok { +- return t.Type +- } +- case *ast.FuncLit: +- if _, ok := info.Types[t]; ok { +- return t.Type +- } +- } +- } +- return nil +-} +diff -urN a/gopls/internal/analysis/stubmethods/stubmethods_test.go b/gopls/internal/analysis/stubmethods/stubmethods_test.go +--- a/gopls/internal/analysis/stubmethods/stubmethods_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/stubmethods/stubmethods_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,17 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +-// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package simplifycompositelit_test +-package stubmethods_test - -import ( - "testing" - - "golang.org/x/tools/go/analysis/analysistest" -- "golang.org/x/tools/gopls/internal/lsp/analysis/simplifycompositelit" +- "golang.org/x/tools/gopls/internal/analysis/stubmethods" -) - -func Test(t *testing.T) { - testdata := analysistest.TestData() -- analysistest.RunWithSuggestedFixes(t, testdata, simplifycompositelit.Analyzer, "a") +- analysistest.Run(t, testdata, stubmethods.Analyzer, "a") -} -diff -urN a/gopls/internal/lsp/analysis/simplifycompositelit/testdata/src/a/a.go b/gopls/internal/lsp/analysis/simplifycompositelit/testdata/src/a/a.go ---- a/gopls/internal/lsp/analysis/simplifycompositelit/testdata/src/a/a.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/simplifycompositelit/testdata/src/a/a.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,234 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/analysis/stubmethods/testdata/src/typeparams/implement.go b/gopls/internal/analysis/stubmethods/testdata/src/typeparams/implement.go +--- a/gopls/internal/analysis/stubmethods/testdata/src/typeparams/implement.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/stubmethods/testdata/src/typeparams/implement.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,15 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package testdata +-package stubmethods - --type T struct { -- x, y int --} +-var _ I = Y{} // want "Implement I" - --type T2 struct { -- w, z int --} +-type I interface{ F() } - --var _ = [42]T{ -- T{}, // want "redundant type from array, slice, or map composite literal" -- T{1, 2}, // want "redundant type from array, slice, or map composite literal" -- T{3, 4}, // want "redundant type from array, slice, or map composite literal" --} +-type X struct{} - --var _ = [...]T{ -- T{}, // want "redundant type from array, slice, or map composite literal" -- T{1, 2}, // want "redundant type from array, slice, or map composite literal" -- T{3, 4}, // want "redundant type from array, slice, or map composite literal" --} +-func (X) F(string) {} - --var _ = []T{ -- T{}, // want "redundant type from array, slice, or map composite literal" -- T{1, 2}, // want "redundant type from array, slice, or map composite literal" -- T{3, 4}, // want "redundant type from array, slice, or map composite literal" --} +-type Y struct{ X } +diff -urN a/gopls/internal/analysis/undeclaredname/doc.go b/gopls/internal/analysis/undeclaredname/doc.go +--- a/gopls/internal/analysis/undeclaredname/doc.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/undeclaredname/doc.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,23 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --var _ = []T{ -- T{}, // want "redundant type from array, slice, or map composite literal" -- 10: T{1, 2}, // want "redundant type from array, slice, or map composite literal" -- 20: T{3, 4}, // want "redundant type from array, slice, or map composite literal" --} +-// Package undeclaredname defines an Analyzer that applies suggested fixes +-// to errors of the type "undeclared name: %s". +-// +-// # Analyzer undeclaredname +-// +-// undeclaredname: suggested fixes for "undeclared name: <>" +-// +-// This checker provides suggested fixes for type errors of the +-// type "undeclared name: <>". It will either insert a new statement, +-// such as: +-// +-// <> := +-// +-// or a new function declaration, such as: +-// +-// func <>(inferred parameters) { +-// panic("implement me!") +-// } +-package undeclaredname +diff -urN a/gopls/internal/analysis/undeclaredname/testdata/src/a/a.go b/gopls/internal/analysis/undeclaredname/testdata/src/a/a.go +--- a/gopls/internal/analysis/undeclaredname/testdata/src/a/a.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/undeclaredname/testdata/src/a/a.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,28 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --var _ = []struct { -- x, y int --}{ -- struct{ x, y int }{}, // want "redundant type from array, slice, or map composite literal" -- 10: struct{ x, y int }{1, 2}, // want "redundant type from array, slice, or map composite literal" -- 20: struct{ x, y int }{3, 4}, // want "redundant type from array, slice, or map composite literal" --} +-package undeclared - --var _ = []interface{}{ -- T{}, -- 10: T{1, 2}, -- 20: T{3, 4}, --} +-func x() int { +- var z int +- z = y // want "(undeclared name|undefined): y" - --var _ = [][]int{ -- []int{}, // want "redundant type from array, slice, or map composite literal" -- []int{1, 2}, // want "redundant type from array, slice, or map composite literal" -- []int{3, 4}, // want "redundant type from array, slice, or map composite literal" --} +- if z == m { // want "(undeclared name|undefined): m" +- z = 1 +- } - --var _ = [][]int{ -- ([]int{}), -- ([]int{1, 2}), -- []int{3, 4}, // want "redundant type from array, slice, or map composite literal" --} +- if z == 1 { +- z = 1 +- } else if z == n+1 { // want "(undeclared name|undefined): n" +- z = 1 +- } - --var _ = [][][]int{ -- [][]int{}, // want "redundant type from array, slice, or map composite literal" -- [][]int{ // want "redundant type from array, slice, or map composite literal" -- []int{}, // want "redundant type from array, slice, or map composite literal" -- []int{0, 1, 2, 3}, // want "redundant type from array, slice, or map composite literal" -- []int{4, 5}, // want "redundant type from array, slice, or map composite literal" -- }, +- switch z { +- case 10: +- z = 1 +- case a: // want "(undeclared name|undefined): a" +- z = 1 +- } +- return z -} +diff -urN a/gopls/internal/analysis/undeclaredname/testdata/src/a/channels.go b/gopls/internal/analysis/undeclaredname/testdata/src/a/channels.go +--- a/gopls/internal/analysis/undeclaredname/testdata/src/a/channels.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/undeclaredname/testdata/src/a/channels.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,13 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --var _ = map[string]T{ -- "foo": T{}, // want "redundant type from array, slice, or map composite literal" -- "bar": T{1, 2}, // want "redundant type from array, slice, or map composite literal" -- "bal": T{3, 4}, // want "redundant type from array, slice, or map composite literal" --} +-package undeclared - --var _ = map[string]struct { -- x, y int --}{ -- "foo": struct{ x, y int }{}, // want "redundant type from array, slice, or map composite literal" -- "bar": struct{ x, y int }{1, 2}, // want "redundant type from array, slice, or map composite literal" -- "bal": struct{ x, y int }{3, 4}, // want "redundant type from array, slice, or map composite literal" +-func channels(s string) { +- undefinedChannels(c()) // want "(undeclared name|undefined): undefinedChannels" -} - --var _ = map[string]interface{}{ -- "foo": T{}, -- "bar": T{1, 2}, -- "bal": T{3, 4}, +-func c() (<-chan string, chan string) { +- return make(<-chan string), make(chan string) -} +diff -urN a/gopls/internal/analysis/undeclaredname/testdata/src/a/consecutive_params.go b/gopls/internal/analysis/undeclaredname/testdata/src/a/consecutive_params.go +--- a/gopls/internal/analysis/undeclaredname/testdata/src/a/consecutive_params.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/undeclaredname/testdata/src/a/consecutive_params.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,10 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --var _ = map[string][]int{ -- "foo": []int{}, // want "redundant type from array, slice, or map composite literal" -- "bar": []int{1, 2}, // want "redundant type from array, slice, or map composite literal" -- "bal": []int{3, 4}, // want "redundant type from array, slice, or map composite literal" --} +-package undeclared - --var _ = map[string][]int{ -- "foo": ([]int{}), -- "bar": ([]int{1, 2}), -- "bal": []int{3, 4}, // want "redundant type from array, slice, or map composite literal" +-func consecutiveParams() { +- var s string +- undefinedConsecutiveParams(s, s) // want "(undeclared name|undefined): undefinedConsecutiveParams" -} +diff -urN a/gopls/internal/analysis/undeclaredname/testdata/src/a/error_param.go b/gopls/internal/analysis/undeclaredname/testdata/src/a/error_param.go +--- a/gopls/internal/analysis/undeclaredname/testdata/src/a/error_param.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/undeclaredname/testdata/src/a/error_param.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,10 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --type Point struct { -- a int -- b int --} -- --type Piece struct { -- a int -- b int -- c Point -- d []Point -- e *Point -- f *Point --} -- --// from exp/4s/data.go --var pieces3 = []Piece{ -- Piece{0, 0, Point{4, 1}, []Point{Point{0, 0}, Point{1, 0}, Point{1, 0}, Point{1, 0}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" -- Piece{1, 0, Point{1, 4}, []Point{Point{0, 0}, Point{0, 1}, Point{0, 1}, Point{0, 1}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" -- Piece{2, 0, Point{4, 1}, []Point{Point{0, 0}, Point{1, 0}, Point{1, 0}, Point{1, 0}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" -- Piece{3, 0, Point{1, 4}, []Point{Point{0, 0}, Point{0, 1}, Point{0, 1}, Point{0, 1}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" --} -- --var _ = [42]*T{ -- &T{}, // want "redundant type from array, slice, or map composite literal" -- &T{1, 2}, // want "redundant type from array, slice, or map composite literal" -- &T{3, 4}, // want "redundant type from array, slice, or map composite literal" --} +-package undeclared - --var _ = [...]*T{ -- &T{}, // want "redundant type from array, slice, or map composite literal" -- &T{1, 2}, // want "redundant type from array, slice, or map composite literal" -- &T{3, 4}, // want "redundant type from array, slice, or map composite literal" +-func errorParam() { +- var err error +- undefinedErrorParam(err) // want "(undeclared name|undefined): undefinedErrorParam" -} +diff -urN a/gopls/internal/analysis/undeclaredname/testdata/src/a/literals.go b/gopls/internal/analysis/undeclaredname/testdata/src/a/literals.go +--- a/gopls/internal/analysis/undeclaredname/testdata/src/a/literals.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/undeclaredname/testdata/src/a/literals.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,11 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --var _ = []*T{ -- &T{}, // want "redundant type from array, slice, or map composite literal" -- &T{1, 2}, // want "redundant type from array, slice, or map composite literal" -- &T{3, 4}, // want "redundant type from array, slice, or map composite literal" --} +-package undeclared - --var _ = []*T{ -- &T{}, // want "redundant type from array, slice, or map composite literal" -- 10: &T{1, 2}, // want "redundant type from array, slice, or map composite literal" -- 20: &T{3, 4}, // want "redundant type from array, slice, or map composite literal" --} +-type T struct{} - --var _ = []*struct { -- x, y int --}{ -- &struct{ x, y int }{}, // want "redundant type from array, slice, or map composite literal" -- 10: &struct{ x, y int }{1, 2}, // want "redundant type from array, slice, or map composite literal" -- 20: &struct{ x, y int }{3, 4}, // want "redundant type from array, slice, or map composite literal" +-func literals() { +- undefinedLiterals("hey compiler", T{}, &T{}) // want "(undeclared name|undefined): undefinedLiterals" -} +diff -urN a/gopls/internal/analysis/undeclaredname/testdata/src/a/operation.go b/gopls/internal/analysis/undeclaredname/testdata/src/a/operation.go +--- a/gopls/internal/analysis/undeclaredname/testdata/src/a/operation.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/undeclaredname/testdata/src/a/operation.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,11 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --var _ = []interface{}{ -- &T{}, -- 10: &T{1, 2}, -- 20: &T{3, 4}, --} +-package undeclared - --var _ = []*[]int{ -- &[]int{}, // want "redundant type from array, slice, or map composite literal" -- &[]int{1, 2}, // want "redundant type from array, slice, or map composite literal" -- &[]int{3, 4}, // want "redundant type from array, slice, or map composite literal" --} +-import "time" - --var _ = []*[]int{ -- (&[]int{}), -- (&[]int{1, 2}), -- &[]int{3, 4}, // want "redundant type from array, slice, or map composite literal" +-func operation() { +- undefinedOperation(10 * time.Second) // want "(undeclared name|undefined): undefinedOperation" -} +diff -urN a/gopls/internal/analysis/undeclaredname/testdata/src/a/selector.go b/gopls/internal/analysis/undeclaredname/testdata/src/a/selector.go +--- a/gopls/internal/analysis/undeclaredname/testdata/src/a/selector.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/undeclaredname/testdata/src/a/selector.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,10 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --var _ = []*[]*[]int{ -- &[]*[]int{}, // want "redundant type from array, slice, or map composite literal" -- &[]*[]int{ // want "redundant type from array, slice, or map composite literal" -- &[]int{}, // want "redundant type from array, slice, or map composite literal" -- &[]int{0, 1, 2, 3}, // want "redundant type from array, slice, or map composite literal" -- &[]int{4, 5}, // want "redundant type from array, slice, or map composite literal" -- }, --} +-package undeclared - --var _ = map[string]*T{ -- "foo": &T{}, // want "redundant type from array, slice, or map composite literal" -- "bar": &T{1, 2}, // want "redundant type from array, slice, or map composite literal" -- "bal": &T{3, 4}, // want "redundant type from array, slice, or map composite literal" +-func selector() { +- m := map[int]bool{} +- undefinedSelector(m[1]) // want "(undeclared name|undefined): undefinedSelector" -} +diff -urN a/gopls/internal/analysis/undeclaredname/testdata/src/a/slice.go b/gopls/internal/analysis/undeclaredname/testdata/src/a/slice.go +--- a/gopls/internal/analysis/undeclaredname/testdata/src/a/slice.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/undeclaredname/testdata/src/a/slice.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,9 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --var _ = map[string]*struct { -- x, y int --}{ -- "foo": &struct{ x, y int }{}, // want "redundant type from array, slice, or map composite literal" -- "bar": &struct{ x, y int }{1, 2}, // want "redundant type from array, slice, or map composite literal" -- "bal": &struct{ x, y int }{3, 4}, // want "redundant type from array, slice, or map composite literal" --} +-package undeclared - --var _ = map[string]interface{}{ -- "foo": &T{}, -- "bar": &T{1, 2}, -- "bal": &T{3, 4}, +-func slice() { +- undefinedSlice([]int{1, 2}) // want "(undeclared name|undefined): undefinedSlice" -} +diff -urN a/gopls/internal/analysis/undeclaredname/testdata/src/a/tuple.go b/gopls/internal/analysis/undeclaredname/testdata/src/a/tuple.go +--- a/gopls/internal/analysis/undeclaredname/testdata/src/a/tuple.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/undeclaredname/testdata/src/a/tuple.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,13 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --var _ = map[string]*[]int{ -- "foo": &[]int{}, // want "redundant type from array, slice, or map composite literal" -- "bar": &[]int{1, 2}, // want "redundant type from array, slice, or map composite literal" -- "bal": &[]int{3, 4}, // want "redundant type from array, slice, or map composite literal" --} +-package undeclared - --var _ = map[string]*[]int{ -- "foo": (&[]int{}), -- "bar": (&[]int{1, 2}), -- "bal": &[]int{3, 4}, // want "redundant type from array, slice, or map composite literal" +-func tuple() { +- undefinedTuple(b()) // want "(undeclared name|undefined): undefinedTuple" -} - --var pieces4 = []*Piece{ -- &Piece{0, 0, Point{4, 1}, []Point{Point{0, 0}, Point{1, 0}, Point{1, 0}, Point{1, 0}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" -- &Piece{1, 0, Point{1, 4}, []Point{Point{0, 0}, Point{0, 1}, Point{0, 1}, Point{0, 1}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" -- &Piece{2, 0, Point{4, 1}, []Point{Point{0, 0}, Point{1, 0}, Point{1, 0}, Point{1, 0}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" -- &Piece{3, 0, Point{1, 4}, []Point{Point{0, 0}, Point{0, 1}, Point{0, 1}, Point{0, 1}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" +-func b() (string, error) { +- return "", nil -} +diff -urN a/gopls/internal/analysis/undeclaredname/testdata/src/a/unique_params.go b/gopls/internal/analysis/undeclaredname/testdata/src/a/unique_params.go +--- a/gopls/internal/analysis/undeclaredname/testdata/src/a/unique_params.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/undeclaredname/testdata/src/a/unique_params.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,11 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --var _ = map[T]T2{ -- T{1, 2}: T2{3, 4}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" -- T{5, 6}: T2{7, 8}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" --} +-package undeclared - --var _ = map[*T]*T2{ -- &T{1, 2}: &T2{3, 4}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" -- &T{5, 6}: &T2{7, 8}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" +-func uniqueArguments() { +- var s string +- var i int +- undefinedUniqueArguments(s, i, s) // want "(undeclared name|undefined): undefinedUniqueArguments" -} -diff -urN a/gopls/internal/lsp/analysis/simplifycompositelit/testdata/src/a/a.go.golden b/gopls/internal/lsp/analysis/simplifycompositelit/testdata/src/a/a.go.golden ---- a/gopls/internal/lsp/analysis/simplifycompositelit/testdata/src/a/a.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/simplifycompositelit/testdata/src/a/a.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,234 +0,0 @@ +diff -urN a/gopls/internal/analysis/undeclaredname/undeclared.go b/gopls/internal/analysis/undeclaredname/undeclared.go +--- a/gopls/internal/analysis/undeclaredname/undeclared.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/undeclaredname/undeclared.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,360 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package testdata -- --type T struct { -- x, y int --} +-package undeclaredname - --type T2 struct { -- w, z int --} +-import ( +- "bytes" +- _ "embed" +- "fmt" +- "go/ast" +- "go/format" +- "go/token" +- "go/types" +- "strings" +- "unicode" - --var _ = [42]T{ -- {}, // want "redundant type from array, slice, or map composite literal" -- {1, 2}, // want "redundant type from array, slice, or map composite literal" -- {3, 4}, // want "redundant type from array, slice, or map composite literal" --} +- "golang.org/x/tools/go/analysis" +- "golang.org/x/tools/go/ast/astutil" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/internal/aliases" +- "golang.org/x/tools/internal/analysisinternal" +-) - --var _ = [...]T{ -- {}, // want "redundant type from array, slice, or map composite literal" -- {1, 2}, // want "redundant type from array, slice, or map composite literal" -- {3, 4}, // want "redundant type from array, slice, or map composite literal" --} +-//go:embed doc.go +-var doc string - --var _ = []T{ -- {}, // want "redundant type from array, slice, or map composite literal" -- {1, 2}, // want "redundant type from array, slice, or map composite literal" -- {3, 4}, // want "redundant type from array, slice, or map composite literal" +-var Analyzer = &analysis.Analyzer{ +- Name: "undeclaredname", +- Doc: analysisinternal.MustExtractDoc(doc, "undeclaredname"), +- Requires: []*analysis.Analyzer{}, +- Run: run, +- RunDespiteErrors: true, +- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/undeclaredname", -} - --var _ = []T{ -- {}, // want "redundant type from array, slice, or map composite literal" -- 10: {1, 2}, // want "redundant type from array, slice, or map composite literal" -- 20: {3, 4}, // want "redundant type from array, slice, or map composite literal" --} +-// The prefix for this error message changed in Go 1.20. +-var undeclaredNamePrefixes = []string{"undeclared name: ", "undefined: "} - --var _ = []struct { -- x, y int --}{ -- {}, // want "redundant type from array, slice, or map composite literal" -- 10: {1, 2}, // want "redundant type from array, slice, or map composite literal" -- 20: {3, 4}, // want "redundant type from array, slice, or map composite literal" +-func run(pass *analysis.Pass) (interface{}, error) { +- for _, err := range pass.TypeErrors { +- runForError(pass, err) +- } +- return nil, nil -} - --var _ = []interface{}{ -- T{}, -- 10: T{1, 2}, -- 20: T{3, 4}, --} +-func runForError(pass *analysis.Pass, err types.Error) { +- // Extract symbol name from error. +- var name string +- for _, prefix := range undeclaredNamePrefixes { +- if !strings.HasPrefix(err.Msg, prefix) { +- continue +- } +- name = strings.TrimPrefix(err.Msg, prefix) +- } +- if name == "" { +- return +- } - --var _ = [][]int{ -- {}, // want "redundant type from array, slice, or map composite literal" -- {1, 2}, // want "redundant type from array, slice, or map composite literal" -- {3, 4}, // want "redundant type from array, slice, or map composite literal" --} +- // Find file enclosing error. +- var file *ast.File +- for _, f := range pass.Files { +- if f.Pos() <= err.Pos && err.Pos < f.End() { +- file = f +- break +- } +- } +- if file == nil { +- return +- } - --var _ = [][]int{ -- ([]int{}), -- ([]int{1, 2}), -- {3, 4}, // want "redundant type from array, slice, or map composite literal" --} +- // Find path to identifier in the error. +- path, _ := astutil.PathEnclosingInterval(file, err.Pos, err.Pos) +- if len(path) < 2 { +- return +- } +- ident, ok := path[0].(*ast.Ident) +- if !ok || ident.Name != name { +- return +- } - --var _ = [][][]int{ -- {}, // want "redundant type from array, slice, or map composite literal" -- { // want "redundant type from array, slice, or map composite literal" -- {}, // want "redundant type from array, slice, or map composite literal" -- {0, 1, 2, 3}, // want "redundant type from array, slice, or map composite literal" -- {4, 5}, // want "redundant type from array, slice, or map composite literal" -- }, --} +- // Skip selector expressions because it might be too complex +- // to try and provide a suggested fix for fields and methods. +- if _, ok := path[1].(*ast.SelectorExpr); ok { +- return +- } - --var _ = map[string]T{ -- "foo": {}, // want "redundant type from array, slice, or map composite literal" -- "bar": {1, 2}, // want "redundant type from array, slice, or map composite literal" -- "bal": {3, 4}, // want "redundant type from array, slice, or map composite literal" --} +- // Undeclared quick fixes only work in function bodies. +- inFunc := false +- for i := range path { +- if _, inFunc = path[i].(*ast.FuncDecl); inFunc { +- if i == 0 { +- return +- } +- if _, isBody := path[i-1].(*ast.BlockStmt); !isBody { +- return +- } +- break +- } +- } +- if !inFunc { +- return +- } - --var _ = map[string]struct { -- x, y int --}{ -- "foo": {}, // want "redundant type from array, slice, or map composite literal" -- "bar": {1, 2}, // want "redundant type from array, slice, or map composite literal" -- "bal": {3, 4}, // want "redundant type from array, slice, or map composite literal" +- // Offer a fix. +- noun := "variable" +- if isCallPosition(path) { +- noun = "function" +- } +- pass.Report(analysis.Diagnostic{ +- Pos: err.Pos, +- End: err.Pos + token.Pos(len(name)), +- Message: err.Msg, +- Category: FixCategory, +- SuggestedFixes: []analysis.SuggestedFix{{ +- Message: fmt.Sprintf("Create %s %q", noun, name), +- // No TextEdits => computed by a gopls command +- }}, +- }) -} - --var _ = map[string]interface{}{ -- "foo": T{}, -- "bar": T{1, 2}, -- "bal": T{3, 4}, --} +-const FixCategory = "undeclaredname" // recognized by gopls ApplyFix - --var _ = map[string][]int{ -- "foo": {}, // want "redundant type from array, slice, or map composite literal" -- "bar": {1, 2}, // want "redundant type from array, slice, or map composite literal" -- "bal": {3, 4}, // want "redundant type from array, slice, or map composite literal" --} +-// SuggestedFix computes the edits for the lazy (no-edits) fix suggested by the analyzer. +-func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, file *ast.File, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { +- pos := start // don't use the end +- path, _ := astutil.PathEnclosingInterval(file, pos, pos) +- if len(path) < 2 { +- return nil, nil, fmt.Errorf("no expression found") +- } +- ident, ok := path[0].(*ast.Ident) +- if !ok { +- return nil, nil, fmt.Errorf("no identifier found") +- } - --var _ = map[string][]int{ -- "foo": ([]int{}), -- "bar": ([]int{1, 2}), -- "bal": {3, 4}, // want "redundant type from array, slice, or map composite literal" --} +- // Check for a possible call expression, in which case we should add a +- // new function declaration. +- if isCallPosition(path) { +- return newFunctionDeclaration(path, file, pkg, info, fset) +- } - --type Point struct { -- a int -- b int --} +- // Get the place to insert the new statement. +- insertBeforeStmt := analysisinternal.StmtToInsertVarBefore(path) +- if insertBeforeStmt == nil { +- return nil, nil, fmt.Errorf("could not locate insertion point") +- } - --type Piece struct { -- a int -- b int -- c Point -- d []Point -- e *Point -- f *Point --} +- insertBefore := safetoken.StartPosition(fset, insertBeforeStmt.Pos()).Offset - --// from exp/4s/data.go --var pieces3 = []Piece{ -- {0, 0, Point{4, 1}, []Point{{0, 0}, {1, 0}, {1, 0}, {1, 0}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" -- {1, 0, Point{1, 4}, []Point{{0, 0}, {0, 1}, {0, 1}, {0, 1}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" -- {2, 0, Point{4, 1}, []Point{{0, 0}, {1, 0}, {1, 0}, {1, 0}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" -- {3, 0, Point{1, 4}, []Point{{0, 0}, {0, 1}, {0, 1}, {0, 1}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" --} +- // Get the indent to add on the line after the new statement. +- // Since this will have a parse error, we can not use format.Source(). +- contentBeforeStmt, indent := content[:insertBefore], "\n" +- if nl := bytes.LastIndex(contentBeforeStmt, []byte("\n")); nl != -1 { +- indent = string(contentBeforeStmt[nl:]) +- } - --var _ = [42]*T{ -- {}, // want "redundant type from array, slice, or map composite literal" -- {1, 2}, // want "redundant type from array, slice, or map composite literal" -- {3, 4}, // want "redundant type from array, slice, or map composite literal" +- // Create the new local variable statement. +- newStmt := fmt.Sprintf("%s := %s", ident.Name, indent) +- return fset, &analysis.SuggestedFix{ +- Message: fmt.Sprintf("Create variable %q", ident.Name), +- TextEdits: []analysis.TextEdit{{ +- Pos: insertBeforeStmt.Pos(), +- End: insertBeforeStmt.Pos(), +- NewText: []byte(newStmt), +- }}, +- }, nil -} - --var _ = [...]*T{ -- {}, // want "redundant type from array, slice, or map composite literal" -- {1, 2}, // want "redundant type from array, slice, or map composite literal" -- {3, 4}, // want "redundant type from array, slice, or map composite literal" --} +-func newFunctionDeclaration(path []ast.Node, file *ast.File, pkg *types.Package, info *types.Info, fset *token.FileSet) (*token.FileSet, *analysis.SuggestedFix, error) { +- if len(path) < 3 { +- return nil, nil, fmt.Errorf("unexpected set of enclosing nodes: %v", path) +- } +- ident, ok := path[0].(*ast.Ident) +- if !ok { +- return nil, nil, fmt.Errorf("no name for function declaration %v (%T)", path[0], path[0]) +- } +- call, ok := path[1].(*ast.CallExpr) +- if !ok { +- return nil, nil, fmt.Errorf("no call expression found %v (%T)", path[1], path[1]) +- } - --var _ = []*T{ -- {}, // want "redundant type from array, slice, or map composite literal" -- {1, 2}, // want "redundant type from array, slice, or map composite literal" -- {3, 4}, // want "redundant type from array, slice, or map composite literal" --} +- // Find the enclosing function, so that we can add the new declaration +- // below. +- var enclosing *ast.FuncDecl +- for _, n := range path { +- if n, ok := n.(*ast.FuncDecl); ok { +- enclosing = n +- break +- } +- } +- // TODO(rstambler): Support the situation when there is no enclosing +- // function. +- if enclosing == nil { +- return nil, nil, fmt.Errorf("no enclosing function found: %v", path) +- } - --var _ = []*T{ -- {}, // want "redundant type from array, slice, or map composite literal" -- 10: {1, 2}, // want "redundant type from array, slice, or map composite literal" -- 20: {3, 4}, // want "redundant type from array, slice, or map composite literal" --} +- pos := enclosing.End() - --var _ = []*struct { -- x, y int --}{ -- {}, // want "redundant type from array, slice, or map composite literal" -- 10: {1, 2}, // want "redundant type from array, slice, or map composite literal" -- 20: {3, 4}, // want "redundant type from array, slice, or map composite literal" --} +- var paramNames []string +- var paramTypes []types.Type +- // keep track of all param names to later ensure uniqueness +- nameCounts := map[string]int{} +- for _, arg := range call.Args { +- typ := info.TypeOf(arg) +- if typ == nil { +- return nil, nil, fmt.Errorf("unable to determine type for %s", arg) +- } - --var _ = []interface{}{ -- &T{}, -- 10: &T{1, 2}, -- 20: &T{3, 4}, --} +- switch t := typ.(type) { +- // this is the case where another function call returning multiple +- // results is used as an argument +- case *types.Tuple: +- n := t.Len() +- for i := 0; i < n; i++ { +- name := typeToArgName(t.At(i).Type()) +- nameCounts[name]++ - --var _ = []*[]int{ -- {}, // want "redundant type from array, slice, or map composite literal" -- {1, 2}, // want "redundant type from array, slice, or map composite literal" -- {3, 4}, // want "redundant type from array, slice, or map composite literal" --} +- paramNames = append(paramNames, name) +- paramTypes = append(paramTypes, types.Default(t.At(i).Type())) +- } - --var _ = []*[]int{ -- (&[]int{}), -- (&[]int{1, 2}), -- {3, 4}, // want "redundant type from array, slice, or map composite literal" --} +- default: +- // does the argument have a name we can reuse? +- // only happens in case of a *ast.Ident +- var name string +- if ident, ok := arg.(*ast.Ident); ok { +- name = ident.Name +- } - --var _ = []*[]*[]int{ -- {}, // want "redundant type from array, slice, or map composite literal" -- { // want "redundant type from array, slice, or map composite literal" -- {}, // want "redundant type from array, slice, or map composite literal" -- {0, 1, 2, 3}, // want "redundant type from array, slice, or map composite literal" -- {4, 5}, // want "redundant type from array, slice, or map composite literal" -- }, --} +- if name == "" { +- name = typeToArgName(typ) +- } - --var _ = map[string]*T{ -- "foo": {}, // want "redundant type from array, slice, or map composite literal" -- "bar": {1, 2}, // want "redundant type from array, slice, or map composite literal" -- "bal": {3, 4}, // want "redundant type from array, slice, or map composite literal" --} +- nameCounts[name]++ - --var _ = map[string]*struct { -- x, y int --}{ -- "foo": {}, // want "redundant type from array, slice, or map composite literal" -- "bar": {1, 2}, // want "redundant type from array, slice, or map composite literal" -- "bal": {3, 4}, // want "redundant type from array, slice, or map composite literal" --} +- paramNames = append(paramNames, name) +- paramTypes = append(paramTypes, types.Default(typ)) +- } +- } - --var _ = map[string]interface{}{ -- "foo": &T{}, -- "bar": &T{1, 2}, -- "bal": &T{3, 4}, --} +- for n, c := range nameCounts { +- // Any names we saw more than once will need a unique suffix added +- // on. Reset the count to 1 to act as the suffix for the first +- // occurrence of that name. +- if c >= 2 { +- nameCounts[n] = 1 +- } else { +- delete(nameCounts, n) +- } +- } - --var _ = map[string]*[]int{ -- "foo": {}, // want "redundant type from array, slice, or map composite literal" -- "bar": {1, 2}, // want "redundant type from array, slice, or map composite literal" -- "bal": {3, 4}, // want "redundant type from array, slice, or map composite literal" --} +- params := &ast.FieldList{} - --var _ = map[string]*[]int{ -- "foo": (&[]int{}), -- "bar": (&[]int{1, 2}), -- "bal": {3, 4}, // want "redundant type from array, slice, or map composite literal" --} -- --var pieces4 = []*Piece{ -- {0, 0, Point{4, 1}, []Point{{0, 0}, {1, 0}, {1, 0}, {1, 0}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" -- {1, 0, Point{1, 4}, []Point{{0, 0}, {0, 1}, {0, 1}, {0, 1}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" -- {2, 0, Point{4, 1}, []Point{{0, 0}, {1, 0}, {1, 0}, {1, 0}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" -- {3, 0, Point{1, 4}, []Point{{0, 0}, {0, 1}, {0, 1}, {0, 1}}, nil, nil}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" --} -- --var _ = map[T]T2{ -- {1, 2}: {3, 4}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" -- {5, 6}: {7, 8}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" --} -- --var _ = map[*T]*T2{ -- {1, 2}: {3, 4}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" -- {5, 6}: {7, 8}, // want "redundant type from array, slice, or map composite literal" "redundant type from array, slice, or map composite literal" --} -diff -urN a/gopls/internal/lsp/analysis/simplifyrange/simplifyrange.go b/gopls/internal/lsp/analysis/simplifyrange/simplifyrange.go ---- a/gopls/internal/lsp/analysis/simplifyrange/simplifyrange.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/simplifyrange/simplifyrange.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,116 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --// Package simplifyrange defines an Analyzer that simplifies range statements. --// https://golang.org/cmd/gofmt/#hdr-The_simplify_command --// https://github.com/golang/go/blob/master/src/cmd/gofmt/simplify.go --package simplifyrange -- --import ( -- "bytes" -- "go/ast" -- "go/printer" -- "go/token" -- -- "golang.org/x/tools/go/analysis" -- "golang.org/x/tools/go/analysis/passes/inspect" -- "golang.org/x/tools/go/ast/inspector" --) -- --const Doc = `check for range statement simplifications +- for i, name := range paramNames { +- if suffix, repeats := nameCounts[name]; repeats { +- nameCounts[name]++ +- name = fmt.Sprintf("%s%d", name, suffix) +- } - --A range of the form: -- for x, _ = range v {...} --will be simplified to: -- for x = range v {...} +- // only worth checking after previous param in the list +- if i > 0 { +- // if type of parameter at hand is the same as the previous one, +- // add it to the previous param list of identifiers so to have: +- // (s1, s2 string) +- // and not +- // (s1 string, s2 string) +- if paramTypes[i] == paramTypes[i-1] { +- params.List[len(params.List)-1].Names = append(params.List[len(params.List)-1].Names, ast.NewIdent(name)) +- continue +- } +- } - --A range of the form: -- for _ = range v {...} --will be simplified to: -- for range v {...} +- params.List = append(params.List, &ast.Field{ +- Names: []*ast.Ident{ +- ast.NewIdent(name), +- }, +- Type: analysisinternal.TypeExpr(file, pkg, paramTypes[i]), +- }) +- } - --This is one of the simplifications that "gofmt -s" applies.` +- decl := &ast.FuncDecl{ +- Name: ast.NewIdent(ident.Name), +- Type: &ast.FuncType{ +- Params: params, +- // TODO(golang/go#47558): Also handle result +- // parameters here based on context of CallExpr. +- }, +- Body: &ast.BlockStmt{ +- List: []ast.Stmt{ +- &ast.ExprStmt{ +- X: &ast.CallExpr{ +- Fun: ast.NewIdent("panic"), +- Args: []ast.Expr{ +- &ast.BasicLit{ +- Value: `"unimplemented"`, +- }, +- }, +- }, +- }, +- }, +- }, +- } - --var Analyzer = &analysis.Analyzer{ -- Name: "simplifyrange", -- Doc: Doc, -- Requires: []*analysis.Analyzer{inspect.Analyzer}, -- Run: run, +- b := bytes.NewBufferString("\n\n") +- if err := format.Node(b, fset, decl); err != nil { +- return nil, nil, err +- } +- return fset, &analysis.SuggestedFix{ +- Message: fmt.Sprintf("Create function %q", ident.Name), +- TextEdits: []analysis.TextEdit{{ +- Pos: pos, +- End: pos, +- NewText: b.Bytes(), +- }}, +- }, nil -} - --func run(pass *analysis.Pass) (interface{}, error) { -- inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) -- nodeFilter := []ast.Node{ -- (*ast.RangeStmt)(nil), +-func typeToArgName(ty types.Type) string { +- s := types.Default(ty).String() +- +- switch t := aliases.Unalias(ty).(type) { +- case *types.Basic: +- // use first letter in type name for basic types +- return s[0:1] +- case *types.Slice: +- // use element type to decide var name for slices +- return typeToArgName(t.Elem()) +- case *types.Array: +- // use element type to decide var name for arrays +- return typeToArgName(t.Elem()) +- case *types.Chan: +- return "ch" - } -- inspect.Preorder(nodeFilter, func(n ast.Node) { -- var copy *ast.RangeStmt -- if stmt, ok := n.(*ast.RangeStmt); ok { -- x := *stmt -- copy = &x -- } -- if copy == nil { -- return -- } -- end := newlineIndex(pass.Fset, copy) - -- // Range statements of the form: for i, _ := range x {} -- var old ast.Expr -- if isBlank(copy.Value) { -- old = copy.Value -- copy.Value = nil -- } -- // Range statements of the form: for _ := range x {} -- if isBlank(copy.Key) && copy.Value == nil { -- old = copy.Key -- copy.Key = nil -- } -- // Return early if neither if condition is met. -- if old == nil { -- return -- } -- pass.Report(analysis.Diagnostic{ -- Pos: old.Pos(), -- End: old.End(), -- Message: "simplify range expression", -- SuggestedFixes: suggestedFixes(pass.Fset, copy, end), -- }) +- s = strings.TrimFunc(s, func(r rune) bool { +- return !unicode.IsLetter(r) - }) -- return nil, nil --} - --func suggestedFixes(fset *token.FileSet, rng *ast.RangeStmt, end token.Pos) []analysis.SuggestedFix { -- var b bytes.Buffer -- printer.Fprint(&b, fset, rng) -- stmt := b.Bytes() -- index := bytes.Index(stmt, []byte("\n")) -- // If there is a new line character, then don't replace the body. -- if index != -1 { -- stmt = stmt[:index] +- if s == "error" { +- return "err" - } -- return []analysis.SuggestedFix{{ -- Message: "Remove empty value", -- TextEdits: []analysis.TextEdit{{ -- Pos: rng.Pos(), -- End: end, -- NewText: stmt[:index], -- }}, -- }} +- +- // remove package (if present) +- // and make first letter lowercase +- a := []rune(s[strings.LastIndexByte(s, '.')+1:]) +- a[0] = unicode.ToLower(a[0]) +- return string(a) -} - --func newlineIndex(fset *token.FileSet, rng *ast.RangeStmt) token.Pos { -- var b bytes.Buffer -- printer.Fprint(&b, fset, rng) -- contents := b.Bytes() -- index := bytes.Index(contents, []byte("\n")) -- if index == -1 { -- return rng.End() -- } -- return rng.Pos() + token.Pos(index) +-// isCallPosition reports whether the path denotes the subtree in call position, f(). +-func isCallPosition(path []ast.Node) bool { +- return len(path) > 1 && +- is[*ast.CallExpr](path[1]) && +- path[1].(*ast.CallExpr).Fun == path[0] -} - --func isBlank(x ast.Expr) bool { -- ident, ok := x.(*ast.Ident) -- return ok && ident.Name == "_" +-func is[T any](x any) bool { +- _, ok := x.(T) +- return ok -} -diff -urN a/gopls/internal/lsp/analysis/simplifyrange/simplifyrange_test.go b/gopls/internal/lsp/analysis/simplifyrange/simplifyrange_test.go ---- a/gopls/internal/lsp/analysis/simplifyrange/simplifyrange_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/simplifyrange/simplifyrange_test.go 1970-01-01 00:00:00.000000000 +0000 +diff -urN a/gopls/internal/analysis/undeclaredname/undeclared_test.go b/gopls/internal/analysis/undeclaredname/undeclared_test.go +--- a/gopls/internal/analysis/undeclaredname/undeclared_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/undeclaredname/undeclared_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,17 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package simplifyrange_test +-package undeclaredname_test - -import ( - "testing" - - "golang.org/x/tools/go/analysis/analysistest" -- "golang.org/x/tools/gopls/internal/lsp/analysis/simplifyrange" +- "golang.org/x/tools/gopls/internal/analysis/undeclaredname" -) - -func Test(t *testing.T) { - testdata := analysistest.TestData() -- analysistest.RunWithSuggestedFixes(t, testdata, simplifyrange.Analyzer, "a") +- analysistest.Run(t, testdata, undeclaredname.Analyzer, "a") -} -diff -urN a/gopls/internal/lsp/analysis/simplifyrange/testdata/src/a/a.go b/gopls/internal/lsp/analysis/simplifyrange/testdata/src/a/a.go ---- a/gopls/internal/lsp/analysis/simplifyrange/testdata/src/a/a.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/simplifyrange/testdata/src/a/a.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,16 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/analysis/unusedparams/cmd/main.go b/gopls/internal/analysis/unusedparams/cmd/main.go +--- a/gopls/internal/analysis/unusedparams/cmd/main.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/unusedparams/cmd/main.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,13 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package testdata +-// The unusedparams command runs the unusedparams analyzer. +-package main - --import "log" +-import ( +- "golang.org/x/tools/go/analysis/singlechecker" +- "golang.org/x/tools/gopls/internal/analysis/unusedparams" +-) - --func m() { -- maps := make(map[string]string) -- for k, _ := range maps { // want "simplify range expression" -- log.Println(k) -- } -- for _ = range maps { // want "simplify range expression" -- } --} -diff -urN a/gopls/internal/lsp/analysis/simplifyrange/testdata/src/a/a.go.golden b/gopls/internal/lsp/analysis/simplifyrange/testdata/src/a/a.go.golden ---- a/gopls/internal/lsp/analysis/simplifyrange/testdata/src/a/a.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/simplifyrange/testdata/src/a/a.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,16 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +-func main() { singlechecker.Main(unusedparams.Analyzer) } +diff -urN a/gopls/internal/analysis/unusedparams/doc.go b/gopls/internal/analysis/unusedparams/doc.go +--- a/gopls/internal/analysis/unusedparams/doc.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/unusedparams/doc.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,34 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package testdata -- --import "log" -- --func m() { -- maps := make(map[string]string) -- for k := range maps { // want "simplify range expression" -- log.Println(k) -- } -- for range maps { // want "simplify range expression" -- } --} -diff -urN a/gopls/internal/lsp/analysis/simplifyslice/simplifyslice.go b/gopls/internal/lsp/analysis/simplifyslice/simplifyslice.go ---- a/gopls/internal/lsp/analysis/simplifyslice/simplifyslice.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/simplifyslice/simplifyslice.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,94 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +-// Package unusedparams defines an analyzer that checks for unused +-// parameters of functions. +-// +-// # Analyzer unusedparams +-// +-// unusedparams: check for unused parameters of functions +-// +-// The unusedparams analyzer checks functions to see if there are +-// any parameters that are not being used. +-// +-// To ensure soundness, it ignores: +-// - "address-taken" functions, that is, functions that are used as +-// a value rather than being called directly; their signatures may +-// be required to conform to a func type. +-// - exported functions or methods, since they may be address-taken +-// in another package. +-// - unexported methods whose name matches an interface method +-// declared in the same package, since the method's signature +-// may be required to conform to the interface type. +-// - functions with empty bodies, or containing just a call to panic. +-// - parameters that are unnamed, or named "_", the blank identifier. +-// +-// The analyzer suggests a fix of replacing the parameter name by "_", +-// but in such cases a deeper fix can be obtained by invoking the +-// "Refactor: remove unused parameter" code action, which will +-// eliminate the parameter entirely, along with all corresponding +-// arguments at call sites, while taking care to preserve any side +-// effects in the argument expressions; see +-// https://github.com/golang/tools/releases/tag/gopls%2Fv0.14. +-package unusedparams +diff -urN a/gopls/internal/analysis/unusedparams/testdata/src/a/a.go b/gopls/internal/analysis/unusedparams/testdata/src/a/a.go +--- a/gopls/internal/analysis/unusedparams/testdata/src/a/a.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/unusedparams/testdata/src/a/a.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,87 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --// Package simplifyslice defines an Analyzer that simplifies slice statements. --// https://github.com/golang/go/blob/master/src/cmd/gofmt/simplify.go --// https://golang.org/cmd/gofmt/#hdr-The_simplify_command --package simplifyslice +-package a - -import ( - "bytes" - "fmt" -- "go/ast" -- "go/printer" -- -- "golang.org/x/tools/go/analysis" -- "golang.org/x/tools/go/analysis/passes/inspect" -- "golang.org/x/tools/go/ast/inspector" +- "net/http" -) - --const Doc = `check for slice simplifications +-type parent interface { +- n(f bool) +-} - --A slice expression of the form: -- s[a:len(s)] --will be simplified to: -- s[a:] +-type yuh struct { +- a int +-} - --This is one of the simplifications that "gofmt -s" applies.` +-func (y *yuh) n(f bool) { +- for i := 0; i < 10; i++ { +- fmt.Println(i) +- } +-} - --var Analyzer = &analysis.Analyzer{ -- Name: "simplifyslice", -- Doc: Doc, -- Requires: []*analysis.Analyzer{inspect.Analyzer}, -- Run: run, +-func a(i1 int, i2 int, i3 int) int { // want "unused parameter: i2" +- i3 += i1 +- _ = func(z int) int { // want "unused parameter: z" +- _ = 1 +- return 1 +- } +- return i3 -} - --// Note: We could also simplify slice expressions of the form s[0:b] to s[:b] --// but we leave them as is since sometimes we want to be very explicit --// about the lower bound. --// An example where the 0 helps: --// x, y, z := b[0:2], b[2:4], b[4:6] --// An example where it does not: --// x, y := b[:n], b[n:] +-func b(c bytes.Buffer) { // want "unused parameter: c" +- _ = 1 +-} - --func run(pass *analysis.Pass) (interface{}, error) { -- inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) -- nodeFilter := []ast.Node{ -- (*ast.SliceExpr)(nil), -- } -- inspect.Preorder(nodeFilter, func(n ast.Node) { -- expr := n.(*ast.SliceExpr) -- // - 3-index slices always require the 2nd and 3rd index -- if expr.Max != nil { -- return -- } -- s, ok := expr.X.(*ast.Ident) -- // the array/slice object is a single, resolved identifier -- if !ok || s.Obj == nil { -- return -- } -- call, ok := expr.High.(*ast.CallExpr) -- // the high expression is a function call with a single argument -- if !ok || len(call.Args) != 1 || call.Ellipsis.IsValid() { -- return -- } -- fun, ok := call.Fun.(*ast.Ident) -- // the function called is "len" and it is not locally defined; and -- // because we don't have dot imports, it must be the predefined len() -- if !ok || fun.Name != "len" || fun.Obj != nil { -- return -- } -- arg, ok := call.Args[0].(*ast.Ident) -- // the len argument is the array/slice object -- if !ok || arg.Obj != s.Obj { -- return -- } -- var b bytes.Buffer -- printer.Fprint(&b, pass.Fset, expr.High) -- pass.Report(analysis.Diagnostic{ -- Pos: expr.High.Pos(), -- End: expr.High.End(), -- Message: fmt.Sprintf("unneeded: %s", b.String()), -- SuggestedFixes: []analysis.SuggestedFix{{ -- Message: fmt.Sprintf("Remove '%s'", b.String()), -- TextEdits: []analysis.TextEdit{{ -- Pos: expr.High.Pos(), -- End: expr.High.End(), -- NewText: []byte{}, -- }}, -- }}, -- }) -- }) -- return nil, nil +-func z(h http.ResponseWriter, _ *http.Request) { // no report: func z is address-taken +- fmt.Println("Before") -} -diff -urN a/gopls/internal/lsp/analysis/simplifyslice/simplifyslice_test.go b/gopls/internal/lsp/analysis/simplifyslice/simplifyslice_test.go ---- a/gopls/internal/lsp/analysis/simplifyslice/simplifyslice_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/simplifyslice/simplifyslice_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,22 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package simplifyslice_test +-func l(h http.Handler) http.Handler { // want "unused parameter: h" +- return http.HandlerFunc(z) +-} - --import ( -- "testing" +-func mult(a, b int) int { // want "unused parameter: b" +- a += 1 +- return a +-} - -- "golang.org/x/tools/go/analysis/analysistest" -- "golang.org/x/tools/gopls/internal/lsp/analysis/simplifyslice" -- "golang.org/x/tools/internal/typeparams" +-func y(a int) { +- panic("yo") +-} +- +-var _ = func(x int) {} // empty body: no diagnostic +- +-var _ = func(x int) { println() } // want "unused parameter: x" +- +-var ( +- calledGlobal = func(x int) { println() } // want "unused parameter: x" +- addressTakenGlobal = func(x int) { println() } // no report: function is address-taken -) - --func Test(t *testing.T) { -- testdata := analysistest.TestData() -- tests := []string{"a"} -- if typeparams.Enabled { -- tests = append(tests, "typeparams") +-func _() { +- calledGlobal(1) +- println(addressTakenGlobal) +-} +- +-func Exported(unused int) {} // no finding: an exported function may be address-taken +- +-type T int +- +-func (T) m(f bool) { println() } // want "unused parameter: f" +-func (T) n(f bool) { println() } // no finding: n may match the interface method parent.n +- +-func _() { +- var fib func(x, y int) int +- fib = func(x, y int) int { // want "unused parameter: y" +- if x < 2 { +- return x +- } +- return fib(x-1, 123) + fib(x-2, 456) - } -- analysistest.RunWithSuggestedFixes(t, testdata, simplifyslice.Analyzer, tests...) +- fib(10, 42) -} -diff -urN a/gopls/internal/lsp/analysis/simplifyslice/testdata/src/a/a.go b/gopls/internal/lsp/analysis/simplifyslice/testdata/src/a/a.go ---- a/gopls/internal/lsp/analysis/simplifyslice/testdata/src/a/a.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/simplifyslice/testdata/src/a/a.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,70 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/analysis/unusedparams/testdata/src/a/a.go.golden b/gopls/internal/analysis/unusedparams/testdata/src/a/a.go.golden +--- a/gopls/internal/analysis/unusedparams/testdata/src/a/a.go.golden 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/unusedparams/testdata/src/a/a.go.golden 1970-01-01 00:00:00.000000000 +0000 +@@ -1,87 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package testdata +-package a - --var ( -- a [10]byte -- b [20]float32 -- s []int -- t struct { -- s []byte +-import ( +- "bytes" +- "fmt" +- "net/http" +-) +- +-type parent interface { +- n(f bool) +-} +- +-type yuh struct { +- a int +-} +- +-func (y *yuh) n(f bool) { +- for i := 0; i < 10; i++ { +- fmt.Println(i) - } +-} - -- _ = a[0:] -- _ = a[1:10] -- _ = a[2:len(a)] // want "unneeded: len\\(a\\)" -- _ = a[3:(len(a))] -- _ = a[len(a)-1 : len(a)] // want "unneeded: len\\(a\\)" -- _ = a[2:len(a):len(a)] +-func a(i1 int, _ int, i3 int) int { // want "unused parameter: i2" +- i3 += i1 +- _ = func(_ int) int { // want "unused parameter: z" +- _ = 1 +- return 1 +- } +- return i3 +-} - -- _ = a[:] -- _ = a[:10] -- _ = a[:len(a)] // want "unneeded: len\\(a\\)" -- _ = a[:(len(a))] -- _ = a[:len(a)-1] -- _ = a[:len(a):len(a)] +-func b(_ bytes.Buffer) { // want "unused parameter: c" +- _ = 1 +-} - -- _ = s[0:] -- _ = s[1:10] -- _ = s[2:len(s)] // want "unneeded: len\\(s\\)" -- _ = s[3:(len(s))] -- _ = s[len(a) : len(s)-1] -- _ = s[0:len(b)] -- _ = s[2:len(s):len(s)] +-func z(h http.ResponseWriter, _ *http.Request) { // no report: func z is address-taken +- fmt.Println("Before") +-} - -- _ = s[:] -- _ = s[:10] -- _ = s[:len(s)] // want "unneeded: len\\(s\\)" -- _ = s[:(len(s))] -- _ = s[:len(s)-1] -- _ = s[:len(b)] -- _ = s[:len(s):len(s)] +-func l(_ http.Handler) http.Handler { // want "unused parameter: h" +- return http.HandlerFunc(z) +-} - -- _ = t.s[0:] -- _ = t.s[1:10] -- _ = t.s[2:len(t.s)] -- _ = t.s[3:(len(t.s))] -- _ = t.s[len(a) : len(t.s)-1] -- _ = t.s[0:len(b)] -- _ = t.s[2:len(t.s):len(t.s)] +-func mult(a, _ int) int { // want "unused parameter: b" +- a += 1 +- return a +-} - -- _ = t.s[:] -- _ = t.s[:10] -- _ = t.s[:len(t.s)] -- _ = t.s[:(len(t.s))] -- _ = t.s[:len(t.s)-1] -- _ = t.s[:len(b)] -- _ = t.s[:len(t.s):len(t.s)] +-func y(a int) { +- panic("yo") +-} +- +-var _ = func(x int) {} // empty body: no diagnostic +- +-var _ = func(_ int) { println() } // want "unused parameter: x" +- +-var ( +- calledGlobal = func(_ int) { println() } // want "unused parameter: x" +- addressTakenGlobal = func(x int) { println() } // no report: function is address-taken -) - -func _() { -- s := s[0:len(s)] // want "unneeded: len\\(s\\)" -- _ = s +- calledGlobal(1) +- println(addressTakenGlobal) -} - --func m() { -- maps := []int{} -- _ = maps[1:len(maps)] // want "unneeded: len\\(maps\\)" +-func Exported(unused int) {} // no finding: an exported function may be address-taken +- +-type T int +- +-func (T) m(_ bool) { println() } // want "unused parameter: f" +-func (T) n(f bool) { println() } // no finding: n may match the interface method parent.n +- +-func _() { +- var fib func(x, y int) int +- fib = func(x, _ int) int { // want "unused parameter: y" +- if x < 2 { +- return x +- } +- return fib(x-1, 123) + fib(x-2, 456) +- } +- fib(10, 42) -} -diff -urN a/gopls/internal/lsp/analysis/simplifyslice/testdata/src/a/a.go.golden b/gopls/internal/lsp/analysis/simplifyslice/testdata/src/a/a.go.golden ---- a/gopls/internal/lsp/analysis/simplifyslice/testdata/src/a/a.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/simplifyslice/testdata/src/a/a.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,70 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/analysis/unusedparams/testdata/src/typeparams/typeparams.go b/gopls/internal/analysis/unusedparams/testdata/src/typeparams/typeparams.go +--- a/gopls/internal/analysis/unusedparams/testdata/src/typeparams/typeparams.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/unusedparams/testdata/src/typeparams/typeparams.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,55 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package testdata +-package typeparams - --var ( -- a [10]byte -- b [20]float32 -- s []int -- t struct { -- s []byte -- } +-import ( +- "bytes" +- "fmt" +- "net/http" +-) - -- _ = a[0:] -- _ = a[1:10] -- _ = a[2:] // want "unneeded: len\\(a\\)" -- _ = a[3:(len(a))] -- _ = a[len(a)-1:] // want "unneeded: len\\(a\\)" -- _ = a[2:len(a):len(a)] +-type parent[T any] interface { +- n(f T) +-} - -- _ = a[:] -- _ = a[:10] -- _ = a[:] // want "unneeded: len\\(a\\)" -- _ = a[:(len(a))] -- _ = a[:len(a)-1] -- _ = a[:len(a):len(a)] +-type yuh[T any] struct { +- a T +-} - -- _ = s[0:] -- _ = s[1:10] -- _ = s[2:] // want "unneeded: len\\(s\\)" -- _ = s[3:(len(s))] -- _ = s[len(a) : len(s)-1] -- _ = s[0:len(b)] -- _ = s[2:len(s):len(s)] +-func (y *yuh[int]) n(f bool) { +- for i := 0; i < 10; i++ { +- fmt.Println(i) +- } +-} - -- _ = s[:] -- _ = s[:10] -- _ = s[:] // want "unneeded: len\\(s\\)" -- _ = s[:(len(s))] -- _ = s[:len(s)-1] -- _ = s[:len(b)] -- _ = s[:len(s):len(s)] +-func a[T comparable](i1 int, i2 T, i3 int) int { // want "unused parameter: i2" +- i3 += i1 +- _ = func(z int) int { // want "unused parameter: z" +- _ = 1 +- return 1 +- } +- return i3 +-} - -- _ = t.s[0:] -- _ = t.s[1:10] -- _ = t.s[2:len(t.s)] -- _ = t.s[3:(len(t.s))] -- _ = t.s[len(a) : len(t.s)-1] -- _ = t.s[0:len(b)] -- _ = t.s[2:len(t.s):len(t.s)] +-func b[T any](c bytes.Buffer) { // want "unused parameter: c" +- _ = 1 +-} - -- _ = t.s[:] -- _ = t.s[:10] -- _ = t.s[:len(t.s)] -- _ = t.s[:(len(t.s))] -- _ = t.s[:len(t.s)-1] -- _ = t.s[:len(b)] -- _ = t.s[:len(t.s):len(t.s)] --) +-func z[T http.ResponseWriter](h T, _ *http.Request) { // no report: func z is address-taken +- fmt.Println("Before") +-} - --func _() { -- s := s[0:] // want "unneeded: len\\(s\\)" -- _ = s +-func l(h http.Handler) http.Handler { // want "unused parameter: h" +- return http.HandlerFunc(z[http.ResponseWriter]) -} - --func m() { -- maps := []int{} -- _ = maps[1:] // want "unneeded: len\\(maps\\)" +-func mult(a, b int) int { // want "unused parameter: b" +- a += 1 +- return a -} -diff -urN a/gopls/internal/lsp/analysis/simplifyslice/testdata/src/typeparams/typeparams.go b/gopls/internal/lsp/analysis/simplifyslice/testdata/src/typeparams/typeparams.go ---- a/gopls/internal/lsp/analysis/simplifyslice/testdata/src/typeparams/typeparams.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/simplifyslice/testdata/src/typeparams/typeparams.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,39 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. +- +-func y[T any](a T) { +- panic("yo") +-} +diff -urN a/gopls/internal/analysis/unusedparams/testdata/src/typeparams/typeparams.go.golden b/gopls/internal/analysis/unusedparams/testdata/src/typeparams/typeparams.go.golden +--- a/gopls/internal/analysis/unusedparams/testdata/src/typeparams/typeparams.go.golden 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/unusedparams/testdata/src/typeparams/typeparams.go.golden 1970-01-01 00:00:00.000000000 +0000 +@@ -1,55 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. --// --//go:build go1.18 --// +build go1.18 -- --package testdata -- --type List[E any] []E -- --// TODO(suzmue): add a test for generic slice expressions when https://github.com/golang/go/issues/48618 is closed. --// type S interface{ ~[]int } - --var ( -- a [10]byte -- b [20]float32 -- p List[int] -- -- _ = p[0:] -- _ = p[1:10] -- _ = p[2:len(p)] // want "unneeded: len\\(p\\)" -- _ = p[3:(len(p))] -- _ = p[len(a) : len(p)-1] -- _ = p[0:len(b)] -- _ = p[2:len(p):len(p)] +-package typeparams - -- _ = p[:] -- _ = p[:10] -- _ = p[:len(p)] // want "unneeded: len\\(p\\)" -- _ = p[:(len(p))] -- _ = p[:len(p)-1] -- _ = p[:len(b)] -- _ = p[:len(p):len(p)] +-import ( +- "bytes" +- "fmt" +- "net/http" -) - --func foo[E any](a List[E]) { -- _ = a[0:len(a)] // want "unneeded: len\\(a\\)" +-type parent[T any] interface { +- n(f T) -} -diff -urN a/gopls/internal/lsp/analysis/simplifyslice/testdata/src/typeparams/typeparams.go.golden b/gopls/internal/lsp/analysis/simplifyslice/testdata/src/typeparams/typeparams.go.golden ---- a/gopls/internal/lsp/analysis/simplifyslice/testdata/src/typeparams/typeparams.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/simplifyslice/testdata/src/typeparams/typeparams.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,39 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. --// --//go:build go1.18 --// +build go1.18 - --package testdata +-type yuh[T any] struct { +- a T +-} - --type List[E any] []E +-func (y *yuh[int]) n(f bool) { +- for i := 0; i < 10; i++ { +- fmt.Println(i) +- } +-} - --// TODO(suzmue): add a test for generic slice expressions when https://github.com/golang/go/issues/48618 is closed. --// type S interface{ ~[]int } +-func a[T comparable](i1 int, _ T, i3 int) int { // want "unused parameter: i2" +- i3 += i1 +- _ = func(_ int) int { // want "unused parameter: z" +- _ = 1 +- return 1 +- } +- return i3 +-} - --var ( -- a [10]byte -- b [20]float32 -- p List[int] +-func b[T any](_ bytes.Buffer) { // want "unused parameter: c" +- _ = 1 +-} - -- _ = p[0:] -- _ = p[1:10] -- _ = p[2:] // want "unneeded: len\\(p\\)" -- _ = p[3:(len(p))] -- _ = p[len(a) : len(p)-1] -- _ = p[0:len(b)] -- _ = p[2:len(p):len(p)] +-func z[T http.ResponseWriter](h T, _ *http.Request) { // no report: func z is address-taken +- fmt.Println("Before") +-} - -- _ = p[:] -- _ = p[:10] -- _ = p[:] // want "unneeded: len\\(p\\)" -- _ = p[:(len(p))] -- _ = p[:len(p)-1] -- _ = p[:len(b)] -- _ = p[:len(p):len(p)] --) +-func l(_ http.Handler) http.Handler { // want "unused parameter: h" +- return http.HandlerFunc(z[http.ResponseWriter]) +-} - --func foo[E any](a List[E]) { -- _ = a[0:] // want "unneeded: len\\(a\\)" +-func mult(a, _ int) int { // want "unused parameter: b" +- a += 1 +- return a -} -diff -urN a/gopls/internal/lsp/analysis/stubmethods/stubmethods.go b/gopls/internal/lsp/analysis/stubmethods/stubmethods.go ---- a/gopls/internal/lsp/analysis/stubmethods/stubmethods.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/stubmethods/stubmethods.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,449 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. +- +-func y[T any](a T) { +- panic("yo") +-} +diff -urN a/gopls/internal/analysis/unusedparams/unusedparams.go b/gopls/internal/analysis/unusedparams/unusedparams.go +--- a/gopls/internal/analysis/unusedparams/unusedparams.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/unusedparams/unusedparams.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,308 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package stubmethods +-package unusedparams - -import ( -- "bytes" +- _ "embed" - "fmt" - "go/ast" -- "go/format" -- "go/token" - "go/types" -- "strconv" -- "strings" - - "golang.org/x/tools/go/analysis" -- "golang.org/x/tools/go/ast/astutil" +- "golang.org/x/tools/go/analysis/passes/inspect" +- "golang.org/x/tools/go/ast/inspector" +- "golang.org/x/tools/gopls/internal/util/slices" - "golang.org/x/tools/internal/analysisinternal" -- "golang.org/x/tools/internal/typesinternal" -) - --const Doc = `stub methods analyzer -- --This analyzer generates method stubs for concrete types --in order to implement a target interface` +-//go:embed doc.go +-var doc string - -var Analyzer = &analysis.Analyzer{ -- Name: "stubmethods", -- Doc: Doc, -- Run: run, -- RunDespiteErrors: true, +- Name: "unusedparams", +- Doc: analysisinternal.MustExtractDoc(doc, "unusedparams"), +- Requires: []*analysis.Analyzer{inspect.Analyzer}, +- Run: run, +- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/unusedparams", -} - --// TODO(rfindley): remove this thin wrapper around the stubmethods refactoring, --// and eliminate the stubmethods analyzer. --// --// Previous iterations used the analysis framework for computing refactorings, --// which proved inefficient. --func run(pass *analysis.Pass) (interface{}, error) { -- for _, err := range pass.TypeErrors { -- var file *ast.File -- for _, f := range pass.Files { -- if f.Pos() <= err.Pos && err.Pos < f.End() { -- file = f -- break -- } -- } -- // Get the end position of the error. -- _, _, end, ok := typesinternal.ReadGo116ErrorData(err) -- if !ok { -- var buf bytes.Buffer -- if err := format.Node(&buf, pass.Fset, file); err != nil { -- continue -- } -- end = analysisinternal.TypeErrorEndPos(pass.Fset, buf.Bytes(), err.Pos) -- } -- if diag, ok := DiagnosticForError(pass.Fset, file, err.Pos, end, err.Msg, pass.TypesInfo); ok { -- pass.Report(diag) -- } -- } +-const FixCategory = "unusedparam" // recognized by gopls ApplyFix - -- return nil, nil --} +-func run(pass *analysis.Pass) (any, error) { +- inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) - --// MatchesMessage reports whether msg matches the error message sought after by --// the stubmethods fix. --func MatchesMessage(msg string) bool { -- return strings.Contains(msg, "missing method") || strings.HasPrefix(msg, "cannot convert") --} +- // First find all "address-taken" functions. +- // We must conservatively assume that their parameters +- // are all required to conform to some signature. +- // +- // A named function is address-taken if it is somewhere +- // used not in call position: +- // +- // f(...) // not address-taken +- // use(f) // address-taken +- // +- // A literal function is address-taken if it is not +- // immediately bound to a variable, or if that variable is +- // used not in call position: +- // +- // f := func() { ... }; f() used only in call position +- // var f func(); f = func() { ...f()... }; f() ditto +- // use(func() { ... }) address-taken +- // - --// DiagnosticForError computes a diagnostic suggesting to implement an --// interface to fix the type checking error defined by (start, end, msg). --// --// If no such fix is possible, the second result is false. --// --// TODO(rfindley): simplify this signature once the stubmethods refactoring is --// no longer wedged into the analysis framework. --func DiagnosticForError(fset *token.FileSet, file *ast.File, start, end token.Pos, msg string, info *types.Info) (analysis.Diagnostic, bool) { -- if !MatchesMessage(msg) { -- return analysis.Diagnostic{}, false -- } +- // Note: this algorithm relies on the assumption that the +- // analyzer is called only for the "widest" package for a +- // given file: that is, p_test in preference to p, if both +- // exist. Analyzing only package p may produce diagnostics +- // that would be falsified based on declarations in p_test.go +- // files. The gopls analysis driver does this, but most +- // drivers to not, so running this command in, say, +- // unitchecker or multichecker may produce incorrect results. - -- path, _ := astutil.PathEnclosingInterval(file, start, end) -- si := GetStubInfo(fset, info, path, start) -- if si == nil { -- return analysis.Diagnostic{}, false -- } -- qf := RelativeToFiles(si.Concrete.Obj().Pkg(), file, nil, nil) -- return analysis.Diagnostic{ -- Pos: start, -- End: end, -- Message: fmt.Sprintf("Implement %s", types.TypeString(si.Interface.Type(), qf)), -- }, true --} +- // Gather global information: +- // - uses of functions not in call position +- // - unexported interface methods +- // - all referenced variables - --// StubInfo represents a concrete type --// that wants to stub out an interface type --type StubInfo struct { -- // Interface is the interface that the client wants to implement. -- // When the interface is defined, the underlying object will be a TypeName. -- // Note that we keep track of types.Object instead of types.Type in order -- // to keep a reference to the declaring object's package and the ast file -- // in the case where the concrete type file requires a new import that happens to be renamed -- // in the interface file. -- // TODO(marwan-at-work): implement interface literals. -- Fset *token.FileSet // the FileSet used to type-check the types below -- Interface *types.TypeName -- Concrete *types.Named -- Pointer bool --} +- usesOutsideCall := make(map[types.Object][]*ast.Ident) +- unexportedIMethodNames := make(map[string]bool) +- { +- callPosn := make(map[*ast.Ident]bool) // all idents f appearing in f() calls +- filter := []ast.Node{ +- (*ast.CallExpr)(nil), +- (*ast.InterfaceType)(nil), +- } +- inspect.Preorder(filter, func(n ast.Node) { +- switch n := n.(type) { +- case *ast.CallExpr: +- // Strip off any generic instantiation. +- fun := n.Fun +- switch fun_ := fun.(type) { +- case *ast.IndexExpr: +- fun = fun_.X // f[T]() (funcs[i]() is rejected below) +- case *ast.IndexListExpr: +- fun = fun_.X // f[K, V]() +- } - --// GetStubInfo determines whether the "missing method error" --// can be used to deduced what the concrete and interface types are. --// --// TODO(adonovan): this function (and its following 5 helpers) tries --// to deduce a pair of (concrete, interface) types that are related by --// an assignment, either explicitly or through a return statement or --// function call. This is essentially what the refactor/satisfy does, --// more generally. Refactor to share logic, after auditing 'satisfy' --// for safety on ill-typed code. --func GetStubInfo(fset *token.FileSet, ti *types.Info, path []ast.Node, pos token.Pos) *StubInfo { -- for _, n := range path { -- switch n := n.(type) { -- case *ast.ValueSpec: -- return fromValueSpec(fset, ti, n, pos) -- case *ast.ReturnStmt: -- // An error here may not indicate a real error the user should know about, but it may. -- // Therefore, it would be best to log it out for debugging/reporting purposes instead of ignoring -- // it. However, event.Log takes a context which is not passed via the analysis package. -- // TODO(marwan-at-work): properly log this error. -- si, _ := fromReturnStmt(fset, ti, pos, path, n) -- return si -- case *ast.AssignStmt: -- return fromAssignStmt(fset, ti, n, pos) -- case *ast.CallExpr: -- // Note that some call expressions don't carry the interface type -- // because they don't point to a function or method declaration elsewhere. -- // For eaxmple, "var Interface = (*Concrete)(nil)". In that case, continue -- // this loop to encounter other possibilities such as *ast.ValueSpec or others. -- si := fromCallExpr(fset, ti, pos, n) -- if si != nil { -- return si +- // Find object: +- // record non-exported function, method, or func-typed var. +- var id *ast.Ident +- switch fun := fun.(type) { +- case *ast.Ident: +- id = fun +- case *ast.SelectorExpr: +- id = fun.Sel +- } +- if id != nil && !id.IsExported() { +- switch pass.TypesInfo.Uses[id].(type) { +- case *types.Func, *types.Var: +- callPosn[id] = true +- } +- } +- +- case *ast.InterfaceType: +- // Record the set of names of unexported interface methods. +- // (It would be more precise to record signatures but +- // generics makes it tricky, and this conservative +- // heuristic is close enough.) +- t := pass.TypesInfo.TypeOf(n).(*types.Interface) +- for i := 0; i < t.NumExplicitMethods(); i++ { +- m := t.ExplicitMethod(i) +- if !m.Exported() && m.Name() != "_" { +- unexportedIMethodNames[m.Name()] = true +- } +- } - } -- } -- } -- return nil --} +- }) - --// fromCallExpr tries to find an *ast.CallExpr's function declaration and --// analyzes a function call's signature against the passed in parameter to deduce --// the concrete and interface types. --func fromCallExpr(fset *token.FileSet, ti *types.Info, pos token.Pos, ce *ast.CallExpr) *StubInfo { -- paramIdx := -1 -- for i, p := range ce.Args { -- if pos >= p.Pos() && pos <= p.End() { -- paramIdx = i -- break -- } -- } -- if paramIdx == -1 { -- return nil -- } -- p := ce.Args[paramIdx] -- concObj, pointer := concreteType(p, ti) -- if concObj == nil || concObj.Obj().Pkg() == nil { -- return nil -- } -- tv, ok := ti.Types[ce.Fun] -- if !ok { -- return nil -- } -- sig, ok := tv.Type.(*types.Signature) -- if !ok { -- return nil -- } -- var paramType types.Type -- if sig.Variadic() && paramIdx >= sig.Params().Len()-1 { -- v := sig.Params().At(sig.Params().Len() - 1) -- if s, _ := v.Type().(*types.Slice); s != nil { -- paramType = s.Elem() +- for id, obj := range pass.TypesInfo.Uses { +- if !callPosn[id] { +- // This includes "f = func() {...}", which we deal with below. +- usesOutsideCall[obj] = append(usesOutsideCall[obj], id) +- } - } -- } else if paramIdx < sig.Params().Len() { -- paramType = sig.Params().At(paramIdx).Type() -- } -- if paramType == nil { -- return nil // A type error prevents us from determining the param type. -- } -- iface := ifaceObjFromType(paramType) -- if iface == nil { -- return nil - } -- return &StubInfo{ -- Fset: fset, -- Concrete: concObj, -- Pointer: pointer, -- Interface: iface, -- } --} - --// fromReturnStmt analyzes a "return" statement to extract --// a concrete type that is trying to be returned as an interface type. --// --// For example, func() io.Writer { return myType{} } --// would return StubInfo with the interface being io.Writer and the concrete type being myType{}. --func fromReturnStmt(fset *token.FileSet, ti *types.Info, pos token.Pos, path []ast.Node, rs *ast.ReturnStmt) (*StubInfo, error) { -- returnIdx := -1 -- for i, r := range rs.Results { -- if pos >= r.Pos() && pos <= r.End() { -- returnIdx = i +- // Find all vars (notably parameters) that are used. +- usedVars := make(map[*types.Var]bool) +- for _, obj := range pass.TypesInfo.Uses { +- if v, ok := obj.(*types.Var); ok { +- if v.IsField() { +- continue // no point gathering these +- } +- usedVars[v] = true - } - } -- if returnIdx == -1 { -- return nil, fmt.Errorf("pos %d not within return statement bounds: [%d-%d]", pos, rs.Pos(), rs.End()) -- } -- concObj, pointer := concreteType(rs.Results[returnIdx], ti) -- if concObj == nil || concObj.Obj().Pkg() == nil { -- return nil, nil -- } -- ef := enclosingFunction(path, ti) -- if ef == nil { -- return nil, fmt.Errorf("could not find the enclosing function of the return statement") -- } -- iface := ifaceType(ef.Results.List[returnIdx].Type, ti) -- if iface == nil { -- return nil, nil +- +- // Check each non-address-taken function's parameters are all used. +- filter := []ast.Node{ +- (*ast.FuncDecl)(nil), +- (*ast.FuncLit)(nil), - } -- return &StubInfo{ -- Fset: fset, -- Concrete: concObj, -- Pointer: pointer, -- Interface: iface, -- }, nil --} +- inspect.WithStack(filter, func(n ast.Node, push bool, stack []ast.Node) bool { +- // (We always return true so that we visit nested FuncLits.) - --// fromValueSpec returns *StubInfo from a variable declaration such as --// var x io.Writer = &T{} --func fromValueSpec(fset *token.FileSet, ti *types.Info, vs *ast.ValueSpec, pos token.Pos) *StubInfo { -- var idx int -- for i, vs := range vs.Values { -- if pos >= vs.Pos() && pos <= vs.End() { -- idx = i -- break +- if !push { +- return true - } -- } - -- valueNode := vs.Values[idx] -- ifaceNode := vs.Type -- callExp, ok := valueNode.(*ast.CallExpr) -- // if the ValueSpec is `var _ = myInterface(...)` -- // as opposed to `var _ myInterface = ...` -- if ifaceNode == nil && ok && len(callExp.Args) == 1 { -- ifaceNode = callExp.Fun -- valueNode = callExp.Args[0] -- } -- concObj, pointer := concreteType(valueNode, ti) -- if concObj == nil || concObj.Obj().Pkg() == nil { -- return nil -- } -- ifaceObj := ifaceType(ifaceNode, ti) -- if ifaceObj == nil { -- return nil -- } -- return &StubInfo{ -- Fset: fset, -- Concrete: concObj, -- Interface: ifaceObj, -- Pointer: pointer, -- } --} +- var ( +- fn types.Object // function symbol (*Func, possibly *Var for a FuncLit) +- ftype *ast.FuncType +- body *ast.BlockStmt +- ) +- switch n := n.(type) { +- case *ast.FuncDecl: +- // We can't analyze non-Go functions. +- if n.Body == nil { +- return true +- } - --// fromAssignStmt returns *StubInfo from a variable re-assignment such as --// var x io.Writer --// x = &T{} --func fromAssignStmt(fset *token.FileSet, ti *types.Info, as *ast.AssignStmt, pos token.Pos) *StubInfo { -- idx := -1 -- var lhs, rhs ast.Expr -- // Given a re-assignment interface conversion error, -- // the compiler error shows up on the right hand side of the expression. -- // For example, x = &T{} where x is io.Writer highlights the error -- // under "&T{}" and not "x". -- for i, hs := range as.Rhs { -- if pos >= hs.Pos() && pos <= hs.End() { -- idx = i -- break -- } -- } -- if idx == -1 { -- return nil -- } -- // Technically, this should never happen as -- // we would get a "cannot assign N values to M variables" -- // before we get an interface conversion error. Nonetheless, -- // guard against out of range index errors. -- if idx >= len(as.Lhs) { -- return nil -- } -- lhs, rhs = as.Lhs[idx], as.Rhs[idx] -- ifaceObj := ifaceType(lhs, ti) -- if ifaceObj == nil { -- return nil -- } -- concType, pointer := concreteType(rhs, ti) -- if concType == nil || concType.Obj().Pkg() == nil { -- return nil -- } -- return &StubInfo{ -- Fset: fset, -- Concrete: concType, -- Interface: ifaceObj, -- Pointer: pointer, -- } --} +- // Ignore exported functions and methods: we +- // must assume they may be address-taken in +- // another package. +- if n.Name.IsExported() { +- return true +- } - --// RelativeToFiles returns a types.Qualifier that formats package --// names according to the import environments of the files that define --// the concrete type and the interface type. (Only the imports of the --// latter file are provided.) --// --// This is similar to types.RelativeTo except if a file imports the package with a different name, --// then it will use it. And if the file does import the package but it is ignored, --// then it will return the original name. It also prefers package names in importEnv in case --// an import is missing from concFile but is present among importEnv. --// --// Additionally, if missingImport is not nil, the function will be called whenever the concFile --// is presented with a package that is not imported. This is useful so that as types.TypeString is --// formatting a function signature, it is identifying packages that will need to be imported when --// stubbing an interface. --// --// TODO(rfindley): investigate if this can be merged with source.Qualifier. --func RelativeToFiles(concPkg *types.Package, concFile *ast.File, ifaceImports []*ast.ImportSpec, missingImport func(name, path string)) types.Qualifier { -- return func(other *types.Package) string { -- if other == concPkg { -- return "" -- } +- // Ignore methods that match the name of any +- // interface method declared in this package, +- // as the method's signature may need to conform +- // to the interface. +- if n.Recv != nil && unexportedIMethodNames[n.Name.Name] { +- return true +- } - -- // Check if the concrete file already has the given import, -- // if so return the default package name or the renamed import statement. -- for _, imp := range concFile.Imports { -- impPath, _ := strconv.Unquote(imp.Path.Value) -- isIgnored := imp.Name != nil && (imp.Name.Name == "." || imp.Name.Name == "_") -- // TODO(adonovan): this comparison disregards a vendor prefix in 'other'. -- if impPath == other.Path() && !isIgnored { -- importName := other.Name() -- if imp.Name != nil { -- importName = imp.Name.Name +- fn = pass.TypesInfo.Defs[n.Name].(*types.Func) +- ftype, body = n.Type, n.Body +- +- case *ast.FuncLit: +- // Find the symbol for the variable (if any) +- // to which the FuncLit is bound. +- // (We don't bother to allow ParenExprs.) +- switch parent := stack[len(stack)-2].(type) { +- case *ast.AssignStmt: +- // f = func() {...} +- // f := func() {...} +- for i, rhs := range parent.Rhs { +- if rhs == n { +- if id, ok := parent.Lhs[i].(*ast.Ident); ok { +- fn = pass.TypesInfo.ObjectOf(id) +- +- // Edge case: f = func() {...} +- // should not count as a use. +- if pass.TypesInfo.Uses[id] != nil { +- usesOutsideCall[fn] = slices.Remove(usesOutsideCall[fn], id) +- } +- +- if fn == nil && id.Name == "_" { +- // Edge case: _ = func() {...} +- // has no var. Fake one. +- fn = types.NewVar(id.Pos(), pass.Pkg, id.Name, pass.TypesInfo.TypeOf(n)) +- } +- } +- break +- } - } -- return importName -- } -- } - -- // If the concrete file does not have the import, check if the package -- // is renamed in the interface file and prefer that. -- var importName string -- for _, imp := range ifaceImports { -- impPath, _ := strconv.Unquote(imp.Path.Value) -- isIgnored := imp.Name != nil && (imp.Name.Name == "." || imp.Name.Name == "_") -- // TODO(adonovan): this comparison disregards a vendor prefix in 'other'. -- if impPath == other.Path() && !isIgnored { -- if imp.Name != nil && imp.Name.Name != concPkg.Name() { -- importName = imp.Name.Name +- case *ast.ValueSpec: +- // var f = func() { ... } +- // (unless f is an exported package-level var) +- for i, val := range parent.Values { +- if val == n { +- v := pass.TypesInfo.Defs[parent.Names[i]] +- if !(v.Parent() == pass.Pkg.Scope() && v.Exported()) { +- fn = v +- } +- break +- } - } -- break - } -- } - -- if missingImport != nil { -- missingImport(importName, other.Path()) +- ftype, body = n.Type, n.Body - } - -- // Up until this point, importName must stay empty when calling missingImport, -- // otherwise we'd end up with `import time "time"` which doesn't look idiomatic. -- if importName == "" { -- importName = other.Name() +- // Ignore address-taken functions and methods: unused +- // parameters may be needed to conform to a func type. +- if fn == nil || len(usesOutsideCall[fn]) > 0 { +- return true - } -- return importName -- } --} -- --// ifaceType will try to extract the types.Object that defines --// the interface given the ast.Expr where the "missing method" --// or "conversion" errors happen. --func ifaceType(n ast.Expr, ti *types.Info) *types.TypeName { -- tv, ok := ti.Types[n] -- if !ok { -- return nil -- } -- return ifaceObjFromType(tv.Type) --} -- --func ifaceObjFromType(t types.Type) *types.TypeName { -- named, ok := t.(*types.Named) -- if !ok { -- return nil -- } -- _, ok = named.Underlying().(*types.Interface) -- if !ok { -- return nil -- } -- // Interfaces defined in the "builtin" package return nil a Pkg(). -- // But they are still real interfaces that we need to make a special case for. -- // Therefore, protect gopls from panicking if a new interface type was added in the future. -- if named.Obj().Pkg() == nil && named.Obj().Name() != "error" { -- return nil -- } -- return named.Obj() --} - --// concreteType tries to extract the *types.Named that defines --// the concrete type given the ast.Expr where the "missing method" --// or "conversion" errors happened. If the concrete type is something --// that cannot have methods defined on it (such as basic types), this --// method will return a nil *types.Named. The second return parameter --// is a boolean that indicates whether the concreteType was defined as a --// pointer or value. --func concreteType(n ast.Expr, ti *types.Info) (*types.Named, bool) { -- tv, ok := ti.Types[n] -- if !ok { -- return nil, false -- } -- typ := tv.Type -- ptr, isPtr := typ.(*types.Pointer) -- if isPtr { -- typ = ptr.Elem() -- } -- named, ok := typ.(*types.Named) -- if !ok { -- return nil, false -- } -- return named, isPtr --} +- // If there are no parameters, there are no unused parameters. +- if ftype.Params.NumFields() == 0 { +- return true +- } - --// enclosingFunction returns the signature and type of the function --// enclosing the given position. --func enclosingFunction(path []ast.Node, info *types.Info) *ast.FuncType { -- for _, node := range path { -- switch t := node.(type) { -- case *ast.FuncDecl: -- if _, ok := info.Defs[t.Name]; ok { -- return t.Type +- // To reduce false positives, ignore functions with an +- // empty or panic body. +- // +- // We choose not to ignore functions whose body is a +- // single return statement (as earlier versions did) +- // func f() { return } +- // func f() { return g(...) } +- // as we suspect that was just heuristic to reduce +- // false positives in the earlier unsound algorithm. +- switch len(body.List) { +- case 0: +- // Empty body. Although the parameter is +- // unnecessary, it's pretty obvious to the +- // reader that that's the case, so we allow it. +- return true // func f() {} +- case 1: +- if stmt, ok := body.List[0].(*ast.ExprStmt); ok { +- // We allow a panic body, as it is often a +- // placeholder for a future implementation: +- // func f() { panic(...) } +- if call, ok := stmt.X.(*ast.CallExpr); ok { +- if fun, ok := call.Fun.(*ast.Ident); ok && fun.Name == "panic" { +- return true +- } +- } - } -- case *ast.FuncLit: -- if _, ok := info.Types[t]; ok { -- return t.Type +- } +- +- // Report each unused parameter. +- for _, field := range ftype.Params.List { +- for _, id := range field.Names { +- if id.Name == "_" { +- continue +- } +- param := pass.TypesInfo.Defs[id].(*types.Var) +- if !usedVars[param] { +- start, end := field.Pos(), field.End() +- if len(field.Names) > 1 { +- start, end = id.Pos(), id.End() +- } +- // This diagnostic carries both an edit-based fix to +- // rename the unused parameter, and a command-based fix +- // to remove it (see golang.RemoveUnusedParameter). +- pass.Report(analysis.Diagnostic{ +- Pos: start, +- End: end, +- Message: fmt.Sprintf("unused parameter: %s", id.Name), +- Category: FixCategory, +- SuggestedFixes: []analysis.SuggestedFix{ +- { +- Message: `Rename parameter to "_"`, +- TextEdits: []analysis.TextEdit{{ +- Pos: id.Pos(), +- End: id.End(), +- NewText: []byte("_"), +- }}, +- }, +- { +- Message: fmt.Sprintf("Remove unused parameter %q", id.Name), +- // No TextEdits => computed by gopls command +- }, +- }, +- }) +- } - } - } -- } -- return nil +- +- return true +- }) +- return nil, nil -} -diff -urN a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/a.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/a.go ---- a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/a.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/a.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,28 +0,0 @@ +diff -urN a/gopls/internal/analysis/unusedparams/unusedparams_test.go b/gopls/internal/analysis/unusedparams/unusedparams_test.go +--- a/gopls/internal/analysis/unusedparams/unusedparams_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/unusedparams/unusedparams_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,17 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package undeclared +-package unusedparams_test - --func x() int { -- var z int -- z = y // want "(undeclared name|undefined): y" +-import ( +- "testing" - -- if z == m { // want "(undeclared name|undefined): m" -- z = 1 +- "golang.org/x/tools/go/analysis/analysistest" +- "golang.org/x/tools/gopls/internal/analysis/unusedparams" +-) +- +-func Test(t *testing.T) { +- testdata := analysistest.TestData() +- analysistest.RunWithSuggestedFixes(t, testdata, unusedparams.Analyzer, "a", "typeparams") +-} +diff -urN a/gopls/internal/analysis/unusedvariable/testdata/src/assign/a.go b/gopls/internal/analysis/unusedvariable/testdata/src/assign/a.go +--- a/gopls/internal/analysis/unusedvariable/testdata/src/assign/a.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/unusedvariable/testdata/src/assign/a.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,74 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package a +- +-import ( +- "fmt" +- "os" +-) +- +-type A struct { +- b int +-} +- +-func singleAssignment() { +- v := "s" // want `v.*declared (and|but) not used` +- +- s := []int{ // want `s.*declared (and|but) not used` +- 1, +- 2, - } - -- if z == 1 { -- z = 1 -- } else if z == n+1 { // want "(undeclared name|undefined): n" -- z = 1 +- a := func(s string) bool { // want `a.*declared (and|but) not used` +- return false - } - -- switch z { -- case 10: -- z = 1 -- case a: // want "(undeclared name|undefined): a" -- z = 1 +- if 1 == 1 { +- s := "v" // want `s.*declared (and|but) not used` - } -- return z --} -diff -urN a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/channels.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/channels.go ---- a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/channels.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/channels.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,13 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package undeclared +- panic("I should survive") +-} - --func channels(s string) { -- undefinedChannels(c()) // want "(undeclared name|undefined): undefinedChannels" +-func noOtherStmtsInBlock() { +- v := "s" // want `v.*declared (and|but) not used` -} - --func c() (<-chan string, chan string) { -- return make(<-chan string), make(chan string) +-func partOfMultiAssignment() { +- f, err := os.Open("file") // want `f.*declared (and|but) not used` +- panic(err) -} -diff -urN a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/consecutive_params.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/consecutive_params.go ---- a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/consecutive_params.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/consecutive_params.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,10 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package undeclared +-func sideEffects(cBool chan bool, cInt chan int) { +- b := <-c // want `b.*declared (and|but) not used` +- s := fmt.Sprint("") // want `s.*declared (and|but) not used` +- a := A{ // want `a.*declared (and|but) not used` +- b: func() int { +- return 1 +- }(), +- } +- c := A{<-cInt} // want `c.*declared (and|but) not used` +- d := fInt() + <-cInt // want `d.*declared (and|but) not used` +- e := fBool() && <-cBool // want `e.*declared (and|but) not used` +- f := map[int]int{ // want `f.*declared (and|but) not used` +- fInt(): <-cInt, +- } +- g := []int{<-cInt} // want `g.*declared (and|but) not used` +- h := func(s string) {} // want `h.*declared (and|but) not used` +- i := func(s string) {}() // want `i.*declared (and|but) not used` +-} - --func consecutiveParams() { -- var s string -- undefinedConsecutiveParams(s, s) // want "(undeclared name|undefined): undefinedConsecutiveParams" +-func commentAbove() { +- // v is a variable +- v := "s" // want `v.*declared (and|but) not used` -} -diff -urN a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/error_param.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/error_param.go ---- a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/error_param.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/error_param.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,10 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package undeclared +-func fBool() bool { +- return true +-} - --func errorParam() { -- var err error -- undefinedErrorParam(err) // want "(undeclared name|undefined): undefinedErrorParam" +-func fInt() int { +- return 1 -} -diff -urN a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/literals.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/literals.go ---- a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/literals.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/literals.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,11 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/analysis/unusedvariable/testdata/src/assign/a.go.golden b/gopls/internal/analysis/unusedvariable/testdata/src/assign/a.go.golden +--- a/gopls/internal/analysis/unusedvariable/testdata/src/assign/a.go.golden 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/unusedvariable/testdata/src/assign/a.go.golden 1970-01-01 00:00:00.000000000 +0000 +@@ -1,59 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package undeclared +-package a - --type T struct{} +-import ( +- "fmt" +- "os" +-) - --func literals() { -- undefinedLiterals("hey compiler", T{}, &T{}) // want "(undeclared name|undefined): undefinedLiterals" +-type A struct { +- b int -} -diff -urN a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/operation.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/operation.go ---- a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/operation.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/operation.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,11 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package undeclared +-func singleAssignment() { +- if 1 == 1 { +- } - --import "time" +- panic("I should survive") +-} - --func operation() { -- undefinedOperation(10 * time.Second) // want "(undeclared name|undefined): undefinedOperation" +-func noOtherStmtsInBlock() { -} -diff -urN a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/selector.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/selector.go ---- a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/selector.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/selector.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,10 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package undeclared +-func partOfMultiAssignment() { +- _, err := os.Open("file") // want `f.*declared (and|but) not used` +- panic(err) +-} - --func selector() { -- m := map[int]bool{} -- undefinedSelector(m[1]) // want "(undeclared name|undefined): undefinedSelector" +-func sideEffects(cBool chan bool, cInt chan int) { +- <-c // want `b.*declared (and|but) not used` +- fmt.Sprint("") // want `s.*declared (and|but) not used` +- A{ // want `a.*declared (and|but) not used` +- b: func() int { +- return 1 +- }(), +- } +- A{<-cInt} // want `c.*declared (and|but) not used` +- fInt() + <-cInt // want `d.*declared (and|but) not used` +- fBool() && <-cBool // want `e.*declared (and|but) not used` +- map[int]int{ // want `f.*declared (and|but) not used` +- fInt(): <-cInt, +- } +- []int{<-cInt} // want `g.*declared (and|but) not used` +- func(s string) {}() // want `i.*declared (and|but) not used` -} -diff -urN a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/slice.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/slice.go ---- a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/slice.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/slice.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,9 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package undeclared +-func commentAbove() { +- // v is a variable +-} - --func slice() { -- undefinedSlice([]int{1, 2}) // want "(undeclared name|undefined): undefinedSlice" +-func fBool() bool { +- return true -} -diff -urN a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/tuple.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/tuple.go ---- a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/tuple.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/tuple.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,13 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. +- +-func fInt() int { +- return 1 +-} +diff -urN a/gopls/internal/analysis/unusedvariable/testdata/src/decl/a.go b/gopls/internal/analysis/unusedvariable/testdata/src/decl/a.go +--- a/gopls/internal/analysis/unusedvariable/testdata/src/decl/a.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/unusedvariable/testdata/src/decl/a.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,30 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package undeclared +-package decl - --func tuple() { -- undefinedTuple(b()) // want "(undeclared name|undefined): undefinedTuple" +-func a() { +- var b, c bool // want `b.*declared (and|but) not used` +- panic(c) +- +- if 1 == 1 { +- var s string // want `s.*declared (and|but) not used` +- } -} - --func b() (string, error) { -- return "", nil +-func b() { +- // b is a variable +- var b bool // want `b.*declared (and|but) not used` -} -diff -urN a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/unique_params.go b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/unique_params.go ---- a/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/unique_params.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/undeclaredname/testdata/src/a/unique_params.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,11 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. +- +-func c() { +- var ( +- d string +- +- // some comment for c +- c bool // want `c.*declared (and|but) not used` +- ) +- +- panic(d) +-} +diff -urN a/gopls/internal/analysis/unusedvariable/testdata/src/decl/a.go.golden b/gopls/internal/analysis/unusedvariable/testdata/src/decl/a.go.golden +--- a/gopls/internal/analysis/unusedvariable/testdata/src/decl/a.go.golden 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/unusedvariable/testdata/src/decl/a.go.golden 1970-01-01 00:00:00.000000000 +0000 +@@ -1,24 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package undeclared +-package decl - --func uniqueArguments() { -- var s string -- var i int -- undefinedUniqueArguments(s, i, s) // want "(undeclared name|undefined): undefinedUniqueArguments" +-func a() { +- var c bool // want `b.*declared (and|but) not used` +- panic(c) +- +- if 1 == 1 { +- } +-} +- +-func b() { +- // b is a variable +-} +- +-func c() { +- var ( +- d string +- ) +- panic(d) -} -diff -urN a/gopls/internal/lsp/analysis/undeclaredname/undeclared.go b/gopls/internal/lsp/analysis/undeclaredname/undeclared.go ---- a/gopls/internal/lsp/analysis/undeclaredname/undeclared.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/undeclaredname/undeclared.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,347 +0,0 @@ +diff -urN a/gopls/internal/analysis/unusedvariable/unusedvariable.go b/gopls/internal/analysis/unusedvariable/unusedvariable.go +--- a/gopls/internal/analysis/unusedvariable/unusedvariable.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/unusedvariable/unusedvariable.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,309 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --// Package undeclaredname defines an Analyzer that applies suggested fixes --// to errors of the type "undeclared name: %s". --package undeclaredname +-// Package unusedvariable defines an analyzer that checks for unused variables. +-package unusedvariable - -import ( - "bytes" @@ -11705,58 +12842,45 @@ diff -urN a/gopls/internal/lsp/analysis/undeclaredname/undeclared.go b/gopls/int - "go/token" - "go/types" - "strings" -- "unicode" - - "golang.org/x/tools/go/analysis" - "golang.org/x/tools/go/ast/astutil" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/internal/analysisinternal" -) - --const Doc = `suggested fixes for "undeclared name: <>" -- --This checker provides suggested fixes for type errors of the --type "undeclared name: <>". It will either insert a new statement, --such as: -- --"<> := " -- --or a new function declaration, such as: -- --func <>(inferred parameters) { -- panic("implement me!") --} --` +-const Doc = `check for unused variables and suggest fixes` - -var Analyzer = &analysis.Analyzer{ -- Name: "undeclaredname", +- Name: "unusedvariable", - Doc: Doc, - Requires: []*analysis.Analyzer{}, - Run: run, -- RunDespiteErrors: true, +- RunDespiteErrors: true, // an unusedvariable diagnostic is a compile error +- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/unusedvariable", -} - --// The prefix for this error message changed in Go 1.20. --var undeclaredNamePrefixes = []string{"undeclared name: ", "undefined: "} +-// The suffix for this error message changed in Go 1.20. +-var unusedVariableSuffixes = []string{" declared and not used", " declared but not used"} - -func run(pass *analysis.Pass) (interface{}, error) { -- for _, err := range pass.TypeErrors { -- runForError(pass, err) +- for _, typeErr := range pass.TypeErrors { +- for _, suffix := range unusedVariableSuffixes { +- if strings.HasSuffix(typeErr.Msg, suffix) { +- varName := strings.TrimSuffix(typeErr.Msg, suffix) +- // Beginning in Go 1.23, go/types began quoting vars as `v'. +- varName = strings.Trim(varName, "'`'") +- +- err := runForError(pass, typeErr, varName) +- if err != nil { +- return nil, err +- } +- } +- } - } +- - return nil, nil -} - --func runForError(pass *analysis.Pass, err types.Error) { -- var name string -- for _, prefix := range undeclaredNamePrefixes { -- if !strings.HasPrefix(err.Msg, prefix) { -- continue -- } -- name = strings.TrimPrefix(err.Msg, prefix) -- } -- if name == "" { -- return -- } +-func runForError(pass *analysis.Pass, err types.Error, name string) error { - var file *ast.File - for _, f := range pass.Files { - if f.Pos() <= err.Pos && err.Pos < f.End() { @@ -11765,4922 +12889,3890 @@ diff -urN a/gopls/internal/lsp/analysis/undeclaredname/undeclared.go b/gopls/int - } - } - if file == nil { -- return +- return nil - } - -- // Get the path for the relevant range. - path, _ := astutil.PathEnclosingInterval(file, err.Pos, err.Pos) - if len(path) < 2 { -- return +- return nil - } +- - ident, ok := path[0].(*ast.Ident) - if !ok || ident.Name != name { -- return +- return nil - } - -- // Undeclared quick fixes only work in function bodies. -- inFunc := false -- for i := range path { -- if _, inFunc = path[i].(*ast.FuncDecl); inFunc { -- if i == 0 { -- return -- } -- if _, isBody := path[i-1].(*ast.BlockStmt); !isBody { -- return -- } -- break -- } -- } -- if !inFunc { -- return -- } -- // Skip selector expressions because it might be too complex -- // to try and provide a suggested fix for fields and methods. -- if _, ok := path[1].(*ast.SelectorExpr); ok { -- return -- } -- tok := pass.Fset.File(file.Pos()) -- if tok == nil { -- return -- } -- offset := safetoken.StartPosition(pass.Fset, err.Pos).Offset -- end := tok.Pos(offset + len(name)) // TODO(adonovan): dubious! err.Pos + len(name)?? -- pass.Report(analysis.Diagnostic{ -- Pos: err.Pos, -- End: end, +- diag := analysis.Diagnostic{ +- Pos: ident.Pos(), +- End: ident.End(), - Message: err.Msg, -- }) --} -- --func SuggestedFix(fset *token.FileSet, start, end token.Pos, content []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { -- pos := start // don't use the end -- path, _ := astutil.PathEnclosingInterval(file, pos, pos) -- if len(path) < 2 { -- return nil, fmt.Errorf("no expression found") -- } -- ident, ok := path[0].(*ast.Ident) -- if !ok { -- return nil, fmt.Errorf("no identifier found") - } - -- // Check for a possible call expression, in which case we should add a -- // new function declaration. -- if len(path) > 1 { -- if _, ok := path[1].(*ast.CallExpr); ok { -- return newFunctionDeclaration(path, file, pkg, info, fset) -- } -- } +- for i := range path { +- switch stmt := path[i].(type) { +- case *ast.ValueSpec: +- // Find GenDecl to which offending ValueSpec belongs. +- if decl, ok := path[i+1].(*ast.GenDecl); ok { +- fixes := removeVariableFromSpec(pass, path, stmt, decl, ident) +- // fixes may be nil +- if len(fixes) > 0 { +- diag.SuggestedFixes = fixes +- pass.Report(diag) +- } +- } - -- // Get the place to insert the new statement. -- insertBeforeStmt := analysisinternal.StmtToInsertVarBefore(path) -- if insertBeforeStmt == nil { -- return nil, fmt.Errorf("could not locate insertion point") -- } +- case *ast.AssignStmt: +- if stmt.Tok != token.DEFINE { +- continue +- } - -- insertBefore := safetoken.StartPosition(fset, insertBeforeStmt.Pos()).Offset +- containsIdent := false +- for _, expr := range stmt.Lhs { +- if expr == ident { +- containsIdent = true +- } +- } +- if !containsIdent { +- continue +- } - -- // Get the indent to add on the line after the new statement. -- // Since this will have a parse error, we can not use format.Source(). -- contentBeforeStmt, indent := content[:insertBefore], "\n" -- if nl := bytes.LastIndex(contentBeforeStmt, []byte("\n")); nl != -1 { -- indent = string(contentBeforeStmt[nl:]) +- fixes := removeVariableFromAssignment(path, stmt, ident) +- // fixes may be nil +- if len(fixes) > 0 { +- diag.SuggestedFixes = fixes +- pass.Report(diag) +- } +- } - } - -- // Create the new local variable statement. -- newStmt := fmt.Sprintf("%s := %s", ident.Name, indent) -- return &analysis.SuggestedFix{ -- Message: fmt.Sprintf("Create variable \"%s\"", ident.Name), -- TextEdits: []analysis.TextEdit{{ -- Pos: insertBeforeStmt.Pos(), -- End: insertBeforeStmt.Pos(), -- NewText: []byte(newStmt), -- }}, -- }, nil +- return nil -} - --func newFunctionDeclaration(path []ast.Node, file *ast.File, pkg *types.Package, info *types.Info, fset *token.FileSet) (*analysis.SuggestedFix, error) { -- if len(path) < 3 { -- return nil, fmt.Errorf("unexpected set of enclosing nodes: %v", path) -- } -- ident, ok := path[0].(*ast.Ident) -- if !ok { -- return nil, fmt.Errorf("no name for function declaration %v (%T)", path[0], path[0]) -- } -- call, ok := path[1].(*ast.CallExpr) -- if !ok { -- return nil, fmt.Errorf("no call expression found %v (%T)", path[1], path[1]) -- } -- -- // Find the enclosing function, so that we can add the new declaration -- // below. -- var enclosing *ast.FuncDecl -- for _, n := range path { -- if n, ok := n.(*ast.FuncDecl); ok { -- enclosing = n -- break -- } -- } -- // TODO(rstambler): Support the situation when there is no enclosing -- // function. -- if enclosing == nil { -- return nil, fmt.Errorf("no enclosing function found: %v", path) -- } -- -- pos := enclosing.End() +-func removeVariableFromSpec(pass *analysis.Pass, path []ast.Node, stmt *ast.ValueSpec, decl *ast.GenDecl, ident *ast.Ident) []analysis.SuggestedFix { +- newDecl := new(ast.GenDecl) +- *newDecl = *decl +- newDecl.Specs = nil - -- var paramNames []string -- var paramTypes []types.Type -- // keep track of all param names to later ensure uniqueness -- nameCounts := map[string]int{} -- for _, arg := range call.Args { -- typ := info.TypeOf(arg) -- if typ == nil { -- return nil, fmt.Errorf("unable to determine type for %s", arg) +- for _, spec := range decl.Specs { +- if spec != stmt { +- newDecl.Specs = append(newDecl.Specs, spec) +- continue - } - -- switch t := typ.(type) { -- // this is the case where another function call returning multiple -- // results is used as an argument -- case *types.Tuple: -- n := t.Len() -- for i := 0; i < n; i++ { -- name := typeToArgName(t.At(i).Type()) -- nameCounts[name]++ -- -- paramNames = append(paramNames, name) -- paramTypes = append(paramTypes, types.Default(t.At(i).Type())) -- } -- -- default: -- // does the argument have a name we can reuse? -- // only happens in case of a *ast.Ident -- var name string -- if ident, ok := arg.(*ast.Ident); ok { -- name = ident.Name -- } +- newSpec := new(ast.ValueSpec) +- *newSpec = *stmt +- newSpec.Names = nil - -- if name == "" { -- name = typeToArgName(typ) +- for _, n := range stmt.Names { +- if n != ident { +- newSpec.Names = append(newSpec.Names, n) - } -- -- nameCounts[name]++ -- -- paramNames = append(paramNames, name) -- paramTypes = append(paramTypes, types.Default(typ)) - } -- } - -- for n, c := range nameCounts { -- // Any names we saw more than once will need a unique suffix added -- // on. Reset the count to 1 to act as the suffix for the first -- // occurrence of that name. -- if c >= 2 { -- nameCounts[n] = 1 -- } else { -- delete(nameCounts, n) +- if len(newSpec.Names) > 0 { +- newDecl.Specs = append(newDecl.Specs, newSpec) - } - } - -- params := &ast.FieldList{} -- -- for i, name := range paramNames { -- if suffix, repeats := nameCounts[name]; repeats { -- nameCounts[name]++ -- name = fmt.Sprintf("%s%d", name, suffix) -- } +- // decl.End() does not include any comments, so if a comment is present we +- // need to account for it when we delete the statement +- end := decl.End() +- if stmt.Comment != nil && stmt.Comment.End() > end { +- end = stmt.Comment.End() +- } - -- // only worth checking after previous param in the list -- if i > 0 { -- // if type of parameter at hand is the same as the previous one, -- // add it to the previous param list of identifiers so to have: -- // (s1, s2 string) -- // and not -- // (s1 string, s2 string) -- if paramTypes[i] == paramTypes[i-1] { -- params.List[len(params.List)-1].Names = append(params.List[len(params.List)-1].Names, ast.NewIdent(name)) -- continue +- // There are no other specs left in the declaration, the whole statement can +- // be deleted +- if len(newDecl.Specs) == 0 { +- // Find parent DeclStmt and delete it +- for _, node := range path { +- if declStmt, ok := node.(*ast.DeclStmt); ok { +- edits := deleteStmtFromBlock(path, declStmt) +- if len(edits) == 0 { +- return nil // can this happen? +- } +- return []analysis.SuggestedFix{ +- { +- Message: suggestedFixMessage(ident.Name), +- TextEdits: edits, +- }, +- } - } - } +- } - -- params.List = append(params.List, &ast.Field{ -- Names: []*ast.Ident{ -- ast.NewIdent(name), -- }, -- Type: analysisinternal.TypeExpr(file, pkg, paramTypes[i]), -- }) +- var b bytes.Buffer +- if err := format.Node(&b, pass.Fset, newDecl); err != nil { +- return nil - } - -- decl := &ast.FuncDecl{ -- Name: ast.NewIdent(ident.Name), -- Type: &ast.FuncType{ -- Params: params, -- // TODO(rstambler): Also handle result parameters here. -- }, -- Body: &ast.BlockStmt{ -- List: []ast.Stmt{ -- &ast.ExprStmt{ -- X: &ast.CallExpr{ -- Fun: ast.NewIdent("panic"), -- Args: []ast.Expr{ -- &ast.BasicLit{ -- Value: `"unimplemented"`, -- }, -- }, -- }, +- return []analysis.SuggestedFix{ +- { +- Message: suggestedFixMessage(ident.Name), +- TextEdits: []analysis.TextEdit{ +- { +- Pos: decl.Pos(), +- // Avoid adding a new empty line +- End: end + 1, +- NewText: b.Bytes(), - }, - }, - }, - } +-} - -- b := bytes.NewBufferString("\n\n") -- if err := format.Node(b, fset, decl); err != nil { -- return nil, err -- } -- return &analysis.SuggestedFix{ -- Message: fmt.Sprintf("Create function \"%s\"", ident.Name), -- TextEdits: []analysis.TextEdit{{ -- Pos: pos, -- End: pos, -- NewText: b.Bytes(), -- }}, -- }, nil --} --func typeToArgName(ty types.Type) string { -- s := types.Default(ty).String() +-func removeVariableFromAssignment(path []ast.Node, stmt *ast.AssignStmt, ident *ast.Ident) []analysis.SuggestedFix { +- // The only variable in the assignment is unused +- if len(stmt.Lhs) == 1 { +- // If LHS has only one expression to be valid it has to have 1 expression +- // on RHS +- // +- // RHS may have side effects, preserve RHS +- if exprMayHaveSideEffects(stmt.Rhs[0]) { +- // Delete until RHS +- return []analysis.SuggestedFix{ +- { +- Message: suggestedFixMessage(ident.Name), +- TextEdits: []analysis.TextEdit{ +- { +- Pos: ident.Pos(), +- End: stmt.Rhs[0].Pos(), +- }, +- }, +- }, +- } +- } - -- switch t := ty.(type) { -- case *types.Basic: -- // use first letter in type name for basic types -- return s[0:1] -- case *types.Slice: -- // use element type to decide var name for slices -- return typeToArgName(t.Elem()) -- case *types.Array: -- // use element type to decide var name for arrays -- return typeToArgName(t.Elem()) -- case *types.Chan: -- return "ch" +- // RHS does not have any side effects, delete the whole statement +- edits := deleteStmtFromBlock(path, stmt) +- if len(edits) == 0 { +- return nil // can this happen? +- } +- return []analysis.SuggestedFix{ +- { +- Message: suggestedFixMessage(ident.Name), +- TextEdits: edits, +- }, +- } - } - -- s = strings.TrimFunc(s, func(r rune) bool { -- return !unicode.IsLetter(r) -- }) -- -- if s == "error" { -- return "err" +- // Otherwise replace ident with `_` +- return []analysis.SuggestedFix{ +- { +- Message: suggestedFixMessage(ident.Name), +- TextEdits: []analysis.TextEdit{ +- { +- Pos: ident.Pos(), +- End: ident.End(), +- NewText: []byte("_"), +- }, +- }, +- }, - } -- -- // remove package (if present) -- // and make first letter lowercase -- a := []rune(s[strings.LastIndexByte(s, '.')+1:]) -- a[0] = unicode.ToLower(a[0]) -- return string(a) --} -diff -urN a/gopls/internal/lsp/analysis/undeclaredname/undeclared_test.go b/gopls/internal/lsp/analysis/undeclaredname/undeclared_test.go ---- a/gopls/internal/lsp/analysis/undeclaredname/undeclared_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/undeclaredname/undeclared_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,17 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package undeclaredname_test -- --import ( -- "testing" -- -- "golang.org/x/tools/go/analysis/analysistest" -- "golang.org/x/tools/gopls/internal/lsp/analysis/undeclaredname" --) -- --func Test(t *testing.T) { -- testdata := analysistest.TestData() -- analysistest.Run(t, testdata, undeclaredname.Analyzer, "a") --} -diff -urN a/gopls/internal/lsp/analysis/unusedparams/cmd/main.go b/gopls/internal/lsp/analysis/unusedparams/cmd/main.go ---- a/gopls/internal/lsp/analysis/unusedparams/cmd/main.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/unusedparams/cmd/main.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,13 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --// The stringintconv command runs the stringintconv analyzer. --package main -- --import ( -- "golang.org/x/tools/go/analysis/singlechecker" -- "golang.org/x/tools/gopls/internal/lsp/analysis/unusedparams" --) -- --func main() { singlechecker.Main(unusedparams.Analyzer) } -diff -urN a/gopls/internal/lsp/analysis/unusedparams/testdata/src/a/a.go b/gopls/internal/lsp/analysis/unusedparams/testdata/src/a/a.go ---- a/gopls/internal/lsp/analysis/unusedparams/testdata/src/a/a.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/unusedparams/testdata/src/a/a.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,55 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package a -- --import ( -- "bytes" -- "fmt" -- "net/http" --) -- --type parent interface { -- n(f bool) -} - --type yuh struct { -- a int +-func suggestedFixMessage(name string) string { +- return fmt.Sprintf("Remove variable %s", name) -} - --func (y *yuh) n(f bool) { -- for i := 0; i < 10; i++ { -- fmt.Println(i) +-func deleteStmtFromBlock(path []ast.Node, stmt ast.Stmt) []analysis.TextEdit { +- // Find innermost enclosing BlockStmt. +- var block *ast.BlockStmt +- for i := range path { +- if blockStmt, ok := path[i].(*ast.BlockStmt); ok { +- block = blockStmt +- break +- } - } --} - --func a(i1 int, i2 int, i3 int) int { // want "potentially unused parameter: 'i2'" -- i3 += i1 -- _ = func(z int) int { // want "potentially unused parameter: 'z'" -- _ = 1 -- return 1 +- nodeIndex := -1 +- for i, blockStmt := range block.List { +- if blockStmt == stmt { +- nodeIndex = i +- break +- } - } -- return i3 --} -- --func b(c bytes.Buffer) { // want "potentially unused parameter: 'c'" -- _ = 1 --} -- --func z(h http.ResponseWriter, _ *http.Request) { // want "potentially unused parameter: 'h'" -- fmt.Println("Before") --} -- --func l(h http.Handler) http.Handler { -- return http.HandlerFunc(z) --} -- --func mult(a, b int) int { // want "potentially unused parameter: 'b'" -- a += 1 -- return a --} -- --func y(a int) { -- panic("yo") --} -diff -urN a/gopls/internal/lsp/analysis/unusedparams/testdata/src/a/a.go.golden b/gopls/internal/lsp/analysis/unusedparams/testdata/src/a/a.go.golden ---- a/gopls/internal/lsp/analysis/unusedparams/testdata/src/a/a.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/unusedparams/testdata/src/a/a.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,55 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package a -- --import ( -- "bytes" -- "fmt" -- "net/http" --) -- --type parent interface { -- n(f bool) --} - --type yuh struct { -- a int --} -- --func (y *yuh) n(f bool) { -- for i := 0; i < 10; i++ { -- fmt.Println(i) +- // The statement we need to delete was not found in BlockStmt +- if nodeIndex == -1 { +- return nil - } --} - --func a(i1 int, _ int, i3 int) int { // want "potentially unused parameter: 'i2'" -- i3 += i1 -- _ = func(_ int) int { // want "potentially unused parameter: 'z'" -- _ = 1 -- return 1 +- // Delete until the end of the block unless there is another statement after +- // the one we are trying to delete +- end := block.Rbrace +- if nodeIndex < len(block.List)-1 { +- end = block.List[nodeIndex+1].Pos() - } -- return i3 --} -- --func b(_ bytes.Buffer) { // want "potentially unused parameter: 'c'" -- _ = 1 --} - --func z(_ http.ResponseWriter, _ *http.Request) { // want "potentially unused parameter: 'h'" -- fmt.Println("Before") --} -- --func l(h http.Handler) http.Handler { -- return http.HandlerFunc(z) +- return []analysis.TextEdit{ +- { +- Pos: stmt.Pos(), +- End: end, +- }, +- } -} - --func mult(a, _ int) int { // want "potentially unused parameter: 'b'" -- a += 1 -- return a --} +-// exprMayHaveSideEffects reports whether the expression may have side effects +-// (because it contains a function call or channel receive). We disregard +-// runtime panics as well written programs should not encounter them. +-func exprMayHaveSideEffects(expr ast.Expr) bool { +- var mayHaveSideEffects bool +- ast.Inspect(expr, func(n ast.Node) bool { +- switch n := n.(type) { +- case *ast.CallExpr: // possible function call +- mayHaveSideEffects = true +- return false +- case *ast.UnaryExpr: +- if n.Op == token.ARROW { // channel receive +- mayHaveSideEffects = true +- return false +- } +- case *ast.FuncLit: +- return false // evaluating what's inside a FuncLit has no effect +- } +- return true +- }) - --func y(a int) { -- panic("yo") +- return mayHaveSideEffects -} -diff -urN a/gopls/internal/lsp/analysis/unusedparams/testdata/src/typeparams/typeparams.go b/gopls/internal/lsp/analysis/unusedparams/testdata/src/typeparams/typeparams.go ---- a/gopls/internal/lsp/analysis/unusedparams/testdata/src/typeparams/typeparams.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/unusedparams/testdata/src/typeparams/typeparams.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,55 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/analysis/unusedvariable/unusedvariable_test.go b/gopls/internal/analysis/unusedvariable/unusedvariable_test.go +--- a/gopls/internal/analysis/unusedvariable/unusedvariable_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/unusedvariable/unusedvariable_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,24 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package typeparams +-package unusedvariable_test - -import ( -- "bytes" -- "fmt" -- "net/http" --) -- --type parent[T any] interface { -- n(f T) --} -- --type yuh[T any] struct { -- a T --} -- --func (y *yuh[int]) n(f bool) { -- for i := 0; i < 10; i++ { -- fmt.Println(i) -- } --} -- --func a[T comparable](i1 int, i2 T, i3 int) int { // want "potentially unused parameter: 'i2'" -- i3 += i1 -- _ = func(z int) int { // want "potentially unused parameter: 'z'" -- _ = 1 -- return 1 -- } -- return i3 --} -- --func b[T any](c bytes.Buffer) { // want "potentially unused parameter: 'c'" -- _ = 1 --} +- "testing" - --func z[T http.ResponseWriter](h T, _ *http.Request) { // want "potentially unused parameter: 'h'" -- fmt.Println("Before") --} +- "golang.org/x/tools/go/analysis/analysistest" +- "golang.org/x/tools/gopls/internal/analysis/unusedvariable" +-) - --func l(h http.Handler) http.Handler { -- return http.HandlerFunc(z[http.ResponseWriter]) --} +-func Test(t *testing.T) { +- testdata := analysistest.TestData() - --func mult(a, b int) int { // want "potentially unused parameter: 'b'" -- a += 1 -- return a --} +- t.Run("decl", func(t *testing.T) { +- analysistest.RunWithSuggestedFixes(t, testdata, unusedvariable.Analyzer, "decl") +- }) - --func y[T any](a T) { -- panic("yo") +- t.Run("assign", func(t *testing.T) { +- analysistest.RunWithSuggestedFixes(t, testdata, unusedvariable.Analyzer, "assign") +- }) -} -diff -urN a/gopls/internal/lsp/analysis/unusedparams/testdata/src/typeparams/typeparams.go.golden b/gopls/internal/lsp/analysis/unusedparams/testdata/src/typeparams/typeparams.go.golden ---- a/gopls/internal/lsp/analysis/unusedparams/testdata/src/typeparams/typeparams.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/unusedparams/testdata/src/typeparams/typeparams.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,55 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/analysis/useany/testdata/src/a/a.go b/gopls/internal/analysis/useany/testdata/src/a/a.go +--- a/gopls/internal/analysis/useany/testdata/src/a/a.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/useany/testdata/src/a/a.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,25 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package typeparams -- --import ( -- "bytes" -- "fmt" -- "net/http" --) +-// This file contains tests for the useany checker. - --type parent[T any] interface { -- n(f T) --} +-package a - --type yuh[T any] struct { -- a T --} +-type Any interface{} - --func (y *yuh[int]) n(f bool) { -- for i := 0; i < 10; i++ { -- fmt.Println(i) -- } --} +-func _[T interface{}]() {} // want "could use \"any\" for this empty interface" +-func _[X any, T interface{}]() {} // want "could use \"any\" for this empty interface" +-func _[any interface{}]() {} // want "could use \"any\" for this empty interface" +-func _[T Any]() {} // want "could use \"any\" for this empty interface" +-func _[T interface{ int | interface{} }]() {} // want "could use \"any\" for this empty interface" +-func _[T interface{ int | Any }]() {} // want "could use \"any\" for this empty interface" +-func _[T any]() {} - --func a[T comparable](i1 int, _ T, i3 int) int { // want "potentially unused parameter: 'i2'" -- i3 += i1 -- _ = func(_ int) int { // want "potentially unused parameter: 'z'" -- _ = 1 -- return 1 -- } -- return i3 --} +-type _[T interface{}] int // want "could use \"any\" for this empty interface" +-type _[X any, T interface{}] int // want "could use \"any\" for this empty interface" +-type _[any interface{}] int // want "could use \"any\" for this empty interface" +-type _[T Any] int // want "could use \"any\" for this empty interface" +-type _[T interface{ int | interface{} }] int // want "could use \"any\" for this empty interface" +-type _[T interface{ int | Any }] int // want "could use \"any\" for this empty interface" +-type _[T any] int +diff -urN a/gopls/internal/analysis/useany/testdata/src/a/a.go.golden b/gopls/internal/analysis/useany/testdata/src/a/a.go.golden +--- a/gopls/internal/analysis/useany/testdata/src/a/a.go.golden 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/useany/testdata/src/a/a.go.golden 1970-01-01 00:00:00.000000000 +0000 +@@ -1,25 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func b[T any](_ bytes.Buffer) { // want "potentially unused parameter: 'c'" -- _ = 1 --} +-// This file contains tests for the useany checker. - --func z[T http.ResponseWriter](_ T, _ *http.Request) { // want "potentially unused parameter: 'h'" -- fmt.Println("Before") --} +-package a - --func l(h http.Handler) http.Handler { -- return http.HandlerFunc(z[http.ResponseWriter]) --} +-type Any interface{} - --func mult(a, _ int) int { // want "potentially unused parameter: 'b'" -- a += 1 -- return a --} +-func _[T any]() {} // want "could use \"any\" for this empty interface" +-func _[X any, T any]() {} // want "could use \"any\" for this empty interface" +-func _[any interface{}]() {} // want "could use \"any\" for this empty interface" +-func _[T any]() {} // want "could use \"any\" for this empty interface" +-func _[T any]() {} // want "could use \"any\" for this empty interface" +-func _[T any]() {} // want "could use \"any\" for this empty interface" +-func _[T any]() {} - --func y[T any](a T) { -- panic("yo") --} -diff -urN a/gopls/internal/lsp/analysis/unusedparams/unusedparams.go b/gopls/internal/lsp/analysis/unusedparams/unusedparams.go ---- a/gopls/internal/lsp/analysis/unusedparams/unusedparams.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/unusedparams/unusedparams.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,166 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +-type _[T any] int // want "could use \"any\" for this empty interface" +-type _[X any, T any] int // want "could use \"any\" for this empty interface" +-type _[any interface{}] int // want "could use \"any\" for this empty interface" +-type _[T any] int // want "could use \"any\" for this empty interface" +-type _[T any] int // want "could use \"any\" for this empty interface" +-type _[T any] int // want "could use \"any\" for this empty interface" +-type _[T any] int +diff -urN a/gopls/internal/analysis/useany/useany.go b/gopls/internal/analysis/useany/useany.go +--- a/gopls/internal/analysis/useany/useany.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/useany/useany.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,98 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --// Package unusedparams defines an analyzer that checks for unused --// parameters of functions. --package unusedparams +-// Package useany defines an Analyzer that checks for usage of interface{} in +-// constraints, rather than the predeclared any. +-package useany - -import ( - "fmt" - "go/ast" +- "go/token" - "go/types" -- "strings" - - "golang.org/x/tools/go/analysis" - "golang.org/x/tools/go/analysis/passes/inspect" - "golang.org/x/tools/go/ast/inspector" -) - --const Doc = `check for unused parameters of functions -- --The unusedparams analyzer checks functions to see if there are --any parameters that are not being used. -- --To reduce false positives it ignores: --- methods --- parameters that do not have a name or have the name '_' (the blank identifier) --- functions in test files --- functions with empty bodies or those with just a return stmt` -- --var ( -- Analyzer = &analysis.Analyzer{ -- Name: "unusedparams", -- Doc: Doc, -- Requires: []*analysis.Analyzer{inspect.Analyzer}, -- Run: run, -- } -- inspectLits bool -- inspectWrappers bool --) -- --func init() { -- Analyzer.Flags.BoolVar(&inspectLits, "lits", true, "inspect function literals") -- Analyzer.Flags.BoolVar(&inspectWrappers, "wrappers", false, "inspect functions whose body consists of a single return statement") --} +-const Doc = `check for constraints that could be simplified to "any"` - --type paramData struct { -- field *ast.Field -- ident *ast.Ident -- typObj types.Object +-var Analyzer = &analysis.Analyzer{ +- Name: "useany", +- Doc: Doc, +- Requires: []*analysis.Analyzer{inspect.Analyzer}, +- Run: run, +- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/useany", -} - -func run(pass *analysis.Pass) (interface{}, error) { - inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) -- nodeFilter := []ast.Node{ -- (*ast.FuncDecl)(nil), -- } -- if inspectLits { -- nodeFilter = append(nodeFilter, (*ast.FuncLit)(nil)) -- } - -- inspect.Preorder(nodeFilter, func(n ast.Node) { -- var fieldList *ast.FieldList -- var body *ast.BlockStmt +- universeAny := types.Universe.Lookup("any") - -- // Get the fieldList and body from the function node. -- switch f := n.(type) { -- case *ast.FuncDecl: -- fieldList, body = f.Type.Params, f.Body -- // TODO(golang/go#36602): add better handling for methods, if we enable methods -- // we will get false positives if a struct is potentially implementing -- // an interface. -- if f.Recv != nil { -- return -- } +- nodeFilter := []ast.Node{ +- (*ast.TypeSpec)(nil), +- (*ast.FuncType)(nil), +- } - -- // Ignore functions in _test.go files to reduce false positives. -- if file := pass.Fset.File(n.Pos()); file != nil && strings.HasSuffix(file.Name(), "_test.go") { -- return -- } -- case *ast.FuncLit: -- fieldList, body = f.Type.Params, f.Body +- inspect.Preorder(nodeFilter, func(node ast.Node) { +- var tparams *ast.FieldList +- switch node := node.(type) { +- case *ast.TypeSpec: +- tparams = node.TypeParams +- case *ast.FuncType: +- tparams = node.TypeParams +- default: +- panic(fmt.Sprintf("unexpected node type %T", node)) - } -- // If there are no arguments or the function is empty, then return. -- if fieldList.NumFields() == 0 || body == nil || len(body.List) == 0 { +- if tparams.NumFields() == 0 { - return - } - -- switch expr := body.List[0].(type) { -- case *ast.ReturnStmt: -- if !inspectWrappers { -- // Ignore functions that only contain a return statement to reduce false positives. -- return -- } -- case *ast.ExprStmt: -- callExpr, ok := expr.X.(*ast.CallExpr) -- if !ok || len(body.List) > 1 { -- break +- for _, field := range tparams.List { +- typ := pass.TypesInfo.Types[field.Type].Type +- if typ == nil { +- continue // something is wrong, but not our concern - } -- // Ignore functions that only contain a panic statement to reduce false positives. -- if fun, ok := callExpr.Fun.(*ast.Ident); ok && fun.Name == "panic" { -- return +- iface, ok := typ.Underlying().(*types.Interface) +- if !ok { +- continue // invalid constraint - } -- } - -- // Get the useful data from each field. -- params := make(map[string]*paramData) -- unused := make(map[*paramData]bool) -- for _, f := range fieldList.List { -- for _, i := range f.Names { -- if i.Name == "_" { +- // If the constraint is the empty interface, offer a fix to use 'any' +- // instead. +- if iface.Empty() { +- id, _ := field.Type.(*ast.Ident) +- if id != nil && pass.TypesInfo.Uses[id] == universeAny { - continue - } -- params[i.Name] = ¶mData{ -- field: f, -- ident: i, -- typObj: pass.TypesInfo.ObjectOf(i), +- +- diag := analysis.Diagnostic{ +- Pos: field.Type.Pos(), +- End: field.Type.End(), +- Message: `could use "any" for this empty interface`, - } -- unused[params[i.Name]] = true -- } -- } - -- // Traverse through the body of the function and -- // check to see which parameters are unused. -- ast.Inspect(body, func(node ast.Node) bool { -- n, ok := node.(*ast.Ident) -- if !ok { -- return true -- } -- param, ok := params[n.Name] -- if !ok { -- return false -- } -- if nObj := pass.TypesInfo.ObjectOf(n); nObj != param.typObj { -- return false -- } -- delete(unused, param) -- return false -- }) +- // Only suggest a fix to 'any' if we actually resolve the predeclared +- // any in this scope. +- if scope := pass.TypesInfo.Scopes[node]; scope != nil { +- if _, any := scope.LookupParent("any", token.NoPos); any == universeAny { +- diag.SuggestedFixes = []analysis.SuggestedFix{{ +- Message: `use "any"`, +- TextEdits: []analysis.TextEdit{{ +- Pos: field.Type.Pos(), +- End: field.Type.End(), +- NewText: []byte("any"), +- }}, +- }} +- } +- } - -- // Create the reports for the unused parameters. -- for u := range unused { -- start, end := u.field.Pos(), u.field.End() -- if len(u.field.Names) > 1 { -- start, end = u.ident.Pos(), u.ident.End() +- pass.Report(diag) - } -- // TODO(golang/go#36602): Add suggested fixes to automatically -- // remove the unused parameter from every use of this -- // function. -- pass.Report(analysis.Diagnostic{ -- Pos: start, -- End: end, -- Message: fmt.Sprintf("potentially unused parameter: '%s'", u.ident.Name), -- SuggestedFixes: []analysis.SuggestedFix{{ -- Message: `Replace with "_"`, -- TextEdits: []analysis.TextEdit{{ -- Pos: u.ident.Pos(), -- End: u.ident.End(), -- NewText: []byte("_"), -- }}, -- }}, -- }) - } - }) - return nil, nil -} -diff -urN a/gopls/internal/lsp/analysis/unusedparams/unusedparams_test.go b/gopls/internal/lsp/analysis/unusedparams/unusedparams_test.go ---- a/gopls/internal/lsp/analysis/unusedparams/unusedparams_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/unusedparams/unusedparams_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,22 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/analysis/useany/useany_test.go b/gopls/internal/analysis/useany/useany_test.go +--- a/gopls/internal/analysis/useany/useany_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/analysis/useany/useany_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,17 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package unusedparams_test +-package useany_test - -import ( - "testing" - - "golang.org/x/tools/go/analysis/analysistest" -- "golang.org/x/tools/gopls/internal/lsp/analysis/unusedparams" -- "golang.org/x/tools/internal/typeparams" +- "golang.org/x/tools/gopls/internal/analysis/useany" -) - -func Test(t *testing.T) { - testdata := analysistest.TestData() -- tests := []string{"a"} -- if typeparams.Enabled { -- tests = append(tests, "typeparams") -- } -- analysistest.RunWithSuggestedFixes(t, testdata, unusedparams.Analyzer, tests...) +- analysistest.RunWithSuggestedFixes(t, testdata, useany.Analyzer, "a") -} -diff -urN a/gopls/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go b/gopls/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go ---- a/gopls/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,74 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/cache/analysis.go b/gopls/internal/cache/analysis.go +--- a/gopls/internal/cache/analysis.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/analysis.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,1581 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package a +-package cache +- +-// This file defines gopls' driver for modular static analysis (go/analysis). - -import ( +- "bytes" +- "context" +- "crypto/sha256" +- "encoding/gob" +- "encoding/json" +- "errors" - "fmt" -- "os" --) -- --type A struct { -- b int --} -- --func singleAssignment() { -- v := "s" // want `v declared (and|but) not used` -- -- s := []int{ // want `s declared (and|but) not used` -- 1, -- 2, -- } -- -- a := func(s string) bool { // want `a declared (and|but) not used` -- return false -- } -- -- if 1 == 1 { -- s := "v" // want `s declared (and|but) not used` -- } -- -- panic("I should survive") --} -- --func noOtherStmtsInBlock() { -- v := "s" // want `v declared (and|but) not used` --} -- --func partOfMultiAssignment() { -- f, err := os.Open("file") // want `f declared (and|but) not used` -- panic(err) --} +- "go/ast" +- "go/parser" +- "go/token" +- "go/types" +- "log" +- urlpkg "net/url" +- "path/filepath" +- "reflect" +- "runtime" +- "runtime/debug" +- "sort" +- "strings" +- "sync" +- "sync/atomic" +- "time" - --func sideEffects(cBool chan bool, cInt chan int) { -- b := <-c // want `b declared (and|but) not used` -- s := fmt.Sprint("") // want `s declared (and|but) not used` -- a := A{ // want `a declared (and|but) not used` -- b: func() int { -- return 1 -- }(), -- } -- c := A{<-cInt} // want `c declared (and|but) not used` -- d := fInt() + <-cInt // want `d declared (and|but) not used` -- e := fBool() && <-cBool // want `e declared (and|but) not used` -- f := map[int]int{ // want `f declared (and|but) not used` -- fInt(): <-cInt, -- } -- g := []int{<-cInt} // want `g declared (and|but) not used` -- h := func(s string) {} // want `h declared (and|but) not used` -- i := func(s string) {}() // want `i declared (and|but) not used` --} +- "golang.org/x/sync/errgroup" +- "golang.org/x/tools/go/analysis" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/filecache" +- "golang.org/x/tools/gopls/internal/progress" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/settings" +- "golang.org/x/tools/gopls/internal/util/astutil" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/gopls/internal/util/frob" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/tag" +- "golang.org/x/tools/internal/facts" +- "golang.org/x/tools/internal/gcimporter" +- "golang.org/x/tools/internal/typesinternal" +- "golang.org/x/tools/internal/versions" +-) - --func commentAbove() { -- // v is a variable -- v := "s" // want `v declared (and|but) not used` --} +-/* - --func fBool() bool { -- return true --} +- DESIGN - --func fInt() int { -- return 1 --} -diff -urN a/gopls/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go.golden b/gopls/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go.golden ---- a/gopls/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/unusedvariable/testdata/src/assign/a.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,59 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +- An analysis request (Snapshot.Analyze) is for a set of Analyzers and +- PackageIDs. The result is the set of diagnostics for those +- packages. Each request constructs a transitively closed DAG of +- nodes, each representing a package, then works bottom up in +- parallel postorder calling runCached to ensure that each node's +- analysis summary is up to date. The summary contains the analysis +- diagnostics as well as the intermediate results required by the +- recursion, such as serialized types and facts. - --package a +- The entire DAG is ephemeral. Each node in the DAG records the set +- of analyzers to run: the complete set for the root packages, and +- the "facty" subset for dependencies. Each package is thus analyzed +- at most once. The entire DAG shares a single FileSet for parsing +- and importing. - --import ( -- "fmt" -- "os" --) +- Each node is processed by runCached. It gets the source file +- content hashes for package p, and the summaries of its "vertical" +- dependencies (direct imports), and from them it computes a key +- representing the unit of work (parsing, type-checking, and +- analysis) that it has to do. The key is a cryptographic hash of the +- "recipe" for this step, including the Metadata, the file contents, +- the set of analyzers, and the type and fact information from the +- vertical dependencies. - --type A struct { -- b int --} +- The key is sought in a machine-global persistent file-system based +- cache. If this gopls process, or another gopls process on the same +- machine, has already performed this analysis step, runCached will +- make a cache hit and load the serialized summary of the results. If +- not, it will have to proceed to run() to parse and type-check the +- package and then apply a set of analyzers to it. (The set of +- analyzers applied to a single package itself forms a graph of +- "actions", and it too is evaluated in parallel postorder; these +- dependency edges within the same package are called "horizontal".) +- Finally it writes a new cache entry. The entry contains serialized +- types (export data) and analysis facts. - --func singleAssignment() { -- if 1 == 1 { -- } +- Each node in the DAG acts like a go/types importer mapping, +- providing a consistent view of packages and their objects: the +- mapping for a node is a superset of its dependencies' mappings. +- Every node has an associated *types.Package, initially nil. A +- package is populated during run (cache miss) by type-checking its +- syntax; but for a cache hit, the package is populated lazily, i.e. +- not until it later becomes necessary because it is imported +- directly or referenced by export data higher up in the DAG. - -- panic("I should survive") --} +- For types, we use "shallow" export data. Historically, the Go +- compiler always produced a summary of the types for a given package +- that included types from other packages that it indirectly +- referenced: "deep" export data. This had the advantage that the +- compiler (and analogous tools such as gopls) need only load one +- file per direct import. However, it meant that the files tended to +- get larger based on the level of the package in the import +- graph. For example, higher-level packages in the kubernetes module +- have over 1MB of "deep" export data, even when they have almost no +- content of their own, merely because they mention a major type that +- references many others. In pathological cases the export data was +- 300x larger than the source for a package due to this quadratic +- growth. - --func noOtherStmtsInBlock() { --} +- "Shallow" export data means that the serialized types describe only +- a single package. If those types mention types from other packages, +- the type checker may need to request additional packages beyond +- just the direct imports. Type information for the entire transitive +- closure of imports is provided (lazily) by the DAG. - --func partOfMultiAssignment() { -- _, err := os.Open("file") // want `f declared (and|but) not used` -- panic(err) --} +- For correct dependency analysis, the digest used as a cache key +- must reflect the "deep" export data, so it is derived recursively +- from the transitive closure. As an optimization, we needn't include +- every package of the transitive closure in the deep hash, only the +- packages that were actually requested by the type checker. This +- allows changes to a package that have no effect on its export data +- to be "pruned". The direct consumer will need to be re-executed, +- but if its export data is unchanged as a result, then indirect +- consumers may not need to be re-executed. This allows, for example, +- one to insert a print statement in a function and not "rebuild" the +- whole application (though export data does record line numbers and +- offsets of types which may be perturbed by otherwise insignificant +- changes.) - --func sideEffects(cBool chan bool, cInt chan int) { -- <-c // want `b declared (and|but) not used` -- fmt.Sprint("") // want `s declared (and|but) not used` -- A{ // want `a declared (and|but) not used` -- b: func() int { -- return 1 -- }(), -- } -- A{<-cInt} // want `c declared (and|but) not used` -- fInt() + <-cInt // want `d declared (and|but) not used` -- fBool() && <-cBool // want `e declared (and|but) not used` -- map[int]int{ // want `f declared (and|but) not used` -- fInt(): <-cInt, -- } -- []int{<-cInt} // want `g declared (and|but) not used` -- func(s string) {}() // want `i declared (and|but) not used` --} +- The summary must record whether a package is transitively +- error-free (whether it would compile) because many analyzers are +- not safe to run on packages with inconsistent types. - --func commentAbove() { -- // v is a variable --} +- For fact encoding, we use the same fact set as the unitchecker +- (vet) to record and serialize analysis facts. The fact +- serialization mechanism is analogous to "deep" export data. - --func fBool() bool { -- return true --} +-*/ - --func fInt() int { -- return 1 --} -diff -urN a/gopls/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go b/gopls/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go ---- a/gopls/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,30 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-// TODO(adonovan): +-// - Add a (white-box) test of pruning when a change doesn't affect export data. +-// - Optimise pruning based on subset of packages mentioned in exportdata. +-// - Better logging so that it is possible to deduce why an analyzer +-// is not being run--often due to very indirect failures. +-// Even if the ultimate consumer decides to ignore errors, +-// tests and other situations want to be assured of freedom from +-// errors, not just missing results. This should be recorded. +-// - Split this into a subpackage, gopls/internal/cache/driver, +-// consisting of this file and three helpers from errors.go. +-// The (*snapshot).Analyze method would stay behind and make calls +-// to the driver package. +-// Steps: +-// - define a narrow driver.Snapshot interface with only these methods: +-// Metadata(PackageID) Metadata +-// ReadFile(Context, URI) (file.Handle, error) +-// View() *View // for Options +-// - share cache.{goVersionRx,parseGoImpl} - --package decl +-// AnalysisProgressTitle is the title of the progress report for ongoing +-// analysis. It is sought by regression tests for the progress reporting +-// feature. +-const AnalysisProgressTitle = "Analyzing Dependencies" - --func a() { -- var b, c bool // want `b declared (and|but) not used` -- panic(c) +-// Analyze applies a set of analyzers to the package denoted by id, +-// and returns their diagnostics for that package. +-// +-// The analyzers list must be duplicate free; order does not matter. +-// +-// Notifications of progress may be sent to the optional reporter. +-func (s *Snapshot) Analyze(ctx context.Context, pkgs map[PackageID]*metadata.Package, analyzers []*settings.Analyzer, reporter *progress.Tracker) ([]*Diagnostic, error) { +- start := time.Now() // for progress reporting - -- if 1 == 1 { -- var s string // want `s declared (and|but) not used` +- var tagStr string // sorted comma-separated list of PackageIDs +- { +- // TODO(adonovan): replace with a generic map[S]any -> string +- // function in the tag package, and use maps.Keys + slices.Sort. +- keys := make([]string, 0, len(pkgs)) +- for id := range pkgs { +- keys = append(keys, string(id)) +- } +- sort.Strings(keys) +- tagStr = strings.Join(keys, ",") - } --} -- --func b() { -- // b is a variable -- var b bool // want `b declared (and|but) not used` --} +- ctx, done := event.Start(ctx, "snapshot.Analyze", tag.Package.Of(tagStr)) +- defer done() - --func c() { -- var ( -- d string +- // Filter and sort enabled root analyzers. +- // A disabled analyzer may still be run if required by another. +- toSrc := make(map[*analysis.Analyzer]*settings.Analyzer) +- var enabled []*analysis.Analyzer // enabled subset + transitive requirements +- for _, a := range analyzers { +- if a.IsEnabled(s.Options()) { +- toSrc[a.Analyzer] = a +- enabled = append(enabled, a.Analyzer) +- } +- } +- sort.Slice(enabled, func(i, j int) bool { +- return enabled[i].Name < enabled[j].Name +- }) +- analyzers = nil // prevent accidental use - -- // some comment for c -- c bool // want `c declared (and|but) not used` -- ) +- enabled = requiredAnalyzers(enabled) - -- panic(d) --} -diff -urN a/gopls/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go.golden b/gopls/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go.golden ---- a/gopls/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/unusedvariable/testdata/src/decl/a.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,24 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +- // Perform basic sanity checks. +- // (Ideally we would do this only once.) +- if err := analysis.Validate(enabled); err != nil { +- return nil, fmt.Errorf("invalid analyzer configuration: %v", err) +- } - --package decl +- stableNames := make(map[*analysis.Analyzer]string) - --func a() { -- var c bool // want `b declared (and|but) not used` -- panic(c) +- var facty []*analysis.Analyzer // facty subset of enabled + transitive requirements +- for _, a := range enabled { +- // TODO(adonovan): reject duplicate stable names (very unlikely). +- stableNames[a] = stableName(a) - -- if 1 == 1 { +- // Register fact types of all required analyzers. +- if len(a.FactTypes) > 0 { +- facty = append(facty, a) +- for _, f := range a.FactTypes { +- gob.Register(f) // <2us +- } +- } - } --} +- facty = requiredAnalyzers(facty) - --func b() { -- // b is a variable --} +- // File set for this batch (entire graph) of analysis. +- fset := token.NewFileSet() - --func c() { -- var ( -- d string -- ) -- panic(d) --} -diff -urN a/gopls/internal/lsp/analysis/unusedvariable/unusedvariable.go b/gopls/internal/lsp/analysis/unusedvariable/unusedvariable.go ---- a/gopls/internal/lsp/analysis/unusedvariable/unusedvariable.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/unusedvariable/unusedvariable.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,300 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +- // Starting from the root packages and following DepsByPkgPath, +- // build the DAG of packages we're going to analyze. +- // +- // Root nodes will run the enabled set of analyzers, +- // whereas dependencies will run only the facty set. +- // Because (by construction) enabled is a superset of facty, +- // we can analyze each node with exactly one set of analyzers. +- nodes := make(map[PackageID]*analysisNode) +- var leaves []*analysisNode // nodes with no unfinished successors +- var makeNode func(from *analysisNode, id PackageID) (*analysisNode, error) +- makeNode = func(from *analysisNode, id PackageID) (*analysisNode, error) { +- an, ok := nodes[id] +- if !ok { +- mp := s.Metadata(id) +- if mp == nil { +- return nil, bug.Errorf("no metadata for %s", id) +- } - --// Package unusedvariable defines an analyzer that checks for unused variables. --package unusedvariable +- // -- preorder -- - --import ( -- "bytes" -- "fmt" -- "go/ast" -- "go/format" -- "go/token" -- "go/types" -- "strings" +- an = &analysisNode{ +- fset: fset, +- mp: mp, +- analyzers: facty, // all nodes run at least the facty analyzers +- allDeps: make(map[PackagePath]*analysisNode), +- exportDeps: make(map[PackagePath]*analysisNode), +- stableNames: stableNames, +- } +- nodes[id] = an - -- "golang.org/x/tools/go/analysis" -- "golang.org/x/tools/go/ast/astutil" --) +- // -- recursion -- - --const Doc = `check for unused variables +- // Build subgraphs for dependencies. +- an.succs = make(map[PackageID]*analysisNode, len(mp.DepsByPkgPath)) +- for _, depID := range mp.DepsByPkgPath { +- dep, err := makeNode(an, depID) +- if err != nil { +- return nil, err +- } +- an.succs[depID] = dep - --The unusedvariable analyzer suggests fixes for unused variables errors. --` +- // Compute the union of all dependencies. +- // (This step has quadratic complexity.) +- for pkgPath, node := range dep.allDeps { +- an.allDeps[pkgPath] = node +- } +- } - --var Analyzer = &analysis.Analyzer{ -- Name: "unusedvariable", -- Doc: Doc, -- Requires: []*analysis.Analyzer{}, -- Run: run, -- RunDespiteErrors: true, // an unusedvariable diagnostic is a compile error --} +- // -- postorder -- - --// The suffix for this error message changed in Go 1.20. --var unusedVariableSuffixes = []string{" declared and not used", " declared but not used"} +- an.allDeps[mp.PkgPath] = an // add self entry (reflexive transitive closure) - --func run(pass *analysis.Pass) (interface{}, error) { -- for _, typeErr := range pass.TypeErrors { -- for _, suffix := range unusedVariableSuffixes { -- if strings.HasSuffix(typeErr.Msg, suffix) { -- varName := strings.TrimSuffix(typeErr.Msg, suffix) -- err := runForError(pass, typeErr, varName) +- // Add leaf nodes (no successors) directly to queue. +- if len(an.succs) == 0 { +- leaves = append(leaves, an) +- } +- +- // Load the contents of each compiled Go file through +- // the snapshot's cache. (These are all cache hits as +- // files are pre-loaded following packages.Load) +- an.files = make([]file.Handle, len(mp.CompiledGoFiles)) +- for i, uri := range mp.CompiledGoFiles { +- fh, err := s.ReadFile(ctx, uri) - if err != nil { - return nil, err - } +- an.files[i] = fh - } - } +- // Add edge from predecessor. +- if from != nil { +- atomic.AddInt32(&from.unfinishedSuccs, 1) // TODO(adonovan): use generics +- an.preds = append(an.preds, from) +- } +- atomic.AddInt32(&an.unfinishedPreds, 1) +- return an, nil - } - -- return nil, nil --} -- --func runForError(pass *analysis.Pass, err types.Error, name string) error { -- var file *ast.File -- for _, f := range pass.Files { -- if f.Pos() <= err.Pos && err.Pos < f.End() { -- file = f -- break +- // For root packages, we run the enabled set of analyzers. +- var roots []*analysisNode +- for id := range pkgs { +- root, err := makeNode(nil, id) +- if err != nil { +- return nil, err - } -- } -- if file == nil { -- return nil +- root.analyzers = enabled +- roots = append(roots, root) - } - -- path, _ := astutil.PathEnclosingInterval(file, err.Pos, err.Pos) -- if len(path) < 2 { -- return nil -- } +- // Now that we have read all files, +- // we no longer need the snapshot. +- // (but options are needed for progress reporting) +- options := s.Options() +- s = nil - -- ident, ok := path[0].(*ast.Ident) -- if !ok || ident.Name != name { -- return nil -- } +- // Progress reporting. If supported, gopls reports progress on analysis +- // passes that are taking a long time. +- maybeReport := func(completed int64) {} - -- diag := analysis.Diagnostic{ -- Pos: ident.Pos(), -- End: ident.End(), -- Message: err.Msg, -- } +- // Enable progress reporting if enabled by the user +- // and we have a capable reporter. +- if reporter != nil && reporter.SupportsWorkDoneProgress() && options.AnalysisProgressReporting { +- var reportAfter = options.ReportAnalysisProgressAfter // tests may set this to 0 +- const reportEvery = 1 * time.Second - -- for i := range path { -- switch stmt := path[i].(type) { -- case *ast.ValueSpec: -- // Find GenDecl to which offending ValueSpec belongs. -- if decl, ok := path[i+1].(*ast.GenDecl); ok { -- fixes := removeVariableFromSpec(pass, path, stmt, decl, ident) -- // fixes may be nil -- if len(fixes) > 0 { -- diag.SuggestedFixes = fixes -- pass.Report(diag) -- } -- } +- ctx, cancel := context.WithCancel(ctx) +- defer cancel() - -- case *ast.AssignStmt: -- if stmt.Tok != token.DEFINE { -- continue -- } +- var ( +- reportMu sync.Mutex +- lastReport time.Time +- wd *progress.WorkDone +- ) +- defer func() { +- reportMu.Lock() +- defer reportMu.Unlock() - -- containsIdent := false -- for _, expr := range stmt.Lhs { -- if expr == ident { -- containsIdent = true -- } +- if wd != nil { +- wd.End(ctx, "Done.") // ensure that the progress report exits - } -- if !containsIdent { -- continue +- }() +- maybeReport = func(completed int64) { +- now := time.Now() +- if now.Sub(start) < reportAfter { +- return - } - -- fixes := removeVariableFromAssignment(pass, path, stmt, ident) -- // fixes may be nil -- if len(fixes) > 0 { -- diag.SuggestedFixes = fixes -- pass.Report(diag) +- reportMu.Lock() +- defer reportMu.Unlock() +- +- if wd == nil { +- wd = reporter.Start(ctx, AnalysisProgressTitle, "", nil, cancel) +- } +- +- if now.Sub(lastReport) > reportEvery { +- lastReport = now +- // Trailing space is intentional: some LSP clients strip newlines. +- msg := fmt.Sprintf(`Indexed %d/%d packages. (Set "analysisProgressReporting" to false to disable notifications.)`, +- completed, len(nodes)) +- pct := 100 * float64(completed) / float64(len(nodes)) +- wd.Report(ctx, msg, pct) - } - } - } - -- return nil --} -- --func removeVariableFromSpec(pass *analysis.Pass, path []ast.Node, stmt *ast.ValueSpec, decl *ast.GenDecl, ident *ast.Ident) []analysis.SuggestedFix { -- newDecl := new(ast.GenDecl) -- *newDecl = *decl -- newDecl.Specs = nil +- // Execute phase: run leaves first, adding +- // new nodes to the queue as they become leaves. +- var g errgroup.Group - -- for _, spec := range decl.Specs { -- if spec != stmt { -- newDecl.Specs = append(newDecl.Specs, spec) -- continue -- } +- // Analysis is CPU-bound. +- // +- // Note: avoid g.SetLimit here: it makes g.Go stop accepting work, which +- // prevents workers from enqeuing, and thus finishing, and thus allowing the +- // group to make progress: deadlock. +- limiter := make(chan unit, runtime.GOMAXPROCS(0)) +- var completed int64 - -- newSpec := new(ast.ValueSpec) -- *newSpec = *stmt -- newSpec.Names = nil +- var enqueue func(*analysisNode) +- enqueue = func(an *analysisNode) { +- g.Go(func() error { +- limiter <- unit{} +- defer func() { <-limiter }() - -- for _, n := range stmt.Names { -- if n != ident { -- newSpec.Names = append(newSpec.Names, n) +- summary, err := an.runCached(ctx) +- if err != nil { +- return err // cancelled, or failed to produce a package - } -- } -- -- if len(newSpec.Names) > 0 { -- newDecl.Specs = append(newDecl.Specs, newSpec) -- } -- } -- -- // decl.End() does not include any comments, so if a comment is present we -- // need to account for it when we delete the statement -- end := decl.End() -- if stmt.Comment != nil && stmt.Comment.End() > end { -- end = stmt.Comment.End() -- } +- maybeReport(atomic.AddInt64(&completed, 1)) +- an.summary = summary - -- // There are no other specs left in the declaration, the whole statement can -- // be deleted -- if len(newDecl.Specs) == 0 { -- // Find parent DeclStmt and delete it -- for _, node := range path { -- if declStmt, ok := node.(*ast.DeclStmt); ok { -- return []analysis.SuggestedFix{ -- { -- Message: suggestedFixMessage(ident.Name), -- TextEdits: deleteStmtFromBlock(path, declStmt), -- }, +- // Notify each waiting predecessor, +- // and enqueue it when it becomes a leaf. +- for _, pred := range an.preds { +- if atomic.AddInt32(&pred.unfinishedSuccs, -1) == 0 { +- enqueue(pred) - } - } -- } -- } - -- var b bytes.Buffer -- if err := format.Node(&b, pass.Fset, newDecl); err != nil { -- return nil +- // Notify each successor that we no longer need +- // its action summaries, which hold Result values. +- // After the last one, delete it, so that we +- // free up large results such as SSA. +- for _, succ := range an.succs { +- succ.decrefPreds() +- } +- return nil +- }) - } -- -- return []analysis.SuggestedFix{ -- { -- Message: suggestedFixMessage(ident.Name), -- TextEdits: []analysis.TextEdit{ -- { -- Pos: decl.Pos(), -- // Avoid adding a new empty line -- End: end + 1, -- NewText: b.Bytes(), -- }, -- }, -- }, +- for _, leaf := range leaves { +- enqueue(leaf) +- } +- if err := g.Wait(); err != nil { +- return nil, err // cancelled, or failed to produce a package - } --} - --func removeVariableFromAssignment(pass *analysis.Pass, path []ast.Node, stmt *ast.AssignStmt, ident *ast.Ident) []analysis.SuggestedFix { -- // The only variable in the assignment is unused -- if len(stmt.Lhs) == 1 { -- // If LHS has only one expression to be valid it has to have 1 expression -- // on RHS -- // -- // RHS may have side effects, preserve RHS -- if exprMayHaveSideEffects(stmt.Rhs[0]) { -- // Delete until RHS -- return []analysis.SuggestedFix{ -- { -- Message: suggestedFixMessage(ident.Name), -- TextEdits: []analysis.TextEdit{ -- { -- Pos: ident.Pos(), -- End: stmt.Rhs[0].Pos(), -- }, -- }, -- }, +- // Report diagnostics only from enabled actions that succeeded. +- // Errors from creating or analyzing packages are ignored. +- // Diagnostics are reported in the order of the analyzers argument. +- // +- // TODO(adonovan): ignoring action errors gives the caller no way +- // to distinguish "there are no problems in this code" from +- // "the code (or analyzers!) are so broken that we couldn't even +- // begin the analysis you asked for". +- // Even if current callers choose to discard the +- // results, we should propagate the per-action errors. +- var results []*Diagnostic +- for _, root := range roots { +- for _, a := range enabled { +- // Skip analyzers that were added only to +- // fulfil requirements of the original set. +- srcAnalyzer, ok := toSrc[a] +- if !ok { +- // Although this 'skip' operation is logically sound, +- // it is nonetheless surprising that its absence should +- // cause #60909 since none of the analyzers currently added for +- // requirements (e.g. ctrlflow, inspect, buildssa) +- // is capable of reporting diagnostics. +- if summary := root.summary.Actions[stableNames[a]]; summary != nil { +- if n := len(summary.Diagnostics); n > 0 { +- bug.Reportf("Internal error: got %d unexpected diagnostics from analyzer %s. This analyzer was added only to fulfil the requirements of the requested set of analyzers, and it is not expected that such analyzers report diagnostics. Please report this in issue #60909.", n, a) +- } +- } +- continue - } -- } - -- // RHS does not have any side effects, delete the whole statement -- return []analysis.SuggestedFix{ -- { -- Message: suggestedFixMessage(ident.Name), -- TextEdits: deleteStmtFromBlock(path, stmt), -- }, +- // Inv: root.summary is the successful result of run (via runCached). +- summary, ok := root.summary.Actions[stableNames[a]] +- if summary == nil { +- panic(fmt.Sprintf("analyzeSummary.Actions[%q] = (nil, %t); got %v (#60551)", +- stableNames[a], ok, root.summary.Actions)) +- } +- if summary.Err != "" { +- continue // action failed +- } +- for _, gobDiag := range summary.Diagnostics { +- results = append(results, toSourceDiagnostic(srcAnalyzer, &gobDiag)) +- } - } - } +- return results, nil +-} - -- // Otherwise replace ident with `_` -- return []analysis.SuggestedFix{ -- { -- Message: suggestedFixMessage(ident.Name), -- TextEdits: []analysis.TextEdit{ -- { -- Pos: ident.Pos(), -- End: ident.End(), -- NewText: []byte("_"), -- }, -- }, -- }, +-func (an *analysisNode) decrefPreds() { +- if atomic.AddInt32(&an.unfinishedPreds, -1) == 0 { +- an.summary.Actions = nil - } -} - --func suggestedFixMessage(name string) string { -- return fmt.Sprintf("Remove variable %s", name) +-// An analysisNode is a node in a doubly-linked DAG isomorphic to the +-// import graph. Each node represents a single package, and the DAG +-// represents a batch of analysis work done at once using a single +-// realm of token.Pos or types.Object values. +-// +-// A complete DAG is created anew for each batch of analysis; +-// subgraphs are not reused over time. Each node's *types.Package +-// field is initially nil and is populated on demand, either from +-// type-checking syntax trees (typeCheck) or from importing export +-// data (_import). When this occurs, the typesOnce event becomes +-// "done". +-// +-// Each node's allDeps map is a "view" of all its dependencies keyed by +-// package path, which defines the types.Importer mapping used when +-// populating the node's types.Package. Different nodes have different +-// views (e.g. due to variants), but two nodes that are related by +-// graph ordering have views that are consistent in their overlap. +-// exportDeps is the subset actually referenced by export data; +-// this is the set for which we attempt to decode facts. +-// +-// Each node's run method is called in parallel postorder. On success, +-// its summary field is populated, either from the cache (hit), or by +-// type-checking and analyzing syntax (miss). +-type analysisNode struct { +- fset *token.FileSet // file set shared by entire batch (DAG) +- mp *metadata.Package // metadata for this package +- files []file.Handle // contents of CompiledGoFiles +- analyzers []*analysis.Analyzer // set of analyzers to run +- preds []*analysisNode // graph edges: +- succs map[PackageID]*analysisNode // (preds -> self -> succs) +- unfinishedSuccs int32 +- unfinishedPreds int32 // effectively a summary.Actions refcount +- allDeps map[PackagePath]*analysisNode // all dependencies including self +- exportDeps map[PackagePath]*analysisNode // subset of allDeps ref'd by export data (+self) +- summary *analyzeSummary // serializable result of analyzing this package +- stableNames map[*analysis.Analyzer]string // cross-process stable names for Analyzers +- +- typesOnce sync.Once // guards lazy population of types and typesErr fields +- types *types.Package // type information lazily imported from summary +- typesErr error // an error producing type information -} - --func deleteStmtFromBlock(path []ast.Node, stmt ast.Stmt) []analysis.TextEdit { -- // Find innermost enclosing BlockStmt. -- var block *ast.BlockStmt -- for i := range path { -- if blockStmt, ok := path[i].(*ast.BlockStmt); ok { -- block = blockStmt -- break -- } -- } +-func (an *analysisNode) String() string { return string(an.mp.ID) } - -- nodeIndex := -1 -- for i, blockStmt := range block.List { -- if blockStmt == stmt { -- nodeIndex = i -- break +-// _import imports this node's types.Package from export data, if not already done. +-// Precondition: analysis was a success. +-// Postcondition: an.types and an.exportDeps are populated. +-func (an *analysisNode) _import() (*types.Package, error) { +- an.typesOnce.Do(func() { +- if an.mp.PkgPath == "unsafe" { +- an.types = types.Unsafe +- return - } -- } -- -- // The statement we need to delete was not found in BlockStmt -- if nodeIndex == -1 { -- return nil -- } -- -- // Delete until the end of the block unless there is another statement after -- // the one we are trying to delete -- end := block.Rbrace -- if nodeIndex < len(block.List)-1 { -- end = block.List[nodeIndex+1].Pos() -- } - -- return []analysis.TextEdit{ -- { -- Pos: stmt.Pos(), -- End: end, -- }, -- } --} +- an.types = types.NewPackage(string(an.mp.PkgPath), string(an.mp.Name)) - --// exprMayHaveSideEffects reports whether the expression may have side effects --// (because it contains a function call or channel receive). We disregard --// runtime panics as well written programs should not encounter them. --func exprMayHaveSideEffects(expr ast.Expr) bool { -- var mayHaveSideEffects bool -- ast.Inspect(expr, func(n ast.Node) bool { -- switch n := n.(type) { -- case *ast.CallExpr: // possible function call -- mayHaveSideEffects = true -- return false -- case *ast.UnaryExpr: -- if n.Op == token.ARROW { // channel receive -- mayHaveSideEffects = true -- return false +- // getPackages recursively imports each dependency +- // referenced by the export data, in parallel. +- getPackages := func(items []gcimporter.GetPackagesItem) error { +- var g errgroup.Group +- for i, item := range items { +- path := PackagePath(item.Path) +- dep, ok := an.allDeps[path] +- if !ok { +- // This early return bypasses Wait; that's ok. +- return fmt.Errorf("%s: unknown dependency %q", an.mp, path) +- } +- an.exportDeps[path] = dep // record, for later fact decoding +- if dep == an { +- if an.typesErr != nil { +- return an.typesErr +- } else { +- items[i].Pkg = an.types +- } +- } else { +- i := i +- g.Go(func() error { +- depPkg, err := dep._import() +- if err == nil { +- items[i].Pkg = depPkg +- } +- return err +- }) +- } - } -- case *ast.FuncLit: -- return false // evaluating what's inside a FuncLit has no effect +- return g.Wait() +- } +- pkg, err := gcimporter.IImportShallow(an.fset, getPackages, an.summary.Export, string(an.mp.PkgPath), bug.Reportf) +- if err != nil { +- an.typesErr = bug.Errorf("%s: invalid export data: %v", an.mp, err) +- an.types = nil +- } else if pkg != an.types { +- log.Fatalf("%s: inconsistent packages", an.mp) - } -- return true - }) -- -- return mayHaveSideEffects +- return an.types, an.typesErr -} -diff -urN a/gopls/internal/lsp/analysis/unusedvariable/unusedvariable_test.go b/gopls/internal/lsp/analysis/unusedvariable/unusedvariable_test.go ---- a/gopls/internal/lsp/analysis/unusedvariable/unusedvariable_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/unusedvariable/unusedvariable_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,24 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package unusedvariable_test +-// analyzeSummary is a gob-serializable summary of successfully +-// applying a list of analyzers to a package. +-type analyzeSummary struct { +- Export []byte // encoded types of package +- DeepExportHash file.Hash // hash of reflexive transitive closure of export data +- Compiles bool // transitively free of list/parse/type errors +- Actions actionMap // maps analyzer stablename to analysis results (*actionSummary) +-} - --import ( -- "testing" +-// actionMap defines a stable Gob encoding for a map. +-// TODO(adonovan): generalize and move to a library when we can use generics. +-type actionMap map[string]*actionSummary - -- "golang.org/x/tools/go/analysis/analysistest" -- "golang.org/x/tools/gopls/internal/lsp/analysis/unusedvariable" +-var ( +- _ gob.GobEncoder = (actionMap)(nil) +- _ gob.GobDecoder = (*actionMap)(nil) -) - --func Test(t *testing.T) { -- testdata := analysistest.TestData() -- -- t.Run("decl", func(t *testing.T) { -- analysistest.RunWithSuggestedFixes(t, testdata, unusedvariable.Analyzer, "decl") -- }) +-type actionsMapEntry struct { +- K string +- V *actionSummary +-} - -- t.Run("assign", func(t *testing.T) { -- analysistest.RunWithSuggestedFixes(t, testdata, unusedvariable.Analyzer, "assign") +-func (m actionMap) GobEncode() ([]byte, error) { +- entries := make([]actionsMapEntry, 0, len(m)) +- for k, v := range m { +- entries = append(entries, actionsMapEntry{k, v}) +- } +- sort.Slice(entries, func(i, j int) bool { +- return entries[i].K < entries[j].K - }) +- var buf bytes.Buffer +- err := gob.NewEncoder(&buf).Encode(entries) +- return buf.Bytes(), err -} -diff -urN a/gopls/internal/lsp/analysis/useany/testdata/src/a/a.go b/gopls/internal/lsp/analysis/useany/testdata/src/a/a.go ---- a/gopls/internal/lsp/analysis/useany/testdata/src/a/a.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/useany/testdata/src/a/a.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,25 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --// This file contains tests for the useany checker. +-func (m *actionMap) GobDecode(data []byte) error { +- var entries []actionsMapEntry +- if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&entries); err != nil { +- return err +- } +- *m = make(actionMap, len(entries)) +- for _, e := range entries { +- (*m)[e.K] = e.V +- } +- return nil +-} - --package a +-// actionSummary is a gob-serializable summary of one possibly failed analysis action. +-// If Err is non-empty, the other fields are undefined. +-type actionSummary struct { +- Facts []byte // the encoded facts.Set +- FactsHash file.Hash // hash(Facts) +- Diagnostics []gobDiagnostic +- Err string // "" => success +-} - --type Any interface{} +-// runCached applies a list of analyzers (plus any others +-// transitively required by them) to a package. It succeeds as long +-// as it could produce a types.Package, even if there were direct or +-// indirect list/parse/type errors, and even if all the analysis +-// actions failed. It usually fails only if the package was unknown, +-// a file was missing, or the operation was cancelled. +-// +-// Postcondition: runCached must not continue to use the snapshot +-// (in background goroutines) after it has returned; see memoize.RefCounted. +-func (an *analysisNode) runCached(ctx context.Context) (*analyzeSummary, error) { +- // At this point we have the action results (serialized +- // packages and facts) of our immediate dependencies, +- // and the metadata and content of this package. +- // +- // We now compute a hash for all our inputs, and consult a +- // global cache of promised results. If nothing material +- // has changed, we'll make a hit in the shared cache. +- // +- // The hash of our inputs is based on the serialized export +- // data and facts so that immaterial changes can be pruned +- // without decoding. +- key := an.cacheKey() - --func _[T interface{}]() {} // want "could use \"any\" for this empty interface" --func _[X any, T interface{}]() {} // want "could use \"any\" for this empty interface" --func _[any interface{}]() {} // want "could use \"any\" for this empty interface" --func _[T Any]() {} // want "could use \"any\" for this empty interface" --func _[T interface{ int | interface{} }]() {} // want "could use \"any\" for this empty interface" --func _[T interface{ int | Any }]() {} // want "could use \"any\" for this empty interface" --func _[T any]() {} +- // Access the cache. +- var summary *analyzeSummary +- const cacheKind = "analysis" +- if data, err := filecache.Get(cacheKind, key); err == nil { +- // cache hit +- analyzeSummaryCodec.Decode(data, &summary) +- } else if err != filecache.ErrNotFound { +- return nil, bug.Errorf("internal error reading shared cache: %v", err) +- } else { +- // Cache miss: do the work. +- var err error +- summary, err = an.run(ctx) +- if err != nil { +- return nil, err +- } - --type _[T interface{}] int // want "could use \"any\" for this empty interface" --type _[X any, T interface{}] int // want "could use \"any\" for this empty interface" --type _[any interface{}] int // want "could use \"any\" for this empty interface" --type _[T Any] int // want "could use \"any\" for this empty interface" --type _[T interface{ int | interface{} }] int // want "could use \"any\" for this empty interface" --type _[T interface{ int | Any }] int // want "could use \"any\" for this empty interface" --type _[T any] int -diff -urN a/gopls/internal/lsp/analysis/useany/testdata/src/a/a.go.golden b/gopls/internal/lsp/analysis/useany/testdata/src/a/a.go.golden ---- a/gopls/internal/lsp/analysis/useany/testdata/src/a/a.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/useany/testdata/src/a/a.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,25 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +- atomic.AddInt32(&an.unfinishedPreds, +1) // incref +- go func() { +- defer an.decrefPreds() //decref - --// This file contains tests for the useany checker. +- cacheLimit <- unit{} // acquire token +- defer func() { <-cacheLimit }() // release token - --package a +- data := analyzeSummaryCodec.Encode(summary) +- if false { +- log.Printf("Set key=%d value=%d id=%s\n", len(key), len(data), an.mp.ID) +- } +- if err := filecache.Set(cacheKind, key, data); err != nil { +- event.Error(ctx, "internal error updating analysis shared cache", err) +- } +- }() +- } - --type Any interface{} +- return summary, nil +-} - --func _[T any]() {} // want "could use \"any\" for this empty interface" --func _[X any, T any]() {} // want "could use \"any\" for this empty interface" --func _[any interface{}]() {} // want "could use \"any\" for this empty interface" --func _[T any]() {} // want "could use \"any\" for this empty interface" --func _[T any]() {} // want "could use \"any\" for this empty interface" --func _[T any]() {} // want "could use \"any\" for this empty interface" --func _[T any]() {} +-// cacheLimit reduces parallelism of cache updates. +-// We allow more than typical GOMAXPROCS as it's a mix of CPU and I/O. +-var cacheLimit = make(chan unit, 32) - --type _[T any] int // want "could use \"any\" for this empty interface" --type _[X any, T any] int // want "could use \"any\" for this empty interface" --type _[any interface{}] int // want "could use \"any\" for this empty interface" --type _[T any] int // want "could use \"any\" for this empty interface" --type _[T any] int // want "could use \"any\" for this empty interface" --type _[T any] int // want "could use \"any\" for this empty interface" --type _[T any] int -diff -urN a/gopls/internal/lsp/analysis/useany/useany.go b/gopls/internal/lsp/analysis/useany/useany.go ---- a/gopls/internal/lsp/analysis/useany/useany.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/useany/useany.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,102 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-// analysisCacheKey returns a cache key that is a cryptographic digest +-// of the all the values that might affect type checking and analysis: +-// the analyzer names, package metadata, names and contents of +-// compiled Go files, and vdeps (successor) information +-// (export data and facts). +-func (an *analysisNode) cacheKey() [sha256.Size]byte { +- hasher := sha256.New() - --// Package useany defines an Analyzer that checks for usage of interface{} in --// constraints, rather than the predeclared any. --package useany +- // In principle, a key must be the hash of an +- // unambiguous encoding of all the relevant data. +- // If it's ambiguous, we risk collisions. - --import ( -- "fmt" -- "go/ast" -- "go/token" -- "go/types" +- // analyzers +- fmt.Fprintf(hasher, "analyzers: %d\n", len(an.analyzers)) +- for _, a := range an.analyzers { +- fmt.Fprintln(hasher, a.Name) +- } - -- "golang.org/x/tools/go/analysis" -- "golang.org/x/tools/go/analysis/passes/inspect" -- "golang.org/x/tools/go/ast/inspector" -- "golang.org/x/tools/internal/typeparams" --) +- // package metadata +- mp := an.mp +- fmt.Fprintf(hasher, "package: %s %s %s\n", mp.ID, mp.Name, mp.PkgPath) +- // We can ignore m.DepsBy{Pkg,Import}Path: although the logic +- // uses those fields, we account for them by hashing vdeps. - --const Doc = `check for constraints that could be simplified to "any"` +- // type sizes +- wordSize := an.mp.TypesSizes.Sizeof(types.Typ[types.Int]) +- maxAlign := an.mp.TypesSizes.Alignof(types.NewPointer(types.Typ[types.Int64])) +- fmt.Fprintf(hasher, "sizes: %d %d\n", wordSize, maxAlign) - --var Analyzer = &analysis.Analyzer{ -- Name: "useany", -- Doc: Doc, -- Requires: []*analysis.Analyzer{inspect.Analyzer}, -- Run: run, --} +- // metadata errors: used for 'compiles' field +- fmt.Fprintf(hasher, "errors: %d", len(mp.Errors)) - --func run(pass *analysis.Pass) (interface{}, error) { -- inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) +- // module Go version +- if mp.Module != nil && mp.Module.GoVersion != "" { +- fmt.Fprintf(hasher, "go %s\n", mp.Module.GoVersion) +- } - -- universeAny := types.Universe.Lookup("any") -- if universeAny == nil { -- // Go <= 1.17. Nothing to check. -- return nil, nil +- // file names and contents +- fmt.Fprintf(hasher, "files: %d\n", len(an.files)) +- for _, fh := range an.files { +- fmt.Fprintln(hasher, fh.Identity()) - } - -- nodeFilter := []ast.Node{ -- (*ast.TypeSpec)(nil), -- (*ast.FuncType)(nil), +- // vdeps, in PackageID order +- depIDs := make([]string, 0, len(an.succs)) +- for depID := range an.succs { +- depIDs = append(depIDs, string(depID)) - } +- sort.Strings(depIDs) // TODO(adonovan): avoid conversions by using slices.Sort[PackageID] +- for _, depID := range depIDs { +- vdep := an.succs[PackageID(depID)] +- fmt.Fprintf(hasher, "dep: %s\n", vdep.mp.PkgPath) +- fmt.Fprintf(hasher, "export: %s\n", vdep.summary.DeepExportHash) - -- inspect.Preorder(nodeFilter, func(node ast.Node) { -- var tparams *ast.FieldList -- switch node := node.(type) { -- case *ast.TypeSpec: -- tparams = typeparams.ForTypeSpec(node) -- case *ast.FuncType: -- tparams = typeparams.ForFuncType(node) -- default: -- panic(fmt.Sprintf("unexpected node type %T", node)) -- } -- if tparams.NumFields() == 0 { -- return +- // action results: errors and facts +- actions := vdep.summary.Actions +- names := make([]string, 0, len(actions)) +- for name := range actions { +- names = append(names, name) - } -- -- for _, field := range tparams.List { -- typ := pass.TypesInfo.Types[field.Type].Type -- if typ == nil { -- continue // something is wrong, but not our concern -- } -- iface, ok := typ.Underlying().(*types.Interface) -- if !ok { -- continue // invalid constraint -- } -- -- // If the constraint is the empty interface, offer a fix to use 'any' -- // instead. -- if iface.Empty() { -- id, _ := field.Type.(*ast.Ident) -- if id != nil && pass.TypesInfo.Uses[id] == universeAny { -- continue -- } -- -- diag := analysis.Diagnostic{ -- Pos: field.Type.Pos(), -- End: field.Type.End(), -- Message: `could use "any" for this empty interface`, -- } -- -- // Only suggest a fix to 'any' if we actually resolve the predeclared -- // any in this scope. -- if scope := pass.TypesInfo.Scopes[node]; scope != nil { -- if _, any := scope.LookupParent("any", token.NoPos); any == universeAny { -- diag.SuggestedFixes = []analysis.SuggestedFix{{ -- Message: `use "any"`, -- TextEdits: []analysis.TextEdit{{ -- Pos: field.Type.Pos(), -- End: field.Type.End(), -- NewText: []byte("any"), -- }}, -- }} -- } -- } -- -- pass.Report(diag) +- sort.Strings(names) +- for _, name := range names { +- summary := actions[name] +- fmt.Fprintf(hasher, "action %s\n", name) +- if summary.Err != "" { +- fmt.Fprintf(hasher, "error %s\n", summary.Err) +- } else { +- fmt.Fprintf(hasher, "facts %s\n", summary.FactsHash) +- // We can safely omit summary.diagnostics +- // from the key since they have no downstream effect. - } - } -- }) -- return nil, nil --} -diff -urN a/gopls/internal/lsp/analysis/useany/useany_test.go b/gopls/internal/lsp/analysis/useany/useany_test.go ---- a/gopls/internal/lsp/analysis/useany/useany_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/analysis/useany/useany_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,21 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package useany_test -- --import ( -- "testing" -- -- "golang.org/x/tools/go/analysis/analysistest" -- "golang.org/x/tools/gopls/internal/lsp/analysis/useany" -- "golang.org/x/tools/internal/typeparams" --) -- --func Test(t *testing.T) { -- if !typeparams.Enabled { -- t.Skip("type params are not enabled") - } -- testdata := analysistest.TestData() -- analysistest.RunWithSuggestedFixes(t, testdata, useany.Analyzer, "a") +- +- var hash [sha256.Size]byte +- hasher.Sum(hash[:0]) +- return hash -} -diff -urN a/gopls/internal/lsp/browser/browser.go b/gopls/internal/lsp/browser/browser.go ---- a/gopls/internal/lsp/browser/browser.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/browser/browser.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,67 +0,0 @@ --// Copyright 2016 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --// Package browser provides utilities for interacting with users' browsers. --package browser +-// run implements the cache-miss case. +-// This function does not access the snapshot. +-// +-// Postcondition: on success, the analyzeSummary.Actions +-// key set is {a.Name for a in analyzers}. +-func (an *analysisNode) run(ctx context.Context) (*analyzeSummary, error) { +- // Parse only the "compiled" Go files. +- // Do the computation in parallel. +- parsed := make([]*parsego.File, len(an.files)) +- { +- var group errgroup.Group +- group.SetLimit(4) // not too much: run itself is already called in parallel +- for i, fh := range an.files { +- i, fh := i, fh +- group.Go(func() error { +- // Call parseGoImpl directly, not the caching wrapper, +- // as cached ASTs require the global FileSet. +- // ast.Object resolution is unfortunately an implied part of the +- // go/analysis contract. +- pgf, err := parseGoImpl(ctx, an.fset, fh, parsego.Full&^parser.SkipObjectResolution, false) +- parsed[i] = pgf +- return err +- }) +- } +- if err := group.Wait(); err != nil { +- return nil, err // cancelled, or catastrophic error (e.g. missing file) +- } +- } - --import ( -- exec "golang.org/x/sys/execabs" -- "os" -- "runtime" -- "time" --) +- // Type-check the package syntax. +- pkg := an.typeCheck(parsed) - --// Commands returns a list of possible commands to use to open a url. --func Commands() [][]string { -- var cmds [][]string -- if exe := os.Getenv("BROWSER"); exe != "" { -- cmds = append(cmds, []string{exe}) -- } -- switch runtime.GOOS { -- case "darwin": -- cmds = append(cmds, []string{"/usr/bin/open"}) -- case "windows": -- cmds = append(cmds, []string{"cmd", "/c", "start"}) -- default: -- if os.Getenv("DISPLAY") != "" { -- // xdg-open is only for use in a desktop environment. -- cmds = append(cmds, []string{"xdg-open"}) -- } +- // Publish the completed package. +- an.typesOnce.Do(func() { an.types = pkg.types }) +- if an.types != pkg.types { +- log.Fatalf("typesOnce prematurely done") - } -- cmds = append(cmds, -- []string{"chrome"}, -- []string{"google-chrome"}, -- []string{"chromium"}, -- []string{"firefox"}, -- ) -- return cmds --} - --// Open tries to open url in a browser and reports whether it succeeded. --func Open(url string) bool { -- for _, args := range Commands() { -- cmd := exec.Command(args[0], append(args[1:], url)...) -- if cmd.Start() == nil && appearsSuccessful(cmd, 3*time.Second) { -- return true +- // Compute the union of exportDeps across our direct imports. +- // This is the set that will be needed by the fact decoder. +- allExportDeps := make(map[PackagePath]*analysisNode) +- for _, succ := range an.succs { +- for k, v := range succ.exportDeps { +- allExportDeps[k] = v - } - } -- return false --} - --// appearsSuccessful reports whether the command appears to have run successfully. --// If the command runs longer than the timeout, it's deemed successful. --// If the command runs within the timeout, it's deemed successful if it exited cleanly. --func appearsSuccessful(cmd *exec.Cmd, timeout time.Duration) bool { -- errc := make(chan error, 1) -- go func() { -- errc <- cmd.Wait() -- }() +- // The fact decoder needs a means to look up a Package by path. +- pkg.factsDecoder = facts.NewDecoderFunc(pkg.types, func(path string) *types.Package { +- // Note: Decode is called concurrently, and thus so is this function. - -- select { -- case <-time.After(timeout): -- return true -- case err := <-errc: -- return err == nil -- } --} -diff -urN a/gopls/internal/lsp/browser/README.md b/gopls/internal/lsp/browser/README.md ---- a/gopls/internal/lsp/browser/README.md 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/browser/README.md 1970-01-01 00:00:00.000000000 +0000 -@@ -1 +0,0 @@ --This package is a copy of cmd/internal/browser from the go distribution -\ No newline at end of file -diff -urN a/gopls/internal/lsp/cache/analysis.go b/gopls/internal/lsp/cache/analysis.go ---- a/gopls/internal/lsp/cache/analysis.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/analysis.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,1553 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +- // Does the fact relate to a package referenced by export data? +- if dep, ok := allExportDeps[PackagePath(path)]; ok { +- dep.typesOnce.Do(func() { log.Fatal("dep.types not populated") }) +- if dep.typesErr == nil { +- return dep.types +- } +- return nil +- } - --package cache +- // If the fact relates to a dependency not referenced +- // by export data, it is safe to ignore it. +- // (In that case dep.types exists but may be unpopulated +- // or in the process of being populated from export data.) +- if an.allDeps[PackagePath(path)] == nil { +- log.Fatalf("fact package %q is not a dependency", path) +- } +- return nil +- }) - --// This file defines gopls' driver for modular static analysis (go/analysis). +- // Poll cancellation state. +- if err := ctx.Err(); err != nil { +- return nil, err +- } - --import ( -- "bytes" -- "context" -- "crypto/sha256" -- "encoding/gob" -- "encoding/json" -- "errors" -- "fmt" -- "go/ast" -- "go/token" -- "go/types" -- "log" -- urlpkg "net/url" -- "path/filepath" -- "reflect" -- "runtime" -- "runtime/debug" -- "sort" -- "strings" -- "sync" -- "sync/atomic" -- "time" +- // -- analysis -- - -- "golang.org/x/sync/errgroup" -- "golang.org/x/tools/go/analysis" -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/filecache" -- "golang.org/x/tools/gopls/internal/lsp/frob" -- "golang.org/x/tools/gopls/internal/lsp/progress" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/tag" -- "golang.org/x/tools/internal/facts" -- "golang.org/x/tools/internal/gcimporter" -- "golang.org/x/tools/internal/typeparams" -- "golang.org/x/tools/internal/typesinternal" --) +- // Build action graph for this package. +- // Each graph node (action) is one unit of analysis. +- actions := make(map[*analysis.Analyzer]*action) +- var mkAction func(a *analysis.Analyzer) *action +- mkAction = func(a *analysis.Analyzer) *action { +- act, ok := actions[a] +- if !ok { +- var hdeps []*action +- for _, req := range a.Requires { +- hdeps = append(hdeps, mkAction(req)) +- } +- act = &action{ +- a: a, +- stableName: an.stableNames[a], +- pkg: pkg, +- vdeps: an.succs, +- hdeps: hdeps, +- } +- actions[a] = act +- } +- return act +- } - --/* +- // Build actions for initial package. +- var roots []*action +- for _, a := range an.analyzers { +- roots = append(roots, mkAction(a)) +- } - -- DESIGN +- // Execute the graph in parallel. +- execActions(roots) +- // Inv: each root's summary is set (whether success or error). - -- An analysis request (Snapshot.Analyze) is for a set of Analyzers and -- PackageIDs. The result is the set of diagnostics for those -- packages. Each request constructs a transitively closed DAG of -- nodes, each representing a package, then works bottom up in -- parallel postorder calling runCached to ensure that each node's -- analysis summary is up to date. The summary contains the analysis -- diagnostics as well as the intermediate results required by the -- recursion, such as serialized types and facts. +- // Don't return (or cache) the result in case of cancellation. +- if err := ctx.Err(); err != nil { +- return nil, err // cancelled +- } - -- The entire DAG is ephemeral. Each node in the DAG records the set -- of analyzers to run: the complete set for the root packages, and -- the "facty" subset for dependencies. Each package is thus analyzed -- at most once. The entire DAG shares a single FileSet for parsing -- and importing. +- // Return summaries only for the requested actions. +- summaries := make(map[string]*actionSummary) +- for _, root := range roots { +- if root.summary == nil { +- panic("root has nil action.summary (#60551)") +- } +- summaries[root.stableName] = root.summary +- } - -- Each node is processed by runCached. It gets the source file -- content hashes for package p, and the summaries of its "vertical" -- dependencies (direct imports), and from them it computes a key -- representing the unit of work (parsing, type-checking, and -- analysis) that it has to do. The key is a cryptographic hash of the -- "recipe" for this step, including the Metadata, the file contents, -- the set of analyzers, and the type and fact information from the -- vertical dependencies. +- return &analyzeSummary{ +- Export: pkg.export, +- DeepExportHash: pkg.deepExportHash, +- Compiles: pkg.compiles, +- Actions: summaries, +- }, nil +-} - -- The key is sought in a machine-global persistent file-system based -- cache. If this gopls process, or another gopls process on the same -- machine, has already performed this analysis step, runCached will -- make a cache hit and load the serialized summary of the results. If -- not, it will have to proceed to run() to parse and type-check the -- package and then apply a set of analyzers to it. (The set of -- analyzers applied to a single package itself forms a graph of -- "actions", and it too is evaluated in parallel postorder; these -- dependency edges within the same package are called "horizontal".) -- Finally it writes a new cache entry. The entry contains serialized -- types (export data) and analysis facts. +-// Postcondition: analysisPackage.types and an.exportDeps are populated. +-func (an *analysisNode) typeCheck(parsed []*parsego.File) *analysisPackage { +- mp := an.mp - -- Each node in the DAG acts like a go/types importer mapping, -- providing a consistent view of packages and their objects: the -- mapping for a node is a superset of its dependencies' mappings. -- Every node has an associated *types.Package, initially nil. A -- package is populated during run (cache miss) by type-checking its -- syntax; but for a cache hit, the package is populated lazily, i.e. -- not until it later becomes necessary because it is imported -- directly or referenced by export data higher up in the DAG. +- if false { // debugging +- log.Println("typeCheck", mp.ID) +- } - -- For types, we use "shallow" export data. Historically, the Go -- compiler always produced a summary of the types for a given package -- that included types from other packages that it indirectly -- referenced: "deep" export data. This had the advantage that the -- compiler (and analogous tools such as gopls) need only load one -- file per direct import. However, it meant that the files tended to -- get larger based on the level of the package in the import -- graph. For example, higher-level packages in the kubernetes module -- have over 1MB of "deep" export data, even when they have almost no -- content of their own, merely because they mention a major type that -- references many others. In pathological cases the export data was -- 300x larger than the source for a package due to this quadratic -- growth. +- pkg := &analysisPackage{ +- mp: mp, +- fset: an.fset, +- parsed: parsed, +- files: make([]*ast.File, len(parsed)), +- compiles: len(mp.Errors) == 0, // false => list error +- types: types.NewPackage(string(mp.PkgPath), string(mp.Name)), +- typesInfo: &types.Info{ +- Types: make(map[ast.Expr]types.TypeAndValue), +- Defs: make(map[*ast.Ident]types.Object), +- Instances: make(map[*ast.Ident]types.Instance), +- Implicits: make(map[ast.Node]types.Object), +- Selections: make(map[*ast.SelectorExpr]*types.Selection), +- Scopes: make(map[ast.Node]*types.Scope), +- Uses: make(map[*ast.Ident]types.Object), +- }, +- typesSizes: mp.TypesSizes, +- } +- versions.InitFileVersions(pkg.typesInfo) - -- "Shallow" export data means that the serialized types describe only -- a single package. If those types mention types from other packages, -- the type checker may need to request additional packages beyond -- just the direct imports. Type information for the entire transitive -- closure of imports is provided (lazily) by the DAG. +- // Unsafe has no syntax. +- if mp.PkgPath == "unsafe" { +- pkg.types = types.Unsafe +- return pkg +- } - -- For correct dependency analysis, the digest used as a cache key -- must reflect the "deep" export data, so it is derived recursively -- from the transitive closure. As an optimization, we needn't include -- every package of the transitive closure in the deep hash, only the -- packages that were actually requested by the type checker. This -- allows changes to a package that have no effect on its export data -- to be "pruned". The direct consumer will need to be re-executed, -- but if its export data is unchanged as a result, then indirect -- consumers may not need to be re-executed. This allows, for example, -- one to insert a print statement in a function and not "rebuild" the -- whole application (though export data does record line numbers and -- offsets of types which may be perturbed by otherwise insignificant -- changes.) +- for i, p := range parsed { +- pkg.files[i] = p.File +- if p.ParseErr != nil { +- pkg.compiles = false // parse error +- } +- } - -- The summary must record whether a package is transitively -- error-free (whether it would compile) because many analyzers are -- not safe to run on packages with inconsistent types. +- for _, vdep := range an.succs { +- if !vdep.summary.Compiles { +- pkg.compiles = false // transitive error +- } +- } - -- For fact encoding, we use the same fact set as the unitchecker -- (vet) to record and serialize analysis facts. The fact -- serialization mechanism is analogous to "deep" export data. +- cfg := &types.Config{ +- Sizes: mp.TypesSizes, +- Error: func(e error) { +- pkg.compiles = false // type error - --*/ +- // Suppress type errors in files with parse errors +- // as parser recovery can be quite lossy (#59888). +- typeError := e.(types.Error) +- for _, p := range parsed { +- if p.ParseErr != nil && astutil.NodeContains(p.File, typeError.Pos) { +- return +- } +- } +- pkg.typeErrors = append(pkg.typeErrors, typeError) +- }, +- Importer: importerFunc(func(importPath string) (*types.Package, error) { +- // Beware that returning an error from this function +- // will cause the type checker to synthesize a fake +- // package whose Path is importPath, potentially +- // losing a vendor/ prefix. If type-checking errors +- // are swallowed, these packages may be confusing. - --// TODO(adonovan): --// - Add a (white-box) test of pruning when a change doesn't affect export data. --// - Optimise pruning based on subset of packages mentioned in exportdata. --// - Better logging so that it is possible to deduce why an analyzer --// is not being run--often due to very indirect failures. --// Even if the ultimate consumer decides to ignore errors, --// tests and other situations want to be assured of freedom from --// errors, not just missing results. This should be recorded. --// - Split this into a subpackage, gopls/internal/lsp/cache/driver, --// consisting of this file and three helpers from errors.go. --// The (*snapshot).Analyze method would stay behind and make calls --// to the driver package. --// Steps: --// - define a narrow driver.Snapshot interface with only these methods: --// Metadata(PackageID) source.Metadata --// ReadFile(Context, URI) (source.FileHandle, error) --// View() *View // for Options --// - share cache.{goVersionRx,parseGoImpl} +- // Map ImportPath to ID. +- id, ok := mp.DepsByImpPath[ImportPath(importPath)] +- if !ok { +- // The import syntax is inconsistent with the metadata. +- // This could be because the import declaration was +- // incomplete and the metadata only includes complete +- // imports; or because the metadata ignores import +- // edges that would lead to cycles in the graph. +- return nil, fmt.Errorf("missing metadata for import of %q", importPath) +- } - --// AnalysisProgressTitle is the title of the progress report for ongoing --// analysis. It is sought by regression tests for the progress reporting --// feature. --const AnalysisProgressTitle = "Analyzing Dependencies" +- // Map ID to node. (id may be "") +- dep := an.succs[id] +- if dep == nil { +- // Analogous to (*snapshot).missingPkgError +- // in the logic for regular type-checking, +- // but without a snapshot we can't provide +- // such detail, and anyway most analysis +- // failures aren't surfaced in the UI. +- return nil, fmt.Errorf("no required module provides analysis package %q (id=%q)", importPath, id) +- } - --// Analyze applies a set of analyzers to the package denoted by id, --// and returns their diagnostics for that package. --// --// The analyzers list must be duplicate free; order does not matter. --// --// Notifications of progress may be sent to the optional reporter. --func (snapshot *snapshot) Analyze(ctx context.Context, pkgs map[PackageID]unit, analyzers []*source.Analyzer, reporter *progress.Tracker) ([]*source.Diagnostic, error) { -- start := time.Now() // for progress reporting +- // (Duplicates logic from check.go.) +- if !metadata.IsValidImport(an.mp.PkgPath, dep.mp.PkgPath) { +- return nil, fmt.Errorf("invalid use of internal package %s", importPath) +- } - -- var tagStr string // sorted comma-separated list of PackageIDs -- { -- // TODO(adonovan): replace with a generic map[S]any -> string -- // function in the tag package, and use maps.Keys + slices.Sort. -- keys := make([]string, 0, len(pkgs)) -- for id := range pkgs { -- keys = append(keys, string(id)) -- } -- sort.Strings(keys) -- tagStr = strings.Join(keys, ",") +- return dep._import() +- }), - } -- ctx, done := event.Start(ctx, "snapshot.Analyze", tag.Package.Of(tagStr)) -- defer done() - -- // Filter and sort enabled root analyzers. -- // A disabled analyzer may still be run if required by another. -- toSrc := make(map[*analysis.Analyzer]*source.Analyzer) -- var enabled []*analysis.Analyzer // enabled subset + transitive requirements -- for _, a := range analyzers { -- if a.IsEnabled(snapshot.Options()) { -- toSrc[a.Analyzer] = a -- enabled = append(enabled, a.Analyzer) +- // Set Go dialect. +- if mp.Module != nil && mp.Module.GoVersion != "" { +- goVersion := "go" + mp.Module.GoVersion +- if validGoVersion(goVersion) { +- cfg.GoVersion = goVersion - } - } -- sort.Slice(enabled, func(i, j int) bool { -- return enabled[i].Name < enabled[j].Name -- }) -- analyzers = nil // prevent accidental use -- -- enabled = requiredAnalyzers(enabled) - -- // Perform basic sanity checks. -- // (Ideally we would do this only once.) -- if err := analysis.Validate(enabled); err != nil { -- return nil, fmt.Errorf("invalid analyzer configuration: %v", err) -- } +- // We want to type check cgo code if go/types supports it. +- // We passed typecheckCgo to go/packages when we Loaded. +- // TODO(adonovan): do we actually need this?? +- typesinternal.SetUsesCgo(cfg) - -- stableNames := make(map[*analysis.Analyzer]string) +- check := types.NewChecker(cfg, pkg.fset, pkg.types, pkg.typesInfo) - -- var facty []*analysis.Analyzer // facty subset of enabled + transitive requirements -- for _, a := range enabled { -- // TODO(adonovan): reject duplicate stable names (very unlikely). -- stableNames[a] = stableName(a) +- // Type checking errors are handled via the config, so ignore them here. +- _ = check.Files(pkg.files) - -- // Register fact types of all required analyzers. -- if len(a.FactTypes) > 0 { -- facty = append(facty, a) -- for _, f := range a.FactTypes { -- gob.Register(f) // <2us -- } +- // debugging (type errors are quite normal) +- if false { +- if pkg.typeErrors != nil { +- log.Printf("package %s has type errors: %v", pkg.types.Path(), pkg.typeErrors) - } - } -- facty = requiredAnalyzers(facty) - -- // File set for this batch (entire graph) of analysis. -- fset := token.NewFileSet() +- // Emit the export data and compute the recursive hash. +- export, err := gcimporter.IExportShallow(pkg.fset, pkg.types, bug.Reportf) +- if err != nil { +- // TODO(adonovan): in light of exporter bugs such as #57729, +- // consider using bug.Report here and retrying the IExportShallow +- // call here using an empty types.Package. +- log.Fatalf("internal error writing shallow export data: %v", err) +- } +- pkg.export = export - -- // Starting from the root packages and following DepsByPkgPath, -- // build the DAG of packages we're going to analyze. -- // -- // Root nodes will run the enabled set of analyzers, -- // whereas dependencies will run only the facty set. -- // Because (by construction) enabled is a superset of facty, -- // we can analyze each node with exactly one set of analyzers. -- nodes := make(map[PackageID]*analysisNode) -- var leaves []*analysisNode // nodes with no unfinished successors -- var makeNode func(from *analysisNode, id PackageID) (*analysisNode, error) -- makeNode = func(from *analysisNode, id PackageID) (*analysisNode, error) { -- an, ok := nodes[id] +- // Compute a recursive hash to account for the export data of +- // this package and each dependency referenced by it. +- // Also, populate exportDeps. +- hash := sha256.New() +- fmt.Fprintf(hash, "%s %d\n", mp.PkgPath, len(export)) +- hash.Write(export) +- paths, err := readShallowManifest(export) +- if err != nil { +- log.Fatalf("internal error: bad export data: %v", err) +- } +- for _, path := range paths { +- dep, ok := an.allDeps[path] - if !ok { -- m := snapshot.Metadata(id) -- if m == nil { -- return nil, bug.Errorf("no metadata for %s", id) -- } +- log.Fatalf("%s: missing dependency: %q", an, path) +- } +- fmt.Fprintf(hash, "%s %s\n", dep.mp.PkgPath, dep.summary.DeepExportHash) +- an.exportDeps[path] = dep +- } +- an.exportDeps[mp.PkgPath] = an // self +- hash.Sum(pkg.deepExportHash[:0]) - -- // -- preorder -- +- return pkg +-} - -- an = &analysisNode{ -- fset: fset, -- m: m, -- analyzers: facty, // all nodes run at least the facty analyzers -- allDeps: make(map[PackagePath]*analysisNode), -- exportDeps: make(map[PackagePath]*analysisNode), -- stableNames: stableNames, +-// readShallowManifest returns the manifest of packages referenced by +-// a shallow export data file for a package (excluding the package itself). +-// TODO(adonovan): add a test. +-func readShallowManifest(export []byte) ([]PackagePath, error) { +- const selfPath = "" // dummy path +- var paths []PackagePath +- getPackages := func(items []gcimporter.GetPackagesItem) error { +- paths = []PackagePath{} // non-nil +- for _, item := range items { +- if item.Path != selfPath { +- paths = append(paths, PackagePath(item.Path)) - } -- nodes[id] = an +- } +- return errors.New("stop") // terminate importer +- } +- _, err := gcimporter.IImportShallow(token.NewFileSet(), getPackages, export, selfPath, bug.Reportf) +- if paths == nil { +- if err != nil { +- return nil, err // failed before getPackages callback +- } +- return nil, bug.Errorf("internal error: IImportShallow did not call getPackages") +- } +- return paths, nil // success +-} - -- // -- recursion -- +-// analysisPackage contains information about a package, including +-// syntax trees, used transiently during its type-checking and analysis. +-type analysisPackage struct { +- mp *metadata.Package +- fset *token.FileSet // local to this package +- parsed []*parsego.File +- files []*ast.File // same as parsed[i].File +- types *types.Package +- compiles bool // package is transitively free of list/parse/type errors +- factsDecoder *facts.Decoder +- export []byte // encoding of types.Package +- deepExportHash file.Hash // reflexive transitive hash of export data +- typesInfo *types.Info +- typeErrors []types.Error +- typesSizes types.Sizes +-} - -- // Build subgraphs for dependencies. -- an.succs = make(map[PackageID]*analysisNode, len(m.DepsByPkgPath)) -- for _, depID := range m.DepsByPkgPath { -- dep, err := makeNode(an, depID) -- if err != nil { -- return nil, err -- } -- an.succs[depID] = dep +-// An action represents one unit of analysis work: the application of +-// one analysis to one package. Actions form a DAG, both within a +-// package (as different analyzers are applied, either in sequence or +-// parallel), and across packages (as dependencies are analyzed). +-type action struct { +- once sync.Once +- a *analysis.Analyzer +- stableName string // cross-process stable name of analyzer +- pkg *analysisPackage +- hdeps []*action // horizontal dependencies +- vdeps map[PackageID]*analysisNode // vertical dependencies - -- // Compute the union of all dependencies. -- // (This step has quadratic complexity.) -- for pkgPath, node := range dep.allDeps { -- an.allDeps[pkgPath] = node +- // results of action.exec(): +- result interface{} // result of Run function, of type a.ResultType +- summary *actionSummary +- err error +-} +- +-func (act *action) String() string { +- return fmt.Sprintf("%s@%s", act.a.Name, act.pkg.mp.ID) +-} +- +-// execActions executes a set of action graph nodes in parallel. +-// Postcondition: each action.summary is set, even in case of error. +-func execActions(actions []*action) { +- var wg sync.WaitGroup +- for _, act := range actions { +- act := act +- wg.Add(1) +- go func() { +- defer wg.Done() +- act.once.Do(func() { +- execActions(act.hdeps) // analyze "horizontal" dependencies +- act.result, act.summary, act.err = act.exec() +- if act.err != nil { +- act.summary = &actionSummary{Err: act.err.Error()} +- // TODO(adonovan): suppress logging. But +- // shouldn't the root error's causal chain +- // include this information? +- if false { // debugging +- log.Printf("act.exec(%v) failed: %v", act, act.err) +- } - } +- }) +- if act.summary == nil { +- panic("nil action.summary (#60551)") - } +- }() +- } +- wg.Wait() +-} - -- // -- postorder -- -- -- an.allDeps[m.PkgPath] = an // add self entry (reflexive transitive closure) +-// exec defines the execution of a single action. +-// It returns the (ephemeral) result of the analyzer's Run function, +-// along with its (serializable) facts and diagnostics. +-// Or it returns an error if the analyzer did not run to +-// completion and deliver a valid result. +-func (act *action) exec() (interface{}, *actionSummary, error) { +- analyzer := act.a +- pkg := act.pkg - -- // Add leaf nodes (no successors) directly to queue. -- if len(an.succs) == 0 { -- leaves = append(leaves, an) -- } +- hasFacts := len(analyzer.FactTypes) > 0 - -- // Load the contents of each compiled Go file through -- // the snapshot's cache. (These are all cache hits as -- // files are pre-loaded following packages.Load) -- an.files = make([]source.FileHandle, len(m.CompiledGoFiles)) -- for i, uri := range m.CompiledGoFiles { -- fh, err := snapshot.ReadFile(ctx, uri) -- if err != nil { -- return nil, err -- } -- an.files[i] = fh +- // Report an error if any action dependency (vertical or horizontal) failed. +- // To avoid long error messages describing chains of failure, +- // we return the dependencies' error' unadorned. +- if hasFacts { +- // TODO(adonovan): use deterministic order. +- for _, vdep := range act.vdeps { +- if summ := vdep.summary.Actions[act.stableName]; summ.Err != "" { +- return nil, nil, errors.New(summ.Err) - } - } -- // Add edge from predecessor. -- if from != nil { -- atomic.AddInt32(&from.unfinishedSuccs, 1) // TODO(adonovan): use generics -- an.preds = append(an.preds, from) +- } +- for _, dep := range act.hdeps { +- if dep.err != nil { +- return nil, nil, dep.err - } -- atomic.AddInt32(&an.unfinishedPreds, 1) -- return an, nil - } +- // Inv: all action dependencies succeeded. - -- // For root packages, we run the enabled set of analyzers. -- var roots []*analysisNode -- for id := range pkgs { -- root, err := makeNode(nil, id) -- if err != nil { -- return nil, err -- } -- root.analyzers = enabled -- roots = append(roots, root) +- // Were there list/parse/type errors that might prevent analysis? +- if !pkg.compiles && !analyzer.RunDespiteErrors { +- return nil, nil, fmt.Errorf("skipping analysis %q because package %q does not compile", analyzer.Name, pkg.mp.ID) - } +- // Inv: package is well-formed enough to proceed with analysis. - -- // Now that we have read all files, -- // we no longer need the snapshot. -- // (but options are needed for progress reporting) -- options := snapshot.Options() -- snapshot = nil +- if false { // debugging +- log.Println("action.exec", act) +- } - -- // Progress reporting. If supported, gopls reports progress on analysis -- // passes that are taking a long time. -- maybeReport := func(completed int64) {} +- // Gather analysis Result values from horizontal dependencies. +- inputs := make(map[*analysis.Analyzer]interface{}) +- for _, dep := range act.hdeps { +- inputs[dep.a] = dep.result +- } - -- // Enable progress reporting if enabled by the user -- // and we have a capable reporter. -- if reporter != nil && reporter.SupportsWorkDoneProgress() && options.AnalysisProgressReporting { -- var reportAfter = options.ReportAnalysisProgressAfter // tests may set this to 0 -- const reportEvery = 1 * time.Second +- // TODO(adonovan): opt: facts.Set works but it may be more +- // efficient to fork and tailor it to our precise needs. +- // +- // We've already sharded the fact encoding by action +- // so that it can be done in parallel. +- // We could eliminate locking. +- // We could also dovetail more closely with the export data +- // decoder to obtain a more compact representation of +- // packages and objects (e.g. its internal IDs, instead +- // of PkgPaths and objectpaths.) +- // More importantly, we should avoid re-export of +- // facts that related to objects that are discarded +- // by "deep" export data. Better still, use a "shallow" approach. - -- ctx, cancel := context.WithCancel(ctx) -- defer cancel() +- // Read and decode analysis facts for each direct import. +- factset, err := pkg.factsDecoder.Decode(func(pkgPath string) ([]byte, error) { +- if !hasFacts { +- return nil, nil // analyzer doesn't use facts, so no vdeps +- } - -- var ( -- reportMu sync.Mutex -- lastReport time.Time -- wd *progress.WorkDone -- ) -- defer func() { -- reportMu.Lock() -- defer reportMu.Unlock() +- // Package.Imports() may contain a fake "C" package. Ignore it. +- if pkgPath == "C" { +- return nil, nil +- } - -- if wd != nil { -- wd.End(ctx, "Done.") // ensure that the progress report exits -- } -- }() -- maybeReport = func(completed int64) { -- now := time.Now() -- if now.Sub(start) < reportAfter { -- return -- } +- id, ok := pkg.mp.DepsByPkgPath[PackagePath(pkgPath)] +- if !ok { +- // This may mean imp was synthesized by the type +- // checker because it failed to import it for any reason +- // (e.g. bug processing export data; metadata ignoring +- // a cycle-forming import). +- // In that case, the fake package's imp.Path +- // is set to the failed importPath (and thus +- // it may lack a "vendor/" prefix). +- // +- // For now, silently ignore it on the assumption +- // that the error is already reported elsewhere. +- // return nil, fmt.Errorf("missing metadata") +- return nil, nil +- } - -- reportMu.Lock() -- defer reportMu.Unlock() +- vdep := act.vdeps[id] +- if vdep == nil { +- return nil, bug.Errorf("internal error in %s: missing vdep for id=%s", pkg.types.Path(), id) +- } - -- if wd == nil { -- wd = reporter.Start(ctx, AnalysisProgressTitle, "", nil, cancel) -- } +- return vdep.summary.Actions[act.stableName].Facts, nil +- }) +- if err != nil { +- return nil, nil, fmt.Errorf("internal error decoding analysis facts: %w", err) +- } - -- if now.Sub(lastReport) > reportEvery { -- lastReport = now -- // Trailing space is intentional: some LSP clients strip newlines. -- msg := fmt.Sprintf(`Indexed %d/%d packages. (Set "analysisProgressReporting" to false to disable notifications.)`, -- completed, len(nodes)) -- pct := 100 * float64(completed) / float64(len(nodes)) -- wd.Report(ctx, msg, pct) -- } +- // TODO(adonovan): make Export*Fact panic rather than discarding +- // undeclared fact types, so that we discover bugs in analyzers. +- factFilter := make(map[reflect.Type]bool) +- for _, f := range analyzer.FactTypes { +- factFilter[reflect.TypeOf(f)] = true +- } +- +- // If the package contains "fixed" files, it's not necessarily an error if we +- // can't convert positions. +- hasFixedFiles := false +- for _, p := range pkg.parsed { +- if p.Fixed() { +- hasFixedFiles = true +- break - } - } - -- // Execute phase: run leaves first, adding -- // new nodes to the queue as they become leaves. -- var g errgroup.Group +- // posToLocation converts from token.Pos to protocol form. +- // TODO(adonovan): improve error messages. +- posToLocation := func(start, end token.Pos) (protocol.Location, error) { +- tokFile := pkg.fset.File(start) - -- // Analysis is CPU-bound. -- // -- // Note: avoid g.SetLimit here: it makes g.Go stop accepting work, which -- // prevents workers from enqeuing, and thus finishing, and thus allowing the -- // group to make progress: deadlock. -- limiter := make(chan unit, runtime.GOMAXPROCS(0)) -- var completed int64 +- for _, p := range pkg.parsed { +- if p.Tok == tokFile { +- if end == token.NoPos { +- end = start +- } - -- var enqueue func(*analysisNode) -- enqueue = func(an *analysisNode) { -- g.Go(func() error { -- limiter <- unit{} -- defer func() { <-limiter }() +- // debugging #64547 +- if start < token.Pos(tokFile.Base()) { +- bug.Reportf("start < start of file") +- } +- if end > token.Pos(tokFile.Base()+tokFile.Size()+1) { +- bug.Reportf("end > end of file + 1") +- } - -- summary, err := an.runCached(ctx) -- if err != nil { -- return err // cancelled, or failed to produce a package +- return p.PosLocation(start, end) - } -- maybeReport(atomic.AddInt64(&completed, 1)) -- an.summary = summary +- } +- errorf := bug.Errorf +- if hasFixedFiles { +- errorf = fmt.Errorf +- } +- return protocol.Location{}, errorf("token.Pos not within package") +- } - -- // Notify each waiting predecessor, -- // and enqueue it when it becomes a leaf. -- for _, pred := range an.preds { -- if atomic.AddInt32(&pred.unfinishedSuccs, -1) == 0 { -- enqueue(pred) +- // Now run the (pkg, analyzer) action. +- var diagnostics []gobDiagnostic +- pass := &analysis.Pass{ +- Analyzer: analyzer, +- Fset: pkg.fset, +- Files: pkg.files, +- Pkg: pkg.types, +- TypesInfo: pkg.typesInfo, +- TypesSizes: pkg.typesSizes, +- TypeErrors: pkg.typeErrors, +- ResultOf: inputs, +- Report: func(d analysis.Diagnostic) { +- diagnostic, err := toGobDiagnostic(posToLocation, analyzer, d) +- if err != nil { +- if !hasFixedFiles { +- bug.Reportf("internal error converting diagnostic from analyzer %q: %v", analyzer.Name, err) - } +- return - } -- -- // Notify each successor that we no longer need -- // its action summaries, which hold Result values. -- // After the last one, delete it, so that we -- // free up large results such as SSA. -- for _, succ := range an.succs { -- succ.decrefPreds() -- } -- return nil -- }) -- } -- for _, leaf := range leaves { -- enqueue(leaf) -- } -- if err := g.Wait(); err != nil { -- return nil, err // cancelled, or failed to produce a package +- diagnostics = append(diagnostics, diagnostic) +- }, +- ImportObjectFact: factset.ImportObjectFact, +- ExportObjectFact: factset.ExportObjectFact, +- ImportPackageFact: factset.ImportPackageFact, +- ExportPackageFact: factset.ExportPackageFact, +- AllObjectFacts: func() []analysis.ObjectFact { return factset.AllObjectFacts(factFilter) }, +- AllPackageFacts: func() []analysis.PackageFact { return factset.AllPackageFacts(factFilter) }, - } - -- // Report diagnostics only from enabled actions that succeeded. -- // Errors from creating or analyzing packages are ignored. -- // Diagnostics are reported in the order of the analyzers argument. -- // -- // TODO(adonovan): ignoring action errors gives the caller no way -- // to distinguish "there are no problems in this code" from -- // "the code (or analyzers!) are so broken that we couldn't even -- // begin the analysis you asked for". -- // Even if current callers choose to discard the -- // results, we should propagate the per-action errors. -- var results []*source.Diagnostic -- for _, root := range roots { -- for _, a := range enabled { -- // Skip analyzers that were added only to -- // fulfil requirements of the original set. -- srcAnalyzer, ok := toSrc[a] -- if !ok { -- // Although this 'skip' operation is logically sound, -- // it is nonetheless surprising that its absence should -- // cause #60909 since none of the analyzers currently added for -- // requirements (e.g. ctrlflow, inspect, buildssa) -- // is capable of reporting diagnostics. -- if summary := root.summary.Actions[stableNames[a]]; summary != nil { -- if n := len(summary.Diagnostics); n > 0 { -- bug.Reportf("Internal error: got %d unexpected diagnostics from analyzer %s. This analyzer was added only to fulfil the requirements of the requested set of analyzers, and it is not expected that such analyzers report diagnostics. Please report this in issue #60909.", n, a) +- // Recover from panics (only) within the analyzer logic. +- // (Use an anonymous function to limit the recover scope.) +- var result interface{} +- func() { +- start := time.Now() +- defer func() { +- if r := recover(); r != nil { +- // An Analyzer panicked, likely due to a bug. +- // +- // In general we want to discover and fix such panics quickly, +- // so we don't suppress them, but some bugs in third-party +- // analyzers cannot be quickly fixed, so we use an allowlist +- // to suppress panics. +- const strict = true +- if strict && bug.PanicOnBugs && +- analyzer.Name != "buildir" { // see https://github.com/dominikh/go-tools/issues/1343 +- // Uncomment this when debugging suspected failures +- // in the driver, not the analyzer. +- if false { +- debug.SetTraceback("all") // show all goroutines - } +- panic(r) +- } else { +- // In production, suppress the panic and press on. +- err = fmt.Errorf("analysis %s for package %s panicked: %v", analyzer.Name, pass.Pkg.Path(), r) - } -- continue - } - -- // Inv: root.summary is the successful result of run (via runCached). -- summary, ok := root.summary.Actions[stableNames[a]] -- if summary == nil { -- panic(fmt.Sprintf("analyzeSummary.Actions[%q] = (nil, %t); got %v (#60551)", -- stableNames[a], ok, root.summary.Actions)) -- } -- if summary.Err != "" { -- continue // action failed -- } -- for _, gobDiag := range summary.Diagnostics { -- results = append(results, toSourceDiagnostic(srcAnalyzer, &gobDiag)) -- } -- } +- // Accumulate running time for each checker. +- analyzerRunTimesMu.Lock() +- analyzerRunTimes[analyzer] += time.Since(start) +- analyzerRunTimesMu.Unlock() +- }() +- +- result, err = pass.Analyzer.Run(pass) +- }() +- if err != nil { +- return nil, nil, err - } -- return results, nil +- +- if got, want := reflect.TypeOf(result), pass.Analyzer.ResultType; got != want { +- return nil, nil, bug.Errorf( +- "internal error: on package %s, analyzer %s returned a result of type %v, but declared ResultType %v", +- pass.Pkg.Path(), pass.Analyzer, got, want) +- } +- +- // Disallow Export*Fact calls after Run. +- // (A panic means the Analyzer is abusing concurrency.) +- pass.ExportObjectFact = func(obj types.Object, fact analysis.Fact) { +- panic(fmt.Sprintf("%v: Pass.ExportObjectFact(%s, %T) called after Run", act, obj, fact)) +- } +- pass.ExportPackageFact = func(fact analysis.Fact) { +- panic(fmt.Sprintf("%v: Pass.ExportPackageFact(%T) called after Run", act, fact)) +- } +- +- factsdata := factset.Encode() +- return result, &actionSummary{ +- Diagnostics: diagnostics, +- Facts: factsdata, +- FactsHash: file.HashOf(factsdata), +- }, nil -} - --func (an *analysisNode) decrefPreds() { -- if atomic.AddInt32(&an.unfinishedPreds, -1) == 0 { -- an.summary.Actions = nil +-var ( +- analyzerRunTimesMu sync.Mutex +- analyzerRunTimes = make(map[*analysis.Analyzer]time.Duration) +-) +- +-type LabelDuration struct { +- Label string +- Duration time.Duration +-} +- +-// AnalyzerTimes returns the accumulated time spent in each Analyzer's +-// Run function since process start, in descending order. +-func AnalyzerRunTimes() []LabelDuration { +- analyzerRunTimesMu.Lock() +- defer analyzerRunTimesMu.Unlock() +- +- slice := make([]LabelDuration, 0, len(analyzerRunTimes)) +- for a, t := range analyzerRunTimes { +- slice = append(slice, LabelDuration{Label: a.Name, Duration: t}) - } +- sort.Slice(slice, func(i, j int) bool { +- return slice[i].Duration > slice[j].Duration +- }) +- return slice -} - --// An analysisNode is a node in a doubly-linked DAG isomorphic to the --// import graph. Each node represents a single package, and the DAG --// represents a batch of analysis work done at once using a single --// realm of token.Pos or types.Object values. --// --// A complete DAG is created anew for each batch of analysis; --// subgraphs are not reused over time. Each node's *types.Package --// field is initially nil and is populated on demand, either from --// type-checking syntax trees (typeCheck) or from importing export --// data (_import). When this occurs, the typesOnce event becomes --// "done". --// --// Each node's allDeps map is a "view" of all its dependencies keyed by --// package path, which defines the types.Importer mapping used when --// populating the node's types.Package. Different nodes have different --// views (e.g. due to variants), but two nodes that are related by --// graph ordering have views that are consistent in their overlap. --// exportDeps is the subset actually referenced by export data; --// this is the set for which we attempt to decode facts. --// --// Each node's run method is called in parallel postorder. On success, --// its summary field is populated, either from the cache (hit), or by --// type-checking and analyzing syntax (miss). --type analysisNode struct { -- fset *token.FileSet // file set shared by entire batch (DAG) -- m *source.Metadata // metadata for this package -- files []source.FileHandle // contents of CompiledGoFiles -- analyzers []*analysis.Analyzer // set of analyzers to run -- preds []*analysisNode // graph edges: -- succs map[PackageID]*analysisNode // (preds -> self -> succs) -- unfinishedSuccs int32 -- unfinishedPreds int32 // effectively a summary.Actions refcount -- allDeps map[PackagePath]*analysisNode // all dependencies including self -- exportDeps map[PackagePath]*analysisNode // subset of allDeps ref'd by export data (+self) -- summary *analyzeSummary // serializable result of analyzing this package -- stableNames map[*analysis.Analyzer]string // cross-process stable names for Analyzers -- -- typesOnce sync.Once // guards lazy population of types and typesErr fields -- types *types.Package // type information lazily imported from summary -- typesErr error // an error producing type information --} -- --func (an *analysisNode) String() string { return string(an.m.ID) } -- --// _import imports this node's types.Package from export data, if not already done. --// Precondition: analysis was a success. --// Postcondition: an.types and an.exportDeps are populated. --func (an *analysisNode) _import() (*types.Package, error) { -- an.typesOnce.Do(func() { -- if an.m.PkgPath == "unsafe" { -- an.types = types.Unsafe -- return -- } -- -- an.types = types.NewPackage(string(an.m.PkgPath), string(an.m.Name)) -- -- // getPackages recursively imports each dependency -- // referenced by the export data, in parallel. -- getPackages := func(items []gcimporter.GetPackagesItem) error { -- var g errgroup.Group -- for i, item := range items { -- path := PackagePath(item.Path) -- dep, ok := an.allDeps[path] -- if !ok { -- // This early return bypasses Wait; that's ok. -- return fmt.Errorf("%s: unknown dependency %q", an.m, path) -- } -- an.exportDeps[path] = dep // record, for later fact decoding -- if dep == an { -- if an.typesErr != nil { -- return an.typesErr -- } else { -- items[i].Pkg = an.types -- } -- } else { -- i := i -- g.Go(func() error { -- depPkg, err := dep._import() -- if err == nil { -- items[i].Pkg = depPkg -- } -- return err -- }) -- } +-// requiredAnalyzers returns the transitive closure of required analyzers in preorder. +-func requiredAnalyzers(analyzers []*analysis.Analyzer) []*analysis.Analyzer { +- var result []*analysis.Analyzer +- seen := make(map[*analysis.Analyzer]bool) +- var visitAll func([]*analysis.Analyzer) +- visitAll = func(analyzers []*analysis.Analyzer) { +- for _, a := range analyzers { +- if !seen[a] { +- seen[a] = true +- result = append(result, a) +- visitAll(a.Requires) - } -- return g.Wait() - } -- pkg, err := gcimporter.IImportShallow(an.fset, getPackages, an.summary.Export, string(an.m.PkgPath), bug.Reportf) -- if err != nil { -- an.typesErr = bug.Errorf("%s: invalid export data: %v", an.m, err) -- an.types = nil -- } else if pkg != an.types { -- log.Fatalf("%s: inconsistent packages", an.m) -- } -- }) -- return an.types, an.typesErr +- } +- visitAll(analyzers) +- return result -} - --// analyzeSummary is a gob-serializable summary of successfully --// applying a list of analyzers to a package. --type analyzeSummary struct { -- Export []byte // encoded types of package -- DeepExportHash source.Hash // hash of reflexive transitive closure of export data -- Compiles bool // transitively free of list/parse/type errors -- Actions actionMap // maps analyzer stablename to analysis results (*actionSummary) --} +-var analyzeSummaryCodec = frob.CodecFor[*analyzeSummary]() - --// actionMap defines a stable Gob encoding for a map. --// TODO(adonovan): generalize and move to a library when we can use generics. --type actionMap map[string]*actionSummary +-// -- data types for serialization of analysis.Diagnostic and golang.Diagnostic -- - --var ( -- _ gob.GobEncoder = (actionMap)(nil) -- _ gob.GobDecoder = (*actionMap)(nil) --) +-// (The name says gob but we use frob.) +-var diagnosticsCodec = frob.CodecFor[[]gobDiagnostic]() - --type actionsMapEntry struct { -- K string -- V *actionSummary +-type gobDiagnostic struct { +- Location protocol.Location +- Severity protocol.DiagnosticSeverity +- Code string +- CodeHref string +- Source string +- Message string +- SuggestedFixes []gobSuggestedFix +- Related []gobRelatedInformation +- Tags []protocol.DiagnosticTag -} - --func (m actionMap) GobEncode() ([]byte, error) { -- entries := make([]actionsMapEntry, 0, len(m)) -- for k, v := range m { -- entries = append(entries, actionsMapEntry{k, v}) -- } -- sort.Slice(entries, func(i, j int) bool { -- return entries[i].K < entries[j].K -- }) -- var buf bytes.Buffer -- err := gob.NewEncoder(&buf).Encode(entries) -- return buf.Bytes(), err +-type gobRelatedInformation struct { +- Location protocol.Location +- Message string -} - --func (m *actionMap) GobDecode(data []byte) error { -- var entries []actionsMapEntry -- if err := gob.NewDecoder(bytes.NewReader(data)).Decode(&entries); err != nil { -- return err -- } -- *m = make(actionMap, len(entries)) -- for _, e := range entries { -- (*m)[e.K] = e.V -- } -- return nil +-type gobSuggestedFix struct { +- Message string +- TextEdits []gobTextEdit +- Command *gobCommand +- ActionKind protocol.CodeActionKind -} - --// actionSummary is a gob-serializable summary of one possibly failed analysis action. --// If Err is non-empty, the other fields are undefined. --type actionSummary struct { -- Facts []byte // the encoded facts.Set -- FactsHash source.Hash // hash(Facts) -- Diagnostics []gobDiagnostic -- Err string // "" => success +-type gobCommand struct { +- Title string +- Command string +- Arguments []json.RawMessage -} - --// runCached applies a list of analyzers (plus any others --// transitively required by them) to a package. It succeeds as long --// as it could produce a types.Package, even if there were direct or --// indirect list/parse/type errors, and even if all the analysis --// actions failed. It usually fails only if the package was unknown, --// a file was missing, or the operation was cancelled. --// --// Postcondition: runCached must not continue to use the snapshot --// (in background goroutines) after it has returned; see memoize.RefCounted. --func (an *analysisNode) runCached(ctx context.Context) (*analyzeSummary, error) { -- // At this point we have the action results (serialized -- // packages and facts) of our immediate dependencies, -- // and the metadata and content of this package. -- // -- // We now compute a hash for all our inputs, and consult a -- // global cache of promised results. If nothing material -- // has changed, we'll make a hit in the shared cache. -- // -- // The hash of our inputs is based on the serialized export -- // data and facts so that immaterial changes can be pruned -- // without decoding. -- key := an.cacheKey() -- -- // Access the cache. -- var summary *analyzeSummary -- const cacheKind = "analysis" -- if data, err := filecache.Get(cacheKind, key); err == nil { -- // cache hit -- analyzeSummaryCodec.Decode(data, &summary) -- } else if err != filecache.ErrNotFound { -- return nil, bug.Errorf("internal error reading shared cache: %v", err) -- } else { -- // Cache miss: do the work. -- var err error -- summary, err = an.run(ctx) -- if err != nil { -- return nil, err -- } -- -- atomic.AddInt32(&an.unfinishedPreds, +1) // incref -- go func() { -- defer an.decrefPreds() //decref -- -- cacheLimit <- unit{} // acquire token -- defer func() { <-cacheLimit }() // release token +-type gobTextEdit struct { +- Location protocol.Location +- NewText []byte +-} - -- data := analyzeSummaryCodec.Encode(summary) -- if false { -- log.Printf("Set key=%d value=%d id=%s\n", len(key), len(data), an.m.ID) -- } -- if err := filecache.Set(cacheKind, key, data); err != nil { -- event.Error(ctx, "internal error updating analysis shared cache", err) +-// toGobDiagnostic converts an analysis.Diagnosic to a serializable gobDiagnostic, +-// which requires expanding token.Pos positions into protocol.Location form. +-func toGobDiagnostic(posToLocation func(start, end token.Pos) (protocol.Location, error), a *analysis.Analyzer, diag analysis.Diagnostic) (gobDiagnostic, error) { +- var fixes []gobSuggestedFix +- for _, fix := range diag.SuggestedFixes { +- var gobEdits []gobTextEdit +- for _, textEdit := range fix.TextEdits { +- loc, err := posToLocation(textEdit.Pos, textEdit.End) +- if err != nil { +- return gobDiagnostic{}, fmt.Errorf("in SuggestedFixes: %w", err) - } -- }() +- gobEdits = append(gobEdits, gobTextEdit{ +- Location: loc, +- NewText: textEdit.NewText, +- }) +- } +- fixes = append(fixes, gobSuggestedFix{ +- Message: fix.Message, +- TextEdits: gobEdits, +- }) - } - -- return summary, nil --} -- --// cacheLimit reduces parallelism of cache updates. --// We allow more than typical GOMAXPROCS as it's a mix of CPU and I/O. --var cacheLimit = make(chan unit, 32) -- --// analysisCacheKey returns a cache key that is a cryptographic digest --// of the all the values that might affect type checking and analysis: --// the analyzer names, package metadata, names and contents of --// compiled Go files, and vdeps (successor) information --// (export data and facts). --func (an *analysisNode) cacheKey() [sha256.Size]byte { -- hasher := sha256.New() -- -- // In principle, a key must be the hash of an -- // unambiguous encoding of all the relevant data. -- // If it's ambiguous, we risk collisions. -- -- // analyzers -- fmt.Fprintf(hasher, "analyzers: %d\n", len(an.analyzers)) -- for _, a := range an.analyzers { -- fmt.Fprintln(hasher, a.Name) +- var related []gobRelatedInformation +- for _, r := range diag.Related { +- loc, err := posToLocation(r.Pos, r.End) +- if err != nil { +- return gobDiagnostic{}, fmt.Errorf("in Related: %w", err) +- } +- related = append(related, gobRelatedInformation{ +- Location: loc, +- Message: r.Message, +- }) - } - -- // package metadata -- m := an.m -- fmt.Fprintf(hasher, "package: %s %s %s\n", m.ID, m.Name, m.PkgPath) -- // We can ignore m.DepsBy{Pkg,Import}Path: although the logic -- // uses those fields, we account for them by hashing vdeps. -- -- // type sizes -- wordSize := an.m.TypesSizes.Sizeof(types.Typ[types.Int]) -- maxAlign := an.m.TypesSizes.Alignof(types.NewPointer(types.Typ[types.Int64])) -- fmt.Fprintf(hasher, "sizes: %d %d\n", wordSize, maxAlign) -- -- // metadata errors: used for 'compiles' field -- fmt.Fprintf(hasher, "errors: %d", len(m.Errors)) -- -- // module Go version -- if m.Module != nil && m.Module.GoVersion != "" { -- fmt.Fprintf(hasher, "go %s\n", m.Module.GoVersion) +- loc, err := posToLocation(diag.Pos, diag.End) +- if err != nil { +- return gobDiagnostic{}, err - } - -- // file names and contents -- fmt.Fprintf(hasher, "files: %d\n", len(an.files)) -- for _, fh := range an.files { -- fmt.Fprintln(hasher, fh.FileIdentity()) +- // The Code column of VSCode's Problems table renders this +- // information as "Source(Code)" where code is a link to CodeHref. +- // (The code field must be nonempty for anything to appear.) +- diagURL := effectiveURL(a, diag) +- code := "default" +- if diag.Category != "" { +- code = diag.Category - } - -- // vdeps, in PackageID order -- depIDs := make([]string, 0, len(an.succs)) -- for depID := range an.succs { -- depIDs = append(depIDs, string(depID)) -- } -- sort.Strings(depIDs) // TODO(adonovan): avoid conversions by using slices.Sort[PackageID] -- for _, depID := range depIDs { -- vdep := an.succs[PackageID(depID)] -- fmt.Fprintf(hasher, "dep: %s\n", vdep.m.PkgPath) -- fmt.Fprintf(hasher, "export: %s\n", vdep.summary.DeepExportHash) +- return gobDiagnostic{ +- Location: loc, +- // Severity for analysis diagnostics is dynamic, +- // based on user configuration per analyzer. +- Code: code, +- CodeHref: diagURL, +- Source: a.Name, +- Message: diag.Message, +- SuggestedFixes: fixes, +- Related: related, +- // Analysis diagnostics do not contain tags. +- }, nil +-} - -- // action results: errors and facts -- actions := vdep.summary.Actions -- names := make([]string, 0, len(actions)) -- for name := range actions { -- names = append(names, name) -- } -- sort.Strings(names) -- for _, name := range names { -- summary := actions[name] -- fmt.Fprintf(hasher, "action %s\n", name) -- if summary.Err != "" { -- fmt.Fprintf(hasher, "error %s\n", summary.Err) -- } else { -- fmt.Fprintf(hasher, "facts %s\n", summary.FactsHash) -- // We can safely omit summary.diagnostics -- // from the key since they have no downstream effect. -- } +-// effectiveURL computes the effective URL of diag, +-// using the algorithm specified at Diagnostic.URL. +-func effectiveURL(a *analysis.Analyzer, diag analysis.Diagnostic) string { +- u := diag.URL +- if u == "" && diag.Category != "" { +- u = "#" + diag.Category +- } +- if base, err := urlpkg.Parse(a.URL); err == nil { +- if rel, err := urlpkg.Parse(u); err == nil { +- u = base.ResolveReference(rel).String() - } - } -- -- var hash [sha256.Size]byte -- hasher.Sum(hash[:0]) -- return hash +- return u -} - --// run implements the cache-miss case. --// This function does not access the snapshot. +-// stableName returns a name for the analyzer that is unique and +-// stable across address spaces. -// --// Postcondition: on success, the analyzeSummary.Actions --// key set is {a.Name for a in analyzers}. --func (an *analysisNode) run(ctx context.Context) (*analyzeSummary, error) { -- // Parse only the "compiled" Go files. -- // Do the computation in parallel. -- parsed := make([]*source.ParsedGoFile, len(an.files)) -- { -- var group errgroup.Group -- group.SetLimit(4) // not too much: run itself is already called in parallel -- for i, fh := range an.files { -- i, fh := i, fh -- group.Go(func() error { -- // Call parseGoImpl directly, not the caching wrapper, -- // as cached ASTs require the global FileSet. -- // ast.Object resolution is unfortunately an implied part of the -- // go/analysis contract. -- pgf, err := parseGoImpl(ctx, an.fset, fh, source.ParseFull&^source.SkipObjectResolution, false) -- parsed[i] = pgf -- return err -- }) -- } -- if err := group.Wait(); err != nil { -- return nil, err // cancelled, or catastrophic error (e.g. missing file) -- } -- } +-// Analyzer names are not unique. For example, gopls includes +-// both x/tools/passes/nilness and staticcheck/nilness. +-// For serialization, we must assign each analyzer a unique identifier +-// that two gopls processes accessing the cache can agree on. +-func stableName(a *analysis.Analyzer) string { +- // Incorporate the file and line of the analyzer's Run function. +- addr := reflect.ValueOf(a.Run).Pointer() +- fn := runtime.FuncForPC(addr) +- file, line := fn.FileLine(addr) - -- // Type-check the package syntax. -- pkg := an.typeCheck(parsed) +- // It is tempting to use just a.Name as the stable name when +- // it is unique, but making them always differ helps avoid +- // name/stablename confusion. +- return fmt.Sprintf("%s(%s:%d)", a.Name, filepath.Base(file), line) +-} +diff -urN a/gopls/internal/cache/cache.go b/gopls/internal/cache/cache.go +--- a/gopls/internal/cache/cache.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/cache.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,76 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- // Publish the completed package. -- an.typesOnce.Do(func() { an.types = pkg.types }) -- if an.types != pkg.types { -- log.Fatalf("typesOnce prematurely done") -- } +-package cache - -- // Compute the union of exportDeps across our direct imports. -- // This is the set that will be needed by the fact decoder. -- allExportDeps := make(map[PackagePath]*analysisNode) -- for _, succ := range an.succs { -- for k, v := range succ.exportDeps { -- allExportDeps[k] = v -- } -- } +-import ( +- "reflect" +- "strconv" +- "sync/atomic" - -- // The fact decoder needs a means to look up a Package by path. -- pkg.factsDecoder = facts.NewDecoderFunc(pkg.types, func(path string) *types.Package { -- // Note: Decode is called concurrently, and thus so is this function. +- "golang.org/x/tools/gopls/internal/protocol/command" +- "golang.org/x/tools/internal/imports" +- "golang.org/x/tools/internal/memoize" +-) - -- // Does the fact relate to a package referenced by export data? -- if dep, ok := allExportDeps[PackagePath(path)]; ok { -- dep.typesOnce.Do(func() { log.Fatal("dep.types not populated") }) -- if dep.typesErr == nil { -- return dep.types -- } -- return nil -- } +-// New Creates a new cache for gopls operation results, using the given file +-// set, shared store, and session options. +-// +-// Both the fset and store may be nil, but if store is non-nil so must be fset +-// (and they must always be used together), otherwise it may be possible to get +-// cached data referencing token.Pos values not mapped by the FileSet. +-func New(store *memoize.Store) *Cache { +- index := atomic.AddInt64(&cacheIndex, 1) - -- // If the fact relates to a dependency not referenced -- // by export data, it is safe to ignore it. -- // (In that case dep.types exists but may be unpopulated -- // or in the process of being populated from export data.) -- if an.allDeps[PackagePath(path)] == nil { -- log.Fatalf("fact package %q is not a dependency", path) -- } -- return nil -- }) +- if store == nil { +- store = &memoize.Store{} +- } - -- // Poll cancellation state. -- if err := ctx.Err(); err != nil { -- return nil, err +- c := &Cache{ +- id: strconv.FormatInt(index, 10), +- store: store, +- memoizedFS: newMemoizedFS(), +- modCache: &sharedModCache{ +- caches: make(map[string]*imports.DirInfoCache), +- timers: make(map[string]*refreshTimer), +- }, - } +- return c +-} - -- // -- analysis -- +-// A Cache holds content that is shared across multiple gopls sessions. +-type Cache struct { +- id string - -- // Build action graph for this package. -- // Each graph node (action) is one unit of analysis. -- actions := make(map[*analysis.Analyzer]*action) -- var mkAction func(a *analysis.Analyzer) *action -- mkAction = func(a *analysis.Analyzer) *action { -- act, ok := actions[a] -- if !ok { -- var hdeps []*action -- for _, req := range a.Requires { -- hdeps = append(hdeps, mkAction(req)) -- } -- act = &action{ -- a: a, -- stableName: an.stableNames[a], -- pkg: pkg, -- vdeps: an.succs, -- hdeps: hdeps, -- } -- actions[a] = act -- } -- return act -- } +- // store holds cached calculations. +- // +- // TODO(rfindley): at this point, these are not important, as we've moved our +- // content-addressable cache to the file system (the filecache package). It +- // is unlikely that this shared cache provides any shared value. We should +- // consider removing it, replacing current uses with a simpler futures cache, +- // as we've done for e.g. type-checked packages. +- store *memoize.Store - -- // Build actions for initial package. -- var roots []*action -- for _, a := range an.analyzers { -- roots = append(roots, mkAction(a)) -- } +- // memoizedFS holds a shared file.Source that caches reads. +- // +- // Reads are invalidated when *any* session gets a didChangeWatchedFile +- // notification. This is fine: it is the responsibility of memoizedFS to hold +- // our best knowledge of the current file system state. +- *memoizedFS - -- // Execute the graph in parallel. -- execActions(roots) -- // Inv: each root's summary is set (whether success or error). +- // modCache holds the +- modCache *sharedModCache +-} - -- // Don't return (or cache) the result in case of cancellation. -- if err := ctx.Err(); err != nil { -- return nil, err // cancelled -- } +-var cacheIndex, sessionIndex, viewIndex int64 - -- // Return summaries only for the requested actions. -- summaries := make(map[string]*actionSummary) -- for _, root := range roots { -- if root.summary == nil { -- panic("root has nil action.summary (#60551)") -- } -- summaries[root.stableName] = root.summary -- } +-func (c *Cache) ID() string { return c.id } +-func (c *Cache) MemStats() map[reflect.Type]int { return c.store.Stats() } - -- return &analyzeSummary{ -- Export: pkg.export, -- DeepExportHash: pkg.deepExportHash, -- Compiles: pkg.compiles, -- Actions: summaries, -- }, nil +-// FileStats returns information about the set of files stored in the cache. +-// It is intended for debugging only. +-func (c *Cache) FileStats() (stats command.FileStats) { +- stats.Total, stats.Largest, stats.Errs = c.fileStats() +- return -} +diff -urN a/gopls/internal/cache/check.go b/gopls/internal/cache/check.go +--- a/gopls/internal/cache/check.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/check.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,1991 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// Postcondition: analysisPackage.types and an.exportDeps are populated. --func (an *analysisNode) typeCheck(parsed []*source.ParsedGoFile) *analysisPackage { -- m := an.m +-package cache - -- if false { // debugging -- log.Println("typeCheck", m.ID) -- } +-import ( +- "context" +- "crypto/sha256" +- "fmt" +- "go/ast" +- "go/build" +- "go/parser" +- "go/token" +- "go/types" +- "regexp" +- "runtime" +- "sort" +- "strings" +- "sync" +- "sync/atomic" - -- pkg := &analysisPackage{ -- m: m, -- fset: an.fset, -- parsed: parsed, -- files: make([]*ast.File, len(parsed)), -- compiles: len(m.Errors) == 0, // false => list error -- types: types.NewPackage(string(m.PkgPath), string(m.Name)), -- typesInfo: &types.Info{ -- Types: make(map[ast.Expr]types.TypeAndValue), -- Defs: make(map[*ast.Ident]types.Object), -- Uses: make(map[*ast.Ident]types.Object), -- Implicits: make(map[ast.Node]types.Object), -- Selections: make(map[*ast.SelectorExpr]*types.Selection), -- Scopes: make(map[ast.Node]*types.Scope), -- }, -- typesSizes: m.TypesSizes, -- } -- typeparams.InitInstanceInfo(pkg.typesInfo) +- "golang.org/x/mod/module" +- "golang.org/x/sync/errgroup" +- "golang.org/x/tools/go/ast/astutil" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/cache/typerefs" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/filecache" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/gopls/internal/util/slices" +- "golang.org/x/tools/internal/analysisinternal" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/tag" +- "golang.org/x/tools/internal/gcimporter" +- "golang.org/x/tools/internal/packagesinternal" +- "golang.org/x/tools/internal/tokeninternal" +- "golang.org/x/tools/internal/typesinternal" +- "golang.org/x/tools/internal/versions" +-) - -- // Unsafe has no syntax. -- if m.PkgPath == "unsafe" { -- pkg.types = types.Unsafe -- return pkg -- } +-// Various optimizations that should not affect correctness. +-const ( +- preserveImportGraph = true // hold on to the import graph for open packages +-) - -- for i, p := range parsed { -- pkg.files[i] = p.File -- if p.ParseErr != nil { -- pkg.compiles = false // parse error -- } -- } +-type unit = struct{} - -- for _, vdep := range an.succs { -- if !vdep.summary.Compiles { -- pkg.compiles = false // transitive error -- } +-// A typeCheckBatch holds data for a logical type-checking operation, which may +-// type-check many unrelated packages. +-// +-// It shares state such as parsed files and imports, to optimize type-checking +-// for packages with overlapping dependency graphs. +-type typeCheckBatch struct { +- activePackageCache interface { +- getActivePackage(id PackageID) *Package +- setActivePackage(id PackageID, pkg *Package) - } +- syntaxIndex map[PackageID]int // requested ID -> index in ids +- pre preTypeCheck +- post postTypeCheck +- handles map[PackageID]*packageHandle +- parseCache *parseCache +- fset *token.FileSet // describes all parsed or imported files +- cpulimit chan unit // concurrency limiter for CPU-bound operations - -- cfg := &types.Config{ -- Sizes: m.TypesSizes, -- Error: func(e error) { -- pkg.compiles = false // type error -- -- // Suppress type errors in files with parse errors -- // as parser recovery can be quite lossy (#59888). -- typeError := e.(types.Error) -- for _, p := range parsed { -- if p.ParseErr != nil && source.NodeContains(p.File, typeError.Pos) { -- return -- } -- } -- pkg.typeErrors = append(pkg.typeErrors, typeError) -- }, -- Importer: importerFunc(func(importPath string) (*types.Package, error) { -- // Beware that returning an error from this function -- // will cause the type checker to synthesize a fake -- // package whose Path is importPath, potentially -- // losing a vendor/ prefix. If type-checking errors -- // are swallowed, these packages may be confusing. -- -- // Map ImportPath to ID. -- id, ok := m.DepsByImpPath[ImportPath(importPath)] -- if !ok { -- // The import syntax is inconsistent with the metadata. -- // This could be because the import declaration was -- // incomplete and the metadata only includes complete -- // imports; or because the metadata ignores import -- // edges that would lead to cycles in the graph. -- return nil, fmt.Errorf("missing metadata for import of %q", importPath) -- } -- -- // Map ID to node. (id may be "") -- dep := an.succs[id] -- if dep == nil { -- // Analogous to (*snapshot).missingPkgError -- // in the logic for regular type-checking, -- // but without a snapshot we can't provide -- // such detail, and anyway most analysis -- // failures aren't surfaced in the UI. -- return nil, fmt.Errorf("no required module provides analysis package %q (id=%q)", importPath, id) -- } -- -- // (Duplicates logic from check.go.) -- if !source.IsValidImport(an.m.PkgPath, dep.m.PkgPath) { -- return nil, fmt.Errorf("invalid use of internal package %s", importPath) -- } -- -- return dep._import() -- }), -- } +- mu sync.Mutex +- syntaxPackages map[PackageID]*futurePackage // results of processing a requested package; may hold (nil, nil) +- importPackages map[PackageID]*futurePackage // package results to use for importing +-} - -- // Set Go dialect. -- if m.Module != nil && m.Module.GoVersion != "" { -- goVersion := "go" + m.Module.GoVersion -- // types.NewChecker panics if GoVersion is invalid. -- // An unparsable mod file should probably stop us -- // before we get here, but double check just in case. -- if goVersionRx.MatchString(goVersion) { -- typesinternal.SetGoVersion(cfg, goVersion) -- } -- } +-// A futurePackage is a future result of type checking or importing a package, +-// to be cached in a map. +-// +-// The goroutine that creates the futurePackage is responsible for evaluating +-// its value, and closing the done channel. +-type futurePackage struct { +- done chan unit +- v pkgOrErr +-} - -- // We want to type check cgo code if go/types supports it. -- // We passed typecheckCgo to go/packages when we Loaded. -- // TODO(adonovan): do we actually need this?? -- typesinternal.SetUsesCgo(cfg) +-type pkgOrErr struct { +- pkg *types.Package +- err error +-} - -- check := types.NewChecker(cfg, pkg.fset, pkg.types, pkg.typesInfo) +-// TypeCheck parses and type-checks the specified packages, +-// and returns them in the same order as the ids. +-// The resulting packages' types may belong to different importers, +-// so types from different packages are incommensurable. +-// +-// The resulting packages slice always contains len(ids) entries, though some +-// of them may be nil if (and only if) the resulting error is non-nil. +-// +-// An error is returned if any of the requested packages fail to type-check. +-// This is different from having type-checking errors: a failure to type-check +-// indicates context cancellation or otherwise significant failure to perform +-// the type-checking operation. +-// +-// In general, clients should never need to type-checked syntax for an +-// intermediate test variant (ITV) package. Callers should apply +-// RemoveIntermediateTestVariants (or equivalent) before this method, or any +-// of the potentially type-checking methods below. +-func (s *Snapshot) TypeCheck(ctx context.Context, ids ...PackageID) ([]*Package, error) { +- pkgs := make([]*Package, len(ids)) - -- // Type checking errors are handled via the config, so ignore them here. -- _ = check.Files(pkg.files) +- var ( +- needIDs []PackageID // ids to type-check +- indexes []int // original index of requested ids +- ) - -- // debugging (type errors are quite normal) -- if false { -- if pkg.typeErrors != nil { -- log.Printf("package %s has type errors: %v", pkg.types.Path(), pkg.typeErrors) +- // Check for existing active packages, as any package will do. +- // +- // This is also done inside forEachPackage, but doing it here avoids +- // unnecessary set up for type checking (e.g. assembling the package handle +- // graph). +- for i, id := range ids { +- if pkg := s.getActivePackage(id); pkg != nil { +- pkgs[i] = pkg +- } else { +- needIDs = append(needIDs, id) +- indexes = append(indexes, i) - } - } - -- // Emit the export data and compute the recursive hash. -- export, err := gcimporter.IExportShallow(pkg.fset, pkg.types, bug.Reportf) -- if err != nil { -- // TODO(adonovan): in light of exporter bugs such as #57729, -- // consider using bug.Report here and retrying the IExportShallow -- // call here using an empty types.Package. -- log.Fatalf("internal error writing shallow export data: %v", err) -- } -- pkg.export = export -- -- // Compute a recursive hash to account for the export data of -- // this package and each dependency referenced by it. -- // Also, populate exportDeps. -- hash := sha256.New() -- fmt.Fprintf(hash, "%s %d\n", m.PkgPath, len(export)) -- hash.Write(export) -- paths, err := readShallowManifest(export) -- if err != nil { -- log.Fatalf("internal error: bad export data: %v", err) -- } -- for _, path := range paths { -- dep, ok := an.allDeps[PackagePath(path)] -- if !ok { -- log.Fatalf("%s: missing dependency: %q", an, path) -- } -- fmt.Fprintf(hash, "%s %s\n", dep.m.PkgPath, dep.summary.DeepExportHash) -- an.exportDeps[PackagePath(path)] = dep +- post := func(i int, pkg *Package) { +- pkgs[indexes[i]] = pkg - } -- an.exportDeps[m.PkgPath] = an // self -- hash.Sum(pkg.deepExportHash[:0]) -- -- return pkg +- return pkgs, s.forEachPackage(ctx, needIDs, nil, post) -} - --// readShallowManifest returns the manifest of packages referenced by --// a shallow export data file for a package (excluding the package itself). --// TODO(adonovan): add a test. --func readShallowManifest(export []byte) ([]PackagePath, error) { -- const selfPath = "" // dummy path -- var paths []PackagePath -- getPackages := func(items []gcimporter.GetPackagesItem) error { -- paths = []PackagePath{} // non-nil -- for _, item := range items { -- if item.Path != selfPath { -- paths = append(paths, PackagePath(item.Path)) -- } -- } -- return errors.New("stop") // terminate importer -- } -- _, err := gcimporter.IImportShallow(token.NewFileSet(), getPackages, export, selfPath, bug.Reportf) -- if paths == nil { -- if err != nil { -- return nil, err // failed before getPackages callback -- } -- return nil, bug.Errorf("internal error: IImportShallow did not call getPackages") +-// getImportGraph returns a shared import graph use for this snapshot, or nil. +-// +-// This is purely an optimization: holding on to more imports allows trading +-// memory for CPU and latency. Currently, getImportGraph returns an import +-// graph containing all packages imported by open packages, since these are +-// highly likely to be needed when packages change. +-// +-// Furthermore, since we memoize active packages, including their imports in +-// the shared import graph means we don't run the risk of pinning duplicate +-// copies of common imports, if active packages are computed in separate type +-// checking batches. +-func (s *Snapshot) getImportGraph(ctx context.Context) *importGraph { +- if !preserveImportGraph { +- return nil - } -- return paths, nil // success --} -- --// analysisPackage contains information about a package, including --// syntax trees, used transiently during its type-checking and analysis. --type analysisPackage struct { -- m *source.Metadata -- fset *token.FileSet // local to this package -- parsed []*source.ParsedGoFile -- files []*ast.File // same as parsed[i].File -- types *types.Package -- compiles bool // package is transitively free of list/parse/type errors -- factsDecoder *facts.Decoder -- export []byte // encoding of types.Package -- deepExportHash source.Hash // reflexive transitive hash of export data -- typesInfo *types.Info -- typeErrors []types.Error -- typesSizes types.Sizes --} -- --// An action represents one unit of analysis work: the application of --// one analysis to one package. Actions form a DAG, both within a --// package (as different analyzers are applied, either in sequence or --// parallel), and across packages (as dependencies are analyzed). --type action struct { -- once sync.Once -- a *analysis.Analyzer -- stableName string // cross-process stable name of analyzer -- pkg *analysisPackage -- hdeps []*action // horizontal dependencies -- vdeps map[PackageID]*analysisNode // vertical dependencies -- -- // results of action.exec(): -- result interface{} // result of Run function, of type a.ResultType -- summary *actionSummary -- err error --} -- --func (act *action) String() string { -- return fmt.Sprintf("%s@%s", act.a.Name, act.pkg.m.ID) --} +- s.mu.Lock() - --// execActions executes a set of action graph nodes in parallel. --// Postcondition: each action.summary is set, even in case of error. --func execActions(actions []*action) { -- var wg sync.WaitGroup -- for _, act := range actions { -- act := act -- wg.Add(1) +- // Evaluate the shared import graph for the snapshot. There are three major +- // codepaths here: +- // +- // 1. importGraphDone == nil, importGraph == nil: it is this goroutine's +- // responsibility to type-check the shared import graph. +- // 2. importGraphDone == nil, importGraph != nil: it is this goroutine's +- // responsibility to resolve the import graph, which may result in +- // type-checking only if the existing importGraph (carried over from the +- // preceding snapshot) is invalid. +- // 3. importGraphDone != nil: some other goroutine is doing (1) or (2), wait +- // for the work to be done. +- done := s.importGraphDone +- if done == nil { +- done = make(chan unit) +- s.importGraphDone = done +- release := s.Acquire() // must acquire to use the snapshot asynchronously - go func() { -- defer wg.Done() -- act.once.Do(func() { -- execActions(act.hdeps) // analyze "horizontal" dependencies -- act.result, act.summary, act.err = act.exec() -- if act.err != nil { -- act.summary = &actionSummary{Err: act.err.Error()} -- // TODO(adonovan): suppress logging. But -- // shouldn't the root error's causal chain -- // include this information? -- if false { // debugging -- log.Printf("act.exec(%v) failed: %v", act, act.err) -- } +- defer release() +- importGraph, err := s.resolveImportGraph() // may be nil +- if err != nil { +- if ctx.Err() == nil { +- event.Error(ctx, "computing the shared import graph", err) - } -- }) -- if act.summary == nil { -- panic("nil action.summary (#60551)") +- importGraph = nil - } +- s.mu.Lock() +- s.importGraph = importGraph +- s.mu.Unlock() +- close(done) - }() - } -- wg.Wait() +- s.mu.Unlock() +- +- select { +- case <-done: +- return s.importGraph +- case <-ctx.Done(): +- return nil +- } -} - --// exec defines the execution of a single action. --// It returns the (ephemeral) result of the analyzer's Run function, --// along with its (serializable) facts and diagnostics. --// Or it returns an error if the analyzer did not run to --// completion and deliver a valid result. --func (act *action) exec() (interface{}, *actionSummary, error) { -- analyzer := act.a -- pkg := act.pkg +-// resolveImportGraph evaluates the shared import graph to use for +-// type-checking in this snapshot. This may involve re-using the import graph +-// of the previous snapshot (stored in s.importGraph), or computing a fresh +-// import graph. +-// +-// resolveImportGraph should only be called from getImportGraph. +-func (s *Snapshot) resolveImportGraph() (*importGraph, error) { +- ctx := s.backgroundCtx +- ctx, done := event.Start(event.Detach(ctx), "cache.resolveImportGraph") +- defer done() - -- hasFacts := len(analyzer.FactTypes) > 0 +- s.mu.Lock() +- lastImportGraph := s.importGraph +- s.mu.Unlock() - -- // Report an error if any action dependency (vertical or horizontal) failed. -- // To avoid long error messages describing chains of failure, -- // we return the dependencies' error' unadorned. -- if hasFacts { -- // TODO(adonovan): use deterministic order. -- for _, vdep := range act.vdeps { -- if summ := vdep.summary.Actions[act.stableName]; summ.Err != "" { -- return nil, nil, errors.New(summ.Err) -- } +- openPackages := make(map[PackageID]bool) +- for _, fh := range s.Overlays() { +- // golang/go#66145: don't call MetadataForFile here. This function, which +- // builds a shared import graph, is an optimization. We don't want it to +- // have the side effect of triggering a load. +- // +- // In the past, a call to MetadataForFile here caused a bunch of +- // unnecessary loads in multi-root workspaces (and as a result, spurious +- // diagnostics). +- g := s.MetadataGraph() +- var mps []*metadata.Package +- for _, id := range g.IDs[fh.URI()] { +- mps = append(mps, g.Packages[id]) - } -- } -- for _, dep := range act.hdeps { -- if dep.err != nil { -- return nil, nil, dep.err +- metadata.RemoveIntermediateTestVariants(&mps) +- for _, mp := range mps { +- openPackages[mp.ID] = true - } - } -- // Inv: all action dependencies succeeded. -- -- // Were there list/parse/type errors that might prevent analysis? -- if !pkg.compiles && !analyzer.RunDespiteErrors { -- return nil, nil, fmt.Errorf("skipping analysis %q because package %q does not compile", analyzer.Name, pkg.m.ID) -- } -- // Inv: package is well-formed enough to proceed with analysis. - -- if false { // debugging -- log.Println("action.exec", act) +- var openPackageIDs []PackageID +- for id := range openPackages { +- openPackageIDs = append(openPackageIDs, id) - } - -- // Gather analysis Result values from horizontal dependencies. -- inputs := make(map[*analysis.Analyzer]interface{}) -- for _, dep := range act.hdeps { -- inputs[dep.a] = dep.result +- handles, err := s.getPackageHandles(ctx, openPackageIDs) +- if err != nil { +- return nil, err - } - -- // TODO(adonovan): opt: facts.Set works but it may be more -- // efficient to fork and tailor it to our precise needs. +- // Subtlety: we erase the upward cone of open packages from the shared import +- // graph, to increase reusability. - // -- // We've already sharded the fact encoding by action -- // so that it can be done in parallel. -- // We could eliminate locking. -- // We could also dovetail more closely with the export data -- // decoder to obtain a more compact representation of -- // packages and objects (e.g. its internal IDs, instead -- // of PkgPaths and objectpaths.) -- // More importantly, we should avoid re-export of -- // facts that related to objects that are discarded -- // by "deep" export data. Better still, use a "shallow" approach. -- -- // Read and decode analysis facts for each direct import. -- factset, err := pkg.factsDecoder.Decode(func(pkgPath string) ([]byte, error) { -- if !hasFacts { -- return nil, nil // analyzer doesn't use facts, so no vdeps +- // This is easiest to understand via an example: suppose A imports B, and B +- // imports C. Now suppose A and B are open. If we preserve the entire set of +- // shared deps by open packages, deps will be {B, C}. But this means that any +- // change to the open package B will invalidate the shared import graph, +- // meaning we will experience no benefit from sharing when B is edited. +- // Consider that this will be a common scenario, when A is foo_test and B is +- // foo. Better to just preserve the shared import C. +- // +- // With precise pruning, we may want to truncate this search based on +- // reachability. +- // +- // TODO(rfindley): this logic could use a unit test. +- volatileDeps := make(map[PackageID]bool) +- var isVolatile func(*packageHandle) bool +- isVolatile = func(ph *packageHandle) (volatile bool) { +- if v, ok := volatileDeps[ph.mp.ID]; ok { +- return v - } -- -- // Package.Imports() may contain a fake "C" package. Ignore it. -- if pkgPath == "C" { -- return nil, nil +- defer func() { +- volatileDeps[ph.mp.ID] = volatile +- }() +- if openPackages[ph.mp.ID] { +- return true - } -- -- id, ok := pkg.m.DepsByPkgPath[PackagePath(pkgPath)] -- if !ok { -- // This may mean imp was synthesized by the type -- // checker because it failed to import it for any reason -- // (e.g. bug processing export data; metadata ignoring -- // a cycle-forming import). -- // In that case, the fake package's imp.Path -- // is set to the failed importPath (and thus -- // it may lack a "vendor/" prefix). -- // -- // For now, silently ignore it on the assumption -- // that the error is already reported elsewhere. -- // return nil, fmt.Errorf("missing metadata") -- return nil, nil +- for _, dep := range ph.mp.DepsByPkgPath { +- if isVolatile(handles[dep]) { +- return true +- } - } +- return false +- } +- for _, dep := range handles { +- isVolatile(dep) +- } +- for id, volatile := range volatileDeps { +- if volatile { +- delete(handles, id) +- } +- } - -- vdep := act.vdeps[id] -- if vdep == nil { -- return nil, bug.Errorf("internal error in %s: missing vdep for id=%s", pkg.types.Path(), id) +- // We reuse the last import graph if and only if none of the dependencies +- // have changed. Doing better would involve analyzing dependencies to find +- // subgraphs that are still valid. Not worth it, especially when in the +- // common case nothing has changed. +- unchanged := lastImportGraph != nil && len(handles) == len(lastImportGraph.depKeys) +- var ids []PackageID +- depKeys := make(map[PackageID]file.Hash) +- for id, ph := range handles { +- ids = append(ids, id) +- depKeys[id] = ph.key +- if unchanged { +- prevKey, ok := lastImportGraph.depKeys[id] +- unchanged = ok && prevKey == ph.key - } +- } - -- return vdep.summary.Actions[act.stableName].Facts, nil -- }) -- if err != nil { -- return nil, nil, fmt.Errorf("internal error decoding analysis facts: %w", err) +- if unchanged { +- return lastImportGraph, nil - } - -- // TODO(adonovan): make Export*Fact panic rather than discarding -- // undeclared fact types, so that we discover bugs in analyzers. -- factFilter := make(map[reflect.Type]bool) -- for _, f := range analyzer.FactTypes { -- factFilter[reflect.TypeOf(f)] = true +- b, err := s.forEachPackageInternal(ctx, nil, ids, nil, nil, nil, handles) +- if err != nil { +- return nil, err - } - -- // posToLocation converts from token.Pos to protocol form. -- // TODO(adonovan): improve error messages. -- posToLocation := func(start, end token.Pos) (protocol.Location, error) { -- tokFile := pkg.fset.File(start) -- for _, p := range pkg.parsed { -- if p.Tok == tokFile { -- if end == token.NoPos { -- end = start -- } -- return p.PosLocation(start, end) -- } +- next := &importGraph{ +- fset: b.fset, +- depKeys: depKeys, +- imports: make(map[PackageID]pkgOrErr), +- } +- for id, fut := range b.importPackages { +- if fut.v.pkg == nil && fut.v.err == nil { +- panic(fmt.Sprintf("internal error: import node %s is not evaluated", id)) - } -- return protocol.Location{}, -- bug.Errorf("internal error: token.Pos not within package") +- next.imports[id] = fut.v - } +- return next, nil +-} - -- // Now run the (pkg, analyzer) action. -- var diagnostics []gobDiagnostic -- pass := &analysis.Pass{ -- Analyzer: analyzer, -- Fset: pkg.fset, -- Files: pkg.files, -- Pkg: pkg.types, -- TypesInfo: pkg.typesInfo, -- TypesSizes: pkg.typesSizes, -- TypeErrors: pkg.typeErrors, -- ResultOf: inputs, -- Report: func(d analysis.Diagnostic) { -- diagnostic, err := toGobDiagnostic(posToLocation, analyzer, d) -- if err != nil { -- bug.Reportf("internal error converting diagnostic from analyzer %q: %v", analyzer.Name, err) -- return -- } -- diagnostics = append(diagnostics, diagnostic) -- }, -- ImportObjectFact: factset.ImportObjectFact, -- ExportObjectFact: factset.ExportObjectFact, -- ImportPackageFact: factset.ImportPackageFact, -- ExportPackageFact: factset.ExportPackageFact, -- AllObjectFacts: func() []analysis.ObjectFact { return factset.AllObjectFacts(factFilter) }, -- AllPackageFacts: func() []analysis.PackageFact { return factset.AllPackageFacts(factFilter) }, -- } +-// An importGraph holds selected results of a type-checking pass, to be re-used +-// by subsequent snapshots. +-type importGraph struct { +- fset *token.FileSet // fileset used for type checking imports +- depKeys map[PackageID]file.Hash // hash of direct dependencies for this graph +- imports map[PackageID]pkgOrErr // results of type checking +-} - -- // Recover from panics (only) within the analyzer logic. -- // (Use an anonymous function to limit the recover scope.) -- var result interface{} -- func() { -- start := time.Now() -- defer func() { -- if r := recover(); r != nil { -- // An Analyzer panicked, likely due to a bug. -- // -- // In general we want to discover and fix such panics quickly, -- // so we don't suppress them, but some bugs in third-party -- // analyzers cannot be quickly fixed, so we use an allowlist -- // to suppress panics. -- const strict = true -- if strict && bug.PanicOnBugs && -- analyzer.Name != "buildir" { // see https://github.com/dominikh/go-tools/issues/1343 -- // Uncomment this when debugging suspected failures -- // in the driver, not the analyzer. -- if false { -- debug.SetTraceback("all") // show all goroutines -- } -- panic(r) -- } else { -- // In production, suppress the panic and press on. -- err = fmt.Errorf("analysis %s for package %s panicked: %v", analyzer.Name, pass.Pkg.Path(), r) -- } -- } +-// Package visiting functions used by forEachPackage; see the documentation of +-// forEachPackage for details. +-type ( +- preTypeCheck = func(int, *packageHandle) bool // false => don't type check +- postTypeCheck = func(int, *Package) +-) - -- // Accumulate running time for each checker. -- analyzerRunTimesMu.Lock() -- analyzerRunTimes[analyzer] += time.Since(start) -- analyzerRunTimesMu.Unlock() -- }() +-// forEachPackage does a pre- and post- order traversal of the packages +-// specified by ids using the provided pre and post functions. +-// +-// The pre func is optional. If set, pre is evaluated after the package +-// handle has been constructed, but before type-checking. If pre returns false, +-// type-checking is skipped for this package handle. +-// +-// post is called with a syntax package after type-checking completes +-// successfully. It is only called if pre returned true. +-// +-// Both pre and post may be called concurrently. +-func (s *Snapshot) forEachPackage(ctx context.Context, ids []PackageID, pre preTypeCheck, post postTypeCheck) error { +- ctx, done := event.Start(ctx, "cache.forEachPackage", tag.PackageCount.Of(len(ids))) +- defer done() - -- result, err = pass.Analyzer.Run(pass) -- }() +- if len(ids) == 0 { +- return nil // short cut: many call sites do not handle empty ids +- } +- +- handles, err := s.getPackageHandles(ctx, ids) - if err != nil { -- return nil, nil, err +- return err - } - -- if got, want := reflect.TypeOf(result), pass.Analyzer.ResultType; got != want { -- return nil, nil, bug.Errorf( -- "internal error: on package %s, analyzer %s returned a result of type %v, but declared ResultType %v", -- pass.Pkg.Path(), pass.Analyzer, got, want) +- impGraph := s.getImportGraph(ctx) +- _, err = s.forEachPackageInternal(ctx, impGraph, nil, ids, pre, post, handles) +- return err +-} +- +-// forEachPackageInternal is used by both forEachPackage and loadImportGraph to +-// type-check a graph of packages. +-// +-// If a non-nil importGraph is provided, imports in this graph will be reused. +-func (s *Snapshot) forEachPackageInternal(ctx context.Context, importGraph *importGraph, importIDs, syntaxIDs []PackageID, pre preTypeCheck, post postTypeCheck, handles map[PackageID]*packageHandle) (*typeCheckBatch, error) { +- b := &typeCheckBatch{ +- activePackageCache: s, +- pre: pre, +- post: post, +- handles: handles, +- parseCache: s.view.parseCache, +- fset: fileSetWithBase(reservedForParsing), +- syntaxIndex: make(map[PackageID]int), +- cpulimit: make(chan unit, runtime.GOMAXPROCS(0)), +- syntaxPackages: make(map[PackageID]*futurePackage), +- importPackages: make(map[PackageID]*futurePackage), - } - -- // Disallow Export*Fact calls after Run. -- // (A panic means the Analyzer is abusing concurrency.) -- pass.ExportObjectFact = func(obj types.Object, fact analysis.Fact) { -- panic(fmt.Sprintf("%v: Pass.ExportObjectFact(%s, %T) called after Run", act, obj, fact)) +- if importGraph != nil { +- // Clone the file set every time, to ensure we do not leak files. +- b.fset = tokeninternal.CloneFileSet(importGraph.fset) +- // Pre-populate future cache with 'done' futures. +- done := make(chan unit) +- close(done) +- for id, res := range importGraph.imports { +- b.importPackages[id] = &futurePackage{done, res} +- } +- } else { +- b.fset = fileSetWithBase(reservedForParsing) - } -- pass.ExportPackageFact = func(fact analysis.Fact) { -- panic(fmt.Sprintf("%v: Pass.ExportPackageFact(%T) called after Run", act, fact)) +- +- for i, id := range syntaxIDs { +- b.syntaxIndex[id] = i - } - -- factsdata := factset.Encode() -- return result, &actionSummary{ -- Diagnostics: diagnostics, -- Facts: factsdata, -- FactsHash: source.HashOf(factsdata), -- }, nil +- // Start a single goroutine for each requested package. +- // +- // Other packages are reached recursively, and will not be evaluated if they +- // are not needed. +- var g errgroup.Group +- for _, id := range importIDs { +- id := id +- g.Go(func() error { +- _, err := b.getImportPackage(ctx, id) +- return err +- }) +- } +- for i, id := range syntaxIDs { +- i := i +- id := id +- g.Go(func() error { +- _, err := b.handleSyntaxPackage(ctx, i, id) +- return err +- }) +- } +- return b, g.Wait() -} - --var ( -- analyzerRunTimesMu sync.Mutex -- analyzerRunTimes = make(map[*analysis.Analyzer]time.Duration) --) -- --type LabelDuration struct { -- Label string -- Duration time.Duration --} +-// TODO(rfindley): re-order the declarations below to read better from top-to-bottom. - --// AnalyzerTimes returns the accumulated time spent in each Analyzer's --// Run function since process start, in descending order. --func AnalyzerRunTimes() []LabelDuration { -- analyzerRunTimesMu.Lock() -- defer analyzerRunTimesMu.Unlock() +-// getImportPackage returns the *types.Package to use for importing the +-// package referenced by id. +-// +-// This may be the package produced by type-checking syntax (as in the case +-// where id is in the set of requested IDs), a package loaded from export data, +-// or a package type-checked for import only. +-func (b *typeCheckBatch) getImportPackage(ctx context.Context, id PackageID) (pkg *types.Package, err error) { +- b.mu.Lock() +- f, ok := b.importPackages[id] +- if ok { +- b.mu.Unlock() - -- slice := make([]LabelDuration, 0, len(analyzerRunTimes)) -- for a, t := range analyzerRunTimes { -- slice = append(slice, LabelDuration{Label: a.Name, Duration: t}) +- select { +- case <-ctx.Done(): +- return nil, ctx.Err() +- case <-f.done: +- return f.v.pkg, f.v.err +- } - } -- sort.Slice(slice, func(i, j int) bool { -- return slice[i].Duration > slice[j].Duration -- }) -- return slice --} - --// requiredAnalyzers returns the transitive closure of required analyzers in preorder. --func requiredAnalyzers(analyzers []*analysis.Analyzer) []*analysis.Analyzer { -- var result []*analysis.Analyzer -- seen := make(map[*analysis.Analyzer]bool) -- var visitAll func([]*analysis.Analyzer) -- visitAll = func(analyzers []*analysis.Analyzer) { -- for _, a := range analyzers { -- if !seen[a] { -- seen[a] = true -- result = append(result, a) -- visitAll(a.Requires) -- } +- f = &futurePackage{done: make(chan unit)} +- b.importPackages[id] = f +- b.mu.Unlock() +- +- defer func() { +- f.v = pkgOrErr{pkg, err} +- close(f.done) +- }() +- +- if index, ok := b.syntaxIndex[id]; ok { +- pkg, err := b.handleSyntaxPackage(ctx, index, id) +- if err != nil { +- return nil, err +- } +- if pkg != nil { +- return pkg, nil - } +- // type-checking was short-circuited by the pre- func. - } -- visitAll(analyzers) -- return result --} - --var analyzeSummaryCodec = frob.CodecFor[*analyzeSummary]() +- // unsafe cannot be imported or type-checked. +- if id == "unsafe" { +- return types.Unsafe, nil +- } - --// -- data types for serialization of analysis.Diagnostic and source.Diagnostic -- +- ph := b.handles[id] - --// (The name says gob but we use frob.) --var diagnosticsCodec = frob.CodecFor[[]gobDiagnostic]() +- // Do a second check for "unsafe" defensively, due to golang/go#60890. +- if ph.mp.PkgPath == "unsafe" { +- bug.Reportf("encountered \"unsafe\" as %s (golang/go#60890)", id) +- return types.Unsafe, nil +- } - --type gobDiagnostic struct { -- Location protocol.Location -- Severity protocol.DiagnosticSeverity -- Code string -- CodeHref string -- Source string -- Message string -- SuggestedFixes []gobSuggestedFix -- Related []gobRelatedInformation -- Tags []protocol.DiagnosticTag +- data, err := filecache.Get(exportDataKind, ph.key) +- if err == filecache.ErrNotFound { +- // No cached export data: type-check as fast as possible. +- return b.checkPackageForImport(ctx, ph) +- } +- if err != nil { +- return nil, fmt.Errorf("failed to read cache data for %s: %v", ph.mp.ID, err) +- } +- return b.importPackage(ctx, ph.mp, data) -} - --type gobRelatedInformation struct { -- Location protocol.Location -- Message string --} +-// handleSyntaxPackage handles one package from the ids slice. +-// +-// If type checking occurred while handling the package, it returns the +-// resulting types.Package so that it may be used for importing. +-// +-// handleSyntaxPackage returns (nil, nil) if pre returned false. +-func (b *typeCheckBatch) handleSyntaxPackage(ctx context.Context, i int, id PackageID) (pkg *types.Package, err error) { +- b.mu.Lock() +- f, ok := b.syntaxPackages[id] +- if ok { +- b.mu.Unlock() +- <-f.done +- return f.v.pkg, f.v.err +- } - --type gobSuggestedFix struct { -- Message string -- TextEdits []gobTextEdit -- Command *gobCommand -- ActionKind protocol.CodeActionKind --} +- f = &futurePackage{done: make(chan unit)} +- b.syntaxPackages[id] = f +- b.mu.Unlock() +- defer func() { +- f.v = pkgOrErr{pkg, err} +- close(f.done) +- }() - --type gobCommand struct { -- Title string -- Command string -- Arguments []json.RawMessage --} +- ph := b.handles[id] +- if b.pre != nil && !b.pre(i, ph) { +- return nil, nil // skip: export data only +- } - --type gobTextEdit struct { -- Location protocol.Location -- NewText []byte --} +- // Check for existing active packages. +- // +- // Since gopls can't depend on package identity, any instance of the +- // requested package must be ok to return. +- // +- // This is an optimization to avoid redundant type-checking: following +- // changes to an open package many LSP clients send several successive +- // requests for package information for the modified package (semantic +- // tokens, code lens, inlay hints, etc.) +- if pkg := b.activePackageCache.getActivePackage(id); pkg != nil { +- b.post(i, pkg) +- return nil, nil // skip: not checked in this batch +- } - --// toGobDiagnostic converts an analysis.Diagnosic to a serializable gobDiagnostic, --// which requires expanding token.Pos positions into protocol.Location form. --func toGobDiagnostic(posToLocation func(start, end token.Pos) (protocol.Location, error), a *analysis.Analyzer, diag analysis.Diagnostic) (gobDiagnostic, error) { -- var fixes []gobSuggestedFix -- for _, fix := range diag.SuggestedFixes { -- var gobEdits []gobTextEdit -- for _, textEdit := range fix.TextEdits { -- loc, err := posToLocation(textEdit.Pos, textEdit.End) -- if err != nil { -- return gobDiagnostic{}, fmt.Errorf("in SuggestedFixes: %w", err) -- } -- gobEdits = append(gobEdits, gobTextEdit{ -- Location: loc, -- NewText: textEdit.NewText, +- // Wait for predecessors. +- { +- var g errgroup.Group +- for _, depID := range ph.mp.DepsByPkgPath { +- depID := depID +- g.Go(func() error { +- _, err := b.getImportPackage(ctx, depID) +- return err - }) - } -- fixes = append(fixes, gobSuggestedFix{ -- Message: fix.Message, -- TextEdits: gobEdits, -- }) +- if err := g.Wait(); err != nil { +- // Failure to import a package should not abort the whole operation. +- // Stop only if the context was cancelled, a likely cause. +- // Import errors will be reported as type diagnostics. +- if ctx.Err() != nil { +- return nil, ctx.Err() +- } +- } - } - -- var related []gobRelatedInformation -- for _, r := range diag.Related { -- loc, err := posToLocation(r.Pos, r.End) -- if err != nil { -- return gobDiagnostic{}, fmt.Errorf("in Related: %w", err) -- } -- related = append(related, gobRelatedInformation{ -- Location: loc, -- Message: r.Message, -- }) +- // Wait to acquire a CPU token. +- // +- // Note: it is important to acquire this token only after awaiting +- // predecessors, to avoid starvation. +- select { +- case <-ctx.Done(): +- return nil, ctx.Err() +- case b.cpulimit <- unit{}: +- defer func() { +- <-b.cpulimit // release CPU token +- }() - } - -- loc, err := posToLocation(diag.Pos, diag.End) +- // Compute the syntax package. +- p, err := b.checkPackage(ctx, ph) - if err != nil { -- return gobDiagnostic{}, err +- return nil, err - } - -- // The Code column of VSCode's Problems table renders this -- // information as "Source(Code)" where code is a link to CodeHref. -- // (The code field must be nonempty for anything to appear.) -- diagURL := effectiveURL(a, diag) -- code := "default" -- if diag.Category != "" { -- code = diag.Category -- } +- // Update caches. +- b.activePackageCache.setActivePackage(id, p) // store active packages in memory +- go storePackageResults(ctx, ph, p) // ...and write all packages to disk - -- return gobDiagnostic{ -- Location: loc, -- // Severity for analysis diagnostics is dynamic, -- // based on user configuration per analyzer. -- Code: code, -- CodeHref: diagURL, -- Source: a.Name, -- Message: diag.Message, -- SuggestedFixes: fixes, -- Related: related, -- // Analysis diagnostics do not contain tags. -- }, nil +- b.post(i, p) +- +- return p.pkg.types, nil -} - --// effectiveURL computes the effective URL of diag, --// using the algorithm specified at Diagnostic.URL. --func effectiveURL(a *analysis.Analyzer, diag analysis.Diagnostic) string { -- u := diag.URL -- if u == "" && diag.Category != "" { -- u = "#" + diag.Category +-// storePackageResults serializes and writes information derived from p to the +-// file cache. +-// The context is used only for logging; cancellation does not affect the operation. +-func storePackageResults(ctx context.Context, ph *packageHandle, p *Package) { +- toCache := map[string][]byte{ +- xrefsKind: p.pkg.xrefs(), +- methodSetsKind: p.pkg.methodsets().Encode(), +- diagnosticsKind: encodeDiagnostics(p.pkg.diagnostics), - } -- if base, err := urlpkg.Parse(a.URL); err == nil { -- if rel, err := urlpkg.Parse(u); err == nil { -- u = base.ResolveReference(rel).String() +- +- if p.metadata.PkgPath != "unsafe" { // unsafe cannot be exported +- exportData, err := gcimporter.IExportShallow(p.pkg.fset, p.pkg.types, bug.Reportf) +- if err != nil { +- bug.Reportf("exporting package %v: %v", p.metadata.ID, err) +- } else { +- toCache[exportDataKind] = exportData - } +- } else if p.metadata.ID != "unsafe" { +- // golang/go#60890: we should only ever see one variant of the "unsafe" +- // package. +- bug.Reportf("encountered \"unsafe\" as %s (golang/go#60890)", p.metadata.ID) - } -- return u --} - --// stableName returns a name for the analyzer that is unique and --// stable across address spaces. --// --// Analyzer names are not unique. For example, gopls includes --// both x/tools/passes/nilness and staticcheck/nilness. --// For serialization, we must assign each analyzer a unique identifier --// that two gopls processes accessing the cache can agree on. --func stableName(a *analysis.Analyzer) string { -- // Incorporate the file and line of the analyzer's Run function. -- addr := reflect.ValueOf(a.Run).Pointer() -- fn := runtime.FuncForPC(addr) -- file, line := fn.FileLine(addr) -- -- // It is tempting to use just a.Name as the stable name when -- // it is unique, but making them always differ helps avoid -- // name/stablename confusion. -- return fmt.Sprintf("%s(%s:%d)", a.Name, filepath.Base(file), line) +- for kind, data := range toCache { +- if err := filecache.Set(kind, ph.key, data); err != nil { +- event.Error(ctx, fmt.Sprintf("storing %s data for %s", kind, ph.mp.ID), err) +- } +- } -} -diff -urN a/gopls/internal/lsp/cache/cache.go b/gopls/internal/lsp/cache/cache.go ---- a/gopls/internal/lsp/cache/cache.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/cache.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,80 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package cache +-// importPackage loads the given package from its export data in p.exportData +-// (which must already be populated). +-func (b *typeCheckBatch) importPackage(ctx context.Context, mp *metadata.Package, data []byte) (*types.Package, error) { +- ctx, done := event.Start(ctx, "cache.typeCheckBatch.importPackage", tag.Package.Of(string(mp.ID))) +- defer done() - --import ( -- "context" -- "reflect" -- "strconv" -- "sync/atomic" -- "time" +- impMap := b.importMap(mp.ID) - -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/gocommand" -- "golang.org/x/tools/internal/memoize" -- "golang.org/x/tools/internal/robustio" --) +- thisPackage := types.NewPackage(string(mp.PkgPath), string(mp.Name)) +- getPackages := func(items []gcimporter.GetPackagesItem) error { +- for i, item := range items { +- var id PackageID +- var pkg *types.Package +- if item.Path == string(mp.PkgPath) { +- id = mp.ID +- pkg = thisPackage - --// New Creates a new cache for gopls operation results, using the given file --// set, shared store, and session options. --// --// Both the fset and store may be nil, but if store is non-nil so must be fset --// (and they must always be used together), otherwise it may be possible to get --// cached data referencing token.Pos values not mapped by the FileSet. --func New(store *memoize.Store) *Cache { -- index := atomic.AddInt64(&cacheIndex, 1) +- // debugging issues #60904, #64235 +- if pkg.Name() != item.Name { +- // This would mean that mp.Name != item.Name, so the +- // manifest in the export data of mp.PkgPath is +- // inconsistent with mp.Name. Or perhaps there +- // are duplicate PkgPath items in the manifest? +- return bug.Errorf("internal error: package name is %q, want %q (id=%q, path=%q) (see issue #60904)", +- pkg.Name(), item.Name, id, item.Path) +- } +- } else { +- id = impMap[item.Path] +- var err error +- pkg, err = b.getImportPackage(ctx, id) +- if err != nil { +- return err +- } - -- if store == nil { -- store = &memoize.Store{} -- } +- // We intentionally duplicate the bug.Errorf calls because +- // telemetry tells us only the program counter, not the message. +- +- // debugging issues #60904, #64235 +- if pkg.Name() != item.Name { +- // This means that, while reading the manifest of the +- // export data of mp.PkgPath, one of its indirect +- // dependencies had a name that differs from the +- // Metadata.Name +- return bug.Errorf("internal error: package name is %q, want %q (id=%q, path=%q) (see issue #60904)", +- pkg.Name(), item.Name, id, item.Path) +- } +- } +- items[i].Pkg = pkg - -- c := &Cache{ -- id: strconv.FormatInt(index, 10), -- store: store, -- memoizedFS: &memoizedFS{filesByID: map[robustio.FileID][]*DiskFile{}}, +- } +- return nil - } -- return c --} - --// A Cache holds caching stores that are bundled together for consistency. --// --// TODO(rfindley): once fset and store need not be bundled together, the Cache --// type can be eliminated. --type Cache struct { -- id string -- -- store *memoize.Store -- -- *memoizedFS // implements source.FileSource --} +- // Importing is potentially expensive, and might not encounter cancellations +- // via dependencies (e.g. if they have already been evaluated). +- if ctx.Err() != nil { +- return nil, ctx.Err() +- } - --// NewSession creates a new gopls session with the given cache and options overrides. --// --// The provided optionsOverrides may be nil. --// --// TODO(rfindley): move this to session.go. --func NewSession(ctx context.Context, c *Cache) *Session { -- index := atomic.AddInt64(&sessionIndex, 1) -- s := &Session{ -- id: strconv.FormatInt(index, 10), -- cache: c, -- gocmdRunner: &gocommand.Runner{}, -- overlayFS: newOverlayFS(c), -- parseCache: newParseCache(1 * time.Minute), // keep recently parsed files for a minute, to optimize typing CPU +- imported, err := gcimporter.IImportShallow(b.fset, getPackages, data, string(mp.PkgPath), bug.Reportf) +- if err != nil { +- return nil, fmt.Errorf("import failed for %q: %v", mp.ID, err) - } -- event.Log(ctx, "New session", KeyCreateSession.Of(s)) -- return s +- return imported, nil -} - --var cacheIndex, sessionIndex, viewIndex int64 +-// checkPackageForImport type checks, but skips function bodies and does not +-// record syntax information. +-func (b *typeCheckBatch) checkPackageForImport(ctx context.Context, ph *packageHandle) (*types.Package, error) { +- ctx, done := event.Start(ctx, "cache.typeCheckBatch.checkPackageForImport", tag.Package.Of(string(ph.mp.ID))) +- defer done() - --func (c *Cache) ID() string { return c.id } --func (c *Cache) MemStats() map[reflect.Type]int { return c.store.Stats() } +- onError := func(e error) { +- // Ignore errors for exporting. +- } +- cfg := b.typesConfig(ctx, ph.localInputs, onError) +- cfg.IgnoreFuncBodies = true - --// FileStats returns information about the set of files stored in the cache. --// It is intended for debugging only. --func (c *Cache) FileStats() (files, largest, errs int) { -- return c.fileStats() --} -diff -urN a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.go ---- a/gopls/internal/lsp/cache/check.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/check.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,1864 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package cache -- --import ( -- "context" -- "crypto/sha256" -- "fmt" -- "go/ast" -- "go/parser" -- "go/token" -- "go/types" -- "regexp" -- "runtime" -- "sort" -- "strings" -- "sync" -- "sync/atomic" +- // Parse the compiled go files, bypassing the parse cache as packages checked +- // for import are unlikely to get cache hits. Additionally, we can optimize +- // parsing slightly by not passing parser.ParseComments. +- pgfs := make([]*parsego.File, len(ph.localInputs.compiledGoFiles)) +- { +- var group errgroup.Group +- // Set an arbitrary concurrency limit; we want some parallelism but don't +- // need GOMAXPROCS, as there is already a lot of concurrency among calls to +- // checkPackageForImport. +- // +- // TODO(rfindley): is there a better way to limit parallelism here? We could +- // have a global limit on the type-check batch, but would have to be very +- // careful to avoid starvation. +- group.SetLimit(4) +- for i, fh := range ph.localInputs.compiledGoFiles { +- i, fh := i, fh +- group.Go(func() error { +- pgf, err := parseGoImpl(ctx, b.fset, fh, parser.SkipObjectResolution, false) +- pgfs[i] = pgf +- return err +- }) +- } +- if err := group.Wait(); err != nil { +- return nil, err // cancelled, or catastrophic error (e.g. missing file) +- } +- } +- pkg := types.NewPackage(string(ph.localInputs.pkgPath), string(ph.localInputs.name)) +- check := types.NewChecker(cfg, b.fset, pkg, nil) - -- "golang.org/x/mod/module" -- "golang.org/x/sync/errgroup" -- "golang.org/x/tools/go/ast/astutil" -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/filecache" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/lsp/source/typerefs" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/tag" -- "golang.org/x/tools/internal/gcimporter" -- "golang.org/x/tools/internal/packagesinternal" -- "golang.org/x/tools/internal/tokeninternal" -- "golang.org/x/tools/internal/typeparams" -- "golang.org/x/tools/internal/typesinternal" --) +- files := make([]*ast.File, len(pgfs)) +- for i, pgf := range pgfs { +- files[i] = pgf.File +- } - --// Various optimizations that should not affect correctness. --const ( -- preserveImportGraph = true // hold on to the import graph for open packages --) +- // Type checking is expensive, and we may not have encountered cancellations +- // via parsing (e.g. if we got nothing but cache hits for parsed files). +- if ctx.Err() != nil { +- return nil, ctx.Err() +- } - --type unit = struct{} +- _ = check.Files(files) // ignore errors - --// A typeCheckBatch holds data for a logical type-checking operation, which may --// type-check many unrelated packages. --// --// It shares state such as parsed files and imports, to optimize type-checking --// for packages with overlapping dependency graphs. --type typeCheckBatch struct { -- activePackageCache interface { -- getActivePackage(id PackageID) *Package -- setActivePackage(id PackageID, pkg *Package) +- // If the context was cancelled, we may have returned a ton of transient +- // errors to the type checker. Swallow them. +- if ctx.Err() != nil { +- return nil, ctx.Err() - } -- syntaxIndex map[PackageID]int // requested ID -> index in ids -- pre preTypeCheck -- post postTypeCheck -- handles map[PackageID]*packageHandle -- parseCache *parseCache -- fset *token.FileSet // describes all parsed or imported files -- cpulimit chan unit // concurrency limiter for CPU-bound operations -- -- mu sync.Mutex -- syntaxPackages map[PackageID]*futurePackage // results of processing a requested package; may hold (nil, nil) -- importPackages map[PackageID]*futurePackage // package results to use for importing --} - --// A futurePackage is a future result of type checking or importing a package, --// to be cached in a map. --// --// The goroutine that creates the futurePackage is responsible for evaluating --// its value, and closing the done channel. --type futurePackage struct { -- done chan unit -- v pkgOrErr +- // Asynchronously record export data. +- go func() { +- exportData, err := gcimporter.IExportShallow(b.fset, pkg, bug.Reportf) +- if err != nil { +- bug.Reportf("exporting package %v: %v", ph.mp.ID, err) +- return +- } +- if err := filecache.Set(exportDataKind, ph.key, exportData); err != nil { +- event.Error(ctx, fmt.Sprintf("storing export data for %s", ph.mp.ID), err) +- } +- }() +- return pkg, nil -} - --type pkgOrErr struct { -- pkg *types.Package -- err error +-// importMap returns the map of package path -> package ID relative to the +-// specified ID. +-func (b *typeCheckBatch) importMap(id PackageID) map[string]PackageID { +- impMap := make(map[string]PackageID) +- var populateDeps func(*metadata.Package) +- populateDeps = func(parent *metadata.Package) { +- for _, id := range parent.DepsByPkgPath { +- mp := b.handles[id].mp +- if prevID, ok := impMap[string(mp.PkgPath)]; ok { +- // debugging #63822 +- if prevID != mp.ID { +- bug.Reportf("inconsistent view of dependencies") +- } +- continue +- } +- impMap[string(mp.PkgPath)] = mp.ID +- populateDeps(mp) +- } +- } +- mp := b.handles[id].mp +- populateDeps(mp) +- return impMap -} - --// TypeCheck type-checks the specified packages. +-// A packageHandle holds inputs required to compute a Package, including +-// metadata, derived diagnostics, files, and settings. Additionally, +-// packageHandles manage a key for these inputs, to use in looking up +-// precomputed results. -// --// The resulting packages slice always contains len(ids) entries, though some --// of them may be nil if (and only if) the resulting error is non-nil. +-// packageHandles may be invalid following an invalidation via snapshot.clone, +-// but the handles returned by getPackageHandles will always be valid. -// --// An error is returned if any of the requested packages fail to type-check. --// This is different from having type-checking errors: a failure to type-check --// indicates context cancellation or otherwise significant failure to perform --// the type-checking operation. --func (s *snapshot) TypeCheck(ctx context.Context, ids ...PackageID) ([]source.Package, error) { -- pkgs := make([]source.Package, len(ids)) -- -- var ( -- needIDs []PackageID // ids to type-check -- indexes []int // original index of requested ids -- ) +-// packageHandles are critical for implementing "precise pruning" in gopls: +-// packageHandle.key is a hash of a precise set of inputs, such as package +-// files and "reachable" syntax, that may affect type checking. +-// +-// packageHandles also keep track of state that allows gopls to compute, and +-// then quickly recompute, these keys. This state is split into two categories: +-// - local state, which depends only on the package's local files and metadata +-// - other state, which includes data derived from dependencies. +-// +-// Dividing the data in this way allows gopls to minimize invalidation when a +-// package is modified. For example, any change to a package file fully +-// invalidates the package handle. On the other hand, if that change was not +-// metadata-affecting it may be the case that packages indirectly depending on +-// the modified package are unaffected by the change. For that reason, we have +-// two types of invalidation, corresponding to the two types of data above: +-// - deletion of the handle, which occurs when the package itself changes +-// - clearing of the validated field, which marks the package as possibly +-// invalid. +-// +-// With the second type of invalidation, packageHandles are re-evaluated from the +-// bottom up. If this process encounters a packageHandle whose deps have not +-// changed (as detected by the depkeys field), then the packageHandle in +-// question must also not have changed, and we need not re-evaluate its key. +-type packageHandle struct { +- mp *metadata.Package +- +- // loadDiagnostics memoizes the result of processing error messages from +- // go/packages (i.e. `go list`). +- // +- // These are derived from metadata using a snapshot. Since they depend on +- // file contents (for translating positions), they should theoretically be +- // invalidated by file changes, but historically haven't been. In practice +- // they are rare and indicate a fundamental error that needs to be corrected +- // before development can continue, so it may not be worth significant +- // engineering effort to implement accurate invalidation here. +- // +- // TODO(rfindley): loadDiagnostics are out of place here, as they don't +- // directly relate to type checking. We should perhaps move the caching of +- // load diagnostics to an entirely separate component, so that Packages need +- // only be concerned with parsing and type checking. +- // (Nevertheless, since the lifetime of load diagnostics matches that of the +- // Metadata, it is convenient to memoize them here.) +- loadDiagnostics []*Diagnostic - -- // Check for existing active packages, as any package will do. -- // -- // This is also done inside forEachPackage, but doing it here avoids -- // unnecessary set up for type checking (e.g. assembling the package handle -- // graph). -- for i, id := range ids { -- if pkg := s.getActivePackage(id); pkg != nil { -- pkgs[i] = pkg -- } else { -- needIDs = append(needIDs, id) -- indexes = append(indexes, i) -- } -- } +- // Local data: - -- post := func(i int, pkg *Package) { -- pkgs[indexes[i]] = pkg -- } -- return pkgs, s.forEachPackage(ctx, needIDs, nil, post) --} +- // localInputs holds all local type-checking localInputs, excluding +- // dependencies. +- localInputs typeCheckInputs +- // localKey is a hash of localInputs. +- localKey file.Hash +- // refs is the result of syntactic dependency analysis produced by the +- // typerefs package. +- refs map[string][]typerefs.Symbol - --// getImportGraph returns a shared import graph use for this snapshot, or nil. --// --// This is purely an optimization: holding on to more imports allows trading --// memory for CPU and latency. Currently, getImportGraph returns an import --// graph containing all packages imported by open packages, since these are --// highly likely to be needed when packages change. --// --// Furthermore, since we memoize active packages, including their imports in --// the shared import graph means we don't run the risk of pinning duplicate --// copies of common imports, if active packages are computed in separate type --// checking batches. --func (s *snapshot) getImportGraph(ctx context.Context) *importGraph { -- if !preserveImportGraph { -- return nil -- } -- s.mu.Lock() +- // Data derived from dependencies: - -- // Evaluate the shared import graph for the snapshot. There are three major -- // codepaths here: +- // validated indicates whether the current packageHandle is known to have a +- // valid key. Invalidated package handles are stored for packages whose +- // type information may have changed. +- validated bool +- // depKeys records the key of each dependency that was used to calculate the +- // key above. If the handle becomes invalid, we must re-check that each still +- // matches. +- depKeys map[PackageID]file.Hash +- // key is the hashed key for the package. - // -- // 1. importGraphDone == nil, importGraph == nil: it is this goroutine's -- // responsibility to type-check the shared import graph. -- // 2. importGraphDone == nil, importGraph != nil: it is this goroutine's -- // responsibility to resolve the import graph, which may result in -- // type-checking only if the existing importGraph (carried over from the -- // preceding snapshot) is invalid. -- // 3. importGraphDone != nil: some other goroutine is doing (1) or (2), wait -- // for the work to be done. -- done := s.importGraphDone -- if done == nil { -- done = make(chan unit) -- s.importGraphDone = done -- release := s.Acquire() // must acquire to use the snapshot asynchronously -- go func() { -- defer release() -- importGraph, err := s.resolveImportGraph() // may be nil -- if err != nil { -- if ctx.Err() == nil { -- event.Error(ctx, "computing the shared import graph", err) -- } -- importGraph = nil -- } -- s.mu.Lock() -- s.importGraph = importGraph -- s.mu.Unlock() -- close(done) -- }() -- } -- s.mu.Unlock() +- // It includes the all bits of the transitive closure of +- // dependencies's sources. +- key file.Hash +-} - -- select { -- case <-done: -- return s.importGraph -- case <-ctx.Done(): -- return nil -- } +-// clone returns a copy of the receiver with the validated bit set to the +-// provided value. +-func (ph *packageHandle) clone(validated bool) *packageHandle { +- copy := *ph +- copy.validated = validated +- return © -} - --// resolveImportGraph evaluates the shared import graph to use for --// type-checking in this snapshot. This may involve re-using the import graph --// of the previous snapshot (stored in s.importGraph), or computing a fresh --// import graph. --// --// resolveImportGraph should only be called from getImportGraph. --func (s *snapshot) resolveImportGraph() (*importGraph, error) { -- ctx := s.backgroundCtx -- ctx, done := event.Start(event.Detach(ctx), "cache.resolveImportGraph") -- defer done() +-// getPackageHandles gets package handles for all given ids and their +-// dependencies, recursively. +-func (s *Snapshot) getPackageHandles(ctx context.Context, ids []PackageID) (map[PackageID]*packageHandle, error) { +- // perform a two-pass traversal. +- // +- // On the first pass, build up a bidirectional graph of handle nodes, and collect leaves. +- // Then build package handles from bottom up. - -- s.mu.Lock() -- lastImportGraph := s.importGraph -- s.mu.Unlock() +- s.mu.Lock() // guard s.meta and s.packages below +- b := &packageHandleBuilder{ +- s: s, +- transitiveRefs: make(map[typerefs.IndexID]*partialRefs), +- nodes: make(map[typerefs.IndexID]*handleNode), +- } - -- openPackages := make(map[PackageID]bool) -- for _, fh := range s.overlays() { -- meta, err := s.MetadataForFile(ctx, fh.URI()) -- if err != nil { -- return nil, err +- var leaves []*handleNode +- var makeNode func(*handleNode, PackageID) *handleNode +- makeNode = func(from *handleNode, id PackageID) *handleNode { +- idxID := b.s.pkgIndex.IndexID(id) +- n, ok := b.nodes[idxID] +- if !ok { +- mp := s.meta.Packages[id] +- if mp == nil { +- panic(fmt.Sprintf("nil metadata for %q", id)) +- } +- n = &handleNode{ +- mp: mp, +- idxID: idxID, +- unfinishedSuccs: int32(len(mp.DepsByPkgPath)), +- } +- if entry, hit := b.s.packages.Get(mp.ID); hit { +- n.ph = entry +- } +- if n.unfinishedSuccs == 0 { +- leaves = append(leaves, n) +- } else { +- n.succs = make(map[PackageID]*handleNode, n.unfinishedSuccs) +- } +- b.nodes[idxID] = n +- for _, depID := range mp.DepsByPkgPath { +- n.succs[depID] = makeNode(n, depID) +- } - } -- source.RemoveIntermediateTestVariants(&meta) -- for _, m := range meta { -- openPackages[m.ID] = true +- // Add edge from predecessor. +- if from != nil { +- n.preds = append(n.preds, from) - } +- return n - } -- -- var openPackageIDs []source.PackageID -- for id := range openPackages { -- openPackageIDs = append(openPackageIDs, id) +- for _, id := range ids { +- makeNode(nil, id) - } +- s.mu.Unlock() - -- handles, err := s.getPackageHandles(ctx, openPackageIDs) -- if err != nil { -- return nil, err -- } +- g, ctx := errgroup.WithContext(ctx) - -- // Subtlety: we erase the upward cone of open packages from the shared import -- // graph, to increase reusability. -- // -- // This is easiest to understand via an example: suppose A imports B, and B -- // imports C. Now suppose A and B are open. If we preserve the entire set of -- // shared deps by open packages, deps will be {B, C}. But this means that any -- // change to the open package B will invalidate the shared import graph, -- // meaning we will experience no benefit from sharing when B is edited. -- // Consider that this will be a common scenario, when A is foo_test and B is -- // foo. Better to just preserve the shared import C. -- // -- // With precise pruning, we may want to truncate this search based on -- // reachability. +- // files are preloaded, so building package handles is CPU-bound. - // -- // TODO(rfindley): this logic could use a unit test. -- volatileDeps := make(map[PackageID]bool) -- var isVolatile func(*packageHandle) bool -- isVolatile = func(ph *packageHandle) (volatile bool) { -- if v, ok := volatileDeps[ph.m.ID]; ok { -- return v -- } -- defer func() { -- volatileDeps[ph.m.ID] = volatile -- }() -- if openPackages[ph.m.ID] { -- return true -- } -- for _, dep := range ph.m.DepsByPkgPath { -- if isVolatile(handles[dep]) { -- return true +- // Note that we can't use g.SetLimit, as that could result in starvation: +- // g.Go blocks until a slot is available, and so all existing goroutines +- // could be blocked trying to enqueue a predecessor. +- limiter := make(chan unit, runtime.GOMAXPROCS(0)) +- +- var enqueue func(*handleNode) +- enqueue = func(n *handleNode) { +- g.Go(func() error { +- limiter <- unit{} +- defer func() { <-limiter }() +- +- if ctx.Err() != nil { +- return ctx.Err() - } -- } -- return false -- } -- for _, dep := range handles { -- isVolatile(dep) -- } -- for id, volatile := range volatileDeps { -- if volatile { -- delete(handles, id) -- } -- } - -- // We reuse the last import graph if and only if none of the dependencies -- // have changed. Doing better would involve analyzing dependencies to find -- // subgraphs that are still valid. Not worth it, especially when in the -- // common case nothing has changed. -- unchanged := lastImportGraph != nil && len(handles) == len(lastImportGraph.depKeys) -- var ids []PackageID -- depKeys := make(map[PackageID]source.Hash) -- for id, ph := range handles { -- ids = append(ids, id) -- depKeys[id] = ph.key -- if unchanged { -- prevKey, ok := lastImportGraph.depKeys[id] -- unchanged = ok && prevKey == ph.key -- } -- } +- b.buildPackageHandle(ctx, n) - -- if unchanged { -- return lastImportGraph, nil +- for _, pred := range n.preds { +- if atomic.AddInt32(&pred.unfinishedSuccs, -1) == 0 { +- enqueue(pred) +- } +- } +- +- return n.err +- }) +- } +- for _, leaf := range leaves { +- enqueue(leaf) - } - -- b, err := s.forEachPackageInternal(ctx, nil, ids, nil, nil, nil, handles) -- if err != nil { +- if err := g.Wait(); err != nil { - return nil, err - } - -- next := &importGraph{ -- fset: b.fset, -- depKeys: depKeys, -- imports: make(map[PackageID]pkgOrErr), -- } -- for id, fut := range b.importPackages { -- if fut.v.pkg == nil && fut.v.err == nil { -- panic(fmt.Sprintf("internal error: import node %s is not evaluated", id)) -- } -- next.imports[id] = fut.v +- // Copy handles into the result map. +- handles := make(map[PackageID]*packageHandle, len(b.nodes)) +- for _, v := range b.nodes { +- assert(v.ph != nil, "nil handle") +- handles[v.mp.ID] = v.ph - } -- return next, nil --} - --// An importGraph holds selected results of a type-checking pass, to be re-used --// by subsequent snapshots. --type importGraph struct { -- fset *token.FileSet // fileset used for type checking imports -- depKeys map[PackageID]source.Hash // hash of direct dependencies for this graph -- imports map[PackageID]pkgOrErr // results of type checking +- return handles, nil -} - --// Package visiting functions used by forEachPackage; see the documentation of --// forEachPackage for details. --type ( -- preTypeCheck = func(int, *packageHandle) bool // false => don't type check -- postTypeCheck = func(int, *Package) --) +-// A packageHandleBuilder computes a batch of packageHandles concurrently, +-// sharing computed transitive reachability sets used to compute package keys. +-type packageHandleBuilder struct { +- s *Snapshot - --// forEachPackage does a pre- and post- order traversal of the packages --// specified by ids using the provided pre and post functions. +- // nodes are assembled synchronously. +- nodes map[typerefs.IndexID]*handleNode +- +- // transitiveRefs is incrementally evaluated as package handles are built. +- transitiveRefsMu sync.Mutex +- transitiveRefs map[typerefs.IndexID]*partialRefs // see getTransitiveRefs +-} +- +-// A handleNode represents a to-be-computed packageHandle within a graph of +-// predecessors and successors. -// --// The pre func is optional. If set, pre is evaluated after the package --// handle has been constructed, but before type-checking. If pre returns false, --// type-checking is skipped for this package handle. +-// It is used to implement a bottom-up construction of packageHandles. +-type handleNode struct { +- mp *metadata.Package +- idxID typerefs.IndexID +- ph *packageHandle +- err error +- preds []*handleNode +- succs map[PackageID]*handleNode +- unfinishedSuccs int32 +-} +- +-// partialRefs maps names declared by a given package to their set of +-// transitive references. -// --// post is called with a syntax package after type-checking completes --// successfully. It is only called if pre returned true. +-// If complete is set, refs is known to be complete for the package in +-// question. Otherwise, it may only map a subset of all names declared by the +-// package. +-type partialRefs struct { +- refs map[string]*typerefs.PackageSet +- complete bool +-} +- +-// getTransitiveRefs gets or computes the set of transitively reachable +-// packages for each exported name in the package specified by id. -// --// Both pre and post may be called concurrently. --func (s *snapshot) forEachPackage(ctx context.Context, ids []PackageID, pre preTypeCheck, post postTypeCheck) error { -- ctx, done := event.Start(ctx, "cache.forEachPackage", tag.PackageCount.Of(len(ids))) -- defer done() +-// The operation may fail if building a predecessor failed. If and only if this +-// occurs, the result will be nil. +-func (b *packageHandleBuilder) getTransitiveRefs(pkgID PackageID) map[string]*typerefs.PackageSet { +- b.transitiveRefsMu.Lock() +- defer b.transitiveRefsMu.Unlock() - -- if len(ids) == 0 { -- return nil // short cut: many call sites do not handle empty ids +- idxID := b.s.pkgIndex.IndexID(pkgID) +- trefs, ok := b.transitiveRefs[idxID] +- if !ok { +- trefs = &partialRefs{ +- refs: make(map[string]*typerefs.PackageSet), +- } +- b.transitiveRefs[idxID] = trefs - } - -- handles, err := s.getPackageHandles(ctx, ids) -- if err != nil { -- return err +- if !trefs.complete { +- trefs.complete = true +- ph := b.nodes[idxID].ph +- for name := range ph.refs { +- if ('A' <= name[0] && name[0] <= 'Z') || token.IsExported(name) { +- if _, ok := trefs.refs[name]; !ok { +- pkgs := b.s.pkgIndex.NewSet() +- for _, sym := range ph.refs[name] { +- pkgs.Add(sym.Package) +- otherSet := b.getOneTransitiveRefLocked(sym) +- pkgs.Union(otherSet) +- } +- trefs.refs[name] = pkgs +- } +- } +- } - } - -- impGraph := s.getImportGraph(ctx) -- _, err = s.forEachPackageInternal(ctx, impGraph, nil, ids, pre, post, handles) -- return err +- return trefs.refs -} - --// forEachPackageInternal is used by both forEachPackage and loadImportGraph to --// type-check a graph of packages. +-// getOneTransitiveRefLocked computes the full set packages transitively +-// reachable through the given sym reference. -// --// If a non-nil importGraph is provided, imports in this graph will be reused. --func (s *snapshot) forEachPackageInternal(ctx context.Context, importGraph *importGraph, importIDs, syntaxIDs []PackageID, pre preTypeCheck, post postTypeCheck, handles map[PackageID]*packageHandle) (*typeCheckBatch, error) { -- b := &typeCheckBatch{ -- activePackageCache: s, -- pre: pre, -- post: post, -- handles: handles, -- parseCache: s.view.parseCache, -- fset: fileSetWithBase(reservedForParsing), -- syntaxIndex: make(map[PackageID]int), -- cpulimit: make(chan unit, runtime.GOMAXPROCS(0)), -- syntaxPackages: make(map[PackageID]*futurePackage), -- importPackages: make(map[PackageID]*futurePackage), -- } +-// It may return nil if the reference is invalid (i.e. the referenced name does +-// not exist). +-func (b *packageHandleBuilder) getOneTransitiveRefLocked(sym typerefs.Symbol) *typerefs.PackageSet { +- assert(token.IsExported(sym.Name), "expected exported symbol") - -- if importGraph != nil { -- // Clone the file set every time, to ensure we do not leak files. -- b.fset = tokeninternal.CloneFileSet(importGraph.fset) -- // Pre-populate future cache with 'done' futures. -- done := make(chan unit) -- close(done) -- for id, res := range importGraph.imports { -- b.importPackages[id] = &futurePackage{done, res} +- trefs := b.transitiveRefs[sym.Package] +- if trefs == nil { +- trefs = &partialRefs{ +- refs: make(map[string]*typerefs.PackageSet), +- complete: false, - } -- } else { -- b.fset = fileSetWithBase(reservedForParsing) -- } -- -- for i, id := range syntaxIDs { -- b.syntaxIndex[id] = i +- b.transitiveRefs[sym.Package] = trefs - } - -- // Start a single goroutine for each requested package. -- // -- // Other packages are reached recursively, and will not be evaluated if they -- // are not needed. -- var g errgroup.Group -- for _, id := range importIDs { -- id := id -- g.Go(func() error { -- _, err := b.getImportPackage(ctx, id) -- return err -- }) -- } -- for i, id := range syntaxIDs { -- i := i -- id := id -- g.Go(func() error { -- _, err := b.handleSyntaxPackage(ctx, i, id) -- return err -- }) +- pkgs, ok := trefs.refs[sym.Name] +- if ok && pkgs == nil { +- // See below, where refs is set to nil before recursing. +- bug.Reportf("cycle detected to %q in reference graph", sym.Name) - } -- return b, g.Wait() --} - --// TODO(rfindley): re-order the declarations below to read better from top-to-bottom. +- // Note that if (!ok && trefs.complete), the name does not exist in the +- // referenced package, and we should not write to trefs as that may introduce +- // a race. +- if !ok && !trefs.complete { +- n := b.nodes[sym.Package] +- if n == nil { +- // We should always have IndexID in our node set, because symbol references +- // should only be recorded for packages that actually exist in the import graph. +- // +- // However, it is not easy to prove this (typerefs are serialized and +- // deserialized), so make this code temporarily defensive while we are on a +- // point release. +- // +- // TODO(rfindley): in the future, we should turn this into an assertion. +- bug.Reportf("missing reference to package %s", b.s.pkgIndex.PackageID(sym.Package)) +- return nil +- } - --// getImportPackage returns the *types.Package to use for importing the --// package referenced by id. --// --// This may be the package produced by type-checking syntax (as in the case --// where id is in the set of requested IDs), a package loaded from export data, --// or a package type-checked for import only. --func (b *typeCheckBatch) getImportPackage(ctx context.Context, id PackageID) (pkg *types.Package, err error) { -- b.mu.Lock() -- f, ok := b.importPackages[id] -- if ok { -- b.mu.Unlock() +- // Break cycles. This is perhaps overly defensive as cycles should not +- // exist at this point: metadata cycles should have been broken at load +- // time, and intra-package reference cycles should have been contracted by +- // the typerefs algorithm. +- // +- // See the "cycle detected" bug report above. +- trefs.refs[sym.Name] = nil - -- select { -- case <-ctx.Done(): -- return nil, ctx.Err() -- case <-f.done: -- return f.v.pkg, f.v.err +- pkgs := b.s.pkgIndex.NewSet() +- for _, sym2 := range n.ph.refs[sym.Name] { +- pkgs.Add(sym2.Package) +- otherSet := b.getOneTransitiveRefLocked(sym2) +- pkgs.Union(otherSet) - } +- trefs.refs[sym.Name] = pkgs - } - -- f = &futurePackage{done: make(chan unit)} -- b.importPackages[id] = f -- b.mu.Unlock() -- -- defer func() { -- f.v = pkgOrErr{pkg, err} -- close(f.done) -- }() +- return pkgs +-} - -- if index, ok := b.syntaxIndex[id]; ok { -- pkg, err := b.handleSyntaxPackage(ctx, index, id) +-// buildPackageHandle gets or builds a package handle for the given id, storing +-// its result in the snapshot.packages map. +-// +-// buildPackageHandle must only be called from getPackageHandles. +-func (b *packageHandleBuilder) buildPackageHandle(ctx context.Context, n *handleNode) { +- var prevPH *packageHandle +- if n.ph != nil { +- // Existing package handle: if it is valid, return it. Otherwise, create a +- // copy to update. +- if n.ph.validated { +- return +- } +- prevPH = n.ph +- // Either prevPH is still valid, or we will update the key and depKeys of +- // this copy. In either case, the result will be valid. +- n.ph = prevPH.clone(true) +- } else { +- // No package handle: read and analyze the package syntax. +- inputs, err := b.s.typeCheckInputs(ctx, n.mp) - if err != nil { -- return nil, err +- n.err = err +- return - } -- if pkg != nil { -- return pkg, nil +- refs, err := b.s.typerefs(ctx, n.mp, inputs.compiledGoFiles) +- if err != nil { +- n.err = err +- return +- } +- n.ph = &packageHandle{ +- mp: n.mp, +- loadDiagnostics: computeLoadDiagnostics(ctx, b.s, n.mp), +- localInputs: inputs, +- localKey: localPackageKey(inputs), +- refs: refs, +- validated: true, - } -- // type-checking was short-circuited by the pre- func. - } - -- // unsafe cannot be imported or type-checked. -- if id == "unsafe" { -- return types.Unsafe, nil +- // ph either did not exist, or was invalid. We must re-evaluate deps and key. +- if err := b.evaluatePackageHandle(prevPH, n); err != nil { +- n.err = err +- return - } - -- ph := b.handles[id] +- assert(n.ph.validated, "unvalidated handle") - -- // Do a second check for "unsafe" defensively, due to golang/go#60890. -- if ph.m.PkgPath == "unsafe" { -- bug.Reportf("encountered \"unsafe\" as %s (golang/go#60890)", id) -- return types.Unsafe, nil -- } +- // Ensure the result (or an equivalent) is recorded in the snapshot. +- b.s.mu.Lock() +- defer b.s.mu.Unlock() - -- data, err := filecache.Get(exportDataKind, ph.key) -- if err == filecache.ErrNotFound { -- // No cached export data: type-check as fast as possible. -- return b.checkPackageForImport(ctx, ph) +- // Check that the metadata has not changed +- // (which should invalidate this handle). +- // +- // TODO(rfindley): eventually promote this to an assert. +- // TODO(rfindley): move this to after building the package handle graph? +- if b.s.meta.Packages[n.mp.ID] != n.mp { +- bug.Reportf("stale metadata for %s", n.mp.ID) - } -- if err != nil { -- return nil, fmt.Errorf("failed to read cache data for %s: %v", ph.m.ID, err) +- +- // Check the packages map again in case another goroutine got there first. +- if alt, ok := b.s.packages.Get(n.mp.ID); ok && alt.validated { +- if alt.mp != n.mp { +- bug.Reportf("existing package handle does not match for %s", n.mp.ID) +- } +- n.ph = alt +- } else { +- b.s.packages.Set(n.mp.ID, n.ph, nil) - } -- return b.importPackage(ctx, ph.m, data) -} - --// handleSyntaxPackage handles one package from the ids slice. +-// evaluatePackageHandle validates and/or computes the key of ph, setting key, +-// depKeys, and the validated flag on ph. -// --// If type checking occurred while handling the package, it returns the --// resulting types.Package so that it may be used for importing. +-// It uses prevPH to avoid recomputing keys that can't have changed, since +-// their depKeys did not change. -// --// handleSyntaxPackage returns (nil, nil) if pre returned false. --func (b *typeCheckBatch) handleSyntaxPackage(ctx context.Context, i int, id PackageID) (pkg *types.Package, err error) { -- b.mu.Lock() -- f, ok := b.syntaxPackages[id] -- if ok { -- b.mu.Unlock() -- <-f.done -- return f.v.pkg, f.v.err +-// See the documentation for packageHandle for more details about packageHandle +-// state, and see the documentation for the typerefs package for more details +-// about precise reachability analysis. +-func (b *packageHandleBuilder) evaluatePackageHandle(prevPH *packageHandle, n *handleNode) error { +- // Opt: if no dep keys have changed, we need not re-evaluate the key. +- if prevPH != nil { +- depsChanged := false +- assert(len(prevPH.depKeys) == len(n.succs), "mismatching dep count") +- for id, succ := range n.succs { +- oldKey, ok := prevPH.depKeys[id] +- assert(ok, "missing dep") +- if oldKey != succ.ph.key { +- depsChanged = true +- break +- } +- } +- if !depsChanged { +- return nil // key cannot have changed +- } - } - -- f = &futurePackage{done: make(chan unit)} -- b.syntaxPackages[id] = f -- b.mu.Unlock() -- defer func() { -- f.v = pkgOrErr{pkg, err} -- close(f.done) -- }() +- // Deps have changed, so we must re-evaluate the key. +- n.ph.depKeys = make(map[PackageID]file.Hash) - -- ph := b.handles[id] -- if b.pre != nil && !b.pre(i, ph) { -- return nil, nil // skip: export data only +- // See the typerefs package: the reachable set of packages is defined to be +- // the set of packages containing syntax that is reachable through the +- // exported symbols in the dependencies of n.ph. +- reachable := b.s.pkgIndex.NewSet() +- for depID, succ := range n.succs { +- n.ph.depKeys[depID] = succ.ph.key +- reachable.Add(succ.idxID) +- trefs := b.getTransitiveRefs(succ.mp.ID) +- if trefs == nil { +- // A predecessor failed to build due to e.g. context cancellation. +- return fmt.Errorf("missing transitive refs for %s", succ.mp.ID) +- } +- for _, set := range trefs { +- reachable.Union(set) +- } - } - -- // Check for existing active packages. -- // -- // Since gopls can't depend on package identity, any instance of the -- // requested package must be ok to return. -- // -- // This is an optimization to avoid redundant type-checking: following -- // changes to an open package many LSP clients send several successive -- // requests for package information for the modified package (semantic -- // tokens, code lens, inlay hints, etc.) -- if pkg := b.activePackageCache.getActivePackage(id); pkg != nil { -- b.post(i, pkg) -- return nil, nil // skip: not checked in this batch +- // Collect reachable handles. +- var reachableHandles []*packageHandle +- // In the presence of context cancellation, any package may be missing. +- // We need all dependencies to produce a valid key. +- missingReachablePackage := false +- reachable.Elems(func(id typerefs.IndexID) { +- dh := b.nodes[id] +- if dh == nil { +- missingReachablePackage = true +- } else { +- assert(dh.ph.validated, "unvalidated dependency") +- reachableHandles = append(reachableHandles, dh.ph) +- } +- }) +- if missingReachablePackage { +- return fmt.Errorf("missing reachable package") - } +- // Sort for stability. +- sort.Slice(reachableHandles, func(i, j int) bool { +- return reachableHandles[i].mp.ID < reachableHandles[j].mp.ID +- }) - -- if err := b.awaitPredecessors(ctx, ph.m); err != nil { -- // One failed precessesor should not fail the entire type checking -- // operation. Errors related to imports will be reported as type checking -- // diagnostics. -- if ctx.Err() != nil { -- return nil, ctx.Err() -- } +- // Key is the hash of the local key, and the local key of all reachable +- // packages. +- depHasher := sha256.New() +- depHasher.Write(n.ph.localKey[:]) +- for _, rph := range reachableHandles { +- depHasher.Write(rph.localKey[:]) - } +- depHasher.Sum(n.ph.key[:0]) - -- // Wait to acquire a CPU token. -- // -- // Note: it is important to acquire this token only after awaiting -- // predecessors, to avoid starvation. -- select { -- case <-ctx.Done(): -- return nil, ctx.Err() -- case b.cpulimit <- unit{}: -- defer func() { -- <-b.cpulimit // release CPU token -- }() +- return nil +-} +- +-// typerefs returns typerefs for the package described by m and cgfs, after +-// either computing it or loading it from the file cache. +-func (s *Snapshot) typerefs(ctx context.Context, mp *metadata.Package, cgfs []file.Handle) (map[string][]typerefs.Symbol, error) { +- imports := make(map[ImportPath]*metadata.Package) +- for impPath, id := range mp.DepsByImpPath { +- if id != "" { +- imports[impPath] = s.Metadata(id) +- } - } - -- // We need a syntax package. -- syntaxPkg, err := b.checkPackage(ctx, ph) +- data, err := s.typerefData(ctx, mp.ID, imports, cgfs) - if err != nil { - return nil, err - } -- b.activePackageCache.setActivePackage(id, syntaxPkg) -- b.post(i, syntaxPkg) -- -- return syntaxPkg.pkg.types, nil --} -- --// importPackage loads the given package from its export data in p.exportData --// (which must already be populated). --func (b *typeCheckBatch) importPackage(ctx context.Context, m *source.Metadata, data []byte) (*types.Package, error) { -- ctx, done := event.Start(ctx, "cache.typeCheckBatch.importPackage", tag.Package.Of(string(m.ID))) -- defer done() -- -- impMap := b.importMap(m.ID) -- -- thisPackage := types.NewPackage(string(m.PkgPath), string(m.Name)) -- getPackages := func(items []gcimporter.GetPackagesItem) error { -- for i, item := range items { -- var id PackageID -- var pkg *types.Package -- if item.Path == string(m.PkgPath) { -- id = m.ID -- pkg = thisPackage -- } else { -- id = impMap[item.Path] -- var err error -- pkg, err = b.getImportPackage(ctx, id) -- if err != nil { -- return err -- } -- } -- items[i].Pkg = pkg -- -- // debugging issue #60904 -- if pkg.Name() != item.Name { -- return bug.Errorf("internal error: package name is %q, want %q (id=%q, path=%q) (see issue #60904)", -- pkg.Name(), item.Name, id, item.Path) -- } +- classes := typerefs.Decode(s.pkgIndex, data) +- refs := make(map[string][]typerefs.Symbol) +- for _, class := range classes { +- for _, decl := range class.Decls { +- refs[decl] = class.Refs - } -- return nil - } +- return refs, nil +-} - -- // Importing is potentially expensive, and might not encounter cancellations -- // via dependencies (e.g. if they have already been evaluated). -- if ctx.Err() != nil { -- return nil, ctx.Err() +-// typerefData retrieves encoded typeref data from the filecache, or computes it on +-// a cache miss. +-func (s *Snapshot) typerefData(ctx context.Context, id PackageID, imports map[ImportPath]*metadata.Package, cgfs []file.Handle) ([]byte, error) { +- key := typerefsKey(id, imports, cgfs) +- if data, err := filecache.Get(typerefsKind, key); err == nil { +- return data, nil +- } else if err != filecache.ErrNotFound { +- bug.Reportf("internal error reading typerefs data: %v", err) - } - -- // TODO(rfindley): collect "deep" hashes here using the getPackages -- // callback, for precise pruning. -- imported, err := gcimporter.IImportShallow(b.fset, getPackages, data, string(m.PkgPath), bug.Reportf) +- pgfs, err := s.view.parseCache.parseFiles(ctx, token.NewFileSet(), parsego.Full&^parser.ParseComments, true, cgfs...) - if err != nil { -- return nil, fmt.Errorf("import failed for %q: %v", m.ID, err) +- return nil, err - } -- return imported, nil --} +- data := typerefs.Encode(pgfs, imports) - --// checkPackageForImport type checks, but skips function bodies and does not --// record syntax information. --func (b *typeCheckBatch) checkPackageForImport(ctx context.Context, ph *packageHandle) (*types.Package, error) { -- ctx, done := event.Start(ctx, "cache.typeCheckBatch.checkPackageForImport", tag.Package.Of(string(ph.m.ID))) -- defer done() +- // Store the resulting data in the cache. +- go func() { +- if err := filecache.Set(typerefsKind, key, data); err != nil { +- event.Error(ctx, fmt.Sprintf("storing typerefs data for %s", id), err) +- } +- }() - -- onError := func(e error) { -- // Ignore errors for exporting. -- } -- cfg := b.typesConfig(ctx, ph.localInputs, onError) -- cfg.IgnoreFuncBodies = true +- return data, nil +-} - -- // Parse the compiled go files, bypassing the parse cache as packages checked -- // for import are unlikely to get cache hits. Additionally, we can optimize -- // parsing slightly by not passing parser.ParseComments. -- pgfs := make([]*source.ParsedGoFile, len(ph.localInputs.compiledGoFiles)) -- { -- var group errgroup.Group -- // Set an arbitrary concurrency limit; we want some parallelism but don't -- // need GOMAXPROCS, as there is already a lot of concurrency among calls to -- // checkPackageForImport. -- // -- // TODO(rfindley): is there a better way to limit parallelism here? We could -- // have a global limit on the type-check batch, but would have to be very -- // careful to avoid starvation. -- group.SetLimit(4) -- for i, fh := range ph.localInputs.compiledGoFiles { -- i, fh := i, fh -- group.Go(func() error { -- pgf, err := parseGoImpl(ctx, b.fset, fh, parser.SkipObjectResolution, false) -- pgfs[i] = pgf -- return err -- }) -- } -- if err := group.Wait(); err != nil { -- return nil, err // cancelled, or catastrophic error (e.g. missing file) -- } -- } -- pkg := types.NewPackage(string(ph.localInputs.pkgPath), string(ph.localInputs.name)) -- check := types.NewChecker(cfg, b.fset, pkg, nil) +-// typerefsKey produces a key for the reference information produced by the +-// typerefs package. +-func typerefsKey(id PackageID, imports map[ImportPath]*metadata.Package, compiledGoFiles []file.Handle) file.Hash { +- hasher := sha256.New() - -- files := make([]*ast.File, len(pgfs)) -- for i, pgf := range pgfs { -- files[i] = pgf.File -- } +- fmt.Fprintf(hasher, "typerefs: %s\n", id) - -- // Type checking is expensive, and we may not have ecountered cancellations -- // via parsing (e.g. if we got nothing but cache hits for parsed files). -- if ctx.Err() != nil { -- return nil, ctx.Err() +- importPaths := make([]string, 0, len(imports)) +- for impPath := range imports { +- importPaths = append(importPaths, string(impPath)) +- } +- sort.Strings(importPaths) +- for _, importPath := range importPaths { +- imp := imports[ImportPath(importPath)] +- // TODO(rfindley): strength reduce the typerefs.Export API to guarantee +- // that it only depends on these attributes of dependencies. +- fmt.Fprintf(hasher, "import %s %s %s", importPath, imp.ID, imp.Name) - } - -- _ = check.Files(files) // ignore errors -- -- // If the context was cancelled, we may have returned a ton of transient -- // errors to the type checker. Swallow them. -- if ctx.Err() != nil { -- return nil, ctx.Err() +- fmt.Fprintf(hasher, "compiledGoFiles: %d\n", len(compiledGoFiles)) +- for _, fh := range compiledGoFiles { +- fmt.Fprintln(hasher, fh.Identity()) - } - -- // Asynchronously record export data. -- go func() { -- exportData, err := gcimporter.IExportShallow(b.fset, pkg, bug.Reportf) -- if err != nil { -- bug.Reportf("exporting package %v: %v", ph.m.ID, err) -- return -- } -- if err := filecache.Set(exportDataKind, ph.key, exportData); err != nil { -- event.Error(ctx, fmt.Sprintf("storing export data for %s", ph.m.ID), err) -- } -- }() -- return pkg, nil +- var hash [sha256.Size]byte +- hasher.Sum(hash[:0]) +- return hash -} - --// checkPackage "fully type checks" to produce a syntax package. --func (b *typeCheckBatch) checkPackage(ctx context.Context, ph *packageHandle) (*Package, error) { -- ctx, done := event.Start(ctx, "cache.typeCheckBatch.checkPackage", tag.Package.Of(string(ph.m.ID))) -- defer done() +-// typeCheckInputs contains the inputs of a call to typeCheckImpl, which +-// type-checks a package. +-// +-// Part of the purpose of this type is to keep type checking in-sync with the +-// package handle key, by explicitly identifying the inputs to type checking. +-type typeCheckInputs struct { +- id PackageID - -- // TODO(rfindley): refactor to inline typeCheckImpl here. There is no need -- // for so many layers to build up the package -- // (checkPackage->typeCheckImpl->doTypeCheck). -- pkg, err := typeCheckImpl(ctx, b, ph.localInputs) +- // Used for type checking: +- pkgPath PackagePath +- name PackageName +- goFiles, compiledGoFiles []file.Handle +- sizes types.Sizes +- depsByImpPath map[ImportPath]PackageID +- goVersion string // packages.Module.GoVersion, e.g. "1.18" - -- if err == nil { -- // Write package data to disk asynchronously. -- go func() { -- toCache := map[string][]byte{ -- xrefsKind: pkg.xrefs(), -- methodSetsKind: pkg.methodsets().Encode(), -- diagnosticsKind: encodeDiagnostics(pkg.diagnostics), -- } +- // Used for type check diagnostics: +- // TODO(rfindley): consider storing less data in gobDiagnostics, and +- // interpreting each diagnostic in the context of a fixed set of options. +- // Then these fields need not be part of the type checking inputs. +- relatedInformation bool +- linkTarget string +- moduleMode bool +-} - -- if ph.m.PkgPath != "unsafe" { // unsafe cannot be exported -- exportData, err := gcimporter.IExportShallow(pkg.fset, pkg.types, bug.Reportf) -- if err != nil { -- bug.Reportf("exporting package %v: %v", ph.m.ID, err) -- } else { -- toCache[exportDataKind] = exportData -- } -- } else if ph.m.ID != "unsafe" { -- // golang/go#60890: we should only ever see one variant of the "unsafe" -- // package. -- bug.Reportf("encountered \"unsafe\" as %s (golang/go#60890)", ph.m.ID) -- } +-func (s *Snapshot) typeCheckInputs(ctx context.Context, mp *metadata.Package) (typeCheckInputs, error) { +- // Read both lists of files of this package. +- // +- // Parallelism is not necessary here as the files will have already been +- // pre-read at load time. +- // +- // goFiles aren't presented to the type checker--nor +- // are they included in the key, unsoundly--but their +- // syntax trees are available from (*pkg).File(URI). +- // TODO(adonovan): consider parsing them on demand? +- // The need should be rare. +- goFiles, err := readFiles(ctx, s, mp.GoFiles) +- if err != nil { +- return typeCheckInputs{}, err +- } +- compiledGoFiles, err := readFiles(ctx, s, mp.CompiledGoFiles) +- if err != nil { +- return typeCheckInputs{}, err +- } - -- for kind, data := range toCache { -- if err := filecache.Set(kind, ph.key, data); err != nil { -- event.Error(ctx, fmt.Sprintf("storing %s data for %s", kind, ph.m.ID), err) -- } -- } -- }() +- goVersion := "" +- if mp.Module != nil && mp.Module.GoVersion != "" { +- goVersion = mp.Module.GoVersion - } - -- return &Package{ph.m, pkg}, err --} +- return typeCheckInputs{ +- id: mp.ID, +- pkgPath: mp.PkgPath, +- name: mp.Name, +- goFiles: goFiles, +- compiledGoFiles: compiledGoFiles, +- sizes: mp.TypesSizes, +- depsByImpPath: mp.DepsByImpPath, +- goVersion: goVersion, - --// awaitPredecessors awaits all packages for m.DepsByPkgPath, returning an --// error if awaiting failed due to context cancellation or if there was an --// unrecoverable error loading export data. --// --// TODO(rfindley): inline, now that this is only called in one place. --func (b *typeCheckBatch) awaitPredecessors(ctx context.Context, m *source.Metadata) error { -- // await predecessors concurrently, as some of them may be non-syntax -- // packages, and therefore will not have been started by the type-checking -- // batch. -- var g errgroup.Group -- for _, depID := range m.DepsByPkgPath { -- depID := depID -- g.Go(func() error { -- _, err := b.getImportPackage(ctx, depID) -- return err -- }) -- } -- return g.Wait() +- relatedInformation: s.Options().RelatedInformationSupported, +- linkTarget: s.Options().LinkTarget, +- moduleMode: s.view.moduleMode(), +- }, nil -} - --// importMap returns the map of package path -> package ID relative to the --// specified ID. --func (b *typeCheckBatch) importMap(id PackageID) map[string]source.PackageID { -- impMap := make(map[string]source.PackageID) -- var populateDeps func(m *source.Metadata) -- populateDeps = func(parent *source.Metadata) { -- for _, id := range parent.DepsByPkgPath { -- m := b.handles[id].m -- if _, ok := impMap[string(m.PkgPath)]; ok { -- continue -- } -- impMap[string(m.PkgPath)] = m.ID -- populateDeps(m) +-// readFiles reads the content of each file URL from the source +-// (e.g. snapshot or cache). +-func readFiles(ctx context.Context, fs file.Source, uris []protocol.DocumentURI) (_ []file.Handle, err error) { +- fhs := make([]file.Handle, len(uris)) +- for i, uri := range uris { +- fhs[i], err = fs.ReadFile(ctx, uri) +- if err != nil { +- return nil, err - } - } -- m := b.handles[id].m -- populateDeps(m) -- return impMap +- return fhs, nil -} - --// A packageHandle holds inputs required to compute a type-checked package, --// including inputs to type checking itself, and a key for looking up --// precomputed data. --// --// packageHandles may be invalid following an invalidation via snapshot.clone, --// but the handles returned by getPackageHandles will always be valid. --// --// packageHandles are critical for implementing "precise pruning" in gopls: --// packageHandle.key is a hash of a precise set of inputs, such as package --// files and "reachable" syntax, that may affect type checking. --// --// packageHandles also keep track of state that allows gopls to compute, and --// then quickly recompute, these keys. This state is split into two categories: --// - local state, which depends only on the package's local files and metadata --// - other state, which includes data derived from dependencies. --// --// Dividing the data in this way allows gopls to minimize invalidation when a --// package is modified. For example, any change to a package file fully --// invalidates the package handle. On the other hand, if that change was not --// metadata-affecting it may be the case that packages indirectly depending on --// the modified package are unaffected by the change. For that reason, we have --// two types of invalidation, corresponding to the two types of data above: --// - deletion of the handle, which occurs when the package itself changes --// - clearing of the validated field, which marks the package as possibly --// invalid. --// --// With the second type of invalidation, packageHandles are re-evaluated from the --// bottom up. If this process encounters a packageHandle whose deps have not --// changed (as detected by the depkeys field), then the packageHandle in --// question must also not have changed, and we need not re-evaluate its key. --type packageHandle struct { -- m *source.Metadata -- -- // Local data: -- -- // localInputs holds all local type-checking localInputs, excluding -- // dependencies. -- localInputs typeCheckInputs -- // localKey is a hash of localInputs. -- localKey source.Hash -- // refs is the result of syntactic dependency analysis produced by the -- // typerefs package. -- refs map[string][]typerefs.Symbol -- -- // Data derived from dependencies: +-// localPackageKey returns a key for local inputs into type-checking, excluding +-// dependency information: files, metadata, and configuration. +-func localPackageKey(inputs typeCheckInputs) file.Hash { +- hasher := sha256.New() - -- // validated indicates whether the current packageHandle is known to have a -- // valid key. Invalidated package handles are stored for packages whose -- // type information may have changed. -- validated bool -- // depKeys records the key of each dependency that was used to calculate the -- // key above. If the handle becomes invalid, we must re-check that each still -- // matches. -- depKeys map[PackageID]source.Hash -- // key is the hashed key for the package. -- // -- // It includes the all bits of the transitive closure of -- // dependencies's sources. -- key source.Hash --} +- // In principle, a key must be the hash of an +- // unambiguous encoding of all the relevant data. +- // If it's ambiguous, we risk collisions. - --// clone returns a copy of the receiver with the validated bit set to the --// provided value. --func (ph *packageHandle) clone(validated bool) *packageHandle { -- copy := *ph -- copy.validated = validated -- return © --} +- // package identifiers +- fmt.Fprintf(hasher, "package: %s %s %s\n", inputs.id, inputs.name, inputs.pkgPath) - --// getPackageHandles gets package handles for all given ids and their --// dependencies, recursively. --func (s *snapshot) getPackageHandles(ctx context.Context, ids []PackageID) (map[PackageID]*packageHandle, error) { -- // perform a two-pass traversal. -- // -- // On the first pass, build up a bidirectional graph of handle nodes, and collect leaves. -- // Then build package handles from bottom up. +- // module Go version +- fmt.Fprintf(hasher, "go %s\n", inputs.goVersion) - -- s.mu.Lock() // guard s.meta and s.packages below -- b := &packageHandleBuilder{ -- s: s, -- transitiveRefs: make(map[typerefs.IndexID]*partialRefs), -- nodes: make(map[typerefs.IndexID]*handleNode), +- // import map +- importPaths := make([]string, 0, len(inputs.depsByImpPath)) +- for impPath := range inputs.depsByImpPath { +- importPaths = append(importPaths, string(impPath)) +- } +- sort.Strings(importPaths) +- for _, impPath := range importPaths { +- fmt.Fprintf(hasher, "import %s %s", impPath, string(inputs.depsByImpPath[ImportPath(impPath)])) - } - -- var leaves []*handleNode -- var makeNode func(*handleNode, PackageID) *handleNode -- makeNode = func(from *handleNode, id PackageID) *handleNode { -- idxID := b.s.pkgIndex.IndexID(id) -- n, ok := b.nodes[idxID] -- if !ok { -- m := s.meta.metadata[id] -- if m == nil { -- panic(fmt.Sprintf("nil metadata for %q", id)) -- } -- n = &handleNode{ -- m: m, -- idxID: idxID, -- unfinishedSuccs: int32(len(m.DepsByPkgPath)), -- } -- if entry, hit := b.s.packages.Get(m.ID); hit { -- n.ph = entry -- } -- if n.unfinishedSuccs == 0 { -- leaves = append(leaves, n) -- } else { -- n.succs = make(map[source.PackageID]*handleNode, n.unfinishedSuccs) -- } -- b.nodes[idxID] = n -- for _, depID := range m.DepsByPkgPath { -- n.succs[depID] = makeNode(n, depID) -- } -- } -- // Add edge from predecessor. -- if from != nil { -- n.preds = append(n.preds, from) -- } -- return n +- // file names and contents +- fmt.Fprintf(hasher, "compiledGoFiles: %d\n", len(inputs.compiledGoFiles)) +- for _, fh := range inputs.compiledGoFiles { +- fmt.Fprintln(hasher, fh.Identity()) - } -- for _, id := range ids { -- makeNode(nil, id) +- fmt.Fprintf(hasher, "goFiles: %d\n", len(inputs.goFiles)) +- for _, fh := range inputs.goFiles { +- fmt.Fprintln(hasher, fh.Identity()) - } -- s.mu.Unlock() -- -- g, ctx := errgroup.WithContext(ctx) -- -- // files are preloaded, so building package handles is CPU-bound. -- // -- // Note that we can't use g.SetLimit, as that could result in starvation: -- // g.Go blocks until a slot is available, and so all existing goroutines -- // could be blocked trying to enqueue a predecessor. -- limiter := make(chan unit, runtime.GOMAXPROCS(0)) - -- var enqueue func(*handleNode) -- enqueue = func(n *handleNode) { -- g.Go(func() error { -- limiter <- unit{} -- defer func() { <-limiter }() +- // types sizes +- wordSize := inputs.sizes.Sizeof(types.Typ[types.Int]) +- maxAlign := inputs.sizes.Alignof(types.NewPointer(types.Typ[types.Int64])) +- fmt.Fprintf(hasher, "sizes: %d %d\n", wordSize, maxAlign) - -- if ctx.Err() != nil { -- return ctx.Err() -- } +- fmt.Fprintf(hasher, "relatedInformation: %t\n", inputs.relatedInformation) +- fmt.Fprintf(hasher, "linkTarget: %s\n", inputs.linkTarget) +- fmt.Fprintf(hasher, "moduleMode: %t\n", inputs.moduleMode) - -- b.buildPackageHandle(ctx, n) +- var hash [sha256.Size]byte +- hasher.Sum(hash[:0]) +- return hash +-} - -- for _, pred := range n.preds { -- if atomic.AddInt32(&pred.unfinishedSuccs, -1) == 0 { -- enqueue(pred) -- } -- } +-// checkPackage type checks the parsed source files in compiledGoFiles. +-// (The resulting pkg also holds the parsed but not type-checked goFiles.) +-// deps holds the future results of type-checking the direct dependencies. +-func (b *typeCheckBatch) checkPackage(ctx context.Context, ph *packageHandle) (*Package, error) { +- inputs := ph.localInputs +- ctx, done := event.Start(ctx, "cache.typeCheckBatch.checkPackage", tag.Package.Of(string(inputs.id))) +- defer done() - -- return n.err -- }) -- } -- for _, leaf := range leaves { -- enqueue(leaf) +- pkg := &syntaxPackage{ +- id: inputs.id, +- fset: b.fset, // must match parse call below +- types: types.NewPackage(string(inputs.pkgPath), string(inputs.name)), +- typesSizes: inputs.sizes, +- typesInfo: &types.Info{ +- Types: make(map[ast.Expr]types.TypeAndValue), +- Defs: make(map[*ast.Ident]types.Object), +- Uses: make(map[*ast.Ident]types.Object), +- Implicits: make(map[ast.Node]types.Object), +- Instances: make(map[*ast.Ident]types.Instance), +- Selections: make(map[*ast.SelectorExpr]*types.Selection), +- Scopes: make(map[ast.Node]*types.Scope), +- }, - } +- versions.InitFileVersions(pkg.typesInfo) - -- if err := g.Wait(); err != nil { +- // Collect parsed files from the type check pass, capturing parse errors from +- // compiled files. +- var err error +- pkg.goFiles, err = b.parseCache.parseFiles(ctx, b.fset, parsego.Full, false, inputs.goFiles...) +- if err != nil { - return nil, err - } -- -- // Copy handles into the result map. -- handles := make(map[PackageID]*packageHandle, len(b.nodes)) -- for _, v := range b.nodes { -- assert(v.ph != nil, "nil handle") -- handles[v.m.ID] = v.ph +- pkg.compiledGoFiles, err = b.parseCache.parseFiles(ctx, b.fset, parsego.Full, false, inputs.compiledGoFiles...) +- if err != nil { +- return nil, err +- } +- for _, pgf := range pkg.compiledGoFiles { +- if pgf.ParseErr != nil { +- pkg.parseErrors = append(pkg.parseErrors, pgf.ParseErr) +- } - } - -- return handles, nil --} +- // Use the default type information for the unsafe package. +- if inputs.pkgPath == "unsafe" { +- // Don't type check Unsafe: it's unnecessary, and doing so exposes a data +- // race to Unsafe.completed. +- pkg.types = types.Unsafe +- } else { - --// A packageHandleBuilder computes a batch of packageHandles concurrently, --// sharing computed transitive reachability sets used to compute package keys. --type packageHandleBuilder struct { -- meta *metadataGraph -- s *snapshot +- if len(pkg.compiledGoFiles) == 0 { +- // No files most likely means go/packages failed. +- // +- // TODO(rfindley): in the past, we would capture go list errors in this +- // case, to present go list errors to the user. However we had no tests for +- // this behavior. It is unclear if anything better can be done here. +- return nil, fmt.Errorf("no parsed files for package %s", inputs.pkgPath) +- } - -- // nodes are assembled synchronously. -- nodes map[typerefs.IndexID]*handleNode +- onError := func(e error) { +- pkg.typeErrors = append(pkg.typeErrors, e.(types.Error)) +- } +- cfg := b.typesConfig(ctx, inputs, onError) +- check := types.NewChecker(cfg, pkg.fset, pkg.types, pkg.typesInfo) - -- // transitiveRefs is incrementally evaluated as package handles are built. -- transitiveRefsMu sync.Mutex -- transitiveRefs map[typerefs.IndexID]*partialRefs // see getTransitiveRefs --} +- var files []*ast.File +- for _, cgf := range pkg.compiledGoFiles { +- files = append(files, cgf.File) +- } - --// A handleNode represents a to-be-computed packageHandle within a graph of --// predecessors and successors. --// --// It is used to implement a bottom-up construction of packageHandles. --type handleNode struct { -- m *source.Metadata -- idxID typerefs.IndexID -- ph *packageHandle -- err error -- preds []*handleNode -- succs map[PackageID]*handleNode -- unfinishedSuccs int32 --} +- // Type checking is expensive, and we may not have encountered cancellations +- // via parsing (e.g. if we got nothing but cache hits for parsed files). +- if ctx.Err() != nil { +- return nil, ctx.Err() +- } - --// partialRefs maps names declared by a given package to their set of --// transitive references. --// --// If complete is set, refs is known to be complete for the package in --// question. Otherwise, it may only map a subset of all names declared by the --// package. --type partialRefs struct { -- refs map[string]*typerefs.PackageSet -- complete bool --} +- // Type checking errors are handled via the config, so ignore them here. +- _ = check.Files(files) // 50us-15ms, depending on size of package - --// getTransitiveRefs gets or computes the set of transitively reachable --// packages for each exported name in the package specified by id. --// --// The operation may fail if building a predecessor failed. If and only if this --// occurs, the result will be nil. --func (b *packageHandleBuilder) getTransitiveRefs(pkgID PackageID) map[string]*typerefs.PackageSet { -- b.transitiveRefsMu.Lock() -- defer b.transitiveRefsMu.Unlock() +- // If the context was cancelled, we may have returned a ton of transient +- // errors to the type checker. Swallow them. +- if ctx.Err() != nil { +- return nil, ctx.Err() +- } - -- idxID := b.s.pkgIndex.IndexID(pkgID) -- trefs, ok := b.transitiveRefs[idxID] -- if !ok { -- trefs = &partialRefs{ -- refs: make(map[string]*typerefs.PackageSet), +- // Collect imports by package path for the DependencyTypes API. +- pkg.importMap = make(map[PackagePath]*types.Package) +- var collectDeps func(*types.Package) +- collectDeps = func(p *types.Package) { +- pkgPath := PackagePath(p.Path()) +- if _, ok := pkg.importMap[pkgPath]; ok { +- return +- } +- pkg.importMap[pkgPath] = p +- for _, imp := range p.Imports() { +- collectDeps(imp) +- } - } -- b.transitiveRefs[idxID] = trefs -- } +- collectDeps(pkg.types) - -- if !trefs.complete { -- trefs.complete = true -- ph := b.nodes[idxID].ph -- for name := range ph.refs { -- if ('A' <= name[0] && name[0] <= 'Z') || token.IsExported(name) { -- if _, ok := trefs.refs[name]; !ok { -- pkgs := b.s.pkgIndex.NewSet() -- for _, sym := range ph.refs[name] { -- pkgs.Add(sym.Package) -- otherSet := b.getOneTransitiveRefLocked(sym) -- pkgs.Union(otherSet) -- } -- trefs.refs[name] = pkgs -- } +- // Work around golang/go#61561: interface instances aren't concurrency-safe +- // as they are not completed by the type checker. +- for _, inst := range pkg.typesInfo.Instances { +- if iface, _ := inst.Type.Underlying().(*types.Interface); iface != nil { +- iface.Complete() - } - } - } - -- return trefs.refs --} +- // Our heuristic for whether to show type checking errors is: +- // + If there is a parse error _in the current file_, suppress type +- // errors in that file. +- // + Otherwise, show type errors even in the presence of parse errors in +- // other package files. go/types attempts to suppress follow-on errors +- // due to bad syntax, so on balance type checking errors still provide +- // a decent signal/noise ratio as long as the file in question parses. - --// getOneTransitiveRefLocked computes the full set packages transitively --// reachable through the given sym reference. --// --// It may return nil if the reference is invalid (i.e. the referenced name does --// not exist). --func (b *packageHandleBuilder) getOneTransitiveRefLocked(sym typerefs.Symbol) *typerefs.PackageSet { -- assert(token.IsExported(sym.Name), "expected exported symbol") +- // Track URIs with parse errors so that we can suppress type errors for these +- // files. +- unparseable := map[protocol.DocumentURI]bool{} +- for _, e := range pkg.parseErrors { +- diags, err := parseErrorDiagnostics(pkg, e) +- if err != nil { +- event.Error(ctx, "unable to compute positions for parse errors", err, tag.Package.Of(string(inputs.id))) +- continue +- } +- for _, diag := range diags { +- unparseable[diag.URI] = true +- pkg.diagnostics = append(pkg.diagnostics, diag) +- } +- } - -- trefs := b.transitiveRefs[sym.Package] -- if trefs == nil { -- trefs = &partialRefs{ -- refs: make(map[string]*typerefs.PackageSet), -- complete: false, +- diags := typeErrorsToDiagnostics(pkg, pkg.typeErrors, inputs.linkTarget, inputs.moduleMode, inputs.relatedInformation) +- for _, diag := range diags { +- // If the file didn't parse cleanly, it is highly likely that type +- // checking errors will be confusing or redundant. But otherwise, type +- // checking usually provides a good enough signal to include. +- if !unparseable[diag.URI] { +- pkg.diagnostics = append(pkg.diagnostics, diag) - } -- b.transitiveRefs[sym.Package] = trefs - } - -- pkgs, ok := trefs.refs[sym.Name] -- if ok && pkgs == nil { -- // See below, where refs is set to nil before recursing. -- bug.Reportf("cycle detected to %q in reference graph", sym.Name) +- return &Package{ph.mp, ph.loadDiagnostics, pkg}, nil +-} +- +-// e.g. "go1" or "go1.2" or "go1.2.3" +-var goVersionRx = regexp.MustCompile(`^go[1-9][0-9]*(?:\.(0|[1-9][0-9]*)){0,2}$`) +- +-func (b *typeCheckBatch) typesConfig(ctx context.Context, inputs typeCheckInputs, onError func(e error)) *types.Config { +- cfg := &types.Config{ +- Sizes: inputs.sizes, +- Error: onError, +- Importer: importerFunc(func(path string) (*types.Package, error) { +- // While all of the import errors could be reported +- // based on the metadata before we start type checking, +- // reporting them via types.Importer places the errors +- // at the correct source location. +- id, ok := inputs.depsByImpPath[ImportPath(path)] +- if !ok { +- // If the import declaration is broken, +- // go list may fail to report metadata about it. +- // See TestFixImportDecl for an example. +- return nil, fmt.Errorf("missing metadata for import of %q", path) +- } +- depPH := b.handles[id] +- if depPH == nil { +- // e.g. missing metadata for dependencies in buildPackageHandle +- return nil, missingPkgError(inputs.id, path, inputs.moduleMode) +- } +- if !metadata.IsValidImport(inputs.pkgPath, depPH.mp.PkgPath) { +- return nil, fmt.Errorf("invalid use of internal package %q", path) +- } +- return b.getImportPackage(ctx, id) +- }), - } - -- // Note that if (!ok && trefs.complete), the name does not exist in the -- // referenced package, and we should not write to trefs as that may introduce -- // a race. -- if !ok && !trefs.complete { -- n := b.nodes[sym.Package] -- if n == nil { -- // We should always have IndexID in our node set, because symbol references -- // should only be recorded for packages that actually exist in the import graph. -- // -- // However, it is not easy to prove this (typerefs are serialized and -- // deserialized), so make this code temporarily defensive while we are on a -- // point release. -- // -- // TODO(rfindley): in the future, we should turn this into an assertion. -- bug.Reportf("missing reference to package %s", b.s.pkgIndex.PackageID(sym.Package)) -- return nil +- if inputs.goVersion != "" { +- goVersion := "go" + inputs.goVersion +- if validGoVersion(goVersion) { +- cfg.GoVersion = goVersion - } +- } - -- // Break cycles. This is perhaps overly defensive as cycles should not -- // exist at this point: metadata cycles should have been broken at load -- // time, and intra-package reference cycles should have been contracted by -- // the typerefs algorithm. -- // -- // See the "cycle detected" bug report above. -- trefs.refs[sym.Name] = nil +- // We want to type check cgo code if go/types supports it. +- // We passed typecheckCgo to go/packages when we Loaded. +- typesinternal.SetUsesCgo(cfg) +- return cfg +-} - -- pkgs := b.s.pkgIndex.NewSet() -- for _, sym2 := range n.ph.refs[sym.Name] { -- pkgs.Add(sym2.Package) -- otherSet := b.getOneTransitiveRefLocked(sym2) -- pkgs.Union(otherSet) -- } -- trefs.refs[sym.Name] = pkgs +-// validGoVersion reports whether goVersion is a valid Go version for go/types. +-// types.NewChecker panics if GoVersion is invalid. +-// +-// Note that, prior to go1.21, go/types required exactly two components to the +-// version number. For example, go types would panic with the Go version +-// go1.21.1. validGoVersion handles this case when built with go1.20 or earlier. +-func validGoVersion(goVersion string) bool { +- if !goVersionRx.MatchString(goVersion) { +- return false // malformed version string - } - -- return pkgs +- // TODO(rfindley): remove once we no longer support building gopls with Go +- // 1.20 or earlier. +- if !slices.Contains(build.Default.ReleaseTags, "go1.21") && strings.Count(goVersion, ".") >= 2 { +- return false // unsupported patch version +- } +- +- return true -} - --// buildPackageHandle gets or builds a package handle for the given id, storing --// its result in the snapshot.packages map. +-// depsErrors creates diagnostics for each metadata error (e.g. import cycle). +-// These may be attached to import declarations in the transitive source files +-// of pkg, or to 'requires' declarations in the package's go.mod file. -// --// buildPackageHandle must only be called from getPackageHandles. --func (b *packageHandleBuilder) buildPackageHandle(ctx context.Context, n *handleNode) { -- var prevPH *packageHandle -- if n.ph != nil { -- // Existing package handle: if it is valid, return it. Otherwise, create a -- // copy to update. -- if n.ph.validated { -- return -- } -- prevPH = n.ph -- // Either prevPH is still valid, or we will update the key and depKeys of -- // this copy. In either case, the result will be valid. -- n.ph = prevPH.clone(true) -- } else { -- // No package handle: read and analyze the package syntax. -- inputs, err := b.s.typeCheckInputs(ctx, n.m) -- if err != nil { -- n.err = err -- return -- } -- refs, err := b.s.typerefs(ctx, n.m, inputs.compiledGoFiles) -- if err != nil { -- n.err = err -- return +-// TODO(rfindley): move this to load.go +-func depsErrors(ctx context.Context, snapshot *Snapshot, mp *metadata.Package) ([]*Diagnostic, error) { +- // Select packages that can't be found, and were imported in non-workspace packages. +- // Workspace packages already show their own errors. +- var relevantErrors []*packagesinternal.PackageError +- for _, depsError := range mp.DepsErrors { +- // Up to Go 1.15, the missing package was included in the stack, which +- // was presumably a bug. We want the next one up. +- directImporterIdx := len(depsError.ImportStack) - 1 +- if directImporterIdx < 0 { +- continue - } -- n.ph = &packageHandle{ -- m: n.m, -- localInputs: inputs, -- localKey: localPackageKey(inputs), -- refs: refs, -- validated: true, +- +- directImporter := depsError.ImportStack[directImporterIdx] +- if snapshot.isWorkspacePackage(PackageID(directImporter)) { +- continue - } +- relevantErrors = append(relevantErrors, depsError) - } - -- // ph either did not exist, or was invalid. We must re-evaluate deps and key. -- if err := b.evaluatePackageHandle(prevPH, n); err != nil { -- n.err = err -- return +- // Don't build the import index for nothing. +- if len(relevantErrors) == 0 { +- return nil, nil - } - -- assert(n.ph.validated, "unvalidated handle") -- -- // Ensure the result (or an equivalent) is recorded in the snapshot. -- b.s.mu.Lock() -- defer b.s.mu.Unlock() -- -- // Check that the metadata has not changed -- // (which should invalidate this handle). -- // -- // TODO(rfindley): eventually promote this to an assert. -- // TODO(rfindley): move this to after building the package handle graph? -- if b.s.meta.metadata[n.m.ID] != n.m { -- bug.Reportf("stale metadata for %s", n.m.ID) +- // Subsequent checks require Go files. +- if len(mp.CompiledGoFiles) == 0 { +- return nil, nil - } - -- // Check the packages map again in case another goroutine got there first. -- if alt, ok := b.s.packages.Get(n.m.ID); ok && alt.validated { -- if alt.m != n.m { -- bug.Reportf("existing package handle does not match for %s", n.m.ID) +- // Build an index of all imports in the package. +- type fileImport struct { +- cgf *parsego.File +- imp *ast.ImportSpec +- } +- allImports := map[string][]fileImport{} +- for _, uri := range mp.CompiledGoFiles { +- pgf, err := parseGoURI(ctx, snapshot, uri, parsego.Header) +- if err != nil { +- return nil, err +- } +- fset := tokeninternal.FileSetFor(pgf.Tok) +- // TODO(adonovan): modify Imports() to accept a single token.File (cgf.Tok). +- for _, group := range astutil.Imports(fset, pgf.File) { +- for _, imp := range group { +- if imp.Path == nil { +- continue +- } +- path := strings.Trim(imp.Path.Value, `"`) +- allImports[path] = append(allImports[path], fileImport{pgf, imp}) +- } - } -- n.ph = alt -- } else { -- b.s.packages.Set(n.m.ID, n.ph, nil) - } --} - --// evaluatePackageHandle validates and/or computes the key of ph, setting key, --// depKeys, and the validated flag on ph. --// --// It uses prevPH to avoid recomputing keys that can't have changed, since --// their depKeys did not change. --// --// See the documentation for packageHandle for more details about packageHandle --// state, and see the documentation for the typerefs package for more details --// about precise reachability analysis. --func (b *packageHandleBuilder) evaluatePackageHandle(prevPH *packageHandle, n *handleNode) error { -- // Opt: if no dep keys have changed, we need not re-evaluate the key. -- if prevPH != nil { -- depsChanged := false -- assert(len(prevPH.depKeys) == len(n.succs), "mismatching dep count") -- for id, succ := range n.succs { -- oldKey, ok := prevPH.depKeys[id] -- assert(ok, "missing dep") -- if oldKey != succ.ph.key { -- depsChanged = true +- // Apply a diagnostic to any import involved in the error, stopping once +- // we reach the workspace. +- var errors []*Diagnostic +- for _, depErr := range relevantErrors { +- for i := len(depErr.ImportStack) - 1; i >= 0; i-- { +- item := depErr.ImportStack[i] +- if snapshot.isWorkspacePackage(PackageID(item)) { - break - } -- } -- if !depsChanged { -- return nil // key cannot have changed +- +- for _, imp := range allImports[item] { +- rng, err := imp.cgf.NodeRange(imp.imp) +- if err != nil { +- return nil, err +- } +- diag := &Diagnostic{ +- URI: imp.cgf.URI, +- Range: rng, +- Severity: protocol.SeverityError, +- Source: TypeError, +- Message: fmt.Sprintf("error while importing %v: %v", item, depErr.Err), +- SuggestedFixes: goGetQuickFixes(mp.Module != nil, imp.cgf.URI, item), +- } +- if !bundleQuickFixes(diag) { +- bug.Reportf("failed to bundle fixes for diagnostic %q", diag.Message) +- } +- errors = append(errors, diag) +- } - } - } - -- // Deps have changed, so we must re-evaluate the key. -- n.ph.depKeys = make(map[PackageID]source.Hash) -- -- // See the typerefs package: the reachable set of packages is defined to be -- // the set of packages containing syntax that is reachable through the -- // exported symbols in the dependencies of n.ph. -- reachable := b.s.pkgIndex.NewSet() -- for depID, succ := range n.succs { -- n.ph.depKeys[depID] = succ.ph.key -- reachable.Add(succ.idxID) -- trefs := b.getTransitiveRefs(succ.m.ID) -- if trefs == nil { -- // A predecessor failed to build due to e.g. context cancellation. -- return fmt.Errorf("missing transitive refs for %s", succ.m.ID) -- } -- for _, set := range trefs { -- reachable.Union(set) -- } -- } -- -- // Collect reachable handles. -- var reachableHandles []*packageHandle -- // In the presence of context cancellation, any package may be missing. -- // We need all dependencies to produce a valid key. -- missingReachablePackage := false -- reachable.Elems(func(id typerefs.IndexID) { -- dh := b.nodes[id] -- if dh == nil { -- missingReachablePackage = true -- } else { -- assert(dh.ph.validated, "unvalidated dependency") -- reachableHandles = append(reachableHandles, dh.ph) -- } -- }) -- if missingReachablePackage { -- return fmt.Errorf("missing reachable package") -- } -- // Sort for stability. -- sort.Slice(reachableHandles, func(i, j int) bool { -- return reachableHandles[i].m.ID < reachableHandles[j].m.ID -- }) -- -- // Key is the hash of the local key, and the local key of all reachable -- // packages. -- depHasher := sha256.New() -- depHasher.Write(n.ph.localKey[:]) -- for _, rph := range reachableHandles { -- depHasher.Write(rph.localKey[:]) -- } -- depHasher.Sum(n.ph.key[:0]) -- -- return nil --} -- --// typerefs returns typerefs for the package described by m and cgfs, after --// either computing it or loading it from the file cache. --func (s *snapshot) typerefs(ctx context.Context, m *source.Metadata, cgfs []source.FileHandle) (map[string][]typerefs.Symbol, error) { -- imports := make(map[ImportPath]*source.Metadata) -- for impPath, id := range m.DepsByImpPath { -- if id != "" { -- imports[impPath] = s.Metadata(id) -- } -- } -- -- data, err := s.typerefData(ctx, m.ID, imports, cgfs) -- if err != nil { -- return nil, err -- } -- classes := typerefs.Decode(s.pkgIndex, m.ID, data) -- refs := make(map[string][]typerefs.Symbol) -- for _, class := range classes { -- for _, decl := range class.Decls { -- refs[decl] = class.Refs -- } -- } -- return refs, nil --} -- --// typerefData retrieves encoded typeref data from the filecache, or computes it on --// a cache miss. --func (s *snapshot) typerefData(ctx context.Context, id PackageID, imports map[ImportPath]*source.Metadata, cgfs []source.FileHandle) ([]byte, error) { -- key := typerefsKey(id, imports, cgfs) -- if data, err := filecache.Get(typerefsKind, key); err == nil { -- return data, nil -- } else if err != filecache.ErrNotFound { -- bug.Reportf("internal error reading typerefs data: %v", err) -- } -- -- pgfs, err := s.view.parseCache.parseFiles(ctx, token.NewFileSet(), source.ParseFull&^parser.ParseComments, true, cgfs...) -- if err != nil { -- return nil, err -- } -- data := typerefs.Encode(pgfs, id, imports) -- -- // Store the resulting data in the cache. -- go func() { -- if err := filecache.Set(typerefsKind, key, data); err != nil { -- event.Error(ctx, fmt.Sprintf("storing typerefs data for %s", id), err) -- } -- }() -- -- return data, nil --} -- --// typerefsKey produces a key for the reference information produced by the --// typerefs package. --func typerefsKey(id PackageID, imports map[ImportPath]*source.Metadata, compiledGoFiles []source.FileHandle) source.Hash { -- hasher := sha256.New() -- -- fmt.Fprintf(hasher, "typerefs: %s\n", id) -- -- importPaths := make([]string, 0, len(imports)) -- for impPath := range imports { -- importPaths = append(importPaths, string(impPath)) -- } -- sort.Strings(importPaths) -- for _, importPath := range importPaths { -- imp := imports[ImportPath(importPath)] -- // TODO(rfindley): strength reduce the typerefs.Export API to guarantee -- // that it only depends on these attributes of dependencies. -- fmt.Fprintf(hasher, "import %s %s %s", importPath, imp.ID, imp.Name) -- } -- -- fmt.Fprintf(hasher, "compiledGoFiles: %d\n", len(compiledGoFiles)) -- for _, fh := range compiledGoFiles { -- fmt.Fprintln(hasher, fh.FileIdentity()) -- } -- -- var hash [sha256.Size]byte -- hasher.Sum(hash[:0]) -- return hash --} -- --// typeCheckInputs contains the inputs of a call to typeCheckImpl, which --// type-checks a package. --// --// Part of the purpose of this type is to keep type checking in-sync with the --// package handle key, by explicitly identifying the inputs to type checking. --type typeCheckInputs struct { -- id PackageID -- -- // Used for type checking: -- pkgPath PackagePath -- name PackageName -- goFiles, compiledGoFiles []source.FileHandle -- sizes types.Sizes -- depsByImpPath map[ImportPath]PackageID -- goVersion string // packages.Module.GoVersion, e.g. "1.18" -- -- // Used for type check diagnostics: -- relatedInformation bool -- linkTarget string -- moduleMode bool --} -- --func (s *snapshot) typeCheckInputs(ctx context.Context, m *source.Metadata) (typeCheckInputs, error) { -- // Read both lists of files of this package. -- // -- // Parallelism is not necessary here as the files will have already been -- // pre-read at load time. -- // -- // goFiles aren't presented to the type checker--nor -- // are they included in the key, unsoundly--but their -- // syntax trees are available from (*pkg).File(URI). -- // TODO(adonovan): consider parsing them on demand? -- // The need should be rare. -- goFiles, err := readFiles(ctx, s, m.GoFiles) -- if err != nil { -- return typeCheckInputs{}, err -- } -- compiledGoFiles, err := readFiles(ctx, s, m.CompiledGoFiles) -- if err != nil { -- return typeCheckInputs{}, err -- } -- -- goVersion := "" -- if m.Module != nil && m.Module.GoVersion != "" { -- goVersion = m.Module.GoVersion -- } -- -- return typeCheckInputs{ -- id: m.ID, -- pkgPath: m.PkgPath, -- name: m.Name, -- goFiles: goFiles, -- compiledGoFiles: compiledGoFiles, -- sizes: m.TypesSizes, -- depsByImpPath: m.DepsByImpPath, -- goVersion: goVersion, -- -- relatedInformation: s.Options().RelatedInformationSupported, -- linkTarget: s.Options().LinkTarget, -- moduleMode: s.view.moduleMode(), -- }, nil --} -- --// readFiles reads the content of each file URL from the source --// (e.g. snapshot or cache). --func readFiles(ctx context.Context, fs source.FileSource, uris []span.URI) (_ []source.FileHandle, err error) { -- fhs := make([]source.FileHandle, len(uris)) -- for i, uri := range uris { -- fhs[i], err = fs.ReadFile(ctx, uri) -- if err != nil { -- return nil, err -- } -- } -- return fhs, nil --} -- --// localPackageKey returns a key for local inputs into type-checking, excluding --// dependency information: files, metadata, and configuration. --func localPackageKey(inputs typeCheckInputs) source.Hash { -- hasher := sha256.New() -- -- // In principle, a key must be the hash of an -- // unambiguous encoding of all the relevant data. -- // If it's ambiguous, we risk collisions. -- -- // package identifiers -- fmt.Fprintf(hasher, "package: %s %s %s\n", inputs.id, inputs.name, inputs.pkgPath) -- -- // module Go version -- fmt.Fprintf(hasher, "go %s\n", inputs.goVersion) -- -- // import map -- importPaths := make([]string, 0, len(inputs.depsByImpPath)) -- for impPath := range inputs.depsByImpPath { -- importPaths = append(importPaths, string(impPath)) -- } -- sort.Strings(importPaths) -- for _, impPath := range importPaths { -- fmt.Fprintf(hasher, "import %s %s", impPath, string(inputs.depsByImpPath[ImportPath(impPath)])) -- } -- -- // file names and contents -- fmt.Fprintf(hasher, "compiledGoFiles: %d\n", len(inputs.compiledGoFiles)) -- for _, fh := range inputs.compiledGoFiles { -- fmt.Fprintln(hasher, fh.FileIdentity()) -- } -- fmt.Fprintf(hasher, "goFiles: %d\n", len(inputs.goFiles)) -- for _, fh := range inputs.goFiles { -- fmt.Fprintln(hasher, fh.FileIdentity()) -- } -- -- // types sizes -- wordSize := inputs.sizes.Sizeof(types.Typ[types.Int]) -- maxAlign := inputs.sizes.Alignof(types.NewPointer(types.Typ[types.Int64])) -- fmt.Fprintf(hasher, "sizes: %d %d\n", wordSize, maxAlign) -- -- fmt.Fprintf(hasher, "relatedInformation: %t\n", inputs.relatedInformation) -- fmt.Fprintf(hasher, "linkTarget: %s\n", inputs.linkTarget) -- fmt.Fprintf(hasher, "moduleMode: %t\n", inputs.moduleMode) -- -- var hash [sha256.Size]byte -- hasher.Sum(hash[:0]) -- return hash --} -- --// typeCheckImpl type checks the parsed source files in compiledGoFiles. --// (The resulting pkg also holds the parsed but not type-checked goFiles.) --// deps holds the future results of type-checking the direct dependencies. --func typeCheckImpl(ctx context.Context, b *typeCheckBatch, inputs typeCheckInputs) (*syntaxPackage, error) { -- ctx, done := event.Start(ctx, "cache.typeCheck", tag.Package.Of(string(inputs.id))) -- defer done() -- -- pkg, err := doTypeCheck(ctx, b, inputs) -- if err != nil { -- return nil, err -- } -- -- // Our heuristic for whether to show type checking errors is: -- // + If any file was 'fixed', don't show type checking errors as we -- // can't guarantee that they reference accurate locations in the source. -- // + If there is a parse error _in the current file_, suppress type -- // errors in that file. -- // + Otherwise, show type errors even in the presence of parse errors in -- // other package files. go/types attempts to suppress follow-on errors -- // due to bad syntax, so on balance type checking errors still provide -- // a decent signal/noise ratio as long as the file in question parses. -- -- // Track URIs with parse errors so that we can suppress type errors for these -- // files. -- unparseable := map[span.URI]bool{} -- for _, e := range pkg.parseErrors { -- diags, err := parseErrorDiagnostics(pkg, e) -- if err != nil { -- event.Error(ctx, "unable to compute positions for parse errors", err, tag.Package.Of(string(inputs.id))) -- continue -- } -- for _, diag := range diags { -- unparseable[diag.URI] = true -- pkg.diagnostics = append(pkg.diagnostics, diag) -- } -- } -- -- if pkg.hasFixedFiles { -- return pkg, nil -- } -- -- unexpanded := pkg.typeErrors -- pkg.typeErrors = nil -- for _, e := range expandErrors(unexpanded, inputs.relatedInformation) { -- diags, err := typeErrorDiagnostics(inputs.moduleMode, inputs.linkTarget, pkg, e) -- if err != nil { -- // If we fail here and there are no parse errors, it means we are hiding -- // a valid type-checking error from the user. This must be a bug, with -- // one exception: relocated primary errors may fail processing, because -- // they reference locations outside of the package. -- if len(pkg.parseErrors) == 0 && !e.relocated { -- bug.Reportf("failed to compute position for type error %v: %v", e, err) -- } -- continue -- } -- pkg.typeErrors = append(pkg.typeErrors, e.primary) -- for _, diag := range diags { -- // If the file didn't parse cleanly, it is highly likely that type -- // checking errors will be confusing or redundant. But otherwise, type -- // checking usually provides a good enough signal to include. -- if !unparseable[diag.URI] { -- pkg.diagnostics = append(pkg.diagnostics, diag) -- } -- } -- } -- -- // Work around golang/go#61561: interface instances aren't concurrency-safe -- // as they are not completed by the type checker. -- for _, inst := range typeparams.GetInstances(pkg.typesInfo) { -- if iface, _ := inst.Type.Underlying().(*types.Interface); iface != nil { -- iface.Complete() -- } -- } -- -- return pkg, nil --} -- --// TODO(golang/go#63472): this looks wrong with the new Go version syntax. --var goVersionRx = regexp.MustCompile(`^go([1-9][0-9]*)\.(0|[1-9][0-9]*)$`) -- --func doTypeCheck(ctx context.Context, b *typeCheckBatch, inputs typeCheckInputs) (*syntaxPackage, error) { -- pkg := &syntaxPackage{ -- id: inputs.id, -- fset: b.fset, // must match parse call below -- types: types.NewPackage(string(inputs.pkgPath), string(inputs.name)), -- typesInfo: &types.Info{ -- Types: make(map[ast.Expr]types.TypeAndValue), -- Defs: make(map[*ast.Ident]types.Object), -- Uses: make(map[*ast.Ident]types.Object), -- Implicits: make(map[ast.Node]types.Object), -- Selections: make(map[*ast.SelectorExpr]*types.Selection), -- Scopes: make(map[ast.Node]*types.Scope), -- }, -- } -- typeparams.InitInstanceInfo(pkg.typesInfo) -- -- // Collect parsed files from the type check pass, capturing parse errors from -- // compiled files. -- var err error -- pkg.goFiles, err = b.parseCache.parseFiles(ctx, b.fset, source.ParseFull, false, inputs.goFiles...) -- if err != nil { -- return nil, err -- } -- pkg.compiledGoFiles, err = b.parseCache.parseFiles(ctx, b.fset, source.ParseFull, false, inputs.compiledGoFiles...) -- if err != nil { -- return nil, err -- } -- for _, pgf := range pkg.compiledGoFiles { -- if pgf.ParseErr != nil { -- pkg.parseErrors = append(pkg.parseErrors, pgf.ParseErr) -- } -- } -- -- // Use the default type information for the unsafe package. -- if inputs.pkgPath == "unsafe" { -- // Don't type check Unsafe: it's unnecessary, and doing so exposes a data -- // race to Unsafe.completed. -- pkg.types = types.Unsafe -- return pkg, nil -- } -- -- if len(pkg.compiledGoFiles) == 0 { -- // No files most likely means go/packages failed. -- // -- // TODO(rfindley): in the past, we would capture go list errors in this -- // case, to present go list errors to the user. However we had no tests for -- // this behavior. It is unclear if anything better can be done here. -- return nil, fmt.Errorf("no parsed files for package %s", inputs.pkgPath) -- } -- -- onError := func(e error) { -- pkg.typeErrors = append(pkg.typeErrors, e.(types.Error)) -- } -- cfg := b.typesConfig(ctx, inputs, onError) -- -- check := types.NewChecker(cfg, pkg.fset, pkg.types, pkg.typesInfo) -- -- var files []*ast.File -- for _, cgf := range pkg.compiledGoFiles { -- files = append(files, cgf.File) -- } -- -- // Type checking is expensive, and we may not have ecountered cancellations -- // via parsing (e.g. if we got nothing but cache hits for parsed files). -- if ctx.Err() != nil { -- return nil, ctx.Err() -- } -- -- // Type checking errors are handled via the config, so ignore them here. -- _ = check.Files(files) // 50us-15ms, depending on size of package -- -- // If the context was cancelled, we may have returned a ton of transient -- // errors to the type checker. Swallow them. -- if ctx.Err() != nil { -- return nil, ctx.Err() -- } -- -- // Collect imports by package path for the DependencyTypes API. -- pkg.importMap = make(map[PackagePath]*types.Package) -- var collectDeps func(*types.Package) -- collectDeps = func(p *types.Package) { -- pkgPath := PackagePath(p.Path()) -- if _, ok := pkg.importMap[pkgPath]; ok { -- return -- } -- pkg.importMap[pkgPath] = p -- for _, imp := range p.Imports() { -- collectDeps(imp) -- } -- } -- collectDeps(pkg.types) -- -- return pkg, nil --} -- --func (b *typeCheckBatch) typesConfig(ctx context.Context, inputs typeCheckInputs, onError func(e error)) *types.Config { -- cfg := &types.Config{ -- Sizes: inputs.sizes, -- Error: onError, -- Importer: importerFunc(func(path string) (*types.Package, error) { -- // While all of the import errors could be reported -- // based on the metadata before we start type checking, -- // reporting them via types.Importer places the errors -- // at the correct source location. -- id, ok := inputs.depsByImpPath[ImportPath(path)] -- if !ok { -- // If the import declaration is broken, -- // go list may fail to report metadata about it. -- // See TestFixImportDecl for an example. -- return nil, fmt.Errorf("missing metadata for import of %q", path) -- } -- depPH := b.handles[id] -- if depPH == nil { -- // e.g. missing metadata for dependencies in buildPackageHandle -- return nil, missingPkgError(inputs.id, path, inputs.moduleMode) -- } -- if !source.IsValidImport(inputs.pkgPath, depPH.m.PkgPath) { -- return nil, fmt.Errorf("invalid use of internal package %q", path) -- } -- return b.getImportPackage(ctx, id) -- }), -- } -- -- if inputs.goVersion != "" { -- goVersion := "go" + inputs.goVersion -- // types.NewChecker panics if GoVersion is invalid. An unparsable mod -- // file should probably stop us before we get here, but double check -- // just in case. -- if goVersionRx.MatchString(goVersion) { -- typesinternal.SetGoVersion(cfg, goVersion) -- } -- } -- -- // We want to type check cgo code if go/types supports it. -- // We passed typecheckCgo to go/packages when we Loaded. -- typesinternal.SetUsesCgo(cfg) -- return cfg --} -- --// depsErrors creates diagnostics for each metadata error (e.g. import cycle). --// These may be attached to import declarations in the transitive source files --// of pkg, or to 'requires' declarations in the package's go.mod file. --// --// TODO(rfindley): move this to load.go --func depsErrors(ctx context.Context, m *source.Metadata, meta *metadataGraph, fs source.FileSource, workspacePackages map[PackageID]PackagePath) ([]*source.Diagnostic, error) { -- // Select packages that can't be found, and were imported in non-workspace packages. -- // Workspace packages already show their own errors. -- var relevantErrors []*packagesinternal.PackageError -- for _, depsError := range m.DepsErrors { -- // Up to Go 1.15, the missing package was included in the stack, which -- // was presumably a bug. We want the next one up. -- directImporterIdx := len(depsError.ImportStack) - 1 -- if directImporterIdx < 0 { -- continue -- } -- -- directImporter := depsError.ImportStack[directImporterIdx] -- if _, ok := workspacePackages[PackageID(directImporter)]; ok { -- continue -- } -- relevantErrors = append(relevantErrors, depsError) -- } -- -- // Don't build the import index for nothing. -- if len(relevantErrors) == 0 { -- return nil, nil -- } -- -- // Subsequent checks require Go files. -- if len(m.CompiledGoFiles) == 0 { -- return nil, nil -- } -- -- // Build an index of all imports in the package. -- type fileImport struct { -- cgf *source.ParsedGoFile -- imp *ast.ImportSpec -- } -- allImports := map[string][]fileImport{} -- for _, uri := range m.CompiledGoFiles { -- pgf, err := parseGoURI(ctx, fs, uri, source.ParseHeader) -- if err != nil { -- return nil, err -- } -- fset := tokeninternal.FileSetFor(pgf.Tok) -- // TODO(adonovan): modify Imports() to accept a single token.File (cgf.Tok). -- for _, group := range astutil.Imports(fset, pgf.File) { -- for _, imp := range group { -- if imp.Path == nil { -- continue -- } -- path := strings.Trim(imp.Path.Value, `"`) -- allImports[path] = append(allImports[path], fileImport{pgf, imp}) -- } -- } -- } -- -- // Apply a diagnostic to any import involved in the error, stopping once -- // we reach the workspace. -- var errors []*source.Diagnostic -- for _, depErr := range relevantErrors { -- for i := len(depErr.ImportStack) - 1; i >= 0; i-- { -- item := depErr.ImportStack[i] -- if _, ok := workspacePackages[PackageID(item)]; ok { -- break -- } -- -- for _, imp := range allImports[item] { -- rng, err := imp.cgf.NodeRange(imp.imp) -- if err != nil { -- return nil, err -- } -- fixes, err := goGetQuickFixes(m.Module != nil, imp.cgf.URI, item) -- if err != nil { -- return nil, err -- } -- diag := &source.Diagnostic{ -- URI: imp.cgf.URI, -- Range: rng, -- Severity: protocol.SeverityError, -- Source: source.TypeError, -- Message: fmt.Sprintf("error while importing %v: %v", item, depErr.Err), -- SuggestedFixes: fixes, -- } -- if !source.BundleQuickFixes(diag) { -- bug.Reportf("failed to bundle fixes for diagnostic %q", diag.Message) -- } -- errors = append(errors, diag) -- } -- } -- } -- -- modFile, err := nearestModFile(ctx, m.CompiledGoFiles[0], fs) +- modFile, err := nearestModFile(ctx, mp.CompiledGoFiles[0], snapshot) - if err != nil { - return nil, err - } -- pm, err := parseModURI(ctx, fs, modFile) +- pm, err := parseModURI(ctx, snapshot, modFile) - if err != nil { - return nil, err - } @@ -16690,11 +16782,11 @@ diff -urN a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.g - for _, depErr := range relevantErrors { - for i := len(depErr.ImportStack) - 1; i >= 0; i-- { - item := depErr.ImportStack[i] -- m := meta.metadata[PackageID(item)] -- if m == nil || m.Module == nil { +- mp := snapshot.Metadata(PackageID(item)) +- if mp == nil || mp.Module == nil { - continue - } -- modVer := module.Version{Path: m.Module.Path, Version: m.Module.Version} +- modVer := module.Version{Path: mp.Module.Path, Version: mp.Module.Version} - reference := findModuleReference(pm.File, modVer) - if reference == nil { - continue @@ -16703,19 +16795,15 @@ diff -urN a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.g - if err != nil { - return nil, err - } -- fixes, err := goGetQuickFixes(true, pm.URI, item) -- if err != nil { -- return nil, err -- } -- diag := &source.Diagnostic{ +- diag := &Diagnostic{ - URI: pm.URI, - Range: rng, - Severity: protocol.SeverityError, -- Source: source.TypeError, +- Source: TypeError, - Message: fmt.Sprintf("error while importing %v: %v", item, depErr.Err), -- SuggestedFixes: fixes, +- SuggestedFixes: goGetQuickFixes(true, pm.URI, item), - } -- if !source.BundleQuickFixes(diag) { +- if !bundleQuickFixes(diag) { - bug.Reportf("failed to bundle fixes for diagnostic %q", diag.Message) - } - errors = append(errors, diag) @@ -16732,7 +16820,7 @@ diff -urN a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.g - // access to the full snapshot, and could provide more information (such as - // the initialization error). - if moduleMode { -- if source.IsCommandLineArguments(from) { +- if metadata.IsCommandLineArguments(from) { - return fmt.Errorf("current file is not included in a workspace module") - } else { - // Previously, we would present the initialization error here. @@ -16744,81 +16832,182 @@ diff -urN a/gopls/internal/lsp/cache/check.go b/gopls/internal/lsp/cache/check.g - } -} - --type extendedError struct { -- relocated bool // if set, this is a relocation of a primary error to a secondary location -- primary types.Error -- secondaries []types.Error --} +-// typeErrorsToDiagnostics translates a slice of types.Errors into a slice of +-// Diagnostics. +-// +-// In addition to simply mapping data such as position information and error +-// codes, this function interprets related go/types "continuation" errors as +-// protocol.DiagnosticRelatedInformation. Continuation errors are go/types +-// errors whose messages starts with "\t". By convention, these errors relate +-// to the previous error in the errs slice (such as if they were printed in +-// sequence to a terminal). +-// +-// The linkTarget, moduleMode, and supportsRelatedInformation parameters affect +-// the construction of protocol objects (see the code for details). +-func typeErrorsToDiagnostics(pkg *syntaxPackage, errs []types.Error, linkTarget string, moduleMode, supportsRelatedInformation bool) []*Diagnostic { +- var result []*Diagnostic +- +- // batch records diagnostics for a set of related types.Errors. +- batch := func(related []types.Error) { +- var diags []*Diagnostic +- for i, e := range related { +- code, start, end, ok := typesinternal.ReadGo116ErrorData(e) +- if !ok || !start.IsValid() || !end.IsValid() { +- start, end = e.Pos, e.Pos +- code = 0 +- } +- if !start.IsValid() { +- // Type checker errors may be missing position information if they +- // relate to synthetic syntax, such as if the file were fixed. In that +- // case, we should have a parse error anyway, so skipping the type +- // checker error is likely benign. +- // +- // TODO(golang/go#64335): we should eventually verify that all type +- // checked syntax has valid positions, and promote this skip to a bug +- // report. +- continue +- } - --func (e extendedError) Error() string { -- return e.primary.Error() --} +- // Invariant: both start and end are IsValid. +- if !end.IsValid() { +- panic("end is invalid") +- } - --// expandErrors duplicates "secondary" errors by mapping them to their main --// error. Some errors returned by the type checker are followed by secondary --// errors which give more information about the error. These are errors in --// their own right, and they are marked by starting with \t. For instance, when --// there is a multiply-defined function, the secondary error points back to the --// definition first noticed. --// --// This function associates the secondary error with its primary error, which can --// then be used as RelatedInformation when the error becomes a diagnostic. --// --// If supportsRelatedInformation is false, the secondary is instead embedded as --// additional context in the primary error. --func expandErrors(errs []types.Error, supportsRelatedInformation bool) []extendedError { -- var result []extendedError -- for i := 0; i < len(errs); { -- original := extendedError{ -- primary: errs[i], -- } -- for i++; i < len(errs); i++ { -- spl := errs[i] -- if len(spl.Msg) == 0 || spl.Msg[0] != '\t' { -- break +- posn := safetoken.StartPosition(e.Fset, start) +- if !posn.IsValid() { +- // All valid positions produced by the type checker should described by +- // its fileset. +- // +- // Note: in golang/go#64488, we observed an error that was positioned +- // over fixed syntax, which overflowed its file. So it's definitely +- // possible that we get here (it's hard to reason about fixing up the +- // AST). Nevertheless, it's a bug. +- bug.Reportf("internal error: type checker error %q outside its Fset", e) +- continue +- } +- pgf, err := pkg.File(protocol.URIFromPath(posn.Filename)) +- if err != nil { +- // Sometimes type-checker errors refer to positions in other packages, +- // such as when a declaration duplicates a dot-imported name. +- // +- // In these cases, we don't want to report an error in the other +- // package (the message would be rather confusing), but we do want to +- // report an error in the current package (golang/go#59005). +- if i == 0 { +- bug.Reportf("internal error: could not locate file for primary type checker error %v: %v", e, err) +- } +- continue - } -- spl.Msg = spl.Msg[1:] -- original.secondaries = append(original.secondaries, spl) -- } - -- // Clone the error to all its related locations -- VS Code, at least, -- // doesn't do it for us. -- result = append(result, original) -- for i, mainSecondary := range original.secondaries { -- // Create the new primary error, with a tweaked message, in the -- // secondary's location. We need to start from the secondary to -- // capture its unexported location fields. -- relocatedSecondary := mainSecondary -- if supportsRelatedInformation { -- relocatedSecondary.Msg = fmt.Sprintf("%v (see details)", original.primary.Msg) +- // debugging #65960 +- // +- // At this point, we know 'start' IsValid, and +- // StartPosition(start) worked (with e.Fset). +- // +- // If the asserted condition is true, 'start' +- // is also in range for pgf.Tok, which means +- // the PosRange failure must be caused by 'end'. +- if pgf.Tok != e.Fset.File(start) { +- bug.Reportf("internal error: inconsistent token.Files for pos") +- } +- +- if end == start { +- // Expand the end position to a more meaningful span. +- end = analysisinternal.TypeErrorEndPos(e.Fset, pgf.Src, start) +- +- // debugging #65960 +- if _, err := safetoken.Offset(pgf.Tok, end); err != nil { +- bug.Reportf("TypeErrorEndPos returned invalid end: %v", err) +- } - } else { -- relocatedSecondary.Msg = fmt.Sprintf("%v (this error: %v)", original.primary.Msg, mainSecondary.Msg) +- // debugging #65960 +- if _, err := safetoken.Offset(pgf.Tok, end); err != nil { +- bug.Reportf("ReadGo116ErrorData returned invalid end: %v", err) +- } - } -- relocatedSecondary.Soft = original.primary.Soft - -- // Copy over the secondary errors, noting the location of the -- // current error we're cloning. -- clonedError := extendedError{relocated: true, primary: relocatedSecondary, secondaries: []types.Error{original.primary}} -- for j, secondary := range original.secondaries { -- if i == j { -- secondary.Msg += " (this error)" +- rng, err := pgf.Mapper.PosRange(pgf.Tok, start, end) +- if err != nil { +- bug.Reportf("internal error: could not compute pos to range for %v: %v", e, err) +- continue +- } +- msg := related[0].Msg +- if i > 0 { +- if supportsRelatedInformation { +- msg += " (see details)" +- } else { +- msg += fmt.Sprintf(" (this error: %v)", e.Msg) - } -- clonedError.secondaries = append(clonedError.secondaries, secondary) - } -- result = append(result, clonedError) +- diag := &Diagnostic{ +- URI: pgf.URI, +- Range: rng, +- Severity: protocol.SeverityError, +- Source: TypeError, +- Message: msg, +- } +- if code != 0 { +- diag.Code = code.String() +- diag.CodeHref = typesCodeHref(linkTarget, code) +- } +- if code == typesinternal.UnusedVar || code == typesinternal.UnusedImport { +- diag.Tags = append(diag.Tags, protocol.Unnecessary) +- } +- if match := importErrorRe.FindStringSubmatch(e.Msg); match != nil { +- diag.SuggestedFixes = append(diag.SuggestedFixes, goGetQuickFixes(moduleMode, pgf.URI, match[1])...) +- } +- if match := unsupportedFeatureRe.FindStringSubmatch(e.Msg); match != nil { +- diag.SuggestedFixes = append(diag.SuggestedFixes, editGoDirectiveQuickFix(moduleMode, pgf.URI, match[1])...) +- } +- +- // Link up related information. For the primary error, all related errors +- // are treated as related information. For secondary errors, only the +- // primary is related. +- // +- // This is because go/types assumes that errors are read top-down, such as +- // in the cycle error "A refers to...". The structure of the secondary +- // error set likely only makes sense for the primary error. +- if i > 0 { +- primary := diags[0] +- primary.Related = append(primary.Related, protocol.DiagnosticRelatedInformation{ +- Location: protocol.Location{URI: diag.URI, Range: diag.Range}, +- Message: related[i].Msg, // use the unmodified secondary error for related errors. +- }) +- diag.Related = []protocol.DiagnosticRelatedInformation{{ +- Location: protocol.Location{URI: primary.URI, Range: primary.Range}, +- }} +- } +- diags = append(diags, diag) - } +- result = append(result, diags...) - } -- return result --} -- --// An importFunc is an implementation of the single-method --// types.Importer interface based on a function value. --type importerFunc func(path string) (*types.Package, error) - --func (f importerFunc) Import(path string) (*types.Package, error) { return f(path) } -diff -urN a/gopls/internal/lsp/cache/constraints.go b/gopls/internal/lsp/cache/constraints.go ---- a/gopls/internal/lsp/cache/constraints.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/constraints.go 1970-01-01 00:00:00.000000000 +0000 +- // Process batches of related errors. +- for len(errs) > 0 { +- related := []types.Error{errs[0]} +- for i := 1; i < len(errs); i++ { +- spl := errs[i] +- if len(spl.Msg) == 0 || spl.Msg[0] != '\t' { +- break +- } +- spl.Msg = spl.Msg[len("\t"):] +- related = append(related, spl) +- } +- batch(related) +- errs = errs[len(related):] +- } +- +- return result +-} +- +-// An importFunc is an implementation of the single-method +-// types.Importer interface based on a function value. +-type importerFunc func(path string) (*types.Package, error) +- +-func (f importerFunc) Import(path string) (*types.Package, error) { return f(path) } +diff -urN a/gopls/internal/cache/constraints.go b/gopls/internal/cache/constraints.go +--- a/gopls/internal/cache/constraints.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/constraints.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,61 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style @@ -16881,10 +17070,10 @@ diff -urN a/gopls/internal/lsp/cache/constraints.go b/gopls/internal/lsp/cache/c - } - } -} -diff -urN a/gopls/internal/lsp/cache/constraints_test.go b/gopls/internal/lsp/cache/constraints_test.go ---- a/gopls/internal/lsp/cache/constraints_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/constraints_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,96 +0,0 @@ +diff -urN a/gopls/internal/cache/constraints_test.go b/gopls/internal/cache/constraints_test.go +--- a/gopls/internal/cache/constraints_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/constraints_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,126 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. @@ -16981,10 +17170,56 @@ diff -urN a/gopls/internal/lsp/cache/constraints_test.go b/gopls/internal/lsp/ca - }) - } -} -diff -urN a/gopls/internal/lsp/cache/cycle_test.go b/gopls/internal/lsp/cache/cycle_test.go ---- a/gopls/internal/lsp/cache/cycle_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/cycle_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,181 +0,0 @@ +- +-func TestVersionRegexp(t *testing.T) { +- // good +- for _, s := range []string{ +- "go1", +- "go1.2", +- "go1.2.3", +- "go1.0.33", +- } { +- if !goVersionRx.MatchString(s) { +- t.Errorf("Valid Go version %q does not match the regexp", s) +- } +- } +- +- // bad +- for _, s := range []string{ +- "go", // missing numbers +- "go0", // Go starts at 1 +- "go01", // leading zero +- "go1.π", // non-decimal +- "go1.-1", // negative +- "go1.02.3", // leading zero +- "go1.2.3.4", // too many segments +- "go1.2.3-pre", // textual suffix +- } { +- if goVersionRx.MatchString(s) { +- t.Errorf("Invalid Go version %q unexpectedly matches the regexp", s) +- } +- } +-} +diff -urN a/gopls/internal/cache/debug.go b/gopls/internal/cache/debug.go +--- a/gopls/internal/cache/debug.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/debug.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,12 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package cache +- +-// assert panics with the given msg if cond is not true. +-func assert(cond bool, msg string) { +- if !cond { +- panic(msg) +- } +-} +diff -urN a/gopls/internal/cache/diagnostics.go b/gopls/internal/cache/diagnostics.go +--- a/gopls/internal/cache/diagnostics.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/diagnostics.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,190 +0,0 @@ -// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. @@ -16992,200 +17227,193 @@ diff -urN a/gopls/internal/lsp/cache/cycle_test.go b/gopls/internal/lsp/cache/cy -package cache - -import ( -- "sort" -- "strings" -- "testing" +- "encoding/json" +- "fmt" - -- "golang.org/x/tools/gopls/internal/lsp/source" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/bug" -) - --// This is an internal test of the breakImportCycles logic. --func TestBreakImportCycles(t *testing.T) { -- -- type Graph = map[PackageID]*source.Metadata -- -- // cyclic returns a description of a cycle, -- // if the graph is cyclic, otherwise "". -- cyclic := func(graph Graph) string { -- const ( -- unvisited = 0 -- visited = 1 -- onstack = 2 -- ) -- color := make(map[PackageID]int) -- var visit func(id PackageID) string -- visit = func(id PackageID) string { -- switch color[id] { -- case unvisited: -- color[id] = onstack -- case onstack: -- return string(id) // cycle! -- case visited: -- return "" -- } -- if m := graph[id]; m != nil { -- for _, depID := range m.DepsByPkgPath { -- if cycle := visit(depID); cycle != "" { -- return string(id) + "->" + cycle -- } -- } -- } -- color[id] = visited -- return "" -- } -- for id := range graph { -- if cycle := visit(id); cycle != "" { -- return cycle -- } -- } -- return "" -- } +-// A InitializationError is an error that causes snapshot initialization to fail. +-// It is either the error returned from go/packages.Load, or an error parsing a +-// workspace go.work or go.mod file. +-// +-// Such an error generally indicates that the View is malformed, and will never +-// be usable. +-type InitializationError struct { +- // MainError is the primary error. Must be non-nil. +- MainError error - -- // parse parses an import dependency graph. -- // The input is a semicolon-separated list of node descriptions. -- // Each node description is a package ID, optionally followed by -- // "->" and a comma-separated list of successor IDs. -- // Thus "a->b;b->c,d;e" represents the set of nodes {a,b,e} -- // and the set of edges {a->b, b->c, b->d}. -- parse := func(s string) Graph { -- m := make(Graph) -- makeNode := func(name string) *source.Metadata { -- id := PackageID(name) -- n, ok := m[id] -- if !ok { -- n = &source.Metadata{ -- ID: id, -- DepsByPkgPath: make(map[PackagePath]PackageID), -- } -- m[id] = n -- } -- return n -- } -- if s != "" { -- for _, item := range strings.Split(s, ";") { -- nodeID, succIDs, ok := strings.Cut(item, "->") -- node := makeNode(nodeID) -- if ok { -- for _, succID := range strings.Split(succIDs, ",") { -- node.DepsByPkgPath[PackagePath(succID)] = PackageID(succID) -- } -- } -- } -- } -- return m -- } +- // Diagnostics contains any supplemental (structured) diagnostics extracted +- // from the load error. +- Diagnostics map[protocol.DocumentURI][]*Diagnostic +-} - -- // Sanity check of cycle detector. -- { -- got := cyclic(parse("a->b;b->c;c->a,d")) -- has := func(s string) bool { return strings.Contains(got, s) } -- if !(has("a->b") && has("b->c") && has("c->a") && !has("d")) { -- t.Fatalf("cyclic: got %q, want a->b->c->a or equivalent", got) -- } -- } +-func byURI(d *Diagnostic) protocol.DocumentURI { return d.URI } // For use in maps.Group. - -- // format formats an import graph, in lexicographic order, -- // in the notation of parse, but with a "!" after the name -- // of each node that has errors. -- format := func(graph Graph) string { -- var items []string -- for _, m := range graph { -- item := string(m.ID) -- if len(m.Errors) > 0 { -- item += "!" -- } -- var succs []string -- for _, depID := range m.DepsByPkgPath { -- succs = append(succs, string(depID)) -- } -- if succs != nil { -- sort.Strings(succs) -- item += "->" + strings.Join(succs, ",") -- } -- items = append(items, item) -- } -- sort.Strings(items) -- return strings.Join(items, ";") -- } +-// An Diagnostic corresponds to an LSP Diagnostic. +-// https://microsoft.github.io/language-server-protocol/specification#diagnostic +-// +-// It is (effectively) gob-serializable; see {encode,decode}Diagnostics. +-type Diagnostic struct { +- URI protocol.DocumentURI // of diagnosed file (not diagnostic documentation) +- Range protocol.Range +- Severity protocol.DiagnosticSeverity +- Code string // analysis.Diagnostic.Category (or "default" if empty) or hidden go/types error code +- CodeHref string - -- // We needn't test self-cycles as they are eliminated at Metadata construction. -- for _, test := range []struct { -- metadata, updates, want string -- }{ -- // Simple 2-cycle. -- {"a->b", "b->a", -- "a->b;b!"}, // broke b->a +- // Source is a human-readable description of the source of the error. +- // Diagnostics generated by an analysis.Analyzer set it to Analyzer.Name. +- Source DiagnosticSource - -- {"a->b;b->c;c", "b->a,c", -- "a->b;b!->c;c"}, // broke b->a +- Message string - -- // Reversing direction of p->s edge creates pqrs cycle. -- {"a->p,q,r,s;p->q,s,z;q->r,z;r->s,z;s->z", "p->q,z;s->p,z", -- "a->p,q,r,s;p!->z;q->r,z;r->s,z;s!->z"}, // broke p->q, s->p +- Tags []protocol.DiagnosticTag +- Related []protocol.DiagnosticRelatedInformation - -- // We break all intra-SCC edges from updated nodes, -- // which may be more than necessary (e.g. a->b). -- {"a->b;b->c;c;d->a", "a->b,e;c->d", -- "a!->e;b->c;c!;d->a"}, // broke a->b, c->d -- } { -- metadata := parse(test.metadata) -- updates := parse(test.updates) +- // Fields below are used internally to generate quick fixes. They aren't +- // part of the LSP spec and historically didn't leave the server. +- // +- // Update(2023-05): version 3.16 of the LSP spec included support for the +- // Diagnostic.data field, which holds arbitrary data preserved in the +- // diagnostic for codeAction requests. This field allows bundling additional +- // information for quick-fixes, and gopls can (and should) use this +- // information to avoid re-evaluating diagnostics in code-action handlers. +- // +- // In order to stage this transition incrementally, the 'BundledFixes' field +- // may store a 'bundled' (=json-serialized) form of the associated +- // SuggestedFixes. Not all diagnostics have their fixes bundled. +- BundledFixes *json.RawMessage +- SuggestedFixes []SuggestedFix +-} - -- if cycle := cyclic(metadata); cycle != "" { -- t.Errorf("initial metadata %s has cycle %s: ", format(metadata), cycle) -- continue -- } +-func (d *Diagnostic) String() string { +- return fmt.Sprintf("%v: %s", d.Range, d.Message) +-} - -- t.Log("initial", format(metadata)) +-type DiagnosticSource string - -- // Apply updates. -- // (parse doesn't have a way to express node deletions, -- // but they aren't very interesting.) -- for id, m := range updates { -- metadata[id] = m -- } +-const ( +- UnknownError DiagnosticSource = "" +- ListError DiagnosticSource = "go list" +- ParseError DiagnosticSource = "syntax" +- TypeError DiagnosticSource = "compiler" +- ModTidyError DiagnosticSource = "go mod tidy" +- OptimizationDetailsError DiagnosticSource = "optimizer details" +- UpgradeNotification DiagnosticSource = "upgrade available" +- Vulncheck DiagnosticSource = "vulncheck imports" +- Govulncheck DiagnosticSource = "govulncheck" +- TemplateError DiagnosticSource = "template" +- WorkFileError DiagnosticSource = "go.work file" +- ConsistencyInfo DiagnosticSource = "consistency" +-) - -- t.Log("updated", format(metadata)) +-// A SuggestedFix represents a suggested fix (for a diagnostic) +-// produced by analysis, in protocol form. +-// +-// The fixes are reported to the client as a set of code actions in +-// response to a CodeAction query for a set of diagnostics. Multiple +-// SuggestedFixes may be produced for the same logical fix, varying +-// only in ActionKind. For example, a fix may be both a Refactor +-// (which should appear on the refactoring menu) and a SourceFixAll (a +-// clear fix that can be safely applied without explicit consent). +-type SuggestedFix struct { +- Title string +- Edits map[protocol.DocumentURI][]protocol.TextEdit +- Command *protocol.Command +- ActionKind protocol.CodeActionKind +-} - -- // breakImportCycles accesses only these fields of Metadata: -- // DepsByImpPath, ID - read -- // DepsByPkgPath - read, updated -- // Errors - updated -- breakImportCycles(metadata, updates) +-// SuggestedFixFromCommand returns a suggested fix to run the given command. +-func SuggestedFixFromCommand(cmd protocol.Command, kind protocol.CodeActionKind) SuggestedFix { +- return SuggestedFix{ +- Title: cmd.Title, +- Command: &cmd, +- ActionKind: kind, +- } +-} - -- t.Log("acyclic", format(metadata)) +-// quickFixesJSON is a JSON-serializable list of quick fixes +-// to be saved in the protocol.Diagnostic.Data field. +-type quickFixesJSON struct { +- // TODO(rfindley): pack some sort of identifier here for later +- // lookup/validation? +- Fixes []protocol.CodeAction +-} - -- if cycle := cyclic(metadata); cycle != "" { -- t.Errorf("resulting metadata %s has cycle %s: ", format(metadata), cycle) +-// bundleQuickFixes attempts to bundle sd.SuggestedFixes into the +-// sd.BundledFixes field, so that it can be round-tripped through the client. +-// It returns false if the quick-fixes cannot be bundled. +-func bundleQuickFixes(sd *Diagnostic) bool { +- if len(sd.SuggestedFixes) == 0 { +- return true +- } +- var actions []protocol.CodeAction +- for _, fix := range sd.SuggestedFixes { +- if fix.Edits != nil { +- // For now, we only support bundled code actions that execute commands. +- // +- // In order to cleanly support bundled edits, we'd have to guarantee that +- // the edits were generated on the current snapshot. But this naively +- // implies that every fix would have to include a snapshot ID, which +- // would require us to republish all diagnostics on each new snapshot. +- // +- // TODO(rfindley): in order to avoid this additional chatter, we'd need +- // to build some sort of registry or other mechanism on the snapshot to +- // check whether a diagnostic is still valid. +- return false - } -- -- got := format(metadata) -- if got != test.want { -- t.Errorf("test.metadata=%s test.updates=%s: got=%s want=%s", -- test.metadata, test.updates, got, test.want) +- action := protocol.CodeAction{ +- Title: fix.Title, +- Kind: fix.ActionKind, +- Command: fix.Command, - } +- actions = append(actions, action) - } +- fixes := quickFixesJSON{ +- Fixes: actions, +- } +- data, err := json.Marshal(fixes) +- if err != nil { +- bug.Reportf("marshalling quick fixes: %v", err) +- return false +- } +- msg := json.RawMessage(data) +- sd.BundledFixes = &msg +- return true -} -diff -urN a/gopls/internal/lsp/cache/debug.go b/gopls/internal/lsp/cache/debug.go ---- a/gopls/internal/lsp/cache/debug.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/debug.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,12 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package cache +-// BundledQuickFixes extracts any bundled codeActions from the +-// diag.Data field. +-func BundledQuickFixes(diag protocol.Diagnostic) []protocol.CodeAction { +- var fix quickFixesJSON +- if diag.Data != nil { +- err := protocol.UnmarshalJSON(*diag.Data, &fix) +- if err != nil { +- bug.Reportf("unmarshalling quick fix: %v", err) +- return nil +- } +- } - --// assert panics with the given msg if cond is not true. --func assert(cond bool, msg string) { -- if !cond { -- panic(msg) +- var actions []protocol.CodeAction +- for _, action := range fix.Fixes { +- // See BundleQuickFixes: for now we only support bundling commands. +- if action.Edit != nil { +- bug.Reportf("bundled fix %q includes workspace edits", action.Title) +- continue +- } +- // associate the action with the incoming diagnostic +- // (Note that this does not mutate the fix.Fixes slice). +- action.Diagnostics = []protocol.Diagnostic{diag} +- actions = append(actions, action) - } +- +- return actions -} -diff -urN a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors.go ---- a/gopls/internal/lsp/cache/errors.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/errors.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,545 +0,0 @@ +diff -urN a/gopls/internal/cache/errors.go b/gopls/internal/cache/errors.go +--- a/gopls/internal/cache/errors.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/errors.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,519 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. @@ -17194,7 +17422,7 @@ diff -urN a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors - -// This file defines routines to convert diagnostics from go list, go -// get, go/packages, parsing, type checking, and analysis into --// source.Diagnostic form, and suggesting quick fixes. +-// golang.Diagnostic form, and suggesting quick fixes. - -import ( - "context" @@ -17202,20 +17430,20 @@ diff -urN a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors - "go/parser" - "go/scanner" - "go/token" -- "go/types" - "log" +- "path/filepath" - "regexp" - "strconv" - "strings" - - "golang.org/x/tools/go/packages" -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/command" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/analysisinternal" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/command" +- "golang.org/x/tools/gopls/internal/settings" +- "golang.org/x/tools/gopls/internal/util/bug" - "golang.org/x/tools/internal/typesinternal" -) - @@ -17223,33 +17451,40 @@ diff -urN a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors -// diagnostic, using the provided metadata and filesource. -// -// The slice of diagnostics may be empty. --func goPackagesErrorDiagnostics(ctx context.Context, e packages.Error, m *source.Metadata, fs source.FileSource) ([]*source.Diagnostic, error) { -- if diag, err := parseGoListImportCycleError(ctx, e, m, fs); err != nil { +-func goPackagesErrorDiagnostics(ctx context.Context, e packages.Error, mp *metadata.Package, fs file.Source) ([]*Diagnostic, error) { +- if diag, err := parseGoListImportCycleError(ctx, e, mp, fs); err != nil { - return nil, err - } else if diag != nil { -- return []*source.Diagnostic{diag}, nil -- } -- -- var spn span.Span -- if e.Pos == "" { -- spn = parseGoListError(e.Msg, m.LoadDir) -- // We may not have been able to parse a valid span. Apply the errors to all files. -- if _, err := spanToRange(ctx, fs, spn); err != nil { -- var diags []*source.Diagnostic -- for _, uri := range m.CompiledGoFiles { -- diags = append(diags, &source.Diagnostic{ -- URI: uri, -- Severity: protocol.SeverityError, -- Source: source.ListError, -- Message: e.Msg, -- }) -- } -- return diags, nil -- } -- } else { -- spn = span.ParseInDir(e.Pos, m.LoadDir) +- return []*Diagnostic{diag}, nil - } - +- // Parse error location and attempt to convert to protocol form. +- loc, err := func() (protocol.Location, error) { +- filename, line, col8 := parseGoListError(e, mp.LoadDir) +- uri := protocol.URIFromPath(filename) +- +- fh, err := fs.ReadFile(ctx, uri) +- if err != nil { +- return protocol.Location{}, err +- } +- content, err := fh.Content() +- if err != nil { +- return protocol.Location{}, err +- } +- mapper := protocol.NewMapper(uri, content) +- posn, err := mapper.LineCol8Position(line, col8) +- if err != nil { +- return protocol.Location{}, err +- } +- return protocol.Location{ +- URI: uri, +- Range: protocol.Range{ +- Start: posn, +- End: posn, +- }, +- }, nil +- }() +- - // TODO(rfindley): in some cases the go command outputs invalid spans, for - // example (from TestGoListErrors): - // @@ -17261,26 +17496,36 @@ diff -urN a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors - // likely because *token.File lacks information about newline termination. - // - // We could do better here by handling that case. -- rng, err := spanToRange(ctx, fs, spn) - if err != nil { -- return nil, err +- // Unable to parse a valid position. +- // Apply the error to all files to be safe. +- var diags []*Diagnostic +- for _, uri := range mp.CompiledGoFiles { +- diags = append(diags, &Diagnostic{ +- URI: uri, +- Severity: protocol.SeverityError, +- Source: ListError, +- Message: e.Msg, +- }) +- } +- return diags, nil - } -- return []*source.Diagnostic{{ -- URI: spn.URI(), -- Range: rng, +- return []*Diagnostic{{ +- URI: loc.URI, +- Range: loc.Range, - Severity: protocol.SeverityError, -- Source: source.ListError, +- Source: ListError, - Message: e.Msg, - }}, nil -} - --func parseErrorDiagnostics(pkg *syntaxPackage, errList scanner.ErrorList) ([]*source.Diagnostic, error) { +-func parseErrorDiagnostics(pkg *syntaxPackage, errList scanner.ErrorList) ([]*Diagnostic, error) { - // The first parser error is likely the root cause of the problem. - if errList.Len() <= 0 { - return nil, fmt.Errorf("no errors in %v", errList) - } - e := errList[0] -- pgf, err := pkg.File(span.URIFromPath(e.Pos.Filename)) +- pgf, err := pkg.File(protocol.URIFromPath(e.Pos.Filename)) - if err != nil { - return nil, err - } @@ -17288,11 +17533,11 @@ diff -urN a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors - if err != nil { - return nil, err - } -- return []*source.Diagnostic{{ +- return []*Diagnostic{{ - URI: pgf.URI, - Range: rng, - Severity: protocol.SeverityError, -- Source: source.ParseError, +- Source: ParseError, - Message: e.Msg, - }}, nil -} @@ -17300,94 +17545,43 @@ diff -urN a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors -var importErrorRe = regexp.MustCompile(`could not import ([^\s]+)`) -var unsupportedFeatureRe = regexp.MustCompile(`.*require.* go(\d+\.\d+) or later`) - --func typeErrorDiagnostics(moduleMode bool, linkTarget string, pkg *syntaxPackage, e extendedError) ([]*source.Diagnostic, error) { -- code, loc, err := typeErrorData(pkg, e.primary) -- if err != nil { -- return nil, err -- } -- diag := &source.Diagnostic{ -- URI: loc.URI.SpanURI(), -- Range: loc.Range, -- Severity: protocol.SeverityError, -- Source: source.TypeError, -- Message: e.primary.Msg, -- } -- if code != 0 { -- diag.Code = code.String() -- diag.CodeHref = typesCodeHref(linkTarget, code) -- } -- switch code { -- case typesinternal.UnusedVar, typesinternal.UnusedImport: -- diag.Tags = append(diag.Tags, protocol.Unnecessary) -- } -- -- for _, secondary := range e.secondaries { -- _, secondaryLoc, err := typeErrorData(pkg, secondary) -- if err != nil { -- // We may not be able to compute type error data in scenarios where the -- // secondary position is outside of the current package. In this case, we -- // don't want to ignore the diagnostic entirely. -- // -- // See golang/go#59005 for an example where gopls was missing diagnostics -- // due to returning an error here. -- continue -- } -- diag.Related = append(diag.Related, protocol.DiagnosticRelatedInformation{ -- Location: secondaryLoc, -- Message: secondary.Msg, -- }) -- } -- -- if match := importErrorRe.FindStringSubmatch(e.primary.Msg); match != nil { -- diag.SuggestedFixes, err = goGetQuickFixes(moduleMode, loc.URI.SpanURI(), match[1]) -- if err != nil { -- return nil, err -- } -- } -- if match := unsupportedFeatureRe.FindStringSubmatch(e.primary.Msg); match != nil { -- diag.SuggestedFixes, err = editGoDirectiveQuickFix(moduleMode, loc.URI.SpanURI(), match[1]) -- if err != nil { -- return nil, err -- } -- } -- return []*source.Diagnostic{diag}, nil --} -- --func goGetQuickFixes(moduleMode bool, uri span.URI, pkg string) ([]source.SuggestedFix, error) { +-func goGetQuickFixes(moduleMode bool, uri protocol.DocumentURI, pkg string) []SuggestedFix { - // Go get only supports module mode for now. - if !moduleMode { -- return nil, nil +- return nil - } - title := fmt.Sprintf("go get package %v", pkg) - cmd, err := command.NewGoGetPackageCommand(title, command.GoGetPackageArgs{ -- URI: protocol.URIFromSpanURI(uri), +- URI: uri, - AddRequire: true, - Pkg: pkg, - }) - if err != nil { -- return nil, err +- bug.Reportf("internal error building 'go get package' fix: %v", err) +- return nil - } -- return []source.SuggestedFix{source.SuggestedFixFromCommand(cmd, protocol.QuickFix)}, nil +- return []SuggestedFix{SuggestedFixFromCommand(cmd, protocol.QuickFix)} -} - --func editGoDirectiveQuickFix(moduleMode bool, uri span.URI, version string) ([]source.SuggestedFix, error) { +-func editGoDirectiveQuickFix(moduleMode bool, uri protocol.DocumentURI, version string) []SuggestedFix { - // Go mod edit only supports module mode. - if !moduleMode { -- return nil, nil +- return nil - } - title := fmt.Sprintf("go mod edit -go=%s", version) - cmd, err := command.NewEditGoDirectiveCommand(title, command.EditGoDirectiveArgs{ -- URI: protocol.URIFromSpanURI(uri), +- URI: uri, - Version: version, - }) - if err != nil { -- return nil, err +- bug.Reportf("internal error constructing 'edit go directive' fix: %v", err) +- return nil - } -- return []source.SuggestedFix{source.SuggestedFixFromCommand(cmd, protocol.QuickFix)}, nil +- return []SuggestedFix{SuggestedFixFromCommand(cmd, protocol.QuickFix)} -} - -// encodeDiagnostics gob-encodes the given diagnostics. --func encodeDiagnostics(srcDiags []*source.Diagnostic) []byte { +-func encodeDiagnostics(srcDiags []*Diagnostic) []byte { - var gobDiags []gobDiagnostic - for _, srcDiag := range srcDiags { - var gobFixes []gobSuggestedFix @@ -17400,7 +17594,7 @@ diff -urN a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors - for _, srcEdit := range srcEdits { - gobFix.TextEdits = append(gobFix.TextEdits, gobTextEdit{ - Location: protocol.Location{ -- URI: protocol.URIFromSpanURI(uri), +- URI: uri, - Range: srcEdit.Range, - }, - NewText: []byte(srcEdit.NewText), @@ -17423,7 +17617,7 @@ diff -urN a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors - } - gobDiag := gobDiagnostic{ - Location: protocol.Location{ -- URI: protocol.URIFromSpanURI(srcDiag.URI), +- URI: srcDiag.URI, - Range: srcDiag.Range, - }, - Severity: srcDiag.Severity, @@ -17441,26 +17635,26 @@ diff -urN a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors -} - -// decodeDiagnostics decodes the given gob-encoded diagnostics. --func decodeDiagnostics(data []byte) []*source.Diagnostic { +-func decodeDiagnostics(data []byte) []*Diagnostic { - var gobDiags []gobDiagnostic - diagnosticsCodec.Decode(data, &gobDiags) -- var srcDiags []*source.Diagnostic +- var srcDiags []*Diagnostic - for _, gobDiag := range gobDiags { -- var srcFixes []source.SuggestedFix +- var srcFixes []SuggestedFix - for _, gobFix := range gobDiag.SuggestedFixes { -- srcFix := source.SuggestedFix{ +- srcFix := SuggestedFix{ - Title: gobFix.Message, - ActionKind: gobFix.ActionKind, - } - for _, gobEdit := range gobFix.TextEdits { - if srcFix.Edits == nil { -- srcFix.Edits = make(map[span.URI][]protocol.TextEdit) +- srcFix.Edits = make(map[protocol.DocumentURI][]protocol.TextEdit) - } - srcEdit := protocol.TextEdit{ - Range: gobEdit.Location.Range, - NewText: string(gobEdit.NewText), - } -- uri := gobEdit.Location.URI.SpanURI() +- uri := gobEdit.Location.URI - srcFix.Edits[uri] = append(srcFix.Edits[uri], srcEdit) - } - if gobCmd := gobFix.Command; gobCmd != nil { @@ -17477,13 +17671,13 @@ diff -urN a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors - srcRel := protocol.DiagnosticRelatedInformation(gobRel) - srcRelated = append(srcRelated, srcRel) - } -- srcDiag := &source.Diagnostic{ -- URI: gobDiag.Location.URI.SpanURI(), +- srcDiag := &Diagnostic{ +- URI: gobDiag.Location.URI, - Range: gobDiag.Location.Range, - Severity: gobDiag.Severity, - Code: gobDiag.Code, - CodeHref: gobDiag.CodeHref, -- Source: source.AnalyzerErrorKind(gobDiag.Source), +- Source: DiagnosticSource(gobDiag.Source), - Message: gobDiag.Message, - Tags: gobDiag.Tags, - Related: srcRelated, @@ -17495,61 +17689,98 @@ diff -urN a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors -} - -// toSourceDiagnostic converts a gobDiagnostic to "source" form. --func toSourceDiagnostic(srcAnalyzer *source.Analyzer, gobDiag *gobDiagnostic) *source.Diagnostic { +-func toSourceDiagnostic(srcAnalyzer *settings.Analyzer, gobDiag *gobDiagnostic) *Diagnostic { - var related []protocol.DiagnosticRelatedInformation - for _, gobRelated := range gobDiag.Related { - related = append(related, protocol.DiagnosticRelatedInformation(gobRelated)) - } - -- kinds := srcAnalyzer.ActionKind -- if len(srcAnalyzer.ActionKind) == 0 { -- kinds = append(kinds, protocol.QuickFix) -- } -- fixes := suggestedAnalysisFixes(gobDiag, kinds) -- if srcAnalyzer.Fix != "" { -- cmd, err := command.NewApplyFixCommand(gobDiag.Message, command.ApplyFixArgs{ -- URI: gobDiag.Location.URI, -- Range: gobDiag.Location.Range, -- Fix: srcAnalyzer.Fix, -- }) -- if err != nil { -- // JSON marshalling of these argument values cannot fail. -- log.Fatalf("internal error in NewApplyFixCommand: %v", err) -- } -- for _, kind := range kinds { -- fixes = append(fixes, source.SuggestedFixFromCommand(cmd, kind)) -- } -- } -- - severity := srcAnalyzer.Severity - if severity == 0 { - severity = protocol.SeverityWarning - } - -- diag := &source.Diagnostic{ -- URI: gobDiag.Location.URI.SpanURI(), +- diag := &Diagnostic{ +- URI: gobDiag.Location.URI, - Range: gobDiag.Location.Range, - Severity: severity, - Code: gobDiag.Code, - CodeHref: gobDiag.CodeHref, -- Source: source.AnalyzerErrorKind(gobDiag.Source), +- Source: DiagnosticSource(gobDiag.Source), - Message: gobDiag.Message, - Related: related, - Tags: srcAnalyzer.Tag, - } -- if srcAnalyzer.FixesDiagnostic(diag) { -- diag.SuggestedFixes = fixes +- +- // We cross the set of fixes (whether edit- or command-based) +- // with the set of kinds, as a single fix may represent more +- // than one kind of action (e.g. refactor, quickfix, fixall), +- // each corresponding to a distinct client UI element +- // or operation. +- kinds := srcAnalyzer.ActionKinds +- if len(kinds) == 0 { +- kinds = []protocol.CodeActionKind{protocol.QuickFix} +- } +- +- var fixes []SuggestedFix +- for _, fix := range gobDiag.SuggestedFixes { +- if len(fix.TextEdits) > 0 { +- // Accumulate edit-based fixes supplied by the diagnostic itself. +- edits := make(map[protocol.DocumentURI][]protocol.TextEdit) +- for _, e := range fix.TextEdits { +- uri := e.Location.URI +- edits[uri] = append(edits[uri], protocol.TextEdit{ +- Range: e.Location.Range, +- NewText: string(e.NewText), +- }) +- } +- for _, kind := range kinds { +- fixes = append(fixes, SuggestedFix{ +- Title: fix.Message, +- Edits: edits, +- ActionKind: kind, +- }) +- } +- +- } else { +- // Accumulate command-based fixes, whose edits +- // are not provided by the analyzer but are computed on demand +- // by logic "adjacent to" the analyzer. +- // +- // The analysis.Diagnostic.Category is used as the fix name. +- cmd, err := command.NewApplyFixCommand(fix.Message, command.ApplyFixArgs{ +- Fix: diag.Code, +- URI: gobDiag.Location.URI, +- Range: gobDiag.Location.Range, +- }) +- if err != nil { +- // JSON marshalling of these argument values cannot fail. +- log.Fatalf("internal error in NewApplyFixCommand: %v", err) +- } +- for _, kind := range kinds { +- fixes = append(fixes, SuggestedFixFromCommand(cmd, kind)) +- } +- +- // Ensure that the analyzer specifies a category for all its no-edit fixes. +- // This is asserted by analysistest.RunWithSuggestedFixes, but there +- // may be gaps in test coverage. +- if diag.Code == "" || diag.Code == "default" { +- bug.Reportf("missing Diagnostic.Code: %#v", *diag) +- } +- } - } +- diag.SuggestedFixes = fixes - - // If the fixes only delete code, assume that the diagnostic is reporting dead code. -- if onlyDeletions(fixes) { +- if onlyDeletions(diag.SuggestedFixes) { - diag.Tags = append(diag.Tags, protocol.Unnecessary) - } - return diag -} - --// onlyDeletions returns true if all of the suggested fixes are deletions. --func onlyDeletions(fixes []source.SuggestedFix) bool { +-// onlyDeletions returns true if fixes is non-empty and all of the suggested +-// fixes are deletions. +-func onlyDeletions(fixes []SuggestedFix) bool { - for _, fix := range fixes { - if fix.Command != nil { - return false @@ -17569,101 +17800,72 @@ diff -urN a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors -} - -func typesCodeHref(linkTarget string, code typesinternal.ErrorCode) string { -- return source.BuildLink(linkTarget, "golang.org/x/tools/internal/typesinternal", code.String()) +- return BuildLink(linkTarget, "golang.org/x/tools/internal/typesinternal", code.String()) -} - --func suggestedAnalysisFixes(diag *gobDiagnostic, kinds []protocol.CodeActionKind) []source.SuggestedFix { -- var fixes []source.SuggestedFix -- for _, fix := range diag.SuggestedFixes { -- edits := make(map[span.URI][]protocol.TextEdit) -- for _, e := range fix.TextEdits { -- uri := span.URI(e.Location.URI) -- edits[uri] = append(edits[uri], protocol.TextEdit{ -- Range: e.Location.Range, -- NewText: string(e.NewText), -- }) -- } -- for _, kind := range kinds { -- fixes = append(fixes, source.SuggestedFix{ -- Title: fix.Message, -- Edits: edits, -- ActionKind: kind, -- }) -- } -- +-// BuildLink constructs a URL with the given target, path, and anchor. +-func BuildLink(target, path, anchor string) string { +- link := fmt.Sprintf("https://%s/%s", target, path) +- if anchor == "" { +- return link - } -- return fixes +- return link + "#" + anchor -} - --func typeErrorData(pkg *syntaxPackage, terr types.Error) (typesinternal.ErrorCode, protocol.Location, error) { -- ecode, start, end, ok := typesinternal.ReadGo116ErrorData(terr) -- if !ok { -- start, end = terr.Pos, terr.Pos -- ecode = 0 -- } -- // go/types may return invalid positions in some cases, such as -- // in errors on tokens missing from the syntax tree. -- if !start.IsValid() { -- return 0, protocol.Location{}, fmt.Errorf("type error (%q, code %d, go116=%t) without position", terr.Msg, ecode, ok) -- } -- // go/types errors retain their FileSet. -- // Sanity-check that we're using the right one. -- fset := pkg.fset -- if fset != terr.Fset { -- return 0, protocol.Location{}, bug.Errorf("wrong FileSet for type error") -- } -- posn := safetoken.StartPosition(fset, start) -- if !posn.IsValid() { -- return 0, protocol.Location{}, fmt.Errorf("position %d of type error %q (code %q) not found in FileSet", start, start, terr) -- } -- pgf, err := pkg.File(span.URIFromPath(posn.Filename)) -- if err != nil { -- return 0, protocol.Location{}, err +-func parseGoListError(e packages.Error, dir string) (filename string, line, col8 int) { +- input := e.Pos +- if input == "" { +- // No position. Attempt to parse one out of a +- // go list error of the form "file:line:col: +- // message" by stripping off the message. +- input = strings.TrimSpace(e.Msg) +- if i := strings.Index(input, ": "); i >= 0 { +- input = input[:i] +- } - } -- if !end.IsValid() || end == start { -- end = analysisinternal.TypeErrorEndPos(fset, pgf.Src, start) +- +- filename, line, col8 = splitFileLineCol(input) +- if !filepath.IsAbs(filename) { +- filename = filepath.Join(dir, filename) - } -- loc, err := pgf.Mapper.PosLocation(pgf.Tok, start, end) -- return ecode, loc, err +- return filename, line, col8 -} - --// spanToRange converts a span.Span to a protocol.Range, by mapping content --// contained in the provided FileSource. --func spanToRange(ctx context.Context, fs source.FileSource, spn span.Span) (protocol.Range, error) { -- uri := spn.URI() -- fh, err := fs.ReadFile(ctx, uri) -- if err != nil { -- return protocol.Range{}, err +-// splitFileLineCol splits s into "filename:line:col", +-// where line and col consist of decimal digits. +-func splitFileLineCol(s string) (file string, line, col8 int) { +- // Beware that the filename may contain colon on Windows. +- +- // stripColonDigits removes a ":%d" suffix, if any. +- stripColonDigits := func(s string) (rest string, num int) { +- if i := strings.LastIndex(s, ":"); i >= 0 { +- if v, err := strconv.ParseInt(s[i+1:], 10, 32); err == nil { +- return s[:i], int(v) +- } +- } +- return s, -1 - } -- content, err := fh.Content() -- if err != nil { -- return protocol.Range{}, err +- +- // strip col ":%d" +- s, n1 := stripColonDigits(s) +- if n1 < 0 { +- return s, 0, 0 // "filename" - } -- mapper := protocol.NewMapper(uri, content) -- return mapper.SpanRange(spn) --} - --// parseGoListError attempts to parse a standard `go list` error message --// by stripping off the trailing error message. --// --// It works only on errors whose message is prefixed by colon, --// followed by a space (": "). For example: --// --// attributes.go:13:1: expected 'package', found 'type' --func parseGoListError(input, wd string) span.Span { -- input = strings.TrimSpace(input) -- msgIndex := strings.Index(input, ": ") -- if msgIndex < 0 { -- return span.Parse(input) +- // strip line ":%d" +- s, n2 := stripColonDigits(s) +- if n2 < 0 { +- return s, n1, 0 // "filename:line" - } -- return span.ParseInDir(input[:msgIndex], wd) +- +- return s, n2, n1 // "filename:line:col" -} - -// parseGoListImportCycleError attempts to parse the given go/packages error as -// an import cycle, returning a diagnostic if successful. -// -// If the error is not detected as an import cycle error, it returns nil, nil. --func parseGoListImportCycleError(ctx context.Context, e packages.Error, m *source.Metadata, fs source.FileSource) (*source.Diagnostic, error) { +-func parseGoListImportCycleError(ctx context.Context, e packages.Error, mp *metadata.Package, fs file.Source) (*Diagnostic, error) { - re := regexp.MustCompile(`(.*): import stack: \[(.+)\]`) - matches := re.FindStringSubmatch(strings.TrimSpace(e.Msg)) - if len(matches) < 3 { @@ -17678,8 +17880,8 @@ diff -urN a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors - } - // Imports have quotation marks around them. - circImp := strconv.Quote(importList[1]) -- for _, uri := range m.CompiledGoFiles { -- pgf, err := parseGoURI(ctx, fs, uri, source.ParseHeader) +- for _, uri := range mp.CompiledGoFiles { +- pgf, err := parseGoURI(ctx, fs, uri, parsego.Header) - if err != nil { - return nil, err - } @@ -17691,11 +17893,11 @@ diff -urN a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors - return nil, nil - } - -- return &source.Diagnostic{ +- return &Diagnostic{ - URI: pgf.URI, - Range: rng.Range(), - Severity: protocol.SeverityError, -- Source: source.ListError, +- Source: ListError, - Message: msg, - }, nil - } @@ -17712,7 +17914,7 @@ diff -urN a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors -// It returns an error if the file could not be read. -// -// TODO(rfindley): eliminate this helper. --func parseGoURI(ctx context.Context, fs source.FileSource, uri span.URI, mode parser.Mode) (*source.ParsedGoFile, error) { +-func parseGoURI(ctx context.Context, fs file.Source, uri protocol.DocumentURI, mode parser.Mode) (*parsego.File, error) { - fh, err := fs.ReadFile(ctx, uri) - if err != nil { - return nil, err @@ -17724,17 +17926,17 @@ diff -urN a/gopls/internal/lsp/cache/errors.go b/gopls/internal/lsp/cache/errors -// source fs. -// -// It returns an error if the file could not be read. --func parseModURI(ctx context.Context, fs source.FileSource, uri span.URI) (*source.ParsedModule, error) { +-func parseModURI(ctx context.Context, fs file.Source, uri protocol.DocumentURI) (*ParsedModule, error) { - fh, err := fs.ReadFile(ctx, uri) - if err != nil { - return nil, err - } - return parseModImpl(ctx, fh) -} -diff -urN a/gopls/internal/lsp/cache/errors_test.go b/gopls/internal/lsp/cache/errors_test.go ---- a/gopls/internal/lsp/cache/errors_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/errors_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,130 +0,0 @@ +diff -urN a/gopls/internal/cache/errors_test.go b/gopls/internal/cache/errors_test.go +--- a/gopls/internal/cache/errors_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/errors_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,128 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. @@ -17747,9 +17949,8 @@ diff -urN a/gopls/internal/lsp/cache/errors_test.go b/gopls/internal/lsp/cache/e - "testing" - - "github.com/google/go-cmp/cmp" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" +- "golang.org/x/tools/go/packages" +- "golang.org/x/tools/gopls/internal/protocol" -) - -func TestParseErrorMessage(t *testing.T) { @@ -17767,35 +17968,34 @@ diff -urN a/gopls/internal/lsp/cache/errors_test.go b/gopls/internal/lsp/cache/e - expectedLine: 13, - expectedColumn: 1, - }, +- { +- name: "windows driver letter", +- in: "C:\\foo\\bar.go:13: message", +- expectedFileName: "bar.go", +- expectedLine: 13, +- expectedColumn: 0, +- }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { -- spn := parseGoListError(tt.in, ".") -- fn := spn.URI().Filename() +- fn, line, col8 := parseGoListError(packages.Error{Msg: tt.in}, ".") - - if !strings.HasSuffix(fn, tt.expectedFileName) { - t.Errorf("expected filename with suffix %v but got %v", tt.expectedFileName, fn) - } -- -- if !spn.HasPosition() { -- t.Fatalf("expected span to have position") -- } -- -- pos := spn.Start() -- if pos.Line() != tt.expectedLine { -- t.Errorf("expected line %v but got %v", tt.expectedLine, pos.Line()) +- if line != tt.expectedLine { +- t.Errorf("expected line %v but got %v", tt.expectedLine, line) - } -- -- if pos.Column() != tt.expectedColumn { -- t.Errorf("expected line %v but got %v", tt.expectedLine, pos.Line()) +- if col8 != tt.expectedColumn { +- t.Errorf("expected col %v but got %v", tt.expectedLine, col8) - } - }) - } -} - -func TestDiagnosticEncoding(t *testing.T) { -- diags := []*source.Diagnostic{ +- diags := []*Diagnostic{ - {}, // empty - { - URI: "file///foo", @@ -17824,10 +18024,10 @@ diff -urN a/gopls/internal/lsp/cache/errors_test.go b/gopls/internal/lsp/cache/e - - // Fields below are used internally to generate quick fixes. They aren't - // part of the LSP spec and don't leave the server. -- SuggestedFixes: []source.SuggestedFix{ +- SuggestedFixes: []SuggestedFix{ - { - Title: "fix it!", -- Edits: map[span.URI][]protocol.TextEdit{ +- Edits: map[protocol.DocumentURI][]protocol.TextEdit{ - "file:///foo": {{ - Range: protocol.Range{ - Start: protocol.Position{Line: 4, Character: 2}, @@ -17865,9 +18065,9 @@ diff -urN a/gopls/internal/lsp/cache/errors_test.go b/gopls/internal/lsp/cache/e - t.Errorf("decoded diagnostics do not match (-original +decoded):\n%s", diff) - } -} -diff -urN a/gopls/internal/lsp/cache/filemap.go b/gopls/internal/lsp/cache/filemap.go ---- a/gopls/internal/lsp/cache/filemap.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/filemap.go 1970-01-01 00:00:00.000000000 +0000 +diff -urN a/gopls/internal/cache/filemap.go b/gopls/internal/cache/filemap.go +--- a/gopls/internal/cache/filemap.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/filemap.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,151 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style @@ -17878,31 +18078,31 @@ diff -urN a/gopls/internal/lsp/cache/filemap.go b/gopls/internal/lsp/cache/filem -import ( - "path/filepath" - -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/persistent" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/persistent" -) - -// A fileMap maps files in the snapshot, with some additional bookkeeping: -// It keeps track of overlays as well as directories containing any observed -// file. -type fileMap struct { -- files *persistent.Map[span.URI, source.FileHandle] -- overlays *persistent.Map[span.URI, *Overlay] // the subset of files that are overlays -- dirs *persistent.Set[string] // all dirs containing files; if nil, dirs have not been initialized +- files *persistent.Map[protocol.DocumentURI, file.Handle] +- overlays *persistent.Map[protocol.DocumentURI, *overlay] // the subset of files that are overlays +- dirs *persistent.Set[string] // all dirs containing files; if nil, dirs have not been initialized -} - -func newFileMap() *fileMap { - return &fileMap{ -- files: new(persistent.Map[span.URI, source.FileHandle]), -- overlays: new(persistent.Map[span.URI, *Overlay]), +- files: new(persistent.Map[protocol.DocumentURI, file.Handle]), +- overlays: new(persistent.Map[protocol.DocumentURI, *overlay]), - dirs: new(persistent.Set[string]), - } -} - --// Clone creates a copy of the fileMap, incorporating the changes specified by +-// clone creates a copy of the fileMap, incorporating the changes specified by -// the changes map. --func (m *fileMap) Clone(changes map[span.URI]source.FileHandle) *fileMap { +-func (m *fileMap) clone(changes map[protocol.DocumentURI]file.Handle) *fileMap { - m2 := &fileMap{ - files: m.files.Clone(), - overlays: m.overlays.Clone(), @@ -17923,18 +18123,18 @@ diff -urN a/gopls/internal/lsp/cache/filemap.go b/gopls/internal/lsp/cache/filem - // first, as a set before a deletion would result in pointless work. - for uri, fh := range changes { - if !fileExists(fh) { -- m2.Delete(uri) +- m2.delete(uri) - } - } - for uri, fh := range changes { - if fileExists(fh) { -- m2.Set(uri, fh) +- m2.set(uri, fh) - } - } - return m2 -} - --func (m *fileMap) Destroy() { +-func (m *fileMap) destroy() { - m.files.Destroy() - m.overlays.Destroy() - if m.dirs != nil { @@ -17942,24 +18142,24 @@ diff -urN a/gopls/internal/lsp/cache/filemap.go b/gopls/internal/lsp/cache/filem - } -} - --// Get returns the file handle mapped by the given key, or (nil, false) if the +-// get returns the file handle mapped by the given key, or (nil, false) if the -// key is not present. --func (m *fileMap) Get(key span.URI) (source.FileHandle, bool) { +-func (m *fileMap) get(key protocol.DocumentURI) (file.Handle, bool) { - return m.files.Get(key) -} - --// Range calls f for each (uri, fh) in the map. --func (m *fileMap) Range(f func(uri span.URI, fh source.FileHandle)) { +-// foreach calls f for each (uri, fh) in the map. +-func (m *fileMap) foreach(f func(uri protocol.DocumentURI, fh file.Handle)) { - m.files.Range(f) -} - --// Set stores the given file handle for key, updating overlays and directories +-// set stores the given file handle for key, updating overlays and directories -// accordingly. --func (m *fileMap) Set(key span.URI, fh source.FileHandle) { +-func (m *fileMap) set(key protocol.DocumentURI, fh file.Handle) { - m.files.Set(key, fh, nil) - - // update overlays -- if o, ok := fh.(*Overlay); ok { +- if o, ok := fh.(*overlay); ok { - m.overlays.Set(key, o, nil) - } else { - // Setting a non-overlay must delete the corresponding overlay, to preserve @@ -17974,17 +18174,17 @@ diff -urN a/gopls/internal/lsp/cache/filemap.go b/gopls/internal/lsp/cache/filem -} - -// addDirs adds all directories containing u to the dirs set. --func (m *fileMap) addDirs(u span.URI) { -- dir := filepath.Dir(u.Filename()) +-func (m *fileMap) addDirs(u protocol.DocumentURI) { +- dir := filepath.Dir(u.Path()) - for dir != "" && !m.dirs.Contains(dir) { - m.dirs.Add(dir) - dir = filepath.Dir(dir) - } -} - --// Delete removes a file from the map, and updates overlays and dirs +-// delete removes a file from the map, and updates overlays and dirs -// accordingly. --func (m *fileMap) Delete(key span.URI) { +-func (m *fileMap) delete(key protocol.DocumentURI) { - m.files.Delete(key) - m.overlays.Delete(key) - @@ -17998,31 +18198,31 @@ diff -urN a/gopls/internal/lsp/cache/filemap.go b/gopls/internal/lsp/cache/filem - } -} - --// Overlays returns a new unordered array of overlay files. --func (m *fileMap) Overlays() []*Overlay { -- var overlays []*Overlay -- m.overlays.Range(func(_ span.URI, o *Overlay) { +-// getOverlays returns a new unordered array of overlay files. +-func (m *fileMap) getOverlays() []*overlay { +- var overlays []*overlay +- m.overlays.Range(func(_ protocol.DocumentURI, o *overlay) { - overlays = append(overlays, o) - }) - return overlays -} - --// Dirs reports returns the set of dirs observed by the fileMap. +-// getDirs reports returns the set of dirs observed by the fileMap. -// -// This operation mutates the fileMap. -// The result must not be mutated by the caller. --func (m *fileMap) Dirs() *persistent.Set[string] { +-func (m *fileMap) getDirs() *persistent.Set[string] { - if m.dirs == nil { - m.dirs = new(persistent.Set[string]) -- m.files.Range(func(u span.URI, _ source.FileHandle) { +- m.files.Range(func(u protocol.DocumentURI, _ file.Handle) { - m.addDirs(u) - }) - } - return m.dirs -} -diff -urN a/gopls/internal/lsp/cache/filemap_test.go b/gopls/internal/lsp/cache/filemap_test.go ---- a/gopls/internal/lsp/cache/filemap_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/filemap_test.go 1970-01-01 00:00:00.000000000 +0000 +diff -urN a/gopls/internal/cache/filemap_test.go b/gopls/internal/cache/filemap_test.go +--- a/gopls/internal/cache/filemap_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/filemap_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,112 +0,0 @@ -// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style @@ -18036,8 +18236,8 @@ diff -urN a/gopls/internal/lsp/cache/filemap_test.go b/gopls/internal/lsp/cache/ - "testing" - - "github.com/google/go-cmp/cmp" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" -) - -func TestFileMap(t *testing.T) { @@ -18093,24 +18293,24 @@ diff -urN a/gopls/internal/lsp/cache/filemap_test.go b/gopls/internal/lsp/cache/ - t.Run(test.label, func(t *testing.T) { - m := newFileMap() - for _, op := range test.ops { -- uri := span.URIFromPath(filepath.FromSlash(op.path)) +- uri := protocol.URIFromPath(filepath.FromSlash(op.path)) - switch op.op { - case set: -- var fh source.FileHandle +- var fh file.Handle - if op.overlay { -- fh = &Overlay{uri: uri} +- fh = &overlay{uri: uri} - } else { -- fh = &DiskFile{uri: uri} +- fh = &diskFile{uri: uri} - } -- m.Set(uri, fh) +- m.set(uri, fh) - case del: -- m.Delete(uri) +- m.delete(uri) - } - } - - var gotFiles []string -- m.Range(func(uri span.URI, _ source.FileHandle) { -- gotFiles = append(gotFiles, normalize(uri.Filename())) +- m.foreach(func(uri protocol.DocumentURI, _ file.Handle) { +- gotFiles = append(gotFiles, normalize(uri.Path())) - }) - sort.Strings(gotFiles) - if diff := cmp.Diff(test.wantFiles, gotFiles); diff != "" { @@ -18118,15 +18318,15 @@ diff -urN a/gopls/internal/lsp/cache/filemap_test.go b/gopls/internal/lsp/cache/ - } - - var gotOverlays []string -- for _, o := range m.Overlays() { -- gotOverlays = append(gotOverlays, normalize(o.URI().Filename())) +- for _, o := range m.getOverlays() { +- gotOverlays = append(gotOverlays, normalize(o.URI().Path())) - } - if diff := cmp.Diff(test.wantOverlays, gotOverlays); diff != "" { - t.Errorf("Overlays mismatch (-want +got):\n%s", diff) - } - - var gotDirs []string -- m.Dirs().Range(func(dir string) { +- m.getDirs().Range(func(dir string) { - gotDirs = append(gotDirs, normalize(dir)) - }) - sort.Strings(gotDirs) @@ -18136,10 +18336,97 @@ diff -urN a/gopls/internal/lsp/cache/filemap_test.go b/gopls/internal/lsp/cache/ - }) - } -} -diff -urN a/gopls/internal/lsp/cache/fs_memoized.go b/gopls/internal/lsp/cache/fs_memoized.go ---- a/gopls/internal/lsp/cache/fs_memoized.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/fs_memoized.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,167 +0,0 @@ +diff -urN a/gopls/internal/cache/filterer.go b/gopls/internal/cache/filterer.go +--- a/gopls/internal/cache/filterer.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/filterer.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,83 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package cache +- +-import ( +- "path" +- "path/filepath" +- "regexp" +- "strings" +-) +- +-type Filterer struct { +- // Whether a filter is excluded depends on the operator (first char of the raw filter). +- // Slices filters and excluded then should have the same length. +- filters []*regexp.Regexp +- excluded []bool +-} +- +-// NewFilterer computes regular expression form of all raw filters +-func NewFilterer(rawFilters []string) *Filterer { +- var f Filterer +- for _, filter := range rawFilters { +- filter = path.Clean(filepath.ToSlash(filter)) +- // TODO(dungtuanle): fix: validate [+-] prefix. +- op, prefix := filter[0], filter[1:] +- // convertFilterToRegexp adds "/" at the end of prefix to handle cases where a filter is a prefix of another filter. +- // For example, it prevents [+foobar, -foo] from excluding "foobar". +- f.filters = append(f.filters, convertFilterToRegexp(filepath.ToSlash(prefix))) +- f.excluded = append(f.excluded, op == '-') +- } +- +- return &f +-} +- +-// Disallow return true if the path is excluded from the filterer's filters. +-func (f *Filterer) Disallow(path string) bool { +- // Ensure trailing but not leading slash. +- path = strings.TrimPrefix(path, "/") +- if !strings.HasSuffix(path, "/") { +- path += "/" +- } +- +- // TODO(adonovan): opt: iterate in reverse and break at first match. +- excluded := false +- for i, filter := range f.filters { +- if filter.MatchString(path) { +- excluded = f.excluded[i] // last match wins +- } +- } +- return excluded +-} +- +-// convertFilterToRegexp replaces glob-like operator substrings in a string file path to their equivalent regex forms. +-// Supporting glob-like operators: +-// - **: match zero or more complete path segments +-func convertFilterToRegexp(filter string) *regexp.Regexp { +- if filter == "" { +- return regexp.MustCompile(".*") +- } +- var ret strings.Builder +- ret.WriteString("^") +- segs := strings.Split(filter, "/") +- for _, seg := range segs { +- // Inv: seg != "" since path is clean. +- if seg == "**" { +- ret.WriteString(".*") +- } else { +- ret.WriteString(regexp.QuoteMeta(seg)) +- } +- ret.WriteString("/") +- } +- pattern := ret.String() +- +- // Remove unnecessary "^.*" prefix, which increased +- // BenchmarkWorkspaceSymbols time by ~20% (even though +- // filter CPU time increased by only by ~2.5%) when the +- // default filter was changed to "**/node_modules". +- pattern = strings.TrimPrefix(pattern, "^.*") +- +- return regexp.MustCompile(pattern) +-} +diff -urN a/gopls/internal/cache/fs_memoized.go b/gopls/internal/cache/fs_memoized.go +--- a/gopls/internal/cache/fs_memoized.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/fs_memoized.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,171 +0,0 @@ -// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. @@ -18152,8 +18439,8 @@ diff -urN a/gopls/internal/lsp/cache/fs_memoized.go b/gopls/internal/lsp/cache/f - "sync" - "time" - -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/event/tag" - "golang.org/x/tools/internal/robustio" @@ -18166,38 +18453,42 @@ diff -urN a/gopls/internal/lsp/cache/fs_memoized.go b/gopls/internal/lsp/cache/f - // filesByID maps existing file inodes to the result of a read. - // (The read may have failed, e.g. due to EACCES or a delete between stat+read.) - // Each slice is a non-empty list of aliases: different URIs. -- filesByID map[robustio.FileID][]*DiskFile +- filesByID map[robustio.FileID][]*diskFile +-} +- +-func newMemoizedFS() *memoizedFS { +- return &memoizedFS{filesByID: make(map[robustio.FileID][]*diskFile)} -} - --// A DiskFile is a file on the filesystem, or a failure to read one. --// It implements the source.FileHandle interface. --type DiskFile struct { -- uri span.URI +-// A diskFile is a file in the filesystem, or a failure to read one. +-// It implements the file.Source interface. +-type diskFile struct { +- uri protocol.DocumentURI - modTime time.Time - content []byte -- hash source.Hash +- hash file.Hash - err error -} - --func (h *DiskFile) URI() span.URI { return h.uri } +-func (h *diskFile) URI() protocol.DocumentURI { return h.uri } - --func (h *DiskFile) FileIdentity() source.FileIdentity { -- return source.FileIdentity{ +-func (h *diskFile) Identity() file.Identity { +- return file.Identity{ - URI: h.uri, - Hash: h.hash, - } -} - --func (h *DiskFile) SameContentsOnDisk() bool { return true } --func (h *DiskFile) Version() int32 { return 0 } --func (h *DiskFile) Content() ([]byte, error) { return h.content, h.err } +-func (h *diskFile) SameContentsOnDisk() bool { return true } +-func (h *diskFile) Version() int32 { return 0 } +-func (h *diskFile) Content() ([]byte, error) { return h.content, h.err } - -// ReadFile stats and (maybe) reads the file, updates the cache, and returns it. --func (fs *memoizedFS) ReadFile(ctx context.Context, uri span.URI) (source.FileHandle, error) { -- id, mtime, err := robustio.GetFileID(uri.Filename()) +-func (fs *memoizedFS) ReadFile(ctx context.Context, uri protocol.DocumentURI) (file.Handle, error) { +- id, mtime, err := robustio.GetFileID(uri.Path()) - if err != nil { - // file does not exist -- return &DiskFile{ +- return &diskFile{ - err: err, - uri: uri, - }, nil @@ -18217,7 +18508,7 @@ diff -urN a/gopls/internal/lsp/cache/fs_memoized.go b/gopls/internal/lsp/cache/f - fs.mu.Lock() - fhs, ok := fs.filesByID[id] - if ok && fhs[0].modTime.Equal(mtime) { -- var fh *DiskFile +- var fh *diskFile - // We have already seen this file and it has not changed. - for _, h := range fhs { - if h.uri == uri { @@ -18246,7 +18537,7 @@ diff -urN a/gopls/internal/lsp/cache/fs_memoized.go b/gopls/internal/lsp/cache/f - - fs.mu.Lock() - if !recentlyModified { -- fs.filesByID[id] = []*DiskFile{fh} +- fs.filesByID[id] = []*diskFile{fh} - } else { - delete(fs.filesByID, id) - } @@ -18279,7 +18570,7 @@ diff -urN a/gopls/internal/lsp/cache/fs_memoized.go b/gopls/internal/lsp/cache/f -// ioLimit limits the number of parallel file reads per process. -var ioLimit = make(chan struct{}, 128) - --func readFile(ctx context.Context, uri span.URI, mtime time.Time) (*DiskFile, error) { +-func readFile(ctx context.Context, uri protocol.DocumentURI, mtime time.Time) (*diskFile, error) { - select { - case ioLimit <- struct{}{}: - case <-ctx.Done(): @@ -18287,7 +18578,7 @@ diff -urN a/gopls/internal/lsp/cache/fs_memoized.go b/gopls/internal/lsp/cache/f - } - defer func() { <-ioLimit }() - -- ctx, done := event.Start(ctx, "cache.readFile", tag.File.Of(uri.Filename())) +- ctx, done := event.Start(ctx, "cache.readFile", tag.File.Of(uri.Path())) - _ = ctx - defer done() - @@ -18295,22 +18586,22 @@ diff -urN a/gopls/internal/lsp/cache/fs_memoized.go b/gopls/internal/lsp/cache/f - // ID, or whose mtime differs from the given mtime. However, in these cases - // we expect the client to notify of a subsequent file change, and the file - // content should be eventually consistent. -- content, err := os.ReadFile(uri.Filename()) // ~20us +- content, err := os.ReadFile(uri.Path()) // ~20us - if err != nil { - content = nil // just in case - } -- return &DiskFile{ +- return &diskFile{ - modTime: mtime, - uri: uri, - content: content, -- hash: source.HashOf(content), +- hash: file.HashOf(content), - err: err, - }, nil -} -diff -urN a/gopls/internal/lsp/cache/fs_overlay.go b/gopls/internal/lsp/cache/fs_overlay.go ---- a/gopls/internal/lsp/cache/fs_overlay.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/fs_overlay.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,78 +0,0 @@ +diff -urN a/gopls/internal/cache/fs_overlay.go b/gopls/internal/cache/fs_overlay.go +--- a/gopls/internal/cache/fs_overlay.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/fs_overlay.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,79 +0,0 @@ -// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. @@ -18321,38 +18612,38 @@ diff -urN a/gopls/internal/lsp/cache/fs_overlay.go b/gopls/internal/lsp/cache/fs - "context" - "sync" - -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" -) - --// An overlayFS is a source.FileSource that keeps track of overlays on top of a +-// An overlayFS is a file.Source that keeps track of overlays on top of a -// delegate FileSource. -type overlayFS struct { -- delegate source.FileSource +- delegate file.Source - - mu sync.Mutex -- overlays map[span.URI]*Overlay +- overlays map[protocol.DocumentURI]*overlay -} - --func newOverlayFS(delegate source.FileSource) *overlayFS { +-func newOverlayFS(delegate file.Source) *overlayFS { - return &overlayFS{ - delegate: delegate, -- overlays: make(map[span.URI]*Overlay), +- overlays: make(map[protocol.DocumentURI]*overlay), - } -} - -// Overlays returns a new unordered array of overlays. --func (fs *overlayFS) Overlays() []*Overlay { +-func (fs *overlayFS) Overlays() []*overlay { - fs.mu.Lock() - defer fs.mu.Unlock() -- overlays := make([]*Overlay, 0, len(fs.overlays)) +- overlays := make([]*overlay, 0, len(fs.overlays)) - for _, overlay := range fs.overlays { - overlays = append(overlays, overlay) - } - return overlays -} - --func (fs *overlayFS) ReadFile(ctx context.Context, uri span.URI) (source.FileHandle, error) { +-func (fs *overlayFS) ReadFile(ctx context.Context, uri protocol.DocumentURI) (file.Handle, error) { - fs.mu.Lock() - overlay, ok := fs.overlays[uri] - fs.mu.Unlock() @@ -18362,439 +18653,177 @@ diff -urN a/gopls/internal/lsp/cache/fs_overlay.go b/gopls/internal/lsp/cache/fs - return fs.delegate.ReadFile(ctx, uri) -} - --// An Overlay is a file open in the editor. It may have unsaved edits. --// It implements the source.FileHandle interface. --type Overlay struct { -- uri span.URI +-// An overlay is a file open in the editor. It may have unsaved edits. +-// It implements the file.Handle interface, and the implicit contract +-// of the debug.FileTmpl template. +-type overlay struct { +- uri protocol.DocumentURI - content []byte -- hash source.Hash +- hash file.Hash - version int32 -- kind source.FileKind +- kind file.Kind - - // saved is true if a file matches the state on disk, - // and therefore does not need to be part of the overlay sent to go/packages. - saved bool -} - --func (o *Overlay) URI() span.URI { return o.uri } +-func (o *overlay) URI() protocol.DocumentURI { return o.uri } - --func (o *Overlay) FileIdentity() source.FileIdentity { -- return source.FileIdentity{ +-func (o *overlay) Identity() file.Identity { +- return file.Identity{ - URI: o.uri, - Hash: o.hash, - } -} - --func (o *Overlay) Content() ([]byte, error) { return o.content, nil } --func (o *Overlay) Version() int32 { return o.version } --func (o *Overlay) SameContentsOnDisk() bool { return o.saved } --func (o *Overlay) Kind() source.FileKind { return o.kind } -diff -urN a/gopls/internal/lsp/cache/graph.go b/gopls/internal/lsp/cache/graph.go ---- a/gopls/internal/lsp/cache/graph.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/graph.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,364 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. +-func (o *overlay) Content() ([]byte, error) { return o.content, nil } +-func (o *overlay) Version() int32 { return o.version } +-func (o *overlay) SameContentsOnDisk() bool { return o.saved } +-func (o *overlay) Kind() file.Kind { return o.kind } +diff -urN a/gopls/internal/cache/imports.go b/gopls/internal/cache/imports.go +--- a/gopls/internal/cache/imports.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/imports.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,229 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cache - -import ( -- "sort" +- "context" +- "fmt" +- "sync" +- "time" - -- "golang.org/x/tools/go/packages" -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/keys" +- "golang.org/x/tools/internal/event/tag" +- "golang.org/x/tools/internal/imports" -) - --// A metadataGraph is an immutable and transitively closed import --// graph of Go packages, as obtained from go/packages. --type metadataGraph struct { -- // metadata maps package IDs to their associated metadata. -- metadata map[PackageID]*source.Metadata -- -- // importedBy maps package IDs to the list of packages that import them. -- importedBy map[PackageID][]PackageID -- -- // ids maps file URIs to package IDs, sorted by (!valid, cli, packageID). -- // A single file may belong to multiple packages due to tests packages. -- // -- // Invariant: all IDs present in the ids map exist in the metadata map. -- ids map[span.URI][]PackageID --} -- --// Metadata implements the source.MetadataSource interface. --func (g *metadataGraph) Metadata(id PackageID) *source.Metadata { -- return g.metadata[id] +-// refreshTimer implements delayed asynchronous refreshing of state. +-// +-// See the [refreshTimer.schedule] documentation for more details. +-type refreshTimer struct { +- mu sync.Mutex +- duration time.Duration +- timer *time.Timer +- refreshFn func() -} - --// Clone creates a new metadataGraph, applying the given updates to the --// receiver. A nil map value represents a deletion. --func (g *metadataGraph) Clone(updates map[PackageID]*source.Metadata) *metadataGraph { -- if len(updates) == 0 { -- // Optimization: since the graph is immutable, we can return the receiver. -- return g -- } -- -- // Copy metadata map then apply updates. -- metadata := make(map[PackageID]*source.Metadata, len(g.metadata)) -- for id, m := range g.metadata { -- metadata[id] = m -- } -- for id, m := range updates { -- if m == nil { -- delete(metadata, id) -- } else { -- metadata[id] = m -- } +-// newRefreshTimer constructs a new refresh timer which schedules refreshes +-// using the given function. +-func newRefreshTimer(refresh func()) *refreshTimer { +- return &refreshTimer{ +- refreshFn: refresh, - } -- -- // Break import cycles involving updated nodes. -- breakImportCycles(metadata, updates) -- -- return newMetadataGraph(metadata) -} - --// newMetadataGraph returns a new metadataGraph, --// deriving relations from the specified metadata. --func newMetadataGraph(metadata map[PackageID]*source.Metadata) *metadataGraph { -- // Build the import graph. -- importedBy := make(map[PackageID][]PackageID) -- for id, m := range metadata { -- for _, depID := range m.DepsByPkgPath { -- importedBy[depID] = append(importedBy[depID], id) -- } -- } +-// schedule schedules the refresh function to run at some point in the future, +-// if no existing refresh is already scheduled. +-// +-// At a minimum, scheduled refreshes are delayed by 30s, but they may be +-// delayed longer to keep their expected execution time under 2% of wall clock +-// time. +-func (t *refreshTimer) schedule() { +- t.mu.Lock() +- defer t.mu.Unlock() - -- // Collect file associations. -- uriIDs := make(map[span.URI][]PackageID) -- for id, m := range metadata { -- uris := map[span.URI]struct{}{} -- for _, uri := range m.CompiledGoFiles { -- uris[uri] = struct{}{} -- } -- for _, uri := range m.GoFiles { -- uris[uri] = struct{}{} -- } -- for uri := range uris { -- uriIDs[uri] = append(uriIDs[uri], id) +- if t.timer == nil { +- // Don't refresh more than twice per minute. +- delay := 30 * time.Second +- // Don't spend more than ~2% of the time refreshing. +- if adaptive := 50 * t.duration; adaptive > delay { +- delay = adaptive - } -- } -- -- // Sort and filter file associations. -- for uri, ids := range uriIDs { -- sort.Slice(ids, func(i, j int) bool { -- cli := source.IsCommandLineArguments(ids[i]) -- clj := source.IsCommandLineArguments(ids[j]) -- if cli != clj { -- return clj -- } -- -- // 2. packages appear in name order. -- return ids[i] < ids[j] +- t.timer = time.AfterFunc(delay, func() { +- start := time.Now() +- t.refreshFn() +- t.mu.Lock() +- t.duration = time.Since(start) +- t.timer = nil +- t.mu.Unlock() - }) -- -- // Choose the best IDs for each URI, according to the following rules: -- // - If there are any valid real packages, choose them. -- // - Else, choose the first valid command-line-argument package, if it exists. -- // -- // TODO(rfindley): it might be better to track all IDs here, and exclude -- // them later when type checking, but this is the existing behavior. -- for i, id := range ids { -- // If we've seen *anything* prior to command-line arguments package, take -- // it. Note that ids[0] may itself be command-line-arguments. -- if i > 0 && source.IsCommandLineArguments(id) { -- uriIDs[uri] = ids[:i] -- break -- } -- } -- } -- -- return &metadataGraph{ -- metadata: metadata, -- importedBy: importedBy, -- ids: uriIDs, - } -} - --// reverseReflexiveTransitiveClosure returns a new mapping containing the --// metadata for the specified packages along with any package that --// transitively imports one of them, keyed by ID, including all the initial packages. --func (g *metadataGraph) reverseReflexiveTransitiveClosure(ids ...PackageID) map[PackageID]*source.Metadata { -- seen := make(map[PackageID]*source.Metadata) -- var visitAll func([]PackageID) -- visitAll = func(ids []PackageID) { -- for _, id := range ids { -- if seen[id] == nil { -- if m := g.metadata[id]; m != nil { -- seen[id] = m -- visitAll(g.importedBy[id]) -- } -- } -- } -- } -- visitAll(ids) -- return seen +-// A sharedModCache tracks goimports state for GOMODCACHE directories +-// (each session may have its own GOMODCACHE). +-// +-// This state is refreshed independently of view-specific imports state. +-type sharedModCache struct { +- mu sync.Mutex +- caches map[string]*imports.DirInfoCache // GOMODCACHE -> cache content; never invalidated +- timers map[string]*refreshTimer // GOMODCACHE -> timer -} - --// breakImportCycles breaks import cycles in the metadata by deleting --// Deps* edges. It modifies only metadata present in the 'updates' --// subset. This function has an internal test. --func breakImportCycles(metadata, updates map[PackageID]*source.Metadata) { -- // 'go list' should never report a cycle without flagging it -- // as such, but we're extra cautious since we're combining -- // information from multiple runs of 'go list'. Also, Bazel -- // may silently report cycles. -- cycles := detectImportCycles(metadata, updates) -- if len(cycles) > 0 { -- // There were cycles (uncommon). Break them. -- // -- // The naive way to break cycles would be to perform a -- // depth-first traversal and to detect and delete -- // cycle-forming edges as we encounter them. -- // However, we're not allowed to modify the existing -- // Metadata records, so we can only break edges out of -- // the 'updates' subset. -- // -- // Another possibility would be to delete not the -- // cycle forming edge but the topmost edge on the -- // stack whose tail is an updated node. -- // However, this would require that we retroactively -- // undo all the effects of the traversals that -- // occurred since that edge was pushed on the stack. -- // -- // We use a simpler scheme: we compute the set of cycles. -- // All cyclic paths necessarily involve at least one -- // updated node, so it is sufficient to break all -- // edges from each updated node to other members of -- // the strong component. -- // -- // This may result in the deletion of dominating -- // edges, causing some dependencies to appear -- // spuriously unreachable. Consider A <-> B -> C -- // where updates={A,B}. The cycle is {A,B} so the -- // algorithm will break both A->B and B->A, causing -- // A to no longer depend on B or C. -- // -- // But that's ok: any error in Metadata.Errors is -- // conservatively assumed by snapshot.clone to be a -- // potential import cycle error, and causes special -- // invalidation so that if B later drops its -- // cycle-forming import of A, both A and B will be -- // invalidated. -- for _, cycle := range cycles { -- cyclic := make(map[PackageID]bool) -- for _, m := range cycle { -- cyclic[m.ID] = true -- } -- for id := range cyclic { -- if m := updates[id]; m != nil { -- for path, depID := range m.DepsByImpPath { -- if cyclic[depID] { -- delete(m.DepsByImpPath, path) -- } -- } -- for path, depID := range m.DepsByPkgPath { -- if cyclic[depID] { -- delete(m.DepsByPkgPath, path) -- } -- } -- -- // Set m.Errors to enable special -- // invalidation logic in snapshot.clone. -- if len(m.Errors) == 0 { -- m.Errors = []packages.Error{{ -- Msg: "detected import cycle", -- Kind: packages.ListError, -- }} -- } -- } -- } -- } +-func (c *sharedModCache) dirCache(dir string) *imports.DirInfoCache { +- c.mu.Lock() +- defer c.mu.Unlock() - -- // double-check when debugging -- if false { -- if cycles := detectImportCycles(metadata, updates); len(cycles) > 0 { -- bug.Reportf("unbroken cycle: %v", cycles) -- } -- } +- cache, ok := c.caches[dir] +- if !ok { +- cache = imports.NewDirInfoCache() +- c.caches[dir] = cache - } +- return cache -} - --// detectImportCycles reports cycles in the metadata graph. It returns a new --// unordered array of all cycles (nontrivial strong components) in the --// metadata graph reachable from a non-nil 'updates' value. --func detectImportCycles(metadata, updates map[PackageID]*source.Metadata) [][]*source.Metadata { -- // We use the depth-first algorithm of Tarjan. -- // https://doi.org/10.1137/0201010 -- // -- // TODO(adonovan): when we can use generics, consider factoring -- // in common with the other implementation of Tarjan (in typerefs), -- // abstracting over the node and edge representation. -- -- // A node wraps a Metadata with its working state. -- // (Unfortunately we can't intrude on shared Metadata.) -- type node struct { -- rep *node -- m *source.Metadata -- index, lowlink int32 -- scc int8 // TODO(adonovan): opt: cram these 1.5 bits into previous word -- } -- nodes := make(map[PackageID]*node, len(metadata)) -- nodeOf := func(id PackageID) *node { -- n, ok := nodes[id] -- if !ok { -- m := metadata[id] -- if m == nil { -- // Dangling import edge. -- // Not sure whether a go/packages driver ever -- // emits this, but create a dummy node in case. -- // Obviously it won't be part of any cycle. -- m = &source.Metadata{ID: id} -- } -- n = &node{m: m} -- n.rep = n -- nodes[id] = n -- } -- return n -- } -- -- // find returns the canonical node decl. -- // (The nodes form a disjoint set forest.) -- var find func(*node) *node -- find = func(n *node) *node { -- rep := n.rep -- if rep != n { -- rep = find(rep) -- n.rep = rep // simple path compression (no union-by-rank) -- } -- return rep -- } -- -- // global state -- var ( -- index int32 = 1 -- stack []*node -- sccs [][]*source.Metadata // set of nontrivial strongly connected components -- ) -- -- // visit implements the depth-first search of Tarjan's SCC algorithm -- // Precondition: x is canonical. -- var visit func(*node) -- visit = func(x *node) { -- x.index = index -- x.lowlink = index -- index++ -- -- stack = append(stack, x) // push -- x.scc = -1 -- -- for _, yid := range x.m.DepsByPkgPath { -- y := nodeOf(yid) -- // Loop invariant: x is canonical. -- y = find(y) -- if x == y { -- continue // nodes already combined (self-edges are impossible) -- } -- -- switch { -- case y.scc > 0: -- // y is already a collapsed SCC -- -- case y.scc < 0: -- // y is on the stack, and thus in the current SCC. -- if y.index < x.lowlink { -- x.lowlink = y.index -- } -- -- default: -- // y is unvisited; visit it now. -- visit(y) -- // Note: x and y are now non-canonical. -- x = find(x) -- if y.lowlink < x.lowlink { -- x.lowlink = y.lowlink -- } -- } -- } -- -- // Is x the root of an SCC? -- if x.lowlink == x.index { -- // Gather all metadata in the SCC (if nontrivial). -- var scc []*source.Metadata -- for { -- // Pop y from stack. -- i := len(stack) - 1 -- y := stack[i] -- stack = stack[:i] -- if x != y || scc != nil { -- scc = append(scc, y.m) -- } -- if x == y { -- break // complete -- } -- // x becomes y's canonical representative. -- y.rep = x -- } -- if scc != nil { -- sccs = append(sccs, scc) -- } -- x.scc = 1 -- } -- } +-// refreshDir schedules a refresh of the given directory, which must be a +-// module cache. +-func (c *sharedModCache) refreshDir(ctx context.Context, dir string, logf func(string, ...any)) { +- cache := c.dirCache(dir) - -- // Visit only the updated nodes: -- // the existing metadata graph has no cycles, -- // so any new cycle must involve an updated node. -- for id, m := range updates { -- if m != nil { -- if n := nodeOf(id); n.index == 0 { // unvisited -- visit(n) -- } -- } +- c.mu.Lock() +- defer c.mu.Unlock() +- timer, ok := c.timers[dir] +- if !ok { +- timer = newRefreshTimer(func() { +- _, done := event.Start(ctx, "cache.sharedModCache.refreshDir", tag.Directory.Of(dir)) +- defer done() +- imports.ScanModuleCache(dir, cache, logf) +- }) +- c.timers[dir] = timer - } - -- return sccs +- timer.schedule() -} -diff -urN a/gopls/internal/lsp/cache/imports.go b/gopls/internal/lsp/cache/imports.go ---- a/gopls/internal/lsp/cache/imports.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/imports.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,182 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package cache -- --import ( -- "context" -- "fmt" -- "reflect" -- "strings" -- "sync" -- "time" -- -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/keys" -- "golang.org/x/tools/internal/gocommand" -- "golang.org/x/tools/internal/imports" --) - +-// importsState tracks view-specific imports state. -type importsState struct { -- ctx context.Context +- ctx context.Context +- modCache *sharedModCache +- refreshTimer *refreshTimer +- +- mu sync.Mutex +- processEnv *imports.ProcessEnv +- cachedModFileHash file.Hash +-} - -- mu sync.Mutex -- processEnv *imports.ProcessEnv -- cacheRefreshDuration time.Duration -- cacheRefreshTimer *time.Timer -- cachedModFileHash source.Hash -- cachedBuildFlags []string -- cachedDirectoryFilters []string +-// newImportsState constructs a new imports state for running goimports +-// functions via [runProcessEnvFunc]. +-// +-// The returned state will automatically refresh itself following a call to +-// runProcessEnvFunc. +-func newImportsState(backgroundCtx context.Context, modCache *sharedModCache, env *imports.ProcessEnv) *importsState { +- s := &importsState{ +- ctx: backgroundCtx, +- modCache: modCache, +- processEnv: env, +- } +- s.refreshTimer = newRefreshTimer(s.refreshProcessEnv) +- return s -} - --func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *snapshot, fn func(context.Context, *imports.Options) error) error { +-// runProcessEnvFunc runs goimports. +-// +-// Any call to runProcessEnvFunc will schedule a refresh of the imports state +-// at some point in the future, if such a refresh is not already scheduled. See +-// [refreshTimer] for more details. +-func (s *importsState) runProcessEnvFunc(ctx context.Context, snapshot *Snapshot, fn func(context.Context, *imports.Options) error) error { - ctx, done := event.Start(ctx, "cache.importsState.runProcessEnvFunc") - defer done() - @@ -18806,42 +18835,21 @@ diff -urN a/gopls/internal/lsp/cache/imports.go b/gopls/internal/lsp/cache/impor - // the mod file shouldn't be changing while people are autocompleting. - // - // TODO(rfindley): consider instead hashing on-disk modfiles here. -- var modFileHash source.Hash -- for m := range snapshot.workspaceModFiles { +- var modFileHash file.Hash +- for m := range snapshot.view.workspaceModFiles { - fh, err := snapshot.ReadFile(ctx, m) - if err != nil { - return err - } -- modFileHash.XORWith(fh.FileIdentity().Hash) +- modFileHash.XORWith(fh.Identity().Hash) - } - -- // view.goEnv is immutable -- changes make a new view. Options can change. -- // We can't compare build flags directly because we may add -modfile. -- localPrefix := snapshot.Options().Local -- currentBuildFlags := snapshot.Options().BuildFlags -- currentDirectoryFilters := snapshot.Options().DirectoryFilters -- changed := !reflect.DeepEqual(currentBuildFlags, s.cachedBuildFlags) || -- snapshot.Options().VerboseOutput != (s.processEnv.Logf != nil) || -- modFileHash != s.cachedModFileHash || -- !reflect.DeepEqual(snapshot.Options().DirectoryFilters, s.cachedDirectoryFilters) -- - // If anything relevant to imports has changed, clear caches and - // update the processEnv. Clearing caches blocks on any background - // scans. -- if changed { -- if err := populateProcessEnvFromSnapshot(ctx, s.processEnv, snapshot); err != nil { -- return err -- } -- -- if resolver, err := s.processEnv.GetResolver(); err == nil { -- if modResolver, ok := resolver.(*imports.ModuleResolver); ok { -- modResolver.ClearForNewMod() -- } -- } -- +- if modFileHash != s.cachedModFileHash { +- s.processEnv.ClearModuleInfo() - s.cachedModFileHash = modFileHash -- s.cachedBuildFlags = currentBuildFlags -- s.cachedDirectoryFilters = currentDirectoryFilters - } - - // Run the user function. @@ -18854,68 +18862,28 @@ diff -urN a/gopls/internal/lsp/cache/imports.go b/gopls/internal/lsp/cache/impor - TabIndent: true, - TabWidth: 8, - Env: s.processEnv, -- LocalPrefix: localPrefix, +- LocalPrefix: snapshot.Options().Local, - } - - if err := fn(ctx, opts); err != nil { - return err - } - -- if s.cacheRefreshTimer == nil { -- // Don't refresh more than twice per minute. -- delay := 30 * time.Second -- // Don't spend more than a couple percent of the time refreshing. -- if adaptive := 50 * s.cacheRefreshDuration; adaptive > delay { -- delay = adaptive -- } -- s.cacheRefreshTimer = time.AfterFunc(delay, s.refreshProcessEnv) -- } -- -- return nil --} -- --// populateProcessEnvFromSnapshot sets the dynamically configurable fields for --// the view's process environment. Assumes that the caller is holding the --// importsState mutex. --func populateProcessEnvFromSnapshot(ctx context.Context, pe *imports.ProcessEnv, snapshot *snapshot) error { -- ctx, done := event.Start(ctx, "cache.populateProcessEnvFromSnapshot") -- defer done() -- -- if snapshot.Options().VerboseOutput { -- pe.Logf = func(format string, args ...interface{}) { -- event.Log(ctx, fmt.Sprintf(format, args...)) -- } -- } else { -- pe.Logf = nil -- } +- // Refresh the imports resolver after usage. This may seem counterintuitive, +- // since it means the first ProcessEnvFunc after a long period of inactivity +- // may be stale, but in practice we run ProcessEnvFuncs frequently during +- // active development (e.g. during completion), and so this mechanism will be +- // active while gopls is in use, and inactive when gopls is idle. +- s.refreshTimer.schedule() - -- // Extract invocation details from the snapshot to use with goimports. -- // -- // TODO(rfindley): refactor to extract the necessary invocation logic into -- // separate functions. Using goCommandInvocation is unnecessarily indirect, -- // and has led to memory leaks in the past, when the snapshot was -- // unintentionally held past its lifetime. -- _, inv, cleanupInvocation, err := snapshot.goCommandInvocation(ctx, source.LoadWorkspace, &gocommand.Invocation{ -- WorkingDir: snapshot.view.goCommandDir.Filename(), -- }) -- if err != nil { -- return err -- } +- // TODO(rfindley): the GOMODCACHE value used here isn't directly tied to the +- // ProcessEnv.Env["GOMODCACHE"], though they should theoretically always +- // agree. It would be better if we guaranteed this, possibly by setting all +- // required environment variables in ProcessEnv.Env, to avoid the redundant +- // Go command invocation. +- gomodcache := snapshot.view.folder.Env.GOMODCACHE +- s.modCache.refreshDir(s.ctx, gomodcache, s.processEnv.Logf) - -- pe.BuildFlags = inv.BuildFlags -- pe.ModFlag = "readonly" // processEnv operations should not mutate the modfile -- pe.Env = map[string]string{} -- for _, kv := range inv.Env { -- split := strings.SplitN(kv, "=", 2) -- if len(split) != 2 { -- continue -- } -- pe.Env[split[0]] = split[1] -- } -- // We don't actually use the invocation, so clean it up now. -- cleanupInvocation() -- // TODO(rfindley): should this simply be inv.WorkingDir? -- pe.WorkingDir = snapshot.view.goCommandDir.Filename() - return nil -} - @@ -18926,33 +18894,38 @@ diff -urN a/gopls/internal/lsp/cache/imports.go b/gopls/internal/lsp/cache/impor - start := time.Now() - - s.mu.Lock() -- env := s.processEnv -- if resolver, err := s.processEnv.GetResolver(); err == nil { -- resolver.ClearForNewScan() -- } +- resolver, err := s.processEnv.GetResolver() - s.mu.Unlock() +- if err != nil { +- return +- } - - event.Log(s.ctx, "background imports cache refresh starting") -- if err := imports.PrimeCache(context.Background(), env); err == nil { +- +- // Prime the new resolver before updating the processEnv, so that gopls +- // doesn't wait on an unprimed cache. +- if err := imports.PrimeCache(context.Background(), resolver); err == nil { - event.Log(ctx, fmt.Sprintf("background refresh finished after %v", time.Since(start))) - } else { - event.Log(ctx, fmt.Sprintf("background refresh finished after %v", time.Since(start)), keys.Err.Of(err)) - } +- - s.mu.Lock() -- s.cacheRefreshDuration = time.Since(start) -- s.cacheRefreshTimer = nil +- s.processEnv.UpdateResolver(resolver) - s.mu.Unlock() -} -diff -urN a/gopls/internal/lsp/cache/keys.go b/gopls/internal/lsp/cache/keys.go ---- a/gopls/internal/lsp/cache/keys.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/keys.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,52 +0,0 @@ +diff -urN a/gopls/internal/cache/keys.go b/gopls/internal/cache/keys.go +--- a/gopls/internal/cache/keys.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/keys.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,54 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cache - +-// session event tracing +- -import ( - "io" - @@ -18999,10 +18972,10 @@ diff -urN a/gopls/internal/lsp/cache/keys.go b/gopls/internal/lsp/cache/keys.go - err, _ := t.UnpackValue().(*Session) - return err -} -diff -urN a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go ---- a/gopls/internal/lsp/cache/load.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/load.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,766 +0,0 @@ +diff -urN a/gopls/internal/cache/load.go b/gopls/internal/cache/load.go +--- a/gopls/internal/cache/load.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/load.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,790 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. @@ -19021,14 +18994,18 @@ diff -urN a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go - "time" - - "golang.org/x/tools/go/packages" -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/gopls/internal/util/immutable" +- "golang.org/x/tools/gopls/internal/util/pathutil" +- "golang.org/x/tools/gopls/internal/util/slices" - "golang.org/x/tools/internal/event" - "golang.org/x/tools/internal/event/tag" - "golang.org/x/tools/internal/gocommand" - "golang.org/x/tools/internal/packagesinternal" +- "golang.org/x/tools/internal/xcontext" -) - -var loadID uint64 // atomic identifier for loads @@ -19043,7 +19020,7 @@ diff -urN a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go -// errors associated with specific modules. -// -// If scopes contains a file scope there must be exactly one scope. --func (s *snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadScope) (err error) { +-func (s *Snapshot) load(ctx context.Context, allowNetwork bool, scopes ...loadScope) (err error) { - id := atomic.AddUint64(&loadID, 1) - eventName := fmt.Sprintf("go/packages.Load #%d", id) // unique name for logging - @@ -19067,12 +19044,12 @@ diff -urN a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go - // information. For example go/packages returns at most one command-line - // arguments package, and does not handle a combination of standalone - // files and packages. -- uri := span.URI(scope) +- uri := protocol.DocumentURI(scope) - if len(scopes) > 1 { - panic(fmt.Sprintf("internal error: load called with multiple scopes when a file scope is present (file: %s)", uri)) - } - fh := s.FindFile(uri) -- if fh == nil || s.FileKind(fh) != source.Go { +- if fh == nil || s.FileKind(fh) != file.Go { - // Don't try to load a file that doesn't exist, or isn't a go file. - continue - } @@ -19082,20 +19059,20 @@ diff -urN a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go - } - if isStandaloneFile(contents, s.Options().StandaloneTags) { - standalone = true -- query = append(query, uri.Filename()) +- query = append(query, uri.Path()) - } else { -- query = append(query, fmt.Sprintf("file=%s", uri.Filename())) +- query = append(query, fmt.Sprintf("file=%s", uri.Path())) - } - - case moduleLoadScope: - modQuery := fmt.Sprintf("%s%c...", scope.dir, filepath.Separator) - query = append(query, modQuery) -- moduleQueries[modQuery] = string(scope.modulePath) +- moduleQueries[modQuery] = scope.modulePath - - case viewLoadScope: - // If we are outside of GOPATH, a module, or some other known - // build system, don't load subdirectories. -- if !s.validBuildConfiguration() { +- if s.view.typ == AdHocView { - query = append(query, "./") - } else { - query = append(query, "./...") @@ -19117,12 +19094,12 @@ diff -urN a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go - ctx, done := event.Start(ctx, "cache.snapshot.load", tag.Query.Of(query)) - defer done() - -- flags := source.LoadWorkspace +- flags := LoadWorkspace - if allowNetwork { -- flags |= source.AllowNetwork +- flags |= AllowNetwork - } - _, inv, cleanup, err := s.goCommandInvocation(ctx, flags, &gocommand.Invocation{ -- WorkingDir: s.view.goCommandDir.Filename(), +- WorkingDir: s.view.root.Path(), - }) - if err != nil { - return err @@ -19146,13 +19123,52 @@ diff -urN a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go - } - - // This log message is sought for by TestReloadOnlyOnce. -- labels := append(source.SnapshotLabels(s), tag.Query.Of(query), tag.PackageCount.Of(len(pkgs))) +- labels := append(s.Labels(), tag.Query.Of(query), tag.PackageCount.Of(len(pkgs))) - if err != nil { - event.Error(ctx, eventName, err, labels...) - } else { - event.Log(ctx, eventName, labels...) - } - +- if standalone { +- // Handle standalone package result. +- // +- // In general, this should just be a single "command-line-arguments" +- // package containing the requested file. However, if the file is a test +- // file, go/packages may return test variants of the command-line-arguments +- // package. We don't support this; theoretically we could, but it seems +- // unnecessarily complicated. +- // +- // Prior to golang/go#64233 we just assumed that we'd get exactly one +- // package here. The categorization of bug reports below may be a bit +- // verbose, but anticipates that perhaps we don't fully understand +- // possible failure modes. +- errorf := bug.Errorf +- if s.view.typ == GoPackagesDriverView { +- errorf = fmt.Errorf // all bets are off +- } +- +- var standalonePkg *packages.Package +- for _, pkg := range pkgs { +- if pkg.ID == "command-line-arguments" { +- if standalonePkg != nil { +- return errorf("internal error: go/packages returned multiple standalone packages") +- } +- standalonePkg = pkg +- } else if packagesinternal.GetForTest(pkg) == "" && !strings.HasSuffix(pkg.ID, ".test") { +- return errorf("internal error: go/packages returned unexpected package %q for standalone file", pkg.ID) +- } +- } +- if standalonePkg == nil { +- return errorf("internal error: go/packages failed to return non-test standalone package") +- } +- if len(standalonePkg.CompiledGoFiles) > 0 { +- pkgs = []*packages.Package{standalonePkg} +- } else { +- pkgs = nil +- } +- } +- - if len(pkgs) == 0 { - if err == nil { - err = errNoPackages @@ -19160,13 +19176,9 @@ diff -urN a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go - return fmt.Errorf("packages.Load error: %w", err) - } - -- if standalone && len(pkgs) > 1 { -- return bug.Errorf("internal error: go/packages returned multiple packages for standalone file") -- } -- - moduleErrs := make(map[string][]packages.Error) // module path -> errors - filterFunc := s.view.filterFunc() -- newMetadata := make(map[PackageID]*source.Metadata) +- newMetadata := make(map[PackageID]*metadata.Package) - for _, pkg := range pkgs { - // The Go command returns synthetic list results for module queries that - // encountered module errors. @@ -19185,7 +19197,7 @@ diff -urN a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go - - if !containsDir || s.Options().VerboseOutput { - event.Log(ctx, eventName, append( -- source.SnapshotLabels(s), +- s.Labels(), - tag.Package.Of(pkg.ID), - tag.Files.Of(pkg.CompiledGoFiles))...) - } @@ -19204,7 +19216,7 @@ diff -urN a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go - continue - } - // Skip test main packages. -- if isTestMain(pkg, s.view.gocache) { +- if isTestMain(pkg, s.view.folder.Env.GOCACHE) { - continue - } - // Skip filtered packages. They may be added anyway if they're @@ -19223,40 +19235,34 @@ diff -urN a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go - - // Assert the invariant s.packages.Get(id).m == s.meta.metadata[id]. - s.packages.Range(func(id PackageID, ph *packageHandle) { -- if s.meta.metadata[id] != ph.m { +- if s.meta.Packages[id] != ph.mp { - panic("inconsistent metadata") - } - }) - - // Compute the minimal metadata updates (for Clone) - // required to preserve the above invariant. -- var files []span.URI // files to preload -- seenFiles := make(map[span.URI]bool) -- updates := make(map[PackageID]*source.Metadata) -- for _, m := range newMetadata { -- if existing := s.meta.metadata[m.ID]; existing == nil { +- var files []protocol.DocumentURI // files to preload +- seenFiles := make(map[protocol.DocumentURI]bool) +- updates := make(map[PackageID]*metadata.Package) +- for _, mp := range newMetadata { +- if existing := s.meta.Packages[mp.ID]; existing == nil { - // Record any new files we should pre-load. -- for _, uri := range m.CompiledGoFiles { +- for _, uri := range mp.CompiledGoFiles { - if !seenFiles[uri] { - seenFiles[uri] = true - files = append(files, uri) - } - } -- updates[m.ID] = m -- delete(s.shouldLoad, m.ID) +- updates[mp.ID] = mp +- s.shouldLoad.Delete(mp.ID) - } - } - - event.Log(ctx, fmt.Sprintf("%s: updating metadata for %d packages", eventName, len(updates))) - -- // Before mutating the snapshot, ensure that we compute load diagnostics -- // successfully. This could fail if the context is cancelled, and we don't -- // want to leave the snapshot metadata in a partial state. -- meta := s.meta.Clone(updates) -- workspacePackages := computeWorkspacePackagesLocked(s, meta) -- for _, update := range updates { -- computeLoadDiagnostics(ctx, update, meta, lockedSnapshot{s}, workspacePackages) -- } +- meta := s.meta.Update(updates) +- workspacePackages := computeWorkspacePackagesLocked(ctx, s, meta) - s.meta = meta - s.workspacePackages = workspacePackages - s.resetActivePackagesLocked() @@ -19305,117 +19311,49 @@ diff -urN a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go - return buf.String() -} - --// workspaceLayoutError returns an error describing a misconfiguration of the --// workspace, along with related diagnostic. --// --// The unusual argument ordering of results is intentional: if the resulting --// error is nil, so must be the resulting diagnostics. --// --// If ctx is cancelled, it may return ctx.Err(), nil. --// --// TODO(rfindley): separate workspace diagnostics from critical workspace --// errors. --func (s *snapshot) workspaceLayoutError(ctx context.Context) (error, []*source.Diagnostic) { -- // TODO(rfindley): both of the checks below should be delegated to the workspace. -- -- if s.view.effectiveGO111MODULE() == off { -- return nil, nil -- } -- -- // If the user is using a go.work file, we assume that they know what they -- // are doing. -- // -- // TODO(golang/go#53880): improve orphaned file diagnostics when using go.work. -- if s.view.gowork != "" { -- return nil, nil -- } -- -- // Apply diagnostics about the workspace configuration to relevant open -- // files. -- openFiles := s.overlays() -- -- // If the snapshot does not have a valid build configuration, it may be -- // that the user has opened a directory that contains multiple modules. -- // Check for that an warn about it. -- if !s.validBuildConfiguration() { -- var msg string -- if s.view.goversion >= 18 { -- msg = `gopls was not able to find modules in your workspace. --When outside of GOPATH, gopls needs to know which modules you are working on. --You can fix this by opening your workspace to a folder inside a Go module, or --by using a go.work file to specify multiple modules. --See the documentation for more information on setting up your workspace: --https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.` -- } else { -- msg = `gopls requires a module at the root of your workspace. --You can work with multiple modules by upgrading to Go 1.18 or later, and using --go workspaces (go.work files). --See the documentation for more information on setting up your workspace: --https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.` -- } -- return fmt.Errorf(msg), s.applyCriticalErrorToFiles(ctx, msg, openFiles) -- } -- -- return nil, nil --} -- --func (s *snapshot) applyCriticalErrorToFiles(ctx context.Context, msg string, files []*Overlay) []*source.Diagnostic { -- var srcDiags []*source.Diagnostic -- for _, fh := range files { -- // Place the diagnostics on the package or module declarations. -- var rng protocol.Range -- switch s.FileKind(fh) { -- case source.Go: -- if pgf, err := s.ParseGo(ctx, fh, source.ParseHeader); err == nil { -- // Check that we have a valid `package foo` range to use for positioning the error. -- if pgf.File.Package.IsValid() && pgf.File.Name != nil && pgf.File.Name.End().IsValid() { -- rng, _ = pgf.PosRange(pgf.File.Package, pgf.File.Name.End()) -- } -- } -- case source.Mod: -- if pmf, err := s.ParseMod(ctx, fh); err == nil { -- if mod := pmf.File.Module; mod != nil && mod.Syntax != nil { -- rng, _ = pmf.Mapper.OffsetRange(mod.Syntax.Start.Byte, mod.Syntax.End.Byte) -- } -- } -- } -- srcDiags = append(srcDiags, &source.Diagnostic{ -- URI: fh.URI(), -- Range: rng, -- Severity: protocol.SeverityError, -- Source: source.ListError, -- Message: msg, -- }) -- } -- return srcDiags --} -- -// buildMetadata populates the updates map with metadata updates to -// apply, based on the given pkg. It recurs through pkg.Imports to ensure that -// metadata exists for all dependencies. --func buildMetadata(updates map[PackageID]*source.Metadata, pkg *packages.Package, loadDir string, standalone bool) { +-// +-// Returns the metadata.Package that was built (or which was already present in +-// updates), or nil if the package could not be built. Notably, the resulting +-// metadata.Package may have an ID that differs from pkg.ID. +-func buildMetadata(updates map[PackageID]*metadata.Package, pkg *packages.Package, loadDir string, standalone bool) *metadata.Package { - // Allow for multiple ad-hoc packages in the workspace (see #47584). - pkgPath := PackagePath(pkg.PkgPath) - id := PackageID(pkg.ID) - -- if source.IsCommandLineArguments(id) { -- if len(pkg.CompiledGoFiles) != 1 { -- bug.Reportf("unexpected files in command-line-arguments package: %v", pkg.CompiledGoFiles) -- return +- if metadata.IsCommandLineArguments(id) { +- var f string // file to use as disambiguating suffix +- if len(pkg.CompiledGoFiles) > 0 { +- f = pkg.CompiledGoFiles[0] +- +- // If there are multiple files, +- // we can't use only the first. +- // (Can this happen? #64557) +- if len(pkg.CompiledGoFiles) > 1 { +- bug.Reportf("unexpected files in command-line-arguments package: %v", pkg.CompiledGoFiles) +- return nil +- } +- } else if len(pkg.IgnoredFiles) > 0 { +- // A file=empty.go query results in IgnoredFiles=[empty.go]. +- f = pkg.IgnoredFiles[0] +- } else { +- bug.Reportf("command-line-arguments package has neither CompiledGoFiles nor IgnoredFiles: %#v", "") //*pkg.Metadata) +- return nil - } -- suffix := pkg.CompiledGoFiles[0] -- id = PackageID(pkg.ID + suffix) -- pkgPath = PackagePath(pkg.PkgPath + suffix) +- id = PackageID(pkg.ID + f) +- pkgPath = PackagePath(pkg.PkgPath + f) - } - - // Duplicate? -- if _, ok := updates[id]; ok { +- if existing, ok := updates[id]; ok { - // A package was encountered twice due to shared - // subgraphs (common) or cycles (rare). Although "go - // list" usually breaks cycles, we don't rely on it. - // breakImportCycles in metadataGraph.Clone takes care - // of it later. -- return +- return existing - } - - if pkg.TypesSizes == nil { @@ -19423,7 +19361,7 @@ diff -urN a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go - } - - // Recreate the metadata rather than reusing it to avoid locking. -- m := &source.Metadata{ +- mp := &metadata.Package{ - ID: id, - PkgPath: pkgPath, - Name: PackageName(pkg.Name), @@ -19436,19 +19374,19 @@ diff -urN a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go - Standalone: standalone, - } - -- updates[id] = m +- updates[id] = mp - - for _, filename := range pkg.CompiledGoFiles { -- uri := span.URIFromPath(filename) -- m.CompiledGoFiles = append(m.CompiledGoFiles, uri) +- uri := protocol.URIFromPath(filename) +- mp.CompiledGoFiles = append(mp.CompiledGoFiles, uri) - } - for _, filename := range pkg.GoFiles { -- uri := span.URIFromPath(filename) -- m.GoFiles = append(m.GoFiles, uri) +- uri := protocol.URIFromPath(filename) +- mp.GoFiles = append(mp.GoFiles, uri) - } - for _, filename := range pkg.IgnoredFiles { -- uri := span.URIFromPath(filename) -- m.IgnoredFiles = append(m.IgnoredFiles, uri) +- uri := protocol.URIFromPath(filename) +- mp.IgnoredFiles = append(mp.IgnoredFiles, uri) - } - - depsByImpPath := make(map[ImportPath]PackageID) @@ -19536,15 +19474,21 @@ diff -urN a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go - continue - } - -- buildMetadata(updates, imported, loadDir, false) // only top level packages can be standalone +- dep := buildMetadata(updates, imported, loadDir, false) // only top level packages can be standalone - - // Don't record edges to packages with no name, as they cause trouble for - // the importer (golang/go#60952). - // +- // Also don't record edges to packages whose ID was modified (i.e. +- // command-line-arguments packages), as encountered in golang/go#66109. In +- // this case, we could theoretically keep the edge through dep.ID, but +- // since this import doesn't make any sense in the first place, we instead +- // choose to consider it invalid. +- // - // However, we do want to insert these packages into the update map - // (buildMetadata above), so that we get type-checking diagnostics for the - // invalid packages. -- if imported.Name == "" { +- if dep == nil || dep.ID != PackageID(imported.ID) || imported.Name == "" { - depsByImpPath[importPath] = "" // missing - continue - } @@ -19552,8 +19496,9 @@ diff -urN a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go - depsByImpPath[importPath] = PackageID(imported.ID) - depsByPkgPath[PackagePath(imported.PkgPath)] = PackageID(imported.ID) - } -- m.DepsByImpPath = depsByImpPath -- m.DepsByPkgPath = depsByPkgPath +- mp.DepsByImpPath = depsByImpPath +- mp.DepsByPkgPath = depsByPkgPath +- return mp - - // m.Diagnostics is set later in the loading pass, using - // computeLoadDiagnostics. @@ -19561,126 +19506,188 @@ diff -urN a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go - -// computeLoadDiagnostics computes and sets m.Diagnostics for the given metadata m. -// --// It should only be called during metadata construction in snapshot.load. --func computeLoadDiagnostics(ctx context.Context, m *source.Metadata, meta *metadataGraph, fs source.FileSource, workspacePackages map[PackageID]PackagePath) { -- for _, packagesErr := range m.Errors { +-// It should only be called during package handle construction in buildPackageHandle. +-func computeLoadDiagnostics(ctx context.Context, snapshot *Snapshot, mp *metadata.Package) []*Diagnostic { +- var diags []*Diagnostic +- for _, packagesErr := range mp.Errors { - // Filter out parse errors from go list. We'll get them when we - // actually parse, and buggy overlay support may generate spurious - // errors. (See TestNewModule_Issue38207.) - if strings.Contains(packagesErr.Msg, "expected '") { - continue - } -- pkgDiags, err := goPackagesErrorDiagnostics(ctx, packagesErr, m, fs) +- pkgDiags, err := goPackagesErrorDiagnostics(ctx, packagesErr, mp, snapshot) - if err != nil { - // There are certain cases where the go command returns invalid - // positions, so we cannot panic or even bug.Reportf here. -- event.Error(ctx, "unable to compute positions for list errors", err, tag.Package.Of(string(m.ID))) +- event.Error(ctx, "unable to compute positions for list errors", err, tag.Package.Of(string(mp.ID))) - continue - } -- m.Diagnostics = append(m.Diagnostics, pkgDiags...) +- diags = append(diags, pkgDiags...) - } - - // TODO(rfindley): this is buggy: an insignificant change to a modfile - // (or an unsaved modfile) could affect the position of deps errors, - // without invalidating the package. -- depsDiags, err := depsErrors(ctx, m, meta, fs, workspacePackages) +- depsDiags, err := depsErrors(ctx, snapshot, mp) - if err != nil { - if ctx.Err() == nil { - // TODO(rfindley): consider making this a bug.Reportf. depsErrors should - // not normally fail. -- event.Error(ctx, "unable to compute deps errors", err, tag.Package.Of(string(m.ID))) +- event.Error(ctx, "unable to compute deps errors", err, tag.Package.Of(string(mp.ID))) - } +- } else { +- diags = append(diags, depsDiags...) - } -- m.Diagnostics = append(m.Diagnostics, depsDiags...) +- return diags -} - --// containsPackageLocked reports whether p is a workspace package for the --// snapshot s. +-// IsWorkspacePackage reports whether id points to a workspace package in s. -// --// s.mu must be held while calling this function. --func containsPackageLocked(s *snapshot, m *source.Metadata) bool { -- // In legacy workspace mode, or if a package does not have an associated -- // module, a package is considered inside the workspace if any of its files -- // are under the workspace root (and not excluded). -- // -- // Otherwise if the package has a module it must be an active module (as -- // defined by the module root or go.work file) and at least one file must not -- // be filtered out by directoryFilters. -- // -- // TODO(rfindley): revisit this function. We should not need to predicate on -- // gowork != "". It should suffice to consider workspace mod files (also, we -- // will hopefully eliminate the concept of a workspace package soon). -- if m.Module != nil && s.view.gowork != "" { -- modURI := span.URIFromPath(m.Module.GoMod) -- _, ok := s.workspaceModFiles[modURI] -- if !ok { -- return false +-// Currently, the result depends on the current set of loaded packages, and so +-// is not guaranteed to be stable. +-func (s *Snapshot) IsWorkspacePackage(ctx context.Context, id PackageID) bool { +- s.mu.Lock() +- defer s.mu.Unlock() +- +- mg := s.meta +- m := mg.Packages[id] +- if m == nil { +- return false +- } +- return isWorkspacePackageLocked(ctx, s, mg, m) +-} +- +-// isWorkspacePackageLocked reports whether p is a workspace package for the +-// snapshot s. +-// +-// Workspace packages are packages that we consider the user to be actively +-// working on. As such, they are re-diagnosed on every keystroke, and searched +-// for various workspace-wide queries such as references or workspace symbols. +-// +-// See the commentary inline for a description of the workspace package +-// heuristics. +-// +-// s.mu must be held while calling this function. +-// +-// TODO(rfindley): remove 'meta' from this function signature. Whether or not a +-// package is a workspace package should depend only on the package, view +-// definition, and snapshot file source. While useful, the heuristic +-// "allFilesHaveRealPackages" does not add that much value and is path +-// dependent as it depends on the timing of loads. +-func isWorkspacePackageLocked(ctx context.Context, s *Snapshot, meta *metadata.Graph, pkg *metadata.Package) bool { +- if metadata.IsCommandLineArguments(pkg.ID) { +- // Ad-hoc command-line-arguments packages aren't workspace packages. +- // With zero-config gopls (golang/go#57979) they should be very rare, as +- // they should only arise when the user opens a file outside the workspace +- // which isn't present in the import graph of a workspace package. +- // +- // Considering them as workspace packages tends to be racy, as they don't +- // deterministically belong to any view. +- if !pkg.Standalone { +- return false - } - -- uris := map[span.URI]struct{}{} -- for _, uri := range m.CompiledGoFiles { -- uris[uri] = struct{}{} +- // If all the files contained in pkg have a real package, we don't need to +- // keep pkg as a workspace package. +- if allFilesHaveRealPackages(meta, pkg) { +- return false - } -- for _, uri := range m.GoFiles { +- +- // For now, allow open standalone packages (i.e. go:build ignore) to be +- // workspace packages, but this means they could belong to multiple views. +- return containsOpenFileLocked(s, pkg) +- } +- +- // If a real package is open, consider it to be part of the workspace. +- // +- // TODO(rfindley): reconsider this. In golang/go#66145, we saw that even if a +- // View sees a real package for a file, it doesn't mean that View is able to +- // cleanly diagnose the package. Yet, we do want to show diagnostics for open +- // packages outside the workspace. Is there a better way to ensure that only +- // the 'best' View gets a workspace package for the open file? +- if containsOpenFileLocked(s, pkg) { +- return true +- } +- +- // Apply filtering logic. +- // +- // Workspace packages must contain at least one non-filtered file. +- filterFunc := s.view.filterFunc() +- uris := make(map[protocol.DocumentURI]unit) // filtered package URIs +- for _, uri := range slices.Concat(pkg.CompiledGoFiles, pkg.GoFiles) { +- if !strings.Contains(string(uri), "/vendor/") && !filterFunc(uri) { - uris[uri] = struct{}{} - } +- } +- if len(uris) == 0 { +- return false // no non-filtered files +- } - -- filterFunc := s.view.filterFunc() +- // For non-module views (of type GOPATH or AdHoc), or if +- // expandWorkspaceToModule is unset, workspace packages must be contained in +- // the workspace folder. +- // +- // For module views (of type GoMod or GoWork), packages must in any case be +- // in a workspace module (enforced below). +- if !s.view.moduleMode() || !s.Options().ExpandWorkspaceToModule { +- folder := s.view.folder.Dir.Path() +- inFolder := false - for uri := range uris { -- // Don't use view.contains here. go.work files may include modules -- // outside of the workspace folder. -- if !strings.Contains(string(uri), "/vendor/") && !filterFunc(uri) { -- return true +- if pathutil.InDir(folder, uri.Path()) { +- inFolder = true +- break - } - } -- return false -- } -- -- return containsFileInWorkspaceLocked(s.view, m) --} -- --// containsOpenFileLocked reports whether any file referenced by m is open in --// the snapshot s. --// --// s.mu must be held while calling this function. --func containsOpenFileLocked(s *snapshot, m *source.Metadata) bool { -- uris := map[span.URI]struct{}{} -- for _, uri := range m.CompiledGoFiles { -- uris[uri] = struct{}{} -- } -- for _, uri := range m.GoFiles { -- uris[uri] = struct{}{} +- if !inFolder { +- return false +- } - } - -- for uri := range uris { -- fh, _ := s.files.Get(uri) -- if _, open := fh.(*Overlay); open { -- return true +- // In module mode, a workspace package must be contained in a workspace +- // module. +- if s.view.moduleMode() { +- var modURI protocol.DocumentURI +- if pkg.Module != nil { +- modURI = protocol.URIFromPath(pkg.Module.GoMod) +- } else { +- // golang/go#65816: for std and cmd, Module is nil. +- // Fall back to an inferior heuristic. +- if len(pkg.CompiledGoFiles) == 0 { +- return false // need at least one file to guess the go.mod file +- } +- dir := pkg.CompiledGoFiles[0].Dir() +- var err error +- modURI, err = findRootPattern(ctx, dir, "go.mod", lockedSnapshot{s}) +- if err != nil || modURI == "" { +- // err != nil implies context cancellation, in which case the result of +- // this query does not matter. +- return false +- } - } +- _, ok := s.view.workspaceModFiles[modURI] +- return ok - } -- return false +- +- return true // an ad-hoc package or GOPATH package -} - --// containsFileInWorkspaceLocked reports whether m contains any file inside the --// workspace of the snapshot s. +-// containsOpenFileLocked reports whether any file referenced by m is open in +-// the snapshot s. -// -// s.mu must be held while calling this function. --func containsFileInWorkspaceLocked(v *View, m *source.Metadata) bool { -- uris := map[span.URI]struct{}{} -- for _, uri := range m.CompiledGoFiles { +-func containsOpenFileLocked(s *Snapshot, mp *metadata.Package) bool { +- uris := map[protocol.DocumentURI]struct{}{} +- for _, uri := range mp.CompiledGoFiles { - uris[uri] = struct{}{} - } -- for _, uri := range m.GoFiles { +- for _, uri := range mp.GoFiles { - uris[uri] = struct{}{} - } - - for uri := range uris { -- // In order for a package to be considered for the workspace, at least one -- // file must be contained in the workspace and not vendored. -- -- // The package's files are in this view. It may be a workspace package. -- // Vendored packages are not likely to be interesting to the user. -- if !strings.Contains(string(uri), "/vendor/") && v.contains(uri) { +- fh, _ := s.files.get(uri) +- if _, open := fh.(*overlay); open { - return true - } - } @@ -19692,42 +19699,32 @@ diff -urN a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go -// contain intermediate test variants. -// -// s.mu must be held while calling this function. --func computeWorkspacePackagesLocked(s *snapshot, meta *metadataGraph) map[PackageID]PackagePath { +-func computeWorkspacePackagesLocked(ctx context.Context, s *Snapshot, meta *metadata.Graph) immutable.Map[PackageID, PackagePath] { +- // The provided context is used for reading snapshot files, which can only +- // fail due to context cancellation. Don't let this happen as it could lead +- // to inconsistent results. +- ctx = xcontext.Detach(ctx) - workspacePackages := make(map[PackageID]PackagePath) -- for _, m := range meta.metadata { -- if !containsPackageLocked(s, m) { +- for _, mp := range meta.Packages { +- if !isWorkspacePackageLocked(ctx, s, meta, mp) { - continue - } - -- if source.IsCommandLineArguments(m.ID) { -- // If all the files contained in m have a real package, we don't need to -- // keep m as a workspace package. -- if allFilesHaveRealPackages(meta, m) { -- continue -- } -- -- // We only care about command-line-arguments packages if they are still -- // open. -- if !containsOpenFileLocked(s, m) { -- continue -- } -- } -- - switch { -- case m.ForTest == "": +- case mp.ForTest == "": - // A normal package. -- workspacePackages[m.ID] = m.PkgPath -- case m.ForTest == m.PkgPath, m.ForTest+"_test" == m.PkgPath: +- workspacePackages[mp.ID] = mp.PkgPath +- case mp.ForTest == mp.PkgPath, mp.ForTest+"_test" == mp.PkgPath: - // The test variant of some workspace package or its x_test. - // To load it, we need to load the non-test variant with -test. - // - // Notably, this excludes intermediate test variants from workspace - // packages. -- assert(!m.IsIntermediateTestVariant(), "unexpected ITV") -- workspacePackages[m.ID] = m.ForTest +- assert(!mp.IsIntermediateTestVariant(), "unexpected ITV") +- workspacePackages[mp.ID] = mp.ForTest - } - } -- return workspacePackages +- return immutable.MapOf(workspacePackages) -} - -// allFilesHaveRealPackages reports whether all files referenced by m are @@ -19737,12 +19734,12 @@ diff -urN a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go -// function returns false. -// -// If m is not a command-line-arguments package, this is trivially true. --func allFilesHaveRealPackages(g *metadataGraph, m *source.Metadata) bool { -- n := len(m.CompiledGoFiles) +-func allFilesHaveRealPackages(g *metadata.Graph, mp *metadata.Package) bool { +- n := len(mp.CompiledGoFiles) -checkURIs: -- for _, uri := range append(m.CompiledGoFiles[0:n:n], m.GoFiles...) { -- for _, id := range g.ids[uri] { -- if !source.IsCommandLineArguments(id) { +- for _, uri := range append(mp.CompiledGoFiles[0:n:n], mp.GoFiles...) { +- for _, id := range g.IDs[uri] { +- if !metadata.IsCommandLineArguments(id) { - continue checkURIs - } - } @@ -19764,1640 +19761,1338 @@ diff -urN a/gopls/internal/lsp/cache/load.go b/gopls/internal/lsp/cache/load.go - if len(pkg.GoFiles) > 1 { - return false - } -- if !source.InDir(gocache, pkg.GoFiles[0]) { +- if !pathutil.InDir(gocache, pkg.GoFiles[0]) { - return false - } - return true -} -diff -urN a/gopls/internal/lsp/cache/mod.go b/gopls/internal/lsp/cache/mod.go ---- a/gopls/internal/lsp/cache/mod.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/mod.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,518 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/cache/metadata/cycle_test.go b/gopls/internal/cache/metadata/cycle_test.go +--- a/gopls/internal/cache/metadata/cycle_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/metadata/cycle_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,146 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package cache +-package metadata - -import ( -- "context" -- "errors" -- "fmt" -- "path/filepath" -- "regexp" +- "sort" - "strings" +- "testing" - -- "golang.org/x/mod/modfile" -- "golang.org/x/mod/module" -- "golang.org/x/tools/gopls/internal/lsp/command" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/tag" -- "golang.org/x/tools/internal/gocommand" -- "golang.org/x/tools/internal/memoize" +- "golang.org/x/tools/gopls/internal/util/bug" -) - --// ParseMod parses a go.mod file, using a cache. It may return partial results and an error. --func (s *snapshot) ParseMod(ctx context.Context, fh source.FileHandle) (*source.ParsedModule, error) { -- uri := fh.URI() -- -- s.mu.Lock() -- entry, hit := s.parseModHandles.Get(uri) -- s.mu.Unlock() -- -- type parseModKey source.FileIdentity -- type parseModResult struct { -- parsed *source.ParsedModule -- err error -- } +-func init() { +- bug.PanicOnBugs = true +-} - -- // cache miss? -- if !hit { -- promise, release := s.store.Promise(parseModKey(fh.FileIdentity()), func(ctx context.Context, _ interface{}) interface{} { -- parsed, err := parseModImpl(ctx, fh) -- return parseModResult{parsed, err} -- }) +-// This is an internal test of the breakImportCycles logic. +-func TestBreakImportCycles(t *testing.T) { - -- entry = promise -- s.mu.Lock() -- s.parseModHandles.Set(uri, entry, func(_, _ interface{}) { release() }) -- s.mu.Unlock() +- // parse parses an import dependency graph. +- // The input is a semicolon-separated list of node descriptions. +- // Each node description is a package ID, optionally followed by +- // "->" and a comma-separated list of successor IDs. +- // Thus "a->b;b->c,d;e" represents the set of nodes {a,b,e} +- // and the set of edges {a->b, b->c, b->d}. +- parse := func(s string) map[PackageID]*Package { +- m := make(map[PackageID]*Package) +- makeNode := func(name string) *Package { +- id := PackageID(name) +- n, ok := m[id] +- if !ok { +- n = &Package{ +- ID: id, +- DepsByPkgPath: make(map[PackagePath]PackageID), +- } +- m[id] = n +- } +- return n +- } +- if s != "" { +- for _, item := range strings.Split(s, ";") { +- nodeID, succIDs, ok := strings.Cut(item, "->") +- node := makeNode(nodeID) +- if ok { +- for _, succID := range strings.Split(succIDs, ",") { +- node.DepsByPkgPath[PackagePath(succID)] = PackageID(succID) +- } +- } +- } +- } +- return m - } - -- // Await result. -- v, err := s.awaitPromise(ctx, entry) -- if err != nil { -- return nil, err +- // Sanity check of cycle detector. +- { +- got := cyclic(parse("a->b;b->c;c->a,d")) +- has := func(s string) bool { return strings.Contains(got, s) } +- if !(has("a->b") && has("b->c") && has("c->a") && !has("d")) { +- t.Fatalf("cyclic: got %q, want a->b->c->a or equivalent", got) +- } - } -- res := v.(parseModResult) -- return res.parsed, res.err --} -- --// parseModImpl parses the go.mod file whose name and contents are in fh. --// It may return partial results and an error. --func parseModImpl(ctx context.Context, fh source.FileHandle) (*source.ParsedModule, error) { -- _, done := event.Start(ctx, "cache.ParseMod", tag.URI.Of(fh.URI())) -- defer done() - -- contents, err := fh.Content() -- if err != nil { -- return nil, err -- } -- m := protocol.NewMapper(fh.URI(), contents) -- file, parseErr := modfile.Parse(fh.URI().Filename(), contents, nil) -- // Attempt to convert the error to a standardized parse error. -- var parseErrors []*source.Diagnostic -- if parseErr != nil { -- mfErrList, ok := parseErr.(modfile.ErrorList) -- if !ok { -- return nil, fmt.Errorf("unexpected parse error type %v", parseErr) -- } -- for _, mfErr := range mfErrList { -- rng, err := m.OffsetRange(mfErr.Pos.Byte, mfErr.Pos.Byte) -- if err != nil { -- return nil, err +- // format formats an import graph, in lexicographic order, +- // in the notation of parse, but with a "!" after the name +- // of each node that has errors. +- format := func(graph map[PackageID]*Package) string { +- var items []string +- for _, mp := range graph { +- item := string(mp.ID) +- if len(mp.Errors) > 0 { +- item += "!" - } -- parseErrors = append(parseErrors, &source.Diagnostic{ -- URI: fh.URI(), -- Range: rng, -- Severity: protocol.SeverityError, -- Source: source.ParseError, -- Message: mfErr.Err.Error(), -- }) +- var succs []string +- for _, depID := range mp.DepsByPkgPath { +- succs = append(succs, string(depID)) +- } +- if succs != nil { +- sort.Strings(succs) +- item += "->" + strings.Join(succs, ",") +- } +- items = append(items, item) - } +- sort.Strings(items) +- return strings.Join(items, ";") - } -- return &source.ParsedModule{ -- URI: fh.URI(), -- Mapper: m, -- File: file, -- ParseErrors: parseErrors, -- }, parseErr --} -- --// ParseWork parses a go.work file, using a cache. It may return partial results and an error. --// TODO(adonovan): move to new work.go file. --func (s *snapshot) ParseWork(ctx context.Context, fh source.FileHandle) (*source.ParsedWorkFile, error) { -- uri := fh.URI() - -- s.mu.Lock() -- entry, hit := s.parseWorkHandles.Get(uri) -- s.mu.Unlock() +- // We needn't test self-cycles as they are eliminated at Metadata construction. +- for _, test := range []struct { +- metadata, updates, want string +- }{ +- // Simple 2-cycle. +- {"a->b", "b->a", +- "a->b;b!"}, // broke b->a - -- type parseWorkKey source.FileIdentity -- type parseWorkResult struct { -- parsed *source.ParsedWorkFile -- err error -- } +- {"a->b;b->c;c", "b->a,c", +- "a->b;b!->c;c"}, // broke b->a - -- // cache miss? -- if !hit { -- handle, release := s.store.Promise(parseWorkKey(fh.FileIdentity()), func(ctx context.Context, _ interface{}) interface{} { -- parsed, err := parseWorkImpl(ctx, fh) -- return parseWorkResult{parsed, err} -- }) +- // Reversing direction of p->s edge creates pqrs cycle. +- {"a->p,q,r,s;p->q,s,z;q->r,z;r->s,z;s->z", "p->q,z;s->p,z", +- "a->p,q,r,s;p!->z;q->r,z;r->s,z;s!->z"}, // broke p->q, s->p - -- entry = handle -- s.mu.Lock() -- s.parseWorkHandles.Set(uri, entry, func(_, _ interface{}) { release() }) -- s.mu.Unlock() -- } +- // We break all intra-SCC edges from updated nodes, +- // which may be more than necessary (e.g. a->b). +- {"a->b;b->c;c;d->a", "a->b,e;c->d", +- "a!->e;b->c;c!;d->a"}, // broke a->b, c->d +- } { +- metadata := parse(test.metadata) +- updates := parse(test.updates) - -- // Await result. -- v, err := s.awaitPromise(ctx, entry) -- if err != nil { -- return nil, err -- } -- res := v.(parseWorkResult) -- return res.parsed, res.err --} +- if cycle := cyclic(metadata); cycle != "" { +- t.Errorf("initial metadata %s has cycle %s: ", format(metadata), cycle) +- continue +- } - --// parseWorkImpl parses a go.work file. It may return partial results and an error. --func parseWorkImpl(ctx context.Context, fh source.FileHandle) (*source.ParsedWorkFile, error) { -- _, done := event.Start(ctx, "cache.ParseWork", tag.URI.Of(fh.URI())) -- defer done() +- t.Log("initial", format(metadata)) - -- content, err := fh.Content() -- if err != nil { -- return nil, err -- } -- m := protocol.NewMapper(fh.URI(), content) -- file, parseErr := modfile.ParseWork(fh.URI().Filename(), content, nil) -- // Attempt to convert the error to a standardized parse error. -- var parseErrors []*source.Diagnostic -- if parseErr != nil { -- mfErrList, ok := parseErr.(modfile.ErrorList) -- if !ok { -- return nil, fmt.Errorf("unexpected parse error type %v", parseErr) +- // Apply updates. +- // (parse doesn't have a way to express node deletions, +- // but they aren't very interesting.) +- for id, mp := range updates { +- metadata[id] = mp - } -- for _, mfErr := range mfErrList { -- rng, err := m.OffsetRange(mfErr.Pos.Byte, mfErr.Pos.Byte) -- if err != nil { -- return nil, err -- } -- parseErrors = append(parseErrors, &source.Diagnostic{ -- URI: fh.URI(), -- Range: rng, -- Severity: protocol.SeverityError, -- Source: source.ParseError, -- Message: mfErr.Err.Error(), -- }) +- +- t.Log("updated", format(metadata)) +- +- // breakImportCycles accesses only these fields of Metadata: +- // DepsByImpPath, ID - read +- // DepsByPkgPath - read, updated +- // Errors - updated +- breakImportCycles(metadata, updates) +- +- t.Log("acyclic", format(metadata)) +- +- if cycle := cyclic(metadata); cycle != "" { +- t.Errorf("resulting metadata %s has cycle %s: ", format(metadata), cycle) - } -- } -- return &source.ParsedWorkFile{ -- URI: fh.URI(), -- Mapper: m, -- File: file, -- ParseErrors: parseErrors, -- }, parseErr --} - --// goSum reads the go.sum file for the go.mod file at modURI, if it exists. If --// it doesn't exist, it returns nil. --func (s *snapshot) goSum(ctx context.Context, modURI span.URI) []byte { -- // Get the go.sum file, either from the snapshot or directly from the -- // cache. Avoid (*snapshot).ReadFile here, as we don't want to add -- // nonexistent file handles to the snapshot if the file does not exist. -- // -- // TODO(rfindley): but that's not right. Changes to sum files should -- // invalidate content, even if it's nonexistent content. -- sumURI := span.URIFromPath(sumFilename(modURI)) -- var sumFH source.FileHandle = s.FindFile(sumURI) -- if sumFH == nil { -- var err error -- sumFH, err = s.view.fs.ReadFile(ctx, sumURI) -- if err != nil { -- return nil +- got := format(metadata) +- if got != test.want { +- t.Errorf("test.metadata=%s test.updates=%s: got=%s want=%s", +- test.metadata, test.updates, got, test.want) - } - } -- content, err := sumFH.Content() -- if err != nil { -- return nil -- } -- return content -} +diff -urN a/gopls/internal/cache/metadata/graph.go b/gopls/internal/cache/metadata/graph.go +--- a/gopls/internal/cache/metadata/graph.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/metadata/graph.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,413 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func sumFilename(modURI span.URI) string { -- return strings.TrimSuffix(modURI.Filename(), ".mod") + ".sum" --} -- --// ModWhy returns the "go mod why" result for each module named in a --// require statement in the go.mod file. --// TODO(adonovan): move to new mod_why.go file. --func (s *snapshot) ModWhy(ctx context.Context, fh source.FileHandle) (map[string]string, error) { -- uri := fh.URI() -- -- if s.FileKind(fh) != source.Mod { -- return nil, fmt.Errorf("%s is not a go.mod file", uri) -- } +-package metadata - -- s.mu.Lock() -- entry, hit := s.modWhyHandles.Get(uri) -- s.mu.Unlock() +-import ( +- "sort" - -- type modWhyResult struct { -- why map[string]string -- err error -- } +- "golang.org/x/tools/go/packages" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/bug" +-) - -- // cache miss? -- if !hit { -- handle := memoize.NewPromise("modWhy", func(ctx context.Context, arg interface{}) interface{} { -- why, err := modWhyImpl(ctx, arg.(*snapshot), fh) -- return modWhyResult{why, err} -- }) +-// A Graph is an immutable and transitively closed graph of [Package] data. +-type Graph struct { +- // Packages maps package IDs to their associated Packages. +- Packages map[PackageID]*Package - -- entry = handle -- s.mu.Lock() -- s.modWhyHandles.Set(uri, entry, nil) -- s.mu.Unlock() -- } +- // ImportedBy maps package IDs to the list of packages that import them. +- ImportedBy map[PackageID][]PackageID - -- // Await result. -- v, err := s.awaitPromise(ctx, entry) -- if err != nil { -- return nil, err -- } -- res := v.(modWhyResult) -- return res.why, res.err +- // IDs maps file URIs to package IDs, sorted by (!valid, cli, packageID). +- // A single file may belong to multiple packages due to tests packages. +- // +- // Invariant: all IDs present in the IDs map exist in the metadata map. +- IDs map[protocol.DocumentURI][]PackageID -} - --// modWhyImpl returns the result of "go mod why -m" on the specified go.mod file. --func modWhyImpl(ctx context.Context, snapshot *snapshot, fh source.FileHandle) (map[string]string, error) { -- ctx, done := event.Start(ctx, "cache.ModWhy", tag.URI.Of(fh.URI())) -- defer done() -- -- pm, err := snapshot.ParseMod(ctx, fh) -- if err != nil { -- return nil, err -- } -- // No requires to explain. -- if len(pm.File.Require) == 0 { -- return nil, nil // empty result -- } -- // Run `go mod why` on all the dependencies. -- inv := &gocommand.Invocation{ -- Verb: "mod", -- Args: []string{"why", "-m"}, -- WorkingDir: filepath.Dir(fh.URI().Filename()), +-// Update creates a new Graph containing the result of applying the given +-// updates to the receiver, though the receiver is not itself mutated. As a +-// special case, if updates is empty, Update just returns the receiver. +-// +-// A nil map value is used to indicate a deletion. +-func (g *Graph) Update(updates map[PackageID]*Package) *Graph { +- if len(updates) == 0 { +- // Optimization: since the graph is immutable, we can return the receiver. +- return g - } -- for _, req := range pm.File.Require { -- inv.Args = append(inv.Args, req.Mod.Path) +- +- // Debugging golang/go#64227, golang/vscode-go#3126: +- // Assert that the existing metadata graph is acyclic. +- if cycle := cyclic(g.Packages); cycle != "" { +- bug.Reportf("metadata is cyclic even before updates: %s", cycle) - } -- stdout, err := snapshot.RunGoCommandDirect(ctx, source.Normal, inv) -- if err != nil { -- return nil, err +- // Assert that the updates contain no self-cycles. +- for id, mp := range updates { +- if mp != nil { +- for _, depID := range mp.DepsByPkgPath { +- if depID == id { +- bug.Reportf("self-cycle in metadata update: %s", id) +- } +- } +- } - } -- whyList := strings.Split(stdout.String(), "\n\n") -- if len(whyList) != len(pm.File.Require) { -- return nil, fmt.Errorf("mismatched number of results: got %v, want %v", len(whyList), len(pm.File.Require)) +- +- // Copy pkgs map then apply updates. +- pkgs := make(map[PackageID]*Package, len(g.Packages)) +- for id, mp := range g.Packages { +- pkgs[id] = mp - } -- why := make(map[string]string, len(pm.File.Require)) -- for i, req := range pm.File.Require { -- why[req.Mod.Path] = whyList[i] +- for id, mp := range updates { +- if mp == nil { +- delete(pkgs, id) +- } else { +- pkgs[id] = mp +- } - } -- return why, nil --} - --// extractGoCommandErrors tries to parse errors that come from the go command --// and shape them into go.mod diagnostics. --// TODO: rename this to 'load errors' --func (s *snapshot) extractGoCommandErrors(ctx context.Context, goCmdError error) []*source.Diagnostic { -- if goCmdError == nil { -- return nil -- } +- // Break import cycles involving updated nodes. +- breakImportCycles(pkgs, updates) - -- type locatedErr struct { -- loc protocol.Location -- msg string -- } -- diagLocations := map[*source.ParsedModule]locatedErr{} -- backupDiagLocations := map[*source.ParsedModule]locatedErr{} +- return newGraph(pkgs) +-} - -- // If moduleErrs is non-nil, go command errors are scoped to specific -- // modules. -- var moduleErrs *moduleErrorMap -- _ = errors.As(goCmdError, &moduleErrs) +-// newGraph returns a new metadataGraph, +-// deriving relations from the specified metadata. +-func newGraph(pkgs map[PackageID]*Package) *Graph { +- // Build the import graph. +- importedBy := make(map[PackageID][]PackageID) +- for id, mp := range pkgs { +- for _, depID := range mp.DepsByPkgPath { +- importedBy[depID] = append(importedBy[depID], id) +- } +- } - -- // Match the error against all the mod files in the workspace. -- for _, uri := range s.ModFiles() { -- fh, err := s.ReadFile(ctx, uri) -- if err != nil { -- event.Error(ctx, "getting modfile for Go command error", err) -- continue +- // Collect file associations. +- uriIDs := make(map[protocol.DocumentURI][]PackageID) +- for id, mp := range pkgs { +- uris := map[protocol.DocumentURI]struct{}{} +- for _, uri := range mp.CompiledGoFiles { +- uris[uri] = struct{}{} - } -- pm, err := s.ParseMod(ctx, fh) -- if err != nil { -- // Parsing errors are reported elsewhere -- return nil +- for _, uri := range mp.GoFiles { +- uris[uri] = struct{}{} - } -- var msgs []string // error messages to consider -- if moduleErrs != nil { -- if pm.File.Module != nil { -- for _, mes := range moduleErrs.errs[pm.File.Module.Mod.Path] { -- msgs = append(msgs, mes.Error()) -- } -- } -- } else { -- msgs = append(msgs, goCmdError.Error()) +- for uri := range uris { +- uriIDs[uri] = append(uriIDs[uri], id) - } -- for _, msg := range msgs { -- if strings.Contains(goCmdError.Error(), "errors parsing go.mod") { -- // The go command emits parse errors for completely invalid go.mod files. -- // Those are reported by our own diagnostics and can be ignored here. -- // As of writing, we are not aware of any other errors that include -- // file/position information, so don't even try to find it. -- continue -- } -- loc, found, err := s.matchErrorToModule(ctx, pm, msg) -- if err != nil { -- event.Error(ctx, "matching error to module", err) -- continue -- } -- le := locatedErr{ -- loc: loc, -- msg: msg, +- } +- +- // Sort and filter file associations. +- for uri, ids := range uriIDs { +- sort.Slice(ids, func(i, j int) bool { +- cli := IsCommandLineArguments(ids[i]) +- clj := IsCommandLineArguments(ids[j]) +- if cli != clj { +- return clj - } -- if found { -- diagLocations[pm] = le -- } else { -- backupDiagLocations[pm] = le +- +- // 2. packages appear in name order. +- return ids[i] < ids[j] +- }) +- +- // Choose the best IDs for each URI, according to the following rules: +- // - If there are any valid real packages, choose them. +- // - Else, choose the first valid command-line-argument package, if it exists. +- // +- // TODO(rfindley): it might be better to track all IDs here, and exclude +- // them later when type checking, but this is the existing behavior. +- for i, id := range ids { +- // If we've seen *anything* prior to command-line arguments package, take +- // it. Note that ids[0] may itself be command-line-arguments. +- if i > 0 && IsCommandLineArguments(id) { +- uriIDs[uri] = ids[:i] +- break - } - } - } - -- // If we didn't find any good matches, assign diagnostics to all go.mod files. -- if len(diagLocations) == 0 { -- diagLocations = backupDiagLocations +- return &Graph{ +- Packages: pkgs, +- ImportedBy: importedBy, +- IDs: uriIDs, - } +-} - -- var srcErrs []*source.Diagnostic -- for pm, le := range diagLocations { -- diag, err := s.goCommandDiagnostic(pm, le.loc, le.msg) -- if err != nil { -- event.Error(ctx, "building go command diagnostic", err) -- continue +-// ReverseReflexiveTransitiveClosure returns a new mapping containing the +-// metadata for the specified packages along with any package that +-// transitively imports one of them, keyed by ID, including all the initial packages. +-func (g *Graph) ReverseReflexiveTransitiveClosure(ids ...PackageID) map[PackageID]*Package { +- seen := make(map[PackageID]*Package) +- var visitAll func([]PackageID) +- visitAll = func(ids []PackageID) { +- for _, id := range ids { +- if seen[id] == nil { +- if mp := g.Packages[id]; mp != nil { +- seen[id] = mp +- visitAll(g.ImportedBy[id]) +- } +- } - } -- srcErrs = append(srcErrs, diag) - } -- return srcErrs +- visitAll(ids) +- return seen -} - --var moduleVersionInErrorRe = regexp.MustCompile(`[:\s]([+-._~0-9A-Za-z]+)@([+-._~0-9A-Za-z]+)[:\s]`) -- --// matchErrorToModule matches a go command error message to a go.mod file. --// Some examples: --// --// example.com@v1.2.2: reading example.com/@v/v1.2.2.mod: no such file or directory --// go: github.com/cockroachdb/apd/v2@v2.0.72: reading github.com/cockroachdb/apd/go.mod at revision v2.0.72: unknown revision v2.0.72 --// go: example.com@v1.2.3 requires\n\trandom.org@v1.2.3: parsing go.mod:\n\tmodule declares its path as: bob.org\n\tbut was required as: random.org --// --// It returns the location of a reference to the one of the modules and true --// if one exists. If none is found it returns a fallback location and false. --func (s *snapshot) matchErrorToModule(ctx context.Context, pm *source.ParsedModule, goCmdError string) (protocol.Location, bool, error) { -- var reference *modfile.Line -- matches := moduleVersionInErrorRe.FindAllStringSubmatch(goCmdError, -1) +-// breakImportCycles breaks import cycles in the metadata by deleting +-// Deps* edges. It modifies only metadata present in the 'updates' +-// subset. This function has an internal test. +-func breakImportCycles(metadata, updates map[PackageID]*Package) { +- // 'go list' should never report a cycle without flagging it +- // as such, but we're extra cautious since we're combining +- // information from multiple runs of 'go list'. Also, Bazel +- // may silently report cycles. +- cycles := detectImportCycles(metadata, updates) +- if len(cycles) > 0 { +- // There were cycles (uncommon). Break them. +- // +- // The naive way to break cycles would be to perform a +- // depth-first traversal and to detect and delete +- // cycle-forming edges as we encounter them. +- // However, we're not allowed to modify the existing +- // Metadata records, so we can only break edges out of +- // the 'updates' subset. +- // +- // Another possibility would be to delete not the +- // cycle forming edge but the topmost edge on the +- // stack whose tail is an updated node. +- // However, this would require that we retroactively +- // undo all the effects of the traversals that +- // occurred since that edge was pushed on the stack. +- // +- // We use a simpler scheme: we compute the set of cycles. +- // All cyclic paths necessarily involve at least one +- // updated node, so it is sufficient to break all +- // edges from each updated node to other members of +- // the strong component. +- // +- // This may result in the deletion of dominating +- // edges, causing some dependencies to appear +- // spuriously unreachable. Consider A <-> B -> C +- // where updates={A,B}. The cycle is {A,B} so the +- // algorithm will break both A->B and B->A, causing +- // A to no longer depend on B or C. +- // +- // But that's ok: any error in Metadata.Errors is +- // conservatively assumed by snapshot.clone to be a +- // potential import cycle error, and causes special +- // invalidation so that if B later drops its +- // cycle-forming import of A, both A and B will be +- // invalidated. +- for _, cycle := range cycles { +- cyclic := make(map[PackageID]bool) +- for _, mp := range cycle { +- cyclic[mp.ID] = true +- } +- for id := range cyclic { +- if mp := updates[id]; mp != nil { +- for path, depID := range mp.DepsByImpPath { +- if cyclic[depID] { +- delete(mp.DepsByImpPath, path) +- } +- } +- for path, depID := range mp.DepsByPkgPath { +- if cyclic[depID] { +- delete(mp.DepsByPkgPath, path) +- } +- } - -- for i := len(matches) - 1; i >= 0; i-- { -- ver := module.Version{Path: matches[i][1], Version: matches[i][2]} -- if err := module.Check(ver.Path, ver.Version); err != nil { -- continue -- } -- reference = findModuleReference(pm.File, ver) -- if reference != nil { -- break +- // Set m.Errors to enable special +- // invalidation logic in snapshot.clone. +- if len(mp.Errors) == 0 { +- mp.Errors = []packages.Error{{ +- Msg: "detected import cycle", +- Kind: packages.ListError, +- }} +- } +- } +- } - } -- } - -- if reference == nil { -- // No match for the module path was found in the go.mod file. -- // Show the error on the module declaration, if one exists, or -- // just the first line of the file. -- var start, end int -- if pm.File.Module != nil && pm.File.Module.Syntax != nil { -- syntax := pm.File.Module.Syntax -- start, end = syntax.Start.Byte, syntax.End.Byte +- // double-check when debugging +- if false { +- if cycles := detectImportCycles(metadata, updates); len(cycles) > 0 { +- bug.Reportf("unbroken cycle: %v", cycles) +- } - } -- loc, err := pm.Mapper.OffsetLocation(start, end) -- return loc, false, err - } -- -- loc, err := pm.Mapper.OffsetLocation(reference.Start.Byte, reference.End.Byte) -- return loc, true, err -} - --// goCommandDiagnostic creates a diagnostic for a given go command error. --func (s *snapshot) goCommandDiagnostic(pm *source.ParsedModule, loc protocol.Location, goCmdError string) (*source.Diagnostic, error) { -- matches := moduleVersionInErrorRe.FindAllStringSubmatch(goCmdError, -1) -- var innermost *module.Version -- for i := len(matches) - 1; i >= 0; i-- { -- ver := module.Version{Path: matches[i][1], Version: matches[i][2]} -- if err := module.Check(ver.Path, ver.Version); err != nil { -- continue -- } -- innermost = &ver -- break -- } -- -- switch { -- case strings.Contains(goCmdError, "inconsistent vendoring"): -- cmd, err := command.NewVendorCommand("Run go mod vendor", command.URIArg{URI: protocol.URIFromSpanURI(pm.URI)}) -- if err != nil { -- return nil, err -- } -- return &source.Diagnostic{ -- URI: pm.URI, -- Range: loc.Range, -- Severity: protocol.SeverityError, -- Source: source.ListError, -- Message: `Inconsistent vendoring detected. Please re-run "go mod vendor". --See https://github.com/golang/go/issues/39164 for more detail on this issue.`, -- SuggestedFixes: []source.SuggestedFix{source.SuggestedFixFromCommand(cmd, protocol.QuickFix)}, -- }, nil -- -- case strings.Contains(goCmdError, "updates to go.sum needed"), strings.Contains(goCmdError, "missing go.sum entry"): -- var args []protocol.DocumentURI -- for _, uri := range s.ModFiles() { -- args = append(args, protocol.URIFromSpanURI(uri)) -- } -- tidyCmd, err := command.NewTidyCommand("Run go mod tidy", command.URIArgs{URIs: args}) -- if err != nil { -- return nil, err -- } -- updateCmd, err := command.NewUpdateGoSumCommand("Update go.sum", command.URIArgs{URIs: args}) -- if err != nil { -- return nil, err +-// cyclic returns a description of a cycle, +-// if the graph is cyclic, otherwise "". +-func cyclic(graph map[PackageID]*Package) string { +- const ( +- unvisited = 0 +- visited = 1 +- onstack = 2 +- ) +- color := make(map[PackageID]int) +- var visit func(id PackageID) string +- visit = func(id PackageID) string { +- switch color[id] { +- case unvisited: +- color[id] = onstack +- case onstack: +- return string(id) // cycle! +- case visited: +- return "" - } -- msg := "go.sum is out of sync with go.mod. Please update it by applying the quick fix." -- if innermost != nil { -- msg = fmt.Sprintf("go.sum is out of sync with go.mod: entry for %v is missing. Please updating it by applying the quick fix.", innermost) +- if mp := graph[id]; mp != nil { +- for _, depID := range mp.DepsByPkgPath { +- if cycle := visit(depID); cycle != "" { +- return string(id) + "->" + cycle +- } +- } - } -- return &source.Diagnostic{ -- URI: pm.URI, -- Range: loc.Range, -- Severity: protocol.SeverityError, -- Source: source.ListError, -- Message: msg, -- SuggestedFixes: []source.SuggestedFix{ -- source.SuggestedFixFromCommand(tidyCmd, protocol.QuickFix), -- source.SuggestedFixFromCommand(updateCmd, protocol.QuickFix), -- }, -- }, nil -- case strings.Contains(goCmdError, "disabled by GOPROXY=off") && innermost != nil: -- title := fmt.Sprintf("Download %v@%v", innermost.Path, innermost.Version) -- cmd, err := command.NewAddDependencyCommand(title, command.DependencyArgs{ -- URI: protocol.URIFromSpanURI(pm.URI), -- AddRequire: false, -- GoCmdArgs: []string{fmt.Sprintf("%v@%v", innermost.Path, innermost.Version)}, -- }) -- if err != nil { -- return nil, err +- color[id] = visited +- return "" +- } +- for id := range graph { +- if cycle := visit(id); cycle != "" { +- return cycle - } -- return &source.Diagnostic{ -- URI: pm.URI, -- Range: loc.Range, -- Severity: protocol.SeverityError, -- Message: fmt.Sprintf("%v@%v has not been downloaded", innermost.Path, innermost.Version), -- Source: source.ListError, -- SuggestedFixes: []source.SuggestedFix{source.SuggestedFixFromCommand(cmd, protocol.QuickFix)}, -- }, nil -- default: -- return &source.Diagnostic{ -- URI: pm.URI, -- Range: loc.Range, -- Severity: protocol.SeverityError, -- Source: source.ListError, -- Message: goCmdError, -- }, nil - } +- return "" -} - --func findModuleReference(mf *modfile.File, ver module.Version) *modfile.Line { -- for _, req := range mf.Require { -- if req.Mod == ver { -- return req.Syntax -- } +-// detectImportCycles reports cycles in the metadata graph. It returns a new +-// unordered array of all cycles (nontrivial strong components) in the +-// metadata graph reachable from a non-nil 'updates' value. +-func detectImportCycles(metadata, updates map[PackageID]*Package) [][]*Package { +- // We use the depth-first algorithm of Tarjan. +- // https://doi.org/10.1137/0201010 +- // +- // TODO(adonovan): when we can use generics, consider factoring +- // in common with the other implementation of Tarjan (in typerefs), +- // abstracting over the node and edge representation. +- +- // A node wraps a Metadata with its working state. +- // (Unfortunately we can't intrude on shared Metadata.) +- type node struct { +- rep *node +- mp *Package +- index, lowlink int32 +- scc int8 // TODO(adonovan): opt: cram these 1.5 bits into previous word - } -- for _, ex := range mf.Exclude { -- if ex.Mod == ver { -- return ex.Syntax +- nodes := make(map[PackageID]*node, len(metadata)) +- nodeOf := func(id PackageID) *node { +- n, ok := nodes[id] +- if !ok { +- mp := metadata[id] +- if mp == nil { +- // Dangling import edge. +- // Not sure whether a go/packages driver ever +- // emits this, but create a dummy node in case. +- // Obviously it won't be part of any cycle. +- mp = &Package{ID: id} +- } +- n = &node{mp: mp} +- n.rep = n +- nodes[id] = n - } +- return n - } -- for _, rep := range mf.Replace { -- if rep.New == ver || rep.Old == ver { -- return rep.Syntax +- +- // find returns the canonical node decl. +- // (The nodes form a disjoint set forest.) +- var find func(*node) *node +- find = func(n *node) *node { +- rep := n.rep +- if rep != n { +- rep = find(rep) +- n.rep = rep // simple path compression (no union-by-rank) - } +- return rep - } -- return nil --} -diff -urN a/gopls/internal/lsp/cache/mod_tidy.go b/gopls/internal/lsp/cache/mod_tidy.go ---- a/gopls/internal/lsp/cache/mod_tidy.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/mod_tidy.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,501 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package cache - --import ( -- "context" -- "fmt" -- "go/ast" -- "go/token" -- "os" -- "path/filepath" -- "strconv" -- "strings" +- // global state +- var ( +- index int32 = 1 +- stack []*node +- sccs [][]*Package // set of nontrivial strongly connected components +- ) - -- "golang.org/x/mod/modfile" -- "golang.org/x/tools/gopls/internal/lsp/command" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/tag" -- "golang.org/x/tools/internal/gocommand" -- "golang.org/x/tools/internal/memoize" --) +- // visit implements the depth-first search of Tarjan's SCC algorithm +- // Precondition: x is canonical. +- var visit func(*node) +- visit = func(x *node) { +- x.index = index +- x.lowlink = index +- index++ - --// ModTidy returns the go.mod file that would be obtained by running --// "go mod tidy". Concurrent requests are combined into a single command. --func (s *snapshot) ModTidy(ctx context.Context, pm *source.ParsedModule) (*source.TidiedModule, error) { -- ctx, done := event.Start(ctx, "cache.snapshot.ModTidy") -- defer done() +- stack = append(stack, x) // push +- x.scc = -1 - -- uri := pm.URI -- if pm.File == nil { -- return nil, fmt.Errorf("cannot tidy unparseable go.mod file: %v", uri) -- } +- for _, yid := range x.mp.DepsByPkgPath { +- y := nodeOf(yid) +- // Loop invariant: x is canonical. +- y = find(y) +- if x == y { +- continue // nodes already combined (self-edges are impossible) +- } - -- s.mu.Lock() -- entry, hit := s.modTidyHandles.Get(uri) -- s.mu.Unlock() +- switch { +- case y.scc > 0: +- // y is already a collapsed SCC - -- type modTidyResult struct { -- tidied *source.TidiedModule -- err error -- } +- case y.scc < 0: +- // y is on the stack, and thus in the current SCC. +- if y.index < x.lowlink { +- x.lowlink = y.index +- } - -- // Cache miss? -- if !hit { -- // If the file handle is an overlay, it may not be written to disk. -- // The go.mod file has to be on disk for `go mod tidy` to work. -- // TODO(rfindley): is this still true with Go 1.16 overlay support? -- fh, err := s.ReadFile(ctx, pm.URI) -- if err != nil { -- return nil, err -- } -- if _, ok := fh.(*Overlay); ok { -- if info, _ := os.Stat(uri.Filename()); info == nil { -- return nil, source.ErrNoModOnDisk +- default: +- // y is unvisited; visit it now. +- visit(y) +- // Note: x and y are now non-canonical. +- x = find(x) +- if y.lowlink < x.lowlink { +- x.lowlink = y.lowlink +- } - } - } - -- if criticalErr := s.CriticalError(ctx); criticalErr != nil { -- return &source.TidiedModule{ -- Diagnostics: criticalErr.Diagnostics, -- }, nil -- } -- if ctx.Err() != nil { // must check ctx after GetCriticalError -- return nil, ctx.Err() +- // Is x the root of an SCC? +- if x.lowlink == x.index { +- // Gather all metadata in the SCC (if nontrivial). +- var scc []*Package +- for { +- // Pop y from stack. +- i := len(stack) - 1 +- y := stack[i] +- stack = stack[:i] +- if x != y || scc != nil { +- scc = append(scc, y.mp) +- } +- if x == y { +- break // complete +- } +- // x becomes y's canonical representative. +- y.rep = x +- } +- if scc != nil { +- sccs = append(sccs, scc) +- } +- x.scc = 1 - } +- } - -- if err := s.awaitLoaded(ctx); err != nil { -- return nil, err +- // Visit only the updated nodes: +- // the existing metadata graph has no cycles, +- // so any new cycle must involve an updated node. +- for id, mp := range updates { +- if mp != nil { +- if n := nodeOf(id); n.index == 0 { // unvisited +- visit(n) +- } - } -- -- handle := memoize.NewPromise("modTidy", func(ctx context.Context, arg interface{}) interface{} { -- tidied, err := modTidyImpl(ctx, arg.(*snapshot), uri.Filename(), pm) -- return modTidyResult{tidied, err} -- }) -- -- entry = handle -- s.mu.Lock() -- s.modTidyHandles.Set(uri, entry, nil) -- s.mu.Unlock() - } - -- // Await result. -- v, err := s.awaitPromise(ctx, entry) -- if err != nil { -- return nil, err -- } -- res := v.(modTidyResult) -- return res.tidied, res.err +- return sccs -} +diff -urN a/gopls/internal/cache/metadata/metadata.go b/gopls/internal/cache/metadata/metadata.go +--- a/gopls/internal/cache/metadata/metadata.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/metadata/metadata.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,254 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// modTidyImpl runs "go mod tidy" on a go.mod file. --func modTidyImpl(ctx context.Context, snapshot *snapshot, filename string, pm *source.ParsedModule) (*source.TidiedModule, error) { -- ctx, done := event.Start(ctx, "cache.ModTidy", tag.URI.Of(filename)) -- defer done() -- -- inv := &gocommand.Invocation{ -- Verb: "mod", -- Args: []string{"tidy"}, -- WorkingDir: filepath.Dir(filename), -- } -- // TODO(adonovan): ensure that unsaved overlays are passed through to 'go'. -- tmpURI, inv, cleanup, err := snapshot.goCommandInvocation(ctx, source.WriteTemporaryModFile, inv) -- if err != nil { -- return nil, err -- } -- // Keep the temporary go.mod file around long enough to parse it. -- defer cleanup() +-// The metadata package defines types and functions for working with package +-// metadata, which describes Go packages and their relationships. +-// +-// Package metadata is loaded by gopls using go/packages, and the [Package] +-// type is itself a projection and translation of data from +-// go/packages.Package. +-// +-// Packages are assembled into an immutable [Graph] +-package metadata - -- if _, err := snapshot.view.gocmdRunner.Run(ctx, *inv); err != nil { -- return nil, err -- } +-import ( +- "go/ast" +- "go/types" +- "sort" +- "strconv" +- "strings" - -- // Go directly to disk to get the temporary mod file, -- // since it is always on disk. -- tempContents, err := os.ReadFile(tmpURI.Filename()) -- if err != nil { -- return nil, err -- } -- ideal, err := modfile.Parse(tmpURI.Filename(), tempContents, nil) -- if err != nil { -- // We do not need to worry about the temporary file's parse errors -- // since it has been "tidied". -- return nil, err -- } +- "golang.org/x/tools/go/packages" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/packagesinternal" +-) - -- // Compare the original and tidied go.mod files to compute errors and -- // suggested fixes. -- diagnostics, err := modTidyDiagnostics(ctx, snapshot, pm, ideal) -- if err != nil { -- return nil, err -- } +-// Declare explicit types for package paths, names, and IDs to ensure that we +-// never use an ID where a path belongs, and vice versa. If we confused these, +-// it would result in confusing errors because package IDs often look like +-// package paths. +-type ( +- PackageID string // go list's unique identifier for a package (e.g. "vendor/example.com/foo [vendor/example.com/bar.test]") +- PackagePath string // name used to prefix linker symbols (e.g. "vendor/example.com/foo") +- PackageName string // identifier in 'package' declaration (e.g. "foo") +- ImportPath string // path that appears in an import declaration (e.g. "example.com/foo") +-) - -- return &source.TidiedModule{ -- Diagnostics: diagnostics, -- TidiedContent: tempContents, -- }, nil --} +-// Package represents package metadata retrieved from go/packages. +-// The DepsBy{Imp,Pkg}Path maps do not contain self-import edges. +-// +-// An ad-hoc package (without go.mod or GOPATH) has its ID, PkgPath, +-// and LoadDir equal to the absolute path of its directory. +-type Package struct { +- ID PackageID +- PkgPath PackagePath +- Name PackageName - --// modTidyDiagnostics computes the differences between the original and tidied --// go.mod files to produce diagnostic and suggested fixes. Some diagnostics --// may appear on the Go files that import packages from missing modules. --func modTidyDiagnostics(ctx context.Context, snapshot *snapshot, pm *source.ParsedModule, ideal *modfile.File) (diagnostics []*source.Diagnostic, err error) { -- // First, determine which modules are unused and which are missing from the -- // original go.mod file. -- var ( -- unused = make(map[string]*modfile.Require, len(pm.File.Require)) -- missing = make(map[string]*modfile.Require, len(ideal.Require)) -- wrongDirectness = make(map[string]*modfile.Require, len(pm.File.Require)) -- ) -- for _, req := range pm.File.Require { -- unused[req.Mod.Path] = req -- } -- for _, req := range ideal.Require { -- origReq := unused[req.Mod.Path] -- if origReq == nil { -- missing[req.Mod.Path] = req -- continue -- } else if origReq.Indirect != req.Indirect { -- wrongDirectness[req.Mod.Path] = origReq -- } -- delete(unused, req.Mod.Path) -- } -- for _, req := range wrongDirectness { -- // Handle dependencies that are incorrectly labeled indirect and -- // vice versa. -- srcDiag, err := directnessDiagnostic(pm.Mapper, req, snapshot.Options().ComputeEdits) -- if err != nil { -- // We're probably in a bad state if we can't compute a -- // directnessDiagnostic, but try to keep going so as to not suppress -- // other, valid diagnostics. -- event.Error(ctx, "computing directness diagnostic", err) -- continue -- } -- diagnostics = append(diagnostics, srcDiag) -- } -- // Next, compute any diagnostics for modules that are missing from the -- // go.mod file. The fixes will be for the go.mod file, but the -- // diagnostics should also appear in both the go.mod file and the import -- // statements in the Go files in which the dependencies are used. -- // Finally, add errors for any unused dependencies. -- if len(missing) > 0 { -- missingModuleDiagnostics, err := missingModuleDiagnostics(ctx, snapshot, pm, ideal, missing) -- if err != nil { -- return nil, err -- } -- diagnostics = append(diagnostics, missingModuleDiagnostics...) -- } +- // these three fields are as defined by go/packages.Package +- GoFiles []protocol.DocumentURI +- CompiledGoFiles []protocol.DocumentURI +- IgnoredFiles []protocol.DocumentURI - -- // Opt: if this is the only diagnostic, we can avoid textual edits and just -- // run the Go command. -- // -- // See also the documentation for command.RemoveDependencyArgs.OnlyDiagnostic. -- onlyDiagnostic := len(diagnostics) == 0 && len(unused) == 1 -- for _, req := range unused { -- srcErr, err := unusedDiagnostic(pm.Mapper, req, onlyDiagnostic) -- if err != nil { -- return nil, err -- } -- diagnostics = append(diagnostics, srcErr) -- } -- return diagnostics, nil +- ForTest PackagePath // q in a "p [q.test]" package, else "" +- TypesSizes types.Sizes +- Errors []packages.Error // must be set for packages in import cycles +- DepsByImpPath map[ImportPath]PackageID // may contain dups; empty ID => missing +- DepsByPkgPath map[PackagePath]PackageID // values are unique and non-empty +- Module *packages.Module +- DepsErrors []*packagesinternal.PackageError +- LoadDir string // directory from which go/packages was run +- Standalone bool // package synthesized for a standalone file (e.g. ignore-tagged) -} - --func missingModuleDiagnostics(ctx context.Context, snapshot *snapshot, pm *source.ParsedModule, ideal *modfile.File, missing map[string]*modfile.Require) ([]*source.Diagnostic, error) { -- missingModuleFixes := map[*modfile.Require][]source.SuggestedFix{} -- var diagnostics []*source.Diagnostic -- for _, req := range missing { -- srcDiag, err := missingModuleDiagnostic(pm, req) -- if err != nil { -- return nil, err -- } -- missingModuleFixes[req] = srcDiag.SuggestedFixes -- diagnostics = append(diagnostics, srcDiag) -- } -- -- // Add diagnostics for missing modules anywhere they are imported in the -- // workspace. -- metas, err := snapshot.WorkspaceMetadata(ctx) -- if err != nil { -- return nil, err -- } -- // TODO(adonovan): opt: opportunities for parallelism abound. -- for _, m := range metas { -- // Read both lists of files of this package. -- // -- // Parallelism is not necessary here as the files will have already been -- // pre-read at load time. -- goFiles, err := readFiles(ctx, snapshot, m.GoFiles) -- if err != nil { -- return nil, err -- } -- compiledGoFiles, err := readFiles(ctx, snapshot, m.CompiledGoFiles) -- if err != nil { -- return nil, err -- } -- -- missingImports := map[string]*modfile.Require{} +-func (mp *Package) String() string { return string(mp.ID) } - -- // If -mod=readonly is not set we may have successfully imported -- // packages from missing modules. Otherwise they'll be in -- // MissingDependencies. Combine both. -- imps, err := parseImports(ctx, snapshot, goFiles) -- if err != nil { -- return nil, err -- } -- for imp := range imps { -- if req, ok := missing[imp]; ok { -- missingImports[imp] = req -- break -- } -- // If the import is a package of the dependency, then add the -- // package to the map, this will eliminate the need to do this -- // prefix package search on each import for each file. -- // Example: -- // -- // import ( -- // "golang.org/x/tools/go/expect" -- // "golang.org/x/tools/go/packages" -- // ) -- // They both are related to the same module: "golang.org/x/tools". -- var match string -- for _, req := range ideal.Require { -- if strings.HasPrefix(imp, req.Mod.Path) && len(req.Mod.Path) > len(match) { -- match = req.Mod.Path -- } -- } -- if req, ok := missing[match]; ok { -- missingImports[imp] = req -- } -- } -- // None of this package's imports are from missing modules. -- if len(missingImports) == 0 { -- continue -- } -- for _, goFile := range compiledGoFiles { -- pgf, err := snapshot.ParseGo(ctx, goFile, source.ParseHeader) -- if err != nil { -- continue -- } -- file, m := pgf.File, pgf.Mapper -- if file == nil || m == nil { -- continue -- } -- imports := make(map[string]*ast.ImportSpec) -- for _, imp := range file.Imports { -- if imp.Path == nil { -- continue -- } -- if target, err := strconv.Unquote(imp.Path.Value); err == nil { -- imports[target] = imp -- } -- } -- if len(imports) == 0 { -- continue -- } -- for importPath, req := range missingImports { -- imp, ok := imports[importPath] -- if !ok { -- continue -- } -- fixes, ok := missingModuleFixes[req] -- if !ok { -- return nil, fmt.Errorf("no missing module fix for %q (%q)", importPath, req.Mod.Path) -- } -- srcErr, err := missingModuleForImport(pgf, imp, req, fixes) -- if err != nil { -- return nil, err -- } -- diagnostics = append(diagnostics, srcErr) -- } -- } -- } -- return diagnostics, nil +-// IsIntermediateTestVariant reports whether the given package is an +-// intermediate test variant (ITV), e.g. "net/http [net/url.test]". +-// +-// An ITV has identical syntax to the regular variant, but different +-// import metadata (DepsBy{Imp,Pkg}Path). +-// +-// Such test variants arise when an x_test package (in this case net/url_test) +-// imports a package (in this case net/http) that itself imports the +-// non-x_test package (in this case net/url). +-// +-// This is done so that the forward transitive closure of net/url_test has +-// only one package for the "net/url" import. +-// The ITV exists to hold the test variant import: +-// +-// net/url_test [net/url.test] +-// +-// | "net/http" -> net/http [net/url.test] +-// | "net/url" -> net/url [net/url.test] +-// | ... +-// +-// net/http [net/url.test] +-// +-// | "net/url" -> net/url [net/url.test] +-// | ... +-// +-// This restriction propagates throughout the import graph of net/http: for +-// every package imported by net/http that imports net/url, there must be an +-// intermediate test variant that instead imports "net/url [net/url.test]". +-// +-// As one can see from the example of net/url and net/http, intermediate test +-// variants can result in many additional packages that are essentially (but +-// not quite) identical. For this reason, we filter these variants wherever +-// possible. +-// +-// # Why we mostly ignore intermediate test variants +-// +-// In projects with complicated tests, there may be a very large +-// number of ITVs--asymptotically more than the number of ordinary +-// variants. Since they have identical syntax, it is fine in most +-// cases to ignore them since the results of analyzing the ordinary +-// variant suffice. However, this is not entirely sound. +-// +-// Consider this package: +-// +-// // p/p.go -- in all variants of p +-// package p +-// type T struct { io.Closer } +-// +-// // p/p_test.go -- in test variant of p +-// package p +-// func (T) Close() error { ... } +-// +-// The ordinary variant "p" defines T with a Close method promoted +-// from io.Closer. But its test variant "p [p.test]" defines a type T +-// with a Close method from p_test.go. +-// +-// Now consider a package q that imports p, perhaps indirectly. Within +-// it, T.Close will resolve to the first Close method: +-// +-// // q/q.go -- in all variants of q +-// package q +-// import "p" +-// var _ = new(p.T).Close +-// +-// Let's assume p also contains this file defining an external test (xtest): +-// +-// // p/p_x_test.go -- external test of p +-// package p_test +-// import ( "q"; "testing" ) +-// func Test(t *testing.T) { ... } +-// +-// Note that q imports p, but p's xtest imports q. Now, in "q +-// [p.test]", the intermediate test variant of q built for p's +-// external test, T.Close resolves not to the io.Closer.Close +-// interface method, but to the concrete method of T.Close +-// declared in p_test.go. +-// +-// If we now request all references to the T.Close declaration in +-// p_test.go, the result should include the reference from q's ITV. +-// (It's not just methods that can be affected; fields can too, though +-// it requires bizarre code to achieve.) +-// +-// As a matter of policy, gopls mostly ignores this subtlety, +-// because to account for it would require that we type-check every +-// intermediate test variant of p, of which there could be many. +-// Good code doesn't rely on such trickery. +-// +-// Most callers of MetadataForFile call RemoveIntermediateTestVariants +-// to discard them before requesting type checking, or the products of +-// type-checking such as the cross-reference index or method set index. +-// +-// MetadataForFile doesn't do this filtering itself becaused in some +-// cases we need to make a reverse dependency query on the metadata +-// graph, and it's important to include the rdeps of ITVs in that +-// query. But the filtering of ITVs should be applied after that step, +-// before type checking. +-// +-// In general, we should never type check an ITV. +-func (mp *Package) IsIntermediateTestVariant() bool { +- return mp.ForTest != "" && mp.ForTest != mp.PkgPath && mp.ForTest+"_test" != mp.PkgPath -} - --// unusedDiagnostic returns a source.Diagnostic for an unused require. --func unusedDiagnostic(m *protocol.Mapper, req *modfile.Require, onlyDiagnostic bool) (*source.Diagnostic, error) { -- rng, err := m.OffsetRange(req.Syntax.Start.Byte, req.Syntax.End.Byte) -- if err != nil { -- return nil, err -- } -- title := fmt.Sprintf("Remove dependency: %s", req.Mod.Path) -- cmd, err := command.NewRemoveDependencyCommand(title, command.RemoveDependencyArgs{ -- URI: protocol.URIFromSpanURI(m.URI), -- OnlyDiagnostic: onlyDiagnostic, -- ModulePath: req.Mod.Path, -- }) -- if err != nil { -- return nil, err -- } -- return &source.Diagnostic{ -- URI: m.URI, -- Range: rng, -- Severity: protocol.SeverityWarning, -- Source: source.ModTidyError, -- Message: fmt.Sprintf("%s is not used in this module", req.Mod.Path), -- SuggestedFixes: []source.SuggestedFix{source.SuggestedFixFromCommand(cmd, protocol.QuickFix)}, -- }, nil +-// A Source maps package IDs to metadata for the packages. +-// +-// TODO(rfindley): replace this with a concrete metadata graph, once it is +-// exposed from the snapshot. +-type Source interface { +- // Metadata returns the [Package] for the given package ID, or nil if it does +- // not exist. +- // TODO(rfindley): consider returning (*Metadata, bool) +- // TODO(rfindley): consider renaming this method. +- Metadata(PackageID) *Package -} - --// directnessDiagnostic extracts errors when a dependency is labeled indirect when --// it should be direct and vice versa. --func directnessDiagnostic(m *protocol.Mapper, req *modfile.Require, computeEdits source.DiffFunction) (*source.Diagnostic, error) { -- rng, err := m.OffsetRange(req.Syntax.Start.Byte, req.Syntax.End.Byte) -- if err != nil { -- return nil, err -- } -- direction := "indirect" -- if req.Indirect { -- direction = "direct" +-// TODO(rfindley): move the utility functions below to a util.go file. - -- // If the dependency should be direct, just highlight the // indirect. -- if comments := req.Syntax.Comment(); comments != nil && len(comments.Suffix) > 0 { -- end := comments.Suffix[0].Start -- end.LineRune += len(comments.Suffix[0].Token) -- end.Byte += len(comments.Suffix[0].Token) -- rng, err = m.OffsetRange(comments.Suffix[0].Start.Byte, end.Byte) -- if err != nil { -- return nil, err +-// IsCommandLineArguments reports whether a given value denotes +-// "command-line-arguments" package, which is a package with an unknown ID +-// created by the go command. It can have a test variant, which is why callers +-// should not check that a value equals "command-line-arguments" directly. +-func IsCommandLineArguments(id PackageID) bool { +- return strings.Contains(string(id), "command-line-arguments") +-} +- +-// SortPostOrder sorts the IDs so that if x depends on y, then y appears before x. +-func SortPostOrder(meta Source, ids []PackageID) { +- postorder := make(map[PackageID]int) +- order := 0 +- var visit func(PackageID) +- visit = func(id PackageID) { +- if _, ok := postorder[id]; !ok { +- postorder[id] = -1 // break recursion +- if mp := meta.Metadata(id); mp != nil { +- for _, depID := range mp.DepsByPkgPath { +- visit(depID) +- } - } +- order++ +- postorder[id] = order - } - } -- // If the dependency should be indirect, add the // indirect. -- edits, err := switchDirectness(req, m, computeEdits) -- if err != nil { -- return nil, err +- for _, id := range ids { +- visit(id) - } -- return &source.Diagnostic{ -- URI: m.URI, -- Range: rng, -- Severity: protocol.SeverityWarning, -- Source: source.ModTidyError, -- Message: fmt.Sprintf("%s should be %s", req.Mod.Path, direction), -- SuggestedFixes: []source.SuggestedFix{{ -- Title: fmt.Sprintf("Change %s to %s", req.Mod.Path, direction), -- Edits: map[span.URI][]protocol.TextEdit{ -- m.URI: edits, -- }, -- ActionKind: protocol.QuickFix, -- }}, -- }, nil +- sort.Slice(ids, func(i, j int) bool { +- return postorder[ids[i]] < postorder[ids[j]] +- }) -} - --func missingModuleDiagnostic(pm *source.ParsedModule, req *modfile.Require) (*source.Diagnostic, error) { -- var rng protocol.Range -- // Default to the start of the file if there is no module declaration. -- if pm.File != nil && pm.File.Module != nil && pm.File.Module.Syntax != nil { -- start, end := pm.File.Module.Syntax.Span() -- var err error -- rng, err = pm.Mapper.OffsetRange(start.Byte, end.Byte) -- if err != nil { -- return nil, err -- } -- } -- title := fmt.Sprintf("Add %s to your go.mod file", req.Mod.Path) -- cmd, err := command.NewAddDependencyCommand(title, command.DependencyArgs{ -- URI: protocol.URIFromSpanURI(pm.Mapper.URI), -- AddRequire: !req.Indirect, -- GoCmdArgs: []string{req.Mod.Path + "@" + req.Mod.Version}, -- }) +-// UnquoteImportPath returns the unquoted import path of s, +-// or "" if the path is not properly quoted. +-func UnquoteImportPath(spec *ast.ImportSpec) ImportPath { +- path, err := strconv.Unquote(spec.Path.Value) - if err != nil { -- return nil, err +- return "" - } -- return &source.Diagnostic{ -- URI: pm.Mapper.URI, -- Range: rng, -- Severity: protocol.SeverityError, -- Source: source.ModTidyError, -- Message: fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path), -- SuggestedFixes: []source.SuggestedFix{source.SuggestedFixFromCommand(cmd, protocol.QuickFix)}, -- }, nil +- return ImportPath(path) -} - --// switchDirectness gets the edits needed to change an indirect dependency to --// direct and vice versa. --func switchDirectness(req *modfile.Require, m *protocol.Mapper, computeEdits source.DiffFunction) ([]protocol.TextEdit, error) { -- // We need a private copy of the parsed go.mod file, since we're going to -- // modify it. -- copied, err := modfile.Parse("", m.Content, nil) -- if err != nil { -- return nil, err -- } -- // Change the directness in the matching require statement. To avoid -- // reordering the require statements, rewrite all of them. -- var requires []*modfile.Require -- seenVersions := make(map[string]string) -- for _, r := range copied.Require { -- if seen := seenVersions[r.Mod.Path]; seen != "" && seen != r.Mod.Version { -- // Avoid a panic in SetRequire below, which panics on conflicting -- // versions. -- return nil, fmt.Errorf("%q has conflicting versions: %q and %q", r.Mod.Path, seen, r.Mod.Version) -- } -- seenVersions[r.Mod.Path] = r.Mod.Version -- if r.Mod.Path == req.Mod.Path { -- requires = append(requires, &modfile.Require{ -- Mod: r.Mod, -- Syntax: r.Syntax, -- Indirect: !r.Indirect, -- }) -- continue +-// RemoveIntermediateTestVariants removes intermediate test variants, modifying +-// the array. We use a pointer to a slice make it impossible to forget to use +-// the result. +-func RemoveIntermediateTestVariants(pmetas *[]*Package) { +- metas := *pmetas +- res := metas[:0] +- for _, mp := range metas { +- if !mp.IsIntermediateTestVariant() { +- res = append(res, mp) - } -- requires = append(requires, r) -- } -- copied.SetRequire(requires) -- newContent, err := copied.Format() -- if err != nil { -- return nil, err - } -- // Calculate the edits to be made due to the change. -- edits := computeEdits(string(m.Content), string(newContent)) -- return source.ToProtocolEdits(m, edits) +- *pmetas = res -} - --// missingModuleForImport creates an error for a given import path that comes --// from a missing module. --func missingModuleForImport(pgf *source.ParsedGoFile, imp *ast.ImportSpec, req *modfile.Require, fixes []source.SuggestedFix) (*source.Diagnostic, error) { -- if req.Syntax == nil { -- return nil, fmt.Errorf("no syntax for %v", req) +-// IsValidImport returns whether importPkgPath is importable +-// by pkgPath. +-func IsValidImport(pkgPath, importPkgPath PackagePath) bool { +- i := strings.LastIndex(string(importPkgPath), "/internal/") +- if i == -1 { +- return true - } -- rng, err := pgf.NodeRange(imp.Path) -- if err != nil { -- return nil, err +- // TODO(rfindley): this looks wrong: IsCommandLineArguments is meant to +- // operate on package IDs, not package paths. +- if IsCommandLineArguments(PackageID(pkgPath)) { +- return true - } -- return &source.Diagnostic{ -- URI: pgf.URI, -- Range: rng, -- Severity: protocol.SeverityError, -- Source: source.ModTidyError, -- Message: fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path), -- SuggestedFixes: fixes, -- }, nil +- // TODO(rfindley): this is wrong. mod.testx/p should not be able to +- // import mod.test/internal: https://go.dev/play/p/-Ca6P-E4V4q +- return strings.HasPrefix(string(pkgPath), string(importPkgPath[:i])) -} +diff -urN a/gopls/internal/cache/methodsets/methodsets.go b/gopls/internal/cache/methodsets/methodsets.go +--- a/gopls/internal/cache/methodsets/methodsets.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/methodsets/methodsets.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,493 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// parseImports parses the headers of the specified files and returns --// the set of strings that appear in import declarations within --// GoFiles. Errors are ignored. +-// Package methodsets defines an incremental, serializable index of +-// method-set information that allows efficient 'implements' queries +-// across packages of the workspace without using the type checker. -// --// (We can't simply use Metadata.Imports because it is based on --// CompiledGoFiles, after cgo processing.) +-// This package provides only the "global" (all workspace) search; the +-// "local" search within a given package uses a different +-// implementation based on type-checker data structures for a single +-// package plus variants; see ../implementation.go. +-// The local algorithm is more precise as it tests function-local types too. -// --// TODO(rfindley): this should key off source.ImportPath. --func parseImports(ctx context.Context, s *snapshot, files []source.FileHandle) (map[string]bool, error) { -- pgfs, err := s.view.parseCache.parseFiles(ctx, token.NewFileSet(), source.ParseHeader, false, files...) -- if err != nil { // e.g. context cancellation -- return nil, err -- } -- -- seen := make(map[string]bool) -- for _, pgf := range pgfs { -- for _, spec := range pgf.File.Imports { -- path, _ := strconv.Unquote(spec.Path.Value) -- seen[path] = true -- } -- } -- return seen, nil --} -diff -urN a/gopls/internal/lsp/cache/mod_vuln.go b/gopls/internal/lsp/cache/mod_vuln.go ---- a/gopls/internal/lsp/cache/mod_vuln.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/mod_vuln.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,48 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-// A global index of function-local types is challenging since they +-// may reference other local types, for which we would need to invent +-// stable names, an unsolved problem described in passing in Go issue +-// 57497. The global algorithm also does not index anonymous interface +-// types, even outside function bodies. +-// +-// Consequently, global results are not symmetric: applying the +-// operation twice may not get you back where you started. +-package methodsets - --package cache +-// DESIGN +-// +-// See https://go.dev/cl/452060 for a minimal exposition of the algorithm. +-// +-// For each method, we compute a fingerprint: a string representing +-// the method name and type such that equal fingerprint strings mean +-// identical method types. +-// +-// For efficiency, the fingerprint is reduced to a single bit +-// of a uint64, so that the method set can be represented as +-// the union of those method bits (a uint64 bitmask). +-// Assignability thus reduces to a subset check on bitmasks +-// followed by equality checks on fingerprints. +-// +-// In earlier experiments, using 128-bit masks instead of 64 reduced +-// the number of candidates by about 2x. Using (like a Bloom filter) a +-// different hash function to compute a second 64-bit mask and +-// performing a second mask test reduced it by about 4x. +-// Neither had much effect on the running time, presumably because a +-// single 64-bit mask is quite effective. See CL 452060 for details. - -import ( -- "context" +- "fmt" +- "go/token" +- "go/types" +- "hash/crc32" +- "strconv" +- "strings" - -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/gopls/internal/vulncheck" -- "golang.org/x/tools/gopls/internal/vulncheck/scan" -- "golang.org/x/tools/internal/memoize" +- "golang.org/x/tools/go/types/objectpath" +- "golang.org/x/tools/gopls/internal/util/frob" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/internal/aliases" -) - --// ModVuln returns import vulnerability analysis for the given go.mod URI. --// Concurrent requests are combined into a single command. --func (s *snapshot) ModVuln(ctx context.Context, modURI span.URI) (*vulncheck.Result, error) { -- s.mu.Lock() -- entry, hit := s.modVulnHandles.Get(modURI) -- s.mu.Unlock() -- -- type modVuln struct { -- result *vulncheck.Result -- err error -- } -- -- // Cache miss? -- if !hit { -- handle := memoize.NewPromise("modVuln", func(ctx context.Context, arg interface{}) interface{} { -- result, err := scan.VulnerablePackages(ctx, arg.(*snapshot)) -- return modVuln{result, err} -- }) -- -- entry = handle -- s.mu.Lock() -- s.modVulnHandles.Set(modURI, entry, nil) -- s.mu.Unlock() -- } +-// An Index records the non-empty method sets of all package-level +-// types in a package in a form that permits assignability queries +-// without the type checker. +-type Index struct { +- pkg gobPackage +-} - -- // Await result. -- v, err := s.awaitPromise(ctx, entry) -- if err != nil { -- return nil, err -- } -- res := v.(modVuln) -- return res.result, res.err +-// Decode decodes the given gob-encoded data as an Index. +-func Decode(data []byte) *Index { +- var pkg gobPackage +- packageCodec.Decode(data, &pkg) +- return &Index{pkg} -} -diff -urN a/gopls/internal/lsp/cache/os_darwin.go b/gopls/internal/lsp/cache/os_darwin.go ---- a/gopls/internal/lsp/cache/os_darwin.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/os_darwin.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,59 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package cache +-// Encode encodes the receiver as gob-encoded data. +-func (index *Index) Encode() []byte { +- return packageCodec.Encode(index.pkg) +-} - --import ( -- "bytes" -- "fmt" -- "os" -- "path/filepath" -- "strings" -- "syscall" -- "unsafe" --) +-// NewIndex returns a new index of method-set information for all +-// package-level types in the specified package. +-func NewIndex(fset *token.FileSet, pkg *types.Package) *Index { +- return new(indexBuilder).build(fset, pkg) +-} - --func init() { -- checkPathCase = darwinCheckPathCase +-// A Location records the extent of an identifier in byte-offset form. +-// +-// Conversion to protocol (UTF-16) form is done by the caller after a +-// search, not during index construction. +-type Location struct { +- Filename string +- Start, End int // byte offsets -} - --func darwinCheckPathCase(path string) error { -- // Darwin provides fcntl(F_GETPATH) to get a path for an arbitrary FD. -- // Conveniently for our purposes, it gives the canonical case back. But -- // there's no guarantee that it will follow the same route through the -- // filesystem that the original path did. +-// A Key represents the method set of a given type in a form suitable +-// to pass to the (*Index).Search method of many different Indexes. +-type Key struct { +- mset gobMethodSet // note: lacks position information +-} - -- path, err := filepath.Abs(path) -- if err != nil { -- return err -- } -- fd, err := syscall.Open(path, os.O_RDONLY, 0) -- if err != nil { -- return err +-// KeyOf returns the search key for the method sets of a given type. +-// It returns false if the type has no methods. +-func KeyOf(t types.Type) (Key, bool) { +- mset := methodSetInfo(t, nil) +- if mset.Mask == 0 { +- return Key{}, false // no methods - } -- defer syscall.Close(fd) -- buf := make([]byte, 4096) // No MAXPATHLEN in syscall, I think it's 1024, this is bigger. +- return Key{mset}, true +-} - -- // Wheeee! syscall doesn't expose a way to call Fcntl except FcntlFlock. -- // As of writing, it just passes the pointer through, so we can just lie. -- if err := syscall.FcntlFlock(uintptr(fd), syscall.F_GETPATH, (*syscall.Flock_t)(unsafe.Pointer(&buf[0]))); err != nil { -- return err -- } -- buf = buf[:bytes.IndexByte(buf, 0)] +-// A Result reports a matching type or method in a method-set search. +-type Result struct { +- Location Location // location of the type or method - -- isRoot := func(p string) bool { -- return p[len(p)-1] == filepath.Separator -- } -- // Darwin seems to like having multiple names for the same folder. Match as much of the suffix as we can. -- for got, want := path, string(buf); !isRoot(got) && !isRoot(want); got, want = filepath.Dir(got), filepath.Dir(want) { -- g, w := filepath.Base(got), filepath.Base(want) -- if !strings.EqualFold(g, w) { -- break -- } -- if g != w { -- return fmt.Errorf("case mismatch in path %q: component %q is listed by macOS as %q", path, g, w) -- } -- } -- return nil --} -diff -urN a/gopls/internal/lsp/cache/os_windows.go b/gopls/internal/lsp/cache/os_windows.go ---- a/gopls/internal/lsp/cache/os_windows.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/os_windows.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,56 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package cache -- --import ( -- "fmt" -- "path/filepath" -- "syscall" --) -- --func init() { -- checkPathCase = windowsCheckPathCase +- // methods only: +- PkgPath string // path of declaring package (may differ due to embedding) +- ObjectPath objectpath.Path // path of method within declaring package -} - --func windowsCheckPathCase(path string) error { -- // Back in the day, Windows used to have short and long filenames, and -- // it still supports those APIs. GetLongPathName gets the real case for a -- // path, so we can use it here. Inspired by -- // http://stackoverflow.com/q/2113822. -- -- // Short paths can be longer than long paths, and unicode, so be generous. -- buflen := 4 * len(path) -- namep, err := syscall.UTF16PtrFromString(path) -- if err != nil { -- return err -- } -- short := make([]uint16, buflen) -- n, err := syscall.GetShortPathName(namep, &short[0], uint32(len(short)*2)) // buflen is in bytes. -- if err != nil { -- return err -- } -- if int(n) > len(short)*2 { -- return fmt.Errorf("short buffer too short: %v vs %v*2", n, len(short)) -- } -- long := make([]uint16, buflen) -- n, err = syscall.GetLongPathName(&short[0], &long[0], uint32(len(long)*2)) -- if err != nil { -- return err -- } -- if int(n) > len(long)*2 { -- return fmt.Errorf("long buffer too short: %v vs %v*2", n, len(long)) -- } -- longstr := syscall.UTF16ToString(long) -- -- isRoot := func(p string) bool { -- return p[len(p)-1] == filepath.Separator -- } -- for got, want := path, longstr; !isRoot(got) && !isRoot(want); got, want = filepath.Dir(got), filepath.Dir(want) { -- if g, w := filepath.Base(got), filepath.Base(want); g != w { -- return fmt.Errorf("case mismatch in path %q: component %q is listed by Windows as %q", path, g, w) +-// Search reports each type that implements (or is implemented by) the +-// type that produced the search key. If methodID is nonempty, only +-// that method of each type is reported. +-// +-// The result does not include the error.Error method. +-// TODO(adonovan): give this special case a more systematic treatment. +-func (index *Index) Search(key Key, methodID string) []Result { +- var results []Result +- for _, candidate := range index.pkg.MethodSets { +- // Traditionally this feature doesn't report +- // interface/interface elements of the relation. +- // I think that's a mistake. +- // TODO(adonovan): UX: change it, here and in the local implementation. +- if candidate.IsInterface && key.mset.IsInterface { +- continue +- } +- if !satisfies(candidate, key.mset) && !satisfies(key.mset, candidate) { +- continue - } -- } -- return nil --} -diff -urN a/gopls/internal/lsp/cache/parse_cache.go b/gopls/internal/lsp/cache/parse_cache.go ---- a/gopls/internal/lsp/cache/parse_cache.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/parse_cache.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,418 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package cache -- --import ( -- "bytes" -- "container/heap" -- "context" -- "fmt" -- "go/parser" -- "go/token" -- "math/bits" -- "runtime" -- "sync" -- "time" - -- "golang.org/x/sync/errgroup" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/memoize" -- "golang.org/x/tools/internal/tokeninternal" --) +- if candidate.Tricky { +- // If any interface method is tricky then extra +- // checking may be needed to eliminate a false positive. +- // TODO(adonovan): implement it. +- } - --// This file contains an implementation of an LRU parse cache, that offsets the --// base token.Pos value of each cached file so that they may be later described --// by a single dedicated FileSet. --// --// This is achieved by tracking a monotonic offset in the token.Pos space, that --// is incremented before parsing allow room for the resulting parsed file. +- if methodID == "" { +- results = append(results, Result{Location: index.location(candidate.Posn)}) +- } else { +- for _, m := range candidate.Methods { +- // Here we exploit knowledge of the shape of the fingerprint string. +- if strings.HasPrefix(m.Fingerprint, methodID) && +- m.Fingerprint[len(methodID)] == '(' { - --// reservedForParsing defines the room in the token.Pos space reserved for --// cached parsed files. --// --// Files parsed through the parseCache are guaranteed not to have overlapping --// spans: the parseCache tracks a monotonic base for newly parsed files. --// --// By offsetting the initial base of a FileSet, we can allow other operations --// accepting the FileSet (such as the gcimporter) to add new files using the --// normal FileSet APIs without overlapping with cached parsed files. --// --// Note that 1<<60 represents an exabyte of parsed data, more than any gopls --// process can ever parse. --// --// On 32-bit systems we don't cache parse results (see parseFiles). --const reservedForParsing = 1 << (bits.UintSize - 4) +- // Don't report error.Error among the results: +- // it has no true source location, no package, +- // and is excluded from the xrefs index. +- if m.PkgPath == 0 || m.ObjectPath == 0 { +- if methodID != "Error" { +- panic("missing info for" + methodID) +- } +- continue +- } - --// fileSetWithBase returns a new token.FileSet with Base() equal to the --// requested base. --// --// If base < 1, fileSetWithBase panics. --// (1 is the smallest permitted FileSet base). --func fileSetWithBase(base int) *token.FileSet { -- fset := token.NewFileSet() -- if base > 1 { -- // Add a dummy file to set the base of fset. We won't ever use the -- // resulting FileSet, so it doesn't matter how we achieve this. -- // -- // FileSets leave a 1-byte padding between files, so we set the base by -- // adding a zero-length file at base-1. -- fset.AddFile("", base-1, 0) -- } -- if fset.Base() != base { -- panic("unexpected FileSet.Base") +- results = append(results, Result{ +- Location: index.location(m.Posn), +- PkgPath: index.pkg.Strings[m.PkgPath], +- ObjectPath: objectpath.Path(index.pkg.Strings[m.ObjectPath]), +- }) +- break +- } +- } +- } - } -- return fset +- return results -} - --const ( -- // Always keep 100 recent files, independent of their wall-clock age, to -- // optimize the case where the user resumes editing after a delay. -- parseCacheMinFiles = 100 --) -- --// parsePadding is additional padding allocated to allow for increases in --// length (such as appending missing braces) caused by fixAST. --// --// This is used to mitigate a chicken and egg problem: we must know the base --// offset of the file we're about to parse, before we start parsing, and yet --// src fixups may affect the actual size of the parsed content (and therefore --// the offsets of subsequent files). --// --// When we encounter a file that no longer fits in its allocated space in the --// fileset, we have no choice but to re-parse it. Leaving a generous padding --// reduces the likelihood of this "slow path". --// --// This value is mutable for testing, so that we can exercise the slow path. --var parsePadding = 1000 // mutable for testing -- --// A parseCache holds recently accessed parsed Go files. After new files are --// stored, older files may be evicted from the cache via garbage collection. --// --// The parseCache.parseFiles method exposes a batch API for parsing (and --// caching) multiple files. This is necessary for type-checking, where files --// must be parsed in a common fileset. --type parseCache struct { -- expireAfter time.Duration // interval at which to collect expired cache entries -- done chan struct{} // closed when GC is stopped -- -- mu sync.Mutex -- m map[parseKey]*parseCacheEntry -- lru queue // min-atime priority queue of *parseCacheEntry -- clock uint64 // clock time, incremented when the cache is updated -- nextBase int // base offset for the next parsed file +-// satisfies does a fast check for whether x satisfies y. +-func satisfies(x, y gobMethodSet) bool { +- return y.IsInterface && x.Mask&y.Mask == y.Mask && subset(y, x) -} - --// newParseCache creates a new parse cache and starts a goroutine to garbage --// collect entries whose age is at least expireAfter. --// --// Callers must call parseCache.stop when the parse cache is no longer in use. --func newParseCache(expireAfter time.Duration) *parseCache { -- c := &parseCache{ -- expireAfter: expireAfter, -- m: make(map[parseKey]*parseCacheEntry), -- done: make(chan struct{}), +-// subset reports whether method set x is a subset of y. +-func subset(x, y gobMethodSet) bool { +-outer: +- for _, mx := range x.Methods { +- for _, my := range y.Methods { +- if mx.Sum == my.Sum && mx.Fingerprint == my.Fingerprint { +- continue outer // found; try next x method +- } +- } +- return false // method of x not found in y - } -- go c.gc() -- return c +- return true // all methods of x found in y -} - --// stop causes the GC goroutine to exit. --func (c *parseCache) stop() { -- close(c.done) +-func (index *Index) location(posn gobPosition) Location { +- return Location{ +- Filename: index.pkg.Strings[posn.File], +- Start: posn.Offset, +- End: posn.Offset + posn.Len, +- } -} - --// parseKey uniquely identifies a parsed Go file. --type parseKey struct { -- uri span.URI -- mode parser.Mode -- purgeFuncBodies bool +-// An indexBuilder builds an index for a single package. +-type indexBuilder struct { +- gobPackage +- stringIndex map[string]int -} - --type parseCacheEntry struct { -- key parseKey -- hash source.Hash -- promise *memoize.Promise // memoize.Promise[*source.ParsedGoFile] -- atime uint64 // clock time of last access, for use in LRU sorting -- walltime time.Time // actual time of last access, for use in time-based eviction; too coarse for LRU on some systems -- lruIndex int // owned by the queue implementation --} +-// build adds to the index all package-level named types of the specified package. +-func (b *indexBuilder) build(fset *token.FileSet, pkg *types.Package) *Index { +- _ = b.string("") // 0 => "" - --// startParse prepares a parsing pass, creating new promises in the cache for --// any cache misses. --// --// The resulting slice has an entry for every given file handle, though some --// entries may be nil if there was an error reading the file (in which case the --// resulting error will be non-nil). --func (c *parseCache) startParse(mode parser.Mode, purgeFuncBodies bool, fhs ...source.FileHandle) ([]*memoize.Promise, error) { -- c.mu.Lock() -- defer c.mu.Unlock() +- objectPos := func(obj types.Object) gobPosition { +- posn := safetoken.StartPosition(fset, obj.Pos()) +- return gobPosition{b.string(posn.Filename), posn.Offset, len(obj.Name())} +- } - -- // Any parsing pass increments the clock, as we'll update access times. -- // (technically, if fhs is empty this isn't necessary, but that's a degenerate case). -- // -- // All entries parsed from a single call get the same access time. -- c.clock++ -- walltime := time.Now() +- objectpathFor := new(objectpath.Encoder).For - -- // Read file data and collect cacheable files. -- var ( -- data = make([][]byte, len(fhs)) // file content for each readable file -- promises = make([]*memoize.Promise, len(fhs)) -- firstReadError error // first error from fh.Read, or nil -- ) -- for i, fh := range fhs { -- content, err := fh.Content() -- if err != nil { -- if firstReadError == nil { -- firstReadError = err -- } -- continue +- // setindexInfo sets the (Posn, PkgPath, ObjectPath) fields for each method declaration. +- setIndexInfo := func(m *gobMethod, method *types.Func) { +- // error.Error has empty Position, PkgPath, and ObjectPath. +- if method.Pkg() == nil { +- return - } -- data[i] = content - -- key := parseKey{ -- uri: fh.URI(), -- mode: mode, -- purgeFuncBodies: purgeFuncBodies, -- } +- m.Posn = objectPos(method) +- m.PkgPath = b.string(method.Pkg().Path()) - -- if e, ok := c.m[key]; ok { -- if e.hash == fh.FileIdentity().Hash { // cache hit -- e.atime = c.clock -- e.walltime = walltime -- heap.Fix(&c.lru, e.lruIndex) -- promises[i] = e.promise -- continue -- } else { -- // A cache hit, for a different version. Delete it. -- delete(c.m, e.key) -- heap.Remove(&c.lru, e.lruIndex) -- } +- // Instantiations of generic methods don't have an +- // object path, so we use the generic. +- if p, err := objectpathFor(method.Origin()); err != nil { +- panic(err) // can't happen for a method of a package-level type +- } else { +- m.ObjectPath = b.string(string(p)) - } +- } - -- uri := fh.URI() -- promise := memoize.NewPromise("parseCache.parse", func(ctx context.Context, _ interface{}) interface{} { -- // Allocate 2*len(content)+parsePadding to allow for re-parsing once -- // inside of parseGoSrc without exceeding the allocated space. -- base, nextBase := c.allocateSpace(2*len(content) + parsePadding) -- -- pgf, fixes1 := ParseGoSrc(ctx, fileSetWithBase(base), uri, content, mode, purgeFuncBodies) -- file := pgf.Tok -- if file.Base()+file.Size()+1 > nextBase { -- // The parsed file exceeds its allocated space, likely due to multiple -- // passes of src fixing. In this case, we have no choice but to re-do -- // the operation with the correct size. -- // -- // Even though the final successful parse requires only file.Size() -- // bytes of Pos space, we need to accommodate all the missteps to get -- // there, as parseGoSrc will repeat them. -- actual := file.Base() + file.Size() - base // actual size consumed, after re-parsing -- base2, nextBase2 := c.allocateSpace(actual) -- pgf2, fixes2 := ParseGoSrc(ctx, fileSetWithBase(base2), uri, content, mode, purgeFuncBodies) -- -- // In golang/go#59097 we observed that this panic condition was hit. -- // One bug was found and fixed, but record more information here in -- // case there is still a bug here. -- if end := pgf2.Tok.Base() + pgf2.Tok.Size(); end != nextBase2-1 { -- var errBuf bytes.Buffer -- fmt.Fprintf(&errBuf, "internal error: non-deterministic parsing result:\n") -- fmt.Fprintf(&errBuf, "\t%q (%d-%d) does not span %d-%d\n", uri, pgf2.Tok.Base(), base2, end, nextBase2-1) -- fmt.Fprintf(&errBuf, "\tfirst %q (%d-%d)\n", pgf.URI, pgf.Tok.Base(), pgf.Tok.Base()+pgf.Tok.Size()) -- fmt.Fprintf(&errBuf, "\tfirst space: (%d-%d), second space: (%d-%d)\n", base, nextBase, base2, nextBase2) -- fmt.Fprintf(&errBuf, "\tfirst mode: %v, second mode: %v", pgf.Mode, pgf2.Mode) -- fmt.Fprintf(&errBuf, "\tfirst err: %v, second err: %v", pgf.ParseErr, pgf2.ParseErr) -- fmt.Fprintf(&errBuf, "\tfirst fixes: %v, second fixes: %v", fixes1, fixes2) -- panic(errBuf.String()) -- } -- pgf = pgf2 +- // We ignore aliases, though in principle they could define a +- // struct{...} or interface{...} type, or an instantiation of +- // a generic, that has a novel method set. +- scope := pkg.Scope() +- for _, name := range scope.Names() { +- if tname, ok := scope.Lookup(name).(*types.TypeName); ok && !tname.IsAlias() { +- if mset := methodSetInfo(tname.Type(), setIndexInfo); mset.Mask != 0 { +- mset.Posn = objectPos(tname) +- // Only record types with non-trivial method sets. +- b.MethodSets = append(b.MethodSets, mset) - } -- return pgf -- }) -- promises[i] = promise -- -- // add new entry; entries are gc'ed asynchronously -- e := &parseCacheEntry{ -- key: key, -- hash: fh.FileIdentity().Hash, -- promise: promise, -- atime: c.clock, -- walltime: walltime, - } -- c.m[e.key] = e -- heap.Push(&c.lru, e) -- } -- -- if len(c.m) != len(c.lru) { -- panic("map and LRU are inconsistent") - } - -- return promises, firstReadError +- return &Index{pkg: b.gobPackage} -} - --func (c *parseCache) gc() { -- const period = 10 * time.Second // gc period -- timer := time.NewTicker(period) -- defer timer.Stop() -- -- for { -- select { -- case <-c.done: -- return -- case <-timer.C: +-// string returns a small integer that encodes the string. +-func (b *indexBuilder) string(s string) int { +- i, ok := b.stringIndex[s] +- if !ok { +- i = len(b.Strings) +- if b.stringIndex == nil { +- b.stringIndex = make(map[string]int) - } -- -- c.gcOnce() +- b.stringIndex[s] = i +- b.Strings = append(b.Strings, s) - } +- return i -} - --func (c *parseCache) gcOnce() { -- now := time.Now() -- c.mu.Lock() -- defer c.mu.Unlock() +-// methodSetInfo returns the method-set fingerprint of a type. +-// It calls the optional setIndexInfo function for each gobMethod. +-// This is used during index construction, but not search (KeyOf), +-// to store extra information. +-func methodSetInfo(t types.Type, setIndexInfo func(*gobMethod, *types.Func)) gobMethodSet { +- // For non-interface types, use *T +- // (if T is not already a pointer) +- // since it may have more methods. +- mset := types.NewMethodSet(EnsurePointer(t)) - -- for len(c.m) > parseCacheMinFiles { -- e := heap.Pop(&c.lru).(*parseCacheEntry) -- if now.Sub(e.walltime) >= c.expireAfter { -- delete(c.m, e.key) -- } else { -- heap.Push(&c.lru, e) -- break +- // Convert the method set into a compact summary. +- var mask uint64 +- tricky := false +- methods := make([]gobMethod, mset.Len()) +- for i := 0; i < mset.Len(); i++ { +- m := mset.At(i).Obj().(*types.Func) +- fp, isTricky := fingerprint(m) +- if isTricky { +- tricky = true +- } +- sum := crc32.ChecksumIEEE([]byte(fp)) +- methods[i] = gobMethod{Fingerprint: fp, Sum: sum} +- if setIndexInfo != nil { +- setIndexInfo(&methods[i], m) // set Position, PkgPath, ObjectPath - } +- mask |= 1 << uint64(((sum>>24)^(sum>>16)^(sum>>8)^sum)&0x3f) +- } +- return gobMethodSet{ +- IsInterface: types.IsInterface(t), +- Tricky: tricky, +- Mask: mask, +- Methods: methods, - } -} - --// allocateSpace reserves the next n bytes of token.Pos space in the --// cache. --// --// It returns the resulting file base, next base, and an offset FileSet to use --// for parsing. --func (c *parseCache) allocateSpace(size int) (int, int) { -- c.mu.Lock() -- defer c.mu.Unlock() -- -- if c.nextBase == 0 { -- // FileSet base values must be at least 1. -- c.nextBase = 1 +-// EnsurePointer wraps T in a types.Pointer if T is a named, non-interface type. +-// This is useful to make sure you consider a named type's full method set. +-func EnsurePointer(T types.Type) types.Type { +- if _, ok := aliases.Unalias(T).(*types.Named); ok && !types.IsInterface(T) { +- return types.NewPointer(T) - } -- base := c.nextBase -- c.nextBase += size + 1 -- return base, c.nextBase +- +- return T -} - --// parseFiles returns a ParsedGoFile for each file handle in fhs, in the --// requested parse mode. --// --// For parsed files that already exists in the cache, access time will be --// updated. For others, parseFiles will parse and store as many results in the --// cache as space allows. +-// fingerprint returns an encoding of a method signature such that two +-// methods with equal encodings have identical types, except for a few +-// tricky types whose encodings may spuriously match and whose exact +-// identity computation requires the type checker to eliminate false +-// positives (which are rare). The boolean result indicates whether +-// the result was one of these tricky types. -// --// The token.File for each resulting parsed file will be added to the provided --// FileSet, using the tokeninternal.AddExistingFiles API. Consequently, the --// given fset should only be used in other APIs if its base is >= --// reservedForParsing. +-// In the standard library, 99.8% of package-level types have a +-// non-tricky method-set. The most common exceptions are due to type +-// parameters. -// --// If parseFiles returns an error, it still returns a slice, --// but with a nil entry for each file that could not be parsed. --func (c *parseCache) parseFiles(ctx context.Context, fset *token.FileSet, mode parser.Mode, purgeFuncBodies bool, fhs ...source.FileHandle) ([]*source.ParsedGoFile, error) { -- pgfs := make([]*source.ParsedGoFile, len(fhs)) -- -- // Temporary fall-back for 32-bit systems, where reservedForParsing is too -- // small to be viable. We don't actually support 32-bit systems, so this -- // workaround is only for tests and can be removed when we stop running -- // 32-bit TryBots for gopls. -- if bits.UintSize == 32 { -- for i, fh := range fhs { -- var err error -- pgfs[i], err = parseGoImpl(ctx, fset, fh, mode, purgeFuncBodies) -- if err != nil { -- return pgfs, err -- } -- } -- return pgfs, nil -- } +-// The fingerprint string starts with method.Id() + "(". +-func fingerprint(method *types.Func) (string, bool) { +- var buf strings.Builder +- tricky := false +- var fprint func(t types.Type) +- fprint = func(t types.Type) { +- switch t := t.(type) { +- case *aliases.Alias: +- fprint(aliases.Unalias(t)) - -- promises, firstErr := c.startParse(mode, purgeFuncBodies, fhs...) +- case *types.Named: +- tname := t.Obj() +- if tname.Pkg() != nil { +- buf.WriteString(strconv.Quote(tname.Pkg().Path())) +- buf.WriteByte('.') +- } else if tname.Name() != "error" && tname.Name() != "comparable" { +- panic(tname) // error and comparable the only named types with no package +- } +- buf.WriteString(tname.Name()) - -- // Await all parsing. -- var g errgroup.Group -- g.SetLimit(runtime.GOMAXPROCS(-1)) // parsing is CPU-bound. -- for i, promise := range promises { -- if promise == nil { -- continue -- } -- i := i -- promise := promise -- g.Go(func() error { -- result, err := promise.Get(ctx, nil) -- if err != nil { -- return err +- case *types.Array: +- fmt.Fprintf(&buf, "[%d]", t.Len()) +- fprint(t.Elem()) +- +- case *types.Slice: +- buf.WriteString("[]") +- fprint(t.Elem()) +- +- case *types.Pointer: +- buf.WriteByte('*') +- fprint(t.Elem()) +- +- case *types.Map: +- buf.WriteString("map[") +- fprint(t.Key()) +- buf.WriteByte(']') +- fprint(t.Elem()) +- +- case *types.Chan: +- switch t.Dir() { +- case types.SendRecv: +- buf.WriteString("chan ") +- case types.SendOnly: +- buf.WriteString("<-chan ") +- case types.RecvOnly: +- buf.WriteString("chan<- ") - } -- pgfs[i] = result.(*source.ParsedGoFile) -- return nil -- }) -- } +- fprint(t.Elem()) - -- if err := g.Wait(); err != nil && firstErr == nil { -- firstErr = err -- } +- case *types.Tuple: +- buf.WriteByte('(') +- for i := 0; i < t.Len(); i++ { +- if i > 0 { +- buf.WriteByte(',') +- } +- fprint(t.At(i).Type()) +- } +- buf.WriteByte(')') - -- // Augment the FileSet to map all parsed files. -- var tokenFiles []*token.File -- for _, pgf := range pgfs { -- if pgf == nil { -- continue -- } -- tokenFiles = append(tokenFiles, pgf.Tok) -- } -- tokeninternal.AddExistingFiles(fset, tokenFiles) +- case *types.Basic: +- // Use canonical names for uint8 and int32 aliases. +- switch t.Kind() { +- case types.Byte: +- buf.WriteString("byte") +- case types.Rune: +- buf.WriteString("rune") +- default: +- buf.WriteString(t.String()) +- } - -- const debugIssue59080 = true -- if debugIssue59080 { -- for _, f := range tokenFiles { -- pos := token.Pos(f.Base()) -- f2 := fset.File(pos) -- if f2 != f { -- panic(fmt.Sprintf("internal error: File(%d (start)) = %v, not %v", pos, f2, f)) +- case *types.Signature: +- buf.WriteString("func") +- fprint(t.Params()) +- if t.Variadic() { +- buf.WriteString("...") // not quite Go syntax - } -- pos = token.Pos(f.Base() + f.Size()) -- f2 = fset.File(pos) -- if f2 != f { -- panic(fmt.Sprintf("internal error: File(%d (end)) = %v, not %v", pos, f2, f)) +- fprint(t.Results()) +- +- case *types.Struct: +- // Non-empty unnamed struct types in method +- // signatures are vanishingly rare. +- buf.WriteString("struct{") +- for i := 0; i < t.NumFields(); i++ { +- if i > 0 { +- buf.WriteByte(';') +- } +- f := t.Field(i) +- // This isn't quite right for embedded type aliases. +- // (See types.TypeString(StructType) and #44410 for context.) +- // But this is vanishingly rare. +- if !f.Embedded() { +- buf.WriteString(f.Id()) +- buf.WriteByte(' ') +- } +- fprint(f.Type()) +- if tag := t.Tag(i); tag != "" { +- buf.WriteByte(' ') +- buf.WriteString(strconv.Quote(tag)) +- } +- } +- buf.WriteString("}") +- +- case *types.Interface: +- if t.NumMethods() == 0 { +- buf.WriteString("any") // common case +- } else { +- // Interface assignability is particularly +- // tricky due to the possibility of recursion. +- tricky = true +- // We could still give more disambiguating precision +- // than "..." if we wanted to. +- buf.WriteString("interface{...}") - } +- +- case *types.TypeParam: +- tricky = true +- // TODO(adonovan): refine this by adding a numeric suffix +- // indicating the index among the receiver type's parameters. +- buf.WriteByte('?') +- +- default: // incl. *types.Union +- panic(t) - } - } - -- return pgfs, firstErr +- buf.WriteString(method.Id()) // e.g. "pkg.Type" +- sig := method.Type().(*types.Signature) +- fprint(sig.Params()) +- fprint(sig.Results()) +- return buf.String(), tricky -} - --// -- priority queue boilerplate -- -- --// queue is a min-atime prority queue of cache entries. --type queue []*parseCacheEntry +-// -- serial format of index -- - --func (q queue) Len() int { return len(q) } +-// (The name says gob but in fact we use frob.) +-var packageCodec = frob.CodecFor[gobPackage]() - --func (q queue) Less(i, j int) bool { return q[i].atime < q[j].atime } +-// A gobPackage records the method set of each package-level type for a single package. +-type gobPackage struct { +- Strings []string // index of strings used by gobPosition.File, gobMethod.{Pkg,Object}Path +- MethodSets []gobMethodSet +-} - --func (q queue) Swap(i, j int) { -- q[i], q[j] = q[j], q[i] -- q[i].lruIndex = i -- q[j].lruIndex = j +-// A gobMethodSet records the method set of a single type. +-type gobMethodSet struct { +- Posn gobPosition +- IsInterface bool +- Tricky bool // at least one method is tricky; assignability requires go/types +- Mask uint64 // mask with 1 bit from each of methods[*].sum +- Methods []gobMethod -} - --func (q *queue) Push(x interface{}) { -- e := x.(*parseCacheEntry) -- e.lruIndex = len(*q) -- *q = append(*q, e) +-// A gobMethod records the name, type, and position of a single method. +-type gobMethod struct { +- Fingerprint string // string of form "methodID(params...)(results)" +- Sum uint32 // checksum of fingerprint +- +- // index records only (zero in KeyOf; also for index of error.Error). +- Posn gobPosition // location of method declaration +- PkgPath int // path of package containing method declaration +- ObjectPath int // object path of method relative to PkgPath -} - --func (q *queue) Pop() interface{} { -- last := len(*q) - 1 -- e := (*q)[last] -- (*q)[last] = nil // aid GC -- *q = (*q)[:last] -- return e +-// A gobPosition records the file, offset, and length of an identifier. +-type gobPosition struct { +- File int // index into gobPackage.Strings +- Offset, Len int // in bytes -} -diff -urN a/gopls/internal/lsp/cache/parse_cache_test.go b/gopls/internal/lsp/cache/parse_cache_test.go ---- a/gopls/internal/lsp/cache/parse_cache_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/parse_cache_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,233 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/cache/mod.go b/gopls/internal/cache/mod.go +--- a/gopls/internal/cache/mod.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/mod.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,531 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - @@ -21405,1208 +21100,1558 @@ diff -urN a/gopls/internal/lsp/cache/parse_cache_test.go b/gopls/internal/lsp/ca - -import ( - "context" +- "errors" - "fmt" -- "go/token" -- "math/bits" -- "testing" -- "time" +- "path/filepath" +- "regexp" +- "strings" - -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" +- "golang.org/x/mod/modfile" +- "golang.org/x/mod/module" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/command" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/tag" +- "golang.org/x/tools/internal/gocommand" +- "golang.org/x/tools/internal/memoize" -) - --func skipIfNoParseCache(t *testing.T) { -- if bits.UintSize == 32 { -- t.Skip("the parse cache is not supported on 32-bit systems") -- } +-// A ParsedModule contains the results of parsing a go.mod file. +-type ParsedModule struct { +- URI protocol.DocumentURI +- File *modfile.File +- Mapper *protocol.Mapper +- ParseErrors []*Diagnostic -} - --func TestParseCache(t *testing.T) { -- skipIfNoParseCache(t) +-// ParseMod parses a go.mod file, using a cache. It may return partial results and an error. +-func (s *Snapshot) ParseMod(ctx context.Context, fh file.Handle) (*ParsedModule, error) { +- uri := fh.URI() - -- ctx := context.Background() -- uri := span.URI("file:///myfile") -- fh := makeFakeFileHandle(uri, []byte("package p\n\nconst _ = \"foo\"")) -- fset := token.NewFileSet() +- s.mu.Lock() +- entry, hit := s.parseModHandles.Get(uri) +- s.mu.Unlock() - -- cache := newParseCache(0) -- pgfs1, err := cache.parseFiles(ctx, fset, source.ParseFull, false, fh) -- if err != nil { -- t.Fatal(err) -- } -- pgf1 := pgfs1[0] -- pgfs2, err := cache.parseFiles(ctx, fset, source.ParseFull, false, fh) -- pgf2 := pgfs2[0] -- if err != nil { -- t.Fatal(err) -- } -- if pgf1 != pgf2 { -- t.Errorf("parseFiles(%q): unexpected cache miss on repeated call", uri) +- type parseModKey file.Identity +- type parseModResult struct { +- parsed *ParsedModule +- err error - } - -- // Fill up the cache with other files, but don't evict the file above. -- cache.gcOnce() -- files := []source.FileHandle{fh} -- files = append(files, dummyFileHandles(parseCacheMinFiles-1)...) +- // cache miss? +- if !hit { +- promise, release := s.store.Promise(parseModKey(fh.Identity()), func(ctx context.Context, _ interface{}) interface{} { +- parsed, err := parseModImpl(ctx, fh) +- return parseModResult{parsed, err} +- }) - -- pgfs3, err := cache.parseFiles(ctx, fset, source.ParseFull, false, files...) -- if err != nil { -- t.Fatal(err) -- } -- pgf3 := pgfs3[0] -- if pgf3 != pgf1 { -- t.Errorf("parseFiles(%q, ...): unexpected cache miss", uri) -- } -- if pgf3.Tok.Base() != pgf1.Tok.Base() || pgf3.Tok.Size() != pgf1.Tok.Size() { -- t.Errorf("parseFiles(%q, ...): result.Tok has base: %d, size: %d, want (%d, %d)", uri, pgf3.Tok.Base(), pgf3.Tok.Size(), pgf1.Tok.Base(), pgf1.Tok.Size()) -- } -- if tok := fset.File(token.Pos(pgf3.Tok.Base())); tok != pgf3.Tok { -- t.Errorf("parseFiles(%q, ...): result.Tok not contained in FileSet", uri) +- entry = promise +- s.mu.Lock() +- s.parseModHandles.Set(uri, entry, func(_, _ interface{}) { release() }) +- s.mu.Unlock() - } - -- // Now overwrite the cache, after which we should get new results. -- cache.gcOnce() -- files = dummyFileHandles(parseCacheMinFiles) -- _, err = cache.parseFiles(ctx, fset, source.ParseFull, false, files...) -- if err != nil { -- t.Fatal(err) -- } -- // force a GC, which should collect the recently parsed files -- cache.gcOnce() -- pgfs4, err := cache.parseFiles(ctx, fset, source.ParseFull, false, fh) +- // Await result. +- v, err := s.awaitPromise(ctx, entry) - if err != nil { -- t.Fatal(err) -- } -- if pgfs4[0] == pgf1 { -- t.Errorf("parseFiles(%q): unexpected cache hit after overwriting cache", uri) +- return nil, err - } +- res := v.(parseModResult) +- return res.parsed, res.err -} - --func TestParseCache_Reparsing(t *testing.T) { -- skipIfNoParseCache(t) -- -- defer func(padding int) { -- parsePadding = padding -- }(parsePadding) -- parsePadding = 0 -- -- files := dummyFileHandles(parseCacheMinFiles) -- danglingSelector := []byte("package p\nfunc _() {\n\tx.\n}") -- files = append(files, makeFakeFileHandle("file:///bad1", danglingSelector)) -- files = append(files, makeFakeFileHandle("file:///bad2", danglingSelector)) +-// parseModImpl parses the go.mod file whose name and contents are in fh. +-// It may return partial results and an error. +-func parseModImpl(ctx context.Context, fh file.Handle) (*ParsedModule, error) { +- _, done := event.Start(ctx, "cache.ParseMod", tag.URI.Of(fh.URI())) +- defer done() - -- // Parsing should succeed even though we overflow the padding. -- cache := newParseCache(0) -- _, err := cache.parseFiles(context.Background(), token.NewFileSet(), source.ParseFull, false, files...) +- contents, err := fh.Content() - if err != nil { -- t.Fatal(err) +- return nil, err +- } +- m := protocol.NewMapper(fh.URI(), contents) +- file, parseErr := modfile.Parse(fh.URI().Path(), contents, nil) +- // Attempt to convert the error to a standardized parse error. +- var parseErrors []*Diagnostic +- if parseErr != nil { +- mfErrList, ok := parseErr.(modfile.ErrorList) +- if !ok { +- return nil, fmt.Errorf("unexpected parse error type %v", parseErr) +- } +- for _, mfErr := range mfErrList { +- rng, err := m.OffsetRange(mfErr.Pos.Byte, mfErr.Pos.Byte) +- if err != nil { +- return nil, err +- } +- parseErrors = append(parseErrors, &Diagnostic{ +- URI: fh.URI(), +- Range: rng, +- Severity: protocol.SeverityError, +- Source: ParseError, +- Message: mfErr.Err.Error(), +- }) +- } - } +- return &ParsedModule{ +- URI: fh.URI(), +- Mapper: m, +- File: file, +- ParseErrors: parseErrors, +- }, parseErr -} - --// Re-parsing the first file should not panic. --func TestParseCache_Issue59097(t *testing.T) { -- skipIfNoParseCache(t) +-// A ParsedWorkFile contains the results of parsing a go.work file. +-type ParsedWorkFile struct { +- URI protocol.DocumentURI +- File *modfile.WorkFile +- Mapper *protocol.Mapper +- ParseErrors []*Diagnostic +-} - -- defer func(padding int) { -- parsePadding = padding -- }(parsePadding) -- parsePadding = 0 +-// ParseWork parses a go.work file, using a cache. It may return partial results and an error. +-// TODO(adonovan): move to new work.go file. +-func (s *Snapshot) ParseWork(ctx context.Context, fh file.Handle) (*ParsedWorkFile, error) { +- uri := fh.URI() - -- danglingSelector := []byte("package p\nfunc _() {\n\tx.\n}") -- files := []source.FileHandle{makeFakeFileHandle("file:///bad", danglingSelector)} +- s.mu.Lock() +- entry, hit := s.parseWorkHandles.Get(uri) +- s.mu.Unlock() - -- // Parsing should succeed even though we overflow the padding. -- cache := newParseCache(0) -- _, err := cache.parseFiles(context.Background(), token.NewFileSet(), source.ParseFull, false, files...) -- if err != nil { -- t.Fatal(err) +- type parseWorkKey file.Identity +- type parseWorkResult struct { +- parsed *ParsedWorkFile +- err error - } --} - --func TestParseCache_TimeEviction(t *testing.T) { -- skipIfNoParseCache(t) -- -- ctx := context.Background() -- fset := token.NewFileSet() -- uri := span.URI("file:///myfile") -- fh := makeFakeFileHandle(uri, []byte("package p\n\nconst _ = \"foo\"")) -- -- const gcDuration = 10 * time.Millisecond -- cache := newParseCache(gcDuration) -- cache.stop() // we'll manage GC manually, for testing. +- // cache miss? +- if !hit { +- handle, release := s.store.Promise(parseWorkKey(fh.Identity()), func(ctx context.Context, _ interface{}) interface{} { +- parsed, err := parseWorkImpl(ctx, fh) +- return parseWorkResult{parsed, err} +- }) - -- pgfs0, err := cache.parseFiles(ctx, fset, source.ParseFull, false, fh, fh) -- if err != nil { -- t.Fatal(err) +- entry = handle +- s.mu.Lock() +- s.parseWorkHandles.Set(uri, entry, func(_, _ interface{}) { release() }) +- s.mu.Unlock() - } - -- files := dummyFileHandles(parseCacheMinFiles) -- _, err = cache.parseFiles(ctx, fset, source.ParseFull, false, files...) +- // Await result. +- v, err := s.awaitPromise(ctx, entry) - if err != nil { -- t.Fatal(err) +- return nil, err - } +- res := v.(parseWorkResult) +- return res.parsed, res.err +-} - -- // Even after filling up the 'min' files, we get a cache hit for our original file. -- pgfs1, err := cache.parseFiles(ctx, fset, source.ParseFull, false, fh, fh) +-// parseWorkImpl parses a go.work file. It may return partial results and an error. +-func parseWorkImpl(ctx context.Context, fh file.Handle) (*ParsedWorkFile, error) { +- _, done := event.Start(ctx, "cache.ParseWork", tag.URI.Of(fh.URI())) +- defer done() +- +- content, err := fh.Content() - if err != nil { -- t.Fatal(err) +- return nil, err - } -- -- if pgfs0[0] != pgfs1[0] { -- t.Errorf("before GC, got unexpected cache miss") +- m := protocol.NewMapper(fh.URI(), content) +- file, parseErr := modfile.ParseWork(fh.URI().Path(), content, nil) +- // Attempt to convert the error to a standardized parse error. +- var parseErrors []*Diagnostic +- if parseErr != nil { +- mfErrList, ok := parseErr.(modfile.ErrorList) +- if !ok { +- return nil, fmt.Errorf("unexpected parse error type %v", parseErr) +- } +- for _, mfErr := range mfErrList { +- rng, err := m.OffsetRange(mfErr.Pos.Byte, mfErr.Pos.Byte) +- if err != nil { +- return nil, err +- } +- parseErrors = append(parseErrors, &Diagnostic{ +- URI: fh.URI(), +- Range: rng, +- Severity: protocol.SeverityError, +- Source: ParseError, +- Message: mfErr.Err.Error(), +- }) +- } - } +- return &ParsedWorkFile{ +- URI: fh.URI(), +- Mapper: m, +- File: file, +- ParseErrors: parseErrors, +- }, parseErr +-} - -- // But after GC, we get a cache miss. -- _, err = cache.parseFiles(ctx, fset, source.ParseFull, false, files...) // mark dummy files as newer -- if err != nil { -- t.Fatal(err) +-// goSum reads the go.sum file for the go.mod file at modURI, if it exists. If +-// it doesn't exist, it returns nil. +-func (s *Snapshot) goSum(ctx context.Context, modURI protocol.DocumentURI) []byte { +- // Get the go.sum file, either from the snapshot or directly from the +- // cache. Avoid (*snapshot).ReadFile here, as we don't want to add +- // nonexistent file handles to the snapshot if the file does not exist. +- // +- // TODO(rfindley): but that's not right. Changes to sum files should +- // invalidate content, even if it's nonexistent content. +- sumURI := protocol.URIFromPath(sumFilename(modURI)) +- sumFH := s.FindFile(sumURI) +- if sumFH == nil { +- var err error +- sumFH, err = s.view.fs.ReadFile(ctx, sumURI) +- if err != nil { +- return nil +- } - } -- time.Sleep(gcDuration) -- cache.gcOnce() -- -- pgfs2, err := cache.parseFiles(ctx, fset, source.ParseFull, false, fh, fh) +- content, err := sumFH.Content() - if err != nil { -- t.Fatal(err) -- } -- -- if pgfs0[0] == pgfs2[0] { -- t.Errorf("after GC, got unexpected cache hit for %s", pgfs0[0].URI) +- return nil - } +- return content -} - --func TestParseCache_Duplicates(t *testing.T) { -- skipIfNoParseCache(t) +-func sumFilename(modURI protocol.DocumentURI) string { +- return strings.TrimSuffix(modURI.Path(), ".mod") + ".sum" +-} - -- ctx := context.Background() -- uri := span.URI("file:///myfile") -- fh := makeFakeFileHandle(uri, []byte("package p\n\nconst _ = \"foo\"")) +-// ModWhy returns the "go mod why" result for each module named in a +-// require statement in the go.mod file. +-// TODO(adonovan): move to new mod_why.go file. +-func (s *Snapshot) ModWhy(ctx context.Context, fh file.Handle) (map[string]string, error) { +- uri := fh.URI() - -- cache := newParseCache(0) -- pgfs, err := cache.parseFiles(ctx, token.NewFileSet(), source.ParseFull, false, fh, fh) -- if err != nil { -- t.Fatal(err) -- } -- if pgfs[0] != pgfs[1] { -- t.Errorf("parseFiles(fh, fh): = [%p, %p], want duplicate files", pgfs[0].File, pgfs[1].File) +- if s.FileKind(fh) != file.Mod { +- return nil, fmt.Errorf("%s is not a go.mod file", uri) - } --} - --func dummyFileHandles(n int) []source.FileHandle { -- var fhs []source.FileHandle -- for i := 0; i < n; i++ { -- uri := span.URI(fmt.Sprintf("file:///_%d", i)) -- src := []byte(fmt.Sprintf("package p\nvar _ = %d", i)) -- fhs = append(fhs, makeFakeFileHandle(uri, src)) -- } -- return fhs --} +- s.mu.Lock() +- entry, hit := s.modWhyHandles.Get(uri) +- s.mu.Unlock() - --func makeFakeFileHandle(uri span.URI, src []byte) fakeFileHandle { -- return fakeFileHandle{ -- uri: uri, -- data: src, -- hash: source.HashOf(src), +- type modWhyResult struct { +- why map[string]string +- err error - } --} -- --type fakeFileHandle struct { -- source.FileHandle -- uri span.URI -- data []byte -- hash source.Hash --} - --func (h fakeFileHandle) URI() span.URI { -- return h.uri --} +- // cache miss? +- if !hit { +- handle := memoize.NewPromise("modWhy", func(ctx context.Context, arg interface{}) interface{} { +- why, err := modWhyImpl(ctx, arg.(*Snapshot), fh) +- return modWhyResult{why, err} +- }) - --func (h fakeFileHandle) Content() ([]byte, error) { -- return h.data, nil --} +- entry = handle +- s.mu.Lock() +- s.modWhyHandles.Set(uri, entry, nil) +- s.mu.Unlock() +- } - --func (h fakeFileHandle) FileIdentity() source.FileIdentity { -- return source.FileIdentity{ -- URI: h.uri, -- Hash: h.hash, +- // Await result. +- v, err := s.awaitPromise(ctx, entry) +- if err != nil { +- return nil, err - } +- res := v.(modWhyResult) +- return res.why, res.err -} -diff -urN a/gopls/internal/lsp/cache/parse.go b/gopls/internal/lsp/cache/parse.go ---- a/gopls/internal/lsp/cache/parse.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/parse.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,969 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package cache -- --import ( -- "bytes" -- "context" -- "fmt" -- "go/ast" -- "go/parser" -- "go/scanner" -- "go/token" -- "path/filepath" -- "reflect" - -- goplsastutil "golang.org/x/tools/gopls/internal/astutil" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/diff" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/tag" --) +-// modWhyImpl returns the result of "go mod why -m" on the specified go.mod file. +-func modWhyImpl(ctx context.Context, snapshot *Snapshot, fh file.Handle) (map[string]string, error) { +- ctx, done := event.Start(ctx, "cache.ModWhy", tag.URI.Of(fh.URI())) +- defer done() - --// ParseGo parses the file whose contents are provided by fh, using a cache. --// The resulting tree may have beeen fixed up. --func (s *snapshot) ParseGo(ctx context.Context, fh source.FileHandle, mode parser.Mode) (*source.ParsedGoFile, error) { -- pgfs, err := s.view.parseCache.parseFiles(ctx, token.NewFileSet(), mode, false, fh) +- pm, err := snapshot.ParseMod(ctx, fh) - if err != nil { - return nil, err - } -- return pgfs[0], nil --} -- --// parseGoImpl parses the Go source file whose content is provided by fh. --func parseGoImpl(ctx context.Context, fset *token.FileSet, fh source.FileHandle, mode parser.Mode, purgeFuncBodies bool) (*source.ParsedGoFile, error) { -- ext := filepath.Ext(fh.URI().Filename()) -- if ext != ".go" && ext != "" { // files generated by cgo have no extension -- return nil, fmt.Errorf("cannot parse non-Go file %s", fh.URI()) +- // No requires to explain. +- if len(pm.File.Require) == 0 { +- return nil, nil // empty result - } -- content, err := fh.Content() +- // Run `go mod why` on all the dependencies. +- inv := &gocommand.Invocation{ +- Verb: "mod", +- Args: []string{"why", "-m"}, +- WorkingDir: filepath.Dir(fh.URI().Path()), +- } +- for _, req := range pm.File.Require { +- inv.Args = append(inv.Args, req.Mod.Path) +- } +- stdout, err := snapshot.RunGoCommandDirect(ctx, Normal, inv) - if err != nil { - return nil, err - } -- // Check for context cancellation before actually doing the parse. -- if ctx.Err() != nil { -- return nil, ctx.Err() +- whyList := strings.Split(stdout.String(), "\n\n") +- if len(whyList) != len(pm.File.Require) { +- return nil, fmt.Errorf("mismatched number of results: got %v, want %v", len(whyList), len(pm.File.Require)) - } -- pgf, _ := ParseGoSrc(ctx, fset, fh.URI(), content, mode, purgeFuncBodies) -- return pgf, nil +- why := make(map[string]string, len(pm.File.Require)) +- for i, req := range pm.File.Require { +- why[req.Mod.Path] = whyList[i] +- } +- return why, nil -} - --// ParseGoSrc parses a buffer of Go source, repairing the tree if necessary. --// --// The provided ctx is used only for logging. --func ParseGoSrc(ctx context.Context, fset *token.FileSet, uri span.URI, src []byte, mode parser.Mode, purgeFuncBodies bool) (res *source.ParsedGoFile, fixes []fixType) { -- if purgeFuncBodies { -- src = goplsastutil.PurgeFuncBodies(src) +-// extractGoCommandErrors tries to parse errors that come from the go command +-// and shape them into go.mod diagnostics. +-// TODO: rename this to 'load errors' +-func (s *Snapshot) extractGoCommandErrors(ctx context.Context, goCmdError error) []*Diagnostic { +- if goCmdError == nil { +- return nil - } -- ctx, done := event.Start(ctx, "cache.ParseGoSrc", tag.File.Of(uri.Filename())) -- defer done() - -- file, err := parser.ParseFile(fset, uri.Filename(), src, mode) -- var parseErr scanner.ErrorList -- if err != nil { -- // We passed a byte slice, so the only possible error is a parse error. -- parseErr = err.(scanner.ErrorList) +- type locatedErr struct { +- loc protocol.Location +- msg string - } +- diagLocations := map[*ParsedModule]locatedErr{} +- backupDiagLocations := map[*ParsedModule]locatedErr{} - -- tok := fset.File(file.Pos()) -- if tok == nil { -- // file.Pos is the location of the package declaration (issue #53202). If there was -- // none, we can't find the token.File that ParseFile created, and we -- // have no choice but to recreate it. -- tok = fset.AddFile(uri.Filename(), -1, len(src)) -- tok.SetLinesForContent(src) -- } +- // If moduleErrs is non-nil, go command errors are scoped to specific +- // modules. +- var moduleErrs *moduleErrorMap +- _ = errors.As(goCmdError, &moduleErrs) - -- fixedSrc := false -- fixedAST := false -- // If there were parse errors, attempt to fix them up. -- if parseErr != nil { -- // Fix any badly parsed parts of the AST. -- astFixes := fixAST(file, tok, src) -- fixedAST = len(fixes) > 0 -- if fixedAST { -- fixes = append(fixes, astFixes...) +- // Match the error against all the mod files in the workspace. +- for _, uri := range s.View().ModFiles() { +- fh, err := s.ReadFile(ctx, uri) +- if err != nil { +- event.Error(ctx, "getting modfile for Go command error", err) +- continue - } -- -- for i := 0; i < 10; i++ { -- // Fix certain syntax errors that render the file unparseable. -- newSrc, srcFix := fixSrc(file, tok, src) -- if newSrc == nil { -- break +- pm, err := s.ParseMod(ctx, fh) +- if err != nil { +- // Parsing errors are reported elsewhere +- return nil +- } +- var msgs []string // error messages to consider +- if moduleErrs != nil { +- if pm.File.Module != nil { +- for _, mes := range moduleErrs.errs[pm.File.Module.Mod.Path] { +- msgs = append(msgs, mes.Error()) +- } - } -- -- // If we thought there was something to fix 10 times in a row, -- // it is likely we got stuck in a loop somehow. Log out a diff -- // of the last changes we made to aid in debugging. -- if i == 9 { -- unified := diff.Unified("before", "after", string(src), string(newSrc)) -- event.Log(ctx, fmt.Sprintf("fixSrc loop - last diff:\n%v", unified), tag.File.Of(tok.Name())) +- } else { +- msgs = append(msgs, goCmdError.Error()) +- } +- for _, msg := range msgs { +- if strings.Contains(goCmdError.Error(), "errors parsing go.mod") { +- // The go command emits parse errors for completely invalid go.mod files. +- // Those are reported by our own diagnostics and can be ignored here. +- // As of writing, we are not aware of any other errors that include +- // file/position information, so don't even try to find it. +- continue - } -- -- newFile, newErr := parser.ParseFile(fset, uri.Filename(), newSrc, mode) -- if newFile == nil { -- break // no progress +- loc, found, err := s.matchErrorToModule(pm, msg) +- if err != nil { +- event.Error(ctx, "matching error to module", err) +- continue - } -- -- // Maintain the original parseError so we don't try formatting the -- // doctored file. -- file = newFile -- src = newSrc -- tok = fset.File(file.Pos()) -- -- // Only now that we accept the fix do we record the src fix from above. -- fixes = append(fixes, srcFix) -- fixedSrc = true -- -- if newErr == nil { -- break // nothing to fix +- le := locatedErr{ +- loc: loc, +- msg: msg, - } -- -- // Note that fixedAST is reset after we fix src. -- astFixes = fixAST(file, tok, src) -- fixedAST = len(astFixes) > 0 -- if fixedAST { -- fixes = append(fixes, astFixes...) +- if found { +- diagLocations[pm] = le +- } else { +- backupDiagLocations[pm] = le - } - } - } - -- return &source.ParsedGoFile{ -- URI: uri, -- Mode: mode, -- Src: src, -- FixedSrc: fixedSrc, -- FixedAST: fixedAST, -- File: file, -- Tok: tok, -- Mapper: protocol.NewMapper(uri, src), -- ParseErr: parseErr, -- }, fixes +- // If we didn't find any good matches, assign diagnostics to all go.mod files. +- if len(diagLocations) == 0 { +- diagLocations = backupDiagLocations +- } +- +- var srcErrs []*Diagnostic +- for pm, le := range diagLocations { +- diag, err := s.goCommandDiagnostic(pm, le.loc, le.msg) +- if err != nil { +- event.Error(ctx, "building go command diagnostic", err) +- continue +- } +- srcErrs = append(srcErrs, diag) +- } +- return srcErrs -} - --// fixAST inspects the AST and potentially modifies any *ast.BadStmts so that it can be --// type-checked more effectively. --// --// If fixAST returns true, the resulting AST is considered "fixed", meaning --// positions have been mangled, and type checker errors may not make sense. --func fixAST(n ast.Node, tok *token.File, src []byte) (fixes []fixType) { -- var err error -- walkASTWithParent(n, func(n, parent ast.Node) bool { -- switch n := n.(type) { -- case *ast.BadStmt: -- if fixDeferOrGoStmt(n, parent, tok, src) { -- fixes = append(fixes, fixedDeferOrGo) -- // Recursively fix in our fixed node. -- moreFixes := fixAST(parent, tok, src) -- fixes = append(fixes, moreFixes...) -- } else { -- err = fmt.Errorf("unable to parse defer or go from *ast.BadStmt: %v", err) -- } -- return false -- case *ast.BadExpr: -- if fixArrayType(n, parent, tok, src) { -- fixes = append(fixes, fixedArrayType) -- // Recursively fix in our fixed node. -- moreFixes := fixAST(parent, tok, src) -- fixes = append(fixes, moreFixes...) -- return false -- } +-var moduleVersionInErrorRe = regexp.MustCompile(`[:\s]([+-._~0-9A-Za-z]+)@([+-._~0-9A-Za-z]+)[:\s]`) - -- // Fix cases where parser interprets if/for/switch "init" -- // statement as "cond" expression, e.g.: -- // -- // // "i := foo" is init statement, not condition. -- // for i := foo -- // -- if fixInitStmt(n, parent, tok, src) { -- fixes = append(fixes, fixedInit) -- } -- return false -- case *ast.SelectorExpr: -- // Fix cases where a keyword prefix results in a phantom "_" selector, e.g.: -- // -- // foo.var<> // want to complete to "foo.variance" -- // -- if fixPhantomSelector(n, tok, src) { -- fixes = append(fixes, fixedPhantomSelector) -- } -- return true +-// matchErrorToModule matches a go command error message to a go.mod file. +-// Some examples: +-// +-// example.com@v1.2.2: reading example.com/@v/v1.2.2.mod: no such file or directory +-// go: github.com/cockroachdb/apd/v2@v2.0.72: reading github.com/cockroachdb/apd/go.mod at revision v2.0.72: unknown revision v2.0.72 +-// go: example.com@v1.2.3 requires\n\trandom.org@v1.2.3: parsing go.mod:\n\tmodule declares its path as: bob.org\n\tbut was required as: random.org +-// +-// It returns the location of a reference to the one of the modules and true +-// if one exists. If none is found it returns a fallback location and false. +-func (s *Snapshot) matchErrorToModule(pm *ParsedModule, goCmdError string) (protocol.Location, bool, error) { +- var reference *modfile.Line +- matches := moduleVersionInErrorRe.FindAllStringSubmatch(goCmdError, -1) - -- case *ast.BlockStmt: -- switch parent.(type) { -- case *ast.SwitchStmt, *ast.TypeSwitchStmt, *ast.SelectStmt: -- // Adjust closing curly brace of empty switch/select -- // statements so we can complete inside them. -- if fixEmptySwitch(n, tok, src) { -- fixes = append(fixes, fixedEmptySwitch) -- } -- } +- for i := len(matches) - 1; i >= 0; i-- { +- ver := module.Version{Path: matches[i][1], Version: matches[i][2]} +- if err := module.Check(ver.Path, ver.Version); err != nil { +- continue +- } +- reference = findModuleReference(pm.File, ver) +- if reference != nil { +- break +- } +- } - -- return true -- default: -- return true +- if reference == nil { +- // No match for the module path was found in the go.mod file. +- // Show the error on the module declaration, if one exists, or +- // just the first line of the file. +- var start, end int +- if pm.File.Module != nil && pm.File.Module.Syntax != nil { +- syntax := pm.File.Module.Syntax +- start, end = syntax.Start.Byte, syntax.End.Byte - } -- }) -- return fixes +- loc, err := pm.Mapper.OffsetLocation(start, end) +- return loc, false, err +- } +- +- loc, err := pm.Mapper.OffsetLocation(reference.Start.Byte, reference.End.Byte) +- return loc, true, err -} - --// walkASTWithParent walks the AST rooted at n. The semantics are --// similar to ast.Inspect except it does not call f(nil). --func walkASTWithParent(n ast.Node, f func(n ast.Node, parent ast.Node) bool) { -- var ancestors []ast.Node -- ast.Inspect(n, func(n ast.Node) (recurse bool) { -- defer func() { -- if recurse { -- ancestors = append(ancestors, n) -- } -- }() +-// goCommandDiagnostic creates a diagnostic for a given go command error. +-func (s *Snapshot) goCommandDiagnostic(pm *ParsedModule, loc protocol.Location, goCmdError string) (*Diagnostic, error) { +- matches := moduleVersionInErrorRe.FindAllStringSubmatch(goCmdError, -1) +- var innermost *module.Version +- for i := len(matches) - 1; i >= 0; i-- { +- ver := module.Version{Path: matches[i][1], Version: matches[i][2]} +- if err := module.Check(ver.Path, ver.Version); err != nil { +- continue +- } +- innermost = &ver +- break +- } - -- if n == nil { -- ancestors = ancestors[:len(ancestors)-1] -- return false +- switch { +- case strings.Contains(goCmdError, "inconsistent vendoring"): +- cmd, err := command.NewVendorCommand("Run go mod vendor", command.URIArg{URI: pm.URI}) +- if err != nil { +- return nil, err - } +- return &Diagnostic{ +- URI: pm.URI, +- Range: loc.Range, +- Severity: protocol.SeverityError, +- Source: ListError, +- Message: `Inconsistent vendoring detected. Please re-run "go mod vendor". +-See https://github.com/golang/go/issues/39164 for more detail on this issue.`, +- SuggestedFixes: []SuggestedFix{SuggestedFixFromCommand(cmd, protocol.QuickFix)}, +- }, nil - -- var parent ast.Node -- if len(ancestors) > 0 { -- parent = ancestors[len(ancestors)-1] +- case strings.Contains(goCmdError, "updates to go.sum needed"), strings.Contains(goCmdError, "missing go.sum entry"): +- var args []protocol.DocumentURI +- args = append(args, s.View().ModFiles()...) +- tidyCmd, err := command.NewTidyCommand("Run go mod tidy", command.URIArgs{URIs: args}) +- if err != nil { +- return nil, err +- } +- updateCmd, err := command.NewUpdateGoSumCommand("Update go.sum", command.URIArgs{URIs: args}) +- if err != nil { +- return nil, err +- } +- msg := "go.sum is out of sync with go.mod. Please update it by applying the quick fix." +- if innermost != nil { +- msg = fmt.Sprintf("go.sum is out of sync with go.mod: entry for %v is missing. Please updating it by applying the quick fix.", innermost) +- } +- return &Diagnostic{ +- URI: pm.URI, +- Range: loc.Range, +- Severity: protocol.SeverityError, +- Source: ListError, +- Message: msg, +- SuggestedFixes: []SuggestedFix{ +- SuggestedFixFromCommand(tidyCmd, protocol.QuickFix), +- SuggestedFixFromCommand(updateCmd, protocol.QuickFix), +- }, +- }, nil +- case strings.Contains(goCmdError, "disabled by GOPROXY=off") && innermost != nil: +- title := fmt.Sprintf("Download %v@%v", innermost.Path, innermost.Version) +- cmd, err := command.NewAddDependencyCommand(title, command.DependencyArgs{ +- URI: pm.URI, +- AddRequire: false, +- GoCmdArgs: []string{fmt.Sprintf("%v@%v", innermost.Path, innermost.Version)}, +- }) +- if err != nil { +- return nil, err - } +- return &Diagnostic{ +- URI: pm.URI, +- Range: loc.Range, +- Severity: protocol.SeverityError, +- Message: fmt.Sprintf("%v@%v has not been downloaded", innermost.Path, innermost.Version), +- Source: ListError, +- SuggestedFixes: []SuggestedFix{SuggestedFixFromCommand(cmd, protocol.QuickFix)}, +- }, nil +- default: +- return &Diagnostic{ +- URI: pm.URI, +- Range: loc.Range, +- Severity: protocol.SeverityError, +- Source: ListError, +- Message: goCmdError, +- }, nil +- } +-} - -- return f(n, parent) -- }) +-func findModuleReference(mf *modfile.File, ver module.Version) *modfile.Line { +- for _, req := range mf.Require { +- if req.Mod == ver { +- return req.Syntax +- } +- } +- for _, ex := range mf.Exclude { +- if ex.Mod == ver { +- return ex.Syntax +- } +- } +- for _, rep := range mf.Replace { +- if rep.New == ver || rep.Old == ver { +- return rep.Syntax +- } +- } +- return nil -} +diff -urN a/gopls/internal/cache/mod_tidy.go b/gopls/internal/cache/mod_tidy.go +--- a/gopls/internal/cache/mod_tidy.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/mod_tidy.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,505 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// TODO(rfindley): revert this intrumentation once we're certain the crash in --// #59097 is fixed. --type fixType int +-package cache - --const ( -- noFix fixType = iota -- fixedCurlies -- fixedDanglingSelector -- fixedDeferOrGo -- fixedArrayType -- fixedInit -- fixedPhantomSelector -- fixedEmptySwitch +-import ( +- "context" +- "errors" +- "fmt" +- "go/ast" +- "go/token" +- "os" +- "path/filepath" +- "strconv" +- "strings" +- +- "golang.org/x/mod/modfile" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/command" +- "golang.org/x/tools/internal/diff" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/tag" +- "golang.org/x/tools/internal/gocommand" +- "golang.org/x/tools/internal/memoize" -) - --// fixSrc attempts to modify the file's source code to fix certain --// syntax errors that leave the rest of the file unparsed. --// --// fixSrc returns a non-nil result if and only if a fix was applied. --func fixSrc(f *ast.File, tf *token.File, src []byte) (newSrc []byte, fix fixType) { -- walkASTWithParent(f, func(n, parent ast.Node) bool { -- if newSrc != nil { -- return false -- } +-// This error is sought by mod diagnostics. +-var ErrNoModOnDisk = errors.New("go.mod file is not on disk") - -- switch n := n.(type) { -- case *ast.BlockStmt: -- newSrc = fixMissingCurlies(f, n, parent, tf, src) -- if newSrc != nil { -- fix = fixedCurlies -- } -- case *ast.SelectorExpr: -- newSrc = fixDanglingSelector(n, tf, src) -- if newSrc != nil { -- fix = fixedDanglingSelector -- } -- } +-// A TidiedModule contains the results of running `go mod tidy` on a module. +-type TidiedModule struct { +- // Diagnostics representing changes made by `go mod tidy`. +- Diagnostics []*Diagnostic +- // The bytes of the go.mod file after it was tidied. +- TidiedContent []byte +-} - -- return newSrc == nil -- }) +-// ModTidy returns the go.mod file that would be obtained by running +-// "go mod tidy". Concurrent requests are combined into a single command. +-func (s *Snapshot) ModTidy(ctx context.Context, pm *ParsedModule) (*TidiedModule, error) { +- ctx, done := event.Start(ctx, "cache.snapshot.ModTidy") +- defer done() - -- return newSrc, fix --} +- uri := pm.URI +- if pm.File == nil { +- return nil, fmt.Errorf("cannot tidy unparseable go.mod file: %v", uri) +- } - --// fixMissingCurlies adds in curly braces for block statements that --// are missing curly braces. For example: --// --// if foo --// --// becomes --// --// if foo {} --func fixMissingCurlies(f *ast.File, b *ast.BlockStmt, parent ast.Node, tok *token.File, src []byte) []byte { -- // If the "{" is already in the source code, there isn't anything to -- // fix since we aren't missing curlies. -- if b.Lbrace.IsValid() { -- braceOffset, err := safetoken.Offset(tok, b.Lbrace) +- s.mu.Lock() +- entry, hit := s.modTidyHandles.Get(uri) +- s.mu.Unlock() +- +- type modTidyResult struct { +- tidied *TidiedModule +- err error +- } +- +- // Cache miss? +- if !hit { +- // If the file handle is an overlay, it may not be written to disk. +- // The go.mod file has to be on disk for `go mod tidy` to work. +- // TODO(rfindley): is this still true with Go 1.16 overlay support? +- fh, err := s.ReadFile(ctx, pm.URI) - if err != nil { -- return nil +- return nil, err - } -- if braceOffset < len(src) && src[braceOffset] == '{' { -- return nil +- if _, ok := fh.(*overlay); ok { +- if info, _ := os.Stat(uri.Path()); info == nil { +- return nil, ErrNoModOnDisk +- } - } -- } - -- parentLine := safetoken.Line(tok, parent.Pos()) +- if err := s.awaitLoaded(ctx); err != nil { +- return nil, err +- } - -- if parentLine >= tok.LineCount() { -- // If we are the last line in the file, no need to fix anything. -- return nil +- handle := memoize.NewPromise("modTidy", func(ctx context.Context, arg interface{}) interface{} { +- tidied, err := modTidyImpl(ctx, arg.(*Snapshot), uri.Path(), pm) +- return modTidyResult{tidied, err} +- }) +- +- entry = handle +- s.mu.Lock() +- s.modTidyHandles.Set(uri, entry, nil) +- s.mu.Unlock() - } - -- // Insert curlies at the end of parent's starting line. The parent -- // is the statement that contains the block, e.g. *ast.IfStmt. The -- // block's Pos()/End() can't be relied upon because they are based -- // on the (missing) curly braces. We assume the statement is a -- // single line for now and try sticking the curly braces at the end. -- insertPos := tok.LineStart(parentLine+1) - 1 +- // Await result. +- v, err := s.awaitPromise(ctx, entry) +- if err != nil { +- return nil, err +- } +- res := v.(modTidyResult) +- return res.tidied, res.err +-} - -- // Scootch position backwards until it's not in a comment. For example: -- // -- // if foo<> // some amazing comment | -- // someOtherCode() -- // -- // insertPos will be located at "|", so we back it out of the comment. -- didSomething := true -- for didSomething { -- didSomething = false -- for _, c := range f.Comments { -- if c.Pos() < insertPos && insertPos <= c.End() { -- insertPos = c.Pos() -- didSomething = true -- } -- } -- } +-// modTidyImpl runs "go mod tidy" on a go.mod file. +-func modTidyImpl(ctx context.Context, snapshot *Snapshot, filename string, pm *ParsedModule) (*TidiedModule, error) { +- ctx, done := event.Start(ctx, "cache.ModTidy", tag.URI.Of(filename)) +- defer done() - -- // Bail out if line doesn't end in an ident or ".". This is to avoid -- // cases like below where we end up making things worse by adding -- // curlies: -- // -- // if foo && -- // bar<> -- switch precedingToken(insertPos, tok, src) { -- case token.IDENT, token.PERIOD: -- // ok -- default: -- return nil +- inv := &gocommand.Invocation{ +- Verb: "mod", +- Args: []string{"tidy"}, +- WorkingDir: filepath.Dir(filename), - } -- -- var buf bytes.Buffer -- buf.Grow(len(src) + 3) -- offset, err := safetoken.Offset(tok, insertPos) +- // TODO(adonovan): ensure that unsaved overlays are passed through to 'go'. +- tmpURI, inv, cleanup, err := snapshot.goCommandInvocation(ctx, WriteTemporaryModFile, inv) - if err != nil { -- return nil -- } -- buf.Write(src[:offset]) -- -- // Detect if we need to insert a semicolon to fix "for" loop situations like: -- // -- // for i := foo(); foo<> -- // -- // Just adding curlies is not sufficient to make things parse well. -- if fs, ok := parent.(*ast.ForStmt); ok { -- if _, ok := fs.Cond.(*ast.BadExpr); !ok { -- if xs, ok := fs.Post.(*ast.ExprStmt); ok { -- if _, ok := xs.X.(*ast.BadExpr); ok { -- buf.WriteByte(';') -- } -- } -- } +- return nil, err - } +- // Keep the temporary go.mod file around long enough to parse it. +- defer cleanup() - -- // Insert "{}" at insertPos. -- buf.WriteByte('{') -- buf.WriteByte('}') -- buf.Write(src[offset:]) -- return buf.Bytes() --} -- --// fixEmptySwitch moves empty switch/select statements' closing curly --// brace down one line. This allows us to properly detect incomplete --// "case" and "default" keywords as inside the switch statement. For --// example: --// --// switch { --// def<> --// } --// --// gets parsed like: --// --// switch { --// } --// --// Later we manually pull out the "def" token, but we need to detect --// that our "<>" position is inside the switch block. To do that we --// move the curly brace so it looks like: --// --// switch { --// --// } --// --// The resulting bool reports whether any fixing occurred. --func fixEmptySwitch(body *ast.BlockStmt, tok *token.File, src []byte) bool { -- // We only care about empty switch statements. -- if len(body.List) > 0 || !body.Rbrace.IsValid() { -- return false +- if _, err := snapshot.view.gocmdRunner.Run(ctx, *inv); err != nil { +- return nil, err - } - -- // If the right brace is actually in the source code at the -- // specified position, don't mess with it. -- braceOffset, err := safetoken.Offset(tok, body.Rbrace) +- // Go directly to disk to get the temporary mod file, +- // since it is always on disk. +- tempContents, err := os.ReadFile(tmpURI.Path()) - if err != nil { -- return false +- return nil, err - } -- if braceOffset < len(src) && src[braceOffset] == '}' { -- return false +- ideal, err := modfile.Parse(tmpURI.Path(), tempContents, nil) +- if err != nil { +- // We do not need to worry about the temporary file's parse errors +- // since it has been "tidied". +- return nil, err - } - -- braceLine := safetoken.Line(tok, body.Rbrace) -- if braceLine >= tok.LineCount() { -- // If we are the last line in the file, no need to fix anything. -- return false +- // Compare the original and tidied go.mod files to compute errors and +- // suggested fixes. +- diagnostics, err := modTidyDiagnostics(ctx, snapshot, pm, ideal) +- if err != nil { +- return nil, err - } - -- // Move the right brace down one line. -- body.Rbrace = tok.LineStart(braceLine + 1) -- return true +- return &TidiedModule{ +- Diagnostics: diagnostics, +- TidiedContent: tempContents, +- }, nil -} - --// fixDanglingSelector inserts real "_" selector expressions in place --// of phantom "_" selectors. For example: --// --// func _() { --// x.<> --// } --// --// var x struct { i int } --// --// To fix completion at "<>", we insert a real "_" after the "." so the --// following declaration of "x" can be parsed and type checked --// normally. --func fixDanglingSelector(s *ast.SelectorExpr, tf *token.File, src []byte) []byte { -- if !isPhantomUnderscore(s.Sel, tf, src) { -- return nil +-// modTidyDiagnostics computes the differences between the original and tidied +-// go.mod files to produce diagnostic and suggested fixes. Some diagnostics +-// may appear on the Go files that import packages from missing modules. +-func modTidyDiagnostics(ctx context.Context, snapshot *Snapshot, pm *ParsedModule, ideal *modfile.File) (diagnostics []*Diagnostic, err error) { +- // First, determine which modules are unused and which are missing from the +- // original go.mod file. +- var ( +- unused = make(map[string]*modfile.Require, len(pm.File.Require)) +- missing = make(map[string]*modfile.Require, len(ideal.Require)) +- wrongDirectness = make(map[string]*modfile.Require, len(pm.File.Require)) +- ) +- for _, req := range pm.File.Require { +- unused[req.Mod.Path] = req - } -- -- if !s.X.End().IsValid() { -- return nil +- for _, req := range ideal.Require { +- origReq := unused[req.Mod.Path] +- if origReq == nil { +- missing[req.Mod.Path] = req +- continue +- } else if origReq.Indirect != req.Indirect { +- wrongDirectness[req.Mod.Path] = origReq +- } +- delete(unused, req.Mod.Path) - } -- -- insertOffset, err := safetoken.Offset(tf, s.X.End()) -- if err != nil { -- return nil +- for _, req := range wrongDirectness { +- // Handle dependencies that are incorrectly labeled indirect and +- // vice versa. +- srcDiag, err := directnessDiagnostic(pm.Mapper, req) +- if err != nil { +- // We're probably in a bad state if we can't compute a +- // directnessDiagnostic, but try to keep going so as to not suppress +- // other, valid diagnostics. +- event.Error(ctx, "computing directness diagnostic", err) +- continue +- } +- diagnostics = append(diagnostics, srcDiag) - } -- // Insert directly after the selector's ".". -- insertOffset++ -- if src[insertOffset-1] != '.' { -- return nil +- // Next, compute any diagnostics for modules that are missing from the +- // go.mod file. The fixes will be for the go.mod file, but the +- // diagnostics should also appear in both the go.mod file and the import +- // statements in the Go files in which the dependencies are used. +- // Finally, add errors for any unused dependencies. +- if len(missing) > 0 { +- missingModuleDiagnostics, err := missingModuleDiagnostics(ctx, snapshot, pm, ideal, missing) +- if err != nil { +- return nil, err +- } +- diagnostics = append(diagnostics, missingModuleDiagnostics...) - } - -- var buf bytes.Buffer -- buf.Grow(len(src) + 1) -- buf.Write(src[:insertOffset]) -- buf.WriteByte('_') -- buf.Write(src[insertOffset:]) -- return buf.Bytes() +- // Opt: if this is the only diagnostic, we can avoid textual edits and just +- // run the Go command. +- // +- // See also the documentation for command.RemoveDependencyArgs.OnlyDiagnostic. +- onlyDiagnostic := len(diagnostics) == 0 && len(unused) == 1 +- for _, req := range unused { +- srcErr, err := unusedDiagnostic(pm.Mapper, req, onlyDiagnostic) +- if err != nil { +- return nil, err +- } +- diagnostics = append(diagnostics, srcErr) +- } +- return diagnostics, nil -} - --// fixPhantomSelector tries to fix selector expressions with phantom --// "_" selectors. In particular, we check if the selector is a --// keyword, and if so we swap in an *ast.Ident with the keyword text. For example: --// --// foo.var --// --// yields a "_" selector instead of "var" since "var" is a keyword. --// --// TODO(rfindley): should this constitute an ast 'fix'? --// --// The resulting bool reports whether any fixing occurred. --func fixPhantomSelector(sel *ast.SelectorExpr, tf *token.File, src []byte) bool { -- if !isPhantomUnderscore(sel.Sel, tf, src) { -- return false +-func missingModuleDiagnostics(ctx context.Context, snapshot *Snapshot, pm *ParsedModule, ideal *modfile.File, missing map[string]*modfile.Require) ([]*Diagnostic, error) { +- missingModuleFixes := map[*modfile.Require][]SuggestedFix{} +- var diagnostics []*Diagnostic +- for _, req := range missing { +- srcDiag, err := missingModuleDiagnostic(pm, req) +- if err != nil { +- return nil, err +- } +- missingModuleFixes[req] = srcDiag.SuggestedFixes +- diagnostics = append(diagnostics, srcDiag) - } - -- // Only consider selectors directly abutting the selector ".". This -- // avoids false positives in cases like: -- // -- // foo. // don't think "var" is our selector -- // var bar = 123 -- // -- if sel.Sel.Pos() != sel.X.End()+1 { -- return false +- // Add diagnostics for missing modules anywhere they are imported in the +- // workspace. +- metas, err := snapshot.WorkspaceMetadata(ctx) +- if err != nil { +- return nil, err - } +- // TODO(adonovan): opt: opportunities for parallelism abound. +- for _, mp := range metas { +- // Read both lists of files of this package. +- // +- // Parallelism is not necessary here as the files will have already been +- // pre-read at load time. +- goFiles, err := readFiles(ctx, snapshot, mp.GoFiles) +- if err != nil { +- return nil, err +- } +- compiledGoFiles, err := readFiles(ctx, snapshot, mp.CompiledGoFiles) +- if err != nil { +- return nil, err +- } - -- maybeKeyword := readKeyword(sel.Sel.Pos(), tf, src) -- if maybeKeyword == "" { -- return false -- } +- missingImports := map[string]*modfile.Require{} - -- return replaceNode(sel, sel.Sel, &ast.Ident{ -- Name: maybeKeyword, -- NamePos: sel.Sel.Pos(), -- }) +- // If -mod=readonly is not set we may have successfully imported +- // packages from missing modules. Otherwise they'll be in +- // MissingDependencies. Combine both. +- imps, err := parseImports(ctx, snapshot, goFiles) +- if err != nil { +- return nil, err +- } +- for imp := range imps { +- if req, ok := missing[imp]; ok { +- missingImports[imp] = req +- break +- } +- // If the import is a package of the dependency, then add the +- // package to the map, this will eliminate the need to do this +- // prefix package search on each import for each file. +- // Example: +- // +- // import ( +- // "golang.org/x/tools/go/expect" +- // "golang.org/x/tools/go/packages" +- // ) +- // They both are related to the same module: "golang.org/x/tools". +- var match string +- for _, req := range ideal.Require { +- if strings.HasPrefix(imp, req.Mod.Path) && len(req.Mod.Path) > len(match) { +- match = req.Mod.Path +- } +- } +- if req, ok := missing[match]; ok { +- missingImports[imp] = req +- } +- } +- // None of this package's imports are from missing modules. +- if len(missingImports) == 0 { +- continue +- } +- for _, goFile := range compiledGoFiles { +- pgf, err := snapshot.ParseGo(ctx, goFile, parsego.Header) +- if err != nil { +- continue +- } +- file, m := pgf.File, pgf.Mapper +- if file == nil || m == nil { +- continue +- } +- imports := make(map[string]*ast.ImportSpec) +- for _, imp := range file.Imports { +- if imp.Path == nil { +- continue +- } +- if target, err := strconv.Unquote(imp.Path.Value); err == nil { +- imports[target] = imp +- } +- } +- if len(imports) == 0 { +- continue +- } +- for importPath, req := range missingImports { +- imp, ok := imports[importPath] +- if !ok { +- continue +- } +- fixes, ok := missingModuleFixes[req] +- if !ok { +- return nil, fmt.Errorf("no missing module fix for %q (%q)", importPath, req.Mod.Path) +- } +- srcErr, err := missingModuleForImport(pgf, imp, req, fixes) +- if err != nil { +- return nil, err +- } +- diagnostics = append(diagnostics, srcErr) +- } +- } +- } +- return diagnostics, nil -} - --// isPhantomUnderscore reports whether the given ident is a phantom --// underscore. The parser sometimes inserts phantom underscores when --// it encounters otherwise unparseable situations. --func isPhantomUnderscore(id *ast.Ident, tok *token.File, src []byte) bool { -- if id == nil || id.Name != "_" { -- return false +-// unusedDiagnostic returns a Diagnostic for an unused require. +-func unusedDiagnostic(m *protocol.Mapper, req *modfile.Require, onlyDiagnostic bool) (*Diagnostic, error) { +- rng, err := m.OffsetRange(req.Syntax.Start.Byte, req.Syntax.End.Byte) +- if err != nil { +- return nil, err - } -- -- // Phantom underscore means the underscore is not actually in the -- // program text. -- offset, err := safetoken.Offset(tok, id.Pos()) +- title := fmt.Sprintf("Remove dependency: %s", req.Mod.Path) +- cmd, err := command.NewRemoveDependencyCommand(title, command.RemoveDependencyArgs{ +- URI: m.URI, +- OnlyDiagnostic: onlyDiagnostic, +- ModulePath: req.Mod.Path, +- }) - if err != nil { -- return false +- return nil, err - } -- return len(src) <= offset || src[offset] != '_' +- return &Diagnostic{ +- URI: m.URI, +- Range: rng, +- Severity: protocol.SeverityWarning, +- Source: ModTidyError, +- Message: fmt.Sprintf("%s is not used in this module", req.Mod.Path), +- SuggestedFixes: []SuggestedFix{SuggestedFixFromCommand(cmd, protocol.QuickFix)}, +- }, nil -} - --// fixInitStmt fixes cases where the parser misinterprets an --// if/for/switch "init" statement as the "cond" conditional. In cases --// like "if i := 0" the user hasn't typed the semicolon yet so the --// parser is looking for the conditional expression. However, "i := 0" --// are not valid expressions, so we get a BadExpr. --// --// The resulting bool reports whether any fixing occurred. --func fixInitStmt(bad *ast.BadExpr, parent ast.Node, tok *token.File, src []byte) bool { -- if !bad.Pos().IsValid() || !bad.End().IsValid() { -- return false +-// directnessDiagnostic extracts errors when a dependency is labeled indirect when +-// it should be direct and vice versa. +-func directnessDiagnostic(m *protocol.Mapper, req *modfile.Require) (*Diagnostic, error) { +- rng, err := m.OffsetRange(req.Syntax.Start.Byte, req.Syntax.End.Byte) +- if err != nil { +- return nil, err - } +- direction := "indirect" +- if req.Indirect { +- direction = "direct" - -- // Try to extract a statement from the BadExpr. -- start, end, err := safetoken.Offsets(tok, bad.Pos(), bad.End()-1) -- if err != nil { -- return false +- // If the dependency should be direct, just highlight the // indirect. +- if comments := req.Syntax.Comment(); comments != nil && len(comments.Suffix) > 0 { +- end := comments.Suffix[0].Start +- end.LineRune += len(comments.Suffix[0].Token) +- end.Byte += len(comments.Suffix[0].Token) +- rng, err = m.OffsetRange(comments.Suffix[0].Start.Byte, end.Byte) +- if err != nil { +- return nil, err +- } +- } - } -- stmtBytes := src[start : end+1] -- stmt, err := parseStmt(bad.Pos(), stmtBytes) +- // If the dependency should be indirect, add the // indirect. +- edits, err := switchDirectness(req, m) - if err != nil { -- return false +- return nil, err - } +- return &Diagnostic{ +- URI: m.URI, +- Range: rng, +- Severity: protocol.SeverityWarning, +- Source: ModTidyError, +- Message: fmt.Sprintf("%s should be %s", req.Mod.Path, direction), +- SuggestedFixes: []SuggestedFix{{ +- Title: fmt.Sprintf("Change %s to %s", req.Mod.Path, direction), +- Edits: map[protocol.DocumentURI][]protocol.TextEdit{ +- m.URI: edits, +- }, +- ActionKind: protocol.QuickFix, +- }}, +- }, nil +-} - -- // If the parent statement doesn't already have an "init" statement, -- // move the extracted statement into the "init" field and insert a -- // dummy expression into the required "cond" field. -- switch p := parent.(type) { -- case *ast.IfStmt: -- if p.Init != nil { -- return false -- } -- p.Init = stmt -- p.Cond = &ast.Ident{ -- Name: "_", -- NamePos: stmt.End(), -- } -- return true -- case *ast.ForStmt: -- if p.Init != nil { -- return false -- } -- p.Init = stmt -- p.Cond = &ast.Ident{ -- Name: "_", -- NamePos: stmt.End(), -- } -- return true -- case *ast.SwitchStmt: -- if p.Init != nil { -- return false +-func missingModuleDiagnostic(pm *ParsedModule, req *modfile.Require) (*Diagnostic, error) { +- var rng protocol.Range +- // Default to the start of the file if there is no module declaration. +- if pm.File != nil && pm.File.Module != nil && pm.File.Module.Syntax != nil { +- start, end := pm.File.Module.Syntax.Span() +- var err error +- rng, err = pm.Mapper.OffsetRange(start.Byte, end.Byte) +- if err != nil { +- return nil, err - } -- p.Init = stmt -- p.Tag = nil -- return true - } -- return false +- title := fmt.Sprintf("Add %s to your go.mod file", req.Mod.Path) +- cmd, err := command.NewAddDependencyCommand(title, command.DependencyArgs{ +- URI: pm.Mapper.URI, +- AddRequire: !req.Indirect, +- GoCmdArgs: []string{req.Mod.Path + "@" + req.Mod.Version}, +- }) +- if err != nil { +- return nil, err +- } +- return &Diagnostic{ +- URI: pm.Mapper.URI, +- Range: rng, +- Severity: protocol.SeverityError, +- Source: ModTidyError, +- Message: fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path), +- SuggestedFixes: []SuggestedFix{SuggestedFixFromCommand(cmd, protocol.QuickFix)}, +- }, nil -} - --// readKeyword reads the keyword starting at pos, if any. --func readKeyword(pos token.Pos, tok *token.File, src []byte) string { -- var kwBytes []byte -- offset, err := safetoken.Offset(tok, pos) +-// switchDirectness gets the edits needed to change an indirect dependency to +-// direct and vice versa. +-func switchDirectness(req *modfile.Require, m *protocol.Mapper) ([]protocol.TextEdit, error) { +- // We need a private copy of the parsed go.mod file, since we're going to +- // modify it. +- copied, err := modfile.Parse("", m.Content, nil) - if err != nil { -- return "" +- return nil, err - } -- for i := offset; i < len(src); i++ { -- // Use a simplified identifier check since keywords are always lowercase ASCII. -- if src[i] < 'a' || src[i] > 'z' { -- break +- // Change the directness in the matching require statement. To avoid +- // reordering the require statements, rewrite all of them. +- var requires []*modfile.Require +- seenVersions := make(map[string]string) +- for _, r := range copied.Require { +- if seen := seenVersions[r.Mod.Path]; seen != "" && seen != r.Mod.Version { +- // Avoid a panic in SetRequire below, which panics on conflicting +- // versions. +- return nil, fmt.Errorf("%q has conflicting versions: %q and %q", r.Mod.Path, seen, r.Mod.Version) - } -- kwBytes = append(kwBytes, src[i]) -- -- // Stop search at arbitrarily chosen too-long-for-a-keyword length. -- if len(kwBytes) > 15 { -- return "" +- seenVersions[r.Mod.Path] = r.Mod.Version +- if r.Mod.Path == req.Mod.Path { +- requires = append(requires, &modfile.Require{ +- Mod: r.Mod, +- Syntax: r.Syntax, +- Indirect: !r.Indirect, +- }) +- continue - } +- requires = append(requires, r) - } -- -- if kw := string(kwBytes); token.Lookup(kw).IsKeyword() { -- return kw +- copied.SetRequire(requires) +- newContent, err := copied.Format() +- if err != nil { +- return nil, err - } +- // Calculate the edits to be made due to the change. +- edits := diff.Bytes(m.Content, newContent) +- return protocol.EditsFromDiffEdits(m, edits) +-} - -- return "" +-// missingModuleForImport creates an error for a given import path that comes +-// from a missing module. +-func missingModuleForImport(pgf *parsego.File, imp *ast.ImportSpec, req *modfile.Require, fixes []SuggestedFix) (*Diagnostic, error) { +- if req.Syntax == nil { +- return nil, fmt.Errorf("no syntax for %v", req) +- } +- rng, err := pgf.NodeRange(imp.Path) +- if err != nil { +- return nil, err +- } +- return &Diagnostic{ +- URI: pgf.URI, +- Range: rng, +- Severity: protocol.SeverityError, +- Source: ModTidyError, +- Message: fmt.Sprintf("%s is not in your go.mod file", req.Mod.Path), +- SuggestedFixes: fixes, +- }, nil -} - --// fixArrayType tries to parse an *ast.BadExpr into an *ast.ArrayType. --// go/parser often turns lone array types like "[]int" into BadExprs --// if it isn't expecting a type. --func fixArrayType(bad *ast.BadExpr, parent ast.Node, tok *token.File, src []byte) bool { -- // Our expected input is a bad expression that looks like "[]someExpr". -- -- from := bad.Pos() -- to := bad.End() -- -- if !from.IsValid() || !to.IsValid() { -- return false +-// parseImports parses the headers of the specified files and returns +-// the set of strings that appear in import declarations within +-// GoFiles. Errors are ignored. +-// +-// (We can't simply use Metadata.Imports because it is based on +-// CompiledGoFiles, after cgo processing.) +-// +-// TODO(rfindley): this should key off ImportPath. +-func parseImports(ctx context.Context, s *Snapshot, files []file.Handle) (map[string]bool, error) { +- pgfs, err := s.view.parseCache.parseFiles(ctx, token.NewFileSet(), parsego.Header, false, files...) +- if err != nil { // e.g. context cancellation +- return nil, err - } - -- exprBytes := make([]byte, 0, int(to-from)+3) -- // Avoid doing tok.Offset(to) since that panics if badExpr ends at EOF. -- // It also panics if the position is not in the range of the file, and -- // badExprs may not necessarily have good positions, so check first. -- fromOffset, toOffset, err := safetoken.Offsets(tok, from, to-1) -- if err != nil { -- return false +- seen := make(map[string]bool) +- for _, pgf := range pgfs { +- for _, spec := range pgf.File.Imports { +- path, _ := strconv.Unquote(spec.Path.Value) +- seen[path] = true +- } - } -- exprBytes = append(exprBytes, src[fromOffset:toOffset+1]...) -- exprBytes = bytes.TrimSpace(exprBytes) +- return seen, nil +-} +diff -urN a/gopls/internal/cache/mod_vuln.go b/gopls/internal/cache/mod_vuln.go +--- a/gopls/internal/cache/mod_vuln.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/mod_vuln.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,389 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- // If our expression ends in "]" (e.g. "[]"), add a phantom selector -- // so we can complete directly after the "[]". -- if len(exprBytes) > 0 && exprBytes[len(exprBytes)-1] == ']' { -- exprBytes = append(exprBytes, '_') -- } +-package cache - -- // Add "{}" to turn our ArrayType into a CompositeLit. This is to -- // handle the case of "[...]int" where we must make it a composite -- // literal to be parseable. -- exprBytes = append(exprBytes, '{', '}') +-import ( +- "context" +- "fmt" +- "io" +- "os" +- "sort" +- "strings" +- "sync" - -- expr, err := parseExpr(from, exprBytes) -- if err != nil { -- return false -- } +- "golang.org/x/mod/semver" +- "golang.org/x/sync/errgroup" +- "golang.org/x/tools/go/packages" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/vulncheck" +- "golang.org/x/tools/gopls/internal/vulncheck/govulncheck" +- "golang.org/x/tools/gopls/internal/vulncheck/osv" +- isem "golang.org/x/tools/gopls/internal/vulncheck/semver" +- "golang.org/x/tools/internal/memoize" +- "golang.org/x/vuln/scan" +-) - -- cl, _ := expr.(*ast.CompositeLit) -- if cl == nil { -- return false -- } +-// ModVuln returns import vulnerability analysis for the given go.mod URI. +-// Concurrent requests are combined into a single command. +-func (s *Snapshot) ModVuln(ctx context.Context, modURI protocol.DocumentURI) (*vulncheck.Result, error) { +- s.mu.Lock() +- entry, hit := s.modVulnHandles.Get(modURI) +- s.mu.Unlock() - -- at, _ := cl.Type.(*ast.ArrayType) -- if at == nil { -- return false +- type modVuln struct { +- result *vulncheck.Result +- err error - } - -- return replaceNode(parent, bad, at) --} -- --// precedingToken scans src to find the token preceding pos. --func precedingToken(pos token.Pos, tok *token.File, src []byte) token.Token { -- s := &scanner.Scanner{} -- s.Init(tok, src, nil, 0) +- // Cache miss? +- if !hit { +- handle := memoize.NewPromise("modVuln", func(ctx context.Context, arg interface{}) interface{} { +- result, err := modVulnImpl(ctx, arg.(*Snapshot)) +- return modVuln{result, err} +- }) - -- var lastTok token.Token -- for { -- p, t, _ := s.Scan() -- if t == token.EOF || p >= pos { -- break -- } +- entry = handle +- s.mu.Lock() +- s.modVulnHandles.Set(modURI, entry, nil) +- s.mu.Unlock() +- } - -- lastTok = t +- // Await result. +- v, err := s.awaitPromise(ctx, entry) +- if err != nil { +- return nil, err - } -- return lastTok +- res := v.(modVuln) +- return res.result, res.err -} - --// fixDeferOrGoStmt tries to parse an *ast.BadStmt into a defer or a go statement. --// --// go/parser packages a statement of the form "defer x." as an *ast.BadStmt because --// it does not include a call expression. This means that go/types skips type-checking --// this statement entirely, and we can't use the type information when completing. --// Here, we try to generate a fake *ast.DeferStmt or *ast.GoStmt to put into the AST, --// instead of the *ast.BadStmt. --func fixDeferOrGoStmt(bad *ast.BadStmt, parent ast.Node, tok *token.File, src []byte) bool { -- // Check if we have a bad statement containing either a "go" or "defer". -- s := &scanner.Scanner{} -- s.Init(tok, src, nil, 0) +-// GoVersionForVulnTest is an internal environment variable used in gopls +-// testing to examine govulncheck behavior with a go version different +-// than what `go version` returns in the system. +-const GoVersionForVulnTest = "_GOPLS_TEST_VULNCHECK_GOVERSION" - -- var ( -- pos token.Pos -- tkn token.Token -- ) -- for { -- if tkn == token.EOF { -- return false -- } -- if pos >= bad.From { -- break -- } -- pos, tkn, _ = s.Scan() +-// modVulnImpl queries the vulndb and reports which vulnerabilities +-// apply to this snapshot. The result contains a set of packages, +-// grouped by vuln ID and by module. This implements the "import-based" +-// vulnerability report on go.mod files. +-func modVulnImpl(ctx context.Context, snapshot *Snapshot) (*vulncheck.Result, error) { +- // TODO(hyangah): can we let 'govulncheck' take a package list +- // used in the workspace and implement this function? +- +- // We want to report the intersection of vulnerable packages in the vulndb +- // and packages transitively imported by this module ('go list -deps all'). +- // We use snapshot.AllMetadata to retrieve the list of packages +- // as an approximation. +- // +- // TODO(hyangah): snapshot.AllMetadata is a superset of +- // `go list all` - e.g. when the workspace has multiple main modules +- // (multiple go.mod files), that can include packages that are not +- // used by this module. Vulncheck behavior with go.work is not well +- // defined. Figure out the meaning, and if we decide to present +- // the result as if each module is analyzed independently, make +- // gopls track a separate build list for each module and use that +- // information instead of snapshot.AllMetadata. +- allMeta, err := snapshot.AllMetadata(ctx) +- if err != nil { +- return nil, err - } - -- var stmt ast.Stmt -- switch tkn { -- case token.DEFER: -- stmt = &ast.DeferStmt{ -- Defer: pos, -- } -- case token.GO: -- stmt = &ast.GoStmt{ -- Go: pos, +- // TODO(hyangah): handle vulnerabilities in the standard library. +- +- // Group packages by modules since vuln db is keyed by module. +- packagesByModule := map[metadata.PackagePath][]*metadata.Package{} +- for _, mp := range allMeta { +- modulePath := metadata.PackagePath(osv.GoStdModulePath) +- if mi := mp.Module; mi != nil { +- modulePath = metadata.PackagePath(mi.Path) - } -- default: -- return false +- packagesByModule[modulePath] = append(packagesByModule[modulePath], mp) - } - - var ( -- from, to, last token.Pos -- lastToken token.Token -- braceDepth int -- phantomSelectors []token.Pos +- mu sync.Mutex +- // Keys are osv.Entry.ID +- osvs = map[string]*osv.Entry{} +- findings []*govulncheck.Finding - ) --FindTo: -- for { -- to, tkn, _ = s.Scan() - -- if from == token.NoPos { -- from = to -- } +- goVersion := snapshot.Options().Env[GoVersionForVulnTest] +- if goVersion == "" { +- goVersion = snapshot.GoVersionString() +- } - -- switch tkn { -- case token.EOF: -- break FindTo -- case token.SEMICOLON: -- // If we aren't in nested braces, end of statement means -- // end of expression. -- if braceDepth == 0 { -- break FindTo +- stdlibModule := &packages.Module{ +- Path: osv.GoStdModulePath, +- Version: goVersion, +- } +- +- // GOVULNDB may point the test db URI. +- db := GetEnv(snapshot, "GOVULNDB") +- +- var group errgroup.Group +- group.SetLimit(10) // limit govulncheck api runs +- for _, mps := range packagesByModule { +- mps := mps +- group.Go(func() error { +- effectiveModule := stdlibModule +- if m := mps[0].Module; m != nil { +- effectiveModule = m +- } +- for effectiveModule.Replace != nil { +- effectiveModule = effectiveModule.Replace +- } +- ver := effectiveModule.Version +- if ver == "" || !isem.Valid(ver) { +- // skip invalid version strings. the underlying scan api is strict. +- return nil - } -- case token.LBRACE: -- braceDepth++ -- } - -- // This handles the common dangling selector case. For example in -- // -- // defer fmt. -- // y := 1 -- // -- // we notice the dangling period and end our expression. -- // -- // If the previous token was a "." and we are looking at a "}", -- // the period is likely a dangling selector and needs a phantom -- // "_". Likewise if the current token is on a different line than -- // the period, the period is likely a dangling selector. -- if lastToken == token.PERIOD && (tkn == token.RBRACE || safetoken.Line(tok, to) > safetoken.Line(tok, last)) { -- // Insert phantom "_" selector after the dangling ".". -- phantomSelectors = append(phantomSelectors, last+1) -- // If we aren't in a block then end the expression after the ".". -- if braceDepth == 0 { -- to = last + 1 -- break +- // TODO(hyangah): batch these requests and add in-memory cache for efficiency. +- vulns, err := osvsByModule(ctx, db, effectiveModule.Path+"@"+ver) +- if err != nil { +- return err +- } +- if len(vulns) == 0 { // No known vulnerability. +- return nil - } -- } - -- lastToken = tkn -- last = to +- // set of packages in this module known to gopls. +- // This will be lazily initialized when we need it. +- var knownPkgs map[metadata.PackagePath]bool - -- switch tkn { -- case token.RBRACE: -- braceDepth-- -- if braceDepth <= 0 { -- if braceDepth == 0 { -- // +1 to include the "}" itself. -- to += 1 +- // Report vulnerabilities that affect packages of this module. +- for _, entry := range vulns { +- var vulnerablePkgs []*govulncheck.Finding +- fixed := fixedVersion(effectiveModule.Path, entry.Affected) +- +- for _, a := range entry.Affected { +- if a.Module.Ecosystem != osv.GoEcosystem || a.Module.Path != effectiveModule.Path { +- continue +- } +- for _, imp := range a.EcosystemSpecific.Packages { +- if knownPkgs == nil { +- knownPkgs = toPackagePathSet(mps) +- } +- if knownPkgs[metadata.PackagePath(imp.Path)] { +- vulnerablePkgs = append(vulnerablePkgs, &govulncheck.Finding{ +- OSV: entry.ID, +- FixedVersion: fixed, +- Trace: []*govulncheck.Frame{ +- { +- Module: effectiveModule.Path, +- Version: effectiveModule.Version, +- Package: imp.Path, +- }, +- }, +- }) +- } +- } - } -- break FindTo +- if len(vulnerablePkgs) == 0 { +- continue +- } +- mu.Lock() +- osvs[entry.ID] = entry +- findings = append(findings, vulnerablePkgs...) +- mu.Unlock() - } -- } -- } -- -- fromOffset, toOffset, err := safetoken.Offsets(tok, from, to) -- if err != nil { -- return false -- } -- if !from.IsValid() || fromOffset >= len(src) { -- return false +- return nil +- }) - } -- if !to.IsValid() || toOffset >= len(src) { -- return false +- if err := group.Wait(); err != nil { +- return nil, err - } - -- // Insert any phantom selectors needed to prevent dangling "." from messing -- // up the AST. -- exprBytes := make([]byte, 0, int(to-from)+len(phantomSelectors)) -- for i, b := range src[fromOffset:toOffset] { -- if len(phantomSelectors) > 0 && from+token.Pos(i) == phantomSelectors[0] { -- exprBytes = append(exprBytes, '_') -- phantomSelectors = phantomSelectors[1:] +- // Sort so the results are deterministic. +- sort.Slice(findings, func(i, j int) bool { +- x, y := findings[i], findings[j] +- if x.OSV != y.OSV { +- return x.OSV < y.OSV - } -- exprBytes = append(exprBytes, b) +- return x.Trace[0].Package < y.Trace[0].Package +- }) +- ret := &vulncheck.Result{ +- Entries: osvs, +- Findings: findings, +- Mode: vulncheck.ModeImports, - } +- return ret, nil +-} - -- if len(phantomSelectors) > 0 { -- exprBytes = append(exprBytes, '_') +-// TODO(rfindley): this function was exposed during refactoring. Reconsider it. +-func GetEnv(snapshot *Snapshot, key string) string { +- val, ok := snapshot.Options().Env[key] +- if ok { +- return val - } +- return os.Getenv(key) +-} - -- expr, err := parseExpr(from, exprBytes) -- if err != nil { -- return false +-// toPackagePathSet transforms the metadata to a set of package paths. +-func toPackagePathSet(mds []*metadata.Package) map[metadata.PackagePath]bool { +- pkgPaths := make(map[metadata.PackagePath]bool, len(mds)) +- for _, md := range mds { +- pkgPaths[md.PkgPath] = true - } +- return pkgPaths +-} - -- // Package the expression into a fake *ast.CallExpr and re-insert -- // into the function. -- call := &ast.CallExpr{ -- Fun: expr, -- Lparen: to, -- Rparen: to, +-func fixedVersion(modulePath string, affected []osv.Affected) string { +- fixed := latestFixed(modulePath, affected) +- if fixed != "" { +- fixed = versionString(modulePath, fixed) - } +- return fixed +-} - -- switch stmt := stmt.(type) { -- case *ast.DeferStmt: -- stmt.Call = call -- case *ast.GoStmt: -- stmt.Call = call +-// latestFixed returns the latest fixed version in the list of affected ranges, +-// or the empty string if there are no fixed versions. +-func latestFixed(modulePath string, as []osv.Affected) string { +- v := "" +- for _, a := range as { +- if a.Module.Path != modulePath { +- continue +- } +- for _, r := range a.Ranges { +- if r.Type == osv.RangeTypeSemver { +- for _, e := range r.Events { +- if e.Fixed != "" && (v == "" || +- semver.Compare(isem.CanonicalizeSemverPrefix(e.Fixed), isem.CanonicalizeSemverPrefix(v)) > 0) { +- v = e.Fixed +- } +- } +- } +- } - } -- -- return replaceNode(parent, bad, stmt) +- return v -} - --// parseStmt parses the statement in src and updates its position to --// start at pos. --func parseStmt(pos token.Pos, src []byte) (ast.Stmt, error) { -- // Wrap our expression to make it a valid Go file we can pass to ParseFile. -- fileSrc := bytes.Join([][]byte{ -- []byte("package fake;func _(){"), -- src, -- []byte("}"), -- }, nil) -- -- // Use ParseFile instead of ParseExpr because ParseFile has -- // best-effort behavior, whereas ParseExpr fails hard on any error. -- fakeFile, err := parser.ParseFile(token.NewFileSet(), "", fileSrc, 0) -- if fakeFile == nil { -- return nil, fmt.Errorf("error reading fake file source: %v", err) +-// versionString prepends a version string prefix (`v` or `go` +-// depending on the modulePath) to the given semver-style version string. +-func versionString(modulePath, version string) string { +- if version == "" { +- return "" - } -- -- // Extract our expression node from inside the fake file. -- if len(fakeFile.Decls) == 0 { -- return nil, fmt.Errorf("error parsing fake file: %v", err) +- v := "v" + version +- // These are internal Go module paths used by the vuln DB +- // when listing vulns in standard library and the go command. +- if modulePath == "stdlib" || modulePath == "toolchain" { +- return semverToGoTag(v) - } +- return v +-} - -- fakeDecl, _ := fakeFile.Decls[0].(*ast.FuncDecl) -- if fakeDecl == nil || len(fakeDecl.Body.List) == 0 { -- return nil, fmt.Errorf("no statement in %s: %v", src, err) +-// semverToGoTag returns the Go standard library repository tag corresponding +-// to semver, a version string without the initial "v". +-// Go tags differ from standard semantic versions in a few ways, +-// such as beginning with "go" instead of "v". +-func semverToGoTag(v string) string { +- if strings.HasPrefix(v, "v0.0.0") { +- return "master" - } -- -- stmt := fakeDecl.Body.List[0] -- -- // parser.ParseFile returns undefined positions. -- // Adjust them for the current file. -- offsetPositions(stmt, pos-1-(stmt.Pos()-1)) -- -- return stmt, nil +- // Special case: v1.0.0 => go1. +- if v == "v1.0.0" { +- return "go1" +- } +- if !semver.IsValid(v) { +- return fmt.Sprintf("", v) +- } +- goVersion := semver.Canonical(v) +- prerelease := semver.Prerelease(goVersion) +- versionWithoutPrerelease := strings.TrimSuffix(goVersion, prerelease) +- patch := strings.TrimPrefix(versionWithoutPrerelease, semver.MajorMinor(goVersion)+".") +- if patch == "0" { +- versionWithoutPrerelease = strings.TrimSuffix(versionWithoutPrerelease, ".0") +- } +- goVersion = fmt.Sprintf("go%s", strings.TrimPrefix(versionWithoutPrerelease, "v")) +- if prerelease != "" { +- // Go prereleases look like "beta1" instead of "beta.1". +- // "beta1" is bad for sorting (since beta10 comes before beta9), so +- // require the dot form. +- i := finalDigitsIndex(prerelease) +- if i >= 1 { +- if prerelease[i-1] != '.' { +- return fmt.Sprintf("", v) +- } +- // Remove the dot. +- prerelease = prerelease[:i-1] + prerelease[i:] +- } +- goVersion += strings.TrimPrefix(prerelease, "-") +- } +- return goVersion -} - --// parseExpr parses the expression in src and updates its position to --// start at pos. --func parseExpr(pos token.Pos, src []byte) (ast.Expr, error) { -- stmt, err := parseStmt(pos, src) -- if err != nil { -- return nil, err +-// finalDigitsIndex returns the index of the first digit in the sequence of digits ending s. +-// If s doesn't end in digits, it returns -1. +-func finalDigitsIndex(s string) int { +- // Assume ASCII (since the semver package does anyway). +- var i int +- for i = len(s) - 1; i >= 0; i-- { +- if s[i] < '0' || s[i] > '9' { +- break +- } - } -- -- exprStmt, ok := stmt.(*ast.ExprStmt) -- if !ok { -- return nil, fmt.Errorf("no expr in %s: %v", src, err) +- if i == len(s)-1 { +- return -1 - } -- -- return exprStmt.X, nil +- return i + 1 -} - --var tokenPosType = reflect.TypeOf(token.NoPos) -- --// offsetPositions applies an offset to the positions in an ast.Node. --func offsetPositions(n ast.Node, offset token.Pos) { -- ast.Inspect(n, func(n ast.Node) bool { -- if n == nil { -- return false -- } +-// osvsByModule runs a govulncheck database query. +-func osvsByModule(ctx context.Context, db, moduleVersion string) ([]*osv.Entry, error) { +- var args []string +- args = append(args, "-mode=query", "-json") +- if db != "" { +- args = append(args, "-db="+db) +- } +- args = append(args, moduleVersion) - -- v := reflect.ValueOf(n).Elem() +- ir, iw := io.Pipe() +- handler := &osvReader{} - -- switch v.Kind() { -- case reflect.Struct: -- for i := 0; i < v.NumField(); i++ { -- f := v.Field(i) -- if f.Type() != tokenPosType { -- continue -- } +- var g errgroup.Group +- g.Go(func() error { +- defer iw.Close() // scan API doesn't close cmd.Stderr/cmd.Stdout. +- cmd := scan.Command(ctx, args...) +- cmd.Stdout = iw +- // TODO(hakim): Do we need to set cmd.Env = getEnvSlices(), +- // or is the process environment good enough? +- if err := cmd.Start(); err != nil { +- return err +- } +- return cmd.Wait() +- }) +- g.Go(func() error { +- return govulncheck.HandleJSON(ir, handler) +- }) - -- if !f.CanSet() { -- continue -- } +- if err := g.Wait(); err != nil { +- return nil, err +- } +- return handler.entry, nil +-} - -- // Don't offset invalid positions: they should stay invalid. -- if !token.Pos(f.Int()).IsValid() { -- continue -- } +-// osvReader implements govulncheck.Handler. +-type osvReader struct { +- entry []*osv.Entry +-} - -- f.SetInt(f.Int() + int64(offset)) -- } -- } +-func (h *osvReader) OSV(entry *osv.Entry) error { +- h.entry = append(h.entry, entry) +- return nil +-} - -- return true -- }) +-func (h *osvReader) Config(config *govulncheck.Config) error { +- return nil -} - --// replaceNode updates parent's child oldChild to be newChild. It --// returns whether it replaced successfully. --func replaceNode(parent, oldChild, newChild ast.Node) bool { -- if parent == nil || oldChild == nil || newChild == nil { -- return false -- } +-func (h *osvReader) Finding(finding *govulncheck.Finding) error { +- return nil +-} - -- parentVal := reflect.ValueOf(parent).Elem() -- if parentVal.Kind() != reflect.Struct { -- return false +-func (h *osvReader) Progress(progress *govulncheck.Progress) error { +- return nil +-} +diff -urN a/gopls/internal/cache/os_darwin.go b/gopls/internal/cache/os_darwin.go +--- a/gopls/internal/cache/os_darwin.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/os_darwin.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,59 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package cache +- +-import ( +- "bytes" +- "fmt" +- "os" +- "path/filepath" +- "strings" +- "syscall" +- "unsafe" +-) +- +-func init() { +- checkPathValid = darwinCheckPathValid +-} +- +-func darwinCheckPathValid(path string) error { +- // Darwin provides fcntl(F_GETPATH) to get a path for an arbitrary FD. +- // Conveniently for our purposes, it gives the canonical case back. But +- // there's no guarantee that it will follow the same route through the +- // filesystem that the original path did. +- +- path, err := filepath.Abs(path) +- if err != nil { +- return err +- } +- fd, err := syscall.Open(path, os.O_RDONLY, 0) +- if err != nil { +- return err - } +- defer syscall.Close(fd) +- buf := make([]byte, 4096) // No MAXPATHLEN in syscall, I think it's 1024, this is bigger. - -- newChildVal := reflect.ValueOf(newChild) +- // Wheeee! syscall doesn't expose a way to call Fcntl except FcntlFlock. +- // As of writing, it just passes the pointer through, so we can just lie. +- if err := syscall.FcntlFlock(uintptr(fd), syscall.F_GETPATH, (*syscall.Flock_t)(unsafe.Pointer(&buf[0]))); err != nil { +- return err +- } +- buf = buf[:bytes.IndexByte(buf, 0)] - -- tryReplace := func(v reflect.Value) bool { -- if !v.CanSet() || !v.CanInterface() { -- return false +- isRoot := func(p string) bool { +- return p[len(p)-1] == filepath.Separator +- } +- // Darwin seems to like having multiple names for the same folder. Match as much of the suffix as we can. +- for got, want := path, string(buf); !isRoot(got) && !isRoot(want); got, want = filepath.Dir(got), filepath.Dir(want) { +- g, w := filepath.Base(got), filepath.Base(want) +- if !strings.EqualFold(g, w) { +- break - } -- -- // If the existing value is oldChild, we found our child. Make -- // sure our newChild is assignable and then make the swap. -- if v.Interface() == oldChild && newChildVal.Type().AssignableTo(v.Type()) { -- v.Set(newChildVal) -- return true +- if g != w { +- return fmt.Errorf("invalid path %q: component %q is listed by macOS as %q", path, g, w) - } -- -- return false - } +- return nil +-} +diff -urN a/gopls/internal/cache/os_windows.go b/gopls/internal/cache/os_windows.go +--- a/gopls/internal/cache/os_windows.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/os_windows.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,56 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- // Loop over parent's struct fields. -- for i := 0; i < parentVal.NumField(); i++ { -- f := parentVal.Field(i) +-package cache - -- switch f.Kind() { -- // Check interface and pointer fields. -- case reflect.Interface, reflect.Ptr: -- if tryReplace(f) { -- return true -- } +-import ( +- "fmt" +- "path/filepath" +- "syscall" +-) - -- // Search through any slice fields. -- case reflect.Slice: -- for i := 0; i < f.Len(); i++ { -- if tryReplace(f.Index(i)) { -- return true -- } -- } -- } +-func init() { +- checkPathValid = windowsCheckPathValid +-} +- +-func windowsCheckPathValid(path string) error { +- // Back in the day, Windows used to have short and long filenames, and +- // it still supports those APIs. GetLongPathName gets the real case for a +- // path, so we can use it here. Inspired by +- // http://stackoverflow.com/q/2113822. +- +- // Short paths can be longer than long paths, and unicode, so be generous. +- buflen := 4 * len(path) +- namep, err := syscall.UTF16PtrFromString(path) +- if err != nil { +- return err +- } +- short := make([]uint16, buflen) +- n, err := syscall.GetShortPathName(namep, &short[0], uint32(len(short)*2)) // buflen is in bytes. +- if err != nil { +- return err +- } +- if int(n) > len(short)*2 { +- return fmt.Errorf("short buffer too short: %v vs %v*2", n, len(short)) +- } +- long := make([]uint16, buflen) +- n, err = syscall.GetLongPathName(&short[0], &long[0], uint32(len(long)*2)) +- if err != nil { +- return err +- } +- if int(n) > len(long)*2 { +- return fmt.Errorf("long buffer too short: %v vs %v*2", n, len(long)) - } +- longstr := syscall.UTF16ToString(long) - -- return false +- isRoot := func(p string) bool { +- return p[len(p)-1] == filepath.Separator +- } +- for got, want := path, longstr; !isRoot(got) && !isRoot(want); got, want = filepath.Dir(got), filepath.Dir(want) { +- if g, w := filepath.Base(got), filepath.Base(want); g != w { +- return fmt.Errorf("invalid path %q: component %q is listed by Windows as %q", path, g, w) +- } +- } +- return nil -} -diff -urN a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go ---- a/gopls/internal/lsp/cache/pkg.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/pkg.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,177 +0,0 @@ +diff -urN a/gopls/internal/cache/package.go b/gopls/internal/cache/package.go +--- a/gopls/internal/cache/package.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/package.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,182 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. @@ -22614,7 +22659,6 @@ diff -urN a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go -package cache - -import ( -- "context" - "fmt" - "go/ast" - "go/scanner" @@ -22622,18 +22666,19 @@ diff -urN a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go - "go/types" - "sync" - -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/lsp/source/methodsets" -- "golang.org/x/tools/gopls/internal/lsp/source/xrefs" -- "golang.org/x/tools/gopls/internal/span" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/cache/methodsets" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/cache/xrefs" +- "golang.org/x/tools/gopls/internal/protocol" -) - --// Convenient local aliases for typed strings. +-// Convenient aliases for very heavily used types. -type ( -- PackageID = source.PackageID -- PackagePath = source.PackagePath -- PackageName = source.PackageName -- ImportPath = source.ImportPath +- PackageID = metadata.PackageID +- PackagePath = metadata.PackagePath +- PackageName = metadata.PackageName +- ImportPath = metadata.ImportPath -) - -// A Package is the union of package metadata and type checking results. @@ -22642,8 +22687,9 @@ diff -urN a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go -// loadDiagnostics, because the value of the snapshot.packages map is just the -// package handle. Fix this. -type Package struct { -- m *source.Metadata -- pkg *syntaxPackage +- metadata *metadata.Package +- loadDiagnostics []*Diagnostic +- pkg *syntaxPackage -} - -// syntaxPackage contains parse trees and type information for a package. @@ -22653,15 +22699,15 @@ diff -urN a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go - - // -- outputs -- - fset *token.FileSet // for now, same as the snapshot's FileSet -- goFiles []*source.ParsedGoFile -- compiledGoFiles []*source.ParsedGoFile -- diagnostics []*source.Diagnostic +- goFiles []*parsego.File +- compiledGoFiles []*parsego.File +- diagnostics []*Diagnostic - parseErrors []scanner.ErrorList - typeErrors []types.Error - types *types.Package - typesInfo *types.Info +- typesSizes types.Sizes - importMap map[PackagePath]*types.Package -- hasFixedFiles bool // if true, AST was sufficiently mangled that we should hide type errors - - xrefsOnce sync.Once - _xrefs []byte // only used by the xrefs method @@ -22684,9 +22730,9 @@ diff -urN a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go - return p._methodsets -} - --func (p *Package) String() string { return string(p.m.ID) } +-func (p *Package) String() string { return string(p.metadata.ID) } - --func (p *Package) Metadata() *source.Metadata { return p.m } +-func (p *Package) Metadata() *metadata.Package { return p.metadata } - -// A loadScope defines a package loading scope for use with go/packages. -// @@ -22695,14 +22741,15 @@ diff -urN a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go - aScope() -} - +-// TODO(rfindley): move to load.go -type ( -- fileLoadScope span.URI // load packages containing a file (including command-line-arguments) -- packageLoadScope string // load a specific package (the value is its PackageID) +- fileLoadScope protocol.DocumentURI // load packages containing a file (including command-line-arguments) +- packageLoadScope string // load a specific package (the value is its PackageID) - moduleLoadScope struct { - dir string // dir containing the go.mod file - modulePath string // parsed module path - } -- viewLoadScope span.URI // load the workspace +- viewLoadScope struct{} // load the workspace -) - -// Implement the loadScope interface. @@ -22711,15 +22758,15 @@ diff -urN a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go -func (moduleLoadScope) aScope() {} -func (viewLoadScope) aScope() {} - --func (p *Package) CompiledGoFiles() []*source.ParsedGoFile { +-func (p *Package) CompiledGoFiles() []*parsego.File { - return p.pkg.compiledGoFiles -} - --func (p *Package) File(uri span.URI) (*source.ParsedGoFile, error) { +-func (p *Package) File(uri protocol.DocumentURI) (*parsego.File, error) { - return p.pkg.File(uri) -} - --func (pkg *syntaxPackage) File(uri span.URI) (*source.ParsedGoFile, error) { +-func (pkg *syntaxPackage) File(uri protocol.DocumentURI) (*parsego.File, error) { - for _, cgf := range pkg.compiledGoFiles { - if cgf.URI == uri { - return cgf, nil @@ -22733,7 +22780,8 @@ diff -urN a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go - return nil, fmt.Errorf("no parsed file for %s in %v", uri, pkg.id) -} - --func (p *Package) GetSyntax() []*ast.File { +-// Syntax returns parsed compiled Go files contained in this package. +-func (p *Package) Syntax() []*ast.File { - var syntax []*ast.File - for _, pgf := range p.pkg.compiledGoFiles { - syntax = append(syntax, pgf.File) @@ -22741,10693 +22789,11788 @@ diff -urN a/gopls/internal/lsp/cache/pkg.go b/gopls/internal/lsp/cache/pkg.go - return syntax -} - +-// FileSet returns the FileSet describing this package's positions. +-// +-// The returned FileSet is guaranteed to describe all Syntax, but may also +-// describe additional files. -func (p *Package) FileSet() *token.FileSet { - return p.pkg.fset -} - --func (p *Package) GetTypes() *types.Package { +-// Types returns the type checked go/types.Package. +-func (p *Package) Types() *types.Package { - return p.pkg.types -} - --func (p *Package) GetTypesInfo() *types.Info { +-// TypesInfo returns the go/types.Info annotating the Syntax of this package +-// with type information. +-// +-// All fields in the resulting Info are populated. +-func (p *Package) TypesInfo() *types.Info { - return p.pkg.typesInfo -} - +-// TypesSizes returns the sizing function used for types in this package. +-func (p *Package) TypesSizes() types.Sizes { +- return p.pkg.typesSizes +-} +- -// DependencyTypes returns the type checker's symbol for the specified -// package. It returns nil if path is not among the transitive -// dependencies of p, or if no symbols from that package were -// referenced during the type-checking of p. --func (p *Package) DependencyTypes(path source.PackagePath) *types.Package { +-func (p *Package) DependencyTypes(path PackagePath) *types.Package { - return p.pkg.importMap[path] -} - --func (p *Package) GetParseErrors() []scanner.ErrorList { +-// ParseErrors returns a slice containing all non-empty parse errors produces +-// while parsing p.Syntax, or nil if the package contains no parse errors. +-func (p *Package) ParseErrors() []scanner.ErrorList { - return p.pkg.parseErrors -} - --func (p *Package) GetTypeErrors() []types.Error { +-// TypeErrors returns the go/types.Errors produced during type checking this +-// package, if any. +-func (p *Package) TypeErrors() []types.Error { - return p.pkg.typeErrors -} -- --func (p *Package) DiagnosticsForFile(ctx context.Context, s source.Snapshot, uri span.URI) ([]*source.Diagnostic, error) { -- var diags []*source.Diagnostic -- for _, diag := range p.m.Diagnostics { -- if diag.URI == uri { -- diags = append(diags, diag) -- } -- } -- for _, diag := range p.pkg.diagnostics { -- if diag.URI == uri { -- diags = append(diags, diag) -- } -- } -- -- return diags, nil --} -diff -urN a/gopls/internal/lsp/cache/session.go b/gopls/internal/lsp/cache/session.go ---- a/gopls/internal/lsp/cache/session.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/session.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,704 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/cache/parse_cache.go b/gopls/internal/cache/parse_cache.go +--- a/gopls/internal/cache/parse_cache.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/parse_cache.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,419 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cache - -import ( +- "bytes" +- "container/heap" - "context" - "fmt" -- "strconv" -- "strings" +- "go/parser" +- "go/token" +- "math/bits" +- "runtime" - "sync" -- "sync/atomic" +- "time" - -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/lsp/source/typerefs" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/gopls/internal/vulncheck" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/gocommand" -- "golang.org/x/tools/internal/imports" +- "golang.org/x/sync/errgroup" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/internal/memoize" -- "golang.org/x/tools/internal/persistent" -- "golang.org/x/tools/internal/xcontext" +- "golang.org/x/tools/internal/tokeninternal" -) - --type Session struct { -- // Unique identifier for this session. -- id string -- -- // Immutable attributes shared across views. -- cache *Cache // shared cache -- gocmdRunner *gocommand.Runner // limits go command concurrency -- -- viewMu sync.Mutex -- views []*View -- viewMap map[span.URI]*View // file->best view -- -- parseCache *parseCache -- -- *overlayFS --} -- --// ID returns the unique identifier for this session on this server. --func (s *Session) ID() string { return s.id } --func (s *Session) String() string { return s.id } -- --// GoCommandRunner returns the gocommand Runner for this session. --func (s *Session) GoCommandRunner() *gocommand.Runner { -- return s.gocmdRunner --} -- --// Shutdown the session and all views it has created. --func (s *Session) Shutdown(ctx context.Context) { -- var views []*View -- s.viewMu.Lock() -- views = append(views, s.views...) -- s.views = nil -- s.viewMap = nil -- s.viewMu.Unlock() -- for _, view := range views { -- view.shutdown() -- } -- s.parseCache.stop() -- event.Log(ctx, "Shutdown session", KeyShutdownSession.Of(s)) --} +-// This file contains an implementation of an LRU parse cache, that offsets the +-// base token.Pos value of each cached file so that they may be later described +-// by a single dedicated FileSet. +-// +-// This is achieved by tracking a monotonic offset in the token.Pos space, that +-// is incremented before parsing allow room for the resulting parsed file. - --// Cache returns the cache that created this session, for debugging only. --func (s *Session) Cache() *Cache { -- return s.cache --} +-// reservedForParsing defines the room in the token.Pos space reserved for +-// cached parsed files. +-// +-// Files parsed through the parseCache are guaranteed not to have overlapping +-// spans: the parseCache tracks a monotonic base for newly parsed files. +-// +-// By offsetting the initial base of a FileSet, we can allow other operations +-// accepting the FileSet (such as the gcimporter) to add new files using the +-// normal FileSet APIs without overlapping with cached parsed files. +-// +-// Note that 1<<60 represents an exabyte of parsed data, more than any gopls +-// process can ever parse. +-// +-// On 32-bit systems we don't cache parse results (see parseFiles). +-const reservedForParsing = 1 << (bits.UintSize - 4) - --// NewView creates a new View, returning it and its first snapshot. If a --// non-empty tempWorkspace directory is provided, the View will record a copy --// of its gopls workspace module in that directory, so that client tooling --// can execute in the same main module. On success it also returns a release --// function that must be called when the Snapshot is no longer needed. --func (s *Session) NewView(ctx context.Context, folder *Folder) (*View, source.Snapshot, func(), error) { -- s.viewMu.Lock() -- defer s.viewMu.Unlock() -- for _, view := range s.views { -- if span.SameExistingFile(view.folder.Dir, folder.Dir) { -- return nil, nil, nil, source.ErrViewExists -- } -- } -- info, err := getWorkspaceInformation(ctx, s.gocmdRunner, s, folder) -- if err != nil { -- return nil, nil, nil, err +-// fileSetWithBase returns a new token.FileSet with Base() equal to the +-// requested base. +-// +-// If base < 1, fileSetWithBase panics. +-// (1 is the smallest permitted FileSet base). +-func fileSetWithBase(base int) *token.FileSet { +- fset := token.NewFileSet() +- if base > 1 { +- // Add a dummy file to set the base of fset. We won't ever use the +- // resulting FileSet, so it doesn't matter how we achieve this. +- // +- // FileSets leave a 1-byte padding between files, so we set the base by +- // adding a zero-length file at base-1. +- fset.AddFile("", base-1, 0) - } -- view, snapshot, release, err := s.createView(ctx, info, folder, 0) -- if err != nil { -- return nil, nil, nil, err +- if fset.Base() != base { +- panic("unexpected FileSet.Base") - } -- s.views = append(s.views, view) -- // we always need to drop the view map -- s.viewMap = make(map[span.URI]*View) -- return view, snapshot, release, nil +- return fset -} - --// TODO(rfindley): clarify that createView can never be cancelled (with the --// possible exception of server shutdown). --// On success, the caller becomes responsible for calling the release function once. --func (s *Session) createView(ctx context.Context, info *workspaceInformation, folder *Folder, seqID uint64) (*View, *snapshot, func(), error) { -- index := atomic.AddInt64(&viewIndex, 1) -- -- gowork, _ := info.GOWORK() -- wsModFiles, wsModFilesErr := computeWorkspaceModFiles(ctx, info.gomod, gowork, info.effectiveGO111MODULE(), s) -- -- // We want a true background context and not a detached context here -- // the spans need to be unrelated and no tag values should pollute it. -- baseCtx := event.Detach(xcontext.Detach(ctx)) -- backgroundCtx, cancel := context.WithCancel(baseCtx) -- -- v := &View{ -- id: strconv.FormatInt(index, 10), -- gocmdRunner: s.gocmdRunner, -- folder: folder, -- initialWorkspaceLoad: make(chan struct{}), -- initializationSema: make(chan struct{}, 1), -- moduleUpgrades: map[span.URI]map[string]string{}, -- vulns: map[span.URI]*vulncheck.Result{}, -- parseCache: s.parseCache, -- fs: s.overlayFS, -- workspaceInformation: info, -- } -- v.importsState = &importsState{ -- ctx: baseCtx, -- processEnv: &imports.ProcessEnv{ -- GocmdRunner: s.gocmdRunner, -- SkipPathInScan: func(dir string) bool { -- prefix := strings.TrimSuffix(string(v.folder.Dir), "/") + "/" -- uri := strings.TrimSuffix(string(span.URIFromPath(dir)), "/") -- if !strings.HasPrefix(uri+"/", prefix) { -- return false -- } -- filterer := source.NewFilterer(folder.Options.DirectoryFilters) -- rel := strings.TrimPrefix(uri, prefix) -- disallow := filterer.Disallow(rel) -- return disallow -- }, -- }, -- } -- v.snapshot = &snapshot{ -- sequenceID: seqID, -- globalID: nextSnapshotID(), -- view: v, -- backgroundCtx: backgroundCtx, -- cancel: cancel, -- store: s.cache.store, -- packages: new(persistent.Map[PackageID, *packageHandle]), -- meta: new(metadataGraph), -- files: newFileMap(), -- activePackages: new(persistent.Map[PackageID, *Package]), -- symbolizeHandles: new(persistent.Map[span.URI, *memoize.Promise]), -- workspacePackages: make(map[PackageID]PackagePath), -- unloadableFiles: new(persistent.Set[span.URI]), -- parseModHandles: new(persistent.Map[span.URI, *memoize.Promise]), -- parseWorkHandles: new(persistent.Map[span.URI, *memoize.Promise]), -- modTidyHandles: new(persistent.Map[span.URI, *memoize.Promise]), -- modVulnHandles: new(persistent.Map[span.URI, *memoize.Promise]), -- modWhyHandles: new(persistent.Map[span.URI, *memoize.Promise]), -- workspaceModFiles: wsModFiles, -- workspaceModFilesErr: wsModFilesErr, -- pkgIndex: typerefs.NewPackageIndex(), -- } -- // Save one reference in the view. -- v.releaseSnapshot = v.snapshot.Acquire() -- -- // Record the environment of the newly created view in the log. -- event.Log(ctx, viewEnv(v)) +-const ( +- // Always keep 100 recent files, independent of their wall-clock age, to +- // optimize the case where the user resumes editing after a delay. +- parseCacheMinFiles = 100 +-) - -- // Initialize the view without blocking. -- initCtx, initCancel := context.WithCancel(xcontext.Detach(ctx)) -- v.initCancelFirstAttempt = initCancel -- snapshot := v.snapshot +-// parsePadding is additional padding allocated to allow for increases in +-// length (such as appending missing braces) caused by fixAST. +-// +-// This is used to mitigate a chicken and egg problem: we must know the base +-// offset of the file we're about to parse, before we start parsing, and yet +-// src fixups may affect the actual size of the parsed content (and therefore +-// the offsets of subsequent files). +-// +-// When we encounter a file that no longer fits in its allocated space in the +-// fileset, we have no choice but to re-parse it. Leaving a generous padding +-// reduces the likelihood of this "slow path". +-// +-// This value is mutable for testing, so that we can exercise the slow path. +-var parsePadding = 1000 // mutable for testing - -- // Pass a second reference to the background goroutine. -- bgRelease := snapshot.Acquire() -- go func() { -- defer bgRelease() -- snapshot.initialize(initCtx, true) -- }() +-// A parseCache holds recently accessed parsed Go files. After new files are +-// stored, older files may be evicted from the cache via garbage collection. +-// +-// The parseCache.parseFiles method exposes a batch API for parsing (and +-// caching) multiple files. This is necessary for type-checking, where files +-// must be parsed in a common fileset. +-type parseCache struct { +- expireAfter time.Duration // interval at which to collect expired cache entries +- done chan struct{} // closed when GC is stopped - -- // Return a third reference to the caller. -- return v, snapshot, snapshot.Acquire(), nil +- mu sync.Mutex +- m map[parseKey]*parseCacheEntry +- lru queue // min-atime priority queue of *parseCacheEntry +- clock uint64 // clock time, incremented when the cache is updated +- nextBase int // base offset for the next parsed file -} - --// ViewByName returns a view with a matching name, if the session has one. --func (s *Session) ViewByName(name string) *View { -- s.viewMu.Lock() -- defer s.viewMu.Unlock() -- for _, view := range s.views { -- if view.Name() == name { -- return view -- } +-// newParseCache creates a new parse cache and starts a goroutine to garbage +-// collect entries whose age is at least expireAfter. +-// +-// Callers must call parseCache.stop when the parse cache is no longer in use. +-func newParseCache(expireAfter time.Duration) *parseCache { +- c := &parseCache{ +- expireAfter: expireAfter, +- m: make(map[parseKey]*parseCacheEntry), +- done: make(chan struct{}), - } -- return nil +- go c.gc() +- return c -} - --// View returns the view with a matching id, if present. --func (s *Session) View(id string) (*View, error) { -- s.viewMu.Lock() -- defer s.viewMu.Unlock() -- for _, view := range s.views { -- if view.ID() == id { -- return view, nil -- } -- } -- return nil, fmt.Errorf("no view with ID %q", id) +-// stop causes the GC goroutine to exit. +-func (c *parseCache) stop() { +- close(c.done) -} - --// ViewOf returns a view corresponding to the given URI. --// If the file is not already associated with a view, pick one using some heuristics. --func (s *Session) ViewOf(uri span.URI) (*View, error) { -- s.viewMu.Lock() -- defer s.viewMu.Unlock() -- return s.viewOfLocked(uri) +-// parseKey uniquely identifies a parsed Go file. +-type parseKey struct { +- uri protocol.DocumentURI +- mode parser.Mode +- purgeFuncBodies bool -} - --// Precondition: caller holds s.viewMu lock. --func (s *Session) viewOfLocked(uri span.URI) (*View, error) { -- // Check if we already know this file. -- if v, found := s.viewMap[uri]; found { -- return v, nil -- } -- // Pick the best view for this file and memoize the result. -- if len(s.views) == 0 { -- return nil, fmt.Errorf("no views in session") -- } -- s.viewMap[uri] = bestViewForURI(uri, s.views) -- return s.viewMap[uri], nil +-type parseCacheEntry struct { +- key parseKey +- hash file.Hash +- promise *memoize.Promise // memoize.Promise[*parsego.File] +- atime uint64 // clock time of last access, for use in LRU sorting +- walltime time.Time // actual time of last access, for use in time-based eviction; too coarse for LRU on some systems +- lruIndex int // owned by the queue implementation -} - --func (s *Session) Views() []*View { -- s.viewMu.Lock() -- defer s.viewMu.Unlock() -- result := make([]*View, len(s.views)) -- copy(result, s.views) -- return result --} +-// startParse prepares a parsing pass, creating new promises in the cache for +-// any cache misses. +-// +-// The resulting slice has an entry for every given file handle, though some +-// entries may be nil if there was an error reading the file (in which case the +-// resulting error will be non-nil). +-func (c *parseCache) startParse(mode parser.Mode, purgeFuncBodies bool, fhs ...file.Handle) ([]*memoize.Promise, error) { +- c.mu.Lock() +- defer c.mu.Unlock() - --// bestViewForURI returns the most closely matching view for the given URI --// out of the given set of views. --func bestViewForURI(uri span.URI, views []*View) *View { -- // we need to find the best view for this file -- var longest *View -- for _, view := range views { -- if longest != nil && len(longest.Folder()) > len(view.Folder()) { +- // Any parsing pass increments the clock, as we'll update access times. +- // (technically, if fhs is empty this isn't necessary, but that's a degenerate case). +- // +- // All entries parsed from a single call get the same access time. +- c.clock++ +- walltime := time.Now() +- +- // Read file data and collect cacheable files. +- var ( +- data = make([][]byte, len(fhs)) // file content for each readable file +- promises = make([]*memoize.Promise, len(fhs)) +- firstReadError error // first error from fh.Read, or nil +- ) +- for i, fh := range fhs { +- content, err := fh.Content() +- if err != nil { +- if firstReadError == nil { +- firstReadError = err +- } - continue - } -- // TODO(rfindley): this should consider the workspace layout (i.e. -- // go.work). -- if view.contains(uri) { -- longest = view -- } -- } -- if longest != nil { -- return longest -- } -- // Try our best to return a view that knows the file. -- for _, view := range views { -- if view.knownFile(uri) { -- return view +- data[i] = content +- +- key := parseKey{ +- uri: fh.URI(), +- mode: mode, +- purgeFuncBodies: purgeFuncBodies, - } -- } -- // TODO: are there any more heuristics we can use? -- return views[0] --} - --// RemoveView removes the view v from the session --func (s *Session) RemoveView(view *View) { -- s.viewMu.Lock() -- defer s.viewMu.Unlock() +- if e, ok := c.m[key]; ok { +- if e.hash == fh.Identity().Hash { // cache hit +- e.atime = c.clock +- e.walltime = walltime +- heap.Fix(&c.lru, e.lruIndex) +- promises[i] = e.promise +- continue +- } else { +- // A cache hit, for a different version. Delete it. +- delete(c.m, e.key) +- heap.Remove(&c.lru, e.lruIndex) +- } +- } - -- i := s.dropView(view) -- if i == -1 { // error reported elsewhere -- return -- } -- // delete this view... we don't care about order but we do want to make -- // sure we can garbage collect the view -- s.views = removeElement(s.views, i) --} +- uri := fh.URI() +- promise := memoize.NewPromise("parseCache.parse", func(ctx context.Context, _ interface{}) interface{} { +- // Allocate 2*len(content)+parsePadding to allow for re-parsing once +- // inside of parseGoSrc without exceeding the allocated space. +- base, nextBase := c.allocateSpace(2*len(content) + parsePadding) - --// updateViewLocked recreates the view with the given options. --// --// If the resulting error is non-nil, the view may or may not have already been --// dropped from the session. --func (s *Session) updateViewLocked(ctx context.Context, view *View, info *workspaceInformation, folder *Folder) (*View, error) { -- // Preserve the snapshot ID if we are recreating the view. -- view.snapshotMu.Lock() -- if view.snapshot == nil { -- view.snapshotMu.Unlock() -- panic("updateView called after View was already shut down") -- } -- // TODO(rfindley): we should probably increment the sequence ID here. -- seqID := view.snapshot.sequenceID // Preserve sequence IDs when updating a view in place. -- view.snapshotMu.Unlock() +- pgf, fixes1 := parsego.Parse(ctx, fileSetWithBase(base), uri, content, mode, purgeFuncBodies) +- file := pgf.Tok +- if file.Base()+file.Size()+1 > nextBase { +- // The parsed file exceeds its allocated space, likely due to multiple +- // passes of src fixing. In this case, we have no choice but to re-do +- // the operation with the correct size. +- // +- // Even though the final successful parse requires only file.Size() +- // bytes of Pos space, we need to accommodate all the missteps to get +- // there, as parseGoSrc will repeat them. +- actual := file.Base() + file.Size() - base // actual size consumed, after re-parsing +- base2, nextBase2 := c.allocateSpace(actual) +- pgf2, fixes2 := parsego.Parse(ctx, fileSetWithBase(base2), uri, content, mode, purgeFuncBodies) - -- i := s.dropView(view) -- if i == -1 { -- return nil, fmt.Errorf("view %q not found", view.id) -- } +- // In golang/go#59097 we observed that this panic condition was hit. +- // One bug was found and fixed, but record more information here in +- // case there is still a bug here. +- if end := pgf2.Tok.Base() + pgf2.Tok.Size(); end != nextBase2-1 { +- var errBuf bytes.Buffer +- fmt.Fprintf(&errBuf, "internal error: non-deterministic parsing result:\n") +- fmt.Fprintf(&errBuf, "\t%q (%d-%d) does not span %d-%d\n", uri, pgf2.Tok.Base(), base2, end, nextBase2-1) +- fmt.Fprintf(&errBuf, "\tfirst %q (%d-%d)\n", pgf.URI, pgf.Tok.Base(), pgf.Tok.Base()+pgf.Tok.Size()) +- fmt.Fprintf(&errBuf, "\tfirst space: (%d-%d), second space: (%d-%d)\n", base, nextBase, base2, nextBase2) +- fmt.Fprintf(&errBuf, "\tfirst mode: %v, second mode: %v", pgf.Mode, pgf2.Mode) +- fmt.Fprintf(&errBuf, "\tfirst err: %v, second err: %v", pgf.ParseErr, pgf2.ParseErr) +- fmt.Fprintf(&errBuf, "\tfirst fixes: %v, second fixes: %v", fixes1, fixes2) +- panic(errBuf.String()) +- } +- pgf = pgf2 +- } +- return pgf +- }) +- promises[i] = promise - -- var ( -- snapshot *snapshot -- release func() -- err error -- ) -- view, snapshot, release, err = s.createView(ctx, info, folder, seqID) -- if err != nil { -- // we have dropped the old view, but could not create the new one -- // this should not happen and is very bad, but we still need to clean -- // up the view array if it happens -- s.views = removeElement(s.views, i) -- return nil, err +- // add new entry; entries are gc'ed asynchronously +- e := &parseCacheEntry{ +- key: key, +- hash: fh.Identity().Hash, +- promise: promise, +- atime: c.clock, +- walltime: walltime, +- } +- c.m[e.key] = e +- heap.Push(&c.lru, e) - } -- defer release() - -- // The new snapshot has lost the history of the previous view. As a result, -- // it may not see open files that aren't in its build configuration (as it -- // would have done via didOpen notifications). This can lead to inconsistent -- // behavior when configuration is changed mid-session. -- // -- // Ensure the new snapshot observes all open files. -- for _, o := range view.fs.Overlays() { -- _, _ = snapshot.ReadFile(ctx, o.URI()) +- if len(c.m) != len(c.lru) { +- panic("map and LRU are inconsistent") - } - -- // substitute the new view into the array where the old view was -- s.views[i] = view -- return view, nil +- return promises, firstReadError -} - --// removeElement removes the ith element from the slice replacing it with the last element. --// TODO(adonovan): generics, someday. --func removeElement(slice []*View, index int) []*View { -- last := len(slice) - 1 -- slice[index] = slice[last] -- slice[last] = nil // aid GC -- return slice[:last] --} +-func (c *parseCache) gc() { +- const period = 10 * time.Second // gc period +- timer := time.NewTicker(period) +- defer timer.Stop() - --// dropView removes v from the set of views for the receiver s and calls --// v.shutdown, returning the index of v in s.views (if found), or -1 if v was --// not found. s.viewMu must be held while calling this function. --func (s *Session) dropView(v *View) int { -- // we always need to drop the view map -- s.viewMap = make(map[span.URI]*View) -- for i := range s.views { -- if v == s.views[i] { -- // we found the view, drop it and return the index it was found at -- s.views[i] = nil -- v.shutdown() -- return i +- for { +- select { +- case <-c.done: +- return +- case <-timer.C: - } +- +- c.gcOnce() - } -- // TODO(rfindley): it looks wrong that we don't shutdown v in this codepath. -- // We should never get here. -- bug.Reportf("tried to drop nonexistent view %q", v.id) -- return -1 -} - --func (s *Session) ModifyFiles(ctx context.Context, changes []source.FileModification) error { -- _, release, err := s.DidModifyFiles(ctx, changes) -- release() -- return err --} +-func (c *parseCache) gcOnce() { +- now := time.Now() +- c.mu.Lock() +- defer c.mu.Unlock() - --// ResetView resets the best view for the given URI. --func (s *Session) ResetView(ctx context.Context, uri span.URI) (*View, error) { -- s.viewMu.Lock() -- defer s.viewMu.Unlock() -- v := bestViewForURI(uri, s.views) -- return s.updateViewLocked(ctx, v, v.workspaceInformation, v.folder) +- for len(c.m) > parseCacheMinFiles { +- e := heap.Pop(&c.lru).(*parseCacheEntry) +- if now.Sub(e.walltime) >= c.expireAfter { +- delete(c.m, e.key) +- } else { +- heap.Push(&c.lru, e) +- break +- } +- } -} - --// DidModifyFiles reports a file modification to the session. It returns --// the new snapshots after the modifications have been applied, paired with --// the affected file URIs for those snapshots. --// On success, it returns a release function that --// must be called when the snapshots are no longer needed. +-// allocateSpace reserves the next n bytes of token.Pos space in the +-// cache. -// --// TODO(rfindley): what happens if this function fails? It must leave us in a --// broken state, which we should surface to the user, probably as a request to --// restart gopls. --func (s *Session) DidModifyFiles(ctx context.Context, changes []source.FileModification) (map[source.Snapshot][]span.URI, func(), error) { -- s.viewMu.Lock() -- defer s.viewMu.Unlock() +-// It returns the resulting file base, next base, and an offset FileSet to use +-// for parsing. +-func (c *parseCache) allocateSpace(size int) (int, int) { +- c.mu.Lock() +- defer c.mu.Unlock() - -- // Update overlays. -- // -- // TODO(rfindley): I think we do this while holding viewMu to prevent views -- // from seeing the updated file content before they have processed -- // invalidations, which could lead to a partial view of the changes (i.e. -- // spurious diagnostics). However, any such view would immediately be -- // invalidated here, so it is possible that we could update overlays before -- // acquiring viewMu. -- if err := s.updateOverlays(ctx, changes); err != nil { -- return nil, nil, err +- if c.nextBase == 0 { +- // FileSet base values must be at least 1. +- c.nextBase = 1 - } +- base := c.nextBase +- c.nextBase += size + 1 +- return base, c.nextBase +-} - -- // Re-create views whose definition may have changed. -- // -- // checkViews controls whether to re-evaluate view definitions when -- // collecting views below. Any addition or deletion of a go.mod or go.work -- // file may have affected the definition of the view. -- checkViews := false -- -- for _, c := range changes { -- // TODO(rfindley): go.work files need not be named "go.work" -- we need to -- // check each view's source. -- if isGoMod(c.URI) || isGoWork(c.URI) { -- // Change, InvalidateMetadata, and UnknownFileAction actions do not cause -- // us to re-evaluate views. -- redoViews := (c.Action != source.Change && -- c.Action != source.UnknownFileAction) -- -- if redoViews { -- checkViews = true -- break -- } -- } -- } +-// parseFiles returns a parsego.File for each file handle in fhs, in the +-// requested parse mode. +-// +-// For parsed files that already exists in the cache, access time will be +-// updated. For others, parseFiles will parse and store as many results in the +-// cache as space allows. +-// +-// The token.File for each resulting parsed file will be added to the provided +-// FileSet, using the tokeninternal.AddExistingFiles API. Consequently, the +-// given fset should only be used in other APIs if its base is >= +-// reservedForParsing. +-// +-// If parseFiles returns an error, it still returns a slice, +-// but with a nil entry for each file that could not be parsed. +-func (c *parseCache) parseFiles(ctx context.Context, fset *token.FileSet, mode parser.Mode, purgeFuncBodies bool, fhs ...file.Handle) ([]*parsego.File, error) { +- pgfs := make([]*parsego.File, len(fhs)) - -- if checkViews { -- for _, view := range s.views { -- // TODO(rfindley): can we avoid running the go command (go env) -- // synchronously to change processing? Can we assume that the env did not -- // change, and derive go.work using a combination of the configured -- // GOWORK value and filesystem? -- info, err := getWorkspaceInformation(ctx, s.gocmdRunner, s, view.folder) +- // Temporary fall-back for 32-bit systems, where reservedForParsing is too +- // small to be viable. We don't actually support 32-bit systems, so this +- // workaround is only for tests and can be removed when we stop running +- // 32-bit TryBots for gopls. +- if bits.UintSize == 32 { +- for i, fh := range fhs { +- var err error +- pgfs[i], err = parseGoImpl(ctx, fset, fh, mode, purgeFuncBodies) - if err != nil { -- // Catastrophic failure, equivalent to a failure of session -- // initialization and therefore should almost never happen. One -- // scenario where this failure mode could occur is if some file -- // permissions have changed preventing us from reading go.mod -- // files. -- // -- // TODO(rfindley): consider surfacing this error more loudly. We -- // could report a bug, but it's not really a bug. -- event.Error(ctx, "fetching workspace information", err) -- } else if *info != *view.workspaceInformation { -- if _, err := s.updateViewLocked(ctx, view, info, view.folder); err != nil { -- // More catastrophic failure. The view may or may not still exist. -- // The best we can do is log and move on. -- event.Error(ctx, "recreating view", err) -- } +- return pgfs, err - } - } +- return pgfs, nil - } - -- // Collect information about views affected by these changes. -- views := make(map[*View]map[span.URI]source.FileHandle) -- affectedViews := map[span.URI][]*View{} -- for _, c := range changes { -- // Build the list of affected views. -- var changedViews []*View -- for _, view := range s.views { -- // Don't propagate changes that are outside of the view's scope -- // or knowledge. -- if !view.relevantChange(c) { -- continue -- } -- changedViews = append(changedViews, view) +- promises, firstErr := c.startParse(mode, purgeFuncBodies, fhs...) +- +- // Await all parsing. +- var g errgroup.Group +- g.SetLimit(runtime.GOMAXPROCS(-1)) // parsing is CPU-bound. +- for i, promise := range promises { +- if promise == nil { +- continue - } -- // If the change is not relevant to any view, but the change is -- // happening in the editor, assign it the most closely matching view. -- if len(changedViews) == 0 { -- if c.OnDisk { -- continue -- } -- bestView, err := s.viewOfLocked(c.URI) +- i := i +- promise := promise +- g.Go(func() error { +- result, err := promise.Get(ctx, nil) - if err != nil { -- return nil, nil, err -- } -- changedViews = append(changedViews, bestView) -- } -- affectedViews[c.URI] = changedViews -- -- // Apply the changes to all affected views. -- fh := mustReadFile(ctx, s, c.URI) -- for _, view := range changedViews { -- // Make sure that the file is added to the view's seenFiles set. -- view.markKnown(c.URI) -- if _, ok := views[view]; !ok { -- views[view] = make(map[span.URI]source.FileHandle) +- return err - } -- views[view][c.URI] = fh -- } -- } -- -- var releases []func() -- viewToSnapshot := map[*View]*snapshot{} -- for view, changed := range views { -- snapshot, release := view.invalidateContent(ctx, changed) -- releases = append(releases, release) -- viewToSnapshot[view] = snapshot +- pgfs[i] = result.(*parsego.File) +- return nil +- }) - } - -- // The release function is called when the -- // returned URIs no longer need to be valid. -- release := func() { -- for _, release := range releases { -- release() -- } +- if err := g.Wait(); err != nil && firstErr == nil { +- firstErr = err - } - -- // We only want to diagnose each changed file once, in the view to which -- // it "most" belongs. We do this by picking the best view for each URI, -- // and then aggregating the set of snapshots and their URIs (to avoid -- // diagnosing the same snapshot multiple times). -- snapshotURIs := map[source.Snapshot][]span.URI{} -- for _, mod := range changes { -- viewSlice, ok := affectedViews[mod.URI] -- if !ok || len(viewSlice) == 0 { +- // Augment the FileSet to map all parsed files. +- var tokenFiles []*token.File +- for _, pgf := range pgfs { +- if pgf == nil { - continue - } -- view := bestViewForURI(mod.URI, viewSlice) -- snapshot, ok := viewToSnapshot[view] -- if !ok { -- panic(fmt.Sprintf("no snapshot for view %s", view.Folder())) -- } -- snapshotURIs[snapshot] = append(snapshotURIs[snapshot], mod.URI) -- } -- -- return snapshotURIs, release, nil --} -- --// ExpandModificationsToDirectories returns the set of changes with the --// directory changes removed and expanded to include all of the files in --// the directory. --func (s *Session) ExpandModificationsToDirectories(ctx context.Context, changes []source.FileModification) []source.FileModification { -- var snapshots []*snapshot -- s.viewMu.Lock() -- for _, v := range s.views { -- snapshot, release, err := v.getSnapshot() -- if err != nil { -- continue // view is shut down; continue with others -- } -- defer release() -- snapshots = append(snapshots, snapshot) +- tokenFiles = append(tokenFiles, pgf.Tok) - } -- s.viewMu.Unlock() +- tokeninternal.AddExistingFiles(fset, tokenFiles) - -- // Expand the modification to any file we could care about, which we define -- // to be any file observed by any of the snapshots. -- // -- // There may be other files in the directory, but if we haven't read them yet -- // we don't need to invalidate them. -- var result []source.FileModification -- for _, c := range changes { -- expanded := make(map[span.URI]bool) -- for _, snapshot := range snapshots { -- for _, uri := range snapshot.filesInDir(c.URI) { -- expanded[uri] = true +- const debugIssue59080 = true +- if debugIssue59080 { +- for _, f := range tokenFiles { +- pos := token.Pos(f.Base()) +- f2 := fset.File(pos) +- if f2 != f { +- panic(fmt.Sprintf("internal error: File(%d (start)) = %v, not %v", pos, f2, f)) - } -- } -- if len(expanded) == 0 { -- result = append(result, c) -- } else { -- for uri := range expanded { -- result = append(result, source.FileModification{ -- URI: uri, -- Action: c.Action, -- LanguageID: "", -- OnDisk: c.OnDisk, -- // changes to directories cannot include text or versions -- }) +- pos = token.Pos(f.Base() + f.Size()) +- f2 = fset.File(pos) +- if f2 != f { +- panic(fmt.Sprintf("internal error: File(%d (end)) = %v, not %v", pos, f2, f)) - } - } - } -- return result --} -- --// Precondition: caller holds s.viewMu lock. --// TODO(rfindley): move this to fs_overlay.go. --func (fs *overlayFS) updateOverlays(ctx context.Context, changes []source.FileModification) error { -- fs.mu.Lock() -- defer fs.mu.Unlock() -- -- for _, c := range changes { -- o, ok := fs.overlays[c.URI] -- -- // If the file is not opened in an overlay and the change is on disk, -- // there's no need to update an overlay. If there is an overlay, we -- // may need to update the overlay's saved value. -- if !ok && c.OnDisk { -- continue -- } -- -- // Determine the file kind on open, otherwise, assume it has been cached. -- var kind source.FileKind -- switch c.Action { -- case source.Open: -- kind = source.FileKindForLang(c.LanguageID) -- default: -- if !ok { -- return fmt.Errorf("updateOverlays: modifying unopened overlay %v", c.URI) -- } -- kind = o.kind -- } - -- // Closing a file just deletes its overlay. -- if c.Action == source.Close { -- delete(fs.overlays, c.URI) -- continue -- } +- return pgfs, firstErr +-} - -- // If the file is on disk, check if its content is the same as in the -- // overlay. Saves and on-disk file changes don't come with the file's -- // content. -- text := c.Text -- if text == nil && (c.Action == source.Save || c.OnDisk) { -- if !ok { -- return fmt.Errorf("no known content for overlay for %s", c.Action) -- } -- text = o.content -- } -- // On-disk changes don't come with versions. -- version := c.Version -- if c.OnDisk || c.Action == source.Save { -- version = o.version -- } -- hash := source.HashOf(text) -- var sameContentOnDisk bool -- switch c.Action { -- case source.Delete: -- // Do nothing. sameContentOnDisk should be false. -- case source.Save: -- // Make sure the version and content (if present) is the same. -- if false && o.version != version { // Client no longer sends the version -- return fmt.Errorf("updateOverlays: saving %s at version %v, currently at %v", c.URI, c.Version, o.version) -- } -- if c.Text != nil && o.hash != hash { -- return fmt.Errorf("updateOverlays: overlay %s changed on save", c.URI) -- } -- sameContentOnDisk = true -- default: -- fh := mustReadFile(ctx, fs.delegate, c.URI) -- _, readErr := fh.Content() -- sameContentOnDisk = (readErr == nil && fh.FileIdentity().Hash == hash) -- } -- o = &Overlay{ -- uri: c.URI, -- version: version, -- content: text, -- kind: kind, -- hash: hash, -- saved: sameContentOnDisk, -- } +-// -- priority queue boilerplate -- - -- // NOTE: previous versions of this code checked here that the overlay had a -- // view and file kind (but we don't know why). +-// queue is a min-atime prority queue of cache entries. +-type queue []*parseCacheEntry - -- fs.overlays[c.URI] = o -- } +-func (q queue) Len() int { return len(q) } - -- return nil --} +-func (q queue) Less(i, j int) bool { return q[i].atime < q[j].atime } - --func mustReadFile(ctx context.Context, fs source.FileSource, uri span.URI) source.FileHandle { -- ctx = xcontext.Detach(ctx) -- fh, err := fs.ReadFile(ctx, uri) -- if err != nil { -- // ReadFile cannot fail with an uncancellable context. -- bug.Reportf("reading file failed unexpectedly: %v", err) -- return brokenFile{uri, err} -- } -- return fh +-func (q queue) Swap(i, j int) { +- q[i], q[j] = q[j], q[i] +- q[i].lruIndex = i +- q[j].lruIndex = j -} - --// A brokenFile represents an unexpected failure to read a file. --type brokenFile struct { -- uri span.URI -- err error +-func (q *queue) Push(x interface{}) { +- e := x.(*parseCacheEntry) +- e.lruIndex = len(*q) +- *q = append(*q, e) -} - --func (b brokenFile) URI() span.URI { return b.uri } --func (b brokenFile) FileIdentity() source.FileIdentity { return source.FileIdentity{URI: b.uri} } --func (b brokenFile) SameContentsOnDisk() bool { return false } --func (b brokenFile) Version() int32 { return 0 } --func (b brokenFile) Content() ([]byte, error) { return nil, b.err } -- --// FileWatchingGlobPatterns returns a new set of glob patterns to --// watch every directory known by the view. For views within a module, --// this is the module root, any directory in the module root, and any --// replace targets. --func (s *Session) FileWatchingGlobPatterns(ctx context.Context) map[string]struct{} { -- s.viewMu.Lock() -- defer s.viewMu.Unlock() -- patterns := map[string]struct{}{} -- for _, view := range s.views { -- snapshot, release, err := view.getSnapshot() -- if err != nil { -- continue // view is shut down; continue with others -- } -- for k, v := range snapshot.fileWatchingGlobPatterns(ctx) { -- patterns[k] = v -- } -- release() -- } -- return patterns +-func (q *queue) Pop() interface{} { +- last := len(*q) - 1 +- e := (*q)[last] +- (*q)[last] = nil // aid GC +- *q = (*q)[:last] +- return e -} -diff -urN a/gopls/internal/lsp/cache/snapshot.go b/gopls/internal/lsp/cache/snapshot.go ---- a/gopls/internal/lsp/cache/snapshot.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/snapshot.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,2464 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/cache/parse_cache_test.go b/gopls/internal/cache/parse_cache_test.go +--- a/gopls/internal/cache/parse_cache_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/parse_cache_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,234 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package cache - -import ( -- "bytes" - "context" -- "errors" - "fmt" -- "go/ast" -- "go/build/constraint" - "go/token" -- "go/types" -- "io" -- "log" -- "os" -- "path/filepath" -- "regexp" -- "runtime" -- "sort" -- "strconv" -- "strings" -- "sync" -- "sync/atomic" -- "unsafe" +- "math/bits" +- "testing" +- "time" - -- "golang.org/x/sync/errgroup" -- "golang.org/x/tools/go/packages" -- "golang.org/x/tools/go/types/objectpath" -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/command" -- "golang.org/x/tools/gopls/internal/lsp/filecache" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/lsp/source/methodsets" -- "golang.org/x/tools/gopls/internal/lsp/source/typerefs" -- "golang.org/x/tools/gopls/internal/lsp/source/xrefs" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/tag" -- "golang.org/x/tools/internal/gocommand" -- "golang.org/x/tools/internal/memoize" -- "golang.org/x/tools/internal/packagesinternal" -- "golang.org/x/tools/internal/persistent" -- "golang.org/x/tools/internal/typesinternal" -- "golang.org/x/tools/internal/xcontext" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" -) - --type snapshot struct { -- sequenceID uint64 -- globalID source.GlobalSnapshotID +-func skipIfNoParseCache(t *testing.T) { +- if bits.UintSize == 32 { +- t.Skip("the parse cache is not supported on 32-bit systems") +- } +-} - -- // TODO(rfindley): the snapshot holding a reference to the view poses -- // lifecycle problems: a view may be shut down and waiting for work -- // associated with this snapshot to complete. While most accesses of the view -- // are benign (options or workspace information), this is not formalized and -- // it is wrong for the snapshot to use a shutdown view. -- // -- // Fix this by passing options and workspace information to the snapshot, -- // both of which should be immutable for the snapshot. -- view *View +-func TestParseCache(t *testing.T) { +- skipIfNoParseCache(t) - -- cancel func() -- backgroundCtx context.Context +- ctx := context.Background() +- uri := protocol.DocumentURI("file:///myfile") +- fh := makeFakeFileHandle(uri, []byte("package p\n\nconst _ = \"foo\"")) +- fset := token.NewFileSet() - -- store *memoize.Store // cache of handles shared by all snapshots +- cache := newParseCache(0) +- pgfs1, err := cache.parseFiles(ctx, fset, parsego.Full, false, fh) +- if err != nil { +- t.Fatal(err) +- } +- pgf1 := pgfs1[0] +- pgfs2, err := cache.parseFiles(ctx, fset, parsego.Full, false, fh) +- pgf2 := pgfs2[0] +- if err != nil { +- t.Fatal(err) +- } +- if pgf1 != pgf2 { +- t.Errorf("parseFiles(%q): unexpected cache miss on repeated call", uri) +- } - -- refcount sync.WaitGroup // number of references -- destroyedBy *string // atomically set to non-nil in Destroy once refcount = 0 +- // Fill up the cache with other files, but don't evict the file above. +- cache.gcOnce() +- files := []file.Handle{fh} +- files = append(files, dummyFileHandles(parseCacheMinFiles-1)...) - -- // initialized reports whether the snapshot has been initialized. Concurrent -- // initialization is guarded by the view.initializationSema. Each snapshot is -- // initialized at most once: concurrent initialization is guarded by -- // view.initializationSema. -- initialized bool -- // initializedErr holds the last error resulting from initialization. If -- // initialization fails, we only retry when the workspace modules change, -- // to avoid too many go/packages calls. -- initializedErr *source.CriticalError +- pgfs3, err := cache.parseFiles(ctx, fset, parsego.Full, false, files...) +- if err != nil { +- t.Fatal(err) +- } +- pgf3 := pgfs3[0] +- if pgf3 != pgf1 { +- t.Errorf("parseFiles(%q, ...): unexpected cache miss", uri) +- } +- if pgf3.Tok.Base() != pgf1.Tok.Base() || pgf3.Tok.Size() != pgf1.Tok.Size() { +- t.Errorf("parseFiles(%q, ...): result.Tok has base: %d, size: %d, want (%d, %d)", uri, pgf3.Tok.Base(), pgf3.Tok.Size(), pgf1.Tok.Base(), pgf1.Tok.Size()) +- } +- if tok := fset.File(token.Pos(pgf3.Tok.Base())); tok != pgf3.Tok { +- t.Errorf("parseFiles(%q, ...): result.Tok not contained in FileSet", uri) +- } - -- // mu guards all of the maps in the snapshot, as well as the builtin URI. -- mu sync.Mutex +- // Now overwrite the cache, after which we should get new results. +- cache.gcOnce() +- files = dummyFileHandles(parseCacheMinFiles) +- _, err = cache.parseFiles(ctx, fset, parsego.Full, false, files...) +- if err != nil { +- t.Fatal(err) +- } +- // force a GC, which should collect the recently parsed files +- cache.gcOnce() +- pgfs4, err := cache.parseFiles(ctx, fset, parsego.Full, false, fh) +- if err != nil { +- t.Fatal(err) +- } +- if pgfs4[0] == pgf1 { +- t.Errorf("parseFiles(%q): unexpected cache hit after overwriting cache", uri) +- } +-} - -- // builtin is the location of builtin.go in GOROOT. -- // -- // TODO(rfindley): would it make more sense to eagerly parse builtin, and -- // instead store a *ParsedGoFile here? -- builtin span.URI +-func TestParseCache_Reparsing(t *testing.T) { +- skipIfNoParseCache(t) - -- // meta holds loaded metadata. -- // -- // meta is guarded by mu, but the metadataGraph itself is immutable. -- // TODO(rfindley): in many places we hold mu while operating on meta, even -- // though we only need to hold mu while reading the pointer. -- meta *metadataGraph +- defer func(padding int) { +- parsePadding = padding +- }(parsePadding) +- parsePadding = 0 - -- // files maps file URIs to their corresponding FileHandles. -- // It may invalidated when a file's content changes. -- files *fileMap +- files := dummyFileHandles(parseCacheMinFiles) +- danglingSelector := []byte("package p\nfunc _() {\n\tx.\n}") +- files = append(files, makeFakeFileHandle("file:///bad1", danglingSelector)) +- files = append(files, makeFakeFileHandle("file:///bad2", danglingSelector)) - -- // symbolizeHandles maps each file URI to a handle for the future -- // result of computing the symbols declared in that file. -- symbolizeHandles *persistent.Map[span.URI, *memoize.Promise] // *memoize.Promise[symbolizeResult] +- // Parsing should succeed even though we overflow the padding. +- cache := newParseCache(0) +- _, err := cache.parseFiles(context.Background(), token.NewFileSet(), parsego.Full, false, files...) +- if err != nil { +- t.Fatal(err) +- } +-} - -- // packages maps a packageKey to a *packageHandle. -- // It may be invalidated when a file's content changes. -- // -- // Invariants to preserve: -- // - packages.Get(id).meta == meta.metadata[id] for all ids -- // - if a package is in packages, then all of its dependencies should also -- // be in packages, unless there is a missing import -- packages *persistent.Map[PackageID, *packageHandle] -- -- // activePackages maps a package ID to a memoized active package, or nil if -- // the package is known not to be open. -- // -- // IDs not contained in the map are not known to be open or not open. -- activePackages *persistent.Map[PackageID, *Package] +-// Re-parsing the first file should not panic. +-func TestParseCache_Issue59097(t *testing.T) { +- skipIfNoParseCache(t) - -- // workspacePackages contains the workspace's packages, which are loaded -- // when the view is created. It contains no intermediate test variants. -- // TODO(rfindley): use a persistent.Map. -- workspacePackages map[PackageID]PackagePath +- defer func(padding int) { +- parsePadding = padding +- }(parsePadding) +- parsePadding = 0 - -- // shouldLoad tracks packages that need to be reloaded, mapping a PackageID -- // to the package paths that should be used to reload it -- // -- // When we try to load a package, we clear it from the shouldLoad map -- // regardless of whether the load succeeded, to prevent endless loads. -- shouldLoad map[PackageID][]PackagePath +- danglingSelector := []byte("package p\nfunc _() {\n\tx.\n}") +- files := []file.Handle{makeFakeFileHandle("file:///bad", danglingSelector)} - -- // unloadableFiles keeps track of files that we've failed to load. -- unloadableFiles *persistent.Set[span.URI] +- // Parsing should succeed even though we overflow the padding. +- cache := newParseCache(0) +- _, err := cache.parseFiles(context.Background(), token.NewFileSet(), parsego.Full, false, files...) +- if err != nil { +- t.Fatal(err) +- } +-} - -- // TODO(rfindley): rename the handles below to "promises". A promise is -- // different from a handle (we mutate the package handle.) +-func TestParseCache_TimeEviction(t *testing.T) { +- skipIfNoParseCache(t) - -- // parseModHandles keeps track of any parseModHandles for the snapshot. -- // The handles need not refer to only the view's go.mod file. -- parseModHandles *persistent.Map[span.URI, *memoize.Promise] // *memoize.Promise[parseModResult] +- ctx := context.Background() +- fset := token.NewFileSet() +- uri := protocol.DocumentURI("file:///myfile") +- fh := makeFakeFileHandle(uri, []byte("package p\n\nconst _ = \"foo\"")) - -- // parseWorkHandles keeps track of any parseWorkHandles for the snapshot. -- // The handles need not refer to only the view's go.work file. -- parseWorkHandles *persistent.Map[span.URI, *memoize.Promise] // *memoize.Promise[parseWorkResult] +- const gcDuration = 10 * time.Millisecond +- cache := newParseCache(gcDuration) +- cache.stop() // we'll manage GC manually, for testing. - -- // Preserve go.mod-related handles to avoid garbage-collecting the results -- // of various calls to the go command. The handles need not refer to only -- // the view's go.mod file. -- modTidyHandles *persistent.Map[span.URI, *memoize.Promise] // *memoize.Promise[modTidyResult] -- modWhyHandles *persistent.Map[span.URI, *memoize.Promise] // *memoize.Promise[modWhyResult] -- modVulnHandles *persistent.Map[span.URI, *memoize.Promise] // *memoize.Promise[modVulnResult] +- pgfs0, err := cache.parseFiles(ctx, fset, parsego.Full, false, fh, fh) +- if err != nil { +- t.Fatal(err) +- } - -- // workspaceModFiles holds the set of mod files active in this snapshot. -- // -- // This is either empty, a single entry for the workspace go.mod file, or the -- // set of mod files used by the workspace go.work file. -- // -- // This set is immutable inside the snapshot, and therefore is not guarded by mu. -- workspaceModFiles map[span.URI]struct{} -- workspaceModFilesErr error // error encountered computing workspaceModFiles +- files := dummyFileHandles(parseCacheMinFiles) +- _, err = cache.parseFiles(ctx, fset, parsego.Full, false, files...) +- if err != nil { +- t.Fatal(err) +- } - -- // importGraph holds a shared import graph to use for type-checking. Adding -- // more packages to this import graph can speed up type checking, at the -- // expense of in-use memory. -- // -- // See getImportGraph for additional documentation. -- importGraphDone chan struct{} // closed when importGraph is set; may be nil -- importGraph *importGraph // copied from preceding snapshot and re-evaluated +- // Even after filling up the 'min' files, we get a cache hit for our original file. +- pgfs1, err := cache.parseFiles(ctx, fset, parsego.Full, false, fh, fh) +- if err != nil { +- t.Fatal(err) +- } - -- // pkgIndex is an index of package IDs, for efficient storage of typerefs. -- pkgIndex *typerefs.PackageIndex +- if pgfs0[0] != pgfs1[0] { +- t.Errorf("before GC, got unexpected cache miss") +- } - -- // Only compute module prefixes once, as they are used with high frequency to -- // detect ignored files. -- ignoreFilterOnce sync.Once -- ignoreFilter *ignoreFilter --} +- // But after GC, we get a cache miss. +- _, err = cache.parseFiles(ctx, fset, parsego.Full, false, files...) // mark dummy files as newer +- if err != nil { +- t.Fatal(err) +- } +- time.Sleep(gcDuration) +- cache.gcOnce() - --var globalSnapshotID uint64 +- pgfs2, err := cache.parseFiles(ctx, fset, parsego.Full, false, fh, fh) +- if err != nil { +- t.Fatal(err) +- } - --func nextSnapshotID() source.GlobalSnapshotID { -- return source.GlobalSnapshotID(atomic.AddUint64(&globalSnapshotID, 1)) +- if pgfs0[0] == pgfs2[0] { +- t.Errorf("after GC, got unexpected cache hit for %s", pgfs0[0].URI) +- } -} - --var _ memoize.RefCounted = (*snapshot)(nil) // snapshots are reference-counted +-func TestParseCache_Duplicates(t *testing.T) { +- skipIfNoParseCache(t) - --// Acquire prevents the snapshot from being destroyed until the returned function is called. --// --// (s.Acquire().release() could instead be expressed as a pair of --// method calls s.IncRef(); s.DecRef(). The latter has the advantage --// that the DecRefs are fungible and don't require holding anything in --// addition to the refcounted object s, but paradoxically that is also --// an advantage of the current approach, which forces the caller to --// consider the release function at every stage, making a reference --// leak more obvious.) --func (s *snapshot) Acquire() func() { -- type uP = unsafe.Pointer -- if destroyedBy := atomic.LoadPointer((*uP)(uP(&s.destroyedBy))); destroyedBy != nil { -- log.Panicf("%d: acquire() after Destroy(%q)", s.globalID, *(*string)(destroyedBy)) -- } -- s.refcount.Add(1) -- return s.refcount.Done --} +- ctx := context.Background() +- uri := protocol.DocumentURI("file:///myfile") +- fh := makeFakeFileHandle(uri, []byte("package p\n\nconst _ = \"foo\"")) - --func (s *snapshot) awaitPromise(ctx context.Context, p *memoize.Promise) (interface{}, error) { -- return p.Get(ctx, s) +- cache := newParseCache(0) +- pgfs, err := cache.parseFiles(ctx, token.NewFileSet(), parsego.Full, false, fh, fh) +- if err != nil { +- t.Fatal(err) +- } +- if pgfs[0] != pgfs[1] { +- t.Errorf("parseFiles(fh, fh): = [%p, %p], want duplicate files", pgfs[0].File, pgfs[1].File) +- } -} - --// destroy waits for all leases on the snapshot to expire then releases --// any resources (reference counts and files) associated with it. --// Snapshots being destroyed can be awaited using v.destroyWG. --// --// TODO(adonovan): move this logic into the release function returned --// by Acquire when the reference count becomes zero. (This would cost --// us the destroyedBy debug info, unless we add it to the signature of --// memoize.RefCounted.Acquire.) --// --// The destroyedBy argument is used for debugging. --// --// v.snapshotMu must be held while calling this function, in order to preserve --// the invariants described by the docstring for v.snapshot. --func (v *View) destroy(s *snapshot, destroyedBy string) { -- v.snapshotWG.Add(1) -- go func() { -- defer v.snapshotWG.Done() -- s.destroy(destroyedBy) -- }() +-func dummyFileHandles(n int) []file.Handle { +- var fhs []file.Handle +- for i := 0; i < n; i++ { +- uri := protocol.DocumentURI(fmt.Sprintf("file:///_%d", i)) +- src := []byte(fmt.Sprintf("package p\nvar _ = %d", i)) +- fhs = append(fhs, makeFakeFileHandle(uri, src)) +- } +- return fhs -} - --func (s *snapshot) destroy(destroyedBy string) { -- // Wait for all leases to end before commencing destruction. -- s.refcount.Wait() -- -- // Report bad state as a debugging aid. -- // Not foolproof: another thread could acquire() at this moment. -- type uP = unsafe.Pointer // looking forward to generics... -- if old := atomic.SwapPointer((*uP)(uP(&s.destroyedBy)), uP(&destroyedBy)); old != nil { -- log.Panicf("%d: Destroy(%q) after Destroy(%q)", s.globalID, destroyedBy, *(*string)(old)) +-func makeFakeFileHandle(uri protocol.DocumentURI, src []byte) fakeFileHandle { +- return fakeFileHandle{ +- uri: uri, +- data: src, +- hash: file.HashOf(src), - } -- -- s.packages.Destroy() -- s.activePackages.Destroy() -- s.files.Destroy() -- s.symbolizeHandles.Destroy() -- s.parseModHandles.Destroy() -- s.parseWorkHandles.Destroy() -- s.modTidyHandles.Destroy() -- s.modVulnHandles.Destroy() -- s.modWhyHandles.Destroy() -- s.unloadableFiles.Destroy() -} - --func (s *snapshot) SequenceID() uint64 { -- return s.sequenceID +-type fakeFileHandle struct { +- file.Handle +- uri protocol.DocumentURI +- data []byte +- hash file.Hash -} - --func (s *snapshot) GlobalID() source.GlobalSnapshotID { -- return s.globalID +-func (h fakeFileHandle) URI() protocol.DocumentURI { +- return h.uri -} - --func (s *snapshot) View() source.View { -- return s.view +-func (h fakeFileHandle) Content() ([]byte, error) { +- return h.data, nil -} - --func (s *snapshot) FileKind(fh source.FileHandle) source.FileKind { -- // The kind of an unsaved buffer comes from the -- // TextDocumentItem.LanguageID field in the didChange event, -- // not from the file name. They may differ. -- if o, ok := fh.(*Overlay); ok { -- if o.kind != source.UnknownKind { -- return o.kind -- } -- } -- -- fext := filepath.Ext(fh.URI().Filename()) -- switch fext { -- case ".go": -- return source.Go -- case ".mod": -- return source.Mod -- case ".sum": -- return source.Sum -- case ".work": -- return source.Work -- } -- exts := s.Options().TemplateExtensions -- for _, ext := range exts { -- if fext == ext || fext == "."+ext { -- return source.Tmpl -- } +-func (h fakeFileHandle) Identity() file.Identity { +- return file.Identity{ +- URI: h.uri, +- Hash: h.hash, - } -- // and now what? This should never happen, but it does for cgo before go1.15 -- return source.Go -} +diff -urN a/gopls/internal/cache/parse.go b/gopls/internal/cache/parse.go +--- a/gopls/internal/cache/parse.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/parse.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,45 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func (s *snapshot) Options() *source.Options { -- return s.view.folder.Options --} +-package cache - --func (s *snapshot) BackgroundContext() context.Context { -- return s.backgroundCtx --} +-import ( +- "context" +- "fmt" +- "go/parser" +- "go/token" +- "path/filepath" - --func (s *snapshot) ModFiles() []span.URI { -- var uris []span.URI -- for modURI := range s.workspaceModFiles { -- uris = append(uris, modURI) +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/file" +-) +- +-// ParseGo parses the file whose contents are provided by fh. +-// The resulting tree may have been fixed up. +-// If the file is not available, returns nil and an error. +-func (s *Snapshot) ParseGo(ctx context.Context, fh file.Handle, mode parser.Mode) (*parsego.File, error) { +- pgfs, err := s.view.parseCache.parseFiles(ctx, token.NewFileSet(), mode, false, fh) +- if err != nil { +- return nil, err - } -- return uris +- return pgfs[0], nil -} - --func (s *snapshot) WorkFile() span.URI { -- gowork, _ := s.view.GOWORK() -- return gowork +-// parseGoImpl parses the Go source file whose content is provided by fh. +-func parseGoImpl(ctx context.Context, fset *token.FileSet, fh file.Handle, mode parser.Mode, purgeFuncBodies bool) (*parsego.File, error) { +- ext := filepath.Ext(fh.URI().Path()) +- if ext != ".go" && ext != "" { // files generated by cgo have no extension +- return nil, fmt.Errorf("cannot parse non-Go file %s", fh.URI()) +- } +- content, err := fh.Content() +- if err != nil { +- return nil, err +- } +- // Check for context cancellation before actually doing the parse. +- if ctx.Err() != nil { +- return nil, ctx.Err() +- } +- pgf, _ := parsego.Parse(ctx, fset, fh.URI(), content, mode, purgeFuncBodies) +- return pgf, nil -} +diff -urN a/gopls/internal/cache/parsego/file.go b/gopls/internal/cache/parsego/file.go +--- a/gopls/internal/cache/parsego/file.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/parsego/file.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,100 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func (s *snapshot) Templates() map[span.URI]source.FileHandle { -- s.mu.Lock() -- defer s.mu.Unlock() +-package parsego - -- tmpls := map[span.URI]source.FileHandle{} -- s.files.Range(func(k span.URI, fh source.FileHandle) { -- if s.FileKind(fh) == source.Tmpl { -- tmpls[k] = fh -- } -- }) -- return tmpls --} +-import ( +- "go/ast" +- "go/parser" +- "go/scanner" +- "go/token" - --func (s *snapshot) validBuildConfiguration() bool { -- // Since we only really understand the `go` command, if the user has a -- // different GOPACKAGESDRIVER, assume that their configuration is valid. -- if s.view.hasGopackagesDriver { -- return true -- } +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/safetoken" +-) - -- // Check if the user is working within a module or if we have found -- // multiple modules in the workspace. -- if len(s.workspaceModFiles) > 0 { -- return true -- } +-// A File contains the results of parsing a Go file. +-type File struct { +- URI protocol.DocumentURI +- Mode parser.Mode +- File *ast.File +- Tok *token.File +- // Source code used to build the AST. It may be different from the +- // actual content of the file if we have fixed the AST. +- Src []byte - -- // TODO(rfindley): this should probably be subject to "if GO111MODULES = off {...}". -- if s.view.inGOPATH { -- return true -- } +- // fixedSrc and fixedAST report on "fixing" that occurred during parsing of +- // this file. +- // +- // fixedSrc means Src holds file content that was modified to improve parsing. +- // fixedAST means File was modified after parsing, so AST positions may not +- // reflect the content of Src. +- // +- // TODO(rfindley): there are many places where we haphazardly use the Src or +- // positions without checking these fields. Audit these places and guard +- // accordingly. After doing so, we may find that we don't need to +- // differentiate fixedSrc and fixedAST. +- fixedSrc bool +- fixedAST bool +- Mapper *protocol.Mapper // may map fixed Src, not file content +- ParseErr scanner.ErrorList +-} - -- return false +-// Fixed reports whether p was "Fixed", meaning that its source or positions +-// may not correlate with the original file. +-func (p File) Fixed() bool { +- return p.fixedSrc || p.fixedAST -} - --// workspaceMode describes the way in which the snapshot's workspace should --// be loaded. --// --// TODO(rfindley): remove this, in favor of specific methods. --func (s *snapshot) workspaceMode() workspaceMode { -- var mode workspaceMode +-// -- go/token domain convenience helpers -- - -- // If the view has an invalid configuration, don't build the workspace -- // module. -- validBuildConfiguration := s.validBuildConfiguration() -- if !validBuildConfiguration { -- return mode -- } -- // If the view is not in a module and contains no modules, but still has a -- // valid workspace configuration, do not create the workspace module. -- // It could be using GOPATH or a different build system entirely. -- if len(s.workspaceModFiles) == 0 && validBuildConfiguration { -- return mode -- } -- mode |= moduleMode -- if s.Options().TempModfile { -- mode |= tempModfile +-// PositionPos returns the token.Pos of protocol position p within the file. +-func (pgf *File) PositionPos(p protocol.Position) (token.Pos, error) { +- offset, err := pgf.Mapper.PositionOffset(p) +- if err != nil { +- return token.NoPos, err - } -- return mode +- return safetoken.Pos(pgf.Tok, offset) -} - --// config returns the configuration used for the snapshot's interaction with --// the go/packages API. It uses the given working directory. --// --// TODO(rstambler): go/packages requires that we do not provide overlays for --// multiple modules in on config, so buildOverlay needs to filter overlays by --// module. --func (s *snapshot) config(ctx context.Context, inv *gocommand.Invocation) *packages.Config { +-// PosRange returns a protocol Range for the token.Pos interval in this file. +-func (pgf *File) PosRange(start, end token.Pos) (protocol.Range, error) { +- return pgf.Mapper.PosRange(pgf.Tok, start, end) +-} - -- cfg := &packages.Config{ -- Context: ctx, -- Dir: inv.WorkingDir, -- Env: inv.Env, -- BuildFlags: inv.BuildFlags, -- Mode: packages.NeedName | -- packages.NeedFiles | -- packages.NeedCompiledGoFiles | -- packages.NeedImports | -- packages.NeedDeps | -- packages.NeedTypesSizes | -- packages.NeedModule | -- packages.NeedEmbedFiles | -- packages.LoadMode(packagesinternal.DepsErrors) | -- packages.LoadMode(packagesinternal.ForTest), -- Fset: nil, // we do our own parsing -- Overlay: s.buildOverlay(), -- ParseFile: func(*token.FileSet, string, []byte) (*ast.File, error) { -- panic("go/packages must not be used to parse files") -- }, -- Logf: func(format string, args ...interface{}) { -- if s.Options().VerboseOutput { -- event.Log(ctx, fmt.Sprintf(format, args...)) -- } -- }, -- Tests: true, -- } -- packagesinternal.SetModFile(cfg, inv.ModFile) -- packagesinternal.SetModFlag(cfg, inv.ModFlag) -- // We want to type check cgo code if go/types supports it. -- if typesinternal.SetUsesCgo(&types.Config{}) { -- cfg.Mode |= packages.LoadMode(packagesinternal.TypecheckCgo) -- } -- packagesinternal.SetGoCmdRunner(cfg, s.view.gocmdRunner) -- return cfg +-// PosMappedRange returns a MappedRange for the token.Pos interval in this file. +-// A MappedRange can be converted to any other form. +-func (pgf *File) PosMappedRange(start, end token.Pos) (protocol.MappedRange, error) { +- return pgf.Mapper.PosMappedRange(pgf.Tok, start, end) -} - --func (s *snapshot) RunGoCommandDirect(ctx context.Context, mode source.InvocationFlags, inv *gocommand.Invocation) (*bytes.Buffer, error) { -- _, inv, cleanup, err := s.goCommandInvocation(ctx, mode, inv) -- if err != nil { -- return nil, err -- } -- defer cleanup() +-// PosLocation returns a protocol Location for the token.Pos interval in this file. +-func (pgf *File) PosLocation(start, end token.Pos) (protocol.Location, error) { +- return pgf.Mapper.PosLocation(pgf.Tok, start, end) +-} - -- return s.view.gocmdRunner.Run(ctx, *inv) +-// NodeRange returns a protocol Range for the ast.Node interval in this file. +-func (pgf *File) NodeRange(node ast.Node) (protocol.Range, error) { +- return pgf.Mapper.NodeRange(pgf.Tok, node) -} - --func (s *snapshot) RunGoCommandPiped(ctx context.Context, mode source.InvocationFlags, inv *gocommand.Invocation, stdout, stderr io.Writer) error { -- _, inv, cleanup, err := s.goCommandInvocation(ctx, mode, inv) -- if err != nil { -- return err -- } -- defer cleanup() -- return s.view.gocmdRunner.RunPiped(ctx, *inv, stdout, stderr) +-// NodeMappedRange returns a MappedRange for the ast.Node interval in this file. +-// A MappedRange can be converted to any other form. +-func (pgf *File) NodeMappedRange(node ast.Node) (protocol.MappedRange, error) { +- return pgf.Mapper.NodeMappedRange(pgf.Tok, node) -} - --func (s *snapshot) RunGoCommands(ctx context.Context, allowNetwork bool, wd string, run func(invoke func(...string) (*bytes.Buffer, error)) error) (bool, []byte, []byte, error) { -- var flags source.InvocationFlags -- if s.workspaceMode()&tempModfile != 0 { -- flags = source.WriteTemporaryModFile -- } else { -- flags = source.Normal -- } -- if allowNetwork { -- flags |= source.AllowNetwork -- } -- tmpURI, inv, cleanup, err := s.goCommandInvocation(ctx, flags, &gocommand.Invocation{WorkingDir: wd}) +-// NodeLocation returns a protocol Location for the ast.Node interval in this file. +-func (pgf *File) NodeLocation(node ast.Node) (protocol.Location, error) { +- return pgf.Mapper.PosLocation(pgf.Tok, node.Pos(), node.End()) +-} +- +-// RangePos parses a protocol Range back into the go/token domain. +-func (pgf *File) RangePos(r protocol.Range) (token.Pos, token.Pos, error) { +- start, end, err := pgf.Mapper.RangeOffsets(r) - if err != nil { -- return false, nil, nil, err -- } -- defer cleanup() -- invoke := func(args ...string) (*bytes.Buffer, error) { -- inv.Verb = args[0] -- inv.Args = args[1:] -- return s.view.gocmdRunner.Run(ctx, *inv) -- } -- if err := run(invoke); err != nil { -- return false, nil, nil, err -- } -- if flags.Mode() != source.WriteTemporaryModFile { -- return false, nil, nil, nil -- } -- var modBytes, sumBytes []byte -- modBytes, err = os.ReadFile(tmpURI.Filename()) -- if err != nil && !os.IsNotExist(err) { -- return false, nil, nil, err -- } -- sumBytes, err = os.ReadFile(strings.TrimSuffix(tmpURI.Filename(), ".mod") + ".sum") -- if err != nil && !os.IsNotExist(err) { -- return false, nil, nil, err +- return token.NoPos, token.NoPos, err - } -- return true, modBytes, sumBytes, nil +- return pgf.Tok.Pos(start), pgf.Tok.Pos(end), nil -} +diff -urN a/gopls/internal/cache/parsego/parse.go b/gopls/internal/cache/parsego/parse.go +--- a/gopls/internal/cache/parsego/parse.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/parsego/parse.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,967 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// goCommandInvocation populates inv with configuration for running go commands on the snapshot. --// --// TODO(rfindley): refactor this function to compose the required configuration --// explicitly, rather than implicitly deriving it from flags and inv. --// --// TODO(adonovan): simplify cleanup mechanism. It's hard to see, but --// it used only after call to tempModFile. Clarify that it is only --// non-nil on success. --func (s *snapshot) goCommandInvocation(ctx context.Context, flags source.InvocationFlags, inv *gocommand.Invocation) (tmpURI span.URI, updatedInv *gocommand.Invocation, cleanup func(), err error) { -- allowModfileModificationOption := s.Options().AllowModfileModifications -- allowNetworkOption := s.Options().AllowImplicitNetworkAccess -- -- // TODO(rfindley): this is very hard to follow, and may not even be doing the -- // right thing: should inv.Env really trample view.options? Do we ever invoke -- // this with a non-empty inv.Env? -- // -- // We should refactor to make it clearer that the correct env is being used. -- inv.Env = append(append(append(os.Environ(), s.Options().EnvSlice()...), inv.Env...), "GO111MODULE="+s.view.GO111MODULE()) -- inv.BuildFlags = append([]string{}, s.Options().BuildFlags...) -- cleanup = func() {} // fallback +-package parsego - -- // All logic below is for module mode. -- if s.workspaceMode()&moduleMode == 0 { -- return "", inv, cleanup, nil -- } +-import ( +- "bytes" +- "context" +- "fmt" +- "go/ast" +- "go/parser" +- "go/scanner" +- "go/token" +- "reflect" - -- mode, allowNetwork := flags.Mode(), flags.AllowNetwork() -- if !allowNetwork && !allowNetworkOption { -- inv.Env = append(inv.Env, "GOPROXY=off") -- } +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/astutil" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/internal/diff" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/tag" +-) - -- // What follows is rather complicated logic for how to actually run the go -- // command. A word of warning: this is the result of various incremental -- // features added to gopls, and varying behavior of the Go command across Go -- // versions. It can surely be cleaned up significantly, but tread carefully. -- // -- // Roughly speaking we need to resolve four things: -- // - the working directory. -- // - the -mod flag -- // - the -modfile flag -- // -- // These are dependent on a number of factors: whether we need to run in a -- // synthetic workspace, whether flags are supported at the current go -- // version, and what we're actually trying to achieve (the -- // source.InvocationFlags). +-// Common parse modes; these should be reused wherever possible to increase +-// cache hits. +-const ( +- // Header specifies that the main package declaration and imports are needed. +- // This is the mode used when attempting to examine the package graph structure. +- Header = parser.AllErrors | parser.ParseComments | parser.ImportsOnly | parser.SkipObjectResolution - -- var modURI span.URI -- // Select the module context to use. -- // If we're type checking, we need to use the workspace context, meaning -- // the main (workspace) module. Otherwise, we should use the module for -- // the passed-in working dir. -- if mode == source.LoadWorkspace { -- if gowork, _ := s.view.GOWORK(); gowork == "" && s.view.gomod != "" { -- modURI = s.view.gomod -- } -- } else { -- modURI = s.GoModForFile(span.URIFromPath(inv.WorkingDir)) -- } +- // Full specifies the full AST is needed. +- // This is used for files of direct interest where the entire contents must +- // be considered. +- Full = parser.AllErrors | parser.ParseComments | parser.SkipObjectResolution +-) - -- var modContent []byte -- if modURI != "" { -- modFH, err := s.ReadFile(ctx, modURI) -- if err != nil { -- return "", nil, cleanup, err -- } -- modContent, err = modFH.Content() -- if err != nil { -- return "", nil, cleanup, err -- } +-// Parse parses a buffer of Go source, repairing the tree if necessary. +-// +-// The provided ctx is used only for logging. +-func Parse(ctx context.Context, fset *token.FileSet, uri protocol.DocumentURI, src []byte, mode parser.Mode, purgeFuncBodies bool) (res *File, fixes []fixType) { +- if purgeFuncBodies { +- src = astutil.PurgeFuncBodies(src) - } +- ctx, done := event.Start(ctx, "cache.ParseGoSrc", tag.File.Of(uri.Path())) +- defer done() - -- // TODO(rfindley): in the case of go.work mode, modURI is empty and we fall -- // back on the default behavior of vendorEnabled with an empty modURI. Figure -- // out what is correct here and implement it explicitly. -- vendorEnabled, err := s.vendorEnabled(ctx, modURI, modContent) +- file, err := parser.ParseFile(fset, uri.Path(), src, mode) +- var parseErr scanner.ErrorList - if err != nil { -- return "", nil, cleanup, err -- } -- -- const mutableModFlag = "mod" -- // If the mod flag isn't set, populate it based on the mode and workspace. -- // TODO(rfindley): this doesn't make sense if we're not in module mode -- if inv.ModFlag == "" { -- switch mode { -- case source.LoadWorkspace, source.Normal: -- if vendorEnabled { -- inv.ModFlag = "vendor" -- } else if !allowModfileModificationOption { -- inv.ModFlag = "readonly" -- } else { -- inv.ModFlag = mutableModFlag -- } -- case source.WriteTemporaryModFile: -- inv.ModFlag = mutableModFlag -- // -mod must be readonly when using go.work files - see issue #48941 -- inv.Env = append(inv.Env, "GOWORK=off") -- } +- // We passed a byte slice, so the only possible error is a parse error. +- parseErr = err.(scanner.ErrorList) - } - -- // Only use a temp mod file if the modfile can actually be mutated. -- needTempMod := inv.ModFlag == mutableModFlag -- useTempMod := s.workspaceMode()&tempModfile != 0 -- if needTempMod && !useTempMod { -- return "", nil, cleanup, source.ErrTmpModfileUnsupported +- tok := fset.File(file.Pos()) +- if tok == nil { +- // file.Pos is the location of the package declaration (issue #53202). If there was +- // none, we can't find the token.File that ParseFile created, and we +- // have no choice but to recreate it. +- tok = fset.AddFile(uri.Path(), -1, len(src)) +- tok.SetLinesForContent(src) - } - -- // We should use -modfile if: -- // - the workspace mode supports it -- // - we're using a go.work file on go1.18+, or we need a temp mod file (for -- // example, if running go mod tidy in a go.work workspace) -- // -- // TODO(rfindley): this is very hard to follow. Refactor. -- if !needTempMod && s.view.gowork != "" { -- // Since we're running in the workspace root, the go command will resolve GOWORK automatically. -- } else if useTempMod { -- if modURI == "" { -- return "", nil, cleanup, fmt.Errorf("no go.mod file found in %s", inv.WorkingDir) -- } -- modFH, err := s.ReadFile(ctx, modURI) -- if err != nil { -- return "", nil, cleanup, err -- } -- // Use the go.sum if it happens to be available. -- gosum := s.goSum(ctx, modURI) -- tmpURI, cleanup, err = tempModFile(modFH, gosum) -- if err != nil { -- return "", nil, cleanup, err +- fixedSrc := false +- fixedAST := false +- // If there were parse errors, attempt to fix them up. +- if parseErr != nil { +- // Fix any badly parsed parts of the AST. +- astFixes := fixAST(file, tok, src) +- fixedAST = len(astFixes) > 0 +- if fixedAST { +- fixes = append(fixes, astFixes...) - } -- inv.ModFile = tmpURI.Filename() -- } - -- return tmpURI, inv, cleanup, nil --} +- for i := 0; i < 10; i++ { +- // Fix certain syntax errors that render the file unparseable. +- newSrc, srcFix := fixSrc(file, tok, src) +- if newSrc == nil { +- break +- } - --func (s *snapshot) buildOverlay() map[string][]byte { -- overlays := make(map[string][]byte) -- for _, overlay := range s.overlays() { -- if overlay.saved { -- continue -- } -- // TODO(rfindley): previously, there was a todo here to make sure we don't -- // send overlays outside of the current view. IMO we should instead make -- // sure this doesn't matter. -- overlays[overlay.URI().Filename()] = overlay.content -- } -- return overlays --} +- // If we thought there was something to fix 10 times in a row, +- // it is likely we got stuck in a loop somehow. Log out a diff +- // of the last changes we made to aid in debugging. +- if i == 9 { +- unified := diff.Unified("before", "after", string(src), string(newSrc)) +- event.Log(ctx, fmt.Sprintf("fixSrc loop - last diff:\n%v", unified), tag.File.Of(tok.Name())) +- } - --func (s *snapshot) overlays() []*Overlay { -- s.mu.Lock() -- defer s.mu.Unlock() +- newFile, newErr := parser.ParseFile(fset, uri.Path(), newSrc, mode) +- if newFile == nil { +- break // no progress +- } - -- return s.files.Overlays() --} +- // Maintain the original parseError so we don't try formatting the +- // doctored file. +- file = newFile +- src = newSrc +- tok = fset.File(file.Pos()) - --// Package data kinds, identifying various package data that may be stored in --// the file cache. --const ( -- xrefsKind = "xrefs" -- methodSetsKind = "methodsets" -- exportDataKind = "export" -- diagnosticsKind = "diagnostics" -- typerefsKind = "typerefs" --) +- // Only now that we accept the fix do we record the src fix from above. +- fixes = append(fixes, srcFix) +- fixedSrc = true - --func (s *snapshot) PackageDiagnostics(ctx context.Context, ids ...PackageID) (map[span.URI][]*source.Diagnostic, error) { -- ctx, done := event.Start(ctx, "cache.snapshot.PackageDiagnostics") -- defer done() +- if newErr == nil { +- break // nothing to fix +- } - -- var mu sync.Mutex -- perFile := make(map[span.URI][]*source.Diagnostic) -- collect := func(diags []*source.Diagnostic) { -- mu.Lock() -- defer mu.Unlock() -- for _, diag := range diags { -- perFile[diag.URI] = append(perFile[diag.URI], diag) +- // Note that fixedAST is reset after we fix src. +- astFixes = fixAST(file, tok, src) +- fixedAST = len(astFixes) > 0 +- if fixedAST { +- fixes = append(fixes, astFixes...) +- } - } - } -- pre := func(_ int, ph *packageHandle) bool { -- data, err := filecache.Get(diagnosticsKind, ph.key) -- if err == nil { // hit -- collect(ph.m.Diagnostics) -- collect(decodeDiagnostics(data)) -- return false -- } else if err != filecache.ErrNotFound { -- event.Error(ctx, "reading diagnostics from filecache", err) -- } -- return true -- } -- post := func(_ int, pkg *Package) { -- collect(pkg.m.Diagnostics) -- collect(pkg.pkg.diagnostics) -- } -- return perFile, s.forEachPackage(ctx, ids, pre, post) +- +- return &File{ +- URI: uri, +- Mode: mode, +- Src: src, +- fixedSrc: fixedSrc, +- fixedAST: fixedAST, +- File: file, +- Tok: tok, +- Mapper: protocol.NewMapper(uri, src), +- ParseErr: parseErr, +- }, fixes -} - --func (s *snapshot) References(ctx context.Context, ids ...PackageID) ([]source.XrefIndex, error) { -- ctx, done := event.Start(ctx, "cache.snapshot.References") -- defer done() +-// fixAST inspects the AST and potentially modifies any *ast.BadStmts so that it can be +-// type-checked more effectively. +-// +-// If fixAST returns true, the resulting AST is considered "fixed", meaning +-// positions have been mangled, and type checker errors may not make sense. +-func fixAST(n ast.Node, tok *token.File, src []byte) (fixes []fixType) { +- var err error +- walkASTWithParent(n, func(n, parent ast.Node) bool { +- switch n := n.(type) { +- case *ast.BadStmt: +- if fixDeferOrGoStmt(n, parent, tok, src) { +- fixes = append(fixes, fixedDeferOrGo) +- // Recursively fix in our fixed node. +- moreFixes := fixAST(parent, tok, src) +- fixes = append(fixes, moreFixes...) +- } else { +- err = fmt.Errorf("unable to parse defer or go from *ast.BadStmt: %v", err) +- } +- return false +- case *ast.BadExpr: +- if fixArrayType(n, parent, tok, src) { +- fixes = append(fixes, fixedArrayType) +- // Recursively fix in our fixed node. +- moreFixes := fixAST(parent, tok, src) +- fixes = append(fixes, moreFixes...) +- return false +- } - -- indexes := make([]source.XrefIndex, len(ids)) -- pre := func(i int, ph *packageHandle) bool { -- data, err := filecache.Get(xrefsKind, ph.key) -- if err == nil { // hit -- indexes[i] = XrefIndex{m: ph.m, data: data} +- // Fix cases where parser interprets if/for/switch "init" +- // statement as "cond" expression, e.g.: +- // +- // // "i := foo" is init statement, not condition. +- // for i := foo +- // +- if fixInitStmt(n, parent, tok, src) { +- fixes = append(fixes, fixedInit) +- } - return false -- } else if err != filecache.ErrNotFound { -- event.Error(ctx, "reading xrefs from filecache", err) -- } -- return true -- } -- post := func(i int, pkg *Package) { -- indexes[i] = XrefIndex{m: pkg.m, data: pkg.pkg.xrefs()} -- } -- return indexes, s.forEachPackage(ctx, ids, pre, post) --} +- case *ast.SelectorExpr: +- // Fix cases where a keyword prefix results in a phantom "_" selector, e.g.: +- // +- // foo.var<> // want to complete to "foo.variance" +- // +- if fixPhantomSelector(n, tok, src) { +- fixes = append(fixes, fixedPhantomSelector) +- } +- return true - --// An XrefIndex is a helper for looking up a package in a given package. --type XrefIndex struct { -- m *source.Metadata -- data []byte --} +- case *ast.BlockStmt: +- switch parent.(type) { +- case *ast.SwitchStmt, *ast.TypeSwitchStmt, *ast.SelectStmt: +- // Adjust closing curly brace of empty switch/select +- // statements so we can complete inside them. +- if fixEmptySwitch(n, tok, src) { +- fixes = append(fixes, fixedEmptySwitch) +- } +- } - --func (index XrefIndex) Lookup(targets map[PackagePath]map[objectpath.Path]struct{}) []protocol.Location { -- return xrefs.Lookup(index.m, index.data, targets) +- return true +- default: +- return true +- } +- }) +- return fixes -} - --func (s *snapshot) MethodSets(ctx context.Context, ids ...PackageID) ([]*methodsets.Index, error) { -- ctx, done := event.Start(ctx, "cache.snapshot.MethodSets") -- defer done() +-// walkASTWithParent walks the AST rooted at n. The semantics are +-// similar to ast.Inspect except it does not call f(nil). +-func walkASTWithParent(n ast.Node, f func(n ast.Node, parent ast.Node) bool) { +- var ancestors []ast.Node +- ast.Inspect(n, func(n ast.Node) (recurse bool) { +- defer func() { +- if recurse { +- ancestors = append(ancestors, n) +- } +- }() - -- indexes := make([]*methodsets.Index, len(ids)) -- pre := func(i int, ph *packageHandle) bool { -- data, err := filecache.Get(methodSetsKind, ph.key) -- if err == nil { // hit -- indexes[i] = methodsets.Decode(data) +- if n == nil { +- ancestors = ancestors[:len(ancestors)-1] - return false -- } else if err != filecache.ErrNotFound { -- event.Error(ctx, "reading methodsets from filecache", err) - } -- return true -- } -- post := func(i int, pkg *Package) { -- indexes[i] = pkg.pkg.methodsets() -- } -- return indexes, s.forEachPackage(ctx, ids, pre, post) --} - --func (s *snapshot) MetadataForFile(ctx context.Context, uri span.URI) ([]*source.Metadata, error) { -- if s.view.ViewType() == AdHocView { -- // As described in golang/go#57209, in ad-hoc workspaces (where we load ./ -- // rather than ./...), preempting the directory load with file loads can -- // lead to an inconsistent outcome, where certain files are loaded with -- // command-line-arguments packages and others are loaded only in the ad-hoc -- // package. Therefore, ensure that the workspace is loaded before doing any -- // file loads. -- if err := s.awaitLoaded(ctx); err != nil { -- return nil, err +- var parent ast.Node +- if len(ancestors) > 0 { +- parent = ancestors[len(ancestors)-1] - } -- } - -- s.mu.Lock() -- -- // Start with the set of package associations derived from the last load. -- ids := s.meta.ids[uri] -- -- shouldLoad := false // whether any packages containing uri are marked 'shouldLoad' -- for _, id := range ids { -- if len(s.shouldLoad[id]) > 0 { -- shouldLoad = true -- } -- } +- return f(n, parent) +- }) +-} - -- // Check if uri is known to be unloadable. -- unloadable := s.unloadableFiles.Contains(uri) +-// TODO(rfindley): revert this intrumentation once we're certain the crash in +-// #59097 is fixed. +-type fixType int - -- s.mu.Unlock() +-const ( +- noFix fixType = iota +- fixedCurlies +- fixedDanglingSelector +- fixedDeferOrGo +- fixedArrayType +- fixedInit +- fixedPhantomSelector +- fixedEmptySwitch +-) - -- // Reload if loading is likely to improve the package associations for uri: -- // - uri is not contained in any valid packages -- // - ...or one of the packages containing uri is marked 'shouldLoad' -- // - ...but uri is not unloadable -- if (shouldLoad || len(ids) == 0) && !unloadable { -- scope := fileLoadScope(uri) -- err := s.load(ctx, false, scope) +-// fixSrc attempts to modify the file's source code to fix certain +-// syntax errors that leave the rest of the file unparsed. +-// +-// fixSrc returns a non-nil result if and only if a fix was applied. +-func fixSrc(f *ast.File, tf *token.File, src []byte) (newSrc []byte, fix fixType) { +- walkASTWithParent(f, func(n, parent ast.Node) bool { +- if newSrc != nil { +- return false +- } - -- // -- // Return the context error here as the current operation is no longer -- // valid. -- if err != nil { -- // Guard against failed loads due to context cancellation. We don't want -- // to mark loads as completed if they failed due to context cancellation. -- if ctx.Err() != nil { -- return nil, ctx.Err() +- switch n := n.(type) { +- case *ast.BlockStmt: +- newSrc = fixMissingCurlies(f, n, parent, tf, src) +- if newSrc != nil { +- fix = fixedCurlies - } -- -- // Don't return an error here, as we may still return stale IDs. -- // Furthermore, the result of MetadataForFile should be consistent upon -- // subsequent calls, even if the file is marked as unloadable. -- if !errors.Is(err, errNoPackages) { -- event.Error(ctx, "MetadataForFile", err) +- case *ast.SelectorExpr: +- newSrc = fixDanglingSelector(n, tf, src) +- if newSrc != nil { +- fix = fixedDanglingSelector - } - } - -- // We must clear scopes after loading. -- // -- // TODO(rfindley): unlike reloadWorkspace, this is simply marking loaded -- // packages as loaded. We could do this from snapshot.load and avoid -- // raciness. -- s.clearShouldLoad(scope) -- } +- return newSrc == nil +- }) - -- // Retrieve the metadata. -- s.mu.Lock() -- defer s.mu.Unlock() -- ids = s.meta.ids[uri] -- metas := make([]*source.Metadata, len(ids)) -- for i, id := range ids { -- metas[i] = s.meta.metadata[id] -- if metas[i] == nil { -- panic("nil metadata") +- return newSrc, fix +-} +- +-// fixMissingCurlies adds in curly braces for block statements that +-// are missing curly braces. For example: +-// +-// if foo +-// +-// becomes +-// +-// if foo {} +-func fixMissingCurlies(f *ast.File, b *ast.BlockStmt, parent ast.Node, tok *token.File, src []byte) []byte { +- // If the "{" is already in the source code, there isn't anything to +- // fix since we aren't missing curlies. +- if b.Lbrace.IsValid() { +- braceOffset, err := safetoken.Offset(tok, b.Lbrace) +- if err != nil { +- return nil +- } +- if braceOffset < len(src) && src[braceOffset] == '{' { +- return nil - } -- } -- // Metadata is only ever added by loading, -- // so if we get here and still have -- // no IDs, uri is unloadable. -- if !unloadable && len(ids) == 0 { -- s.unloadableFiles.Add(uri) - } - -- // Sort packages "narrowest" to "widest" (in practice: -- // non-tests before tests), and regular packages before -- // their intermediate test variants (which have the same -- // files but different imports). -- sort.Slice(metas, func(i, j int) bool { -- x, y := metas[i], metas[j] -- xfiles, yfiles := len(x.CompiledGoFiles), len(y.CompiledGoFiles) -- if xfiles != yfiles { -- return xfiles < yfiles -- } -- return boolLess(x.IsIntermediateTestVariant(), y.IsIntermediateTestVariant()) -- }) +- parentLine := safetoken.Line(tok, parent.Pos()) - -- return metas, nil --} +- if parentLine >= tok.LineCount() { +- // If we are the last line in the file, no need to fix anything. +- return nil +- } - --func boolLess(x, y bool) bool { return !x && y } // false < true +- // Insert curlies at the end of parent's starting line. The parent +- // is the statement that contains the block, e.g. *ast.IfStmt. The +- // block's Pos()/End() can't be relied upon because they are based +- // on the (missing) curly braces. We assume the statement is a +- // single line for now and try sticking the curly braces at the end. +- insertPos := tok.LineStart(parentLine+1) - 1 - --func (s *snapshot) ReverseDependencies(ctx context.Context, id PackageID, transitive bool) (map[PackageID]*source.Metadata, error) { -- if err := s.awaitLoaded(ctx); err != nil { -- return nil, err +- // Scootch position backwards until it's not in a comment. For example: +- // +- // if foo<> // some amazing comment | +- // someOtherCode() +- // +- // insertPos will be located at "|", so we back it out of the comment. +- didSomething := true +- for didSomething { +- didSomething = false +- for _, c := range f.Comments { +- if c.Pos() < insertPos && insertPos <= c.End() { +- insertPos = c.Pos() +- didSomething = true +- } +- } - } -- s.mu.Lock() -- meta := s.meta -- s.mu.Unlock() - -- var rdeps map[PackageID]*source.Metadata -- if transitive { -- rdeps = meta.reverseReflexiveTransitiveClosure(id) +- // Bail out if line doesn't end in an ident or ".". This is to avoid +- // cases like below where we end up making things worse by adding +- // curlies: +- // +- // if foo && +- // bar<> +- switch precedingToken(insertPos, tok, src) { +- case token.IDENT, token.PERIOD: +- // ok +- default: +- return nil +- } - -- // Remove the original package ID from the map. -- // (Callers all want irreflexivity but it's easier -- // to compute reflexively then subtract.) -- delete(rdeps, id) +- var buf bytes.Buffer +- buf.Grow(len(src) + 3) +- offset, err := safetoken.Offset(tok, insertPos) +- if err != nil { +- return nil +- } +- buf.Write(src[:offset]) - -- } else { -- // direct reverse dependencies -- rdeps = make(map[PackageID]*source.Metadata) -- for _, rdepID := range meta.importedBy[id] { -- if rdep := meta.metadata[rdepID]; rdep != nil { -- rdeps[rdepID] = rdep +- // Detect if we need to insert a semicolon to fix "for" loop situations like: +- // +- // for i := foo(); foo<> +- // +- // Just adding curlies is not sufficient to make things parse well. +- if fs, ok := parent.(*ast.ForStmt); ok { +- if _, ok := fs.Cond.(*ast.BadExpr); !ok { +- if xs, ok := fs.Post.(*ast.ExprStmt); ok { +- if _, ok := xs.X.(*ast.BadExpr); ok { +- buf.WriteByte(';') +- } - } - } - } - -- return rdeps, nil +- // Insert "{}" at insertPos. +- buf.WriteByte('{') +- buf.WriteByte('}') +- buf.Write(src[offset:]) +- return buf.Bytes() -} - --// -- Active package tracking -- +-// fixEmptySwitch moves empty switch/select statements' closing curly +-// brace down one line. This allows us to properly detect incomplete +-// "case" and "default" keywords as inside the switch statement. For +-// example: -// --// We say a package is "active" if any of its files are open. --// This is an optimization: the "active" concept is an --// implementation detail of the cache and is not exposed --// in the source or Snapshot API. --// After type-checking we keep active packages in memory. --// The activePackages persistent map does bookkeeping for --// the set of active packages. -- --// getActivePackage returns a the memoized active package for id, if it exists. --// If id is not active or has not yet been type-checked, it returns nil. --func (s *snapshot) getActivePackage(id PackageID) *Package { -- s.mu.Lock() -- defer s.mu.Unlock() -- -- if value, ok := s.activePackages.Get(id); ok { -- return value +-// switch { +-// def<> +-// } +-// +-// gets parsed like: +-// +-// switch { +-// } +-// +-// Later we manually pull out the "def" token, but we need to detect +-// that our "<>" position is inside the switch block. To do that we +-// move the curly brace so it looks like: +-// +-// switch { +-// +-// } +-// +-// The resulting bool reports whether any fixing occurred. +-func fixEmptySwitch(body *ast.BlockStmt, tok *token.File, src []byte) bool { +- // We only care about empty switch statements. +- if len(body.List) > 0 || !body.Rbrace.IsValid() { +- return false - } -- return nil --} -- --// setActivePackage checks if pkg is active, and if so either records it in --// the active packages map or returns the existing memoized active package for id. --func (s *snapshot) setActivePackage(id PackageID, pkg *Package) { -- s.mu.Lock() -- defer s.mu.Unlock() - -- if _, ok := s.activePackages.Get(id); ok { -- return // already memoized +- // If the right brace is actually in the source code at the +- // specified position, don't mess with it. +- braceOffset, err := safetoken.Offset(tok, body.Rbrace) +- if err != nil { +- return false +- } +- if braceOffset < len(src) && src[braceOffset] == '}' { +- return false - } - -- if containsOpenFileLocked(s, pkg.Metadata()) { -- s.activePackages.Set(id, pkg, nil) -- } else { -- s.activePackages.Set(id, (*Package)(nil), nil) // remember that pkg is not open +- braceLine := safetoken.Line(tok, body.Rbrace) +- if braceLine >= tok.LineCount() { +- // If we are the last line in the file, no need to fix anything. +- return false - } --} - --func (s *snapshot) resetActivePackagesLocked() { -- s.activePackages.Destroy() -- s.activePackages = new(persistent.Map[PackageID, *Package]) +- // Move the right brace down one line. +- body.Rbrace = tok.LineStart(braceLine + 1) +- return true -} - --func (s *snapshot) fileWatchingGlobPatterns(ctx context.Context) map[string]struct{} { -- extensions := "go,mod,sum,work" -- for _, ext := range s.Options().TemplateExtensions { -- extensions += "," + ext -- } -- // Work-around microsoft/vscode#100870 by making sure that we are, -- // at least, watching the user's entire workspace. This will still be -- // applied to every folder in the workspace. -- patterns := map[string]struct{}{ -- fmt.Sprintf("**/*.{%s}", extensions): {}, +-// fixDanglingSelector inserts real "_" selector expressions in place +-// of phantom "_" selectors. For example: +-// +-// func _() { +-// x.<> +-// } +-// +-// var x struct { i int } +-// +-// To fix completion at "<>", we insert a real "_" after the "." so the +-// following declaration of "x" can be parsed and type checked +-// normally. +-func fixDanglingSelector(s *ast.SelectorExpr, tf *token.File, src []byte) []byte { +- if !isPhantomUnderscore(s.Sel, tf, src) { +- return nil - } - -- // If GOWORK is outside the folder, ensure we are watching it. -- gowork, _ := s.view.GOWORK() -- if gowork != "" && !source.InDir(s.view.folder.Dir.Filename(), gowork.Filename()) { -- patterns[gowork.Filename()] = struct{}{} +- if !s.X.End().IsValid() { +- return nil - } - -- // Add a pattern for each Go module in the workspace that is not within the view. -- dirs := s.workspaceDirs(ctx) -- for _, dir := range dirs { -- // If the directory is within the view's folder, we're already watching -- // it with the first pattern above. -- if source.InDir(s.view.folder.Dir.Filename(), dir) { -- continue -- } -- // TODO(rstambler): If microsoft/vscode#3025 is resolved before -- // microsoft/vscode#101042, we will need a work-around for Windows -- // drive letter casing. -- patterns[fmt.Sprintf("%s/**/*.{%s}", dir, extensions)] = struct{}{} +- insertOffset, err := safetoken.Offset(tf, s.X.End()) +- if err != nil { +- return nil - } -- -- if s.watchSubdirs() { -- // Some clients (e.g. VS Code) do not send notifications for changes to -- // directories that contain Go code (golang/go#42348). To handle this, -- // explicitly watch all of the directories in the workspace. We find them -- // by adding the directories of every file in the snapshot's workspace -- // directories. There may be thousands of patterns, each a single -- // directory. -- // -- // We compute this set by looking at files that we've previously observed. -- // This may miss changed to directories that we haven't observed, but that -- // shouldn't matter as there is nothing to invalidate (if a directory falls -- // in forest, etc). -- // -- // (A previous iteration created a single glob pattern holding a union of -- // all the directories, but this was found to cause VS Code to get stuck -- // for several minutes after a buffer was saved twice in a workspace that -- // had >8000 watched directories.) -- // -- // Some clients (notably coc.nvim, which uses watchman for globs) perform -- // poorly with a large list of individual directories. -- s.addKnownSubdirs(patterns, dirs) +- // Insert directly after the selector's ".". +- insertOffset++ +- if src[insertOffset-1] != '.' { +- return nil - } - -- return patterns --} -- --func (s *snapshot) addKnownSubdirs(patterns map[string]unit, wsDirs []string) { -- s.mu.Lock() -- defer s.mu.Unlock() -- -- s.files.Dirs().Range(func(dir string) { -- for _, wsDir := range wsDirs { -- if source.InDir(wsDir, dir) { -- patterns[dir] = unit{} -- } -- } -- }) +- var buf bytes.Buffer +- buf.Grow(len(src) + 1) +- buf.Write(src[:insertOffset]) +- buf.WriteByte('_') +- buf.Write(src[insertOffset:]) +- return buf.Bytes() -} - --// workspaceDirs returns the workspace directories for the loaded modules. +-// fixPhantomSelector tries to fix selector expressions with phantom +-// "_" selectors. In particular, we check if the selector is a +-// keyword, and if so we swap in an *ast.Ident with the keyword text. For example: -// --// A workspace directory is, roughly speaking, a directory for which we care --// about file changes. --func (s *snapshot) workspaceDirs(ctx context.Context) []string { -- dirSet := make(map[string]unit) -- -- // Dirs should, at the very least, contain the working directory and folder. -- dirSet[s.view.goCommandDir.Filename()] = unit{} -- dirSet[s.view.folder.Dir.Filename()] = unit{} +-// foo.var +-// +-// yields a "_" selector instead of "var" since "var" is a keyword. +-// +-// TODO(rfindley): should this constitute an ast 'fix'? +-// +-// The resulting bool reports whether any fixing occurred. +-func fixPhantomSelector(sel *ast.SelectorExpr, tf *token.File, src []byte) bool { +- if !isPhantomUnderscore(sel.Sel, tf, src) { +- return false +- } - -- // Additionally, if e.g. go.work indicates other workspace modules, we should -- // include their directories too. -- if s.workspaceModFilesErr == nil { -- for modFile := range s.workspaceModFiles { -- dir := filepath.Dir(modFile.Filename()) -- dirSet[dir] = unit{} -- } +- // Only consider selectors directly abutting the selector ".". This +- // avoids false positives in cases like: +- // +- // foo. // don't think "var" is our selector +- // var bar = 123 +- // +- if sel.Sel.Pos() != sel.X.End()+1 { +- return false - } -- var dirs []string -- for d := range dirSet { -- dirs = append(dirs, d) +- +- maybeKeyword := readKeyword(sel.Sel.Pos(), tf, src) +- if maybeKeyword == "" { +- return false - } -- sort.Strings(dirs) -- return dirs +- +- return replaceNode(sel, sel.Sel, &ast.Ident{ +- Name: maybeKeyword, +- NamePos: sel.Sel.Pos(), +- }) -} - --// watchSubdirs reports whether gopls should request separate file watchers for --// each relevant subdirectory. This is necessary only for clients (namely VS --// Code) that do not send notifications for individual files in a directory --// when the entire directory is deleted. --func (s *snapshot) watchSubdirs() bool { -- switch p := s.Options().SubdirWatchPatterns; p { -- case source.SubdirWatchPatternsOn: -- return true -- case source.SubdirWatchPatternsOff: -- return false -- case source.SubdirWatchPatternsAuto: -- // See the documentation of InternalOptions.SubdirWatchPatterns for an -- // explanation of why VS Code gets a different default value here. -- // -- // Unfortunately, there is no authoritative list of client names, nor any -- // requirements that client names do not change. We should update the VS -- // Code extension to set a default value of "subdirWatchPatterns" to "on", -- // so that this workaround is only temporary. -- if s.Options().ClientInfo != nil && s.Options().ClientInfo.Name == "Visual Studio Code" { -- return true -- } -- return false -- default: -- bug.Reportf("invalid subdirWatchPatterns: %q", p) +-// isPhantomUnderscore reports whether the given ident is a phantom +-// underscore. The parser sometimes inserts phantom underscores when +-// it encounters otherwise unparseable situations. +-func isPhantomUnderscore(id *ast.Ident, tok *token.File, src []byte) bool { +- if id == nil || id.Name != "_" { - return false - } --} -- --// filesInDir returns all files observed by the snapshot that are contained in --// a directory with the provided URI. --func (s *snapshot) filesInDir(uri span.URI) []span.URI { -- s.mu.Lock() -- defer s.mu.Unlock() -- -- dir := uri.Filename() -- if !s.files.Dirs().Contains(dir) { -- return nil -- } -- var files []span.URI -- s.files.Range(func(uri span.URI, _ source.FileHandle) { -- if source.InDir(dir, uri.Filename()) { -- files = append(files, uri) -- } -- }) -- return files --} -- --func (s *snapshot) WorkspaceMetadata(ctx context.Context) ([]*source.Metadata, error) { -- if err := s.awaitLoaded(ctx); err != nil { -- return nil, err -- } -- -- s.mu.Lock() -- defer s.mu.Unlock() - -- meta := make([]*source.Metadata, 0, len(s.workspacePackages)) -- for id := range s.workspacePackages { -- meta = append(meta, s.meta.metadata[id]) +- // Phantom underscore means the underscore is not actually in the +- // program text. +- offset, err := safetoken.Offset(tok, id.Pos()) +- if err != nil { +- return false - } -- return meta, nil +- return len(src) <= offset || src[offset] != '_' -} - --// Symbols extracts and returns symbol information for every file contained in --// a loaded package. It awaits snapshot loading. +-// fixInitStmt fixes cases where the parser misinterprets an +-// if/for/switch "init" statement as the "cond" conditional. In cases +-// like "if i := 0" the user hasn't typed the semicolon yet so the +-// parser is looking for the conditional expression. However, "i := 0" +-// are not valid expressions, so we get a BadExpr. -// --// TODO(rfindley): move this to the top of cache/symbols.go --func (s *snapshot) Symbols(ctx context.Context, workspaceOnly bool) (map[span.URI][]source.Symbol, error) { -- if err := s.awaitLoaded(ctx); err != nil { -- return nil, err +-// The resulting bool reports whether any fixing occurred. +-func fixInitStmt(bad *ast.BadExpr, parent ast.Node, tok *token.File, src []byte) bool { +- if !bad.Pos().IsValid() || !bad.End().IsValid() { +- return false - } - -- var ( -- meta []*source.Metadata -- err error -- ) -- if workspaceOnly { -- meta, err = s.WorkspaceMetadata(ctx) -- } else { -- meta, err = s.AllMetadata(ctx) +- // Try to extract a statement from the BadExpr. +- start, end, err := safetoken.Offsets(tok, bad.Pos(), bad.End()-1) +- if err != nil { +- return false - } +- stmtBytes := src[start : end+1] +- stmt, err := parseStmt(tok, bad.Pos(), stmtBytes) - if err != nil { -- return nil, fmt.Errorf("loading metadata: %v", err) +- return false - } - -- goFiles := make(map[span.URI]struct{}) -- for _, m := range meta { -- for _, uri := range m.GoFiles { -- goFiles[uri] = struct{}{} +- // If the parent statement doesn't already have an "init" statement, +- // move the extracted statement into the "init" field and insert a +- // dummy expression into the required "cond" field. +- switch p := parent.(type) { +- case *ast.IfStmt: +- if p.Init != nil { +- return false - } -- for _, uri := range m.CompiledGoFiles { -- goFiles[uri] = struct{}{} +- p.Init = stmt +- p.Cond = &ast.Ident{ +- Name: "_", +- NamePos: stmt.End(), - } -- } -- -- // Symbolize them in parallel. -- var ( -- group errgroup.Group -- nprocs = 2 * runtime.GOMAXPROCS(-1) // symbolize is a mix of I/O and CPU -- resultMu sync.Mutex -- result = make(map[span.URI][]source.Symbol) -- ) -- group.SetLimit(nprocs) -- for uri := range goFiles { -- uri := uri -- group.Go(func() error { -- symbols, err := s.symbolize(ctx, uri) -- if err != nil { -- return err -- } -- resultMu.Lock() -- result[uri] = symbols -- resultMu.Unlock() -- return nil -- }) -- } -- // Keep going on errors, but log the first failure. -- // Partial results are better than no symbol results. -- if err := group.Wait(); err != nil { -- event.Error(ctx, "getting snapshot symbols", err) -- } -- return result, nil --} -- --func (s *snapshot) AllMetadata(ctx context.Context) ([]*source.Metadata, error) { -- if err := s.awaitLoaded(ctx); err != nil { -- return nil, err -- } -- -- s.mu.Lock() -- g := s.meta -- s.mu.Unlock() -- -- meta := make([]*source.Metadata, 0, len(g.metadata)) -- for _, m := range g.metadata { -- meta = append(meta, m) -- } -- return meta, nil --} -- --// TODO(rfindley): clarify that this is only active modules. Or update to just --// use findRootPattern. --func (s *snapshot) GoModForFile(uri span.URI) span.URI { -- return moduleForURI(s.workspaceModFiles, uri) --} -- --func moduleForURI(modFiles map[span.URI]struct{}, uri span.URI) span.URI { -- var match span.URI -- for modURI := range modFiles { -- if !source.InDir(filepath.Dir(modURI.Filename()), uri.Filename()) { -- continue +- return true +- case *ast.ForStmt: +- if p.Init != nil { +- return false - } -- if len(modURI) > len(match) { -- match = modURI +- p.Init = stmt +- p.Cond = &ast.Ident{ +- Name: "_", +- NamePos: stmt.End(), +- } +- return true +- case *ast.SwitchStmt: +- if p.Init != nil { +- return false - } +- p.Init = stmt +- p.Tag = nil +- return true - } -- return match +- return false -} - --// nearestModFile finds the nearest go.mod file contained in the directory --// containing uri, or a parent of that directory. --// --// The given uri must be a file, not a directory. --func nearestModFile(ctx context.Context, uri span.URI, fs source.FileSource) (span.URI, error) { -- dir := filepath.Dir(uri.Filename()) -- mod, err := findRootPattern(ctx, dir, "go.mod", fs) +-// readKeyword reads the keyword starting at pos, if any. +-func readKeyword(pos token.Pos, tok *token.File, src []byte) string { +- var kwBytes []byte +- offset, err := safetoken.Offset(tok, pos) - if err != nil { -- return "", err +- return "" - } -- return span.URIFromPath(mod), nil --} -- --func (s *snapshot) Metadata(id PackageID) *source.Metadata { -- s.mu.Lock() -- defer s.mu.Unlock() -- return s.meta.metadata[id] --} -- --// clearShouldLoad clears package IDs that no longer need to be reloaded after --// scopes has been loaded. --func (s *snapshot) clearShouldLoad(scopes ...loadScope) { -- s.mu.Lock() -- defer s.mu.Unlock() +- for i := offset; i < len(src); i++ { +- // Use a simplified identifier check since keywords are always lowercase ASCII. +- if src[i] < 'a' || src[i] > 'z' { +- break +- } +- kwBytes = append(kwBytes, src[i]) - -- for _, scope := range scopes { -- switch scope := scope.(type) { -- case packageLoadScope: -- scopePath := PackagePath(scope) -- var toDelete []PackageID -- for id, pkgPaths := range s.shouldLoad { -- for _, pkgPath := range pkgPaths { -- if pkgPath == scopePath { -- toDelete = append(toDelete, id) -- } -- } -- } -- for _, id := range toDelete { -- delete(s.shouldLoad, id) -- } -- case fileLoadScope: -- uri := span.URI(scope) -- ids := s.meta.ids[uri] -- for _, id := range ids { -- delete(s.shouldLoad, id) -- } +- // Stop search at arbitrarily chosen too-long-for-a-keyword length. +- if len(kwBytes) > 15 { +- return "" - } - } --} - --func (s *snapshot) FindFile(uri span.URI) source.FileHandle { -- s.view.markKnown(uri) -- -- s.mu.Lock() -- defer s.mu.Unlock() +- if kw := string(kwBytes); token.Lookup(kw).IsKeyword() { +- return kw +- } - -- result, _ := s.files.Get(uri) -- return result +- return "" -} - --// ReadFile returns a File for the given URI. If the file is unknown it is added --// to the managed set. --// --// ReadFile succeeds even if the file does not exist. A non-nil error return --// indicates some type of internal error, for example if ctx is cancelled. --func (s *snapshot) ReadFile(ctx context.Context, uri span.URI) (source.FileHandle, error) { -- s.mu.Lock() -- defer s.mu.Unlock() +-// fixArrayType tries to parse an *ast.BadExpr into an *ast.ArrayType. +-// go/parser often turns lone array types like "[]int" into BadExprs +-// if it isn't expecting a type. +-func fixArrayType(bad *ast.BadExpr, parent ast.Node, tok *token.File, src []byte) bool { +- // Our expected input is a bad expression that looks like "[]someExpr". - -- return lockedSnapshot{s}.ReadFile(ctx, uri) --} +- from := bad.Pos() +- to := bad.End() - --// preloadFiles delegates to the view FileSource to read the requested uris in --// parallel, without holding the snapshot lock. --func (s *snapshot) preloadFiles(ctx context.Context, uris []span.URI) { -- files := make([]source.FileHandle, len(uris)) -- var wg sync.WaitGroup -- iolimit := make(chan struct{}, 20) // I/O concurrency limiting semaphore -- for i, uri := range uris { -- wg.Add(1) -- iolimit <- struct{}{} -- go func(i int, uri span.URI) { -- defer wg.Done() -- fh, err := s.view.fs.ReadFile(ctx, uri) -- <-iolimit -- if err != nil && ctx.Err() == nil { -- event.Error(ctx, fmt.Sprintf("reading %s", uri), err) -- return -- } -- files[i] = fh -- }(i, uri) +- if !from.IsValid() || !to.IsValid() { +- return false - } -- wg.Wait() - -- s.mu.Lock() -- defer s.mu.Unlock() +- exprBytes := make([]byte, 0, int(to-from)+3) +- // Avoid doing tok.Offset(to) since that panics if badExpr ends at EOF. +- // It also panics if the position is not in the range of the file, and +- // badExprs may not necessarily have good positions, so check first. +- fromOffset, toOffset, err := safetoken.Offsets(tok, from, to-1) +- if err != nil { +- return false +- } +- exprBytes = append(exprBytes, src[fromOffset:toOffset+1]...) +- exprBytes = bytes.TrimSpace(exprBytes) - -- for i, fh := range files { -- if fh == nil { -- continue // error logged above -- } -- uri := uris[i] -- if _, ok := s.files.Get(uri); !ok { -- s.files.Set(uri, fh) -- } +- // If our expression ends in "]" (e.g. "[]"), add a phantom selector +- // so we can complete directly after the "[]". +- if len(exprBytes) > 0 && exprBytes[len(exprBytes)-1] == ']' { +- exprBytes = append(exprBytes, '_') - } --} - --// A lockedSnapshot implements the source.FileSource interface while holding --// the lock for the wrapped snapshot. --type lockedSnapshot struct{ wrapped *snapshot } +- // Add "{}" to turn our ArrayType into a CompositeLit. This is to +- // handle the case of "[...]int" where we must make it a composite +- // literal to be parseable. +- exprBytes = append(exprBytes, '{', '}') - --func (s lockedSnapshot) ReadFile(ctx context.Context, uri span.URI) (source.FileHandle, error) { -- s.wrapped.view.markKnown(uri) -- if fh, ok := s.wrapped.files.Get(uri); ok { -- return fh, nil +- expr, err := parseExpr(tok, from, exprBytes) +- if err != nil { +- return false - } - -- fh, err := s.wrapped.view.fs.ReadFile(ctx, uri) -- if err != nil { -- return nil, err +- cl, _ := expr.(*ast.CompositeLit) +- if cl == nil { +- return false - } -- s.wrapped.files.Set(uri, fh) -- return fh, nil --} - --func (s *snapshot) IsOpen(uri span.URI) bool { -- s.mu.Lock() -- defer s.mu.Unlock() +- at, _ := cl.Type.(*ast.ArrayType) +- if at == nil { +- return false +- } - -- fh, _ := s.files.Get(uri) -- _, open := fh.(*Overlay) -- return open +- return replaceNode(parent, bad, at) -} - --// TODO(rfindley): it would make sense for awaitLoaded to return metadata. --func (s *snapshot) awaitLoaded(ctx context.Context) error { -- loadErr := s.awaitLoadedAllErrors(ctx) +-// precedingToken scans src to find the token preceding pos. +-func precedingToken(pos token.Pos, tok *token.File, src []byte) token.Token { +- s := &scanner.Scanner{} +- s.Init(tok, src, nil, 0) +- +- var lastTok token.Token +- for { +- p, t, _ := s.Scan() +- if t == token.EOF || p >= pos { +- break +- } - -- // TODO(rfindley): eliminate this function as part of simplifying -- // CriticalErrors. -- if loadErr != nil { -- return loadErr.MainError +- lastTok = t - } -- return nil +- return lastTok -} - --func (s *snapshot) CriticalError(ctx context.Context) *source.CriticalError { -- // If we couldn't compute workspace mod files, then the load below is -- // invalid. -- // -- // TODO(rfindley): is this a clear error to present to the user? -- if s.workspaceModFilesErr != nil { -- return &source.CriticalError{MainError: s.workspaceModFilesErr} -- } +-// fixDeferOrGoStmt tries to parse an *ast.BadStmt into a defer or a go statement. +-// +-// go/parser packages a statement of the form "defer x." as an *ast.BadStmt because +-// it does not include a call expression. This means that go/types skips type-checking +-// this statement entirely, and we can't use the type information when completing. +-// Here, we try to generate a fake *ast.DeferStmt or *ast.GoStmt to put into the AST, +-// instead of the *ast.BadStmt. +-func fixDeferOrGoStmt(bad *ast.BadStmt, parent ast.Node, tok *token.File, src []byte) bool { +- // Check if we have a bad statement containing either a "go" or "defer". +- s := &scanner.Scanner{} +- s.Init(tok, src, nil, 0) - -- loadErr := s.awaitLoadedAllErrors(ctx) -- if loadErr != nil && errors.Is(loadErr.MainError, context.Canceled) { -- return nil +- var ( +- pos token.Pos +- tkn token.Token +- ) +- for { +- if tkn == token.EOF { +- return false +- } +- if pos >= bad.From { +- break +- } +- pos, tkn, _ = s.Scan() - } - -- // Even if packages didn't fail to load, we still may want to show -- // additional warnings. -- if loadErr == nil { -- active, _ := s.WorkspaceMetadata(ctx) -- if msg := shouldShowAdHocPackagesWarning(s, active); msg != "" { -- return &source.CriticalError{ -- MainError: errors.New(msg), -- } +- var stmt ast.Stmt +- switch tkn { +- case token.DEFER: +- stmt = &ast.DeferStmt{ +- Defer: pos, - } -- // Even if workspace packages were returned, there still may be an error -- // with the user's workspace layout. Workspace packages that only have the -- // ID "command-line-arguments" are usually a symptom of a bad workspace -- // configuration. -- // -- // This heuristic is path-dependent: we only get command-line-arguments -- // packages when we've loaded using file scopes, which only occurs -- // on-demand or via orphaned file reloading. -- // -- // TODO(rfindley): re-evaluate this heuristic. -- if containsCommandLineArguments(active) { -- err, diags := s.workspaceLayoutError(ctx) -- if err != nil { -- if ctx.Err() != nil { -- return nil // see the API documentation for source.Snapshot -- } -- return &source.CriticalError{ -- MainError: err, -- Diagnostics: diags, -- } -- } +- case token.GO: +- stmt = &ast.GoStmt{ +- Go: pos, - } -- return nil +- default: +- return false - } - -- if errMsg := loadErr.MainError.Error(); strings.Contains(errMsg, "cannot find main module") || strings.Contains(errMsg, "go.mod file not found") { -- err, diags := s.workspaceLayoutError(ctx) -- if err != nil { -- if ctx.Err() != nil { -- return nil // see the API documentation for source.Snapshot +- var ( +- from, to, last token.Pos +- lastToken token.Token +- braceDepth int +- phantomSelectors []token.Pos +- ) +-FindTo: +- for { +- to, tkn, _ = s.Scan() +- +- if from == token.NoPos { +- from = to +- } +- +- switch tkn { +- case token.EOF: +- break FindTo +- case token.SEMICOLON: +- // If we aren't in nested braces, end of statement means +- // end of expression. +- if braceDepth == 0 { +- break FindTo - } -- return &source.CriticalError{ -- MainError: err, -- Diagnostics: diags, +- case token.LBRACE: +- braceDepth++ +- } +- +- // This handles the common dangling selector case. For example in +- // +- // defer fmt. +- // y := 1 +- // +- // we notice the dangling period and end our expression. +- // +- // If the previous token was a "." and we are looking at a "}", +- // the period is likely a dangling selector and needs a phantom +- // "_". Likewise if the current token is on a different line than +- // the period, the period is likely a dangling selector. +- if lastToken == token.PERIOD && (tkn == token.RBRACE || safetoken.Line(tok, to) > safetoken.Line(tok, last)) { +- // Insert phantom "_" selector after the dangling ".". +- phantomSelectors = append(phantomSelectors, last+1) +- // If we aren't in a block then end the expression after the ".". +- if braceDepth == 0 { +- to = last + 1 +- break - } - } -- } -- return loadErr --} - --// A portion of this text is expected by TestBrokenWorkspace_OutsideModule. --const adHocPackagesWarning = `You are outside of a module and outside of $GOPATH/src. --If you are using modules, please open your editor to a directory in your module. --If you believe this warning is incorrect, please file an issue: https://github.com/golang/go/issues/new.` +- lastToken = tkn +- last = to - --func shouldShowAdHocPackagesWarning(snapshot *snapshot, active []*source.Metadata) string { -- if !snapshot.validBuildConfiguration() { -- for _, m := range active { -- // A blank entry in DepsByImpPath -- // indicates a missing dependency. -- for _, importID := range m.DepsByImpPath { -- if importID == "" { -- return adHocPackagesWarning +- switch tkn { +- case token.RBRACE: +- braceDepth-- +- if braceDepth <= 0 { +- if braceDepth == 0 { +- // +1 to include the "}" itself. +- to += 1 - } +- break FindTo - } - } - } -- return "" --} - --func containsCommandLineArguments(metas []*source.Metadata) bool { -- for _, m := range metas { -- if source.IsCommandLineArguments(m.ID) { -- return true -- } +- fromOffset, toOffset, err := safetoken.Offsets(tok, from, to) +- if err != nil { +- return false - } -- return false --} -- --func (s *snapshot) awaitLoadedAllErrors(ctx context.Context) *source.CriticalError { -- // Do not return results until the snapshot's view has been initialized. -- s.AwaitInitialized(ctx) -- -- // TODO(rfindley): Should we be more careful about returning the -- // initialization error? Is it possible for the initialization error to be -- // corrected without a successful reinitialization? -- if err := s.getInitializationError(); err != nil { -- return err +- if !from.IsValid() || fromOffset >= len(src) { +- return false - } -- -- // TODO(rfindley): revisit this handling. Calling reloadWorkspace with a -- // cancelled context should have the same effect, so this preemptive handling -- // should not be necessary. -- // -- // Also: GetCriticalError ignores context cancellation errors. Should we be -- // returning nil here? -- if ctx.Err() != nil { -- return &source.CriticalError{MainError: ctx.Err()} +- if !to.IsValid() || toOffset >= len(src) { +- return false - } - -- // TODO(rfindley): reloading is not idempotent: if we try to reload or load -- // orphaned files below and fail, we won't try again. For that reason, we -- // could get different results from subsequent calls to this function, which -- // may cause critical errors to be suppressed. -- -- if err := s.reloadWorkspace(ctx); err != nil { -- diags := s.extractGoCommandErrors(ctx, err) -- return &source.CriticalError{ -- MainError: err, -- Diagnostics: diags, +- // Insert any phantom selectors needed to prevent dangling "." from messing +- // up the AST. +- exprBytes := make([]byte, 0, int(to-from)+len(phantomSelectors)) +- for i, b := range src[fromOffset:toOffset] { +- if len(phantomSelectors) > 0 && from+token.Pos(i) == phantomSelectors[0] { +- exprBytes = append(exprBytes, '_') +- phantomSelectors = phantomSelectors[1:] - } +- exprBytes = append(exprBytes, b) - } - -- if err := s.reloadOrphanedOpenFiles(ctx); err != nil { -- diags := s.extractGoCommandErrors(ctx, err) -- return &source.CriticalError{ -- MainError: err, -- Diagnostics: diags, -- } +- if len(phantomSelectors) > 0 { +- exprBytes = append(exprBytes, '_') - } -- return nil --} - --func (s *snapshot) getInitializationError() *source.CriticalError { -- s.mu.Lock() -- defer s.mu.Unlock() +- expr, err := parseExpr(tok, from, exprBytes) +- if err != nil { +- return false +- } - -- return s.initializedErr --} +- // Package the expression into a fake *ast.CallExpr and re-insert +- // into the function. +- call := &ast.CallExpr{ +- Fun: expr, +- Lparen: to, +- Rparen: to, +- } - --func (s *snapshot) AwaitInitialized(ctx context.Context) { -- select { -- case <-ctx.Done(): -- return -- case <-s.view.initialWorkspaceLoad: +- switch stmt := stmt.(type) { +- case *ast.DeferStmt: +- stmt.Call = call +- case *ast.GoStmt: +- stmt.Call = call - } -- // We typically prefer to run something as intensive as the IWL without -- // blocking. I'm not sure if there is a way to do that here. -- s.initialize(ctx, false) +- +- return replaceNode(parent, bad, stmt) -} - --// reloadWorkspace reloads the metadata for all invalidated workspace packages. --func (s *snapshot) reloadWorkspace(ctx context.Context) error { -- var scopes []loadScope -- var seen map[PackagePath]bool -- s.mu.Lock() -- for _, pkgPaths := range s.shouldLoad { -- for _, pkgPath := range pkgPaths { -- if seen == nil { -- seen = make(map[PackagePath]bool) -- } -- if seen[pkgPath] { -- continue -- } -- seen[pkgPath] = true -- scopes = append(scopes, packageLoadScope(pkgPath)) -- } +-// parseStmt parses the statement in src and updates its position to +-// start at pos. +-// +-// tok is the original file containing pos. Used to ensure that all adjusted +-// positions are valid. +-func parseStmt(tok *token.File, pos token.Pos, src []byte) (ast.Stmt, error) { +- // Wrap our expression to make it a valid Go file we can pass to ParseFile. +- fileSrc := bytes.Join([][]byte{ +- []byte("package fake;func _(){"), +- src, +- []byte("}"), +- }, nil) +- +- // Use ParseFile instead of ParseExpr because ParseFile has +- // best-effort behavior, whereas ParseExpr fails hard on any error. +- fakeFile, err := parser.ParseFile(token.NewFileSet(), "", fileSrc, 0) +- if fakeFile == nil { +- return nil, fmt.Errorf("error reading fake file source: %v", err) - } -- s.mu.Unlock() - -- if len(scopes) == 0 { -- return nil +- // Extract our expression node from inside the fake file. +- if len(fakeFile.Decls) == 0 { +- return nil, fmt.Errorf("error parsing fake file: %v", err) - } - -- // If the view's build configuration is invalid, we cannot reload by -- // package path. Just reload the directory instead. -- if !s.validBuildConfiguration() { -- scopes = []loadScope{viewLoadScope("LOAD_INVALID_VIEW")} +- fakeDecl, _ := fakeFile.Decls[0].(*ast.FuncDecl) +- if fakeDecl == nil || len(fakeDecl.Body.List) == 0 { +- return nil, fmt.Errorf("no statement in %s: %v", src, err) - } - -- err := s.load(ctx, false, scopes...) +- stmt := fakeDecl.Body.List[0] - -- // Unless the context was canceled, set "shouldLoad" to false for all -- // of the metadata we attempted to load. -- if !errors.Is(err, context.Canceled) { -- s.clearShouldLoad(scopes...) -- } +- // parser.ParseFile returns undefined positions. +- // Adjust them for the current file. +- offsetPositions(tok, stmt, pos-1-(stmt.Pos()-1)) - -- return err +- return stmt, nil -} - --// reloadOrphanedOpenFiles attempts to load a package for each open file that --// does not yet have an associated package. If loading finishes without being --// canceled, any files still not contained in a package are marked as unloadable. --// --// An error is returned if the load is canceled. --func (s *snapshot) reloadOrphanedOpenFiles(ctx context.Context) error { -- s.mu.Lock() -- meta := s.meta -- s.mu.Unlock() -- // When we load ./... or a package path directly, we may not get packages -- // that exist only in overlays. As a workaround, we search all of the files -- // available in the snapshot and reload their metadata individually using a -- // file= query if the metadata is unavailable. -- open := s.overlays() -- var files []*Overlay -- for _, o := range open { -- uri := o.URI() -- if s.IsBuiltin(uri) || s.FileKind(o) != source.Go { -- continue -- } -- if len(meta.ids[uri]) == 0 { -- files = append(files, o) -- } +-// parseExpr parses the expression in src and updates its position to +-// start at pos. +-func parseExpr(tok *token.File, pos token.Pos, src []byte) (ast.Expr, error) { +- stmt, err := parseStmt(tok, pos, src) +- if err != nil { +- return nil, err - } - -- // Filter to files that are not known to be unloadable. -- s.mu.Lock() -- loadable := files[:0] -- for _, file := range files { -- if !s.unloadableFiles.Contains(file.URI()) { -- loadable = append(loadable, file) -- } +- exprStmt, ok := stmt.(*ast.ExprStmt) +- if !ok { +- return nil, fmt.Errorf("no expr in %s: %v", src, err) - } -- files = loadable -- s.mu.Unlock() - -- if len(files) == 0 { -- return nil -- } +- return exprStmt.X, nil +-} - -- var uris []span.URI -- for _, file := range files { -- uris = append(uris, file.URI()) -- } +-var tokenPosType = reflect.TypeOf(token.NoPos) - -- event.Log(ctx, "reloadOrphanedFiles reloading", tag.Files.Of(uris)) +-// offsetPositions applies an offset to the positions in an ast.Node. +-func offsetPositions(tok *token.File, n ast.Node, offset token.Pos) { +- fileBase := int64(tok.Base()) +- fileEnd := fileBase + int64(tok.Size()) +- ast.Inspect(n, func(n ast.Node) bool { +- if n == nil { +- return false +- } - -- var g errgroup.Group +- v := reflect.ValueOf(n).Elem() - -- cpulimit := runtime.GOMAXPROCS(0) -- g.SetLimit(cpulimit) +- switch v.Kind() { +- case reflect.Struct: +- for i := 0; i < v.NumField(); i++ { +- f := v.Field(i) +- if f.Type() != tokenPosType { +- continue +- } - -- // Load files one-at-a-time. go/packages can return at most one -- // command-line-arguments package per query. -- for _, file := range files { -- file := file -- g.Go(func() error { -- return s.load(ctx, false, fileLoadScope(file.URI())) -- }) -- } +- if !f.CanSet() { +- continue +- } - -- // If we failed to load some files, i.e. they have no metadata, -- // mark the failures so we don't bother retrying until the file's -- // content changes. -- // -- // TODO(rfindley): is it possible that the load stopped early for an -- // unrelated errors? If so, add a fallback? +- // Don't offset invalid positions: they should stay invalid. +- if !token.Pos(f.Int()).IsValid() { +- continue +- } - -- if err := g.Wait(); err != nil { -- // Check for context cancellation so that we don't incorrectly mark files -- // as unloadable, but don't return before setting all workspace packages. -- if ctx.Err() != nil { -- return ctx.Err() +- // Clamp value to valid range; see #64335. +- // +- // TODO(golang/go#64335): this is a hack, because our fixes should not +- // produce positions that overflow (but they do: golang/go#64488). +- pos := f.Int() + int64(offset) +- if pos < fileBase { +- pos = fileBase +- } +- if pos > fileEnd { +- pos = fileEnd +- } +- f.SetInt(pos) +- } - } - -- if !errors.Is(err, errNoPackages) { -- event.Error(ctx, "reloadOrphanedFiles: failed to load", err, tag.Files.Of(uris)) -- } -- } +- return true +- }) +-} - -- // If the context was not canceled, we assume that the result of loading -- // packages is deterministic (though as we saw in golang/go#59318, it may not -- // be in the presence of bugs). Marking all unloaded files as unloadable here -- // prevents us from falling into recursive reloading where we only make a bit -- // of progress each time. -- s.mu.Lock() -- defer s.mu.Unlock() -- for _, file := range files { -- // TODO(rfindley): instead of locking here, we should have load return the -- // metadata graph that resulted from loading. -- uri := file.URI() -- if len(s.meta.ids[uri]) == 0 { -- s.unloadableFiles.Add(uri) -- } +-// replaceNode updates parent's child oldChild to be newChild. It +-// returns whether it replaced successfully. +-func replaceNode(parent, oldChild, newChild ast.Node) bool { +- if parent == nil || oldChild == nil || newChild == nil { +- return false - } - -- return nil --} -- --// OrphanedFileDiagnostics reports diagnostics describing why open files have --// no packages or have only command-line-arguments packages. --// --// If the resulting diagnostic is nil, the file is either not orphaned or we --// can't produce a good diagnostic. --// --// TODO(rfindley): reconcile the definition of "orphaned" here with --// reloadOrphanedFiles. The latter does not include files with --// command-line-arguments packages. --func (s *snapshot) OrphanedFileDiagnostics(ctx context.Context) (map[span.URI]*source.Diagnostic, error) { -- if err := s.awaitLoaded(ctx); err != nil { -- return nil, err +- parentVal := reflect.ValueOf(parent).Elem() +- if parentVal.Kind() != reflect.Struct { +- return false - } - -- var files []*Overlay +- newChildVal := reflect.ValueOf(newChild) - --searchOverlays: -- for _, o := range s.overlays() { -- uri := o.URI() -- if s.IsBuiltin(uri) || s.FileKind(o) != source.Go { -- continue -- } -- md, err := s.MetadataForFile(ctx, uri) -- if err != nil { -- return nil, err +- tryReplace := func(v reflect.Value) bool { +- if !v.CanSet() || !v.CanInterface() { +- return false - } -- for _, m := range md { -- if !source.IsCommandLineArguments(m.ID) || m.Standalone { -- continue searchOverlays -- } +- +- // If the existing value is oldChild, we found our child. Make +- // sure our newChild is assignable and then make the swap. +- if v.Interface() == oldChild && newChildVal.Type().AssignableTo(v.Type()) { +- v.Set(newChildVal) +- return true - } -- files = append(files, o) -- } -- if len(files) == 0 { -- return nil, nil +- +- return false - } - -- loadedModFiles := make(map[span.URI]struct{}) // all mod files, including dependencies -- ignoredFiles := make(map[span.URI]bool) // files reported in packages.Package.IgnoredFiles +- // Loop over parent's struct fields. +- for i := 0; i < parentVal.NumField(); i++ { +- f := parentVal.Field(i) - -- meta, err := s.AllMetadata(ctx) -- if err != nil { -- return nil, err -- } +- switch f.Kind() { +- // Check interface and pointer fields. +- case reflect.Interface, reflect.Ptr: +- if tryReplace(f) { +- return true +- } - -- for _, meta := range meta { -- if meta.Module != nil && meta.Module.GoMod != "" { -- gomod := span.URIFromPath(meta.Module.GoMod) -- loadedModFiles[gomod] = struct{}{} -- } -- for _, ignored := range meta.IgnoredFiles { -- ignoredFiles[ignored] = true +- // Search through any slice fields. +- case reflect.Slice: +- for i := 0; i < f.Len(); i++ { +- if tryReplace(f.Index(i)) { +- return true +- } +- } - } - } - -- diagnostics := make(map[span.URI]*source.Diagnostic) -- for _, fh := range files { -- // Only warn about orphaned files if the file is well-formed enough to -- // actually be part of a package. -- // -- // Use ParseGo as for open files this is likely to be a cache hit (we'll have ) -- pgf, err := s.ParseGo(ctx, fh, source.ParseHeader) -- if err != nil { -- continue -- } -- if !pgf.File.Name.Pos().IsValid() { -- continue -- } -- rng, err := pgf.PosRange(pgf.File.Name.Pos(), pgf.File.Name.End()) -- if err != nil { -- continue -- } -- -- var ( -- msg string // if non-empty, report a diagnostic with this message -- suggestedFixes []source.SuggestedFix // associated fixes, if any -- ) +- return false +-} +diff -urN a/gopls/internal/cache/parsego/parse_test.go b/gopls/internal/cache/parsego/parse_test.go +--- a/gopls/internal/cache/parsego/parse_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/parsego/parse_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,46 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- // If we have a relevant go.mod file, check whether the file is orphaned -- // due to its go.mod file being inactive. We could also offer a -- // prescriptive diagnostic in the case that there is no go.mod file, but it -- // is harder to be precise in that case, and less important. -- if goMod, err := nearestModFile(ctx, fh.URI(), s); err == nil && goMod != "" { -- if _, ok := loadedModFiles[goMod]; !ok { -- modDir := filepath.Dir(goMod.Filename()) -- viewDir := s.view.folder.Dir.Filename() +-package parsego_test - -- // When the module is underneath the view dir, we offer -- // "use all modules" quick-fixes. -- inDir := source.InDir(viewDir, modDir) +-import ( +- "context" +- "go/ast" +- "go/token" +- "testing" - -- if rel, err := filepath.Rel(viewDir, modDir); err == nil { -- modDir = rel -- } +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/internal/tokeninternal" +-) - -- var fix string -- if s.view.goversion >= 18 { -- if s.view.gowork != "" { -- fix = fmt.Sprintf("To fix this problem, you can add this module to your go.work file (%s)", s.view.gowork) -- if cmd, err := command.NewRunGoWorkCommandCommand("Run `go work use`", command.RunGoWorkArgs{ -- ViewID: s.view.ID(), -- Args: []string{"use", modDir}, -- }); err == nil { -- suggestedFixes = append(suggestedFixes, source.SuggestedFix{ -- Title: "Use this module in your go.work file", -- Command: &cmd, -- ActionKind: protocol.QuickFix, -- }) -- } +-// TODO(golang/go#64335): we should have many more tests for fixed syntax. - -- if inDir { -- if cmd, err := command.NewRunGoWorkCommandCommand("Run `go work use -r`", command.RunGoWorkArgs{ -- ViewID: s.view.ID(), -- Args: []string{"use", "-r", "."}, -- }); err == nil { -- suggestedFixes = append(suggestedFixes, source.SuggestedFix{ -- Title: "Use all modules in your workspace", -- Command: &cmd, -- ActionKind: protocol.QuickFix, -- }) -- } -- } -- } else { -- fix = "To fix this problem, you can add a go.work file that uses this directory." +-func TestFixPosition_Issue64488(t *testing.T) { +- // This test reproduces the conditions of golang/go#64488, where a type error +- // on fixed syntax overflows the token.File. +- const src = ` +-package foo - -- if cmd, err := command.NewRunGoWorkCommandCommand("Run `go work init && go work use`", command.RunGoWorkArgs{ -- ViewID: s.view.ID(), -- InitFirst: true, -- Args: []string{"use", modDir}, -- }); err == nil { -- suggestedFixes = []source.SuggestedFix{ -- { -- Title: "Add a go.work file using this module", -- Command: &cmd, -- ActionKind: protocol.QuickFix, -- }, -- } -- } +-func _() { +- type myThing struct{} +- var foo []myThing +- for ${1:}, ${2:} := range foo { +- $0 +-} +-} +-` - -- if inDir { -- if cmd, err := command.NewRunGoWorkCommandCommand("Run `go work init && go work use -r`", command.RunGoWorkArgs{ -- ViewID: s.view.ID(), -- InitFirst: true, -- Args: []string{"use", "-r", "."}, -- }); err == nil { -- suggestedFixes = append(suggestedFixes, source.SuggestedFix{ -- Title: "Add a go.work file using all modules in your workspace", -- Command: &cmd, -- ActionKind: protocol.QuickFix, -- }) -- } -- } -- } -- } else { -- fix = `To work with multiple modules simultaneously, please upgrade to Go 1.18 or --later, reinstall gopls, and use a go.work file.` -- } -- msg = fmt.Sprintf(`This file is within module %q, which is not included in your workspace. --%s --See the documentation for more information on setting up your workspace: --https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.`, modDir, fix) +- pgf, _ := parsego.Parse(context.Background(), token.NewFileSet(), "file://foo.go", []byte(src), parsego.Full, false) +- fset := tokeninternal.FileSetFor(pgf.Tok) +- ast.Inspect(pgf.File, func(n ast.Node) bool { +- if n != nil { +- posn := safetoken.StartPosition(fset, n.Pos()) +- if !posn.IsValid() { +- t.Fatalf("invalid position for %T (%v): %v not in [%d, %d]", n, n, n.Pos(), pgf.Tok.Base(), pgf.Tok.Base()+pgf.Tok.Size()) - } - } +- return true +- }) +-} +diff -urN a/gopls/internal/cache/port.go b/gopls/internal/cache/port.go +--- a/gopls/internal/cache/port.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/port.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,204 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- if msg == "" && ignoredFiles[fh.URI()] { -- // TODO(rfindley): use the constraint package to check if the file -- // _actually_ satisfies the current build context. -- hasConstraint := false -- walkConstraints(pgf.File, func(constraint.Expr) bool { -- hasConstraint = true -- return false -- }) -- var fix string -- if hasConstraint { -- fix = `This file may be excluded due to its build tags; try adding "-tags=" to your gopls "buildFlags" configuration --See the documentation for more information on working with build tags: --https://github.com/golang/tools/blob/master/gopls/doc/settings.md#buildflags-string.` -- } else if strings.Contains(filepath.Base(fh.URI().Filename()), "_") { -- fix = `This file may be excluded due to its GOOS/GOARCH, or other build constraints.` -- } else { -- fix = `This file is ignored by your gopls build.` // we don't know why -- } -- msg = fmt.Sprintf("No packages found for open file %s.\n%s", fh.URI().Filename(), fix) -- } +-package cache - -- if msg != "" { -- d := &source.Diagnostic{ -- URI: fh.URI(), -- Range: rng, -- Severity: protocol.SeverityWarning, -- Source: source.ListError, -- Message: msg, -- SuggestedFixes: suggestedFixes, -- } -- if ok := source.BundleQuickFixes(d); !ok { -- bug.Reportf("failed to bundle quick fixes for %v", d) +-import ( +- "bytes" +- "go/build" +- "go/parser" +- "go/token" +- "io" +- "path/filepath" +- "strings" +- +- "golang.org/x/tools/gopls/internal/util/bug" +-) +- +-type port struct{ GOOS, GOARCH string } +- +-var ( +- // preferredPorts holds GOOS/GOARCH combinations for which we dynamically +- // create new Views, by setting GOOS=... and GOARCH=... on top of +- // user-provided configuration when we detect that the default build +- // configuration does not match an open file. Ports are matched in the order +- // defined below, so that when multiple ports match a file we use the port +- // occurring at a lower index in the slice. For that reason, we sort first +- // class ports ahead of secondary ports, and (among first class ports) 64-bit +- // ports ahead of the less common 32-bit ports. +- preferredPorts = []port{ +- // First class ports, from https://go.dev/wiki/PortingPolicy. +- {"darwin", "amd64"}, +- {"darwin", "arm64"}, +- {"linux", "amd64"}, +- {"linux", "arm64"}, +- {"windows", "amd64"}, +- {"linux", "arm"}, +- {"linux", "386"}, +- {"windows", "386"}, +- +- // Secondary ports, from GOROOT/src/internal/platform/zosarch.go. +- // (First class ports are commented out.) +- {"aix", "ppc64"}, +- {"dragonfly", "amd64"}, +- {"freebsd", "386"}, +- {"freebsd", "amd64"}, +- {"freebsd", "arm"}, +- {"freebsd", "arm64"}, +- {"illumos", "amd64"}, +- {"linux", "ppc64"}, +- {"linux", "ppc64le"}, +- {"linux", "mips"}, +- {"linux", "mipsle"}, +- {"linux", "mips64"}, +- {"linux", "mips64le"}, +- {"linux", "riscv64"}, +- {"linux", "s390x"}, +- {"android", "386"}, +- {"android", "amd64"}, +- {"android", "arm"}, +- {"android", "arm64"}, +- {"ios", "arm64"}, +- {"ios", "amd64"}, +- {"js", "wasm"}, +- {"netbsd", "386"}, +- {"netbsd", "amd64"}, +- {"netbsd", "arm"}, +- {"netbsd", "arm64"}, +- {"openbsd", "386"}, +- {"openbsd", "amd64"}, +- {"openbsd", "arm"}, +- {"openbsd", "arm64"}, +- {"openbsd", "mips64"}, +- {"plan9", "386"}, +- {"plan9", "amd64"}, +- {"plan9", "arm"}, +- {"solaris", "amd64"}, +- {"windows", "arm"}, +- {"windows", "arm64"}, +- +- {"aix", "ppc64"}, +- {"android", "386"}, +- {"android", "amd64"}, +- {"android", "arm"}, +- {"android", "arm64"}, +- // {"darwin", "amd64"}, +- // {"darwin", "arm64"}, +- {"dragonfly", "amd64"}, +- {"freebsd", "386"}, +- {"freebsd", "amd64"}, +- {"freebsd", "arm"}, +- {"freebsd", "arm64"}, +- {"freebsd", "riscv64"}, +- {"illumos", "amd64"}, +- {"ios", "amd64"}, +- {"ios", "arm64"}, +- {"js", "wasm"}, +- // {"linux", "386"}, +- // {"linux", "amd64"}, +- // {"linux", "arm"}, +- // {"linux", "arm64"}, +- {"linux", "loong64"}, +- {"linux", "mips"}, +- {"linux", "mips64"}, +- {"linux", "mips64le"}, +- {"linux", "mipsle"}, +- {"linux", "ppc64"}, +- {"linux", "ppc64le"}, +- {"linux", "riscv64"}, +- {"linux", "s390x"}, +- {"linux", "sparc64"}, +- {"netbsd", "386"}, +- {"netbsd", "amd64"}, +- {"netbsd", "arm"}, +- {"netbsd", "arm64"}, +- {"openbsd", "386"}, +- {"openbsd", "amd64"}, +- {"openbsd", "arm"}, +- {"openbsd", "arm64"}, +- {"openbsd", "mips64"}, +- {"openbsd", "ppc64"}, +- {"openbsd", "riscv64"}, +- {"plan9", "386"}, +- {"plan9", "amd64"}, +- {"plan9", "arm"}, +- {"solaris", "amd64"}, +- {"wasip1", "wasm"}, +- // {"windows", "386"}, +- // {"windows", "amd64"}, +- {"windows", "arm"}, +- {"windows", "arm64"}, +- } +-) +- +-// matches reports whether the port matches a file with the given absolute path +-// and content. +-// +-// Note that this function accepts content rather than e.g. a file.Handle, +-// because we trim content before matching for performance reasons, and +-// therefore need to do this outside of matches when considering multiple ports. +-func (p port) matches(path string, content []byte) bool { +- ctxt := build.Default // make a copy +- ctxt.UseAllFiles = false +- dir, name := filepath.Split(path) +- +- // The only virtualized operation called by MatchFile is OpenFile. +- ctxt.OpenFile = func(p string) (io.ReadCloser, error) { +- if p != path { +- return nil, bug.Errorf("unexpected file %q", p) +- } +- return io.NopCloser(bytes.NewReader(content)), nil +- } +- +- ctxt.GOOS = p.GOOS +- ctxt.GOARCH = p.GOARCH +- ok, err := ctxt.MatchFile(dir, name) +- return err == nil && ok +-} +- +-// trimContentForPortMatch trims the given Go file content to a minimal file +-// containing the same build constraints, if any. +-// +-// This is an unfortunate but necessary optimization, as matching build +-// constraints using go/build has significant overhead, and involves parsing +-// more than just the build constraint. +-// +-// TestMatchingPortsConsistency enforces consistency by comparing results +-// without trimming content. +-func trimContentForPortMatch(content []byte) []byte { +- buildComment := buildComment(content) +- return []byte(buildComment + "\npackage p") // package name does not matter +-} +- +-// buildComment returns the first matching //go:build comment in the given +-// content, or "" if none exists. +-func buildComment(content []byte) string { +- f, err := parser.ParseFile(token.NewFileSet(), "", content, parser.PackageClauseOnly|parser.ParseComments) +- if err != nil { +- return "" +- } +- +- for _, cg := range f.Comments { +- for _, c := range cg.List { +- if isGoBuildComment(c.Text) { +- return c.Text - } -- // Only report diagnostics if we detect an actual exclusion. -- diagnostics[fh.URI()] = d - } - } -- return diagnostics, nil +- return "" -} - --// TODO(golang/go#53756): this function needs to consider more than just the --// absolute URI, for example: --// - the position of /vendor/ with respect to the relevant module root --// - whether or not go.work is in use (as vendoring isn't supported in workspace mode) +-// Adapted from go/build/build.go. -// --// Most likely, each call site of inVendor needs to be reconsidered to --// understand and correctly implement the desired behavior. --func inVendor(uri span.URI) bool { -- _, after, found := strings.Cut(string(uri), "/vendor/") -- // Only subdirectories of /vendor/ are considered vendored -- // (/vendor/a/foo.go is vendored, /vendor/foo.go is not). -- return found && strings.Contains(after, "/") +-// TODO(rfindley): use constraint.IsGoBuild once we are on 1.19+. +-func isGoBuildComment(line string) bool { +- const goBuildComment = "//go:build" +- if !strings.HasPrefix(line, goBuildComment) { +- return false +- } +- // Report whether //go:build is followed by a word boundary. +- line = strings.TrimSpace(line) +- rest := line[len(goBuildComment):] +- return len(rest) == 0 || len(strings.TrimSpace(rest)) < len(rest) -} +diff -urN a/gopls/internal/cache/port_test.go b/gopls/internal/cache/port_test.go +--- a/gopls/internal/cache/port_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/port_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,124 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func (s *snapshot) clone(ctx context.Context, changes map[span.URI]source.FileHandle) (*snapshot, func()) { -- ctx, done := event.Start(ctx, "cache.snapshot.clone") -- defer done() +-package cache - -- s.mu.Lock() -- defer s.mu.Unlock() +-import ( +- "os" +- "testing" - -- backgroundCtx, cancel := context.WithCancel(event.Detach(xcontext.Detach(s.backgroundCtx))) -- result := &snapshot{ -- sequenceID: s.sequenceID + 1, -- globalID: nextSnapshotID(), -- store: s.store, -- view: s.view, -- backgroundCtx: backgroundCtx, -- cancel: cancel, -- builtin: s.builtin, -- initialized: s.initialized, -- initializedErr: s.initializedErr, -- packages: s.packages.Clone(), -- activePackages: s.activePackages.Clone(), -- files: s.files.Clone(changes), -- symbolizeHandles: cloneWithout(s.symbolizeHandles, changes), -- workspacePackages: make(map[PackageID]PackagePath, len(s.workspacePackages)), -- unloadableFiles: s.unloadableFiles.Clone(), // not cloneWithout: typing in a file doesn't necessarily make it loadable -- parseModHandles: cloneWithout(s.parseModHandles, changes), -- parseWorkHandles: cloneWithout(s.parseWorkHandles, changes), -- modTidyHandles: cloneWithout(s.modTidyHandles, changes), -- modWhyHandles: cloneWithout(s.modWhyHandles, changes), -- modVulnHandles: cloneWithout(s.modVulnHandles, changes), -- workspaceModFiles: s.workspaceModFiles, -- workspaceModFilesErr: s.workspaceModFilesErr, -- importGraph: s.importGraph, -- pkgIndex: s.pkgIndex, -- } -- -- // Create a lease on the new snapshot. -- // (Best to do this early in case the code below hides an -- // incref/decref operation that might destroy it prematurely.) -- release := result.Acquire() +- "github.com/google/go-cmp/cmp" +- "golang.org/x/sync/errgroup" +- "golang.org/x/tools/go/packages" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/internal/testenv" +-) - -- reinit := false +-func TestMain(m *testing.M) { +- bug.PanicOnBugs = true +- os.Exit(m.Run()) +-} - -- // Changes to vendor tree may require reinitialization, -- // either because of an initialization error -- // (e.g. "inconsistent vendoring detected"), or because -- // one or more modules may have moved into or out of the -- // vendor tree after 'go mod vendor' or 'rm -fr vendor/'. -- // -- // TODO(rfindley): revisit the location of this check. -- for uri := range changes { -- if inVendor(uri) && s.initializedErr != nil || -- strings.HasSuffix(string(uri), "/vendor/modules.txt") { -- reinit = true -- break -- } +-func TestMatchingPortsStdlib(t *testing.T) { +- // This test checks that we don't encounter a bug when matching ports, and +- // sanity checks that the optimization to use trimmed/fake file content +- // before delegating to go/build.Context.MatchFile does not affect +- // correctness. +- if testing.Short() { +- t.Skip("skipping in short mode: takes to long on slow file systems") - } - -- // Collect observed file handles for changed URIs from the old snapshot, if -- // they exist. Importantly, we don't call ReadFile here: consider the case -- // where a file is added on disk; we don't want to read the newly added file -- // into the old snapshot, as that will break our change detection below. -- oldFiles := make(map[span.URI]source.FileHandle) -- for uri := range changes { -- if fh, ok := s.files.Get(uri); ok { -- oldFiles[uri] = fh -- } +- testenv.NeedsTool(t, "go") +- +- // Load, parse and type-check the program. +- cfg := &packages.Config{ +- Mode: packages.LoadFiles, +- Tests: true, - } -- // changedOnDisk determines if the new file handle may have changed on disk. -- // It over-approximates, returning true if the new file is saved and either -- // the old file wasn't saved, or the on-disk contents changed. -- // -- // oldFH may be nil. -- changedOnDisk := func(oldFH, newFH source.FileHandle) bool { -- if !newFH.SameContentsOnDisk() { -- return false -- } -- if oe, ne := (oldFH != nil && fileExists(oldFH)), fileExists(newFH); !oe || !ne { -- return oe != ne -- } -- return !oldFH.SameContentsOnDisk() || oldFH.FileIdentity() != newFH.FileIdentity() +- pkgs, err := packages.Load(cfg, "std", "cmd") +- if err != nil { +- t.Fatal(err) - } - -- if workURI, _ := s.view.GOWORK(); workURI != "" { -- if newFH, ok := changes[workURI]; ok { -- result.workspaceModFiles, result.workspaceModFilesErr = computeWorkspaceModFiles(ctx, s.view.gomod, workURI, s.view.effectiveGO111MODULE(), result) -- if changedOnDisk(oldFiles[workURI], newFH) { -- reinit = true -- } +- var g errgroup.Group +- packages.Visit(pkgs, nil, func(pkg *packages.Package) { +- for _, f := range pkg.CompiledGoFiles { +- f := f +- g.Go(func() error { +- content, err := os.ReadFile(f) +- // We report errors via t.Error, not by returning, +- // so that a single test can report multiple test failures. +- if err != nil { +- t.Errorf("failed to read %s: %v", f, err) +- return nil +- } +- fh := makeFakeFileHandle(protocol.URIFromPath(f), content) +- fastPorts := matchingPreferredPorts(t, fh, true) +- slowPorts := matchingPreferredPorts(t, fh, false) +- if diff := cmp.Diff(fastPorts, slowPorts); diff != "" { +- t.Errorf("%s: ports do not match (-trimmed +untrimmed):\n%s", f, diff) +- return nil +- } +- return nil +- }) - } -- } +- }) +- g.Wait() +-} - -- // Reinitialize if any workspace mod file has changed on disk. -- for uri, newFH := range changes { -- if _, ok := result.workspaceModFiles[uri]; ok && changedOnDisk(oldFiles[uri], newFH) { -- reinit = true -- } +-func matchingPreferredPorts(tb testing.TB, fh file.Handle, trimContent bool) map[port]unit { +- content, err := fh.Content() +- if err != nil { +- tb.Fatal(err) - } -- -- // Finally, process sumfile changes that may affect loading. -- for uri, newFH := range changes { -- if !changedOnDisk(oldFiles[uri], newFH) { -- continue // like with go.mod files, we only reinit when things change on disk -- } -- dir, base := filepath.Split(uri.Filename()) -- if base == "go.work.sum" && s.view.gowork != "" { -- if dir == filepath.Dir(s.view.gowork) { -- reinit = true -- } -- } -- if base == "go.sum" { -- modURI := span.URIFromPath(filepath.Join(dir, "go.mod")) -- if _, active := result.workspaceModFiles[modURI]; active { -- reinit = true -- } +- if trimContent { +- content = trimContentForPortMatch(content) +- } +- path := fh.URI().Path() +- matching := make(map[port]unit) +- for _, port := range preferredPorts { +- if port.matches(path, content) { +- matching[port] = unit{} - } - } +- return matching +-} - -- // The snapshot should be initialized if either s was uninitialized, or we've -- // detected a change that triggers reinitialization. -- if reinit { -- result.initialized = false +-func BenchmarkMatchingPreferredPorts(b *testing.B) { +- // Copy of robustio_posix.go +- const src = ` +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-//go:build unix +-// +build unix +- +-package robustio +- +-import ( +- "os" +- "syscall" +- "time" +-) +- +-func getFileID(filename string) (FileID, time.Time, error) { +- fi, err := os.Stat(filename) +- if err != nil { +- return FileID{}, time.Time{}, err +- } +- stat := fi.Sys().(*syscall.Stat_t) +- return FileID{ +- device: uint64(stat.Dev), // (int32 on darwin, uint64 on linux) +- inode: stat.Ino, +- }, fi.ModTime(), nil +-} +-` +- fh := makeFakeFileHandle("file:///path/to/test/file.go", []byte(src)) +- for i := 0; i < b.N; i++ { +- _ = matchingPreferredPorts(b, fh, true) - } +-} +diff -urN a/gopls/internal/cache/session.go b/gopls/internal/cache/session.go +--- a/gopls/internal/cache/session.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/session.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,1182 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- // directIDs keeps track of package IDs that have directly changed. -- // Note: this is not a set, it's a map from id to invalidateMetadata. -- directIDs := map[PackageID]bool{} +-package cache - -- // Invalidate all package metadata if the workspace module has changed. -- if reinit { -- for k := range s.meta.metadata { -- // TODO(rfindley): this seems brittle; can we just start over? -- directIDs[k] = true -- } +-import ( +- "context" +- "errors" +- "fmt" +- "os" +- "path/filepath" +- "sort" +- "strconv" +- "strings" +- "sync" +- "sync/atomic" +- "time" +- +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/cache/typerefs" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/gopls/internal/util/persistent" +- "golang.org/x/tools/gopls/internal/util/slices" +- "golang.org/x/tools/gopls/internal/vulncheck" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/gocommand" +- "golang.org/x/tools/internal/imports" +- "golang.org/x/tools/internal/memoize" +- "golang.org/x/tools/internal/xcontext" +-) +- +-// NewSession creates a new gopls session with the given cache. +-func NewSession(ctx context.Context, c *Cache) *Session { +- index := atomic.AddInt64(&sessionIndex, 1) +- s := &Session{ +- id: strconv.FormatInt(index, 10), +- cache: c, +- gocmdRunner: &gocommand.Runner{}, +- overlayFS: newOverlayFS(c), +- parseCache: newParseCache(1 * time.Minute), // keep recently parsed files for a minute, to optimize typing CPU +- viewMap: make(map[protocol.DocumentURI]*View), - } +- event.Log(ctx, "New session", KeyCreateSession.Of(s)) +- return s +-} - -- // Compute invalidations based on file changes. -- anyImportDeleted := false // import deletions can resolve cycles -- anyFileOpenedOrClosed := false // opened files affect workspace packages -- anyFileAdded := false // adding a file can resolve missing dependencies +-// A Session holds the state (views, file contents, parse cache, +-// memoized computations) of a gopls server process. +-// +-// It implements the file.Source interface. +-type Session struct { +- // Unique identifier for this session. +- id string - -- for uri, newFH := range changes { -- // The original FileHandle for this URI is cached on the snapshot. -- oldFH, _ := oldFiles[uri] // may be nil -- _, oldOpen := oldFH.(*Overlay) -- _, newOpen := newFH.(*Overlay) +- // Immutable attributes shared across views. +- cache *Cache // shared cache +- gocmdRunner *gocommand.Runner // limits go command concurrency - -- anyFileOpenedOrClosed = anyFileOpenedOrClosed || (oldOpen != newOpen) -- anyFileAdded = anyFileAdded || (oldFH == nil || !fileExists(oldFH)) && fileExists(newFH) +- viewMu sync.Mutex +- views []*View +- viewMap map[protocol.DocumentURI]*View // file->best view; nil after shutdown - -- // If uri is a Go file, check if it has changed in a way that would -- // invalidate metadata. Note that we can't use s.view.FileKind here, -- // because the file type that matters is not what the *client* tells us, -- // but what the Go command sees. -- var invalidateMetadata, pkgFileChanged, importDeleted bool -- if strings.HasSuffix(uri.Filename(), ".go") { -- invalidateMetadata, pkgFileChanged, importDeleted = metadataChanges(ctx, s, oldFH, newFH) -- } -- if invalidateMetadata { -- // If this is a metadata-affecting change, perhaps a reload will succeed. -- result.unloadableFiles.Remove(uri) -- } +- // snapshots is a counting semaphore that records the number +- // of unreleased snapshots associated with this session. +- // Shutdown waits for it to fall to zero. +- snapshotWG sync.WaitGroup - -- invalidateMetadata = invalidateMetadata || reinit -- anyImportDeleted = anyImportDeleted || importDeleted +- parseCache *parseCache - -- // Mark all of the package IDs containing the given file. -- filePackageIDs := invalidatedPackageIDs(uri, s.meta.ids, pkgFileChanged) -- for id := range filePackageIDs { -- directIDs[id] = directIDs[id] || invalidateMetadata // may insert 'false' -- } +- *overlayFS +-} - -- // Invalidate the previous modTidyHandle if any of the files have been -- // saved or if any of the metadata has been invalidated. -- // -- // TODO(rfindley): this seems like too-aggressive invalidation of mod -- // results. We should instead thread through overlays to the Go command -- // invocation and only run this if invalidateMetadata (and perhaps then -- // still do it less frequently). -- if invalidateMetadata || fileWasSaved(oldFH, newFH) { -- // Only invalidate mod tidy results for the most relevant modfile in the -- // workspace. This is a potentially lossy optimization for workspaces -- // with many modules (such as google-cloud-go, which has 145 modules as -- // of writing). -- // -- // While it is theoretically possible that a change in workspace module A -- // could affect the mod-tidiness of workspace module B (if B transitively -- // requires A), such changes are probably unlikely and not worth the -- // penalty of re-running go mod tidy for everything. Note that mod tidy -- // ignores GOWORK, so the two modules would have to be related by a chain -- // of replace directives. -- // -- // We could improve accuracy by inspecting replace directives, using -- // overlays in go mod tidy, and/or checking for metadata changes from the -- // on-disk content. -- // -- // Note that we iterate the modTidyHandles map here, rather than e.g. -- // using nearestModFile, because we don't have access to an accurate -- // FileSource at this point in the snapshot clone. -- const onlyInvalidateMostRelevant = true -- if onlyInvalidateMostRelevant { -- deleteMostRelevantModFile(result.modTidyHandles, uri) -- } else { -- result.modTidyHandles.Clear() -- } +-// ID returns the unique identifier for this session on this server. +-func (s *Session) ID() string { return s.id } +-func (s *Session) String() string { return s.id } - -- // TODO(rfindley): should we apply the above heuristic to mod vuln or mod -- // why handles as well? -- // -- // TODO(rfindley): no tests fail if I delete the line below. -- result.modWhyHandles.Clear() -- result.modVulnHandles.Clear() -- } +-// GoCommandRunner returns the gocommand Runner for this session. +-func (s *Session) GoCommandRunner() *gocommand.Runner { +- return s.gocmdRunner +-} +- +-// Shutdown the session and all views it has created. +-func (s *Session) Shutdown(ctx context.Context) { +- var views []*View +- s.viewMu.Lock() +- views = append(views, s.views...) +- s.views = nil +- s.viewMap = nil +- s.viewMu.Unlock() +- for _, view := range views { +- view.shutdown() - } +- s.parseCache.stop() +- s.snapshotWG.Wait() // wait for all work on associated snapshots to finish +- event.Log(ctx, "Shutdown session", KeyShutdownSession.Of(s)) +-} - -- // Deleting an import can cause list errors due to import cycles to be -- // resolved. The best we can do without parsing the list error message is to -- // hope that list errors may have been resolved by a deleted import. -- // -- // We could do better by parsing the list error message. We already do this -- // to assign a better range to the list error, but for such critical -- // functionality as metadata, it's better to be conservative until it proves -- // impractical. -- // -- // We could also do better by looking at which imports were deleted and -- // trying to find cycles they are involved in. This fails when the file goes -- // from an unparseable state to a parseable state, as we don't have a -- // starting point to compare with. -- if anyImportDeleted { -- for id, metadata := range s.meta.metadata { -- if len(metadata.Errors) > 0 { -- directIDs[id] = true +-// Cache returns the cache that created this session, for debugging only. +-func (s *Session) Cache() *Cache { +- return s.cache +-} +- +-// TODO(rfindley): is the logic surrounding this error actually necessary? +-var ErrViewExists = errors.New("view already exists for session") +- +-// NewView creates a new View, returning it and its first snapshot. If a +-// non-empty tempWorkspace directory is provided, the View will record a copy +-// of its gopls workspace module in that directory, so that client tooling +-// can execute in the same main module. On success it also returns a release +-// function that must be called when the Snapshot is no longer needed. +-func (s *Session) NewView(ctx context.Context, folder *Folder) (*View, *Snapshot, func(), error) { +- s.viewMu.Lock() +- defer s.viewMu.Unlock() +- +- // Querying the file system to check whether +- // two folders denote the same existing directory. +- if inode1, err := os.Stat(filepath.FromSlash(folder.Dir.Path())); err == nil { +- for _, view := range s.views { +- inode2, err := os.Stat(filepath.FromSlash(view.folder.Dir.Path())) +- if err == nil && os.SameFile(inode1, inode2) { +- return nil, nil, nil, ErrViewExists - } - } - } - -- // Adding a file can resolve missing dependencies from existing packages. -- // -- // We could be smart here and try to guess which packages may have been -- // fixed, but until that proves necessary, just invalidate metadata for any -- // package with missing dependencies. -- if anyFileAdded { -- for id, metadata := range s.meta.metadata { -- for _, impID := range metadata.DepsByImpPath { -- if impID == "" { // missing import -- directIDs[id] = true -- break -- } -- } -- } +- def, err := defineView(ctx, s, folder, nil) +- if err != nil { +- return nil, nil, nil, err - } +- view, snapshot, release := s.createView(ctx, def) +- s.views = append(s.views, view) +- // we always need to drop the view map +- s.viewMap = make(map[protocol.DocumentURI]*View) +- return view, snapshot, release, nil +-} - -- // Invalidate reverse dependencies too. -- // idsToInvalidate keeps track of transitive reverse dependencies. -- // If an ID is present in the map, invalidate its types. -- // If an ID's value is true, invalidate its metadata too. -- idsToInvalidate := map[PackageID]bool{} -- var addRevDeps func(PackageID, bool) -- addRevDeps = func(id PackageID, invalidateMetadata bool) { -- current, seen := idsToInvalidate[id] -- newInvalidateMetadata := current || invalidateMetadata +-// createView creates a new view, with an initial snapshot that retains the +-// supplied context, detached from events and cancelation. +-// +-// The caller is responsible for calling the release function once. +-func (s *Session) createView(ctx context.Context, def *viewDefinition) (*View, *Snapshot, func()) { +- index := atomic.AddInt64(&viewIndex, 1) - -- // If we've already seen this ID, and the value of invalidate -- // metadata has not changed, we can return early. -- if seen && current == newInvalidateMetadata { -- return -- } -- idsToInvalidate[id] = newInvalidateMetadata -- for _, rid := range s.meta.importedBy[id] { -- addRevDeps(rid, invalidateMetadata) -- } -- } -- for id, invalidateMetadata := range directIDs { -- addRevDeps(id, invalidateMetadata) -- } +- // We want a true background context and not a detached context here +- // the spans need to be unrelated and no tag values should pollute it. +- baseCtx := event.Detach(xcontext.Detach(ctx)) +- backgroundCtx, cancel := context.WithCancel(baseCtx) - -- // Invalidated package information. -- for id, invalidateMetadata := range idsToInvalidate { -- if _, ok := directIDs[id]; ok || invalidateMetadata { -- result.packages.Delete(id) -- } else { -- if entry, hit := result.packages.Get(id); hit { -- ph := entry.clone(false) -- result.packages.Set(id, ph, nil) +- // Compute a skip function to use for module cache scanning. +- // +- // Note that unlike other filtering operations, we definitely don't want to +- // exclude the gomodcache here, even if it is contained in the workspace +- // folder. +- // +- // TODO(rfindley): consolidate with relPathExcludedByFilter(Func), Filterer, +- // View.filterFunc. +- var skipPath func(string) bool +- { +- // Compute a prefix match, respecting segment boundaries, by ensuring +- // the pattern (dir) has a trailing slash. +- dirPrefix := strings.TrimSuffix(string(def.folder.Dir), "/") + "/" +- filterer := NewFilterer(def.folder.Options.DirectoryFilters) +- skipPath = func(dir string) bool { +- uri := strings.TrimSuffix(string(protocol.URIFromPath(dir)), "/") +- // Note that the logic below doesn't handle the case where uri == +- // v.folder.Dir, because there is no point in excluding the entire +- // workspace folder! +- if rel := strings.TrimPrefix(uri, dirPrefix); rel != uri { +- return filterer.Disallow(rel) - } +- return false - } -- result.activePackages.Delete(id) - } - -- // Any packages that need loading in s still need loading in the new -- // snapshot. -- for k, v := range s.shouldLoad { -- if result.shouldLoad == nil { -- result.shouldLoad = make(map[PackageID][]PackagePath) +- var ignoreFilter *ignoreFilter +- { +- var dirs []string +- if len(def.workspaceModFiles) == 0 { +- for _, entry := range filepath.SplitList(def.folder.Env.GOPATH) { +- dirs = append(dirs, filepath.Join(entry, "src")) +- } +- } else { +- dirs = append(dirs, def.folder.Env.GOMODCACHE) +- for m := range def.workspaceModFiles { +- dirs = append(dirs, filepath.Dir(m.Path())) +- } - } -- result.shouldLoad[k] = v +- ignoreFilter = newIgnoreFilter(dirs) - } - -- // Compute which metadata updates are required. We only need to invalidate -- // packages directly containing the affected file, and only if it changed in -- // a relevant way. -- metadataUpdates := make(map[PackageID]*source.Metadata) -- for k, v := range s.meta.metadata { -- invalidateMetadata := idsToInvalidate[k] -- -- // For metadata that has been newly invalidated, capture package paths -- // requiring reloading in the shouldLoad map. -- if invalidateMetadata && !source.IsCommandLineArguments(v.ID) { -- if result.shouldLoad == nil { -- result.shouldLoad = make(map[PackageID][]PackagePath) -- } -- needsReload := []PackagePath{v.PkgPath} -- if v.ForTest != "" && v.ForTest != v.PkgPath { -- // When reloading test variants, always reload their ForTest package as -- // well. Otherwise, we may miss test variants in the resulting load. -- // -- // TODO(rfindley): is this actually sufficient? Is it possible that -- // other test variants may be invalidated? Either way, we should -- // determine exactly what needs to be reloaded here. -- needsReload = append(needsReload, v.ForTest) +- var pe *imports.ProcessEnv +- { +- env := make(map[string]string) +- envSlice := slices.Concat(os.Environ(), def.folder.Options.EnvSlice(), []string{"GO111MODULE=" + def.adjustedGO111MODULE()}) +- for _, kv := range envSlice { +- if k, v, ok := strings.Cut(kv, "="); ok { +- env[k] = v - } -- result.shouldLoad[k] = needsReload - } -- -- // Check whether the metadata should be deleted. -- if invalidateMetadata { -- metadataUpdates[k] = nil -- continue +- pe = &imports.ProcessEnv{ +- GocmdRunner: s.gocmdRunner, +- BuildFlags: slices.Clone(def.folder.Options.BuildFlags), +- // TODO(rfindley): an old comment said "processEnv operations should not mutate the modfile" +- // But shouldn't we honor the default behavior of mod vendoring? +- ModFlag: "readonly", +- SkipPathInScan: skipPath, +- Env: env, +- WorkingDir: def.root.Path(), +- ModCache: s.cache.modCache.dirCache(def.folder.Env.GOMODCACHE), +- } +- if def.folder.Options.VerboseOutput { +- pe.Logf = func(format string, args ...interface{}) { +- event.Log(ctx, fmt.Sprintf(format, args...)) +- } - } -- - } - -- // Update metadata, if necessary. -- result.meta = s.meta.Clone(metadataUpdates) -- -- // Update workspace and active packages, if necessary. -- if result.meta != s.meta || anyFileOpenedOrClosed { -- result.workspacePackages = computeWorkspacePackagesLocked(result, result.meta) -- result.resetActivePackagesLocked() -- } else { -- result.workspacePackages = s.workspacePackages +- v := &View{ +- id: strconv.FormatInt(index, 10), +- gocmdRunner: s.gocmdRunner, +- initialWorkspaceLoad: make(chan struct{}), +- initializationSema: make(chan struct{}, 1), +- baseCtx: baseCtx, +- parseCache: s.parseCache, +- ignoreFilter: ignoreFilter, +- fs: s.overlayFS, +- viewDefinition: def, +- importsState: newImportsState(backgroundCtx, s.cache.modCache, pe), +- } +- +- s.snapshotWG.Add(1) +- v.snapshot = &Snapshot{ +- view: v, +- backgroundCtx: backgroundCtx, +- cancel: cancel, +- store: s.cache.store, +- refcount: 1, // Snapshots are born referenced. +- done: s.snapshotWG.Done, +- packages: new(persistent.Map[PackageID, *packageHandle]), +- meta: new(metadata.Graph), +- files: newFileMap(), +- activePackages: new(persistent.Map[PackageID, *Package]), +- symbolizeHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]), +- shouldLoad: new(persistent.Map[PackageID, []PackagePath]), +- unloadableFiles: new(persistent.Set[protocol.DocumentURI]), +- parseModHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]), +- parseWorkHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]), +- modTidyHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]), +- modVulnHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]), +- modWhyHandles: new(persistent.Map[protocol.DocumentURI, *memoize.Promise]), +- pkgIndex: typerefs.NewPackageIndex(), +- moduleUpgrades: new(persistent.Map[protocol.DocumentURI, map[string]string]), +- vulns: new(persistent.Map[protocol.DocumentURI, *vulncheck.Result]), +- } +- +- // Snapshots must observe all open files, as there are some caching +- // heuristics that change behavior depending on open files. +- for _, o := range s.overlayFS.Overlays() { +- _, _ = v.snapshot.ReadFile(ctx, o.URI()) - } - -- // Don't bother copying the importedBy graph, -- // as it changes each time we update metadata. +- // Record the environment of the newly created view in the log. +- event.Log(ctx, viewEnv(v)) +- +- // Initialize the view without blocking. +- initCtx, initCancel := context.WithCancel(xcontext.Detach(ctx)) +- v.cancelInitialWorkspaceLoad = initCancel +- snapshot := v.snapshot - -- // TODO(rfindley): consolidate the this workspace mode detection with -- // workspace invalidation. -- workspaceModeChanged := s.workspaceMode() != result.workspaceMode() +- // Pass a second reference to the background goroutine. +- bgRelease := snapshot.Acquire() +- go func() { +- defer bgRelease() +- snapshot.initialize(initCtx, true) +- }() - -- // If the snapshot's workspace mode has changed, the packages loaded using -- // the previous mode are no longer relevant, so clear them out. -- if workspaceModeChanged { -- result.workspacePackages = map[PackageID]PackagePath{} -- } -- return result, release +- // Return a third reference to the caller. +- return v, snapshot, snapshot.Acquire() -} - --func cloneWithout[V any](m *persistent.Map[span.URI, V], changes map[span.URI]source.FileHandle) *persistent.Map[span.URI, V] { -- m2 := m.Clone() -- for k := range changes { -- m2.Delete(k) +-// RemoveView removes from the session the view rooted at the specified directory. +-// It reports whether a view of that directory was removed. +-func (s *Session) RemoveView(dir protocol.DocumentURI) bool { +- s.viewMu.Lock() +- defer s.viewMu.Unlock() +- for _, view := range s.views { +- if view.folder.Dir == dir { +- i := s.dropView(view) +- if i == -1 { +- return false // can't happen +- } +- // delete this view... we don't care about order but we do want to make +- // sure we can garbage collect the view +- s.views = removeElement(s.views, i) +- return true +- } - } -- return m2 +- return false -} - --// deleteMostRelevantModFile deletes the mod file most likely to be the mod --// file for the changed URI, if it exists. --// --// Specifically, this is the longest mod file path in a directory containing --// changed. This might not be accurate if there is another mod file closer to --// changed that happens not to be present in the map, but that's OK: the goal --// of this function is to guarantee that IF the nearest mod file is present in --// the map, it is invalidated. --func deleteMostRelevantModFile(m *persistent.Map[span.URI, *memoize.Promise], changed span.URI) { -- var mostRelevant span.URI -- changedFile := changed.Filename() -- -- m.Range(func(modURI span.URI, _ *memoize.Promise) { -- if len(modURI) > len(mostRelevant) { -- if source.InDir(filepath.Dir(modURI.Filename()), changedFile) { -- mostRelevant = modURI -- } +-// View returns the view with a matching id, if present. +-func (s *Session) View(id string) (*View, error) { +- s.viewMu.Lock() +- defer s.viewMu.Unlock() +- for _, view := range s.views { +- if view.ID() == id { +- return view, nil - } -- }) -- if mostRelevant != "" { -- m.Delete(mostRelevant) - } +- return nil, fmt.Errorf("no view with ID %q", id) -} - --// invalidatedPackageIDs returns all packages invalidated by a change to uri. --// If we haven't seen this URI before, we guess based on files in the same --// directory. This is of course incorrect in build systems where packages are --// not organized by directory. +-// SnapshotOf returns a Snapshot corresponding to the given URI. -// --// If packageFileChanged is set, the file is either a new file, or has a new --// package name. In this case, all known packages in the directory will be --// invalidated. --func invalidatedPackageIDs(uri span.URI, known map[span.URI][]PackageID, packageFileChanged bool) map[PackageID]struct{} { -- invalidated := make(map[PackageID]struct{}) +-// In the case where the file can be can be associated with a View by +-// bestViewForURI (based on directory information alone, without package +-// metadata), SnapshotOf returns the current Snapshot for that View. Otherwise, +-// it awaits loading package metadata and returns a Snapshot for the first View +-// containing a real (=not command-line-arguments) package for the file. +-// +-// If that also fails to find a View, SnapshotOf returns a Snapshot for the +-// first view in s.views that is not shut down (i.e. s.views[0] unless we lose +-// a race), for determinism in tests and so that we tend to aggregate the +-// resulting command-line-arguments packages into a single view. +-// +-// SnapshotOf returns an error if a failure occurs along the way (most likely due +-// to context cancellation), or if there are no Views in the Session. +-// +-// On success, the caller must call the returned function to release the snapshot. +-func (s *Session) SnapshotOf(ctx context.Context, uri protocol.DocumentURI) (*Snapshot, func(), error) { +- // Fast path: if the uri has a static association with a view, return it. +- s.viewMu.Lock() +- v, err := s.viewOfLocked(ctx, uri) +- s.viewMu.Unlock() - -- // At a minimum, we invalidate packages known to contain uri. -- for _, id := range known[uri] { -- invalidated[id] = struct{}{} +- if err != nil { +- return nil, nil, err - } - -- // If the file didn't move to a new package, we should only invalidate the -- // packages it is currently contained inside. -- if !packageFileChanged && len(invalidated) > 0 { -- return invalidated +- if v != nil { +- snapshot, release, err := v.Snapshot() +- if err == nil { +- return snapshot, release, nil +- } +- // View is shut down. Forget this association. +- s.viewMu.Lock() +- if s.viewMap[uri] == v { +- delete(s.viewMap, uri) +- } +- s.viewMu.Unlock() - } - -- // This is a file we don't yet know about, or which has moved packages. Guess -- // relevant packages by considering files in the same directory. -- -- // Cache of FileInfo to avoid unnecessary stats for multiple files in the -- // same directory. -- stats := make(map[string]struct { -- os.FileInfo -- error -- }) -- getInfo := func(dir string) (os.FileInfo, error) { -- if res, ok := stats[dir]; ok { -- return res.FileInfo, res.error +- // Fall-back: none of the views could be associated with uri based on +- // directory information alone. +- // +- // Don't memoize the view association in viewMap, as it is not static: Views +- // may change as metadata changes. +- // +- // TODO(rfindley): we could perhaps optimize this case by peeking at existing +- // metadata before awaiting the load (after all, a load only adds metadata). +- // But that seems potentially tricky, when in the common case no loading +- // should be required. +- views := s.Views() +- for _, v := range views { +- snapshot, release, err := v.Snapshot() +- if err != nil { +- continue // view was shut down - } -- fi, err := os.Stat(dir) -- stats[dir] = struct { -- os.FileInfo -- error -- }{fi, err} -- return fi, err -- } -- dir := filepath.Dir(uri.Filename()) -- fi, err := getInfo(dir) -- if err == nil { -- // Aggregate all possibly relevant package IDs. -- for knownURI, ids := range known { -- knownDir := filepath.Dir(knownURI.Filename()) -- knownFI, err := getInfo(knownDir) -- if err != nil { -- continue -- } -- if os.SameFile(fi, knownFI) { -- for _, id := range ids { -- invalidated[id] = struct{}{} -- } +- _ = snapshot.awaitLoaded(ctx) // ignore error +- g := snapshot.MetadataGraph() +- // We don't check the error from awaitLoaded, because a load failure (that +- // doesn't result from context cancelation) should not prevent us from +- // continuing to search for the best view. +- if ctx.Err() != nil { +- release() +- return nil, nil, ctx.Err() +- } +- // Special handling for the builtin file, since it doesn't have packages. +- if snapshot.IsBuiltin(uri) { +- return snapshot, release, nil +- } +- // Only match this view if it loaded a real package for the file. +- // +- // Any view can load a command-line-arguments package; aggregate those into +- // views[0] below. +- for _, id := range g.IDs[uri] { +- if !metadata.IsCommandLineArguments(id) || g.Packages[id].Standalone { +- return snapshot, release, nil - } - } +- release() - } -- return invalidated --} - --// fileWasSaved reports whether the FileHandle passed in has been saved. It --// accomplishes this by checking to see if the original and current FileHandles --// are both overlays, and if the current FileHandle is saved while the original --// FileHandle was not saved. --func fileWasSaved(originalFH, currentFH source.FileHandle) bool { -- c, ok := currentFH.(*Overlay) -- if !ok || c == nil { -- return true -- } -- o, ok := originalFH.(*Overlay) -- if !ok || o == nil { -- return c.saved +- for _, v := range views { +- snapshot, release, err := v.Snapshot() +- if err == nil { +- return snapshot, release, nil // first valid snapshot +- } - } -- return !o.saved && c.saved +- return nil, nil, errNoViews -} - --// metadataChanges detects features of the change from oldFH->newFH that may --// affect package metadata. +-// errNoViews is sought by orphaned file diagnostics, to detect the case where +-// we have no view containing a file. +-var errNoViews = errors.New("no views") +- +-// viewOfLocked wraps bestViewForURI, memoizing its result. -// --// It uses lockedSnapshot to access cached parse information. lockedSnapshot --// must be locked. +-// Precondition: caller holds s.viewMu lock. -// --// The result parameters have the following meaning: --// - invalidate means that package metadata for packages containing the file --// should be invalidated. --// - pkgFileChanged means that the file->package associates for the file have --// changed (possibly because the file is new, or because its package name has --// changed). --// - importDeleted means that an import has been deleted, or we can't --// determine if an import was deleted due to errors. --func metadataChanges(ctx context.Context, lockedSnapshot *snapshot, oldFH, newFH source.FileHandle) (invalidate, pkgFileChanged, importDeleted bool) { -- if oe, ne := oldFH != nil && fileExists(oldFH), fileExists(newFH); !oe || !ne { // existential changes -- changed := oe != ne -- return changed, changed, !ne // we don't know if an import was deleted +-// May return (nil, nil). +-func (s *Session) viewOfLocked(ctx context.Context, uri protocol.DocumentURI) (*View, error) { +- v, hit := s.viewMap[uri] +- if !hit { +- // Cache miss: compute (and memoize) the best view. +- fh, err := s.ReadFile(ctx, uri) +- if err != nil { +- return nil, err +- } +- v, err = bestView(ctx, s, fh, s.views) +- if err != nil { +- return nil, err +- } +- if s.viewMap == nil { +- return nil, errors.New("session is shut down") +- } +- s.viewMap[uri] = v - } +- return v, nil +-} - -- // If the file hasn't changed, there's no need to reload. -- if oldFH.FileIdentity() == newFH.FileIdentity() { -- return false, false, false -- } +-func (s *Session) Views() []*View { +- s.viewMu.Lock() +- defer s.viewMu.Unlock() +- result := make([]*View, len(s.views)) +- copy(result, s.views) +- return result +-} - -- fset := token.NewFileSet() -- // Parse headers to compare package names and imports. -- oldHeads, oldErr := lockedSnapshot.view.parseCache.parseFiles(ctx, fset, source.ParseHeader, false, oldFH) -- newHeads, newErr := lockedSnapshot.view.parseCache.parseFiles(ctx, fset, source.ParseHeader, false, newFH) +-// selectViewDefs constructs the best set of views covering the provided workspace +-// folders and open files. +-// +-// This implements the zero-config algorithm of golang/go#57979. +-func selectViewDefs(ctx context.Context, fs file.Source, folders []*Folder, openFiles []protocol.DocumentURI) ([]*viewDefinition, error) { +- var defs []*viewDefinition - -- if oldErr != nil || newErr != nil { -- errChanged := (oldErr == nil) != (newErr == nil) -- return errChanged, errChanged, (newErr != nil) // we don't know if an import was deleted +- // First, compute a default view for each workspace folder. +- // TODO(golang/go#57979): technically, this is path dependent, since +- // DidChangeWorkspaceFolders could introduce a path-dependent ordering on +- // folders. We should keep folders sorted, or sort them here. +- for _, folder := range folders { +- def, err := defineView(ctx, fs, folder, nil) +- if err != nil { +- return nil, err +- } +- defs = append(defs, def) - } - -- oldHead := oldHeads[0] -- newHead := newHeads[0] +- // Next, ensure that the set of views covers all open files contained in a +- // workspace folder. +- // +- // We only do this for files contained in a workspace folder, because other +- // open files are most likely the result of jumping to a definition from a +- // workspace file; we don't want to create additional views in those cases: +- // they should be resolved after initialization. - -- // `go list` fails completely if the file header cannot be parsed. If we go -- // from a non-parsing state to a parsing state, we should reload. -- if oldHead.ParseErr != nil && newHead.ParseErr == nil { -- return true, true, true // We don't know what changed, so fall back on full invalidation. -- } +- folderForFile := func(uri protocol.DocumentURI) *Folder { +- var longest *Folder +- for _, folder := range folders { +- // Check that this is a better match than longest, but not through a +- // vendor directory. Count occurrences of "/vendor/" as a quick check +- // that the vendor directory is between the folder and the file. Note the +- // addition of a trailing "/" to handle the odd case where the folder is named +- // vendor (which I hope is exceedingly rare in any case). +- // +- // Vendored packages are, by definition, part of an existing view. +- if (longest == nil || len(folder.Dir) > len(longest.Dir)) && +- folder.Dir.Encloses(uri) && +- strings.Count(string(uri), "/vendor/") == strings.Count(string(folder.Dir)+"/", "/vendor/") { - -- // If a package name has changed, the set of package imports may have changed -- // in ways we can't detect here. Assume an import has been deleted. -- if oldHead.File.Name.Name != newHead.File.Name.Name { -- return true, true, true +- longest = folder +- } +- } +- return longest - } - -- // Check whether package imports have changed. Only consider potentially -- // valid imports paths. -- oldImports := validImports(oldHead.File.Imports) -- newImports := validImports(newHead.File.Imports) -- -- for path := range newImports { -- if _, ok := oldImports[path]; ok { -- delete(oldImports, path) -- } else { -- invalidate = true // a new, potentially valid import was added +-checkFiles: +- for _, uri := range openFiles { +- folder := folderForFile(uri) +- if folder == nil || !folder.Options.ZeroConfig { +- continue // only guess views for open files +- } +- fh, err := fs.ReadFile(ctx, uri) +- if err != nil { +- return nil, err +- } +- def, err := bestView(ctx, fs, fh, defs) +- if err != nil { +- // We should never call selectViewDefs with a cancellable context, so +- // this should never fail. +- return nil, bug.Errorf("failed to find best view for open file: %v", err) +- } +- if def != nil { +- continue // file covered by an existing view - } +- def, err = defineView(ctx, fs, folder, fh) +- if err != nil { +- // We should never call selectViewDefs with a cancellable context, so +- // this should never fail. +- return nil, bug.Errorf("failed to define view for open file: %v", err) +- } +- // It need not strictly be the case that the best view for a file is +- // distinct from other views, as the logic of getViewDefinition and +- // bestViewForURI does not align perfectly. This is not necessarily a bug: +- // there may be files for which we can't construct a valid view. +- // +- // Nevertheless, we should not create redundant views. +- for _, alt := range defs { +- if viewDefinitionsEqual(alt, def) { +- continue checkFiles +- } +- } +- defs = append(defs, def) - } - -- if len(oldImports) > 0 { -- invalidate = true -- importDeleted = true +- return defs, nil +-} +- +-// The viewDefiner interface allows the bestView algorithm to operate on both +-// Views and viewDefinitions. +-type viewDefiner interface{ definition() *viewDefinition } +- +-// BestViews returns the most relevant subset of views for a given uri. +-// +-// This may be used to filter diagnostics to the most relevant builds. +-func BestViews[V viewDefiner](ctx context.Context, fs file.Source, uri protocol.DocumentURI, views []V) ([]V, error) { +- if len(views) == 0 { +- return nil, nil // avoid the call to findRootPattern +- } +- dir := uri.Dir() +- modURI, err := findRootPattern(ctx, dir, "go.mod", fs) +- if err != nil { +- return nil, err - } - -- // If the change does not otherwise invalidate metadata, get the full ASTs in -- // order to check magic comments. +- // Prefer GoWork > GoMod > GOPATH > GoPackages > AdHoc. +- var ( +- goPackagesViews []V // prefer longest +- workViews []V // prefer longest +- modViews []V // exact match +- gopathViews []V // prefer longest +- adHocViews []V // exact match +- ) +- +- // pushView updates the views slice with the matching view v, using the +- // heuristic that views with a longer root are preferable. Accordingly, +- // pushView may be a no op if v's root is shorter than the roots in the views +- // slice. - // -- // Note: if this affects performance we can probably avoid parsing in the -- // common case by first scanning the source for potential comments. -- if !invalidate { -- origFulls, oldErr := lockedSnapshot.view.parseCache.parseFiles(ctx, fset, source.ParseFull, false, oldFH) -- newFulls, newErr := lockedSnapshot.view.parseCache.parseFiles(ctx, fset, source.ParseFull, false, newFH) -- if oldErr == nil && newErr == nil { -- invalidate = magicCommentsChanged(origFulls[0].File, newFulls[0].File) -- } else { -- // At this point, we shouldn't ever fail to produce a ParsedGoFile, as -- // we're already past header parsing. -- bug.Reportf("metadataChanges: unparseable file %v (old error: %v, new error: %v)", oldFH.URI(), oldErr, newErr) +- // Invariant: the length of all roots in views is the same. +- pushView := func(views *[]V, v V) { +- if len(*views) == 0 { +- *views = []V{v} +- return +- } +- better := func(l, r V) bool { +- return len(l.definition().root) > len(r.definition().root) +- } +- existing := (*views)[0] +- switch { +- case better(existing, v): +- case better(v, existing): +- *views = []V{v} +- default: +- *views = append(*views, v) - } - } - -- return invalidate, pkgFileChanged, importDeleted --} -- --func magicCommentsChanged(original *ast.File, current *ast.File) bool { -- oldComments := extractMagicComments(original) -- newComments := extractMagicComments(current) -- if len(oldComments) != len(newComments) { -- return true -- } -- for i := range oldComments { -- if oldComments[i] != newComments[i] { -- return true +- for _, view := range views { +- switch def := view.definition(); def.Type() { +- case GoPackagesDriverView: +- if def.root.Encloses(dir) { +- pushView(&goPackagesViews, view) +- } +- case GoWorkView: +- if _, ok := def.workspaceModFiles[modURI]; ok || uri == def.gowork { +- pushView(&workViews, view) +- } +- case GoModView: +- if _, ok := def.workspaceModFiles[modURI]; ok { +- modViews = append(modViews, view) +- } +- case GOPATHView: +- if def.root.Encloses(dir) { +- pushView(&gopathViews, view) +- } +- case AdHocView: +- if def.root == dir { +- adHocViews = append(adHocViews, view) +- } - } - } -- return false --} - --// validImports extracts the set of valid import paths from imports. --func validImports(imports []*ast.ImportSpec) map[string]struct{} { -- m := make(map[string]struct{}) -- for _, spec := range imports { -- if path := spec.Path.Value; validImportPath(path) { -- m[path] = struct{}{} -- } +- // Now that we've collected matching views, choose the best match, +- // considering ports. +- // +- // We only consider one type of view, since the matching view created by +- // defineView should be of the best type. +- var bestViews []V +- switch { +- case len(workViews) > 0: +- bestViews = workViews +- case len(modViews) > 0: +- bestViews = modViews +- case len(gopathViews) > 0: +- bestViews = gopathViews +- case len(goPackagesViews) > 0: +- bestViews = goPackagesViews +- case len(adHocViews) > 0: +- bestViews = adHocViews - } -- return m +- +- return bestViews, nil -} - --func validImportPath(path string) bool { -- path, err := strconv.Unquote(path) -- if err != nil { -- return false -- } -- if path == "" { -- return false -- } -- if path[len(path)-1] == '/' { -- return false +-// bestView returns the best View or viewDefinition that contains the +-// given file, or (nil, nil) if no matching view is found. +-// +-// bestView only returns an error in the event of context cancellation. +-// +-// Making this function generic is convenient so that we can avoid mapping view +-// definitions back to views inside Session.DidModifyFiles, where performance +-// matters. It is, however, not the cleanest application of generics. +-// +-// Note: keep this function in sync with defineView. +-func bestView[V viewDefiner](ctx context.Context, fs file.Source, fh file.Handle, views []V) (V, error) { +- var zero V +- bestViews, err := BestViews(ctx, fs, fh.URI(), views) +- if err != nil || len(bestViews) == 0 { +- return zero, err - } -- return true --} - --var buildConstraintOrEmbedRe = regexp.MustCompile(`^//(go:embed|go:build|\s*\+build).*`) +- content, err := fh.Content() +- // Port matching doesn't apply to non-go files, or files that no longer exist. +- // Note that the behavior here on non-existent files shouldn't matter much, +- // since there will be a subsequent failure. But it is simpler to preserve +- // the invariant that bestView only fails on context cancellation. +- if fileKind(fh) != file.Go || err != nil { +- return bestViews[0], nil +- } - --// extractMagicComments finds magic comments that affect metadata in f. --func extractMagicComments(f *ast.File) []string { -- var results []string -- for _, cg := range f.Comments { -- for _, c := range cg.List { -- if buildConstraintOrEmbedRe.MatchString(c.Text) { -- results = append(results, c.Text) -- } +- // Find the first view that matches constraints. +- // Content trimming is nontrivial, so do this outside of the loop below. +- path := fh.URI().Path() +- content = trimContentForPortMatch(content) +- for _, v := range bestViews { +- def := v.definition() +- viewPort := port{def.GOOS(), def.GOARCH()} +- if viewPort.matches(path, content) { +- return v, nil - } - } -- return results --} -- --func (s *snapshot) BuiltinFile(ctx context.Context) (*source.ParsedGoFile, error) { -- s.AwaitInitialized(ctx) - -- s.mu.Lock() -- builtin := s.builtin -- s.mu.Unlock() +- return zero, nil // no view found +-} - -- if builtin == "" { -- return nil, fmt.Errorf("no builtin package for view %s", s.view.folder.Name) +-// updateViewLocked recreates the view with the given options. +-// +-// If the resulting error is non-nil, the view may or may not have already been +-// dropped from the session. +-func (s *Session) updateViewLocked(ctx context.Context, view *View, def *viewDefinition) (*View, error) { +- i := s.dropView(view) +- if i == -1 { +- return nil, fmt.Errorf("view %q not found", view.id) - } - -- fh, err := s.ReadFile(ctx, builtin) -- if err != nil { -- return nil, err -- } -- // For the builtin file only, we need syntactic object resolution -- // (since we can't type check). -- mode := source.ParseFull &^ source.SkipObjectResolution -- pgfs, err := s.view.parseCache.parseFiles(ctx, token.NewFileSet(), mode, false, fh) -- if err != nil { -- return nil, err -- } -- return pgfs[0], nil --} +- view, _, release := s.createView(ctx, def) +- defer release() - --func (s *snapshot) IsBuiltin(uri span.URI) bool { -- s.mu.Lock() -- defer s.mu.Unlock() -- // We should always get the builtin URI in a canonical form, so use simple -- // string comparison here. span.CompareURI is too expensive. -- return uri == s.builtin +- // substitute the new view into the array where the old view was +- s.views[i] = view +- s.viewMap = make(map[protocol.DocumentURI]*View) +- return view, nil -} - --func (s *snapshot) setBuiltin(path string) { -- s.mu.Lock() -- defer s.mu.Unlock() -- -- s.builtin = span.URIFromPath(path) +-// removeElement removes the ith element from the slice replacing it with the last element. +-// TODO(adonovan): generics, someday. +-func removeElement(slice []*View, index int) []*View { +- last := len(slice) - 1 +- slice[index] = slice[last] +- slice[last] = nil // aid GC +- return slice[:last] -} -diff -urN a/gopls/internal/lsp/cache/symbols.go b/gopls/internal/lsp/cache/symbols.go ---- a/gopls/internal/lsp/cache/symbols.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/symbols.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,192 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package cache -- --import ( -- "context" -- "go/ast" -- "go/token" -- "go/types" -- "strings" -- -- "golang.org/x/tools/gopls/internal/astutil" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" --) -- --// symbolize returns the result of symbolizing the file identified by uri, using a cache. --func (s *snapshot) symbolize(ctx context.Context, uri span.URI) ([]source.Symbol, error) { -- -- s.mu.Lock() -- entry, hit := s.symbolizeHandles.Get(uri) -- s.mu.Unlock() -- -- type symbolizeResult struct { -- symbols []source.Symbol -- err error -- } - -- // Cache miss? -- if !hit { -- fh, err := s.ReadFile(ctx, uri) -- if err != nil { -- return nil, err +-// dropView removes v from the set of views for the receiver s and calls +-// v.shutdown, returning the index of v in s.views (if found), or -1 if v was +-// not found. s.viewMu must be held while calling this function. +-func (s *Session) dropView(v *View) int { +- // we always need to drop the view map +- s.viewMap = make(map[protocol.DocumentURI]*View) +- for i := range s.views { +- if v == s.views[i] { +- // we found the view, drop it and return the index it was found at +- s.views[i] = nil +- v.shutdown() +- return i - } -- type symbolHandleKey source.Hash -- key := symbolHandleKey(fh.FileIdentity().Hash) -- promise, release := s.store.Promise(key, func(ctx context.Context, arg interface{}) interface{} { -- symbols, err := symbolizeImpl(ctx, arg.(*snapshot), fh) -- return symbolizeResult{symbols, err} -- }) -- -- entry = promise -- -- s.mu.Lock() -- s.symbolizeHandles.Set(uri, entry, func(_, _ interface{}) { release() }) -- s.mu.Unlock() - } +- // TODO(rfindley): it looks wrong that we don't shutdown v in this codepath. +- // We should never get here. +- bug.Reportf("tried to drop nonexistent view %q", v.id) +- return -1 +-} - -- // Await result. -- v, err := s.awaitPromise(ctx, entry) +-// ResetView resets the best view for the given URI. +-func (s *Session) ResetView(ctx context.Context, uri protocol.DocumentURI) (*View, error) { +- s.viewMu.Lock() +- defer s.viewMu.Unlock() +- v, err := s.viewOfLocked(ctx, uri) - if err != nil { - return nil, err - } -- res := v.(symbolizeResult) -- return res.symbols, res.err +- return s.updateViewLocked(ctx, v, v.viewDefinition) -} - --// symbolizeImpl reads and parses a file and extracts symbols from it. --func symbolizeImpl(ctx context.Context, snapshot *snapshot, fh source.FileHandle) ([]source.Symbol, error) { -- pgfs, err := snapshot.view.parseCache.parseFiles(ctx, token.NewFileSet(), source.ParseFull, false, fh) +-// DidModifyFiles reports a file modification to the session. It returns +-// the new snapshots after the modifications have been applied, paired with +-// the affected file URIs for those snapshots. +-// On success, it returns a release function that +-// must be called when the snapshots are no longer needed. +-// +-// TODO(rfindley): what happens if this function fails? It must leave us in a +-// broken state, which we should surface to the user, probably as a request to +-// restart gopls. +-func (s *Session) DidModifyFiles(ctx context.Context, modifications []file.Modification) (map[*View][]protocol.DocumentURI, error) { +- s.viewMu.Lock() +- defer s.viewMu.Unlock() +- +- // Update overlays. +- // +- // This is done while holding viewMu because the set of open files affects +- // the set of views, and to prevent views from seeing updated file content +- // before they have processed invalidations. +- replaced, err := s.updateOverlays(ctx, modifications) - if err != nil { - return nil, err - } - -- w := &symbolWalker{ -- tokFile: pgfs[0].Tok, -- mapper: pgfs[0].Mapper, -- } -- w.fileDecls(pgfs[0].File.Decls) +- // checkViews controls whether the set of views needs to be recomputed, for +- // example because a go.mod file was created or deleted, or a go.work file +- // changed on disk. +- checkViews := false - -- return w.symbols, w.firstError --} +- changed := make(map[protocol.DocumentURI]file.Handle) +- for _, c := range modifications { +- fh := mustReadFile(ctx, s, c.URI) +- changed[c.URI] = fh - --type symbolWalker struct { -- // for computing positions -- tokFile *token.File -- mapper *protocol.Mapper +- // Any change to the set of open files causes views to be recomputed. +- if c.Action == file.Open || c.Action == file.Close { +- checkViews = true +- } - -- symbols []source.Symbol -- firstError error --} +- // Any on-disk change to a go.work or go.mod file causes recomputing views. +- // +- // TODO(rfindley): go.work files need not be named "go.work" -- we need to +- // check each view's source to handle the case of an explicit GOWORK value. +- // Write a test that fails, and fix this. +- if (isGoWork(c.URI) || isGoMod(c.URI)) && (c.Action == file.Save || c.OnDisk) { +- checkViews = true +- } - --func (w *symbolWalker) atNode(node ast.Node, name string, kind protocol.SymbolKind, path ...*ast.Ident) { -- var b strings.Builder -- for _, ident := range path { -- if ident != nil { -- b.WriteString(ident.Name) -- b.WriteString(".") +- // Any change to the set of supported ports in a file may affect view +- // selection. This is perhaps more subtle than it first seems: since the +- // algorithm for selecting views considers open files in a deterministic +- // order, a change in supported ports may cause a different port to be +- // chosen, even if all open files still match an existing View! +- // +- // We endeavor to avoid that sort of path dependence, so must re-run the +- // view selection algorithm whenever any input changes. +- // +- // However, extracting the build comment is nontrivial, so we don't want to +- // pay this cost when e.g. processing a bunch of on-disk changes due to a +- // branch change. Be careful to only do this if both files are open Go +- // files. +- if old, ok := replaced[c.URI]; ok && !checkViews && fileKind(fh) == file.Go { +- if new, ok := fh.(*overlay); ok { +- if buildComment(old.content) != buildComment(new.content) { +- checkViews = true +- } +- } - } - } -- b.WriteString(name) - -- rng, err := w.mapper.NodeRange(w.tokFile, node) -- if err != nil { -- w.error(err) -- return -- } -- sym := source.Symbol{ -- Name: b.String(), -- Kind: kind, -- Range: rng, -- } -- w.symbols = append(w.symbols, sym) --} +- if checkViews { +- // Hack: collect folders from existing views. +- // TODO(golang/go#57979): we really should track folders independent of +- // Views, but since we always have a default View for each folder, this +- // works for now. +- var folders []*Folder // preserve folder order +- seen := make(map[*Folder]unit) +- for _, v := range s.views { +- if _, ok := seen[v.folder]; ok { +- continue +- } +- seen[v.folder] = unit{} +- folders = append(folders, v.folder) +- } - --func (w *symbolWalker) error(err error) { -- if err != nil && w.firstError == nil { -- w.firstError = err -- } --} +- var openFiles []protocol.DocumentURI +- for _, o := range s.Overlays() { +- openFiles = append(openFiles, o.URI()) +- } +- // Sort for determinism. +- sort.Slice(openFiles, func(i, j int) bool { +- return openFiles[i] < openFiles[j] +- }) - --func (w *symbolWalker) fileDecls(decls []ast.Decl) { -- for _, decl := range decls { -- switch decl := decl.(type) { -- case *ast.FuncDecl: -- kind := protocol.Function -- var recv *ast.Ident -- if decl.Recv.NumFields() > 0 { -- kind = protocol.Method -- _, recv, _ = astutil.UnpackRecv(decl.Recv.List[0].Type) -- } -- w.atNode(decl.Name, decl.Name.Name, kind, recv) -- case *ast.GenDecl: -- for _, spec := range decl.Specs { -- switch spec := spec.(type) { -- case *ast.TypeSpec: -- kind := guessKind(spec) -- w.atNode(spec.Name, spec.Name.Name, kind) -- w.walkType(spec.Type, spec.Name) -- case *ast.ValueSpec: -- for _, name := range spec.Names { -- kind := protocol.Variable -- if decl.Tok == token.CONST { -- kind = protocol.Constant -- } -- w.atNode(name, name.Name, kind) +- // TODO(rfindley): can we avoid running the go command (go env) +- // synchronously to change processing? Can we assume that the env did not +- // change, and derive go.work using a combination of the configured +- // GOWORK value and filesystem? +- defs, err := selectViewDefs(ctx, s, folders, openFiles) +- if err != nil { +- // Catastrophic failure, equivalent to a failure of session +- // initialization and therefore should almost never happen. One +- // scenario where this failure mode could occur is if some file +- // permissions have changed preventing us from reading go.mod +- // files. +- // +- // TODO(rfindley): consider surfacing this error more loudly. We +- // could report a bug, but it's not really a bug. +- event.Error(ctx, "selecting new views", err) +- } else { +- kept := make(map[*View]unit) +- var newViews []*View +- for _, def := range defs { +- var newView *View +- // Reuse existing view? +- for _, v := range s.views { +- if viewDefinitionsEqual(def, v.viewDefinition) { +- newView = v +- kept[v] = unit{} +- break - } - } +- if newView == nil { +- v, _, release := s.createView(ctx, def) +- release() +- newView = v +- } +- newViews = append(newViews, newView) +- } +- for _, v := range s.views { +- if _, ok := kept[v]; !ok { +- v.shutdown() +- } - } +- s.views = newViews +- s.viewMap = make(map[protocol.DocumentURI]*View) - } - } --} - --func guessKind(spec *ast.TypeSpec) protocol.SymbolKind { -- switch spec.Type.(type) { -- case *ast.InterfaceType: -- return protocol.Interface -- case *ast.StructType: -- return protocol.Struct -- case *ast.FuncType: -- return protocol.Function +- // We only want to run fast-path diagnostics (i.e. diagnoseChangedFiles) once +- // for each changed file, in its best view. +- viewsToDiagnose := map[*View][]protocol.DocumentURI{} +- for _, mod := range modifications { +- v, err := s.viewOfLocked(ctx, mod.URI) +- if err != nil { +- // bestViewForURI only returns an error in the event of context +- // cancellation. Since state changes should occur on an uncancellable +- // context, an error here is a bug. +- bug.Reportf("finding best view for change: %v", err) +- continue +- } +- if v != nil { +- viewsToDiagnose[v] = append(viewsToDiagnose[v], mod.URI) +- } - } -- return protocol.Class --} - --// walkType processes symbols related to a type expression. path is path of --// nested type identifiers to the type expression. --func (w *symbolWalker) walkType(typ ast.Expr, path ...*ast.Ident) { -- switch st := typ.(type) { -- case *ast.StructType: -- for _, field := range st.Fields.List { -- w.walkField(field, protocol.Field, protocol.Field, path...) -- } -- case *ast.InterfaceType: -- for _, field := range st.Methods.List { -- w.walkField(field, protocol.Interface, protocol.Method, path...) +- // ...but changes may be relevant to other views, for example if they are +- // changes to a shared package. +- for _, v := range s.views { +- _, release, needsDiagnosis := s.invalidateViewLocked(ctx, v, StateChange{Modifications: modifications, Files: changed}) +- release() +- +- if needsDiagnosis || checkViews { +- if _, ok := viewsToDiagnose[v]; !ok { +- viewsToDiagnose[v] = nil +- } - } - } +- +- return viewsToDiagnose, nil -} - --// walkField processes symbols related to the struct field or interface method. --// --// unnamedKind and namedKind are the symbol kinds if the field is resp. unnamed --// or named. path is the path of nested identifiers containing the field. --func (w *symbolWalker) walkField(field *ast.Field, unnamedKind, namedKind protocol.SymbolKind, path ...*ast.Ident) { -- if len(field.Names) == 0 { -- switch typ := field.Type.(type) { -- case *ast.SelectorExpr: -- // embedded qualified type -- w.atNode(field, typ.Sel.Name, unnamedKind, path...) -- default: -- w.atNode(field, types.ExprString(field.Type), unnamedKind, path...) +-// ExpandModificationsToDirectories returns the set of changes with the +-// directory changes removed and expanded to include all of the files in +-// the directory. +-func (s *Session) ExpandModificationsToDirectories(ctx context.Context, changes []file.Modification) []file.Modification { +- var snapshots []*Snapshot +- s.viewMu.Lock() +- for _, v := range s.views { +- snapshot, release, err := v.Snapshot() +- if err != nil { +- continue // view is shut down; continue with others - } +- defer release() +- snapshots = append(snapshots, snapshot) - } -- for _, name := range field.Names { -- w.atNode(name, name.Name, namedKind, path...) -- w.walkType(field.Type, append(path, name)...) +- s.viewMu.Unlock() +- +- // Expand the modification to any file we could care about, which we define +- // to be any file observed by any of the snapshots. +- // +- // There may be other files in the directory, but if we haven't read them yet +- // we don't need to invalidate them. +- var result []file.Modification +- for _, c := range changes { +- expanded := make(map[protocol.DocumentURI]bool) +- for _, snapshot := range snapshots { +- for _, uri := range snapshot.filesInDir(c.URI) { +- expanded[uri] = true +- } +- } +- if len(expanded) == 0 { +- result = append(result, c) +- } else { +- for uri := range expanded { +- result = append(result, file.Modification{ +- URI: uri, +- Action: c.Action, +- LanguageID: "", +- OnDisk: c.OnDisk, +- // changes to directories cannot include text or versions +- }) +- } +- } - } +- return result -} -diff -urN a/gopls/internal/lsp/cache/view.go b/gopls/internal/lsp/cache/view.go ---- a/gopls/internal/lsp/cache/view.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/view.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,1234 +0,0 @@ --// Copyright 2018 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --// Package cache implements the caching layer for gopls. --package cache -- --import ( -- "bytes" -- "context" -- "encoding/json" -- "errors" -- "fmt" -- "os" -- "path" -- "path/filepath" -- "regexp" -- "sort" -- "strings" -- "sync" -- "time" - -- "golang.org/x/mod/modfile" -- "golang.org/x/mod/semver" -- exec "golang.org/x/sys/execabs" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/gopls/internal/vulncheck" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/gocommand" -- "golang.org/x/tools/internal/imports" -- "golang.org/x/tools/internal/xcontext" --) -- --// A Folder represents an LSP workspace folder, together with its per-folder --// options. --// --// Folders (Name and Dir) are specified by the 'initialize' and subsequent --// 'didChangeWorkspaceFolders' requests; their options come from --// didChangeConfiguration. +-// updateOverlays updates the set of overlays and returns a map of any existing +-// overlay values that were replaced. -// --// Folders must not be mutated, as they may be shared across multiple views. --type Folder struct { -- Dir span.URI -- Name string -- Options *source.Options --} -- --type View struct { -- id string -- -- gocmdRunner *gocommand.Runner // limits go command concurrency -- -- folder *Folder -- -- // Workspace information. The fields below are immutable, and together with -- // options define the build list. Any change to these fields results in a new -- // View. -- *workspaceInformation // Go environment information -- -- importsState *importsState -- -- // moduleUpgrades tracks known upgrades for module paths in each modfile. -- // Each modfile has a map of module name to upgrade version. -- moduleUpgradesMu sync.Mutex -- moduleUpgrades map[span.URI]map[string]string +-// Precondition: caller holds s.viewMu lock. +-// TODO(rfindley): move this to fs_overlay.go. +-func (fs *overlayFS) updateOverlays(ctx context.Context, changes []file.Modification) (map[protocol.DocumentURI]*overlay, error) { +- fs.mu.Lock() +- defer fs.mu.Unlock() - -- // vulns maps each go.mod file's URI to its known vulnerabilities. -- vulnsMu sync.Mutex -- vulns map[span.URI]*vulncheck.Result +- replaced := make(map[protocol.DocumentURI]*overlay) +- for _, c := range changes { +- o, ok := fs.overlays[c.URI] +- if ok { +- replaced[c.URI] = o +- } - -- // parseCache holds an LRU cache of recently parsed files. -- parseCache *parseCache +- // If the file is not opened in an overlay and the change is on disk, +- // there's no need to update an overlay. If there is an overlay, we +- // may need to update the overlay's saved value. +- if !ok && c.OnDisk { +- continue +- } - -- // fs is the file source used to populate this view. -- fs *overlayFS +- // Determine the file kind on open, otherwise, assume it has been cached. +- var kind file.Kind +- switch c.Action { +- case file.Open: +- kind = file.KindForLang(c.LanguageID) +- default: +- if !ok { +- return nil, fmt.Errorf("updateOverlays: modifying unopened overlay %v", c.URI) +- } +- kind = o.kind +- } - -- // knownFiles tracks files that the view has accessed. -- // TODO(golang/go#57558): this notion is fundamentally problematic, and -- // should be removed. -- knownFilesMu sync.Mutex -- knownFiles map[span.URI]bool +- // Closing a file just deletes its overlay. +- if c.Action == file.Close { +- delete(fs.overlays, c.URI) +- continue +- } - -- // initCancelFirstAttempt can be used to terminate the view's first -- // attempt at initialization. -- initCancelFirstAttempt context.CancelFunc +- // If the file is on disk, check if its content is the same as in the +- // overlay. Saves and on-disk file changes don't come with the file's +- // content. +- text := c.Text +- if text == nil && (c.Action == file.Save || c.OnDisk) { +- if !ok { +- return nil, fmt.Errorf("no known content for overlay for %s", c.Action) +- } +- text = o.content +- } +- // On-disk changes don't come with versions. +- version := c.Version +- if c.OnDisk || c.Action == file.Save { +- version = o.version +- } +- hash := file.HashOf(text) +- var sameContentOnDisk bool +- switch c.Action { +- case file.Delete: +- // Do nothing. sameContentOnDisk should be false. +- case file.Save: +- // Make sure the version and content (if present) is the same. +- if false && o.version != version { // Client no longer sends the version +- return nil, fmt.Errorf("updateOverlays: saving %s at version %v, currently at %v", c.URI, c.Version, o.version) +- } +- if c.Text != nil && o.hash != hash { +- return nil, fmt.Errorf("updateOverlays: overlay %s changed on save", c.URI) +- } +- sameContentOnDisk = true +- default: +- fh := mustReadFile(ctx, fs.delegate, c.URI) +- _, readErr := fh.Content() +- sameContentOnDisk = (readErr == nil && fh.Identity().Hash == hash) +- } +- o = &overlay{ +- uri: c.URI, +- version: version, +- content: text, +- kind: kind, +- hash: hash, +- saved: sameContentOnDisk, +- } - -- // Track the latest snapshot via the snapshot field, guarded by snapshotMu. -- // -- // Invariant: whenever the snapshot field is overwritten, destroy(snapshot) -- // is called on the previous (overwritten) snapshot while snapshotMu is held, -- // incrementing snapshotWG. During shutdown the final snapshot is -- // overwritten with nil and destroyed, guaranteeing that all observed -- // snapshots have been destroyed via the destroy method, and snapshotWG may -- // be waited upon to let these destroy operations complete. -- snapshotMu sync.Mutex -- snapshot *snapshot // latest snapshot; nil after shutdown has been called -- releaseSnapshot func() // called when snapshot is no longer needed -- snapshotWG sync.WaitGroup // refcount for pending destroy operations +- // NOTE: previous versions of this code checked here that the overlay had a +- // view and file kind (but we don't know why). - -- // initialWorkspaceLoad is closed when the first workspace initialization has -- // completed. If we failed to load, we only retry if the go.mod file changes, -- // to avoid too many go/packages calls. -- initialWorkspaceLoad chan struct{} +- fs.overlays[c.URI] = o +- } - -- // initializationSema is used limit concurrent initialization of snapshots in -- // the view. We use a channel instead of a mutex to avoid blocking when a -- // context is canceled. -- // -- // This field (along with snapshot.initialized) guards against duplicate -- // initialization of snapshots. Do not change it without adjusting snapshot -- // accordingly. -- initializationSema chan struct{} +- return replaced, nil -} - --// workspaceInformation holds the defining features of the View workspace. --// --// This type is compared to see if the View needs to be reconstructed. --type workspaceInformation struct { -- // `go env` variables that need to be tracked by gopls. -- goEnv -- -- // gomod holds the relevant go.mod file for this workspace. -- gomod span.URI -- -- // The Go version in use: X in Go 1.X. -- goversion int -- -- // The complete output of the go version command. -- // (Call gocommand.ParseGoVersionOutput to extract a version -- // substring such as go1.19.1 or go1.20-rc.1, go1.21-abcdef01.) -- goversionOutput string -- -- // hasGopackagesDriver is true if the user has a value set for the -- // GOPACKAGESDRIVER environment variable or a gopackagesdriver binary on -- // their machine. -- hasGopackagesDriver bool -- -- // inGOPATH reports whether the workspace directory is contained in a GOPATH -- // directory. -- inGOPATH bool -- -- // goCommandDir is the dir to use for running go commands. -- // -- // The only case where this should matter is if we've narrowed the workspace to -- // a single nested module. In that case, the go command won't be able to find -- // the module unless we tell it the nested directory. -- goCommandDir span.URI +-func mustReadFile(ctx context.Context, fs file.Source, uri protocol.DocumentURI) file.Handle { +- ctx = xcontext.Detach(ctx) +- fh, err := fs.ReadFile(ctx, uri) +- if err != nil { +- // ReadFile cannot fail with an uncancellable context. +- bug.Reportf("reading file failed unexpectedly: %v", err) +- return brokenFile{uri, err} +- } +- return fh -} - --// effectiveGO111MODULE reports the value of GO111MODULE effective in the go --// command at this go version, assuming at least Go 1.16. --func (w workspaceInformation) effectiveGO111MODULE() go111module { -- switch w.GO111MODULE() { -- case "off": -- return off -- case "on", "": -- return on -- default: -- return auto -- } +-// A brokenFile represents an unexpected failure to read a file. +-type brokenFile struct { +- uri protocol.DocumentURI +- err error -} - --// A ViewType describes how we load package information for a view. +-func (b brokenFile) URI() protocol.DocumentURI { return b.uri } +-func (b brokenFile) Identity() file.Identity { return file.Identity{URI: b.uri} } +-func (b brokenFile) SameContentsOnDisk() bool { return false } +-func (b brokenFile) Version() int32 { return 0 } +-func (b brokenFile) Content() ([]byte, error) { return nil, b.err } +- +-// FileWatchingGlobPatterns returns a set of glob patterns that the client is +-// required to watch for changes, and notify the server of them, in order to +-// keep the server's state up to date. -// --// This is used for constructing the go/packages.Load query, and for --// interpreting missing packages, imports, or errors. +-// This set includes +-// 1. all go.mod and go.work files in the workspace; and +-// 2. for each Snapshot, its modules (or directory for ad-hoc views). In +-// module mode, this is the set of active modules (and for VS Code, all +-// workspace directories within them, due to golang/go#42348). -// --// Each view has a ViewType which is derived from its immutable workspace --// information -- any environment change that would affect the view type --// results in a new view. --type ViewType int -- --const ( -- // GoPackagesDriverView is a view with a non-empty GOPACKAGESDRIVER -- // environment variable. -- GoPackagesDriverView ViewType = iota -- -- // GOPATHView is a view in GOPATH mode. -- // -- // I.e. in GOPATH, with GO111MODULE=off, or GO111MODULE=auto with no -- // go.mod file. -- GOPATHView -- -- // GoModuleView is a view in module mode with a single Go module. -- GoModuleView -- -- // GoWorkView is a view in module mode with a go.work file. -- GoWorkView -- -- // An AdHocView is a collection of files in a given directory, not in GOPATH -- // or a module. -- AdHocView --) -- --// ViewType derives the type of the view from its workspace information. +-// The watch for workspace go.work and go.mod files in (1) is sufficient to +-// capture changes to the repo structure that may affect the set of views. +-// Whenever this set changes, we reload the workspace and invalidate memoized +-// files. -// --// TODO(rfindley): this logic is overlapping and slightly inconsistent with --// validBuildConfiguration. As part of zero-config-gopls (golang/go#57979), fix --// this inconsistency and consolidate on the ViewType abstraction. --func (w workspaceInformation) ViewType() ViewType { -- if w.hasGopackagesDriver { -- return GoPackagesDriverView -- } -- go111module := w.effectiveGO111MODULE() -- if w.gowork != "" && go111module != off { -- return GoWorkView -- } -- if w.gomod != "" && go111module != off { -- return GoModuleView -- } -- if w.inGOPATH && go111module != on { -- return GOPATHView -- } -- return AdHocView --} -- --// moduleMode reports whether the current snapshot uses Go modules. +-// The watch for workspace directories in (2) should keep each View up to date, +-// as it should capture any newly added/modified/deleted Go files. -// --// From https://go.dev/ref/mod, module mode is active if either of the --// following hold: --// - GO111MODULE=on --// - GO111MODULE=auto and we are inside a module or have a GOWORK value. +-// Patterns are returned as a set of protocol.RelativePatterns, since they can +-// always be later translated to glob patterns (i.e. strings) if the client +-// lacks relative pattern support. By convention, any pattern returned with +-// empty baseURI should be served as a glob pattern. -// --// Additionally, this method returns false if GOPACKAGESDRIVER is set. +-// In general, we prefer to serve relative patterns, as they work better on +-// most clients that support both, and do not have issues with Windows driver +-// letter casing: +-// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#relativePattern -// --// TODO(rfindley): use this more widely. --func (w workspaceInformation) moduleMode() bool { -- switch w.ViewType() { -- case GoModuleView, GoWorkView: -- return true -- default: -- return false +-// TODO(golang/go#57979): we need to reset the memoizedFS when a view changes. +-// Consider the case where we incidentally read a file, then it moved outside +-// of an active module, and subsequently changed: we would still observe the +-// original file state. +-func (s *Session) FileWatchingGlobPatterns(ctx context.Context) map[protocol.RelativePattern]unit { +- s.viewMu.Lock() +- defer s.viewMu.Unlock() +- +- // Always watch files that may change the set of views. +- patterns := map[protocol.RelativePattern]unit{ +- {Pattern: "**/*.{mod,work}"}: {}, - } --} - --// GOWORK returns the effective GOWORK value for this workspace, if --// any, in URI form. --// --// The second result reports whether the effective GOWORK value is "" because --// GOWORK=off. --func (w workspaceInformation) GOWORK() (span.URI, bool) { -- if w.gowork == "off" || w.gowork == "" { -- return "", w.gowork == "off" +- for _, view := range s.views { +- snapshot, release, err := view.Snapshot() +- if err != nil { +- continue // view is shut down; continue with others +- } +- for k, v := range snapshot.fileWatchingGlobPatterns() { +- patterns[k] = v +- } +- release() - } -- return span.URIFromPath(w.gowork), false +- return patterns -} - --// GO111MODULE returns the value of GO111MODULE to use for running the go --// command. It differs from the user's environment in order to allow for the --// more forgiving default value "auto" when using recent go versions. +-// OrphanedFileDiagnostics reports diagnostics describing why open files have +-// no packages or have only command-line-arguments packages. -// --// TODO(rfindley): it is probably not worthwhile diverging from the go command --// here. The extra forgiveness may be nice, but breaks the invariant that --// running the go command from the command line produces the same build list. +-// If the resulting diagnostic is nil, the file is either not orphaned or we +-// can't produce a good diagnostic. -// --// Put differently: we shouldn't go out of our way to make GOPATH work, when --// the go command does not. --func (w workspaceInformation) GO111MODULE() string { -- if w.go111module == "" { -- return "auto" +-// The caller must not mutate the result. +-func (s *Session) OrphanedFileDiagnostics(ctx context.Context) (map[protocol.DocumentURI][]*Diagnostic, error) { +- // Note: diagnostics holds a slice for consistency with other diagnostic +- // funcs. +- diagnostics := make(map[protocol.DocumentURI][]*Diagnostic) +- +- byView := make(map[*View][]*overlay) +- for _, o := range s.Overlays() { +- uri := o.URI() +- snapshot, release, err := s.SnapshotOf(ctx, uri) +- if err != nil { +- // TODO(golang/go#57979): we have to use the .go suffix as an approximation for +- // file kind here, because we don't have access to Options if no View was +- // matched. +- // +- // But Options are really a property of Folder, not View, and we could +- // match a folder here. +- // +- // Refactor so that Folders are tracked independently of Views, and use +- // the correct options here to get the most accurate file kind. +- // +- // TODO(golang/go#57979): once we switch entirely to the zeroconfig +- // logic, we should use this diagnostic for the fallback case of +- // s.views[0] in the ViewOf logic. +- if errors.Is(err, errNoViews) { +- if strings.HasSuffix(string(uri), ".go") { +- if _, rng, ok := orphanedFileDiagnosticRange(ctx, s.parseCache, o); ok { +- diagnostics[uri] = []*Diagnostic{{ +- URI: uri, +- Range: rng, +- Severity: protocol.SeverityWarning, +- Source: ListError, +- Message: fmt.Sprintf("No active builds contain %s: consider opening a new workspace folder containing it", uri.Path()), +- }} +- } +- } +- continue +- } +- return nil, err +- } +- v := snapshot.View() +- release() +- byView[v] = append(byView[v], o) +- } +- +- for view, overlays := range byView { +- snapshot, release, err := view.Snapshot() +- if err != nil { +- continue // view is shutting down +- } +- defer release() +- diags, err := snapshot.orphanedFileDiagnostics(ctx, overlays) +- if err != nil { +- return nil, err +- } +- for _, d := range diags { +- diagnostics[d.URI] = append(diagnostics[d.URI], d) +- } - } -- return w.go111module +- return diagnostics, nil -} +diff -urN a/gopls/internal/cache/session_test.go b/gopls/internal/cache/session_test.go +--- a/gopls/internal/cache/session_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/session_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,405 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --type go111module int +-package cache - --const ( -- off = go111module(iota) -- auto -- on +-import ( +- "context" +- "os" +- "path" +- "path/filepath" +- "testing" +- +- "github.com/google/go-cmp/cmp" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/settings" +- "golang.org/x/tools/gopls/internal/test/integration/fake" +- "golang.org/x/tools/internal/testenv" -) - --// goEnv holds important environment variables that gopls cares about. --type goEnv struct { -- gocache, gopath, goroot, goprivate, gomodcache, gowork, goflags string +-func TestZeroConfigAlgorithm(t *testing.T) { +- testenv.NeedsExec(t) // executes the Go command +- t.Setenv("GOPACKAGESDRIVER", "off") - -- // go111module holds the value of GO111MODULE as reported by go env. -- // -- // Don't use this value directly, because we choose to use a different -- // default (auto) on Go 1.16 and later, to avoid spurious errors. Use -- // the effectiveGO111MODULE method instead. -- go111module string --} +- type viewSummary struct { +- // fields exported for cmp.Diff +- Type ViewType +- Root string +- Env []string +- } - --// loadGoEnv loads `go env` values into the receiver, using the provided user --// environment and go command runner. --func (env *goEnv) load(ctx context.Context, folder string, configEnv []string, runner *gocommand.Runner) error { -- vars := env.vars() +- type folderSummary struct { +- dir string +- options func(dir string) map[string]any // options may refer to the temp dir +- } - -- // We can save ~200 ms by requesting only the variables we care about. -- args := []string{"-json"} -- for k := range vars { -- args = append(args, k) +- includeReplaceInWorkspace := func(string) map[string]any { +- return map[string]any{ +- "includeReplaceInWorkspace": true, +- } - } - -- inv := gocommand.Invocation{ -- Verb: "env", -- Args: args, -- Env: configEnv, -- WorkingDir: folder, +- type test struct { +- name string +- files map[string]string // use a map rather than txtar as file content is tiny +- folders []folderSummary +- open []string // open files +- want []viewSummary - } -- stdout, err := runner.Run(ctx, inv) -- if err != nil { -- return err -- } -- envMap := make(map[string]string) -- if err := json.Unmarshal(stdout.Bytes(), &envMap); err != nil { -- return fmt.Errorf("internal error unmarshaling JSON from 'go env': %w", err) -- } -- for key, ptr := range vars { -- *ptr = envMap[key] -- } -- -- return nil --} -- --func (env goEnv) String() string { -- var vars []string -- for govar, ptr := range env.vars() { -- vars = append(vars, fmt.Sprintf("%s=%s", govar, *ptr)) -- } -- sort.Strings(vars) -- return "[" + strings.Join(vars, ", ") + "]" --} -- --// vars returns a map from Go environment variable to field value containing it. --func (env *goEnv) vars() map[string]*string { -- return map[string]*string{ -- "GOCACHE": &env.gocache, -- "GOPATH": &env.gopath, -- "GOROOT": &env.goroot, -- "GOPRIVATE": &env.goprivate, -- "GOMODCACHE": &env.gomodcache, -- "GO111MODULE": &env.go111module, -- "GOWORK": &env.gowork, -- "GOFLAGS": &env.goflags, -- } --} - --// workspaceMode holds various flags defining how the gopls workspace should --// behave. They may be derived from the environment, user configuration, or --// depend on the Go version. --// --// TODO(rfindley): remove workspace mode, in favor of explicit checks. --type workspaceMode int +- tests := []test{ +- // TODO(rfindley): add a test for GOPACKAGESDRIVER. +- // Doing so doesn't yet work using options alone (user env is not honored) - --const ( -- moduleMode workspaceMode = 1 << iota +- // TODO(rfindley): add a test for degenerate cases, such as missing +- // workspace folders (once we decide on the correct behavior). +- { +- "basic go.work workspace", +- map[string]string{ +- "go.work": "go 1.18\nuse (\n\t./a\n\t./b\n)\n", +- "a/go.mod": "module golang.org/a\ngo 1.18\n", +- "b/go.mod": "module golang.org/b\ngo 1.18\n", +- }, +- []folderSummary{{dir: "."}}, +- nil, +- []viewSummary{{GoWorkView, ".", nil}}, +- }, +- { +- "basic go.mod workspace", +- map[string]string{ +- "go.mod": "module golang.org/a\ngo 1.18\n", +- }, +- []folderSummary{{dir: "."}}, +- nil, +- []viewSummary{{GoModView, ".", nil}}, +- }, +- { +- "basic GOPATH workspace", +- map[string]string{ +- "src/golang.org/a/a.go": "package a", +- "src/golang.org/b/b.go": "package b", +- }, +- []folderSummary{{ +- dir: "src", +- options: func(dir string) map[string]any { +- return map[string]any{ +- "env": map[string]any{ +- "GOPATH": dir, +- }, +- } +- }, +- }}, +- []string{"src/golang.org/a//a.go", "src/golang.org/b/b.go"}, +- []viewSummary{{GOPATHView, "src", nil}}, +- }, +- { +- "basic AdHoc workspace", +- map[string]string{ +- "foo.go": "package foo", +- }, +- []folderSummary{{dir: "."}}, +- nil, +- []viewSummary{{AdHocView, ".", nil}}, +- }, +- { +- "multi-folder workspace", +- map[string]string{ +- "a/go.mod": "module golang.org/a\ngo 1.18\n", +- "b/go.mod": "module golang.org/b\ngo 1.18\n", +- }, +- []folderSummary{{dir: "a"}, {dir: "b"}}, +- nil, +- []viewSummary{{GoModView, "a", nil}, {GoModView, "b", nil}}, +- }, +- { +- "multi-module workspace", +- map[string]string{ +- "a/go.mod": "module golang.org/a\ngo 1.18\n", +- "b/go.mod": "module golang.org/b\ngo 1.18\n", +- }, +- []folderSummary{{dir: "."}}, +- nil, +- []viewSummary{{AdHocView, ".", nil}}, +- }, +- { +- "zero-config open module", +- map[string]string{ +- "a/go.mod": "module golang.org/a\ngo 1.18\n", +- "a/a.go": "package a", +- "b/go.mod": "module golang.org/b\ngo 1.18\n", +- "b/b.go": "package b", +- }, +- []folderSummary{{dir: "."}}, +- []string{"a/a.go"}, +- []viewSummary{ +- {AdHocView, ".", nil}, +- {GoModView, "a", nil}, +- }, +- }, +- { +- "zero-config open modules", +- map[string]string{ +- "a/go.mod": "module golang.org/a\ngo 1.18\n", +- "a/a.go": "package a", +- "b/go.mod": "module golang.org/b\ngo 1.18\n", +- "b/b.go": "package b", +- }, +- []folderSummary{{dir: "."}}, +- []string{"a/a.go", "b/b.go"}, +- []viewSummary{ +- {AdHocView, ".", nil}, +- {GoModView, "a", nil}, +- {GoModView, "b", nil}, +- }, +- }, +- { +- "unified workspace", +- map[string]string{ +- "go.work": "go 1.18\nuse (\n\t./a\n\t./b\n)\n", +- "a/go.mod": "module golang.org/a\ngo 1.18\n", +- "a/a.go": "package a", +- "b/go.mod": "module golang.org/b\ngo 1.18\n", +- "b/b.go": "package b", +- }, +- []folderSummary{{dir: "."}}, +- []string{"a/a.go", "b/b.go"}, +- []viewSummary{{GoWorkView, ".", nil}}, +- }, +- { +- "go.work from env", +- map[string]string{ +- "nested/go.work": "go 1.18\nuse (\n\t../a\n\t../b\n)\n", +- "a/go.mod": "module golang.org/a\ngo 1.18\n", +- "a/a.go": "package a", +- "b/go.mod": "module golang.org/b\ngo 1.18\n", +- "b/b.go": "package b", +- }, +- []folderSummary{{ +- dir: ".", +- options: func(dir string) map[string]any { +- return map[string]any{ +- "env": map[string]any{ +- "GOWORK": filepath.Join(dir, "nested", "go.work"), +- }, +- } +- }, +- }}, +- []string{"a/a.go", "b/b.go"}, +- []viewSummary{{GoWorkView, ".", nil}}, +- }, +- { +- "independent module view", +- map[string]string{ +- "go.work": "go 1.18\nuse (\n\t./a\n)\n", // not using b +- "a/go.mod": "module golang.org/a\ngo 1.18\n", +- "a/a.go": "package a", +- "b/go.mod": "module golang.org/a\ngo 1.18\n", +- "b/b.go": "package b", +- }, +- []folderSummary{{dir: "."}}, +- []string{"a/a.go", "b/b.go"}, +- []viewSummary{ +- {GoWorkView, ".", nil}, +- {GoModView, "b", []string{"GOWORK=off"}}, +- }, +- }, +- { +- "multiple go.work", +- map[string]string{ +- "go.work": "go 1.18\nuse (\n\t./a\n\t./b\n)\n", +- "a/go.mod": "module golang.org/a\ngo 1.18\n", +- "a/a.go": "package a", +- "b/go.work": "go 1.18\nuse (\n\t.\n\t./c\n)\n", +- "b/go.mod": "module golang.org/b\ngo 1.18\n", +- "b/b.go": "package b", +- "b/c/go.mod": "module golang.org/c\ngo 1.18\n", +- }, +- []folderSummary{{dir: "."}}, +- []string{"a/a.go", "b/b.go", "b/c/c.go"}, +- []viewSummary{{GoWorkView, ".", nil}, {GoWorkView, "b", nil}}, +- }, +- { +- "multiple go.work, c unused", +- map[string]string{ +- "go.work": "go 1.18\nuse (\n\t./a\n\t./b\n)\n", +- "a/go.mod": "module golang.org/a\ngo 1.18\n", +- "a/a.go": "package a", +- "b/go.work": "go 1.18\nuse (\n\t.\n)\n", +- "b/go.mod": "module golang.org/b\ngo 1.18\n", +- "b/b.go": "package b", +- "b/c/go.mod": "module golang.org/c\ngo 1.18\n", +- }, +- []folderSummary{{dir: "."}}, +- []string{"a/a.go", "b/b.go", "b/c/c.go"}, +- []viewSummary{{GoWorkView, ".", nil}, {GoModView, "b/c", []string{"GOWORK=off"}}}, +- }, +- { +- "go.mod with nested replace", +- map[string]string{ +- "go.mod": "module golang.org/a\n require golang.org/b v1.2.3\nreplace example.com/b => ./b", +- "a.go": "package a", +- "b/go.mod": "module golang.org/b\ngo 1.18\n", +- "b/b.go": "package b", +- }, +- []folderSummary{{dir: ".", options: includeReplaceInWorkspace}}, +- []string{"a/a.go", "b/b.go"}, +- []viewSummary{{GoModView, ".", nil}}, +- }, +- { +- "go.mod with parent replace, parent folder", +- map[string]string{ +- "go.mod": "module golang.org/a", +- "a.go": "package a", +- "b/go.mod": "module golang.org/b\ngo 1.18\nrequire golang.org/a v1.2.3\nreplace golang.org/a => ../", +- "b/b.go": "package b", +- }, +- []folderSummary{{dir: ".", options: includeReplaceInWorkspace}}, +- []string{"a/a.go", "b/b.go"}, +- []viewSummary{{GoModView, ".", nil}, {GoModView, "b", nil}}, +- }, +- { +- "go.mod with multiple replace", +- map[string]string{ +- "go.mod": ` +-module golang.org/root - -- // tempModfile indicates whether or not the -modfile flag should be used. -- tempModfile +-require ( +- golang.org/a v1.2.3 +- golang.org/b v1.2.3 +- golang.org/c v1.2.3 -) - --func (v *View) ID() string { return v.id } -- --// tempModFile creates a temporary go.mod file based on the contents --// of the given go.mod file. On success, it is the caller's --// responsibility to call the cleanup function when the file is no --// longer needed. --func tempModFile(modFh source.FileHandle, gosum []byte) (tmpURI span.URI, cleanup func(), err error) { -- filenameHash := source.Hashf("%s", modFh.URI().Filename()) -- tmpMod, err := os.CreateTemp("", fmt.Sprintf("go.%s.*.mod", filenameHash)) -- if err != nil { -- return "", nil, err +-replace ( +- golang.org/b => ./b +- golang.org/c => ./c +- // Note: d is not replaced +-) +-`, +- "a.go": "package a", +- "b/go.mod": "module golang.org/b\ngo 1.18", +- "b/b.go": "package b", +- "c/go.mod": "module golang.org/c\ngo 1.18", +- "c/c.go": "package c", +- "d/go.mod": "module golang.org/d\ngo 1.18", +- "d/d.go": "package d", +- }, +- []folderSummary{{dir: ".", options: includeReplaceInWorkspace}}, +- []string{"b/b.go", "c/c.go", "d/d.go"}, +- []viewSummary{{GoModView, ".", nil}, {GoModView, "d", nil}}, +- }, +- { +- "go.mod with replace outside the workspace", +- map[string]string{ +- "go.mod": "module golang.org/a\ngo 1.18", +- "a.go": "package a", +- "b/go.mod": "module golang.org/b\ngo 1.18\nrequire golang.org/a v1.2.3\nreplace golang.org/a => ../", +- "b/b.go": "package b", +- }, +- []folderSummary{{dir: "b"}}, +- []string{"a.go", "b/b.go"}, +- []viewSummary{{GoModView, "b", nil}}, +- }, +- { +- "go.mod with replace directive; workspace replace off", +- map[string]string{ +- "go.mod": "module golang.org/a\n require golang.org/b v1.2.3\nreplace example.com/b => ./b", +- "a.go": "package a", +- "b/go.mod": "module golang.org/b\ngo 1.18\n", +- "b/b.go": "package b", +- }, +- []folderSummary{{ +- dir: ".", +- options: func(string) map[string]any { +- return map[string]any{ +- "includeReplaceInWorkspace": false, +- } +- }, +- }}, +- []string{"a/a.go", "b/b.go"}, +- []viewSummary{{GoModView, ".", nil}, {GoModView, "b", nil}}, +- }, - } -- defer tmpMod.Close() - -- tmpURI = span.URIFromPath(tmpMod.Name()) -- tmpSumName := sumFilename(tmpURI) +- for _, test := range tests { +- ctx := context.Background() +- t.Run(test.name, func(t *testing.T) { +- dir := writeFiles(t, test.files) +- rel := fake.RelativeTo(dir) +- fs := newMemoizedFS() +- +- toURI := func(path string) protocol.DocumentURI { +- return protocol.URIFromPath(rel.AbsPath(path)) +- } +- +- var folders []*Folder +- for _, f := range test.folders { +- opts := settings.DefaultOptions() +- if f.options != nil { +- results := settings.SetOptions(opts, f.options(dir)) +- for _, r := range results { +- if r.Error != nil { +- t.Fatalf("setting option %v: %v", r.Name, r.Error) +- } +- } +- } +- env, err := FetchGoEnv(ctx, toURI(f.dir), opts) +- if err != nil { +- t.Fatalf("FetchGoEnv failed: %v", err) +- } +- folders = append(folders, &Folder{ +- Dir: toURI(f.dir), +- Name: path.Base(f.dir), +- Options: opts, +- Env: env, +- }) +- } - -- content, err := modFh.Content() -- if err != nil { -- return "", nil, err -- } +- var openFiles []protocol.DocumentURI +- for _, path := range test.open { +- openFiles = append(openFiles, toURI(path)) +- } - -- if _, err := tmpMod.Write(content); err != nil { -- return "", nil, err +- defs, err := selectViewDefs(ctx, fs, folders, openFiles) +- if err != nil { +- t.Fatal(err) +- } +- var got []viewSummary +- for _, def := range defs { +- got = append(got, viewSummary{ +- Type: def.Type(), +- Root: rel.RelPath(def.root.Path()), +- Env: def.EnvOverlay(), +- }) +- } +- if diff := cmp.Diff(test.want, got); diff != "" { +- t.Errorf("selectViews() mismatch (-want +got):\n%s", diff) +- } +- }) - } +-} - -- // We use a distinct name here to avoid subtlety around the fact -- // that both 'return' and 'defer' update the "cleanup" variable. -- doCleanup := func() { -- _ = os.Remove(tmpSumName) -- _ = os.Remove(tmpURI.Filename()) +-// TODO(rfindley): this function could be meaningfully factored with the +-// various other test helpers of this nature. +-func writeFiles(t *testing.T, files map[string]string) string { +- root := t.TempDir() +- +- // This unfortunate step is required because gopls output +- // expands symbolic links in its input file names (arguably it +- // should not), and on macOS the temp dir is in /var -> private/var. +- root, err := filepath.EvalSymlinks(root) +- if err != nil { +- t.Fatal(err) - } - -- // Be careful to clean up if we return an error from this function. -- defer func() { -- if err != nil { -- doCleanup() -- cleanup = nil +- for name, content := range files { +- filename := filepath.Join(root, name) +- if err := os.MkdirAll(filepath.Dir(filename), 0777); err != nil { +- t.Fatal(err) - } -- }() -- -- // Create an analogous go.sum, if one exists. -- if gosum != nil { -- if err := os.WriteFile(tmpSumName, gosum, 0655); err != nil { -- return "", nil, err +- if err := os.WriteFile(filename, []byte(content), 0666); err != nil { +- t.Fatal(err) - } - } -- -- return tmpURI, doCleanup, nil +- return root -} +diff -urN a/gopls/internal/cache/snapshot.go b/gopls/internal/cache/snapshot.go +--- a/gopls/internal/cache/snapshot.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/snapshot.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,2356 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// Name returns the user visible name of this view. --func (v *View) Name() string { -- return v.folder.Name --} +-package cache - --// Folder returns the folder at the base of this view. --func (v *View) Folder() span.URI { -- return v.folder.Dir --} +-import ( +- "bytes" +- "context" +- "errors" +- "fmt" +- "go/ast" +- "go/build/constraint" +- "go/parser" +- "go/token" +- "go/types" +- "io" +- "os" +- "path" +- "path/filepath" +- "regexp" +- "runtime" +- "sort" +- "strconv" +- "strings" +- "sync" +- +- "golang.org/x/sync/errgroup" +- "golang.org/x/tools/go/packages" +- "golang.org/x/tools/go/types/objectpath" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/cache/methodsets" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/cache/typerefs" +- "golang.org/x/tools/gopls/internal/cache/xrefs" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/filecache" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/command" +- "golang.org/x/tools/gopls/internal/settings" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/gopls/internal/util/constraints" +- "golang.org/x/tools/gopls/internal/util/immutable" +- "golang.org/x/tools/gopls/internal/util/pathutil" +- "golang.org/x/tools/gopls/internal/util/persistent" +- "golang.org/x/tools/gopls/internal/util/slices" +- "golang.org/x/tools/gopls/internal/vulncheck" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/label" +- "golang.org/x/tools/internal/event/tag" +- "golang.org/x/tools/internal/gocommand" +- "golang.org/x/tools/internal/memoize" +- "golang.org/x/tools/internal/packagesinternal" +- "golang.org/x/tools/internal/typesinternal" +-) - --// SetFolderOptions updates the options of each View associated with the folder --// of the given URI. +-// A Snapshot represents the current state for a given view. -// --// Calling this may cause each related view to be invalidated and a replacement --// view added to the session. --func (s *Session) SetFolderOptions(ctx context.Context, uri span.URI, options *source.Options) error { -- s.viewMu.Lock() -- defer s.viewMu.Unlock() +-// It is first and foremost an idempotent implementation of file.Source whose +-// ReadFile method returns consistent information about the existence and +-// content of each file throughout its lifetime. +-// +-// However, the snapshot also manages additional state (such as parsed files +-// and packages) that are derived from file content. +-// +-// Snapshots are responsible for bookkeeping and invalidation of this state, +-// implemented in Snapshot.clone. +-type Snapshot struct { +- // sequenceID is the monotonically increasing ID of this snapshot within its View. +- // +- // Sequence IDs for Snapshots from different Views cannot be compared. +- sequenceID uint64 - -- for _, v := range s.views { -- if v.folder.Dir == uri { -- folder2 := *v.folder -- folder2.Options = options -- info, err := getWorkspaceInformation(ctx, s.gocmdRunner, s, &folder2) -- if err != nil { -- return err -- } -- if _, err := s.updateViewLocked(ctx, v, info, &folder2); err != nil { -- return err -- } -- } -- } -- return nil --} +- // TODO(rfindley): the snapshot holding a reference to the view poses +- // lifecycle problems: a view may be shut down and waiting for work +- // associated with this snapshot to complete. While most accesses of the view +- // are benign (options or workspace information), this is not formalized and +- // it is wrong for the snapshot to use a shutdown view. +- // +- // Fix this by passing options and workspace information to the snapshot, +- // both of which should be immutable for the snapshot. +- view *View - --// viewEnv returns a string describing the environment of a newly created view. --// --// It must not be called concurrently with any other view methods. --func viewEnv(v *View) string { -- env := v.folder.Options.EnvSlice() -- buildFlags := append([]string{}, v.folder.Options.BuildFlags...) +- cancel func() +- backgroundCtx context.Context - -- var buf bytes.Buffer -- fmt.Fprintf(&buf, `go info for %v --(go dir %s) --(go version %s) --(valid build configuration = %v) --(build flags: %v) --(selected go env: %v) --`, -- v.folder.Dir.Filename(), -- v.goCommandDir.Filename(), -- strings.TrimRight(v.workspaceInformation.goversionOutput, "\n"), -- v.snapshot.validBuildConfiguration(), -- buildFlags, -- v.goEnv, -- ) +- store *memoize.Store // cache of handles shared by all snapshots - -- for _, v := range env { -- s := strings.SplitN(v, "=", 2) -- if len(s) != 2 { -- continue -- } -- } +- refMu sync.Mutex - -- return buf.String() --} +- // refcount holds the number of outstanding references to the current +- // Snapshot. When refcount is decremented to 0, the Snapshot maps are +- // destroyed and the done function is called. +- // +- // TODO(rfindley): use atomic.Int32 on Go 1.19+. +- refcount int +- done func() // for implementing Session.Shutdown - --func (s *snapshot) RunProcessEnvFunc(ctx context.Context, fn func(context.Context, *imports.Options) error) error { -- return s.view.importsState.runProcessEnvFunc(ctx, s, fn) --} +- // mu guards all of the maps in the snapshot, as well as the builtin URI and +- // initialized. +- mu sync.Mutex - --// separated out from its sole use in locateTemplateFiles for testability --func fileHasExtension(path string, suffixes []string) bool { -- ext := filepath.Ext(path) -- if ext != "" && ext[0] == '.' { -- ext = ext[1:] -- } -- for _, s := range suffixes { -- if s != "" && ext == s { -- return true -- } -- } -- return false --} +- // initialized reports whether the snapshot has been initialized. Concurrent +- // initialization is guarded by the view.initializationSema. Each snapshot is +- // initialized at most once: concurrent initialization is guarded by +- // view.initializationSema. +- initialized bool - --// locateTemplateFiles ensures that the snapshot has mapped template files --// within the workspace folder. --func (s *snapshot) locateTemplateFiles(ctx context.Context) { -- suffixes := s.Options().TemplateExtensions -- if len(suffixes) == 0 { -- return -- } +- // initialErr holds the last error resulting from initialization. If +- // initialization fails, we only retry when the workspace modules change, +- // to avoid too many go/packages calls. +- // If initialized is false, initialErr stil holds the error resulting from +- // the previous initialization. +- // TODO(rfindley): can we unify the lifecycle of initialized and initialErr. +- initialErr *InitializationError - -- searched := 0 -- filterFunc := s.view.filterFunc() -- err := filepath.WalkDir(s.view.folder.Dir.Filename(), func(path string, entry os.DirEntry, err error) error { -- if err != nil { -- return err -- } -- if entry.IsDir() { -- return nil -- } -- if fileLimit > 0 && searched > fileLimit { -- return errExhausted -- } -- searched++ -- if !fileHasExtension(path, suffixes) { -- return nil -- } -- uri := span.URIFromPath(path) -- if filterFunc(uri) { -- return nil -- } -- // Get the file in order to include it in the snapshot. -- // TODO(golang/go#57558): it is fundamentally broken to track files in this -- // way; we may lose them if configuration or layout changes cause a view to -- // be recreated. -- // -- // Furthermore, this operation must ignore errors, including context -- // cancellation, or risk leaving the snapshot in an undefined state. -- s.ReadFile(ctx, uri) -- return nil -- }) -- if err != nil { -- event.Error(ctx, "searching for template files failed", err) -- } --} +- // builtin is the location of builtin.go in GOROOT. +- // +- // TODO(rfindley): would it make more sense to eagerly parse builtin, and +- // instead store a *parsego.File here? +- builtin protocol.DocumentURI - --func (v *View) contains(uri span.URI) bool { -- // If we've expanded the go dir to a parent directory, consider if the -- // expanded dir contains the uri. -- // TODO(rfindley): should we ignore the root here? It is not provided by the -- // user. It would be better to explicitly consider the set of active modules -- // wherever relevant. -- inGoDir := false -- if source.InDir(v.goCommandDir.Filename(), v.folder.Dir.Filename()) { -- inGoDir = source.InDir(v.goCommandDir.Filename(), uri.Filename()) -- } -- inFolder := source.InDir(v.folder.Dir.Filename(), uri.Filename()) +- // meta holds loaded metadata. +- // +- // meta is guarded by mu, but the Graph itself is immutable. +- // +- // TODO(rfindley): in many places we hold mu while operating on meta, even +- // though we only need to hold mu while reading the pointer. +- meta *metadata.Graph - -- if !inGoDir && !inFolder { -- return false -- } +- // files maps file URIs to their corresponding FileHandles. +- // It may invalidated when a file's content changes. +- files *fileMap - -- return !v.filterFunc()(uri) --} +- // symbolizeHandles maps each file URI to a handle for the future +- // result of computing the symbols declared in that file. +- symbolizeHandles *persistent.Map[protocol.DocumentURI, *memoize.Promise] // *memoize.Promise[symbolizeResult] - --// filterFunc returns a func that reports whether uri is filtered by the currently configured --// directoryFilters. --func (v *View) filterFunc() func(span.URI) bool { -- folderDir := v.folder.Dir.Filename() -- filterer := buildFilterer(folderDir, v.gomodcache, v.folder.Options) -- return func(uri span.URI) bool { -- // Only filter relative to the configured root directory. -- if source.InDir(folderDir, uri.Filename()) { -- return pathExcludedByFilter(strings.TrimPrefix(uri.Filename(), folderDir), filterer) -- } -- return false -- } --} +- // packages maps a packageKey to a *packageHandle. +- // It may be invalidated when a file's content changes. +- // +- // Invariants to preserve: +- // - packages.Get(id).meta == meta.metadata[id] for all ids +- // - if a package is in packages, then all of its dependencies should also +- // be in packages, unless there is a missing import +- packages *persistent.Map[PackageID, *packageHandle] - --func (v *View) relevantChange(c source.FileModification) bool { -- // If the file is known to the view, the change is relevant. -- if v.knownFile(c.URI) { -- return true -- } -- // The go.work file may not be "known" because we first access it through the -- // session. As a result, treat changes to the view's go.work file as always -- // relevant, even if they are only on-disk changes. +- // activePackages maps a package ID to a memoized active package, or nil if +- // the package is known not to be open. - // -- // TODO(rfindley): Make sure the go.work files are always known -- // to the view. -- if gowork, _ := v.GOWORK(); gowork == c.URI { -- return true -- } +- // IDs not contained in the map are not known to be open or not open. +- activePackages *persistent.Map[PackageID, *Package] - -- // Note: CL 219202 filtered out on-disk changes here that were not known to -- // the view, but this introduces a race when changes arrive before the view -- // is initialized (and therefore, before it knows about files). Since that CL -- // had neither test nor associated issue, and cited only emacs behavior, this -- // logic was deleted. +- // workspacePackages contains the workspace's packages, which are loaded +- // when the view is created. It does not contain intermediate test variants. +- workspacePackages immutable.Map[PackageID, PackagePath] - -- return v.contains(c.URI) --} +- // shouldLoad tracks packages that need to be reloaded, mapping a PackageID +- // to the package paths that should be used to reload it +- // +- // When we try to load a package, we clear it from the shouldLoad map +- // regardless of whether the load succeeded, to prevent endless loads. +- shouldLoad *persistent.Map[PackageID, []PackagePath] - --func (v *View) markKnown(uri span.URI) { -- v.knownFilesMu.Lock() -- defer v.knownFilesMu.Unlock() -- if v.knownFiles == nil { -- v.knownFiles = make(map[span.URI]bool) -- } -- v.knownFiles[uri] = true --} +- // unloadableFiles keeps track of files that we've failed to load. +- unloadableFiles *persistent.Set[protocol.DocumentURI] - --// knownFile reports whether the specified valid URI (or an alias) is known to the view. --func (v *View) knownFile(uri span.URI) bool { -- v.knownFilesMu.Lock() -- defer v.knownFilesMu.Unlock() -- return v.knownFiles[uri] --} +- // TODO(rfindley): rename the handles below to "promises". A promise is +- // different from a handle (we mutate the package handle.) - --// shutdown releases resources associated with the view, and waits for ongoing --// work to complete. --func (v *View) shutdown() { -- // Cancel the initial workspace load if it is still running. -- v.initCancelFirstAttempt() +- // parseModHandles keeps track of any parseModHandles for the snapshot. +- // The handles need not refer to only the view's go.mod file. +- parseModHandles *persistent.Map[protocol.DocumentURI, *memoize.Promise] // *memoize.Promise[parseModResult] - -- v.snapshotMu.Lock() -- if v.snapshot != nil { -- v.snapshot.cancel() -- v.releaseSnapshot() -- v.destroy(v.snapshot, "View.shutdown") -- v.snapshot = nil -- v.releaseSnapshot = nil -- } -- v.snapshotMu.Unlock() +- // parseWorkHandles keeps track of any parseWorkHandles for the snapshot. +- // The handles need not refer to only the view's go.work file. +- parseWorkHandles *persistent.Map[protocol.DocumentURI, *memoize.Promise] // *memoize.Promise[parseWorkResult] +- +- // Preserve go.mod-related handles to avoid garbage-collecting the results +- // of various calls to the go command. The handles need not refer to only +- // the view's go.mod file. +- modTidyHandles *persistent.Map[protocol.DocumentURI, *memoize.Promise] // *memoize.Promise[modTidyResult] +- modWhyHandles *persistent.Map[protocol.DocumentURI, *memoize.Promise] // *memoize.Promise[modWhyResult] +- modVulnHandles *persistent.Map[protocol.DocumentURI, *memoize.Promise] // *memoize.Promise[modVulnResult] +- +- // importGraph holds a shared import graph to use for type-checking. Adding +- // more packages to this import graph can speed up type checking, at the +- // expense of in-use memory. +- // +- // See getImportGraph for additional documentation. +- importGraphDone chan struct{} // closed when importGraph is set; may be nil +- importGraph *importGraph // copied from preceding snapshot and re-evaluated +- +- // pkgIndex is an index of package IDs, for efficient storage of typerefs. +- pkgIndex *typerefs.PackageIndex - -- v.snapshotWG.Wait() +- // moduleUpgrades tracks known upgrades for module paths in each modfile. +- // Each modfile has a map of module name to upgrade version. +- moduleUpgrades *persistent.Map[protocol.DocumentURI, map[string]string] +- +- // vulns maps each go.mod file's URI to its known vulnerabilities. +- vulns *persistent.Map[protocol.DocumentURI, *vulncheck.Result] +- +- // gcOptimizationDetails describes the packages for which we want +- // optimization details to be included in the diagnostics. +- gcOptimizationDetails map[metadata.PackageID]unit -} - --// While go list ./... skips directories starting with '.', '_', or 'testdata', --// gopls may still load them via file queries. Explicitly filter them out. --func (s *snapshot) IgnoredFile(uri span.URI) bool { -- // Fast path: if uri doesn't contain '.', '_', or 'testdata', it is not -- // possible that it is ignored. -- { -- uriStr := string(uri) -- if !strings.Contains(uriStr, ".") && !strings.Contains(uriStr, "_") && !strings.Contains(uriStr, "testdata") { -- return false -- } -- } +-var _ memoize.RefCounted = (*Snapshot)(nil) // snapshots are reference-counted - -- s.ignoreFilterOnce.Do(func() { -- var dirs []string -- if len(s.workspaceModFiles) == 0 { -- for _, entry := range filepath.SplitList(s.view.gopath) { -- dirs = append(dirs, filepath.Join(entry, "src")) -- } -- } else { -- dirs = append(dirs, s.view.gomodcache) -- for m := range s.workspaceModFiles { -- dirs = append(dirs, filepath.Dir(m.Filename())) -- } -- } -- s.ignoreFilter = newIgnoreFilter(dirs) -- }) +-func (s *Snapshot) awaitPromise(ctx context.Context, p *memoize.Promise) (interface{}, error) { +- return p.Get(ctx, s) +-} - -- return s.ignoreFilter.ignored(uri.Filename()) +-// Acquire prevents the snapshot from being destroyed until the returned +-// function is called. +-// +-// (s.Acquire().release() could instead be expressed as a pair of +-// method calls s.IncRef(); s.DecRef(). The latter has the advantage +-// that the DecRefs are fungible and don't require holding anything in +-// addition to the refcounted object s, but paradoxically that is also +-// an advantage of the current approach, which forces the caller to +-// consider the release function at every stage, making a reference +-// leak more obvious.) +-func (s *Snapshot) Acquire() func() { +- s.refMu.Lock() +- defer s.refMu.Unlock() +- assert(s.refcount > 0, "non-positive refs") +- s.refcount++ +- +- return s.decref +-} +- +-// decref should only be referenced by Acquire, and by View when it frees its +-// reference to View.snapshot. +-func (s *Snapshot) decref() { +- s.refMu.Lock() +- defer s.refMu.Unlock() +- +- assert(s.refcount > 0, "non-positive refs") +- s.refcount-- +- if s.refcount == 0 { +- s.packages.Destroy() +- s.activePackages.Destroy() +- s.files.destroy() +- s.symbolizeHandles.Destroy() +- s.parseModHandles.Destroy() +- s.parseWorkHandles.Destroy() +- s.modTidyHandles.Destroy() +- s.modVulnHandles.Destroy() +- s.modWhyHandles.Destroy() +- s.unloadableFiles.Destroy() +- s.moduleUpgrades.Destroy() +- s.vulns.Destroy() +- s.done() +- } +-} +- +-// SequenceID is the sequence id of this snapshot within its containing +-// view. +-// +-// Relative to their view sequence ids are monotonically increasing, but this +-// does not hold globally: when new views are created their initial snapshot +-// has sequence ID 0. +-func (s *Snapshot) SequenceID() uint64 { +- return s.sequenceID -} - --// An ignoreFilter implements go list's exclusion rules via its 'ignored' method. --type ignoreFilter struct { -- prefixes []string // root dirs, ending in filepath.Separator +-// SnapshotLabels returns a new slice of labels that should be used for events +-// related to a snapshot. +-func (s *Snapshot) Labels() []label.Label { +- return []label.Label{tag.Snapshot.Of(s.SequenceID()), tag.Directory.Of(s.Folder())} -} - --// newIgnoreFilter returns a new ignoreFilter implementing exclusion rules --// relative to the provided directories. --func newIgnoreFilter(dirs []string) *ignoreFilter { -- f := new(ignoreFilter) -- for _, d := range dirs { -- f.prefixes = append(f.prefixes, filepath.Clean(d)+string(filepath.Separator)) -- } -- return f +-// Folder returns the folder at the base of this snapshot. +-func (s *Snapshot) Folder() protocol.DocumentURI { +- return s.view.folder.Dir -} - --func (f *ignoreFilter) ignored(filename string) bool { -- for _, prefix := range f.prefixes { -- if suffix := strings.TrimPrefix(filename, prefix); suffix != filename { -- if checkIgnored(suffix) { -- return true -- } -- } -- } -- return false +-// View returns the View associated with this snapshot. +-func (s *Snapshot) View() *View { +- return s.view -} - --// checkIgnored implements go list's exclusion rules. --// Quoting “go help list”: +-// FileKind returns the kind of a file. -// --// Directory and file names that begin with "." or "_" are ignored --// by the go tool, as are directories named "testdata". --func checkIgnored(suffix string) bool { -- // Note: this could be further optimized by writing a HasSegment helper, a -- // segment-boundary respecting variant of strings.Contains. -- for _, component := range strings.Split(suffix, string(filepath.Separator)) { -- if len(component) == 0 { -- continue -- } -- if component[0] == '.' || component[0] == '_' || component == "testdata" { -- return true +-// We can't reliably deduce the kind from the file name alone, +-// as some editors can be told to interpret a buffer as +-// language different from the file name heuristic, e.g. that +-// an .html file actually contains Go "html/template" syntax, +-// or even that a .go file contains Python. +-func (s *Snapshot) FileKind(fh file.Handle) file.Kind { +- if k := fileKind(fh); k != file.UnknownKind { +- return k +- } +- fext := filepath.Ext(fh.URI().Path()) +- exts := s.Options().TemplateExtensions +- for _, ext := range exts { +- if fext == ext || fext == "."+ext { +- return file.Tmpl - } - } -- return false --} - --func (v *View) Snapshot() (source.Snapshot, func(), error) { -- return v.getSnapshot() +- // and now what? This should never happen, but it does for cgo before go1.15 +- // +- // TODO(rfindley): this doesn't look right. We should default to UnknownKind. +- // Also, I don't understand the comment above, though I'd guess before go1.15 +- // we encountered cgo files without the .go extension. +- return file.Go -} - --func (v *View) getSnapshot() (*snapshot, func(), error) { -- v.snapshotMu.Lock() -- defer v.snapshotMu.Unlock() -- if v.snapshot == nil { -- return nil, nil, errors.New("view is shutdown") +-// fileKind returns the default file kind for a file, before considering +-// template file extensions. See [Snapshot.FileKind]. +-func fileKind(fh file.Handle) file.Kind { +- // The kind of an unsaved buffer comes from the +- // TextDocumentItem.LanguageID field in the didChange event, +- // not from the file name. They may differ. +- if o, ok := fh.(*overlay); ok { +- if o.kind != file.UnknownKind { +- return o.kind +- } - } -- return v.snapshot, v.snapshot.Acquire(), nil --} - --func (s *snapshot) initialize(ctx context.Context, firstAttempt bool) { -- select { -- case <-ctx.Done(): -- return -- case s.view.initializationSema <- struct{}{}: +- fext := filepath.Ext(fh.URI().Path()) +- switch fext { +- case ".go": +- return file.Go +- case ".mod": +- return file.Mod +- case ".sum": +- return file.Sum +- case ".work": +- return file.Work - } +- return file.UnknownKind +-} - -- defer func() { -- <-s.view.initializationSema -- }() +-// Options returns the options associated with this snapshot. +-func (s *Snapshot) Options() *settings.Options { +- return s.view.folder.Options +-} - -- s.mu.Lock() -- initialized := s.initialized -- s.mu.Unlock() +-// BackgroundContext returns a context used for all background processing +-// on behalf of this snapshot. +-func (s *Snapshot) BackgroundContext() context.Context { +- return s.backgroundCtx +-} - -- if initialized { -- return -- } +-// Templates returns the .tmpl files. +-func (s *Snapshot) Templates() map[protocol.DocumentURI]file.Handle { +- s.mu.Lock() +- defer s.mu.Unlock() - -- s.loadWorkspace(ctx, firstAttempt) +- tmpls := map[protocol.DocumentURI]file.Handle{} +- s.files.foreach(func(k protocol.DocumentURI, fh file.Handle) { +- if s.FileKind(fh) == file.Tmpl { +- tmpls[k] = fh +- } +- }) +- return tmpls -} - --func (s *snapshot) loadWorkspace(ctx context.Context, firstAttempt bool) (loadErr error) { -- // A failure is retryable if it may have been due to context cancellation, -- // and this is not the initial workspace load (firstAttempt==true). -- // -- // The IWL runs on a detached context with a long (~10m) timeout, so -- // if the context was canceled we consider loading to have failed -- // permanently. -- retryableFailure := func() bool { -- return loadErr != nil && ctx.Err() != nil && !firstAttempt -- } -- defer func() { -- if !retryableFailure() { -- s.mu.Lock() -- s.initialized = true -- s.mu.Unlock() -- } -- if firstAttempt { -- close(s.view.initialWorkspaceLoad) -- } -- }() -- -- // TODO(rFindley): we should only locate template files on the first attempt, -- // or guard it via a different mechanism. -- s.locateTemplateFiles(ctx) -- -- // Collect module paths to load by parsing go.mod files. If a module fails to -- // parse, capture the parsing failure as a critical diagnostic. -- var scopes []loadScope // scopes to load -- var modDiagnostics []*source.Diagnostic // diagnostics for broken go.mod files -- addError := func(uri span.URI, err error) { -- modDiagnostics = append(modDiagnostics, &source.Diagnostic{ -- URI: uri, -- Severity: protocol.SeverityError, -- Source: source.ListError, -- Message: err.Error(), -- }) -- } +-// config returns the configuration used for the snapshot's interaction with +-// the go/packages API. It uses the given working directory. +-// +-// TODO(rstambler): go/packages requires that we do not provide overlays for +-// multiple modules in on config, so buildOverlay needs to filter overlays by +-// module. +-func (s *Snapshot) config(ctx context.Context, inv *gocommand.Invocation) *packages.Config { - -- // TODO(rfindley): this should be predicated on the s.view.moduleMode(). -- // There is no point loading ./... if we have an empty go.work. -- if len(s.workspaceModFiles) > 0 { -- for modURI := range s.workspaceModFiles { -- // Verify that the modfile is valid before trying to load it. -- // -- // TODO(rfindley): now that we no longer need to parse the modfile in -- // order to load scope, we could move these diagnostics to a more general -- // location where we diagnose problems with modfiles or the workspace. -- // -- // Be careful not to add context cancellation errors as critical module -- // errors. -- fh, err := s.ReadFile(ctx, modURI) -- if err != nil { -- if ctx.Err() != nil { -- return ctx.Err() -- } -- addError(modURI, err) -- continue -- } -- parsed, err := s.ParseMod(ctx, fh) -- if err != nil { -- if ctx.Err() != nil { -- return ctx.Err() -- } -- addError(modURI, err) -- continue -- } -- if parsed.File == nil || parsed.File.Module == nil { -- addError(modURI, fmt.Errorf("no module path for %s", modURI)) -- continue +- cfg := &packages.Config{ +- Context: ctx, +- Dir: inv.WorkingDir, +- Env: inv.Env, +- BuildFlags: inv.BuildFlags, +- Mode: packages.NeedName | +- packages.NeedFiles | +- packages.NeedCompiledGoFiles | +- packages.NeedImports | +- packages.NeedDeps | +- packages.NeedTypesSizes | +- packages.NeedModule | +- packages.NeedEmbedFiles | +- packages.LoadMode(packagesinternal.DepsErrors) | +- packages.LoadMode(packagesinternal.ForTest), +- Fset: nil, // we do our own parsing +- Overlay: s.buildOverlay(), +- ParseFile: func(*token.FileSet, string, []byte) (*ast.File, error) { +- panic("go/packages must not be used to parse files") +- }, +- Logf: func(format string, args ...interface{}) { +- if s.Options().VerboseOutput { +- event.Log(ctx, fmt.Sprintf(format, args...)) - } -- moduleDir := filepath.Dir(modURI.Filename()) -- // Previously, we loaded /... for each module path, but that -- // is actually incorrect when the pattern may match packages in more than -- // one module. See golang/go#59458 for more details. -- scopes = append(scopes, moduleLoadScope{dir: moduleDir, modulePath: parsed.File.Module.Mod.Path}) -- } -- } else { -- scopes = append(scopes, viewLoadScope("LOAD_VIEW")) +- }, +- Tests: true, - } -- -- // If we're loading anything, ensure we also load builtin, -- // since it provides fake definitions (and documentation) -- // for types like int that are used everywhere. -- if len(scopes) > 0 { -- scopes = append(scopes, packageLoadScope("builtin")) +- packagesinternal.SetModFile(cfg, inv.ModFile) +- packagesinternal.SetModFlag(cfg, inv.ModFlag) +- // We want to type check cgo code if go/types supports it. +- if typesinternal.SetUsesCgo(&types.Config{}) { +- cfg.Mode |= packages.LoadMode(packagesinternal.TypecheckCgo) - } -- loadErr = s.load(ctx, true, scopes...) +- return cfg +-} - -- if retryableFailure() { -- return loadErr -- } +-// InvocationFlags represents the settings of a particular go command invocation. +-// It is a mode, plus a set of flag bits. +-type InvocationFlags int - -- var criticalErr *source.CriticalError -- switch { -- case loadErr != nil && ctx.Err() != nil: -- event.Error(ctx, fmt.Sprintf("initial workspace load: %v", loadErr), loadErr) -- criticalErr = &source.CriticalError{ -- MainError: loadErr, -- } -- case loadErr != nil: -- event.Error(ctx, "initial workspace load failed", loadErr) -- extractedDiags := s.extractGoCommandErrors(ctx, loadErr) -- criticalErr = &source.CriticalError{ -- MainError: loadErr, -- Diagnostics: append(modDiagnostics, extractedDiags...), -- } -- case len(modDiagnostics) == 1: -- criticalErr = &source.CriticalError{ -- MainError: fmt.Errorf(modDiagnostics[0].Message), -- Diagnostics: modDiagnostics, -- } -- case len(modDiagnostics) > 1: -- criticalErr = &source.CriticalError{ -- MainError: fmt.Errorf("error loading module names"), -- Diagnostics: modDiagnostics, -- } -- } +-const ( +- // Normal is appropriate for commands that might be run by a user and don't +- // deliberately modify go.mod files, e.g. `go test`. +- Normal InvocationFlags = iota +- // WriteTemporaryModFile is for commands that need information from a +- // modified version of the user's go.mod file, e.g. `go mod tidy` used to +- // generate diagnostics. +- WriteTemporaryModFile +- // LoadWorkspace is for packages.Load, and other operations that should +- // consider the whole workspace at once. +- LoadWorkspace +- // AllowNetwork is a flag bit that indicates the invocation should be +- // allowed to access the network. +- AllowNetwork InvocationFlags = 1 << 10 +-) - -- // Lock the snapshot when setting the initialized error. -- s.mu.Lock() -- defer s.mu.Unlock() -- s.initializedErr = criticalErr -- return loadErr +-func (m InvocationFlags) Mode() InvocationFlags { +- return m & (AllowNetwork - 1) -} - --// invalidateContent invalidates the content of a Go file, --// including any position and type information that depends on it. --// --// invalidateContent returns a non-nil snapshot for the new content, along with --// a callback which the caller must invoke to release that snapshot. --// --// newOptions may be nil, in which case options remain unchanged. --func (v *View) invalidateContent(ctx context.Context, changes map[span.URI]source.FileHandle) (*snapshot, func()) { -- // Detach the context so that content invalidation cannot be canceled. -- ctx = xcontext.Detach(ctx) -- -- // This should be the only time we hold the view's snapshot lock for any period of time. -- v.snapshotMu.Lock() -- defer v.snapshotMu.Unlock() -- -- prevSnapshot, prevReleaseSnapshot := v.snapshot, v.releaseSnapshot +-func (m InvocationFlags) AllowNetwork() bool { +- return m&AllowNetwork != 0 +-} - -- if prevSnapshot == nil { -- panic("invalidateContent called after shutdown") +-// RunGoCommandDirect runs the given `go` command. Verb, Args, and +-// WorkingDir must be specified. +-func (s *Snapshot) RunGoCommandDirect(ctx context.Context, mode InvocationFlags, inv *gocommand.Invocation) (*bytes.Buffer, error) { +- _, inv, cleanup, err := s.goCommandInvocation(ctx, mode, inv) +- if err != nil { +- return nil, err - } +- defer cleanup() - -- // Cancel all still-running previous requests, since they would be -- // operating on stale data. -- prevSnapshot.cancel() -- -- // Do not clone a snapshot until its view has finished initializing. -- prevSnapshot.AwaitInitialized(ctx) -- -- // Save one lease of the cloned snapshot in the view. -- v.snapshot, v.releaseSnapshot = prevSnapshot.clone(ctx, changes) -- -- prevReleaseSnapshot() -- v.destroy(prevSnapshot, "View.invalidateContent") +- return s.view.gocmdRunner.Run(ctx, *inv) +-} - -- // Return a second lease to the caller. -- return v.snapshot, v.snapshot.Acquire() +-// RunGoCommandPiped runs the given `go` command, writing its output +-// to stdout and stderr. Verb, Args, and WorkingDir must be specified. +-// +-// RunGoCommandPiped runs the command serially using gocommand.RunPiped, +-// enforcing that this command executes exclusively to other commands on the +-// server. +-func (s *Snapshot) RunGoCommandPiped(ctx context.Context, mode InvocationFlags, inv *gocommand.Invocation, stdout, stderr io.Writer) error { +- _, inv, cleanup, err := s.goCommandInvocation(ctx, mode, inv) +- if err != nil { +- return err +- } +- defer cleanup() +- return s.view.gocmdRunner.RunPiped(ctx, *inv, stdout, stderr) -} - --func getWorkspaceInformation(ctx context.Context, runner *gocommand.Runner, fs source.FileSource, folder *Folder) (*workspaceInformation, error) { -- if err := checkPathCase(folder.Dir.Filename()); err != nil { -- return nil, fmt.Errorf("invalid workspace folder path: %w; check that the casing of the configured workspace folder path agrees with the casing reported by the operating system", err) +-// RunGoModUpdateCommands runs a series of `go` commands that updates the go.mod +-// and go.sum file for wd, and returns their updated contents. +-// +-// TODO(rfindley): the signature of RunGoModUpdateCommands is very confusing. +-// Simplify it. +-func (s *Snapshot) RunGoModUpdateCommands(ctx context.Context, wd string, run func(invoke func(...string) (*bytes.Buffer, error)) error) ([]byte, []byte, error) { +- flags := WriteTemporaryModFile | AllowNetwork +- tmpURI, inv, cleanup, err := s.goCommandInvocation(ctx, flags, &gocommand.Invocation{WorkingDir: wd}) +- if err != nil { +- return nil, nil, err - } -- info := new(workspaceInformation) -- var err error -- inv := gocommand.Invocation{ -- WorkingDir: folder.Dir.Filename(), -- Env: folder.Options.EnvSlice(), +- defer cleanup() +- invoke := func(args ...string) (*bytes.Buffer, error) { +- inv.Verb = args[0] +- inv.Args = args[1:] +- return s.view.gocmdRunner.Run(ctx, *inv) - } -- info.goversion, err = gocommand.GoVersion(ctx, inv, runner) -- if err != nil { -- return info, err +- if err := run(invoke); err != nil { +- return nil, nil, err - } -- info.goversionOutput, err = gocommand.GoVersionOutput(ctx, inv, runner) -- if err != nil { -- return info, err +- if flags.Mode() != WriteTemporaryModFile { +- return nil, nil, nil +- } +- var modBytes, sumBytes []byte +- modBytes, err = os.ReadFile(tmpURI.Path()) +- if err != nil && !os.IsNotExist(err) { +- return nil, nil, err - } -- if err := info.load(ctx, folder.Dir.Filename(), folder.Options.EnvSlice(), runner); err != nil { -- return info, err +- sumBytes, err = os.ReadFile(strings.TrimSuffix(tmpURI.Path(), ".mod") + ".sum") +- if err != nil && !os.IsNotExist(err) { +- return nil, nil, err - } -- // The value of GOPACKAGESDRIVER is not returned through the go command. -- gopackagesdriver := os.Getenv("GOPACKAGESDRIVER") -- // A user may also have a gopackagesdriver binary on their machine, which -- // works the same way as setting GOPACKAGESDRIVER. -- tool, _ := exec.LookPath("gopackagesdriver") -- info.hasGopackagesDriver = gopackagesdriver != "off" && (gopackagesdriver != "" || tool != "") +- return modBytes, sumBytes, nil +-} - -- // filterFunc is the path filter function for this workspace folder. Notably, -- // it is relative to folder (which is specified by the user), not root. -- filterFunc := pathExcludedByFilterFunc(folder.Dir.Filename(), info.gomodcache, folder.Options) -- info.gomod, err = findWorkspaceModFile(ctx, folder.Dir, fs, filterFunc) -- if err != nil { -- return info, err +-// goCommandInvocation populates inv with configuration for running go commands on the snapshot. +-// +-// TODO(rfindley): refactor this function to compose the required configuration +-// explicitly, rather than implicitly deriving it from flags and inv. +-// +-// TODO(adonovan): simplify cleanup mechanism. It's hard to see, but +-// it used only after call to tempModFile. +-func (s *Snapshot) goCommandInvocation(ctx context.Context, flags InvocationFlags, inv *gocommand.Invocation) (tmpURI protocol.DocumentURI, updatedInv *gocommand.Invocation, cleanup func(), err error) { +- allowModfileModificationOption := s.Options().AllowModfileModifications +- allowNetworkOption := s.Options().AllowImplicitNetworkAccess +- +- // TODO(rfindley): it's not clear that this is doing the right thing. +- // Should inv.Env really overwrite view.options? Should s.view.envOverlay +- // overwrite inv.Env? (Do we ever invoke this with a non-empty inv.Env?) +- // +- // We should survey existing uses and write down rules for how env is +- // applied. +- inv.Env = slices.Concat( +- os.Environ(), +- s.Options().EnvSlice(), +- inv.Env, +- []string{"GO111MODULE=" + s.view.adjustedGO111MODULE()}, +- s.view.EnvOverlay(), +- ) +- inv.BuildFlags = append([]string{}, s.Options().BuildFlags...) +- cleanup = func() {} // fallback +- +- // All logic below is for module mode. +- if len(s.view.workspaceModFiles) == 0 { +- return "", inv, cleanup, nil - } - -- // Check if the workspace is within any GOPATH directory. -- for _, gp := range filepath.SplitList(info.gopath) { -- if source.InDir(filepath.Join(gp, "src"), folder.Dir.Filename()) { -- info.inGOPATH = true -- break -- } +- mode, allowNetwork := flags.Mode(), flags.AllowNetwork() +- if !allowNetwork && !allowNetworkOption { +- inv.Env = append(inv.Env, "GOPROXY=off") - } - -- // Compute the "working directory", which is where we run go commands. +- // What follows is rather complicated logic for how to actually run the go +- // command. A word of warning: this is the result of various incremental +- // features added to gopls, and varying behavior of the Go command across Go +- // versions. It can surely be cleaned up significantly, but tread carefully. - // -- // Note: if gowork is in use, this will default to the workspace folder. In -- // the past, we would instead use the folder containing go.work. This should -- // not make a difference, and in fact may improve go list error messages. +- // Roughly speaking we need to resolve four things: +- // - the working directory. +- // - the -mod flag +- // - the -modfile flag - // -- // TODO(golang/go#57514): eliminate the expandWorkspaceToModule setting -- // entirely. -- if folder.Options.ExpandWorkspaceToModule && info.gomod != "" { -- info.goCommandDir = span.URIFromPath(filepath.Dir(info.gomod.Filename())) -- } else { -- info.goCommandDir = folder.Dir -- } -- return info, nil --} +- // These are dependent on a number of factors: whether we need to run in a +- // synthetic workspace, whether flags are supported at the current go +- // version, and what we're actually trying to achieve (the +- // InvocationFlags). +- // +- // TODO(rfindley): should we set -overlays here? - --// findWorkspaceModFile searches for a single go.mod file relative to the given --// folder URI, using the following algorithm: --// 1. if there is a go.mod file in a parent directory, return it --// 2. else, if there is exactly one nested module, return it --// 3. else, return "" --func findWorkspaceModFile(ctx context.Context, folderURI span.URI, fs source.FileSource, excludePath func(string) bool) (span.URI, error) { -- folder := folderURI.Filename() -- match, err := findRootPattern(ctx, folder, "go.mod", fs) -- if err != nil { -- if ctxErr := ctx.Err(); ctxErr != nil { -- return "", ctxErr +- const mutableModFlag = "mod" +- +- // If the mod flag isn't set, populate it based on the mode and workspace. +- // +- // (As noted in various TODOs throughout this function, this is very +- // confusing and not obviously correct, but tests pass and we will eventually +- // rewrite this entire function.) +- if inv.ModFlag == "" { +- switch mode { +- case LoadWorkspace, Normal: +- if allowModfileModificationOption { +- inv.ModFlag = mutableModFlag +- } +- case WriteTemporaryModFile: +- inv.ModFlag = mutableModFlag +- // -mod must be readonly when using go.work files - see issue #48941 +- inv.Env = append(inv.Env, "GOWORK=off") - } -- return "", err -- } -- if match != "" { -- return span.URIFromPath(match), nil - } - -- // ...else we should check if there's exactly one nested module. -- all, err := findModules(folderURI, excludePath, 2) -- if err == errExhausted { -- // Fall-back behavior: if we don't find any modules after searching 10000 -- // files, assume there are none. -- event.Log(ctx, fmt.Sprintf("stopped searching for modules after %d files", fileLimit)) -- return "", nil -- } -- if err != nil { -- return "", err -- } -- if len(all) == 1 { -- // range to access first element. -- for uri := range all { -- return uri, nil +- // TODO(rfindley): if inv.ModFlag was already set to "mod", we may not have +- // set GOWORK=off here. But that doesn't happen. Clean up this entire API so +- // that we don't have this mutation of the invocation, which is quite hard to +- // follow. +- +- // If the invocation needs to mutate the modfile, we must use a temp mod. +- if inv.ModFlag == mutableModFlag { +- var modURI protocol.DocumentURI +- // Select the module context to use. +- // If we're type checking, we need to use the workspace context, meaning +- // the main (workspace) module. Otherwise, we should use the module for +- // the passed-in working dir. +- if mode == LoadWorkspace { +- // TODO(rfindley): this seems unnecessary and overly complicated. Remove +- // this along with 'allowModFileModifications'. +- if s.view.typ == GoModView { +- modURI = s.view.gomod +- } +- } else { +- modURI = s.GoModForFile(protocol.URIFromPath(inv.WorkingDir)) - } -- } -- return "", nil --} - --// findRootPattern looks for files with the given basename in dir or any parent --// directory of dir, using the provided FileSource. It returns the first match, --// starting from dir and search parents. --// --// The resulting string is either the file path of a matching file with the --// given basename, or "" if none was found. --func findRootPattern(ctx context.Context, dir, basename string, fs source.FileSource) (string, error) { -- for dir != "" { -- target := filepath.Join(dir, basename) -- fh, err := fs.ReadFile(ctx, span.URIFromPath(target)) -- if err != nil { -- return "", err // context cancelled +- var modContent []byte +- if modURI != "" { +- modFH, err := s.ReadFile(ctx, modURI) +- if err != nil { +- return "", nil, cleanup, err +- } +- modContent, err = modFH.Content() +- if err != nil { +- return "", nil, cleanup, err +- } - } -- if fileExists(fh) { -- return target, nil +- if modURI == "" { +- return "", nil, cleanup, fmt.Errorf("no go.mod file found in %s", inv.WorkingDir) - } -- // Trailing separators must be trimmed, otherwise filepath.Split is a noop. -- next, _ := filepath.Split(strings.TrimRight(dir, string(filepath.Separator))) -- if next == dir { -- break +- // Use the go.sum if it happens to be available. +- gosum := s.goSum(ctx, modURI) +- tmpURI, cleanup, err = tempModFile(modURI, modContent, gosum) +- if err != nil { +- return "", nil, cleanup, err - } -- dir = next +- inv.ModFile = tmpURI.Path() - } -- return "", nil --} -- --// OS-specific path case check, for case-insensitive filesystems. --var checkPathCase = defaultCheckPathCase - --func defaultCheckPathCase(path string) error { -- return nil +- return tmpURI, inv, cleanup, nil -} - --func (v *View) IsGoPrivatePath(target string) bool { -- return globsMatchPath(v.goprivate, target) +-func (s *Snapshot) buildOverlay() map[string][]byte { +- overlays := make(map[string][]byte) +- for _, overlay := range s.Overlays() { +- if overlay.saved { +- continue +- } +- // TODO(rfindley): previously, there was a todo here to make sure we don't +- // send overlays outside of the current view. IMO we should instead make +- // sure this doesn't matter. +- overlays[overlay.URI().Path()] = overlay.content +- } +- return overlays -} - --func (v *View) ModuleUpgrades(modfile span.URI) map[string]string { -- v.moduleUpgradesMu.Lock() -- defer v.moduleUpgradesMu.Unlock() +-// Overlays returns the set of overlays at this snapshot. +-// +-// Note that this may differ from the set of overlays on the server, if the +-// snapshot observed a historical state. +-func (s *Snapshot) Overlays() []*overlay { +- s.mu.Lock() +- defer s.mu.Unlock() - -- upgrades := map[string]string{} -- for mod, ver := range v.moduleUpgrades[modfile] { -- upgrades[mod] = ver -- } -- return upgrades +- return s.files.getOverlays() -} - --func (v *View) RegisterModuleUpgrades(modfile span.URI, upgrades map[string]string) { -- // Return early if there are no upgrades. -- if len(upgrades) == 0 { -- return -- } +-// Package data kinds, identifying various package data that may be stored in +-// the file cache. +-const ( +- xrefsKind = "xrefs" +- methodSetsKind = "methodsets" +- exportDataKind = "export" +- diagnosticsKind = "diagnostics" +- typerefsKind = "typerefs" +-) - -- v.moduleUpgradesMu.Lock() -- defer v.moduleUpgradesMu.Unlock() +-// PackageDiagnostics returns diagnostics for files contained in specified +-// packages. +-// +-// If these diagnostics cannot be loaded from cache, the requested packages +-// may be type-checked. +-func (s *Snapshot) PackageDiagnostics(ctx context.Context, ids ...PackageID) (map[protocol.DocumentURI][]*Diagnostic, error) { +- ctx, done := event.Start(ctx, "cache.snapshot.PackageDiagnostics") +- defer done() - -- m := v.moduleUpgrades[modfile] -- if m == nil { -- m = make(map[string]string) -- v.moduleUpgrades[modfile] = m +- var mu sync.Mutex +- perFile := make(map[protocol.DocumentURI][]*Diagnostic) +- collect := func(diags []*Diagnostic) { +- mu.Lock() +- defer mu.Unlock() +- for _, diag := range diags { +- perFile[diag.URI] = append(perFile[diag.URI], diag) +- } +- } +- pre := func(_ int, ph *packageHandle) bool { +- data, err := filecache.Get(diagnosticsKind, ph.key) +- if err == nil { // hit +- collect(ph.loadDiagnostics) +- collect(decodeDiagnostics(data)) +- return false +- } else if err != filecache.ErrNotFound { +- event.Error(ctx, "reading diagnostics from filecache", err) +- } +- return true - } -- for mod, ver := range upgrades { -- m[mod] = ver +- post := func(_ int, pkg *Package) { +- collect(pkg.loadDiagnostics) +- collect(pkg.pkg.diagnostics) - } +- return perFile, s.forEachPackage(ctx, ids, pre, post) -} - --func (v *View) ClearModuleUpgrades(modfile span.URI) { -- v.moduleUpgradesMu.Lock() -- defer v.moduleUpgradesMu.Unlock() -- -- delete(v.moduleUpgrades, modfile) --} -- --const maxGovulncheckResultAge = 1 * time.Hour // Invalidate results older than this limit. --var timeNow = time.Now // for testing -- --func (v *View) Vulnerabilities(modfiles ...span.URI) map[span.URI]*vulncheck.Result { -- m := make(map[span.URI]*vulncheck.Result) -- now := timeNow() -- v.vulnsMu.Lock() -- defer v.vulnsMu.Unlock() +-// References returns cross-reference indexes for the specified packages. +-// +-// If these indexes cannot be loaded from cache, the requested packages may +-// be type-checked. +-func (s *Snapshot) References(ctx context.Context, ids ...PackageID) ([]xrefIndex, error) { +- ctx, done := event.Start(ctx, "cache.snapshot.References") +- defer done() - -- if len(modfiles) == 0 { // empty means all modfiles -- for modfile := range v.vulns { -- modfiles = append(modfiles, modfile) +- indexes := make([]xrefIndex, len(ids)) +- pre := func(i int, ph *packageHandle) bool { +- data, err := filecache.Get(xrefsKind, ph.key) +- if err == nil { // hit +- indexes[i] = xrefIndex{mp: ph.mp, data: data} +- return false +- } else if err != filecache.ErrNotFound { +- event.Error(ctx, "reading xrefs from filecache", err) - } +- return true - } -- for _, modfile := range modfiles { -- vuln := v.vulns[modfile] -- if vuln != nil && now.Sub(vuln.AsOf) > maxGovulncheckResultAge { -- v.vulns[modfile] = nil // same as SetVulnerabilities(modfile, nil) -- vuln = nil -- } -- m[modfile] = vuln +- post := func(i int, pkg *Package) { +- indexes[i] = xrefIndex{mp: pkg.metadata, data: pkg.pkg.xrefs()} - } -- return m +- return indexes, s.forEachPackage(ctx, ids, pre, post) -} - --func (v *View) SetVulnerabilities(modfile span.URI, vulns *vulncheck.Result) { -- v.vulnsMu.Lock() -- defer v.vulnsMu.Unlock() -- -- v.vulns[modfile] = vulns +-// An xrefIndex is a helper for looking up references in a given package. +-type xrefIndex struct { +- mp *metadata.Package +- data []byte -} - --func (v *View) GoVersion() int { -- return v.workspaceInformation.goversion +-func (index xrefIndex) Lookup(targets map[PackagePath]map[objectpath.Path]struct{}) []protocol.Location { +- return xrefs.Lookup(index.mp, index.data, targets) -} - --func (v *View) GoVersionString() string { -- return gocommand.ParseGoVersionOutput(v.workspaceInformation.goversionOutput) --} +-// MethodSets returns method-set indexes for the specified packages. +-// +-// If these indexes cannot be loaded from cache, the requested packages may +-// be type-checked. +-func (s *Snapshot) MethodSets(ctx context.Context, ids ...PackageID) ([]*methodsets.Index, error) { +- ctx, done := event.Start(ctx, "cache.snapshot.MethodSets") +- defer done() - --// Copied from --// https://cs.opensource.google/go/go/+/master:src/cmd/go/internal/str/path.go;l=58;drc=2910c5b4a01a573ebc97744890a07c1a3122c67a --func globsMatchPath(globs, target string) bool { -- for globs != "" { -- // Extract next non-empty glob in comma-separated list. -- var glob string -- if i := strings.Index(globs, ","); i >= 0 { -- glob, globs = globs[:i], globs[i+1:] -- } else { -- glob, globs = globs, "" -- } -- if glob == "" { -- continue +- indexes := make([]*methodsets.Index, len(ids)) +- pre := func(i int, ph *packageHandle) bool { +- data, err := filecache.Get(methodSetsKind, ph.key) +- if err == nil { // hit +- indexes[i] = methodsets.Decode(data) +- return false +- } else if err != filecache.ErrNotFound { +- event.Error(ctx, "reading methodsets from filecache", err) - } +- return true +- } +- post := func(i int, pkg *Package) { +- indexes[i] = pkg.pkg.methodsets() +- } +- return indexes, s.forEachPackage(ctx, ids, pre, post) +-} - -- // A glob with N+1 path elements (N slashes) needs to be matched -- // against the first N+1 path elements of target, -- // which end just before the N+1'th slash. -- n := strings.Count(glob, "/") -- prefix := target -- // Walk target, counting slashes, truncating at the N+1'th slash. -- for i := 0; i < len(target); i++ { -- if target[i] == '/' { -- if n == 0 { -- prefix = target[:i] -- break -- } -- n-- -- } -- } -- if n > 0 { -- // Not enough prefix elements. -- continue -- } -- matched, _ := path.Match(glob, prefix) -- if matched { -- return true +-// MetadataForFile returns a new slice containing metadata for each +-// package containing the Go file identified by uri, ordered by the +-// number of CompiledGoFiles (i.e. "narrowest" to "widest" package), +-// and secondarily by IsIntermediateTestVariant (false < true). +-// The result may include tests and intermediate test variants of +-// importable packages. +-// It returns an error if the context was cancelled. +-func (s *Snapshot) MetadataForFile(ctx context.Context, uri protocol.DocumentURI) ([]*metadata.Package, error) { +- if s.view.typ == AdHocView { +- // As described in golang/go#57209, in ad-hoc workspaces (where we load ./ +- // rather than ./...), preempting the directory load with file loads can +- // lead to an inconsistent outcome, where certain files are loaded with +- // command-line-arguments packages and others are loaded only in the ad-hoc +- // package. Therefore, ensure that the workspace is loaded before doing any +- // file loads. +- if err := s.awaitLoaded(ctx); err != nil { +- return nil, err - } - } -- return false --} - --var modFlagRegexp = regexp.MustCompile(`-mod[ =](\w+)`) +- s.mu.Lock() - --// TODO(rstambler): Consolidate modURI and modContent back into a FileHandle --// after we have a version of the workspace go.mod file on disk. Getting a --// FileHandle from the cache for temporary files is problematic, since we --// cannot delete it. --func (s *snapshot) vendorEnabled(ctx context.Context, modURI span.URI, modContent []byte) (bool, error) { -- // Legacy GOPATH workspace? -- if s.workspaceMode()&moduleMode == 0 { -- return false, nil -- } +- // Start with the set of package associations derived from the last load. +- ids := s.meta.IDs[uri] - -- // Explicit -mod flag? -- matches := modFlagRegexp.FindStringSubmatch(s.view.goflags) -- if len(matches) != 0 { -- modFlag := matches[1] -- if modFlag != "" { -- // Don't override an explicit '-mod=vendor' argument. -- // We do want to override '-mod=readonly': it would break various module code lenses, -- // and on 1.16 we know -modfile is available, so we won't mess with go.mod anyway. -- return modFlag == "vendor", nil +- shouldLoad := false // whether any packages containing uri are marked 'shouldLoad' +- for _, id := range ids { +- if pkgs, _ := s.shouldLoad.Get(id); len(pkgs) > 0 { +- shouldLoad = true - } - } - -- modFile, err := modfile.Parse(modURI.Filename(), modContent, nil) -- if err != nil { -- return false, err -- } +- // Check if uri is known to be unloadable. +- unloadable := s.unloadableFiles.Contains(uri) - -- // No vendor directory? -- // TODO(golang/go#57514): this is wrong if the working dir is not the module -- // root. -- if fi, err := os.Stat(filepath.Join(s.view.goCommandDir.Filename(), "vendor")); err != nil || !fi.IsDir() { -- return false, nil -- } +- s.mu.Unlock() - -- // Vendoring enabled by default by go declaration in go.mod? -- vendorEnabled := modFile.Go != nil && modFile.Go.Version != "" && semver.Compare("v"+modFile.Go.Version, "v1.14") >= 0 -- return vendorEnabled, nil --} +- // Reload if loading is likely to improve the package associations for uri: +- // - uri is not contained in any valid packages +- // - ...or one of the packages containing uri is marked 'shouldLoad' +- // - ...but uri is not unloadable +- if (shouldLoad || len(ids) == 0) && !unloadable { +- scope := fileLoadScope(uri) +- err := s.load(ctx, false, scope) - --// TODO(rfindley): clean up the redundancy of allFilesExcluded, --// pathExcludedByFilterFunc, pathExcludedByFilter, view.filterFunc... --func allFilesExcluded(files []string, filterFunc func(span.URI) bool) bool { -- for _, f := range files { -- uri := span.URIFromPath(f) -- if !filterFunc(uri) { -- return false +- // +- // Return the context error here as the current operation is no longer +- // valid. +- if err != nil { +- // Guard against failed loads due to context cancellation. We don't want +- // to mark loads as completed if they failed due to context cancellation. +- if ctx.Err() != nil { +- return nil, ctx.Err() +- } +- +- // Don't return an error here, as we may still return stale IDs. +- // Furthermore, the result of MetadataForFile should be consistent upon +- // subsequent calls, even if the file is marked as unloadable. +- if !errors.Is(err, errNoPackages) { +- event.Error(ctx, "MetadataForFile", err) +- } - } +- +- // We must clear scopes after loading. +- // +- // TODO(rfindley): unlike reloadWorkspace, this is simply marking loaded +- // packages as loaded. We could do this from snapshot.load and avoid +- // raciness. +- s.clearShouldLoad(scope) - } -- return true --} - --func pathExcludedByFilterFunc(folder, gomodcache string, opts *source.Options) func(string) bool { -- filterer := buildFilterer(folder, gomodcache, opts) -- return func(path string) bool { -- return pathExcludedByFilter(path, filterer) +- // Retrieve the metadata. +- s.mu.Lock() +- defer s.mu.Unlock() +- ids = s.meta.IDs[uri] +- metas := make([]*metadata.Package, len(ids)) +- for i, id := range ids { +- metas[i] = s.meta.Packages[id] +- if metas[i] == nil { +- panic("nil metadata") +- } +- } +- // Metadata is only ever added by loading, +- // so if we get here and still have +- // no IDs, uri is unloadable. +- if !unloadable && len(ids) == 0 { +- s.unloadableFiles.Add(uri) - } --} - --// pathExcludedByFilter reports whether the path (relative to the workspace --// folder) should be excluded by the configured directory filters. --// --// TODO(rfindley): passing root and gomodcache here makes it confusing whether --// path should be absolute or relative, and has already caused at least one --// bug. --func pathExcludedByFilter(path string, filterer *source.Filterer) bool { -- path = strings.TrimPrefix(filepath.ToSlash(path), "/") -- return filterer.Disallow(path) +- // Sort packages "narrowest" to "widest" (in practice: +- // non-tests before tests), and regular packages before +- // their intermediate test variants (which have the same +- // files but different imports). +- sort.Slice(metas, func(i, j int) bool { +- x, y := metas[i], metas[j] +- xfiles, yfiles := len(x.CompiledGoFiles), len(y.CompiledGoFiles) +- if xfiles != yfiles { +- return xfiles < yfiles +- } +- return boolLess(x.IsIntermediateTestVariant(), y.IsIntermediateTestVariant()) +- }) +- +- return metas, nil -} - --func buildFilterer(folder, gomodcache string, opts *source.Options) *source.Filterer { -- filters := opts.DirectoryFilters +-func boolLess(x, y bool) bool { return !x && y } // false < true - -- if pref := strings.TrimPrefix(gomodcache, folder); pref != gomodcache { -- modcacheFilter := "-" + strings.TrimPrefix(filepath.ToSlash(pref), "/") -- filters = append(filters, modcacheFilter) +-// ReverseDependencies returns a new mapping whose entries are +-// the ID and Metadata of each package in the workspace that +-// directly or transitively depend on the package denoted by id, +-// excluding id itself. +-func (s *Snapshot) ReverseDependencies(ctx context.Context, id PackageID, transitive bool) (map[PackageID]*metadata.Package, error) { +- if err := s.awaitLoaded(ctx); err != nil { +- return nil, err - } -- return source.NewFilterer(filters) --} -diff -urN a/gopls/internal/lsp/cache/view_test.go b/gopls/internal/lsp/cache/view_test.go ---- a/gopls/internal/lsp/cache/view_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/view_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,303 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. --package cache -- --import ( -- "context" -- "encoding/json" -- "os" -- "path/filepath" -- "testing" -- "time" -- -- "github.com/google/go-cmp/cmp" -- "golang.org/x/tools/gopls/internal/lsp/fake" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/gopls/internal/vulncheck" --) - --func TestCaseInsensitiveFilesystem(t *testing.T) { -- base := t.TempDir() +- meta := s.MetadataGraph() +- var rdeps map[PackageID]*metadata.Package +- if transitive { +- rdeps = meta.ReverseReflexiveTransitiveClosure(id) - -- inner := filepath.Join(base, "a/B/c/DEFgh") -- if err := os.MkdirAll(inner, 0777); err != nil { -- t.Fatal(err) -- } -- file := filepath.Join(inner, "f.go") -- if err := os.WriteFile(file, []byte("hi"), 0777); err != nil { -- t.Fatal(err) -- } -- if _, err := os.Stat(filepath.Join(inner, "F.go")); err != nil { -- t.Skip("filesystem is case-sensitive") -- } +- // Remove the original package ID from the map. +- // (Callers all want irreflexivity but it's easier +- // to compute reflexively then subtract.) +- delete(rdeps, id) - -- tests := []struct { -- path string -- err bool -- }{ -- {file, false}, -- {filepath.Join(inner, "F.go"), true}, -- {filepath.Join(base, "a/b/c/defgh/f.go"), true}, -- } -- for _, tt := range tests { -- err := checkPathCase(tt.path) -- if err != nil != tt.err { -- t.Errorf("checkPathCase(%q) = %v, wanted error: %v", tt.path, err, tt.err) +- } else { +- // direct reverse dependencies +- rdeps = make(map[PackageID]*metadata.Package) +- for _, rdepID := range meta.ImportedBy[id] { +- if rdep := meta.Packages[rdepID]; rdep != nil { +- rdeps[rdepID] = rdep +- } - } - } +- +- return rdeps, nil -} - --func TestFindWorkspaceModFile(t *testing.T) { -- workspace := ` ---- a/go.mod -- --module a ---- a/x/x.go --package x ---- a/x/y/y.go --package x ---- b/go.mod -- --module b ---- b/c/go.mod -- --module bc ---- d/gopls.mod -- --module d-goplsworkspace ---- d/e/go.mod -- --module de ---- f/g/go.mod -- --module fg --` -- dir, err := fake.Tempdir(fake.UnpackTxt(workspace)) -- if err != nil { -- t.Fatal(err) -- } -- defer os.RemoveAll(dir) +-// -- Active package tracking -- +-// +-// We say a package is "active" if any of its files are open. +-// This is an optimization: the "active" concept is an +-// implementation detail of the cache and is not exposed +-// in the source or Snapshot API. +-// After type-checking we keep active packages in memory. +-// The activePackages persistent map does bookkeeping for +-// the set of active packages. - -- tests := []struct { -- folder, want string -- }{ -- {"", ""}, // no module at root, and more than one nested module -- {"a", "a/go.mod"}, -- {"a/x", "a/go.mod"}, -- {"a/x/y", "a/go.mod"}, -- {"b/c", "b/c/go.mod"}, -- {"d", "d/e/go.mod"}, -- {"d/e", "d/e/go.mod"}, -- {"f", "f/g/go.mod"}, -- } +-// getActivePackage returns a the memoized active package for id, if it exists. +-// If id is not active or has not yet been type-checked, it returns nil. +-func (s *Snapshot) getActivePackage(id PackageID) *Package { +- s.mu.Lock() +- defer s.mu.Unlock() - -- for _, test := range tests { -- ctx := context.Background() -- rel := fake.RelativeTo(dir) -- folderURI := span.URIFromPath(rel.AbsPath(test.folder)) -- excludeNothing := func(string) bool { return false } -- got, err := findWorkspaceModFile(ctx, folderURI, New(nil), excludeNothing) -- if err != nil { -- t.Fatal(err) -- } -- want := span.URI("") -- if test.want != "" { -- want = span.URIFromPath(rel.AbsPath(test.want)) -- } -- if got != want { -- t.Errorf("findWorkspaceModFile(%q) = %q, want %q", test.folder, got, want) -- } +- if value, ok := s.activePackages.Get(id); ok { +- return value - } +- return nil -} - --func TestInVendor(t *testing.T) { -- for _, tt := range []struct { -- path string -- inVendor bool -- }{ -- {"foo/vendor/x.go", false}, -- {"foo/vendor/x/x.go", true}, -- {"foo/x.go", false}, -- {"foo/vendor/foo.txt", false}, -- {"foo/vendor/modules.txt", false}, -- } { -- if got := inVendor(span.URIFromPath(tt.path)); got != tt.inVendor { -- t.Errorf("expected %s inVendor %v, got %v", tt.path, tt.inVendor, got) -- } -- } --} +-// setActivePackage checks if pkg is active, and if so either records it in +-// the active packages map or returns the existing memoized active package for id. +-func (s *Snapshot) setActivePackage(id PackageID, pkg *Package) { +- s.mu.Lock() +- defer s.mu.Unlock() - --func TestFilters(t *testing.T) { -- tests := []struct { -- filters []string -- included []string -- excluded []string -- }{ -- { -- included: []string{"x"}, -- }, -- { -- filters: []string{"-"}, -- excluded: []string{"x", "x/a"}, -- }, -- { -- filters: []string{"-x", "+y"}, -- included: []string{"y", "y/a", "z"}, -- excluded: []string{"x", "x/a"}, -- }, -- { -- filters: []string{"-x", "+x/y", "-x/y/z"}, -- included: []string{"x/y", "x/y/a", "a"}, -- excluded: []string{"x", "x/a", "x/y/z/a"}, -- }, -- { -- filters: []string{"+foobar", "-foo"}, -- included: []string{"foobar", "foobar/a"}, -- excluded: []string{"foo", "foo/a"}, -- }, +- if _, ok := s.activePackages.Get(id); ok { +- return // already memoized - } - -- for _, tt := range tests { -- filterer := source.NewFilterer(tt.filters) -- for _, inc := range tt.included { -- if pathExcludedByFilter(inc, filterer) { -- t.Errorf("filters %q excluded %v, wanted included", tt.filters, inc) -- } -- } -- for _, exc := range tt.excluded { -- if !pathExcludedByFilter(exc, filterer) { -- t.Errorf("filters %q included %v, wanted excluded", tt.filters, exc) -- } -- } +- if containsOpenFileLocked(s, pkg.Metadata()) { +- s.activePackages.Set(id, pkg, nil) +- } else { +- s.activePackages.Set(id, (*Package)(nil), nil) // remember that pkg is not open - } -} - --func TestSuffixes(t *testing.T) { -- type file struct { -- path string -- want bool -- } -- type cases struct { -- option []string -- files []file -- } -- tests := []cases{ -- {[]string{"tmpl", "gotmpl"}, []file{ // default -- {"foo", false}, -- {"foo.tmpl", true}, -- {"foo.gotmpl", true}, -- {"tmpl", false}, -- {"tmpl.go", false}}, -- }, -- {[]string{"tmpl", "gotmpl", "html", "gohtml"}, []file{ -- {"foo.gotmpl", true}, -- {"foo.html", true}, -- {"foo.gohtml", true}, -- {"html", false}}, -- }, -- {[]string{"tmpl", "gotmpl", ""}, []file{ // possible user mistake -- {"foo.gotmpl", true}, -- {"foo.go", false}, -- {"foo", false}}, -- }, -- } -- for _, a := range tests { -- suffixes := a.option -- for _, b := range a.files { -- got := fileHasExtension(b.path, suffixes) -- if got != b.want { -- t.Errorf("got %v, want %v, option %q, file %q (%+v)", -- got, b.want, a.option, b.path, b) -- } +-func (s *Snapshot) resetActivePackagesLocked() { +- s.activePackages.Destroy() +- s.activePackages = new(persistent.Map[PackageID, *Package]) +-} +- +-// See Session.FileWatchingGlobPatterns for a description of gopls' file +-// watching heuristic. +-func (s *Snapshot) fileWatchingGlobPatterns() map[protocol.RelativePattern]unit { +- // Always watch files that may change the view definition. +- patterns := make(map[protocol.RelativePattern]unit) +- +- // If GOWORK is outside the folder, ensure we are watching it. +- if s.view.gowork != "" && !s.view.folder.Dir.Encloses(s.view.gowork) { +- workPattern := protocol.RelativePattern{ +- BaseURI: s.view.gowork.Dir(), +- Pattern: path.Base(string(s.view.gowork)), - } +- patterns[workPattern] = unit{} - } --} - --func TestView_Vulnerabilities(t *testing.T) { -- // TODO(hyangah): use t.Cleanup when we get rid of go1.13 legacy CI. -- defer func() { timeNow = time.Now }() +- extensions := "go,mod,sum,work" +- for _, ext := range s.Options().TemplateExtensions { +- extensions += "," + ext +- } +- watchGoFiles := fmt.Sprintf("**/*.{%s}", extensions) - -- now := time.Now() +- var dirs []string +- if s.view.moduleMode() { +- if s.view.typ == GoWorkView { +- workVendorDir := filepath.Join(s.view.gowork.Dir().Path(), "vendor") +- workVendorURI := protocol.URIFromPath(workVendorDir) +- patterns[protocol.RelativePattern{BaseURI: workVendorURI, Pattern: watchGoFiles}] = unit{} +- } - -- view := &View{ -- vulns: make(map[span.URI]*vulncheck.Result), +- // In module mode, watch directories containing active modules, and collect +- // these dirs for later filtering the set of known directories. +- // +- // The assumption is that the user is not actively editing non-workspace +- // modules, so don't pay the price of file watching. +- for modFile := range s.view.workspaceModFiles { +- dir := filepath.Dir(modFile.Path()) +- dirs = append(dirs, dir) +- +- // TODO(golang/go#64724): thoroughly test these patterns, particularly on +- // on Windows. +- // +- // Note that glob patterns should use '/' on Windows: +- // https://code.visualstudio.com/docs/editor/glob-patterns +- patterns[protocol.RelativePattern{BaseURI: modFile.Dir(), Pattern: watchGoFiles}] = unit{} +- } +- } else { +- // In non-module modes (GOPATH or AdHoc), we just watch the workspace root. +- dirs = []string{s.view.root.Path()} +- patterns[protocol.RelativePattern{Pattern: watchGoFiles}] = unit{} - } -- file1, file2 := span.URIFromPath("f1/go.mod"), span.URIFromPath("f2/go.mod") - -- vuln1 := &vulncheck.Result{AsOf: now.Add(-(maxGovulncheckResultAge * 3) / 4)} // already ~3/4*maxGovulncheckResultAge old -- view.SetVulnerabilities(file1, vuln1) +- if s.watchSubdirs() { +- // Some clients (e.g. VS Code) do not send notifications for changes to +- // directories that contain Go code (golang/go#42348). To handle this, +- // explicitly watch all of the directories in the workspace. We find them +- // by adding the directories of every file in the snapshot's workspace +- // directories. There may be thousands of patterns, each a single +- // directory. +- // +- // We compute this set by looking at files that we've previously observed. +- // This may miss changed to directories that we haven't observed, but that +- // shouldn't matter as there is nothing to invalidate (if a directory falls +- // in forest, etc). +- // +- // (A previous iteration created a single glob pattern holding a union of +- // all the directories, but this was found to cause VS Code to get stuck +- // for several minutes after a buffer was saved twice in a workspace that +- // had >8000 watched directories.) +- // +- // Some clients (notably coc.nvim, which uses watchman for globs) perform +- // poorly with a large list of individual directories. +- s.addKnownSubdirs(patterns, dirs) +- } - -- vuln2 := &vulncheck.Result{AsOf: now} // fresh. -- view.SetVulnerabilities(file2, vuln2) +- return patterns +-} - -- t.Run("fresh", func(t *testing.T) { -- got := view.Vulnerabilities() -- want := map[span.URI]*vulncheck.Result{ -- file1: vuln1, -- file2: vuln2, -- } +-func (s *Snapshot) addKnownSubdirs(patterns map[protocol.RelativePattern]unit, wsDirs []string) { +- s.mu.Lock() +- defer s.mu.Unlock() - -- if diff := cmp.Diff(toJSON(want), toJSON(got)); diff != "" { -- t.Errorf("view.Vulnerabilities() mismatch (-want +got):\n%s", diff) +- s.files.getDirs().Range(func(dir string) { +- for _, wsDir := range wsDirs { +- if pathutil.InDir(wsDir, dir) { +- patterns[protocol.RelativePattern{Pattern: filepath.ToSlash(dir)}] = unit{} +- } - } - }) +-} - -- // maxGovulncheckResultAge/2 later -- timeNow = func() time.Time { return now.Add(maxGovulncheckResultAge / 2) } -- t.Run("after30min", func(t *testing.T) { -- got := view.Vulnerabilities() -- want := map[span.URI]*vulncheck.Result{ -- file1: nil, // expired. -- file2: vuln2, +-// watchSubdirs reports whether gopls should request separate file watchers for +-// each relevant subdirectory. This is necessary only for clients (namely VS +-// Code) that do not send notifications for individual files in a directory +-// when the entire directory is deleted. +-func (s *Snapshot) watchSubdirs() bool { +- switch p := s.Options().SubdirWatchPatterns; p { +- case settings.SubdirWatchPatternsOn: +- return true +- case settings.SubdirWatchPatternsOff: +- return false +- case settings.SubdirWatchPatternsAuto: +- // See the documentation of InternalOptions.SubdirWatchPatterns for an +- // explanation of why VS Code gets a different default value here. +- // +- // Unfortunately, there is no authoritative list of client names, nor any +- // requirements that client names do not change. We should update the VS +- // Code extension to set a default value of "subdirWatchPatterns" to "on", +- // so that this workaround is only temporary. +- if s.Options().ClientInfo != nil && s.Options().ClientInfo.Name == "Visual Studio Code" { +- return true - } +- return false +- default: +- bug.Reportf("invalid subdirWatchPatterns: %q", p) +- return false +- } +-} +- +-// filesInDir returns all files observed by the snapshot that are contained in +-// a directory with the provided URI. +-func (s *Snapshot) filesInDir(uri protocol.DocumentURI) []protocol.DocumentURI { +- s.mu.Lock() +- defer s.mu.Unlock() - -- if diff := cmp.Diff(toJSON(want), toJSON(got)); diff != "" { -- t.Errorf("view.Vulnerabilities() mismatch (-want +got):\n%s", diff) +- dir := uri.Path() +- if !s.files.getDirs().Contains(dir) { +- return nil +- } +- var files []protocol.DocumentURI +- s.files.foreach(func(uri protocol.DocumentURI, _ file.Handle) { +- if pathutil.InDir(dir, uri.Path()) { +- files = append(files, uri) - } - }) +- return files +-} - -- // maxGovulncheckResultAge later -- timeNow = func() time.Time { return now.Add(maxGovulncheckResultAge + time.Minute) } +-// WorkspaceMetadata returns a new, unordered slice containing +-// metadata for all ordinary and test packages (but not +-// intermediate test variants) in the workspace. +-// +-// The workspace is the set of modules typically defined by a +-// go.work file. It is not transitively closed: for example, +-// the standard library is not usually part of the workspace +-// even though every module in the workspace depends on it. +-// +-// Operations that must inspect all the dependencies of the +-// workspace packages should instead use AllMetadata. +-func (s *Snapshot) WorkspaceMetadata(ctx context.Context) ([]*metadata.Package, error) { +- if err := s.awaitLoaded(ctx); err != nil { +- return nil, err +- } - -- t.Run("after1hr", func(t *testing.T) { -- got := view.Vulnerabilities() -- want := map[span.URI]*vulncheck.Result{ -- file1: nil, -- file2: nil, -- } +- s.mu.Lock() +- defer s.mu.Unlock() - -- if diff := cmp.Diff(toJSON(want), toJSON(got)); diff != "" { -- t.Errorf("view.Vulnerabilities() mismatch (-want +got):\n%s", diff) -- } +- meta := make([]*metadata.Package, 0, s.workspacePackages.Len()) +- s.workspacePackages.Range(func(id PackageID, _ PackagePath) { +- meta = append(meta, s.meta.Packages[id]) - }) +- return meta, nil -} - --func toJSON(x interface{}) string { -- b, _ := json.MarshalIndent(x, "", " ") -- return string(b) +-// isWorkspacePackage reports whether the given package ID refers to a +-// workspace package for the snapshot. +-func (s *Snapshot) isWorkspacePackage(id PackageID) bool { +- s.mu.Lock() +- defer s.mu.Unlock() +- _, ok := s.workspacePackages.Value(id) +- return ok -} - --func TestIgnoreFilter(t *testing.T) { -- tests := []struct { -- dirs []string -- path string -- want bool -- }{ -- {[]string{"a"}, "a/testdata/foo", true}, -- {[]string{"a"}, "a/_ignore/foo", true}, -- {[]string{"a"}, "a/.ignore/foo", true}, -- {[]string{"a"}, "b/testdata/foo", false}, -- {[]string{"a"}, "testdata/foo", false}, -- {[]string{"a", "b"}, "b/testdata/foo", true}, -- {[]string{"a"}, "atestdata/foo", false}, +-// Symbols extracts and returns symbol information for every file contained in +-// a loaded package. It awaits snapshot loading. +-// +-// If workspaceOnly is set, this only includes symbols from files in a +-// workspace package. Otherwise, it returns symbols from all loaded packages. +-// +-// TODO(rfindley): move to symbols.go. +-func (s *Snapshot) Symbols(ctx context.Context, workspaceOnly bool) (map[protocol.DocumentURI][]Symbol, error) { +- var ( +- meta []*metadata.Package +- err error +- ) +- if workspaceOnly { +- meta, err = s.WorkspaceMetadata(ctx) +- } else { +- meta, err = s.AllMetadata(ctx) +- } +- if err != nil { +- return nil, fmt.Errorf("loading metadata: %v", err) - } - -- for _, test := range tests { -- // convert to filepaths, for convenience -- for i, dir := range test.dirs { -- test.dirs[i] = filepath.FromSlash(dir) +- goFiles := make(map[protocol.DocumentURI]struct{}) +- for _, mp := range meta { +- for _, uri := range mp.GoFiles { +- goFiles[uri] = struct{}{} - } -- test.path = filepath.FromSlash(test.path) -- -- f := newIgnoreFilter(test.dirs) -- if got := f.ignored(test.path); got != test.want { -- t.Errorf("newIgnoreFilter(%q).ignore(%q) = %t, want %t", test.dirs, test.path, got, test.want) +- for _, uri := range mp.CompiledGoFiles { +- goFiles[uri] = struct{}{} - } - } +- +- // Symbolize them in parallel. +- var ( +- group errgroup.Group +- nprocs = 2 * runtime.GOMAXPROCS(-1) // symbolize is a mix of I/O and CPU +- resultMu sync.Mutex +- result = make(map[protocol.DocumentURI][]Symbol) +- ) +- group.SetLimit(nprocs) +- for uri := range goFiles { +- uri := uri +- group.Go(func() error { +- symbols, err := s.symbolize(ctx, uri) +- if err != nil { +- return err +- } +- resultMu.Lock() +- result[uri] = symbols +- resultMu.Unlock() +- return nil +- }) +- } +- // Keep going on errors, but log the first failure. +- // Partial results are better than no symbol results. +- if err := group.Wait(); err != nil { +- event.Error(ctx, "getting snapshot symbols", err) +- } +- return result, nil -} -diff -urN a/gopls/internal/lsp/cache/workspace.go b/gopls/internal/lsp/cache/workspace.go ---- a/gopls/internal/lsp/cache/workspace.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cache/workspace.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,133 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package cache +-// AllMetadata returns a new unordered array of metadata for +-// all packages known to this snapshot, which includes the +-// packages of all workspace modules plus their transitive +-// import dependencies. +-// +-// It may also contain ad-hoc packages for standalone files. +-// It includes all test variants. +-// +-// TODO(rfindley): Replace this with s.MetadataGraph(). +-func (s *Snapshot) AllMetadata(ctx context.Context) ([]*metadata.Package, error) { +- if err := s.awaitLoaded(ctx); err != nil { +- return nil, err +- } - --import ( -- "context" -- "errors" -- "fmt" -- "io/fs" -- "path/filepath" -- "strings" +- g := s.MetadataGraph() - -- "golang.org/x/mod/modfile" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" --) +- meta := make([]*metadata.Package, 0, len(g.Packages)) +- for _, mp := range g.Packages { +- meta = append(meta, mp) +- } +- return meta, nil +-} - --// TODO(rfindley): now that experimentalWorkspaceModule is gone, this file can --// be massively cleaned up and/or removed. +-// GoModForFile returns the URI of the go.mod file for the given URI. +-// +-// TODO(rfindley): clarify that this is only active modules. Or update to just +-// use findRootPattern. +-func (s *Snapshot) GoModForFile(uri protocol.DocumentURI) protocol.DocumentURI { +- return moduleForURI(s.view.workspaceModFiles, uri) +-} - --// computeWorkspaceModFiles computes the set of workspace mod files based on the --// value of go.mod, go.work, and GO111MODULE. --func computeWorkspaceModFiles(ctx context.Context, gomod, gowork span.URI, go111module go111module, fs source.FileSource) (map[span.URI]struct{}, error) { -- if go111module == off { -- return nil, nil -- } -- if gowork != "" { -- fh, err := fs.ReadFile(ctx, gowork) -- if err != nil { -- return nil, err -- } -- content, err := fh.Content() -- if err != nil { -- return nil, err -- } -- filename := gowork.Filename() -- dir := filepath.Dir(filename) -- workFile, err := modfile.ParseWork(filename, content, nil) -- if err != nil { -- return nil, fmt.Errorf("parsing go.work: %w", err) +-func moduleForURI(modFiles map[protocol.DocumentURI]struct{}, uri protocol.DocumentURI) protocol.DocumentURI { +- var match protocol.DocumentURI +- for modURI := range modFiles { +- if !modURI.Dir().Encloses(uri) { +- continue - } -- modFiles := make(map[span.URI]struct{}) -- for _, use := range workFile.Use { -- modDir := filepath.FromSlash(use.Path) -- if !filepath.IsAbs(modDir) { -- modDir = filepath.Join(dir, modDir) -- } -- modURI := span.URIFromPath(filepath.Join(modDir, "go.mod")) -- modFiles[modURI] = struct{}{} +- if len(modURI) > len(match) { +- match = modURI - } -- return modFiles, nil -- } -- if gomod != "" { -- return map[span.URI]struct{}{gomod: {}}, nil - } -- return nil, nil --} -- --// isGoMod reports if uri is a go.mod file. --func isGoMod(uri span.URI) bool { -- return filepath.Base(uri.Filename()) == "go.mod" +- return match -} - --// isGoWork reports if uri is a go.work file. --func isGoWork(uri span.URI) bool { -- return filepath.Base(uri.Filename()) == "go.work" +-// nearestModFile finds the nearest go.mod file contained in the directory +-// containing uri, or a parent of that directory. +-// +-// The given uri must be a file, not a directory. +-func nearestModFile(ctx context.Context, uri protocol.DocumentURI, fs file.Source) (protocol.DocumentURI, error) { +- dir := filepath.Dir(uri.Path()) +- return findRootPattern(ctx, protocol.URIFromPath(dir), "go.mod", fs) -} - --// fileExists reports whether the file has a Content (which may be empty). --// An overlay exists even if it is not reflected in the file system. --func fileExists(fh source.FileHandle) bool { -- _, err := fh.Content() -- return err == nil +-// Metadata returns the metadata for the specified package, +-// or nil if it was not found. +-func (s *Snapshot) Metadata(id PackageID) *metadata.Package { +- s.mu.Lock() +- defer s.mu.Unlock() +- return s.meta.Packages[id] -} - --// errExhausted is returned by findModules if the file scan limit is reached. --var errExhausted = errors.New("exhausted") -- --// Limit go.mod search to 1 million files. As a point of reference, --// Kubernetes has 22K files (as of 2020-11-24). --// --// Note: per golang/go#56496, the previous limit of 1M files was too slow, at --// which point this limit was decreased to 100K. --const fileLimit = 100_000 +-// clearShouldLoad clears package IDs that no longer need to be reloaded after +-// scopes has been loaded. +-func (s *Snapshot) clearShouldLoad(scopes ...loadScope) { +- s.mu.Lock() +- defer s.mu.Unlock() - --// findModules recursively walks the root directory looking for go.mod files, --// returning the set of modules it discovers. If modLimit is non-zero, --// searching stops once modLimit modules have been found. --// --// TODO(rfindley): consider overlays. --func findModules(root span.URI, excludePath func(string) bool, modLimit int) (map[span.URI]struct{}, error) { -- // Walk the view's folder to find all modules in the view. -- modFiles := make(map[span.URI]struct{}) -- searched := 0 -- errDone := errors.New("done") -- err := filepath.WalkDir(root.Filename(), func(path string, info fs.DirEntry, err error) error { -- if err != nil { -- // Probably a permission error. Keep looking. -- return filepath.SkipDir -- } -- // For any path that is not the workspace folder, check if the path -- // would be ignored by the go command. Vendor directories also do not -- // contain workspace modules. -- if info.IsDir() && path != root.Filename() { -- suffix := strings.TrimPrefix(path, root.Filename()) -- switch { -- case checkIgnored(suffix), -- strings.Contains(filepath.ToSlash(suffix), "/vendor/"), -- excludePath(suffix): -- return filepath.SkipDir +- for _, scope := range scopes { +- switch scope := scope.(type) { +- case packageLoadScope: +- scopePath := PackagePath(scope) +- var toDelete []PackageID +- s.shouldLoad.Range(func(id PackageID, pkgPaths []PackagePath) { +- for _, pkgPath := range pkgPaths { +- if pkgPath == scopePath { +- toDelete = append(toDelete, id) +- } +- } +- }) +- for _, id := range toDelete { +- s.shouldLoad.Delete(id) +- } +- case fileLoadScope: +- uri := protocol.DocumentURI(scope) +- ids := s.meta.IDs[uri] +- for _, id := range ids { +- s.shouldLoad.Delete(id) - } - } -- // We're only interested in go.mod files. -- uri := span.URIFromPath(path) -- if isGoMod(uri) { -- modFiles[uri] = struct{}{} -- } -- if modLimit > 0 && len(modFiles) >= modLimit { -- return errDone -- } -- searched++ -- if fileLimit > 0 && searched >= fileLimit { -- return errExhausted -- } -- return nil -- }) -- if err == errDone { -- return modFiles, nil - } -- return modFiles, err -} -diff -urN a/gopls/internal/lsp/call_hierarchy.go b/gopls/internal/lsp/call_hierarchy.go ---- a/gopls/internal/lsp/call_hierarchy.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/call_hierarchy.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,52 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package lsp -- --import ( -- "context" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/internal/event" --) +-// FindFile returns the FileHandle for the given URI, if it is already +-// in the given snapshot. +-// TODO(adonovan): delete this operation; use ReadFile instead. +-func (s *Snapshot) FindFile(uri protocol.DocumentURI) file.Handle { +- s.mu.Lock() +- defer s.mu.Unlock() - --func (s *Server) prepareCallHierarchy(ctx context.Context, params *protocol.CallHierarchyPrepareParams) ([]protocol.CallHierarchyItem, error) { -- ctx, done := event.Start(ctx, "lsp.Server.prepareCallHierarchy") -- defer done() +- result, _ := s.files.get(uri) +- return result +-} - -- snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go) -- defer release() -- if !ok { -- return nil, err -- } +-// ReadFile returns a File for the given URI. If the file is unknown it is added +-// to the managed set. +-// +-// ReadFile succeeds even if the file does not exist. A non-nil error return +-// indicates some type of internal error, for example if ctx is cancelled. +-func (s *Snapshot) ReadFile(ctx context.Context, uri protocol.DocumentURI) (file.Handle, error) { +- s.mu.Lock() +- defer s.mu.Unlock() - -- return source.PrepareCallHierarchy(ctx, snapshot, fh, params.Position) +- return lockedSnapshot{s}.ReadFile(ctx, uri) -} - --func (s *Server) incomingCalls(ctx context.Context, params *protocol.CallHierarchyIncomingCallsParams) ([]protocol.CallHierarchyIncomingCall, error) { -- ctx, done := event.Start(ctx, "lsp.Server.incomingCalls") -- defer done() +-// lockedSnapshot implements the file.Source interface, while holding s.mu. +-// +-// TODO(rfindley): This unfortunate type had been eliminated, but it had to be +-// restored to fix golang/go#65801. We should endeavor to remove it again. +-type lockedSnapshot struct { +- s *Snapshot +-} - -- snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.Item.URI, source.Go) -- defer release() +-func (s lockedSnapshot) ReadFile(ctx context.Context, uri protocol.DocumentURI) (file.Handle, error) { +- fh, ok := s.s.files.get(uri) - if !ok { -- return nil, err +- var err error +- fh, err = s.s.view.fs.ReadFile(ctx, uri) +- if err != nil { +- return nil, err +- } +- s.s.files.set(uri, fh) - } -- -- return source.IncomingCalls(ctx, snapshot, fh, params.Item.Range.Start) +- return fh, nil -} - --func (s *Server) outgoingCalls(ctx context.Context, params *protocol.CallHierarchyOutgoingCallsParams) ([]protocol.CallHierarchyOutgoingCall, error) { -- ctx, done := event.Start(ctx, "lsp.Server.outgoingCalls") -- defer done() -- -- snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.Item.URI, source.Go) -- defer release() -- if !ok { -- return nil, err +-// preloadFiles delegates to the view FileSource to read the requested uris in +-// parallel, without holding the snapshot lock. +-func (s *Snapshot) preloadFiles(ctx context.Context, uris []protocol.DocumentURI) { +- files := make([]file.Handle, len(uris)) +- var wg sync.WaitGroup +- iolimit := make(chan struct{}, 20) // I/O concurrency limiting semaphore +- for i, uri := range uris { +- wg.Add(1) +- iolimit <- struct{}{} +- go func(i int, uri protocol.DocumentURI) { +- defer wg.Done() +- fh, err := s.view.fs.ReadFile(ctx, uri) +- <-iolimit +- if err != nil && ctx.Err() == nil { +- event.Error(ctx, fmt.Sprintf("reading %s", uri), err) +- return +- } +- files[i] = fh +- }(i, uri) - } +- wg.Wait() - -- return source.OutgoingCalls(ctx, snapshot, fh, params.Item.Range.Start) --} -diff -urN a/gopls/internal/lsp/cmd/call_hierarchy.go b/gopls/internal/lsp/cmd/call_hierarchy.go ---- a/gopls/internal/lsp/cmd/call_hierarchy.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/call_hierarchy.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,144 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +- s.mu.Lock() +- defer s.mu.Unlock() - --package cmd +- for i, fh := range files { +- if fh == nil { +- continue // error logged above +- } +- uri := uris[i] +- if _, ok := s.files.get(uri); !ok { +- s.files.set(uri, fh) +- } +- } +-} - --import ( -- "context" -- "flag" -- "fmt" -- "strings" +-// IsOpen returns whether the editor currently has a file open. +-func (s *Snapshot) IsOpen(uri protocol.DocumentURI) bool { +- s.mu.Lock() +- defer s.mu.Unlock() - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/tool" --) +- fh, _ := s.files.get(uri) +- _, open := fh.(*overlay) +- return open +-} - --// callHierarchy implements the callHierarchy verb for gopls. --type callHierarchy struct { -- app *Application +-// MetadataGraph returns the current metadata graph for the Snapshot. +-func (s *Snapshot) MetadataGraph() *metadata.Graph { +- s.mu.Lock() +- defer s.mu.Unlock() +- return s.meta -} - --func (c *callHierarchy) Name() string { return "call_hierarchy" } --func (c *callHierarchy) Parent() string { return c.app.Name() } --func (c *callHierarchy) Usage() string { return "" } --func (c *callHierarchy) ShortHelp() string { return "display selected identifier's call hierarchy" } --func (c *callHierarchy) DetailedHelp(f *flag.FlagSet) { -- fmt.Fprint(f.Output(), ` --Example: +-// InitializationError returns the last error from initialization. +-func (s *Snapshot) InitializationError() *InitializationError { +- s.mu.Lock() +- defer s.mu.Unlock() +- return s.initialErr +-} - -- $ # 1-indexed location (:line:column or :#offset) of the target identifier -- $ gopls call_hierarchy helper/helper.go:8:6 -- $ gopls call_hierarchy helper/helper.go:#53 --`) -- printFlagDefaults(f) +-// awaitLoaded awaits initialization and package reloading, and returns +-// ctx.Err(). +-func (s *Snapshot) awaitLoaded(ctx context.Context) error { +- // Do not return results until the snapshot's view has been initialized. +- s.AwaitInitialized(ctx) +- s.reloadWorkspace(ctx) +- return ctx.Err() -} - --func (c *callHierarchy) Run(ctx context.Context, args ...string) error { -- if len(args) != 1 { -- return tool.CommandLineErrorf("call_hierarchy expects 1 argument (position)") +-// AwaitInitialized waits until the snapshot's view is initialized. +-func (s *Snapshot) AwaitInitialized(ctx context.Context) { +- select { +- case <-ctx.Done(): +- return +- case <-s.view.initialWorkspaceLoad: - } +- // We typically prefer to run something as intensive as the IWL without +- // blocking. I'm not sure if there is a way to do that here. +- s.initialize(ctx, false) +-} - -- conn, err := c.app.connect(ctx, nil) -- if err != nil { -- return err -- } -- defer conn.terminate(ctx) +-// reloadWorkspace reloads the metadata for all invalidated workspace packages. +-func (s *Snapshot) reloadWorkspace(ctx context.Context) { +- var scopes []loadScope +- var seen map[PackagePath]bool +- s.mu.Lock() +- s.shouldLoad.Range(func(_ PackageID, pkgPaths []PackagePath) { +- for _, pkgPath := range pkgPaths { +- if seen == nil { +- seen = make(map[PackagePath]bool) +- } +- if seen[pkgPath] { +- continue +- } +- seen[pkgPath] = true +- scopes = append(scopes, packageLoadScope(pkgPath)) +- } +- }) +- s.mu.Unlock() - -- from := span.Parse(args[0]) -- file, err := conn.openFile(ctx, from.URI()) -- if err != nil { -- return err +- if len(scopes) == 0 { +- return - } - -- loc, err := file.mapper.SpanLocation(from) -- if err != nil { -- return err +- // For an ad-hoc view, we cannot reload by package path. Just reload the view. +- if s.view.typ == AdHocView { +- scopes = []loadScope{viewLoadScope{}} - } - -- p := protocol.CallHierarchyPrepareParams{ -- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), -- } +- err := s.load(ctx, false, scopes...) - -- callItems, err := conn.PrepareCallHierarchy(ctx, &p) -- if err != nil { -- return err +- // Unless the context was canceled, set "shouldLoad" to false for all +- // of the metadata we attempted to load. +- if !errors.Is(err, context.Canceled) { +- s.clearShouldLoad(scopes...) +- if err != nil { +- event.Error(ctx, "reloading workspace", err, s.Labels()...) +- } - } -- if len(callItems) == 0 { -- return fmt.Errorf("function declaration identifier not found at %v", args[0]) +-} +- +-func (s *Snapshot) orphanedFileDiagnostics(ctx context.Context, overlays []*overlay) ([]*Diagnostic, error) { +- if err := s.awaitLoaded(ctx); err != nil { +- return nil, err - } - -- for _, item := range callItems { -- incomingCalls, err := conn.IncomingCalls(ctx, &protocol.CallHierarchyIncomingCallsParams{Item: item}) +- var diagnostics []*Diagnostic +- var orphaned []*overlay +-searchOverlays: +- for _, o := range overlays { +- uri := o.URI() +- if s.IsBuiltin(uri) || s.FileKind(o) != file.Go { +- continue +- } +- mps, err := s.MetadataForFile(ctx, uri) - if err != nil { -- return err +- return nil, err - } -- for i, call := range incomingCalls { -- // From the spec: CallHierarchyIncomingCall.FromRanges is relative to -- // the caller denoted by CallHierarchyIncomingCall.from. -- printString, err := callItemPrintString(ctx, conn, call.From, call.From.URI, call.FromRanges) -- if err != nil { -- return err +- for _, mp := range mps { +- if !metadata.IsCommandLineArguments(mp.ID) || mp.Standalone { +- continue searchOverlays - } -- fmt.Printf("caller[%d]: %s\n", i, printString) -- } -- -- printString, err := callItemPrintString(ctx, conn, item, "", nil) -- if err != nil { -- return err - } -- fmt.Printf("identifier: %s\n", printString) +- metadata.RemoveIntermediateTestVariants(&mps) - -- outgoingCalls, err := conn.OutgoingCalls(ctx, &protocol.CallHierarchyOutgoingCallsParams{Item: item}) -- if err != nil { -- return err -- } -- for i, call := range outgoingCalls { -- // From the spec: CallHierarchyOutgoingCall.FromRanges is the range -- // relative to the caller, e.g the item passed to -- printString, err := callItemPrintString(ctx, conn, call.To, item.URI, call.FromRanges) +- // With zero-config gopls (golang/go#57979), orphaned file diagnostics +- // include diagnostics for orphaned files -- not just diagnostics relating +- // to the reason the files are opened. +- // +- // This is because orphaned files are never considered part of a workspace +- // package: if they are loaded by a view, that view is arbitrary, and they +- // may be loaded by multiple views. If they were to be diagnosed by +- // multiple views, their diagnostics may become inconsistent. +- if len(mps) > 0 { +- diags, err := s.PackageDiagnostics(ctx, mps[0].ID) - if err != nil { -- return err +- return nil, err - } -- fmt.Printf("callee[%d]: %s\n", i, printString) +- diagnostics = append(diagnostics, diags[uri]...) - } +- orphaned = append(orphaned, o) - } - -- return nil --} -- --// callItemPrintString returns a protocol.CallHierarchyItem object represented as a string. --// item and call ranges (protocol.Range) are converted to user friendly spans (1-indexed). --func callItemPrintString(ctx context.Context, conn *connection, item protocol.CallHierarchyItem, callsURI protocol.DocumentURI, calls []protocol.Range) (string, error) { -- itemFile, err := conn.openFile(ctx, item.URI.SpanURI()) -- if err != nil { -- return "", err -- } -- itemSpan, err := itemFile.mapper.RangeSpan(item.Range) -- if err != nil { -- return "", err +- if len(orphaned) == 0 { +- return nil, nil - } - -- var callRanges []string -- if callsURI != "" { -- callsFile, err := conn.openFile(ctx, callsURI.SpanURI()) -- if err != nil { -- return "", err +- loadedModFiles := make(map[protocol.DocumentURI]struct{}) // all mod files, including dependencies +- ignoredFiles := make(map[protocol.DocumentURI]bool) // files reported in packages.Package.IgnoredFiles +- +- g := s.MetadataGraph() +- for _, meta := range g.Packages { +- if meta.Module != nil && meta.Module.GoMod != "" { +- gomod := protocol.URIFromPath(meta.Module.GoMod) +- loadedModFiles[gomod] = struct{}{} - } -- for _, rng := range calls { -- call, err := callsFile.mapper.RangeSpan(rng) -- if err != nil { -- return "", err -- } -- callRange := fmt.Sprintf("%d:%d-%d", call.Start().Line(), call.Start().Column(), call.End().Column()) -- callRanges = append(callRanges, callRange) +- for _, ignored := range meta.IgnoredFiles { +- ignoredFiles[ignored] = true - } - } - -- printString := fmt.Sprintf("function %s in %v", item.Name, itemSpan) -- if len(calls) > 0 { -- printString = fmt.Sprintf("ranges %s in %s from/to %s", strings.Join(callRanges, ", "), callsURI.SpanURI().Filename(), printString) -- } -- return printString, nil --} -diff -urN a/gopls/internal/lsp/cmd/capabilities_test.go b/gopls/internal/lsp/cmd/capabilities_test.go ---- a/gopls/internal/lsp/cmd/capabilities_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/capabilities_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,176 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +- initialErr := s.InitializationError() - --package cmd +- for _, fh := range orphaned { +- pgf, rng, ok := orphanedFileDiagnosticRange(ctx, s.view.parseCache, fh) +- if !ok { +- continue // e.g. cancellation or parse error +- } - --import ( -- "context" -- "fmt" -- "os" -- "path/filepath" -- "testing" +- var ( +- msg string // if non-empty, report a diagnostic with this message +- suggestedFixes []SuggestedFix // associated fixes, if any +- ) +- if initialErr != nil { +- msg = fmt.Sprintf("initialization failed: %v", initialErr.MainError) +- } else if goMod, err := nearestModFile(ctx, fh.URI(), s); err == nil && goMod != "" { +- // If we have a relevant go.mod file, check whether the file is orphaned +- // due to its go.mod file being inactive. We could also offer a +- // prescriptive diagnostic in the case that there is no go.mod file, but it +- // is harder to be precise in that case, and less important. +- if _, ok := loadedModFiles[goMod]; !ok { +- modDir := filepath.Dir(goMod.Path()) +- viewDir := s.view.folder.Dir.Path() - -- "golang.org/x/tools/gopls/internal/lsp" -- "golang.org/x/tools/gopls/internal/lsp/cache" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/internal/testenv" --) +- // When the module is underneath the view dir, we offer +- // "use all modules" quick-fixes. +- inDir := pathutil.InDir(viewDir, modDir) - --// TestCapabilities does some minimal validation of the server's adherence to the LSP. --// The checks in the test are added as changes are made and errors noticed. --func TestCapabilities(t *testing.T) { -- // TODO(bcmills): This test fails on js/wasm, which is not unexpected, but the -- // failure mode is that the DidOpen call below reports "no views in session", -- // which seems a little too cryptic. -- // Is there some missing error reporting somewhere? -- testenv.NeedsTool(t, "go") +- if rel, err := filepath.Rel(viewDir, modDir); err == nil { +- modDir = rel +- } - -- tmpDir, err := os.MkdirTemp("", "fake") -- if err != nil { -- t.Fatal(err) -- } -- tmpFile := filepath.Join(tmpDir, "fake.go") -- if err := os.WriteFile(tmpFile, []byte(""), 0775); err != nil { -- t.Fatal(err) -- } -- if err := os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte("module fake\n\ngo 1.12\n"), 0775); err != nil { -- t.Fatal(err) -- } -- defer os.RemoveAll(tmpDir) +- var fix string +- if s.view.folder.Env.GoVersion >= 18 { +- if s.view.gowork != "" { +- fix = fmt.Sprintf("To fix this problem, you can add this module to your go.work file (%s)", s.view.gowork) +- if cmd, err := command.NewRunGoWorkCommandCommand("Run `go work use`", command.RunGoWorkArgs{ +- ViewID: s.view.ID(), +- Args: []string{"use", modDir}, +- }); err == nil { +- suggestedFixes = append(suggestedFixes, SuggestedFix{ +- Title: "Use this module in your go.work file", +- Command: &cmd, +- ActionKind: protocol.QuickFix, +- }) +- } - -- app := New("gopls-test", tmpDir, os.Environ(), nil) +- if inDir { +- if cmd, err := command.NewRunGoWorkCommandCommand("Run `go work use -r`", command.RunGoWorkArgs{ +- ViewID: s.view.ID(), +- Args: []string{"use", "-r", "."}, +- }); err == nil { +- suggestedFixes = append(suggestedFixes, SuggestedFix{ +- Title: "Use all modules in your workspace", +- Command: &cmd, +- ActionKind: protocol.QuickFix, +- }) +- } +- } +- } else { +- fix = "To fix this problem, you can add a go.work file that uses this directory." - -- params := &protocol.ParamInitialize{} -- params.RootURI = protocol.URIFromPath(app.wd) -- params.Capabilities.Workspace.Configuration = true +- if cmd, err := command.NewRunGoWorkCommandCommand("Run `go work init && go work use`", command.RunGoWorkArgs{ +- ViewID: s.view.ID(), +- InitFirst: true, +- Args: []string{"use", modDir}, +- }); err == nil { +- suggestedFixes = []SuggestedFix{ +- { +- Title: "Add a go.work file using this module", +- Command: &cmd, +- ActionKind: protocol.QuickFix, +- }, +- } +- } - -- // Send an initialize request to the server. -- ctx := context.Background() -- client := newClient(app, nil) -- options := source.DefaultOptions(app.options) -- server := lsp.NewServer(cache.NewSession(ctx, cache.New(nil)), client, options) -- result, err := server.Initialize(ctx, params) +- if inDir { +- if cmd, err := command.NewRunGoWorkCommandCommand("Run `go work init && go work use -r`", command.RunGoWorkArgs{ +- ViewID: s.view.ID(), +- InitFirst: true, +- Args: []string{"use", "-r", "."}, +- }); err == nil { +- suggestedFixes = append(suggestedFixes, SuggestedFix{ +- Title: "Add a go.work file using all modules in your workspace", +- Command: &cmd, +- ActionKind: protocol.QuickFix, +- }) +- } +- } +- } +- } else { +- fix = `To work with multiple modules simultaneously, please upgrade to Go 1.18 or +-later, reinstall gopls, and use a go.work file.` +- } +- +- msg = fmt.Sprintf(`This file is within module %q, which is not included in your workspace. +-%s +-See the documentation for more information on setting up your workspace: +-https://github.com/golang/tools/blob/master/gopls/doc/workspace.md.`, modDir, fix) +- } +- } +- +- if msg == "" { +- if ignoredFiles[fh.URI()] { +- // TODO(rfindley): use the constraint package to check if the file +- // _actually_ satisfies the current build context. +- hasConstraint := false +- walkConstraints(pgf.File, func(constraint.Expr) bool { +- hasConstraint = true +- return false +- }) +- var fix string +- if hasConstraint { +- fix = `This file may be excluded due to its build tags; try adding "-tags=" to your gopls "buildFlags" configuration +-See the documentation for more information on working with build tags: +-https://github.com/golang/tools/blob/master/gopls/doc/settings.md#buildflags-string.` +- } else if strings.Contains(filepath.Base(fh.URI().Path()), "_") { +- fix = `This file may be excluded due to its GOOS/GOARCH, or other build constraints.` +- } else { +- fix = `This file is ignored by your gopls build.` // we don't know why +- } +- msg = fmt.Sprintf("No packages found for open file %s.\n%s", fh.URI().Path(), fix) +- } else { +- // Fall back: we're not sure why the file is orphaned. +- // TODO(rfindley): we could do better here, diagnosing the lack of a +- // go.mod file and malformed file names (see the perc%ent marker test). +- msg = fmt.Sprintf("No packages found for open file %s.", fh.URI().Path()) +- } +- } +- +- if msg != "" { +- d := &Diagnostic{ +- URI: fh.URI(), +- Range: rng, +- Severity: protocol.SeverityWarning, +- Source: ListError, +- Message: msg, +- SuggestedFixes: suggestedFixes, +- } +- if ok := bundleQuickFixes(d); !ok { +- bug.Reportf("failed to bundle quick fixes for %v", d) +- } +- // Only report diagnostics if we detect an actual exclusion. +- diagnostics = append(diagnostics, d) +- } +- } +- return diagnostics, nil +-} +- +-// orphanedFileDiagnosticRange returns the position to use for orphaned file diagnostics. +-// We only warn about an orphaned file if it is well-formed enough to actually +-// be part of a package. Otherwise, we need more information. +-func orphanedFileDiagnosticRange(ctx context.Context, cache *parseCache, fh file.Handle) (*parsego.File, protocol.Range, bool) { +- pgfs, err := cache.parseFiles(ctx, token.NewFileSet(), parsego.Header, false, fh) - if err != nil { -- t.Fatal(err) +- return nil, protocol.Range{}, false - } -- // Validate initialization result. -- if err := validateCapabilities(result); err != nil { -- t.Error(err) +- pgf := pgfs[0] +- if !pgf.File.Name.Pos().IsValid() { +- return nil, protocol.Range{}, false - } -- // Complete initialization of server. -- if err := server.Initialized(ctx, &protocol.InitializedParams{}); err != nil { -- t.Fatal(err) +- rng, err := pgf.PosRange(pgf.File.Name.Pos(), pgf.File.Name.End()) +- if err != nil { +- return nil, protocol.Range{}, false - } +- return pgf, rng, true +-} - -- c := newConnection(server, client) -- defer c.terminate(ctx) +-// TODO(golang/go#53756): this function needs to consider more than just the +-// absolute URI, for example: +-// - the position of /vendor/ with respect to the relevant module root +-// - whether or not go.work is in use (as vendoring isn't supported in workspace mode) +-// +-// Most likely, each call site of inVendor needs to be reconsidered to +-// understand and correctly implement the desired behavior. +-func inVendor(uri protocol.DocumentURI) bool { +- _, after, found := strings.Cut(string(uri), "/vendor/") +- // Only subdirectories of /vendor/ are considered vendored +- // (/vendor/a/foo.go is vendored, /vendor/foo.go is not). +- return found && strings.Contains(after, "/") +-} - -- // Open the file on the server side. -- uri := protocol.URIFromPath(tmpFile) -- if err := c.Server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{ -- TextDocument: protocol.TextDocumentItem{ -- URI: uri, -- LanguageID: "go", -- Version: 1, -- Text: `package main; func main() {};`, -- }, -- }); err != nil { -- t.Fatal(err) -- } +-// clone copies state from the receiver into a new Snapshot, applying the given +-// state changes. +-// +-// The caller of clone must call Snapshot.decref on the returned +-// snapshot when they are finished using it. +-// +-// The resulting bool reports whether the change invalidates any derived +-// diagnostics for the snapshot, for example because it invalidates Packages or +-// parsed go.mod files. This is used to mark a view as needing diagnosis in the +-// server. +-// +-// TODO(rfindley): long term, it may be better to move responsibility for +-// diagnostics into the Snapshot (e.g. a Snapshot.Diagnostics method), at which +-// point the Snapshot could be responsible for tracking and forwarding a +-// 'viewsToDiagnose' field. As is, this field is instead externalized in the +-// server.viewsToDiagnose map. Moving it to the snapshot would entirely +-// eliminate any 'relevance' heuristics from Session.DidModifyFiles, but would +-// also require more strictness about diagnostic dependencies. For example, +-// template.Diagnostics currently re-parses every time: there is no Snapshot +-// data responsible for providing these diagnostics. +-func (s *Snapshot) clone(ctx, bgCtx context.Context, changed StateChange, done func()) (*Snapshot, bool) { +- changedFiles := changed.Files +- ctx, stop := event.Start(ctx, "cache.snapshot.clone") +- defer stop() - -- // If we are sending a full text change, the change.Range must be nil. -- // It is not enough for the Change to be empty, as that is ambiguous. -- if err := c.Server.DidChange(ctx, &protocol.DidChangeTextDocumentParams{ -- TextDocument: protocol.VersionedTextDocumentIdentifier{ -- TextDocumentIdentifier: protocol.TextDocumentIdentifier{ -- URI: uri, -- }, -- Version: 2, -- }, -- ContentChanges: []protocol.TextDocumentContentChangeEvent{ -- { -- Range: nil, -- Text: `package main; func main() { fmt.Println("") }`, -- }, -- }, -- }); err != nil { -- t.Fatal(err) -- } +- s.mu.Lock() +- defer s.mu.Unlock() - -- // Send a code action request to validate expected types. -- actions, err := c.Server.CodeAction(ctx, &protocol.CodeActionParams{ -- TextDocument: protocol.TextDocumentIdentifier{ -- URI: uri, -- }, -- }) -- if err != nil { -- t.Fatal(err) -- } -- for _, action := range actions { -- // Validate that an empty command is sent along with import organization responses. -- if action.Kind == protocol.SourceOrganizeImports && action.Command != nil { -- t.Errorf("unexpected command for import organization") +- // TODO(rfindley): reorganize this function to make the derivation of +- // needsDiagnosis clearer. +- needsDiagnosis := len(changed.GCDetails) > 0 || len(changed.ModuleUpgrades) > 0 || len(changed.Vulns) > 0 +- +- bgCtx, cancel := context.WithCancel(bgCtx) +- result := &Snapshot{ +- sequenceID: s.sequenceID + 1, +- store: s.store, +- refcount: 1, // Snapshots are born referenced. +- done: done, +- view: s.view, +- backgroundCtx: bgCtx, +- cancel: cancel, +- builtin: s.builtin, +- initialized: s.initialized, +- initialErr: s.initialErr, +- packages: s.packages.Clone(), +- activePackages: s.activePackages.Clone(), +- files: s.files.clone(changedFiles), +- symbolizeHandles: cloneWithout(s.symbolizeHandles, changedFiles, nil), +- workspacePackages: s.workspacePackages, +- shouldLoad: s.shouldLoad.Clone(), // not cloneWithout: shouldLoad is cleared on loads +- unloadableFiles: s.unloadableFiles.Clone(), // not cloneWithout: typing in a file doesn't necessarily make it loadable +- parseModHandles: cloneWithout(s.parseModHandles, changedFiles, &needsDiagnosis), +- parseWorkHandles: cloneWithout(s.parseWorkHandles, changedFiles, &needsDiagnosis), +- modTidyHandles: cloneWithout(s.modTidyHandles, changedFiles, &needsDiagnosis), +- modWhyHandles: cloneWithout(s.modWhyHandles, changedFiles, &needsDiagnosis), +- modVulnHandles: cloneWithout(s.modVulnHandles, changedFiles, &needsDiagnosis), +- importGraph: s.importGraph, +- pkgIndex: s.pkgIndex, +- moduleUpgrades: cloneWith(s.moduleUpgrades, changed.ModuleUpgrades), +- vulns: cloneWith(s.vulns, changed.Vulns), +- } +- +- // Compute the new set of packages for which we want gc details, after +- // applying changed.GCDetails. +- if len(s.gcOptimizationDetails) > 0 || len(changed.GCDetails) > 0 { +- newGCDetails := make(map[metadata.PackageID]unit) +- for id := range s.gcOptimizationDetails { +- if _, ok := changed.GCDetails[id]; !ok { +- newGCDetails[id] = unit{} // no change +- } +- } +- for id, want := range changed.GCDetails { +- if want { +- newGCDetails[id] = unit{} +- } +- } +- if len(newGCDetails) > 0 { +- result.gcOptimizationDetails = newGCDetails - } - } - -- if err := c.Server.DidSave(ctx, &protocol.DidSaveTextDocumentParams{ -- TextDocument: protocol.TextDocumentIdentifier{ -- URI: uri, -- }, -- // LSP specifies that a file can be saved with optional text, so this field must be nil. -- Text: nil, -- }); err != nil { -- t.Fatal(err) +- reinit := false +- +- // Changes to vendor tree may require reinitialization, +- // either because of an initialization error +- // (e.g. "inconsistent vendoring detected"), or because +- // one or more modules may have moved into or out of the +- // vendor tree after 'go mod vendor' or 'rm -fr vendor/'. +- // +- // In this case, we consider the actual modification to see if was a creation +- // or deletion. +- // +- // TODO(rfindley): revisit the location of this check. +- for _, mod := range changed.Modifications { +- if inVendor(mod.URI) && (mod.Action == file.Create || mod.Action == file.Delete) || +- strings.HasSuffix(string(mod.URI), "/vendor/modules.txt") { +- +- reinit = true +- break +- } - } - -- // Send a completion request to validate expected types. -- list, err := c.Server.Completion(ctx, &protocol.CompletionParams{ -- TextDocumentPositionParams: protocol.TextDocumentPositionParams{ -- TextDocument: protocol.TextDocumentIdentifier{ -- URI: uri, -- }, -- Position: protocol.Position{ -- Line: 0, -- Character: 28, -- }, -- }, -- }) -- if err != nil { -- t.Fatal(err) +- // Collect observed file handles for changed URIs from the old snapshot, if +- // they exist. Importantly, we don't call ReadFile here: consider the case +- // where a file is added on disk; we don't want to read the newly added file +- // into the old snapshot, as that will break our change detection below. +- // +- // TODO(rfindley): it may be more accurate to rely on the modification type +- // here, similarly to what we do for vendored files above. If we happened not +- // to have read a file in the previous snapshot, that's not the same as it +- // actually being created. +- oldFiles := make(map[protocol.DocumentURI]file.Handle) +- for uri := range changedFiles { +- if fh, ok := s.files.get(uri); ok { +- oldFiles[uri] = fh +- } - } -- for _, item := range list.Items { -- // All other completion items should have nil commands. -- // An empty command will be treated as a command with the name '' by VS Code. -- // This causes VS Code to report errors to users about invalid commands. -- if item.Command != nil { -- t.Errorf("unexpected command for completion item") +- // changedOnDisk determines if the new file handle may have changed on disk. +- // It over-approximates, returning true if the new file is saved and either +- // the old file wasn't saved, or the on-disk contents changed. +- // +- // oldFH may be nil. +- changedOnDisk := func(oldFH, newFH file.Handle) bool { +- if !newFH.SameContentsOnDisk() { +- return false - } -- // The item's TextEdit must be a pointer, as VS Code considers TextEdits -- // that don't contain the cursor position to be invalid. -- var textEdit interface{} = item.TextEdit -- if _, ok := textEdit.(*protocol.TextEdit); !ok { -- t.Errorf("textEdit is not a *protocol.TextEdit, instead it is %T", textEdit) +- if oe, ne := (oldFH != nil && fileExists(oldFH)), fileExists(newFH); !oe || !ne { +- return oe != ne - } +- return !oldFH.SameContentsOnDisk() || oldFH.Identity() != newFH.Identity() - } -- if err := c.Server.Shutdown(ctx); err != nil { -- t.Fatal(err) -- } -- if err := c.Server.Exit(ctx); err != nil { -- t.Fatal(err) +- +- // Reinitialize if any workspace mod file has changed on disk. +- for uri, newFH := range changedFiles { +- if _, ok := result.view.workspaceModFiles[uri]; ok && changedOnDisk(oldFiles[uri], newFH) { +- reinit = true +- } - } --} - --func validateCapabilities(result *protocol.InitializeResult) error { -- // If the client sends "false" for RenameProvider.PrepareSupport, -- // the server must respond with a boolean. -- if v, ok := result.Capabilities.RenameProvider.(bool); !ok { -- return fmt.Errorf("RenameProvider must be a boolean if PrepareSupport is false (got %T)", v) +- // Finally, process sumfile changes that may affect loading. +- for uri, newFH := range changedFiles { +- if !changedOnDisk(oldFiles[uri], newFH) { +- continue // like with go.mod files, we only reinit when things change on disk +- } +- dir, base := filepath.Split(uri.Path()) +- if base == "go.work.sum" && s.view.typ == GoWorkView && dir == filepath.Dir(s.view.gowork.Path()) { +- reinit = true +- } +- if base == "go.sum" { +- modURI := protocol.URIFromPath(filepath.Join(dir, "go.mod")) +- if _, active := result.view.workspaceModFiles[modURI]; active { +- reinit = true +- } +- } - } -- // The same goes for CodeActionKind.ValueSet. -- if v, ok := result.Capabilities.CodeActionProvider.(bool); !ok { -- return fmt.Errorf("CodeActionSupport must be a boolean if CodeActionKind.ValueSet has length 0 (got %T)", v) +- +- // The snapshot should be initialized if either s was uninitialized, or we've +- // detected a change that triggers reinitialization. +- if reinit { +- result.initialized = false +- needsDiagnosis = true - } -- return nil --} -diff -urN a/gopls/internal/lsp/cmd/check.go b/gopls/internal/lsp/cmd/check.go ---- a/gopls/internal/lsp/cmd/check.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/check.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,73 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package cmd +- // directIDs keeps track of package IDs that have directly changed. +- // Note: this is not a set, it's a map from id to invalidateMetadata. +- directIDs := map[PackageID]bool{} - --import ( -- "context" -- "flag" -- "fmt" +- // Invalidate all package metadata if the workspace module has changed. +- if reinit { +- for k := range s.meta.Packages { +- // TODO(rfindley): this seems brittle; can we just start over? +- directIDs[k] = true +- } +- } - -- "golang.org/x/tools/gopls/internal/span" --) +- // Compute invalidations based on file changes. +- anyImportDeleted := false // import deletions can resolve cycles +- anyFileOpenedOrClosed := false // opened files affect workspace packages +- anyFileAdded := false // adding a file can resolve missing dependencies - --// check implements the check verb for gopls. --type check struct { -- app *Application --} +- for uri, newFH := range changedFiles { +- // The original FileHandle for this URI is cached on the snapshot. +- oldFH := oldFiles[uri] // may be nil +- _, oldOpen := oldFH.(*overlay) +- _, newOpen := newFH.(*overlay) - --func (c *check) Name() string { return "check" } --func (c *check) Parent() string { return c.app.Name() } --func (c *check) Usage() string { return "" } --func (c *check) ShortHelp() string { return "show diagnostic results for the specified file" } --func (c *check) DetailedHelp(f *flag.FlagSet) { -- fmt.Fprint(f.Output(), ` --Example: show the diagnostic results of this file: +- anyFileOpenedOrClosed = anyFileOpenedOrClosed || (oldOpen != newOpen) +- anyFileAdded = anyFileAdded || (oldFH == nil || !fileExists(oldFH)) && fileExists(newFH) - -- $ gopls check internal/lsp/cmd/check.go --`) -- printFlagDefaults(f) --} +- // If uri is a Go file, check if it has changed in a way that would +- // invalidate metadata. Note that we can't use s.view.FileKind here, +- // because the file type that matters is not what the *client* tells us, +- // but what the Go command sees. +- var invalidateMetadata, pkgFileChanged, importDeleted bool +- if strings.HasSuffix(uri.Path(), ".go") { +- invalidateMetadata, pkgFileChanged, importDeleted = metadataChanges(ctx, s, oldFH, newFH) +- } +- if invalidateMetadata { +- // If this is a metadata-affecting change, perhaps a reload will succeed. +- result.unloadableFiles.Remove(uri) +- needsDiagnosis = true +- } - --// Run performs the check on the files specified by args and prints the --// results to stdout. --func (c *check) Run(ctx context.Context, args ...string) error { -- if len(args) == 0 { -- // no files, so no results -- return nil +- invalidateMetadata = invalidateMetadata || reinit +- anyImportDeleted = anyImportDeleted || importDeleted +- +- // Mark all of the package IDs containing the given file. +- filePackageIDs := invalidatedPackageIDs(uri, s.meta.IDs, pkgFileChanged) +- for id := range filePackageIDs { +- directIDs[id] = directIDs[id] || invalidateMetadata // may insert 'false' +- } +- +- // Invalidate the previous modTidyHandle if any of the files have been +- // saved or if any of the metadata has been invalidated. +- // +- // TODO(rfindley): this seems like too-aggressive invalidation of mod +- // results. We should instead thread through overlays to the Go command +- // invocation and only run this if invalidateMetadata (and perhaps then +- // still do it less frequently). +- if invalidateMetadata || fileWasSaved(oldFH, newFH) { +- // Only invalidate mod tidy results for the most relevant modfile in the +- // workspace. This is a potentially lossy optimization for workspaces +- // with many modules (such as google-cloud-go, which has 145 modules as +- // of writing). +- // +- // While it is theoretically possible that a change in workspace module A +- // could affect the mod-tidiness of workspace module B (if B transitively +- // requires A), such changes are probably unlikely and not worth the +- // penalty of re-running go mod tidy for everything. Note that mod tidy +- // ignores GOWORK, so the two modules would have to be related by a chain +- // of replace directives. +- // +- // We could improve accuracy by inspecting replace directives, using +- // overlays in go mod tidy, and/or checking for metadata changes from the +- // on-disk content. +- // +- // Note that we iterate the modTidyHandles map here, rather than e.g. +- // using nearestModFile, because we don't have access to an accurate +- // FileSource at this point in the snapshot clone. +- const onlyInvalidateMostRelevant = true +- if onlyInvalidateMostRelevant { +- deleteMostRelevantModFile(result.modTidyHandles, uri) +- } else { +- result.modTidyHandles.Clear() +- } +- +- // TODO(rfindley): should we apply the above heuristic to mod vuln or mod +- // why handles as well? +- // +- // TODO(rfindley): no tests fail if I delete the line below. +- result.modWhyHandles.Clear() +- result.modVulnHandles.Clear() +- } - } -- checking := map[span.URI]*cmdFile{} -- var uris []span.URI -- // now we ready to kick things off -- conn, err := c.app.connect(ctx, nil) -- if err != nil { -- return err +- +- // Deleting an import can cause list errors due to import cycles to be +- // resolved. The best we can do without parsing the list error message is to +- // hope that list errors may have been resolved by a deleted import. +- // +- // We could do better by parsing the list error message. We already do this +- // to assign a better range to the list error, but for such critical +- // functionality as metadata, it's better to be conservative until it proves +- // impractical. +- // +- // We could also do better by looking at which imports were deleted and +- // trying to find cycles they are involved in. This fails when the file goes +- // from an unparseable state to a parseable state, as we don't have a +- // starting point to compare with. +- if anyImportDeleted { +- for id, mp := range s.meta.Packages { +- if len(mp.Errors) > 0 { +- directIDs[id] = true +- } +- } - } -- defer conn.terminate(ctx) -- for _, arg := range args { -- uri := span.URIFromPath(arg) -- uris = append(uris, uri) -- file, err := conn.openFile(ctx, uri) -- if err != nil { -- return err +- +- // Adding a file can resolve missing dependencies from existing packages. +- // +- // We could be smart here and try to guess which packages may have been +- // fixed, but until that proves necessary, just invalidate metadata for any +- // package with missing dependencies. +- if anyFileAdded { +- for id, mp := range s.meta.Packages { +- for _, impID := range mp.DepsByImpPath { +- if impID == "" { // missing import +- directIDs[id] = true +- break +- } +- } - } -- checking[uri] = file - } -- if err := conn.diagnoseFiles(ctx, uris); err != nil { -- return err +- +- // Invalidate reverse dependencies too. +- // idsToInvalidate keeps track of transitive reverse dependencies. +- // If an ID is present in the map, invalidate its types. +- // If an ID's value is true, invalidate its metadata too. +- idsToInvalidate := map[PackageID]bool{} +- var addRevDeps func(PackageID, bool) +- addRevDeps = func(id PackageID, invalidateMetadata bool) { +- current, seen := idsToInvalidate[id] +- newInvalidateMetadata := current || invalidateMetadata +- +- // If we've already seen this ID, and the value of invalidate +- // metadata has not changed, we can return early. +- if seen && current == newInvalidateMetadata { +- return +- } +- idsToInvalidate[id] = newInvalidateMetadata +- for _, rid := range s.meta.ImportedBy[id] { +- addRevDeps(rid, invalidateMetadata) +- } +- } +- for id, invalidateMetadata := range directIDs { +- addRevDeps(id, invalidateMetadata) - } -- conn.client.filesMu.Lock() -- defer conn.client.filesMu.Unlock() - -- for _, file := range checking { -- for _, d := range file.diagnostics { -- spn, err := file.mapper.RangeSpan(d.Range) -- if err != nil { -- return fmt.Errorf("Could not convert position %v for %q", d.Range, d.Message) +- // Invalidated package information. +- for id, invalidateMetadata := range idsToInvalidate { +- if _, ok := directIDs[id]; ok || invalidateMetadata { +- if result.packages.Delete(id) { +- needsDiagnosis = true - } -- fmt.Printf("%v: %v\n", spn, d.Message) +- } else { +- if entry, hit := result.packages.Get(id); hit { +- needsDiagnosis = true +- ph := entry.clone(false) +- result.packages.Set(id, ph, nil) +- } +- } +- if result.activePackages.Delete(id) { +- needsDiagnosis = true - } - } -- return nil --} -diff -urN a/gopls/internal/lsp/cmd/cmd.go b/gopls/internal/lsp/cmd/cmd.go ---- a/gopls/internal/lsp/cmd/cmd.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/cmd.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,801 +0,0 @@ --// Copyright 2018 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --// Package cmd handles the gopls command line. --// It contains a handler for each of the modes, along with all the flag handling --// and the command line output format. --package cmd +- // Compute which metadata updates are required. We only need to invalidate +- // packages directly containing the affected file, and only if it changed in +- // a relevant way. +- metadataUpdates := make(map[PackageID]*metadata.Package) +- for id, mp := range s.meta.Packages { +- invalidateMetadata := idsToInvalidate[id] - --import ( -- "context" -- "flag" -- "fmt" -- "log" -- "os" -- "reflect" -- "sort" -- "strings" -- "sync" -- "text/tabwriter" -- "time" +- // For metadata that has been newly invalidated, capture package paths +- // requiring reloading in the shouldLoad map. +- if invalidateMetadata && !metadata.IsCommandLineArguments(mp.ID) { +- needsReload := []PackagePath{mp.PkgPath} +- if mp.ForTest != "" && mp.ForTest != mp.PkgPath { +- // When reloading test variants, always reload their ForTest package as +- // well. Otherwise, we may miss test variants in the resulting load. +- // +- // TODO(rfindley): is this actually sufficient? Is it possible that +- // other test variants may be invalidated? Either way, we should +- // determine exactly what needs to be reloaded here. +- needsReload = append(needsReload, mp.ForTest) +- } +- result.shouldLoad.Set(id, needsReload, nil) +- } - -- "golang.org/x/tools/gopls/internal/lsp" -- "golang.org/x/tools/gopls/internal/lsp/browser" -- "golang.org/x/tools/gopls/internal/lsp/cache" -- "golang.org/x/tools/gopls/internal/lsp/debug" -- "golang.org/x/tools/gopls/internal/lsp/filecache" -- "golang.org/x/tools/gopls/internal/lsp/lsprpc" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/diff" -- "golang.org/x/tools/internal/jsonrpc2" -- "golang.org/x/tools/internal/tool" -- "golang.org/x/tools/internal/xcontext" --) +- // Check whether the metadata should be deleted. +- if invalidateMetadata { +- needsDiagnosis = true +- metadataUpdates[id] = nil +- continue +- } +- } - --// Application is the main application as passed to tool.Main --// It handles the main command line parsing and dispatch to the sub commands. --type Application struct { -- // Core application flags +- // Update metadata, if necessary. +- result.meta = s.meta.Update(metadataUpdates) - -- // Embed the basic profiling flags supported by the tool package -- tool.Profile +- // Update workspace and active packages, if necessary. +- if result.meta != s.meta || anyFileOpenedOrClosed { +- needsDiagnosis = true +- result.workspacePackages = computeWorkspacePackagesLocked(ctx, result, result.meta) +- result.resetActivePackagesLocked() +- } else { +- result.workspacePackages = s.workspacePackages +- } - -- // We include the server configuration directly for now, so the flags work -- // even without the verb. -- // TODO: Remove this when we stop allowing the serve verb by default. -- Serve Serve +- return result, needsDiagnosis +-} - -- // the options configuring function to invoke when building a server -- options func(*source.Options) +-// cloneWithout clones m then deletes from it the keys of changes. +-// +-// The optional didDelete variable is set to true if there were deletions. +-func cloneWithout[K constraints.Ordered, V1, V2 any](m *persistent.Map[K, V1], changes map[K]V2, didDelete *bool) *persistent.Map[K, V1] { +- m2 := m.Clone() +- for k := range changes { +- if m2.Delete(k) && didDelete != nil { +- *didDelete = true +- } +- } +- return m2 +-} - -- // The name of the binary, used in help and telemetry. -- name string +-// cloneWith clones m then inserts the changes into it. +-func cloneWith[K constraints.Ordered, V any](m *persistent.Map[K, V], changes map[K]V) *persistent.Map[K, V] { +- m2 := m.Clone() +- for k, v := range changes { +- m2.Set(k, v, nil) +- } +- return m2 +-} - -- // The working directory to run commands in. -- wd string +-// deleteMostRelevantModFile deletes the mod file most likely to be the mod +-// file for the changed URI, if it exists. +-// +-// Specifically, this is the longest mod file path in a directory containing +-// changed. This might not be accurate if there is another mod file closer to +-// changed that happens not to be present in the map, but that's OK: the goal +-// of this function is to guarantee that IF the nearest mod file is present in +-// the map, it is invalidated. +-func deleteMostRelevantModFile(m *persistent.Map[protocol.DocumentURI, *memoize.Promise], changed protocol.DocumentURI) { +- var mostRelevant protocol.DocumentURI +- changedFile := changed.Path() - -- // The environment variables to use. -- env []string +- m.Range(func(modURI protocol.DocumentURI, _ *memoize.Promise) { +- if len(modURI) > len(mostRelevant) { +- if pathutil.InDir(filepath.Dir(modURI.Path()), changedFile) { +- mostRelevant = modURI +- } +- } +- }) +- if mostRelevant != "" { +- m.Delete(mostRelevant) +- } +-} - -- // Support for remote LSP server. -- Remote string `flag:"remote" help:"forward all commands to a remote lsp specified by this flag. With no special prefix, this is assumed to be a TCP address. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. If 'auto', or prefixed by 'auto;', the remote address is automatically resolved based on the executing environment."` +-// invalidatedPackageIDs returns all packages invalidated by a change to uri. +-// If we haven't seen this URI before, we guess based on files in the same +-// directory. This is of course incorrect in build systems where packages are +-// not organized by directory. +-// +-// If packageFileChanged is set, the file is either a new file, or has a new +-// package name. In this case, all known packages in the directory will be +-// invalidated. +-func invalidatedPackageIDs(uri protocol.DocumentURI, known map[protocol.DocumentURI][]PackageID, packageFileChanged bool) map[PackageID]struct{} { +- invalidated := make(map[PackageID]struct{}) - -- // Verbose enables verbose logging. -- Verbose bool `flag:"v,verbose" help:"verbose output"` +- // At a minimum, we invalidate packages known to contain uri. +- for _, id := range known[uri] { +- invalidated[id] = struct{}{} +- } - -- // VeryVerbose enables a higher level of verbosity in logging output. -- VeryVerbose bool `flag:"vv,veryverbose" help:"very verbose output"` +- // If the file didn't move to a new package, we should only invalidate the +- // packages it is currently contained inside. +- if !packageFileChanged && len(invalidated) > 0 { +- return invalidated +- } - -- // Control ocagent export of telemetry -- OCAgent string `flag:"ocagent" help:"the address of the ocagent (e.g. http://localhost:55678), or off"` +- // This is a file we don't yet know about, or which has moved packages. Guess +- // relevant packages by considering files in the same directory. - -- // PrepareOptions is called to update the options when a new view is built. -- // It is primarily to allow the behavior of gopls to be modified by hooks. -- PrepareOptions func(*source.Options) +- // Cache of FileInfo to avoid unnecessary stats for multiple files in the +- // same directory. +- stats := make(map[string]struct { +- os.FileInfo +- error +- }) +- getInfo := func(dir string) (os.FileInfo, error) { +- if res, ok := stats[dir]; ok { +- return res.FileInfo, res.error +- } +- fi, err := os.Stat(dir) +- stats[dir] = struct { +- os.FileInfo +- error +- }{fi, err} +- return fi, err +- } +- dir := filepath.Dir(uri.Path()) +- fi, err := getInfo(dir) +- if err == nil { +- // Aggregate all possibly relevant package IDs. +- for knownURI, ids := range known { +- knownDir := filepath.Dir(knownURI.Path()) +- knownFI, err := getInfo(knownDir) +- if err != nil { +- continue +- } +- if os.SameFile(fi, knownFI) { +- for _, id := range ids { +- invalidated[id] = struct{}{} +- } +- } +- } +- } +- return invalidated +-} - -- // editFlags holds flags that control how file edit operations -- // are applied, in particular when the server makes an ApplyEdits -- // downcall to the client. Present only for commands that apply edits. -- editFlags *EditFlags +-// fileWasSaved reports whether the FileHandle passed in has been saved. It +-// accomplishes this by checking to see if the original and current FileHandles +-// are both overlays, and if the current FileHandle is saved while the original +-// FileHandle was not saved. +-func fileWasSaved(originalFH, currentFH file.Handle) bool { +- c, ok := currentFH.(*overlay) +- if !ok || c == nil { +- return true +- } +- o, ok := originalFH.(*overlay) +- if !ok || o == nil { +- return c.saved +- } +- return !o.saved && c.saved -} - --// EditFlags defines flags common to {fix,format,imports,rename} --// that control how edits are applied to the client's files. +-// metadataChanges detects features of the change from oldFH->newFH that may +-// affect package metadata. -// --// The type is exported for flag reflection. +-// It uses lockedSnapshot to access cached parse information. lockedSnapshot +-// must be locked. -// --// The -write, -diff, and -list flags are orthogonal but any --// of them suppresses the default behavior, which is to print --// the edited file contents. --type EditFlags struct { -- Write bool `flag:"w,write" help:"write edited content to source files"` -- Preserve bool `flag:"preserve" help:"with -write, make copies of original files"` -- Diff bool `flag:"d,diff" help:"display diffs instead of edited file content"` -- List bool `flag:"l,list" help:"display names of edited files"` --} -- --func (app *Application) verbose() bool { -- return app.Verbose || app.VeryVerbose --} -- --// New returns a new Application ready to run. --func New(name, wd string, env []string, options func(*source.Options)) *Application { -- if wd == "" { -- wd, _ = os.Getwd() +-// The result parameters have the following meaning: +-// - invalidate means that package metadata for packages containing the file +-// should be invalidated. +-// - pkgFileChanged means that the file->package associates for the file have +-// changed (possibly because the file is new, or because its package name has +-// changed). +-// - importDeleted means that an import has been deleted, or we can't +-// determine if an import was deleted due to errors. +-func metadataChanges(ctx context.Context, lockedSnapshot *Snapshot, oldFH, newFH file.Handle) (invalidate, pkgFileChanged, importDeleted bool) { +- if oe, ne := oldFH != nil && fileExists(oldFH), fileExists(newFH); !oe || !ne { // existential changes +- changed := oe != ne +- return changed, changed, !ne // we don't know if an import was deleted - } -- app := &Application{ -- options: options, -- name: name, -- wd: wd, -- env: env, -- OCAgent: "off", //TODO: Remove this line to default the exporter to on - -- Serve: Serve{ -- RemoteListenTimeout: 1 * time.Minute, -- }, +- // If the file hasn't changed, there's no need to reload. +- if oldFH.Identity() == newFH.Identity() { +- return false, false, false - } -- app.Serve.app = app -- return app --} - --// Name implements tool.Application returning the binary name. --func (app *Application) Name() string { return app.name } -- --// Usage implements tool.Application returning empty extra argument usage. --func (app *Application) Usage() string { return "" } +- fset := token.NewFileSet() +- // Parse headers to compare package names and imports. +- oldHeads, oldErr := lockedSnapshot.view.parseCache.parseFiles(ctx, fset, parsego.Header, false, oldFH) +- newHeads, newErr := lockedSnapshot.view.parseCache.parseFiles(ctx, fset, parsego.Header, false, newFH) - --// ShortHelp implements tool.Application returning the main binary help. --func (app *Application) ShortHelp() string { -- return "" --} +- if oldErr != nil || newErr != nil { +- errChanged := (oldErr == nil) != (newErr == nil) +- return errChanged, errChanged, (newErr != nil) // we don't know if an import was deleted +- } - --// DetailedHelp implements tool.Application returning the main binary help. --// This includes the short help for all the sub commands. --func (app *Application) DetailedHelp(f *flag.FlagSet) { -- w := tabwriter.NewWriter(f.Output(), 0, 0, 2, ' ', 0) -- defer w.Flush() +- oldHead := oldHeads[0] +- newHead := newHeads[0] - -- fmt.Fprint(w, ` --gopls is a Go language server. +- // `go list` fails completely if the file header cannot be parsed. If we go +- // from a non-parsing state to a parsing state, we should reload. +- if oldHead.ParseErr != nil && newHead.ParseErr == nil { +- return true, true, true // We don't know what changed, so fall back on full invalidation. +- } - --It is typically used with an editor to provide language features. When no --command is specified, gopls will default to the 'serve' command. The language --features can also be accessed via the gopls command-line interface. +- // If a package name has changed, the set of package imports may have changed +- // in ways we can't detect here. Assume an import has been deleted. +- if oldHead.File.Name.Name != newHead.File.Name.Name { +- return true, true, true +- } - --Usage: -- gopls help [] +- // Check whether package imports have changed. Only consider potentially +- // valid imports paths. +- oldImports := validImports(oldHead.File.Imports) +- newImports := validImports(newHead.File.Imports) - --Command: --`) -- fmt.Fprint(w, "\nMain\t\n") -- for _, c := range app.mainCommands() { -- fmt.Fprintf(w, " %s\t%s\n", c.Name(), c.ShortHelp()) -- } -- fmt.Fprint(w, "\t\nFeatures\t\n") -- for _, c := range app.featureCommands() { -- fmt.Fprintf(w, " %s\t%s\n", c.Name(), c.ShortHelp()) -- } -- if app.verbose() { -- fmt.Fprint(w, "\t\nInternal Use Only\t\n") -- for _, c := range app.internalCommands() { -- fmt.Fprintf(w, " %s\t%s\n", c.Name(), c.ShortHelp()) +- for path := range newImports { +- if _, ok := oldImports[path]; ok { +- delete(oldImports, path) +- } else { +- invalidate = true // a new, potentially valid import was added - } - } -- fmt.Fprint(w, "\nflags:\n") -- printFlagDefaults(f) --} - --// this is a slightly modified version of flag.PrintDefaults to give us control --func printFlagDefaults(s *flag.FlagSet) { -- var flags [][]*flag.Flag -- seen := map[flag.Value]int{} -- s.VisitAll(func(f *flag.Flag) { -- if i, ok := seen[f.Value]; !ok { -- seen[f.Value] = len(flags) -- flags = append(flags, []*flag.Flag{f}) -- } else { -- flags[i] = append(flags[i], f) -- } -- }) -- for _, entry := range flags { -- sort.SliceStable(entry, func(i, j int) bool { -- return len(entry[i].Name) < len(entry[j].Name) -- }) -- var b strings.Builder -- for i, f := range entry { -- switch i { -- case 0: -- b.WriteString(" -") -- default: -- b.WriteString(",-") -- } -- b.WriteString(f.Name) -- } +- if len(oldImports) > 0 { +- invalidate = true +- importDeleted = true +- } - -- f := entry[0] -- name, usage := flag.UnquoteUsage(f) -- if len(name) > 0 { -- b.WriteString("=") -- b.WriteString(name) -- } -- // Boolean flags of one ASCII letter are so common we -- // treat them specially, putting their usage on the same line. -- if b.Len() <= 4 { // space, space, '-', 'x'. -- b.WriteString("\t") +- // If the change does not otherwise invalidate metadata, get the full ASTs in +- // order to check magic comments. +- // +- // Note: if this affects performance we can probably avoid parsing in the +- // common case by first scanning the source for potential comments. +- if !invalidate { +- origFulls, oldErr := lockedSnapshot.view.parseCache.parseFiles(ctx, fset, parsego.Full, false, oldFH) +- newFulls, newErr := lockedSnapshot.view.parseCache.parseFiles(ctx, fset, parsego.Full, false, newFH) +- if oldErr == nil && newErr == nil { +- invalidate = magicCommentsChanged(origFulls[0].File, newFulls[0].File) - } else { -- // Four spaces before the tab triggers good alignment -- // for both 4- and 8-space tab stops. -- b.WriteString("\n \t") -- } -- b.WriteString(strings.ReplaceAll(usage, "\n", "\n \t")) -- if !isZeroValue(f, f.DefValue) { -- if reflect.TypeOf(f.Value).Elem().Name() == "stringValue" { -- fmt.Fprintf(&b, " (default %q)", f.DefValue) -- } else { -- fmt.Fprintf(&b, " (default %v)", f.DefValue) -- } +- // At this point, we shouldn't ever fail to produce a parsego.File, as +- // we're already past header parsing. +- bug.Reportf("metadataChanges: unparseable file %v (old error: %v, new error: %v)", oldFH.URI(), oldErr, newErr) - } -- fmt.Fprint(s.Output(), b.String(), "\n") - } --} - --// isZeroValue is copied from the flags package --func isZeroValue(f *flag.Flag, value string) bool { -- // Build a zero value of the flag's Value type, and see if the -- // result of calling its String method equals the value passed in. -- // This works unless the Value type is itself an interface type. -- typ := reflect.TypeOf(f.Value) -- var z reflect.Value -- if typ.Kind() == reflect.Ptr { -- z = reflect.New(typ.Elem()) -- } else { -- z = reflect.Zero(typ) -- } -- return value == z.Interface().(flag.Value).String() +- return invalidate, pkgFileChanged, importDeleted -} - --// Run takes the args after top level flag processing, and invokes the correct --// sub command as specified by the first argument. --// If no arguments are passed it will invoke the server sub command, as a --// temporary measure for compatibility. --func (app *Application) Run(ctx context.Context, args ...string) error { -- // In the category of "things we can do while waiting for the Go command": -- // Pre-initialize the filecache, which takes ~50ms to hash the gopls -- // executable, and immediately runs a gc. -- filecache.Start() -- -- ctx = debug.WithInstance(ctx, app.wd, app.OCAgent) -- if len(args) == 0 { -- s := flag.NewFlagSet(app.Name(), flag.ExitOnError) -- return tool.Run(ctx, s, &app.Serve, args) +-func magicCommentsChanged(original *ast.File, current *ast.File) bool { +- oldComments := extractMagicComments(original) +- newComments := extractMagicComments(current) +- if len(oldComments) != len(newComments) { +- return true - } -- command, args := args[0], args[1:] -- for _, c := range app.Commands() { -- if c.Name() == command { -- s := flag.NewFlagSet(app.Name(), flag.ExitOnError) -- return tool.Run(ctx, s, c, args) +- for i := range oldComments { +- if oldComments[i] != newComments[i] { +- return true - } - } -- return tool.CommandLineErrorf("Unknown command %v", command) --} -- --// Commands returns the set of commands supported by the gopls tool on the --// command line. --// The command is specified by the first non flag argument. --func (app *Application) Commands() []tool.Application { -- var commands []tool.Application -- commands = append(commands, app.mainCommands()...) -- commands = append(commands, app.featureCommands()...) -- commands = append(commands, app.internalCommands()...) -- return commands +- return false -} - --func (app *Application) mainCommands() []tool.Application { -- return []tool.Application{ -- &app.Serve, -- &version{app: app}, -- &bug{app: app}, -- &help{app: app}, -- &apiJSON{app: app}, -- &licenses{app: app}, +-// validImports extracts the set of valid import paths from imports. +-func validImports(imports []*ast.ImportSpec) map[string]struct{} { +- m := make(map[string]struct{}) +- for _, spec := range imports { +- if path := spec.Path.Value; validImportPath(path) { +- m[path] = struct{}{} +- } - } +- return m -} - --func (app *Application) internalCommands() []tool.Application { -- return []tool.Application{ -- &vulncheck{app: app}, +-func validImportPath(path string) bool { +- path, err := strconv.Unquote(path) +- if err != nil { +- return false +- } +- if path == "" { +- return false +- } +- if path[len(path)-1] == '/' { +- return false - } +- return true -} - --func (app *Application) featureCommands() []tool.Application { -- return []tool.Application{ -- &callHierarchy{app: app}, -- &check{app: app}, -- &definition{app: app}, -- &foldingRanges{app: app}, -- &format{app: app}, -- &highlight{app: app}, -- &implementation{app: app}, -- &imports{app: app}, -- newRemote(app, ""), -- newRemote(app, "inspect"), -- &links{app: app}, -- &prepareRename{app: app}, -- &references{app: app}, -- &rename{app: app}, -- &semtok{app: app}, -- &signature{app: app}, -- &stats{app: app}, -- &suggestedFix{app: app}, -- &symbols{app: app}, +-var buildConstraintOrEmbedRe = regexp.MustCompile(`^//(go:embed|go:build|\s*\+build).*`) - -- &workspaceSymbol{app: app}, +-// extractMagicComments finds magic comments that affect metadata in f. +-func extractMagicComments(f *ast.File) []string { +- var results []string +- for _, cg := range f.Comments { +- for _, c := range cg.List { +- if buildConstraintOrEmbedRe.MatchString(c.Text) { +- results = append(results, c.Text) +- } +- } - } +- return results -} - --var ( -- internalMu sync.Mutex -- internalConnections = make(map[string]*connection) --) +-// BuiltinFile returns information about the special builtin package. +-func (s *Snapshot) BuiltinFile(ctx context.Context) (*parsego.File, error) { +- s.AwaitInitialized(ctx) - --// connect creates and initializes a new in-process gopls session. --// --// If onProgress is set, it is called for each new progress notification. --func (app *Application) connect(ctx context.Context, onProgress func(*protocol.ProgressParams)) (*connection, error) { -- switch { -- case app.Remote == "": -- client := newClient(app, onProgress) -- options := source.DefaultOptions(app.options) -- server := lsp.NewServer(cache.NewSession(ctx, cache.New(nil)), client, options) -- conn := newConnection(server, client) -- if err := conn.initialize(protocol.WithClient(ctx, client), app.options); err != nil { -- return nil, err -- } -- return conn, nil +- s.mu.Lock() +- builtin := s.builtin +- s.mu.Unlock() - -- case strings.HasPrefix(app.Remote, "internal@"): -- internalMu.Lock() -- defer internalMu.Unlock() -- opts := source.DefaultOptions(app.options) -- key := fmt.Sprintf("%s %v %v %v", app.wd, opts.PreferredContentFormat, opts.HierarchicalDocumentSymbolSupport, opts.SymbolMatcher) -- if c := internalConnections[key]; c != nil { -- return c, nil -- } -- remote := app.Remote[len("internal@"):] -- ctx := xcontext.Detach(ctx) //TODO:a way of shutting down the internal server -- connection, err := app.connectRemote(ctx, remote) -- if err != nil { -- return nil, err -- } -- internalConnections[key] = connection -- return connection, nil -- default: -- return app.connectRemote(ctx, app.Remote) +- if builtin == "" { +- return nil, fmt.Errorf("no builtin package for view %s", s.view.folder.Name) - } --} - --func (app *Application) connectRemote(ctx context.Context, remote string) (*connection, error) { -- conn, err := lsprpc.ConnectToRemote(ctx, remote) +- fh, err := s.ReadFile(ctx, builtin) - if err != nil { - return nil, err - } -- stream := jsonrpc2.NewHeaderStream(conn) -- cc := jsonrpc2.NewConn(stream) -- server := protocol.ServerDispatcher(cc) -- client := newClient(app, nil) -- connection := newConnection(server, client) -- ctx = protocol.WithClient(ctx, connection.client) -- cc.Go(ctx, -- protocol.Handlers( -- protocol.ClientHandler(client, jsonrpc2.MethodNotFound))) -- return connection, connection.initialize(ctx, app.options) +- // For the builtin file only, we need syntactic object resolution +- // (since we can't type check). +- mode := parsego.Full &^ parser.SkipObjectResolution +- pgfs, err := s.view.parseCache.parseFiles(ctx, token.NewFileSet(), mode, false, fh) +- if err != nil { +- return nil, err +- } +- return pgfs[0], nil -} - --var matcherString = map[source.SymbolMatcher]string{ -- source.SymbolFuzzy: "fuzzy", -- source.SymbolCaseSensitive: "caseSensitive", -- source.SymbolCaseInsensitive: "caseInsensitive", +-// IsBuiltin reports whether uri is part of the builtin package. +-func (s *Snapshot) IsBuiltin(uri protocol.DocumentURI) bool { +- s.mu.Lock() +- defer s.mu.Unlock() +- // We should always get the builtin URI in a canonical form, so use simple +- // string comparison here. span.CompareURI is too expensive. +- return uri == s.builtin -} - --func (c *connection) initialize(ctx context.Context, options func(*source.Options)) error { -- params := &protocol.ParamInitialize{} -- params.RootURI = protocol.URIFromPath(c.client.app.wd) -- params.Capabilities.Workspace.Configuration = true -- -- // Make sure to respect configured options when sending initialize request. -- opts := source.DefaultOptions(options) -- // If you add an additional option here, you must update the map key in connect. -- params.Capabilities.TextDocument.Hover = &protocol.HoverClientCapabilities{ -- ContentFormat: []protocol.MarkupKind{opts.PreferredContentFormat}, -- } -- params.Capabilities.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport = opts.HierarchicalDocumentSymbolSupport -- params.Capabilities.TextDocument.SemanticTokens = protocol.SemanticTokensClientCapabilities{} -- params.Capabilities.TextDocument.SemanticTokens.Formats = []protocol.TokenFormat{"relative"} -- params.Capabilities.TextDocument.SemanticTokens.Requests.Range.Value = true -- params.Capabilities.TextDocument.SemanticTokens.Requests.Full.Value = true -- params.Capabilities.TextDocument.SemanticTokens.TokenTypes = lsp.SemanticTypes() -- params.Capabilities.TextDocument.SemanticTokens.TokenModifiers = lsp.SemanticModifiers() -- -- // If the subcommand has registered a progress handler, report the progress -- // capability. -- if c.client.onProgress != nil { -- params.Capabilities.Window.WorkDoneProgress = true -- } +-func (s *Snapshot) setBuiltin(path string) { +- s.mu.Lock() +- defer s.mu.Unlock() - -- params.InitializationOptions = map[string]interface{}{ -- "symbolMatcher": matcherString[opts.SymbolMatcher], -- } -- if _, err := c.Server.Initialize(ctx, params); err != nil { -- return err -- } -- if err := c.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil { -- return err -- } -- return nil +- s.builtin = protocol.URIFromPath(path) -} - --type connection struct { -- protocol.Server -- client *cmdClient +-// WantGCDetails reports whether to compute GC optimization details for the +-// specified package. +-func (s *Snapshot) WantGCDetails(id metadata.PackageID) bool { +- _, ok := s.gcOptimizationDetails[id] +- return ok -} +diff -urN a/gopls/internal/cache/symbols.go b/gopls/internal/cache/symbols.go +--- a/gopls/internal/cache/symbols.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/symbols.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,201 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// cmdClient defines the protocol.Client interface behavior of the gopls CLI tool. --type cmdClient struct { -- app *Application -- onProgress func(*protocol.ProgressParams) +-package cache - -- diagnosticsMu sync.Mutex -- diagnosticsDone chan struct{} +-import ( +- "context" +- "go/ast" +- "go/token" +- "go/types" +- "strings" - -- filesMu sync.Mutex // guards files map and each cmdFile.diagnostics -- files map[span.URI]*cmdFile --} +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/astutil" +-) - --type cmdFile struct { -- uri span.URI -- mapper *protocol.Mapper -- err error -- diagnostics []protocol.Diagnostic +-// Symbol holds a precomputed symbol value. Note: we avoid using the +-// protocol.SymbolInformation struct here in order to reduce the size of each +-// symbol. +-type Symbol struct { +- Name string +- Kind protocol.SymbolKind +- Range protocol.Range -} - --func newClient(app *Application, onProgress func(*protocol.ProgressParams)) *cmdClient { -- return &cmdClient{ -- app: app, -- onProgress: onProgress, -- files: make(map[span.URI]*cmdFile), -- } --} +-// symbolize returns the result of symbolizing the file identified by uri, using a cache. +-func (s *Snapshot) symbolize(ctx context.Context, uri protocol.DocumentURI) ([]Symbol, error) { - --func newConnection(server protocol.Server, client *cmdClient) *connection { -- return &connection{ -- Server: server, -- client: client, -- } --} +- s.mu.Lock() +- entry, hit := s.symbolizeHandles.Get(uri) +- s.mu.Unlock() - --// fileURI converts a DocumentURI to a file:// span.URI, panicking if it's not a file. --func fileURI(uri protocol.DocumentURI) span.URI { -- sURI := uri.SpanURI() -- if !sURI.IsFile() { -- panic(fmt.Sprintf("%q is not a file URI", uri)) +- type symbolizeResult struct { +- symbols []Symbol +- err error - } -- return sURI --} - --func (c *cmdClient) CodeLensRefresh(context.Context) error { return nil } +- // Cache miss? +- if !hit { +- fh, err := s.ReadFile(ctx, uri) +- if err != nil { +- return nil, err +- } +- type symbolHandleKey file.Hash +- key := symbolHandleKey(fh.Identity().Hash) +- promise, release := s.store.Promise(key, func(ctx context.Context, arg interface{}) interface{} { +- symbols, err := symbolizeImpl(ctx, arg.(*Snapshot), fh) +- return symbolizeResult{symbols, err} +- }) - --func (c *cmdClient) LogTrace(context.Context, *protocol.LogTraceParams) error { return nil } +- entry = promise - --func (c *cmdClient) ShowMessage(ctx context.Context, p *protocol.ShowMessageParams) error { return nil } +- s.mu.Lock() +- s.symbolizeHandles.Set(uri, entry, func(_, _ interface{}) { release() }) +- s.mu.Unlock() +- } - --func (c *cmdClient) ShowMessageRequest(ctx context.Context, p *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error) { -- return nil, nil +- // Await result. +- v, err := s.awaitPromise(ctx, entry) +- if err != nil { +- return nil, err +- } +- res := v.(symbolizeResult) +- return res.symbols, res.err -} - --func (c *cmdClient) LogMessage(ctx context.Context, p *protocol.LogMessageParams) error { -- switch p.Type { -- case protocol.Error: -- log.Print("Error:", p.Message) -- case protocol.Warning: -- log.Print("Warning:", p.Message) -- case protocol.Info: -- if c.app.verbose() { -- log.Print("Info:", p.Message) -- } -- case protocol.Log: -- if c.app.verbose() { -- log.Print("Log:", p.Message) -- } -- default: -- if c.app.verbose() { -- log.Print(p.Message) -- } +-// symbolizeImpl reads and parses a file and extracts symbols from it. +-func symbolizeImpl(ctx context.Context, snapshot *Snapshot, fh file.Handle) ([]Symbol, error) { +- pgfs, err := snapshot.view.parseCache.parseFiles(ctx, token.NewFileSet(), parsego.Full, false, fh) +- if err != nil { +- return nil, err - } -- return nil --} - --func (c *cmdClient) Event(ctx context.Context, t *interface{}) error { return nil } +- w := &symbolWalker{ +- tokFile: pgfs[0].Tok, +- mapper: pgfs[0].Mapper, +- } +- w.fileDecls(pgfs[0].File.Decls) - --func (c *cmdClient) RegisterCapability(ctx context.Context, p *protocol.RegistrationParams) error { -- return nil +- return w.symbols, w.firstError -} - --func (c *cmdClient) UnregisterCapability(ctx context.Context, p *protocol.UnregistrationParams) error { -- return nil --} +-type symbolWalker struct { +- // for computing positions +- tokFile *token.File +- mapper *protocol.Mapper - --func (c *cmdClient) WorkspaceFolders(ctx context.Context) ([]protocol.WorkspaceFolder, error) { -- return nil, nil +- symbols []Symbol +- firstError error -} - --func (c *cmdClient) Configuration(ctx context.Context, p *protocol.ParamConfiguration) ([]interface{}, error) { -- results := make([]interface{}, len(p.Items)) -- for i, item := range p.Items { -- if item.Section != "gopls" { -- continue -- } -- env := map[string]interface{}{} -- for _, value := range c.app.env { -- l := strings.SplitN(value, "=", 2) -- if len(l) != 2 { -- continue -- } -- env[l[0]] = l[1] -- } -- m := map[string]interface{}{ -- "env": env, -- "analyses": map[string]bool{ -- "fillreturns": true, -- "nonewvars": true, -- "noresultvalues": true, -- "undeclaredname": true, -- }, -- } -- if c.app.VeryVerbose { -- m["verboseOutput"] = true +-func (w *symbolWalker) atNode(node ast.Node, name string, kind protocol.SymbolKind, path ...*ast.Ident) { +- var b strings.Builder +- for _, ident := range path { +- if ident != nil { +- b.WriteString(ident.Name) +- b.WriteString(".") - } -- results[i] = m -- } -- return results, nil --} -- --func (c *cmdClient) ApplyEdit(ctx context.Context, p *protocol.ApplyWorkspaceEditParams) (*protocol.ApplyWorkspaceEditResult, error) { -- if err := c.applyWorkspaceEdit(&p.Edit); err != nil { -- return &protocol.ApplyWorkspaceEditResult{FailureReason: err.Error()}, nil - } -- return &protocol.ApplyWorkspaceEditResult{Applied: true}, nil --} +- b.WriteString(name) - --// applyWorkspaceEdit applies a complete WorkspaceEdit to the client's --// files, honoring the preferred edit mode specified by cli.app.editMode. --// (Used by rename and by ApplyEdit downcalls.) --func (cli *cmdClient) applyWorkspaceEdit(edit *protocol.WorkspaceEdit) error { -- var orderedURIs []string -- edits := map[span.URI][]protocol.TextEdit{} -- for _, c := range edit.DocumentChanges { -- if c.TextDocumentEdit != nil { -- uri := fileURI(c.TextDocumentEdit.TextDocument.URI) -- edits[uri] = append(edits[uri], c.TextDocumentEdit.Edits...) -- orderedURIs = append(orderedURIs, string(uri)) -- } -- if c.RenameFile != nil { -- return fmt.Errorf("client does not support file renaming (%s -> %s)", -- c.RenameFile.OldURI, -- c.RenameFile.NewURI) -- } +- rng, err := w.mapper.NodeRange(w.tokFile, node) +- if err != nil { +- w.error(err) +- return - } -- sort.Strings(orderedURIs) -- for _, u := range orderedURIs { -- uri := span.URIFromURI(u) -- f := cli.openFile(uri) -- if f.err != nil { -- return f.err -- } -- if err := applyTextEdits(f.mapper, edits[uri], cli.app.editFlags); err != nil { -- return err -- } +- sym := Symbol{ +- Name: b.String(), +- Kind: kind, +- Range: rng, - } -- return nil +- w.symbols = append(w.symbols, sym) -} - --// applyTextEdits applies a list of edits to the mapper file content, --// using the preferred edit mode. It is a no-op if there are no edits. --func applyTextEdits(mapper *protocol.Mapper, edits []protocol.TextEdit, flags *EditFlags) error { -- if len(edits) == 0 { -- return nil -- } -- newContent, renameEdits, err := source.ApplyProtocolEdits(mapper, edits) -- if err != nil { -- return err -- } -- -- filename := mapper.URI.Filename() -- -- if flags.List { -- fmt.Println(filename) +-func (w *symbolWalker) error(err error) { +- if err != nil && w.firstError == nil { +- w.firstError = err - } +-} - -- if flags.Write { -- if flags.Preserve { -- if err := os.Rename(filename, filename+".orig"); err != nil { -- return err +-func (w *symbolWalker) fileDecls(decls []ast.Decl) { +- for _, decl := range decls { +- switch decl := decl.(type) { +- case *ast.FuncDecl: +- kind := protocol.Function +- var recv *ast.Ident +- if decl.Recv.NumFields() > 0 { +- kind = protocol.Method +- _, recv, _ = astutil.UnpackRecv(decl.Recv.List[0].Type) +- } +- w.atNode(decl.Name, decl.Name.Name, kind, recv) +- case *ast.GenDecl: +- for _, spec := range decl.Specs { +- switch spec := spec.(type) { +- case *ast.TypeSpec: +- kind := guessKind(spec) +- w.atNode(spec.Name, spec.Name.Name, kind) +- w.walkType(spec.Type, spec.Name) +- case *ast.ValueSpec: +- for _, name := range spec.Names { +- kind := protocol.Variable +- if decl.Tok == token.CONST { +- kind = protocol.Constant +- } +- w.atNode(name, name.Name, kind) +- } +- } - } - } -- if err := os.WriteFile(filename, newContent, 0644); err != nil { -- return err -- } -- } -- -- if flags.Diff { -- unified, err := diff.ToUnified(filename+".orig", filename, string(mapper.Content), renameEdits, diff.DefaultContextLines) -- if err != nil { -- return err -- } -- fmt.Print(unified) -- } -- -- // No flags: just print edited file content. -- // TODO(adonovan): how is this ever useful with multiple files? -- if !(flags.List || flags.Write || flags.Diff) { -- os.Stdout.Write(newContent) - } -- -- return nil -} - --func (c *cmdClient) PublishDiagnostics(ctx context.Context, p *protocol.PublishDiagnosticsParams) error { -- if p.URI == "gopls://diagnostics-done" { -- close(c.diagnosticsDone) -- } -- // Don't worry about diagnostics without versions. -- if p.Version == 0 { -- return nil +-func guessKind(spec *ast.TypeSpec) protocol.SymbolKind { +- switch spec.Type.(type) { +- case *ast.InterfaceType: +- return protocol.Interface +- case *ast.StructType: +- return protocol.Struct +- case *ast.FuncType: +- return protocol.Function - } +- return protocol.Class +-} - -- c.filesMu.Lock() -- defer c.filesMu.Unlock() -- -- file := c.getFile(fileURI(p.URI)) -- file.diagnostics = append(file.diagnostics, p.Diagnostics...) -- -- // Perform a crude in-place deduplication. -- // TODO(golang/go#60122): replace the ad-hoc gopls/diagnoseFiles -- // non-standard request with support for textDocument/diagnostic, -- // so that we don't need to do this de-duplication. -- type key [6]interface{} -- seen := make(map[key]bool) -- out := file.diagnostics[:0] -- for _, d := range file.diagnostics { -- var codeHref string -- if desc := d.CodeDescription; desc != nil { -- codeHref = desc.Href +-// walkType processes symbols related to a type expression. path is path of +-// nested type identifiers to the type expression. +-func (w *symbolWalker) walkType(typ ast.Expr, path ...*ast.Ident) { +- switch st := typ.(type) { +- case *ast.StructType: +- for _, field := range st.Fields.List { +- w.walkField(field, protocol.Field, protocol.Field, path...) - } -- k := key{d.Range, d.Severity, d.Code, codeHref, d.Source, d.Message} -- if !seen[k] { -- seen[k] = true -- out = append(out, d) +- case *ast.InterfaceType: +- for _, field := range st.Methods.List { +- w.walkField(field, protocol.Interface, protocol.Method, path...) - } - } -- file.diagnostics = out -- -- return nil -} - --func (c *cmdClient) Progress(_ context.Context, params *protocol.ProgressParams) error { -- if c.onProgress != nil { -- c.onProgress(params) +-// walkField processes symbols related to the struct field or interface method. +-// +-// unnamedKind and namedKind are the symbol kinds if the field is resp. unnamed +-// or named. path is the path of nested identifiers containing the field. +-func (w *symbolWalker) walkField(field *ast.Field, unnamedKind, namedKind protocol.SymbolKind, path ...*ast.Ident) { +- if len(field.Names) == 0 { +- switch typ := field.Type.(type) { +- case *ast.SelectorExpr: +- // embedded qualified type +- w.atNode(field, typ.Sel.Name, unnamedKind, path...) +- default: +- w.atNode(field, types.ExprString(field.Type), unnamedKind, path...) +- } - } -- return nil --} -- --func (c *cmdClient) ShowDocument(ctx context.Context, params *protocol.ShowDocumentParams) (*protocol.ShowDocumentResult, error) { -- var success bool -- if params.External { -- // Open URI in external browser. -- success = browser.Open(string(params.URI)) -- } else { -- // Open file in editor, optionally taking focus and selecting a range. -- // (cmdClient has no editor. Should it fork+exec $EDITOR?) -- log.Printf("Server requested that client editor open %q (takeFocus=%t, selection=%+v)", -- params.URI, params.TakeFocus, params.Selection) -- success = true +- for _, name := range field.Names { +- w.atNode(name, name.Name, namedKind, path...) +- w.walkType(field.Type, append(path, name)...) - } -- return &protocol.ShowDocumentResult{Success: success}, nil -} +diff -urN a/gopls/internal/cache/typerefs/doc.go b/gopls/internal/cache/typerefs/doc.go +--- a/gopls/internal/cache/typerefs/doc.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/typerefs/doc.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,151 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func (c *cmdClient) WorkDoneProgressCreate(context.Context, *protocol.WorkDoneProgressCreateParams) error { -- return nil +-// Package typerefs extracts symbol-level reachability information +-// from the syntax of a Go package. +-// +-// # Background +-// +-// The goal of this analysis is to determine, for each package P, a nearly +-// minimal set of packages that could affect the type checking of P. This set +-// may contain false positives, but the smaller this set the better we can +-// invalidate and prune packages in gopls. +-// +-// More precisely, for each package P we define the set of "reachable" packages +-// from P as the set of packages that may affect the (deep) export data of the +-// direct dependencies of P. By this definition, the complement of this set +-// cannot affect any information derived from type checking P, such as +-// diagnostics, cross references, or method sets. Therefore we need not +-// invalidate any results for P when a package in the complement of this set +-// changes. +-// +-// # Computing references +-// +-// For a given declaration D, references are computed based on identifiers or +-// dotted identifiers referenced in the declaration of D, that may affect +-// the type of D. However, these references reflect only local knowledge of the +-// package and its dependency metadata, and do not depend on any analysis of +-// the dependencies themselves. This allows the reference information for +-// a package to be cached independent of all others. +-// +-// Specifically, if a referring identifier I appears in the declaration, we +-// record an edge from D to each object possibly referenced by I. We search for +-// references within type syntax, but do not actually type-check, so we can't +-// reliably determine whether an expression is a type or a term, or whether a +-// function is a builtin or generic. For example, the type of x in var x = +-// p.F(W) only depends on W if p.F is a builtin or generic function, which we +-// cannot know without type-checking package p. So we may over-approximate in +-// this way. +-// +-// - If I is declared in the current package, record a reference to its +-// declaration. +-// - Otherwise, if there are any dot imports in the current +-// file and I is exported, record a (possibly dangling) edge to +-// the corresponding declaration in each dot-imported package. +-// +-// If a dotted identifier q.I appears in the declaration, we +-// perform a similar operation: +-// +-// - If q is declared in the current package, we record a reference to that +-// object. It may be a var or const that has a field or method I. +-// - Otherwise, if q is a valid import name based on imports in the current file +-// and the provided metadata for dependency package names, record a +-// reference to the object I in that package. +-// - Additionally, handle the case where Q is exported, and Q.I may refer to +-// a field or method in a dot-imported package. +-// +-// That is essentially the entire algorithm, though there is some subtlety to +-// visiting the set of identifiers or dotted identifiers that may affect the +-// declaration type. See the visitDeclOrSpec function for the details of this +-// analysis. Notably, we also skip identifiers that refer to type parameters in +-// generic declarations. +-// +-// # Graph optimizations +-// +-// The references extracted from the syntax are used to construct +-// edges between nodes representing declarations. Edges are of two +-// kinds: internal references, from one package-level declaration to +-// another; and external references, from a symbol in this package to +-// a symbol imported from a direct dependency. +-// +-// Once the symbol reference graph is constructed, we find its +-// strongly connected components (SCCs) using Tarjan's algorithm. +-// As we coalesce the nodes of each SCC we compute the union of +-// external references reached by each package-level declaration. +-// The final result is the mapping from each exported package-level +-// declaration to the set of external (imported) declarations that it +-// reaches. +-// +-// Because it is common for many package members to have the same +-// reachability, the result takes the form of a set of equivalence +-// classes, each mapping a set of package-level declarations to a set +-// of external symbols. We use a hash table to canonicalize sets so that +-// repeated occurrences of the same set (which are common) are only +-// represented once in memory or in the file system. +-// For example, all declarations that ultimately reference only +-// {fmt.Println,strings.Join} would be classed as equivalent. +-// +-// This approach was inspired by the Hash-Value Numbering (HVN) +-// optimization described by Hardekopf and Lin. See +-// golang.org/x/tools/go/pointer/hvn.go for an implementation. (Like +-// pointer analysis, this problem is fundamentally one of graph +-// reachability.) The HVN algorithm takes the compression a step +-// further by preserving the topology of the SCC DAG, in which edges +-// represent "is a superset of" constraints. Redundant edges that +-// don't increase the solution can be deleted. We could apply the same +-// technique here to further reduce the worst-case size of the result, +-// but the current implementation seems adequate. +-// +-// # API +-// +-// The main entry point for this analysis is the [Encode] function, +-// which implements the analysis described above for one package, and +-// encodes the result as a binary message. +-// +-// The [Decode] function decodes the message into a usable form: a set +-// of equivalence classes. The decoder uses a shared [PackageIndex] to +-// enable more compact representations of sets of packages +-// ([PackageSet]) during the global reacahability computation. +-// +-// The [BuildPackageGraph] constructor implements a whole-graph analysis similar +-// to that which will be implemented by gopls, but for various reasons the +-// logic for this analysis will eventually live in the +-// [golang.org/x/tools/gopls/internal/cache] package. Nevertheless, +-// BuildPackageGraph and its test serve to verify the syntactic analysis, and +-// may serve as a proving ground for new optimizations of the whole-graph analysis. +-// +-// # Export data is insufficient +-// +-// At first it may seem that the simplest way to implement this analysis would +-// be to consider the types.Packages of the dependencies of P, for example +-// during export. After all, it makes sense that the type checked packages +-// themselves could describe their dependencies. However, this does not work as +-// type information does not describe certain syntactic relationships. +-// +-// For example, the following scenarios cause type information to miss +-// syntactic relationships: +-// +-// Named type forwarding: +-// +-// package a; type A b.B +-// package b; type B int +-// +-// Aliases: +-// +-// package a; func A(f b.B) +-// package b; type B = func() +-// +-// Initializers: +-// +-// package a; var A = b.B() +-// package b; func B() string { return "hi" } +-// +-// Use of the unsafe package: +-// +-// package a; type A [unsafe.Sizeof(B{})]int +-// package b; type B struct { f1, f2, f3 int } +-// +-// In all of these examples, types do not contain information about the edge +-// between the a.A and b.B declarations. +-package typerefs +diff -urN a/gopls/internal/cache/typerefs/packageset.go b/gopls/internal/cache/typerefs/packageset.go +--- a/gopls/internal/cache/typerefs/packageset.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/typerefs/packageset.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,148 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package typerefs +- +-import ( +- "fmt" +- "math/bits" +- "sort" +- "strings" +- "sync" +- +- "golang.org/x/tools/gopls/internal/cache/metadata" +-) +- +-// PackageIndex stores common data to enable efficient representation of +-// references and package sets. +-type PackageIndex struct { +- // For now, PackageIndex just indexes package ids, to save space and allow for +- // faster unions via sparse int vectors. +- mu sync.Mutex +- ids []metadata.PackageID +- m map[metadata.PackageID]IndexID -} - --func (c *cmdClient) DiagnosticRefresh(context.Context) error { -- return nil +-// NewPackageIndex creates a new PackageIndex instance for use in building +-// reference and package sets. +-func NewPackageIndex() *PackageIndex { +- return &PackageIndex{ +- m: make(map[metadata.PackageID]IndexID), +- } -} - --func (c *cmdClient) InlayHintRefresh(context.Context) error { -- return nil +-// IndexID returns the packageIdx referencing id, creating one if id is not yet +-// tracked by the receiver. +-func (index *PackageIndex) IndexID(id metadata.PackageID) IndexID { +- index.mu.Lock() +- defer index.mu.Unlock() +- if i, ok := index.m[id]; ok { +- return i +- } +- i := IndexID(len(index.ids)) +- index.m[id] = i +- index.ids = append(index.ids, id) +- return i -} - --func (c *cmdClient) SemanticTokensRefresh(context.Context) error { -- return nil +-// PackageID returns the PackageID for idx. +-// +-// idx must have been created by this PackageIndex instance. +-func (index *PackageIndex) PackageID(idx IndexID) metadata.PackageID { +- index.mu.Lock() +- defer index.mu.Unlock() +- return index.ids[idx] -} - --func (c *cmdClient) InlineValueRefresh(context.Context) error { -- return nil +-// A PackageSet is a set of metadata.PackageIDs, optimized for inuse memory +-// footprint and efficient union operations. +-type PackageSet struct { +- // PackageSet is a sparse int vector of package indexes from parent. +- parent *PackageIndex +- sparse map[int]blockType // high bits in key, set of low bits in value -} - --func (c *cmdClient) getFile(uri span.URI) *cmdFile { -- file, found := c.files[uri] -- if !found || file.err != nil { -- file = &cmdFile{ -- uri: uri, -- } -- c.files[uri] = file -- } -- if file.mapper == nil { -- content, err := os.ReadFile(uri.Filename()) -- if err != nil { -- file.err = fmt.Errorf("getFile: %v: %v", uri, err) -- return file -- } -- file.mapper = protocol.NewMapper(uri, content) +-type blockType = uint // type of each sparse vector element +-const blockSize = bits.UintSize +- +-// NewSet creates a new PackageSet bound to this PackageIndex instance. +-// +-// PackageSets may only be combined with other PackageSets from the same +-// instance. +-func (index *PackageIndex) NewSet() *PackageSet { +- return &PackageSet{ +- parent: index, +- sparse: make(map[int]blockType), - } -- return file -} - --func (c *cmdClient) openFile(uri span.URI) *cmdFile { -- c.filesMu.Lock() -- defer c.filesMu.Unlock() -- return c.getFile(uri) +-// DeclaringPackage returns the ID of the symbol's declaring package. +-// The package index must be the one used during decoding. +-func (index *PackageIndex) DeclaringPackage(sym Symbol) metadata.PackageID { +- return index.PackageID(sym.Package) -} - --// TODO(adonovan): provide convenience helpers to: --// - map a (URI, protocol.Range) to a MappedRange; --// - parse a command-line argument to a MappedRange. --func (c *connection) openFile(ctx context.Context, uri span.URI) (*cmdFile, error) { -- file := c.client.openFile(uri) -- if file.err != nil { -- return nil, file.err -- } -- -- p := &protocol.DidOpenTextDocumentParams{ -- TextDocument: protocol.TextDocumentItem{ -- URI: protocol.URIFromSpanURI(uri), -- LanguageID: "go", -- Version: 1, -- Text: string(file.mapper.Content), -- }, -- } -- if err := c.Server.DidOpen(ctx, p); err != nil { -- // TODO(adonovan): is this assignment concurrency safe? -- file.err = fmt.Errorf("%v: %v", uri, err) -- return nil, file.err -- } -- return file, nil +-// Add records a new element in the package set, for the provided package ID. +-func (s *PackageSet) AddPackage(id metadata.PackageID) { +- s.Add(s.parent.IndexID(id)) -} - --func (c *connection) semanticTokens(ctx context.Context, p *protocol.SemanticTokensRangeParams) (*protocol.SemanticTokens, error) { -- // use range to avoid limits on full -- resp, err := c.Server.SemanticTokensRange(ctx, p) -- if err != nil { -- return nil, err -- } -- return resp, nil +-// Add records a new element in the package set. +-// It is the caller's responsibility to ensure that idx was created with the +-// same PackageIndex as the PackageSet. +-func (s *PackageSet) Add(idx IndexID) { +- i := int(idx) +- s.sparse[i/blockSize] |= 1 << (i % blockSize) -} - --func (c *connection) diagnoseFiles(ctx context.Context, files []span.URI) error { -- var untypedFiles []interface{} -- for _, file := range files { -- untypedFiles = append(untypedFiles, string(file)) +-// Union records all elements from other into the receiver, mutating the +-// receiver set but not the argument set. The receiver must not be nil, but the +-// argument set may be nil. +-// +-// Precondition: both package sets were created with the same PackageIndex. +-func (s *PackageSet) Union(other *PackageSet) { +- if other == nil { +- return // e.g. unsafe - } -- c.client.diagnosticsMu.Lock() -- defer c.client.diagnosticsMu.Unlock() -- -- c.client.diagnosticsDone = make(chan struct{}) -- _, err := c.Server.NonstandardRequest(ctx, "gopls/diagnoseFiles", map[string]interface{}{"files": untypedFiles}) -- if err != nil { -- close(c.client.diagnosticsDone) -- return err +- if other.parent != s.parent { +- panic("other set is from a different PackageIndex instance") +- } +- for k, v := range other.sparse { +- if v0 := s.sparse[k]; v0 != v { +- s.sparse[k] = v0 | v +- } - } +-} - -- <-c.client.diagnosticsDone -- return nil +-// Contains reports whether id is contained in the receiver set. +-func (s *PackageSet) Contains(id metadata.PackageID) bool { +- i := int(s.parent.IndexID(id)) +- return s.sparse[i/blockSize]&(1<<(i%blockSize)) != 0 -} - --func (c *connection) terminate(ctx context.Context) { -- if strings.HasPrefix(c.client.app.Remote, "internal@") { -- // internal connections need to be left alive for the next test -- return +-// Elems calls f for each element of the set in ascending order. +-func (s *PackageSet) Elems(f func(IndexID)) { +- blockIndexes := make([]int, 0, len(s.sparse)) +- for k := range s.sparse { +- blockIndexes = append(blockIndexes, k) +- } +- sort.Ints(blockIndexes) +- for _, i := range blockIndexes { +- v := s.sparse[i] +- for b := 0; b < blockSize; b++ { +- if (v & (1 << b)) != 0 { +- f(IndexID(i*blockSize + b)) +- } +- } - } -- //TODO: do we need to handle errors on these calls? -- c.Shutdown(ctx) -- //TODO: right now calling exit terminates the process, we should rethink that -- //server.Exit(ctx) -} - --// Implement io.Closer. --func (c *cmdClient) Close() error { -- return nil +-// String returns a human-readable representation of the set: {A, B, ...}. +-func (s *PackageSet) String() string { +- var ids []string +- s.Elems(func(id IndexID) { +- ids = append(ids, string(s.parent.PackageID(id))) +- }) +- return fmt.Sprintf("{%s}", strings.Join(ids, ", ")) -} -diff -urN a/gopls/internal/lsp/cmd/definition.go b/gopls/internal/lsp/cmd/definition.go ---- a/gopls/internal/lsp/cmd/definition.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/definition.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,138 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/cache/typerefs/pkggraph_test.go b/gopls/internal/cache/typerefs/pkggraph_test.go +--- a/gopls/internal/cache/typerefs/pkggraph_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/typerefs/pkggraph_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,244 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package cmd +-package typerefs_test +- +-// This file is logically part of the test in pkgrefs_test.go: that +-// file defines the test assertion logic; this file provides a +-// reference implementation of a client of the typerefs package. - -import ( +- "bytes" - "context" -- "encoding/json" -- "flag" - "fmt" - "os" -- "strings" +- "runtime" +- "sync" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/tool" +- "golang.org/x/sync/errgroup" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/cache/typerefs" +- "golang.org/x/tools/gopls/internal/protocol" -) - --// A Definition is the result of a 'definition' query. --type Definition struct { -- Span span.Span `json:"span"` // span of the definition -- Description string `json:"description"` // description of the denoted object --} -- --// These constant is printed in the help, and then used in a test to verify the --// help is still valid. --// They refer to "Set" in "flag.FlagSet" from the DetailedHelp method below. -const ( -- exampleLine = 44 -- exampleColumn = 47 -- exampleOffset = 1270 +- // trace enables additional trace output to stdout, for debugging. +- // +- // Warning: produces a lot of output! Best to run with small package queries. +- trace = false -) - --// definition implements the definition verb for gopls. --type definition struct { -- app *Application +-// A Package holds reference information for a single package. +-type Package struct { +- // metapkg holds metapkg about this package and its dependencies. +- metapkg *metadata.Package - -- JSON bool `flag:"json" help:"emit output in JSON format"` -- MarkdownSupported bool `flag:"markdown" help:"support markdown in responses"` --} +- // transitiveRefs records, for each exported declaration in the package, the +- // transitive set of packages within the containing graph that are +- // transitively reachable through references, starting with the given decl. +- transitiveRefs map[string]*typerefs.PackageSet - --func (d *definition) Name() string { return "definition" } --func (d *definition) Parent() string { return d.app.Name() } --func (d *definition) Usage() string { return "[definition-flags] " } --func (d *definition) ShortHelp() string { return "show declaration of selected identifier" } --func (d *definition) DetailedHelp(f *flag.FlagSet) { -- fmt.Fprintf(f.Output(), ` --Example: show the definition of the identifier at syntax at offset %[1]v in this file (flag.FlagSet): +- // ReachesViaDeps records the set of packages in the containing graph whose +- // syntax may affect the current package's types. See the package +- // documentation for more details of what this means. +- ReachesByDeps *typerefs.PackageSet +-} - -- $ gopls definition internal/lsp/cmd/definition.go:%[1]v:%[2]v -- $ gopls definition internal/lsp/cmd/definition.go:#%[3]v +-// A PackageGraph represents a fully analyzed graph of packages and their +-// dependencies. +-type PackageGraph struct { +- pkgIndex *typerefs.PackageIndex +- meta metadata.Source +- parse func(context.Context, protocol.DocumentURI) (*parsego.File, error) - --definition-flags: --`, exampleLine, exampleColumn, exampleOffset) -- printFlagDefaults(f) +- mu sync.Mutex +- packages map[metadata.PackageID]*futurePackage -} - --// Run performs the definition query as specified by args and prints the --// results to stdout. --func (d *definition) Run(ctx context.Context, args ...string) error { -- if len(args) != 1 { -- return tool.CommandLineErrorf("definition expects 1 argument") -- } -- // Plaintext makes more sense for the command line. -- opts := d.app.options -- d.app.options = func(o *source.Options) { -- if opts != nil { -- opts(o) -- } -- o.PreferredContentFormat = protocol.PlainText -- if d.MarkdownSupported { -- o.PreferredContentFormat = protocol.Markdown -- } -- } -- conn, err := d.app.connect(ctx, nil) -- if err != nil { -- return err -- } -- defer conn.terminate(ctx) -- from := span.Parse(args[0]) -- file, err := conn.openFile(ctx, from.URI()) -- if err != nil { -- return err -- } -- loc, err := file.mapper.SpanLocation(from) -- if err != nil { -- return err -- } -- p := protocol.DefinitionParams{ -- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), -- } -- locs, err := conn.Definition(ctx, &p) -- if err != nil { -- return fmt.Errorf("%v: %v", from, err) +-// BuildPackageGraph analyzes the package graph for the requested ids, whose +-// metadata is described by meta. +-// +-// The provided parse function is used to parse the CompiledGoFiles of each package. +-// +-// The resulting PackageGraph is fully evaluated, and may be investigated using +-// the Package method. +-// +-// See the package documentation for more information on the package reference +-// algorithm. +-func BuildPackageGraph(ctx context.Context, meta metadata.Source, ids []metadata.PackageID, parse func(context.Context, protocol.DocumentURI) (*parsego.File, error)) (*PackageGraph, error) { +- g := &PackageGraph{ +- pkgIndex: typerefs.NewPackageIndex(), +- meta: meta, +- parse: parse, +- packages: make(map[metadata.PackageID]*futurePackage), - } +- metadata.SortPostOrder(meta, ids) - -- if len(locs) == 0 { -- return fmt.Errorf("%v: not an identifier", from) +- workers := runtime.GOMAXPROCS(0) +- if trace { +- workers = 1 - } -- file, err = conn.openFile(ctx, fileURI(locs[0].URI)) -- if err != nil { -- return fmt.Errorf("%v: %v", from, err) +- +- var eg errgroup.Group +- eg.SetLimit(workers) +- for _, id := range ids { +- id := id +- eg.Go(func() error { +- _, err := g.Package(ctx, id) +- return err +- }) - } -- definition, err := file.mapper.LocationSpan(locs[0]) -- if err != nil { -- return fmt.Errorf("%v: %v", from, err) +- return g, eg.Wait() +-} +- +-// futurePackage is a future result of analyzing a package, for use from Package only. +-type futurePackage struct { +- done chan struct{} +- pkg *Package +- err error +-} +- +-// Package gets the result of analyzing references for a single package. +-func (g *PackageGraph) Package(ctx context.Context, id metadata.PackageID) (*Package, error) { +- g.mu.Lock() +- fut, ok := g.packages[id] +- if ok { +- g.mu.Unlock() +- select { +- case <-fut.done: +- case <-ctx.Done(): +- return nil, ctx.Err() +- } +- } else { +- fut = &futurePackage{done: make(chan struct{})} +- g.packages[id] = fut +- g.mu.Unlock() +- fut.pkg, fut.err = g.buildPackage(ctx, id) +- close(fut.done) - } +- return fut.pkg, fut.err +-} - -- q := protocol.HoverParams{ -- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), +-// buildPackage parses a package and extracts its reference graph. It should +-// only be called from Package. +-func (g *PackageGraph) buildPackage(ctx context.Context, id metadata.PackageID) (*Package, error) { +- p := &Package{ +- metapkg: g.meta.Metadata(id), +- transitiveRefs: make(map[string]*typerefs.PackageSet), - } -- hover, err := conn.Hover(ctx, &q) -- if err != nil { -- return fmt.Errorf("%v: %v", from, err) +- var files []*parsego.File +- for _, filename := range p.metapkg.CompiledGoFiles { +- f, err := g.parse(ctx, filename) +- if err != nil { +- return nil, err +- } +- files = append(files, f) - } -- var description string -- if hover != nil { -- description = strings.TrimSpace(hover.Contents.Value) +- imports := make(map[metadata.ImportPath]*metadata.Package) +- for impPath, depID := range p.metapkg.DepsByImpPath { +- if depID != "" { +- imports[impPath] = g.meta.Metadata(depID) +- } - } - -- result := &Definition{ -- Span: definition, -- Description: description, -- } -- if d.JSON { -- enc := json.NewEncoder(os.Stdout) -- enc.SetIndent("", "\t") -- return enc.Encode(result) -- } -- fmt.Printf("%v", result.Span) -- if len(result.Description) > 0 { -- fmt.Printf(": defined here as %s", result.Description) -- } -- fmt.Printf("\n") -- return nil --} -diff -urN a/gopls/internal/lsp/cmd/folding_range.go b/gopls/internal/lsp/cmd/folding_range.go ---- a/gopls/internal/lsp/cmd/folding_range.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/folding_range.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,72 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package cmd -- --import ( -- "context" -- "flag" -- "fmt" -- -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/tool" --) -- --// foldingRanges implements the folding_ranges verb for gopls --type foldingRanges struct { -- app *Application --} +- // Compute the symbol-level dependencies through this package. +- data := typerefs.Encode(files, imports) - --func (r *foldingRanges) Name() string { return "folding_ranges" } --func (r *foldingRanges) Parent() string { return r.app.Name() } --func (r *foldingRanges) Usage() string { return "" } --func (r *foldingRanges) ShortHelp() string { return "display selected file's folding ranges" } --func (r *foldingRanges) DetailedHelp(f *flag.FlagSet) { -- fmt.Fprint(f.Output(), ` --Example: +- // data can be persisted in a filecache, keyed +- // by hash(id, CompiledGoFiles, imports). - -- $ gopls folding_ranges helper/helper.go --`) -- printFlagDefaults(f) --} +- // This point separates the local preprocessing +- // -- of a single package (above) from the global -- +- // transitive reachability query (below). - --func (r *foldingRanges) Run(ctx context.Context, args ...string) error { -- if len(args) != 1 { -- return tool.CommandLineErrorf("folding_ranges expects 1 argument (file)") -- } +- // classes records syntactic edges between declarations in this +- // package and declarations in this package or another +- // package. See the package documentation for a detailed +- // description of what these edges do (and do not) represent. +- classes := typerefs.Decode(g.pkgIndex, data) - -- conn, err := r.app.connect(ctx, nil) -- if err != nil { -- return err +- // Debug +- if trace && len(classes) > 0 { +- var buf bytes.Buffer +- fmt.Fprintf(&buf, "%s\n", id) +- for _, class := range classes { +- for i, name := range class.Decls { +- if i == 0 { +- fmt.Fprintf(&buf, "\t") +- } +- fmt.Fprintf(&buf, " .%s", name) +- } +- // Group symbols by package. +- var prevID PackageID +- for _, sym := range class.Refs { +- id := g.pkgIndex.DeclaringPackage(sym) +- if id != prevID { +- prevID = id +- fmt.Fprintf(&buf, "\n\t\t-> %s:", id) +- } +- fmt.Fprintf(&buf, " .%s", sym.Name) +- } +- fmt.Fprintln(&buf) +- } +- os.Stderr.Write(buf.Bytes()) - } -- defer conn.terminate(ctx) - -- from := span.Parse(args[0]) -- if _, err := conn.openFile(ctx, from.URI()); err != nil { -- return err -- } +- // Now compute the transitive closure of packages reachable +- // from any exported symbol of this package. +- for _, class := range classes { +- set := g.pkgIndex.NewSet() - -- p := protocol.FoldingRangeParams{ -- TextDocument: protocol.TextDocumentIdentifier{ -- URI: protocol.URIFromSpanURI(from.URI()), -- }, +- // The Refs slice is sorted by (PackageID, name), +- // so we can economize by calling g.Package only +- // when the package id changes. +- depP := p +- for _, sym := range class.Refs { +- symPkgID := g.pkgIndex.DeclaringPackage(sym) +- if symPkgID == id { +- panic("intra-package edge") +- } +- if depP.metapkg.ID != symPkgID { +- // package changed +- var err error +- depP, err = g.Package(ctx, symPkgID) +- if err != nil { +- return nil, err +- } +- } +- set.Add(sym.Package) +- set.Union(depP.transitiveRefs[sym.Name]) +- } +- for _, name := range class.Decls { +- p.transitiveRefs[name] = set +- } - } - -- ranges, err := conn.FoldingRange(ctx, &p) +- // Finally compute the union of transitiveRefs +- // across the direct deps of this package. +- byDeps, err := g.reachesByDeps(ctx, p.metapkg) - if err != nil { -- return err -- } -- -- for _, r := range ranges { -- fmt.Printf("%v:%v-%v:%v\n", -- r.StartLine+1, -- r.StartCharacter+1, -- r.EndLine+1, -- r.EndCharacter+1, -- ) +- return nil, err - } +- p.ReachesByDeps = byDeps - -- return nil --} -diff -urN a/gopls/internal/lsp/cmd/format.go b/gopls/internal/lsp/cmd/format.go ---- a/gopls/internal/lsp/cmd/format.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/format.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,76 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package cmd -- --import ( -- "context" -- "flag" -- "fmt" -- -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/span" --) -- --// format implements the format verb for gopls. --type format struct { -- EditFlags -- app *Application --} -- --func (c *format) Name() string { return "format" } --func (c *format) Parent() string { return c.app.Name() } --func (c *format) Usage() string { return "[format-flags] " } --func (c *format) ShortHelp() string { return "format the code according to the go standard" } --func (c *format) DetailedHelp(f *flag.FlagSet) { -- fmt.Fprint(f.Output(), ` --The arguments supplied may be simple file names, or ranges within files. -- --Example: reformat this file: -- -- $ gopls format -w internal/lsp/cmd/check.go -- --format-flags: --`) -- printFlagDefaults(f) +- return p, nil -} - --// Run performs the check on the files specified by args and prints the --// results to stdout. --func (c *format) Run(ctx context.Context, args ...string) error { -- if len(args) == 0 { -- return nil -- } -- c.app.editFlags = &c.EditFlags -- conn, err := c.app.connect(ctx, nil) -- if err != nil { -- return err -- } -- defer conn.terminate(ctx) -- for _, arg := range args { -- spn := span.Parse(arg) -- file, err := conn.openFile(ctx, spn.URI()) -- if err != nil { -- return err -- } -- loc, err := file.mapper.SpanLocation(spn) -- if err != nil { -- return err -- } -- if loc.Range.Start != loc.Range.End { -- return fmt.Errorf("only full file formatting supported") -- } -- p := protocol.DocumentFormattingParams{ -- TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, -- } -- edits, err := conn.Formatting(ctx, &p) +-// reachesByDeps computes the set of packages that are reachable through +-// dependencies of the package m. +-func (g *PackageGraph) reachesByDeps(ctx context.Context, mp *metadata.Package) (*typerefs.PackageSet, error) { +- transitive := g.pkgIndex.NewSet() +- for _, depID := range mp.DepsByPkgPath { +- dep, err := g.Package(ctx, depID) - if err != nil { -- return fmt.Errorf("%v: %v", spn, err) +- return nil, err - } -- if err := applyTextEdits(file.mapper, edits, c.app.editFlags); err != nil { -- return err +- transitive.AddPackage(dep.metapkg.ID) +- for _, set := range dep.transitiveRefs { +- transitive.Union(set) - } - } -- return nil +- return transitive, nil -} -diff -urN a/gopls/internal/lsp/cmd/help_test.go b/gopls/internal/lsp/cmd/help_test.go ---- a/gopls/internal/lsp/cmd/help_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/help_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,84 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/cache/typerefs/pkgrefs_test.go b/gopls/internal/cache/typerefs/pkgrefs_test.go +--- a/gopls/internal/cache/typerefs/pkgrefs_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/typerefs/pkgrefs_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,406 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package cmd_test +-package typerefs_test - -import ( - "bytes" - "context" - "flag" +- "fmt" +- "go/token" +- "go/types" - "os" -- "path/filepath" +- "sort" +- "strings" +- "sync" - "testing" +- "time" - -- "github.com/google/go-cmp/cmp" -- "golang.org/x/tools/gopls/internal/lsp/cmd" +- "golang.org/x/tools/go/gcexportdata" +- "golang.org/x/tools/go/packages" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/cache/typerefs" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/astutil" +- "golang.org/x/tools/internal/packagesinternal" - "golang.org/x/tools/internal/testenv" -- "golang.org/x/tools/internal/tool" -) - --//go:generate go test -run Help -update-help-files +-var ( +- dir = flag.String("dir", "", "dir to run go/packages from") +- query = flag.String("query", "std", "go/packages load query to use for walkdecl tests") +- verify = flag.Bool("verify", true, "whether to verify reachable packages using export data (may be slow on large graphs)") +-) - --var updateHelpFiles = flag.Bool("update-help-files", false, "Write out the help files instead of checking them") +-type ( +- packageName = metadata.PackageName +- PackageID = metadata.PackageID +- ImportPath = metadata.ImportPath +- PackagePath = metadata.PackagePath +- Metadata = metadata.Package +- MetadataSource = metadata.Source +-) - --const appName = "gopls" +-// TestBuildPackageGraph tests the BuildPackageGraph constructor, which uses +-// the reference analysis of the Refs function to build a graph of +-// relationships between packages. +-// +-// It simulates the operation of gopls at startup: packages are loaded via +-// go/packages, and their syntax+metadata analyzed to determine which packages +-// are reachable from others. +-// +-// The test then verifies that the 'load' graph (the graph of relationships in +-// export data) is a subgraph of the 'reach' graph constructed by +-// BuildPackageGraph. While doing so, it constructs some statistics about the +-// relative sizes of these graphs, along with the 'transitive imports' graph, +-// to report the effectiveness of the reachability analysis. +-// +-// The following flags affect this test: +-// - dir sets the dir from which to run go/packages +-// - query sets the go/packages query to load +-// - verify toggles the verification w.r.t. the load graph (which may be +-// prohibitively expensive with large queries). +-func TestBuildPackageGraph(t *testing.T) { +- if testing.Short() { +- t.Skip("skipping with -short: loading the packages can take a long time with a cold cache") +- } +- testenv.NeedsGoBuild(t) // for go/packages - --func TestHelpFiles(t *testing.T) { -- testenv.NeedsGoBuild(t) // This is a lie. We actually need the source code. -- app := cmd.New(appName, "", nil, nil) -- ctx := context.Background() -- for _, page := range append(app.Commands(), app) { -- t.Run(page.Name(), func(t *testing.T) { -- var buf bytes.Buffer -- s := flag.NewFlagSet(page.Name(), flag.ContinueOnError) -- s.SetOutput(&buf) -- tool.Run(ctx, s, page, []string{"-h"}) -- name := page.Name() -- if name == appName { -- name = "usage" -- } -- helpFile := filepath.Join("usage", name+".hlp") -- got := buf.Bytes() -- if *updateHelpFiles { -- if err := os.WriteFile(helpFile, got, 0666); err != nil { -- t.Errorf("Failed writing %v: %v", helpFile, err) -- } -- return -- } -- want, err := os.ReadFile(helpFile) -- if err != nil { -- t.Fatalf("Missing help file %q", helpFile) -- } -- if diff := cmp.Diff(string(want), string(got)); diff != "" { -- t.Errorf("Help file %q did not match, run with -update-help-files to fix (-want +got)\n%s", helpFile, diff) -- } -- }) +- t0 := time.Now() +- exports, meta, err := loadPackages(*query, *verify) +- if err != nil { +- t.Fatalf("loading failed: %v", err) - } --} +- t.Logf("loaded %d packages in %v", len(exports), time.Since(t0)) - --func TestVerboseHelp(t *testing.T) { -- testenv.NeedsGoBuild(t) // This is a lie. We actually need the source code. -- app := cmd.New(appName, "", nil, nil) - ctx := context.Background() -- var buf bytes.Buffer -- s := flag.NewFlagSet(appName, flag.ContinueOnError) -- s.SetOutput(&buf) -- tool.Run(ctx, s, app, []string{"-v", "-h"}) -- got := buf.Bytes() -- -- helpFile := filepath.Join("usage", "usage-v.hlp") -- if *updateHelpFiles { -- if err := os.WriteFile(helpFile, got, 0666); err != nil { -- t.Errorf("Failed writing %v: %v", helpFile, err) -- } -- return +- var ids []PackageID +- for id := range exports { +- ids = append(ids, id) - } -- want, err := os.ReadFile(helpFile) +- sort.Slice(ids, func(i, j int) bool { +- return ids[i] < ids[j] +- }) +- +- t0 = time.Now() +- g, err := BuildPackageGraph(ctx, meta, ids, newParser().parse) - if err != nil { -- t.Fatalf("Missing help file %q", helpFile) +- t.Fatal(err) - } -- if diff := cmp.Diff(string(want), string(got)); diff != "" { -- t.Errorf("Help file %q did not match, run with -update-help-files to fix (-want +got)\n%s", helpFile, diff) +- t.Logf("building package graph took %v", time.Since(t0)) +- +- // Collect information about the edges between packages for later analysis. +- // +- // We compare the following package graphs: +- // - the imports graph: edges are transitive imports +- // - the reaches graph: edges are reachability relationships through syntax +- // of imports (as defined in the package doc) +- // - the loads graph: edges are packages loaded through the export data of +- // imports +- // +- // By definition, loads < reaches < imports. +- type edgeSet map[PackageID]map[PackageID]bool +- var ( +- imports = make(edgeSet) // A imports B transitively +- importedBy = make(edgeSet) // A is imported by B transitively +- reaches = make(edgeSet) // A reaches B through top-level declaration syntax +- reachedBy = make(edgeSet) // A is reached by B through top-level declaration syntax +- loads = make(edgeSet) // A loads B through export data of its direct dependencies +- loadedBy = make(edgeSet) // A is loaded by B through export data of B's direct dependencies +- ) +- recordEdge := func(from, to PackageID, fwd, rev edgeSet) { +- if fwd[from] == nil { +- fwd[from] = make(map[PackageID]bool) +- } +- fwd[from][to] = true +- if rev[to] == nil { +- rev[to] = make(map[PackageID]bool) +- } +- rev[to][from] = true - } --} -diff -urN a/gopls/internal/lsp/cmd/highlight.go b/gopls/internal/lsp/cmd/highlight.go ---- a/gopls/internal/lsp/cmd/highlight.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/highlight.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,82 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package cmd +- exportedPackages := make(map[PackageID]*types.Package) +- importPackage := func(id PackageID) *types.Package { +- exportFile := exports[id] +- if exportFile == "" { +- return nil // no exported symbols +- } +- mp := meta.Metadata(id) +- tpkg, ok := exportedPackages[id] +- if !ok { +- pkgPath := string(mp.PkgPath) +- tpkg, err = importFromExportData(pkgPath, exportFile) +- if err != nil { +- t.Fatalf("importFromExportData(%s, %s) failed: %v", pkgPath, exportFile, err) +- } +- exportedPackages[id] = tpkg +- } +- return tpkg +- } - --import ( -- "context" -- "flag" -- "fmt" +- for _, id := range ids { +- pkg, err := g.Package(ctx, id) +- if err != nil { +- t.Fatal(err) +- } +- pkg.ReachesByDeps.Elems(func(id2 typerefs.IndexID) { +- recordEdge(id, g.pkgIndex.PackageID(id2), reaches, reachedBy) +- }) - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/tool" --) +- importMap := importMap(id, meta) +- for _, id2 := range importMap { +- recordEdge(id, id2, imports, importedBy) +- } - --// highlight implements the highlight verb for gopls. --type highlight struct { -- app *Application --} +- if *verify { +- for _, depID := range meta.Metadata(id).DepsByPkgPath { +- tpkg := importPackage(depID) +- if tpkg == nil { +- continue +- } +- for _, imp := range tpkg.Imports() { +- depID, ok := importMap[PackagePath(imp.Path())] +- if !ok { +- t.Errorf("import map (len: %d) for %s missing imported types.Package %s", len(importMap), id, imp.Path()) +- continue +- } +- recordEdge(id, depID, loads, loadedBy) +- } +- } - --func (r *highlight) Name() string { return "highlight" } --func (r *highlight) Parent() string { return r.app.Name() } --func (r *highlight) Usage() string { return "" } --func (r *highlight) ShortHelp() string { return "display selected identifier's highlights" } --func (r *highlight) DetailedHelp(f *flag.FlagSet) { -- fmt.Fprint(f.Output(), ` --Example: +- for depID := range loads[id] { +- if !pkg.ReachesByDeps.Contains(depID) { +- t.Errorf("package %s was imported by %s, but not detected as reachable", depID, id) +- } +- } +- } +- } - -- $ # 1-indexed location (:line:column or :#offset) of the target identifier -- $ gopls highlight helper/helper.go:8:6 -- $ gopls highlight helper/helper.go:#53 --`) -- printFlagDefaults(f) +- if testing.Verbose() { +- fmt.Printf("%-52s%8s%8s%8s%8s%8s%8s\n", "package ID", "imp", "impBy", "reach", "reachBy", "load", "loadBy") +- for _, id := range ids { +- fmt.Printf("%-52s%8d%8d%8d%8d%8d%8d\n", id, len(imports[id]), len(importedBy[id]), len(reaches[id]), len(reachedBy[id]), len(loads[id]), len(loadedBy[id])) +- } +- fmt.Println(strings.Repeat("-", 100)) +- fmt.Printf("%-52s%8s%8s%8s%8s%8s%8s\n", "package ID", "imp", "impBy", "reach", "reachBy", "load", "loadBy") +- +- avg := func(m edgeSet) float64 { +- var avg float64 +- for _, id := range ids { +- s := m[id] +- avg += float64(len(s)) / float64(len(ids)) +- } +- return avg +- } +- fmt.Printf("%52s%8.1f%8.1f%8.1f%8.1f%8.1f%8.1f\n", "averages:", avg(imports), avg(importedBy), avg(reaches), avg(reachedBy), avg(loads), avg(loadedBy)) +- } -} - --func (r *highlight) Run(ctx context.Context, args ...string) error { -- if len(args) != 1 { -- return tool.CommandLineErrorf("highlight expects 1 argument (position)") +-func importMap(id PackageID, meta MetadataSource) map[PackagePath]PackageID { +- imports := make(map[PackagePath]PackageID) +- var recordIDs func(PackageID) +- recordIDs = func(id PackageID) { +- mp := meta.Metadata(id) +- if _, ok := imports[mp.PkgPath]; ok { +- return +- } +- imports[mp.PkgPath] = id +- for _, id := range mp.DepsByPkgPath { +- recordIDs(id) +- } +- } +- for _, id := range meta.Metadata(id).DepsByPkgPath { +- recordIDs(id) - } +- return imports +-} - -- conn, err := r.app.connect(ctx, nil) +-func importFromExportData(pkgPath, exportFile string) (*types.Package, error) { +- file, err := os.Open(exportFile) - if err != nil { -- return err +- return nil, err - } -- defer conn.terminate(ctx) -- -- from := span.Parse(args[0]) -- file, err := conn.openFile(ctx, from.URI()) +- r, err := gcexportdata.NewReader(file) - if err != nil { -- return err +- file.Close() +- return nil, err - } -- -- loc, err := file.mapper.SpanLocation(from) +- fset := token.NewFileSet() +- tpkg, err := gcexportdata.Read(r, fset, make(map[string]*types.Package), pkgPath) +- file.Close() - if err != nil { -- return err +- return nil, err - } -- -- p := protocol.DocumentHighlightParams{ -- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), +- // The export file reported by go/packages is produced by the compiler, which +- // has additional package dependencies due to inlining. +- // +- // Export and re-import so that we only observe dependencies from the +- // exported API. +- var out bytes.Buffer +- err = gcexportdata.Write(&out, fset, tpkg) +- if err != nil { +- return nil, err - } -- highlights, err := conn.DocumentHighlight(ctx, &p) +- return gcexportdata.Read(&out, token.NewFileSet(), make(map[string]*types.Package), pkgPath) +-} +- +-func BenchmarkBuildPackageGraph(b *testing.B) { +- t0 := time.Now() +- exports, meta, err := loadPackages(*query, *verify) - if err != nil { -- return err +- b.Fatalf("loading failed: %v", err) +- } +- b.Logf("loaded %d packages in %v", len(exports), time.Since(t0)) +- ctx := context.Background() +- var ids []PackageID +- for id := range exports { +- ids = append(ids, id) - } +- b.ResetTimer() - -- var results []span.Span -- for _, h := range highlights { -- s, err := file.mapper.RangeSpan(h.Range) +- for i := 0; i < b.N; i++ { +- _, err := BuildPackageGraph(ctx, meta, ids, newParser().parse) - if err != nil { -- return err +- b.Fatal(err) - } -- results = append(results, s) - } -- // Sort results to make tests deterministic since DocumentHighlight uses a map. -- span.SortSpans(results) +-} - -- for _, s := range results { -- fmt.Println(s) -- } -- return nil --} -diff -urN a/gopls/internal/lsp/cmd/implementation.go b/gopls/internal/lsp/cmd/implementation.go ---- a/gopls/internal/lsp/cmd/implementation.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/implementation.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,87 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package cmd -- --import ( -- "context" -- "flag" -- "fmt" -- "sort" -- -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/tool" --) -- --// implementation implements the implementation verb for gopls --type implementation struct { -- app *Application +-type memoizedParser struct { +- mu sync.Mutex +- files map[protocol.DocumentURI]*futureParse -} - --func (i *implementation) Name() string { return "implementation" } --func (i *implementation) Parent() string { return i.app.Name() } --func (i *implementation) Usage() string { return "" } --func (i *implementation) ShortHelp() string { return "display selected identifier's implementation" } --func (i *implementation) DetailedHelp(f *flag.FlagSet) { -- fmt.Fprint(f.Output(), ` --Example: -- -- $ # 1-indexed location (:line:column or :#offset) of the target identifier -- $ gopls implementation helper/helper.go:8:6 -- $ gopls implementation helper/helper.go:#53 --`) -- printFlagDefaults(f) +-type futureParse struct { +- done chan struct{} +- pgf *parsego.File +- err error -} - --func (i *implementation) Run(ctx context.Context, args ...string) error { -- if len(args) != 1 { -- return tool.CommandLineErrorf("implementation expects 1 argument (position)") -- } -- -- conn, err := i.app.connect(ctx, nil) -- if err != nil { -- return err -- } -- defer conn.terminate(ctx) -- -- from := span.Parse(args[0]) -- file, err := conn.openFile(ctx, from.URI()) -- if err != nil { -- return err -- } -- -- loc, err := file.mapper.SpanLocation(from) -- if err != nil { -- return err -- } -- -- p := protocol.ImplementationParams{ -- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), -- } -- implementations, err := conn.Implementation(ctx, &p) -- if err != nil { -- return err +-func newParser() *memoizedParser { +- return &memoizedParser{ +- files: make(map[protocol.DocumentURI]*futureParse), - } +-} - -- var spans []string -- for _, impl := range implementations { -- f, err := conn.openFile(ctx, fileURI(impl.URI)) -- if err != nil { -- return err -- } -- span, err := f.mapper.LocationSpan(impl) +-func (p *memoizedParser) parse(ctx context.Context, uri protocol.DocumentURI) (*parsego.File, error) { +- doParse := func(ctx context.Context, uri protocol.DocumentURI) (*parsego.File, error) { +- // TODO(adonovan): hoist this operation outside the benchmark critsec. +- content, err := os.ReadFile(uri.Path()) - if err != nil { -- return err +- return nil, err - } -- spans = append(spans, fmt.Sprint(span)) +- content = astutil.PurgeFuncBodies(content) +- pgf, _ := parsego.Parse(ctx, token.NewFileSet(), uri, content, parsego.Full, false) +- return pgf, nil - } -- sort.Strings(spans) - -- for _, s := range spans { -- fmt.Println(s) +- p.mu.Lock() +- fut, ok := p.files[uri] +- if ok { +- p.mu.Unlock() +- select { +- case <-fut.done: +- case <-ctx.Done(): +- return nil, ctx.Err() +- } +- } else { +- fut = &futureParse{done: make(chan struct{})} +- p.files[uri] = fut +- p.mu.Unlock() +- fut.pgf, fut.err = doParse(ctx, uri) +- close(fut.done) - } -- -- return nil +- return fut.pgf, fut.err -} -diff -urN a/gopls/internal/lsp/cmd/imports.go b/gopls/internal/lsp/cmd/imports.go ---- a/gopls/internal/lsp/cmd/imports.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/imports.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,81 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package cmd -- --import ( -- "context" -- "flag" -- "fmt" -- -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/tool" --) - --// imports implements the import verb for gopls. --type imports struct { -- EditFlags -- app *Application +-type mapMetadataSource struct { +- m map[PackageID]*Metadata -} - --func (t *imports) Name() string { return "imports" } --func (t *imports) Parent() string { return t.app.Name() } --func (t *imports) Usage() string { return "[imports-flags] " } --func (t *imports) ShortHelp() string { return "updates import statements" } --func (t *imports) DetailedHelp(f *flag.FlagSet) { -- fmt.Fprintf(f.Output(), ` --Example: update imports statements in a file: -- -- $ gopls imports -w internal/lsp/cmd/check.go -- --imports-flags: --`) -- printFlagDefaults(f) +-func (s mapMetadataSource) Metadata(id PackageID) *Metadata { +- return s.m[id] -} - --// Run performs diagnostic checks on the file specified and either; --// - if -w is specified, updates the file in place; --// - if -d is specified, prints out unified diffs of the changes; or --// - otherwise, prints the new versions to stdout. --func (t *imports) Run(ctx context.Context, args ...string) error { -- if len(args) != 1 { -- return tool.CommandLineErrorf("imports expects 1 argument") -- } -- t.app.editFlags = &t.EditFlags -- conn, err := t.app.connect(ctx, nil) -- if err != nil { -- return err +-// This function is a compressed version of snapshot.load from the +-// internal/cache package, for use in testing. +-// +-// TODO(rfindley): it may be valuable to extract this logic from the snapshot, +-// since it is otherwise standalone. +-func loadPackages(query string, needExport bool) (map[PackageID]string, MetadataSource, error) { +- cfg := &packages.Config{ +- Dir: *dir, +- Mode: packages.NeedName | +- packages.NeedFiles | +- packages.NeedCompiledGoFiles | +- packages.NeedImports | +- packages.NeedDeps | +- packages.NeedTypesSizes | +- packages.NeedModule | +- packages.NeedEmbedFiles | +- packages.LoadMode(packagesinternal.DepsErrors) | +- packages.LoadMode(packagesinternal.ForTest), +- Tests: true, - } -- defer conn.terminate(ctx) -- -- from := span.Parse(args[0]) -- uri := from.URI() -- file, err := conn.openFile(ctx, uri) -- if err != nil { -- return err +- if needExport { +- cfg.Mode |= packages.NeedExportFile // ExportFile is not requested by gopls: this is used to verify reachability - } -- actions, err := conn.CodeAction(ctx, &protocol.CodeActionParams{ -- TextDocument: protocol.TextDocumentIdentifier{ -- URI: protocol.URIFromSpanURI(uri), -- }, -- }) +- pkgs, err := packages.Load(cfg, query) - if err != nil { -- return fmt.Errorf("%v: %v", from, err) +- return nil, nil, err - } -- var edits []protocol.TextEdit -- for _, a := range actions { -- if a.Title != "Organize Imports" { -- continue +- +- meta := make(map[PackageID]*Metadata) +- var buildMetadata func(pkg *packages.Package) +- buildMetadata = func(pkg *packages.Package) { +- id := PackageID(pkg.ID) +- if meta[id] != nil { +- return - } -- for _, c := range a.Edit.DocumentChanges { -- if c.TextDocumentEdit != nil { -- if fileURI(c.TextDocumentEdit.TextDocument.URI) == uri { -- edits = append(edits, c.TextDocumentEdit.Edits...) -- } +- mp := &Metadata{ +- ID: id, +- PkgPath: PackagePath(pkg.PkgPath), +- Name: packageName(pkg.Name), +- ForTest: PackagePath(packagesinternal.GetForTest(pkg)), +- TypesSizes: pkg.TypesSizes, +- LoadDir: cfg.Dir, +- Module: pkg.Module, +- Errors: pkg.Errors, +- DepsErrors: packagesinternal.GetDepsErrors(pkg), +- } +- meta[id] = mp +- +- for _, filename := range pkg.CompiledGoFiles { +- mp.CompiledGoFiles = append(mp.CompiledGoFiles, protocol.URIFromPath(filename)) +- } +- for _, filename := range pkg.GoFiles { +- mp.GoFiles = append(mp.GoFiles, protocol.URIFromPath(filename)) +- } +- +- mp.DepsByImpPath = make(map[ImportPath]PackageID) +- mp.DepsByPkgPath = make(map[PackagePath]PackageID) +- for importPath, imported := range pkg.Imports { +- importPath := ImportPath(importPath) +- +- // see note in gopls/internal/cache/load.go for an explanation of this check. +- if importPath != "unsafe" && len(imported.CompiledGoFiles) == 0 { +- mp.DepsByImpPath[importPath] = "" // missing +- continue - } +- +- mp.DepsByImpPath[importPath] = PackageID(imported.ID) +- mp.DepsByPkgPath[PackagePath(imported.PkgPath)] = PackageID(imported.ID) +- buildMetadata(imported) - } - } -- return applyTextEdits(file.mapper, edits, t.app.editFlags) +- +- exportFiles := make(map[PackageID]string) +- for _, pkg := range pkgs { +- exportFiles[PackageID(pkg.ID)] = pkg.ExportFile +- buildMetadata(pkg) +- } +- return exportFiles, &mapMetadataSource{meta}, nil -} -diff -urN a/gopls/internal/lsp/cmd/info.go b/gopls/internal/lsp/cmd/info.go ---- a/gopls/internal/lsp/cmd/info.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/info.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,311 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/cache/typerefs/refs.go b/gopls/internal/cache/typerefs/refs.go +--- a/gopls/internal/cache/typerefs/refs.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/typerefs/refs.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,832 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package cmd +-package typerefs - -import ( -- "bytes" -- "context" -- "encoding/json" -- "flag" - "fmt" -- "net/url" -- "os" +- "go/ast" +- "go/token" - "sort" - "strings" - -- goplsbug "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/browser" -- "golang.org/x/tools/gopls/internal/lsp/debug" -- "golang.org/x/tools/gopls/internal/lsp/filecache" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/internal/tool" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/util/astutil" +- "golang.org/x/tools/gopls/internal/util/frob" -) - --// help implements the help command. --type help struct { -- app *Application +-// Encode analyzes the Go syntax trees of a package, constructs a +-// reference graph, and uses it to compute, for each exported +-// declaration, the set of exported symbols of directly imported +-// packages that it references, perhaps indirectly. +-// +-// It returns a serializable index of this information. +-// Use Decode to expand the result. +-func Encode(files []*parsego.File, imports map[metadata.ImportPath]*metadata.Package) []byte { +- return index(files, imports) -} - --func (h *help) Name() string { return "help" } --func (h *help) Parent() string { return h.app.Name() } --func (h *help) Usage() string { return "" } --func (h *help) ShortHelp() string { return "print usage information for subcommands" } --func (h *help) DetailedHelp(f *flag.FlagSet) { -- fmt.Fprint(f.Output(), ` +-// Decode decodes a serializable index of symbol +-// reachability produced by Encode. +-// +-// Because many declarations reference the exact same set of symbols, +-// the results are grouped into equivalence classes. +-// Classes are sorted by Decls[0], ascending. +-// The class with empty reachability is omitted. +-// +-// See the package documentation for more details as to what a +-// reference does (and does not) represent. +-func Decode(pkgIndex *PackageIndex, data []byte) []Class { +- return decode(pkgIndex, data) +-} - --Examples: --$ gopls help # main gopls help message --$ gopls help remote # help on 'remote' command --$ gopls help remote sessions # help on 'remote sessions' subcommand --`) -- printFlagDefaults(f) +-// A Class is a reachability equivalence class. +-// +-// It attests that each exported package-level declaration in Decls +-// references (perhaps indirectly) one of the external (imported) +-// symbols in Refs. +-// +-// Because many Decls reach the same Refs, +-// it is more efficient to group them into classes. +-type Class struct { +- Decls []string // sorted set of names of exported decls with same reachability +- Refs []Symbol // set of external symbols, in ascending (PackageID, Name) order -} - --// Run prints help information about a subcommand. --func (h *help) Run(ctx context.Context, args ...string) error { -- find := func(cmds []tool.Application, name string) tool.Application { -- for _, cmd := range cmds { -- if cmd.Name() == name { -- return cmd -- } -- } -- return nil -- } +-// A Symbol represents an external (imported) symbol +-// referenced by the analyzed package. +-type Symbol struct { +- Package IndexID // w.r.t. PackageIndex passed to decoder +- Name string +-} - -- // Find the subcommand denoted by args (empty => h.app). -- var cmd tool.Application = h.app -- for i, arg := range args { -- cmd = find(getSubcommands(cmd), arg) -- if cmd == nil { -- return tool.CommandLineErrorf( -- "no such subcommand: %s", strings.Join(args[:i+1], " ")) -- } -- } +-// An IndexID is a small integer that uniquely identifies a package within a +-// given PackageIndex. +-type IndexID int - -- // 'gopls help cmd subcmd' is equivalent to 'gopls cmd subcmd -h'. -- // The flag package prints the usage information (defined by tool.Run) -- // when it sees the -h flag. -- fs := flag.NewFlagSet(cmd.Name(), flag.ExitOnError) -- return tool.Run(ctx, fs, h.app, append(args[:len(args):len(args)], "-h")) --} +-// -- internals -- - --// version implements the version command. --type version struct { -- JSON bool `flag:"json" help:"outputs in json format."` +-// A symbolSet is a set of symbols used internally during index construction. +-// +-// TODO(adonovan): opt: evaluate unifying Symbol and symbol. +-// (Encode would have to create a private PackageIndex.) +-type symbolSet map[symbol]bool - -- app *Application +-// A symbol is the internal representation of an external +-// (imported) symbol referenced by the analyzed package. +-type symbol struct { +- pkg metadata.PackageID +- name string -} - --func (v *version) Name() string { return "version" } --func (v *version) Parent() string { return v.app.Name() } --func (v *version) Usage() string { return "" } --func (v *version) ShortHelp() string { return "print the gopls version information" } --func (v *version) DetailedHelp(f *flag.FlagSet) { -- fmt.Fprint(f.Output(), ``) -- printFlagDefaults(f) --} +-// declNode holds information about a package-level declaration +-// (or more than one with the same name, in ill-typed code). +-// +-// It is a node in the symbol reference graph, whose outgoing edges +-// are of two kinds: intRefs and extRefs. +-type declNode struct { +- name string +- rep *declNode // canonical representative of this SCC (initially self) - --// Run prints version information to stdout. --func (v *version) Run(ctx context.Context, args ...string) error { -- var mode = debug.PlainText -- if v.JSON { -- mode = debug.JSON -- } +- // outgoing graph edges +- intRefs map[*declNode]bool // to symbols in this package +- extRefs symbolSet // to imported symbols +- extRefsClass int // extRefs equivalence class number (-1 until set at end) - -- return debug.PrintVersionInfo(ctx, os.Stdout, v.app.verbose(), mode) +- // Tarjan's SCC algorithm +- index, lowlink int32 // Tarjan numbering +- scc int32 // -ve => on stack; 0 => unvisited; +ve => node is root of a found SCC -} - --// bug implements the bug command. --type bug struct { -- app *Application --} +-// state holds the working state of the Refs algorithm for a single package. +-// +-// The number of distinct symbols referenced by a single package +-// (measured across all of kubernetes), was found to be: +-// - max = 1750. +-// - Several packages reference > 100 symbols. +-// - p95 = 32, p90 = 22, p50 = 8. +-type state struct { +- // numbering of unique symbol sets +- class []symbolSet // unique symbol sets +- classIndex map[string]int // index of above (using SymbolSet.hash as key) - --func (b *bug) Name() string { return "bug" } --func (b *bug) Parent() string { return b.app.Name() } --func (b *bug) Usage() string { return "" } --func (b *bug) ShortHelp() string { return "report a bug in gopls" } --func (b *bug) DetailedHelp(f *flag.FlagSet) { -- fmt.Fprint(f.Output(), ``) -- printFlagDefaults(f) +- // Tarjan's SCC algorithm +- index int32 +- stack []*declNode -} - --const goplsBugPrefix = "x/tools/gopls: " --const goplsBugHeader = `ATTENTION: Please answer these questions BEFORE submitting your issue. Thanks! -- --#### What did you do? --If possible, provide a recipe for reproducing the error. --A complete runnable program is good. --A link on play.golang.org is better. --A failing unit test is the best. +-// getClassIndex returns the small integer (an index into +-// state.class) that identifies the given set. +-func (st *state) getClassIndex(set symbolSet) int { +- key := classKey(set) +- i, ok := st.classIndex[key] +- if !ok { +- i = len(st.class) +- st.classIndex[key] = i +- st.class = append(st.class, set) +- } +- return i +-} - --#### What did you expect to see? +-// appendSorted appends the symbols to syms, sorts by ascending +-// (PackageID, name), and returns the result. +-// The argument must be an empty slice, ideally with capacity len(set). +-func (set symbolSet) appendSorted(syms []symbol) []symbol { +- for sym := range set { +- syms = append(syms, sym) +- } +- sort.Slice(syms, func(i, j int) bool { +- x, y := syms[i], syms[j] +- if x.pkg != y.pkg { +- return x.pkg < y.pkg +- } +- return x.name < y.name +- }) +- return syms +-} - +-// classKey returns a key such that equal keys imply equal sets. +-// (e.g. a sorted string representation, or a cryptographic hash of same). +-func classKey(set symbolSet) string { +- // Sort symbols into a stable order. +- // TODO(adonovan): opt: a cheap crypto hash (e.g. BLAKE2b) might +- // make a cheaper map key than a large string. +- // Try using a hasher instead of a builder. +- var s strings.Builder +- for _, sym := range set.appendSorted(make([]symbol, 0, len(set))) { +- fmt.Fprintf(&s, "%s:%s;", sym.pkg, sym.name) +- } +- return s.String() +-} - --#### What did you see instead? +-// index builds the reference graph and encodes the index. +-func index(pgfs []*parsego.File, imports map[metadata.ImportPath]*metadata.Package) []byte { +- // First pass: gather package-level names and create a declNode for each. +- // +- // In ill-typed code, there may be multiple declarations of the +- // same name; a single declInfo node will represent them all. +- decls := make(map[string]*declNode) +- addDecl := func(id *ast.Ident) { +- if name := id.Name; name != "_" && decls[name] == nil { +- node := &declNode{name: name, extRefsClass: -1} +- node.rep = node +- decls[name] = node +- } +- } +- for _, pgf := range pgfs { +- for _, d := range pgf.File.Decls { +- switch d := d.(type) { +- case *ast.GenDecl: +- switch d.Tok { +- case token.TYPE: +- for _, spec := range d.Specs { +- addDecl(spec.(*ast.TypeSpec).Name) +- } - +- case token.VAR, token.CONST: +- for _, spec := range d.Specs { +- for _, ident := range spec.(*ast.ValueSpec).Names { +- addDecl(ident) +- } +- } +- } - --` +- case *ast.FuncDecl: +- // non-method functions +- if d.Recv.NumFields() == 0 { +- addDecl(d.Name) +- } +- } +- } +- } - --// Run collects some basic information and then prepares an issue ready to --// be reported. --func (b *bug) Run(ctx context.Context, args ...string) error { -- // This undocumented environment variable allows -- // the cmd integration test (and maintainers) to -- // trigger a call to bug.Report. -- if msg := os.Getenv("TEST_GOPLS_BUG"); msg != "" { -- filecache.Start() // register bug handler -- goplsbug.Report(msg) -- return nil +- // Second pass: process files to collect referring identifiers. +- st := &state{classIndex: make(map[string]int)} +- for _, pgf := range pgfs { +- visitFile(pgf.File, imports, decls) - } - -- // Enumerate bug reports, grouped and sorted. -- _, reports := filecache.BugReports() -- sort.Slice(reports, func(i, j int) bool { -- x, y := reports[i], reports[i] -- if x.Key != y.Key { -- return x.Key < y.Key // ascending key order +- // Find the strong components of the declNode graph +- // using Tarjan's algorithm, and coalesce each component. +- st.index = 1 +- for _, decl := range decls { +- if decl.index == 0 { // unvisited +- st.visit(decl) - } -- return y.AtTime.Before(x.AtTime) // most recent first -- }) -- keyDenom := make(map[string]int) // key is "file:line" -- for _, report := range reports { -- keyDenom[report.Key]++ - } - -- // Privacy: the content of 'public' will be posted to GitHub -- // to populate an issue textarea. Even though the user must -- // submit the form to share the information with the world, -- // merely populating the form causes us to share the -- // information with GitHub itself. -- // -- // For that reason, we cannot write private information to -- // public, such as bug reports, which may quote source code. -- public := &bytes.Buffer{} -- fmt.Fprint(public, goplsBugHeader) -- if len(reports) > 0 { -- fmt.Fprintf(public, "#### Internal errors\n\n") -- fmt.Fprintf(public, "Gopls detected %d internal errors, %d distinct:\n", -- len(reports), len(keyDenom)) -- for key, denom := range keyDenom { -- fmt.Fprintf(public, "- %s (%d)\n", key, denom) +- // TODO(adonovan): opt: consider compressing the serialized +- // representation by recording not the classes but the DAG of +- // non-trivial union operations (the "pointer equivalence" +- // optimization of Hardekopf & Lin). Unlike that algorithm, +- // which piggybacks on SCC coalescing, in our case it would +- // be better to make a forward traversal from the exported +- // decls, since it avoids visiting unreachable nodes, and +- // results in a dense (not sparse) numbering of the sets. +- +- // Tabulate the unique reachability sets of +- // each exported package member. +- classNames := make(map[int][]string) // set of decls (names) for a given reachability set +- for name, decl := range decls { +- if !ast.IsExported(name) { +- continue - } -- fmt.Fprintf(public, "\nPlease copy the full information printed by `gopls bug` here, if you are comfortable sharing it.\n\n") -- } -- debug.PrintVersionInfo(ctx, public, true, debug.Markdown) -- body := public.String() -- title := strings.Join(args, " ") -- if !strings.HasPrefix(title, goplsBugPrefix) { -- title = goplsBugPrefix + title -- } -- if !browser.Open("https://github.com/golang/go/issues/new?title=" + url.QueryEscape(title) + "&body=" + url.QueryEscape(body)) { -- fmt.Print("Please file a new issue at golang.org/issue/new using this template:\n\n") -- fmt.Print(body) -- } - -- // Print bug reports to stdout (not GitHub). -- keyNum := make(map[string]int) -- for _, report := range reports { -- fmt.Printf("-- %v -- \n", report.AtTime) +- decl = decl.find() - -- // Append seq number (e.g. " (1/2)") for repeated keys. -- var seq string -- if denom := keyDenom[report.Key]; denom > 1 { -- keyNum[report.Key]++ -- seq = fmt.Sprintf(" (%d/%d)", keyNum[report.Key], denom) +- // Skip decls with empty reachability. +- if len(decl.extRefs) == 0 { +- continue - } - -- // Privacy: -- // - File and Stack may contain the name of the user that built gopls. -- // - Description may contain names of the user's packages/files/symbols. -- fmt.Printf("%s:%d: %s%s\n\n", report.File, report.Line, report.Description, seq) -- fmt.Printf("%s\n\n", report.Stack) -- } -- if len(reports) > 0 { -- fmt.Printf("Please copy the above information into the GitHub issue, if you are comfortable sharing it.\n") +- // Canonicalize the set (and memoize). +- class := decl.extRefsClass +- if class < 0 { +- class = st.getClassIndex(decl.extRefs) +- decl.extRefsClass = class +- } +- classNames[class] = append(classNames[class], name) - } - -- return nil --} -- --type apiJSON struct { -- app *Application +- return encode(classNames, st.class) -} - --func (j *apiJSON) Name() string { return "api-json" } --func (j *apiJSON) Parent() string { return j.app.Name() } --func (j *apiJSON) Usage() string { return "" } --func (j *apiJSON) ShortHelp() string { return "print json describing gopls API" } --func (j *apiJSON) DetailedHelp(f *flag.FlagSet) { -- fmt.Fprint(f.Output(), ``) -- printFlagDefaults(f) --} +-// visitFile inspects the file syntax for referring identifiers, and +-// populates the internal and external references of decls. +-func visitFile(file *ast.File, imports map[metadata.ImportPath]*metadata.Package, decls map[string]*declNode) { +- // Import information for this file. Multiple packages +- // may be referenced by a given name in the presence +- // of type errors (or multiple dot imports, which are +- // keyed by "."). +- fileImports := make(map[string][]metadata.PackageID) - --func (j *apiJSON) Run(ctx context.Context, args ...string) error { -- js, err := json.MarshalIndent(source.GeneratedAPIJSON, "", "\t") -- if err != nil { -- return err +- // importEdge records a reference from decl to an imported symbol +- // (pkgname.name). The package name may be ".". +- importEdge := func(decl *declNode, pkgname, name string) { +- if token.IsExported(name) { +- for _, depID := range fileImports[pkgname] { +- if decl.extRefs == nil { +- decl.extRefs = make(symbolSet) +- } +- decl.extRefs[symbol{depID, name}] = true +- } +- } - } -- fmt.Fprint(os.Stdout, string(js)) -- return nil --} - --type licenses struct { -- app *Application --} +- // visit finds refs within node and builds edges from fromId's decl. +- // References to the type parameters are ignored. +- visit := func(fromId *ast.Ident, node ast.Node, tparams map[string]bool) { +- if fromId.Name == "_" { +- return +- } +- from := decls[fromId.Name] +- // When visiting a method, there may not be a valid type declaration for +- // the receiver. In this case there is no way to refer to the method, so +- // we need not record edges. +- if from == nil { +- return +- } - --func (l *licenses) Name() string { return "licenses" } --func (l *licenses) Parent() string { return l.app.Name() } --func (l *licenses) Usage() string { return "" } --func (l *licenses) ShortHelp() string { return "print licenses of included software" } --func (l *licenses) DetailedHelp(f *flag.FlagSet) { -- fmt.Fprint(f.Output(), ``) -- printFlagDefaults(f) --} +- // Visit each reference to name or name.sel. +- visitDeclOrSpec(node, func(name, sel string) { +- // Ignore references to type parameters. +- if tparams[name] { +- return +- } - --const licensePreamble = ` --gopls is made available under the following BSD-style license: +- // If name is declared in the package scope, +- // record an edge whether or not sel is empty. +- // A field or method selector may affect the +- // type of the current decl via initializers: +- // +- // package p +- // var x = y.F +- // var y = struct{ F int }{} +- if to, ok := decls[name]; ok { +- if from.intRefs == nil { +- from.intRefs = make(map[*declNode]bool) +- } +- from.intRefs[to] = true - --Copyright (c) 2009 The Go Authors. All rights reserved. +- } else { +- // Only record an edge to dot-imported packages +- // if there was no edge to a local name. +- // This assumes that there are no duplicate declarations. +- // We conservatively, assume that this name comes from +- // every dot-imported package. +- importEdge(from, ".", name) +- } - --Redistribution and use in source and binary forms, with or without --modification, are permitted provided that the following conditions are --met: +- // Record an edge to an import if it matches the name, even if that +- // name collides with a package level name. Unlike the case of dotted +- // imports, we know the package is invalid here, and choose to fail +- // conservatively. +- if sel != "" { +- importEdge(from, name, sel) +- } +- }) +- } - -- * Redistributions of source code must retain the above copyright --notice, this list of conditions and the following disclaimer. -- * Redistributions in binary form must reproduce the above --copyright notice, this list of conditions and the following disclaimer --in the documentation and/or other materials provided with the --distribution. -- * Neither the name of Google Inc. nor the names of its --contributors may be used to endorse or promote products derived from --this software without specific prior written permission. +- // Visit the declarations and gather reference edges. +- // Import declarations appear before all others. +- for _, d := range file.Decls { +- switch d := d.(type) { +- case *ast.GenDecl: +- switch d.Tok { +- case token.IMPORT: +- // Record local import names for this file. +- for _, spec := range d.Specs { +- spec := spec.(*ast.ImportSpec) +- path := metadata.UnquoteImportPath(spec) +- if path == "" { +- continue +- } +- dep := imports[path] +- if dep == nil { +- // Note here that we don't try to "guess" +- // the name of an import based on e.g. +- // its importPath. Doing so would only +- // result in edges that don't go anywhere. +- continue +- } +- name := string(dep.Name) +- if spec.Name != nil { +- if spec.Name.Name == "_" { +- continue +- } +- name = spec.Name.Name // possibly "." +- } +- fileImports[name] = append(fileImports[name], dep.ID) +- } - --THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS --"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT --LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR --A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT --OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, --SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT --LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, --DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY --THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT --(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE --OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +- case token.TYPE: +- for _, spec := range d.Specs { +- spec := spec.(*ast.TypeSpec) +- tparams := tparamsMap(spec.TypeParams) +- visit(spec.Name, spec, tparams) +- } - --gopls implements the LSP specification, which is made available under the following license: +- case token.VAR, token.CONST: +- for _, spec := range d.Specs { +- spec := spec.(*ast.ValueSpec) +- for _, name := range spec.Names { +- visit(name, spec, nil) +- } +- } +- } - --Copyright (c) Microsoft Corporation +- case *ast.FuncDecl: +- // This check for NumFields() > 0 is consistent with go/types, +- // which reports an error but treats the declaration like a +- // normal function when Recv is non-nil but empty +- // (as in func () f()). +- if d.Recv.NumFields() > 0 { +- // Method. Associate it with the receiver. +- _, id, typeParams := astutil.UnpackRecv(d.Recv.List[0].Type) +- if id != nil { +- var tparams map[string]bool +- if len(typeParams) > 0 { +- tparams = make(map[string]bool) +- for _, tparam := range typeParams { +- if tparam.Name != "_" { +- tparams[tparam.Name] = true +- } +- } +- } +- visit(id, d, tparams) +- } +- } else { +- // Non-method. +- tparams := tparamsMap(d.Type.TypeParams) +- visit(d.Name, d, tparams) +- } +- } +- } +-} - --All rights reserved. +-// tparamsMap returns a set recording each name declared by the provided field +-// list. It so happens that we only care about names declared by type parameter +-// lists. +-func tparamsMap(tparams *ast.FieldList) map[string]bool { +- if tparams == nil || len(tparams.List) == 0 { +- return nil +- } +- m := make(map[string]bool) +- for _, f := range tparams.List { +- for _, name := range f.Names { +- if name.Name != "_" { +- m[name.Name] = true +- } +- } +- } +- return m +-} - --MIT License +-// A refVisitor visits referring identifiers and dotted identifiers. +-// +-// For a referring identifier I, name="I" and sel="". For a dotted identifier +-// q.I, name="q" and sel="I". +-type refVisitor = func(name, sel string) - --Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation --files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, --modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software --is furnished to do so, subject to the following conditions: +-// visitDeclOrSpec visits referring idents or dotted idents that may affect +-// the type of the declaration at the given node, which must be an ast.Decl or +-// ast.Spec. +-func visitDeclOrSpec(node ast.Node, f refVisitor) { +- // Declarations +- switch n := node.(type) { +- // ImportSpecs should not appear here, and will panic in the default case. - --The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. +- case *ast.ValueSpec: +- // Skip Doc, Names, Comments, which do not affect the decl type. +- // Initializers only affect the type of a value spec if the type is unset. +- if n.Type != nil { +- visitExpr(n.Type, f) +- } else { // only need to walk expr list if type is nil +- visitExprList(n.Values, f) +- } - --THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES --OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS --BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT --OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +- case *ast.TypeSpec: +- // Skip Doc, Name, and Comment, which do not affect the decl type. +- if tparams := n.TypeParams; tparams != nil { +- visitFieldList(tparams, f) +- } +- visitExpr(n.Type, f) - --gopls also includes software made available under these licenses: --` +- case *ast.BadDecl: +- // nothing to do - --func (l *licenses) Run(ctx context.Context, args ...string) error { -- opts := source.DefaultOptions(l.app.options) -- txt := licensePreamble -- if opts.LicensesText == "" { -- txt += "(development gopls, license information not available)" -- } else { -- txt += opts.LicensesText -- } -- fmt.Fprint(os.Stdout, txt) -- return nil --} -diff -urN a/gopls/internal/lsp/cmd/links.go b/gopls/internal/lsp/cmd/links.go ---- a/gopls/internal/lsp/cmd/links.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/links.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,77 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +- // We should not reach here with a GenDecl, so panic below in the default case. - --package cmd +- case *ast.FuncDecl: +- // Skip Doc, Name, and Body, which do not affect the type. +- // Recv is handled by Refs: methods are associated with their type. +- visitExpr(n.Type, f) - --import ( -- "context" -- "encoding/json" -- "flag" -- "fmt" -- "os" +- default: +- panic(fmt.Sprintf("unexpected node type %T", node)) +- } +-} - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/tool" --) +-// visitExpr visits referring idents and dotted idents that may affect the +-// type of expr. +-// +-// visitExpr can't reliably distinguish a dotted ident pkg.X from a +-// selection expr.f or T.method. +-func visitExpr(expr ast.Expr, f refVisitor) { +- switch n := expr.(type) { +- // These four cases account for about two thirds of all nodes, +- // so we place them first to shorten the common control paths. +- // (See go.dev/cl/480915.) +- case *ast.Ident: +- f(n.Name, "") - --// links implements the links verb for gopls. --type links struct { -- JSON bool `flag:"json" help:"emit document links in JSON format"` +- case *ast.BasicLit: +- // nothing to do - -- app *Application --} +- case *ast.SelectorExpr: +- if ident, ok := n.X.(*ast.Ident); ok { +- f(ident.Name, n.Sel.Name) +- } else { +- visitExpr(n.X, f) +- // Skip n.Sel as we don't care about which field or method is selected, +- // as we'll have recorded an edge to all declarations relevant to the +- // receiver type via visiting n.X above. +- } - --func (l *links) Name() string { return "links" } --func (l *links) Parent() string { return l.app.Name() } --func (l *links) Usage() string { return "[links-flags] " } --func (l *links) ShortHelp() string { return "list links in a file" } --func (l *links) DetailedHelp(f *flag.FlagSet) { -- fmt.Fprintf(f.Output(), ` --Example: list links contained within a file: +- case *ast.CallExpr: +- visitExpr(n.Fun, f) +- visitExprList(n.Args, f) // args affect types for unsafe.Sizeof or builtins or generics - -- $ gopls links internal/lsp/cmd/check.go +- // Expressions +- case *ast.Ellipsis: +- if n.Elt != nil { +- visitExpr(n.Elt, f) +- } - --links-flags: --`) -- printFlagDefaults(f) --} +- case *ast.FuncLit: +- visitExpr(n.Type, f) +- // Skip Body, which does not affect the type. - --// Run finds all the links within a document --// - if -json is specified, outputs location range and uri --// - otherwise, prints the a list of unique links --func (l *links) Run(ctx context.Context, args ...string) error { -- if len(args) != 1 { -- return tool.CommandLineErrorf("links expects 1 argument") -- } -- conn, err := l.app.connect(ctx, nil) -- if err != nil { -- return err -- } -- defer conn.terminate(ctx) +- case *ast.CompositeLit: +- if n.Type != nil { +- visitExpr(n.Type, f) +- } +- // Skip Elts, which do not affect the type. - -- from := span.Parse(args[0]) -- uri := from.URI() +- case *ast.ParenExpr: +- visitExpr(n.X, f) - -- if _, err := conn.openFile(ctx, uri); err != nil { -- return err -- } -- results, err := conn.DocumentLink(ctx, &protocol.DocumentLinkParams{ -- TextDocument: protocol.TextDocumentIdentifier{ -- URI: protocol.URIFromSpanURI(uri), -- }, -- }) -- if err != nil { -- return fmt.Errorf("%v: %v", from, err) -- } -- if l.JSON { -- enc := json.NewEncoder(os.Stdout) -- enc.SetIndent("", "\t") -- return enc.Encode(results) -- } -- for _, v := range results { -- fmt.Println(*v.Target) -- } -- return nil --} -diff -urN a/gopls/internal/lsp/cmd/prepare_rename.go b/gopls/internal/lsp/cmd/prepare_rename.go ---- a/gopls/internal/lsp/cmd/prepare_rename.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/prepare_rename.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,80 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +- case *ast.IndexExpr: +- visitExpr(n.X, f) +- visitExpr(n.Index, f) // may affect type for instantiations - --package cmd +- case *ast.IndexListExpr: +- visitExpr(n.X, f) +- for _, index := range n.Indices { +- visitExpr(index, f) // may affect the type for instantiations +- } - --import ( -- "context" -- "errors" -- "flag" -- "fmt" +- case *ast.SliceExpr: +- visitExpr(n.X, f) +- // skip Low, High, and Max, which do not affect type. - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/tool" --) +- case *ast.TypeAssertExpr: +- // Skip X, as it doesn't actually affect the resulting type of the type +- // assertion. +- if n.Type != nil { +- visitExpr(n.Type, f) +- } - --// prepareRename implements the prepare_rename verb for gopls. --type prepareRename struct { -- app *Application --} +- case *ast.StarExpr: +- visitExpr(n.X, f) - --func (r *prepareRename) Name() string { return "prepare_rename" } --func (r *prepareRename) Parent() string { return r.app.Name() } --func (r *prepareRename) Usage() string { return "" } --func (r *prepareRename) ShortHelp() string { return "test validity of a rename operation at location" } --func (r *prepareRename) DetailedHelp(f *flag.FlagSet) { -- fmt.Fprint(f.Output(), ` --Example: +- case *ast.UnaryExpr: +- visitExpr(n.X, f) - -- $ # 1-indexed location (:line:column or :#offset) of the target identifier -- $ gopls prepare_rename helper/helper.go:8:6 -- $ gopls prepare_rename helper/helper.go:#53 --`) -- printFlagDefaults(f) --} +- case *ast.BinaryExpr: +- visitExpr(n.X, f) +- visitExpr(n.Y, f) - --// ErrInvalidRenamePosition is returned when prepareRename is run at a position that --// is not a candidate for renaming. --var ErrInvalidRenamePosition = errors.New("request is not valid at the given position") +- case *ast.KeyValueExpr: +- panic("unreachable") // unreachable, as we don't descend into elts of composite lits. - --func (r *prepareRename) Run(ctx context.Context, args ...string) error { -- if len(args) != 1 { -- return tool.CommandLineErrorf("prepare_rename expects 1 argument (file)") -- } +- case *ast.ArrayType: +- if n.Len != nil { +- visitExpr(n.Len, f) +- } +- visitExpr(n.Elt, f) - -- conn, err := r.app.connect(ctx, nil) -- if err != nil { -- return err -- } -- defer conn.terminate(ctx) +- case *ast.StructType: +- visitFieldList(n.Fields, f) - -- from := span.Parse(args[0]) -- file, err := conn.openFile(ctx, from.URI()) -- if err != nil { -- return err -- } -- loc, err := file.mapper.SpanLocation(from) -- if err != nil { -- return err -- } -- p := protocol.PrepareRenameParams{ -- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), -- } -- result, err := conn.PrepareRename(ctx, &p) -- if err != nil { -- return fmt.Errorf("prepare_rename failed: %w", err) -- } -- if result == nil { -- return ErrInvalidRenamePosition -- } -- -- s, err := file.mapper.RangeSpan(result.Range) -- if err != nil { -- return err -- } -- -- fmt.Println(s) -- return nil --} -diff -urN a/gopls/internal/lsp/cmd/references.go b/gopls/internal/lsp/cmd/references.go ---- a/gopls/internal/lsp/cmd/references.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/references.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,92 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package cmd -- --import ( -- "context" -- "flag" -- "fmt" -- "sort" -- -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/tool" --) +- case *ast.FuncType: +- if tparams := n.TypeParams; tparams != nil { +- visitFieldList(tparams, f) +- } +- if n.Params != nil { +- visitFieldList(n.Params, f) +- } +- if n.Results != nil { +- visitFieldList(n.Results, f) +- } - --// references implements the references verb for gopls --type references struct { -- IncludeDeclaration bool `flag:"d,declaration" help:"include the declaration of the specified identifier in the results"` +- case *ast.InterfaceType: +- visitFieldList(n.Methods, f) - -- app *Application --} +- case *ast.MapType: +- visitExpr(n.Key, f) +- visitExpr(n.Value, f) - --func (r *references) Name() string { return "references" } --func (r *references) Parent() string { return r.app.Name() } --func (r *references) Usage() string { return "[references-flags] " } --func (r *references) ShortHelp() string { return "display selected identifier's references" } --func (r *references) DetailedHelp(f *flag.FlagSet) { -- fmt.Fprint(f.Output(), ` --Example: +- case *ast.ChanType: +- visitExpr(n.Value, f) - -- $ # 1-indexed location (:line:column or :#offset) of the target identifier -- $ gopls references helper/helper.go:8:6 -- $ gopls references helper/helper.go:#53 +- case *ast.BadExpr: +- // nothing to do - --references-flags: --`) -- printFlagDefaults(f) +- default: +- panic(fmt.Sprintf("ast.Walk: unexpected node type %T", n)) +- } -} - --func (r *references) Run(ctx context.Context, args ...string) error { -- if len(args) != 1 { -- return tool.CommandLineErrorf("references expects 1 argument (position)") +-func visitExprList(list []ast.Expr, f refVisitor) { +- for _, x := range list { +- visitExpr(x, f) - } +-} - -- conn, err := r.app.connect(ctx, nil) -- if err != nil { -- return err +-func visitFieldList(n *ast.FieldList, f refVisitor) { +- for _, field := range n.List { +- visitExpr(field.Type, f) - } -- defer conn.terminate(ctx) +-} - -- from := span.Parse(args[0]) -- file, err := conn.openFile(ctx, from.URI()) -- if err != nil { -- return err -- } -- loc, err := file.mapper.SpanLocation(from) -- if err != nil { -- return err -- } -- p := protocol.ReferenceParams{ -- Context: protocol.ReferenceContext{ -- IncludeDeclaration: r.IncludeDeclaration, -- }, -- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), -- } -- locations, err := conn.References(ctx, &p) -- if err != nil { -- return err -- } -- var spans []string -- for _, l := range locations { -- f, err := conn.openFile(ctx, fileURI(l.URI)) -- if err != nil { -- return err -- } -- // convert location to span for user-friendly 1-indexed line -- // and column numbers -- span, err := f.mapper.LocationSpan(l) -- if err != nil { -- return err -- } -- spans = append(spans, fmt.Sprint(span)) -- } +-// -- strong component graph construction (plundered from go/pointer) -- - -- sort.Strings(spans) -- for _, s := range spans { -- fmt.Println(s) -- } -- return nil --} -diff -urN a/gopls/internal/lsp/cmd/remote.go b/gopls/internal/lsp/cmd/remote.go ---- a/gopls/internal/lsp/cmd/remote.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/remote.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,164 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-// visit implements the depth-first search of Tarjan's SCC algorithm +-// (see https://doi.org/10.1137/0201010). +-// Precondition: x is canonical. +-func (st *state) visit(x *declNode) { +- checkCanonical(x) +- x.index = st.index +- x.lowlink = st.index +- st.index++ - --package cmd +- st.stack = append(st.stack, x) // push +- assert(x.scc == 0, "node revisited") +- x.scc = -1 - --import ( -- "context" -- "encoding/json" -- "errors" -- "flag" -- "fmt" -- "log" -- "os" +- for y := range x.intRefs { +- // Loop invariant: x is canonical. - -- "golang.org/x/tools/gopls/internal/lsp/command" -- "golang.org/x/tools/gopls/internal/lsp/lsprpc" --) +- y := y.find() - --type remote struct { -- app *Application -- subcommands +- if x == y { +- continue // nodes already coalesced +- } - -- // For backward compatibility, allow aliasing this command (it was previously -- // called 'inspect'). -- // -- // TODO(rFindley): delete this after allowing some transition time in case -- // there were any users of 'inspect' (I suspect not). -- alias string --} +- switch { +- case y.scc > 0: +- // y is already a collapsed SCC - --func newRemote(app *Application, alias string) *remote { -- return &remote{ -- app: app, -- subcommands: subcommands{ -- &listSessions{app: app}, -- &startDebugging{app: app}, -- }, -- alias: alias, -- } --} +- case y.scc < 0: +- // y is on the stack, and thus in the current SCC. +- if y.index < x.lowlink { +- x.lowlink = y.index +- } - --func (r *remote) Name() string { -- if r.alias != "" { -- return r.alias -- } -- return "remote" --} +- default: +- // y is unvisited; visit it now. +- st.visit(y) +- // Note: x and y are now non-canonical. - --func (r *remote) Parent() string { return r.app.Name() } +- x = x.find() - --func (r *remote) ShortHelp() string { -- short := "interact with the gopls daemon" -- if r.alias != "" { -- short += " (deprecated: use 'remote')" +- if y.lowlink < x.lowlink { +- x.lowlink = y.lowlink +- } +- } - } -- return short --} +- checkCanonical(x) - --// listSessions is an inspect subcommand to list current sessions. --type listSessions struct { -- app *Application --} +- // Is x the root of an SCC? +- if x.lowlink == x.index { +- // Coalesce all nodes in the SCC. +- for { +- // Pop y from stack. +- i := len(st.stack) - 1 +- y := st.stack[i] +- st.stack = st.stack[:i] - --func (c *listSessions) Name() string { return "sessions" } --func (c *listSessions) Parent() string { return c.app.Name() } --func (c *listSessions) Usage() string { return "" } --func (c *listSessions) ShortHelp() string { -- return "print information about current gopls sessions" --} +- checkCanonical(x) +- checkCanonical(y) - --const listSessionsExamples = ` --Examples: +- if x == y { +- break // SCC is complete. +- } +- coalesce(x, y) +- } - --1) list sessions for the default daemon: +- // Accumulate union of extRefs over +- // internal edges (to other SCCs). +- for y := range x.intRefs { +- y := y.find() +- if y == x { +- continue // already coalesced +- } +- assert(y.scc == 1, "edge to non-scc node") +- for z := range y.extRefs { +- if x.extRefs == nil { +- x.extRefs = make(symbolSet) +- } +- x.extRefs[z] = true // extRefs: x U= y +- } +- } - --$ gopls -remote=auto remote sessions --or just --$ gopls remote sessions +- x.scc = 1 +- } +-} - --2) list sessions for a specific daemon: +-// coalesce combines two nodes in the strong component graph. +-// Precondition: x and y are canonical. +-func coalesce(x, y *declNode) { +- // x becomes y's canonical representative. +- y.rep = x - --$ gopls -remote=localhost:8082 remote sessions --` +- // x accumulates y's internal references. +- for z := range y.intRefs { +- x.intRefs[z] = true +- } +- y.intRefs = nil - --func (c *listSessions) DetailedHelp(f *flag.FlagSet) { -- fmt.Fprint(f.Output(), listSessionsExamples) -- printFlagDefaults(f) +- // x accumulates y's external references. +- for z := range y.extRefs { +- if x.extRefs == nil { +- x.extRefs = make(symbolSet) +- } +- x.extRefs[z] = true +- } +- y.extRefs = nil -} - --func (c *listSessions) Run(ctx context.Context, args ...string) error { -- remote := c.app.Remote -- if remote == "" { -- remote = "auto" -- } -- state, err := lsprpc.QueryServerState(ctx, remote) -- if err != nil { -- return err -- } -- v, err := json.MarshalIndent(state, "", "\t") -- if err != nil { -- log.Fatal(err) +-// find returns the canonical node decl. +-// (The nodes form a disjoint set forest.) +-func (decl *declNode) find() *declNode { +- rep := decl.rep +- if rep != decl { +- rep = rep.find() +- decl.rep = rep // simple path compression (no union-by-rank) - } -- os.Stdout.Write(v) -- return nil +- return rep -} - --type startDebugging struct { -- app *Application --} +-const debugSCC = false // enable assertions in strong-component algorithm - --func (c *startDebugging) Name() string { return "debug" } --func (c *startDebugging) Usage() string { return "[host:port]" } --func (c *startDebugging) ShortHelp() string { -- return "start the debug server" +-func checkCanonical(x *declNode) { +- if debugSCC { +- assert(x == x.find(), "not canonical") +- } -} - --const startDebuggingExamples = ` --Examples: -- --1) start a debug server for the default daemon, on an arbitrary port: +-func assert(cond bool, msg string) { +- if debugSCC && !cond { +- panic(msg) +- } +-} - --$ gopls -remote=auto remote debug --or just --$ gopls remote debug +-// -- serialization -- - --2) start for a specific daemon, on a specific port: +-// (The name says gob but in fact we use frob.) +-var classesCodec = frob.CodecFor[gobClasses]() - --$ gopls -remote=localhost:8082 remote debug localhost:8083 --` +-type gobClasses struct { +- Strings []string // table of strings (PackageIDs and names) +- Classes []gobClass +-} - --func (c *startDebugging) DetailedHelp(f *flag.FlagSet) { -- fmt.Fprint(f.Output(), startDebuggingExamples) -- printFlagDefaults(f) +-type gobClass struct { +- Decls []int32 // indices into gobClasses.Strings +- Refs []int32 // list of (package, name) pairs, each an index into gobClasses.Strings -} - --func (c *startDebugging) Run(ctx context.Context, args ...string) error { -- if len(args) > 1 { -- fmt.Fprintln(os.Stderr, c.Usage()) -- return errors.New("invalid usage") -- } -- remote := c.app.Remote -- if remote == "" { -- remote = "auto" -- } -- debugAddr := "" -- if len(args) > 0 { -- debugAddr = args[0] -- } -- debugArgs := command.DebuggingArgs{ -- Addr: debugAddr, -- } -- var result command.DebuggingResult -- if err := lsprpc.ExecuteCommand(ctx, remote, command.StartDebugging.ID(), debugArgs, &result); err != nil { -- return err -- } -- if len(result.URLs) == 0 { -- return errors.New("no debugging URLs") +-// encode encodes the equivalence classes, +-// (classNames[i], classes[i]), for i in range classes. +-// +-// With the current encoding, across kubernetes, +-// the encoded size distribution has +-// p50 = 511B, p95 = 4.4KB, max = 108K. +-func encode(classNames map[int][]string, classes []symbolSet) []byte { +- payload := gobClasses{ +- Classes: make([]gobClass, 0, len(classNames)), - } -- for _, url := range result.URLs { -- fmt.Printf("debugging on %s\n", url) +- +- // index of unique strings +- strings := make(map[string]int32) +- stringIndex := func(s string) int32 { +- i, ok := strings[s] +- if !ok { +- i = int32(len(payload.Strings)) +- strings[s] = i +- payload.Strings = append(payload.Strings, s) +- } +- return i - } -- return nil --} -diff -urN a/gopls/internal/lsp/cmd/rename.go b/gopls/internal/lsp/cmd/rename.go ---- a/gopls/internal/lsp/cmd/rename.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/rename.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,74 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package cmd +- var refs []symbol // recycled temporary +- for class, names := range classNames { +- set := classes[class] - --import ( -- "context" -- "flag" -- "fmt" +- // names, sorted +- sort.Strings(names) +- gobDecls := make([]int32, len(names)) +- for i, name := range names { +- gobDecls[i] = stringIndex(name) +- } - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/tool" --) +- // refs, sorted by ascending (PackageID, name) +- gobRefs := make([]int32, 0, 2*len(set)) +- for _, sym := range set.appendSorted(refs[:0]) { +- gobRefs = append(gobRefs, +- stringIndex(string(sym.pkg)), +- stringIndex(sym.name)) +- } +- payload.Classes = append(payload.Classes, gobClass{ +- Decls: gobDecls, +- Refs: gobRefs, +- }) +- } - --// rename implements the rename verb for gopls. --type rename struct { -- EditFlags -- app *Application +- return classesCodec.Encode(payload) -} - --func (r *rename) Name() string { return "rename" } --func (r *rename) Parent() string { return r.app.Name() } --func (r *rename) Usage() string { return "[rename-flags] " } --func (r *rename) ShortHelp() string { return "rename selected identifier" } --func (r *rename) DetailedHelp(f *flag.FlagSet) { -- fmt.Fprint(f.Output(), ` --Example: -- -- $ # 1-based location (:line:column or :#position) of the thing to change -- $ gopls rename helper/helper.go:8:6 Foo -- $ gopls rename helper/helper.go:#53 Foo -- --rename-flags: --`) -- printFlagDefaults(f) --} +-func decode(pkgIndex *PackageIndex, data []byte) []Class { +- var payload gobClasses +- classesCodec.Decode(data, &payload) - --// Run renames the specified identifier and either; --// - if -w is specified, updates the file(s) in place; --// - if -d is specified, prints out unified diffs of the changes; or --// - otherwise, prints the new versions to stdout. --func (r *rename) Run(ctx context.Context, args ...string) error { -- if len(args) != 2 { -- return tool.CommandLineErrorf("definition expects 2 arguments (position, new name)") -- } -- r.app.editFlags = &r.EditFlags -- conn, err := r.app.connect(ctx, nil) -- if err != nil { -- return err +- classes := make([]Class, len(payload.Classes)) +- for i, gobClass := range payload.Classes { +- decls := make([]string, len(gobClass.Decls)) +- for i, decl := range gobClass.Decls { +- decls[i] = payload.Strings[decl] +- } +- refs := make([]Symbol, len(gobClass.Refs)/2) +- for i := range refs { +- pkgID := pkgIndex.IndexID(metadata.PackageID(payload.Strings[gobClass.Refs[2*i]])) +- name := payload.Strings[gobClass.Refs[2*i+1]] +- refs[i] = Symbol{Package: pkgID, Name: name} +- } +- classes[i] = Class{ +- Decls: decls, +- Refs: refs, +- } - } -- defer conn.terminate(ctx) - -- from := span.Parse(args[0]) -- file, err := conn.openFile(ctx, from.URI()) -- if err != nil { -- return err -- } -- loc, err := file.mapper.SpanLocation(from) -- if err != nil { -- return err -- } -- p := protocol.RenameParams{ -- TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, -- Position: loc.Range.Start, -- NewName: args[1], -- } -- edit, err := conn.Rename(ctx, &p) -- if err != nil { -- return err -- } -- return conn.client.applyWorkspaceEdit(edit) +- // Sort by ascending Decls[0]. +- // TODO(adonovan): move sort to encoder. Determinism is good. +- sort.Slice(classes, func(i, j int) bool { +- return classes[i].Decls[0] < classes[j].Decls[0] +- }) +- +- return classes -} -diff -urN a/gopls/internal/lsp/cmd/semantictokens.go b/gopls/internal/lsp/cmd/semantictokens.go ---- a/gopls/internal/lsp/cmd/semantictokens.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/semantictokens.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,224 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/cache/typerefs/refs_test.go b/gopls/internal/cache/typerefs/refs_test.go +--- a/gopls/internal/cache/typerefs/refs_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/typerefs/refs_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,549 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package cmd +-package typerefs_test - -import ( -- "bytes" - "context" -- "flag" - "fmt" -- "go/parser" - "go/token" -- "log" -- "os" -- "unicode/utf8" +- "sort" +- "testing" - -- "golang.org/x/tools/gopls/internal/lsp" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" +- "github.com/google/go-cmp/cmp" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/cache/typerefs" +- "golang.org/x/tools/gopls/internal/protocol" -) - --// generate semantic tokens and interpolate them in the file -- --// The output is the input file decorated with comments showing the --// syntactic tokens. The comments are stylized: --// /*,,[ is the length of the token in runes, is one --// of the supported semantic token types, and " } --func (c *semtok) ShortHelp() string { return "show semantic tokens for the specified file" } --func (c *semtok) DetailedHelp(f *flag.FlagSet) { -- fmt.Fprint(f.Output(), ` --Example: show the semantic tokens for this file: +- tests := []struct { +- label string +- srcs []string // source for the local package; package name must be p +- imports map[string]string // for simplicity: importPath -> pkgID/pkgName (we set pkgName == pkgID); 'ext' is always available. +- want map[string][]string // decl name -> id. +- allowErrs bool // whether we expect parsing errors +- }{ +- { +- label: "empty package", +- want: map[string][]string{}, +- }, +- { +- label: "fields", +- srcs: []string{` +-package p - -- $ gopls semtok internal/lsp/cmd/semtok.go --`) -- printFlagDefaults(f) --} +-import "ext" - --// Run performs the semtok on the files specified by args and prints the --// results to stdout in the format described above. --func (c *semtok) Run(ctx context.Context, args ...string) error { -- if len(args) != 1 { -- return fmt.Errorf("expected one file name, got %d", len(args)) -- } -- // perhaps simpler if app had just had a FlagSet member -- origOptions := c.app.options -- c.app.options = func(opts *source.Options) { -- origOptions(opts) -- opts.SemanticTokens = true -- } -- conn, err := c.app.connect(ctx, nil) -- if err != nil { -- return err -- } -- defer conn.terminate(ctx) -- uri := span.URIFromPath(args[0]) -- file, err := conn.openFile(ctx, uri) -- if err != nil { -- return err -- } +-type A struct{ b B } +-type B func(c C) (d D) +-type C ext.C +-type D ext.D - -- buf, err := os.ReadFile(args[0]) -- if err != nil { -- return err -- } -- lines := bytes.Split(buf, []byte{'\n'}) -- p := &protocol.SemanticTokensRangeParams{ -- TextDocument: protocol.TextDocumentIdentifier{ -- URI: protocol.URIFromSpanURI(uri), -- }, -- Range: protocol.Range{Start: protocol.Position{Line: 0, Character: 0}, -- End: protocol.Position{ -- Line: uint32(len(lines) - 1), -- Character: uint32(len(lines[len(lines)-1]))}, +-// Should not be referenced by field names. +-type b ext.B_ +-type c int.C_ +-type d ext.D_ +-`}, +- want: map[string][]string{ +- "A": {"ext.C", "ext.D"}, +- "B": {"ext.C", "ext.D"}, +- "C": {"ext.C"}, +- "D": {"ext.D"}, +- }, - }, -- } -- resp, err := conn.semanticTokens(ctx, p) -- if err != nil { -- return err -- } -- fset := token.NewFileSet() -- f, err := parser.ParseFile(fset, args[0], buf, 0) -- if err != nil { -- log.Printf("parsing %s failed %v", args[0], err) -- return err -- } -- tok := fset.File(f.Pos()) -- if tok == nil { -- // can't happen; just parsed this file -- return fmt.Errorf("can't find %s in fset", args[0]) -- } -- colmap = protocol.NewMapper(uri, buf) -- err = decorate(file.uri.Filename(), resp.Data) -- if err != nil { -- return err -- } -- return nil --} -- --type mark struct { -- line, offset int // 1-based, from RangeSpan -- len int // bytes, not runes -- typ string -- mods []string --} +- { +- label: "embedding", +- srcs: []string{` +-package p - --// prefixes for semantic token comments --const ( -- SemanticLeft = "/*⇐" -- SemanticRight = "/*⇒" --) +-import "ext" - --func markLine(m mark, lines [][]byte) { -- l := lines[m.line-1] // mx is 1-based -- length := utf8.RuneCount(l[m.offset-1 : m.offset-1+m.len]) -- splitAt := m.offset - 1 -- insert := "" -- if m.typ == "namespace" && m.offset-1+m.len < len(l) && l[m.offset-1+m.len] == '"' { -- // it is the last component of an import spec -- // cannot put a comment inside a string -- insert = fmt.Sprintf("%s%d,namespace,[]*/", SemanticLeft, length) -- splitAt = m.offset + m.len -- } else { -- // be careful not to generate //* -- spacer := "" -- if splitAt-1 >= 0 && l[splitAt-1] == '/' { -- spacer = " " -- } -- insert = fmt.Sprintf("%s%s%d,%s,%v*/", spacer, SemanticRight, length, m.typ, m.mods) +-type A struct{ +- B +- _ struct { +- C - } -- x := append([]byte(insert), l[splitAt:]...) -- l = append(l[:splitAt], x...) -- lines[m.line-1] = l +- D -} -- --func decorate(file string, result []uint32) error { -- buf, err := os.ReadFile(file) -- if err != nil { -- return err -- } -- marks := newMarks(result) -- if len(marks) == 0 { -- return nil -- } -- lines := bytes.Split(buf, []byte{'\n'}) -- for i := len(marks) - 1; i >= 0; i-- { -- mx := marks[i] -- markLine(mx, lines) -- } -- os.Stdout.Write(bytes.Join(lines, []byte{'\n'})) -- return nil +-type B ext.B +-type C ext.C +-type D interface{ +- B -} -- --func newMarks(d []uint32) []mark { -- ans := []mark{} -- // the following two loops could be merged, at the cost -- // of making the logic slightly more complicated to understand -- // first, convert from deltas to absolute, in LSP coordinates -- lspLine := make([]uint32, len(d)/5) -- lspChar := make([]uint32, len(d)/5) -- var line, char uint32 -- for i := 0; 5*i < len(d); i++ { -- lspLine[i] = line + d[5*i+0] -- if d[5*i+0] > 0 { -- char = 0 -- } -- lspChar[i] = char + d[5*i+1] -- char = lspChar[i] -- line = lspLine[i] -- } -- // second, convert to gopls coordinates -- for i := 0; 5*i < len(d); i++ { -- pr := protocol.Range{ -- Start: protocol.Position{ -- Line: lspLine[i], -- Character: lspChar[i], -- }, -- End: protocol.Position{ -- Line: lspLine[i], -- Character: lspChar[i] + d[5*i+2], +-`}, +- want: map[string][]string{ +- "A": {"ext.B", "ext.C"}, +- "B": {"ext.B"}, +- "C": {"ext.C"}, +- "D": {"ext.B"}, - }, -- } -- spn, err := colmap.RangeSpan(pr) -- if err != nil { -- log.Fatal(err) -- } -- m := mark{ -- line: spn.Start().Line(), -- offset: spn.Start().Column(), -- len: spn.End().Column() - spn.Start().Column(), -- typ: lsp.SemType(int(d[5*i+3])), -- mods: lsp.SemMods(int(d[5*i+4])), -- } -- ans = append(ans, m) -- } -- return ans --} -diff -urN a/gopls/internal/lsp/cmd/serve.go b/gopls/internal/lsp/cmd/serve.go ---- a/gopls/internal/lsp/cmd/serve.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/serve.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,146 +0,0 @@ --// Copyright 2018 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package cmd -- --import ( -- "context" -- "errors" -- "flag" -- "fmt" -- "io" -- "log" -- "os" -- "time" -- -- "golang.org/x/tools/gopls/internal/lsp/cache" -- "golang.org/x/tools/gopls/internal/lsp/debug" -- "golang.org/x/tools/gopls/internal/lsp/lsprpc" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/internal/fakenet" -- "golang.org/x/tools/internal/jsonrpc2" -- "golang.org/x/tools/internal/tool" --) -- --// Serve is a struct that exposes the configurable parts of the LSP server as --// flags, in the right form for tool.Main to consume. --type Serve struct { -- Logfile string `flag:"logfile" help:"filename to log to. if value is \"auto\", then logging to a default output file is enabled"` -- Mode string `flag:"mode" help:"no effect"` -- Port int `flag:"port" help:"port on which to run gopls for debugging purposes"` -- Address string `flag:"listen" help:"address on which to listen for remote connections. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. Otherwise, TCP is used."` -- IdleTimeout time.Duration `flag:"listen.timeout" help:"when used with -listen, shut down the server when there are no connected clients for this duration"` -- Trace bool `flag:"rpc.trace" help:"print the full rpc trace in lsp inspector format"` -- Debug string `flag:"debug" help:"serve debug information on the supplied address"` +- }, +- { +- label: "constraint embedding", +- srcs: []string{` +-package p - -- RemoteListenTimeout time.Duration `flag:"remote.listen.timeout" help:"when used with -remote=auto, the -listen.timeout value used to start the daemon"` -- RemoteDebug string `flag:"remote.debug" help:"when used with -remote=auto, the -debug value used to start the daemon"` -- RemoteLogfile string `flag:"remote.logfile" help:"when used with -remote=auto, the -logfile value used to start the daemon"` +-import "ext" - -- app *Application +-type A interface{ +- int | B | ~C +- struct{D} -} - --func (s *Serve) Name() string { return "serve" } --func (s *Serve) Parent() string { return s.app.Name() } --func (s *Serve) Usage() string { return "[server-flags]" } --func (s *Serve) ShortHelp() string { -- return "run a server for Go code using the Language Server Protocol" --} --func (s *Serve) DetailedHelp(f *flag.FlagSet) { -- fmt.Fprint(f.Output(), ` gopls [flags] [server-flags] +-type B ext.B +-type C ext.C +-type D ext.D +-`}, +- want: map[string][]string{ +- "A": {"ext.B", "ext.C", "ext.D"}, +- "B": {"ext.B"}, +- "C": {"ext.C"}, +- "D": {"ext.D"}, +- }, +- }, +- { +- label: "funcs", +- srcs: []string{` +-package p - --The server communicates using JSONRPC2 on stdin and stdout, and is intended to be run directly as --a child of an editor process. +-import "ext" - --server-flags: --`) -- printFlagDefaults(f) +-type A ext.A +-type B ext.B +-const C B = 2 +-func F(A) B { +- return C -} +-var V = F(W) +-var W A +-`}, +- want: map[string][]string{ +- "A": {"ext.A"}, +- "B": {"ext.B"}, +- "C": {"ext.B"}, +- "F": {"ext.A", "ext.B"}, +- "V": { +- "ext.A", // via F +- "ext.B", // via W: can't be eliminated: F could be builtin or generic +- }, +- "W": {"ext.A"}, +- }, +- }, +- { +- label: "methods", +- srcs: []string{`package p - --func (s *Serve) remoteArgs(network, address string) []string { -- args := []string{"serve", -- "-listen", fmt.Sprintf(`%s;%s`, network, address), -- } -- if s.RemoteDebug != "" { -- args = append(args, "-debug", s.RemoteDebug) -- } -- if s.RemoteListenTimeout != 0 { -- args = append(args, "-listen.timeout", s.RemoteListenTimeout.String()) -- } -- if s.RemoteLogfile != "" { -- args = append(args, "-logfile", s.RemoteLogfile) -- } -- return args --} +-import "ext" - --// Run configures a server based on the flags, and then runs it. --// It blocks until the server shuts down. --func (s *Serve) Run(ctx context.Context, args ...string) error { -- if len(args) > 0 { -- return tool.CommandLineErrorf("server does not take arguments, got %v", args) -- } +-type A ext.A +-type B ext.B +-`, `package p - -- di := debug.GetInstance(ctx) -- isDaemon := s.Address != "" || s.Port != 0 -- if di != nil { -- closeLog, err := di.SetLogFile(s.Logfile, isDaemon) -- if err != nil { -- return err -- } -- defer closeLog() -- di.ServerAddress = s.Address -- di.Serve(ctx, s.Debug) -- } -- var ss jsonrpc2.StreamServer -- if s.app.Remote != "" { -- var err error -- ss, err = lsprpc.NewForwarder(s.app.Remote, s.remoteArgs) -- if err != nil { -- return fmt.Errorf("creating forwarder: %w", err) -- } -- } else { -- ss = lsprpc.NewStreamServer(cache.New(nil), isDaemon, s.app.options) -- } +-func (A) M(B) +-func (*B) M(A) +-`}, +- want: map[string][]string{ +- "A": {"ext.A", "ext.B"}, +- "B": {"ext.A", "ext.B"}, +- }, +- }, +- { +- label: "initializers", +- srcs: []string{` +-package p - -- var network, addr string -- if s.Address != "" { -- network, addr = lsprpc.ParseAddr(s.Address) -- } -- if s.Port != 0 { -- network = "tcp" -- // TODO(adonovan): should gopls ever be listening on network -- // sockets, or only local ones? -- // -- // Ian says this was added in anticipation of -- // something related to "VS Code remote" that turned -- // out to be unnecessary. So I propose we limit it to -- // localhost, if only so that we avoid the macOS -- // firewall prompt. -- // -- // Hana says: "s.Address is for the remote access (LSP) -- // and s.Port is for debugging purpose (according to -- // the Server type documentation). I am not sure why the -- // existing code here is mixing up and overwriting addr. -- // For debugging endpoint, I think localhost makes perfect sense." -- // -- // TODO(adonovan): disentangle Address and Port, -- // and use only localhost for the latter. -- addr = fmt.Sprintf(":%v", s.Port) -- } -- if addr != "" { -- log.Printf("Gopls daemon: listening on %s network, address %s...", network, addr) -- defer log.Printf("Gopls daemon: exiting") -- return jsonrpc2.ListenAndServe(ctx, network, addr, ss, s.IdleTimeout) -- } -- stream := jsonrpc2.NewHeaderStream(fakenet.NewConn("stdio", os.Stdin, os.Stdout)) -- if s.Trace && di != nil { -- stream = protocol.LoggingStream(stream, di.LogWriter) -- } -- conn := jsonrpc2.NewConn(stream) -- err := ss.ServeStream(ctx, conn) -- if errors.Is(err, io.EOF) { -- return nil -- } -- return err --} -diff -urN a/gopls/internal/lsp/cmd/signature.go b/gopls/internal/lsp/cmd/signature.go ---- a/gopls/internal/lsp/cmd/signature.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/signature.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,88 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-import "ext" - --package cmd +-var A b = C // type does not depend on C +-type b ext.B +-var C = d // type does depend on D +-var d b - --import ( -- "context" -- "flag" -- "fmt" +-var e = d + a - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/tool" --) +-var F = func() B { return E } - --// signature implements the signature verb for gopls --type signature struct { -- app *Application --} +-var G = struct{ +- A b +- _ [unsafe.Sizeof(ext.V)]int // array size + Sizeof creates edge to a var +- _ [unsafe.Sizeof(G)]int // creates a self edge; doesn't affect output though +-}{} - --func (r *signature) Name() string { return "signature" } --func (r *signature) Parent() string { return r.app.Name() } --func (r *signature) Usage() string { return "" } --func (r *signature) ShortHelp() string { return "display selected identifier's signature" } --func (r *signature) DetailedHelp(f *flag.FlagSet) { -- fmt.Fprint(f.Output(), ` --Example: +-var H = (D + A + C*C) - -- $ # 1-indexed location (:line:column or :#offset) of the target identifier -- $ gopls signature helper/helper.go:8:6 -- $ gopls signature helper/helper.go:#53 --`) -- printFlagDefaults(f) --} +-var I = (A+C).F +-`}, +- want: map[string][]string{ +- "A": {"ext.B"}, +- "C": {"ext.B"}, // via d +- "G": {"ext.B", "ext.V"}, // via b,C +- "H": {"ext.B"}, // via d,A,C +- "I": {"ext.B"}, +- }, +- }, +- { +- label: "builtins", +- srcs: []string{`package p - --func (r *signature) Run(ctx context.Context, args ...string) error { -- if len(args) != 1 { -- return tool.CommandLineErrorf("signature expects 1 argument (position)") -- } +-import "ext" - -- conn, err := r.app.connect(ctx, nil) -- if err != nil { -- return err -- } -- defer conn.terminate(ctx) +-var A = new(b) +-type b struct{ ext.B } - -- from := span.Parse(args[0]) -- file, err := conn.openFile(ctx, from.URI()) -- if err != nil { -- return err -- } +-type C chan d +-type d ext.D - -- loc, err := file.mapper.SpanLocation(from) -- if err != nil { -- return err -- } +-type S []ext.S +-type t ext.T +-var U = append(([]*S)(nil), new(t)) - -- p := protocol.SignatureHelpParams{ -- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), -- } +-type X map[k]v +-type k ext.K +-type v ext.V - -- s, err := conn.SignatureHelp(ctx, &p) -- if err != nil { -- return err -- } +-var Z = make(map[k]A) - -- if s == nil || len(s.Signatures) == 0 { -- return tool.CommandLineErrorf("%v: not a function", from) -- } +-// close, delete, and panic cannot occur outside of statements +-`}, +- want: map[string][]string{ +- "A": {"ext.B"}, +- "C": {"ext.D"}, +- "S": {"ext.S"}, +- "U": {"ext.S", "ext.T"}, // ext.T edge could be eliminated +- "X": {"ext.K", "ext.V"}, +- "Z": {"ext.B", "ext.K"}, +- }, +- }, +- { +- label: "builtin shadowing", +- srcs: []string{`package p - -- // there is only ever one possible signature, -- // see toProtocolSignatureHelp in lsp/signature_help.go -- signature := s.Signatures[0] -- fmt.Printf("%s\n", signature.Label) -- switch x := signature.Documentation.Value.(type) { -- case string: -- if x != "" { -- fmt.Printf("\n%s\n", x) -- } -- case protocol.MarkupContent: -- if x.Value != "" { -- fmt.Printf("\n%s\n", x.Value) -- } +-import "ext" +- +-var A = new(ext.B) +-func new() c +-type c ext.C +-`}, +- want: map[string][]string{ +- "A": {"ext.B", "ext.C"}, +- }, +- }, +- { +- label: "named forwarding", +- srcs: []string{`package p +- +-import "ext" +- +-type A B +-type B c +-type c ext.C +-`}, +- want: map[string][]string{ +- "A": {"ext.C"}, +- "B": {"ext.C"}, +- }, +- }, +- { +- label: "aliases", +- srcs: []string{`package p +- +-import "ext" +- +-type A = B +-type B = C +-type C = ext.C +-`}, +- want: map[string][]string{ +- "A": {"ext.C"}, +- "B": {"ext.C"}, +- "C": {"ext.C"}, +- }, +- }, +- { +- label: "array length", +- srcs: []string{`package p +- +-import "ext" +-import "unsafe" +- +-type A [unsafe.Sizeof(ext.B{ext.C})]int +-type A2 [unsafe.Sizeof(ext.B{f:ext.C})]int // use a KeyValueExpr +- +-type D [unsafe.Sizeof(struct{ f E })]int +-type E ext.E +- +-type F [3]G +-type G [ext.C]int +-`}, +- want: map[string][]string{ +- "A": {"ext.B"}, // no ext.C: doesn't enter CompLit +- "A2": {"ext.B"}, // ditto +- "D": {"ext.E"}, +- "E": {"ext.E"}, +- "F": {"ext.C"}, +- "G": {"ext.C"}, +- }, +- }, +- { +- label: "imports", +- srcs: []string{`package p +- +-import "ext" +- +-import ( +- "q" +- r2 "r" +- "s" // note: package name is t +- "z" +-) +- +-type A struct { +- q.Q +- r2.R +- s.S // invalid ref +- z.Z // references both external z.Z as well as package-level type z +-} +- +-type B struct { +- r.R // invalid ref +- t.T +-} +- +-var X int = q.V // X={}: no descent into RHS of 'var v T = rhs' +-var Y = q.V.W +- +-type z ext.Z +-`}, +- imports: map[string]string{"q": "q", "r": "r", "s": "t", "z": "z"}, +- want: map[string][]string{ +- "A": {"ext.Z", "q.Q", "r.R", "z.Z"}, +- "B": {"t.T"}, +- "Y": {"q.V"}, +- }, +- }, +- { +- label: "import blank", +- srcs: []string{`package p +- +-import _ "q" +- +-type A q.Q +-`}, +- imports: map[string]string{"q": "q"}, +- want: map[string][]string{}, +- }, +- { +- label: "import dot", +- srcs: []string{`package p +- +-import . "q" +- +-type A q.Q // not actually an edge, since q is imported . +-type B struct { +- C // assumed to be an edge to q +- D // resolved to package decl +-} +- +- +-type E error // unexported, therefore must be universe.error +-type F Field +-var G = Field.X +-`, `package p +- +-import "ext" +-import "q" +- +-type D ext.D +-`}, +- imports: map[string]string{"q": "q"}, +- want: map[string][]string{ +- "B": {"ext.D", "q.C"}, +- "D": {"ext.D"}, +- "F": {"q.Field"}, +- "G": {"q.Field"}, +- }, +- }, +- { +- label: "typeparams", +- srcs: []string{`package p +- +-import "ext" +- +-type A[T any] struct { +- t T +- b B +-} +- +-type B ext.B +- +-func F1[T any](T, B) +-func F2[T C]()(T, B) +- +-type T ext.T +- +-type C ext.C +- +-func F3[T1 ~[]T2, T2 ~[]T3](t1 T1, t2 T2) +-type T3 ext.T3 +-`, `package p +- +-func (A[B]) M(C) {} +-`}, +- want: map[string][]string{ +- "A": {"ext.B", "ext.C"}, +- "B": {"ext.B"}, +- "C": {"ext.C"}, +- "F1": {"ext.B"}, +- "F2": {"ext.B", "ext.C"}, +- "F3": {"ext.T3"}, +- "T": {"ext.T"}, +- "T3": {"ext.T3"}, +- }, +- }, +- { +- label: "instances", +- srcs: []string{`package p +- +-import "ext" +- +-type A[T any] ext.A +-type B[T1, T2 any] ext.B +- +-type C A[int] +-type D B[int, A[E]] +-type E ext.E +-`}, +- want: map[string][]string{ +- "A": {"ext.A"}, +- "B": {"ext.B"}, +- "C": {"ext.A"}, +- "D": {"ext.A", "ext.B", "ext.E"}, +- "E": {"ext.E"}, +- }, +- }, +- { +- label: "duplicate decls", +- srcs: []string{`package p +- +-import "a" +-import "ext" +- +-type a a.A +-type A a +-type b ext.B +-type C a.A +-func (C) Foo(x) {} // invalid parameter, but that does not matter +-type C b +-func (C) Bar(y) {} // invalid parameter, but that does not matter +- +-var x ext.X +-var y ext.Y +-`}, +- imports: map[string]string{"a": "a", "b": "b"}, // "b" import should not matter, since it isn't in this file +- want: map[string][]string{ +- "A": {"a.A"}, +- "C": {"a.A", "ext.B", "ext.X", "ext.Y"}, +- }, +- }, +- { +- label: "invalid decls", +- srcs: []string{`package p +- +-import "ext" +- +-type A B +- +-func () Foo(B){} +- +-var B struct{ ext.B +-`}, +- want: map[string][]string{ +- "A": {"ext.B"}, +- "B": {"ext.B"}, +- "Foo": {"ext.B"}, +- }, +- allowErrs: true, +- }, +- { +- label: "unmapped receiver", +- srcs: []string{`package p +- +-type P struct{} +- +-func (a) x(P) +-`}, +- want: map[string][]string{}, +- allowErrs: true, +- }, +- { +- label: "SCC special case", +- srcs: []string{`package p +- +-import "ext" +- +-type X Y +-type Y struct { Z; *X } +-type Z map[ext.A]ext.B +-`}, +- want: map[string][]string{ +- "X": {"ext.A", "ext.B"}, +- "Y": {"ext.A", "ext.B"}, +- "Z": {"ext.A", "ext.B"}, +- }, +- allowErrs: true, +- }, - } - -- return nil +- for _, test := range tests { +- t.Run(test.label, func(t *testing.T) { +- var pgfs []*parsego.File +- for i, src := range test.srcs { +- uri := protocol.DocumentURI(fmt.Sprintf("file:///%d.go", i)) +- pgf, _ := parsego.Parse(ctx, token.NewFileSet(), uri, []byte(src), parsego.Full, false) +- if !test.allowErrs && pgf.ParseErr != nil { +- t.Fatalf("ParseGoSrc(...) returned parse errors: %v", pgf.ParseErr) +- } +- pgfs = append(pgfs, pgf) +- } +- +- imports := map[metadata.ImportPath]*metadata.Package{ +- "ext": {ID: "ext", Name: "ext"}, // this one comes for free +- } +- for path, mp := range test.imports { +- imports[metadata.ImportPath(path)] = &metadata.Package{ +- ID: metadata.PackageID(mp), +- Name: metadata.PackageName(mp), +- } +- } +- +- data := typerefs.Encode(pgfs, imports) +- +- got := make(map[string][]string) +- index := typerefs.NewPackageIndex() +- for _, class := range typerefs.Decode(index, data) { +- // We redundantly expand out the name x refs cross product +- // here since that's what the existing tests expect. +- for _, name := range class.Decls { +- var syms []string +- for _, sym := range class.Refs { +- syms = append(syms, fmt.Sprintf("%s.%s", index.DeclaringPackage(sym), sym.Name)) +- } +- sort.Strings(syms) +- got[name] = syms +- } +- } +- +- if diff := cmp.Diff(test.want, got); diff != "" { +- t.Errorf("Refs(...) returned unexpected refs (-want +got):\n%s", diff) +- } +- }) +- } -} -diff -urN a/gopls/internal/lsp/cmd/stats.go b/gopls/internal/lsp/cmd/stats.go ---- a/gopls/internal/lsp/cmd/stats.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/stats.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,274 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/cache/view.go b/gopls/internal/cache/view.go +--- a/gopls/internal/cache/view.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/view.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,1299 +0,0 @@ +-// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package cmd +-// Package cache is the core of gopls: it is concerned with state +-// management, dependency analysis, and invalidation; and it holds the +-// machinery of type checking and modular static analysis. Its +-// principal types are [Session], [Folder], [View], [Snapshot], +-// [Cache], and [Package]. +-package cache - -import ( +- "bytes" - "context" - "encoding/json" -- "flag" +- "errors" - "fmt" -- "go/token" -- "io/fs" - "os" +- "os/exec" +- "path" - "path/filepath" -- "reflect" -- "runtime" +- "regexp" +- "sort" - "strings" - "sync" - "time" - -- goplsbug "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp" -- "golang.org/x/tools/gopls/internal/lsp/command" -- "golang.org/x/tools/gopls/internal/lsp/debug" -- "golang.org/x/tools/gopls/internal/lsp/filecache" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/settings" +- "golang.org/x/tools/gopls/internal/util/maps" +- "golang.org/x/tools/gopls/internal/util/pathutil" +- "golang.org/x/tools/gopls/internal/util/slices" +- "golang.org/x/tools/gopls/internal/vulncheck" - "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/gocommand" +- "golang.org/x/tools/internal/imports" +- "golang.org/x/tools/internal/xcontext" -) - --type stats struct { -- app *Application +-// A Folder represents an LSP workspace folder, together with its per-folder +-// options and environment variables that affect build configuration. +-// +-// Folders (Name and Dir) are specified by the 'initialize' and subsequent +-// 'didChangeWorkspaceFolders' requests; their options come from +-// didChangeConfiguration. +-// +-// Folders must not be mutated, as they may be shared across multiple views. +-type Folder struct { +- Dir protocol.DocumentURI +- Name string // decorative name for UI; not necessarily unique +- Options *settings.Options +- Env *GoEnv +-} +- +-// GoEnv holds the environment variables and data from the Go command that is +-// required for operating on a workspace folder. +-type GoEnv struct { +- // Go environment variables. These correspond directly with the Go env var of +- // the same name. +- GOOS string +- GOARCH string +- GOCACHE string +- GOMODCACHE string +- GOPATH string +- GOPRIVATE string +- GOFLAGS string +- GO111MODULE string +- +- // Go version output. +- GoVersion int // The X in Go 1.X +- GoVersionOutput string // complete go version output +- +- // OS environment variables (notably not go env). +- GOWORK string +- GOPACKAGESDRIVER string +-} +- +-// View represents a single build for a workspace. +-// +-// A View is a logical build (the viewDefinition) along with a state of that +-// build (the Snapshot). +-type View struct { +- id string // a unique string to identify this View in (e.g.) serialized Commands - -- Anon bool `flag:"anon" help:"hide any fields that may contain user names, file names, or source code"` +- *viewDefinition // build configuration +- +- gocmdRunner *gocommand.Runner // limits go command concurrency +- +- // baseCtx is the context handed to NewView. This is the parent of all +- // background contexts created for this view. +- baseCtx context.Context +- +- importsState *importsState +- +- // parseCache holds an LRU cache of recently parsed files. +- parseCache *parseCache +- +- // fs is the file source used to populate this view. +- fs *overlayFS +- +- // ignoreFilter is used for fast checking of ignored files. +- ignoreFilter *ignoreFilter +- +- // cancelInitialWorkspaceLoad can be used to terminate the view's first +- // attempt at initialization. +- cancelInitialWorkspaceLoad context.CancelFunc +- +- snapshotMu sync.Mutex +- snapshot *Snapshot // latest snapshot; nil after shutdown has been called +- +- // initialWorkspaceLoad is closed when the first workspace initialization has +- // completed. If we failed to load, we only retry if the go.mod file changes, +- // to avoid too many go/packages calls. +- initialWorkspaceLoad chan struct{} +- +- // initializationSema is used limit concurrent initialization of snapshots in +- // the view. We use a channel instead of a mutex to avoid blocking when a +- // context is canceled. +- // +- // This field (along with snapshot.initialized) guards against duplicate +- // initialization of snapshots. Do not change it without adjusting snapshot +- // accordingly. +- initializationSema chan struct{} +- +- // Document filters are constructed once, in View.filterFunc. +- filterFuncOnce sync.Once +- _filterFunc func(protocol.DocumentURI) bool // only accessed by View.filterFunc -} - --func (s *stats) Name() string { return "stats" } --func (r *stats) Parent() string { return r.app.Name() } --func (s *stats) Usage() string { return "" } --func (s *stats) ShortHelp() string { return "print workspace statistics" } +-// definition implements the viewDefiner interface. +-func (v *View) definition() *viewDefinition { return v.viewDefinition } - --func (s *stats) DetailedHelp(f *flag.FlagSet) { -- fmt.Fprint(f.Output(), ` --Load the workspace for the current directory, and output a JSON summary of --workspace information relevant to performance. As a side effect, this command --populates the gopls file cache for the current workspace. +-// A viewDefinition is a logical build, i.e. configuration (Folder) along with +-// a build directory and possibly an environment overlay (e.g. GOWORK=off or +-// GOOS, GOARCH=...) to affect the build. +-// +-// This type is immutable, and compared to see if the View needs to be +-// reconstructed. +-// +-// Note: whenever modifying this type, also modify the equivalence relation +-// implemented by viewDefinitionsEqual. +-// +-// TODO(golang/go#57979): viewDefinition should be sufficient for running +-// go/packages. Enforce this in the API. +-type viewDefinition struct { +- folder *Folder // pointer comparison is OK, as any new Folder creates a new def - --By default, this command may include output that refers to the location or --content of user code. When the -anon flag is set, fields that may refer to user --code are hidden. +- typ ViewType +- root protocol.DocumentURI // root directory; where to run the Go command +- gomod protocol.DocumentURI // the nearest go.mod file, or "" +- gowork protocol.DocumentURI // the nearest go.work file, or "" - --Example: -- $ gopls stats -anon --`) -- printFlagDefaults(f) +- // workspaceModFiles holds the set of mod files active in this snapshot. +- // +- // For a go.work workspace, this is the set of workspace modfiles. For a +- // go.mod workspace, this contains the go.mod file defining the workspace +- // root, as well as any locally replaced modules (if +- // "includeReplaceInWorkspace" is set). +- // +- // TODO(rfindley): should we just run `go list -m` to compute this set? +- workspaceModFiles map[protocol.DocumentURI]struct{} +- workspaceModFilesErr error // error encountered computing workspaceModFiles +- +- // envOverlay holds additional environment to apply to this viewDefinition. +- envOverlay map[string]string -} - --func (s *stats) Run(ctx context.Context, args ...string) error { -- if s.app.Remote != "" { -- // stats does not work with -remote. -- // Other sessions on the daemon may interfere with results. -- // Additionally, the type assertions in below only work if progress -- // notifications bypass jsonrpc2 serialization. -- return fmt.Errorf("the stats subcommand does not work with -remote") -- } +-// definition implements the viewDefiner interface. +-func (d *viewDefinition) definition() *viewDefinition { return d } - -- if !s.app.Verbose { -- event.SetExporter(nil) // don't log errors to stderr -- } +-// Type returns the ViewType type, which determines how go/packages are loaded +-// for this View. +-func (d *viewDefinition) Type() ViewType { return d.typ } - -- stats := GoplsStats{ -- GOOS: runtime.GOOS, -- GOARCH: runtime.GOARCH, -- GOPLSCACHE: os.Getenv("GOPLSCACHE"), -- GoVersion: runtime.Version(), -- GoplsVersion: debug.Version(), -- GOPACKAGESDRIVER: os.Getenv("GOPACKAGESDRIVER"), +-// Root returns the view root, which determines where packages are loaded from. +-func (d *viewDefinition) Root() protocol.DocumentURI { return d.root } +- +-// GoMod returns the nearest go.mod file for this view's root, or "". +-func (d *viewDefinition) GoMod() protocol.DocumentURI { return d.gomod } +- +-// GoWork returns the nearest go.work file for this view's root, or "". +-func (d *viewDefinition) GoWork() protocol.DocumentURI { return d.gowork } +- +-// EnvOverlay returns a new sorted slice of environment variables (in the form +-// "k=v") for this view definition's env overlay. +-func (d *viewDefinition) EnvOverlay() []string { +- var env []string +- for k, v := range d.envOverlay { +- env = append(env, fmt.Sprintf("%s=%s", k, v)) - } +- sort.Strings(env) +- return env +-} - -- opts := s.app.options -- s.app.options = func(o *source.Options) { -- if opts != nil { -- opts(o) -- } -- o.VerboseWorkDoneProgress = true +-// GOOS returns the effective GOOS value for this view definition, accounting +-// for its env overlay. +-func (d *viewDefinition) GOOS() string { +- if goos, ok := d.envOverlay["GOOS"]; ok { +- return goos - } -- var ( -- iwlMu sync.Mutex -- iwlToken protocol.ProgressToken -- iwlDone = make(chan struct{}) -- ) +- return d.folder.Env.GOOS +-} - -- onProgress := func(p *protocol.ProgressParams) { -- switch v := p.Value.(type) { -- case *protocol.WorkDoneProgressBegin: -- if v.Title == lsp.DiagnosticWorkTitle(lsp.FromInitialWorkspaceLoad) { -- iwlMu.Lock() -- iwlToken = p.Token -- iwlMu.Unlock() -- } -- case *protocol.WorkDoneProgressEnd: -- iwlMu.Lock() -- tok := iwlToken -- iwlMu.Unlock() +-// GOOS returns the effective GOARCH value for this view definition, accounting +-// for its env overlay. +-func (d *viewDefinition) GOARCH() string { +- if goarch, ok := d.envOverlay["GOARCH"]; ok { +- return goarch +- } +- return d.folder.Env.GOARCH +-} - -- if p.Token == tok { -- close(iwlDone) -- } -- } +-// adjustedGO111MODULE is the value of GO111MODULE to use for loading packages. +-// It is adjusted to default to "auto" rather than "on", since if we are in +-// GOPATH and have no module, we may as well allow a GOPATH view to work. +-func (d viewDefinition) adjustedGO111MODULE() string { +- if d.folder.Env.GO111MODULE != "" { +- return d.folder.Env.GO111MODULE - } +- return "auto" +-} - -- // do executes a timed section of the stats command. -- do := func(name string, f func() error) (time.Duration, error) { -- start := time.Now() -- fmt.Fprintf(os.Stderr, "%-30s", name+"...") -- if err := f(); err != nil { -- return time.Since(start), err -- } -- d := time.Since(start) -- fmt.Fprintf(os.Stderr, "done (%v)\n", d) -- return d, nil +-// ModFiles are the go.mod files enclosed in the snapshot's view and known +-// to the snapshot. +-func (d viewDefinition) ModFiles() []protocol.DocumentURI { +- var uris []protocol.DocumentURI +- for modURI := range d.workspaceModFiles { +- uris = append(uris, modURI) - } +- return uris +-} - -- var conn *connection -- iwlDuration, err := do("Initializing workspace", func() error { -- var err error -- conn, err = s.app.connect(ctx, onProgress) -- if err != nil { -- return err +-// viewDefinitionsEqual reports whether x and y are equivalent. +-func viewDefinitionsEqual(x, y *viewDefinition) bool { +- if (x.workspaceModFilesErr == nil) != (y.workspaceModFilesErr == nil) { +- return false +- } +- if x.workspaceModFilesErr != nil { +- if x.workspaceModFilesErr.Error() != y.workspaceModFilesErr.Error() { +- return false - } -- select { -- case <-iwlDone: -- case <-ctx.Done(): -- return ctx.Err() +- } else if !maps.SameKeys(x.workspaceModFiles, y.workspaceModFiles) { +- return false +- } +- if len(x.envOverlay) != len(y.envOverlay) { +- return false +- } +- for i, xv := range x.envOverlay { +- if xv != y.envOverlay[i] { +- return false - } -- return nil -- }) -- stats.InitialWorkspaceLoadDuration = fmt.Sprint(iwlDuration) -- if err != nil { -- return err - } -- defer conn.terminate(ctx) +- return x.folder == y.folder && +- x.typ == y.typ && +- x.root == y.root && +- x.gomod == y.gomod && +- x.gowork == y.gowork +-} - -- // Gather bug reports produced by any process using -- // this executable and persisted in the cache. -- do("Gathering bug reports", func() error { -- stats.CacheDir, stats.BugReports = filecache.BugReports() -- if stats.BugReports == nil { -- stats.BugReports = []goplsbug.Bug{} // non-nil for JSON -- } -- return nil -- }) +-// A ViewType describes how we load package information for a view. +-// +-// This is used for constructing the go/packages.Load query, and for +-// interpreting missing packages, imports, or errors. +-// +-// See the documentation for individual ViewType values for details. +-type ViewType int - -- if _, err := do("Querying memstats", func() error { -- memStats, err := conn.ExecuteCommand(ctx, &protocol.ExecuteCommandParams{ -- Command: command.MemStats.ID(), -- }) -- if err != nil { -- return err -- } -- stats.MemStats = memStats.(command.MemStatsResult) -- return nil -- }); err != nil { -- return err +-const ( +- // GoPackagesDriverView is a view with a non-empty GOPACKAGESDRIVER +- // environment variable. +- // +- // Load: ./... from the workspace folder. +- GoPackagesDriverView ViewType = iota +- +- // GOPATHView is a view in GOPATH mode. +- // +- // I.e. in GOPATH, with GO111MODULE=off, or GO111MODULE=auto with no +- // go.mod file. +- // +- // Load: ./... from the workspace folder. +- GOPATHView +- +- // GoModView is a view in module mode with a single Go module. +- // +- // Load: /... from the module root. +- GoModView +- +- // GoWorkView is a view in module mode with a go.work file. +- // +- // Load: /... from the workspace folder, for each module. +- GoWorkView +- +- // An AdHocView is a collection of files in a given directory, not in GOPATH +- // or a module. +- // +- // Load: . from the workspace folder. +- AdHocView +-) +- +-func (t ViewType) String() string { +- switch t { +- case GoPackagesDriverView: +- return "GoPackagesDriverView" +- case GOPATHView: +- return "GOPATHView" +- case GoModView: +- return "GoModView" +- case GoWorkView: +- return "GoWorkView" +- case AdHocView: +- return "AdHocView" +- default: +- return "Unknown" - } +-} - -- if _, err := do("Querying workspace stats", func() error { -- wsStats, err := conn.ExecuteCommand(ctx, &protocol.ExecuteCommandParams{ -- Command: command.WorkspaceStats.ID(), -- }) -- if err != nil { -- return err -- } -- stats.WorkspaceStats = wsStats.(command.WorkspaceStatsResult) -- return nil -- }); err != nil { -- return err +-// moduleMode reports whether the view uses Go modules. +-func (w viewDefinition) moduleMode() bool { +- switch w.typ { +- case GoModView, GoWorkView: +- return true +- default: +- return false - } +-} - -- if _, err := do("Collecting directory info", func() error { -- var err error -- stats.DirStats, err = findDirStats(ctx) +-func (v *View) ID() string { return v.id } +- +-// tempModFile creates a temporary go.mod file based on the contents +-// of the given go.mod file. On success, it is the caller's +-// responsibility to call the cleanup function when the file is no +-// longer needed. +-func tempModFile(modURI protocol.DocumentURI, gomod, gosum []byte) (tmpURI protocol.DocumentURI, cleanup func(), err error) { +- filenameHash := file.HashOf([]byte(modURI.Path())) +- tmpMod, err := os.CreateTemp("", fmt.Sprintf("go.%s.*.mod", filenameHash)) +- if err != nil { +- return "", nil, err +- } +- defer tmpMod.Close() +- +- tmpURI = protocol.URIFromPath(tmpMod.Name()) +- tmpSumName := sumFilename(tmpURI) +- +- if _, err := tmpMod.Write(gomod); err != nil { +- return "", nil, err +- } +- +- // We use a distinct name here to avoid subtlety around the fact +- // that both 'return' and 'defer' update the "cleanup" variable. +- doCleanup := func() { +- _ = os.Remove(tmpSumName) +- _ = os.Remove(tmpURI.Path()) +- } +- +- // Be careful to clean up if we return an error from this function. +- defer func() { - if err != nil { -- return err +- doCleanup() +- cleanup = nil - } -- return nil -- }); err != nil { -- return err -- } +- }() - -- // Filter JSON output to fields that are consistent with s.Anon. -- okFields := make(map[string]interface{}) -- { -- v := reflect.ValueOf(stats) -- t := v.Type() -- for i := 0; i < t.NumField(); i++ { -- f := t.Field(i) -- if !token.IsExported(f.Name) { -- continue -- } -- vf := v.FieldByName(f.Name) -- if s.Anon && f.Tag.Get("anon") != "ok" && !vf.IsZero() { -- // Fields that can be served with -anon must be explicitly marked as OK. -- // But, if it's zero value, it's ok to print. -- continue -- } -- okFields[f.Name] = vf.Interface() +- // Create an analogous go.sum, if one exists. +- if gosum != nil { +- if err := os.WriteFile(tmpSumName, gosum, 0655); err != nil { +- return "", nil, err - } - } -- data, err := json.MarshalIndent(okFields, "", " ") +- +- return tmpURI, doCleanup, nil +-} +- +-// Folder returns the folder at the base of this view. +-func (v *View) Folder() *Folder { +- return v.folder +-} +- +-// UpdateFolders updates the set of views for the new folders. +-// +-// Calling this causes each view to be reinitialized. +-func (s *Session) UpdateFolders(ctx context.Context, newFolders []*Folder) error { +- s.viewMu.Lock() +- defer s.viewMu.Unlock() +- +- overlays := s.Overlays() +- var openFiles []protocol.DocumentURI +- for _, o := range overlays { +- openFiles = append(openFiles, o.URI()) +- } +- +- defs, err := selectViewDefs(ctx, s, newFolders, openFiles) - if err != nil { - return err - } -- -- os.Stdout.Write(data) -- fmt.Println() +- var newViews []*View +- for _, def := range defs { +- v, _, release := s.createView(ctx, def) +- release() +- newViews = append(newViews, v) +- } +- for _, v := range s.views { +- v.shutdown() +- } +- s.views = newViews - return nil -} - --// GoplsStats holds information extracted from a gopls session in the current --// workspace. +-// viewEnv returns a string describing the environment of a newly created view. -// --// Fields that should be printed with the -anon flag should be explicitly --// marked as `anon:"ok"`. Only fields that cannot refer to user files or code --// should be marked as such. --type GoplsStats struct { -- GOOS, GOARCH string `anon:"ok"` -- GOPLSCACHE string -- GoVersion string `anon:"ok"` -- GoplsVersion string `anon:"ok"` -- GOPACKAGESDRIVER string -- InitialWorkspaceLoadDuration string `anon:"ok"` // in time.Duration string form -- CacheDir string -- BugReports []goplsbug.Bug -- MemStats command.MemStatsResult `anon:"ok"` -- WorkspaceStats command.WorkspaceStatsResult `anon:"ok"` -- DirStats dirStats `anon:"ok"` +-// It must not be called concurrently with any other view methods. +-// TODO(rfindley): rethink this function, or inline sole call. +-func viewEnv(v *View) string { +- var buf bytes.Buffer +- fmt.Fprintf(&buf, `go info for %v +-(view type %v) +-(root dir %s) +-(go version %s) +-(build flags: %v) +-(go env: %+v) +-(env overlay: %v) +-`, +- v.folder.Dir.Path(), +- v.typ, +- v.root.Path(), +- strings.TrimRight(v.folder.Env.GoVersionOutput, "\n"), +- v.folder.Options.BuildFlags, +- *v.folder.Env, +- v.envOverlay, +- ) +- +- return buf.String() -} - --type dirStats struct { -- Files int -- TestdataFiles int -- GoFiles int -- ModFiles int -- Dirs int +-// RunProcessEnvFunc runs fn with the process env for this snapshot's view. +-// Note: the process env contains cached module and filesystem state. +-func (s *Snapshot) RunProcessEnvFunc(ctx context.Context, fn func(context.Context, *imports.Options) error) error { +- return s.view.importsState.runProcessEnvFunc(ctx, s, fn) -} - --// findDirStats collects information about the current directory and its --// subdirectories. --func findDirStats(ctx context.Context) (dirStats, error) { -- var ds dirStats -- filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error { +-// separated out from its sole use in locateTemplateFiles for testability +-func fileHasExtension(path string, suffixes []string) bool { +- ext := filepath.Ext(path) +- if ext != "" && ext[0] == '.' { +- ext = ext[1:] +- } +- for _, s := range suffixes { +- if s != "" && ext == s { +- return true +- } +- } +- return false +-} +- +-// locateTemplateFiles ensures that the snapshot has mapped template files +-// within the workspace folder. +-func (s *Snapshot) locateTemplateFiles(ctx context.Context) { +- suffixes := s.Options().TemplateExtensions +- if len(suffixes) == 0 { +- return +- } +- +- searched := 0 +- filterFunc := s.view.filterFunc() +- err := filepath.WalkDir(s.view.folder.Dir.Path(), func(path string, entry os.DirEntry, err error) error { - if err != nil { - return err - } -- if d.IsDir() { -- ds.Dirs++ -- } else { -- ds.Files++ -- slashed := filepath.ToSlash(path) -- switch { -- case strings.Contains(slashed, "/testdata/") || strings.HasPrefix(slashed, "testdata/"): -- ds.TestdataFiles++ -- case strings.HasSuffix(path, ".go"): -- ds.GoFiles++ -- case strings.HasSuffix(path, ".mod"): -- ds.ModFiles++ -- } +- if entry.IsDir() { +- return nil +- } +- if fileLimit > 0 && searched > fileLimit { +- return errExhausted +- } +- searched++ +- if !fileHasExtension(path, suffixes) { +- return nil +- } +- uri := protocol.URIFromPath(path) +- if filterFunc(uri) { +- return nil - } +- // Get the file in order to include it in the snapshot. +- // TODO(golang/go#57558): it is fundamentally broken to track files in this +- // way; we may lose them if configuration or layout changes cause a view to +- // be recreated. +- // +- // Furthermore, this operation must ignore errors, including context +- // cancellation, or risk leaving the snapshot in an undefined state. +- s.ReadFile(ctx, uri) - return nil - }) -- return ds, nil +- if err != nil { +- event.Error(ctx, "searching for template files failed", err) +- } -} -diff -urN a/gopls/internal/lsp/cmd/subcommands.go b/gopls/internal/lsp/cmd/subcommands.go ---- a/gopls/internal/lsp/cmd/subcommands.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/subcommands.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,59 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package cmd - --import ( -- "context" -- "flag" -- "fmt" -- "text/tabwriter" +-// filterFunc returns a func that reports whether uri is filtered by the currently configured +-// directoryFilters. +-func (v *View) filterFunc() func(protocol.DocumentURI) bool { +- v.filterFuncOnce.Do(func() { +- folderDir := v.folder.Dir.Path() +- gomodcache := v.folder.Env.GOMODCACHE +- var filters []string +- filters = append(filters, v.folder.Options.DirectoryFilters...) +- if pref := strings.TrimPrefix(gomodcache, folderDir); pref != gomodcache { +- modcacheFilter := "-" + strings.TrimPrefix(filepath.ToSlash(pref), "/") +- filters = append(filters, modcacheFilter) +- } +- filterer := NewFilterer(filters) +- v._filterFunc = func(uri protocol.DocumentURI) bool { +- // Only filter relative to the configured root directory. +- if pathutil.InDir(folderDir, uri.Path()) { +- return relPathExcludedByFilter(strings.TrimPrefix(uri.Path(), folderDir), filterer) +- } +- return false +- } +- }) +- return v._filterFunc +-} - -- "golang.org/x/tools/internal/tool" --) +-// shutdown releases resources associated with the view. +-func (v *View) shutdown() { +- // Cancel the initial workspace load if it is still running. +- v.cancelInitialWorkspaceLoad() - --// subcommands is a helper that may be embedded for commands that delegate to --// subcommands. --type subcommands []tool.Application +- v.snapshotMu.Lock() +- if v.snapshot != nil { +- v.snapshot.cancel() +- v.snapshot.decref() +- v.snapshot = nil +- } +- v.snapshotMu.Unlock() +-} - --func (s subcommands) DetailedHelp(f *flag.FlagSet) { -- w := tabwriter.NewWriter(f.Output(), 0, 0, 2, ' ', 0) -- defer w.Flush() -- fmt.Fprint(w, "\nSubcommand:\n") -- for _, c := range s { -- fmt.Fprintf(w, " %s\t%s\n", c.Name(), c.ShortHelp()) +-// IgnoredFile reports if a file would be ignored by a `go list` of the whole +-// workspace. +-// +-// While go list ./... skips directories starting with '.', '_', or 'testdata', +-// gopls may still load them via file queries. Explicitly filter them out. +-func (s *Snapshot) IgnoredFile(uri protocol.DocumentURI) bool { +- // Fast path: if uri doesn't contain '.', '_', or 'testdata', it is not +- // possible that it is ignored. +- { +- uriStr := string(uri) +- if !strings.Contains(uriStr, ".") && !strings.Contains(uriStr, "_") && !strings.Contains(uriStr, "testdata") { +- return false +- } - } -- printFlagDefaults(f) +- +- return s.view.ignoreFilter.ignored(uri.Path()) -} - --func (s subcommands) Usage() string { return " [arg]..." } +-// An ignoreFilter implements go list's exclusion rules via its 'ignored' method. +-type ignoreFilter struct { +- prefixes []string // root dirs, ending in filepath.Separator +-} - --func (s subcommands) Run(ctx context.Context, args ...string) error { -- if len(args) == 0 { -- return tool.CommandLineErrorf("must provide subcommand") +-// newIgnoreFilter returns a new ignoreFilter implementing exclusion rules +-// relative to the provided directories. +-func newIgnoreFilter(dirs []string) *ignoreFilter { +- f := new(ignoreFilter) +- for _, d := range dirs { +- f.prefixes = append(f.prefixes, filepath.Clean(d)+string(filepath.Separator)) - } -- command, args := args[0], args[1:] -- for _, c := range s { -- if c.Name() == command { -- s := flag.NewFlagSet(c.Name(), flag.ExitOnError) -- return tool.Run(ctx, s, c, args) +- return f +-} +- +-func (f *ignoreFilter) ignored(filename string) bool { +- for _, prefix := range f.prefixes { +- if suffix := strings.TrimPrefix(filename, prefix); suffix != filename { +- if checkIgnored(suffix) { +- return true +- } - } - } -- return tool.CommandLineErrorf("unknown subcommand %v", command) +- return false -} - --func (s subcommands) Commands() []tool.Application { return s } -- --// getSubcommands returns the subcommands of a given Application. --func getSubcommands(a tool.Application) []tool.Application { -- // This interface is satisfied both by tool.Applications -- // that embed subcommands, and by *cmd.Application. -- type hasCommands interface { -- Commands() []tool.Application -- } -- if sub, ok := a.(hasCommands); ok { -- return sub.Commands() +-// checkIgnored implements go list's exclusion rules. +-// Quoting “go help list”: +-// +-// Directory and file names that begin with "." or "_" are ignored +-// by the go tool, as are directories named "testdata". +-func checkIgnored(suffix string) bool { +- // Note: this could be further optimized by writing a HasSegment helper, a +- // segment-boundary respecting variant of strings.Contains. +- for _, component := range strings.Split(suffix, string(filepath.Separator)) { +- if len(component) == 0 { +- continue +- } +- if component[0] == '.' || component[0] == '_' || component == "testdata" { +- return true +- } - } -- return nil +- return false -} -diff -urN a/gopls/internal/lsp/cmd/suggested_fix.go b/gopls/internal/lsp/cmd/suggested_fix.go ---- a/gopls/internal/lsp/cmd/suggested_fix.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/suggested_fix.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,193 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package cmd +-// Snapshot returns the current snapshot for the view, and a +-// release function that must be called when the Snapshot is +-// no longer needed. +-// +-// The resulting error is non-nil if and only if the view is shut down, in +-// which case the resulting release function will also be nil. +-func (v *View) Snapshot() (*Snapshot, func(), error) { +- v.snapshotMu.Lock() +- defer v.snapshotMu.Unlock() +- if v.snapshot == nil { +- return nil, nil, errors.New("view is shutdown") +- } +- return v.snapshot, v.snapshot.Acquire(), nil +-} - --import ( -- "context" -- "flag" -- "fmt" +-// initialize loads the metadata (and currently, file contents, due to +-// golang/go#57558) for the main package query of the View, which depends on +-// the view type (see ViewType). If s.initialized is already true, initialize +-// is a no op. +-// +-// The first attempt--which populates the first snapshot for a new view--must +-// be allowed to run to completion without being cancelled. +-// +-// Subsequent attempts are triggered by conditions where gopls can't enumerate +-// specific packages that require reloading, such as a change to a go.mod file. +-// These attempts may be cancelled, and then retried by a later call. +-// +-// Postcondition: if ctx was not cancelled, s.initialized is true, s.initialErr +-// holds the error resulting from initialization, if any, and s.metadata holds +-// the resulting metadata graph. +-func (s *Snapshot) initialize(ctx context.Context, firstAttempt bool) { +- // Acquire initializationSema, which is +- // (in effect) a mutex with a timeout. +- select { +- case <-ctx.Done(): +- return +- case s.view.initializationSema <- struct{}{}: +- } - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/tool" --) +- defer func() { +- <-s.view.initializationSema +- }() - --// TODO(adonovan): this command has a very poor user interface. It --// should have a way to query the available fixes for a file (without --// a span), enumerate the valid fix kinds, enable all fixes, and not --// require the pointless -all flag. See issue #60290. +- s.mu.Lock() +- initialized := s.initialized +- s.mu.Unlock() - --// suggestedFix implements the fix verb for gopls. --type suggestedFix struct { -- EditFlags -- All bool `flag:"a,all" help:"apply all fixes, not just preferred fixes"` +- if initialized { +- return +- } - -- app *Application --} +- defer func() { +- if firstAttempt { +- close(s.view.initialWorkspaceLoad) +- } +- }() - --func (s *suggestedFix) Name() string { return "fix" } --func (s *suggestedFix) Parent() string { return s.app.Name() } --func (s *suggestedFix) Usage() string { return "[fix-flags] " } --func (s *suggestedFix) ShortHelp() string { return "apply suggested fixes" } --func (s *suggestedFix) DetailedHelp(f *flag.FlagSet) { -- fmt.Fprintf(f.Output(), ` --Example: apply fixes to this file, rewriting it: +- // TODO(rFindley): we should only locate template files on the first attempt, +- // or guard it via a different mechanism. +- s.locateTemplateFiles(ctx) - -- $ gopls fix -a -w internal/lsp/cmd/check.go +- // Collect module paths to load by parsing go.mod files. If a module fails to +- // parse, capture the parsing failure as a critical diagnostic. +- var scopes []loadScope // scopes to load +- var modDiagnostics []*Diagnostic // diagnostics for broken go.mod files +- addError := func(uri protocol.DocumentURI, err error) { +- modDiagnostics = append(modDiagnostics, &Diagnostic{ +- URI: uri, +- Severity: protocol.SeverityError, +- Source: ListError, +- Message: err.Error(), +- }) +- } - --The -a (-all) flag causes all fixes, not just preferred ones, to be --applied, but since no fixes are currently preferred, this flag is --essentially mandatory. +- if len(s.view.workspaceModFiles) > 0 { +- for modURI := range s.view.workspaceModFiles { +- // Verify that the modfile is valid before trying to load it. +- // +- // TODO(rfindley): now that we no longer need to parse the modfile in +- // order to load scope, we could move these diagnostics to a more general +- // location where we diagnose problems with modfiles or the workspace. +- // +- // Be careful not to add context cancellation errors as critical module +- // errors. +- fh, err := s.ReadFile(ctx, modURI) +- if err != nil { +- if ctx.Err() != nil { +- return +- } +- addError(modURI, err) +- continue +- } +- parsed, err := s.ParseMod(ctx, fh) +- if err != nil { +- if ctx.Err() != nil { +- return +- } +- addError(modURI, err) +- continue +- } +- if parsed.File == nil || parsed.File.Module == nil { +- addError(modURI, fmt.Errorf("no module path for %s", modURI)) +- continue +- } +- moduleDir := filepath.Dir(modURI.Path()) +- // Previously, we loaded /... for each module path, but that +- // is actually incorrect when the pattern may match packages in more than +- // one module. See golang/go#59458 for more details. +- scopes = append(scopes, moduleLoadScope{dir: moduleDir, modulePath: parsed.File.Module.Mod.Path}) +- } +- } else { +- scopes = append(scopes, viewLoadScope{}) +- } - --Arguments after the filename are interpreted as LSP CodeAction kinds --to be applied; the default set is {"quickfix"}, but valid kinds include: +- // If we're loading anything, ensure we also load builtin, +- // since it provides fake definitions (and documentation) +- // for types like int that are used everywhere. +- if len(scopes) > 0 { +- scopes = append(scopes, packageLoadScope("builtin")) +- } +- loadErr := s.load(ctx, true, scopes...) - -- quickfix -- refactor -- refactor.extract -- refactor.inline -- refactor.rewrite -- source.organizeImports -- source.fixAll +- // A failure is retryable if it may have been due to context cancellation, +- // and this is not the initial workspace load (firstAttempt==true). +- // +- // The IWL runs on a detached context with a long (~10m) timeout, so +- // if the context was canceled we consider loading to have failed +- // permanently. +- if loadErr != nil && ctx.Err() != nil && !firstAttempt { +- return +- } - --CodeAction kinds are hierarchical, so "refactor" includes --"refactor.inline". There is currently no way to enable or even --enumerate all kinds. +- var initialErr *InitializationError +- switch { +- case loadErr != nil && ctx.Err() != nil: +- event.Error(ctx, fmt.Sprintf("initial workspace load: %v", loadErr), loadErr) +- initialErr = &InitializationError{ +- MainError: loadErr, +- } +- case loadErr != nil: +- event.Error(ctx, "initial workspace load failed", loadErr) +- extractedDiags := s.extractGoCommandErrors(ctx, loadErr) +- initialErr = &InitializationError{ +- MainError: loadErr, +- Diagnostics: maps.Group(extractedDiags, byURI), +- } +- case s.view.workspaceModFilesErr != nil: +- initialErr = &InitializationError{ +- MainError: s.view.workspaceModFilesErr, +- } +- case len(modDiagnostics) > 0: +- initialErr = &InitializationError{ +- MainError: fmt.Errorf(modDiagnostics[0].Message), +- } +- } - --Example: apply any "refactor.rewrite" fixes at the specific byte --offset within this file: +- s.mu.Lock() +- defer s.mu.Unlock() - -- $ gopls fix -a internal/lsp/cmd/check.go:#43 refactor.rewrite +- s.initialized = true +- s.initialErr = initialErr +-} - --fix-flags: --`) -- printFlagDefaults(f) +-// A StateChange describes external state changes that may affect a snapshot. +-// +-// By far the most common of these is a change to file state, but a query of +-// module upgrade information or vulnerabilities also affects gopls' behavior. +-type StateChange struct { +- Modifications []file.Modification // if set, the raw modifications originating this change +- Files map[protocol.DocumentURI]file.Handle +- ModuleUpgrades map[protocol.DocumentURI]map[string]string +- Vulns map[protocol.DocumentURI]*vulncheck.Result +- GCDetails map[metadata.PackageID]bool // package -> whether or not we want details -} - --// Run performs diagnostic checks on the file specified and either; --// - if -w is specified, updates the file in place; --// - if -d is specified, prints out unified diffs of the changes; or --// - otherwise, prints the new versions to stdout. --func (s *suggestedFix) Run(ctx context.Context, args ...string) error { -- if len(args) < 1 { -- return tool.CommandLineErrorf("fix expects at least 1 argument") -- } -- s.app.editFlags = &s.EditFlags -- conn, err := s.app.connect(ctx, nil) -- if err != nil { -- return err -- } -- defer conn.terminate(ctx) +-// InvalidateView processes the provided state change, invalidating any derived +-// results that depend on the changed state. +-// +-// The resulting snapshot is non-nil, representing the outcome of the state +-// change. The second result is a function that must be called to release the +-// snapshot when the snapshot is no longer needed. +-// +-// An error is returned if the given view is no longer active in the session. +-func (s *Session) InvalidateView(ctx context.Context, view *View, changed StateChange) (*Snapshot, func(), error) { +- s.viewMu.Lock() +- defer s.viewMu.Unlock() - -- from := span.Parse(args[0]) -- uri := from.URI() -- file, err := conn.openFile(ctx, uri) -- if err != nil { -- return err -- } -- rng, err := file.mapper.SpanRange(from) -- if err != nil { -- return err +- if !slices.Contains(s.views, view) { +- return nil, nil, fmt.Errorf("view is no longer active") - } +- snapshot, release, _ := s.invalidateViewLocked(ctx, view, changed) +- return snapshot, release, nil +-} - -- // Get diagnostics. -- if err := conn.diagnoseFiles(ctx, []span.URI{uri}); err != nil { -- return err -- } -- diagnostics := []protocol.Diagnostic{} // LSP wants non-nil slice -- conn.client.filesMu.Lock() -- diagnostics = append(diagnostics, file.diagnostics...) -- conn.client.filesMu.Unlock() +-// invalidateViewLocked invalidates the content of the given view. +-// (See [Session.InvalidateView]). +-// +-// The resulting bool reports whether the View needs to be re-diagnosed. +-// (See [Snapshot.clone]). +-// +-// s.viewMu must be held while calling this method. +-func (s *Session) invalidateViewLocked(ctx context.Context, v *View, changed StateChange) (*Snapshot, func(), bool) { +- // Detach the context so that content invalidation cannot be canceled. +- ctx = xcontext.Detach(ctx) - -- // Request code actions -- codeActionKinds := []protocol.CodeActionKind{protocol.QuickFix} -- if len(args) > 1 { -- codeActionKinds = []protocol.CodeActionKind{} -- for _, k := range args[1:] { -- codeActionKinds = append(codeActionKinds, protocol.CodeActionKind(k)) -- } -- } -- p := protocol.CodeActionParams{ -- TextDocument: protocol.TextDocumentIdentifier{ -- URI: protocol.URIFromSpanURI(uri), -- }, -- Context: protocol.CodeActionContext{ -- Only: codeActionKinds, -- Diagnostics: diagnostics, -- }, -- Range: rng, -- } -- actions, err := conn.CodeAction(ctx, &p) -- if err != nil { -- return fmt.Errorf("%v: %v", from, err) +- // This should be the only time we hold the view's snapshot lock for any period of time. +- v.snapshotMu.Lock() +- defer v.snapshotMu.Unlock() +- +- prevSnapshot := v.snapshot +- +- if prevSnapshot == nil { +- panic("invalidateContent called after shutdown") - } - -- // Gather edits from matching code actions. -- var edits []protocol.TextEdit -- for _, a := range actions { -- // Without -all, apply only "preferred" fixes. -- if !a.IsPreferred && !s.All { -- continue -- } +- // Cancel all still-running previous requests, since they would be +- // operating on stale data. +- prevSnapshot.cancel() - -- // Execute any command. -- // This may cause the server to make -- // an ApplyEdit downcall to the client. -- if a.Command != nil { -- if _, err := conn.ExecuteCommand(ctx, &protocol.ExecuteCommandParams{ -- Command: a.Command.Command, -- Arguments: a.Command.Arguments, -- }); err != nil { -- return err -- } -- // The specification says that commands should -- // be executed _after_ edits are applied, not -- // instead of them, but we don't want to -- // duplicate edits. -- continue -- } +- // Do not clone a snapshot until its view has finished initializing. +- // +- // TODO(rfindley): shouldn't we do this before canceling? +- prevSnapshot.AwaitInitialized(ctx) - -- // Partially apply CodeAction.Edit, a WorkspaceEdit. -- // (See also conn.Client.applyWorkspaceEdit(a.Edit)). -- if !from.HasPosition() { -- for _, c := range a.Edit.DocumentChanges { -- if c.TextDocumentEdit != nil { -- if fileURI(c.TextDocumentEdit.TextDocument.URI) == uri { -- edits = append(edits, c.TextDocumentEdit.Edits...) +- var needsDiagnosis bool +- s.snapshotWG.Add(1) +- v.snapshot, needsDiagnosis = prevSnapshot.clone(ctx, v.baseCtx, changed, s.snapshotWG.Done) +- +- // Remove the initial reference created when prevSnapshot was created. +- prevSnapshot.decref() +- +- // Return a second lease to the caller. +- return v.snapshot, v.snapshot.Acquire(), needsDiagnosis +-} +- +-// defineView computes the view definition for the provided workspace folder +-// and URI. +-// +-// If forURI is non-empty, this view should be the best view including forURI. +-// Otherwise, it is the default view for the folder. +-// +-// defineView only returns an error in the event of context cancellation. +-// +-// Note: keep this function in sync with bestView. +-// +-// TODO(rfindley): we should be able to remove the error return, as +-// findModules is going away, and all other I/O is memoized. +-// +-// TODO(rfindley): pass in a narrower interface for the file.Source +-// (e.g. fileExists func(DocumentURI) bool) to make clear that this +-// process depends only on directory information, not file contents. +-func defineView(ctx context.Context, fs file.Source, folder *Folder, forFile file.Handle) (*viewDefinition, error) { +- if err := checkPathValid(folder.Dir.Path()); err != nil { +- return nil, fmt.Errorf("invalid workspace folder path: %w; check that the spelling of the configured workspace folder path agrees with the spelling reported by the operating system", err) +- } +- dir := folder.Dir.Path() +- if forFile != nil { +- dir = filepath.Dir(forFile.URI().Path()) +- } +- +- def := new(viewDefinition) +- def.folder = folder +- +- if forFile != nil && fileKind(forFile) == file.Go { +- // If the file has GOOS/GOARCH build constraints that +- // don't match the folder's environment (which comes from +- // 'go env' in the folder, plus user options), +- // add those constraints to the viewDefinition's environment. +- +- // Content trimming is nontrivial, so do this outside of the loop below. +- // Keep this in sync with bestView. +- path := forFile.URI().Path() +- if content, err := forFile.Content(); err == nil { +- // Note the err == nil condition above: by convention a non-existent file +- // does not have any constraints. See the related note in bestView: this +- // choice of behavior shouldn't actually matter. In this case, we should +- // only call defineView with Overlays, which always have content. +- content = trimContentForPortMatch(content) +- viewPort := port{def.folder.Env.GOOS, def.folder.Env.GOARCH} +- if !viewPort.matches(path, content) { +- for _, p := range preferredPorts { +- if p.matches(path, content) { +- if def.envOverlay == nil { +- def.envOverlay = make(map[string]string) +- } +- def.envOverlay["GOOS"] = p.GOOS +- def.envOverlay["GOARCH"] = p.GOARCH +- break - } - } - } -- continue - } +- } - -- // The provided span has a position (not just offsets). -- // Find the code action that has the same range as it. -- for _, diag := range a.Diagnostics { -- if diag.Range.Start == rng.Start { -- for _, c := range a.Edit.DocumentChanges { -- if c.TextDocumentEdit != nil { -- if fileURI(c.TextDocumentEdit.TextDocument.URI) == uri { -- edits = append(edits, c.TextDocumentEdit.Edits...) -- } -- } -- } -- break +- var err error +- dirURI := protocol.URIFromPath(dir) +- goworkFromEnv := false +- if folder.Env.GOWORK != "off" && folder.Env.GOWORK != "" { +- goworkFromEnv = true +- def.gowork = protocol.URIFromPath(folder.Env.GOWORK) +- } else { +- def.gowork, err = findRootPattern(ctx, dirURI, "go.work", fs) +- if err != nil { +- return nil, err +- } +- } +- +- // When deriving the best view for a given file, we only want to search +- // up the directory hierarchy for modfiles. +- def.gomod, err = findRootPattern(ctx, dirURI, "go.mod", fs) +- if err != nil { +- return nil, err +- } +- +- // Determine how we load and where to load package information for this view +- // +- // Specifically, set +- // - def.typ +- // - def.root +- // - def.workspaceModFiles, and +- // - def.envOverlay. +- +- // If GOPACKAGESDRIVER is set it takes precedence. +- { +- // The value of GOPACKAGESDRIVER is not returned through the go command. +- gopackagesdriver := os.Getenv("GOPACKAGESDRIVER") +- // A user may also have a gopackagesdriver binary on their machine, which +- // works the same way as setting GOPACKAGESDRIVER. +- // +- // TODO(rfindley): remove this call to LookPath. We should not support this +- // undocumented method of setting GOPACKAGESDRIVER. +- tool, err := exec.LookPath("gopackagesdriver") +- if gopackagesdriver != "off" && (gopackagesdriver != "" || (err == nil && tool != "")) { +- def.typ = GoPackagesDriverView +- def.root = dirURI +- return def, nil +- } +- } +- +- // From go.dev/ref/mod, module mode is active if GO111MODULE=on, or +- // GO111MODULE=auto or "" and we are inside a module or have a GOWORK value. +- // But gopls is less strict, allowing GOPATH mode if GO111MODULE="", and +- // AdHoc views if no module is found. +- +- // gomodWorkspace is a helper to compute the correct set of workspace +- // modfiles for a go.mod file, based on folder options. +- gomodWorkspace := func() map[protocol.DocumentURI]unit { +- modFiles := map[protocol.DocumentURI]struct{}{def.gomod: {}} +- if folder.Options.IncludeReplaceInWorkspace { +- includingReplace, err := goModModules(ctx, def.gomod, fs) +- if err == nil { +- modFiles = includingReplace +- } else { +- // If the go.mod file fails to parse, we don't know anything about +- // replace directives, so fall back to a view of just the root module. - } - } +- return modFiles +- } - -- // If suggested fix is not a diagnostic, still must collect edits. -- if len(a.Diagnostics) == 0 { -- for _, c := range a.Edit.DocumentChanges { -- if c.TextDocumentEdit != nil { -- if fileURI(c.TextDocumentEdit.TextDocument.URI) == uri { -- edits = append(edits, c.TextDocumentEdit.Edits...) -- } +- // Prefer a go.work file if it is available and contains the module relevant +- // to forURI. +- if def.adjustedGO111MODULE() != "off" && folder.Env.GOWORK != "off" && def.gowork != "" { +- def.typ = GoWorkView +- if goworkFromEnv { +- // The go.work file could be anywhere, which can lead to confusing error +- // messages. +- def.root = dirURI +- } else { +- // The go.work file could be anywhere, which can lead to confusing error +- def.root = def.gowork.Dir() +- } +- def.workspaceModFiles, def.workspaceModFilesErr = goWorkModules(ctx, def.gowork, fs) +- +- // If forURI is in a module but that module is not +- // included in the go.work file, use a go.mod view with GOWORK=off. +- if forFile != nil && def.workspaceModFilesErr == nil && def.gomod != "" { +- if _, ok := def.workspaceModFiles[def.gomod]; !ok { +- def.typ = GoModView +- def.root = def.gomod.Dir() +- def.workspaceModFiles = gomodWorkspace() +- if def.envOverlay == nil { +- def.envOverlay = make(map[string]string) - } +- def.envOverlay["GOWORK"] = "off" - } - } +- return def, nil - } - -- return applyTextEdits(file.mapper, edits, s.app.editFlags) --} -diff -urN a/gopls/internal/lsp/cmd/symbols.go b/gopls/internal/lsp/cmd/symbols.go ---- a/gopls/internal/lsp/cmd/symbols.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/symbols.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,116 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package cmd -- --import ( -- "context" -- "encoding/json" -- "flag" -- "fmt" -- "sort" +- // Otherwise, use the active module, if in module mode. +- // +- // Note, we could override GO111MODULE here via envOverlay if we wanted to +- // support the case where someone opens a module with GO111MODULE=off. But +- // that is probably not worth worrying about (at this point, folks probably +- // shouldn't be setting GO111MODULE). +- if def.adjustedGO111MODULE() != "off" && def.gomod != "" { +- def.typ = GoModView +- def.root = def.gomod.Dir() +- def.workspaceModFiles = gomodWorkspace() +- return def, nil +- } - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/tool" --) +- // Check if the workspace is within any GOPATH directory. +- inGOPATH := false +- for _, gp := range filepath.SplitList(folder.Env.GOPATH) { +- if pathutil.InDir(filepath.Join(gp, "src"), dir) { +- inGOPATH = true +- break +- } +- } +- if def.adjustedGO111MODULE() != "on" && inGOPATH { +- def.typ = GOPATHView +- def.root = dirURI +- return def, nil +- } - --// symbols implements the symbols verb for gopls --type symbols struct { -- app *Application +- // We're not in a workspace, module, or GOPATH, so have no better choice than +- // an ad-hoc view. +- def.typ = AdHocView +- def.root = dirURI +- return def, nil -} - --func (r *symbols) Name() string { return "symbols" } --func (r *symbols) Parent() string { return r.app.Name() } --func (r *symbols) Usage() string { return "" } --func (r *symbols) ShortHelp() string { return "display selected file's symbols" } --func (r *symbols) DetailedHelp(f *flag.FlagSet) { -- fmt.Fprint(f.Output(), ` --Example: -- $ gopls symbols helper/helper.go --`) -- printFlagDefaults(f) --} --func (r *symbols) Run(ctx context.Context, args ...string) error { -- if len(args) != 1 { -- return tool.CommandLineErrorf("symbols expects 1 argument (position)") +-// FetchGoEnv queries the environment and Go command to collect environment +-// variables necessary for the workspace folder. +-func FetchGoEnv(ctx context.Context, folder protocol.DocumentURI, opts *settings.Options) (*GoEnv, error) { +- dir := folder.Path() +- // All of the go commands invoked here should be fast. No need to share a +- // runner with other operations. +- runner := new(gocommand.Runner) +- inv := gocommand.Invocation{ +- WorkingDir: dir, +- Env: opts.EnvSlice(), - } - -- conn, err := r.app.connect(ctx, nil) -- if err != nil { -- return err +- var ( +- env = new(GoEnv) +- err error +- ) +- envvars := map[string]*string{ +- "GOOS": &env.GOOS, +- "GOARCH": &env.GOARCH, +- "GOCACHE": &env.GOCACHE, +- "GOPATH": &env.GOPATH, +- "GOPRIVATE": &env.GOPRIVATE, +- "GOMODCACHE": &env.GOMODCACHE, +- "GOFLAGS": &env.GOFLAGS, +- "GO111MODULE": &env.GO111MODULE, +- } +- if err := loadGoEnv(ctx, dir, opts.EnvSlice(), runner, envvars); err != nil { +- return nil, err - } -- defer conn.terminate(ctx) - -- from := span.Parse(args[0]) -- p := protocol.DocumentSymbolParams{ -- TextDocument: protocol.TextDocumentIdentifier{ -- URI: protocol.URIFromSpanURI(from.URI()), -- }, +- env.GoVersion, err = gocommand.GoVersion(ctx, inv, runner) +- if err != nil { +- return nil, err - } -- symbols, err := conn.DocumentSymbol(ctx, &p) +- env.GoVersionOutput, err = gocommand.GoVersionOutput(ctx, inv, runner) - if err != nil { -- return err +- return nil, err - } -- for _, s := range symbols { -- if m, ok := s.(map[string]interface{}); ok { -- s, err = mapToSymbol(m) -- if err != nil { -- return err +- +- // The value of GOPACKAGESDRIVER is not returned through the go command. +- if driver, ok := opts.Env["GOPACKAGESDRIVER"]; ok { +- env.GOPACKAGESDRIVER = driver +- } else { +- env.GOPACKAGESDRIVER = os.Getenv("GOPACKAGESDRIVER") +- // A user may also have a gopackagesdriver binary on their machine, which +- // works the same way as setting GOPACKAGESDRIVER. +- // +- // TODO(rfindley): remove this call to LookPath. We should not support this +- // undocumented method of setting GOPACKAGESDRIVER. +- if env.GOPACKAGESDRIVER == "" { +- tool, err := exec.LookPath("gopackagesdriver") +- if err == nil && tool != "" { +- env.GOPACKAGESDRIVER = tool - } - } -- switch t := s.(type) { -- case protocol.DocumentSymbol: -- printDocumentSymbol(t) -- case protocol.SymbolInformation: -- printSymbolInformation(t) -- } - } -- return nil --} - --func mapToSymbol(m map[string]interface{}) (interface{}, error) { -- b, err := json.Marshal(m) -- if err != nil { -- return nil, err +- // While GOWORK is available through the Go command, we want to differentiate +- // between an explicit GOWORK value and one which is implicit from the file +- // system. The former doesn't change unless the environment changes. +- if gowork, ok := opts.Env["GOWORK"]; ok { +- env.GOWORK = gowork +- } else { +- env.GOWORK = os.Getenv("GOWORK") - } +- return env, nil +-} - -- if _, ok := m["selectionRange"]; ok { -- var s protocol.DocumentSymbol -- if err := json.Unmarshal(b, &s); err != nil { -- return nil, err -- } -- return s, nil +-// loadGoEnv loads `go env` values into the provided map, keyed by Go variable +-// name. +-func loadGoEnv(ctx context.Context, dir string, configEnv []string, runner *gocommand.Runner, vars map[string]*string) error { +- // We can save ~200 ms by requesting only the variables we care about. +- args := []string{"-json"} +- for k := range vars { +- args = append(args, k) - } - -- var s protocol.SymbolInformation -- if err := json.Unmarshal(b, &s); err != nil { -- return nil, err +- inv := gocommand.Invocation{ +- Verb: "env", +- Args: args, +- Env: configEnv, +- WorkingDir: dir, - } -- return s, nil +- stdout, err := runner.Run(ctx, inv) +- if err != nil { +- return err +- } +- envMap := make(map[string]string) +- if err := json.Unmarshal(stdout.Bytes(), &envMap); err != nil { +- return fmt.Errorf("internal error unmarshaling JSON from 'go env': %w", err) +- } +- for key, ptr := range vars { +- *ptr = envMap[key] +- } +- +- return nil -} - --func printDocumentSymbol(s protocol.DocumentSymbol) { -- fmt.Printf("%s %s %s\n", s.Name, s.Kind, positionToString(s.SelectionRange)) -- // Sort children for consistency -- sort.Slice(s.Children, func(i, j int) bool { -- return s.Children[i].Name < s.Children[j].Name -- }) -- for _, c := range s.Children { -- fmt.Printf("\t%s %s %s\n", c.Name, c.Kind, positionToString(c.SelectionRange)) +-// findRootPattern looks for files with the given basename in dir or any parent +-// directory of dir, using the provided FileSource. It returns the first match, +-// starting from dir and search parents. +-// +-// The resulting string is either the file path of a matching file with the +-// given basename, or "" if none was found. +-// +-// findRootPattern only returns an error in the case of context cancellation. +-func findRootPattern(ctx context.Context, dirURI protocol.DocumentURI, basename string, fs file.Source) (protocol.DocumentURI, error) { +- dir := dirURI.Path() +- for dir != "" { +- target := filepath.Join(dir, basename) +- uri := protocol.URIFromPath(target) +- fh, err := fs.ReadFile(ctx, uri) +- if err != nil { +- return "", err // context cancelled +- } +- if fileExists(fh) { +- return uri, nil +- } +- // Trailing separators must be trimmed, otherwise filepath.Split is a noop. +- next, _ := filepath.Split(strings.TrimRight(dir, string(filepath.Separator))) +- if next == dir { +- break +- } +- dir = next - } +- return "", nil -} - --func printSymbolInformation(s protocol.SymbolInformation) { -- fmt.Printf("%s %s %s\n", s.Name, s.Kind, positionToString(s.Location.Range)) +-// checkPathValid performs an OS-specific path validity check. The +-// implementation varies for filesystems that are case-insensitive +-// (e.g. macOS, Windows), and for those that disallow certain file +-// names (e.g. path segments ending with a period on Windows, or +-// reserved names such as "com"; see +-// https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file). +-var checkPathValid = defaultCheckPathValid +- +-// CheckPathValid checks whether a directory is suitable as a workspace folder. +-func CheckPathValid(dir string) error { return checkPathValid(dir) } +- +-func defaultCheckPathValid(path string) error { +- return nil -} - --func positionToString(r protocol.Range) string { -- return fmt.Sprintf("%v:%v-%v:%v", -- r.Start.Line+1, -- r.Start.Character+1, -- r.End.Line+1, -- r.End.Character+1, -- ) +-// IsGoPrivatePath reports whether target is a private import path, as identified +-// by the GOPRIVATE environment variable. +-func (s *Snapshot) IsGoPrivatePath(target string) bool { +- return globsMatchPath(s.view.folder.Env.GOPRIVATE, target) -} -diff -urN a/gopls/internal/lsp/cmd/test/integration_test.go b/gopls/internal/lsp/cmd/test/integration_test.go ---- a/gopls/internal/lsp/cmd/test/integration_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/test/integration_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,1048 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --// Package cmdtest contains the test suite for the command line behavior of gopls. --package cmdtest +-// ModuleUpgrades returns known module upgrades for the dependencies of +-// modfile. +-func (s *Snapshot) ModuleUpgrades(modfile protocol.DocumentURI) map[string]string { +- s.mu.Lock() +- defer s.mu.Unlock() +- upgrades := map[string]string{} +- orig, _ := s.moduleUpgrades.Get(modfile) +- for mod, ver := range orig { +- upgrades[mod] = ver +- } +- return upgrades +-} - --// This file defines integration tests of each gopls subcommand that --// fork+exec the command in a separate process. +-// MaxGovulncheckResultsAge defines the maximum vulnerability age considered +-// valid by gopls. -// --// (Rather than execute 'go build gopls' during the test, we reproduce --// the main entrypoint in the test executable.) +-// Mutable for testing. +-var MaxGovulncheckResultAge = 1 * time.Hour +- +-// Vulnerabilities returns known vulnerabilities for the given modfile. -// --// The purpose of this test is to exercise client-side logic such as --// argument parsing and formatting of LSP RPC responses, not server --// behavior; see lsp_test for that. +-// Results more than an hour old are excluded. -// --// All tests run in parallel. +-// TODO(suzmue): replace command.Vuln with a different type, maybe +-// https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck/govulnchecklib#Summary? -// --// TODO(adonovan): --// - Use markers to represent positions in the input and in assertions. --// - Coverage of cross-cutting things like cwd, environ, span parsing, etc. --// - Subcommands that accept -write and -diff flags implement them --// consistently; factor their tests. --// - Add missing test for 'vulncheck' subcommand. --// - Add tests for client-only commands: serve, bug, help, api-json, licenses. -- --import ( -- "bytes" -- "context" -- "encoding/json" -- "fmt" -- "math/rand" -- "os" -- "path/filepath" -- "regexp" -- "strings" -- "testing" -- -- exec "golang.org/x/sys/execabs" -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/hooks" -- "golang.org/x/tools/gopls/internal/lsp/cmd" -- "golang.org/x/tools/gopls/internal/lsp/debug" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/internal/testenv" -- "golang.org/x/tools/internal/tool" -- "golang.org/x/tools/txtar" --) -- --// TestVersion tests the 'version' subcommand (../info.go). --func TestVersion(t *testing.T) { -- t.Parallel() -- -- tree := writeTree(t, "") +-// TODO(rfindley): move to snapshot.go +-func (s *Snapshot) Vulnerabilities(modfiles ...protocol.DocumentURI) map[protocol.DocumentURI]*vulncheck.Result { +- m := make(map[protocol.DocumentURI]*vulncheck.Result) +- now := time.Now() - -- // There's not much we can robustly assert about the actual version. -- want := debug.Version() // e.g. "master" +- s.mu.Lock() +- defer s.mu.Unlock() - -- // basic -- { -- res := gopls(t, tree, "version") -- res.checkExit(true) -- res.checkStdout(want) +- if len(modfiles) == 0 { // empty means all modfiles +- modfiles = s.vulns.Keys() - } -- -- // -json flag -- { -- res := gopls(t, tree, "version", "-json") -- res.checkExit(true) -- var v debug.ServerVersion -- if res.toJSON(&v) { -- if v.Version != want { -- t.Errorf("expected Version %q, got %q (%v)", want, v.Version, res) -- } +- for _, modfile := range modfiles { +- vuln, _ := s.vulns.Get(modfile) +- if vuln != nil && now.Sub(vuln.AsOf) > MaxGovulncheckResultAge { +- vuln = nil - } +- m[modfile] = vuln - } +- return m -} - --// TestCheck tests the 'check' subcommand (../check.go). --func TestCheck(t *testing.T) { -- t.Parallel() +-// GoVersion returns the effective release Go version (the X in go1.X) for this +-// view. +-func (v *View) GoVersion() int { +- return v.folder.Env.GoVersion +-} - -- tree := writeTree(t, ` ---- go.mod -- --module example.com --go 1.18 +-// GoVersionString returns the effective Go version string for this view. +-// +-// Unlike [GoVersion], this encodes the minor version and commit hash information. +-func (v *View) GoVersionString() string { +- return gocommand.ParseGoVersionOutput(v.folder.Env.GoVersionOutput) +-} - ---- a.go -- --package a --import "fmt" --var _ = fmt.Sprintf("%s", 123) +-// GoVersionString is temporarily available from the snapshot. +-// +-// TODO(rfindley): refactor so that this method is not necessary. +-func (s *Snapshot) GoVersionString() string { +- return s.view.GoVersionString() +-} - ---- b.go -- --package a --import "fmt" --var _ = fmt.Sprintf("%d", "123") --`) +-// Copied from +-// https://cs.opensource.google/go/go/+/master:src/cmd/go/internal/str/path.go;l=58;drc=2910c5b4a01a573ebc97744890a07c1a3122c67a +-func globsMatchPath(globs, target string) bool { +- for globs != "" { +- // Extract next non-empty glob in comma-separated list. +- var glob string +- if i := strings.Index(globs, ","); i >= 0 { +- glob, globs = globs[:i], globs[i+1:] +- } else { +- glob, globs = globs, "" +- } +- if glob == "" { +- continue +- } - -- // no files -- { -- res := gopls(t, tree, "check") -- res.checkExit(true) -- if res.stdout != "" { -- t.Errorf("unexpected output: %v", res) +- // A glob with N+1 path elements (N slashes) needs to be matched +- // against the first N+1 path elements of target, +- // which end just before the N+1'th slash. +- n := strings.Count(glob, "/") +- prefix := target +- // Walk target, counting slashes, truncating at the N+1'th slash. +- for i := 0; i < len(target); i++ { +- if target[i] == '/' { +- if n == 0 { +- prefix = target[:i] +- break +- } +- n-- +- } +- } +- if n > 0 { +- // Not enough prefix elements. +- continue +- } +- matched, _ := path.Match(glob, prefix) +- if matched { +- return true - } - } +- return false +-} - -- // one file -- { -- res := gopls(t, tree, "check", "./a.go") -- res.checkExit(true) -- res.checkStdout("fmt.Sprintf format %s has arg 123 of wrong type int") -- } +-var modFlagRegexp = regexp.MustCompile(`-mod[ =](\w+)`) - -- // two files -- { -- res := gopls(t, tree, "check", "./a.go", "./b.go") -- res.checkExit(true) -- res.checkStdout(`a.go:.* fmt.Sprintf format %s has arg 123 of wrong type int`) -- res.checkStdout(`b.go:.* fmt.Sprintf format %d has arg "123" of wrong type string`) +-// TODO(rfindley): clean up the redundancy of allFilesExcluded, +-// pathExcludedByFilterFunc, pathExcludedByFilter, view.filterFunc... +-func allFilesExcluded(files []string, filterFunc func(protocol.DocumentURI) bool) bool { +- for _, f := range files { +- uri := protocol.URIFromPath(f) +- if !filterFunc(uri) { +- return false +- } - } +- return true -} - --// TestCallHierarchy tests the 'call_hierarchy' subcommand (../call_hierarchy.go). --func TestCallHierarchy(t *testing.T) { -- t.Parallel() +-func relPathExcludedByFilter(path string, filterer *Filterer) bool { +- path = strings.TrimPrefix(filepath.ToSlash(path), "/") +- return filterer.Disallow(path) +-} +diff -urN a/gopls/internal/cache/view_test.go b/gopls/internal/cache/view_test.go +--- a/gopls/internal/cache/view_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/view_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,175 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +-package cache - -- tree := writeTree(t, ` ---- go.mod -- --module example.com --go 1.18 +-import ( +- "os" +- "path/filepath" +- "testing" - ---- a.go -- --package a --func f() {} --func g() { -- f() --} --func h() { -- f() -- f() --} --`) -- // missing position -- { -- res := gopls(t, tree, "call_hierarchy") -- res.checkExit(false) -- res.checkStderr("expects 1 argument") +- "golang.org/x/tools/gopls/internal/protocol" +-) +- +-func TestCaseInsensitiveFilesystem(t *testing.T) { +- base := t.TempDir() +- +- inner := filepath.Join(base, "a/B/c/DEFgh") +- if err := os.MkdirAll(inner, 0777); err != nil { +- t.Fatal(err) - } -- // wrong place -- { -- res := gopls(t, tree, "call_hierarchy", "a.go:1") -- res.checkExit(false) -- res.checkStderr("identifier not found") +- file := filepath.Join(inner, "f.go") +- if err := os.WriteFile(file, []byte("hi"), 0777); err != nil { +- t.Fatal(err) - } -- // f is called once from g and twice from h. -- { -- res := gopls(t, tree, "call_hierarchy", "a.go:2:6") -- res.checkExit(true) -- // We use regexp '.' as an OS-agnostic path separator. -- res.checkStdout("ranges 7:2-3, 8:2-3 in ..a.go from/to function h in ..a.go:6:6-7") -- res.checkStdout("ranges 4:2-3 in ..a.go from/to function g in ..a.go:3:6-7") -- res.checkStdout("identifier: function f in ..a.go:2:6-7") +- if _, err := os.Stat(filepath.Join(inner, "F.go")); err != nil { +- t.Skip("filesystem is case-sensitive") - } --} -- --// TestDefinition tests the 'definition' subcommand (../definition.go). --func TestDefinition(t *testing.T) { -- t.Parallel() -- -- tree := writeTree(t, ` ---- go.mod -- --module example.com --go 1.18 - ---- a.go -- --package a --import "fmt" --func f() { -- fmt.Println() --} --func g() { -- f() --} --`) -- // missing position -- { -- res := gopls(t, tree, "definition") -- res.checkExit(false) -- res.checkStderr("expects 1 argument") -- } -- // intra-package -- { -- res := gopls(t, tree, "definition", "a.go:7:2") // "f()" -- res.checkExit(true) -- res.checkStdout("a.go:3:6-7: defined here as func f") -- } -- // cross-package -- { -- res := gopls(t, tree, "definition", "a.go:4:7") // "Println" -- res.checkExit(true) -- res.checkStdout("print.go.* defined here as func fmt.Println") -- res.checkStdout("Println formats using the default formats for its operands") +- tests := []struct { +- path string +- err bool +- }{ +- {file, false}, +- {filepath.Join(inner, "F.go"), true}, +- {filepath.Join(base, "a/b/c/defgh/f.go"), true}, - } -- // -json and -markdown -- { -- res := gopls(t, tree, "definition", "-json", "-markdown", "a.go:4:7") -- res.checkExit(true) -- var defn cmd.Definition -- if res.toJSON(&defn) { -- if !strings.HasPrefix(defn.Description, "```go\nfunc fmt.Println") { -- t.Errorf("Description does not start with markdown code block. Got: %s", defn.Description) -- } +- for _, tt := range tests { +- err := checkPathValid(tt.path) +- if err != nil != tt.err { +- t.Errorf("checkPathValid(%q) = %v, wanted error: %v", tt.path, err, tt.err) - } - } -} - --// TestFoldingRanges tests the 'folding_ranges' subcommand (../folding_range.go). --func TestFoldingRanges(t *testing.T) { -- t.Parallel() -- -- tree := writeTree(t, ` ---- go.mod -- --module example.com --go 1.18 -- ---- a.go -- --package a --func f(x int) { -- // hello --} --`) -- // missing filename -- { -- res := gopls(t, tree, "folding_ranges") -- res.checkExit(false) -- res.checkStderr("expects 1 argument") -- } -- // success -- { -- res := gopls(t, tree, "folding_ranges", "a.go") -- res.checkExit(true) -- res.checkStdout("2:8-2:13") // params (x int) -- res.checkStdout("2:16-4:1") // body { ... } +-func TestInVendor(t *testing.T) { +- for _, tt := range []struct { +- path string +- inVendor bool +- }{ +- {"foo/vendor/x.go", false}, +- {"foo/vendor/x/x.go", true}, +- {"foo/x.go", false}, +- {"foo/vendor/foo.txt", false}, +- {"foo/vendor/modules.txt", false}, +- } { +- if got := inVendor(protocol.URIFromPath(tt.path)); got != tt.inVendor { +- t.Errorf("expected %s inVendor %v, got %v", tt.path, tt.inVendor, got) +- } - } -} - --// TestFormat tests the 'format' subcommand (../format.go). --func TestFormat(t *testing.T) { -- t.Parallel() -- -- tree := writeTree(t, ` ---- a.go -- --package a ; func f ( ) { } --`) -- const want = `package a -- --func f() {} --` -- -- // no files => nop -- { -- res := gopls(t, tree, "format") -- res.checkExit(true) +-func TestFilters(t *testing.T) { +- tests := []struct { +- filters []string +- included []string +- excluded []string +- }{ +- { +- included: []string{"x"}, +- }, +- { +- filters: []string{"-"}, +- excluded: []string{"x", "x/a"}, +- }, +- { +- filters: []string{"-x", "+y"}, +- included: []string{"y", "y/a", "z"}, +- excluded: []string{"x", "x/a"}, +- }, +- { +- filters: []string{"-x", "+x/y", "-x/y/z"}, +- included: []string{"x/y", "x/y/a", "a"}, +- excluded: []string{"x", "x/a", "x/y/z/a"}, +- }, +- { +- filters: []string{"+foobar", "-foo"}, +- included: []string{"foobar", "foobar/a"}, +- excluded: []string{"foo", "foo/a"}, +- }, - } -- // default => print formatted result -- { -- res := gopls(t, tree, "format", "a.go") -- res.checkExit(true) -- if res.stdout != want { -- t.Errorf("format: got <<%s>>, want <<%s>>", res.stdout, want) +- +- for _, tt := range tests { +- filterer := NewFilterer(tt.filters) +- for _, inc := range tt.included { +- if relPathExcludedByFilter(inc, filterer) { +- t.Errorf("filters %q excluded %v, wanted included", tt.filters, inc) +- } +- } +- for _, exc := range tt.excluded { +- if !relPathExcludedByFilter(exc, filterer) { +- t.Errorf("filters %q included %v, wanted excluded", tt.filters, exc) +- } - } - } -- // start/end position not supported (unless equal to start/end of file) -- { -- res := gopls(t, tree, "format", "a.go:1-2") -- res.checkExit(false) -- res.checkStderr("only full file formatting supported") +-} +- +-func TestSuffixes(t *testing.T) { +- type file struct { +- path string +- want bool - } -- // -list: show only file names -- { -- res := gopls(t, tree, "format", "-list", "a.go") -- res.checkExit(true) -- res.checkStdout("a.go") +- type cases struct { +- option []string +- files []file - } -- // -diff prints a unified diff -- { -- res := gopls(t, tree, "format", "-diff", "a.go") -- res.checkExit(true) -- // We omit the filenames as they vary by OS. -- want := ` ---package a ; func f ( ) { } --+package a --+ --+func f() {} --` -- res.checkStdout(regexp.QuoteMeta(want)) +- tests := []cases{ +- {[]string{"tmpl", "gotmpl"}, []file{ // default +- {"foo", false}, +- {"foo.tmpl", true}, +- {"foo.gotmpl", true}, +- {"tmpl", false}, +- {"tmpl.go", false}}, +- }, +- {[]string{"tmpl", "gotmpl", "html", "gohtml"}, []file{ +- {"foo.gotmpl", true}, +- {"foo.html", true}, +- {"foo.gohtml", true}, +- {"html", false}}, +- }, +- {[]string{"tmpl", "gotmpl", ""}, []file{ // possible user mistake +- {"foo.gotmpl", true}, +- {"foo.go", false}, +- {"foo", false}}, +- }, - } -- // -write updates the file -- { -- res := gopls(t, tree, "format", "-write", "a.go") -- res.checkExit(true) -- res.checkStdout("^$") // empty -- checkContent(t, filepath.Join(tree, "a.go"), want) +- for _, a := range tests { +- suffixes := a.option +- for _, b := range a.files { +- got := fileHasExtension(b.path, suffixes) +- if got != b.want { +- t.Errorf("got %v, want %v, option %q, file %q (%+v)", +- got, b.want, a.option, b.path, b) +- } +- } - } -} - --// TestHighlight tests the 'highlight' subcommand (../highlight.go). --func TestHighlight(t *testing.T) { -- t.Parallel() -- -- tree := writeTree(t, ` ---- a.go -- --package a --import "fmt" --func f() { -- fmt.Println() -- fmt.Println() --} --`) -- -- // no arguments -- { -- res := gopls(t, tree, "highlight") -- res.checkExit(false) -- res.checkStderr("expects 1 argument") -- } -- // all occurrences of Println -- { -- res := gopls(t, tree, "highlight", "a.go:4:7") -- res.checkExit(true) -- res.checkStdout("a.go:4:6-13") -- res.checkStdout("a.go:5:6-13") +-func TestIgnoreFilter(t *testing.T) { +- tests := []struct { +- dirs []string +- path string +- want bool +- }{ +- {[]string{"a"}, "a/testdata/foo", true}, +- {[]string{"a"}, "a/_ignore/foo", true}, +- {[]string{"a"}, "a/.ignore/foo", true}, +- {[]string{"a"}, "b/testdata/foo", false}, +- {[]string{"a"}, "testdata/foo", false}, +- {[]string{"a", "b"}, "b/testdata/foo", true}, +- {[]string{"a"}, "atestdata/foo", false}, - } --} -- --// TestImplementations tests the 'implementation' subcommand (../implementation.go). --func TestImplementations(t *testing.T) { -- t.Parallel() - -- tree := writeTree(t, ` ---- a.go -- --package a --import "fmt" --type T int --func (T) String() string { return "" } --`) +- for _, test := range tests { +- // convert to filepaths, for convenience +- for i, dir := range test.dirs { +- test.dirs[i] = filepath.FromSlash(dir) +- } +- test.path = filepath.FromSlash(test.path) - -- // no arguments -- { -- res := gopls(t, tree, "implementation") -- res.checkExit(false) -- res.checkStderr("expects 1 argument") -- } -- // T.String -- { -- res := gopls(t, tree, "implementation", "a.go:4:10") -- res.checkExit(true) -- // TODO(adonovan): extract and check the content of the reported ranges? -- // We use regexp '.' as an OS-agnostic path separator. -- res.checkStdout("fmt.print.go:") // fmt.Stringer.String -- res.checkStdout("runtime.error.go:") // runtime.stringer.String +- f := newIgnoreFilter(test.dirs) +- if got := f.ignored(test.path); got != test.want { +- t.Errorf("newIgnoreFilter(%q).ignore(%q) = %t, want %t", test.dirs, test.path, got, test.want) +- } - } -} +diff -urN a/gopls/internal/cache/workspace.go b/gopls/internal/cache/workspace.go +--- a/gopls/internal/cache/workspace.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/workspace.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,112 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// TestImports tests the 'imports' subcommand (../imports.go). --func TestImports(t *testing.T) { -- t.Parallel() +-package cache - -- tree := writeTree(t, ` ---- a.go -- --package a --func _() { -- fmt.Println() --} --`) +-import ( +- "context" +- "errors" +- "fmt" +- "path/filepath" - -- want := ` --package a +- "golang.org/x/mod/modfile" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +-) - --import "fmt" --func _() { -- fmt.Println() +-// isGoWork reports if uri is a go.work file. +-func isGoWork(uri protocol.DocumentURI) bool { +- return filepath.Base(uri.Path()) == "go.work" -} --`[1:] - -- // no arguments -- { -- res := gopls(t, tree, "imports") -- res.checkExit(false) -- res.checkStderr("expects 1 argument") +-// goWorkModules returns the URIs of go.mod files named by the go.work file. +-func goWorkModules(ctx context.Context, gowork protocol.DocumentURI, fs file.Source) (map[protocol.DocumentURI]unit, error) { +- fh, err := fs.ReadFile(ctx, gowork) +- if err != nil { +- return nil, err // canceled - } -- // default: print with imports -- { -- res := gopls(t, tree, "imports", "a.go") -- res.checkExit(true) -- if res.stdout != want { -- t.Errorf("imports: got <<%s>>, want <<%s>>", res.stdout, want) -- } +- content, err := fh.Content() +- if err != nil { +- return nil, err - } -- // -diff: show a unified diff -- { -- res := gopls(t, tree, "imports", "-diff", "a.go") -- res.checkExit(true) -- res.checkStdout(regexp.QuoteMeta(`+import "fmt"`)) +- filename := gowork.Path() +- dir := filepath.Dir(filename) +- workFile, err := modfile.ParseWork(filename, content, nil) +- if err != nil { +- return nil, fmt.Errorf("parsing go.work: %w", err) - } -- // -write: update file -- { -- res := gopls(t, tree, "imports", "-write", "a.go") -- res.checkExit(true) -- checkContent(t, filepath.Join(tree, "a.go"), want) +- var usedDirs []string +- for _, use := range workFile.Use { +- usedDirs = append(usedDirs, use.Path) - } +- return localModFiles(dir, usedDirs), nil -} - --// TestLinks tests the 'links' subcommand (../links.go). --func TestLinks(t *testing.T) { -- t.Parallel() -- -- tree := writeTree(t, ` ---- a.go -- --// Link in package doc: https://pkg.go.dev/ --package a +-// localModFiles builds a set of local go.mod files referenced by +-// goWorkOrModPaths, which is a slice of paths as contained in a go.work 'use' +-// directive or go.mod 'replace' directive (and which therefore may use either +-// '/' or '\' as a path separator). +-func localModFiles(relativeTo string, goWorkOrModPaths []string) map[protocol.DocumentURI]unit { +- modFiles := make(map[protocol.DocumentURI]unit) +- for _, path := range goWorkOrModPaths { +- modDir := filepath.FromSlash(path) +- if !filepath.IsAbs(modDir) { +- modDir = filepath.Join(relativeTo, modDir) +- } +- modURI := protocol.URIFromPath(filepath.Join(modDir, "go.mod")) +- modFiles[modURI] = unit{} +- } +- return modFiles +-} - --// Link in internal comment: https://go.dev/cl +-// isGoMod reports if uri is a go.mod file. +-func isGoMod(uri protocol.DocumentURI) bool { +- return filepath.Base(uri.Path()) == "go.mod" +-} - --// Doc comment link: https://blog.go.dev/ --func f() {} --`) -- // no arguments -- { -- res := gopls(t, tree, "links") -- res.checkExit(false) -- res.checkStderr("expects 1 argument") +-// goModModules returns the URIs of "workspace" go.mod files defined by a +-// go.mod file. This set is defined to be the given go.mod file itself, as well +-// as the modfiles of any locally replaced modules in the go.mod file. +-func goModModules(ctx context.Context, gomod protocol.DocumentURI, fs file.Source) (map[protocol.DocumentURI]unit, error) { +- fh, err := fs.ReadFile(ctx, gomod) +- if err != nil { +- return nil, err // canceled - } -- // success -- { -- res := gopls(t, tree, "links", "a.go") -- res.checkExit(true) -- res.checkStdout("https://go.dev/cl") -- res.checkStdout("https://pkg.go.dev") -- res.checkStdout("https://blog.go.dev/") +- content, err := fh.Content() +- if err != nil { +- return nil, err - } -- // -json -- { -- res := gopls(t, tree, "links", "-json", "a.go") -- res.checkExit(true) -- res.checkStdout("https://pkg.go.dev") -- res.checkStdout("https://go.dev/cl") -- res.checkStdout("https://blog.go.dev/") // at 5:21-5:41 -- var links []protocol.DocumentLink -- if res.toJSON(&links) { -- // Check just one of the three locations. -- if got, want := fmt.Sprint(links[2].Range), "5:21-5:41"; got != want { -- t.Errorf("wrong link location: got %v, want %v", got, want) -- } +- filename := gomod.Path() +- dir := filepath.Dir(filename) +- modFile, err := modfile.Parse(filename, content, nil) +- if err != nil { +- return nil, err +- } +- var localReplaces []string +- for _, replace := range modFile.Replace { +- if modfile.IsDirectoryPath(replace.New.Path) { +- localReplaces = append(localReplaces, replace.New.Path) - } - } +- modFiles := localModFiles(dir, localReplaces) +- modFiles[gomod] = unit{} +- return modFiles, nil -} - --// TestReferences tests the 'references' subcommand (../references.go). --func TestReferences(t *testing.T) { -- t.Parallel() +-// fileExists reports whether the file has a Content (which may be empty). +-// An overlay exists even if it is not reflected in the file system. +-func fileExists(fh file.Handle) bool { +- _, err := fh.Content() +- return err == nil +-} - -- tree := writeTree(t, ` ---- go.mod -- --module example.com --go 1.18 +-// errExhausted is returned by findModules if the file scan limit is reached. +-var errExhausted = errors.New("exhausted") - ---- a.go -- --package a --import "fmt" --func f() { -- fmt.Println() --} +-// Limit go.mod search to 1 million files. As a point of reference, +-// Kubernetes has 22K files (as of 2020-11-24). +-// +-// Note: per golang/go#56496, the previous limit of 1M files was too slow, at +-// which point this limit was decreased to 100K. +-const fileLimit = 100_000 +diff -urN a/gopls/internal/cache/xrefs/xrefs.go b/gopls/internal/cache/xrefs/xrefs.go +--- a/gopls/internal/cache/xrefs/xrefs.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cache/xrefs/xrefs.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,194 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - ---- b.go -- --package a --import "fmt" --func g() { -- fmt.Println() --} --`) -- // no arguments -- { -- res := gopls(t, tree, "references") -- res.checkExit(false) -- res.checkStderr("expects 1 argument") -- } -- // fmt.Println -- { -- res := gopls(t, tree, "references", "a.go:4:10") -- res.checkExit(true) -- res.checkStdout("a.go:4:6-13") -- res.checkStdout("b.go:4:6-13") -- } --} +-// Package xrefs defines the serializable index of cross-package +-// references that is computed during type checking. +-// +-// See ../references.go for the 'references' query. +-package xrefs - --// TestSignature tests the 'signature' subcommand (../signature.go). --func TestSignature(t *testing.T) { -- t.Parallel() +-import ( +- "go/ast" +- "go/types" +- "sort" - -- tree := writeTree(t, ` ---- go.mod -- --module example.com --go 1.18 +- "golang.org/x/tools/go/types/objectpath" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/frob" +- "golang.org/x/tools/gopls/internal/util/typesutil" +-) - ---- a.go -- --package a --import "fmt" --func f() { -- fmt.Println(123) --} --`) -- // no arguments -- { -- res := gopls(t, tree, "signature") -- res.checkExit(false) -- res.checkStderr("expects 1 argument") -- } -- // at 123 inside fmt.Println() call -- { -- res := gopls(t, tree, "signature", "a.go:4:15") -- res.checkExit(true) -- res.checkStdout("Println\\(a ...") -- res.checkStdout("Println formats using the default formats...") +-// Index constructs a serializable index of outbound cross-references +-// for the specified type-checked package. +-func Index(files []*parsego.File, pkg *types.Package, info *types.Info) []byte { +- // pkgObjects maps each referenced package Q to a mapping: +- // from each referenced symbol in Q to the ordered list +- // of references to that symbol from this package. +- // A nil types.Object indicates a reference +- // to the package as a whole: an import. +- pkgObjects := make(map[*types.Package]map[types.Object]*gobObject) +- +- // getObjects returns the object-to-references mapping for a package. +- getObjects := func(pkg *types.Package) map[types.Object]*gobObject { +- objects, ok := pkgObjects[pkg] +- if !ok { +- objects = make(map[types.Object]*gobObject) +- pkgObjects[pkg] = objects +- } +- return objects - } --} - --// TestPrepareRename tests the 'prepare_rename' subcommand (../prepare_rename.go). --func TestPrepareRename(t *testing.T) { -- t.Parallel() +- objectpathFor := new(objectpath.Encoder).For - -- tree := writeTree(t, ` ---- go.mod -- --module example.com --go 1.18 +- for fileIndex, pgf := range files { - ---- a.go -- --package a --func oldname() {} --`) -- // no arguments -- { -- res := gopls(t, tree, "prepare_rename") -- res.checkExit(false) -- res.checkStderr("expects 1 argument") -- } -- // in 'package' keyword -- { -- res := gopls(t, tree, "prepare_rename", "a.go:1:3") -- res.checkExit(false) -- res.checkStderr("request is not valid at the given position") -- } -- // in 'package' identifier (not supported by client) -- { -- res := gopls(t, tree, "prepare_rename", "a.go:1:9") -- res.checkExit(false) -- res.checkStderr("can't rename package") -- } -- // in func oldname -- { -- res := gopls(t, tree, "prepare_rename", "a.go:2:9") -- res.checkExit(true) -- res.checkStdout("a.go:2:6-13") // all of "oldname" -- } --} +- nodeRange := func(n ast.Node) protocol.Range { +- rng, err := pgf.PosRange(n.Pos(), n.End()) +- if err != nil { +- panic(err) // can't fail +- } +- return rng +- } - --// TestRename tests the 'rename' subcommand (../rename.go). --func TestRename(t *testing.T) { -- t.Parallel() +- ast.Inspect(pgf.File, func(n ast.Node) bool { +- switch n := n.(type) { +- case *ast.Ident: +- // Report a reference for each identifier that +- // uses a symbol exported from another package. +- // (The built-in error.Error method has no package.) +- if n.IsExported() { +- if obj, ok := info.Uses[n]; ok && +- obj.Pkg() != nil && +- obj.Pkg() != pkg { - -- tree := writeTree(t, ` ---- go.mod -- --module example.com --go 1.18 +- // For instantiations of generic methods, +- // use the generic object (see issue #60622). +- if fn, ok := obj.(*types.Func); ok { +- obj = fn.Origin() +- } - ---- a.go -- --package a --func oldname() {} --`) -- // no arguments -- { -- res := gopls(t, tree, "rename") -- res.checkExit(false) -- res.checkStderr("expects 2 arguments") -- } -- // missing newname -- { -- res := gopls(t, tree, "rename", "a.go:1:3") -- res.checkExit(false) -- res.checkStderr("expects 2 arguments") -- } -- // in 'package' keyword -- { -- res := gopls(t, tree, "rename", "a.go:1:3", "newname") -- res.checkExit(false) -- res.checkStderr("no identifier found") +- objects := getObjects(obj.Pkg()) +- gobObj, ok := objects[obj] +- if !ok { +- path, err := objectpathFor(obj) +- if err != nil { +- // Capitalized but not exported +- // (e.g. local const/var/type). +- return true +- } +- gobObj = &gobObject{Path: path} +- objects[obj] = gobObj +- } +- +- gobObj.Refs = append(gobObj.Refs, gobRef{ +- FileIndex: fileIndex, +- Range: nodeRange(n), +- }) +- } +- } +- +- case *ast.ImportSpec: +- // Report a reference from each import path +- // string to the imported package. +- pkgname, ok := typesutil.ImportedPkgName(info, n) +- if !ok { +- return true // missing import +- } +- objects := getObjects(pkgname.Imported()) +- gobObj, ok := objects[nil] +- if !ok { +- gobObj = &gobObject{Path: ""} +- objects[nil] = gobObj +- } +- gobObj.Refs = append(gobObj.Refs, gobRef{ +- FileIndex: fileIndex, +- Range: nodeRange(n.Path), +- }) +- } +- return true +- }) - } -- // in 'package' identifier -- { -- res := gopls(t, tree, "rename", "a.go:1:9", "newname") -- res.checkExit(false) -- res.checkStderr(`cannot rename package: module path .* same as the package path, so .* no effect`) +- +- // Flatten the maps into slices, and sort for determinism. +- var packages []*gobPackage +- for p := range pkgObjects { +- objects := pkgObjects[p] +- gp := &gobPackage{ +- PkgPath: metadata.PackagePath(p.Path()), +- Objects: make([]*gobObject, 0, len(objects)), +- } +- for _, gobObj := range objects { +- gp.Objects = append(gp.Objects, gobObj) +- } +- sort.Slice(gp.Objects, func(i, j int) bool { +- return gp.Objects[i].Path < gp.Objects[j].Path +- }) +- packages = append(packages, gp) - } -- // success, func oldname (and -diff) -- { -- res := gopls(t, tree, "rename", "-diff", "a.go:2:9", "newname") -- res.checkExit(true) -- res.checkStdout(regexp.QuoteMeta("-func oldname() {}")) -- res.checkStdout(regexp.QuoteMeta("+func newname() {}")) +- sort.Slice(packages, func(i, j int) bool { +- return packages[i].PkgPath < packages[j].PkgPath +- }) +- +- return packageCodec.Encode(packages) +-} +- +-// Lookup searches a serialized index produced by an indexPackage +-// operation on m, and returns the locations of all references from m +-// to any object in the target set. Each object is denoted by a pair +-// of (package path, object path). +-func Lookup(mp *metadata.Package, data []byte, targets map[metadata.PackagePath]map[objectpath.Path]struct{}) (locs []protocol.Location) { +- var packages []*gobPackage +- packageCodec.Decode(data, &packages) +- for _, gp := range packages { +- if objectSet, ok := targets[gp.PkgPath]; ok { +- for _, gobObj := range gp.Objects { +- if _, ok := objectSet[gobObj.Path]; ok { +- for _, ref := range gobObj.Refs { +- uri := mp.CompiledGoFiles[ref.FileIndex] +- locs = append(locs, protocol.Location{ +- URI: uri, +- Range: ref.Range, +- }) +- } +- } +- } +- } - } +- +- return locs -} - --// TestSymbols tests the 'symbols' subcommand (../symbols.go). --func TestSymbols(t *testing.T) { -- t.Parallel() +-// -- serialized representation -- - -- tree := writeTree(t, ` ---- go.mod -- --module example.com --go 1.18 +-// The cross-reference index records the location of all references +-// from one package to symbols defined in other packages +-// (dependencies). It does not record within-package references. +-// The index for package P consists of a list of gopPackage records, +-// each enumerating references to symbols defined a single dependency, Q. - ---- a.go -- --package a --func f() --var v int --const c = 0 --`) -- // no files -- { -- res := gopls(t, tree, "symbols") -- res.checkExit(false) -- res.checkStderr("expects 1 argument") -- } -- // success -- { -- res := gopls(t, tree, "symbols", "a.go:123:456") // (line/col ignored) -- res.checkExit(true) -- res.checkStdout("f Function 2:6-2:7") -- res.checkStdout("v Variable 3:5-3:6") -- res.checkStdout("c Constant 4:7-4:8") -- } --} +-// TODO(adonovan): opt: choose a more compact encoding. +-// The gobRef.Range field is the obvious place to begin. - --// TestSemtok tests the 'semtok' subcommand (../semantictokens.go). --func TestSemtok(t *testing.T) { -- t.Parallel() +-// (The name says gob but in fact we use frob.) +-var packageCodec = frob.CodecFor[[]*gobPackage]() - -- tree := writeTree(t, ` ---- go.mod -- --module example.com --go 1.18 +-// A gobPackage records the set of outgoing references from the index +-// package to symbols defined in a dependency package. +-type gobPackage struct { +- PkgPath metadata.PackagePath // defining package (Q) +- Objects []*gobObject // set of Q objects referenced by P +-} - ---- a.go -- --package a --func f() --var v int --const c = 0 --`) -- // no files -- { -- res := gopls(t, tree, "semtok") -- res.checkExit(false) -- res.checkStderr("expected one file name") -- } -- // success -- { -- res := gopls(t, tree, "semtok", "a.go") -- res.checkExit(true) -- got := res.stdout -- want := ` --/*⇒7,keyword,[]*/package /*⇒1,namespace,[]*/a --/*⇒4,keyword,[]*/func /*⇒1,function,[definition]*/f() --/*⇒3,keyword,[]*/var /*⇒1,variable,[definition]*/v /*⇒3,type,[defaultLibrary]*/int --/*⇒5,keyword,[]*/const /*⇒1,variable,[definition readonly]*/c = /*⇒1,number,[]*/0 --`[1:] -- if got != want { -- t.Errorf("semtok: got <<%s>>, want <<%s>>", got, want) -- } -- } +-// A gobObject records all references to a particular symbol. +-type gobObject struct { +- Path objectpath.Path // symbol name within package; "" => import of package itself +- Refs []gobRef // locations of references within P, in lexical order -} - --func TestStats(t *testing.T) { -- t.Parallel() +-type gobRef struct { +- FileIndex int // index of enclosing file within P's CompiledGoFiles +- Range protocol.Range // source range of reference +-} +diff -urN a/gopls/internal/cmd/call_hierarchy.go b/gopls/internal/cmd/call_hierarchy.go +--- a/gopls/internal/cmd/call_hierarchy.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/call_hierarchy.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,143 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- tree := writeTree(t, ` ---- go.mod -- --module example.com --go 1.18 +-package cmd - ---- a.go -- --package a ---- b/b.go -- --package b ---- testdata/foo.go -- --package foo +-import ( +- "context" +- "flag" +- "fmt" +- "strings" +- +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/tool" +-) +- +-// callHierarchy implements the callHierarchy verb for gopls. +-type callHierarchy struct { +- app *Application +-} +- +-func (c *callHierarchy) Name() string { return "call_hierarchy" } +-func (c *callHierarchy) Parent() string { return c.app.Name() } +-func (c *callHierarchy) Usage() string { return "" } +-func (c *callHierarchy) ShortHelp() string { return "display selected identifier's call hierarchy" } +-func (c *callHierarchy) DetailedHelp(f *flag.FlagSet) { +- fmt.Fprint(f.Output(), ` +-Example: +- +- $ # 1-indexed location (:line:column or :#offset) of the target identifier +- $ gopls call_hierarchy helper/helper.go:8:6 +- $ gopls call_hierarchy helper/helper.go:#53 -`) +- printFlagDefaults(f) +-} - -- // Trigger a bug report with a distinctive string -- // and check that it was durably recorded. -- oops := fmt.Sprintf("oops-%d", rand.Int()) -- { -- env := []string{"TEST_GOPLS_BUG=" + oops} -- res := goplsWithEnv(t, tree, env, "bug") -- res.checkExit(true) +-func (c *callHierarchy) Run(ctx context.Context, args ...string) error { +- if len(args) != 1 { +- return tool.CommandLineErrorf("call_hierarchy expects 1 argument (position)") - } - -- res := gopls(t, tree, "stats") -- res.checkExit(true) +- conn, err := c.app.connect(ctx, nil) +- if err != nil { +- return err +- } +- defer conn.terminate(ctx) - -- var stats cmd.GoplsStats -- if err := json.Unmarshal([]byte(res.stdout), &stats); err != nil { -- t.Fatalf("failed to unmarshal JSON output of stats command: %v", err) +- from := parseSpan(args[0]) +- file, err := conn.openFile(ctx, from.URI()) +- if err != nil { +- return err - } - -- // a few sanity checks -- checks := []struct { -- field string -- got int -- want int -- }{ -- { -- "WorkspaceStats.Views[0].WorkspaceModules", -- stats.WorkspaceStats.Views[0].WorkspacePackages.Modules, -- 1, -- }, -- { -- "WorkspaceStats.Views[0].WorkspacePackages", -- stats.WorkspaceStats.Views[0].WorkspacePackages.Packages, -- 2, -- }, -- {"DirStats.Files", stats.DirStats.Files, 4}, -- {"DirStats.GoFiles", stats.DirStats.GoFiles, 2}, -- {"DirStats.ModFiles", stats.DirStats.ModFiles, 1}, -- {"DirStats.TestdataFiles", stats.DirStats.TestdataFiles, 1}, +- loc, err := file.spanLocation(from) +- if err != nil { +- return err - } -- for _, check := range checks { -- if check.got != check.want { -- t.Errorf("stats.%s = %d, want %d", check.field, check.got, check.want) -- } +- +- p := protocol.CallHierarchyPrepareParams{ +- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), - } - -- // Check that we got a BugReport with the expected message. -- { -- got := fmt.Sprint(stats.BugReports) -- wants := []string{ -- "cmd/info.go", // File containing call to bug.Report -- oops, // Description +- callItems, err := conn.PrepareCallHierarchy(ctx, &p) +- if err != nil { +- return err +- } +- if len(callItems) == 0 { +- return fmt.Errorf("function declaration identifier not found at %v", args[0]) +- } +- +- for _, item := range callItems { +- incomingCalls, err := conn.IncomingCalls(ctx, &protocol.CallHierarchyIncomingCallsParams{Item: item}) +- if err != nil { +- return err - } -- for _, want := range wants { -- if !strings.Contains(got, want) { -- t.Errorf("BugReports does not contain %q. Got:<<%s>>", want, got) -- break +- for i, call := range incomingCalls { +- // From the spec: CallHierarchyIncomingCall.FromRanges is relative to +- // the caller denoted by CallHierarchyIncomingCall.from. +- printString, err := callItemPrintString(ctx, conn, call.From, call.From.URI, call.FromRanges) +- if err != nil { +- return err - } +- fmt.Printf("caller[%d]: %s\n", i, printString) - } -- } -- -- // Check that -anon suppresses fields containing user information. -- { -- res2 := gopls(t, tree, "stats", "-anon") -- res2.checkExit(true) - -- var stats2 cmd.GoplsStats -- if err := json.Unmarshal([]byte(res2.stdout), &stats2); err != nil { -- t.Fatalf("failed to unmarshal JSON output of stats command: %v", err) -- } -- if got := len(stats2.BugReports); got > 0 { -- t.Errorf("Got %d bug reports with -anon, want 0. Reports:%+v", got, stats2.BugReports) +- printString, err := callItemPrintString(ctx, conn, item, "", nil) +- if err != nil { +- return err - } -- var stats2AsMap map[string]interface{} -- if err := json.Unmarshal([]byte(res2.stdout), &stats2AsMap); err != nil { -- t.Fatalf("failed to unmarshal JSON output of stats command: %v", err) +- fmt.Printf("identifier: %s\n", printString) +- +- outgoingCalls, err := conn.OutgoingCalls(ctx, &protocol.CallHierarchyOutgoingCallsParams{Item: item}) +- if err != nil { +- return err - } -- // GOPACKAGESDRIVER is user information, but is ok to print zero value. -- if v, ok := stats2AsMap["GOPACKAGESDRIVER"]; !ok || v != "" { -- t.Errorf(`Got GOPACKAGESDRIVER=(%q, %v); want ("", true(found))`, v, ok) +- for i, call := range outgoingCalls { +- // From the spec: CallHierarchyOutgoingCall.FromRanges is the range +- // relative to the caller, e.g the item passed to +- printString, err := callItemPrintString(ctx, conn, call.To, item.URI, call.FromRanges) +- if err != nil { +- return err +- } +- fmt.Printf("callee[%d]: %s\n", i, printString) - } - } - -- // Check that -anon suppresses fields containing non-zero user information. -- { -- res3 := goplsWithEnv(t, tree, []string{"GOPACKAGESDRIVER=off"}, "stats", "-anon") -- res3.checkExit(true) +- return nil +-} - -- var statsAsMap3 map[string]interface{} -- if err := json.Unmarshal([]byte(res3.stdout), &statsAsMap3); err != nil { -- t.Fatalf("failed to unmarshal JSON output of stats command: %v", err) +-// callItemPrintString returns a protocol.CallHierarchyItem object represented as a string. +-// item and call ranges (protocol.Range) are converted to user friendly spans (1-indexed). +-func callItemPrintString(ctx context.Context, conn *connection, item protocol.CallHierarchyItem, callsURI protocol.DocumentURI, calls []protocol.Range) (string, error) { +- itemFile, err := conn.openFile(ctx, item.URI) +- if err != nil { +- return "", err +- } +- itemSpan, err := itemFile.rangeSpan(item.Range) +- if err != nil { +- return "", err +- } +- +- var callRanges []string +- if callsURI != "" { +- callsFile, err := conn.openFile(ctx, callsURI) +- if err != nil { +- return "", err - } -- // GOPACKAGESDRIVER is user information, want non-empty value to be omitted. -- if v, ok := statsAsMap3["GOPACKAGESDRIVER"]; ok { -- t.Errorf(`Got GOPACKAGESDRIVER=(%q, %v); want ("", false(not found))`, v, ok) +- for _, rng := range calls { +- call, err := callsFile.rangeSpan(rng) +- if err != nil { +- return "", err +- } +- callRange := fmt.Sprintf("%d:%d-%d", call.Start().Line(), call.Start().Column(), call.End().Column()) +- callRanges = append(callRanges, callRange) - } - } +- +- printString := fmt.Sprintf("function %s in %v", item.Name, itemSpan) +- if len(calls) > 0 { +- printString = fmt.Sprintf("ranges %s in %s from/to %s", strings.Join(callRanges, ", "), callsURI.Path(), printString) +- } +- return printString, nil -} +diff -urN a/gopls/internal/cmd/capabilities_test.go b/gopls/internal/cmd/capabilities_test.go +--- a/gopls/internal/cmd/capabilities_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/capabilities_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,176 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// TestFix tests the 'fix' subcommand (../suggested_fix.go). --func TestFix(t *testing.T) { -- t.Parallel() +-package cmd - -- tree := writeTree(t, ` ---- go.mod -- --module example.com --go 1.18 +-import ( +- "context" +- "fmt" +- "os" +- "path/filepath" +- "testing" - ---- a.go -- --package a --type T int --func f() (int, string) { return } +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/server" +- "golang.org/x/tools/gopls/internal/settings" +- "golang.org/x/tools/internal/testenv" +-) - ---- b.go -- --package a --import "io" --var _ io.Reader = C{} --type C struct{} --`) +-// TestCapabilities does some minimal validation of the server's adherence to the LSP. +-// The checks in the test are added as changes are made and errors noticed. +-func TestCapabilities(t *testing.T) { +- // TODO(bcmills): This test fails on js/wasm, which is not unexpected, but the +- // failure mode is that the DidOpen call below reports "no views in session", +- // which seems a little too cryptic. +- // Is there some missing error reporting somewhere? +- testenv.NeedsTool(t, "go") - -- // no arguments -- { -- res := gopls(t, tree, "fix") -- res.checkExit(false) -- res.checkStderr("expects at least 1 argument") +- tmpDir, err := os.MkdirTemp("", "fake") +- if err != nil { +- t.Fatal(err) - } -- // success with default kinds, {quickfix}. -- // -a is always required because no fix is currently "preferred" (!) -- { -- res := gopls(t, tree, "fix", "-a", "a.go") -- res.checkExit(true) -- got := res.stdout -- want := ` --package a --type T int --func f() (int, string) { return 0, "" } -- --`[1:] -- if got != want { -- t.Errorf("fix: got <<%s>>, want <<%s>>\nstderr:\n%s", got, want, res.stderr) -- } +- tmpFile := filepath.Join(tmpDir, "fake.go") +- if err := os.WriteFile(tmpFile, []byte(""), 0775); err != nil { +- t.Fatal(err) - } -- // success, with explicit CodeAction kind and diagnostics span. -- { -- res := gopls(t, tree, "fix", "-a", "b.go:#40", "quickfix") -- res.checkExit(true) -- got := res.stdout -- want := ` --package a -- --import "io" -- --var _ io.Reader = C{} -- --type C struct{} -- --// Read implements io.Reader. --func (C) Read(p []byte) (n int, err error) { -- panic("unimplemented") --} --`[1:] -- if got != want { -- t.Errorf("fix: got <<%s>>, want <<%s>>\nstderr:\n%s", got, want, res.stderr) -- } +- if err := os.WriteFile(filepath.Join(tmpDir, "go.mod"), []byte("module fake\n\ngo 1.12\n"), 0775); err != nil { +- t.Fatal(err) - } --} +- defer os.RemoveAll(tmpDir) - --// TestWorkspaceSymbol tests the 'workspace_symbol' subcommand (../workspace_symbol.go). --func TestWorkspaceSymbol(t *testing.T) { -- t.Parallel() +- app := New(nil) - -- tree := writeTree(t, ` ---- go.mod -- --module example.com --go 1.18 +- params := &protocol.ParamInitialize{} +- params.RootURI = protocol.URIFromPath(tmpDir) +- params.Capabilities.Workspace.Configuration = true - ---- a.go -- --package a --func someFunctionName() --`) -- // no files -- { -- res := gopls(t, tree, "workspace_symbol") -- res.checkExit(false) -- res.checkStderr("expects 1 argument") +- // Send an initialize request to the server. +- ctx := context.Background() +- client := newClient(app, nil) +- options := settings.DefaultOptions(app.options) +- server := server.New(cache.NewSession(ctx, cache.New(nil)), client, options) +- result, err := server.Initialize(ctx, params) +- if err != nil { +- t.Fatal(err) - } -- // success -- { -- res := gopls(t, tree, "workspace_symbol", "meFun") -- res.checkExit(true) -- res.checkStdout("a.go:2:6-22 someFunctionName Function") +- // Validate initialization result. +- if err := validateCapabilities(result); err != nil { +- t.Error(err) +- } +- // Complete initialization of server. +- if err := server.Initialized(ctx, &protocol.InitializedParams{}); err != nil { +- t.Fatal(err) - } --} - --// -- test framework -- +- c := newConnection(server, client) +- defer c.terminate(ctx) - --func TestMain(m *testing.M) { -- switch os.Getenv("ENTRYPOINT") { -- case "goplsMain": -- goplsMain() -- default: -- os.Exit(m.Run()) +- // Open the file on the server side. +- uri := protocol.URIFromPath(tmpFile) +- if err := c.Server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{ +- TextDocument: protocol.TextDocumentItem{ +- URI: uri, +- LanguageID: "go", +- Version: 1, +- Text: `package main; func main() {};`, +- }, +- }); err != nil { +- t.Fatal(err) - } --} - --// This function is a stand-in for gopls.main in ../../../../main.go. --func goplsMain() { -- // Panic on bugs (unlike the production gopls command), -- // except in tests that inject calls to bug.Report. -- if os.Getenv("TEST_GOPLS_BUG") == "" { -- bug.PanicOnBugs = true +- // If we are sending a full text change, the change.Range must be nil. +- // It is not enough for the Change to be empty, as that is ambiguous. +- if err := c.Server.DidChange(ctx, &protocol.DidChangeTextDocumentParams{ +- TextDocument: protocol.VersionedTextDocumentIdentifier{ +- TextDocumentIdentifier: protocol.TextDocumentIdentifier{ +- URI: uri, +- }, +- Version: 2, +- }, +- ContentChanges: []protocol.TextDocumentContentChangeEvent{ +- { +- Range: nil, +- Text: `package main; func main() { fmt.Println("") }`, +- }, +- }, +- }); err != nil { +- t.Fatal(err) - } - -- tool.Main(context.Background(), cmd.New("gopls", "", nil, hooks.Options), os.Args[1:]) --} -- --// writeTree extracts a txtar archive into a new directory and returns its path. --func writeTree(t *testing.T, archive string) string { -- root := t.TempDir() -- -- // This unfortunate step is required because gopls output -- // expands symbolic links it its input file names (arguably it -- // should not), and on macOS the temp dir is in /var -> private/var. -- root, err := filepath.EvalSymlinks(root) +- // Send a code action request to validate expected types. +- actions, err := c.Server.CodeAction(ctx, &protocol.CodeActionParams{ +- TextDocument: protocol.TextDocumentIdentifier{ +- URI: uri, +- }, +- }) - if err != nil { - t.Fatal(err) - } -- -- for _, f := range txtar.Parse([]byte(archive)).Files { -- filename := filepath.Join(root, f.Name) -- if err := os.MkdirAll(filepath.Dir(filename), 0777); err != nil { -- t.Fatal(err) -- } -- if err := os.WriteFile(filename, f.Data, 0666); err != nil { -- t.Fatal(err) +- for _, action := range actions { +- // Validate that an empty command is sent along with import organization responses. +- if action.Kind == protocol.SourceOrganizeImports && action.Command != nil { +- t.Errorf("unexpected command for import organization") - } - } -- return root --} -- --// gopls executes gopls in a child process. --func gopls(t *testing.T, dir string, args ...string) *result { -- return goplsWithEnv(t, dir, nil, args...) --} - --func goplsWithEnv(t *testing.T, dir string, env []string, args ...string) *result { -- testenv.NeedsTool(t, "go") -- -- // Catch inadvertent use of dir=".", which would make -- // the ReplaceAll below unpredictable. -- if !filepath.IsAbs(dir) { -- t.Fatalf("dir is not absolute: %s", dir) +- if err := c.Server.DidSave(ctx, &protocol.DidSaveTextDocumentParams{ +- TextDocument: protocol.TextDocumentIdentifier{ +- URI: uri, +- }, +- // LSP specifies that a file can be saved with optional text, so this field must be nil. +- Text: nil, +- }); err != nil { +- t.Fatal(err) - } - -- goplsCmd := exec.Command(os.Args[0], args...) -- goplsCmd.Env = append(os.Environ(), "ENTRYPOINT=goplsMain") -- goplsCmd.Env = append(goplsCmd.Env, env...) -- goplsCmd.Dir = dir -- goplsCmd.Stdout = new(bytes.Buffer) -- goplsCmd.Stderr = new(bytes.Buffer) -- -- cmdErr := goplsCmd.Run() -- -- stdout := strings.ReplaceAll(fmt.Sprint(goplsCmd.Stdout), dir, ".") -- stderr := strings.ReplaceAll(fmt.Sprint(goplsCmd.Stderr), dir, ".") -- exitcode := 0 -- if cmdErr != nil { -- if exitErr, ok := cmdErr.(*exec.ExitError); ok { -- exitcode = exitErr.ExitCode() -- } else { -- stderr = cmdErr.Error() // (execve failure) -- exitcode = -1 +- // Send a completion request to validate expected types. +- list, err := c.Server.Completion(ctx, &protocol.CompletionParams{ +- TextDocumentPositionParams: protocol.TextDocumentPositionParams{ +- TextDocument: protocol.TextDocumentIdentifier{ +- URI: uri, +- }, +- Position: protocol.Position{ +- Line: 0, +- Character: 28, +- }, +- }, +- }) +- if err != nil { +- t.Fatal(err) +- } +- for _, item := range list.Items { +- // All other completion items should have nil commands. +- // An empty command will be treated as a command with the name '' by VS Code. +- // This causes VS Code to report errors to users about invalid commands. +- if item.Command != nil { +- t.Errorf("unexpected command for completion item") +- } +- // The item's TextEdit must be a pointer, as VS Code considers TextEdits +- // that don't contain the cursor position to be invalid. +- var textEdit interface{} = item.TextEdit +- if _, ok := textEdit.(*protocol.TextEdit); !ok { +- t.Errorf("textEdit is not a *protocol.TextEdit, instead it is %T", textEdit) - } - } -- res := &result{ -- t: t, -- command: "gopls " + strings.Join(args, " "), -- exitcode: exitcode, -- stdout: stdout, -- stderr: stderr, +- if err := c.Server.Shutdown(ctx); err != nil { +- t.Fatal(err) - } -- if false { -- t.Log(res) +- if err := c.Server.Exit(ctx); err != nil { +- t.Fatal(err) - } -- return res -} - --// A result holds the result of a gopls invocation, and provides assertion helpers. --type result struct { -- t *testing.T -- command string -- exitcode int -- stdout, stderr string +-func validateCapabilities(result *protocol.InitializeResult) error { +- // If the client sends "false" for RenameProvider.PrepareSupport, +- // the server must respond with a boolean. +- if v, ok := result.Capabilities.RenameProvider.(bool); !ok { +- return fmt.Errorf("RenameProvider must be a boolean if PrepareSupport is false (got %T)", v) +- } +- // The same goes for CodeActionKind.ValueSet. +- if v, ok := result.Capabilities.CodeActionProvider.(bool); !ok { +- return fmt.Errorf("CodeActionSupport must be a boolean if CodeActionKind.ValueSet has length 0 (got %T)", v) +- } +- return nil -} +diff -urN a/gopls/internal/cmd/check.go b/gopls/internal/cmd/check.go +--- a/gopls/internal/cmd/check.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/check.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,73 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func (res *result) String() string { -- return fmt.Sprintf("%s: exit=%d stdout=<<%s>> stderr=<<%s>>", -- res.command, res.exitcode, res.stdout, res.stderr) --} +-package cmd - --// checkExit asserts that gopls returned the expected exit code. --func (res *result) checkExit(success bool) { -- res.t.Helper() -- if (res.exitcode == 0) != success { -- res.t.Errorf("%s: exited with code %d, want success: %t (%s)", -- res.command, res.exitcode, success, res) -- } --} +-import ( +- "context" +- "flag" +- "fmt" - --// checkStdout asserts that the gopls standard output matches the pattern. --func (res *result) checkStdout(pattern string) { -- res.t.Helper() -- res.checkOutput(pattern, "stdout", res.stdout) --} +- "golang.org/x/tools/gopls/internal/protocol" +-) - --// checkStderr asserts that the gopls standard error matches the pattern. --func (res *result) checkStderr(pattern string) { -- res.t.Helper() -- res.checkOutput(pattern, "stderr", res.stderr) +-// check implements the check verb for gopls. +-type check struct { +- app *Application -} - --func (res *result) checkOutput(pattern, name, content string) { -- res.t.Helper() -- if match, err := regexp.MatchString(pattern, content); err != nil { -- res.t.Errorf("invalid regexp: %v", err) -- } else if !match { -- res.t.Errorf("%s: %s does not match [%s]; got <<%s>>", -- res.command, name, pattern, content) -- } --} +-func (c *check) Name() string { return "check" } +-func (c *check) Parent() string { return c.app.Name() } +-func (c *check) Usage() string { return "" } +-func (c *check) ShortHelp() string { return "show diagnostic results for the specified file" } +-func (c *check) DetailedHelp(f *flag.FlagSet) { +- fmt.Fprint(f.Output(), ` +-Example: show the diagnostic results of this file: - --// toJSON decodes res.stdout as JSON into to *ptr and reports its success. --func (res *result) toJSON(ptr interface{}) bool { -- if err := json.Unmarshal([]byte(res.stdout), ptr); err != nil { -- res.t.Errorf("invalid JSON %v", err) -- return false -- } -- return true +- $ gopls check internal/cmd/check.go +-`) +- printFlagDefaults(f) -} - --// checkContent checks that the contents of the file are as expected. --func checkContent(t *testing.T, filename, want string) { -- data, err := os.ReadFile(filename) +-// Run performs the check on the files specified by args and prints the +-// results to stdout. +-func (c *check) Run(ctx context.Context, args ...string) error { +- if len(args) == 0 { +- // no files, so no results +- return nil +- } +- checking := map[protocol.DocumentURI]*cmdFile{} +- var uris []protocol.DocumentURI +- // now we ready to kick things off +- conn, err := c.app.connect(ctx, nil) - if err != nil { -- t.Error(err) -- return +- return err - } -- if got := string(data); got != want { -- t.Errorf("content of %s is <<%s>>, want <<%s>>", filename, got, want) +- defer conn.terminate(ctx) +- for _, arg := range args { +- uri := protocol.URIFromPath(arg) +- uris = append(uris, uri) +- file, err := conn.openFile(ctx, uri) +- if err != nil { +- return err +- } +- checking[uri] = file - } --} -diff -urN a/gopls/internal/lsp/cmd/usage/api-json.hlp b/gopls/internal/lsp/cmd/usage/api-json.hlp ---- a/gopls/internal/lsp/cmd/usage/api-json.hlp 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/usage/api-json.hlp 1970-01-01 00:00:00.000000000 +0000 -@@ -1,4 +0,0 @@ --print json describing gopls API -- --Usage: -- gopls [flags] api-json -diff -urN a/gopls/internal/lsp/cmd/usage/bug.hlp b/gopls/internal/lsp/cmd/usage/bug.hlp ---- a/gopls/internal/lsp/cmd/usage/bug.hlp 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/usage/bug.hlp 1970-01-01 00:00:00.000000000 +0000 -@@ -1,4 +0,0 @@ --report a bug in gopls +- if err := conn.diagnoseFiles(ctx, uris); err != nil { +- return err +- } +- conn.client.filesMu.Lock() +- defer conn.client.filesMu.Unlock() - --Usage: -- gopls [flags] bug -diff -urN a/gopls/internal/lsp/cmd/usage/call_hierarchy.hlp b/gopls/internal/lsp/cmd/usage/call_hierarchy.hlp ---- a/gopls/internal/lsp/cmd/usage/call_hierarchy.hlp 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/usage/call_hierarchy.hlp 1970-01-01 00:00:00.000000000 +0000 -@@ -1,10 +0,0 @@ --display selected identifier's call hierarchy +- for _, file := range checking { +- for _, d := range file.diagnostics { +- spn, err := file.rangeSpan(d.Range) +- if err != nil { +- return fmt.Errorf("Could not convert position %v for %q", d.Range, d.Message) +- } +- fmt.Printf("%v: %v\n", spn, d.Message) +- } +- } +- return nil +-} +diff -urN a/gopls/internal/cmd/cmd.go b/gopls/internal/cmd/cmd.go +--- a/gopls/internal/cmd/cmd.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/cmd.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,843 +0,0 @@ +-// Copyright 2018 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --Usage: -- gopls [flags] call_hierarchy +-// Package cmd handles the gopls command line. +-// It contains a handler for each of the modes, along with all the flag handling +-// and the command line output format. +-package cmd - --Example: +-import ( +- "context" +- "flag" +- "fmt" +- "log" +- "os" +- "path/filepath" +- "reflect" +- "sort" +- "strings" +- "sync" +- "text/tabwriter" +- "time" - -- $ # 1-indexed location (:line:column or :#offset) of the target identifier -- $ gopls call_hierarchy helper/helper.go:8:6 -- $ gopls call_hierarchy helper/helper.go:#53 -diff -urN a/gopls/internal/lsp/cmd/usage/check.hlp b/gopls/internal/lsp/cmd/usage/check.hlp ---- a/gopls/internal/lsp/cmd/usage/check.hlp 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/usage/check.hlp 1970-01-01 00:00:00.000000000 +0000 -@@ -1,8 +0,0 @@ --show diagnostic results for the specified file +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/debug" +- "golang.org/x/tools/gopls/internal/filecache" +- "golang.org/x/tools/gopls/internal/lsprpc" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/command" +- "golang.org/x/tools/gopls/internal/server" +- "golang.org/x/tools/gopls/internal/settings" +- "golang.org/x/tools/gopls/internal/util/browser" +- bugpkg "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/gopls/internal/util/constraints" +- "golang.org/x/tools/internal/diff" +- "golang.org/x/tools/internal/jsonrpc2" +- "golang.org/x/tools/internal/tool" +-) - --Usage: -- gopls [flags] check +-// Application is the main application as passed to tool.Main +-// It handles the main command line parsing and dispatch to the sub commands. +-type Application struct { +- // Core application flags - --Example: show the diagnostic results of this file: +- // Embed the basic profiling flags supported by the tool package +- tool.Profile - -- $ gopls check internal/lsp/cmd/check.go -diff -urN a/gopls/internal/lsp/cmd/usage/definition.hlp b/gopls/internal/lsp/cmd/usage/definition.hlp ---- a/gopls/internal/lsp/cmd/usage/definition.hlp 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/usage/definition.hlp 1970-01-01 00:00:00.000000000 +0000 -@@ -1,15 +0,0 @@ --show declaration of selected identifier +- // We include the server configuration directly for now, so the flags work +- // even without the verb. +- // TODO: Remove this when we stop allowing the serve verb by default. +- Serve Serve - --Usage: -- gopls [flags] definition [definition-flags] +- // the options configuring function to invoke when building a server +- options func(*settings.Options) - --Example: show the definition of the identifier at syntax at offset 44 in this file (flag.FlagSet): +- // Support for remote LSP server. +- Remote string `flag:"remote" help:"forward all commands to a remote lsp specified by this flag. With no special prefix, this is assumed to be a TCP address. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. If 'auto', or prefixed by 'auto;', the remote address is automatically resolved based on the executing environment."` - -- $ gopls definition internal/lsp/cmd/definition.go:44:47 -- $ gopls definition internal/lsp/cmd/definition.go:#1270 +- // Verbose enables verbose logging. +- Verbose bool `flag:"v,verbose" help:"verbose output"` - --definition-flags: -- -json -- emit output in JSON format -- -markdown -- support markdown in responses -diff -urN a/gopls/internal/lsp/cmd/usage/fix.hlp b/gopls/internal/lsp/cmd/usage/fix.hlp ---- a/gopls/internal/lsp/cmd/usage/fix.hlp 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/usage/fix.hlp 1970-01-01 00:00:00.000000000 +0000 -@@ -1,44 +0,0 @@ --apply suggested fixes +- // VeryVerbose enables a higher level of verbosity in logging output. +- VeryVerbose bool `flag:"vv,veryverbose" help:"very verbose output"` - --Usage: -- gopls [flags] fix [fix-flags] +- // Control ocagent export of telemetry +- OCAgent string `flag:"ocagent" help:"the address of the ocagent (e.g. http://localhost:55678), or off"` - --Example: apply fixes to this file, rewriting it: +- // PrepareOptions is called to update the options when a new view is built. +- // It is primarily to allow the behavior of gopls to be modified by hooks. +- PrepareOptions func(*settings.Options) - -- $ gopls fix -a -w internal/lsp/cmd/check.go +- // editFlags holds flags that control how file edit operations +- // are applied, in particular when the server makes an ApplyEdits +- // downcall to the client. Present only for commands that apply edits. +- editFlags *EditFlags +-} - --The -a (-all) flag causes all fixes, not just preferred ones, to be --applied, but since no fixes are currently preferred, this flag is --essentially mandatory. +-// EditFlags defines flags common to {fix,format,imports,rename} +-// that control how edits are applied to the client's files. +-// +-// The type is exported for flag reflection. +-// +-// The -write, -diff, and -list flags are orthogonal but any +-// of them suppresses the default behavior, which is to print +-// the edited file contents. +-type EditFlags struct { +- Write bool `flag:"w,write" help:"write edited content to source files"` +- Preserve bool `flag:"preserve" help:"with -write, make copies of original files"` +- Diff bool `flag:"d,diff" help:"display diffs instead of edited file content"` +- List bool `flag:"l,list" help:"display names of edited files"` +-} - --Arguments after the filename are interpreted as LSP CodeAction kinds --to be applied; the default set is {"quickfix"}, but valid kinds include: +-func (app *Application) verbose() bool { +- return app.Verbose || app.VeryVerbose +-} - -- quickfix -- refactor -- refactor.extract -- refactor.inline -- refactor.rewrite -- source.organizeImports -- source.fixAll +-// New returns a new Application ready to run. +-func New(options func(*settings.Options)) *Application { +- app := &Application{ +- options: options, +- OCAgent: "off", //TODO: Remove this line to default the exporter to on - --CodeAction kinds are hierarchical, so "refactor" includes --"refactor.inline". There is currently no way to enable or even --enumerate all kinds. +- Serve: Serve{ +- RemoteListenTimeout: 1 * time.Minute, +- }, +- } +- app.Serve.app = app +- return app +-} - --Example: apply any "refactor.rewrite" fixes at the specific byte --offset within this file: +-// Name implements tool.Application returning the binary name. +-func (app *Application) Name() string { return "gopls" } - -- $ gopls fix -a internal/lsp/cmd/check.go:#43 refactor.rewrite +-// Usage implements tool.Application returning empty extra argument usage. +-func (app *Application) Usage() string { return "" } - --fix-flags: -- -a,-all -- apply all fixes, not just preferred fixes -- -d,-diff -- display diffs instead of edited file content -- -l,-list -- display names of edited files -- -preserve -- with -write, make copies of original files -- -w,-write -- write edited content to source files -diff -urN a/gopls/internal/lsp/cmd/usage/folding_ranges.hlp b/gopls/internal/lsp/cmd/usage/folding_ranges.hlp ---- a/gopls/internal/lsp/cmd/usage/folding_ranges.hlp 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/usage/folding_ranges.hlp 1970-01-01 00:00:00.000000000 +0000 -@@ -1,8 +0,0 @@ --display selected file's folding ranges +-// ShortHelp implements tool.Application returning the main binary help. +-func (app *Application) ShortHelp() string { +- return "" +-} - --Usage: -- gopls [flags] folding_ranges +-// DetailedHelp implements tool.Application returning the main binary help. +-// This includes the short help for all the sub commands. +-func (app *Application) DetailedHelp(f *flag.FlagSet) { +- w := tabwriter.NewWriter(f.Output(), 0, 0, 2, ' ', 0) +- defer w.Flush() - --Example: +- fmt.Fprint(w, ` +-gopls is a Go language server. - -- $ gopls folding_ranges helper/helper.go -diff -urN a/gopls/internal/lsp/cmd/usage/format.hlp b/gopls/internal/lsp/cmd/usage/format.hlp ---- a/gopls/internal/lsp/cmd/usage/format.hlp 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/usage/format.hlp 1970-01-01 00:00:00.000000000 +0000 -@@ -1,20 +0,0 @@ --format the code according to the go standard +-It is typically used with an editor to provide language features. When no +-command is specified, gopls will default to the 'serve' command. The language +-features can also be accessed via the gopls command-line interface. - -Usage: -- gopls [flags] format [format-flags] +- gopls help [] - --The arguments supplied may be simple file names, or ranges within files. +-Command: +-`) +- fmt.Fprint(w, "\nMain\t\n") +- for _, c := range app.mainCommands() { +- fmt.Fprintf(w, " %s\t%s\n", c.Name(), c.ShortHelp()) +- } +- fmt.Fprint(w, "\t\nFeatures\t\n") +- for _, c := range app.featureCommands() { +- fmt.Fprintf(w, " %s\t%s\n", c.Name(), c.ShortHelp()) +- } +- if app.verbose() { +- fmt.Fprint(w, "\t\nInternal Use Only\t\n") +- for _, c := range app.internalCommands() { +- fmt.Fprintf(w, " %s\t%s\n", c.Name(), c.ShortHelp()) +- } +- } +- fmt.Fprint(w, "\nflags:\n") +- printFlagDefaults(f) +-} - --Example: reformat this file: +-// this is a slightly modified version of flag.PrintDefaults to give us control +-func printFlagDefaults(s *flag.FlagSet) { +- var flags [][]*flag.Flag +- seen := map[flag.Value]int{} +- s.VisitAll(func(f *flag.Flag) { +- if i, ok := seen[f.Value]; !ok { +- seen[f.Value] = len(flags) +- flags = append(flags, []*flag.Flag{f}) +- } else { +- flags[i] = append(flags[i], f) +- } +- }) +- for _, entry := range flags { +- sort.SliceStable(entry, func(i, j int) bool { +- return len(entry[i].Name) < len(entry[j].Name) +- }) +- var b strings.Builder +- for i, f := range entry { +- switch i { +- case 0: +- b.WriteString(" -") +- default: +- b.WriteString(",-") +- } +- b.WriteString(f.Name) +- } - -- $ gopls format -w internal/lsp/cmd/check.go +- f := entry[0] +- name, usage := flag.UnquoteUsage(f) +- if len(name) > 0 { +- b.WriteString("=") +- b.WriteString(name) +- } +- // Boolean flags of one ASCII letter are so common we +- // treat them specially, putting their usage on the same line. +- if b.Len() <= 4 { // space, space, '-', 'x'. +- b.WriteString("\t") +- } else { +- // Four spaces before the tab triggers good alignment +- // for both 4- and 8-space tab stops. +- b.WriteString("\n \t") +- } +- b.WriteString(strings.ReplaceAll(usage, "\n", "\n \t")) +- if !isZeroValue(f, f.DefValue) { +- if reflect.TypeOf(f.Value).Elem().Name() == "stringValue" { +- fmt.Fprintf(&b, " (default %q)", f.DefValue) +- } else { +- fmt.Fprintf(&b, " (default %v)", f.DefValue) +- } +- } +- fmt.Fprint(s.Output(), b.String(), "\n") +- } +-} - --format-flags: -- -d,-diff -- display diffs instead of edited file content -- -l,-list -- display names of edited files -- -preserve -- with -write, make copies of original files -- -w,-write -- write edited content to source files -diff -urN a/gopls/internal/lsp/cmd/usage/help.hlp b/gopls/internal/lsp/cmd/usage/help.hlp ---- a/gopls/internal/lsp/cmd/usage/help.hlp 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/usage/help.hlp 1970-01-01 00:00:00.000000000 +0000 -@@ -1,10 +0,0 @@ --print usage information for subcommands +-// isZeroValue is copied from the flags package +-func isZeroValue(f *flag.Flag, value string) bool { +- // Build a zero value of the flag's Value type, and see if the +- // result of calling its String method equals the value passed in. +- // This works unless the Value type is itself an interface type. +- typ := reflect.TypeOf(f.Value) +- var z reflect.Value +- if typ.Kind() == reflect.Ptr { +- z = reflect.New(typ.Elem()) +- } else { +- z = reflect.Zero(typ) +- } +- return value == z.Interface().(flag.Value).String() +-} - --Usage: -- gopls [flags] help +-// Run takes the args after top level flag processing, and invokes the correct +-// sub command as specified by the first argument. +-// If no arguments are passed it will invoke the server sub command, as a +-// temporary measure for compatibility. +-func (app *Application) Run(ctx context.Context, args ...string) error { +- // In the category of "things we can do while waiting for the Go command": +- // Pre-initialize the filecache, which takes ~50ms to hash the gopls +- // executable, and immediately runs a gc. +- filecache.Start() - +- ctx = debug.WithInstance(ctx, app.OCAgent) +- if len(args) == 0 { +- s := flag.NewFlagSet(app.Name(), flag.ExitOnError) +- return tool.Run(ctx, s, &app.Serve, args) +- } +- command, args := args[0], args[1:] +- for _, c := range app.Commands() { +- if c.Name() == command { +- s := flag.NewFlagSet(app.Name(), flag.ExitOnError) +- return tool.Run(ctx, s, c, args) +- } +- } +- return tool.CommandLineErrorf("Unknown command %v", command) +-} - --Examples: --$ gopls help # main gopls help message --$ gopls help remote # help on 'remote' command --$ gopls help remote sessions # help on 'remote sessions' subcommand -diff -urN a/gopls/internal/lsp/cmd/usage/highlight.hlp b/gopls/internal/lsp/cmd/usage/highlight.hlp ---- a/gopls/internal/lsp/cmd/usage/highlight.hlp 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/usage/highlight.hlp 1970-01-01 00:00:00.000000000 +0000 -@@ -1,10 +0,0 @@ --display selected identifier's highlights +-// Commands returns the set of commands supported by the gopls tool on the +-// command line. +-// The command is specified by the first non flag argument. +-func (app *Application) Commands() []tool.Application { +- var commands []tool.Application +- commands = append(commands, app.mainCommands()...) +- commands = append(commands, app.featureCommands()...) +- commands = append(commands, app.internalCommands()...) +- return commands +-} - --Usage: -- gopls [flags] highlight +-func (app *Application) mainCommands() []tool.Application { +- return []tool.Application{ +- &app.Serve, +- &version{app: app}, +- &bug{app: app}, +- &help{app: app}, +- &apiJSON{app: app}, +- &licenses{app: app}, +- } +-} - --Example: +-func (app *Application) internalCommands() []tool.Application { +- return []tool.Application{ +- &vulncheck{app: app}, +- } +-} - -- $ # 1-indexed location (:line:column or :#offset) of the target identifier -- $ gopls highlight helper/helper.go:8:6 -- $ gopls highlight helper/helper.go:#53 -diff -urN a/gopls/internal/lsp/cmd/usage/implementation.hlp b/gopls/internal/lsp/cmd/usage/implementation.hlp ---- a/gopls/internal/lsp/cmd/usage/implementation.hlp 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/usage/implementation.hlp 1970-01-01 00:00:00.000000000 +0000 -@@ -1,10 +0,0 @@ --display selected identifier's implementation +-func (app *Application) featureCommands() []tool.Application { +- return []tool.Application{ +- &callHierarchy{app: app}, +- &check{app: app}, +- &codelens{app: app}, +- &definition{app: app}, +- &execute{app: app}, +- &foldingRanges{app: app}, +- &format{app: app}, +- &highlight{app: app}, +- &implementation{app: app}, +- &imports{app: app}, +- newRemote(app, ""), +- newRemote(app, "inspect"), +- &links{app: app}, +- &prepareRename{app: app}, +- &references{app: app}, +- &rename{app: app}, +- &semtok{app: app}, +- &signature{app: app}, +- &stats{app: app}, +- &suggestedFix{app: app}, +- &symbols{app: app}, - --Usage: -- gopls [flags] implementation +- &workspaceSymbol{app: app}, +- } +-} - --Example: +-var ( +- internalMu sync.Mutex +- internalConnections = make(map[string]*connection) +-) - -- $ # 1-indexed location (:line:column or :#offset) of the target identifier -- $ gopls implementation helper/helper.go:8:6 -- $ gopls implementation helper/helper.go:#53 -diff -urN a/gopls/internal/lsp/cmd/usage/imports.hlp b/gopls/internal/lsp/cmd/usage/imports.hlp ---- a/gopls/internal/lsp/cmd/usage/imports.hlp 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/usage/imports.hlp 1970-01-01 00:00:00.000000000 +0000 -@@ -1,18 +0,0 @@ --updates import statements +-// connect creates and initializes a new in-process gopls session. +-// +-// If onProgress is set, it is called for each new progress notification. +-func (app *Application) connect(ctx context.Context, onProgress func(*protocol.ProgressParams)) (*connection, error) { +- switch { +- case app.Remote == "": +- client := newClient(app, onProgress) +- options := settings.DefaultOptions(app.options) +- server := server.New(cache.NewSession(ctx, cache.New(nil)), client, options) +- conn := newConnection(server, client) +- if err := conn.initialize(protocol.WithClient(ctx, client), app.options); err != nil { +- return nil, err +- } +- return conn, nil - --Usage: -- gopls [flags] imports [imports-flags] +- default: +- return app.connectRemote(ctx, app.Remote) +- } +-} - --Example: update imports statements in a file: +-func (app *Application) connectRemote(ctx context.Context, remote string) (*connection, error) { +- conn, err := lsprpc.ConnectToRemote(ctx, remote) +- if err != nil { +- return nil, err +- } +- stream := jsonrpc2.NewHeaderStream(conn) +- cc := jsonrpc2.NewConn(stream) +- server := protocol.ServerDispatcher(cc) +- client := newClient(app, nil) +- connection := newConnection(server, client) +- ctx = protocol.WithClient(ctx, connection.client) +- cc.Go(ctx, +- protocol.Handlers( +- protocol.ClientHandler(client, jsonrpc2.MethodNotFound))) +- return connection, connection.initialize(ctx, app.options) +-} - -- $ gopls imports -w internal/lsp/cmd/check.go +-func (c *connection) initialize(ctx context.Context, options func(*settings.Options)) error { +- wd, err := os.Getwd() +- if err != nil { +- return fmt.Errorf("finding workdir: %v", err) +- } +- params := &protocol.ParamInitialize{} +- params.RootURI = protocol.URIFromPath(wd) +- params.Capabilities.Workspace.Configuration = true - --imports-flags: -- -d,-diff -- display diffs instead of edited file content -- -l,-list -- display names of edited files -- -preserve -- with -write, make copies of original files -- -w,-write -- write edited content to source files -diff -urN a/gopls/internal/lsp/cmd/usage/inspect.hlp b/gopls/internal/lsp/cmd/usage/inspect.hlp ---- a/gopls/internal/lsp/cmd/usage/inspect.hlp 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/usage/inspect.hlp 1970-01-01 00:00:00.000000000 +0000 -@@ -1,8 +0,0 @@ --interact with the gopls daemon (deprecated: use 'remote') +- // Make sure to respect configured options when sending initialize request. +- opts := settings.DefaultOptions(options) +- // If you add an additional option here, you must update the map key in connect. +- params.Capabilities.TextDocument.Hover = &protocol.HoverClientCapabilities{ +- ContentFormat: []protocol.MarkupKind{opts.PreferredContentFormat}, +- } +- params.Capabilities.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport = opts.HierarchicalDocumentSymbolSupport +- params.Capabilities.TextDocument.SemanticTokens = protocol.SemanticTokensClientCapabilities{} +- params.Capabilities.TextDocument.SemanticTokens.Formats = []protocol.TokenFormat{"relative"} +- params.Capabilities.TextDocument.SemanticTokens.Requests.Range = &protocol.Or_ClientSemanticTokensRequestOptions_range{Value: true} +- //params.Capabilities.TextDocument.SemanticTokens.Requests.Range.Value = true +- params.Capabilities.TextDocument.SemanticTokens.Requests.Full = &protocol.Or_ClientSemanticTokensRequestOptions_full{Value: true} +- params.Capabilities.TextDocument.SemanticTokens.TokenTypes = protocol.SemanticTypes() +- params.Capabilities.TextDocument.SemanticTokens.TokenModifiers = protocol.SemanticModifiers() - --Usage: -- gopls [flags] inspect [arg]... +- // If the subcommand has registered a progress handler, report the progress +- // capability. +- if c.client.onProgress != nil { +- params.Capabilities.Window.WorkDoneProgress = true +- } - --Subcommand: -- sessions print information about current gopls sessions -- debug start the debug server -diff -urN a/gopls/internal/lsp/cmd/usage/licenses.hlp b/gopls/internal/lsp/cmd/usage/licenses.hlp ---- a/gopls/internal/lsp/cmd/usage/licenses.hlp 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/usage/licenses.hlp 1970-01-01 00:00:00.000000000 +0000 -@@ -1,4 +0,0 @@ --print licenses of included software +- params.InitializationOptions = map[string]interface{}{ +- "symbolMatcher": string(opts.SymbolMatcher), +- } +- if _, err := c.Server.Initialize(ctx, params); err != nil { +- return err +- } +- if err := c.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil { +- return err +- } +- return nil +-} - --Usage: -- gopls [flags] licenses -diff -urN a/gopls/internal/lsp/cmd/usage/links.hlp b/gopls/internal/lsp/cmd/usage/links.hlp ---- a/gopls/internal/lsp/cmd/usage/links.hlp 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/usage/links.hlp 1970-01-01 00:00:00.000000000 +0000 -@@ -1,12 +0,0 @@ --list links in a file +-type connection struct { +- protocol.Server +- client *cmdClient +-} - --Usage: -- gopls [flags] links [links-flags] +-// cmdClient defines the protocol.Client interface behavior of the gopls CLI tool. +-type cmdClient struct { +- app *Application +- onProgress func(*protocol.ProgressParams) - --Example: list links contained within a file: +- filesMu sync.Mutex // guards files map and each cmdFile.diagnostics +- files map[protocol.DocumentURI]*cmdFile +-} - -- $ gopls links internal/lsp/cmd/check.go +-type cmdFile struct { +- uri protocol.DocumentURI +- mapper *protocol.Mapper +- err error +- diagnostics []protocol.Diagnostic +-} - --links-flags: -- -json -- emit document links in JSON format -diff -urN a/gopls/internal/lsp/cmd/usage/prepare_rename.hlp b/gopls/internal/lsp/cmd/usage/prepare_rename.hlp ---- a/gopls/internal/lsp/cmd/usage/prepare_rename.hlp 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/usage/prepare_rename.hlp 1970-01-01 00:00:00.000000000 +0000 -@@ -1,10 +0,0 @@ --test validity of a rename operation at location +-func newClient(app *Application, onProgress func(*protocol.ProgressParams)) *cmdClient { +- return &cmdClient{ +- app: app, +- onProgress: onProgress, +- files: make(map[protocol.DocumentURI]*cmdFile), +- } +-} - --Usage: -- gopls [flags] prepare_rename +-func newConnection(server protocol.Server, client *cmdClient) *connection { +- return &connection{ +- Server: server, +- client: client, +- } +-} - --Example: +-func (c *cmdClient) CodeLensRefresh(context.Context) error { return nil } - -- $ # 1-indexed location (:line:column or :#offset) of the target identifier -- $ gopls prepare_rename helper/helper.go:8:6 -- $ gopls prepare_rename helper/helper.go:#53 -diff -urN a/gopls/internal/lsp/cmd/usage/references.hlp b/gopls/internal/lsp/cmd/usage/references.hlp ---- a/gopls/internal/lsp/cmd/usage/references.hlp 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/usage/references.hlp 1970-01-01 00:00:00.000000000 +0000 -@@ -1,14 +0,0 @@ --display selected identifier's references +-func (c *cmdClient) FoldingRangeRefresh(context.Context) error { return nil } - --Usage: -- gopls [flags] references [references-flags] +-func (c *cmdClient) LogTrace(context.Context, *protocol.LogTraceParams) error { return nil } - --Example: +-func (c *cmdClient) ShowMessage(ctx context.Context, p *protocol.ShowMessageParams) error { +- fmt.Fprintf(os.Stderr, "%s: %s\n", p.Type, p.Message) +- return nil +-} - -- $ # 1-indexed location (:line:column or :#offset) of the target identifier -- $ gopls references helper/helper.go:8:6 -- $ gopls references helper/helper.go:#53 +-func (c *cmdClient) ShowMessageRequest(ctx context.Context, p *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error) { +- return nil, nil +-} - --references-flags: -- -d,-declaration -- include the declaration of the specified identifier in the results -diff -urN a/gopls/internal/lsp/cmd/usage/remote.hlp b/gopls/internal/lsp/cmd/usage/remote.hlp ---- a/gopls/internal/lsp/cmd/usage/remote.hlp 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/usage/remote.hlp 1970-01-01 00:00:00.000000000 +0000 -@@ -1,8 +0,0 @@ --interact with the gopls daemon +-func (c *cmdClient) LogMessage(ctx context.Context, p *protocol.LogMessageParams) error { +- // This logic causes server logging to be double-prefixed with a timestamp. +- // 2023/11/08 10:50:21 Error:2023/11/08 10:50:21 +- // TODO(adonovan): print just p.Message, plus a newline if needed? +- switch p.Type { +- case protocol.Error: +- log.Print("Error:", p.Message) +- case protocol.Warning: +- log.Print("Warning:", p.Message) +- case protocol.Info: +- if c.app.verbose() { +- log.Print("Info:", p.Message) +- } +- case protocol.Log: +- if c.app.verbose() { +- log.Print("Log:", p.Message) +- } +- default: +- if c.app.verbose() { +- log.Print(p.Message) +- } +- } +- return nil +-} - --Usage: -- gopls [flags] remote [arg]... +-func (c *cmdClient) Event(ctx context.Context, t *interface{}) error { return nil } - --Subcommand: -- sessions print information about current gopls sessions -- debug start the debug server -diff -urN a/gopls/internal/lsp/cmd/usage/rename.hlp b/gopls/internal/lsp/cmd/usage/rename.hlp ---- a/gopls/internal/lsp/cmd/usage/rename.hlp 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/usage/rename.hlp 1970-01-01 00:00:00.000000000 +0000 -@@ -1,20 +0,0 @@ --rename selected identifier +-func (c *cmdClient) RegisterCapability(ctx context.Context, p *protocol.RegistrationParams) error { +- return nil +-} - --Usage: -- gopls [flags] rename [rename-flags] +-func (c *cmdClient) UnregisterCapability(ctx context.Context, p *protocol.UnregistrationParams) error { +- return nil +-} - --Example: +-func (c *cmdClient) WorkspaceFolders(ctx context.Context) ([]protocol.WorkspaceFolder, error) { +- return nil, nil +-} - -- $ # 1-based location (:line:column or :#position) of the thing to change -- $ gopls rename helper/helper.go:8:6 Foo -- $ gopls rename helper/helper.go:#53 Foo +-func (c *cmdClient) Configuration(ctx context.Context, p *protocol.ParamConfiguration) ([]interface{}, error) { +- results := make([]interface{}, len(p.Items)) +- for i, item := range p.Items { +- if item.Section != "gopls" { +- continue +- } +- m := map[string]interface{}{ +- "analyses": map[string]any{ +- "fillreturns": true, +- "nonewvars": true, +- "noresultvalues": true, +- "undeclaredname": true, +- }, +- } +- if c.app.VeryVerbose { +- m["verboseOutput"] = true +- } +- results[i] = m +- } +- return results, nil +-} - --rename-flags: -- -d,-diff -- display diffs instead of edited file content -- -l,-list -- display names of edited files -- -preserve -- with -write, make copies of original files -- -w,-write -- write edited content to source files -diff -urN a/gopls/internal/lsp/cmd/usage/semtok.hlp b/gopls/internal/lsp/cmd/usage/semtok.hlp ---- a/gopls/internal/lsp/cmd/usage/semtok.hlp 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/usage/semtok.hlp 1970-01-01 00:00:00.000000000 +0000 -@@ -1,8 +0,0 @@ --show semantic tokens for the specified file +-func (c *cmdClient) ApplyEdit(ctx context.Context, p *protocol.ApplyWorkspaceEditParams) (*protocol.ApplyWorkspaceEditResult, error) { +- if err := c.applyWorkspaceEdit(&p.Edit); err != nil { +- return &protocol.ApplyWorkspaceEditResult{FailureReason: err.Error()}, nil +- } +- return &protocol.ApplyWorkspaceEditResult{Applied: true}, nil +-} - --Usage: -- gopls [flags] semtok +-// applyWorkspaceEdit applies a complete WorkspaceEdit to the client's +-// files, honoring the preferred edit mode specified by cli.app.editMode. +-// (Used by rename and by ApplyEdit downcalls.) +-func (cli *cmdClient) applyWorkspaceEdit(edit *protocol.WorkspaceEdit) error { +- var orderedURIs []protocol.DocumentURI +- edits := map[protocol.DocumentURI][]protocol.TextEdit{} +- for _, c := range edit.DocumentChanges { +- if c.TextDocumentEdit != nil { +- uri := c.TextDocumentEdit.TextDocument.URI +- edits[uri] = append(edits[uri], protocol.AsTextEdits(c.TextDocumentEdit.Edits)...) +- orderedURIs = append(orderedURIs, uri) +- } +- if c.RenameFile != nil { +- return fmt.Errorf("client does not support file renaming (%s -> %s)", +- c.RenameFile.OldURI, +- c.RenameFile.NewURI) +- } +- } +- sortSlice(orderedURIs) +- for _, uri := range orderedURIs { +- f := cli.openFile(uri) +- if f.err != nil { +- return f.err +- } +- if err := applyTextEdits(f.mapper, edits[uri], cli.app.editFlags); err != nil { +- return err +- } +- } +- return nil +-} - --Example: show the semantic tokens for this file: +-func sortSlice[T constraints.Ordered](slice []T) { +- sort.Slice(slice, func(i, j int) bool { return slice[i] < slice[j] }) +-} - -- $ gopls semtok internal/lsp/cmd/semtok.go -diff -urN a/gopls/internal/lsp/cmd/usage/serve.hlp b/gopls/internal/lsp/cmd/usage/serve.hlp ---- a/gopls/internal/lsp/cmd/usage/serve.hlp 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/usage/serve.hlp 1970-01-01 00:00:00.000000000 +0000 -@@ -1,30 +0,0 @@ --run a server for Go code using the Language Server Protocol +-// applyTextEdits applies a list of edits to the mapper file content, +-// using the preferred edit mode. It is a no-op if there are no edits. +-func applyTextEdits(mapper *protocol.Mapper, edits []protocol.TextEdit, flags *EditFlags) error { +- if len(edits) == 0 { +- return nil +- } +- newContent, renameEdits, err := protocol.ApplyEdits(mapper, edits) +- if err != nil { +- return err +- } - --Usage: -- gopls [flags] serve [server-flags] -- gopls [flags] [server-flags] +- filename := mapper.URI.Path() - --The server communicates using JSONRPC2 on stdin and stdout, and is intended to be run directly as --a child of an editor process. +- if flags.List { +- fmt.Println(filename) +- } - --server-flags: -- -debug=string -- serve debug information on the supplied address -- -listen=string -- address on which to listen for remote connections. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. Otherwise, TCP is used. -- -listen.timeout=duration -- when used with -listen, shut down the server when there are no connected clients for this duration -- -logfile=string -- filename to log to. if value is "auto", then logging to a default output file is enabled -- -mode=string -- no effect -- -port=int -- port on which to run gopls for debugging purposes -- -remote.debug=string -- when used with -remote=auto, the -debug value used to start the daemon -- -remote.listen.timeout=duration -- when used with -remote=auto, the -listen.timeout value used to start the daemon (default 1m0s) -- -remote.logfile=string -- when used with -remote=auto, the -logfile value used to start the daemon -- -rpc.trace -- print the full rpc trace in lsp inspector format -diff -urN a/gopls/internal/lsp/cmd/usage/signature.hlp b/gopls/internal/lsp/cmd/usage/signature.hlp ---- a/gopls/internal/lsp/cmd/usage/signature.hlp 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/usage/signature.hlp 1970-01-01 00:00:00.000000000 +0000 -@@ -1,10 +0,0 @@ --display selected identifier's signature +- if flags.Write { +- if flags.Preserve { +- if err := os.Rename(filename, filename+".orig"); err != nil { +- return err +- } +- } +- if err := os.WriteFile(filename, newContent, 0644); err != nil { +- return err +- } +- } - --Usage: -- gopls [flags] signature +- if flags.Diff { +- unified, err := diff.ToUnified(filename+".orig", filename, string(mapper.Content), renameEdits, diff.DefaultContextLines) +- if err != nil { +- return err +- } +- fmt.Print(unified) +- } - --Example: +- // No flags: just print edited file content. +- // TODO(adonovan): how is this ever useful with multiple files? +- if !(flags.List || flags.Write || flags.Diff) { +- os.Stdout.Write(newContent) +- } - -- $ # 1-indexed location (:line:column or :#offset) of the target identifier -- $ gopls signature helper/helper.go:8:6 -- $ gopls signature helper/helper.go:#53 -diff -urN a/gopls/internal/lsp/cmd/usage/stats.hlp b/gopls/internal/lsp/cmd/usage/stats.hlp ---- a/gopls/internal/lsp/cmd/usage/stats.hlp 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/usage/stats.hlp 1970-01-01 00:00:00.000000000 +0000 -@@ -1,17 +0,0 @@ --print workspace statistics +- return nil +-} - --Usage: -- gopls [flags] stats +-func (c *cmdClient) PublishDiagnostics(ctx context.Context, p *protocol.PublishDiagnosticsParams) error { +- // Don't worry about diagnostics without versions. +- if p.Version == 0 { +- return nil +- } - --Load the workspace for the current directory, and output a JSON summary of --workspace information relevant to performance. As a side effect, this command --populates the gopls file cache for the current workspace. -- --By default, this command may include output that refers to the location or --content of user code. When the -anon flag is set, fields that may refer to user --code are hidden. -- --Example: -- $ gopls stats -anon -- -anon -- hide any fields that may contain user names, file names, or source code -diff -urN a/gopls/internal/lsp/cmd/usage/symbols.hlp b/gopls/internal/lsp/cmd/usage/symbols.hlp ---- a/gopls/internal/lsp/cmd/usage/symbols.hlp 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/usage/symbols.hlp 1970-01-01 00:00:00.000000000 +0000 -@@ -1,7 +0,0 @@ --display selected file's symbols -- --Usage: -- gopls [flags] symbols -- --Example: -- $ gopls symbols helper/helper.go -diff -urN a/gopls/internal/lsp/cmd/usage/usage.hlp b/gopls/internal/lsp/cmd/usage/usage.hlp ---- a/gopls/internal/lsp/cmd/usage/usage.hlp 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/usage/usage.hlp 1970-01-01 00:00:00.000000000 +0000 -@@ -1,79 +0,0 @@ -- --gopls is a Go language server. -- --It is typically used with an editor to provide language features. When no --command is specified, gopls will default to the 'serve' command. The language --features can also be accessed via the gopls command-line interface. -- --Usage: -- gopls help [] +- c.filesMu.Lock() +- defer c.filesMu.Unlock() - --Command: +- file := c.getFile(p.URI) +- file.diagnostics = append(file.diagnostics, p.Diagnostics...) - --Main -- serve run a server for Go code using the Language Server Protocol -- version print the gopls version information -- bug report a bug in gopls -- help print usage information for subcommands -- api-json print json describing gopls API -- licenses print licenses of included software -- --Features -- call_hierarchy display selected identifier's call hierarchy -- check show diagnostic results for the specified file -- definition show declaration of selected identifier -- folding_ranges display selected file's folding ranges -- format format the code according to the go standard -- highlight display selected identifier's highlights -- implementation display selected identifier's implementation -- imports updates import statements -- remote interact with the gopls daemon -- inspect interact with the gopls daemon (deprecated: use 'remote') -- links list links in a file -- prepare_rename test validity of a rename operation at location -- references display selected identifier's references -- rename rename selected identifier -- semtok show semantic tokens for the specified file -- signature display selected identifier's signature -- stats print workspace statistics -- fix apply suggested fixes -- symbols display selected file's symbols -- workspace_symbol search symbols in workspace +- // Perform a crude in-place deduplication. +- // TODO(golang/go#60122): replace the gopls.diagnose_files +- // command with support for textDocument/diagnostic, +- // so that we don't need to do this de-duplication. +- type key [6]interface{} +- seen := make(map[key]bool) +- out := file.diagnostics[:0] +- for _, d := range file.diagnostics { +- var codeHref string +- if desc := d.CodeDescription; desc != nil { +- codeHref = desc.Href +- } +- k := key{d.Range, d.Severity, d.Code, codeHref, d.Source, d.Message} +- if !seen[k] { +- seen[k] = true +- out = append(out, d) +- } +- } +- file.diagnostics = out - --flags: -- -debug=string -- serve debug information on the supplied address -- -listen=string -- address on which to listen for remote connections. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. Otherwise, TCP is used. -- -listen.timeout=duration -- when used with -listen, shut down the server when there are no connected clients for this duration -- -logfile=string -- filename to log to. if value is "auto", then logging to a default output file is enabled -- -mode=string -- no effect -- -ocagent=string -- the address of the ocagent (e.g. http://localhost:55678), or off (default "off") -- -port=int -- port on which to run gopls for debugging purposes -- -profile.alloc=string -- write alloc profile to this file -- -profile.cpu=string -- write CPU profile to this file -- -profile.mem=string -- write memory profile to this file -- -profile.trace=string -- write trace log to this file -- -remote=string -- forward all commands to a remote lsp specified by this flag. With no special prefix, this is assumed to be a TCP address. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. If 'auto', or prefixed by 'auto;', the remote address is automatically resolved based on the executing environment. -- -remote.debug=string -- when used with -remote=auto, the -debug value used to start the daemon -- -remote.listen.timeout=duration -- when used with -remote=auto, the -listen.timeout value used to start the daemon (default 1m0s) -- -remote.logfile=string -- when used with -remote=auto, the -logfile value used to start the daemon -- -rpc.trace -- print the full rpc trace in lsp inspector format -- -v,-verbose -- verbose output -- -vv,-veryverbose -- very verbose output -diff -urN a/gopls/internal/lsp/cmd/usage/usage-v.hlp b/gopls/internal/lsp/cmd/usage/usage-v.hlp ---- a/gopls/internal/lsp/cmd/usage/usage-v.hlp 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/usage/usage-v.hlp 1970-01-01 00:00:00.000000000 +0000 -@@ -1,82 +0,0 @@ +- return nil +-} - --gopls is a Go language server. +-func (c *cmdClient) Progress(_ context.Context, params *protocol.ProgressParams) error { +- if c.onProgress != nil { +- c.onProgress(params) +- } +- return nil +-} - --It is typically used with an editor to provide language features. When no --command is specified, gopls will default to the 'serve' command. The language --features can also be accessed via the gopls command-line interface. +-func (c *cmdClient) ShowDocument(ctx context.Context, params *protocol.ShowDocumentParams) (*protocol.ShowDocumentResult, error) { +- var success bool +- if params.External { +- // Open URI in external browser. +- success = browser.Open(params.URI) +- } else { +- // Open file in editor, optionally taking focus and selecting a range. +- // (cmdClient has no editor. Should it fork+exec $EDITOR?) +- log.Printf("Server requested that client editor open %q (takeFocus=%t, selection=%+v)", +- params.URI, params.TakeFocus, params.Selection) +- success = true +- } +- return &protocol.ShowDocumentResult{Success: success}, nil +-} - --Usage: -- gopls help [] +-func (c *cmdClient) WorkDoneProgressCreate(context.Context, *protocol.WorkDoneProgressCreateParams) error { +- return nil +-} - --Command: +-func (c *cmdClient) DiagnosticRefresh(context.Context) error { +- return nil +-} - --Main -- serve run a server for Go code using the Language Server Protocol -- version print the gopls version information -- bug report a bug in gopls -- help print usage information for subcommands -- api-json print json describing gopls API -- licenses print licenses of included software -- --Features -- call_hierarchy display selected identifier's call hierarchy -- check show diagnostic results for the specified file -- definition show declaration of selected identifier -- folding_ranges display selected file's folding ranges -- format format the code according to the go standard -- highlight display selected identifier's highlights -- implementation display selected identifier's implementation -- imports updates import statements -- remote interact with the gopls daemon -- inspect interact with the gopls daemon (deprecated: use 'remote') -- links list links in a file -- prepare_rename test validity of a rename operation at location -- references display selected identifier's references -- rename rename selected identifier -- semtok show semantic tokens for the specified file -- signature display selected identifier's signature -- stats print workspace statistics -- fix apply suggested fixes -- symbols display selected file's symbols -- workspace_symbol search symbols in workspace -- --Internal Use Only -- vulncheck run vulncheck analysis (internal-use only) +-func (c *cmdClient) InlayHintRefresh(context.Context) error { +- return nil +-} - --flags: -- -debug=string -- serve debug information on the supplied address -- -listen=string -- address on which to listen for remote connections. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. Otherwise, TCP is used. -- -listen.timeout=duration -- when used with -listen, shut down the server when there are no connected clients for this duration -- -logfile=string -- filename to log to. if value is "auto", then logging to a default output file is enabled -- -mode=string -- no effect -- -ocagent=string -- the address of the ocagent (e.g. http://localhost:55678), or off (default "off") -- -port=int -- port on which to run gopls for debugging purposes -- -profile.alloc=string -- write alloc profile to this file -- -profile.cpu=string -- write CPU profile to this file -- -profile.mem=string -- write memory profile to this file -- -profile.trace=string -- write trace log to this file -- -remote=string -- forward all commands to a remote lsp specified by this flag. With no special prefix, this is assumed to be a TCP address. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. If 'auto', or prefixed by 'auto;', the remote address is automatically resolved based on the executing environment. -- -remote.debug=string -- when used with -remote=auto, the -debug value used to start the daemon -- -remote.listen.timeout=duration -- when used with -remote=auto, the -listen.timeout value used to start the daemon (default 1m0s) -- -remote.logfile=string -- when used with -remote=auto, the -logfile value used to start the daemon -- -rpc.trace -- print the full rpc trace in lsp inspector format -- -v,-verbose -- verbose output -- -vv,-veryverbose -- very verbose output -diff -urN a/gopls/internal/lsp/cmd/usage/version.hlp b/gopls/internal/lsp/cmd/usage/version.hlp ---- a/gopls/internal/lsp/cmd/usage/version.hlp 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/usage/version.hlp 1970-01-01 00:00:00.000000000 +0000 -@@ -1,6 +0,0 @@ --print the gopls version information +-func (c *cmdClient) SemanticTokensRefresh(context.Context) error { +- return nil +-} - --Usage: -- gopls [flags] version -- -json -- outputs in json format. -diff -urN a/gopls/internal/lsp/cmd/usage/vulncheck.hlp b/gopls/internal/lsp/cmd/usage/vulncheck.hlp ---- a/gopls/internal/lsp/cmd/usage/vulncheck.hlp 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/usage/vulncheck.hlp 1970-01-01 00:00:00.000000000 +0000 -@@ -1,13 +0,0 @@ --run vulncheck analysis (internal-use only) +-func (c *cmdClient) InlineValueRefresh(context.Context) error { +- return nil +-} - --Usage: -- gopls [flags] vulncheck +-func (c *cmdClient) getFile(uri protocol.DocumentURI) *cmdFile { +- file, found := c.files[uri] +- if !found || file.err != nil { +- file = &cmdFile{ +- uri: uri, +- } +- c.files[uri] = file +- } +- if file.mapper == nil { +- content, err := os.ReadFile(uri.Path()) +- if err != nil { +- file.err = fmt.Errorf("getFile: %v: %v", uri, err) +- return file +- } +- file.mapper = protocol.NewMapper(uri, content) +- } +- return file +-} - -- WARNING: this command is for internal-use only. +-func (c *cmdClient) openFile(uri protocol.DocumentURI) *cmdFile { +- c.filesMu.Lock() +- defer c.filesMu.Unlock() +- return c.getFile(uri) +-} - -- By default, the command outputs a JSON-encoded -- golang.org/x/tools/gopls/internal/lsp/command.VulncheckResult -- message. -- Example: -- $ gopls vulncheck +-// TODO(adonovan): provide convenience helpers to: +-// - map a (URI, protocol.Range) to a MappedRange; +-// - parse a command-line argument to a MappedRange. +-func (c *connection) openFile(ctx context.Context, uri protocol.DocumentURI) (*cmdFile, error) { +- file := c.client.openFile(uri) +- if file.err != nil { +- return nil, file.err +- } - -diff -urN a/gopls/internal/lsp/cmd/usage/workspace_symbol.hlp b/gopls/internal/lsp/cmd/usage/workspace_symbol.hlp ---- a/gopls/internal/lsp/cmd/usage/workspace_symbol.hlp 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/usage/workspace_symbol.hlp 1970-01-01 00:00:00.000000000 +0000 -@@ -1,13 +0,0 @@ --search symbols in workspace +- p := &protocol.DidOpenTextDocumentParams{ +- TextDocument: protocol.TextDocumentItem{ +- URI: uri, +- LanguageID: "go", +- Version: 1, +- Text: string(file.mapper.Content), +- }, +- } +- if err := c.Server.DidOpen(ctx, p); err != nil { +- // TODO(adonovan): is this assignment concurrency safe? +- file.err = fmt.Errorf("%v: %v", uri, err) +- return nil, file.err +- } +- return file, nil +-} - --Usage: -- gopls [flags] workspace_symbol [workspace_symbol-flags] +-func (c *connection) semanticTokens(ctx context.Context, p *protocol.SemanticTokensRangeParams) (*protocol.SemanticTokens, error) { +- // use range to avoid limits on full +- resp, err := c.Server.SemanticTokensRange(ctx, p) +- if err != nil { +- return nil, err +- } +- return resp, nil +-} - --Example: +-func (c *connection) diagnoseFiles(ctx context.Context, files []protocol.DocumentURI) error { +- cmd, err := command.NewDiagnoseFilesCommand("Diagnose files", command.DiagnoseFilesArgs{ +- Files: files, +- }) +- if err != nil { +- return err +- } +- _, err = c.ExecuteCommand(ctx, &protocol.ExecuteCommandParams{ +- Command: cmd.Command, +- Arguments: cmd.Arguments, +- }) +- return err +-} - -- $ gopls workspace_symbol -matcher fuzzy 'wsymbols' +-func (c *connection) terminate(ctx context.Context) { +- //TODO: do we need to handle errors on these calls? +- c.Shutdown(ctx) +- //TODO: right now calling exit terminates the process, we should rethink that +- //server.Exit(ctx) +-} - --workspace_symbol-flags: -- -matcher=string -- specifies the type of matcher: fuzzy, fastfuzzy, casesensitive, or caseinsensitive. -- The default is caseinsensitive. -diff -urN a/gopls/internal/lsp/cmd/vulncheck.go b/gopls/internal/lsp/cmd/vulncheck.go ---- a/gopls/internal/lsp/cmd/vulncheck.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/vulncheck.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,47 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-// Implement io.Closer. +-func (c *cmdClient) Close() error { +- return nil +-} - --package cmd +-// -- conversions to span (UTF-8) domain -- - --import ( -- "context" -- "flag" -- "fmt" -- "os" +-// locationSpan converts a protocol (UTF-16) Location to a (UTF-8) span. +-// Precondition: the URIs of Location and Mapper match. +-func (f *cmdFile) locationSpan(loc protocol.Location) (span, error) { +- // TODO(adonovan): check that l.URI matches m.URI. +- return f.rangeSpan(loc.Range) +-} - -- "golang.org/x/tools/gopls/internal/vulncheck/scan" --) +-// rangeSpan converts a protocol (UTF-16) range to a (UTF-8) span. +-// The resulting span has valid Positions and Offsets. +-func (f *cmdFile) rangeSpan(r protocol.Range) (span, error) { +- start, end, err := f.mapper.RangeOffsets(r) +- if err != nil { +- return span{}, err +- } +- return f.offsetSpan(start, end) +-} - --// vulncheck implements the vulncheck command. --// TODO(hakim): hide from the public. --type vulncheck struct { -- app *Application +-// offsetSpan converts a byte-offset interval to a (UTF-8) span. +-// The resulting span contains line, column, and offset information. +-func (f *cmdFile) offsetSpan(start, end int) (span, error) { +- if start > end { +- return span{}, fmt.Errorf("start offset (%d) > end (%d)", start, end) +- } +- startPoint, err := offsetPoint(f.mapper, start) +- if err != nil { +- return span{}, fmt.Errorf("start: %v", err) +- } +- endPoint, err := offsetPoint(f.mapper, end) +- if err != nil { +- return span{}, fmt.Errorf("end: %v", err) +- } +- return newSpan(f.mapper.URI, startPoint, endPoint), nil -} - --func (v *vulncheck) Name() string { return "vulncheck" } --func (v *vulncheck) Parent() string { return v.app.Name() } --func (v *vulncheck) Usage() string { return "" } --func (v *vulncheck) ShortHelp() string { -- return "run vulncheck analysis (internal-use only)" +-// offsetPoint converts a byte offset to a span (UTF-8) point. +-// The resulting point contains line, column, and offset information. +-func offsetPoint(m *protocol.Mapper, offset int) (point, error) { +- if !(0 <= offset && offset <= len(m.Content)) { +- return point{}, fmt.Errorf("invalid offset %d (want 0-%d)", offset, len(m.Content)) +- } +- line, col8 := m.OffsetLineCol8(offset) +- return newPoint(line, col8, offset), nil -} --func (v *vulncheck) DetailedHelp(f *flag.FlagSet) { -- fmt.Fprint(f.Output(), ` -- WARNING: this command is for internal-use only. - -- By default, the command outputs a JSON-encoded -- golang.org/x/tools/gopls/internal/lsp/command.VulncheckResult -- message. -- Example: -- $ gopls vulncheck +-// -- conversions from span (UTF-8) domain -- - --`) +-// spanLocation converts a (UTF-8) span to a protocol (UTF-16) range. +-// Precondition: the URIs of spanLocation and Mapper match. +-func (f *cmdFile) spanLocation(s span) (protocol.Location, error) { +- rng, err := f.spanRange(s) +- if err != nil { +- return protocol.Location{}, err +- } +- return f.mapper.RangeLocation(rng), nil -} - --func (v *vulncheck) Run(ctx context.Context, args ...string) error { -- if err := scan.Main(ctx, args...); err != nil { -- fmt.Fprintln(os.Stderr, err) -- os.Exit(1) +-// spanRange converts a (UTF-8) span to a protocol (UTF-16) range. +-// Precondition: the URIs of span and Mapper match. +-func (f *cmdFile) spanRange(s span) (protocol.Range, error) { +- // Assert that we aren't using the wrong mapper. +- // We check only the base name, and case insensitively, +- // because we can't assume clean paths, no symbolic links, +- // case-sensitive directories. The authoritative answer +- // requires querying the file system, and we don't want +- // to do that. +- if !strings.EqualFold(filepath.Base(string(f.mapper.URI)), filepath.Base(string(s.URI()))) { +- return protocol.Range{}, bugpkg.Errorf("mapper is for file %q instead of %q", f.mapper.URI, s.URI()) - } -- return nil +- start, err := pointPosition(f.mapper, s.Start()) +- if err != nil { +- return protocol.Range{}, fmt.Errorf("start: %w", err) +- } +- end, err := pointPosition(f.mapper, s.End()) +- if err != nil { +- return protocol.Range{}, fmt.Errorf("end: %w", err) +- } +- return protocol.Range{Start: start, End: end}, nil -} -diff -urN a/gopls/internal/lsp/cmd/workspace_symbol.go b/gopls/internal/lsp/cmd/workspace_symbol.go ---- a/gopls/internal/lsp/cmd/workspace_symbol.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/cmd/workspace_symbol.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,89 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +- +-// pointPosition converts a valid span (UTF-8) point to a protocol (UTF-16) position. +-func pointPosition(m *protocol.Mapper, p point) (protocol.Position, error) { +- if p.HasPosition() { +- return m.LineCol8Position(p.Line(), p.Column()) +- } +- if p.HasOffset() { +- return m.OffsetPosition(p.Offset()) +- } +- return protocol.Position{}, fmt.Errorf("point has neither offset nor line/column") +-} +diff -urN a/gopls/internal/cmd/codelens.go b/gopls/internal/cmd/codelens.go +--- a/gopls/internal/cmd/codelens.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/codelens.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,138 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - @@ -33437,77836 +34580,83163 @@ diff -urN a/gopls/internal/lsp/cmd/workspace_symbol.go b/gopls/internal/lsp/cmd/ - "context" - "flag" - "fmt" -- "strings" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/settings" - "golang.org/x/tools/internal/tool" -) - --// workspaceSymbol implements the workspace_symbol verb for gopls. --type workspaceSymbol struct { -- Matcher string `flag:"matcher" help:"specifies the type of matcher: fuzzy, fastfuzzy, casesensitive, or caseinsensitive.\nThe default is caseinsensitive."` -- +-// codelens implements the codelens verb for gopls. +-type codelens struct { +- EditFlags - app *Application +- +- Exec bool `flag:"exec" help:"execute the first matching code lens"` -} - --func (r *workspaceSymbol) Name() string { return "workspace_symbol" } --func (r *workspaceSymbol) Parent() string { return r.app.Name() } --func (r *workspaceSymbol) Usage() string { return "[workspace_symbol-flags] " } --func (r *workspaceSymbol) ShortHelp() string { return "search symbols in workspace" } --func (r *workspaceSymbol) DetailedHelp(f *flag.FlagSet) { +-func (r *codelens) Name() string { return "codelens" } +-func (r *codelens) Parent() string { return r.app.Name() } +-func (r *codelens) Usage() string { return "[codelens-flags] file[:line[:col]] [title]" } +-func (r *codelens) ShortHelp() string { return "List or execute code lenses for a file" } +-func (r *codelens) DetailedHelp(f *flag.FlagSet) { - fmt.Fprint(f.Output(), ` +-The codelens command lists or executes code lenses for the specified +-file, or line within a file. A code lens is a command associated with +-a position in the code. +- +-With an optional title argment, only code lenses matching that +-title are considered. +- +-By default, the codelens command lists the available lenses for the +-specified file or line within a file, including the title and +-title of the command. With the -exec flag, the first matching command +-is executed, and its output is printed to stdout. +- -Example: - -- $ gopls workspace_symbol -matcher fuzzy 'wsymbols' +- $ gopls codelens a_test.go # list code lenses in a file +- $ gopls codelens a_test.go:10 # list code lenses on line 10 +- $ gopls codelens a_test.go gopls.test # list gopls.test commands +- $ gopls codelens -run a_test.go:10 gopls.test # run a specific test - --workspace_symbol-flags: +-codelens-flags: -`) - printFlagDefaults(f) -} - --func (r *workspaceSymbol) Run(ctx context.Context, args ...string) error { -- if len(args) != 1 { -- return tool.CommandLineErrorf("workspace_symbol expects 1 argument") +-func (r *codelens) Run(ctx context.Context, args ...string) error { +- var filename, title string +- switch len(args) { +- case 0: +- return tool.CommandLineErrorf("codelens requires a file name") +- case 2: +- title = args[1] +- fallthrough +- case 1: +- filename = args[0] +- default: +- return tool.CommandLineErrorf("codelens expects at most two arguments") - } - -- opts := r.app.options -- r.app.options = func(o *source.Options) { -- if opts != nil { -- opts(o) -- } -- switch strings.ToLower(r.Matcher) { -- case "fuzzy": -- o.SymbolMatcher = source.SymbolFuzzy -- case "casesensitive": -- o.SymbolMatcher = source.SymbolCaseSensitive -- case "fastfuzzy": -- o.SymbolMatcher = source.SymbolFastFuzzy -- default: -- o.SymbolMatcher = source.SymbolCaseInsensitive +- r.app.editFlags = &r.EditFlags // in case a codelens perform an edit +- +- // Override the default setting for codelenses[Test], which is +- // off by default because VS Code has a superior client-side +- // implementation. But this client is not VS Code. +- // See golang.LensFuncs(). +- origOptions := r.app.options +- r.app.options = func(opts *settings.Options) { +- origOptions(opts) +- if opts.Codelenses == nil { +- opts.Codelenses = make(map[string]bool) - } +- opts.Codelenses["test"] = true - } - -- conn, err := r.app.connect(ctx, nil) +- // TODO(adonovan): cleanup: factor progress with stats subcommand. +- cmdDone, onProgress := commandProgress() +- +- conn, err := r.app.connect(ctx, onProgress) - if err != nil { - return err - } - defer conn.terminate(ctx) - -- p := protocol.WorkspaceSymbolParams{ -- Query: args[0], +- filespan := parseSpan(filename) +- file, err := conn.openFile(ctx, filespan.URI()) +- if err != nil { +- return err +- } +- loc, err := file.spanLocation(filespan) +- if err != nil { +- return err - } - -- symbols, err := conn.Symbol(ctx, &p) +- p := protocol.CodeLensParams{ +- TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, +- } +- lenses, err := conn.CodeLens(ctx, &p) - if err != nil { - return err - } -- for _, s := range symbols { -- f, err := conn.openFile(ctx, fileURI(s.Location.URI)) +- +- for _, lens := range lenses { +- sp, err := file.rangeSpan(lens.Range) - if err != nil { -- return err +- return nil - } -- span, err := f.mapper.LocationSpan(s.Location) -- if err != nil { +- +- if title != "" && lens.Command.Title != title { +- continue // title was specified but does not match +- } +- if filespan.HasPosition() && !protocol.Intersect(loc.Range, lens.Range) { +- continue // position was specified but does not match +- } +- +- // -exec: run the first matching code lens. +- if r.Exec { +- _, err := conn.executeCommand(ctx, cmdDone, lens.Command) - return err - } -- fmt.Printf("%s %s %s\n", span, s.Name, s.Kind) +- +- // No -exec: list matching code lenses. +- fmt.Printf("%v: %q [%s]\n", sp, lens.Command.Title, lens.Command.Command) - } - +- if r.Exec { +- return fmt.Errorf("no code lens at %s with title %q", filespan, title) +- } - return nil -} -diff -urN a/gopls/internal/lsp/code_action.go b/gopls/internal/lsp/code_action.go ---- a/gopls/internal/lsp/code_action.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/code_action.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,715 +0,0 @@ --// Copyright 2018 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/cmd/definition.go b/gopls/internal/cmd/definition.go +--- a/gopls/internal/cmd/definition.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/definition.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,137 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package lsp +-package cmd - -import ( - "context" +- "encoding/json" +- "flag" - "fmt" -- "go/ast" -- "sort" +- "os" - "strings" - -- "golang.org/x/tools/go/ast/inspector" -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/analysis/fillstruct" -- "golang.org/x/tools/gopls/internal/lsp/analysis/infertypeargs" -- "golang.org/x/tools/gopls/internal/lsp/analysis/stubmethods" -- "golang.org/x/tools/gopls/internal/lsp/command" -- "golang.org/x/tools/gopls/internal/lsp/mod" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/tag" -- "golang.org/x/tools/internal/imports" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/settings" +- "golang.org/x/tools/internal/tool" -) - --func (s *Server) codeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) { -- ctx, done := event.Start(ctx, "lsp.Server.codeAction") -- defer done() +-// A Definition is the result of a 'definition' query. +-type Definition struct { +- Span span `json:"span"` // span of the definition +- Description string `json:"description"` // description of the denoted object +-} - -- snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind) -- defer release() -- if !ok { -- return nil, err -- } -- uri := fh.URI() +-// These constant is printed in the help, and then used in a test to verify the +-// help is still valid. +-// They refer to "Set" in "flag.FlagSet" from the DetailedHelp method below. +-const ( +- exampleLine = 44 +- exampleColumn = 47 +- exampleOffset = 1270 +-) - -- // Determine the supported actions for this file kind. -- kind := snapshot.FileKind(fh) -- supportedCodeActions, ok := snapshot.Options().SupportedCodeActions[kind] -- if !ok { -- return nil, fmt.Errorf("no supported code actions for %v file kind", kind) -- } -- if len(supportedCodeActions) == 0 { -- return nil, nil // not an error if there are none supported -- } +-// definition implements the definition verb for gopls. +-type definition struct { +- app *Application - -- // The Only field of the context specifies which code actions the client wants. -- // If Only is empty, assume that the client wants all of the non-explicit code actions. -- var want map[protocol.CodeActionKind]bool -- { -- // Explicit Code Actions are opt-in and shouldn't be returned to the client unless -- // requested using Only. -- // TODO: Add other CodeLenses such as GoGenerate, RegenerateCgo, etc.. -- explicit := map[protocol.CodeActionKind]bool{ -- protocol.GoTest: true, -- } +- JSON bool `flag:"json" help:"emit output in JSON format"` +- MarkdownSupported bool `flag:"markdown" help:"support markdown in responses"` +-} - -- if len(params.Context.Only) == 0 { -- want = supportedCodeActions -- } else { -- want = make(map[protocol.CodeActionKind]bool) -- for _, only := range params.Context.Only { -- for k, v := range supportedCodeActions { -- if only == k || strings.HasPrefix(string(k), string(only)+".") { -- want[k] = want[k] || v -- } -- } -- want[only] = want[only] || explicit[only] -- } -- } -- } -- if len(want) == 0 { -- return nil, fmt.Errorf("no supported code action to execute for %s, wanted %v", uri, params.Context.Only) -- } -- -- switch kind { -- case source.Mod: -- var actions []protocol.CodeAction -- -- fixes, err := s.codeActionsMatchingDiagnostics(ctx, fh.URI(), snapshot, params.Context.Diagnostics, want) -- if err != nil { -- return nil, err -- } -- -- // Group vulnerability fixes by their range, and select only the most -- // appropriate upgrades. -- // -- // TODO(rfindley): can this instead be accomplished on the diagnosis side, -- // so that code action handling remains uniform? -- vulnFixes := make(map[protocol.Range][]protocol.CodeAction) -- searchFixes: -- for _, fix := range fixes { -- for _, diag := range fix.Diagnostics { -- if diag.Source == string(source.Govulncheck) || diag.Source == string(source.Vulncheck) { -- vulnFixes[diag.Range] = append(vulnFixes[diag.Range], fix) -- continue searchFixes -- } -- } -- actions = append(actions, fix) -- } -- -- for _, fixes := range vulnFixes { -- fixes = mod.SelectUpgradeCodeActions(fixes) -- actions = append(actions, fixes...) -- } +-func (d *definition) Name() string { return "definition" } +-func (d *definition) Parent() string { return d.app.Name() } +-func (d *definition) Usage() string { return "[definition-flags] " } +-func (d *definition) ShortHelp() string { return "show declaration of selected identifier" } +-func (d *definition) DetailedHelp(f *flag.FlagSet) { +- fmt.Fprintf(f.Output(), ` +-Example: show the definition of the identifier at syntax at offset %[1]v in this file (flag.FlagSet): - -- return actions, nil +- $ gopls definition internal/cmd/definition.go:%[1]v:%[2]v +- $ gopls definition internal/cmd/definition.go:#%[3]v - -- case source.Go: -- diagnostics := params.Context.Diagnostics +-definition-flags: +-`, exampleLine, exampleColumn, exampleOffset) +- printFlagDefaults(f) +-} - -- // Don't suggest fixes for generated files, since they are generally -- // not useful and some editors may apply them automatically on save. -- if source.IsGenerated(ctx, snapshot, uri) { -- return nil, nil +-// Run performs the definition query as specified by args and prints the +-// results to stdout. +-func (d *definition) Run(ctx context.Context, args ...string) error { +- if len(args) != 1 { +- return tool.CommandLineErrorf("definition expects 1 argument") +- } +- // Plaintext makes more sense for the command line. +- opts := d.app.options +- d.app.options = func(o *settings.Options) { +- if opts != nil { +- opts(o) - } -- -- actions, err := s.codeActionsMatchingDiagnostics(ctx, uri, snapshot, diagnostics, want) -- if err != nil { -- return nil, err +- o.PreferredContentFormat = protocol.PlainText +- if d.MarkdownSupported { +- o.PreferredContentFormat = protocol.Markdown - } +- } +- conn, err := d.app.connect(ctx, nil) +- if err != nil { +- return err +- } +- defer conn.terminate(ctx) +- from := parseSpan(args[0]) +- file, err := conn.openFile(ctx, from.URI()) +- if err != nil { +- return err +- } +- loc, err := file.spanLocation(from) +- if err != nil { +- return err +- } +- p := protocol.DefinitionParams{ +- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), +- } +- locs, err := conn.Definition(ctx, &p) +- if err != nil { +- return fmt.Errorf("%v: %v", from, err) +- } - -- // Only compute quick fixes if there are any diagnostics to fix. -- wantQuickFixes := want[protocol.QuickFix] && len(diagnostics) > 0 -- -- // Code actions requiring syntax information alone. -- if wantQuickFixes || want[protocol.SourceOrganizeImports] || want[protocol.RefactorExtract] { -- pgf, err := snapshot.ParseGo(ctx, fh, source.ParseFull) -- if err != nil { -- return nil, err -- } -- -- // Process any missing imports and pair them with the diagnostics they -- // fix. -- if wantQuickFixes || want[protocol.SourceOrganizeImports] { -- importEdits, importEditsPerFix, err := source.AllImportsFixes(ctx, snapshot, pgf) -- if err != nil { -- event.Error(ctx, "imports fixes", err, tag.File.Of(fh.URI().Filename())) -- importEdits = nil -- importEditsPerFix = nil -- } +- if len(locs) == 0 { +- return fmt.Errorf("%v: not an identifier", from) +- } +- file, err = conn.openFile(ctx, locs[0].URI) +- if err != nil { +- return fmt.Errorf("%v: %v", from, err) +- } +- definition, err := file.locationSpan(locs[0]) +- if err != nil { +- return fmt.Errorf("%v: %v", from, err) +- } - -- // Separate this into a set of codeActions per diagnostic, where -- // each action is the addition, removal, or renaming of one import. -- if wantQuickFixes { -- for _, importFix := range importEditsPerFix { -- fixed := fixedByImportFix(importFix.Fix, diagnostics) -- if len(fixed) == 0 { -- continue -- } -- actions = append(actions, protocol.CodeAction{ -- Title: importFixTitle(importFix.Fix), -- Kind: protocol.QuickFix, -- Edit: &protocol.WorkspaceEdit{ -- DocumentChanges: documentChanges(fh, importFix.Edits), -- }, -- Diagnostics: fixed, -- }) -- } -- } +- q := protocol.HoverParams{ +- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), +- } +- hover, err := conn.Hover(ctx, &q) +- if err != nil { +- return fmt.Errorf("%v: %v", from, err) +- } +- var description string +- if hover != nil { +- description = strings.TrimSpace(hover.Contents.Value) +- } - -- // Send all of the import edits as one code action if the file is -- // being organized. -- if want[protocol.SourceOrganizeImports] && len(importEdits) > 0 { -- actions = append(actions, protocol.CodeAction{ -- Title: "Organize Imports", -- Kind: protocol.SourceOrganizeImports, -- Edit: &protocol.WorkspaceEdit{ -- DocumentChanges: documentChanges(fh, importEdits), -- }, -- }) -- } -- } +- result := &Definition{ +- Span: definition, +- Description: description, +- } +- if d.JSON { +- enc := json.NewEncoder(os.Stdout) +- enc.SetIndent("", "\t") +- return enc.Encode(result) +- } +- fmt.Printf("%v", result.Span) +- if len(result.Description) > 0 { +- fmt.Printf(": defined here as %s", result.Description) +- } +- fmt.Printf("\n") +- return nil +-} +diff -urN a/gopls/internal/cmd/execute.go b/gopls/internal/cmd/execute.go +--- a/gopls/internal/cmd/execute.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/execute.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,155 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- if want[protocol.RefactorExtract] { -- extractions, err := refactorExtract(ctx, snapshot, pgf, params.Range) -- if err != nil { -- return nil, err -- } -- actions = append(actions, extractions...) -- } -- } +-package cmd - -- var stubMethodsDiagnostics []protocol.Diagnostic -- if wantQuickFixes && snapshot.Options().IsAnalyzerEnabled(stubmethods.Analyzer.Name) { -- for _, pd := range diagnostics { -- if stubmethods.MatchesMessage(pd.Message) { -- stubMethodsDiagnostics = append(stubMethodsDiagnostics, pd) -- } -- } -- } +-import ( +- "context" +- "encoding/json" +- "flag" +- "fmt" +- "log" +- "os" +- "strings" - -- // Code actions requiring type information. -- if len(stubMethodsDiagnostics) > 0 || -- want[protocol.RefactorRewrite] || -- want[protocol.RefactorInline] || -- want[protocol.GoTest] { -- pkg, pgf, err := source.NarrowestPackageForFile(ctx, snapshot, fh.URI()) -- if err != nil { -- return nil, err -- } -- for _, pd := range stubMethodsDiagnostics { -- start, end, err := pgf.RangePos(pd.Range) -- if err != nil { -- return nil, err -- } -- action, ok, err := func() (_ protocol.CodeAction, _ bool, rerr error) { -- // golang/go#61693: code actions were refactored to run outside of the -- // analysis framework, but as a result they lost their panic recovery. -- // -- // Stubmethods "should never fail"", but put back the panic recovery as a -- // defensive measure. -- defer func() { -- if r := recover(); r != nil { -- rerr = bug.Errorf("stubmethods panicked: %v", r) -- } -- }() -- d, ok := stubmethods.DiagnosticForError(pkg.FileSet(), pgf.File, start, end, pd.Message, pkg.GetTypesInfo()) -- if !ok { -- return protocol.CodeAction{}, false, nil -- } -- cmd, err := command.NewApplyFixCommand(d.Message, command.ApplyFixArgs{ -- URI: protocol.URIFromSpanURI(pgf.URI), -- Fix: source.StubMethods, -- Range: pd.Range, -- }) -- if err != nil { -- return protocol.CodeAction{}, false, err -- } -- return protocol.CodeAction{ -- Title: d.Message, -- Kind: protocol.QuickFix, -- Command: &cmd, -- Diagnostics: []protocol.Diagnostic{pd}, -- }, true, nil -- }() -- if err != nil { -- return nil, err -- } -- if ok { -- actions = append(actions, action) -- } -- } +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/command" +- "golang.org/x/tools/gopls/internal/server" +- "golang.org/x/tools/gopls/internal/util/slices" +- "golang.org/x/tools/internal/tool" +-) - -- if want[protocol.RefactorRewrite] { -- rewrites, err := refactorRewrite(ctx, snapshot, pkg, pgf, fh, params.Range) -- if err != nil { -- return nil, err -- } -- actions = append(actions, rewrites...) -- } +-// execute implements the LSP ExecuteCommand verb for gopls. +-type execute struct { +- EditFlags +- app *Application +-} - -- if want[protocol.RefactorInline] { -- rewrites, err := refactorInline(ctx, snapshot, pkg, pgf, fh, params.Range) -- if err != nil { -- return nil, err -- } -- actions = append(actions, rewrites...) -- } +-func (e *execute) Name() string { return "execute" } +-func (e *execute) Parent() string { return e.app.Name() } +-func (e *execute) Usage() string { return "[flags] command argument..." } +-func (e *execute) ShortHelp() string { return "Execute a gopls custom LSP command" } +-func (e *execute) DetailedHelp(f *flag.FlagSet) { +- fmt.Fprint(f.Output(), ` +-The execute command sends an LSP ExecuteCommand request to gopls, +-with a set of optional JSON argument values. +-Some commands return a result, also JSON. - -- if want[protocol.GoTest] { -- fixes, err := goTest(ctx, snapshot, pkg, pgf, params.Range) -- if err != nil { -- return nil, err -- } -- actions = append(actions, fixes...) -- } -- } +-Available commands are documented at: - -- return actions, nil +- https://github.com/golang/tools/blob/master/gopls/doc/commands.md - -- default: -- // Unsupported file kind for a code action. -- return nil, nil -- } --} +-This interface is experimental and commands may change or disappear without notice. - --func (s *Server) findMatchingDiagnostics(uri span.URI, pd protocol.Diagnostic) []*source.Diagnostic { -- s.diagnosticsMu.Lock() -- defer s.diagnosticsMu.Unlock() +-Examples: - -- var sds []*source.Diagnostic -- for _, report := range s.diagnostics[uri].reports { -- for _, sd := range report.diags { -- sameDiagnostic := (pd.Message == strings.TrimSpace(sd.Message) && // extra space may have been trimmed when converting to protocol.Diagnostic -- protocol.CompareRange(pd.Range, sd.Range) == 0 && -- pd.Source == string(sd.Source)) +- $ gopls execute gopls.add_import '{"ImportPath": "fmt", "URI": "file:///hello.go"}' +- $ gopls execute gopls.run_tests '{"URI": "file:///a_test.go", "Tests": ["Test"]}' +- $ gopls execute gopls.list_known_packages '{"URI": "file:///hello.go"}' - -- if sameDiagnostic { -- sds = append(sds, sd) -- } -- } -- } -- return sds +-execute-flags: +-`) +- printFlagDefaults(f) -} - --func (s *Server) getSupportedCodeActions() []protocol.CodeActionKind { -- allCodeActionKinds := make(map[protocol.CodeActionKind]struct{}) -- for _, kinds := range s.Options().SupportedCodeActions { -- for kind := range kinds { -- allCodeActionKinds[kind] = struct{}{} -- } -- } -- var result []protocol.CodeActionKind -- for kind := range allCodeActionKinds { -- result = append(result, kind) +-func (e *execute) Run(ctx context.Context, args ...string) error { +- if len(args) == 0 { +- return tool.CommandLineErrorf("execute requires a command name") - } -- sort.Slice(result, func(i, j int) bool { -- return result[i] < result[j] -- }) -- return result --} -- --func importFixTitle(fix *imports.ImportFix) string { -- var str string -- switch fix.FixType { -- case imports.AddImport: -- str = fmt.Sprintf("Add import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath) -- case imports.DeleteImport: -- str = fmt.Sprintf("Delete import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath) -- case imports.SetImportName: -- str = fmt.Sprintf("Rename import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath) +- cmd := args[0] +- if !slices.Contains(command.Commands, command.Command(strings.TrimPrefix(cmd, "gopls."))) { +- return tool.CommandLineErrorf("unrecognized command: %s", cmd) - } -- return str --} - --// fixedByImportFix filters the provided slice of diagnostics to those that --// would be fixed by the provided imports fix. --func fixedByImportFix(fix *imports.ImportFix, diagnostics []protocol.Diagnostic) []protocol.Diagnostic { -- var results []protocol.Diagnostic -- for _, diagnostic := range diagnostics { -- switch { -- // "undeclared name: X" may be an unresolved import. -- case strings.HasPrefix(diagnostic.Message, "undeclared name: "): -- ident := strings.TrimPrefix(diagnostic.Message, "undeclared name: ") -- if ident == fix.IdentName { -- results = append(results, diagnostic) -- } -- // "undefined: X" may be an unresolved import at Go 1.20+. -- case strings.HasPrefix(diagnostic.Message, "undefined: "): -- ident := strings.TrimPrefix(diagnostic.Message, "undefined: ") -- if ident == fix.IdentName { -- results = append(results, diagnostic) -- } -- // "could not import: X" may be an invalid import. -- case strings.HasPrefix(diagnostic.Message, "could not import: "): -- ident := strings.TrimPrefix(diagnostic.Message, "could not import: ") -- if ident == fix.IdentName { -- results = append(results, diagnostic) -- } -- // "X imported but not used" is an unused import. -- // "X imported but not used as Y" is an unused import. -- case strings.Contains(diagnostic.Message, " imported but not used"): -- idx := strings.Index(diagnostic.Message, " imported but not used") -- importPath := diagnostic.Message[:idx] -- if importPath == fmt.Sprintf("%q", fix.StmtInfo.ImportPath) { -- results = append(results, diagnostic) -- } +- // A command may have multiple arguments, though the only one +- // that currently does so is the "legacy" gopls.test, +- // so we don't show an example of it. +- var jsonArgs []json.RawMessage +- for i, arg := range args[1:] { +- var dummy any +- if err := json.Unmarshal([]byte(arg), &dummy); err != nil { +- return fmt.Errorf("argument %d is not valid JSON: %v", i+1, err) - } +- jsonArgs = append(jsonArgs, json.RawMessage(arg)) - } -- return results --} - --func refactorExtract(ctx context.Context, snapshot source.Snapshot, pgf *source.ParsedGoFile, rng protocol.Range) ([]protocol.CodeAction, error) { -- if rng.Start == rng.End { -- return nil, nil -- } +- e.app.editFlags = &e.EditFlags // in case command performs an edit - -- start, end, err := pgf.RangePos(rng) +- cmdDone, onProgress := commandProgress() +- conn, err := e.app.connect(ctx, onProgress) - if err != nil { -- return nil, err +- return err - } -- puri := protocol.URIFromSpanURI(pgf.URI) -- var commands []protocol.Command -- if _, ok, methodOk, _ := source.CanExtractFunction(pgf.Tok, start, end, pgf.Src, pgf.File); ok { -- cmd, err := command.NewApplyFixCommand("Extract function", command.ApplyFixArgs{ -- URI: puri, -- Fix: source.ExtractFunction, -- Range: rng, -- }) -- if err != nil { -- return nil, err -- } -- commands = append(commands, cmd) -- if methodOk { -- cmd, err := command.NewApplyFixCommand("Extract method", command.ApplyFixArgs{ -- URI: puri, -- Fix: source.ExtractMethod, -- Range: rng, -- }) -- if err != nil { -- return nil, err -- } -- commands = append(commands, cmd) -- } +- defer conn.terminate(ctx) +- +- res, err := conn.executeCommand(ctx, cmdDone, &protocol.Command{ +- Command: cmd, +- Arguments: jsonArgs, +- }) +- if err != nil { +- return err - } -- if _, _, ok, _ := source.CanExtractVariable(start, end, pgf.File); ok { -- cmd, err := command.NewApplyFixCommand("Extract variable", command.ApplyFixArgs{ -- URI: puri, -- Fix: source.ExtractVariable, -- Range: rng, -- }) +- if res != nil { +- data, err := json.MarshalIndent(res, "", "\t") - if err != nil { -- return nil, err +- log.Fatal(err) - } -- commands = append(commands, cmd) -- } -- var actions []protocol.CodeAction -- for i := range commands { -- actions = append(actions, protocol.CodeAction{ -- Title: commands[i].Title, -- Kind: protocol.RefactorExtract, -- Command: &commands[i], -- }) +- fmt.Printf("%s\n", data) - } -- return actions, nil +- return nil -} - --func refactorRewrite(ctx context.Context, snapshot source.Snapshot, pkg source.Package, pgf *source.ParsedGoFile, fh source.FileHandle, rng protocol.Range) (_ []protocol.CodeAction, rerr error) { -- // golang/go#61693: code actions were refactored to run outside of the -- // analysis framework, but as a result they lost their panic recovery. -- // -- // These code actions should never fail, but put back the panic recovery as a -- // defensive measure. -- defer func() { -- if r := recover(); r != nil { -- rerr = bug.Errorf("refactor.rewrite code actions panicked: %v", r) -- } -- }() +-// -- shared command helpers -- - -- var actions []protocol.CodeAction +-const cmdProgressToken = "cmd-progress" - -- if canRemoveParameter(pkg, pgf, rng) { -- cmd, err := command.NewChangeSignatureCommand("remove unused parameter", command.ChangeSignatureArgs{ -- RemoveParameter: protocol.Location{ -- URI: protocol.URIFromSpanURI(pgf.URI), -- Range: rng, -- }, -- }) -- if err != nil { -- return nil, err +-// TODO(adonovan): disentangle this from app.connect, and factor with +-// conn.executeCommand used by codelens and execute. Seems like +-// connection needs a way to register and unregister independent +-// handlers, later than at connect time. +-func commandProgress() (<-chan bool, func(p *protocol.ProgressParams)) { +- cmdDone := make(chan bool, 1) +- onProgress := func(p *protocol.ProgressParams) { +- switch v := p.Value.(type) { +- case *protocol.WorkDoneProgressReport: +- // TODO(adonovan): how can we segregate command's stdout and +- // stderr so that structure is preserved? +- fmt.Fprintln(os.Stderr, v.Message) +- +- case *protocol.WorkDoneProgressEnd: +- if p.Token == cmdProgressToken { +- // commandHandler.run sends message = canceled | failed | completed +- cmdDone <- v.Message == server.CommandCompleted +- } - } -- actions = append(actions, protocol.CodeAction{ -- Title: "Refactor: remove unused parameter", -- Kind: protocol.RefactorRewrite, -- Command: &cmd, -- }) - } +- return cmdDone, onProgress +-} - -- start, end, err := pgf.RangePos(rng) +-func (conn *connection) executeCommand(ctx context.Context, done <-chan bool, cmd *protocol.Command) (any, error) { +- res, err := conn.ExecuteCommand(ctx, &protocol.ExecuteCommandParams{ +- Command: cmd.Command, +- Arguments: cmd.Arguments, +- WorkDoneProgressParams: protocol.WorkDoneProgressParams{ +- WorkDoneToken: cmdProgressToken, +- }, +- }) - if err != nil { - return nil, err - } - -- var commands []protocol.Command -- if _, ok, _ := source.CanInvertIfCondition(pgf.File, start, end); ok { -- cmd, err := command.NewApplyFixCommand("Invert if condition", command.ApplyFixArgs{ -- URI: protocol.URIFromSpanURI(pgf.URI), -- Fix: source.InvertIfCondition, -- Range: rng, -- }) -- if err != nil { -- return nil, err -- } -- commands = append(commands, cmd) -- } -- -- // N.B.: an inspector only pays for itself after ~5 passes, which means we're -- // currently not getting a good deal on this inspection. +- // Wait for it to finish (by watching for a progress token). - // -- // TODO: Consider removing the inspection after convenienceAnalyzers are removed. -- inspect := inspector.New([]*ast.File{pgf.File}) -- if snapshot.Options().IsAnalyzerEnabled(fillstruct.Analyzer.Name) { -- for _, d := range fillstruct.DiagnoseFillableStructs(inspect, start, end, pkg.GetTypes(), pkg.GetTypesInfo()) { -- rng, err := pgf.Mapper.PosRange(pgf.Tok, d.Pos, d.End) -- if err != nil { -- return nil, err -- } -- cmd, err := command.NewApplyFixCommand(d.Message, command.ApplyFixArgs{ -- URI: protocol.URIFromSpanURI(pgf.URI), -- Fix: source.FillStruct, -- Range: rng, -- }) -- if err != nil { -- return nil, err -- } -- commands = append(commands, cmd) -- } +- // In theory this is only necessary for the two async +- // commands (RunGovulncheck and RunTests), but the tests +- // fail for Test as well (why?), and there is no cost to +- // waiting in all cases. TODO(adonovan): investigate. +- if success := <-done; !success { +- // TODO(adonovan): suppress this message; +- // the command's stderr should suffice. +- return nil, fmt.Errorf("command failed") - } - -- for i := range commands { -- actions = append(actions, protocol.CodeAction{ -- Title: commands[i].Title, -- Kind: protocol.RefactorRewrite, -- Command: &commands[i], -- }) -- } +- return res, nil +-} +diff -urN a/gopls/internal/cmd/folding_range.go b/gopls/internal/cmd/folding_range.go +--- a/gopls/internal/cmd/folding_range.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/folding_range.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,71 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- if snapshot.Options().IsAnalyzerEnabled(infertypeargs.Analyzer.Name) { -- for _, d := range infertypeargs.DiagnoseInferableTypeArgs(pkg.FileSet(), inspect, start, end, pkg.GetTypes(), pkg.GetTypesInfo()) { -- if len(d.SuggestedFixes) != 1 { -- panic(fmt.Sprintf("unexpected number of suggested fixes from infertypeargs: %v", len(d.SuggestedFixes))) -- } -- fix := d.SuggestedFixes[0] -- var edits []protocol.TextEdit -- for _, analysisEdit := range fix.TextEdits { -- rng, err := pgf.Mapper.PosRange(pgf.Tok, analysisEdit.Pos, analysisEdit.End) -- if err != nil { -- return nil, err -- } -- edits = append(edits, protocol.TextEdit{ -- Range: rng, -- NewText: string(analysisEdit.NewText), -- }) -- } -- actions = append(actions, protocol.CodeAction{ -- Title: "Simplify type arguments", -- Kind: protocol.RefactorRewrite, -- Edit: &protocol.WorkspaceEdit{ -- DocumentChanges: documentChanges(fh, edits), -- }, -- }) -- } -- } +-package cmd - -- return actions, nil +-import ( +- "context" +- "flag" +- "fmt" +- +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/tool" +-) +- +-// foldingRanges implements the folding_ranges verb for gopls +-type foldingRanges struct { +- app *Application -} - --// canRemoveParameter reports whether we can remove the function parameter --// indicated by the given [start, end) range. --// --// This is true if: --// - [start, end) is contained within an unused field or parameter name --// - ... of a non-method function declaration. --func canRemoveParameter(pkg source.Package, pgf *source.ParsedGoFile, rng protocol.Range) bool { -- info := source.FindParam(pgf, rng) -- if info.Decl == nil || info.Field == nil { -- return false +-func (r *foldingRanges) Name() string { return "folding_ranges" } +-func (r *foldingRanges) Parent() string { return r.app.Name() } +-func (r *foldingRanges) Usage() string { return "" } +-func (r *foldingRanges) ShortHelp() string { return "display selected file's folding ranges" } +-func (r *foldingRanges) DetailedHelp(f *flag.FlagSet) { +- fmt.Fprint(f.Output(), ` +-Example: +- +- $ gopls folding_ranges helper/helper.go +-`) +- printFlagDefaults(f) +-} +- +-func (r *foldingRanges) Run(ctx context.Context, args ...string) error { +- if len(args) != 1 { +- return tool.CommandLineErrorf("folding_ranges expects 1 argument (file)") - } - -- if info.Decl.Body == nil { -- return false // external function +- conn, err := r.app.connect(ctx, nil) +- if err != nil { +- return err - } +- defer conn.terminate(ctx) - -- if len(info.Field.Names) == 0 { -- return true // no names => field is unused +- from := parseSpan(args[0]) +- if _, err := conn.openFile(ctx, from.URI()); err != nil { +- return err - } -- if info.Name == nil { -- return false // no name is indicated +- +- p := protocol.FoldingRangeParams{ +- TextDocument: protocol.TextDocumentIdentifier{ +- URI: from.URI(), +- }, - } -- if info.Name.Name == "_" { -- return true // trivially unused +- +- ranges, err := conn.FoldingRange(ctx, &p) +- if err != nil { +- return err - } - -- obj := pkg.GetTypesInfo().Defs[info.Name] -- if obj == nil { -- return false // something went wrong +- for _, r := range ranges { +- fmt.Printf("%v:%v-%v:%v\n", +- r.StartLine+1, +- r.StartCharacter+1, +- r.EndLine+1, +- r.EndCharacter+1, +- ) - } - -- used := false -- ast.Inspect(info.Decl.Body, func(node ast.Node) bool { -- if n, ok := node.(*ast.Ident); ok && pkg.GetTypesInfo().Uses[n] == obj { -- used = true -- } -- return !used // keep going until we find a use -- }) -- return !used +- return nil -} +diff -urN a/gopls/internal/cmd/format.go b/gopls/internal/cmd/format.go +--- a/gopls/internal/cmd/format.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/format.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,75 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// refactorInline returns inline actions available at the specified range. --func refactorInline(ctx context.Context, snapshot source.Snapshot, pkg source.Package, pgf *source.ParsedGoFile, fh source.FileHandle, rng protocol.Range) ([]protocol.CodeAction, error) { -- var commands []protocol.Command +-package cmd - -- // If range is within call expression, offer inline action. -- if _, fn, err := source.EnclosingStaticCall(pkg, pgf, rng); err == nil { -- cmd, err := command.NewApplyFixCommand(fmt.Sprintf("Inline call to %s", fn.Name()), command.ApplyFixArgs{ -- URI: protocol.URIFromSpanURI(pgf.URI), -- Fix: source.InlineCall, -- Range: rng, -- }) -- if err != nil { -- return nil, err -- } -- commands = append(commands, cmd) -- } +-import ( +- "context" +- "flag" +- "fmt" - -- // Convert commands to actions. -- var actions []protocol.CodeAction -- for i := range commands { -- actions = append(actions, protocol.CodeAction{ -- Title: commands[i].Title, -- Kind: protocol.RefactorInline, -- Command: &commands[i], -- }) -- } -- return actions, nil --} +- "golang.org/x/tools/gopls/internal/protocol" +-) - --func documentChanges(fh source.FileHandle, edits []protocol.TextEdit) []protocol.DocumentChanges { -- return []protocol.DocumentChanges{ -- { -- TextDocumentEdit: &protocol.TextDocumentEdit{ -- TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{ -- Version: fh.Version(), -- TextDocumentIdentifier: protocol.TextDocumentIdentifier{ -- URI: protocol.URIFromSpanURI(fh.URI()), -- }, -- }, -- Edits: nonNilSliceTextEdit(edits), -- }, -- }, -- } +-// format implements the format verb for gopls. +-type format struct { +- EditFlags +- app *Application -} - --// codeActionsMatchingDiagnostics fetches code actions for the provided --// diagnostics, by first attempting to unmarshal code actions directly from the --// bundled protocol.Diagnostic.Data field, and failing that by falling back on --// fetching a matching source.Diagnostic from the set of stored diagnostics for --// this file. --func (s *Server) codeActionsMatchingDiagnostics(ctx context.Context, uri span.URI, snapshot source.Snapshot, pds []protocol.Diagnostic, want map[protocol.CodeActionKind]bool) ([]protocol.CodeAction, error) { -- var actions []protocol.CodeAction -- var unbundled []protocol.Diagnostic // diagnostics without bundled code actions in their Data field -- for _, pd := range pds { -- bundled := source.BundledQuickFixes(pd) -- if len(bundled) > 0 { -- for _, fix := range bundled { -- if want[fix.Kind] { -- actions = append(actions, fix) -- } -- } -- } else { -- // No bundled actions: keep searching for a match. -- unbundled = append(unbundled, pd) +-func (c *format) Name() string { return "format" } +-func (c *format) Parent() string { return c.app.Name() } +-func (c *format) Usage() string { return "[format-flags] " } +-func (c *format) ShortHelp() string { return "format the code according to the go standard" } +-func (c *format) DetailedHelp(f *flag.FlagSet) { +- fmt.Fprint(f.Output(), ` +-The arguments supplied may be simple file names, or ranges within files. +- +-Example: reformat this file: +- +- $ gopls format -w internal/cmd/check.go +- +-format-flags: +-`) +- printFlagDefaults(f) +-} +- +-// Run performs the check on the files specified by args and prints the +-// results to stdout. +-func (c *format) Run(ctx context.Context, args ...string) error { +- if len(args) == 0 { +- return nil +- } +- c.app.editFlags = &c.EditFlags +- conn, err := c.app.connect(ctx, nil) +- if err != nil { +- return err +- } +- defer conn.terminate(ctx) +- for _, arg := range args { +- spn := parseSpan(arg) +- file, err := conn.openFile(ctx, spn.URI()) +- if err != nil { +- return err +- } +- loc, err := file.spanLocation(spn) +- if err != nil { +- return err +- } +- if loc.Range.Start != loc.Range.End { +- return fmt.Errorf("only full file formatting supported") +- } +- p := protocol.DocumentFormattingParams{ +- TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, +- } +- edits, err := conn.Formatting(ctx, &p) +- if err != nil { +- return fmt.Errorf("%v: %v", spn, err) +- } +- if err := applyTextEdits(file.mapper, edits, c.app.editFlags); err != nil { +- return err - } - } +- return nil +-} +diff -urN a/gopls/internal/cmd/help_test.go b/gopls/internal/cmd/help_test.go +--- a/gopls/internal/cmd/help_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/help_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,90 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- for _, pd := range unbundled { -- for _, sd := range s.findMatchingDiagnostics(uri, pd) { -- diagActions, err := codeActionsForDiagnostic(ctx, snapshot, sd, &pd, want) +-package cmd_test +- +-// This file defines tests to ensure the cmd/usage/*.hlp files match +-// the output of the tool. The .hlp files are not actually needed by +-// the executable (they are not //go:embed-ded, say), but they make it +-// easier to review changes to the gopls command's help logic since +-// any effects are manifest as changes to these files. +- +-//go:generate go test -run Help -update-help-files +- +-import ( +- "bytes" +- "context" +- "flag" +- "os" +- "path/filepath" +- "testing" +- +- "github.com/google/go-cmp/cmp" +- "golang.org/x/tools/gopls/internal/cmd" +- "golang.org/x/tools/internal/testenv" +- "golang.org/x/tools/internal/tool" +-) +- +-var updateHelpFiles = flag.Bool("update-help-files", false, "Write out the help files instead of checking them") +- +-const appName = "gopls" +- +-func TestHelpFiles(t *testing.T) { +- testenv.NeedsGoBuild(t) // This is a lie. We actually need the source code. +- app := cmd.New(nil) +- ctx := context.Background() +- for _, page := range append(app.Commands(), app) { +- t.Run(page.Name(), func(t *testing.T) { +- var buf bytes.Buffer +- s := flag.NewFlagSet(page.Name(), flag.ContinueOnError) +- s.SetOutput(&buf) +- tool.Run(ctx, s, page, []string{"-h"}) +- name := page.Name() +- if name == appName { +- name = "usage" +- } +- helpFile := filepath.Join("usage", name+".hlp") +- got := buf.Bytes() +- if *updateHelpFiles { +- if err := os.WriteFile(helpFile, got, 0666); err != nil { +- t.Errorf("Failed writing %v: %v", helpFile, err) +- } +- return +- } +- want, err := os.ReadFile(helpFile) - if err != nil { -- return nil, err +- t.Fatalf("Missing help file %q", helpFile) - } -- actions = append(actions, diagActions...) -- } +- if diff := cmp.Diff(string(want), string(got)); diff != "" { +- t.Errorf("Help file %q did not match, run with -update-help-files to fix (-want +got)\n%s", helpFile, diff) +- } +- }) - } -- return actions, nil -} - --func codeActionsForDiagnostic(ctx context.Context, snapshot source.Snapshot, sd *source.Diagnostic, pd *protocol.Diagnostic, want map[protocol.CodeActionKind]bool) ([]protocol.CodeAction, error) { -- var actions []protocol.CodeAction -- for _, fix := range sd.SuggestedFixes { -- if !want[fix.ActionKind] { -- continue -- } -- changes := []protocol.DocumentChanges{} // must be a slice -- for uri, edits := range fix.Edits { -- fh, err := snapshot.ReadFile(ctx, uri) -- if err != nil { -- return nil, err -- } -- changes = append(changes, documentChanges(fh, edits)...) -- } -- action := protocol.CodeAction{ -- Title: fix.Title, -- Kind: fix.ActionKind, -- Edit: &protocol.WorkspaceEdit{ -- DocumentChanges: changes, -- }, -- Command: fix.Command, +-func TestVerboseHelp(t *testing.T) { +- testenv.NeedsGoBuild(t) // This is a lie. We actually need the source code. +- app := cmd.New(nil) +- ctx := context.Background() +- var buf bytes.Buffer +- s := flag.NewFlagSet(appName, flag.ContinueOnError) +- s.SetOutput(&buf) +- tool.Run(ctx, s, app, []string{"-v", "-h"}) +- got := buf.Bytes() +- +- helpFile := filepath.Join("usage", "usage-v.hlp") +- if *updateHelpFiles { +- if err := os.WriteFile(helpFile, got, 0666); err != nil { +- t.Errorf("Failed writing %v: %v", helpFile, err) - } -- action.Diagnostics = []protocol.Diagnostic{*pd} -- actions = append(actions, action) +- return +- } +- want, err := os.ReadFile(helpFile) +- if err != nil { +- t.Fatalf("Missing help file %q", helpFile) +- } +- if diff := cmp.Diff(string(want), string(got)); diff != "" { +- t.Errorf("Help file %q did not match, run with -update-help-files to fix (-want +got)\n%s", helpFile, diff) - } -- return actions, nil -} +diff -urN a/gopls/internal/cmd/highlight.go b/gopls/internal/cmd/highlight.go +--- a/gopls/internal/cmd/highlight.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/highlight.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,81 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func goTest(ctx context.Context, snapshot source.Snapshot, pkg source.Package, pgf *source.ParsedGoFile, rng protocol.Range) ([]protocol.CodeAction, error) { -- fns, err := source.TestsAndBenchmarks(pkg, pgf) -- if err != nil { -- return nil, err +-package cmd +- +-import ( +- "context" +- "flag" +- "fmt" +- +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/tool" +-) +- +-// highlight implements the highlight verb for gopls. +-type highlight struct { +- app *Application +-} +- +-func (r *highlight) Name() string { return "highlight" } +-func (r *highlight) Parent() string { return r.app.Name() } +-func (r *highlight) Usage() string { return "" } +-func (r *highlight) ShortHelp() string { return "display selected identifier's highlights" } +-func (r *highlight) DetailedHelp(f *flag.FlagSet) { +- fmt.Fprint(f.Output(), ` +-Example: +- +- $ # 1-indexed location (:line:column or :#offset) of the target identifier +- $ gopls highlight helper/helper.go:8:6 +- $ gopls highlight helper/helper.go:#53 +-`) +- printFlagDefaults(f) +-} +- +-func (r *highlight) Run(ctx context.Context, args ...string) error { +- if len(args) != 1 { +- return tool.CommandLineErrorf("highlight expects 1 argument (position)") - } - -- var tests, benchmarks []string -- for _, fn := range fns.Tests { -- if !protocol.Intersect(fn.Rng, rng) { -- continue -- } -- tests = append(tests, fn.Name) +- conn, err := r.app.connect(ctx, nil) +- if err != nil { +- return err - } -- for _, fn := range fns.Benchmarks { -- if !protocol.Intersect(fn.Rng, rng) { -- continue -- } -- benchmarks = append(benchmarks, fn.Name) +- defer conn.terminate(ctx) +- +- from := parseSpan(args[0]) +- file, err := conn.openFile(ctx, from.URI()) +- if err != nil { +- return err - } - -- if len(tests) == 0 && len(benchmarks) == 0 { -- return nil, nil +- loc, err := file.spanLocation(from) +- if err != nil { +- return err - } - -- cmd, err := command.NewTestCommand("Run tests and benchmarks", protocol.URIFromSpanURI(pgf.URI), tests, benchmarks) +- p := protocol.DocumentHighlightParams{ +- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), +- } +- highlights, err := conn.DocumentHighlight(ctx, &p) - if err != nil { -- return nil, err +- return err - } -- return []protocol.CodeAction{{ -- Title: cmd.Title, -- Kind: protocol.GoTest, -- Command: &cmd, -- }}, nil --} - --type unit = struct{} -diff -urN a/gopls/internal/lsp/code_lens.go b/gopls/internal/lsp/code_lens.go ---- a/gopls/internal/lsp/code_lens.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/code_lens.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,61 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +- var results []span +- for _, h := range highlights { +- s, err := file.rangeSpan(h.Range) +- if err != nil { +- return err +- } +- results = append(results, s) +- } +- // Sort results to make tests deterministic since DocumentHighlight uses a map. +- sortSpans(results) +- +- for _, s := range results { +- fmt.Println(s) +- } +- return nil +-} +diff -urN a/gopls/internal/cmd/implementation.go b/gopls/internal/cmd/implementation.go +--- a/gopls/internal/cmd/implementation.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/implementation.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,86 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package lsp +-package cmd - -import ( - "context" +- "flag" - "fmt" - "sort" - -- "golang.org/x/tools/gopls/internal/lsp/command" -- "golang.org/x/tools/gopls/internal/lsp/mod" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/tag" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/tool" -) - --func (s *Server) codeLens(ctx context.Context, params *protocol.CodeLensParams) ([]protocol.CodeLens, error) { -- ctx, done := event.Start(ctx, "lsp.Server.codeLens", tag.URI.Of(params.TextDocument.URI)) -- defer done() +-// implementation implements the implementation verb for gopls +-type implementation struct { +- app *Application +-} - -- snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind) -- defer release() -- if !ok { -- return nil, err +-func (i *implementation) Name() string { return "implementation" } +-func (i *implementation) Parent() string { return i.app.Name() } +-func (i *implementation) Usage() string { return "" } +-func (i *implementation) ShortHelp() string { return "display selected identifier's implementation" } +-func (i *implementation) DetailedHelp(f *flag.FlagSet) { +- fmt.Fprint(f.Output(), ` +-Example: +- +- $ # 1-indexed location (:line:column or :#offset) of the target identifier +- $ gopls implementation helper/helper.go:8:6 +- $ gopls implementation helper/helper.go:#53 +-`) +- printFlagDefaults(f) +-} +- +-func (i *implementation) Run(ctx context.Context, args ...string) error { +- if len(args) != 1 { +- return tool.CommandLineErrorf("implementation expects 1 argument (position)") - } -- var lenses map[command.Command]source.LensFunc -- switch snapshot.FileKind(fh) { -- case source.Mod: -- lenses = mod.LensFuncs() -- case source.Go: -- lenses = source.LensFuncs() -- default: -- // Unsupported file kind for a code lens. -- return nil, nil +- +- conn, err := i.app.connect(ctx, nil) +- if err != nil { +- return err - } -- var result []protocol.CodeLens -- for cmd, lf := range lenses { -- if !snapshot.Options().Codelenses[string(cmd)] { -- continue +- defer conn.terminate(ctx) +- +- from := parseSpan(args[0]) +- file, err := conn.openFile(ctx, from.URI()) +- if err != nil { +- return err +- } +- +- loc, err := file.spanLocation(from) +- if err != nil { +- return err +- } +- +- p := protocol.ImplementationParams{ +- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), +- } +- implementations, err := conn.Implementation(ctx, &p) +- if err != nil { +- return err +- } +- +- var spans []string +- for _, impl := range implementations { +- f, err := conn.openFile(ctx, impl.URI) +- if err != nil { +- return err - } -- added, err := lf(ctx, snapshot, fh) -- // Code lens is called on every keystroke, so we should just operate in -- // a best-effort mode, ignoring errors. +- span, err := f.locationSpan(impl) - if err != nil { -- event.Error(ctx, fmt.Sprintf("code lens %s failed", cmd), err) -- continue +- return err - } -- result = append(result, added...) +- spans = append(spans, fmt.Sprint(span)) - } -- sort.Slice(result, func(i, j int) bool { -- a, b := result[i], result[j] -- if cmp := protocol.CompareRange(a.Range, b.Range); cmp != 0 { -- return cmp < 0 -- } -- return a.Command.Command < b.Command.Command -- }) -- return result, nil +- sort.Strings(spans) +- +- for _, s := range spans { +- fmt.Println(s) +- } +- +- return nil -} -diff -urN a/gopls/internal/lsp/command/command_gen.go b/gopls/internal/lsp/command/command_gen.go ---- a/gopls/internal/lsp/command/command_gen.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/command/command_gen.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,641 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/cmd/imports.go b/gopls/internal/cmd/imports.go +--- a/gopls/internal/cmd/imports.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/imports.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,80 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --// Don't include this file during code generation, or it will break the build --// if existing interface methods have been modified. --//go:build !generate --// +build !generate -- --// Code generated by generate.go. DO NOT EDIT. -- --package command +-package cmd - -import ( - "context" +- "flag" - "fmt" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/tool" -) - --const ( -- AddDependency Command = "add_dependency" -- AddImport Command = "add_import" -- AddTelemetryCounters Command = "add_telemetry_counters" -- ApplyFix Command = "apply_fix" -- ChangeSignature Command = "change_signature" -- CheckUpgrades Command = "check_upgrades" -- EditGoDirective Command = "edit_go_directive" -- FetchVulncheckResult Command = "fetch_vulncheck_result" -- GCDetails Command = "gc_details" -- Generate Command = "generate" -- GoGetPackage Command = "go_get_package" -- ListImports Command = "list_imports" -- ListKnownPackages Command = "list_known_packages" -- MaybePromptForTelemetry Command = "maybe_prompt_for_telemetry" -- MemStats Command = "mem_stats" -- RegenerateCgo Command = "regenerate_cgo" -- RemoveDependency Command = "remove_dependency" -- ResetGoModDiagnostics Command = "reset_go_mod_diagnostics" -- RunGoWorkCommand Command = "run_go_work_command" -- RunGovulncheck Command = "run_govulncheck" -- RunTests Command = "run_tests" -- StartDebugging Command = "start_debugging" -- StartProfile Command = "start_profile" -- StopProfile Command = "stop_profile" -- Test Command = "test" -- Tidy Command = "tidy" -- ToggleGCDetails Command = "toggle_gc_details" -- UpdateGoSum Command = "update_go_sum" -- UpgradeDependency Command = "upgrade_dependency" -- Vendor Command = "vendor" -- WorkspaceStats Command = "workspace_stats" --) +-// imports implements the import verb for gopls. +-type imports struct { +- EditFlags +- app *Application +-} - --var Commands = []Command{ -- AddDependency, -- AddImport, -- AddTelemetryCounters, -- ApplyFix, -- ChangeSignature, -- CheckUpgrades, -- EditGoDirective, -- FetchVulncheckResult, -- GCDetails, -- Generate, -- GoGetPackage, -- ListImports, -- ListKnownPackages, -- MaybePromptForTelemetry, -- MemStats, -- RegenerateCgo, -- RemoveDependency, -- ResetGoModDiagnostics, -- RunGoWorkCommand, -- RunGovulncheck, -- RunTests, -- StartDebugging, -- StartProfile, -- StopProfile, -- Test, -- Tidy, -- ToggleGCDetails, -- UpdateGoSum, -- UpgradeDependency, -- Vendor, -- WorkspaceStats, +-func (t *imports) Name() string { return "imports" } +-func (t *imports) Parent() string { return t.app.Name() } +-func (t *imports) Usage() string { return "[imports-flags] " } +-func (t *imports) ShortHelp() string { return "updates import statements" } +-func (t *imports) DetailedHelp(f *flag.FlagSet) { +- fmt.Fprintf(f.Output(), ` +-Example: update imports statements in a file: +- +- $ gopls imports -w internal/cmd/check.go +- +-imports-flags: +-`) +- printFlagDefaults(f) -} - --func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Interface) (interface{}, error) { -- switch params.Command { -- case "gopls.add_dependency": -- var a0 DependencyArgs -- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { -- return nil, err -- } -- return nil, s.AddDependency(ctx, a0) -- case "gopls.add_import": -- var a0 AddImportArgs -- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { -- return nil, err +-// Run performs diagnostic checks on the file specified and either; +-// - if -w is specified, updates the file in place; +-// - if -d is specified, prints out unified diffs of the changes; or +-// - otherwise, prints the new versions to stdout. +-func (t *imports) Run(ctx context.Context, args ...string) error { +- if len(args) != 1 { +- return tool.CommandLineErrorf("imports expects 1 argument") +- } +- t.app.editFlags = &t.EditFlags +- conn, err := t.app.connect(ctx, nil) +- if err != nil { +- return err +- } +- defer conn.terminate(ctx) +- +- from := parseSpan(args[0]) +- uri := from.URI() +- file, err := conn.openFile(ctx, uri) +- if err != nil { +- return err +- } +- actions, err := conn.CodeAction(ctx, &protocol.CodeActionParams{ +- TextDocument: protocol.TextDocumentIdentifier{ +- URI: uri, +- }, +- }) +- if err != nil { +- return fmt.Errorf("%v: %v", from, err) +- } +- var edits []protocol.TextEdit +- for _, a := range actions { +- if a.Title != "Organize Imports" { +- continue - } -- return nil, s.AddImport(ctx, a0) -- case "gopls.add_telemetry_counters": -- var a0 AddTelemetryCountersArgs -- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { -- return nil, err +- for _, c := range a.Edit.DocumentChanges { +- if c.TextDocumentEdit != nil { +- if c.TextDocumentEdit.TextDocument.URI == uri { +- edits = append(edits, protocol.AsTextEdits(c.TextDocumentEdit.Edits)...) +- } +- } - } -- return nil, s.AddTelemetryCounters(ctx, a0) -- case "gopls.apply_fix": -- var a0 ApplyFixArgs -- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { -- return nil, err -- } -- return nil, s.ApplyFix(ctx, a0) -- case "gopls.change_signature": -- var a0 ChangeSignatureArgs -- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { -- return nil, err -- } -- return nil, s.ChangeSignature(ctx, a0) -- case "gopls.check_upgrades": -- var a0 CheckUpgradesArgs -- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { -- return nil, err -- } -- return nil, s.CheckUpgrades(ctx, a0) -- case "gopls.edit_go_directive": -- var a0 EditGoDirectiveArgs -- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { -- return nil, err -- } -- return nil, s.EditGoDirective(ctx, a0) -- case "gopls.fetch_vulncheck_result": -- var a0 URIArg -- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { -- return nil, err -- } -- return s.FetchVulncheckResult(ctx, a0) -- case "gopls.gc_details": -- var a0 protocol.DocumentURI -- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { -- return nil, err -- } -- return nil, s.GCDetails(ctx, a0) -- case "gopls.generate": -- var a0 GenerateArgs -- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { -- return nil, err -- } -- return nil, s.Generate(ctx, a0) -- case "gopls.go_get_package": -- var a0 GoGetPackageArgs -- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { -- return nil, err -- } -- return nil, s.GoGetPackage(ctx, a0) -- case "gopls.list_imports": -- var a0 URIArg -- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { -- return nil, err -- } -- return s.ListImports(ctx, a0) -- case "gopls.list_known_packages": -- var a0 URIArg -- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { -- return nil, err -- } -- return s.ListKnownPackages(ctx, a0) -- case "gopls.maybe_prompt_for_telemetry": -- return nil, s.MaybePromptForTelemetry(ctx) -- case "gopls.mem_stats": -- return s.MemStats(ctx) -- case "gopls.regenerate_cgo": -- var a0 URIArg -- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { -- return nil, err -- } -- return nil, s.RegenerateCgo(ctx, a0) -- case "gopls.remove_dependency": -- var a0 RemoveDependencyArgs -- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { -- return nil, err -- } -- return nil, s.RemoveDependency(ctx, a0) -- case "gopls.reset_go_mod_diagnostics": -- var a0 ResetGoModDiagnosticsArgs -- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { -- return nil, err -- } -- return nil, s.ResetGoModDiagnostics(ctx, a0) -- case "gopls.run_go_work_command": -- var a0 RunGoWorkArgs -- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { -- return nil, err -- } -- return nil, s.RunGoWorkCommand(ctx, a0) -- case "gopls.run_govulncheck": -- var a0 VulncheckArgs -- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { -- return nil, err -- } -- return s.RunGovulncheck(ctx, a0) -- case "gopls.run_tests": -- var a0 RunTestsArgs -- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { -- return nil, err -- } -- return nil, s.RunTests(ctx, a0) -- case "gopls.start_debugging": -- var a0 DebuggingArgs -- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { -- return nil, err -- } -- return s.StartDebugging(ctx, a0) -- case "gopls.start_profile": -- var a0 StartProfileArgs -- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { -- return nil, err -- } -- return s.StartProfile(ctx, a0) -- case "gopls.stop_profile": -- var a0 StopProfileArgs -- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { -- return nil, err -- } -- return s.StopProfile(ctx, a0) -- case "gopls.test": -- var a0 protocol.DocumentURI -- var a1 []string -- var a2 []string -- if err := UnmarshalArgs(params.Arguments, &a0, &a1, &a2); err != nil { -- return nil, err -- } -- return nil, s.Test(ctx, a0, a1, a2) -- case "gopls.tidy": -- var a0 URIArgs -- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { -- return nil, err -- } -- return nil, s.Tidy(ctx, a0) -- case "gopls.toggle_gc_details": -- var a0 URIArg -- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { -- return nil, err -- } -- return nil, s.ToggleGCDetails(ctx, a0) -- case "gopls.update_go_sum": -- var a0 URIArgs -- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { -- return nil, err -- } -- return nil, s.UpdateGoSum(ctx, a0) -- case "gopls.upgrade_dependency": -- var a0 DependencyArgs -- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { -- return nil, err -- } -- return nil, s.UpgradeDependency(ctx, a0) -- case "gopls.vendor": -- var a0 URIArg -- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { -- return nil, err -- } -- return nil, s.Vendor(ctx, a0) -- case "gopls.workspace_stats": -- return s.WorkspaceStats(ctx) - } -- return nil, fmt.Errorf("unsupported command %q", params.Command) +- return applyTextEdits(file.mapper, edits, t.app.editFlags) -} +diff -urN a/gopls/internal/cmd/info.go b/gopls/internal/cmd/info.go +--- a/gopls/internal/cmd/info.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/info.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,313 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func NewAddDependencyCommand(title string, a0 DependencyArgs) (protocol.Command, error) { -- args, err := MarshalArgs(a0) -- if err != nil { -- return protocol.Command{}, err -- } -- return protocol.Command{ -- Title: title, -- Command: "gopls.add_dependency", -- Arguments: args, -- }, nil +-package cmd +- +-// This file defines the help, bug, version, api-json, licenses commands. +- +-import ( +- "bytes" +- "context" +- "encoding/json" +- "flag" +- "fmt" +- "net/url" +- "os" +- "sort" +- "strings" +- +- "golang.org/x/tools/gopls/internal/debug" +- "golang.org/x/tools/gopls/internal/filecache" +- "golang.org/x/tools/gopls/internal/settings" +- "golang.org/x/tools/gopls/internal/util/browser" +- goplsbug "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/internal/tool" +-) +- +-// help implements the help command. +-type help struct { +- app *Application -} - --func NewAddImportCommand(title string, a0 AddImportArgs) (protocol.Command, error) { -- args, err := MarshalArgs(a0) -- if err != nil { -- return protocol.Command{}, err -- } -- return protocol.Command{ -- Title: title, -- Command: "gopls.add_import", -- Arguments: args, -- }, nil +-func (h *help) Name() string { return "help" } +-func (h *help) Parent() string { return h.app.Name() } +-func (h *help) Usage() string { return "" } +-func (h *help) ShortHelp() string { return "print usage information for subcommands" } +-func (h *help) DetailedHelp(f *flag.FlagSet) { +- fmt.Fprint(f.Output(), ` +- +-Examples: +-$ gopls help # main gopls help message +-$ gopls help remote # help on 'remote' command +-$ gopls help remote sessions # help on 'remote sessions' subcommand +-`) +- printFlagDefaults(f) -} - --func NewAddTelemetryCountersCommand(title string, a0 AddTelemetryCountersArgs) (protocol.Command, error) { -- args, err := MarshalArgs(a0) -- if err != nil { -- return protocol.Command{}, err +-// Run prints help information about a subcommand. +-func (h *help) Run(ctx context.Context, args ...string) error { +- find := func(cmds []tool.Application, name string) tool.Application { +- for _, cmd := range cmds { +- if cmd.Name() == name { +- return cmd +- } +- } +- return nil - } -- return protocol.Command{ -- Title: title, -- Command: "gopls.add_telemetry_counters", -- Arguments: args, -- }, nil --} - --func NewApplyFixCommand(title string, a0 ApplyFixArgs) (protocol.Command, error) { -- args, err := MarshalArgs(a0) -- if err != nil { -- return protocol.Command{}, err +- // Find the subcommand denoted by args (empty => h.app). +- var cmd tool.Application = h.app +- for i, arg := range args { +- cmd = find(getSubcommands(cmd), arg) +- if cmd == nil { +- return tool.CommandLineErrorf( +- "no such subcommand: %s", strings.Join(args[:i+1], " ")) +- } - } -- return protocol.Command{ -- Title: title, -- Command: "gopls.apply_fix", -- Arguments: args, -- }, nil +- +- // 'gopls help cmd subcmd' is equivalent to 'gopls cmd subcmd -h'. +- // The flag package prints the usage information (defined by tool.Run) +- // when it sees the -h flag. +- fs := flag.NewFlagSet(cmd.Name(), flag.ExitOnError) +- return tool.Run(ctx, fs, h.app, append(args[:len(args):len(args)], "-h")) -} - --func NewChangeSignatureCommand(title string, a0 ChangeSignatureArgs) (protocol.Command, error) { -- args, err := MarshalArgs(a0) -- if err != nil { -- return protocol.Command{}, err -- } -- return protocol.Command{ -- Title: title, -- Command: "gopls.change_signature", -- Arguments: args, -- }, nil +-// version implements the version command. +-type version struct { +- JSON bool `flag:"json" help:"outputs in json format."` +- +- app *Application -} - --func NewCheckUpgradesCommand(title string, a0 CheckUpgradesArgs) (protocol.Command, error) { -- args, err := MarshalArgs(a0) -- if err != nil { -- return protocol.Command{}, err -- } -- return protocol.Command{ -- Title: title, -- Command: "gopls.check_upgrades", -- Arguments: args, -- }, nil +-func (v *version) Name() string { return "version" } +-func (v *version) Parent() string { return v.app.Name() } +-func (v *version) Usage() string { return "" } +-func (v *version) ShortHelp() string { return "print the gopls version information" } +-func (v *version) DetailedHelp(f *flag.FlagSet) { +- fmt.Fprint(f.Output(), ``) +- printFlagDefaults(f) -} - --func NewEditGoDirectiveCommand(title string, a0 EditGoDirectiveArgs) (protocol.Command, error) { -- args, err := MarshalArgs(a0) -- if err != nil { -- return protocol.Command{}, err +-// Run prints version information to stdout. +-func (v *version) Run(ctx context.Context, args ...string) error { +- var mode = debug.PlainText +- if v.JSON { +- mode = debug.JSON - } -- return protocol.Command{ -- Title: title, -- Command: "gopls.edit_go_directive", -- Arguments: args, -- }, nil +- +- return debug.PrintVersionInfo(ctx, os.Stdout, v.app.verbose(), mode) -} - --func NewFetchVulncheckResultCommand(title string, a0 URIArg) (protocol.Command, error) { -- args, err := MarshalArgs(a0) -- if err != nil { -- return protocol.Command{}, err -- } -- return protocol.Command{ -- Title: title, -- Command: "gopls.fetch_vulncheck_result", -- Arguments: args, -- }, nil +-// bug implements the bug command. +-type bug struct { +- app *Application -} - --func NewGCDetailsCommand(title string, a0 protocol.DocumentURI) (protocol.Command, error) { -- args, err := MarshalArgs(a0) -- if err != nil { -- return protocol.Command{}, err -- } -- return protocol.Command{ -- Title: title, -- Command: "gopls.gc_details", -- Arguments: args, -- }, nil +-func (b *bug) Name() string { return "bug" } +-func (b *bug) Parent() string { return b.app.Name() } +-func (b *bug) Usage() string { return "" } +-func (b *bug) ShortHelp() string { return "report a bug in gopls" } +-func (b *bug) DetailedHelp(f *flag.FlagSet) { +- fmt.Fprint(f.Output(), ``) +- printFlagDefaults(f) -} - --func NewGenerateCommand(title string, a0 GenerateArgs) (protocol.Command, error) { -- args, err := MarshalArgs(a0) -- if err != nil { -- return protocol.Command{}, err +-const goplsBugPrefix = "x/tools/gopls: " +-const goplsBugHeader = `ATTENTION: Please answer these questions BEFORE submitting your issue. Thanks! +- +-#### What did you do? +-If possible, provide a recipe for reproducing the error. +-A complete runnable program is good. +-A link on play.golang.org is better. +-A failing unit test is the best. +- +-#### What did you expect to see? +- +- +-#### What did you see instead? +- +- +-` +- +-// Run collects some basic information and then prepares an issue ready to +-// be reported. +-func (b *bug) Run(ctx context.Context, args ...string) error { +- // This undocumented environment variable allows +- // the cmd integration test (and maintainers) to +- // trigger a call to bug.Report. +- if msg := os.Getenv("TEST_GOPLS_BUG"); msg != "" { +- filecache.Start() // register bug handler +- goplsbug.Report(msg) +- return nil - } -- return protocol.Command{ -- Title: title, -- Command: "gopls.generate", -- Arguments: args, -- }, nil --} - --func NewGoGetPackageCommand(title string, a0 GoGetPackageArgs) (protocol.Command, error) { -- args, err := MarshalArgs(a0) -- if err != nil { -- return protocol.Command{}, err +- // Enumerate bug reports, grouped and sorted. +- _, reports := filecache.BugReports() +- sort.Slice(reports, func(i, j int) bool { +- x, y := reports[i], reports[i] +- if x.Key != y.Key { +- return x.Key < y.Key // ascending key order +- } +- return y.AtTime.Before(x.AtTime) // most recent first +- }) +- keyDenom := make(map[string]int) // key is "file:line" +- for _, report := range reports { +- keyDenom[report.Key]++ - } -- return protocol.Command{ -- Title: title, -- Command: "gopls.go_get_package", -- Arguments: args, -- }, nil --} - --func NewListImportsCommand(title string, a0 URIArg) (protocol.Command, error) { -- args, err := MarshalArgs(a0) -- if err != nil { -- return protocol.Command{}, err +- // Privacy: the content of 'public' will be posted to GitHub +- // to populate an issue textarea. Even though the user must +- // submit the form to share the information with the world, +- // merely populating the form causes us to share the +- // information with GitHub itself. +- // +- // For that reason, we cannot write private information to +- // public, such as bug reports, which may quote source code. +- public := &bytes.Buffer{} +- fmt.Fprint(public, goplsBugHeader) +- if len(reports) > 0 { +- fmt.Fprintf(public, "#### Internal errors\n\n") +- fmt.Fprintf(public, "Gopls detected %d internal errors, %d distinct:\n", +- len(reports), len(keyDenom)) +- for key, denom := range keyDenom { +- fmt.Fprintf(public, "- %s (%d)\n", key, denom) +- } +- fmt.Fprintf(public, "\nPlease copy the full information printed by `gopls bug` here, if you are comfortable sharing it.\n\n") +- } +- debug.PrintVersionInfo(ctx, public, true, debug.Markdown) +- body := public.String() +- title := strings.Join(args, " ") +- if !strings.HasPrefix(title, goplsBugPrefix) { +- title = goplsBugPrefix + title +- } +- if !browser.Open("https://github.com/golang/go/issues/new?title=" + url.QueryEscape(title) + "&body=" + url.QueryEscape(body)) { +- fmt.Print("Please file a new issue at golang.org/issue/new using this template:\n\n") +- fmt.Print(body) - } -- return protocol.Command{ -- Title: title, -- Command: "gopls.list_imports", -- Arguments: args, -- }, nil --} - --func NewListKnownPackagesCommand(title string, a0 URIArg) (protocol.Command, error) { -- args, err := MarshalArgs(a0) -- if err != nil { -- return protocol.Command{}, err +- // Print bug reports to stdout (not GitHub). +- keyNum := make(map[string]int) +- for _, report := range reports { +- fmt.Printf("-- %v -- \n", report.AtTime) +- +- // Append seq number (e.g. " (1/2)") for repeated keys. +- var seq string +- if denom := keyDenom[report.Key]; denom > 1 { +- keyNum[report.Key]++ +- seq = fmt.Sprintf(" (%d/%d)", keyNum[report.Key], denom) +- } +- +- // Privacy: +- // - File and Stack may contain the name of the user that built gopls. +- // - Description may contain names of the user's packages/files/symbols. +- fmt.Printf("%s:%d: %s%s\n\n", report.File, report.Line, report.Description, seq) +- fmt.Printf("%s\n\n", report.Stack) - } -- return protocol.Command{ -- Title: title, -- Command: "gopls.list_known_packages", -- Arguments: args, -- }, nil +- if len(reports) > 0 { +- fmt.Printf("Please copy the above information into the GitHub issue, if you are comfortable sharing it.\n") +- } +- +- return nil -} - --func NewMaybePromptForTelemetryCommand(title string) (protocol.Command, error) { -- args, err := MarshalArgs() -- if err != nil { -- return protocol.Command{}, err -- } -- return protocol.Command{ -- Title: title, -- Command: "gopls.maybe_prompt_for_telemetry", -- Arguments: args, -- }, nil +-type apiJSON struct { +- app *Application -} - --func NewMemStatsCommand(title string) (protocol.Command, error) { -- args, err := MarshalArgs() -- if err != nil { -- return protocol.Command{}, err -- } -- return protocol.Command{ -- Title: title, -- Command: "gopls.mem_stats", -- Arguments: args, -- }, nil +-func (j *apiJSON) Name() string { return "api-json" } +-func (j *apiJSON) Parent() string { return j.app.Name() } +-func (j *apiJSON) Usage() string { return "" } +-func (j *apiJSON) ShortHelp() string { return "print JSON describing gopls API" } +-func (j *apiJSON) DetailedHelp(f *flag.FlagSet) { +- fmt.Fprint(f.Output(), ``) +- printFlagDefaults(f) -} - --func NewRegenerateCgoCommand(title string, a0 URIArg) (protocol.Command, error) { -- args, err := MarshalArgs(a0) +-func (j *apiJSON) Run(ctx context.Context, args ...string) error { +- js, err := json.MarshalIndent(settings.GeneratedAPIJSON, "", "\t") - if err != nil { -- return protocol.Command{}, err +- return err - } -- return protocol.Command{ -- Title: title, -- Command: "gopls.regenerate_cgo", -- Arguments: args, -- }, nil +- fmt.Fprint(os.Stdout, string(js)) +- return nil -} - --func NewRemoveDependencyCommand(title string, a0 RemoveDependencyArgs) (protocol.Command, error) { -- args, err := MarshalArgs(a0) -- if err != nil { -- return protocol.Command{}, err -- } -- return protocol.Command{ -- Title: title, -- Command: "gopls.remove_dependency", -- Arguments: args, -- }, nil +-type licenses struct { +- app *Application -} - --func NewResetGoModDiagnosticsCommand(title string, a0 ResetGoModDiagnosticsArgs) (protocol.Command, error) { -- args, err := MarshalArgs(a0) -- if err != nil { -- return protocol.Command{}, err -- } -- return protocol.Command{ -- Title: title, -- Command: "gopls.reset_go_mod_diagnostics", -- Arguments: args, -- }, nil +-func (l *licenses) Name() string { return "licenses" } +-func (l *licenses) Parent() string { return l.app.Name() } +-func (l *licenses) Usage() string { return "" } +-func (l *licenses) ShortHelp() string { return "print licenses of included software" } +-func (l *licenses) DetailedHelp(f *flag.FlagSet) { +- fmt.Fprint(f.Output(), ``) +- printFlagDefaults(f) -} - --func NewRunGoWorkCommandCommand(title string, a0 RunGoWorkArgs) (protocol.Command, error) { -- args, err := MarshalArgs(a0) -- if err != nil { -- return protocol.Command{}, err -- } -- return protocol.Command{ -- Title: title, -- Command: "gopls.run_go_work_command", -- Arguments: args, -- }, nil --} +-const licensePreamble = ` +-gopls is made available under the following BSD-style license: - --func NewRunGovulncheckCommand(title string, a0 VulncheckArgs) (protocol.Command, error) { -- args, err := MarshalArgs(a0) -- if err != nil { -- return protocol.Command{}, err -- } -- return protocol.Command{ -- Title: title, -- Command: "gopls.run_govulncheck", -- Arguments: args, -- }, nil --} +-Copyright (c) 2009 The Go Authors. All rights reserved. - --func NewRunTestsCommand(title string, a0 RunTestsArgs) (protocol.Command, error) { -- args, err := MarshalArgs(a0) -- if err != nil { -- return protocol.Command{}, err -- } -- return protocol.Command{ -- Title: title, -- Command: "gopls.run_tests", -- Arguments: args, -- }, nil --} +-Redistribution and use in source and binary forms, with or without +-modification, are permitted provided that the following conditions are +-met: - --func NewStartDebuggingCommand(title string, a0 DebuggingArgs) (protocol.Command, error) { -- args, err := MarshalArgs(a0) -- if err != nil { -- return protocol.Command{}, err -- } -- return protocol.Command{ -- Title: title, -- Command: "gopls.start_debugging", -- Arguments: args, -- }, nil --} +- * Redistributions of source code must retain the above copyright +-notice, this list of conditions and the following disclaimer. +- * Redistributions in binary form must reproduce the above +-copyright notice, this list of conditions and the following disclaimer +-in the documentation and/or other materials provided with the +-distribution. +- * Neither the name of Google Inc. nor the names of its +-contributors may be used to endorse or promote products derived from +-this software without specific prior written permission. - --func NewStartProfileCommand(title string, a0 StartProfileArgs) (protocol.Command, error) { -- args, err := MarshalArgs(a0) -- if err != nil { -- return protocol.Command{}, err -- } -- return protocol.Command{ -- Title: title, -- Command: "gopls.start_profile", -- Arguments: args, -- }, nil --} +-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +-OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - --func NewStopProfileCommand(title string, a0 StopProfileArgs) (protocol.Command, error) { -- args, err := MarshalArgs(a0) -- if err != nil { -- return protocol.Command{}, err -- } -- return protocol.Command{ -- Title: title, -- Command: "gopls.stop_profile", -- Arguments: args, -- }, nil --} +-gopls implements the LSP specification, which is made available under the following license: - --func NewTestCommand(title string, a0 protocol.DocumentURI, a1 []string, a2 []string) (protocol.Command, error) { -- args, err := MarshalArgs(a0, a1, a2) -- if err != nil { -- return protocol.Command{}, err -- } -- return protocol.Command{ -- Title: title, -- Command: "gopls.test", -- Arguments: args, -- }, nil --} +-Copyright (c) Microsoft Corporation - --func NewTidyCommand(title string, a0 URIArgs) (protocol.Command, error) { -- args, err := MarshalArgs(a0) -- if err != nil { -- return protocol.Command{}, err -- } -- return protocol.Command{ -- Title: title, -- Command: "gopls.tidy", -- Arguments: args, -- }, nil --} +-All rights reserved. - --func NewToggleGCDetailsCommand(title string, a0 URIArg) (protocol.Command, error) { -- args, err := MarshalArgs(a0) -- if err != nil { -- return protocol.Command{}, err -- } -- return protocol.Command{ -- Title: title, -- Command: "gopls.toggle_gc_details", -- Arguments: args, -- }, nil --} +-MIT License - --func NewUpdateGoSumCommand(title string, a0 URIArgs) (protocol.Command, error) { -- args, err := MarshalArgs(a0) -- if err != nil { -- return protocol.Command{}, err -- } -- return protocol.Command{ -- Title: title, -- Command: "gopls.update_go_sum", -- Arguments: args, -- }, nil --} +-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation +-files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, +-modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software +-is furnished to do so, subject to the following conditions: - --func NewUpgradeDependencyCommand(title string, a0 DependencyArgs) (protocol.Command, error) { -- args, err := MarshalArgs(a0) -- if err != nil { -- return protocol.Command{}, err -- } -- return protocol.Command{ -- Title: title, -- Command: "gopls.upgrade_dependency", -- Arguments: args, -- }, nil --} +-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - --func NewVendorCommand(title string, a0 URIArg) (protocol.Command, error) { -- args, err := MarshalArgs(a0) -- if err != nil { -- return protocol.Command{}, err -- } -- return protocol.Command{ -- Title: title, -- Command: "gopls.vendor", -- Arguments: args, -- }, nil --} +-THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +-OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +-BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT +-OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - --func NewWorkspaceStatsCommand(title string) (protocol.Command, error) { -- args, err := MarshalArgs() -- if err != nil { -- return protocol.Command{}, err +-gopls also includes software made available under these licenses: +-` +- +-func (l *licenses) Run(ctx context.Context, args ...string) error { +- opts := settings.DefaultOptions(l.app.options) +- txt := licensePreamble +- if opts.LicensesText == "" { +- txt += "(development gopls, license information not available)" +- } else { +- txt += opts.LicensesText - } -- return protocol.Command{ -- Title: title, -- Command: "gopls.workspace_stats", -- Arguments: args, -- }, nil +- fmt.Fprint(os.Stdout, txt) +- return nil -} -diff -urN a/gopls/internal/lsp/command/commandmeta/meta.go b/gopls/internal/lsp/command/commandmeta/meta.go ---- a/gopls/internal/lsp/command/commandmeta/meta.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/command/commandmeta/meta.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,259 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/cmd/integration_test.go b/gopls/internal/cmd/integration_test.go +--- a/gopls/internal/cmd/integration_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/integration_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,1187 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --// Package commandmeta provides metadata about LSP commands, by analyzing the --// command.Interface type. --package commandmeta +-// Package cmdtest contains the test suite for the command line behavior of gopls. +-package cmd_test +- +-// This file defines integration tests of each gopls subcommand that +-// fork+exec the command in a separate process. +-// +-// (Rather than execute 'go build gopls' during the test, we reproduce +-// the main entrypoint in the test executable.) +-// +-// The purpose of this test is to exercise client-side logic such as +-// argument parsing and formatting of LSP RPC responses, not server +-// behavior; see lsp_test for that. +-// +-// All tests run in parallel. +-// +-// TODO(adonovan): +-// - Use markers to represent positions in the input and in assertions. +-// - Coverage of cross-cutting things like cwd, environ, span parsing, etc. +-// - Subcommands that accept -write and -diff flags implement them +-// consistently; factor their tests. +-// - Add missing test for 'vulncheck' subcommand. +-// - Add tests for client-only commands: serve, bug, help, api-json, licenses. - -import ( +- "bytes" +- "context" +- "encoding/json" - "fmt" -- "go/ast" -- "go/token" -- "go/types" -- "reflect" +- "math/rand" +- "os" +- "os/exec" +- "path/filepath" +- "regexp" - "strings" -- "unicode" +- "testing" - -- "golang.org/x/tools/go/ast/astutil" -- "golang.org/x/tools/go/packages" -- "golang.org/x/tools/gopls/internal/lsp/command" +- "golang.org/x/tools/gopls/internal/cmd" +- "golang.org/x/tools/gopls/internal/debug" +- "golang.org/x/tools/gopls/internal/hooks" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/gopls/internal/version" +- "golang.org/x/tools/internal/testenv" +- "golang.org/x/tools/internal/tool" +- "golang.org/x/tools/txtar" -) - --type Command struct { -- MethodName string -- Name string -- // TODO(rFindley): I think Title can actually be eliminated. In all cases -- // where we use it, there is probably a more appropriate contextual title. -- Title string -- Doc string -- Args []*Field -- Result *Field --} +-// TestVersion tests the 'version' subcommand (../info.go). +-func TestVersion(t *testing.T) { +- t.Parallel() - --func (c *Command) ID() string { -- return command.ID(c.Name) --} +- tree := writeTree(t, "") - --type Field struct { -- Name string -- Doc string -- JSONTag string -- Type types.Type -- FieldMod string -- // In some circumstances, we may want to recursively load additional field -- // descriptors for fields of struct types, documenting their internals. -- Fields []*Field --} +- // There's not much we can robustly assert about the actual version. +- want := version.Version() // e.g. "master" - --func Load() (*packages.Package, []*Command, error) { -- pkgs, err := packages.Load( -- &packages.Config{ -- Mode: packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedImports | packages.NeedDeps, -- BuildFlags: []string{"-tags=generate"}, -- }, -- "golang.org/x/tools/gopls/internal/lsp/command", -- ) -- if err != nil { -- return nil, nil, fmt.Errorf("packages.Load: %v", err) -- } -- pkg := pkgs[0] -- if len(pkg.Errors) > 0 { -- return pkg, nil, pkg.Errors[0] +- // basic +- { +- res := gopls(t, tree, "version") +- res.checkExit(true) +- res.checkStdout(want) - } - -- // For a bit of type safety, use reflection to get the interface name within -- // the package scope. -- it := reflect.TypeOf((*command.Interface)(nil)).Elem() -- obj := pkg.Types.Scope().Lookup(it.Name()).Type().Underlying().(*types.Interface) +- // basic, with version override +- { +- res := goplsWithEnv(t, tree, []string{"TEST_GOPLS_VERSION=v1.2.3"}, "version") +- res.checkExit(true) +- res.checkStdout(`v1\.2\.3`) +- } - -- // Load command metadata corresponding to each interface method. -- var commands []*Command -- loader := fieldLoader{make(map[types.Object]*Field)} -- for i := 0; i < obj.NumMethods(); i++ { -- m := obj.Method(i) -- c, err := loader.loadMethod(pkg, m) -- if err != nil { -- return nil, nil, fmt.Errorf("loading %s: %v", m.Name(), err) +- // -json flag +- { +- res := gopls(t, tree, "version", "-json") +- res.checkExit(true) +- var v debug.ServerVersion +- if res.toJSON(&v) { +- if v.Version != want { +- t.Errorf("expected Version %q, got %q (%v)", want, v.Version, res) +- } - } -- commands = append(commands, c) - } -- return pkg, commands, nil -} - --// fieldLoader loads field information, memoizing results to prevent infinite --// recursion. --type fieldLoader struct { -- loaded map[types.Object]*Field --} +-// TestCheck tests the 'check' subcommand (../check.go). +-func TestCheck(t *testing.T) { +- t.Parallel() - --var universeError = types.Universe.Lookup("error").Type() +- tree := writeTree(t, ` +--- go.mod -- +-module example.com +-go 1.18 - --func (l *fieldLoader) loadMethod(pkg *packages.Package, m *types.Func) (*Command, error) { -- node, err := findField(pkg, m.Pos()) -- if err != nil { -- return nil, err -- } -- title, doc := splitDoc(node.Doc.Text()) -- c := &Command{ -- MethodName: m.Name(), -- Name: lspName(m.Name()), -- Doc: doc, -- Title: title, -- } -- sig := m.Type().Underlying().(*types.Signature) -- rlen := sig.Results().Len() -- if rlen > 2 || rlen == 0 { -- return nil, fmt.Errorf("must have 1 or 2 returns, got %d", rlen) -- } -- finalResult := sig.Results().At(rlen - 1) -- if !types.Identical(finalResult.Type(), universeError) { -- return nil, fmt.Errorf("final return must be error") -- } -- if rlen == 2 { -- obj := sig.Results().At(0) -- c.Result, err = l.loadField(pkg, obj, "", "") -- if err != nil { -- return nil, err -- } -- } -- for i := 0; i < sig.Params().Len(); i++ { -- obj := sig.Params().At(i) -- fld, err := l.loadField(pkg, obj, "", "") -- if err != nil { -- return nil, err -- } -- if i == 0 { -- // Lazy check that the first argument is a context. We could relax this, -- // but then the generated code gets more complicated. -- if named, ok := fld.Type.(*types.Named); !ok || named.Obj().Name() != "Context" || named.Obj().Pkg().Path() != "context" { -- return nil, fmt.Errorf("first method parameter must be context.Context") -- } -- // Skip the context argument, as it is implied. -- continue +--- a.go -- +-package a +-import "fmt" +-var _ = fmt.Sprintf("%s", 123) +- +--- b.go -- +-package a +-import "fmt" +-var _ = fmt.Sprintf("%d", "123") +-`) +- +- // no files +- { +- res := gopls(t, tree, "check") +- res.checkExit(true) +- if res.stdout != "" { +- t.Errorf("unexpected output: %v", res) - } -- c.Args = append(c.Args, fld) - } -- return c, nil --} - --func (l *fieldLoader) loadField(pkg *packages.Package, obj *types.Var, doc, tag string) (*Field, error) { -- if existing, ok := l.loaded[obj]; ok { -- return existing, nil -- } -- fld := &Field{ -- Name: obj.Name(), -- Doc: strings.TrimSpace(doc), -- Type: obj.Type(), -- JSONTag: reflect.StructTag(tag).Get("json"), -- } -- under := fld.Type.Underlying() -- // Quick-and-dirty handling for various underlying types. -- switch p := under.(type) { -- case *types.Pointer: -- under = p.Elem().Underlying() -- case *types.Array: -- under = p.Elem().Underlying() -- fld.FieldMod = fmt.Sprintf("[%d]", p.Len()) -- case *types.Slice: -- under = p.Elem().Underlying() -- fld.FieldMod = "[]" +- // one file +- { +- res := gopls(t, tree, "check", "./a.go") +- res.checkExit(true) +- res.checkStdout("fmt.Sprintf format %s has arg 123 of wrong type int") - } - -- if s, ok := under.(*types.Struct); ok { -- for i := 0; i < s.NumFields(); i++ { -- obj2 := s.Field(i) -- pkg2 := pkg -- if obj2.Pkg() != pkg2.Types { -- pkg2, ok = pkg.Imports[obj2.Pkg().Path()] -- if !ok { -- return nil, fmt.Errorf("missing import for %q: %q", pkg.ID, obj2.Pkg().Path()) -- } -- } -- node, err := findField(pkg2, obj2.Pos()) -- if err != nil { -- return nil, err -- } -- tag := s.Tag(i) -- structField, err := l.loadField(pkg2, obj2, node.Doc.Text(), tag) -- if err != nil { -- return nil, err -- } -- fld.Fields = append(fld.Fields, structField) -- } +- // two files +- { +- res := gopls(t, tree, "check", "./a.go", "./b.go") +- res.checkExit(true) +- res.checkStdout(`a.go:.* fmt.Sprintf format %s has arg 123 of wrong type int`) +- res.checkStdout(`b.go:.* fmt.Sprintf format %d has arg "123" of wrong type string`) - } -- return fld, nil -} - --// splitDoc parses a command doc string to separate the title from normal --// documentation. --// --// The doc comment should be of the form: "MethodName: Title\nDocumentation" --func splitDoc(text string) (title, doc string) { -- docParts := strings.SplitN(text, "\n", 2) -- titleParts := strings.SplitN(docParts[0], ":", 2) -- if len(titleParts) > 1 { -- title = strings.TrimSpace(titleParts[1]) +-// TestCallHierarchy tests the 'call_hierarchy' subcommand (../call_hierarchy.go). +-func TestCallHierarchy(t *testing.T) { +- t.Parallel() +- +- tree := writeTree(t, ` +--- go.mod -- +-module example.com +-go 1.18 +- +--- a.go -- +-package a +-func f() {} +-func g() { +- f() +-} +-func h() { +- f() +- f() +-} +-`) +- // missing position +- { +- res := gopls(t, tree, "call_hierarchy") +- res.checkExit(false) +- res.checkStderr("expects 1 argument") - } -- if len(docParts) > 1 { -- doc = strings.TrimSpace(docParts[1]) +- // wrong place +- { +- res := gopls(t, tree, "call_hierarchy", "a.go:1") +- res.checkExit(false) +- res.checkStderr("identifier not found") +- } +- // f is called once from g and twice from h. +- { +- res := gopls(t, tree, "call_hierarchy", "a.go:2:6") +- res.checkExit(true) +- // We use regexp '.' as an OS-agnostic path separator. +- res.checkStdout("ranges 7:2-3, 8:2-3 in ..a.go from/to function h in ..a.go:6:6-7") +- res.checkStdout("ranges 4:2-3 in ..a.go from/to function g in ..a.go:3:6-7") +- res.checkStdout("identifier: function f in ..a.go:2:6-7") - } -- return title, doc -} - --// lspName returns the normalized command name to use in the LSP. --func lspName(methodName string) string { -- words := splitCamel(methodName) -- for i := range words { -- words[i] = strings.ToLower(words[i]) -- } -- return strings.Join(words, "_") --} +-// TestCodeLens tests the 'codelens' subcommand (../codelens.go). +-func TestCodeLens(t *testing.T) { +- t.Parallel() - --// splitCamel splits s into words, according to camel-case word boundaries. --// Initialisms are grouped as a single word. --// --// For example: --// --// "RunTests" -> []string{"Run", "Tests"} --// "GCDetails" -> []string{"GC", "Details"} --func splitCamel(s string) []string { -- var words []string -- for len(s) > 0 { -- last := strings.LastIndexFunc(s, unicode.IsUpper) -- if last < 0 { -- last = 0 -- } -- if last == len(s)-1 { -- // Group initialisms as a single word. -- last = 1 + strings.LastIndexFunc(s[:last], func(r rune) bool { return !unicode.IsUpper(r) }) -- } -- words = append(words, s[last:]) -- s = s[:last] +- tree := writeTree(t, ` +--- go.mod -- +-module example.com +-go 1.18 +- +--- a/a.go -- +-package a +--- a/a_test.go -- +-package a_test +-import "testing" +-func TestPass(t *testing.T) {} +-func TestFail(t *testing.T) { t.Fatal("fail") } +-`) +- // missing position +- { +- res := gopls(t, tree, "codelens") +- res.checkExit(false) +- res.checkStderr("requires a file name") - } -- for i := 0; i < len(words)/2; i++ { -- j := len(words) - i - 1 -- words[i], words[j] = words[j], words[i] +- // list code lenses +- { +- res := gopls(t, tree, "codelens", "./a/a_test.go") +- res.checkExit(true) +- res.checkStdout(`a_test.go:3: "run test" \[gopls.test\]`) +- res.checkStdout(`a_test.go:4: "run test" \[gopls.test\]`) - } -- return words --} -- --// findField finds the struct field or interface method positioned at pos, --// within the AST. --func findField(pkg *packages.Package, pos token.Pos) (*ast.Field, error) { -- fset := pkg.Fset -- var file *ast.File -- for _, f := range pkg.Syntax { -- if fset.File(f.Pos()).Name() == fset.File(pos).Name() { -- file = f -- break -- } +- // no codelens with title/position +- { +- res := gopls(t, tree, "codelens", "-exec", "./a/a_test.go:1", "nope") +- res.checkExit(false) +- res.checkStderr(`no code lens at .* with title "nope"`) - } -- if file == nil { -- return nil, fmt.Errorf("no file for pos %v", pos) +- // run the passing test +- { +- res := gopls(t, tree, "codelens", "-exec", "./a/a_test.go:3", "run test") +- res.checkExit(true) +- res.checkStderr(`PASS: TestPass`) // from go test +- res.checkStderr("Info: all tests passed") // from gopls.test +- } +- // run the failing test +- { +- res := gopls(t, tree, "codelens", "-exec", "./a/a_test.go:4", "run test") +- res.checkExit(false) +- res.checkStderr(`FAIL example.com/a`) +- res.checkStderr("Info: 1 / 1 tests failed") - } -- path, _ := astutil.PathEnclosingInterval(file, pos, pos) -- // This is fragile, but in the cases we care about, the field will be in -- // path[1]. -- return path[1].(*ast.Field), nil -} -diff -urN a/gopls/internal/lsp/command/gen/gen.go b/gopls/internal/lsp/command/gen/gen.go ---- a/gopls/internal/lsp/command/gen/gen.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/command/gen/gen.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,155 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --// Package gen is used to generate command bindings from the gopls command --// interface. --package gen +-// TestDefinition tests the 'definition' subcommand (../definition.go). +-func TestDefinition(t *testing.T) { +- t.Parallel() - --import ( -- "bytes" -- "fmt" -- "go/types" -- "text/template" +- tree := writeTree(t, ` +--- go.mod -- +-module example.com +-go 1.18 - -- "golang.org/x/tools/gopls/internal/lsp/command/commandmeta" -- "golang.org/x/tools/internal/imports" --) +--- a.go -- +-package a +-import "fmt" +-func f() { +- fmt.Println() +-} +-func g() { +- f() +-} +-`) +- // missing position +- { +- res := gopls(t, tree, "definition") +- res.checkExit(false) +- res.checkStderr("expects 1 argument") +- } +- // intra-package +- { +- res := gopls(t, tree, "definition", "a.go:7:2") // "f()" +- res.checkExit(true) +- res.checkStdout("a.go:3:6-7: defined here as func f") +- } +- // cross-package +- { +- res := gopls(t, tree, "definition", "a.go:4:7") // "Println" +- res.checkExit(true) +- res.checkStdout("print.go.* defined here as func fmt.Println") +- res.checkStdout("Println formats using the default formats for its operands") +- } +- // -json and -markdown +- { +- res := gopls(t, tree, "definition", "-json", "-markdown", "a.go:4:7") +- res.checkExit(true) +- var defn cmd.Definition +- if res.toJSON(&defn) { +- if !strings.HasPrefix(defn.Description, "```go\nfunc fmt.Println") { +- t.Errorf("Description does not start with markdown code block. Got: %s", defn.Description) +- } +- } +- } +-} - --const src = `// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-// TestExecute tests the 'execute' subcommand (../execute.go). +-func TestExecute(t *testing.T) { +- t.Parallel() - --// Don't include this file during code generation, or it will break the build --// if existing interface methods have been modified. --//go:build !generate --// +build !generate +- tree := writeTree(t, ` +--- go.mod -- +-module example.com +-go 1.18 - --// Code generated by generate.go. DO NOT EDIT. +--- hello.go -- +-package a +-func main() {} - --package command +--- hello_test.go -- +-package a +-import "testing" +-func TestHello(t *testing.T) { +- t.Fatal("oops") +-} +-`) +- // missing command name +- { +- res := gopls(t, tree, "execute") +- res.checkExit(false) +- res.checkStderr("requires a command") +- } +- // bad command +- { +- res := gopls(t, tree, "execute", "gopls.foo") +- res.checkExit(false) +- res.checkStderr("unrecognized command: gopls.foo") +- } +- // too few arguments +- { +- res := gopls(t, tree, "execute", "gopls.run_tests") +- res.checkExit(false) +- res.checkStderr("expected 1 input arguments, got 0") +- } +- // too many arguments +- { +- res := gopls(t, tree, "execute", "gopls.run_tests", "null", "null") +- res.checkExit(false) +- res.checkStderr("expected 1 input arguments, got 2") +- } +- // argument is not JSON +- { +- res := gopls(t, tree, "execute", "gopls.run_tests", "hello") +- res.checkExit(false) +- res.checkStderr("argument 1 is not valid JSON: invalid character 'h'") +- } +- // add import, show diff +- hello := "file://" + filepath.ToSlash(tree) + "/hello.go" +- { +- res := gopls(t, tree, "execute", "-d", "gopls.add_import", `{"ImportPath": "fmt", "URI": "`+hello+`"}`) +- res.checkExit(true) +- res.checkStdout(`[+]import "fmt"`) +- } +- // list known packages (has a result) +- { +- res := gopls(t, tree, "execute", "gopls.list_known_packages", `{"URI": "`+hello+`"}`) +- res.checkExit(true) +- res.checkStdout(`"fmt"`) +- res.checkStdout(`"encoding/json"`) +- } +- // run tests +- { +- helloTest := "file://" + filepath.ToSlash(tree) + "/hello_test.go" +- res := gopls(t, tree, "execute", "gopls.run_tests", `{"URI": "`+helloTest+`", "Tests": ["TestHello"]}`) +- res.checkExit(false) +- res.checkStderr(`hello_test.go:4: oops`) +- res.checkStderr(`1 / 1 tests failed`) +- } +-} - --import ( -- {{range $k, $v := .Imports -}} -- "{{$k}}" -- {{end}} --) +-// TestFoldingRanges tests the 'folding_ranges' subcommand (../folding_range.go). +-func TestFoldingRanges(t *testing.T) { +- t.Parallel() - --const ( --{{- range .Commands}} -- {{.MethodName}} Command = "{{.Name}}" --{{- end}} --) +- tree := writeTree(t, ` +--- go.mod -- +-module example.com +-go 1.18 - --var Commands = []Command { --{{- range .Commands}} -- {{.MethodName}}, --{{- end}} +--- a.go -- +-package a +-func f(x int) { +- // hello -} -- --func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Interface) (interface{}, error) { -- switch params.Command { -- {{- range .Commands}} -- case "{{.ID}}": -- {{- if .Args -}} -- {{- range $i, $v := .Args}} -- var a{{$i}} {{typeString $v.Type}} -- {{- end}} -- if err := UnmarshalArgs(params.Arguments{{range $i, $v := .Args}}, &a{{$i}}{{end}}); err != nil { -- return nil, err -- } -- {{end -}} -- return {{if not .Result}}nil, {{end}}s.{{.MethodName}}(ctx{{range $i, $v := .Args}}, a{{$i}}{{end}}) -- {{- end}} +-`) +- // missing filename +- { +- res := gopls(t, tree, "folding_ranges") +- res.checkExit(false) +- res.checkStderr("expects 1 argument") - } -- return nil, fmt.Errorf("unsupported command %q", params.Command) --} --{{- range .Commands}} -- --func New{{.MethodName}}Command(title string, {{range $i, $v := .Args}}{{if $i}}, {{end}}a{{$i}} {{typeString $v.Type}}{{end}}) (protocol.Command, error) { -- args, err := MarshalArgs({{range $i, $v := .Args}}{{if $i}}, {{end}}a{{$i}}{{end}}) -- if err != nil { -- return protocol.Command{}, err +- // success +- { +- res := gopls(t, tree, "folding_ranges", "a.go") +- res.checkExit(true) +- res.checkStdout("2:8-2:13") // params (x int) +- res.checkStdout("2:16-4:1") // body { ... } - } -- return protocol.Command{ -- Title: title, -- Command: "{{.ID}}", -- Arguments: args, -- }, nil -} --{{end}} --` - --type data struct { -- Imports map[string]bool -- Commands []*commandmeta.Command --} +-// TestFormat tests the 'format' subcommand (../format.go). +-func TestFormat(t *testing.T) { +- t.Parallel() - --func Generate() ([]byte, error) { -- pkg, cmds, err := commandmeta.Load() -- if err != nil { -- return nil, fmt.Errorf("loading command data: %v", err) +- tree := writeTree(t, ` +--- a.go -- +-package a ; func f ( ) { } +-`) +- const want = `package a +- +-func f() {} +-` +- +- // no files => nop +- { +- res := gopls(t, tree, "format") +- res.checkExit(true) - } -- qf := func(p *types.Package) string { -- if p == pkg.Types { -- return "" +- // default => print formatted result +- { +- res := gopls(t, tree, "format", "a.go") +- res.checkExit(true) +- if res.stdout != want { +- t.Errorf("format: got <<%s>>, want <<%s>>", res.stdout, want) - } -- return p.Name() - } -- tmpl, err := template.New("").Funcs(template.FuncMap{ -- "typeString": func(t types.Type) string { -- return types.TypeString(t, qf) -- }, -- }).Parse(src) -- if err != nil { -- return nil, err +- // start/end position not supported (unless equal to start/end of file) +- { +- res := gopls(t, tree, "format", "a.go:1-2") +- res.checkExit(false) +- res.checkStderr("only full file formatting supported") - } -- d := data{ -- Commands: cmds, -- Imports: map[string]bool{ -- "context": true, -- "fmt": true, -- "golang.org/x/tools/gopls/internal/lsp/protocol": true, -- }, +- // -list: show only file names +- { +- res := gopls(t, tree, "format", "-list", "a.go") +- res.checkExit(true) +- res.checkStdout("a.go") - } -- const thispkg = "golang.org/x/tools/gopls/internal/lsp/command" -- for _, c := range d.Commands { -- for _, arg := range c.Args { -- pth := pkgPath(arg.Type) -- if pth != "" && pth != thispkg { -- d.Imports[pth] = true -- } -- } -- if c.Result != nil { -- pth := pkgPath(c.Result.Type) -- if pth != "" && pth != thispkg { -- d.Imports[pth] = true -- } -- } +- // -diff prints a unified diff +- { +- res := gopls(t, tree, "format", "-diff", "a.go") +- res.checkExit(true) +- // We omit the filenames as they vary by OS. +- want := ` +--package a ; func f ( ) { } +-+package a +-+ +-+func f() {} +-` +- res.checkStdout(regexp.QuoteMeta(want)) - } -- -- var buf bytes.Buffer -- if err := tmpl.Execute(&buf, d); err != nil { -- return nil, fmt.Errorf("executing: %v", err) +- // -write updates the file +- { +- res := gopls(t, tree, "format", "-write", "a.go") +- res.checkExit(true) +- res.checkStdout("^$") // empty +- checkContent(t, filepath.Join(tree, "a.go"), want) - } +-} - -- opts := &imports.Options{ -- AllErrors: true, -- FormatOnly: true, -- Comments: true, -- } -- content, err := imports.Process("", buf.Bytes(), opts) -- if err != nil { -- return nil, fmt.Errorf("goimports: %v", err) -- } -- return content, nil +-// TestHighlight tests the 'highlight' subcommand (../highlight.go). +-func TestHighlight(t *testing.T) { +- t.Parallel() +- +- tree := writeTree(t, ` +--- a.go -- +-package a +-import "fmt" +-func f() { +- fmt.Println() +- fmt.Println() -} +-`) - --func pkgPath(t types.Type) string { -- if n, ok := t.(*types.Named); ok { -- if pkg := n.Obj().Pkg(); pkg != nil { -- return pkg.Path() -- } +- // no arguments +- { +- res := gopls(t, tree, "highlight") +- res.checkExit(false) +- res.checkStderr("expects 1 argument") +- } +- // all occurrences of Println +- { +- res := gopls(t, tree, "highlight", "a.go:4:7") +- res.checkExit(true) +- res.checkStdout("a.go:4:6-13") +- res.checkStdout("a.go:5:6-13") - } -- return "" -} -diff -urN a/gopls/internal/lsp/command/generate.go b/gopls/internal/lsp/command/generate.go ---- a/gopls/internal/lsp/command/generate.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/command/generate.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,25 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --//go:build ignore --// +build ignore - --package main -- --import ( -- "log" -- "os" +-// TestImplementations tests the 'implementation' subcommand (../implementation.go). +-func TestImplementations(t *testing.T) { +- t.Parallel() - -- "golang.org/x/tools/gopls/internal/lsp/command/gen" --) +- tree := writeTree(t, ` +--- a.go -- +-package a +-import "fmt" +-type T int +-func (T) String() string { return "" } +-`) - --func main() { -- content, err := gen.Generate() -- if err != nil { -- log.Fatal(err) +- // no arguments +- { +- res := gopls(t, tree, "implementation") +- res.checkExit(false) +- res.checkStderr("expects 1 argument") - } -- if err := os.WriteFile("command_gen.go", content, 0644); err != nil { -- log.Fatal(err) +- // T.String +- { +- res := gopls(t, tree, "implementation", "a.go:4:10") +- res.checkExit(true) +- // TODO(adonovan): extract and check the content of the reported ranges? +- // We use regexp '.' as an OS-agnostic path separator. +- res.checkStdout("fmt.print.go:") // fmt.Stringer.String +- res.checkStdout("runtime.error.go:") // runtime.stringer.String - } -} -diff -urN a/gopls/internal/lsp/command/interface.go b/gopls/internal/lsp/command/interface.go ---- a/gopls/internal/lsp/command/interface.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/command/interface.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,532 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --//go:generate go run -tags=generate generate.go +-// TestImports tests the 'imports' subcommand (../imports.go). +-func TestImports(t *testing.T) { +- t.Parallel() - --// Package command defines the interface provided by gopls for the --// workspace/executeCommand LSP request. --// --// This interface is fully specified by the Interface type, provided it --// conforms to the restrictions outlined in its doc string. --// --// Bindings for server-side command dispatch and client-side serialization are --// also provided by this package, via code generation. --package command +- tree := writeTree(t, ` +--- a.go -- +-package a +-func _() { +- fmt.Println() +-} +-`) - --import ( -- "context" +- want := ` +-package a - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/vulncheck" --) +-import "fmt" +-func _() { +- fmt.Println() +-} +-`[1:] - --// Interface defines the interface gopls exposes for the --// workspace/executeCommand request. --// --// This interface is used to generate marshaling/unmarshaling code, dispatch, --// and documentation, and so has some additional restrictions: --// 1. All method arguments must be JSON serializable. --// 2. Methods must return either error or (T, error), where T is a --// JSON serializable type. --// 3. The first line of the doc string is special. Everything after the colon --// is considered the command 'Title'. --// TODO(rFindley): reconsider this -- Title may be unnecessary. --type Interface interface { -- // ApplyFix: Apply a fix -- // -- // Applies a fix to a region of source code. -- ApplyFix(context.Context, ApplyFixArgs) error +- // no arguments +- { +- res := gopls(t, tree, "imports") +- res.checkExit(false) +- res.checkStderr("expects 1 argument") +- } +- // default: print with imports +- { +- res := gopls(t, tree, "imports", "a.go") +- res.checkExit(true) +- if res.stdout != want { +- t.Errorf("imports: got <<%s>>, want <<%s>>", res.stdout, want) +- } +- } +- // -diff: show a unified diff +- { +- res := gopls(t, tree, "imports", "-diff", "a.go") +- res.checkExit(true) +- res.checkStdout(regexp.QuoteMeta(`+import "fmt"`)) +- } +- // -write: update file +- { +- res := gopls(t, tree, "imports", "-write", "a.go") +- res.checkExit(true) +- checkContent(t, filepath.Join(tree, "a.go"), want) +- } +-} - -- // Test: Run test(s) (legacy) -- // -- // Runs `go test` for a specific set of test or benchmark functions. -- Test(context.Context, protocol.DocumentURI, []string, []string) error +-// TestLinks tests the 'links' subcommand (../links.go). +-func TestLinks(t *testing.T) { +- t.Parallel() - -- // TODO: deprecate Test in favor of RunTests below. +- tree := writeTree(t, ` +--- a.go -- +-// Link in package doc: https://pkg.go.dev/ +-package a - -- // Test: Run test(s) -- // -- // Runs `go test` for a specific set of test or benchmark functions. -- RunTests(context.Context, RunTestsArgs) error +-// Link in internal comment: https://go.dev/cl - -- // Generate: Run go generate -- // -- // Runs `go generate` for a given directory. -- Generate(context.Context, GenerateArgs) error +-// Doc comment link: https://blog.go.dev/ +-func f() {} +-`) +- // no arguments +- { +- res := gopls(t, tree, "links") +- res.checkExit(false) +- res.checkStderr("expects 1 argument") +- } +- // success +- { +- res := gopls(t, tree, "links", "a.go") +- res.checkExit(true) +- res.checkStdout("https://go.dev/cl") +- res.checkStdout("https://pkg.go.dev") +- res.checkStdout("https://blog.go.dev/") +- } +- // -json +- { +- res := gopls(t, tree, "links", "-json", "a.go") +- res.checkExit(true) +- res.checkStdout("https://pkg.go.dev") +- res.checkStdout("https://go.dev/cl") +- res.checkStdout("https://blog.go.dev/") // at 5:21-5:41 +- var links []protocol.DocumentLink +- if res.toJSON(&links) { +- // Check just one of the three locations. +- if got, want := fmt.Sprint(links[2].Range), "5:21-5:41"; got != want { +- t.Errorf("wrong link location: got %v, want %v", got, want) +- } +- } +- } +-} - -- // RegenerateCgo: Regenerate cgo -- // -- // Regenerates cgo definitions. -- RegenerateCgo(context.Context, URIArg) error +-// TestReferences tests the 'references' subcommand (../references.go). +-func TestReferences(t *testing.T) { +- t.Parallel() - -- // Tidy: Run go mod tidy -- // -- // Runs `go mod tidy` for a module. -- Tidy(context.Context, URIArgs) error +- tree := writeTree(t, ` +--- go.mod -- +-module example.com +-go 1.18 - -- // Vendor: Run go mod vendor -- // -- // Runs `go mod vendor` for a module. -- Vendor(context.Context, URIArg) error +--- a.go -- +-package a +-import "fmt" +-func f() { +- fmt.Println() +-} - -- // EditGoDirective: Run go mod edit -go=version -- // -- // Runs `go mod edit -go=version` for a module. -- EditGoDirective(context.Context, EditGoDirectiveArgs) error +--- b.go -- +-package a +-import "fmt" +-func g() { +- fmt.Println() +-} +-`) +- // no arguments +- { +- res := gopls(t, tree, "references") +- res.checkExit(false) +- res.checkStderr("expects 1 argument") +- } +- // fmt.Println +- { +- res := gopls(t, tree, "references", "a.go:4:10") +- res.checkExit(true) +- res.checkStdout("a.go:4:6-13") +- res.checkStdout("b.go:4:6-13") +- } +-} - -- // UpdateGoSum: Update go.sum -- // -- // Updates the go.sum file for a module. -- UpdateGoSum(context.Context, URIArgs) error +-// TestSignature tests the 'signature' subcommand (../signature.go). +-func TestSignature(t *testing.T) { +- t.Parallel() - -- // CheckUpgrades: Check for upgrades -- // -- // Checks for module upgrades. -- CheckUpgrades(context.Context, CheckUpgradesArgs) error +- tree := writeTree(t, ` +--- go.mod -- +-module example.com +-go 1.18 - -- // AddDependency: Add a dependency -- // -- // Adds a dependency to the go.mod file for a module. -- AddDependency(context.Context, DependencyArgs) error +--- a.go -- +-package a +-import "fmt" +-func f() { +- fmt.Println(123) +-} +-`) +- // no arguments +- { +- res := gopls(t, tree, "signature") +- res.checkExit(false) +- res.checkStderr("expects 1 argument") +- } +- // at 123 inside fmt.Println() call +- { +- res := gopls(t, tree, "signature", "a.go:4:15") +- res.checkExit(true) +- res.checkStdout("Println\\(a ...") +- res.checkStdout("Println formats using the default formats...") +- } +-} - -- // UpgradeDependency: Upgrade a dependency -- // -- // Upgrades a dependency in the go.mod file for a module. -- UpgradeDependency(context.Context, DependencyArgs) error +-// TestPrepareRename tests the 'prepare_rename' subcommand (../prepare_rename.go). +-func TestPrepareRename(t *testing.T) { +- t.Parallel() - -- // RemoveDependency: Remove a dependency -- // -- // Removes a dependency from the go.mod file of a module. -- RemoveDependency(context.Context, RemoveDependencyArgs) error +- tree := writeTree(t, ` +--- go.mod -- +-module example.com +-go 1.18 - -- // ResetGoModDiagnostics: Reset go.mod diagnostics -- // -- // Reset diagnostics in the go.mod file of a module. -- ResetGoModDiagnostics(context.Context, ResetGoModDiagnosticsArgs) error +--- a.go -- +-package a +-func oldname() {} +-`) +- // no arguments +- { +- res := gopls(t, tree, "prepare_rename") +- res.checkExit(false) +- res.checkStderr("expects 1 argument") +- } +- // in 'package' keyword +- { +- res := gopls(t, tree, "prepare_rename", "a.go:1:3") +- res.checkExit(false) +- res.checkStderr("request is not valid at the given position") +- } +- // in 'package' identifier (not supported by client) +- { +- res := gopls(t, tree, "prepare_rename", "a.go:1:9") +- res.checkExit(false) +- res.checkStderr("can't rename package") +- } +- // in func oldname +- { +- res := gopls(t, tree, "prepare_rename", "a.go:2:9") +- res.checkExit(true) +- res.checkStdout("a.go:2:6-13") // all of "oldname" +- } +-} - -- // GoGetPackage: go get a package -- // -- // Runs `go get` to fetch a package. -- GoGetPackage(context.Context, GoGetPackageArgs) error +-// TestRename tests the 'rename' subcommand (../rename.go). +-func TestRename(t *testing.T) { +- t.Parallel() - -- // GCDetails: Toggle gc_details -- // -- // Toggle the calculation of gc annotations. -- GCDetails(context.Context, protocol.DocumentURI) error +- tree := writeTree(t, ` +--- go.mod -- +-module example.com +-go 1.18 - -- // TODO: deprecate GCDetails in favor of ToggleGCDetails below. +--- a.go -- +-package a +-func oldname() {} +-`) +- // no arguments +- { +- res := gopls(t, tree, "rename") +- res.checkExit(false) +- res.checkStderr("expects 2 arguments") +- } +- // missing newname +- { +- res := gopls(t, tree, "rename", "a.go:1:3") +- res.checkExit(false) +- res.checkStderr("expects 2 arguments") +- } +- // in 'package' keyword +- { +- res := gopls(t, tree, "rename", "a.go:1:3", "newname") +- res.checkExit(false) +- res.checkStderr("no identifier found") +- } +- // in 'package' identifier +- { +- res := gopls(t, tree, "rename", "a.go:1:9", "newname") +- res.checkExit(false) +- res.checkStderr(`cannot rename package: module path .* same as the package path, so .* no effect`) +- } +- // success, func oldname (and -diff) +- { +- res := gopls(t, tree, "rename", "-diff", "a.go:2:9", "newname") +- res.checkExit(true) +- res.checkStdout(regexp.QuoteMeta("-func oldname() {}")) +- res.checkStdout(regexp.QuoteMeta("+func newname() {}")) +- } +-} - -- // ToggleGCDetails: Toggle gc_details -- // -- // Toggle the calculation of gc annotations. -- ToggleGCDetails(context.Context, URIArg) error +-// TestSymbols tests the 'symbols' subcommand (../symbols.go). +-func TestSymbols(t *testing.T) { +- t.Parallel() - -- // ListKnownPackages: List known packages -- // -- // Retrieve a list of packages that are importable from the given URI. -- ListKnownPackages(context.Context, URIArg) (ListKnownPackagesResult, error) +- tree := writeTree(t, ` +--- go.mod -- +-module example.com +-go 1.18 - -- // ListImports: List imports of a file and its package -- // -- // Retrieve a list of imports in the given Go file, and the package it -- // belongs to. -- ListImports(context.Context, URIArg) (ListImportsResult, error) +--- a.go -- +-package a +-func f() +-var v int +-const c = 0 +-`) +- // no files +- { +- res := gopls(t, tree, "symbols") +- res.checkExit(false) +- res.checkStderr("expects 1 argument") +- } +- // success +- { +- res := gopls(t, tree, "symbols", "a.go:123:456") // (line/col ignored) +- res.checkExit(true) +- res.checkStdout("f Function 2:6-2:7") +- res.checkStdout("v Variable 3:5-3:6") +- res.checkStdout("c Constant 4:7-4:8") +- } +-} - -- // AddImport: Add an import -- // -- // Ask the server to add an import path to a given Go file. The method will -- // call applyEdit on the client so that clients don't have to apply the edit -- // themselves. -- AddImport(context.Context, AddImportArgs) error +-// TestSemtok tests the 'semtok' subcommand (../semantictokens.go). +-func TestSemtok(t *testing.T) { +- t.Parallel() - -- // StartDebugging: Start the gopls debug server -- // -- // Start the gopls debug server if it isn't running, and return the debug -- // address. -- StartDebugging(context.Context, DebuggingArgs) (DebuggingResult, error) +- tree := writeTree(t, ` +--- go.mod -- +-module example.com +-go 1.18 - -- // StartProfile: start capturing a profile of gopls' execution. -- // -- // Start a new pprof profile. Before using the resulting file, profiling must -- // be stopped with a corresponding call to StopProfile. -- // -- // This command is intended for internal use only, by the gopls benchmark -- // runner. -- StartProfile(context.Context, StartProfileArgs) (StartProfileResult, error) +--- a.go -- +-package a +-func f() +-var v int +-const c = 0 +-`) +- // no files +- { +- res := gopls(t, tree, "semtok") +- res.checkExit(false) +- res.checkStderr("expected one file name") +- } +- // success +- { +- res := gopls(t, tree, "semtok", "a.go") +- res.checkExit(true) +- got := res.stdout +- want := ` +-/*⇒7,keyword,[]*/package /*⇒1,namespace,[]*/a +-/*⇒4,keyword,[]*/func /*⇒1,function,[definition]*/f() +-/*⇒3,keyword,[]*/var /*⇒1,variable,[definition]*/v /*⇒3,type,[defaultLibrary]*/int +-/*⇒5,keyword,[]*/const /*⇒1,variable,[definition readonly]*/c = /*⇒1,number,[]*/0 +-`[1:] +- if got != want { +- t.Errorf("semtok: got <<%s>>, want <<%s>>", got, want) +- } +- } +-} - -- // StopProfile: stop an ongoing profile. -- // -- // This command is intended for internal use only, by the gopls benchmark -- // runner. -- StopProfile(context.Context, StopProfileArgs) (StopProfileResult, error) +-func TestStats(t *testing.T) { +- t.Parallel() - -- // RunGovulncheck: Run vulncheck. -- // -- // Run vulnerability check (`govulncheck`). -- RunGovulncheck(context.Context, VulncheckArgs) (RunVulncheckResult, error) +- tree := writeTree(t, ` +--- go.mod -- +-module example.com +-go 1.18 - -- // FetchVulncheckResult: Get known vulncheck result -- // -- // Fetch the result of latest vulnerability check (`govulncheck`). -- FetchVulncheckResult(context.Context, URIArg) (map[protocol.DocumentURI]*vulncheck.Result, error) +--- a.go -- +-package a +--- b/b.go -- +-package b +--- testdata/foo.go -- +-package foo +-`) - -- // MemStats: fetch memory statistics -- // -- // Call runtime.GC multiple times and return memory statistics as reported by -- // runtime.MemStats. -- // -- // This command is used for benchmarking, and may change in the future. -- MemStats(context.Context) (MemStatsResult, error) +- // Trigger a bug report with a distinctive string +- // and check that it was durably recorded. +- oops := fmt.Sprintf("oops-%d", rand.Int()) +- { +- env := []string{"TEST_GOPLS_BUG=" + oops} +- res := goplsWithEnv(t, tree, env, "bug") +- res.checkExit(true) +- } - -- // WorkspaceStats: fetch workspace statistics -- // -- // Query statistics about workspace builds, modules, packages, and files. -- // -- // This command is intended for internal use only, by the gopls stats -- // command. -- WorkspaceStats(context.Context) (WorkspaceStatsResult, error) +- res := gopls(t, tree, "stats") +- res.checkExit(true) - -- // RunGoWorkCommand: run `go work [args...]`, and apply the resulting go.work -- // edits to the current go.work file. -- RunGoWorkCommand(context.Context, RunGoWorkArgs) error +- var stats cmd.GoplsStats +- if err := json.Unmarshal([]byte(res.stdout), &stats); err != nil { +- t.Fatalf("failed to unmarshal JSON output of stats command: %v", err) +- } - -- // AddTelemetryCounters: update the given telemetry counters. -- // -- // Gopls will prepend "fwd/" to all the counters updated using this command -- // to avoid conflicts with other counters gopls collects. -- AddTelemetryCounters(context.Context, AddTelemetryCountersArgs) error +- // a few sanity checks +- checks := []struct { +- field string +- got int +- want int +- }{ +- { +- "WorkspaceStats.Views[0].WorkspaceModules", +- stats.WorkspaceStats.Views[0].WorkspacePackages.Modules, +- 1, +- }, +- { +- "WorkspaceStats.Views[0].WorkspacePackages", +- stats.WorkspaceStats.Views[0].WorkspacePackages.Packages, +- 2, +- }, +- {"DirStats.Files", stats.DirStats.Files, 4}, +- {"DirStats.GoFiles", stats.DirStats.GoFiles, 2}, +- {"DirStats.ModFiles", stats.DirStats.ModFiles, 1}, +- {"DirStats.TestdataFiles", stats.DirStats.TestdataFiles, 1}, +- } +- for _, check := range checks { +- if check.got != check.want { +- t.Errorf("stats.%s = %d, want %d", check.field, check.got, check.want) +- } +- } - -- // MaybePromptForTelemetry: checks for the right conditions, and then prompts -- // the user to ask if they want to enable Go telemetry uploading. If the user -- // responds 'Yes', the telemetry mode is set to "on". -- MaybePromptForTelemetry(context.Context) error +- // Check that we got a BugReport with the expected message. +- { +- got := fmt.Sprint(stats.BugReports) +- wants := []string{ +- "cmd/info.go", // File containing call to bug.Report +- oops, // Description +- } +- for _, want := range wants { +- if !strings.Contains(got, want) { +- t.Errorf("BugReports does not contain %q. Got:<<%s>>", want, got) +- break +- } +- } +- } - -- // ChangeSignature: performs a "change signature" refactoring. -- // -- // This command is experimental, currently only supporting parameter removal. -- // Its signature will certainly change in the future (pun intended). -- ChangeSignature(context.Context, ChangeSignatureArgs) error --} +- // Check that -anon suppresses fields containing user information. +- { +- res2 := gopls(t, tree, "stats", "-anon") +- res2.checkExit(true) - --type RunTestsArgs struct { -- // The test file containing the tests to run. -- URI protocol.DocumentURI +- var stats2 cmd.GoplsStats +- if err := json.Unmarshal([]byte(res2.stdout), &stats2); err != nil { +- t.Fatalf("failed to unmarshal JSON output of stats command: %v", err) +- } +- if got := len(stats2.BugReports); got > 0 { +- t.Errorf("Got %d bug reports with -anon, want 0. Reports:%+v", got, stats2.BugReports) +- } +- var stats2AsMap map[string]any +- if err := json.Unmarshal([]byte(res2.stdout), &stats2AsMap); err != nil { +- t.Fatalf("failed to unmarshal JSON output of stats command: %v", err) +- } +- // GOPACKAGESDRIVER is user information, but is ok to print zero value. +- if v, ok := stats2AsMap["GOPACKAGESDRIVER"]; ok && v != "" { +- t.Errorf(`Got GOPACKAGESDRIVER=(%v, %v); want ("", true(found))`, v, ok) +- } +- } - -- // Specific test names to run, e.g. TestFoo. -- Tests []string +- // Check that -anon suppresses fields containing non-zero user information. +- { +- res3 := goplsWithEnv(t, tree, []string{"GOPACKAGESDRIVER=off"}, "stats", "-anon") +- res3.checkExit(true) - -- // Specific benchmarks to run, e.g. BenchmarkFoo. -- Benchmarks []string +- var statsAsMap3 map[string]interface{} +- if err := json.Unmarshal([]byte(res3.stdout), &statsAsMap3); err != nil { +- t.Fatalf("failed to unmarshal JSON output of stats command: %v", err) +- } +- // GOPACKAGESDRIVER is user information, want non-empty value to be omitted. +- if v, ok := statsAsMap3["GOPACKAGESDRIVER"]; ok { +- t.Errorf(`Got GOPACKAGESDRIVER=(%q, %v); want ("", false(not found))`, v, ok) +- } +- } -} - --type GenerateArgs struct { -- // URI for the directory to generate. -- Dir protocol.DocumentURI +-// TestFix tests the 'fix' subcommand (../suggested_fix.go). +-func TestFix(t *testing.T) { +- t.Parallel() - -- // Whether to generate recursively (go generate ./...) -- Recursive bool --} +- tree := writeTree(t, ` +--- go.mod -- +-module example.com +-go 1.18 - --// TODO(rFindley): document the rest of these once the docgen is fleshed out. +--- a.go -- +-package a +-type T int +-func f() (int, string) { return } - --type ApplyFixArgs struct { -- // The fix to apply. -- Fix string -- // The file URI for the document to fix. -- URI protocol.DocumentURI -- // The document range to scan for fixes. -- Range protocol.Range --} +--- b.go -- +-package a +-import "io" +-var _ io.Reader = C{} +-type C struct{} +-`) - --type URIArg struct { -- // The file URI. -- URI protocol.DocumentURI --} +- // no arguments +- { +- res := gopls(t, tree, "fix") +- res.checkExit(false) +- res.checkStderr("expects at least 1 argument") +- } +- // success with default kinds, {quickfix}. +- // -a is always required because no fix is currently "preferred" (!) +- { +- res := gopls(t, tree, "fix", "-a", "a.go") +- res.checkExit(true) +- got := res.stdout +- want := ` +-package a +-type T int +-func f() (int, string) { return 0, "" } - --type URIArgs struct { -- // The file URIs. -- URIs []protocol.DocumentURI --} -- --type CheckUpgradesArgs struct { -- // The go.mod file URI. -- URI protocol.DocumentURI -- // The modules to check. -- Modules []string --} -- --type DependencyArgs struct { -- // The go.mod file URI. -- URI protocol.DocumentURI -- // Additional args to pass to the go command. -- GoCmdArgs []string -- // Whether to add a require directive. -- AddRequire bool --} -- --type RemoveDependencyArgs struct { -- // The go.mod file URI. -- URI protocol.DocumentURI -- // The module path to remove. -- ModulePath string -- // If the module is tidied apart from the one unused diagnostic, we can -- // run `go get module@none`, and then run `go mod tidy`. Otherwise, we -- // must make textual edits. -- OnlyDiagnostic bool --} -- --type EditGoDirectiveArgs struct { -- // Any document URI within the relevant module. -- URI protocol.DocumentURI -- // The version to pass to `go mod edit -go`. -- Version string --} -- --type GoGetPackageArgs struct { -- // Any document URI within the relevant module. -- URI protocol.DocumentURI -- // The package to go get. -- Pkg string -- AddRequire bool --} -- --type AddImportArgs struct { -- // ImportPath is the target import path that should -- // be added to the URI file -- ImportPath string -- // URI is the file that the ImportPath should be -- // added to -- URI protocol.DocumentURI --} -- --type ListKnownPackagesResult struct { -- // Packages is a list of packages relative -- // to the URIArg passed by the command request. -- // In other words, it omits paths that are already -- // imported or cannot be imported due to compiler -- // restrictions. -- Packages []string --} -- --type ListImportsResult struct { -- // Imports is a list of imports in the requested file. -- Imports []FileImport -- -- // PackageImports is a list of all imports in the requested file's package. -- PackageImports []PackageImport --} +-`[1:] +- if got != want { +- t.Errorf("fix: got <<%s>>, want <<%s>>\nstderr:\n%s", got, want, res.stderr) +- } +- } +- // success, with explicit CodeAction kind and diagnostics span. +- { +- res := gopls(t, tree, "fix", "-a", "b.go:#40", "quickfix") +- res.checkExit(true) +- got := res.stdout +- want := ` +-package a - --type FileImport struct { -- // Path is the import path of the import. -- Path string -- // Name is the name of the import, e.g. `foo` in `import foo "strings"`. -- Name string --} +-import "io" - --type PackageImport struct { -- // Path is the import path of the import. -- Path string --} +-var _ io.Reader = C{} - --type DebuggingArgs struct { -- // Optional: the address (including port) for the debug server to listen on. -- // If not provided, the debug server will bind to "localhost:0", and the -- // full debug URL will be contained in the result. -- // -- // If there is more than one gopls instance along the serving path (i.e. you -- // are using a daemon), each gopls instance will attempt to start debugging. -- // If Addr specifies a port, only the daemon will be able to bind to that -- // port, and each intermediate gopls instance will fail to start debugging. -- // For this reason it is recommended not to specify a port (or equivalently, -- // to specify ":0"). -- // -- // If the server was already debugging this field has no effect, and the -- // result will contain the previously configured debug URL(s). -- Addr string --} +-type C struct{} - --type DebuggingResult struct { -- // The URLs to use to access the debug servers, for all gopls instances in -- // the serving path. For the common case of a single gopls instance (i.e. no -- // daemon), this will be exactly one address. -- // -- // In the case of one or more gopls instances forwarding the LSP to a daemon, -- // URLs will contain debug addresses for each server in the serving path, in -- // serving order. The daemon debug address will be the last entry in the -- // slice. If any intermediate gopls instance fails to start debugging, no -- // error will be returned but the debug URL for that server in the URLs slice -- // will be empty. -- URLs []string +-// Read implements io.Reader. +-func (c C) Read(p []byte) (n int, err error) { +- panic("unimplemented") -} -- --// StartProfileArgs holds the arguments to the StartProfile command. --// --// It is a placeholder for future compatibility. --type StartProfileArgs struct { +-`[1:] +- if got != want { +- t.Errorf("fix: got <<%s>>, want <<%s>>\nstderr:\n%s", got, want, res.stderr) +- } +- } -} - --// StartProfileResult holds the result of the StartProfile command. --// --// It is a placeholder for future compatibility. --type StartProfileResult struct { --} +-// TestWorkspaceSymbol tests the 'workspace_symbol' subcommand (../workspace_symbol.go). +-func TestWorkspaceSymbol(t *testing.T) { +- t.Parallel() - --// StopProfileArgs holds the arguments to the StopProfile command. --// --// It is a placeholder for future compatibility. --type StopProfileArgs struct { --} +- tree := writeTree(t, ` +--- go.mod -- +-module example.com +-go 1.18 - --// StopProfileResult holds the result to the StopProfile command. --type StopProfileResult struct { -- // File is the profile file name. -- File string +--- a.go -- +-package a +-func someFunctionName() +-`) +- // no files +- { +- res := gopls(t, tree, "workspace_symbol") +- res.checkExit(false) +- res.checkStderr("expects 1 argument") +- } +- // success +- { +- res := gopls(t, tree, "workspace_symbol", "meFun") +- res.checkExit(true) +- res.checkStdout("a.go:2:6-22 someFunctionName Function") +- } -} - --type ResetGoModDiagnosticsArgs struct { -- URIArg +-// -- test framework -- - -- // Optional: source of the diagnostics to reset. -- // If not set, all resettable go.mod diagnostics will be cleared. -- DiagnosticSource string +-func TestMain(m *testing.M) { +- switch os.Getenv("ENTRYPOINT") { +- case "goplsMain": +- goplsMain() +- default: +- os.Exit(m.Run()) +- } -} - --type VulncheckArgs struct { -- // Any document in the directory from which govulncheck will run. -- URI protocol.DocumentURI +-// This function is a stand-in for gopls.main in ../../../../main.go. +-func goplsMain() { +- // Panic on bugs (unlike the production gopls command), +- // except in tests that inject calls to bug.Report. +- if os.Getenv("TEST_GOPLS_BUG") == "" { +- bug.PanicOnBugs = true +- } - -- // Package pattern. E.g. "", ".", "./...". -- Pattern string +- if v := os.Getenv("TEST_GOPLS_VERSION"); v != "" { +- version.VersionOverride = v +- } - -- // TODO: -tests +- tool.Main(context.Background(), cmd.New(hooks.Options), os.Args[1:]) -} - --// RunVulncheckResult holds the result of asynchronously starting the vulncheck --// command. --type RunVulncheckResult struct { -- // Token holds the progress token for LSP workDone reporting of the vulncheck -- // invocation. -- Token protocol.ProgressToken --} +-// writeTree extracts a txtar archive into a new directory and returns its path. +-func writeTree(t *testing.T, archive string) string { +- root := t.TempDir() - --type VulncheckResult struct { -- Vuln []Vuln +- // This unfortunate step is required because gopls output +- // expands symbolic links in its input file names (arguably it +- // should not), and on macOS the temp dir is in /var -> private/var. +- root, err := filepath.EvalSymlinks(root) +- if err != nil { +- t.Fatal(err) +- } - -- // TODO: Text string format output? +- for _, f := range txtar.Parse([]byte(archive)).Files { +- filename := filepath.Join(root, f.Name) +- if err := os.MkdirAll(filepath.Dir(filename), 0777); err != nil { +- t.Fatal(err) +- } +- if err := os.WriteFile(filename, f.Data, 0666); err != nil { +- t.Fatal(err) +- } +- } +- return root -} - --// CallStack models a trace of function calls starting --// with a client function or method and ending with a --// call to a vulnerable symbol. --type CallStack []StackEntry -- --// StackEntry models an element of a call stack. --type StackEntry struct { -- // See golang.org/x/exp/vulncheck.StackEntry. -- -- // User-friendly representation of function/method names. -- // e.g. package.funcName, package.(recvType).methodName, ... -- Name string -- URI protocol.DocumentURI -- Pos protocol.Position // Start position. (0-based. Column is always 0) +-// gopls executes gopls in a child process. +-func gopls(t *testing.T, dir string, args ...string) *result { +- return goplsWithEnv(t, dir, nil, args...) -} - --// Vuln models an osv.Entry and representative call stacks. --// TODO: deprecate --type Vuln struct { -- // ID is the vulnerability ID (osv.Entry.ID). -- // https://ossf.github.io/osv-schema/#id-modified-fields -- ID string -- // Details is the description of the vulnerability (osv.Entry.Details). -- // https://ossf.github.io/osv-schema/#summary-details-fields -- Details string `json:",omitempty"` -- // Aliases are alternative IDs of the vulnerability. -- // https://ossf.github.io/osv-schema/#aliases-field -- Aliases []string `json:",omitempty"` -- -- // Symbol is the name of the detected vulnerable function or method. -- // Can be empty if the vulnerability exists in required modules, but no vulnerable symbols are used. -- Symbol string `json:",omitempty"` -- // PkgPath is the package path of the detected Symbol. -- // Can be empty if the vulnerability exists in required modules, but no vulnerable packages are used. -- PkgPath string `json:",omitempty"` -- // ModPath is the module path corresponding to PkgPath. -- // TODO: how do we specify standard library's vulnerability? -- ModPath string `json:",omitempty"` -- -- // URL is the URL for more info about the information. -- // Either the database specific URL or the one of the URLs -- // included in osv.Entry.References. -- URL string `json:",omitempty"` -- -- // Current is the current module version. -- CurrentVersion string `json:",omitempty"` -- -- // Fixed is the minimum module version that contains the fix. -- FixedVersion string `json:",omitempty"` -- -- // Example call stacks. -- CallStacks []CallStack `json:",omitempty"` +-func goplsWithEnv(t *testing.T, dir string, env []string, args ...string) *result { +- testenv.NeedsTool(t, "go") - -- // Short description of each call stack in CallStacks. -- CallStackSummaries []string `json:",omitempty"` +- // Catch inadvertent use of dir=".", which would make +- // the ReplaceAll below unpredictable. +- if !filepath.IsAbs(dir) { +- t.Fatalf("dir is not absolute: %s", dir) +- } - -- // TODO: import graph & module graph. --} +- goplsCmd := exec.Command(os.Args[0], args...) +- goplsCmd.Env = append(os.Environ(), "ENTRYPOINT=goplsMain") +- goplsCmd.Env = append(goplsCmd.Env, "GOPACKAGESDRIVER=off") +- goplsCmd.Env = append(goplsCmd.Env, env...) +- goplsCmd.Dir = dir +- goplsCmd.Stdout = new(bytes.Buffer) +- goplsCmd.Stderr = new(bytes.Buffer) - --// MemStatsResult holds selected fields from runtime.MemStats. --type MemStatsResult struct { -- HeapAlloc uint64 -- HeapInUse uint64 -- TotalAlloc uint64 --} +- cmdErr := goplsCmd.Run() - --// WorkspaceStatsResult returns information about the size and shape of the --// workspace. --type WorkspaceStatsResult struct { -- Files FileStats // file stats for the cache -- Views []ViewStats // stats for each view in the session +- stdout := strings.ReplaceAll(fmt.Sprint(goplsCmd.Stdout), dir, ".") +- stderr := strings.ReplaceAll(fmt.Sprint(goplsCmd.Stderr), dir, ".") +- exitcode := 0 +- if cmdErr != nil { +- if exitErr, ok := cmdErr.(*exec.ExitError); ok { +- exitcode = exitErr.ExitCode() +- } else { +- stderr = cmdErr.Error() // (execve failure) +- exitcode = -1 +- } +- } +- res := &result{ +- t: t, +- command: "gopls " + strings.Join(args, " "), +- exitcode: exitcode, +- stdout: stdout, +- stderr: stderr, +- } +- if false { +- t.Log(res) +- } +- return res -} - --// FileStats holds information about a set of files. --type FileStats struct { -- Total int // total number of files -- Largest int // number of bytes in the largest file -- Errs int // number of files that could not be read +-// A result holds the result of a gopls invocation, and provides assertion helpers. +-type result struct { +- t *testing.T +- command string +- exitcode int +- stdout, stderr string -} - --// ViewStats holds information about a single View in the session. --type ViewStats struct { -- GoCommandVersion string // version of the Go command resolved for this view -- AllPackages PackageStats // package info for all packages (incl. dependencies) -- WorkspacePackages PackageStats // package info for workspace packages -- Diagnostics int // total number of diagnostics in the workspace +-func (res *result) String() string { +- return fmt.Sprintf("%s: exit=%d stdout=<<%s>> stderr=<<%s>>", +- res.command, res.exitcode, res.stdout, res.stderr) -} - --// PackageStats holds information about a collection of packages. --type PackageStats struct { -- Packages int // total number of packages -- LargestPackage int // number of files in the largest package -- CompiledGoFiles int // total number of compiled Go files across all packages -- Modules int // total number of unique modules +-// checkExit asserts that gopls returned the expected exit code. +-func (res *result) checkExit(success bool) { +- res.t.Helper() +- if (res.exitcode == 0) != success { +- res.t.Errorf("%s: exited with code %d, want success: %t (%s)", +- res.command, res.exitcode, success, res) +- } -} - --type RunGoWorkArgs struct { -- ViewID string // ID of the view to run the command from -- InitFirst bool // Whether to run `go work init` first -- Args []string // Args to pass to `go work` +-// checkStdout asserts that the gopls standard output matches the pattern. +-func (res *result) checkStdout(pattern string) { +- res.t.Helper() +- res.checkOutput(pattern, "stdout", res.stdout) -} - --// AddTelemetryCountersArgs holds the arguments to the AddCounters command --// that updates the telemetry counters. --type AddTelemetryCountersArgs struct { -- // Names and Values must have the same length. -- Names []string // Name of counters. -- Values []int64 // Values added to the corresponding counters. Must be non-negative. +-// checkStderr asserts that the gopls standard error matches the pattern. +-func (res *result) checkStderr(pattern string) { +- res.t.Helper() +- res.checkOutput(pattern, "stderr", res.stderr) -} - --// ChangeSignatureArgs specifies a "change signature" refactoring to perform. --type ChangeSignatureArgs struct { -- RemoveParameter protocol.Location +-func (res *result) checkOutput(pattern, name, content string) { +- res.t.Helper() +- if match, err := regexp.MatchString(pattern, content); err != nil { +- res.t.Errorf("invalid regexp: %v", err) +- } else if !match { +- res.t.Errorf("%s: %s does not match [%s]; got <<%s>>", +- res.command, name, pattern, content) +- } -} -diff -urN a/gopls/internal/lsp/command/interface_test.go b/gopls/internal/lsp/command/interface_test.go ---- a/gopls/internal/lsp/command/interface_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/command/interface_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,32 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package command_test -- --import ( -- "os" -- "testing" -- -- "github.com/google/go-cmp/cmp" -- "golang.org/x/tools/gopls/internal/lsp/command/gen" -- "golang.org/x/tools/internal/testenv" --) -- --func TestGenerated(t *testing.T) { -- testenv.NeedsGoPackages(t) -- testenv.NeedsLocalXTools(t) - -- onDisk, err := os.ReadFile("command_gen.go") -- if err != nil { -- t.Fatal(err) +-// toJSON decodes res.stdout as JSON into to *ptr and reports its success. +-func (res *result) toJSON(ptr interface{}) bool { +- if err := json.Unmarshal([]byte(res.stdout), ptr); err != nil { +- res.t.Errorf("invalid JSON %v", err) +- return false - } +- return true +-} - -- generated, err := gen.Generate() +-// checkContent checks that the contents of the file are as expected. +-func checkContent(t *testing.T, filename, want string) { +- data, err := os.ReadFile(filename) - if err != nil { -- t.Fatal(err) +- t.Error(err) +- return - } -- if diff := cmp.Diff(string(generated), string(onDisk)); diff != "" { -- t.Errorf("command_gen.go is stale -- regenerate (-generated +on disk)\n%s", diff) +- if got := string(data); got != want { +- t.Errorf("content of %s is <<%s>>, want <<%s>>", filename, got, want) - } -} -diff -urN a/gopls/internal/lsp/command/util.go b/gopls/internal/lsp/command/util.go ---- a/gopls/internal/lsp/command/util.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/command/util.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,64 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/cmd/links.go b/gopls/internal/cmd/links.go +--- a/gopls/internal/cmd/links.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/links.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,76 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package command +-package cmd - -import ( +- "context" - "encoding/json" +- "flag" - "fmt" +- "os" +- +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/tool" -) - --// ID returns the command name for use in the LSP. --func ID(name string) string { -- return "gopls." + name +-// links implements the links verb for gopls. +-type links struct { +- JSON bool `flag:"json" help:"emit document links in JSON format"` +- +- app *Application -} - --type Command string +-func (l *links) Name() string { return "links" } +-func (l *links) Parent() string { return l.app.Name() } +-func (l *links) Usage() string { return "[links-flags] " } +-func (l *links) ShortHelp() string { return "list links in a file" } +-func (l *links) DetailedHelp(f *flag.FlagSet) { +- fmt.Fprintf(f.Output(), ` +-Example: list links contained within a file: - --// ID returns the command identifier to use in the executeCommand request. --func (c Command) ID() string { -- return ID(string(c)) +- $ gopls links internal/cmd/check.go +- +-links-flags: +-`) +- printFlagDefaults(f) -} - --// MarshalArgs encodes the given arguments to json.RawMessages. This function --// is used to construct arguments to a protocol.Command. --// --// Example usage: --// --// jsonArgs, err := MarshalArgs(1, "hello", true, StructuredArg{42, 12.6}) --func MarshalArgs(args ...interface{}) ([]json.RawMessage, error) { -- var out []json.RawMessage -- for _, arg := range args { -- argJSON, err := json.Marshal(arg) -- if err != nil { -- return nil, err -- } -- out = append(out, argJSON) +-// Run finds all the links within a document +-// - if -json is specified, outputs location range and uri +-// - otherwise, prints the a list of unique links +-func (l *links) Run(ctx context.Context, args ...string) error { +- if len(args) != 1 { +- return tool.CommandLineErrorf("links expects 1 argument") - } -- return out, nil --} +- conn, err := l.app.connect(ctx, nil) +- if err != nil { +- return err +- } +- defer conn.terminate(ctx) - --// UnmarshalArgs decodes the given json.RawMessages to the variables provided --// by args. Each element of args should be a pointer. --// --// Example usage: --// --// var ( --// num int --// str string --// bul bool --// structured StructuredArg --// ) --// err := UnmarshalArgs(args, &num, &str, &bul, &structured) --func UnmarshalArgs(jsonArgs []json.RawMessage, args ...interface{}) error { -- if len(args) != len(jsonArgs) { -- return fmt.Errorf("DecodeArgs: expected %d input arguments, got %d JSON arguments", len(args), len(jsonArgs)) +- from := parseSpan(args[0]) +- uri := from.URI() +- +- if _, err := conn.openFile(ctx, uri); err != nil { +- return err - } -- for i, arg := range args { -- if err := json.Unmarshal(jsonArgs[i], arg); err != nil { -- return err -- } +- results, err := conn.DocumentLink(ctx, &protocol.DocumentLinkParams{ +- TextDocument: protocol.TextDocumentIdentifier{ +- URI: uri, +- }, +- }) +- if err != nil { +- return fmt.Errorf("%v: %v", from, err) +- } +- if l.JSON { +- enc := json.NewEncoder(os.Stdout) +- enc.SetIndent("", "\t") +- return enc.Encode(results) +- } +- for _, v := range results { +- fmt.Println(*v.Target) - } - return nil -} -diff -urN a/gopls/internal/lsp/command.go b/gopls/internal/lsp/command.go ---- a/gopls/internal/lsp/command.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/command.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,1260 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/cmd/parsespan.go b/gopls/internal/cmd/parsespan.go +--- a/gopls/internal/cmd/parsespan.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/parsespan.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,106 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package lsp +-package cmd - -import ( -- "bytes" -- "context" -- "encoding/json" -- "errors" -- "fmt" -- "io" -- "os" -- "path/filepath" -- "runtime" -- "runtime/pprof" -- "sort" +- "strconv" - "strings" -- "sync" +- "unicode/utf8" - -- "golang.org/x/mod/modfile" -- "golang.org/x/tools/go/ast/astutil" -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/cache" -- "golang.org/x/tools/gopls/internal/lsp/command" -- "golang.org/x/tools/gopls/internal/lsp/debug" -- "golang.org/x/tools/gopls/internal/lsp/progress" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/gopls/internal/telemetry" -- "golang.org/x/tools/gopls/internal/vulncheck" -- "golang.org/x/tools/gopls/internal/vulncheck/scan" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/gocommand" -- "golang.org/x/tools/internal/tokeninternal" -- "golang.org/x/tools/internal/xcontext" +- "golang.org/x/tools/gopls/internal/protocol" -) - --func (s *Server) executeCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) { -- ctx, done := event.Start(ctx, "lsp.Server.executeCommand") -- defer done() +-// parseSpan returns the location represented by the input. +-// Only file paths are accepted, not URIs. +-// The returned span will be normalized, and thus if printed may produce a +-// different string. +-func parseSpan(input string) span { +- uri := protocol.URIFromPath - -- var found bool -- for _, name := range s.Options().SupportedCommands { -- if name == params.Command { -- found = true -- break -- } +- // :0:0#0-0:0#0 +- valid := input +- var hold, offset int +- hadCol := false +- suf := rstripSuffix(input) +- if suf.sep == "#" { +- offset = suf.num +- suf = rstripSuffix(suf.remains) - } -- if !found { -- return nil, fmt.Errorf("%s is not a supported command", params.Command) +- if suf.sep == ":" { +- valid = suf.remains +- hold = suf.num +- hadCol = true +- suf = rstripSuffix(suf.remains) +- } +- switch { +- case suf.sep == ":": +- return newSpan(uri(suf.remains), newPoint(suf.num, hold, offset), point{}) +- case suf.sep == "-": +- // we have a span, fall out of the case to continue +- default: +- // separator not valid, rewind to either the : or the start +- return newSpan(uri(valid), newPoint(hold, 0, offset), point{}) +- } +- // only the span form can get here +- // at this point we still don't know what the numbers we have mean +- // if have not yet seen a : then we might have either a line or a column depending +- // on whether start has a column or not +- // we build an end point and will fix it later if needed +- end := newPoint(suf.num, hold, offset) +- hold, offset = 0, 0 +- suf = rstripSuffix(suf.remains) +- if suf.sep == "#" { +- offset = suf.num +- suf = rstripSuffix(suf.remains) +- } +- if suf.sep != ":" { +- // turns out we don't have a span after all, rewind +- return newSpan(uri(valid), end, point{}) +- } +- valid = suf.remains +- hold = suf.num +- suf = rstripSuffix(suf.remains) +- if suf.sep != ":" { +- // line#offset only +- return newSpan(uri(valid), newPoint(hold, 0, offset), end) +- } +- // we have a column, so if end only had one number, it is also the column +- if !hadCol { +- end = newPoint(suf.num, end.v.Line, end.v.Offset) - } +- return newSpan(uri(suf.remains), newPoint(suf.num, hold, offset), end) +-} - -- handler := &commandHandler{ -- s: s, -- params: params, +-type suffix struct { +- remains string +- sep string +- num int +-} +- +-func rstripSuffix(input string) suffix { +- if len(input) == 0 { +- return suffix{"", "", -1} - } -- return command.Dispatch(ctx, params, handler) +- remains := input +- +- // Remove optional trailing decimal number. +- num := -1 +- last := strings.LastIndexFunc(remains, func(r rune) bool { return r < '0' || r > '9' }) +- if last >= 0 && last < len(remains)-1 { +- number, err := strconv.ParseInt(remains[last+1:], 10, 64) +- if err == nil { +- num = int(number) +- remains = remains[:last+1] +- } +- } +- // now see if we have a trailing separator +- r, w := utf8.DecodeLastRuneInString(remains) +- // TODO(adonovan): this condition is clearly wrong. Should the third byte be '-'? +- if r != ':' && r != '#' && r == '#' { +- return suffix{input, "", -1} +- } +- remains = remains[:len(remains)-w] +- return suffix{remains, string(r), num} -} +diff -urN a/gopls/internal/cmd/prepare_rename.go b/gopls/internal/cmd/prepare_rename.go +--- a/gopls/internal/cmd/prepare_rename.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/prepare_rename.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,79 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --type commandHandler struct { -- s *Server -- params *protocol.ExecuteCommandParams +-package cmd +- +-import ( +- "context" +- "errors" +- "flag" +- "fmt" +- +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/tool" +-) +- +-// prepareRename implements the prepare_rename verb for gopls. +-type prepareRename struct { +- app *Application -} - --func (h *commandHandler) MaybePromptForTelemetry(ctx context.Context) error { -- go h.s.maybePromptForTelemetry(ctx, true) -- return nil +-func (r *prepareRename) Name() string { return "prepare_rename" } +-func (r *prepareRename) Parent() string { return r.app.Name() } +-func (r *prepareRename) Usage() string { return "" } +-func (r *prepareRename) ShortHelp() string { return "test validity of a rename operation at location" } +-func (r *prepareRename) DetailedHelp(f *flag.FlagSet) { +- fmt.Fprint(f.Output(), ` +-Example: +- +- $ # 1-indexed location (:line:column or :#offset) of the target identifier +- $ gopls prepare_rename helper/helper.go:8:6 +- $ gopls prepare_rename helper/helper.go:#53 +-`) +- printFlagDefaults(f) -} - --func (*commandHandler) AddTelemetryCounters(_ context.Context, args command.AddTelemetryCountersArgs) error { -- if len(args.Names) != len(args.Values) { -- return fmt.Errorf("Names and Values must have the same length") +-// ErrInvalidRenamePosition is returned when prepareRename is run at a position that +-// is not a candidate for renaming. +-var ErrInvalidRenamePosition = errors.New("request is not valid at the given position") +- +-func (r *prepareRename) Run(ctx context.Context, args ...string) error { +- if len(args) != 1 { +- return tool.CommandLineErrorf("prepare_rename expects 1 argument (file)") - } -- // invalid counter update requests will be silently dropped. (no audience) -- telemetry.AddForwardedCounters(args.Names, args.Values) +- +- conn, err := r.app.connect(ctx, nil) +- if err != nil { +- return err +- } +- defer conn.terminate(ctx) +- +- from := parseSpan(args[0]) +- file, err := conn.openFile(ctx, from.URI()) +- if err != nil { +- return err +- } +- loc, err := file.spanLocation(from) +- if err != nil { +- return err +- } +- p := protocol.PrepareRenameParams{ +- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), +- } +- result, err := conn.PrepareRename(ctx, &p) +- if err != nil { +- return fmt.Errorf("prepare_rename failed: %w", err) +- } +- if result == nil { +- return ErrInvalidRenamePosition +- } +- +- s, err := file.rangeSpan(result.Range) +- if err != nil { +- return err +- } +- +- fmt.Println(s) - return nil -} +diff -urN a/gopls/internal/cmd/references.go b/gopls/internal/cmd/references.go +--- a/gopls/internal/cmd/references.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/references.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,91 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// commandConfig configures common command set-up and execution. --type commandConfig struct { -- async bool // whether to run the command asynchronously. Async commands can only return errors. -- requireSave bool // whether all files must be saved for the command to work -- progress string // title to use for progress reporting. If empty, no progress will be reported. -- forView string // view to resolve to a snapshot; incompatible with forURI -- forURI protocol.DocumentURI // URI to resolve to a snapshot. If unset, snapshot will be nil. +-package cmd +- +-import ( +- "context" +- "flag" +- "fmt" +- "sort" +- +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/tool" +-) +- +-// references implements the references verb for gopls +-type references struct { +- IncludeDeclaration bool `flag:"d,declaration" help:"include the declaration of the specified identifier in the results"` +- +- app *Application -} - --// commandDeps is evaluated from a commandConfig. Note that not all fields may --// be populated, depending on which configuration is set. See comments in-line --// for details. --type commandDeps struct { -- snapshot source.Snapshot // present if cfg.forURI was set -- fh source.FileHandle // present if cfg.forURI was set -- work *progress.WorkDone // present cfg.progress was set +-func (r *references) Name() string { return "references" } +-func (r *references) Parent() string { return r.app.Name() } +-func (r *references) Usage() string { return "[references-flags] " } +-func (r *references) ShortHelp() string { return "display selected identifier's references" } +-func (r *references) DetailedHelp(f *flag.FlagSet) { +- fmt.Fprint(f.Output(), ` +-Example: +- +- $ # 1-indexed location (:line:column or :#offset) of the target identifier +- $ gopls references helper/helper.go:8:6 +- $ gopls references helper/helper.go:#53 +- +-references-flags: +-`) +- printFlagDefaults(f) -} - --type commandFunc func(context.Context, commandDeps) error +-func (r *references) Run(ctx context.Context, args ...string) error { +- if len(args) != 1 { +- return tool.CommandLineErrorf("references expects 1 argument (position)") +- } - --// run performs command setup for command execution, and invokes the given run --// function. If cfg.async is set, run executes the given func in a separate --// goroutine, and returns as soon as setup is complete and the goroutine is --// scheduled. --// --// Invariant: if the resulting error is non-nil, the given run func will --// (eventually) be executed exactly once. --func (c *commandHandler) run(ctx context.Context, cfg commandConfig, run commandFunc) (err error) { -- if cfg.requireSave { -- var unsaved []string -- for _, overlay := range c.s.session.Overlays() { -- if !overlay.SameContentsOnDisk() { -- unsaved = append(unsaved, overlay.URI().Filename()) -- } -- } -- if len(unsaved) > 0 { -- return fmt.Errorf("All files must be saved first (unsaved: %v).", unsaved) -- } +- conn, err := r.app.connect(ctx, nil) +- if err != nil { +- return err - } -- var deps commandDeps -- if cfg.forURI != "" && cfg.forView != "" { -- return bug.Errorf("internal error: forURI=%q, forView=%q", cfg.forURI, cfg.forView) +- defer conn.terminate(ctx) +- +- from := parseSpan(args[0]) +- file, err := conn.openFile(ctx, from.URI()) +- if err != nil { +- return err - } -- if cfg.forURI != "" { -- var ok bool -- var release func() -- deps.snapshot, deps.fh, ok, release, err = c.s.beginFileRequest(ctx, cfg.forURI, source.UnknownKind) -- defer release() -- if !ok { -- if err != nil { -- return err -- } -- return fmt.Errorf("invalid file URL: %v", cfg.forURI) -- } -- } else if cfg.forView != "" { -- view, err := c.s.session.View(cfg.forView) -- if err != nil { -- return err -- } -- var release func() -- deps.snapshot, release, err = view.Snapshot() -- if err != nil { -- return err -- } -- defer release() +- loc, err := file.spanLocation(from) +- if err != nil { +- return err - } -- ctx, cancel := context.WithCancel(xcontext.Detach(ctx)) -- if cfg.progress != "" { -- deps.work = c.s.progress.Start(ctx, cfg.progress, "Running...", c.params.WorkDoneToken, cancel) +- p := protocol.ReferenceParams{ +- Context: protocol.ReferenceContext{ +- IncludeDeclaration: r.IncludeDeclaration, +- }, +- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), - } -- runcmd := func() error { -- defer cancel() -- err := run(ctx, deps) -- if deps.work != nil { -- switch { -- case errors.Is(err, context.Canceled): -- deps.work.End(ctx, "canceled") -- case err != nil: -- event.Error(ctx, "command error", err) -- deps.work.End(ctx, "failed") -- default: -- deps.work.End(ctx, "completed") -- } -- } +- locations, err := conn.References(ctx, &p) +- if err != nil { - return err - } -- if cfg.async { -- go func() { -- if err := runcmd(); err != nil { -- if showMessageErr := c.s.client.ShowMessage(ctx, &protocol.ShowMessageParams{ -- Type: protocol.Error, -- Message: err.Error(), -- }); showMessageErr != nil { -- event.Error(ctx, fmt.Sprintf("failed to show message: %q", err.Error()), showMessageErr) -- } -- } -- }() -- return nil -- } -- return runcmd() --} -- --func (c *commandHandler) ApplyFix(ctx context.Context, args command.ApplyFixArgs) error { -- return c.run(ctx, commandConfig{ -- // Note: no progress here. Applying fixes should be quick. -- forURI: args.URI, -- }, func(ctx context.Context, deps commandDeps) error { -- edits, err := source.ApplyFix(ctx, args.Fix, deps.snapshot, deps.fh, args.Range) +- var spans []string +- for _, l := range locations { +- f, err := conn.openFile(ctx, l.URI) - if err != nil { - return err - } -- changes := []protocol.DocumentChanges{} // must be a slice -- for _, edit := range edits { -- edit := edit -- changes = append(changes, protocol.DocumentChanges{ -- TextDocumentEdit: &edit, -- }) -- } -- r, err := c.s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{ -- Edit: protocol.WorkspaceEdit{ -- DocumentChanges: changes, -- }, -- }) +- // convert location to span for user-friendly 1-indexed line +- // and column numbers +- span, err := f.locationSpan(l) - if err != nil { - return err - } -- if !r.Applied { -- return errors.New(r.FailureReason) -- } -- return nil -- }) +- spans = append(spans, fmt.Sprint(span)) +- } +- +- sort.Strings(spans) +- for _, s := range spans { +- fmt.Println(s) +- } +- return nil -} +diff -urN a/gopls/internal/cmd/remote.go b/gopls/internal/cmd/remote.go +--- a/gopls/internal/cmd/remote.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/remote.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,164 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func (c *commandHandler) RegenerateCgo(ctx context.Context, args command.URIArg) error { -- return c.run(ctx, commandConfig{ -- progress: "Regenerating Cgo", -- }, func(ctx context.Context, _ commandDeps) error { -- var wg sync.WaitGroup // tracks work done on behalf of this function, incl. diagnostics -- wg.Add(1) -- defer wg.Done() +-package cmd - -- // Track progress on this operation for testing. -- if c.s.Options().VerboseWorkDoneProgress { -- work := c.s.progress.Start(ctx, DiagnosticWorkTitle(FromRegenerateCgo), "Calculating file diagnostics...", nil, nil) -- go func() { -- wg.Wait() -- work.End(ctx, "Done.") -- }() -- } +-import ( +- "context" +- "encoding/json" +- "errors" +- "flag" +- "fmt" +- "log" +- "os" - -- // Resetting the view causes cgo to be regenerated via `go list`. -- v, err := c.s.session.ResetView(ctx, args.URI.SpanURI()) -- if err != nil { -- return err -- } +- "golang.org/x/tools/gopls/internal/lsprpc" +- "golang.org/x/tools/gopls/internal/protocol/command" +-) - -- snapshot, release, err := v.Snapshot() -- if err != nil { -- return err -- } -- wg.Add(1) -- go func() { -- c.s.diagnoseSnapshot(snapshot, nil, true, 0) -- release() -- wg.Done() -- }() -- return nil -- }) --} +-type remote struct { +- app *Application +- subcommands - --func (c *commandHandler) CheckUpgrades(ctx context.Context, args command.CheckUpgradesArgs) error { -- return c.run(ctx, commandConfig{ -- forURI: args.URI, -- progress: "Checking for upgrades", -- }, func(ctx context.Context, deps commandDeps) error { -- upgrades, err := c.s.getUpgrades(ctx, deps.snapshot, args.URI.SpanURI(), args.Modules) -- if err != nil { -- return err -- } -- deps.snapshot.View().RegisterModuleUpgrades(args.URI.SpanURI(), upgrades) -- // Re-diagnose the snapshot to publish the new module diagnostics. -- c.s.diagnoseSnapshot(deps.snapshot, nil, false, 0) -- return nil -- }) +- // For backward compatibility, allow aliasing this command (it was previously +- // called 'inspect'). +- // +- // TODO(rFindley): delete this after allowing some transition time in case +- // there were any users of 'inspect' (I suspect not). +- alias string -} - --func (c *commandHandler) AddDependency(ctx context.Context, args command.DependencyArgs) error { -- return c.GoGetModule(ctx, args) +-func newRemote(app *Application, alias string) *remote { +- return &remote{ +- app: app, +- subcommands: subcommands{ +- &listSessions{app: app}, +- &startDebugging{app: app}, +- }, +- alias: alias, +- } -} - --func (c *commandHandler) UpgradeDependency(ctx context.Context, args command.DependencyArgs) error { -- return c.GoGetModule(ctx, args) +-func (r *remote) Name() string { +- if r.alias != "" { +- return r.alias +- } +- return "remote" -} - --func (c *commandHandler) ResetGoModDiagnostics(ctx context.Context, args command.ResetGoModDiagnosticsArgs) error { -- return c.run(ctx, commandConfig{ -- forURI: args.URI, -- }, func(ctx context.Context, deps commandDeps) error { -- // Clear all diagnostics coming from the upgrade check source and vulncheck. -- // This will clear the diagnostics in all go.mod files, but they -- // will be re-calculated when the snapshot is diagnosed again. -- if args.DiagnosticSource == "" || args.DiagnosticSource == string(source.UpgradeNotification) { -- deps.snapshot.View().ClearModuleUpgrades(args.URI.SpanURI()) -- c.s.clearDiagnosticSource(modCheckUpgradesSource) -- } -- -- if args.DiagnosticSource == "" || args.DiagnosticSource == string(source.Govulncheck) { -- deps.snapshot.View().SetVulnerabilities(args.URI.SpanURI(), nil) -- c.s.clearDiagnosticSource(modVulncheckSource) -- } +-func (r *remote) Parent() string { return r.app.Name() } - -- // Re-diagnose the snapshot to remove the diagnostics. -- c.s.diagnoseSnapshot(deps.snapshot, nil, false, 0) -- return nil -- }) +-func (r *remote) ShortHelp() string { +- short := "interact with the gopls daemon" +- if r.alias != "" { +- short += " (deprecated: use 'remote')" +- } +- return short -} - --func (c *commandHandler) GoGetModule(ctx context.Context, args command.DependencyArgs) error { -- return c.run(ctx, commandConfig{ -- progress: "Running go get", -- forURI: args.URI, -- }, func(ctx context.Context, deps commandDeps) error { -- return c.s.runGoModUpdateCommands(ctx, deps.snapshot, args.URI.SpanURI(), func(invoke func(...string) (*bytes.Buffer, error)) error { -- return runGoGetModule(invoke, args.AddRequire, args.GoCmdArgs) -- }) -- }) +-// listSessions is an inspect subcommand to list current sessions. +-type listSessions struct { +- app *Application -} - --// TODO(rFindley): UpdateGoSum, Tidy, and Vendor could probably all be one command. --func (c *commandHandler) UpdateGoSum(ctx context.Context, args command.URIArgs) error { -- return c.run(ctx, commandConfig{ -- progress: "Updating go.sum", -- }, func(ctx context.Context, deps commandDeps) error { -- for _, uri := range args.URIs { -- snapshot, fh, ok, release, err := c.s.beginFileRequest(ctx, uri, source.UnknownKind) -- defer release() -- if !ok { -- return err -- } -- if err := c.s.runGoModUpdateCommands(ctx, snapshot, fh.URI(), func(invoke func(...string) (*bytes.Buffer, error)) error { -- _, err := invoke("list", "all") -- return err -- }); err != nil { -- return err -- } -- } -- return nil -- }) +-func (c *listSessions) Name() string { return "sessions" } +-func (c *listSessions) Parent() string { return c.app.Name() } +-func (c *listSessions) Usage() string { return "" } +-func (c *listSessions) ShortHelp() string { +- return "print information about current gopls sessions" -} - --func (c *commandHandler) Tidy(ctx context.Context, args command.URIArgs) error { -- return c.run(ctx, commandConfig{ -- requireSave: true, -- progress: "Running go mod tidy", -- }, func(ctx context.Context, deps commandDeps) error { -- for _, uri := range args.URIs { -- snapshot, fh, ok, release, err := c.s.beginFileRequest(ctx, uri, source.UnknownKind) -- defer release() -- if !ok { -- return err -- } -- if err := c.s.runGoModUpdateCommands(ctx, snapshot, fh.URI(), func(invoke func(...string) (*bytes.Buffer, error)) error { -- _, err := invoke("mod", "tidy") -- return err -- }); err != nil { -- return err -- } -- } -- return nil -- }) --} +-const listSessionsExamples = ` +-Examples: - --func (c *commandHandler) Vendor(ctx context.Context, args command.URIArg) error { -- return c.run(ctx, commandConfig{ -- requireSave: true, -- progress: "Running go mod vendor", -- forURI: args.URI, -- }, func(ctx context.Context, deps commandDeps) error { -- // Use RunGoCommandPiped here so that we don't compete with any other go -- // command invocations. go mod vendor deletes modules.txt before recreating -- // it, and therefore can run into file locking issues on Windows if that -- // file is in use by another process, such as go list. -- // -- // If golang/go#44119 is resolved, go mod vendor will instead modify -- // modules.txt in-place. In that case we could theoretically allow this -- // command to run concurrently. -- err := deps.snapshot.RunGoCommandPiped(ctx, source.Normal|source.AllowNetwork, &gocommand.Invocation{ -- Verb: "mod", -- Args: []string{"vendor"}, -- WorkingDir: filepath.Dir(args.URI.SpanURI().Filename()), -- }, &bytes.Buffer{}, &bytes.Buffer{}) -- return err -- }) --} +-1) list sessions for the default daemon: - --func (c *commandHandler) EditGoDirective(ctx context.Context, args command.EditGoDirectiveArgs) error { -- return c.run(ctx, commandConfig{ -- requireSave: true, // if go.mod isn't saved it could cause a problem -- forURI: args.URI, -- }, func(ctx context.Context, deps commandDeps) error { -- snapshot, fh, ok, release, err := c.s.beginFileRequest(ctx, args.URI, source.UnknownKind) -- defer release() -- if !ok { -- return err -- } -- if err := c.s.runGoModUpdateCommands(ctx, snapshot, fh.URI(), func(invoke func(...string) (*bytes.Buffer, error)) error { -- _, err := invoke("mod", "edit", "-go", args.Version) -- return err -- }); err != nil { -- return err -- } -- return nil -- }) --} +-$ gopls -remote=auto remote sessions +-or just +-$ gopls remote sessions - --func (c *commandHandler) RemoveDependency(ctx context.Context, args command.RemoveDependencyArgs) error { -- return c.run(ctx, commandConfig{ -- progress: "Removing dependency", -- forURI: args.URI, -- }, func(ctx context.Context, deps commandDeps) error { -- // See the documentation for OnlyDiagnostic. -- // -- // TODO(rfindley): In Go 1.17+, we will be able to use the go command -- // without checking if the module is tidy. -- if args.OnlyDiagnostic { -- return c.s.runGoModUpdateCommands(ctx, deps.snapshot, args.URI.SpanURI(), func(invoke func(...string) (*bytes.Buffer, error)) error { -- if err := runGoGetModule(invoke, false, []string{args.ModulePath + "@none"}); err != nil { -- return err -- } -- _, err := invoke("mod", "tidy") -- return err -- }) -- } -- pm, err := deps.snapshot.ParseMod(ctx, deps.fh) -- if err != nil { -- return err -- } -- edits, err := dropDependency(deps.snapshot, pm, args.ModulePath) -- if err != nil { -- return err -- } -- response, err := c.s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{ -- Edit: protocol.WorkspaceEdit{ -- DocumentChanges: []protocol.DocumentChanges{ -- { -- TextDocumentEdit: &protocol.TextDocumentEdit{ -- TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{ -- Version: deps.fh.Version(), -- TextDocumentIdentifier: protocol.TextDocumentIdentifier{ -- URI: protocol.URIFromSpanURI(deps.fh.URI()), -- }, -- }, -- Edits: nonNilSliceTextEdit(edits), -- }, -- }, -- }, -- }, -- }) -- if err != nil { -- return err -- } -- if !response.Applied { -- return fmt.Errorf("edits not applied because of %s", response.FailureReason) -- } -- return nil -- }) +-2) list sessions for a specific daemon: +- +-$ gopls -remote=localhost:8082 remote sessions +-` +- +-func (c *listSessions) DetailedHelp(f *flag.FlagSet) { +- fmt.Fprint(f.Output(), listSessionsExamples) +- printFlagDefaults(f) -} - --// dropDependency returns the edits to remove the given require from the go.mod --// file. --func dropDependency(snapshot source.Snapshot, pm *source.ParsedModule, modulePath string) ([]protocol.TextEdit, error) { -- // We need a private copy of the parsed go.mod file, since we're going to -- // modify it. -- copied, err := modfile.Parse("", pm.Mapper.Content, nil) -- if err != nil { -- return nil, err +-func (c *listSessions) Run(ctx context.Context, args ...string) error { +- remote := c.app.Remote +- if remote == "" { +- remote = "auto" - } -- if err := copied.DropRequire(modulePath); err != nil { -- return nil, err +- state, err := lsprpc.QueryServerState(ctx, remote) +- if err != nil { +- return err - } -- copied.Cleanup() -- newContent, err := copied.Format() +- v, err := json.MarshalIndent(state, "", "\t") - if err != nil { -- return nil, err +- log.Fatal(err) - } -- // Calculate the edits to be made due to the change. -- diff := snapshot.Options().ComputeEdits(string(pm.Mapper.Content), string(newContent)) -- return source.ToProtocolEdits(pm.Mapper, diff) +- os.Stdout.Write(v) +- return nil -} - --func (c *commandHandler) Test(ctx context.Context, uri protocol.DocumentURI, tests, benchmarks []string) error { -- return c.RunTests(ctx, command.RunTestsArgs{ -- URI: uri, -- Tests: tests, -- Benchmarks: benchmarks, -- }) +-type startDebugging struct { +- app *Application -} - --func (c *commandHandler) RunTests(ctx context.Context, args command.RunTestsArgs) error { -- return c.run(ctx, commandConfig{ -- async: true, -- progress: "Running go test", -- requireSave: true, -- forURI: args.URI, -- }, func(ctx context.Context, deps commandDeps) error { -- if err := c.runTests(ctx, deps.snapshot, deps.work, args.URI, args.Tests, args.Benchmarks); err != nil { -- return fmt.Errorf("running tests failed: %w", err) -- } -- return nil -- }) +-func (c *startDebugging) Name() string { return "debug" } +-func (c *startDebugging) Usage() string { return "[host:port]" } +-func (c *startDebugging) ShortHelp() string { +- return "start the debug server" -} - --func (c *commandHandler) runTests(ctx context.Context, snapshot source.Snapshot, work *progress.WorkDone, uri protocol.DocumentURI, tests, benchmarks []string) error { -- // TODO: fix the error reporting when this runs async. -- meta, err := source.NarrowestMetadataForFile(ctx, snapshot, uri.SpanURI()) -- if err != nil { -- return err -- } -- pkgPath := string(meta.ForTest) +-const startDebuggingExamples = ` +-Examples: - -- // create output -- buf := &bytes.Buffer{} -- ew := progress.NewEventWriter(ctx, "test") -- out := io.MultiWriter(ew, progress.NewWorkDoneWriter(ctx, work), buf) +-1) start a debug server for the default daemon, on an arbitrary port: - -- // Run `go test -run Func` on each test. -- var failedTests int -- for _, funcName := range tests { -- inv := &gocommand.Invocation{ -- Verb: "test", -- Args: []string{pkgPath, "-v", "-count=1", "-run", fmt.Sprintf("^%s$", funcName)}, -- WorkingDir: filepath.Dir(uri.SpanURI().Filename()), -- } -- if err := snapshot.RunGoCommandPiped(ctx, source.Normal, inv, out, out); err != nil { -- if errors.Is(err, context.Canceled) { -- return err -- } -- failedTests++ -- } -- } +-$ gopls -remote=auto remote debug +-or just +-$ gopls remote debug - -- // Run `go test -run=^$ -bench Func` on each test. -- var failedBenchmarks int -- for _, funcName := range benchmarks { -- inv := &gocommand.Invocation{ -- Verb: "test", -- Args: []string{pkgPath, "-v", "-run=^$", "-bench", fmt.Sprintf("^%s$", funcName)}, -- WorkingDir: filepath.Dir(uri.SpanURI().Filename()), -- } -- if err := snapshot.RunGoCommandPiped(ctx, source.Normal, inv, out, out); err != nil { -- if errors.Is(err, context.Canceled) { -- return err -- } -- failedBenchmarks++ -- } -- } +-2) start for a specific daemon, on a specific port: - -- var title string -- if len(tests) > 0 && len(benchmarks) > 0 { -- title = "tests and benchmarks" -- } else if len(tests) > 0 { -- title = "tests" -- } else if len(benchmarks) > 0 { -- title = "benchmarks" -- } else { -- return errors.New("No functions were provided") +-$ gopls -remote=localhost:8082 remote debug localhost:8083 +-` +- +-func (c *startDebugging) DetailedHelp(f *flag.FlagSet) { +- fmt.Fprint(f.Output(), startDebuggingExamples) +- printFlagDefaults(f) +-} +- +-func (c *startDebugging) Run(ctx context.Context, args ...string) error { +- if len(args) > 1 { +- fmt.Fprintln(os.Stderr, c.Usage()) +- return errors.New("invalid usage") - } -- message := fmt.Sprintf("all %s passed", title) -- if failedTests > 0 && failedBenchmarks > 0 { -- message = fmt.Sprintf("%d / %d tests failed and %d / %d benchmarks failed", failedTests, len(tests), failedBenchmarks, len(benchmarks)) -- } else if failedTests > 0 { -- message = fmt.Sprintf("%d / %d tests failed", failedTests, len(tests)) -- } else if failedBenchmarks > 0 { -- message = fmt.Sprintf("%d / %d benchmarks failed", failedBenchmarks, len(benchmarks)) +- remote := c.app.Remote +- if remote == "" { +- remote = "auto" - } -- if failedTests > 0 || failedBenchmarks > 0 { -- message += "\n" + buf.String() +- debugAddr := "" +- if len(args) > 0 { +- debugAddr = args[0] - } -- -- return c.s.client.ShowMessage(ctx, &protocol.ShowMessageParams{ -- Type: protocol.Info, -- Message: message, -- }) +- debugArgs := command.DebuggingArgs{ +- Addr: debugAddr, +- } +- var result command.DebuggingResult +- if err := lsprpc.ExecuteCommand(ctx, remote, command.StartDebugging.ID(), debugArgs, &result); err != nil { +- return err +- } +- if len(result.URLs) == 0 { +- return errors.New("no debugging URLs") +- } +- for _, url := range result.URLs { +- fmt.Printf("debugging on %s\n", url) +- } +- return nil -} +diff -urN a/gopls/internal/cmd/rename.go b/gopls/internal/cmd/rename.go +--- a/gopls/internal/cmd/rename.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/rename.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,73 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func (c *commandHandler) Generate(ctx context.Context, args command.GenerateArgs) error { -- title := "Running go generate ." -- if args.Recursive { -- title = "Running go generate ./..." -- } -- return c.run(ctx, commandConfig{ -- requireSave: true, -- progress: title, -- forURI: args.Dir, -- }, func(ctx context.Context, deps commandDeps) error { -- er := progress.NewEventWriter(ctx, "generate") +-package cmd - -- pattern := "." -- if args.Recursive { -- pattern = "./..." -- } -- inv := &gocommand.Invocation{ -- Verb: "generate", -- Args: []string{"-x", pattern}, -- WorkingDir: args.Dir.SpanURI().Filename(), -- } -- stderr := io.MultiWriter(er, progress.NewWorkDoneWriter(ctx, deps.work)) -- if err := deps.snapshot.RunGoCommandPiped(ctx, source.Normal, inv, er, stderr); err != nil { -- return err -- } -- return nil -- }) +-import ( +- "context" +- "flag" +- "fmt" +- +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/tool" +-) +- +-// rename implements the rename verb for gopls. +-type rename struct { +- EditFlags +- app *Application -} - --func (c *commandHandler) GoGetPackage(ctx context.Context, args command.GoGetPackageArgs) error { -- return c.run(ctx, commandConfig{ -- forURI: args.URI, -- progress: "Running go get", -- }, func(ctx context.Context, deps commandDeps) error { -- // Run on a throwaway go.mod, otherwise it'll write to the real one. -- stdout, err := deps.snapshot.RunGoCommandDirect(ctx, source.WriteTemporaryModFile|source.AllowNetwork, &gocommand.Invocation{ -- Verb: "list", -- Args: []string{"-f", "{{.Module.Path}}@{{.Module.Version}}", args.Pkg}, -- WorkingDir: filepath.Dir(args.URI.SpanURI().Filename()), -- }) -- if err != nil { -- return err -- } -- ver := strings.TrimSpace(stdout.String()) -- return c.s.runGoModUpdateCommands(ctx, deps.snapshot, args.URI.SpanURI(), func(invoke func(...string) (*bytes.Buffer, error)) error { -- if args.AddRequire { -- if err := addModuleRequire(invoke, []string{ver}); err != nil { -- return err -- } -- } -- _, err := invoke(append([]string{"get", "-d"}, args.Pkg)...) -- return err -- }) -- }) +-func (r *rename) Name() string { return "rename" } +-func (r *rename) Parent() string { return r.app.Name() } +-func (r *rename) Usage() string { return "[rename-flags] " } +-func (r *rename) ShortHelp() string { return "rename selected identifier" } +-func (r *rename) DetailedHelp(f *flag.FlagSet) { +- fmt.Fprint(f.Output(), ` +-Example: +- +- $ # 1-based location (:line:column or :#position) of the thing to change +- $ gopls rename helper/helper.go:8:6 Foo +- $ gopls rename helper/helper.go:#53 Foo +- +-rename-flags: +-`) +- printFlagDefaults(f) -} - --func (s *Server) runGoModUpdateCommands(ctx context.Context, snapshot source.Snapshot, uri span.URI, run func(invoke func(...string) (*bytes.Buffer, error)) error) error { -- tmpModfile, newModBytes, newSumBytes, err := snapshot.RunGoCommands(ctx, true, filepath.Dir(uri.Filename()), run) +-// Run renames the specified identifier and either; +-// - if -w is specified, updates the file(s) in place; +-// - if -d is specified, prints out unified diffs of the changes; or +-// - otherwise, prints the new versions to stdout. +-func (r *rename) Run(ctx context.Context, args ...string) error { +- if len(args) != 2 { +- return tool.CommandLineErrorf("rename expects 2 arguments (position, new name)") +- } +- r.app.editFlags = &r.EditFlags +- conn, err := r.app.connect(ctx, nil) - if err != nil { - return err - } -- if !tmpModfile { -- return nil +- defer conn.terminate(ctx) +- +- from := parseSpan(args[0]) +- file, err := conn.openFile(ctx, from.URI()) +- if err != nil { +- return err - } -- modURI := snapshot.GoModForFile(uri) -- sumURI := span.URIFromPath(strings.TrimSuffix(modURI.Filename(), ".mod") + ".sum") -- modEdits, err := collectFileEdits(ctx, snapshot, modURI, newModBytes) +- loc, err := file.spanLocation(from) - if err != nil { - return err - } -- sumEdits, err := collectFileEdits(ctx, snapshot, sumURI, newSumBytes) +- p := protocol.RenameParams{ +- TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, +- Position: loc.Range.Start, +- NewName: args[1], +- } +- edit, err := conn.Rename(ctx, &p) - if err != nil { - return err - } -- return applyFileEdits(ctx, s.client, append(sumEdits, modEdits...)) +- return conn.client.applyWorkspaceEdit(edit) -} +diff -urN a/gopls/internal/cmd/semantictokens.go b/gopls/internal/cmd/semantictokens.go +--- a/gopls/internal/cmd/semantictokens.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/semantictokens.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,194 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// collectFileEdits collects any file edits required to transform the snapshot --// file specified by uri to the provided new content. --// --// If the file is not open, collectFileEdits simply writes the new content to --// disk. --// --// TODO(rfindley): fix this API asymmetry. It should be up to the caller to --// write the file or apply the edits. --func collectFileEdits(ctx context.Context, snapshot source.Snapshot, uri span.URI, newContent []byte) ([]protocol.TextDocumentEdit, error) { -- fh, err := snapshot.ReadFile(ctx, uri) -- if err != nil { -- return nil, err -- } -- oldContent, err := fh.Content() -- if err != nil && !os.IsNotExist(err) { -- return nil, err -- } +-package cmd - -- if bytes.Equal(oldContent, newContent) { -- return nil, nil -- } +-import ( +- "bytes" +- "context" +- "flag" +- "fmt" +- "log" +- "os" +- "unicode/utf8" - -- // Sending a workspace edit to a closed file causes VS Code to open the -- // file and leave it unsaved. We would rather apply the changes directly, -- // especially to go.sum, which should be mostly invisible to the user. -- if !snapshot.IsOpen(uri) { -- err := os.WriteFile(uri.Filename(), newContent, 0666) -- return nil, err -- } +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/settings" +-) - -- m := protocol.NewMapper(fh.URI(), oldContent) -- diff := snapshot.Options().ComputeEdits(string(oldContent), string(newContent)) -- edits, err := source.ToProtocolEdits(m, diff) -- if err != nil { -- return nil, err -- } -- return []protocol.TextDocumentEdit{{ -- TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{ -- Version: fh.Version(), -- TextDocumentIdentifier: protocol.TextDocumentIdentifier{ -- URI: protocol.URIFromSpanURI(uri), -- }, -- }, -- Edits: edits, -- }}, nil +-// generate semantic tokens and interpolate them in the file +- +-// The output is the input file decorated with comments showing the +-// syntactic tokens. The comments are stylized: +-// /*,,[ is the length of the token in runes, is one +-// of the supported semantic token types, and " } +-func (c *semtok) ShortHelp() string { return "show semantic tokens for the specified file" } +-func (c *semtok) DetailedHelp(f *flag.FlagSet) { +- fmt.Fprint(f.Output(), ` +-Example: show the semantic tokens for this file: +- +- $ gopls semtok internal/cmd/semtok.go +-`) +- printFlagDefaults(f) +-} +- +-// Run performs the semtok on the files specified by args and prints the +-// results to stdout in the format described above. +-func (c *semtok) Run(ctx context.Context, args ...string) error { +- if len(args) != 1 { +- return fmt.Errorf("expected one file name, got %d", len(args)) - } -- documentChanges := []protocol.DocumentChanges{} // must be a slice -- for _, change := range edits { -- change := change -- documentChanges = append(documentChanges, protocol.DocumentChanges{ -- TextDocumentEdit: &change, -- }) +- // perhaps simpler if app had just had a FlagSet member +- origOptions := c.app.options +- c.app.options = func(opts *settings.Options) { +- origOptions(opts) +- opts.SemanticTokens = true - } -- response, err := cli.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{ -- Edit: protocol.WorkspaceEdit{ -- DocumentChanges: documentChanges, -- }, -- }) +- conn, err := c.app.connect(ctx, nil) - if err != nil { - return err - } -- if !response.Applied { -- return fmt.Errorf("edits not applied because of %s", response.FailureReason) +- defer conn.terminate(ctx) +- uri := protocol.URIFromPath(args[0]) +- file, err := conn.openFile(ctx, uri) +- if err != nil { +- return err - } -- return nil --} - --func runGoGetModule(invoke func(...string) (*bytes.Buffer, error), addRequire bool, args []string) error { -- if addRequire { -- if err := addModuleRequire(invoke, args); err != nil { -- return err -- } +- lines := bytes.Split(file.mapper.Content, []byte{'\n'}) +- p := &protocol.SemanticTokensRangeParams{ +- TextDocument: protocol.TextDocumentIdentifier{ +- URI: uri, +- }, +- Range: protocol.Range{Start: protocol.Position{Line: 0, Character: 0}, +- End: protocol.Position{ +- Line: uint32(len(lines) - 1), +- Character: uint32(len(lines[len(lines)-1]))}, +- }, - } -- _, err := invoke(append([]string{"get", "-d"}, args...)...) -- return err +- resp, err := conn.semanticTokens(ctx, p) +- if err != nil { +- return err +- } +- return decorate(file, resp.Data) -} - --func addModuleRequire(invoke func(...string) (*bytes.Buffer, error), args []string) error { -- // Using go get to create a new dependency results in an -- // `// indirect` comment we may not want. The only way to avoid it -- // is to add the require as direct first. Then we can use go get to -- // update go.sum and tidy up. -- _, err := invoke(append([]string{"mod", "edit", "-require"}, args...)...) -- return err +-type mark struct { +- line, offset int // 1-based, from RangeSpan +- len int // bytes, not runes +- typ string +- mods []string -} - --func (s *Server) getUpgrades(ctx context.Context, snapshot source.Snapshot, uri span.URI, modules []string) (map[string]string, error) { -- stdout, err := snapshot.RunGoCommandDirect(ctx, source.Normal|source.AllowNetwork, &gocommand.Invocation{ -- Verb: "list", -- Args: append([]string{"-m", "-u", "-json"}, modules...), -- WorkingDir: filepath.Dir(uri.Filename()), -- ModFlag: "readonly", -- }) -- if err != nil { -- return nil, err -- } +-// prefixes for semantic token comments +-const ( +- SemanticLeft = "/*⇐" +- SemanticRight = "/*⇒" +-) - -- upgrades := map[string]string{} -- for dec := json.NewDecoder(stdout); dec.More(); { -- mod := &gocommand.ModuleJSON{} -- if err := dec.Decode(mod); err != nil { -- return nil, err -- } -- if mod.Update == nil { -- continue +-func markLine(m mark, lines [][]byte) { +- l := lines[m.line-1] // mx is 1-based +- length := utf8.RuneCount(l[m.offset-1 : m.offset-1+m.len]) +- splitAt := m.offset - 1 +- insert := "" +- if m.typ == "namespace" && m.offset-1+m.len < len(l) && l[m.offset-1+m.len] == '"' { +- // it is the last component of an import spec +- // cannot put a comment inside a string +- insert = fmt.Sprintf("%s%d,namespace,[]*/", SemanticLeft, length) +- splitAt = m.offset + m.len +- } else { +- // be careful not to generate //* +- spacer := "" +- if splitAt-1 >= 0 && l[splitAt-1] == '/' { +- spacer = " " - } -- upgrades[mod.Path] = mod.Update.Version +- insert = fmt.Sprintf("%s%s%d,%s,%v*/", spacer, SemanticRight, length, m.typ, m.mods) - } -- return upgrades, nil +- x := append([]byte(insert), l[splitAt:]...) +- l = append(l[:splitAt], x...) +- lines[m.line-1] = l -} - --func (c *commandHandler) GCDetails(ctx context.Context, uri protocol.DocumentURI) error { -- return c.ToggleGCDetails(ctx, command.URIArg{URI: uri}) +-func decorate(file *cmdFile, result []uint32) error { +- marks := newMarks(file, result) +- if len(marks) == 0 { +- return nil +- } +- lines := bytes.Split(file.mapper.Content, []byte{'\n'}) +- for i := len(marks) - 1; i >= 0; i-- { +- mx := marks[i] +- markLine(mx, lines) +- } +- os.Stdout.Write(bytes.Join(lines, []byte{'\n'})) +- return nil -} - --func (c *commandHandler) ToggleGCDetails(ctx context.Context, args command.URIArg) error { -- return c.run(ctx, commandConfig{ -- requireSave: true, -- progress: "Toggling GC Details", -- forURI: args.URI, -- }, func(ctx context.Context, deps commandDeps) error { -- meta, err := source.NarrowestMetadataForFile(ctx, deps.snapshot, deps.fh.URI()) -- if err != nil { -- return err -- } -- c.s.gcOptimizationDetailsMu.Lock() -- if _, ok := c.s.gcOptimizationDetails[meta.ID]; ok { -- delete(c.s.gcOptimizationDetails, meta.ID) -- c.s.clearDiagnosticSource(gcDetailsSource) -- } else { -- c.s.gcOptimizationDetails[meta.ID] = struct{}{} -- } -- c.s.gcOptimizationDetailsMu.Unlock() -- c.s.diagnoseSnapshot(deps.snapshot, nil, false, 0) -- return nil -- }) --} -- --func (c *commandHandler) ListKnownPackages(ctx context.Context, args command.URIArg) (command.ListKnownPackagesResult, error) { -- var result command.ListKnownPackagesResult -- err := c.run(ctx, commandConfig{ -- progress: "Listing packages", -- forURI: args.URI, -- }, func(ctx context.Context, deps commandDeps) error { -- pkgs, err := source.KnownPackagePaths(ctx, deps.snapshot, deps.fh) -- for _, pkg := range pkgs { -- result.Packages = append(result.Packages, string(pkg)) -- } -- return err -- }) -- return result, err --} -- --func (c *commandHandler) ListImports(ctx context.Context, args command.URIArg) (command.ListImportsResult, error) { -- var result command.ListImportsResult -- err := c.run(ctx, commandConfig{ -- forURI: args.URI, -- }, func(ctx context.Context, deps commandDeps) error { -- fh, err := deps.snapshot.ReadFile(ctx, args.URI.SpanURI()) -- if err != nil { -- return err -- } -- pgf, err := deps.snapshot.ParseGo(ctx, fh, source.ParseHeader) -- if err != nil { -- return err +-func newMarks(file *cmdFile, d []uint32) []mark { +- ans := []mark{} +- // the following two loops could be merged, at the cost +- // of making the logic slightly more complicated to understand +- // first, convert from deltas to absolute, in LSP coordinates +- lspLine := make([]uint32, len(d)/5) +- lspChar := make([]uint32, len(d)/5) +- var line, char uint32 +- for i := 0; 5*i < len(d); i++ { +- lspLine[i] = line + d[5*i+0] +- if d[5*i+0] > 0 { +- char = 0 - } -- fset := tokeninternal.FileSetFor(pgf.Tok) -- for _, group := range astutil.Imports(fset, pgf.File) { -- for _, imp := range group { -- if imp.Path == nil { -- continue -- } -- var name string -- if imp.Name != nil { -- name = imp.Name.Name -- } -- result.Imports = append(result.Imports, command.FileImport{ -- Path: string(source.UnquoteImportPath(imp)), -- Name: name, -- }) -- } +- lspChar[i] = char + d[5*i+1] +- char = lspChar[i] +- line = lspLine[i] +- } +- // second, convert to gopls coordinates +- for i := 0; 5*i < len(d); i++ { +- pr := protocol.Range{ +- Start: protocol.Position{ +- Line: lspLine[i], +- Character: lspChar[i], +- }, +- End: protocol.Position{ +- Line: lspLine[i], +- Character: lspChar[i] + d[5*i+2], +- }, - } -- meta, err := source.NarrowestMetadataForFile(ctx, deps.snapshot, args.URI.SpanURI()) +- spn, err := file.rangeSpan(pr) - if err != nil { -- return err // e.g. cancelled +- log.Fatal(err) - } -- for pkgPath := range meta.DepsByPkgPath { -- result.PackageImports = append(result.PackageImports, -- command.PackageImport{Path: string(pkgPath)}) +- m := mark{ +- line: spn.Start().Line(), +- offset: spn.Start().Column(), +- len: spn.End().Column() - spn.Start().Column(), +- typ: protocol.SemType(int(d[5*i+3])), +- mods: protocol.SemMods(int(d[5*i+4])), - } -- sort.Slice(result.PackageImports, func(i, j int) bool { -- return result.PackageImports[i].Path < result.PackageImports[j].Path -- }) -- return nil -- }) -- return result, err +- ans = append(ans, m) +- } +- return ans -} +diff -urN a/gopls/internal/cmd/serve.go b/gopls/internal/cmd/serve.go +--- a/gopls/internal/cmd/serve.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/serve.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,150 +0,0 @@ +-// Copyright 2018 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func (c *commandHandler) AddImport(ctx context.Context, args command.AddImportArgs) error { -- return c.run(ctx, commandConfig{ -- progress: "Adding import", -- forURI: args.URI, -- }, func(ctx context.Context, deps commandDeps) error { -- edits, err := source.AddImport(ctx, deps.snapshot, deps.fh, args.ImportPath) -- if err != nil { -- return fmt.Errorf("could not add import: %v", err) -- } -- if _, err := c.s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{ -- Edit: protocol.WorkspaceEdit{ -- DocumentChanges: documentChanges(deps.fh, edits), -- }, -- }); err != nil { -- return fmt.Errorf("could not apply import edits: %v", err) -- } -- return nil -- }) --} +-package cmd - --func (c *commandHandler) StartDebugging(ctx context.Context, args command.DebuggingArgs) (result command.DebuggingResult, _ error) { -- addr := args.Addr -- if addr == "" { -- addr = "localhost:0" -- } -- di := debug.GetInstance(ctx) -- if di == nil { -- return result, errors.New("internal error: server has no debugging instance") -- } -- listenedAddr, err := di.Serve(ctx, addr) -- if err != nil { -- return result, fmt.Errorf("starting debug server: %w", err) -- } -- result.URLs = []string{"http://" + listenedAddr} -- openClientBrowser(ctx, c.s.client, result.URLs[0]) -- return result, nil --} +-import ( +- "context" +- "errors" +- "flag" +- "fmt" +- "io" +- "log" +- "os" +- "time" - --func (c *commandHandler) StartProfile(ctx context.Context, args command.StartProfileArgs) (result command.StartProfileResult, _ error) { -- file, err := os.CreateTemp("", "gopls-profile-*") -- if err != nil { -- return result, fmt.Errorf("creating temp profile file: %v", err) -- } +- "golang.org/x/telemetry/upload" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/debug" +- "golang.org/x/tools/gopls/internal/lsprpc" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/fakenet" +- "golang.org/x/tools/internal/jsonrpc2" +- "golang.org/x/tools/internal/tool" +-) - -- c.s.ongoingProfileMu.Lock() -- defer c.s.ongoingProfileMu.Unlock() +-// Serve is a struct that exposes the configurable parts of the LSP server as +-// flags, in the right form for tool.Main to consume. +-type Serve struct { +- Logfile string `flag:"logfile" help:"filename to log to. if value is \"auto\", then logging to a default output file is enabled"` +- Mode string `flag:"mode" help:"no effect"` +- Port int `flag:"port" help:"port on which to run gopls for debugging purposes"` +- Address string `flag:"listen" help:"address on which to listen for remote connections. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. Otherwise, TCP is used."` +- IdleTimeout time.Duration `flag:"listen.timeout" help:"when used with -listen, shut down the server when there are no connected clients for this duration"` +- Trace bool `flag:"rpc.trace" help:"print the full rpc trace in lsp inspector format"` +- Debug string `flag:"debug" help:"serve debug information on the supplied address"` - -- if c.s.ongoingProfile != nil { -- file.Close() // ignore error -- return result, fmt.Errorf("profile already started (for %q)", c.s.ongoingProfile.Name()) -- } +- RemoteListenTimeout time.Duration `flag:"remote.listen.timeout" help:"when used with -remote=auto, the -listen.timeout value used to start the daemon"` +- RemoteDebug string `flag:"remote.debug" help:"when used with -remote=auto, the -debug value used to start the daemon"` +- RemoteLogfile string `flag:"remote.logfile" help:"when used with -remote=auto, the -logfile value used to start the daemon"` - -- if err := pprof.StartCPUProfile(file); err != nil { -- file.Close() // ignore error -- return result, fmt.Errorf("starting profile: %v", err) -- } +- app *Application +-} - -- c.s.ongoingProfile = file -- return result, nil +-func (s *Serve) Name() string { return "serve" } +-func (s *Serve) Parent() string { return s.app.Name() } +-func (s *Serve) Usage() string { return "[server-flags]" } +-func (s *Serve) ShortHelp() string { +- return "run a server for Go code using the Language Server Protocol" -} +-func (s *Serve) DetailedHelp(f *flag.FlagSet) { +- fmt.Fprint(f.Output(), ` gopls [flags] [server-flags] - --func (c *commandHandler) StopProfile(ctx context.Context, args command.StopProfileArgs) (result command.StopProfileResult, _ error) { -- c.s.ongoingProfileMu.Lock() -- defer c.s.ongoingProfileMu.Unlock() +-The server communicates using JSONRPC2 on stdin and stdout, and is intended to be run directly as +-a child of an editor process. - -- prof := c.s.ongoingProfile -- c.s.ongoingProfile = nil +-server-flags: +-`) +- printFlagDefaults(f) +-} - -- if prof == nil { -- return result, fmt.Errorf("no ongoing profile") +-func (s *Serve) remoteArgs(network, address string) []string { +- args := []string{"serve", +- "-listen", fmt.Sprintf(`%s;%s`, network, address), - } -- -- pprof.StopCPUProfile() -- if err := prof.Close(); err != nil { -- return result, fmt.Errorf("closing profile file: %v", err) +- if s.RemoteDebug != "" { +- args = append(args, "-debug", s.RemoteDebug) - } -- result.File = prof.Name() -- return result, nil --} -- --// Copy of pkgLoadConfig defined in internal/lsp/cmd/vulncheck.go --// TODO(hyangah): decide where to define this. --type pkgLoadConfig struct { -- // BuildFlags is a list of command-line flags to be passed through to -- // the build system's query tool. -- BuildFlags []string -- -- // If Tests is set, the loader includes related test packages. -- Tests bool +- if s.RemoteListenTimeout != 0 { +- args = append(args, "-listen.timeout", s.RemoteListenTimeout.String()) +- } +- if s.RemoteLogfile != "" { +- args = append(args, "-logfile", s.RemoteLogfile) +- } +- return args -} - --func (c *commandHandler) FetchVulncheckResult(ctx context.Context, arg command.URIArg) (map[protocol.DocumentURI]*vulncheck.Result, error) { -- ret := map[protocol.DocumentURI]*vulncheck.Result{} -- err := c.run(ctx, commandConfig{forURI: arg.URI}, func(ctx context.Context, deps commandDeps) error { -- if deps.snapshot.Options().Vulncheck == source.ModeVulncheckImports { -- for _, modfile := range deps.snapshot.ModFiles() { -- res, err := deps.snapshot.ModVuln(ctx, modfile) -- if err != nil { -- return err -- } -- ret[protocol.URIFromSpanURI(modfile)] = res -- } -- } -- // Overwrite if there is any govulncheck-based result. -- for modfile, result := range deps.snapshot.View().Vulnerabilities() { -- ret[protocol.URIFromSpanURI(modfile)] = result -- } -- return nil -- }) -- return ret, err --} +-// Run configures a server based on the flags, and then runs it. +-// It blocks until the server shuts down. +-func (s *Serve) Run(ctx context.Context, args ...string) error { +- // TODO(adonovan): eliminate this once telemetry.Start has this effect. +- go upload.Run(nil) // start telemetry uploader - --func (c *commandHandler) RunGovulncheck(ctx context.Context, args command.VulncheckArgs) (command.RunVulncheckResult, error) { -- if args.URI == "" { -- return command.RunVulncheckResult{}, errors.New("VulncheckArgs is missing URI field") +- if len(args) > 0 { +- return tool.CommandLineErrorf("server does not take arguments, got %v", args) - } - -- // Return the workdone token so that clients can identify when this -- // vulncheck invocation is complete. -- // -- // Since the run function executes asynchronously, we use a channel to -- // synchronize the start of the run and return the token. -- tokenChan := make(chan protocol.ProgressToken, 1) -- err := c.run(ctx, commandConfig{ -- async: true, // need to be async to be cancellable -- progress: "govulncheck", -- requireSave: true, -- forURI: args.URI, -- }, func(ctx context.Context, deps commandDeps) error { -- tokenChan <- deps.work.Token() -- -- workDoneWriter := progress.NewWorkDoneWriter(ctx, deps.work) -- dir := filepath.Dir(args.URI.SpanURI().Filename()) -- pattern := args.Pattern -- -- result, err := scan.RunGovulncheck(ctx, pattern, deps.snapshot, dir, workDoneWriter) +- di := debug.GetInstance(ctx) +- isDaemon := s.Address != "" || s.Port != 0 +- if di != nil { +- closeLog, err := di.SetLogFile(s.Logfile, isDaemon) - if err != nil { - return err - } -- -- deps.snapshot.View().SetVulnerabilities(args.URI.SpanURI(), result) -- c.s.diagnoseSnapshot(deps.snapshot, nil, false, 0) -- -- affecting := make(map[string]bool, len(result.Entries)) -- for _, finding := range result.Findings { -- if len(finding.Trace) > 1 { // at least 2 frames if callstack exists (vulnerability, entry) -- affecting[finding.OSV] = true -- } -- } -- if len(affecting) == 0 { -- return c.s.client.ShowMessage(ctx, &protocol.ShowMessageParams{ -- Type: protocol.Info, -- Message: "No vulnerabilities found", -- }) -- } -- affectingOSVs := make([]string, 0, len(affecting)) -- for id := range affecting { -- affectingOSVs = append(affectingOSVs, id) +- defer closeLog() +- di.ServerAddress = s.Address +- di.Serve(ctx, s.Debug) +- } +- var ss jsonrpc2.StreamServer +- if s.app.Remote != "" { +- var err error +- ss, err = lsprpc.NewForwarder(s.app.Remote, s.remoteArgs) +- if err != nil { +- return fmt.Errorf("creating forwarder: %w", err) - } -- sort.Strings(affectingOSVs) -- return c.s.client.ShowMessage(ctx, &protocol.ShowMessageParams{ -- Type: protocol.Warning, -- Message: fmt.Sprintf("Found %v", strings.Join(affectingOSVs, ", ")), -- }) -- }) -- if err != nil { -- return command.RunVulncheckResult{}, err +- } else { +- ss = lsprpc.NewStreamServer(cache.New(nil), isDaemon, s.app.options) - } -- select { -- case <-ctx.Done(): -- return command.RunVulncheckResult{}, ctx.Err() -- case token := <-tokenChan: -- return command.RunVulncheckResult{Token: token}, nil +- +- var network, addr string +- if s.Address != "" { +- network, addr = lsprpc.ParseAddr(s.Address) +- } +- if s.Port != 0 { +- network = "tcp" +- // TODO(adonovan): should gopls ever be listening on network +- // sockets, or only local ones? +- // +- // Ian says this was added in anticipation of +- // something related to "VS Code remote" that turned +- // out to be unnecessary. So I propose we limit it to +- // localhost, if only so that we avoid the macOS +- // firewall prompt. +- // +- // Hana says: "s.Address is for the remote access (LSP) +- // and s.Port is for debugging purpose (according to +- // the Server type documentation). I am not sure why the +- // existing code here is mixing up and overwriting addr. +- // For debugging endpoint, I think localhost makes perfect sense." +- // +- // TODO(adonovan): disentangle Address and Port, +- // and use only localhost for the latter. +- addr = fmt.Sprintf(":%v", s.Port) +- } +- if addr != "" { +- log.Printf("Gopls daemon: listening on %s network, address %s...", network, addr) +- defer log.Printf("Gopls daemon: exiting") +- return jsonrpc2.ListenAndServe(ctx, network, addr, ss, s.IdleTimeout) +- } +- stream := jsonrpc2.NewHeaderStream(fakenet.NewConn("stdio", os.Stdin, os.Stdout)) +- if s.Trace && di != nil { +- stream = protocol.LoggingStream(stream, di.LogWriter) +- } +- conn := jsonrpc2.NewConn(stream) +- err := ss.ServeStream(ctx, conn) +- if errors.Is(err, io.EOF) { +- return nil - } +- return err -} +diff -urN a/gopls/internal/cmd/signature.go b/gopls/internal/cmd/signature.go +--- a/gopls/internal/cmd/signature.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/signature.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,87 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// MemStats implements the MemStats command. It returns an error as a --// future-proof API, but the resulting error is currently always nil. --func (c *commandHandler) MemStats(ctx context.Context) (command.MemStatsResult, error) { -- // GC a few times for stable results. -- runtime.GC() -- runtime.GC() -- runtime.GC() -- var m runtime.MemStats -- runtime.ReadMemStats(&m) -- return command.MemStatsResult{ -- HeapAlloc: m.HeapAlloc, -- HeapInUse: m.HeapInuse, -- TotalAlloc: m.TotalAlloc, -- }, nil +-package cmd +- +-import ( +- "context" +- "flag" +- "fmt" +- +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/tool" +-) +- +-// signature implements the signature verb for gopls +-type signature struct { +- app *Application -} - --// WorkspaceStats implements the WorkspaceStats command, reporting information --// about the current state of the loaded workspace for the current session. --func (c *commandHandler) WorkspaceStats(ctx context.Context) (command.WorkspaceStatsResult, error) { -- var res command.WorkspaceStatsResult -- res.Files.Total, res.Files.Largest, res.Files.Errs = c.s.session.Cache().FileStats() +-func (r *signature) Name() string { return "signature" } +-func (r *signature) Parent() string { return r.app.Name() } +-func (r *signature) Usage() string { return "" } +-func (r *signature) ShortHelp() string { return "display selected identifier's signature" } +-func (r *signature) DetailedHelp(f *flag.FlagSet) { +- fmt.Fprint(f.Output(), ` +-Example: - -- for _, view := range c.s.session.Views() { -- vs, err := collectViewStats(ctx, view) -- if err != nil { -- return res, err -- } -- res.Views = append(res.Views, vs) -- } -- return res, nil +- $ # 1-indexed location (:line:column or :#offset) of the target identifier +- $ gopls signature helper/helper.go:8:6 +- $ gopls signature helper/helper.go:#53 +-`) +- printFlagDefaults(f) -} - --func collectViewStats(ctx context.Context, view *cache.View) (command.ViewStats, error) { -- s, release, err := view.Snapshot() +-func (r *signature) Run(ctx context.Context, args ...string) error { +- if len(args) != 1 { +- return tool.CommandLineErrorf("signature expects 1 argument (position)") +- } +- +- conn, err := r.app.connect(ctx, nil) - if err != nil { -- return command.ViewStats{}, err +- return err - } -- defer release() +- defer conn.terminate(ctx) - -- allMD, err := s.AllMetadata(ctx) +- from := parseSpan(args[0]) +- file, err := conn.openFile(ctx, from.URI()) - if err != nil { -- return command.ViewStats{}, err +- return err - } -- allPackages := collectPackageStats(allMD) - -- wsMD, err := s.WorkspaceMetadata(ctx) +- loc, err := file.spanLocation(from) - if err != nil { -- return command.ViewStats{}, err +- return err - } -- workspacePackages := collectPackageStats(wsMD) - -- var ids []source.PackageID -- for _, m := range wsMD { -- ids = append(ids, m.ID) +- p := protocol.SignatureHelpParams{ +- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), - } - -- diags, err := s.PackageDiagnostics(ctx, ids...) +- s, err := conn.SignatureHelp(ctx, &p) - if err != nil { -- return command.ViewStats{}, err +- return err - } - -- ndiags := 0 -- for _, d := range diags { -- ndiags += len(d) +- if s == nil || len(s.Signatures) == 0 { +- return tool.CommandLineErrorf("%v: not a function", from) - } - -- return command.ViewStats{ -- GoCommandVersion: view.GoVersionString(), -- AllPackages: allPackages, -- WorkspacePackages: workspacePackages, -- Diagnostics: ndiags, -- }, nil +- // there is only ever one possible signature, +- // see toProtocolSignatureHelp in lsp/signature_help.go +- signature := s.Signatures[0] +- fmt.Printf("%s\n", signature.Label) +- switch x := signature.Documentation.Value.(type) { +- case string: +- if x != "" { +- fmt.Printf("\n%s\n", x) +- } +- case protocol.MarkupContent: +- if x.Value != "" { +- fmt.Printf("\n%s\n", x.Value) +- } +- } +- +- return nil -} +diff -urN a/gopls/internal/cmd/spanformat_test.go b/gopls/internal/cmd/spanformat_test.go +--- a/gopls/internal/cmd/spanformat_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/spanformat_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,55 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func collectPackageStats(md []*source.Metadata) command.PackageStats { -- var stats command.PackageStats -- stats.Packages = len(md) -- modules := make(map[string]bool) +-package cmd - -- for _, m := range md { -- n := len(m.CompiledGoFiles) -- stats.CompiledGoFiles += n -- if n > stats.LargestPackage { -- stats.LargestPackage = n +-import ( +- "fmt" +- "path/filepath" +- "strings" +- "testing" +-) +- +-func TestSpanFormat(t *testing.T) { +- formats := []string{"%v", "%#v", "%+v"} +- +- // Element 0 is the input, and the elements 0-2 are the expected +- // output in [%v %#v %+v] formats. Thus the first must be in +- // canonical form (invariant under parseSpan + fmt.Sprint). +- // The '#' form displays offsets; the '+' form outputs a URI. +- // If len=4, element 0 is a noncanonical input and 1-3 are expected outputs. +- for _, test := range [][]string{ +- {"C:/file_a", "C:/file_a", "file:///C:/file_a:#0"}, +- {"C:/file_b:1:2", "C:/file_b:1:2", "file:///C:/file_b:1:2"}, +- {"C:/file_c:1000", "C:/file_c:1000", "file:///C:/file_c:1000:1"}, +- {"C:/file_d:14:9", "C:/file_d:14:9", "file:///C:/file_d:14:9"}, +- {"C:/file_e:1:2-7", "C:/file_e:1:2-7", "file:///C:/file_e:1:2-1:7"}, +- {"C:/file_f:500-502", "C:/file_f:500-502", "file:///C:/file_f:500:1-502:1"}, +- {"C:/file_g:3:7-8", "C:/file_g:3:7-8", "file:///C:/file_g:3:7-3:8"}, +- {"C:/file_h:3:7-4:8", "C:/file_h:3:7-4:8", "file:///C:/file_h:3:7-4:8"}, +- {"C:/file_i:#100", "C:/file_i:#100", "file:///C:/file_i:#100"}, +- {"C:/file_j:#26-#28", "C:/file_j:#26-#28", "file:///C:/file_j:#26-0#28"}, // 0#28? +- {"C:/file_h:3:7#26-4:8#37", // not canonical +- "C:/file_h:3:7-4:8", "C:/file_h:#26-#37", "file:///C:/file_h:3:7#26-4:8#37"}} { +- input := test[0] +- spn := parseSpan(input) +- wants := test[0:3] +- if len(test) == 4 { +- wants = test[1:4] - } -- if m.Module != nil { -- modules[m.Module.Path] = true +- for i, format := range formats { +- want := toPath(wants[i]) +- if got := fmt.Sprintf(format, spn); got != want { +- t.Errorf("Sprintf(%q, %q) = %q, want %q", format, input, got, want) +- } - } - } -- stats.Modules = len(modules) +-} - -- return stats +-func toPath(value string) string { +- if strings.HasPrefix(value, "file://") { +- return value +- } +- return filepath.FromSlash(value) -} +diff -urN a/gopls/internal/cmd/span.go b/gopls/internal/cmd/span.go +--- a/gopls/internal/cmd/span.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/span.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,238 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// RunGoWorkCommand invokes `go work ` with the provided arguments. --// --// args.InitFirst controls whether to first run `go work init`. This allows a --// single command to both create and recursively populate a go.work file -- as --// of writing there is no `go work init -r`. --// --// Some thought went into implementing this command. Unlike the go.mod commands --// above, this command simply invokes the go command and relies on the client --// to notify gopls of file changes via didChangeWatchedFile notifications. --// We could instead run these commands with GOWORK set to a temp file, but that --// poses the following problems: --// - directory locations in the resulting temp go.work file will be computed --// relative to the directory containing that go.work. If the go.work is in a --// tempdir, the directories will need to be translated to/from that dir. --// - it would be simpler to use a temp go.work file in the workspace --// directory, or whichever directory contains the real go.work file, but --// that sets a bad precedent of writing to a user-owned directory. We --// shouldn't start doing that. --// - Sending workspace edits to create a go.work file would require using --// the CreateFile resource operation, which would need to be tested in every --// client as we haven't used it before. We don't have time for that right --// now. --// --// Therefore, we simply require that the current go.work file is saved (if it --// exists), and delegate to the go command. --func (c *commandHandler) RunGoWorkCommand(ctx context.Context, args command.RunGoWorkArgs) error { -- return c.run(ctx, commandConfig{ -- progress: "Running go work command", -- forView: args.ViewID, -- }, func(ctx context.Context, deps commandDeps) (runErr error) { -- snapshot := deps.snapshot -- view := snapshot.View().(*cache.View) -- viewDir := view.Folder().Filename() +-package cmd - -- // If the user has explicitly set GOWORK=off, we should warn them -- // explicitly and avoid potentially misleading errors below. -- goworkURI, off := view.GOWORK() -- if off { -- return fmt.Errorf("cannot modify go.work files when GOWORK=off") -- } -- gowork := goworkURI.Filename() +-// span and point represent positions and ranges in text files. - -- if goworkURI != "" { -- fh, err := snapshot.ReadFile(ctx, goworkURI) -- if err != nil { -- return fmt.Errorf("reading current go.work file: %v", err) -- } -- if !fh.SameContentsOnDisk() { -- return fmt.Errorf("must save workspace file %s before running go work commands", goworkURI) -- } -- } else { -- if !args.InitFirst { -- // If go.work does not exist, we should have detected that and asked -- // for InitFirst. -- return bug.Errorf("internal error: cannot run go work command: required go.work file not found") -- } -- gowork = filepath.Join(viewDir, "go.work") -- if err := c.invokeGoWork(ctx, viewDir, gowork, []string{"init"}); err != nil { -- return fmt.Errorf("running `go work init`: %v", err) -- } -- } +-import ( +- "encoding/json" +- "fmt" +- "path" +- "sort" +- "strings" - -- return c.invokeGoWork(ctx, viewDir, gowork, args.Args) -- }) --} +- "golang.org/x/tools/gopls/internal/protocol" +-) - --func (c *commandHandler) invokeGoWork(ctx context.Context, viewDir, gowork string, args []string) error { -- inv := gocommand.Invocation{ -- Verb: "work", -- Args: args, -- WorkingDir: viewDir, -- Env: append(os.Environ(), fmt.Sprintf("GOWORK=%s", gowork)), -- } -- if _, err := c.s.session.GoCommandRunner().Run(ctx, inv); err != nil { -- return fmt.Errorf("running go work command: %v", err) -- } -- return nil +-// A span represents a range of text within a source file. The start +-// and end points of a valid span may be hold either its byte offset, +-// or its (line, column) pair, or both. Columns are measured in bytes. +-// +-// Spans are appropriate in user interfaces (e.g. command-line tools) +-// and tests where a position is notated without access to the content +-// of the file. +-// +-// Use protocol.Mapper to convert between span and other +-// representations, such as go/token (also UTF-8) or the LSP protocol +-// (UTF-16). The latter requires access to file contents. +-// +-// See overview comments at ../protocol/mapper.go. +-type span struct { +- v _span -} - --// openClientBrowser causes the LSP client to open the specified URL --// in an external browser. --func openClientBrowser(ctx context.Context, cli protocol.Client, url protocol.URI) { -- showDocumentImpl(ctx, cli, url, nil) +-// point represents a single point within a file. +-// In general this should only be used as part of a span, as on its own it +-// does not carry enough information. +-type point struct { +- v _point -} - --// openClientEditor causes the LSP client to open the specified document --// and select the indicated range. --func openClientEditor(ctx context.Context, cli protocol.Client, loc protocol.Location) { -- showDocumentImpl(ctx, cli, protocol.URI(loc.URI), &loc.Range) +-// The span_/point_ types have public fields to support JSON encoding, +-// but the span/point types hide these fields by defining methods that +-// shadow them. (This is used by a few of the command-line tool +-// subcommands, which emit spans and have a -json flag.) +-// +-// TODO(adonovan): simplify now that it's all internal to cmd. +- +-type _span struct { +- URI protocol.DocumentURI `json:"uri"` +- Start _point `json:"start"` +- End _point `json:"end"` -} - --func showDocumentImpl(ctx context.Context, cli protocol.Client, url protocol.URI, rangeOpt *protocol.Range) { -- // In principle we shouldn't send a showDocument request to a -- // client that doesn't support it, as reported by -- // ShowDocumentClientCapabilities. But even clients that do -- // support it may defer the real work of opening the document -- // asynchronously, to avoid deadlocks due to rentrancy. -- // -- // For example: client sends request to server; server sends -- // showDocument to client; client opens editor; editor causes -- // new RPC to be sent to server, which is still busy with -- // previous request. (This happens in eglot.) -- // -- // So we can't rely on the success/failure information. -- // That's the reason this function doesn't return an error. +-type _point struct { +- Line int `json:"line"` // 1-based line number +- Column int `json:"column"` // 1-based, UTF-8 codes (bytes) +- Offset int `json:"offset"` // 0-based byte offset +-} - -- // "External" means run the system-wide handler (e.g. open(1) -- // on macOS or xdg-open(1) on Linux) for this URL, ignoring -- // TakeFocus and Selection. Note that this may still end up -- // opening the same editor (e.g. VSCode) for a file: URL. -- res, err := cli.ShowDocument(ctx, &protocol.ShowDocumentParams{ -- URI: url, -- External: rangeOpt == nil, -- TakeFocus: true, -- Selection: rangeOpt, // optional -- }) -- if err != nil { -- event.Error(ctx, "client.showDocument: %v", err) -- } else if res != nil && !res.Success { -- event.Log(ctx, fmt.Sprintf("client declined to open document %v", url)) -- } +-func newSpan(uri protocol.DocumentURI, start, end point) span { +- s := span{v: _span{URI: uri, Start: start.v, End: end.v}} +- s.v.clean() +- return s -} - --func (c *commandHandler) ChangeSignature(ctx context.Context, args command.ChangeSignatureArgs) error { -- return c.run(ctx, commandConfig{ -- forURI: args.RemoveParameter.URI, -- }, func(ctx context.Context, deps commandDeps) error { -- // For now, gopls only supports removing unused parameters. -- changes, err := source.RemoveUnusedParameter(ctx, deps.fh, args.RemoveParameter.Range, deps.snapshot) -- if err != nil { -- return err -- } -- r, err := c.s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{ -- Edit: protocol.WorkspaceEdit{ -- DocumentChanges: changes, -- }, -- }) -- if !r.Applied { -- return fmt.Errorf("failed to apply edits: %v", r.FailureReason) -- } +-func newPoint(line, col, offset int) point { +- p := point{v: _point{Line: line, Column: col, Offset: offset}} +- p.v.clean() +- return p +-} - -- return nil +-// sortSpans sorts spans into a stable but unspecified order. +-func sortSpans(spans []span) { +- sort.SliceStable(spans, func(i, j int) bool { +- return compare(spans[i], spans[j]) < 0 - }) -} -diff -urN a/gopls/internal/lsp/completion.go b/gopls/internal/lsp/completion.go ---- a/gopls/internal/lsp/completion.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/completion.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,149 +0,0 @@ --// Copyright 2018 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package lsp -- --import ( -- "context" -- "fmt" -- "strings" -- -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/lsp/source/completion" -- "golang.org/x/tools/gopls/internal/lsp/template" -- "golang.org/x/tools/gopls/internal/lsp/work" -- "golang.org/x/tools/gopls/internal/telemetry" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/tag" --) - --func (s *Server) completion(ctx context.Context, params *protocol.CompletionParams) (_ *protocol.CompletionList, rerr error) { -- recordLatency := telemetry.StartLatencyTimer("completion") -- defer func() { -- recordLatency(ctx, rerr) -- }() -- -- ctx, done := event.Start(ctx, "lsp.Server.completion", tag.URI.Of(params.TextDocument.URI)) -- defer done() -- -- snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind) -- defer release() -- if !ok { -- return nil, err +-// compare implements a three-valued ordered comparison of Spans. +-func compare(a, b span) int { +- // This is a textual comparison. It does not perform path +- // cleaning, case folding, resolution of symbolic links, +- // testing for existence, or any I/O. +- if cmp := strings.Compare(string(a.URI()), string(b.URI())); cmp != 0 { +- return cmp - } -- var candidates []completion.CompletionItem -- var surrounding *completion.Selection -- switch snapshot.FileKind(fh) { -- case source.Go: -- candidates, surrounding, err = completion.Completion(ctx, snapshot, fh, params.Position, params.Context) -- case source.Mod: -- candidates, surrounding = nil, nil -- case source.Work: -- cl, err := work.Completion(ctx, snapshot, fh, params.Position) -- if err != nil { -- break +- if cmp := comparePoint(a.v.Start, b.v.Start); cmp != 0 { +- return cmp +- } +- return comparePoint(a.v.End, b.v.End) +-} +- +-func comparePoint(a, b _point) int { +- if !a.hasPosition() { +- if a.Offset < b.Offset { +- return -1 - } -- return cl, nil -- case source.Tmpl: -- var cl *protocol.CompletionList -- cl, err = template.Completion(ctx, snapshot, fh, params.Position, params.Context) -- if err != nil { -- break // use common error handling, candidates==nil +- if a.Offset > b.Offset { +- return 1 - } -- return cl, nil +- return 0 - } -- if err != nil { -- event.Error(ctx, "no completions found", err, tag.Position.Of(params.Position)) +- if a.Line < b.Line { +- return -1 - } -- if candidates == nil { -- return &protocol.CompletionList{ -- IsIncomplete: true, -- Items: []protocol.CompletionItem{}, -- }, nil +- if a.Line > b.Line { +- return 1 - } -- -- rng, err := surrounding.Range() -- if err != nil { -- return nil, err +- if a.Column < b.Column { +- return -1 - } +- if a.Column > b.Column { +- return 1 +- } +- return 0 +-} - -- // When using deep completions/fuzzy matching, report results as incomplete so -- // client fetches updated completions after every key stroke. -- options := snapshot.Options() -- incompleteResults := options.DeepCompletion || options.Matcher == source.Fuzzy -- -- items := toProtocolCompletionItems(candidates, rng, options) -- -- return &protocol.CompletionList{ -- IsIncomplete: incompleteResults, -- Items: items, -- }, nil +-func (s span) HasPosition() bool { return s.v.Start.hasPosition() } +-func (s span) HasOffset() bool { return s.v.Start.hasOffset() } +-func (s span) IsValid() bool { return s.v.Start.isValid() } +-func (s span) IsPoint() bool { return s.v.Start == s.v.End } +-func (s span) URI() protocol.DocumentURI { return s.v.URI } +-func (s span) Start() point { return point{s.v.Start} } +-func (s span) End() point { return point{s.v.End} } +-func (s *span) MarshalJSON() ([]byte, error) { return json.Marshal(&s.v) } +-func (s *span) UnmarshalJSON(b []byte) error { return json.Unmarshal(b, &s.v) } +- +-func (p point) HasPosition() bool { return p.v.hasPosition() } +-func (p point) HasOffset() bool { return p.v.hasOffset() } +-func (p point) IsValid() bool { return p.v.isValid() } +-func (p *point) MarshalJSON() ([]byte, error) { return json.Marshal(&p.v) } +-func (p *point) UnmarshalJSON(b []byte) error { return json.Unmarshal(b, &p.v) } +-func (p point) Line() int { +- if !p.v.hasPosition() { +- panic(fmt.Errorf("position not set in %v", p.v)) +- } +- return p.v.Line +-} +-func (p point) Column() int { +- if !p.v.hasPosition() { +- panic(fmt.Errorf("position not set in %v", p.v)) +- } +- return p.v.Column +-} +-func (p point) Offset() int { +- if !p.v.hasOffset() { +- panic(fmt.Errorf("offset not set in %v", p.v)) +- } +- return p.v.Offset -} - --func toProtocolCompletionItems(candidates []completion.CompletionItem, rng protocol.Range, options *source.Options) []protocol.CompletionItem { -- var ( -- items = make([]protocol.CompletionItem, 0, len(candidates)) -- numDeepCompletionsSeen int -- ) -- for i, candidate := range candidates { -- // Limit the number of deep completions to not overwhelm the user in cases -- // with dozens of deep completion matches. -- if candidate.Depth > 0 { -- if !options.DeepCompletion { -- continue -- } -- if numDeepCompletionsSeen >= completion.MaxDeepCompletions { -- continue -- } -- numDeepCompletionsSeen++ -- } -- insertText := candidate.InsertText -- if options.InsertTextFormat == protocol.SnippetTextFormat { -- insertText = candidate.Snippet() -- } +-func (p _point) hasPosition() bool { return p.Line > 0 } +-func (p _point) hasOffset() bool { return p.Offset >= 0 } +-func (p _point) isValid() bool { return p.hasPosition() || p.hasOffset() } +-func (p _point) isZero() bool { +- return (p.Line == 1 && p.Column == 1) || (!p.hasPosition() && p.Offset == 0) +-} - -- // This can happen if the client has snippets disabled but the -- // candidate only supports snippet insertion. -- if insertText == "" { -- continue -- } +-func (s *_span) clean() { +- //this presumes the points are already clean +- if !s.End.isValid() || (s.End == _point{}) { +- s.End = s.Start +- } +-} - -- doc := &protocol.Or_CompletionItem_documentation{ -- Value: protocol.MarkupContent{ -- Kind: protocol.Markdown, -- Value: source.CommentToMarkdown(candidate.Documentation, options), -- }, -- } -- if options.PreferredContentFormat != protocol.Markdown { -- doc.Value = candidate.Documentation +-func (p *_point) clean() { +- if p.Line < 0 { +- p.Line = 0 +- } +- if p.Column <= 0 { +- if p.Line > 0 { +- p.Column = 1 +- } else { +- p.Column = 0 - } -- item := protocol.CompletionItem{ -- Label: candidate.Label, -- Detail: candidate.Detail, -- Kind: candidate.Kind, -- TextEdit: &protocol.TextEdit{ -- NewText: insertText, -- Range: rng, -- }, -- InsertTextFormat: &options.InsertTextFormat, -- AdditionalTextEdits: candidate.AdditionalTextEdits, -- // This is a hack so that the client sorts completion results in the order -- // according to their score. This can be removed upon the resolution of -- // https://github.com/Microsoft/language-server-protocol/issues/348. -- SortText: fmt.Sprintf("%05d", i), -- -- // Trim operators (VSCode doesn't like weird characters in -- // filterText). -- FilterText: strings.TrimLeft(candidate.InsertText, "&*"), +- } +- if p.Offset == 0 && (p.Line > 1 || p.Column > 1) { +- p.Offset = -1 +- } +-} - -- Preselect: i == 0, -- Documentation: doc, -- Tags: nonNilSliceCompletionItemTag(candidate.Tags), -- Deprecated: candidate.Deprecated, +-// Format implements fmt.Formatter to print the Location in a standard form. +-// The format produced is one that can be read back in using parseSpan. +-// +-// TODO(adonovan): this is esoteric, and the formatting options are +-// never used outside of TestFormat. Replace with something simpler +-// along the lines of MappedRange.String. +-func (s span) Format(f fmt.State, c rune) { +- fullForm := f.Flag('+') +- preferOffset := f.Flag('#') +- // we should always have a uri, simplify if it is file format +- //TODO: make sure the end of the uri is unambiguous +- uri := string(s.v.URI) +- if c == 'f' { +- uri = path.Base(uri) +- } else if !fullForm { +- uri = s.v.URI.Path() +- } +- fmt.Fprint(f, uri) +- if !s.IsValid() || (!fullForm && s.v.Start.isZero() && s.v.End.isZero()) { +- return +- } +- // see which bits of start to write +- printOffset := s.HasOffset() && (fullForm || preferOffset || !s.HasPosition()) +- printLine := s.HasPosition() && (fullForm || !printOffset) +- printColumn := printLine && (fullForm || (s.v.Start.Column > 1 || s.v.End.Column > 1)) +- fmt.Fprint(f, ":") +- if printLine { +- fmt.Fprintf(f, "%d", s.v.Start.Line) +- } +- if printColumn { +- fmt.Fprintf(f, ":%d", s.v.Start.Column) +- } +- if printOffset { +- fmt.Fprintf(f, "#%d", s.v.Start.Offset) +- } +- // start is written, do we need end? +- if s.IsPoint() { +- return +- } +- // we don't print the line if it did not change +- printLine = fullForm || (printLine && s.v.End.Line > s.v.Start.Line) +- fmt.Fprint(f, "-") +- if printLine { +- fmt.Fprintf(f, "%d", s.v.End.Line) +- } +- if printColumn { +- if printLine { +- fmt.Fprint(f, ":") - } -- items = append(items, item) +- fmt.Fprintf(f, "%d", s.v.End.Column) +- } +- if printOffset { +- fmt.Fprintf(f, "#%d", s.v.End.Offset) - } -- return items -} -diff -urN a/gopls/internal/lsp/debug/info.go b/gopls/internal/lsp/debug/info.go ---- a/gopls/internal/lsp/debug/info.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/debug/info.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,159 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/cmd/stats.go b/gopls/internal/cmd/stats.go +--- a/gopls/internal/cmd/stats.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/stats.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,274 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --// Package debug exports debug information for gopls. --package debug +-package cmd - -import ( - "context" - "encoding/json" +- "flag" - "fmt" -- "io" +- "go/token" +- "io/fs" - "os" +- "path/filepath" +- "reflect" - "runtime" -- "runtime/debug" - "strings" --) -- --type PrintMode int +- "sync" +- "time" - --const ( -- PlainText = PrintMode(iota) -- Markdown -- HTML -- JSON +- "golang.org/x/tools/gopls/internal/filecache" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/command" +- "golang.org/x/tools/gopls/internal/server" +- "golang.org/x/tools/gopls/internal/settings" +- bugpkg "golang.org/x/tools/gopls/internal/util/bug" +- versionpkg "golang.org/x/tools/gopls/internal/version" +- "golang.org/x/tools/internal/event" -) - --// Version is a manually-updated mechanism for tracking versions. --func Version() string { -- if info, ok := debug.ReadBuildInfo(); ok { -- if info.Main.Version != "" { -- return info.Main.Version -- } -- } -- return "(unknown)" --} +-type stats struct { +- app *Application - --// ServerVersion is the format used by gopls to report its version to the --// client. This format is structured so that the client can parse it easily. --type ServerVersion struct { -- *debug.BuildInfo -- Version string +- Anon bool `flag:"anon" help:"hide any fields that may contain user names, file names, or source code"` -} - --// VersionInfo returns the build info for the gopls process. If it was not --// built in module mode, we return a GOPATH-specific message with the --// hardcoded version. --func VersionInfo() *ServerVersion { -- if info, ok := debug.ReadBuildInfo(); ok { -- return &ServerVersion{ -- Version: Version(), -- BuildInfo: info, -- } -- } -- return &ServerVersion{ -- Version: Version(), -- BuildInfo: &debug.BuildInfo{ -- Path: "gopls, built in GOPATH mode", -- GoVersion: runtime.Version(), -- }, -- } --} +-func (s *stats) Name() string { return "stats" } +-func (r *stats) Parent() string { return r.app.Name() } +-func (s *stats) Usage() string { return "" } +-func (s *stats) ShortHelp() string { return "print workspace statistics" } - --// PrintServerInfo writes HTML debug info to w for the Instance. --func (i *Instance) PrintServerInfo(ctx context.Context, w io.Writer) { -- section(w, HTML, "Server Instance", func() { -- fmt.Fprintf(w, "Start time: %v\n", i.StartTime) -- fmt.Fprintf(w, "LogFile: %s\n", i.Logfile) -- fmt.Fprintf(w, "pid: %d\n", os.Getpid()) -- fmt.Fprintf(w, "Working directory: %s\n", i.Workdir) -- fmt.Fprintf(w, "Address: %s\n", i.ServerAddress) -- fmt.Fprintf(w, "Debug address: %s\n", i.DebugAddress()) -- }) -- PrintVersionInfo(ctx, w, true, HTML) -- section(w, HTML, "Command Line", func() { -- fmt.Fprintf(w, "cmdline") -- }) --} +-func (s *stats) DetailedHelp(f *flag.FlagSet) { +- fmt.Fprint(f.Output(), ` +-Load the workspace for the current directory, and output a JSON summary of +-workspace information relevant to performance. As a side effect, this command +-populates the gopls file cache for the current workspace. - --// PrintVersionInfo writes version information to w, using the output format --// specified by mode. verbose controls whether additional information is --// written, including section headers. --func PrintVersionInfo(_ context.Context, w io.Writer, verbose bool, mode PrintMode) error { -- info := VersionInfo() -- if mode == JSON { -- return printVersionInfoJSON(w, info) -- } +-By default, this command may include output that refers to the location or +-content of user code. When the -anon flag is set, fields that may refer to user +-code are hidden. - -- if !verbose { -- printBuildInfo(w, info, false, mode) -- return nil -- } -- section(w, mode, "Build info", func() { -- printBuildInfo(w, info, true, mode) -- }) -- return nil +-Example: +- $ gopls stats -anon +-`) +- printFlagDefaults(f) -} - --func printVersionInfoJSON(w io.Writer, info *ServerVersion) error { -- js, err := json.MarshalIndent(info, "", "\t") -- if err != nil { -- return err +-func (s *stats) Run(ctx context.Context, args ...string) error { +- if s.app.Remote != "" { +- // stats does not work with -remote. +- // Other sessions on the daemon may interfere with results. +- // Additionally, the type assertions in below only work if progress +- // notifications bypass jsonrpc2 serialization. +- return fmt.Errorf("the stats subcommand does not work with -remote") - } -- _, err = fmt.Fprint(w, string(js)) -- return err --} - --func section(w io.Writer, mode PrintMode, title string, body func()) { -- switch mode { -- case PlainText: -- fmt.Fprintln(w, title) -- fmt.Fprintln(w, strings.Repeat("-", len(title))) -- body() -- case Markdown: -- fmt.Fprintf(w, "#### %s\n\n```\n", title) -- body() -- fmt.Fprintf(w, "```\n") -- case HTML: -- fmt.Fprintf(w, "

%s

\n
\n", title)
--		body()
--		fmt.Fprint(w, "
\n") +- if !s.app.Verbose { +- event.SetExporter(nil) // don't log errors to stderr - } --} - --func printBuildInfo(w io.Writer, info *ServerVersion, verbose bool, mode PrintMode) { -- fmt.Fprintf(w, "%v %v\n", info.Path, Version()) -- printModuleInfo(w, info.Main, mode) -- if !verbose { -- return -- } -- for _, dep := range info.Deps { -- printModuleInfo(w, *dep, mode) -- } -- fmt.Fprintf(w, "go: %v\n", info.GoVersion) --} +- stats := GoplsStats{ +- GOOS: runtime.GOOS, +- GOARCH: runtime.GOARCH, +- GOPLSCACHE: os.Getenv("GOPLSCACHE"), +- GoVersion: runtime.Version(), +- GoplsVersion: versionpkg.Version(), +- GOPACKAGESDRIVER: os.Getenv("GOPACKAGESDRIVER"), +- } - --func printModuleInfo(w io.Writer, m debug.Module, _ PrintMode) { -- fmt.Fprintf(w, " %s@%s", m.Path, m.Version) -- if m.Sum != "" { -- fmt.Fprintf(w, " %s", m.Sum) +- opts := s.app.options +- s.app.options = func(o *settings.Options) { +- if opts != nil { +- opts(o) +- } +- o.VerboseWorkDoneProgress = true - } -- if m.Replace != nil { -- fmt.Fprintf(w, " => %v", m.Replace.Path) +- var ( +- iwlMu sync.Mutex +- iwlToken protocol.ProgressToken +- iwlDone = make(chan struct{}) +- ) +- +- onProgress := func(p *protocol.ProgressParams) { +- switch v := p.Value.(type) { +- case *protocol.WorkDoneProgressBegin: +- if v.Title == server.DiagnosticWorkTitle(server.FromInitialWorkspaceLoad) { +- iwlMu.Lock() +- iwlToken = p.Token +- iwlMu.Unlock() +- } +- case *protocol.WorkDoneProgressEnd: +- iwlMu.Lock() +- tok := iwlToken +- iwlMu.Unlock() +- +- if p.Token == tok { +- close(iwlDone) +- } +- } - } -- fmt.Fprintf(w, "\n") +- +- // do executes a timed section of the stats command. +- do := func(name string, f func() error) (time.Duration, error) { +- start := time.Now() +- fmt.Fprintf(os.Stderr, "%-30s", name+"...") +- if err := f(); err != nil { +- return time.Since(start), err +- } +- d := time.Since(start) +- fmt.Fprintf(os.Stderr, "done (%v)\n", d) +- return d, nil +- } +- +- var conn *connection +- iwlDuration, err := do("Initializing workspace", func() error { +- var err error +- conn, err = s.app.connect(ctx, onProgress) +- if err != nil { +- return err +- } +- select { +- case <-iwlDone: +- case <-ctx.Done(): +- return ctx.Err() +- } +- return nil +- }) +- stats.InitialWorkspaceLoadDuration = fmt.Sprint(iwlDuration) +- if err != nil { +- return err +- } +- defer conn.terminate(ctx) +- +- // Gather bug reports produced by any process using +- // this executable and persisted in the cache. +- do("Gathering bug reports", func() error { +- stats.CacheDir, stats.BugReports = filecache.BugReports() +- if stats.BugReports == nil { +- stats.BugReports = []bugpkg.Bug{} // non-nil for JSON +- } +- return nil +- }) +- +- if _, err := do("Querying memstats", func() error { +- memStats, err := conn.ExecuteCommand(ctx, &protocol.ExecuteCommandParams{ +- Command: command.MemStats.ID(), +- }) +- if err != nil { +- return err +- } +- stats.MemStats = memStats.(command.MemStatsResult) +- return nil +- }); err != nil { +- return err +- } +- +- if _, err := do("Querying workspace stats", func() error { +- wsStats, err := conn.ExecuteCommand(ctx, &protocol.ExecuteCommandParams{ +- Command: command.WorkspaceStats.ID(), +- }) +- if err != nil { +- return err +- } +- stats.WorkspaceStats = wsStats.(command.WorkspaceStatsResult) +- return nil +- }); err != nil { +- return err +- } +- +- if _, err := do("Collecting directory info", func() error { +- var err error +- stats.DirStats, err = findDirStats() +- if err != nil { +- return err +- } +- return nil +- }); err != nil { +- return err +- } +- +- // Filter JSON output to fields that are consistent with s.Anon. +- okFields := make(map[string]interface{}) +- { +- v := reflect.ValueOf(stats) +- t := v.Type() +- for i := 0; i < t.NumField(); i++ { +- f := t.Field(i) +- if !token.IsExported(f.Name) { +- continue +- } +- vf := v.FieldByName(f.Name) +- if s.Anon && f.Tag.Get("anon") != "ok" && !vf.IsZero() { +- // Fields that can be served with -anon must be explicitly marked as OK. +- // But, if it's zero value, it's ok to print. +- continue +- } +- okFields[f.Name] = vf.Interface() +- } +- } +- data, err := json.MarshalIndent(okFields, "", " ") +- if err != nil { +- return err +- } +- +- os.Stdout.Write(data) +- fmt.Println() +- return nil -} - --type field struct { -- index []int +-// GoplsStats holds information extracted from a gopls session in the current +-// workspace. +-// +-// Fields that should be printed with the -anon flag should be explicitly +-// marked as `anon:"ok"`. Only fields that cannot refer to user files or code +-// should be marked as such. +-type GoplsStats struct { +- GOOS, GOARCH string `anon:"ok"` +- GOPLSCACHE string +- GoVersion string `anon:"ok"` +- GoplsVersion string `anon:"ok"` +- GOPACKAGESDRIVER string +- InitialWorkspaceLoadDuration string `anon:"ok"` // in time.Duration string form +- CacheDir string +- BugReports []bugpkg.Bug +- MemStats command.MemStatsResult `anon:"ok"` +- WorkspaceStats command.WorkspaceStatsResult `anon:"ok"` +- DirStats dirStats `anon:"ok"` -} - --var fields []field +-type dirStats struct { +- Files int +- TestdataFiles int +- GoFiles int +- ModFiles int +- Dirs int +-} - --type sessionOption struct { -- Name string -- Type string -- Current string -- Default string +-// findDirStats collects information about the current directory and its +-// subdirectories. +-func findDirStats() (dirStats, error) { +- var ds dirStats +- filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error { +- if err != nil { +- return err +- } +- if d.IsDir() { +- ds.Dirs++ +- } else { +- ds.Files++ +- slashed := filepath.ToSlash(path) +- switch { +- case strings.Contains(slashed, "/testdata/") || strings.HasPrefix(slashed, "testdata/"): +- ds.TestdataFiles++ +- case strings.HasSuffix(path, ".go"): +- ds.GoFiles++ +- case strings.HasSuffix(path, ".mod"): +- ds.ModFiles++ +- } +- } +- return nil +- }) +- return ds, nil -} -diff -urN a/gopls/internal/lsp/debug/info_test.go b/gopls/internal/lsp/debug/info_test.go ---- a/gopls/internal/lsp/debug/info_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/debug/info_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,48 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/cmd/subcommands.go b/gopls/internal/cmd/subcommands.go +--- a/gopls/internal/cmd/subcommands.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/subcommands.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,59 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --// Package debug exports debug information for gopls. --package debug +-package cmd - -import ( -- "bytes" - "context" -- "encoding/json" -- "runtime" -- "testing" +- "flag" +- "fmt" +- "text/tabwriter" +- +- "golang.org/x/tools/internal/tool" -) - --func TestPrintVersionInfoJSON(t *testing.T) { -- buf := new(bytes.Buffer) -- if err := PrintVersionInfo(context.Background(), buf, true, JSON); err != nil { -- t.Fatalf("PrintVersionInfo failed: %v", err) -- } -- res := buf.Bytes() +-// subcommands is a helper that may be embedded for commands that delegate to +-// subcommands. +-type subcommands []tool.Application - -- var got ServerVersion -- if err := json.Unmarshal(res, &got); err != nil { -- t.Fatalf("unexpected output: %v\n%s", err, res) +-func (s subcommands) DetailedHelp(f *flag.FlagSet) { +- w := tabwriter.NewWriter(f.Output(), 0, 0, 2, ' ', 0) +- defer w.Flush() +- fmt.Fprint(w, "\nSubcommand:\n") +- for _, c := range s { +- fmt.Fprintf(w, " %s\t%s\n", c.Name(), c.ShortHelp()) - } -- if g, w := got.GoVersion, runtime.Version(); g != w { -- t.Errorf("go version = %v, want %v", g, w) +- printFlagDefaults(f) +-} +- +-func (s subcommands) Usage() string { return " [arg]..." } +- +-func (s subcommands) Run(ctx context.Context, args ...string) error { +- if len(args) == 0 { +- return tool.CommandLineErrorf("must provide subcommand") - } -- if g, w := got.Version, Version(); g != w { -- t.Errorf("gopls version = %v, want %v", g, w) +- command, args := args[0], args[1:] +- for _, c := range s { +- if c.Name() == command { +- s := flag.NewFlagSet(c.Name(), flag.ExitOnError) +- return tool.Run(ctx, s, c, args) +- } - } -- // Other fields of BuildInfo may not be available during test. +- return tool.CommandLineErrorf("unknown subcommand %v", command) -} - --func TestPrintVersionInfoPlainText(t *testing.T) { -- buf := new(bytes.Buffer) -- if err := PrintVersionInfo(context.Background(), buf, true, PlainText); err != nil { -- t.Fatalf("PrintVersionInfo failed: %v", err) -- } -- res := buf.Bytes() +-func (s subcommands) Commands() []tool.Application { return s } - -- // Other fields of BuildInfo may not be available during test. -- wantGoplsVersion, wantGoVersion := Version(), runtime.Version() -- if !bytes.Contains(res, []byte(wantGoplsVersion)) || !bytes.Contains(res, []byte(wantGoVersion)) { -- t.Errorf("plaintext output = %q,\nwant (version: %v, go: %v)", res, wantGoplsVersion, wantGoVersion) +-// getSubcommands returns the subcommands of a given Application. +-func getSubcommands(a tool.Application) []tool.Application { +- // This interface is satisfied both by tool.Applications +- // that embed subcommands, and by *cmd.Application. +- type hasCommands interface { +- Commands() []tool.Application +- } +- if sub, ok := a.(hasCommands); ok { +- return sub.Commands() - } +- return nil -} -diff -urN a/gopls/internal/lsp/debug/log/log.go b/gopls/internal/lsp/debug/log/log.go ---- a/gopls/internal/lsp/debug/log/log.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/debug/log/log.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,43 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/cmd/suggested_fix.go b/gopls/internal/cmd/suggested_fix.go +--- a/gopls/internal/cmd/suggested_fix.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/suggested_fix.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,173 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --// Package log provides helper methods for exporting log events to the --// internal/event package. --package log +-package cmd - -import ( - "context" +- "flag" - "fmt" - -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/label" -- "golang.org/x/tools/internal/event/tag" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/slices" +- "golang.org/x/tools/internal/tool" -) - --// Level parameterizes log severity. --type Level int +-// TODO(adonovan): this command has a very poor user interface. It +-// should have a way to query the available fixes for a file (without +-// a span), enumerate the valid fix kinds, enable all fixes, and not +-// require the pointless -all flag. See issue #60290. - --const ( -- _ Level = iota -- Error -- Warning -- Info -- Debug -- Trace --) +-// suggestedFix implements the fix verb for gopls. +-type suggestedFix struct { +- EditFlags +- All bool `flag:"a,all" help:"apply all fixes, not just preferred fixes"` - --// Log exports a log event labeled with level l. --func (l Level) Log(ctx context.Context, msg string) { -- event.Log(ctx, msg, tag.Level.Of(int(l))) +- app *Application -} - --// Logf formats and exports a log event labeled with level l. --func (l Level) Logf(ctx context.Context, format string, args ...interface{}) { -- l.Log(ctx, fmt.Sprintf(format, args...)) --} +-func (s *suggestedFix) Name() string { return "fix" } +-func (s *suggestedFix) Parent() string { return s.app.Name() } +-func (s *suggestedFix) Usage() string { return "[fix-flags] " } +-func (s *suggestedFix) ShortHelp() string { return "apply suggested fixes" } +-func (s *suggestedFix) DetailedHelp(f *flag.FlagSet) { +- fmt.Fprintf(f.Output(), ` +-Example: apply fixes to this file, rewriting it: - --// LabeledLevel extracts the labeled log l --func LabeledLevel(lm label.Map) Level { -- return Level(tag.Level.Get(lm)) --} -diff -urN a/gopls/internal/lsp/debug/metrics.go b/gopls/internal/lsp/debug/metrics.go ---- a/gopls/internal/lsp/debug/metrics.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/debug/metrics.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,58 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +- $ gopls fix -a -w internal/cmd/check.go - --package debug +-The -a (-all) flag causes all fixes, not just preferred ones, to be +-applied, but since no fixes are currently preferred, this flag is +-essentially mandatory. - --import ( -- "golang.org/x/tools/internal/event/export/metric" -- "golang.org/x/tools/internal/event/label" -- "golang.org/x/tools/internal/event/tag" --) +-Arguments after the filename are interpreted as LSP CodeAction kinds +-to be applied; the default set is {"quickfix"}, but valid kinds include: - --var ( -- // the distributions we use for histograms -- bytesDistribution = []int64{1 << 10, 1 << 11, 1 << 12, 1 << 14, 1 << 16, 1 << 20} -- millisecondsDistribution = []float64{0.1, 0.5, 1, 2, 5, 10, 50, 100, 500, 1000, 5000, 10000, 50000, 100000} +- quickfix +- refactor +- refactor.extract +- refactor.inline +- refactor.rewrite +- source.organizeImports +- source.fixAll - -- receivedBytes = metric.HistogramInt64{ -- Name: "received_bytes", -- Description: "Distribution of received bytes, by method.", -- Keys: []label.Key{tag.RPCDirection, tag.Method}, -- Buckets: bytesDistribution, +-CodeAction kinds are hierarchical, so "refactor" includes +-"refactor.inline". There is currently no way to enable or even +-enumerate all kinds. +- +-Example: apply any "refactor.rewrite" fixes at the specific byte +-offset within this file: +- +- $ gopls fix -a internal/cmd/check.go:#43 refactor.rewrite +- +-fix-flags: +-`) +- printFlagDefaults(f) +-} +- +-// Run performs diagnostic checks on the file specified and either; +-// - if -w is specified, updates the file in place; +-// - if -d is specified, prints out unified diffs of the changes; or +-// - otherwise, prints the new versions to stdout. +-func (s *suggestedFix) Run(ctx context.Context, args ...string) error { +- if len(args) < 1 { +- return tool.CommandLineErrorf("fix expects at least 1 argument") +- } +- s.app.editFlags = &s.EditFlags +- conn, err := s.app.connect(ctx, nil) +- if err != nil { +- return err - } +- defer conn.terminate(ctx) - -- sentBytes = metric.HistogramInt64{ -- Name: "sent_bytes", -- Description: "Distribution of sent bytes, by method.", -- Keys: []label.Key{tag.RPCDirection, tag.Method}, -- Buckets: bytesDistribution, +- from := parseSpan(args[0]) +- uri := from.URI() +- file, err := conn.openFile(ctx, uri) +- if err != nil { +- return err +- } +- rng, err := file.spanRange(from) +- if err != nil { +- return err - } - -- latency = metric.HistogramFloat64{ -- Name: "latency", -- Description: "Distribution of latency in milliseconds, by method.", -- Keys: []label.Key{tag.RPCDirection, tag.Method}, -- Buckets: millisecondsDistribution, +- // Get diagnostics. +- if err := conn.diagnoseFiles(ctx, []protocol.DocumentURI{uri}); err != nil { +- return err - } +- diagnostics := []protocol.Diagnostic{} // LSP wants non-nil slice +- conn.client.filesMu.Lock() +- diagnostics = append(diagnostics, file.diagnostics...) +- conn.client.filesMu.Unlock() - -- started = metric.Scalar{ -- Name: "started", -- Description: "Count of RPCs started by method.", -- Keys: []label.Key{tag.RPCDirection, tag.Method}, +- // Request code actions +- codeActionKinds := []protocol.CodeActionKind{protocol.QuickFix} +- if len(args) > 1 { +- codeActionKinds = []protocol.CodeActionKind{} +- for _, k := range args[1:] { +- codeActionKinds = append(codeActionKinds, protocol.CodeActionKind(k)) +- } +- } +- p := protocol.CodeActionParams{ +- TextDocument: protocol.TextDocumentIdentifier{ +- URI: uri, +- }, +- Context: protocol.CodeActionContext{ +- Only: codeActionKinds, +- Diagnostics: diagnostics, +- }, +- Range: rng, +- } +- actions, err := conn.CodeAction(ctx, &p) +- if err != nil { +- return fmt.Errorf("%v: %v", from, err) - } - -- completed = metric.Scalar{ -- Name: "completed", -- Description: "Count of RPCs completed by method and status.", -- Keys: []label.Key{tag.RPCDirection, tag.Method, tag.StatusCode}, +- // Gather edits from matching code actions. +- var edits []protocol.TextEdit +- for _, a := range actions { +- // Without -all, apply only "preferred" fixes. +- if !a.IsPreferred && !s.All { +- continue +- } +- +- // Execute any command. +- // This may cause the server to make +- // an ApplyEdit downcall to the client. +- if a.Command != nil { +- if _, err := conn.ExecuteCommand(ctx, &protocol.ExecuteCommandParams{ +- Command: a.Command.Command, +- Arguments: a.Command.Arguments, +- }); err != nil { +- return err +- } +- // The specification says that commands should +- // be executed _after_ edits are applied, not +- // instead of them, but we don't want to +- // duplicate edits. +- continue +- } +- +- // If the provided span has a position (not just offsets), +- // and the action has diagnostics, the action must have a +- // diagnostic with the same range as it. +- if from.HasPosition() && len(a.Diagnostics) > 0 && +- !slices.ContainsFunc(a.Diagnostics, func(diag protocol.Diagnostic) bool { +- return diag.Range.Start == rng.Start +- }) { +- continue +- } +- +- // Partially apply CodeAction.Edit, a WorkspaceEdit. +- // (See also conn.Client.applyWorkspaceEdit(a.Edit)). +- for _, c := range a.Edit.DocumentChanges { +- tde := c.TextDocumentEdit +- if tde != nil && tde.TextDocument.URI == uri { +- edits = append(edits, protocol.AsTextEdits(tde.Edits)...) +- } +- } - } --) - --func registerMetrics(m *metric.Config) { -- receivedBytes.Record(m, tag.ReceivedBytes) -- sentBytes.Record(m, tag.SentBytes) -- latency.Record(m, tag.Latency) -- started.Count(m, tag.Started) -- completed.Count(m, tag.Latency) +- return applyTextEdits(file.mapper, edits, s.app.editFlags) -} -diff -urN a/gopls/internal/lsp/debug/rpc.go b/gopls/internal/lsp/debug/rpc.go ---- a/gopls/internal/lsp/debug/rpc.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/debug/rpc.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,239 +0,0 @@ +diff -urN a/gopls/internal/cmd/symbols.go b/gopls/internal/cmd/symbols.go +--- a/gopls/internal/cmd/symbols.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/symbols.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,115 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package debug +-package cmd - -import ( - "context" +- "encoding/json" +- "flag" - "fmt" -- "html/template" -- "net/http" - "sort" -- "sync" -- "time" - -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/core" -- "golang.org/x/tools/internal/event/export" -- "golang.org/x/tools/internal/event/label" -- "golang.org/x/tools/internal/event/tag" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/tool" -) - --var RPCTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` --{{define "title"}}RPC Information{{end}} --{{define "body"}} --

Inbound

-- {{template "rpcSection" .Inbound}} --

Outbound

-- {{template "rpcSection" .Outbound}} --{{end}} --{{define "rpcSection"}} -- {{range .}}

-- {{.Method}} {{.Started}} traces ({{.InProgress}} in progress) --
-- Latency {{with .Latency}}{{.Mean}} ({{.Min}}<{{.Max}}){{end}} -- By bucket 0s {{range .Latency.Values}}{{if gt .Count 0}}{{.Count}} {{.Limit}} {{end}}{{end}} --
-- Received {{.Received}} (avg. {{.ReceivedMean}}) -- Sent {{.Sent}} (avg. {{.SentMean}}) --
-- Result codes {{range .Codes}}{{.Key}}={{.Count}} {{end}} --

-- {{end}} --{{end}} --`)) +-// symbols implements the symbols verb for gopls +-type symbols struct { +- app *Application +-} - --type Rpcs struct { // exported for testing -- mu sync.Mutex -- Inbound []*rpcStats // stats for incoming lsp rpcs sorted by method name -- Outbound []*rpcStats // stats for outgoing lsp rpcs sorted by method name --} -- --type rpcStats struct { -- Method string -- Started int64 -- Completed int64 -- -- Latency rpcTimeHistogram -- Received byteUnits -- Sent byteUnits -- Codes []*rpcCodeBucket --} -- --type rpcTimeHistogram struct { -- Sum timeUnits -- Count int64 -- Min timeUnits -- Max timeUnits -- Values []rpcTimeBucket --} -- --type rpcTimeBucket struct { -- Limit timeUnits -- Count int64 --} -- --type rpcCodeBucket struct { -- Key string -- Count int64 +-func (r *symbols) Name() string { return "symbols" } +-func (r *symbols) Parent() string { return r.app.Name() } +-func (r *symbols) Usage() string { return "" } +-func (r *symbols) ShortHelp() string { return "display selected file's symbols" } +-func (r *symbols) DetailedHelp(f *flag.FlagSet) { +- fmt.Fprint(f.Output(), ` +-Example: +- $ gopls symbols helper/helper.go +-`) +- printFlagDefaults(f) -} -- --func (r *Rpcs) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) context.Context { -- r.mu.Lock() -- defer r.mu.Unlock() -- switch { -- case event.IsStart(ev): -- if _, stats := r.getRPCSpan(ctx, ev); stats != nil { -- stats.Started++ -- } -- case event.IsEnd(ev): -- span, stats := r.getRPCSpan(ctx, ev) -- if stats != nil { -- endRPC(ctx, ev, span, stats) -- } -- case event.IsMetric(ev): -- sent := byteUnits(tag.SentBytes.Get(lm)) -- rec := byteUnits(tag.ReceivedBytes.Get(lm)) -- if sent != 0 || rec != 0 { -- if _, stats := r.getRPCSpan(ctx, ev); stats != nil { -- stats.Sent += sent -- stats.Received += rec -- } -- } +-func (r *symbols) Run(ctx context.Context, args ...string) error { +- if len(args) != 1 { +- return tool.CommandLineErrorf("symbols expects 1 argument (position)") - } -- return ctx --} - --func endRPC(ctx context.Context, ev core.Event, span *export.Span, stats *rpcStats) { -- // update the basic counts -- stats.Completed++ +- conn, err := r.app.connect(ctx, nil) +- if err != nil { +- return err +- } +- defer conn.terminate(ctx) - -- // get and record the status code -- if status := getStatusCode(span); status != "" { -- var b *rpcCodeBucket -- for c, entry := range stats.Codes { -- if entry.Key == status { -- b = stats.Codes[c] -- break +- from := parseSpan(args[0]) +- p := protocol.DocumentSymbolParams{ +- TextDocument: protocol.TextDocumentIdentifier{ +- URI: from.URI(), +- }, +- } +- symbols, err := conn.DocumentSymbol(ctx, &p) +- if err != nil { +- return err +- } +- for _, s := range symbols { +- if m, ok := s.(map[string]interface{}); ok { +- s, err = mapToSymbol(m) +- if err != nil { +- return err - } - } -- if b == nil { -- b = &rpcCodeBucket{Key: status} -- stats.Codes = append(stats.Codes, b) -- sort.Slice(stats.Codes, func(i int, j int) bool { -- return stats.Codes[i].Key < stats.Codes[j].Key -- }) +- switch t := s.(type) { +- case protocol.DocumentSymbol: +- printDocumentSymbol(t) +- case protocol.SymbolInformation: +- printSymbolInformation(t) - } -- b.Count++ - } +- return nil +-} - -- // calculate latency if this was an rpc span -- elapsedTime := span.Finish().At().Sub(span.Start().At()) -- latencyMillis := timeUnits(elapsedTime) / timeUnits(time.Millisecond) -- if stats.Latency.Count == 0 { -- stats.Latency.Min = latencyMillis -- stats.Latency.Max = latencyMillis -- } else { -- if stats.Latency.Min > latencyMillis { -- stats.Latency.Min = latencyMillis -- } -- if stats.Latency.Max < latencyMillis { -- stats.Latency.Max = latencyMillis -- } +-func mapToSymbol(m map[string]interface{}) (interface{}, error) { +- b, err := json.Marshal(m) +- if err != nil { +- return nil, err - } -- stats.Latency.Count++ -- stats.Latency.Sum += latencyMillis -- for i := range stats.Latency.Values { -- if stats.Latency.Values[i].Limit > latencyMillis { -- stats.Latency.Values[i].Count++ -- break +- +- if _, ok := m["selectionRange"]; ok { +- var s protocol.DocumentSymbol +- if err := json.Unmarshal(b, &s); err != nil { +- return nil, err - } +- return s, nil - } --} - --func (r *Rpcs) getRPCSpan(ctx context.Context, ev core.Event) (*export.Span, *rpcStats) { -- // get the span -- span := export.GetSpan(ctx) -- if span == nil { -- return nil, nil +- var s protocol.SymbolInformation +- if err := json.Unmarshal(b, &s); err != nil { +- return nil, err - } -- // use the span start event look up the correct stats block -- // we do this because it prevents us matching a sub span -- return span, r.getRPCStats(span.Start()) +- return s, nil -} - --func (r *Rpcs) getRPCStats(lm label.Map) *rpcStats { -- method := tag.Method.Get(lm) -- if method == "" { -- return nil -- } -- set := &r.Inbound -- if tag.RPCDirection.Get(lm) != tag.Inbound { -- set = &r.Outbound -- } -- // get the record for this method -- index := sort.Search(len(*set), func(i int) bool { -- return (*set)[i].Method >= method +-func printDocumentSymbol(s protocol.DocumentSymbol) { +- fmt.Printf("%s %s %s\n", s.Name, s.Kind, positionToString(s.SelectionRange)) +- // Sort children for consistency +- sort.Slice(s.Children, func(i, j int) bool { +- return s.Children[i].Name < s.Children[j].Name - }) -- -- if index < len(*set) && (*set)[index].Method == method { -- return (*set)[index] -- } -- -- old := *set -- *set = make([]*rpcStats, len(old)+1) -- copy(*set, old[:index]) -- copy((*set)[index+1:], old[index:]) -- stats := &rpcStats{Method: method} -- stats.Latency.Values = make([]rpcTimeBucket, len(millisecondsDistribution)) -- for i, m := range millisecondsDistribution { -- stats.Latency.Values[i].Limit = timeUnits(m) +- for _, c := range s.Children { +- fmt.Printf("\t%s %s %s\n", c.Name, c.Kind, positionToString(c.SelectionRange)) - } -- (*set)[index] = stats -- return stats -} - --func (s *rpcStats) InProgress() int64 { return s.Started - s.Completed } --func (s *rpcStats) SentMean() byteUnits { return s.Sent / byteUnits(s.Started) } --func (s *rpcStats) ReceivedMean() byteUnits { return s.Received / byteUnits(s.Started) } -- --func (h *rpcTimeHistogram) Mean() timeUnits { return h.Sum / timeUnits(h.Count) } -- --func getStatusCode(span *export.Span) string { -- for _, ev := range span.Events() { -- if status := tag.StatusCode.Get(ev); status != "" { -- return status -- } -- } -- return "" +-func printSymbolInformation(s protocol.SymbolInformation) { +- fmt.Printf("%s %s %s\n", s.Name, s.Kind, positionToString(s.Location.Range)) -} - --func (r *Rpcs) getData(req *http.Request) interface{} { -- return r +-func positionToString(r protocol.Range) string { +- return fmt.Sprintf("%v:%v-%v:%v", +- r.Start.Line+1, +- r.Start.Character+1, +- r.End.Line+1, +- r.End.Character+1, +- ) -} +diff -urN a/gopls/internal/cmd/usage/api-json.hlp b/gopls/internal/cmd/usage/api-json.hlp +--- a/gopls/internal/cmd/usage/api-json.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/api-json.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,4 +0,0 @@ +-print JSON describing gopls API - --func units(v float64, suffixes []string) string { -- s := "" -- for _, s = range suffixes { -- n := v / 1000 -- if n < 1 { -- break -- } -- v = n -- } -- return fmt.Sprintf("%.2f%s", v, s) --} +-Usage: +- gopls [flags] api-json +diff -urN a/gopls/internal/cmd/usage/bug.hlp b/gopls/internal/cmd/usage/bug.hlp +--- a/gopls/internal/cmd/usage/bug.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/bug.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,4 +0,0 @@ +-report a bug in gopls - --type timeUnits float64 +-Usage: +- gopls [flags] bug +diff -urN a/gopls/internal/cmd/usage/call_hierarchy.hlp b/gopls/internal/cmd/usage/call_hierarchy.hlp +--- a/gopls/internal/cmd/usage/call_hierarchy.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/call_hierarchy.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,10 +0,0 @@ +-display selected identifier's call hierarchy - --func (v timeUnits) String() string { -- v = v * 1000 * 1000 -- return units(float64(v), []string{"ns", "μs", "ms", "s"}) --} +-Usage: +- gopls [flags] call_hierarchy - --type byteUnits float64 +-Example: - --func (v byteUnits) String() string { -- return units(float64(v), []string{"B", "KB", "MB", "GB", "TB"}) --} -diff -urN a/gopls/internal/lsp/debug/serve.go b/gopls/internal/lsp/debug/serve.go ---- a/gopls/internal/lsp/debug/serve.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/debug/serve.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,864 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +- $ # 1-indexed location (:line:column or :#offset) of the target identifier +- $ gopls call_hierarchy helper/helper.go:8:6 +- $ gopls call_hierarchy helper/helper.go:#53 +diff -urN a/gopls/internal/cmd/usage/check.hlp b/gopls/internal/cmd/usage/check.hlp +--- a/gopls/internal/cmd/usage/check.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/check.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,8 +0,0 @@ +-show diagnostic results for the specified file - --package debug +-Usage: +- gopls [flags] check - --import ( -- "bytes" -- "context" -- "errors" -- "fmt" -- "html/template" -- "io" -- stdlog "log" -- "net" -- "net/http" -- "net/http/pprof" -- "os" -- "path" -- "path/filepath" -- "runtime" -- "strconv" -- "strings" -- "sync" -- "time" +-Example: show the diagnostic results of this file: - -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/cache" -- "golang.org/x/tools/gopls/internal/lsp/debug/log" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/core" -- "golang.org/x/tools/internal/event/export" -- "golang.org/x/tools/internal/event/export/metric" -- "golang.org/x/tools/internal/event/export/ocagent" -- "golang.org/x/tools/internal/event/export/prometheus" -- "golang.org/x/tools/internal/event/keys" -- "golang.org/x/tools/internal/event/label" -- "golang.org/x/tools/internal/event/tag" --) +- $ gopls check internal/cmd/check.go +diff -urN a/gopls/internal/cmd/usage/codelens.hlp b/gopls/internal/cmd/usage/codelens.hlp +--- a/gopls/internal/cmd/usage/codelens.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/codelens.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,35 +0,0 @@ +-List or execute code lenses for a file - --type contextKeyType int +-Usage: +- gopls [flags] codelens [codelens-flags] file[:line[:col]] [title] - --const ( -- instanceKey contextKeyType = iota -- traceKey --) +-The codelens command lists or executes code lenses for the specified +-file, or line within a file. A code lens is a command associated with +-a position in the code. - --// An Instance holds all debug information associated with a gopls instance. --type Instance struct { -- Logfile string -- StartTime time.Time -- ServerAddress string -- Workdir string -- OCAgentConfig string +-With an optional title argment, only code lenses matching that +-title are considered. - -- LogWriter io.Writer +-By default, the codelens command lists the available lenses for the +-specified file or line within a file, including the title and +-title of the command. With the -exec flag, the first matching command +-is executed, and its output is printed to stdout. - -- exporter event.Exporter +-Example: - -- ocagent *ocagent.Exporter -- prometheus *prometheus.Exporter -- rpcs *Rpcs -- traces *traces -- State *State +- $ gopls codelens a_test.go # list code lenses in a file +- $ gopls codelens a_test.go:10 # list code lenses on line 10 +- $ gopls codelens a_test.go gopls.test # list gopls.test commands +- $ gopls codelens -run a_test.go:10 gopls.test # run a specific test - -- serveMu sync.Mutex -- debugAddress string -- listenedDebugAddress string --} +-codelens-flags: +- -d,-diff +- display diffs instead of edited file content +- -exec +- execute the first matching code lens +- -l,-list +- display names of edited files +- -preserve +- with -write, make copies of original files +- -w,-write +- write edited content to source files +diff -urN a/gopls/internal/cmd/usage/definition.hlp b/gopls/internal/cmd/usage/definition.hlp +--- a/gopls/internal/cmd/usage/definition.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/definition.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,15 +0,0 @@ +-show declaration of selected identifier - --// State holds debugging information related to the server state. --type State struct { -- mu sync.Mutex -- clients []*Client -- servers []*Server --} +-Usage: +- gopls [flags] definition [definition-flags] - --func (st *State) Bugs() []bug.Bug { -- return bug.List() --} +-Example: show the definition of the identifier at syntax at offset 44 in this file (flag.FlagSet): - --// Caches returns the set of Cache objects currently being served. --func (st *State) Caches() []*cache.Cache { -- var caches []*cache.Cache -- seen := make(map[string]struct{}) -- for _, client := range st.Clients() { -- cache := client.Session.Cache() -- if _, found := seen[cache.ID()]; found { -- continue -- } -- seen[cache.ID()] = struct{}{} -- caches = append(caches, cache) -- } -- return caches --} +- $ gopls definition internal/cmd/definition.go:44:47 +- $ gopls definition internal/cmd/definition.go:#1270 - --// Cache returns the Cache that matches the supplied id. --func (st *State) Cache(id string) *cache.Cache { -- for _, c := range st.Caches() { -- if c.ID() == id { -- return c -- } -- } -- return nil --} +-definition-flags: +- -json +- emit output in JSON format +- -markdown +- support markdown in responses +diff -urN a/gopls/internal/cmd/usage/execute.hlp b/gopls/internal/cmd/usage/execute.hlp +--- a/gopls/internal/cmd/usage/execute.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/execute.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,30 +0,0 @@ +-Execute a gopls custom LSP command - --// Analysis returns the global Analysis template value. --func (st *State) Analysis() (_ analysisTmpl) { return } +-Usage: +- gopls [flags] execute [flags] command argument... - --type analysisTmpl struct{} +-The execute command sends an LSP ExecuteCommand request to gopls, +-with a set of optional JSON argument values. +-Some commands return a result, also JSON. - --func (analysisTmpl) AnalyzerRunTimes() []cache.LabelDuration { return cache.AnalyzerRunTimes() } +-Available commands are documented at: - --// Sessions returns the set of Session objects currently being served. --func (st *State) Sessions() []*cache.Session { -- var sessions []*cache.Session -- for _, client := range st.Clients() { -- sessions = append(sessions, client.Session) -- } -- return sessions --} +- https://github.com/golang/tools/blob/master/gopls/doc/commands.md - --// Session returns the Session that matches the supplied id. --func (st *State) Session(id string) *cache.Session { -- for _, s := range st.Sessions() { -- if s.ID() == id { -- return s -- } -- } -- return nil --} +-This interface is experimental and commands may change or disappear without notice. - --// Views returns the set of View objects currently being served. --func (st *State) Views() []*cache.View { -- var views []*cache.View -- for _, s := range st.Sessions() { -- views = append(views, s.Views()...) -- } -- return views --} +-Examples: - --// View returns the View that matches the supplied id. --func (st *State) View(id string) *cache.View { -- for _, v := range st.Views() { -- if v.ID() == id { -- return v -- } -- } -- return nil --} +- $ gopls execute gopls.add_import '{"ImportPath": "fmt", "URI": "file:///hello.go"}' +- $ gopls execute gopls.run_tests '{"URI": "file:///a_test.go", "Tests": ["Test"]}' +- $ gopls execute gopls.list_known_packages '{"URI": "file:///hello.go"}' - --// Clients returns the set of Clients currently being served. --func (st *State) Clients() []*Client { -- st.mu.Lock() -- defer st.mu.Unlock() -- clients := make([]*Client, len(st.clients)) -- copy(clients, st.clients) -- return clients --} +-execute-flags: +- -d,-diff +- display diffs instead of edited file content +- -l,-list +- display names of edited files +- -preserve +- with -write, make copies of original files +- -w,-write +- write edited content to source files +diff -urN a/gopls/internal/cmd/usage/fix.hlp b/gopls/internal/cmd/usage/fix.hlp +--- a/gopls/internal/cmd/usage/fix.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/fix.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,44 +0,0 @@ +-apply suggested fixes - --// Client returns the Client matching the supplied id. --func (st *State) Client(id string) *Client { -- for _, c := range st.Clients() { -- if c.Session.ID() == id { -- return c -- } -- } -- return nil --} +-Usage: +- gopls [flags] fix [fix-flags] - --// Servers returns the set of Servers the instance is currently connected to. --func (st *State) Servers() []*Server { -- st.mu.Lock() -- defer st.mu.Unlock() -- servers := make([]*Server, len(st.servers)) -- copy(servers, st.servers) -- return servers --} +-Example: apply fixes to this file, rewriting it: - --// A Client is an incoming connection from a remote client. --type Client struct { -- Session *cache.Session -- DebugAddress string -- Logfile string -- GoplsPath string -- ServerID string -- Service protocol.Server --} +- $ gopls fix -a -w internal/cmd/check.go - --// A Server is an outgoing connection to a remote LSP server. --type Server struct { -- ID string -- DebugAddress string -- Logfile string -- GoplsPath string -- ClientID string --} +-The -a (-all) flag causes all fixes, not just preferred ones, to be +-applied, but since no fixes are currently preferred, this flag is +-essentially mandatory. - --// addClient adds a client to the set being served. --func (st *State) addClient(session *cache.Session) { -- st.mu.Lock() -- defer st.mu.Unlock() -- st.clients = append(st.clients, &Client{Session: session}) --} +-Arguments after the filename are interpreted as LSP CodeAction kinds +-to be applied; the default set is {"quickfix"}, but valid kinds include: - --// dropClient removes a client from the set being served. --func (st *State) dropClient(session *cache.Session) { -- st.mu.Lock() -- defer st.mu.Unlock() -- for i, c := range st.clients { -- if c.Session == session { -- copy(st.clients[i:], st.clients[i+1:]) -- st.clients[len(st.clients)-1] = nil -- st.clients = st.clients[:len(st.clients)-1] -- return -- } -- } --} +- quickfix +- refactor +- refactor.extract +- refactor.inline +- refactor.rewrite +- source.organizeImports +- source.fixAll - --// updateServer updates a server to the set being queried. In practice, there should --// be at most one remote server. --func (st *State) updateServer(server *Server) { -- st.mu.Lock() -- defer st.mu.Unlock() -- for i, existing := range st.servers { -- if existing.ID == server.ID { -- // Replace, rather than mutate, to avoid a race. -- newServers := make([]*Server, len(st.servers)) -- copy(newServers, st.servers[:i]) -- newServers[i] = server -- copy(newServers[i+1:], st.servers[i+1:]) -- st.servers = newServers -- return -- } -- } -- st.servers = append(st.servers, server) --} +-CodeAction kinds are hierarchical, so "refactor" includes +-"refactor.inline". There is currently no way to enable or even +-enumerate all kinds. - --// dropServer drops a server from the set being queried. --func (st *State) dropServer(id string) { -- st.mu.Lock() -- defer st.mu.Unlock() -- for i, s := range st.servers { -- if s.ID == id { -- copy(st.servers[i:], st.servers[i+1:]) -- st.servers[len(st.servers)-1] = nil -- st.servers = st.servers[:len(st.servers)-1] -- return -- } -- } --} +-Example: apply any "refactor.rewrite" fixes at the specific byte +-offset within this file: - --// an http.ResponseWriter that filters writes --type filterResponse struct { -- w http.ResponseWriter -- edit func([]byte) []byte --} +- $ gopls fix -a internal/cmd/check.go:#43 refactor.rewrite - --func (c filterResponse) Header() http.Header { -- return c.w.Header() --} +-fix-flags: +- -a,-all +- apply all fixes, not just preferred fixes +- -d,-diff +- display diffs instead of edited file content +- -l,-list +- display names of edited files +- -preserve +- with -write, make copies of original files +- -w,-write +- write edited content to source files +diff -urN a/gopls/internal/cmd/usage/folding_ranges.hlp b/gopls/internal/cmd/usage/folding_ranges.hlp +--- a/gopls/internal/cmd/usage/folding_ranges.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/folding_ranges.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,8 +0,0 @@ +-display selected file's folding ranges - --func (c filterResponse) Write(buf []byte) (int, error) { -- ans := c.edit(buf) -- return c.w.Write(ans) --} +-Usage: +- gopls [flags] folding_ranges - --func (c filterResponse) WriteHeader(n int) { -- c.w.WriteHeader(n) --} +-Example: - --// replace annoying nuls by spaces --func cmdline(w http.ResponseWriter, r *http.Request) { -- fake := filterResponse{ -- w: w, -- edit: func(buf []byte) []byte { -- return bytes.ReplaceAll(buf, []byte{0}, []byte{' '}) -- }, -- } -- pprof.Cmdline(fake, r) --} +- $ gopls folding_ranges helper/helper.go +diff -urN a/gopls/internal/cmd/usage/format.hlp b/gopls/internal/cmd/usage/format.hlp +--- a/gopls/internal/cmd/usage/format.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/format.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,20 +0,0 @@ +-format the code according to the go standard - --func (i *Instance) getCache(r *http.Request) interface{} { -- return i.State.Cache(path.Base(r.URL.Path)) --} +-Usage: +- gopls [flags] format [format-flags] - --func (i *Instance) getAnalysis(r *http.Request) interface{} { -- return i.State.Analysis() --} +-The arguments supplied may be simple file names, or ranges within files. - --func (i *Instance) getSession(r *http.Request) interface{} { -- return i.State.Session(path.Base(r.URL.Path)) --} +-Example: reformat this file: - --func (i *Instance) getClient(r *http.Request) interface{} { -- return i.State.Client(path.Base(r.URL.Path)) --} +- $ gopls format -w internal/cmd/check.go - --func (i *Instance) getServer(r *http.Request) interface{} { -- i.State.mu.Lock() -- defer i.State.mu.Unlock() -- id := path.Base(r.URL.Path) -- for _, s := range i.State.servers { -- if s.ID == id { -- return s -- } -- } -- return nil --} +-format-flags: +- -d,-diff +- display diffs instead of edited file content +- -l,-list +- display names of edited files +- -preserve +- with -write, make copies of original files +- -w,-write +- write edited content to source files +diff -urN a/gopls/internal/cmd/usage/help.hlp b/gopls/internal/cmd/usage/help.hlp +--- a/gopls/internal/cmd/usage/help.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/help.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,10 +0,0 @@ +-print usage information for subcommands - --func (i *Instance) getView(r *http.Request) interface{} { -- return i.State.View(path.Base(r.URL.Path)) --} +-Usage: +- gopls [flags] help - --func (i *Instance) getFile(r *http.Request) interface{} { -- identifier := path.Base(r.URL.Path) -- sid := path.Base(path.Dir(r.URL.Path)) -- s := i.State.Session(sid) -- if s == nil { -- return nil -- } -- for _, o := range s.Overlays() { -- // TODO(adonovan): understand and document this comparison. -- if o.FileIdentity().Hash.String() == identifier { -- return o -- } -- } -- return nil --} - --func (i *Instance) getInfo(r *http.Request) interface{} { -- buf := &bytes.Buffer{} -- i.PrintServerInfo(r.Context(), buf) -- return template.HTML(buf.String()) --} +-Examples: +-$ gopls help # main gopls help message +-$ gopls help remote # help on 'remote' command +-$ gopls help remote sessions # help on 'remote sessions' subcommand +diff -urN a/gopls/internal/cmd/usage/highlight.hlp b/gopls/internal/cmd/usage/highlight.hlp +--- a/gopls/internal/cmd/usage/highlight.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/highlight.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,10 +0,0 @@ +-display selected identifier's highlights - --func (i *Instance) AddService(s protocol.Server, session *cache.Session) { -- for _, c := range i.State.clients { -- if c.Session == session { -- c.Service = s -- return -- } -- } -- stdlog.Printf("unable to find a Client to add the protocol.Server to") --} +-Usage: +- gopls [flags] highlight - --func getMemory(_ *http.Request) interface{} { -- var m runtime.MemStats -- runtime.ReadMemStats(&m) -- return m --} +-Example: - --func init() { -- event.SetExporter(makeGlobalExporter(os.Stderr)) --} +- $ # 1-indexed location (:line:column or :#offset) of the target identifier +- $ gopls highlight helper/helper.go:8:6 +- $ gopls highlight helper/helper.go:#53 +diff -urN a/gopls/internal/cmd/usage/implementation.hlp b/gopls/internal/cmd/usage/implementation.hlp +--- a/gopls/internal/cmd/usage/implementation.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/implementation.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,10 +0,0 @@ +-display selected identifier's implementation - --func GetInstance(ctx context.Context) *Instance { -- if ctx == nil { -- return nil -- } -- v := ctx.Value(instanceKey) -- if v == nil { -- return nil -- } -- return v.(*Instance) --} +-Usage: +- gopls [flags] implementation - --// WithInstance creates debug instance ready for use using the supplied --// configuration and stores it in the returned context. --func WithInstance(ctx context.Context, workdir, agent string) context.Context { -- i := &Instance{ -- StartTime: time.Now(), -- Workdir: workdir, -- OCAgentConfig: agent, -- } -- i.LogWriter = os.Stderr -- ocConfig := ocagent.Discover() -- //TODO: we should not need to adjust the discovered configuration -- ocConfig.Address = i.OCAgentConfig -- i.ocagent = ocagent.Connect(ocConfig) -- i.prometheus = prometheus.New() -- i.rpcs = &Rpcs{} -- i.traces = &traces{} -- i.State = &State{} -- i.exporter = makeInstanceExporter(i) -- return context.WithValue(ctx, instanceKey, i) --} +-Example: - --// SetLogFile sets the logfile for use with this instance. --func (i *Instance) SetLogFile(logfile string, isDaemon bool) (func(), error) { -- // TODO: probably a better solution for deferring closure to the caller would -- // be for the debug instance to itself be closed, but this fixes the -- // immediate bug of logs not being captured. -- closeLog := func() {} -- if logfile != "" { -- if logfile == "auto" { -- if isDaemon { -- logfile = filepath.Join(os.TempDir(), fmt.Sprintf("gopls-daemon-%d.log", os.Getpid())) -- } else { -- logfile = filepath.Join(os.TempDir(), fmt.Sprintf("gopls-%d.log", os.Getpid())) -- } -- } -- f, err := os.Create(logfile) -- if err != nil { -- return nil, fmt.Errorf("unable to create log file: %w", err) -- } -- closeLog = func() { -- defer f.Close() -- } -- stdlog.SetOutput(io.MultiWriter(os.Stderr, f)) -- i.LogWriter = f -- } -- i.Logfile = logfile -- return closeLog, nil --} +- $ # 1-indexed location (:line:column or :#offset) of the target identifier +- $ gopls implementation helper/helper.go:8:6 +- $ gopls implementation helper/helper.go:#53 +diff -urN a/gopls/internal/cmd/usage/imports.hlp b/gopls/internal/cmd/usage/imports.hlp +--- a/gopls/internal/cmd/usage/imports.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/imports.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,18 +0,0 @@ +-updates import statements - --// Serve starts and runs a debug server in the background on the given addr. --// It also logs the port the server starts on, to allow for :0 auto assigned --// ports. --func (i *Instance) Serve(ctx context.Context, addr string) (string, error) { -- stdlog.SetFlags(stdlog.Lshortfile) -- if addr == "" { -- return "", nil -- } -- i.serveMu.Lock() -- defer i.serveMu.Unlock() +-Usage: +- gopls [flags] imports [imports-flags] - -- if i.listenedDebugAddress != "" { -- // Already serving. Return the bound address. -- return i.listenedDebugAddress, nil -- } +-Example: update imports statements in a file: - -- i.debugAddress = addr -- listener, err := net.Listen("tcp", i.debugAddress) -- if err != nil { -- return "", err -- } -- i.listenedDebugAddress = listener.Addr().String() +- $ gopls imports -w internal/cmd/check.go - -- port := listener.Addr().(*net.TCPAddr).Port -- if strings.HasSuffix(i.debugAddress, ":0") { -- stdlog.Printf("debug server listening at http://localhost:%d", port) -- } -- event.Log(ctx, "Debug serving", tag.Port.Of(port)) -- go func() { -- mux := http.NewServeMux() -- mux.HandleFunc("/", render(MainTmpl, func(*http.Request) interface{} { return i })) -- mux.HandleFunc("/debug/", render(DebugTmpl, nil)) -- mux.HandleFunc("/debug/pprof/", pprof.Index) -- mux.HandleFunc("/debug/pprof/cmdline", cmdline) -- mux.HandleFunc("/debug/pprof/profile", pprof.Profile) -- mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) -- mux.HandleFunc("/debug/pprof/trace", pprof.Trace) -- if i.prometheus != nil { -- mux.HandleFunc("/metrics/", i.prometheus.Serve) -- } -- if i.rpcs != nil { -- mux.HandleFunc("/rpc/", render(RPCTmpl, i.rpcs.getData)) -- } -- if i.traces != nil { -- mux.HandleFunc("/trace/", render(TraceTmpl, i.traces.getData)) -- } -- mux.HandleFunc("/analysis/", render(AnalysisTmpl, i.getAnalysis)) -- mux.HandleFunc("/cache/", render(CacheTmpl, i.getCache)) -- mux.HandleFunc("/session/", render(SessionTmpl, i.getSession)) -- mux.HandleFunc("/view/", render(ViewTmpl, i.getView)) -- mux.HandleFunc("/client/", render(ClientTmpl, i.getClient)) -- mux.HandleFunc("/server/", render(ServerTmpl, i.getServer)) -- mux.HandleFunc("/file/", render(FileTmpl, i.getFile)) -- mux.HandleFunc("/info", render(InfoTmpl, i.getInfo)) -- mux.HandleFunc("/memory", render(MemoryTmpl, getMemory)) +-imports-flags: +- -d,-diff +- display diffs instead of edited file content +- -l,-list +- display names of edited files +- -preserve +- with -write, make copies of original files +- -w,-write +- write edited content to source files +diff -urN a/gopls/internal/cmd/usage/inspect.hlp b/gopls/internal/cmd/usage/inspect.hlp +--- a/gopls/internal/cmd/usage/inspect.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/inspect.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,8 +0,0 @@ +-interact with the gopls daemon (deprecated: use 'remote') - -- // Internal debugging helpers. -- mux.HandleFunc("/gc", func(w http.ResponseWriter, r *http.Request) { -- runtime.GC() -- runtime.GC() -- runtime.GC() -- http.Redirect(w, r, "/memory", http.StatusTemporaryRedirect) -- }) -- mux.HandleFunc("/_makeabug", func(w http.ResponseWriter, r *http.Request) { -- bug.Report("bug here") -- http.Error(w, "made a bug", http.StatusOK) -- }) +-Usage: +- gopls [flags] inspect [arg]... - -- if err := http.Serve(listener, mux); err != nil { -- event.Error(ctx, "Debug server failed", err) -- return -- } -- event.Log(ctx, "Debug server finished") -- }() -- return i.listenedDebugAddress, nil --} +-Subcommand: +- sessions print information about current gopls sessions +- debug start the debug server +diff -urN a/gopls/internal/cmd/usage/licenses.hlp b/gopls/internal/cmd/usage/licenses.hlp +--- a/gopls/internal/cmd/usage/licenses.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/licenses.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,4 +0,0 @@ +-print licenses of included software - --func (i *Instance) DebugAddress() string { -- i.serveMu.Lock() -- defer i.serveMu.Unlock() -- return i.debugAddress --} +-Usage: +- gopls [flags] licenses +diff -urN a/gopls/internal/cmd/usage/links.hlp b/gopls/internal/cmd/usage/links.hlp +--- a/gopls/internal/cmd/usage/links.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/links.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,12 +0,0 @@ +-list links in a file - --func (i *Instance) ListenedDebugAddress() string { -- i.serveMu.Lock() -- defer i.serveMu.Unlock() -- return i.listenedDebugAddress --} +-Usage: +- gopls [flags] links [links-flags] - --func makeGlobalExporter(stderr io.Writer) event.Exporter { -- p := export.Printer{} -- var pMu sync.Mutex -- return func(ctx context.Context, ev core.Event, lm label.Map) context.Context { -- i := GetInstance(ctx) +-Example: list links contained within a file: - -- if event.IsLog(ev) { -- // Don't log context cancellation errors. -- if err := keys.Err.Get(ev); errors.Is(err, context.Canceled) { -- return ctx -- } -- // Make sure any log messages without an instance go to stderr. -- if i == nil { -- pMu.Lock() -- p.WriteEvent(stderr, ev, lm) -- pMu.Unlock() -- } -- level := log.LabeledLevel(lm) -- // Exclude trace logs from LSP logs. -- if level < log.Trace { -- ctx = protocol.LogEvent(ctx, ev, lm, messageType(level)) -- } -- } -- if i == nil { -- return ctx -- } -- return i.exporter(ctx, ev, lm) -- } --} +- $ gopls links internal/cmd/check.go - --func messageType(l log.Level) protocol.MessageType { -- switch l { -- case log.Error: -- return protocol.Error -- case log.Warning: -- return protocol.Warning -- case log.Debug: -- return protocol.Log -- } -- return protocol.Info --} +-links-flags: +- -json +- emit document links in JSON format +diff -urN a/gopls/internal/cmd/usage/prepare_rename.hlp b/gopls/internal/cmd/usage/prepare_rename.hlp +--- a/gopls/internal/cmd/usage/prepare_rename.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/prepare_rename.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,10 +0,0 @@ +-test validity of a rename operation at location - --func makeInstanceExporter(i *Instance) event.Exporter { -- exporter := func(ctx context.Context, ev core.Event, lm label.Map) context.Context { -- if i.ocagent != nil { -- ctx = i.ocagent.ProcessEvent(ctx, ev, lm) -- } -- if i.prometheus != nil { -- ctx = i.prometheus.ProcessEvent(ctx, ev, lm) -- } -- if i.rpcs != nil { -- ctx = i.rpcs.ProcessEvent(ctx, ev, lm) -- } -- if i.traces != nil { -- ctx = i.traces.ProcessEvent(ctx, ev, lm) -- } -- if event.IsLog(ev) { -- if s := cache.KeyCreateSession.Get(ev); s != nil { -- i.State.addClient(s) -- } -- if sid := tag.NewServer.Get(ev); sid != "" { -- i.State.updateServer(&Server{ -- ID: sid, -- Logfile: tag.Logfile.Get(ev), -- DebugAddress: tag.DebugAddress.Get(ev), -- GoplsPath: tag.GoplsPath.Get(ev), -- ClientID: tag.ClientID.Get(ev), -- }) -- } -- if s := cache.KeyShutdownSession.Get(ev); s != nil { -- i.State.dropClient(s) -- } -- if sid := tag.EndServer.Get(ev); sid != "" { -- i.State.dropServer(sid) -- } -- if s := cache.KeyUpdateSession.Get(ev); s != nil { -- if c := i.State.Client(s.ID()); c != nil { -- c.DebugAddress = tag.DebugAddress.Get(ev) -- c.Logfile = tag.Logfile.Get(ev) -- c.ServerID = tag.ServerID.Get(ev) -- c.GoplsPath = tag.GoplsPath.Get(ev) -- } -- } -- } -- return ctx -- } -- // StdTrace must be above export.Spans below (by convention, export -- // middleware applies its wrapped exporter last). -- exporter = StdTrace(exporter) -- metrics := metric.Config{} -- registerMetrics(&metrics) -- exporter = metrics.Exporter(exporter) -- exporter = export.Spans(exporter) -- exporter = export.Labels(exporter) -- return exporter --} +-Usage: +- gopls [flags] prepare_rename - --type dataFunc func(*http.Request) interface{} +-Example: - --func render(tmpl *template.Template, fun dataFunc) func(http.ResponseWriter, *http.Request) { -- return func(w http.ResponseWriter, r *http.Request) { -- var data interface{} -- if fun != nil { -- data = fun(r) -- } -- if err := tmpl.Execute(w, data); err != nil { -- event.Error(context.Background(), "", err) -- http.Error(w, err.Error(), http.StatusInternalServerError) -- } -- } --} +- $ # 1-indexed location (:line:column or :#offset) of the target identifier +- $ gopls prepare_rename helper/helper.go:8:6 +- $ gopls prepare_rename helper/helper.go:#53 +diff -urN a/gopls/internal/cmd/usage/references.hlp b/gopls/internal/cmd/usage/references.hlp +--- a/gopls/internal/cmd/usage/references.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/references.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,14 +0,0 @@ +-display selected identifier's references - --func commas(s string) string { -- for i := len(s); i > 3; { -- i -= 3 -- s = s[:i] + "," + s[i:] -- } -- return s --} +-Usage: +- gopls [flags] references [references-flags] - --func fuint64(v uint64) string { -- return commas(strconv.FormatUint(v, 10)) --} +-Example: - --func fuint32(v uint32) string { -- return commas(strconv.FormatUint(uint64(v), 10)) --} +- $ # 1-indexed location (:line:column or :#offset) of the target identifier +- $ gopls references helper/helper.go:8:6 +- $ gopls references helper/helper.go:#53 - --func fcontent(v []byte) string { -- return string(v) --} +-references-flags: +- -d,-declaration +- include the declaration of the specified identifier in the results +diff -urN a/gopls/internal/cmd/usage/remote.hlp b/gopls/internal/cmd/usage/remote.hlp +--- a/gopls/internal/cmd/usage/remote.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/remote.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,8 +0,0 @@ +-interact with the gopls daemon - --var BaseTemplate = template.Must(template.New("").Parse(` -- -- --{{template "title" .}} -- --{{block "head" .}}{{end}} -- -- --Main --Info --Memory --Profiling --Metrics --RPC --Trace --Analysis --
--

{{template "title" .}}

--{{block "body" .}} --Unknown page --{{end}} -- -- +-Usage: +- gopls [flags] remote [arg]... - --{{define "cachelink"}}Cache {{.}}{{end}} --{{define "clientlink"}}Client {{.}}{{end}} --{{define "serverlink"}}Server {{.}}{{end}} --{{define "sessionlink"}}Session {{.}}{{end}} --{{define "viewlink"}}View {{.}}{{end}} --`)).Funcs(template.FuncMap{ -- "fuint64": fuint64, -- "fuint32": fuint32, -- "fcontent": fcontent, -- "localAddress": func(s string) string { -- // Try to translate loopback addresses to localhost, both for cosmetics and -- // because unspecified ipv6 addresses can break links on Windows. -- // -- // TODO(rfindley): In the future, it would be better not to assume the -- // server is running on localhost, and instead construct this address using -- // the remote host. -- host, port, err := net.SplitHostPort(s) -- if err != nil { -- return s -- } -- ip := net.ParseIP(host) -- if ip == nil { -- return s -- } -- if ip.IsLoopback() || ip.IsUnspecified() { -- return "localhost:" + port -- } -- return s -- }, -- // TODO(rfindley): re-enable option inspection. -- // "options": func(s *cache.Session) []sessionOption { -- // return showOptions(s.Options()) -- // }, --}) -- --var MainTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` --{{define "title"}}GoPls server information{{end}} --{{define "body"}} --

Caches

--
    {{range .State.Caches}}
  • {{template "cachelink" .ID}}
  • {{end}}
--

Sessions

--
    {{range .State.Sessions}}
  • {{template "sessionlink" .ID}} from {{template "cachelink" .Cache.ID}}
  • {{end}}
--

Clients

--
    {{range .State.Clients}}
  • {{template "clientlink" .Session.ID}}
  • {{end}}
--

Servers

--
    {{range .State.Servers}}
  • {{template "serverlink" .ID}}
  • {{end}}
--

Bug reports

--
{{range .State.Bugs}}
{{.Key}}
{{.Description}}
{{end}}
--{{end}} --`)) -- --var InfoTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` --{{define "title"}}GoPls version information{{end}} --{{define "body"}} --{{.}} --{{end}} --`)) -- --var MemoryTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` --{{define "title"}}Gopls memory usage{{end}} --{{define "head"}}{{end}} --{{define "body"}} --
--

Stats

-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
Allocated bytes{{fuint64 .HeapAlloc}}
Total allocated bytes{{fuint64 .TotalAlloc}}
System bytes{{fuint64 .Sys}}
Heap system bytes{{fuint64 .HeapSys}}
Malloc calls{{fuint64 .Mallocs}}
Frees{{fuint64 .Frees}}
Idle heap bytes{{fuint64 .HeapIdle}}
In use bytes{{fuint64 .HeapInuse}}
Released to system bytes{{fuint64 .HeapReleased}}
Heap object count{{fuint64 .HeapObjects}}
Stack in use bytes{{fuint64 .StackInuse}}
Stack from system bytes{{fuint64 .StackSys}}
Bucket hash bytes{{fuint64 .BuckHashSys}}
GC metadata bytes{{fuint64 .GCSys}}
Off heap bytes{{fuint64 .OtherSys}}
--

By size

-- -- --{{range .BySize}}{{end}} --
SizeMallocsFrees
{{fuint32 .Size}}{{fuint64 .Mallocs}}{{fuint64 .Frees}}
--{{end}} --`)) -- --var DebugTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` --{{define "title"}}GoPls Debug pages{{end}} --{{define "body"}} --Profiling --{{end}} --`)) +-Subcommand: +- sessions print information about current gopls sessions +- debug start the debug server +diff -urN a/gopls/internal/cmd/usage/rename.hlp b/gopls/internal/cmd/usage/rename.hlp +--- a/gopls/internal/cmd/usage/rename.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/rename.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,20 +0,0 @@ +-rename selected identifier - --var CacheTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` --{{define "title"}}Cache {{.ID}}{{end}} --{{define "body"}} --

memoize.Store entries

--
    {{range $k,$v := .MemStats}}
  • {{$k}} - {{$v}}
  • {{end}}
--{{end}} --`)) +-Usage: +- gopls [flags] rename [rename-flags] - --var AnalysisTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` --{{define "title"}}Analysis{{end}} --{{define "body"}} --

Analyzer.Run times

--
    {{range .AnalyzerRunTimes}}
  • {{.Duration}} {{.Label}}
  • {{end}}
--{{end}} --`)) +-Example: - --var ClientTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` --{{define "title"}}Client {{.Session.ID}}{{end}} --{{define "body"}} --Using session: {{template "sessionlink" .Session.ID}}
--{{if .DebugAddress}}Debug this client at: {{localAddress .DebugAddress}}
{{end}} --Logfile: {{.Logfile}}
--Gopls Path: {{.GoplsPath}}
--

Diagnostics

--{{/*Service: []protocol.Server; each server has map[uri]fileReports; -- each fileReport: map[diagnosticSoure]diagnosticReport -- diagnosticSource is one of 5 source -- diagnosticReport: snapshotID and map[hash]*source.Diagnostic -- sourceDiagnostic: struct { -- Range protocol.Range -- Message string -- Source string -- Code string -- CodeHref string -- Severity protocol.DiagnosticSeverity -- Tags []protocol.DiagnosticTag -- -- Related []RelatedInformation -- } -- RelatedInformation: struct { -- URI span.URI -- Range protocol.Range -- Message string -- } -- */}} --
    {{range $k, $v := .Service.Diagnostics}}
  • {{$k}}:
      {{range $v}}
    1. {{.}}
    2. {{end}}
  • {{end}}
--{{end}} --`)) +- $ # 1-based location (:line:column or :#position) of the thing to change +- $ gopls rename helper/helper.go:8:6 Foo +- $ gopls rename helper/helper.go:#53 Foo - --var ServerTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` --{{define "title"}}Server {{.ID}}{{end}} --{{define "body"}} --{{if .DebugAddress}}Debug this server at: {{localAddress .DebugAddress}}
{{end}} --Logfile: {{.Logfile}}
--Gopls Path: {{.GoplsPath}}
--{{end}} --`)) +-rename-flags: +- -d,-diff +- display diffs instead of edited file content +- -l,-list +- display names of edited files +- -preserve +- with -write, make copies of original files +- -w,-write +- write edited content to source files +diff -urN a/gopls/internal/cmd/usage/semtok.hlp b/gopls/internal/cmd/usage/semtok.hlp +--- a/gopls/internal/cmd/usage/semtok.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/semtok.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,8 +0,0 @@ +-show semantic tokens for the specified file - --var SessionTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` --{{define "title"}}Session {{.ID}}{{end}} --{{define "body"}} --From: {{template "cachelink" .Cache.ID}}
--

Views

--
    {{range .Views}}
  • {{.Name}} is {{template "viewlink" .ID}} in {{.Folder}}
  • {{end}}
--

Overlays

--{{$session := .}} -- --{{end}} --`)) +-Usage: +- gopls [flags] semtok - --var ViewTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` --{{define "title"}}View {{.ID}}{{end}} --{{define "body"}} --Name: {{.Name}}
--Folder: {{.Folder}}
--{{end}} --`)) +-Example: show the semantic tokens for this file: - --var FileTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` --{{define "title"}}Overlay {{.FileIdentity.Hash}}{{end}} --{{define "body"}} --{{with .}} -- URI: {{.URI}}
-- Identifier: {{.FileIdentity.Hash}}
-- Version: {{.Version}}
-- Kind: {{.Kind}}
--{{end}} --

Contents

--
{{fcontent .Content}}
--{{end}} --`)) -diff -urN a/gopls/internal/lsp/debug/trace.go b/gopls/internal/lsp/debug/trace.go ---- a/gopls/internal/lsp/debug/trace.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/debug/trace.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,320 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +- $ gopls semtok internal/cmd/semtok.go +diff -urN a/gopls/internal/cmd/usage/serve.hlp b/gopls/internal/cmd/usage/serve.hlp +--- a/gopls/internal/cmd/usage/serve.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/serve.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,30 +0,0 @@ +-run a server for Go code using the Language Server Protocol - --package debug +-Usage: +- gopls [flags] serve [server-flags] +- gopls [flags] [server-flags] - --import ( -- "bytes" -- "context" -- "fmt" -- "html/template" -- "net/http" -- "runtime/trace" -- "sort" -- "strings" -- "sync" -- "time" +-The server communicates using JSONRPC2 on stdin and stdout, and is intended to be run directly as +-a child of an editor process. - -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/core" -- "golang.org/x/tools/internal/event/export" -- "golang.org/x/tools/internal/event/label" --) +-server-flags: +- -debug=string +- serve debug information on the supplied address +- -listen=string +- address on which to listen for remote connections. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. Otherwise, TCP is used. +- -listen.timeout=duration +- when used with -listen, shut down the server when there are no connected clients for this duration +- -logfile=string +- filename to log to. if value is "auto", then logging to a default output file is enabled +- -mode=string +- no effect +- -port=int +- port on which to run gopls for debugging purposes +- -remote.debug=string +- when used with -remote=auto, the -debug value used to start the daemon +- -remote.listen.timeout=duration +- when used with -remote=auto, the -listen.timeout value used to start the daemon (default 1m0s) +- -remote.logfile=string +- when used with -remote=auto, the -logfile value used to start the daemon +- -rpc.trace +- print the full rpc trace in lsp inspector format +diff -urN a/gopls/internal/cmd/usage/signature.hlp b/gopls/internal/cmd/usage/signature.hlp +--- a/gopls/internal/cmd/usage/signature.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/signature.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,10 +0,0 @@ +-display selected identifier's signature - --// TraceTmpl extends BaseTemplate and renders a TraceResults, e.g. from getData(). --var TraceTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` --{{define "title"}}Trace Information{{end}} --{{define "body"}} -- {{range .Traces}}{{.Name}} last: {{.Last.Duration}}, longest: {{.Longest.Duration}}
{{end}} -- {{if .Selected}} --

{{.Selected.Name}}

-- {{if .Selected.Last}}

Last

    {{template "completeSpan" .Selected.Last}}
{{end}} -- {{if .Selected.Longest}}

Longest

    {{template "completeSpan" .Selected.Longest}}
{{end}} -- {{end}} +-Usage: +- gopls [flags] signature - --

Recent spans (oldest first)

--

-- A finite number of recent span start/end times are shown below. -- The nesting represents the children of a parent span (and the log events within a span). -- A span may appear twice: chronologically at toplevel, and nested within its parent. --

--
    {{range .Recent}}{{template "spanStartEnd" .}}{{end}}
--{{end}} --{{define "spanStartEnd"}} -- {{if .Start}} --
  • {{.Span.Header .Start}}
  • -- {{else}} -- {{template "completeSpan" .Span}} -- {{end}} --{{end}} --{{define "completeSpan"}} --
  • {{.Header false}}
  • -- {{if .Events}}
      {{range .Events}}
    • {{.Header}}
    • {{end}}
    {{end}} -- {{if .ChildStartEnd}}
      {{range .ChildStartEnd}}{{template "spanStartEnd" .}}{{end}}
    {{end}} --{{end}} --`)) +-Example: - --type traces struct { -- mu sync.Mutex -- sets map[string]*traceSet -- unfinished map[export.SpanContext]*traceSpan -- recent []spanStartEnd -- recentEvictions int --} +- $ # 1-indexed location (:line:column or :#offset) of the target identifier +- $ gopls signature helper/helper.go:8:6 +- $ gopls signature helper/helper.go:#53 +diff -urN a/gopls/internal/cmd/usage/stats.hlp b/gopls/internal/cmd/usage/stats.hlp +--- a/gopls/internal/cmd/usage/stats.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/stats.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,17 +0,0 @@ +-print workspace statistics - --// A spanStartEnd records the start or end of a span. --// If Start, the span may be unfinished, so some fields (e.g. Finish) --// may be unset and others (e.g. Events) may be being actively populated. --type spanStartEnd struct { -- Start bool -- Span *traceSpan --} +-Usage: +- gopls [flags] stats - --func (ev spanStartEnd) Time() time.Time { -- if ev.Start { -- return ev.Span.Start -- } else { -- return ev.Span.Finish -- } --} +-Load the workspace for the current directory, and output a JSON summary of +-workspace information relevant to performance. As a side effect, this command +-populates the gopls file cache for the current workspace. - --// A TraceResults is the subject for the /trace HTML template. --type TraceResults struct { // exported for testing -- Traces []*traceSet -- Selected *traceSet -- Recent []spanStartEnd --} +-By default, this command may include output that refers to the location or +-content of user code. When the -anon flag is set, fields that may refer to user +-code are hidden. - --// A traceSet holds two representative spans of a given span name. --type traceSet struct { -- Name string -- Last *traceSpan -- Longest *traceSpan --} +-Example: +- $ gopls stats -anon +- -anon +- hide any fields that may contain user names, file names, or source code +diff -urN a/gopls/internal/cmd/usage/symbols.hlp b/gopls/internal/cmd/usage/symbols.hlp +--- a/gopls/internal/cmd/usage/symbols.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/symbols.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,7 +0,0 @@ +-display selected file's symbols - --// A traceSpan holds information about a single span. --type traceSpan struct { -- TraceID export.TraceID -- SpanID export.SpanID -- ParentID export.SpanID -- Name string -- Start time.Time -- Finish time.Time // set at end -- Duration time.Duration // set at end -- Tags string -- Events []traceEvent // set at end -- ChildStartEnd []spanStartEnd // populated while active +-Usage: +- gopls [flags] symbols - -- parent *traceSpan --} +-Example: +- $ gopls symbols helper/helper.go +diff -urN a/gopls/internal/cmd/usage/usage.hlp b/gopls/internal/cmd/usage/usage.hlp +--- a/gopls/internal/cmd/usage/usage.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/usage.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,81 +0,0 @@ - --const timeFormat = "15:04:05.000" +-gopls is a Go language server. - --// Header renders the time, name, tags, and (if !start), --// duration of a span start or end event. --func (span *traceSpan) Header(start bool) string { -- if start { -- return fmt.Sprintf("%s start %s %s", -- span.Start.Format(timeFormat), span.Name, span.Tags) -- } else { -- return fmt.Sprintf("%s end %s (+%s) %s", -- span.Finish.Format(timeFormat), span.Name, span.Duration, span.Tags) -- } --} +-It is typically used with an editor to provide language features. When no +-command is specified, gopls will default to the 'serve' command. The language +-features can also be accessed via the gopls command-line interface. - --type traceEvent struct { -- Time time.Time -- Offset time.Duration // relative to start of span -- Tags string --} +-Usage: +- gopls help [] - --func (ev traceEvent) Header() string { -- return fmt.Sprintf("%s event (+%s) %s", ev.Time.Format(timeFormat), ev.Offset, ev.Tags) --} +-Command: - --func StdTrace(exporter event.Exporter) event.Exporter { -- return func(ctx context.Context, ev core.Event, lm label.Map) context.Context { -- span := export.GetSpan(ctx) -- if span == nil { -- return exporter(ctx, ev, lm) -- } -- switch { -- case event.IsStart(ev): -- if span.ParentID.IsValid() { -- region := trace.StartRegion(ctx, span.Name) -- ctx = context.WithValue(ctx, traceKey, region) -- } else { -- var task *trace.Task -- ctx, task = trace.NewTask(ctx, span.Name) -- ctx = context.WithValue(ctx, traceKey, task) -- } -- // Log the start event as it may contain useful labels. -- msg := formatEvent(ctx, ev, lm) -- trace.Log(ctx, "start", msg) -- case event.IsLog(ev): -- category := "" -- if event.IsError(ev) { -- category = "error" -- } -- msg := formatEvent(ctx, ev, lm) -- trace.Log(ctx, category, msg) -- case event.IsEnd(ev): -- if v := ctx.Value(traceKey); v != nil { -- v.(interface{ End() }).End() -- } -- } -- return exporter(ctx, ev, lm) -- } --} +-Main +- serve run a server for Go code using the Language Server Protocol +- version print the gopls version information +- bug report a bug in gopls +- help print usage information for subcommands +- api-json print JSON describing gopls API +- licenses print licenses of included software +- +-Features +- call_hierarchy display selected identifier's call hierarchy +- check show diagnostic results for the specified file +- codelens List or execute code lenses for a file +- definition show declaration of selected identifier +- execute Execute a gopls custom LSP command +- folding_ranges display selected file's folding ranges +- format format the code according to the go standard +- highlight display selected identifier's highlights +- implementation display selected identifier's implementation +- imports updates import statements +- remote interact with the gopls daemon +- inspect interact with the gopls daemon (deprecated: use 'remote') +- links list links in a file +- prepare_rename test validity of a rename operation at location +- references display selected identifier's references +- rename rename selected identifier +- semtok show semantic tokens for the specified file +- signature display selected identifier's signature +- stats print workspace statistics +- fix apply suggested fixes +- symbols display selected file's symbols +- workspace_symbol search symbols in workspace - --func formatEvent(ctx context.Context, ev core.Event, lm label.Map) string { -- buf := &bytes.Buffer{} -- p := export.Printer{} -- p.WriteEvent(buf, ev, lm) -- return buf.String() --} +-flags: +- -debug=string +- serve debug information on the supplied address +- -listen=string +- address on which to listen for remote connections. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. Otherwise, TCP is used. +- -listen.timeout=duration +- when used with -listen, shut down the server when there are no connected clients for this duration +- -logfile=string +- filename to log to. if value is "auto", then logging to a default output file is enabled +- -mode=string +- no effect +- -ocagent=string +- the address of the ocagent (e.g. http://localhost:55678), or off (default "off") +- -port=int +- port on which to run gopls for debugging purposes +- -profile.alloc=string +- write alloc profile to this file +- -profile.cpu=string +- write CPU profile to this file +- -profile.mem=string +- write memory profile to this file +- -profile.trace=string +- write trace log to this file +- -remote=string +- forward all commands to a remote lsp specified by this flag. With no special prefix, this is assumed to be a TCP address. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. If 'auto', or prefixed by 'auto;', the remote address is automatically resolved based on the executing environment. +- -remote.debug=string +- when used with -remote=auto, the -debug value used to start the daemon +- -remote.listen.timeout=duration +- when used with -remote=auto, the -listen.timeout value used to start the daemon (default 1m0s) +- -remote.logfile=string +- when used with -remote=auto, the -logfile value used to start the daemon +- -rpc.trace +- print the full rpc trace in lsp inspector format +- -v,-verbose +- verbose output +- -vv,-veryverbose +- very verbose output +diff -urN a/gopls/internal/cmd/usage/usage-v.hlp b/gopls/internal/cmd/usage/usage-v.hlp +--- a/gopls/internal/cmd/usage/usage-v.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/usage-v.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,84 +0,0 @@ - --func (t *traces) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) context.Context { -- span := export.GetSpan(ctx) -- if span == nil { -- return ctx -- } +-gopls is a Go language server. - -- switch { -- case event.IsStart(ev): -- // Just starting: add it to the unfinished map. -- // Allocate before the critical section. -- td := &traceSpan{ -- TraceID: span.ID.TraceID, -- SpanID: span.ID.SpanID, -- ParentID: span.ParentID, -- Name: span.Name, -- Start: span.Start().At(), -- Tags: renderLabels(span.Start()), -- } +-It is typically used with an editor to provide language features. When no +-command is specified, gopls will default to the 'serve' command. The language +-features can also be accessed via the gopls command-line interface. - -- t.mu.Lock() -- defer t.mu.Unlock() +-Usage: +- gopls help [] - -- t.addRecentLocked(td, true) // add start event +-Command: - -- if t.sets == nil { -- t.sets = make(map[string]*traceSet) -- t.unfinished = make(map[export.SpanContext]*traceSpan) -- } -- t.unfinished[span.ID] = td +-Main +- serve run a server for Go code using the Language Server Protocol +- version print the gopls version information +- bug report a bug in gopls +- help print usage information for subcommands +- api-json print JSON describing gopls API +- licenses print licenses of included software +- +-Features +- call_hierarchy display selected identifier's call hierarchy +- check show diagnostic results for the specified file +- codelens List or execute code lenses for a file +- definition show declaration of selected identifier +- execute Execute a gopls custom LSP command +- folding_ranges display selected file's folding ranges +- format format the code according to the go standard +- highlight display selected identifier's highlights +- implementation display selected identifier's implementation +- imports updates import statements +- remote interact with the gopls daemon +- inspect interact with the gopls daemon (deprecated: use 'remote') +- links list links in a file +- prepare_rename test validity of a rename operation at location +- references display selected identifier's references +- rename rename selected identifier +- semtok show semantic tokens for the specified file +- signature display selected identifier's signature +- stats print workspace statistics +- fix apply suggested fixes +- symbols display selected file's symbols +- workspace_symbol search symbols in workspace +- +-Internal Use Only +- vulncheck run vulncheck analysis (internal-use only) - -- // Wire up parents if we have them. -- if span.ParentID.IsValid() { -- parentID := export.SpanContext{TraceID: span.ID.TraceID, SpanID: span.ParentID} -- if parent, ok := t.unfinished[parentID]; ok { -- td.parent = parent -- parent.ChildStartEnd = append(parent.ChildStartEnd, spanStartEnd{true, td}) -- } -- } -- -- case event.IsEnd(ev): -- // Finishing: must be already in the map. -- // Allocate events before the critical section. -- events := span.Events() -- tdEvents := make([]traceEvent, len(events)) -- for i, event := range events { -- tdEvents[i] = traceEvent{ -- Time: event.At(), -- Tags: renderLabels(event), -- } -- } -- -- t.mu.Lock() -- defer t.mu.Unlock() -- td, found := t.unfinished[span.ID] -- if !found { -- return ctx // if this happens we are in a bad place -- } -- delete(t.unfinished, span.ID) -- td.Finish = span.Finish().At() -- td.Duration = span.Finish().At().Sub(span.Start().At()) -- td.Events = tdEvents -- t.addRecentLocked(td, false) // add end event +-flags: +- -debug=string +- serve debug information on the supplied address +- -listen=string +- address on which to listen for remote connections. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. Otherwise, TCP is used. +- -listen.timeout=duration +- when used with -listen, shut down the server when there are no connected clients for this duration +- -logfile=string +- filename to log to. if value is "auto", then logging to a default output file is enabled +- -mode=string +- no effect +- -ocagent=string +- the address of the ocagent (e.g. http://localhost:55678), or off (default "off") +- -port=int +- port on which to run gopls for debugging purposes +- -profile.alloc=string +- write alloc profile to this file +- -profile.cpu=string +- write CPU profile to this file +- -profile.mem=string +- write memory profile to this file +- -profile.trace=string +- write trace log to this file +- -remote=string +- forward all commands to a remote lsp specified by this flag. With no special prefix, this is assumed to be a TCP address. If prefixed by 'unix;', the subsequent address is assumed to be a unix domain socket. If 'auto', or prefixed by 'auto;', the remote address is automatically resolved based on the executing environment. +- -remote.debug=string +- when used with -remote=auto, the -debug value used to start the daemon +- -remote.listen.timeout=duration +- when used with -remote=auto, the -listen.timeout value used to start the daemon (default 1m0s) +- -remote.logfile=string +- when used with -remote=auto, the -logfile value used to start the daemon +- -rpc.trace +- print the full rpc trace in lsp inspector format +- -v,-verbose +- verbose output +- -vv,-veryverbose +- very verbose output +diff -urN a/gopls/internal/cmd/usage/version.hlp b/gopls/internal/cmd/usage/version.hlp +--- a/gopls/internal/cmd/usage/version.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/version.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,6 +0,0 @@ +-print the gopls version information - -- set, ok := t.sets[span.Name] -- if !ok { -- set = &traceSet{Name: span.Name} -- t.sets[span.Name] = set -- } -- set.Last = td -- if set.Longest == nil || set.Last.Duration > set.Longest.Duration { -- set.Longest = set.Last -- } -- if td.parent != nil { -- td.parent.ChildStartEnd = append(td.parent.ChildStartEnd, spanStartEnd{false, td}) -- } else { -- fillOffsets(td, td.Start) -- } -- } -- return ctx --} +-Usage: +- gopls [flags] version +- -json +- outputs in json format. +diff -urN a/gopls/internal/cmd/usage/vulncheck.hlp b/gopls/internal/cmd/usage/vulncheck.hlp +--- a/gopls/internal/cmd/usage/vulncheck.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/vulncheck.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,13 +0,0 @@ +-run vulncheck analysis (internal-use only) - --// addRecentLocked appends a start or end event to the "recent" log, --// evicting an old entry if necessary. --func (t *traces) addRecentLocked(span *traceSpan, start bool) { -- t.recent = append(t.recent, spanStartEnd{Start: start, Span: span}) +-Usage: +- gopls [flags] vulncheck - -- const maxRecent = 100 // number of log entries before eviction -- for len(t.recent) > maxRecent { -- t.recent[0] = spanStartEnd{} // aid GC -- t.recent = t.recent[1:] -- t.recentEvictions++ +- WARNING: this command is for internal-use only. - -- // Using a slice as a FIFO queue leads to unbounded growth -- // as Go's GC cannot collect the ever-growing unused prefix. -- // So, compact it periodically. -- if t.recentEvictions%maxRecent == 0 { -- t.recent = append([]spanStartEnd(nil), t.recent...) -- } -- } --} +- By default, the command outputs a JSON-encoded +- golang.org/x/tools/gopls/internal/protocol/command.VulncheckResult +- message. +- Example: +- $ gopls vulncheck - --// getData returns the TraceResults rendered by TraceTmpl for the /trace[/name] endpoint. --func (t *traces) getData(req *http.Request) interface{} { -- // TODO(adonovan): the HTTP request doesn't acquire the mutex -- // for t or for each span! Audit and fix. +diff -urN a/gopls/internal/cmd/usage/workspace_symbol.hlp b/gopls/internal/cmd/usage/workspace_symbol.hlp +--- a/gopls/internal/cmd/usage/workspace_symbol.hlp 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/usage/workspace_symbol.hlp 1970-01-01 00:00:00.000000000 +0000 +@@ -1,13 +0,0 @@ +-search symbols in workspace - -- // Sort last/longest sets by name. -- traces := make([]*traceSet, 0, len(t.sets)) -- for _, set := range t.sets { -- traces = append(traces, set) -- } -- sort.Slice(traces, func(i, j int) bool { -- return traces[i].Name < traces[j].Name -- }) +-Usage: +- gopls [flags] workspace_symbol [workspace_symbol-flags] - -- return TraceResults{ -- Traces: traces, -- Selected: t.sets[strings.TrimPrefix(req.URL.Path, "/trace/")], // may be nil -- Recent: t.recent, -- } --} +-Example: - --func fillOffsets(td *traceSpan, start time.Time) { -- for i := range td.Events { -- td.Events[i].Offset = td.Events[i].Time.Sub(start) -- } -- for _, child := range td.ChildStartEnd { -- if !child.Start { -- fillOffsets(child.Span, start) -- } -- } --} +- $ gopls workspace_symbol -matcher fuzzy 'wsymbols' - --func renderLabels(labels label.List) string { -- buf := &bytes.Buffer{} -- for index := 0; labels.Valid(index); index++ { -- // The 'start' label duplicates the span name, so discard it. -- if l := labels.Label(index); l.Valid() && l.Key().Name() != "start" { -- fmt.Fprintf(buf, "%v ", l) -- } -- } -- return buf.String() --} -diff -urN a/gopls/internal/lsp/definition.go b/gopls/internal/lsp/definition.go ---- a/gopls/internal/lsp/definition.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/definition.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,60 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. +-workspace_symbol-flags: +- -matcher=string +- specifies the type of matcher: fuzzy, fastfuzzy, casesensitive, or caseinsensitive. +- The default is caseinsensitive. +diff -urN a/gopls/internal/cmd/vulncheck.go b/gopls/internal/cmd/vulncheck.go +--- a/gopls/internal/cmd/vulncheck.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/vulncheck.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,47 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package lsp +-package cmd - -import ( - "context" +- "flag" - "fmt" +- "os" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/lsp/template" -- "golang.org/x/tools/gopls/internal/telemetry" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/tag" +- "golang.org/x/tools/gopls/internal/vulncheck/scan" -) - --func (s *Server) definition(ctx context.Context, params *protocol.DefinitionParams) (_ []protocol.Location, rerr error) { -- recordLatency := telemetry.StartLatencyTimer("definition") -- defer func() { -- recordLatency(ctx, rerr) -- }() -- -- ctx, done := event.Start(ctx, "lsp.Server.definition", tag.URI.Of(params.TextDocument.URI)) -- defer done() +-// vulncheck implements the vulncheck command. +-// TODO(hakim): hide from the public. +-type vulncheck struct { +- app *Application +-} - -- // TODO(rfindley): definition requests should be multiplexed across all views. -- snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind) -- defer release() -- if !ok { -- return nil, err -- } -- switch kind := snapshot.FileKind(fh); kind { -- case source.Tmpl: -- return template.Definition(snapshot, fh, params.Position) -- case source.Go: -- return source.Definition(ctx, snapshot, fh, params.Position) -- default: -- return nil, fmt.Errorf("can't find definitions for file type %s", kind) -- } +-func (v *vulncheck) Name() string { return "vulncheck" } +-func (v *vulncheck) Parent() string { return v.app.Name() } +-func (v *vulncheck) Usage() string { return "" } +-func (v *vulncheck) ShortHelp() string { +- return "run vulncheck analysis (internal-use only)" -} +-func (v *vulncheck) DetailedHelp(f *flag.FlagSet) { +- fmt.Fprint(f.Output(), ` +- WARNING: this command is for internal-use only. - --func (s *Server) typeDefinition(ctx context.Context, params *protocol.TypeDefinitionParams) ([]protocol.Location, error) { -- ctx, done := event.Start(ctx, "lsp.Server.typeDefinition", tag.URI.Of(params.TextDocument.URI)) -- defer done() +- By default, the command outputs a JSON-encoded +- golang.org/x/tools/gopls/internal/protocol/command.VulncheckResult +- message. +- Example: +- $ gopls vulncheck - -- // TODO(rfindley): type definition requests should be multiplexed across all views. -- snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go) -- defer release() -- if !ok { -- return nil, err -- } -- switch kind := snapshot.FileKind(fh); kind { -- case source.Go: -- return source.TypeDefinition(ctx, snapshot, fh, params.Position) -- default: -- return nil, fmt.Errorf("can't find type definitions for file type %s", kind) +-`) +-} +- +-func (v *vulncheck) Run(ctx context.Context, args ...string) error { +- if err := scan.Main(ctx, args...); err != nil { +- fmt.Fprintln(os.Stderr, err) +- os.Exit(1) - } +- return nil -} -diff -urN a/gopls/internal/lsp/diagnostics.go b/gopls/internal/lsp/diagnostics.go ---- a/gopls/internal/lsp/diagnostics.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/diagnostics.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,870 +0,0 @@ --// Copyright 2018 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/cmd/workspace_symbol.go b/gopls/internal/cmd/workspace_symbol.go +--- a/gopls/internal/cmd/workspace_symbol.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/cmd/workspace_symbol.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,89 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package lsp +-package cmd - -import ( - "context" -- "crypto/sha256" -- "errors" +- "flag" - "fmt" -- "os" -- "path/filepath" -- "sort" - "strings" -- "sync" -- "time" - -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/mod" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/lsp/template" -- "golang.org/x/tools/gopls/internal/lsp/work" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/tag" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/settings" +- "golang.org/x/tools/internal/tool" -) - --// TODO(rfindley): simplify this very complicated logic for publishing --// diagnostics. While doing so, ensure that we can test subtle logic such as --// for multi-pass diagnostics. -- --// diagnosticSource differentiates different sources of diagnostics. --// --// Diagnostics from the same source overwrite each other, whereas diagnostics --// from different sources do not. Conceptually, the server state is a mapping --// from diagnostics source to a set of diagnostics, and each storeDiagnostics --// operation updates one entry of that mapping. --type diagnosticSource int +-// workspaceSymbol implements the workspace_symbol verb for gopls. +-type workspaceSymbol struct { +- Matcher string `flag:"matcher" help:"specifies the type of matcher: fuzzy, fastfuzzy, casesensitive, or caseinsensitive.\nThe default is caseinsensitive."` - --const ( -- modParseSource diagnosticSource = iota -- modTidySource -- gcDetailsSource -- analysisSource -- typeCheckSource -- orphanedSource -- workSource -- modCheckUpgradesSource -- modVulncheckSource // source.Govulncheck + source.Vulncheck --) -- --// A diagnosticReport holds results for a single diagnostic source. --type diagnosticReport struct { -- snapshotID source.GlobalSnapshotID // global snapshot ID on which the report was computed -- publishedHash string // last published hash for this (URI, source) -- diags map[string]*source.Diagnostic --} -- --// fileReports holds a collection of diagnostic reports for a single file, as --// well as the hash of the last published set of diagnostics. --type fileReports struct { -- // publishedSnapshotID is the last snapshot ID for which we have "published" -- // diagnostics (though the publishDiagnostics notification may not have -- // actually been sent, if nothing changed). -- // -- // Specifically, publishedSnapshotID is updated to a later snapshot ID when -- // we either: -- // (1) publish diagnostics for the file for a snapshot, or -- // (2) determine that published diagnostics are valid for a new snapshot. -- // -- // Notably publishedSnapshotID may not match the snapshot id on individual reports in -- // the reports map: -- // - we may have published partial diagnostics from only a subset of -- // diagnostic sources for which new results have been computed, or -- // - we may have started computing reports for an even new snapshot, but not -- // yet published. -- // -- // This prevents gopls from publishing stale diagnostics. -- publishedSnapshotID source.GlobalSnapshotID -- -- // publishedHash is a hash of the latest diagnostics published for the file. -- publishedHash string -- -- // If set, mustPublish marks diagnostics as needing publication, independent -- // of whether their publishedHash has changed. -- mustPublish bool -- -- // The last stored diagnostics for each diagnostic source. -- reports map[diagnosticSource]*diagnosticReport --} -- --func (d diagnosticSource) String() string { -- switch d { -- case modParseSource: -- return "FromModParse" -- case modTidySource: -- return "FromModTidy" -- case gcDetailsSource: -- return "FromGCDetails" -- case analysisSource: -- return "FromAnalysis" -- case typeCheckSource: -- return "FromTypeChecking" -- case orphanedSource: -- return "FromOrphans" -- case workSource: -- return "FromGoWork" -- case modCheckUpgradesSource: -- return "FromCheckForUpgrades" -- case modVulncheckSource: -- return "FromModVulncheck" -- default: -- return fmt.Sprintf("From?%d?", d) -- } +- app *Application -} - --// hashDiagnostics computes a hash to identify diags. --// --// hashDiagnostics mutates its argument (via sorting). --func hashDiagnostics(diags ...*source.Diagnostic) string { -- if len(diags) == 0 { -- return emptyDiagnosticsHash -- } -- return computeDiagnosticHash(diags...) +-func (r *workspaceSymbol) Name() string { return "workspace_symbol" } +-func (r *workspaceSymbol) Parent() string { return r.app.Name() } +-func (r *workspaceSymbol) Usage() string { return "[workspace_symbol-flags] " } +-func (r *workspaceSymbol) ShortHelp() string { return "search symbols in workspace" } +-func (r *workspaceSymbol) DetailedHelp(f *flag.FlagSet) { +- fmt.Fprint(f.Output(), ` +-Example: +- +- $ gopls workspace_symbol -matcher fuzzy 'wsymbols' +- +-workspace_symbol-flags: +-`) +- printFlagDefaults(f) -} - --// opt: pre-computed hash for empty diagnostics --var emptyDiagnosticsHash = computeDiagnosticHash() +-func (r *workspaceSymbol) Run(ctx context.Context, args ...string) error { +- if len(args) != 1 { +- return tool.CommandLineErrorf("workspace_symbol expects 1 argument") +- } - --// computeDiagnosticHash should only be called from hashDiagnostics. --// --// TODO(rfindley): this should use source.Hash. --func computeDiagnosticHash(diags ...*source.Diagnostic) string { -- source.SortDiagnostics(diags) -- h := sha256.New() -- for _, d := range diags { -- for _, t := range d.Tags { -- fmt.Fprintf(h, "tag: %s\n", t) -- } -- for _, r := range d.Related { -- fmt.Fprintf(h, "related: %s %s %s\n", r.Location.URI.SpanURI(), r.Message, r.Location.Range) +- opts := r.app.options +- r.app.options = func(o *settings.Options) { +- if opts != nil { +- opts(o) - } -- fmt.Fprintf(h, "code: %s\n", d.Code) -- fmt.Fprintf(h, "codeHref: %s\n", d.CodeHref) -- fmt.Fprintf(h, "message: %s\n", d.Message) -- fmt.Fprintf(h, "range: %s\n", d.Range) -- fmt.Fprintf(h, "severity: %s\n", d.Severity) -- fmt.Fprintf(h, "source: %s\n", d.Source) -- if d.BundledFixes != nil { -- fmt.Fprintf(h, "fixes: %s\n", *d.BundledFixes) +- switch strings.ToLower(r.Matcher) { +- case "fuzzy": +- o.SymbolMatcher = settings.SymbolFuzzy +- case "casesensitive": +- o.SymbolMatcher = settings.SymbolCaseSensitive +- case "fastfuzzy": +- o.SymbolMatcher = settings.SymbolFastFuzzy +- default: +- o.SymbolMatcher = settings.SymbolCaseInsensitive - } - } -- return fmt.Sprintf("%x", h.Sum(nil)) --} - --func (s *Server) diagnoseSnapshots(snapshots map[source.Snapshot][]span.URI, onDisk bool, cause ModificationSource) { -- var diagnosticWG sync.WaitGroup -- for snapshot, uris := range snapshots { -- if snapshot.Options().DiagnosticsTrigger == source.DiagnosticsOnSave && cause == FromDidChange { -- continue // user requested to update the diagnostics only on save. do not diagnose yet. -- } -- diagnosticWG.Add(1) -- go func(snapshot source.Snapshot, uris []span.URI) { -- defer diagnosticWG.Done() -- s.diagnoseSnapshot(snapshot, uris, onDisk, snapshot.Options().DiagnosticsDelay) -- }(snapshot, uris) +- conn, err := r.app.connect(ctx, nil) +- if err != nil { +- return err - } -- diagnosticWG.Wait() --} -- --// diagnoseSnapshot computes and publishes diagnostics for the given snapshot. --// --// If delay is non-zero, computing diagnostics does not start until after this --// delay has expired, to allow work to be cancelled by subsequent changes. --// --// If changedURIs is non-empty, it is a set of recently changed files that --// should be diagnosed immediately, and onDisk reports whether these file --// changes came from a change to on-disk files. --// --// TODO(rfindley): eliminate the onDisk parameter, which looks misplaced. If we --// don't want to diagnose changes on disk, filter out the changedURIs. --func (s *Server) diagnoseSnapshot(snapshot source.Snapshot, changedURIs []span.URI, onDisk bool, delay time.Duration) { -- ctx := snapshot.BackgroundContext() -- ctx, done := event.Start(ctx, "Server.diagnoseSnapshot", source.SnapshotLabels(snapshot)...) -- defer done() -- -- if delay > 0 { -- // 2-phase diagnostics. -- // -- // The first phase just parses and type-checks (but -- // does not analyze) packages directly affected by -- // file modifications. -- // -- // The second phase runs after the delay, and does everything. -- // -- // We wait a brief delay before the first phase, to allow higher priority -- // work such as autocompletion to acquire the type checking mutex (though -- // typically both diagnosing changed files and performing autocompletion -- // will be doing the same work: recomputing active packages). -- const minDelay = 20 * time.Millisecond -- select { -- case <-time.After(minDelay): -- case <-ctx.Done(): -- return -- } +- defer conn.terminate(ctx) - -- if len(changedURIs) > 0 { -- s.diagnoseChangedFiles(ctx, snapshot, changedURIs, onDisk) -- s.publishDiagnostics(ctx, false, snapshot) -- } +- p := protocol.WorkspaceSymbolParams{ +- Query: args[0], +- } - -- if delay < minDelay { -- delay = 0 -- } else { -- delay -= minDelay +- symbols, err := conn.Symbol(ctx, &p) +- if err != nil { +- return err +- } +- for _, s := range symbols { +- f, err := conn.openFile(ctx, s.Location.URI) +- if err != nil { +- return err - } -- -- select { -- case <-time.After(delay): -- case <-ctx.Done(): -- return +- span, err := f.locationSpan(s.Location) +- if err != nil { +- return err - } +- fmt.Printf("%s %s %s\n", span, s.Name, s.Kind) - } - -- s.diagnose(ctx, snapshot, analyzeOpenPackages) -- s.publishDiagnostics(ctx, true, snapshot) +- return nil -} +diff -urN a/gopls/internal/debug/info.go b/gopls/internal/debug/info.go +--- a/gopls/internal/debug/info.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/debug/info.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,139 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func (s *Server) diagnoseChangedFiles(ctx context.Context, snapshot source.Snapshot, uris []span.URI, onDisk bool) { -- ctx, done := event.Start(ctx, "Server.diagnoseChangedFiles", source.SnapshotLabels(snapshot)...) -- defer done() -- -- toDiagnose := make(map[source.PackageID]*source.Metadata) -- for _, uri := range uris { -- // If the change is only on-disk and the file is not open, don't -- // directly request its package. It may not be a workspace package. -- if onDisk && !snapshot.IsOpen(uri) { -- continue -- } -- // If the file is not known to the snapshot (e.g., if it was deleted), -- // don't diagnose it. -- if snapshot.FindFile(uri) == nil { -- continue -- } -- -- // Don't request type-checking for builtin.go: it's not a real package. -- if snapshot.IsBuiltin(uri) { -- continue -- } +-// Package debug exports debug information for gopls. +-package debug - -- // Don't diagnose files that are ignored by `go list` (e.g. testdata). -- if snapshot.IgnoredFile(uri) { -- continue -- } +-import ( +- "context" +- "encoding/json" +- "fmt" +- "io" +- "os" +- "runtime" +- "runtime/debug" +- "strings" - -- // Find all packages that include this file and diagnose them in parallel. -- meta, err := source.NarrowestMetadataForFile(ctx, snapshot, uri) -- if err != nil { -- if ctx.Err() != nil { -- return -- } -- // TODO(findleyr): we should probably do something with the error here, -- // but as of now this can fail repeatedly if load fails, so can be too -- // noisy to log (and we'll handle things later in the slow pass). -- continue -- } -- toDiagnose[meta.ID] = meta -- } -- s.diagnosePkgs(ctx, snapshot, toDiagnose, nil) --} +- "golang.org/x/tools/gopls/internal/version" +-) - --// analysisMode parameterizes analysis behavior of a call to diagnosePkgs. --type analysisMode int +-type PrintMode int - -const ( -- analyzeNothing analysisMode = iota // don't run any analysis -- analyzeOpenPackages // run analysis on packages with open files -- analyzeEverything // run analysis on all packages +- PlainText = PrintMode(iota) +- Markdown +- HTML +- JSON -) - --// diagnose is a helper function for running diagnostics with a given context. --// Do not call it directly. forceAnalysis is only true for testing purposes. --func (s *Server) diagnose(ctx context.Context, snapshot source.Snapshot, analyze analysisMode) { -- ctx, done := event.Start(ctx, "Server.diagnose", source.SnapshotLabels(snapshot)...) -- defer done() -- -- // Wait for a free diagnostics slot. -- // TODO(adonovan): opt: shouldn't it be the analysis implementation's -- // job to de-dup and limit resource consumption? In any case this -- // function spends most its time waiting for awaitLoaded, at -- // least initially. -- select { -- case <-ctx.Done(): -- return -- case s.diagnosticsSema <- struct{}{}: -- } -- defer func() { -- <-s.diagnosticsSema -- }() +-// ServerVersion is the format used by gopls to report its version to the +-// client. This format is structured so that the client can parse it easily. +-type ServerVersion struct { +- *debug.BuildInfo +- Version string +-} - -- // common code for dispatching diagnostics -- store := func(dsource diagnosticSource, operation string, diagsByFile map[span.URI][]*source.Diagnostic, err error, merge bool) { -- if err != nil { -- event.Error(ctx, "warning: while "+operation, err, source.SnapshotLabels(snapshot)...) -- } -- for uri, diags := range diagsByFile { -- if uri == "" { -- event.Error(ctx, "missing URI while "+operation, fmt.Errorf("empty URI"), tag.Directory.Of(snapshot.View().Folder().Filename())) -- continue -- } -- s.storeDiagnostics(snapshot, uri, dsource, diags, merge) +-// VersionInfo returns the build info for the gopls process. If it was not +-// built in module mode, we return a GOPATH-specific message with the +-// hardcoded version. +-func VersionInfo() *ServerVersion { +- if info, ok := debug.ReadBuildInfo(); ok { +- return &ServerVersion{ +- Version: version.Version(), +- BuildInfo: info, - } - } +- return &ServerVersion{ +- Version: version.Version(), +- BuildInfo: &debug.BuildInfo{ +- Path: "gopls, built in GOPATH mode", +- GoVersion: runtime.Version(), +- }, +- } +-} - -- // Diagnostics below are organized by increasing specificity: -- // go.work > mod > mod upgrade > mod vuln > package, etc. +-// PrintServerInfo writes HTML debug info to w for the Instance. +-func (i *Instance) PrintServerInfo(ctx context.Context, w io.Writer) { +- workDir, _ := os.Getwd() +- section(w, HTML, "Server Instance", func() { +- fmt.Fprintf(w, "Start time: %v\n", i.StartTime) +- fmt.Fprintf(w, "LogFile: %s\n", i.Logfile) +- fmt.Fprintf(w, "pid: %d\n", os.Getpid()) +- fmt.Fprintf(w, "Working directory: %s\n", workDir) +- fmt.Fprintf(w, "Address: %s\n", i.ServerAddress) +- fmt.Fprintf(w, "Debug address: %s\n", i.DebugAddress()) +- }) +- PrintVersionInfo(ctx, w, true, HTML) +- section(w, HTML, "Command Line", func() { +- fmt.Fprintf(w, "cmdline") +- }) +-} - -- // Diagnose go.work file. -- workReports, workErr := work.Diagnostics(ctx, snapshot) -- if ctx.Err() != nil { -- return +-// PrintVersionInfo writes version information to w, using the output format +-// specified by mode. verbose controls whether additional information is +-// written, including section headers. +-func PrintVersionInfo(_ context.Context, w io.Writer, verbose bool, mode PrintMode) error { +- info := VersionInfo() +- if mode == JSON { +- return printVersionInfoJSON(w, info) - } -- store(workSource, "diagnosing go.work file", workReports, workErr, true) - -- // Diagnose go.mod file. -- modReports, modErr := mod.Diagnostics(ctx, snapshot) -- if ctx.Err() != nil { -- return +- if !verbose { +- printBuildInfo(w, info, false, mode) +- return nil - } -- store(modParseSource, "diagnosing go.mod file", modReports, modErr, true) +- section(w, mode, "Build info", func() { +- printBuildInfo(w, info, true, mode) +- }) +- return nil +-} - -- // Diagnose go.mod upgrades. -- upgradeReports, upgradeErr := mod.UpgradeDiagnostics(ctx, snapshot) -- if ctx.Err() != nil { -- return +-func printVersionInfoJSON(w io.Writer, info *ServerVersion) error { +- js, err := json.MarshalIndent(info, "", "\t") +- if err != nil { +- return err - } -- store(modCheckUpgradesSource, "diagnosing go.mod upgrades", upgradeReports, upgradeErr, true) +- _, err = fmt.Fprint(w, string(js)) +- return err +-} - -- // Diagnose vulnerabilities. -- vulnReports, vulnErr := mod.VulnerabilityDiagnostics(ctx, snapshot) -- if ctx.Err() != nil { -- return +-func section(w io.Writer, mode PrintMode, title string, body func()) { +- switch mode { +- case PlainText: +- fmt.Fprintln(w, title) +- fmt.Fprintln(w, strings.Repeat("-", len(title))) +- body() +- case Markdown: +- fmt.Fprintf(w, "#### %s\n\n```\n", title) +- body() +- fmt.Fprintf(w, "```\n") +- case HTML: +- fmt.Fprintf(w, "

    %s

    \n
    \n", title)
    +-		body()
    +-		fmt.Fprint(w, "
    \n") - } -- store(modVulncheckSource, "diagnosing vulnerabilities", vulnReports, vulnErr, false) +-} - -- workspace, err := snapshot.WorkspaceMetadata(ctx) -- if s.shouldIgnoreError(ctx, snapshot, err) { +-func printBuildInfo(w io.Writer, info *ServerVersion, verbose bool, mode PrintMode) { +- fmt.Fprintf(w, "%v %v\n", info.Path, version.Version()) +- if !verbose { - return - } -- criticalErr := snapshot.CriticalError(ctx) -- if ctx.Err() != nil { // must check ctx after GetCriticalError -- return +- printModuleInfo(w, info.Main, mode) +- for _, dep := range info.Deps { +- printModuleInfo(w, *dep, mode) - } +- fmt.Fprintf(w, "go: %v\n", info.GoVersion) +-} - -- // Show the error as a progress error report so that it appears in the -- // status bar. If a client doesn't support progress reports, the error -- // will still be shown as a ShowMessage. If there is no error, any running -- // error progress reports will be closed. -- s.showCriticalErrorStatus(ctx, snapshot, criticalErr) -- -- // Diagnose template (.tmpl) files. -- for _, f := range snapshot.Templates() { -- diags := template.Diagnose(f) -- s.storeDiagnostics(snapshot, f.URI(), typeCheckSource, diags, true) -- } -- -- // If there are no workspace packages, there is nothing to diagnose and -- // there are no orphaned files. -- if len(workspace) == 0 { -- return -- } -- -- var wg sync.WaitGroup // for potentially slow operations below -- -- // Maybe run go mod tidy (if it has been invalidated). -- // -- // Since go mod tidy can be slow, we run it concurrently to diagnostics. -- wg.Add(1) -- go func() { -- defer wg.Done() -- modTidyReports, err := mod.TidyDiagnostics(ctx, snapshot) -- store(modTidySource, "running go mod tidy", modTidyReports, err, true) -- }() -- -- // Run type checking and go/analysis diagnosis of packages in parallel. -- var ( -- seen = map[span.URI]struct{}{} -- toDiagnose = make(map[source.PackageID]*source.Metadata) -- toAnalyze = make(map[source.PackageID]unit) -- ) -- for _, m := range workspace { -- var hasNonIgnored, hasOpenFile bool -- for _, uri := range m.CompiledGoFiles { -- seen[uri] = struct{}{} -- if !hasNonIgnored && !snapshot.IgnoredFile(uri) { -- hasNonIgnored = true -- } -- if !hasOpenFile && snapshot.IsOpen(uri) { -- hasOpenFile = true -- } -- } -- if hasNonIgnored { -- toDiagnose[m.ID] = m -- if analyze == analyzeEverything || analyze == analyzeOpenPackages && hasOpenFile { -- toAnalyze[m.ID] = unit{} -- } -- } +-func printModuleInfo(w io.Writer, m debug.Module, _ PrintMode) { +- fmt.Fprintf(w, " %s@%s", m.Path, m.Version) +- if m.Sum != "" { +- fmt.Fprintf(w, " %s", m.Sum) - } -- -- wg.Add(1) -- go func() { -- s.diagnosePkgs(ctx, snapshot, toDiagnose, toAnalyze) -- wg.Done() -- }() -- -- wg.Wait() -- -- // Orphaned files. -- // Confirm that every opened file belongs to a package (if any exist in -- // the workspace). Otherwise, add a diagnostic to the file. -- if diags, err := snapshot.OrphanedFileDiagnostics(ctx); err == nil { -- for uri, diag := range diags { -- s.storeDiagnostics(snapshot, uri, orphanedSource, []*source.Diagnostic{diag}, true) -- } -- } else { -- if ctx.Err() == nil { -- event.Error(ctx, "computing orphaned file diagnostics", err, source.SnapshotLabels(snapshot)...) -- } +- if m.Replace != nil { +- fmt.Fprintf(w, " => %v", m.Replace.Path) - } +- fmt.Fprintf(w, "\n") -} +diff -urN a/gopls/internal/debug/info_test.go b/gopls/internal/debug/info_test.go +--- a/gopls/internal/debug/info_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/debug/info_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,50 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// diagnosePkgs type checks packages in toDiagnose, and analyzes packages in --// toAnalyze, merging their diagnostics. Packages in toAnalyze must be a subset --// of the packages in toDiagnose. --// --// It also implements gc_details diagnostics. --// --// TODO(rfindley): revisit handling of analysis gc_details. It may be possible --// to merge this function with Server.diagnose, thereby avoiding the two layers --// of concurrent dispatch: as of writing we concurrently run TidyDiagnostics --// and diagnosePkgs, and diagnosePkgs concurrently runs PackageDiagnostics and --// analysis. --func (s *Server) diagnosePkgs(ctx context.Context, snapshot source.Snapshot, toDiagnose map[source.PackageID]*source.Metadata, toAnalyze map[source.PackageID]unit) { -- ctx, done := event.Start(ctx, "Server.diagnosePkgs", source.SnapshotLabels(snapshot)...) -- defer done() -- -- // Analyze and type-check concurrently, since they are independent -- // operations. -- var ( -- wg sync.WaitGroup -- pkgDiags map[span.URI][]*source.Diagnostic -- analysisDiags = make(map[span.URI][]*source.Diagnostic) -- ) -- -- // Collect package diagnostics. -- wg.Add(1) -- go func() { -- defer wg.Done() -- var ids []source.PackageID -- for id := range toDiagnose { -- ids = append(ids, id) -- } -- var err error -- pkgDiags, err = snapshot.PackageDiagnostics(ctx, ids...) -- if err != nil { -- event.Error(ctx, "warning: diagnostics failed", err, source.SnapshotLabels(snapshot)...) -- } -- }() -- -- // Get diagnostics from analysis framework. -- // This includes type-error analyzers, which suggest fixes to compiler errors. -- wg.Add(1) -- go func() { -- defer wg.Done() -- diags, err := source.Analyze(ctx, snapshot, toAnalyze, s.progress) -- if err != nil { -- var tagStr string // sorted comma-separated list of package IDs -- { -- // TODO(adonovan): replace with a generic map[S]any -> string -- // function in the tag package, and use maps.Keys + slices.Sort. -- keys := make([]string, 0, len(toDiagnose)) -- for id := range toDiagnose { -- keys = append(keys, string(id)) -- } -- sort.Strings(keys) -- tagStr = strings.Join(keys, ",") -- } -- event.Error(ctx, "warning: analyzing package", err, append(source.SnapshotLabels(snapshot), tag.Package.Of(tagStr))...) -- return -- } -- for uri, diags := range diags { -- analysisDiags[uri] = append(analysisDiags[uri], diags...) -- } -- }() -- -- wg.Wait() +-// Package debug exports debug information for gopls. +-package debug - -- // TODO(rfindley): remove the guards against snapshot.IsBuiltin, after the -- // gopls@v0.12.0 release. Packages should not be producing diagnostics for -- // the builtin file: I do not know why this logic existed previously. +-import ( +- "bytes" +- "context" +- "encoding/json" +- "runtime" +- "testing" - -- // Merge analysis diagnostics with package diagnostics, and store the -- // resulting analysis diagnostics. -- for uri, adiags := range analysisDiags { -- if snapshot.IsBuiltin(uri) { -- bug.Reportf("go/analysis reported diagnostics for the builtin file: %v", adiags) -- continue -- } -- tdiags := pkgDiags[uri] -- var tdiags2, adiags2 []*source.Diagnostic -- source.CombineDiagnostics(tdiags, adiags, &tdiags2, &adiags2) -- pkgDiags[uri] = tdiags2 -- s.storeDiagnostics(snapshot, uri, analysisSource, adiags2, true) -- } +- "golang.org/x/tools/gopls/internal/version" +-) - -- // golang/go#59587: guarantee that we store type-checking diagnostics for every compiled -- // package file. -- // -- // Without explicitly storing empty diagnostics, the eager diagnostics -- // publication for changed files will not publish anything for files with -- // empty diagnostics. -- storedPkgDiags := make(map[span.URI]bool) -- for _, m := range toDiagnose { -- for _, uri := range m.CompiledGoFiles { -- s.storeDiagnostics(snapshot, uri, typeCheckSource, pkgDiags[uri], true) -- storedPkgDiags[uri] = true -- } -- } -- // Store the package diagnostics. -- for uri, diags := range pkgDiags { -- if storedPkgDiags[uri] { -- continue -- } -- // builtin.go exists only for documentation purposes, and is not valid Go code. -- // Don't report distracting errors -- if snapshot.IsBuiltin(uri) { -- bug.Reportf("type checking reported diagnostics for the builtin file: %v", diags) -- continue -- } -- s.storeDiagnostics(snapshot, uri, typeCheckSource, diags, true) +-func TestPrintVersionInfoJSON(t *testing.T) { +- buf := new(bytes.Buffer) +- if err := PrintVersionInfo(context.Background(), buf, true, JSON); err != nil { +- t.Fatalf("PrintVersionInfo failed: %v", err) - } +- res := buf.Bytes() - -- // Process requested gc_details diagnostics. -- // -- // TODO(rfindley): this could be improved: -- // 1. This should memoize its results if the package has not changed. -- // 2. This should not even run gc_details if the package contains unsaved -- // files. -- // 3. See note below about using FindFile. -- var toGCDetail map[source.PackageID]*source.Metadata -- s.gcOptimizationDetailsMu.Lock() -- for id := range s.gcOptimizationDetails { -- if m, ok := toDiagnose[id]; ok { -- if toGCDetail == nil { -- toGCDetail = make(map[source.PackageID]*source.Metadata) -- } -- toGCDetail[id] = m -- } +- var got ServerVersion +- if err := json.Unmarshal(res, &got); err != nil { +- t.Fatalf("unexpected output: %v\n%s", err, res) - } -- s.gcOptimizationDetailsMu.Unlock() -- -- for _, m := range toGCDetail { -- gcReports, err := source.GCOptimizationDetails(ctx, snapshot, m) -- if err != nil { -- event.Error(ctx, "warning: gc details", err, append(source.SnapshotLabels(snapshot), tag.Package.Of(string(m.ID)))...) -- } -- s.gcOptimizationDetailsMu.Lock() -- _, enableGCDetails := s.gcOptimizationDetails[m.ID] -- -- // NOTE(golang/go#44826): hold the gcOptimizationDetails lock, and re-check -- // whether gc optimization details are enabled, while storing gc_details -- // results. This ensures that the toggling of GC details and clearing of -- // diagnostics does not race with storing the results here. -- if enableGCDetails { -- for uri, diags := range gcReports { -- // TODO(rfindley): remove the use of FindFile here, and use ReadFile -- // instead. Isn't it enough to know that the package came from the -- // snapshot? Any reports should apply to the snapshot. -- fh := snapshot.FindFile(uri) -- // Don't publish gc details for unsaved buffers, since the underlying -- // logic operates on the file on disk. -- if fh == nil || !fh.SameContentsOnDisk() { -- continue -- } -- s.storeDiagnostics(snapshot, uri, gcDetailsSource, diags, true) -- } -- } -- s.gcOptimizationDetailsMu.Unlock() +- if g, w := got.GoVersion, runtime.Version(); g != w { +- t.Errorf("go version = %v, want %v", g, w) - } --} -- --// mustPublishDiagnostics marks the uri as needing publication, independent of --// whether the published contents have changed. --// --// This can be used for ensuring gopls publishes diagnostics after certain file --// events. --func (s *Server) mustPublishDiagnostics(uri span.URI) { -- s.diagnosticsMu.Lock() -- defer s.diagnosticsMu.Unlock() -- -- if s.diagnostics[uri] == nil { -- s.diagnostics[uri] = &fileReports{ -- publishedHash: hashDiagnostics(), // Hash for 0 diagnostics. -- reports: map[diagnosticSource]*diagnosticReport{}, -- } +- if g, w := got.Version, version.Version(); g != w { +- t.Errorf("gopls version = %v, want %v", g, w) - } -- s.diagnostics[uri].mustPublish = true +- // Other fields of BuildInfo may not be available during test. -} - --// storeDiagnostics stores results from a single diagnostic source. If merge is --// true, it merges results into any existing results for this snapshot. --// --// Mutates (sorts) diags. --// --// TODO(hyangah): investigate whether we can unconditionally overwrite previous report.diags --// with the new diags and eliminate the need for the `merge` flag. --func (s *Server) storeDiagnostics(snapshot source.Snapshot, uri span.URI, dsource diagnosticSource, diags []*source.Diagnostic, merge bool) { -- // Safeguard: ensure that the file actually exists in the snapshot -- // (see golang.org/issues/38602). -- fh := snapshot.FindFile(uri) -- if fh == nil { -- return +-func TestPrintVersionInfoPlainText(t *testing.T) { +- buf := new(bytes.Buffer) +- if err := PrintVersionInfo(context.Background(), buf, true, PlainText); err != nil { +- t.Fatalf("PrintVersionInfo failed: %v", err) - } +- res := buf.Bytes() - -- s.diagnosticsMu.Lock() -- defer s.diagnosticsMu.Unlock() -- if s.diagnostics[uri] == nil { -- s.diagnostics[uri] = &fileReports{ -- publishedHash: hashDiagnostics(), // Hash for 0 diagnostics. -- reports: map[diagnosticSource]*diagnosticReport{}, -- } -- } -- report := s.diagnostics[uri].reports[dsource] -- if report == nil { -- report = new(diagnosticReport) -- s.diagnostics[uri].reports[dsource] = report -- } -- // Don't set obsolete diagnostics. -- if report.snapshotID > snapshot.GlobalID() { -- return -- } -- if report.diags == nil || report.snapshotID != snapshot.GlobalID() || !merge { -- report.diags = map[string]*source.Diagnostic{} -- } -- report.snapshotID = snapshot.GlobalID() -- for _, d := range diags { -- report.diags[hashDiagnostics(d)] = d +- // Other fields of BuildInfo may not be available during test. +- wantGoplsVersion, wantGoVersion := version.Version(), runtime.Version() +- if !bytes.Contains(res, []byte(wantGoplsVersion)) || !bytes.Contains(res, []byte(wantGoVersion)) { +- t.Errorf("plaintext output = %q,\nwant (version: %v, go: %v)", res, wantGoplsVersion, wantGoVersion) - } -} +diff -urN a/gopls/internal/debug/log/log.go b/gopls/internal/debug/log/log.go +--- a/gopls/internal/debug/log/log.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/debug/log/log.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,43 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// clearDiagnosticSource clears all diagnostics for a given source type. It is --// necessary for cases where diagnostics have been invalidated by something --// other than a snapshot change, for example when gc_details is toggled. --func (s *Server) clearDiagnosticSource(dsource diagnosticSource) { -- s.diagnosticsMu.Lock() -- defer s.diagnosticsMu.Unlock() -- for _, reports := range s.diagnostics { -- delete(reports.reports, dsource) -- } --} +-// Package log provides helper methods for exporting log events to the +-// internal/event package. +-package log - --const WorkspaceLoadFailure = "Error loading workspace" +-import ( +- "context" +- "fmt" - --// showCriticalErrorStatus shows the error as a progress report. --// If the error is nil, it clears any existing error progress report. --func (s *Server) showCriticalErrorStatus(ctx context.Context, snapshot source.Snapshot, err *source.CriticalError) { -- s.criticalErrorStatusMu.Lock() -- defer s.criticalErrorStatusMu.Unlock() +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/label" +- "golang.org/x/tools/internal/event/tag" +-) - -- // Remove all newlines so that the error message can be formatted in a -- // status bar. -- var errMsg string -- if err != nil { -- event.Error(ctx, "errors loading workspace", err.MainError, source.SnapshotLabels(snapshot)...) -- for _, d := range err.Diagnostics { -- s.storeDiagnostics(snapshot, d.URI, modParseSource, []*source.Diagnostic{d}, true) -- } -- errMsg = strings.ReplaceAll(err.MainError.Error(), "\n", " ") -- } +-// Level parameterizes log severity. +-type Level int - -- if s.criticalErrorStatus == nil { -- if errMsg != "" { -- s.criticalErrorStatus = s.progress.Start(ctx, WorkspaceLoadFailure, errMsg, nil, nil) -- } -- return -- } +-const ( +- _ Level = iota +- Error +- Warning +- Info +- Debug +- Trace +-) - -- // If an error is already shown to the user, update it or mark it as -- // resolved. -- if errMsg == "" { -- s.criticalErrorStatus.End(ctx, "Done.") -- s.criticalErrorStatus = nil -- } else { -- s.criticalErrorStatus.Report(ctx, errMsg, 0) -- } +-// Log exports a log event labeled with level l. +-func (l Level) Log(ctx context.Context, msg string) { +- event.Log(ctx, msg, tag.Level.Of(int(l))) -} - --// publishDiagnostics collects and publishes any unpublished diagnostic reports. --func (s *Server) publishDiagnostics(ctx context.Context, final bool, snapshot source.Snapshot) { -- ctx, done := event.Start(ctx, "Server.publishDiagnostics", source.SnapshotLabels(snapshot)...) -- defer done() -- -- s.diagnosticsMu.Lock() -- defer s.diagnosticsMu.Unlock() +-// Logf formats and exports a log event labeled with level l. +-func (l Level) Logf(ctx context.Context, format string, args ...interface{}) { +- l.Log(ctx, fmt.Sprintf(format, args...)) +-} - -- for uri, r := range s.diagnostics { -- // Global snapshot IDs are monotonic, so we use them to enforce an ordering -- // for diagnostics. -- // -- // If we've already delivered diagnostics for a future snapshot for this -- // file, do not deliver them. See golang/go#42837 for an example of why -- // this is necessary. -- // -- // TODO(rfindley): even using a global snapshot ID, this mechanism is -- // potentially racy: elsewhere in the code (e.g. invalidateContent) we -- // allow for multiple views track a given file. In this case, we should -- // either only report diagnostics for snapshots from the "best" view of a -- // URI, or somehow merge diagnostics from multiple views. -- if r.publishedSnapshotID > snapshot.GlobalID() { -- continue -- } +-// LabeledLevel extracts the labeled log l +-func LabeledLevel(lm label.Map) Level { +- return Level(tag.Level.Get(lm)) +-} +diff -urN a/gopls/internal/debug/metrics.go b/gopls/internal/debug/metrics.go +--- a/gopls/internal/debug/metrics.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/debug/metrics.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,58 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- anyReportsChanged := false -- reportHashes := map[diagnosticSource]string{} -- var diags []*source.Diagnostic -- for dsource, report := range r.reports { -- if report.snapshotID != snapshot.GlobalID() { -- continue -- } -- var reportDiags []*source.Diagnostic -- for _, d := range report.diags { -- diags = append(diags, d) -- reportDiags = append(reportDiags, d) -- } +-package debug - -- hash := hashDiagnostics(reportDiags...) -- if hash != report.publishedHash { -- anyReportsChanged = true -- } -- reportHashes[dsource] = hash -- } +-import ( +- "golang.org/x/tools/internal/event/export/metric" +- "golang.org/x/tools/internal/event/label" +- "golang.org/x/tools/internal/event/tag" +-) - -- if !final && !anyReportsChanged { -- // Don't invalidate existing reports on the client if we haven't got any -- // new information. -- continue -- } +-var ( +- // the distributions we use for histograms +- bytesDistribution = []int64{1 << 10, 1 << 11, 1 << 12, 1 << 14, 1 << 16, 1 << 20} +- millisecondsDistribution = []float64{0.1, 0.5, 1, 2, 5, 10, 50, 100, 500, 1000, 5000, 10000, 50000, 100000} - -- hash := hashDiagnostics(diags...) -- if hash == r.publishedHash && !r.mustPublish { -- // Update snapshotID to be the latest snapshot for which this diagnostic -- // hash is valid. -- r.publishedSnapshotID = snapshot.GlobalID() -- continue -- } -- var version int32 -- if fh := snapshot.FindFile(uri); fh != nil { // file may have been deleted -- version = fh.Version() -- } -- if err := s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{ -- Diagnostics: toProtocolDiagnostics(diags), -- URI: protocol.URIFromSpanURI(uri), -- Version: version, -- }); err == nil { -- r.publishedHash = hash -- r.mustPublish = false // diagnostics have been successfully published -- r.publishedSnapshotID = snapshot.GlobalID() -- // When we publish diagnostics for a file, we must update the -- // publishedHash for every report, not just the reports that were -- // published. Eliding a report is equivalent to publishing empty -- // diagnostics. -- for dsource, report := range r.reports { -- if hash, ok := reportHashes[dsource]; ok { -- report.publishedHash = hash -- } else { -- // The report was not (yet) stored for this snapshot. Record that we -- // published no diagnostics from this source. -- report.publishedHash = hashDiagnostics() -- } -- } -- } else { -- if ctx.Err() != nil { -- // Publish may have failed due to a cancelled context. -- return -- } -- event.Error(ctx, "publishReports: failed to deliver diagnostic", err, tag.URI.Of(uri)) -- } +- receivedBytes = metric.HistogramInt64{ +- Name: "received_bytes", +- Description: "Distribution of received bytes, by method.", +- Keys: []label.Key{tag.RPCDirection, tag.Method}, +- Buckets: bytesDistribution, - } --} - --func toProtocolDiagnostics(diagnostics []*source.Diagnostic) []protocol.Diagnostic { -- reports := []protocol.Diagnostic{} -- for _, diag := range diagnostics { -- pdiag := protocol.Diagnostic{ -- // diag.Message might start with \n or \t -- Message: strings.TrimSpace(diag.Message), -- Range: diag.Range, -- Severity: diag.Severity, -- Source: string(diag.Source), -- Tags: emptySliceDiagnosticTag(diag.Tags), -- RelatedInformation: diag.Related, -- Data: diag.BundledFixes, -- } -- if diag.Code != "" { -- pdiag.Code = diag.Code -- } -- if diag.CodeHref != "" { -- pdiag.CodeDescription = &protocol.CodeDescription{Href: diag.CodeHref} -- } -- reports = append(reports, pdiag) +- sentBytes = metric.HistogramInt64{ +- Name: "sent_bytes", +- Description: "Distribution of sent bytes, by method.", +- Keys: []label.Key{tag.RPCDirection, tag.Method}, +- Buckets: bytesDistribution, - } -- return reports --} - --func (s *Server) shouldIgnoreError(ctx context.Context, snapshot source.Snapshot, err error) bool { -- if err == nil { // if there is no error at all -- return false -- } -- if errors.Is(err, context.Canceled) { -- return true +- latency = metric.HistogramFloat64{ +- Name: "latency", +- Description: "Distribution of latency in milliseconds, by method.", +- Keys: []label.Key{tag.RPCDirection, tag.Method}, +- Buckets: millisecondsDistribution, - } -- // If the folder has no Go code in it, we shouldn't spam the user with a warning. -- // TODO(rfindley): surely it is not correct to walk the folder here just to -- // suppress diagnostics, every time we compute diagnostics. -- var hasGo bool -- _ = filepath.Walk(snapshot.View().Folder().Filename(), func(path string, info os.FileInfo, err error) error { -- if err != nil { -- return err -- } -- if !strings.HasSuffix(info.Name(), ".go") { -- return nil -- } -- hasGo = true -- return errors.New("done") -- }) -- return !hasGo --} - --// Diagnostics formattedfor the debug server --// (all the relevant fields of Server are private) --// (The alternative is to export them) --func (s *Server) Diagnostics() map[string][]string { -- ans := make(map[string][]string) -- s.diagnosticsMu.Lock() -- defer s.diagnosticsMu.Unlock() -- for k, v := range s.diagnostics { -- fn := k.Filename() -- for typ, d := range v.reports { -- if len(d.diags) == 0 { -- continue -- } -- for _, dx := range d.diags { -- ans[fn] = append(ans[fn], auxStr(dx, d, typ)) -- } -- } +- started = metric.Scalar{ +- Name: "started", +- Description: "Count of RPCs started by method.", +- Keys: []label.Key{tag.RPCDirection, tag.Method}, - } -- return ans --} - --func auxStr(v *source.Diagnostic, d *diagnosticReport, typ diagnosticSource) string { -- // Tags? RelatedInformation? -- msg := fmt.Sprintf("(%s)%q(source:%q,code:%q,severity:%s,snapshot:%d,type:%s)", -- v.Range, v.Message, v.Source, v.Code, v.Severity, d.snapshotID, typ) -- for _, r := range v.Related { -- msg += fmt.Sprintf(" [%s:%s,%q]", r.Location.URI.SpanURI().Filename(), r.Location.Range, r.Message) +- completed = metric.Scalar{ +- Name: "completed", +- Description: "Count of RPCs completed by method and status.", +- Keys: []label.Key{tag.RPCDirection, tag.Method, tag.StatusCode}, - } -- return msg +-) +- +-func registerMetrics(m *metric.Config) { +- receivedBytes.Record(m, tag.ReceivedBytes) +- sentBytes.Record(m, tag.SentBytes) +- latency.Record(m, tag.Latency) +- started.Count(m, tag.Started) +- completed.Count(m, tag.Latency) -} -diff -urN a/gopls/internal/lsp/fake/client.go b/gopls/internal/lsp/fake/client.go ---- a/gopls/internal/lsp/fake/client.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/fake/client.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,193 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/debug/rpc.go b/gopls/internal/debug/rpc.go +--- a/gopls/internal/debug/rpc.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/debug/rpc.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,239 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package fake +-package debug - -import ( - "context" -- "encoding/json" - "fmt" +- "html/template" +- "net/http" +- "sort" +- "sync" +- "time" - -- "golang.org/x/tools/gopls/internal/lsp/glob" -- "golang.org/x/tools/gopls/internal/lsp/protocol" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/core" +- "golang.org/x/tools/internal/event/export" +- "golang.org/x/tools/internal/event/label" +- "golang.org/x/tools/internal/event/tag" -) - --// ClientHooks are a set of optional hooks called during handling of --// the corresponding client method (see protocol.Client for the --// LSP server-to-client RPCs) in order to make test expectations --// awaitable. --type ClientHooks struct { -- OnLogMessage func(context.Context, *protocol.LogMessageParams) error -- OnDiagnostics func(context.Context, *protocol.PublishDiagnosticsParams) error -- OnWorkDoneProgressCreate func(context.Context, *protocol.WorkDoneProgressCreateParams) error -- OnProgress func(context.Context, *protocol.ProgressParams) error -- OnShowDocument func(context.Context, *protocol.ShowDocumentParams) error -- OnShowMessage func(context.Context, *protocol.ShowMessageParams) error -- OnShowMessageRequest func(context.Context, *protocol.ShowMessageRequestParams) error -- OnRegisterCapability func(context.Context, *protocol.RegistrationParams) error -- OnUnregisterCapability func(context.Context, *protocol.UnregistrationParams) error -- OnApplyEdit func(context.Context, *protocol.ApplyWorkspaceEditParams) error --} +-var RPCTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` +-{{define "title"}}RPC Information{{end}} +-{{define "body"}} +-

    Inbound

    +- {{template "rpcSection" .Inbound}} +-

    Outbound

    +- {{template "rpcSection" .Outbound}} +-{{end}} +-{{define "rpcSection"}} +- {{range .}}

    +- {{.Method}} {{.Started}} traces ({{.InProgress}} in progress) +-
    +- Latency {{with .Latency}}{{.Mean}} ({{.Min}}<{{.Max}}){{end}} +- By bucket 0s {{range .Latency.Values}}{{if gt .Count 0}}{{.Count}} {{.Limit}} {{end}}{{end}} +-
    +- Received {{.Received}} (avg. {{.ReceivedMean}}) +- Sent {{.Sent}} (avg. {{.SentMean}}) +-
    +- Result codes {{range .Codes}}{{.Key}}={{.Count}} {{end}} +-

    +- {{end}} +-{{end}} +-`)) - --// Client is an adapter that converts an *Editor into an LSP Client. It mostly --// delegates functionality to hooks that can be configured by tests. --type Client struct { -- editor *Editor -- hooks ClientHooks -- skipApplyEdits bool // don't apply edits from ApplyEdit downcalls to Editor +-type Rpcs struct { // exported for testing +- mu sync.Mutex +- Inbound []*rpcStats // stats for incoming lsp rpcs sorted by method name +- Outbound []*rpcStats // stats for outgoing lsp rpcs sorted by method name -} - --func (c *Client) CodeLensRefresh(context.Context) error { return nil } -- --func (c *Client) InlayHintRefresh(context.Context) error { return nil } -- --func (c *Client) DiagnosticRefresh(context.Context) error { return nil } -- --func (c *Client) InlineValueRefresh(context.Context) error { return nil } -- --func (c *Client) SemanticTokensRefresh(context.Context) error { return nil } -- --func (c *Client) LogTrace(context.Context, *protocol.LogTraceParams) error { return nil } +-type rpcStats struct { +- Method string +- Started int64 +- Completed int64 - --func (c *Client) ShowMessage(ctx context.Context, params *protocol.ShowMessageParams) error { -- if c.hooks.OnShowMessage != nil { -- return c.hooks.OnShowMessage(ctx, params) -- } -- return nil +- Latency rpcTimeHistogram +- Received byteUnits +- Sent byteUnits +- Codes []*rpcCodeBucket -} - --func (c *Client) ShowMessageRequest(ctx context.Context, params *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error) { -- if c.hooks.OnShowMessageRequest != nil { -- if err := c.hooks.OnShowMessageRequest(ctx, params); err != nil { -- return nil, err -- } -- } -- if c.editor.config.MessageResponder != nil { -- return c.editor.config.MessageResponder(params) -- } -- return nil, nil // don't choose, which is effectively dismissing the message +-type rpcTimeHistogram struct { +- Sum timeUnits +- Count int64 +- Min timeUnits +- Max timeUnits +- Values []rpcTimeBucket -} - --func (c *Client) LogMessage(ctx context.Context, params *protocol.LogMessageParams) error { -- if c.hooks.OnLogMessage != nil { -- return c.hooks.OnLogMessage(ctx, params) -- } -- return nil +-type rpcTimeBucket struct { +- Limit timeUnits +- Count int64 -} - --func (c *Client) Event(ctx context.Context, event *interface{}) error { -- return nil +-type rpcCodeBucket struct { +- Key string +- Count int64 -} - --func (c *Client) PublishDiagnostics(ctx context.Context, params *protocol.PublishDiagnosticsParams) error { -- if c.hooks.OnDiagnostics != nil { -- return c.hooks.OnDiagnostics(ctx, params) +-func (r *Rpcs) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) context.Context { +- r.mu.Lock() +- defer r.mu.Unlock() +- switch { +- case event.IsStart(ev): +- if _, stats := r.getRPCSpan(ctx); stats != nil { +- stats.Started++ +- } +- case event.IsEnd(ev): +- span, stats := r.getRPCSpan(ctx) +- if stats != nil { +- endRPC(span, stats) +- } +- case event.IsMetric(ev): +- sent := byteUnits(tag.SentBytes.Get(lm)) +- rec := byteUnits(tag.ReceivedBytes.Get(lm)) +- if sent != 0 || rec != 0 { +- if _, stats := r.getRPCSpan(ctx); stats != nil { +- stats.Sent += sent +- stats.Received += rec +- } +- } - } -- return nil +- return ctx -} - --func (c *Client) WorkspaceFolders(context.Context) ([]protocol.WorkspaceFolder, error) { -- return []protocol.WorkspaceFolder{}, nil --} +-func endRPC(span *export.Span, stats *rpcStats) { +- // update the basic counts +- stats.Completed++ - --func (c *Client) Configuration(_ context.Context, p *protocol.ParamConfiguration) ([]interface{}, error) { -- results := make([]interface{}, len(p.Items)) -- for i, item := range p.Items { -- if item.Section == "gopls" { -- config := c.editor.Config() -- results[i] = makeSettings(c.editor.sandbox, config) +- // get and record the status code +- if status := getStatusCode(span); status != "" { +- var b *rpcCodeBucket +- for c, entry := range stats.Codes { +- if entry.Key == status { +- b = stats.Codes[c] +- break +- } +- } +- if b == nil { +- b = &rpcCodeBucket{Key: status} +- stats.Codes = append(stats.Codes, b) +- sort.Slice(stats.Codes, func(i int, j int) bool { +- return stats.Codes[i].Key < stats.Codes[j].Key +- }) - } +- b.Count++ - } -- return results, nil --} - --func (c *Client) RegisterCapability(ctx context.Context, params *protocol.RegistrationParams) error { -- if c.hooks.OnRegisterCapability != nil { -- if err := c.hooks.OnRegisterCapability(ctx, params); err != nil { -- return err +- // calculate latency if this was an rpc span +- elapsedTime := span.Finish().At().Sub(span.Start().At()) +- latencyMillis := timeUnits(elapsedTime) / timeUnits(time.Millisecond) +- if stats.Latency.Count == 0 { +- stats.Latency.Min = latencyMillis +- stats.Latency.Max = latencyMillis +- } else { +- if stats.Latency.Min > latencyMillis { +- stats.Latency.Min = latencyMillis +- } +- if stats.Latency.Max < latencyMillis { +- stats.Latency.Max = latencyMillis - } - } -- // Update file watching patterns. -- // -- // TODO(rfindley): We could verify more here, like verify that the -- // registration ID is distinct, and that the capability is not currently -- // registered. -- for _, registration := range params.Registrations { -- if registration.Method == "workspace/didChangeWatchedFiles" { -- // Marshal and unmarshal to interpret RegisterOptions as -- // DidChangeWatchedFilesRegistrationOptions. -- raw, err := json.Marshal(registration.RegisterOptions) -- if err != nil { -- return fmt.Errorf("marshaling registration options: %v", err) -- } -- var opts protocol.DidChangeWatchedFilesRegistrationOptions -- if err := json.Unmarshal(raw, &opts); err != nil { -- return fmt.Errorf("unmarshaling registration options: %v", err) -- } -- var globs []*glob.Glob -- for _, watcher := range opts.Watchers { -- // TODO(rfindley): honor the watch kind. -- g, err := glob.Parse(watcher.GlobPattern) -- if err != nil { -- return fmt.Errorf("error parsing glob pattern %q: %v", watcher.GlobPattern, err) -- } -- globs = append(globs, g) -- } -- c.editor.mu.Lock() -- c.editor.watchPatterns = globs -- c.editor.mu.Unlock() +- stats.Latency.Count++ +- stats.Latency.Sum += latencyMillis +- for i := range stats.Latency.Values { +- if stats.Latency.Values[i].Limit > latencyMillis { +- stats.Latency.Values[i].Count++ +- break - } - } -- return nil -} - --func (c *Client) UnregisterCapability(ctx context.Context, params *protocol.UnregistrationParams) error { -- if c.hooks.OnUnregisterCapability != nil { -- return c.hooks.OnUnregisterCapability(ctx, params) +-func (r *Rpcs) getRPCSpan(ctx context.Context) (*export.Span, *rpcStats) { +- // get the span +- span := export.GetSpan(ctx) +- if span == nil { +- return nil, nil - } -- return nil +- // use the span start event look up the correct stats block +- // we do this because it prevents us matching a sub span +- return span, r.getRPCStats(span.Start()) -} - --func (c *Client) Progress(ctx context.Context, params *protocol.ProgressParams) error { -- if c.hooks.OnProgress != nil { -- return c.hooks.OnProgress(ctx, params) +-func (r *Rpcs) getRPCStats(lm label.Map) *rpcStats { +- method := tag.Method.Get(lm) +- if method == "" { +- return nil - } -- return nil --} +- set := &r.Inbound +- if tag.RPCDirection.Get(lm) != tag.Inbound { +- set = &r.Outbound +- } +- // get the record for this method +- index := sort.Search(len(*set), func(i int) bool { +- return (*set)[i].Method >= method +- }) - --func (c *Client) WorkDoneProgressCreate(ctx context.Context, params *protocol.WorkDoneProgressCreateParams) error { -- if c.hooks.OnWorkDoneProgressCreate != nil { -- return c.hooks.OnWorkDoneProgressCreate(ctx, params) +- if index < len(*set) && (*set)[index].Method == method { +- return (*set)[index] - } -- return nil --} - --func (c *Client) ShowDocument(ctx context.Context, params *protocol.ShowDocumentParams) (*protocol.ShowDocumentResult, error) { -- if c.hooks.OnShowDocument != nil { -- if err := c.hooks.OnShowDocument(ctx, params); err != nil { -- return nil, err -- } -- return &protocol.ShowDocumentResult{Success: true}, nil +- old := *set +- *set = make([]*rpcStats, len(old)+1) +- copy(*set, old[:index]) +- copy((*set)[index+1:], old[index:]) +- stats := &rpcStats{Method: method} +- stats.Latency.Values = make([]rpcTimeBucket, len(millisecondsDistribution)) +- for i, m := range millisecondsDistribution { +- stats.Latency.Values[i].Limit = timeUnits(m) - } -- return nil, nil +- (*set)[index] = stats +- return stats -} - --func (c *Client) ApplyEdit(ctx context.Context, params *protocol.ApplyWorkspaceEditParams) (*protocol.ApplyWorkspaceEditResult, error) { -- if len(params.Edit.Changes) != 0 { -- return &protocol.ApplyWorkspaceEditResult{FailureReason: "Edit.Changes is unsupported"}, nil -- } -- if c.hooks.OnApplyEdit != nil { -- if err := c.hooks.OnApplyEdit(ctx, params); err != nil { -- return nil, err -- } -- } -- if !c.skipApplyEdits { -- for _, change := range params.Edit.DocumentChanges { -- if err := c.editor.applyDocumentChange(ctx, change); err != nil { -- return nil, err -- } +-func (s *rpcStats) InProgress() int64 { return s.Started - s.Completed } +-func (s *rpcStats) SentMean() byteUnits { return s.Sent / byteUnits(s.Started) } +-func (s *rpcStats) ReceivedMean() byteUnits { return s.Received / byteUnits(s.Started) } +- +-func (h *rpcTimeHistogram) Mean() timeUnits { return h.Sum / timeUnits(h.Count) } +- +-func getStatusCode(span *export.Span) string { +- for _, ev := range span.Events() { +- if status := tag.StatusCode.Get(ev); status != "" { +- return status - } - } -- return &protocol.ApplyWorkspaceEditResult{Applied: true}, nil +- return "" -} -diff -urN a/gopls/internal/lsp/fake/doc.go b/gopls/internal/lsp/fake/doc.go ---- a/gopls/internal/lsp/fake/doc.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/fake/doc.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,19 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --// Package fake provides fake implementations of a text editor, LSP client --// plugin, and Sandbox environment for use in tests. --// --// The Editor type provides a high level API for text editor operations --// (open/modify/save/close a buffer, jump to definition, etc.), and the Client --// type exposes an LSP client for the editor that can be connected to a --// language server. By default, the Editor and Client should be compliant with --// the LSP spec: their intended use is to verify server compliance with the --// spec in a variety of environment. Possible future enhancements of these --// types may allow them to misbehave in configurable ways, but that is not --// their primary use. --// --// The Sandbox type provides a facility for executing tests with a temporary --// directory, module proxy, and GOPATH. --package fake -diff -urN a/gopls/internal/lsp/fake/edit.go b/gopls/internal/lsp/fake/edit.go ---- a/gopls/internal/lsp/fake/edit.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/fake/edit.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,51 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package fake -- --import ( -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/internal/diff" --) +-func (r *Rpcs) getData(req *http.Request) interface{} { +- return r +-} - --// NewEdit creates an edit replacing all content between the 0-based --// (startLine, startColumn) and (endLine, endColumn) with text. --// --// Columns measure UTF-16 codes. --func NewEdit(startLine, startColumn, endLine, endColumn uint32, text string) protocol.TextEdit { -- return protocol.TextEdit{ -- Range: protocol.Range{ -- Start: protocol.Position{Line: startLine, Character: startColumn}, -- End: protocol.Position{Line: endLine, Character: endColumn}, -- }, -- NewText: text, +-func units(v float64, suffixes []string) string { +- s := "" +- for _, s = range suffixes { +- n := v / 1000 +- if n < 1 { +- break +- } +- v = n - } +- return fmt.Sprintf("%.2f%s", v, s) -} - --func EditToChangeEvent(e protocol.TextEdit) protocol.TextDocumentContentChangeEvent { -- var rng protocol.Range = e.Range -- return protocol.TextDocumentContentChangeEvent{ -- Range: &rng, -- Text: e.NewText, -- } +-type timeUnits float64 +- +-func (v timeUnits) String() string { +- v = v * 1000 * 1000 +- return units(float64(v), []string{"ns", "μs", "ms", "s"}) -} - --// applyEdits applies the edits to a file with the specified lines, --// and returns a new slice containing the lines of the patched file. --// It is a wrapper around diff.Apply; see that function for preconditions. --func applyEdits(mapper *protocol.Mapper, edits []protocol.TextEdit, windowsLineEndings bool) ([]byte, error) { -- diffEdits, err := source.FromProtocolEdits(mapper, edits) -- if err != nil { -- return nil, err -- } -- patched, err := diff.ApplyBytes(mapper.Content, diffEdits) -- if err != nil { -- return nil, err -- } -- if windowsLineEndings { -- patched = toWindowsLineEndings(patched) -- } -- return patched, nil +-type byteUnits float64 +- +-func (v byteUnits) String() string { +- return units(float64(v), []string{"B", "KB", "MB", "GB", "TB"}) -} -diff -urN a/gopls/internal/lsp/fake/editor.go b/gopls/internal/lsp/fake/editor.go ---- a/gopls/internal/lsp/fake/editor.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/fake/editor.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,1571 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/debug/serve.go b/gopls/internal/debug/serve.go +--- a/gopls/internal/debug/serve.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/debug/serve.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,850 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package fake +-package debug - -import ( - "bytes" - "context" -- "encoding/json" - "errors" - "fmt" +- "html/template" +- "io" +- stdlog "log" +- "net" +- "net/http" +- "net/http/pprof" - "os" - "path" - "path/filepath" -- "regexp" +- "runtime" +- "strconv" - "strings" - "sync" +- "time" - -- "golang.org/x/tools/gopls/internal/lsp/command" -- "golang.org/x/tools/gopls/internal/lsp/glob" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/jsonrpc2" -- "golang.org/x/tools/internal/jsonrpc2/servertest" -- "golang.org/x/tools/internal/xcontext" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/debug/log" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/core" +- "golang.org/x/tools/internal/event/export" +- "golang.org/x/tools/internal/event/export/metric" +- "golang.org/x/tools/internal/event/export/ocagent" +- "golang.org/x/tools/internal/event/export/prometheus" +- "golang.org/x/tools/internal/event/keys" +- "golang.org/x/tools/internal/event/label" +- "golang.org/x/tools/internal/event/tag" -) - --// Editor is a fake editor client. It keeps track of client state and can be --// used for writing LSP tests. --type Editor struct { +-type contextKeyType int - -- // Server, client, and sandbox are concurrency safe and written only -- // at construction time, so do not require synchronization. -- Server protocol.Server -- cancelConn func() -- serverConn jsonrpc2.Conn -- client *Client -- sandbox *Sandbox +-const ( +- instanceKey contextKeyType = iota +- traceKey +-) - -- // TODO(adonovan): buffers should be keyed by protocol.DocumentURI. -- mu sync.Mutex -- config EditorConfig // editor configuration -- buffers map[string]buffer // open buffers (relative path -> buffer content) -- serverCapabilities protocol.ServerCapabilities // capabilities / options -- semTokOpts protocol.SemanticTokensOptions -- watchPatterns []*glob.Glob // glob patterns to watch +-// An Instance holds all debug information associated with a gopls instance. +-type Instance struct { +- Logfile string +- StartTime time.Time +- ServerAddress string +- OCAgentConfig string - -- // Call metrics for the purpose of expectations. This is done in an ad-hoc -- // manner for now. Perhaps in the future we should do something more -- // systematic. Guarded with a separate mutex as calls may need to be accessed -- // asynchronously via callbacks into the Editor. -- callsMu sync.Mutex -- calls CallCounts --} +- LogWriter io.Writer - --// CallCounts tracks the number of protocol notifications of different types. --type CallCounts struct { -- DidOpen, DidChange, DidSave, DidChangeWatchedFiles, DidClose, DidChangeConfiguration uint64 --} +- exporter event.Exporter - --// buffer holds information about an open buffer in the editor. --type buffer struct { -- version int // monotonic version; incremented on edits -- path string // relative path in the workspace -- mapper *protocol.Mapper // buffer content -- dirty bool // if true, content is unsaved (TODO(rfindley): rename this field) --} +- ocagent *ocagent.Exporter +- prometheus *prometheus.Exporter +- rpcs *Rpcs +- traces *traces +- State *State - --func (b buffer) text() string { -- return string(b.mapper.Content) +- serveMu sync.Mutex +- debugAddress string +- listenedDebugAddress string -} - --// EditorConfig configures the editor's LSP session. This is similar to --// source.UserOptions, but we use a separate type here so that we expose only --// that configuration which we support. --// --// The zero value for EditorConfig is the default configuration. --type EditorConfig struct { -- // ClientName sets the clientInfo.name for the LSP session (in the initialize request). -- // -- // Since this can only be set during initialization, changing this field via -- // Editor.ChangeConfiguration has no effect. -- ClientName string -- -- // Env holds environment variables to apply on top of the default editor -- // environment. When applying these variables, the special string -- // $SANDBOX_WORKDIR is replaced by the absolute path to the sandbox working -- // directory. -- Env map[string]string -- -- // WorkspaceFolders is the workspace folders to configure on the LSP server, -- // relative to the sandbox workdir. -- // -- // As a special case, if WorkspaceFolders is nil the editor defaults to -- // configuring a single workspace folder corresponding to the workdir root. -- // To explicitly send no workspace folders, use an empty (non-nil) slice. -- WorkspaceFolders []string -- -- // Whether to edit files with windows line endings. -- WindowsLineEndings bool -- -- // Map of language ID -> regexp to match, used to set the file type of new -- // buffers. Applied as an overlay on top of the following defaults: -- // "go" -> ".*\.go" -- // "go.mod" -> "go\.mod" -- // "go.sum" -> "go\.sum" -- // "gotmpl" -> ".*tmpl" -- FileAssociations map[string]string -- -- // Settings holds user-provided configuration for the LSP server. -- Settings map[string]interface{} +-// State holds debugging information related to the server state. +-type State struct { +- mu sync.Mutex +- clients []*Client +- servers []*Server +-} - -- // CapabilitiesJSON holds JSON client capabilities to overlay over the -- // editor's default client capabilities. -- // -- // Specifically, this JSON string will be unmarshalled into the editor's -- // client capabilities struct, before sending to the server. -- CapabilitiesJSON []byte +-func (st *State) Bugs() []bug.Bug { +- return bug.List() +-} - -- // If non-nil, MessageResponder is used to respond to ShowMessageRequest -- // messages. -- MessageResponder func(params *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error) +-// Caches returns the set of Cache objects currently being served. +-func (st *State) Caches() []*cache.Cache { +- var caches []*cache.Cache +- seen := make(map[string]struct{}) +- for _, client := range st.Clients() { +- cache := client.Session.Cache() +- if _, found := seen[cache.ID()]; found { +- continue +- } +- seen[cache.ID()] = struct{}{} +- caches = append(caches, cache) +- } +- return caches -} - --// NewEditor creates a new Editor. --func NewEditor(sandbox *Sandbox, config EditorConfig) *Editor { -- return &Editor{ -- buffers: make(map[string]buffer), -- sandbox: sandbox, -- config: config, +-// Cache returns the Cache that matches the supplied id. +-func (st *State) Cache(id string) *cache.Cache { +- for _, c := range st.Caches() { +- if c.ID() == id { +- return c +- } - } +- return nil -} - --// Connect configures the editor to communicate with an LSP server on conn. It --// is not concurrency safe, and should be called at most once, before using the --// editor. --// --// It returns the editor, so that it may be called as follows: --// --// editor, err := NewEditor(s).Connect(ctx, conn, hooks) --func (e *Editor) Connect(ctx context.Context, connector servertest.Connector, hooks ClientHooks, skipApplyEdits bool) (*Editor, error) { -- bgCtx, cancelConn := context.WithCancel(xcontext.Detach(ctx)) -- conn := connector.Connect(bgCtx) -- e.cancelConn = cancelConn +-// Analysis returns the global Analysis template value. +-func (st *State) Analysis() (_ analysisTmpl) { return } - -- e.serverConn = conn -- e.Server = protocol.ServerDispatcher(conn) -- e.client = &Client{editor: e, hooks: hooks, skipApplyEdits: skipApplyEdits} -- conn.Go(bgCtx, -- protocol.Handlers( -- protocol.ClientHandler(e.client, -- jsonrpc2.MethodNotFound))) +-type analysisTmpl struct{} - -- if err := e.initialize(ctx); err != nil { -- return nil, err -- } -- e.sandbox.Workdir.AddWatcher(e.onFileChanges) -- return e, nil --} +-func (analysisTmpl) AnalyzerRunTimes() []cache.LabelDuration { return cache.AnalyzerRunTimes() } - --func (e *Editor) Stats() CallCounts { -- e.callsMu.Lock() -- defer e.callsMu.Unlock() -- return e.calls +-// Sessions returns the set of Session objects currently being served. +-func (st *State) Sessions() []*cache.Session { +- var sessions []*cache.Session +- for _, client := range st.Clients() { +- sessions = append(sessions, client.Session) +- } +- return sessions -} - --// Shutdown issues the 'shutdown' LSP notification. --func (e *Editor) Shutdown(ctx context.Context) error { -- if e.Server != nil { -- if err := e.Server.Shutdown(ctx); err != nil { -- return fmt.Errorf("Shutdown: %w", err) +-// Session returns the Session that matches the supplied id. +-func (st *State) Session(id string) *cache.Session { +- for _, s := range st.Sessions() { +- if s.ID() == id { +- return s - } - } - return nil -} - --// Exit issues the 'exit' LSP notification. --func (e *Editor) Exit(ctx context.Context) error { -- if e.Server != nil { -- // Not all LSP clients issue the exit RPC, but we do so here to ensure that -- // we gracefully handle it on multi-session servers. -- if err := e.Server.Exit(ctx); err != nil { -- return fmt.Errorf("Exit: %w", err) +-// Views returns the set of View objects currently being served. +-func (st *State) Views() []*cache.View { +- var views []*cache.View +- for _, s := range st.Sessions() { +- views = append(views, s.Views()...) +- } +- return views +-} +- +-// View returns the View that matches the supplied id. +-func (st *State) View(id string) *cache.View { +- for _, v := range st.Views() { +- if v.ID() == id { +- return v - } - } - return nil -} - --// Close issues the shutdown and exit sequence an editor should. --func (e *Editor) Close(ctx context.Context) error { -- if err := e.Shutdown(ctx); err != nil { -- return err -- } -- if err := e.Exit(ctx); err != nil { -- return err -- } -- defer func() { -- e.cancelConn() -- }() +-// Clients returns the set of Clients currently being served. +-func (st *State) Clients() []*Client { +- st.mu.Lock() +- defer st.mu.Unlock() +- clients := make([]*Client, len(st.clients)) +- copy(clients, st.clients) +- return clients +-} - -- // called close on the editor should result in the connection closing -- select { -- case <-e.serverConn.Done(): -- // connection closed itself -- return nil -- case <-ctx.Done(): -- return fmt.Errorf("connection not closed: %w", ctx.Err()) +-// Client returns the Client matching the supplied id. +-func (st *State) Client(id string) *Client { +- for _, c := range st.Clients() { +- if c.Session.ID() == id { +- return c +- } - } +- return nil -} - --// Client returns the LSP client for this editor. --func (e *Editor) Client() *Client { -- return e.client +-// Servers returns the set of Servers the instance is currently connected to. +-func (st *State) Servers() []*Server { +- st.mu.Lock() +- defer st.mu.Unlock() +- servers := make([]*Server, len(st.servers)) +- copy(servers, st.servers) +- return servers -} - --// makeSettings builds the settings map for use in LSP settings RPCs. --func makeSettings(sandbox *Sandbox, config EditorConfig) map[string]interface{} { -- env := make(map[string]string) -- for k, v := range sandbox.GoEnv() { -- env[k] = v -- } -- for k, v := range config.Env { -- env[k] = v -- } -- for k, v := range env { -- v = strings.ReplaceAll(v, "$SANDBOX_WORKDIR", sandbox.Workdir.RootURI().SpanURI().Filename()) -- env[k] = v -- } -- -- settings := map[string]interface{}{ -- "env": env, +-// A Client is an incoming connection from a remote client. +-type Client struct { +- Session *cache.Session +- DebugAddress string +- Logfile string +- GoplsPath string +- ServerID string +- Service protocol.Server +-} - -- // Use verbose progress reporting so that regtests can assert on -- // asynchronous operations being completed (such as diagnosing a snapshot). -- "verboseWorkDoneProgress": true, +-// A Server is an outgoing connection to a remote LSP server. +-type Server struct { +- ID string +- DebugAddress string +- Logfile string +- GoplsPath string +- ClientID string +-} - -- // Set an unlimited completion budget, so that tests don't flake because -- // completions are too slow. -- "completionBudget": "0s", -- } +-// addClient adds a client to the set being served. +-func (st *State) addClient(session *cache.Session) { +- st.mu.Lock() +- defer st.mu.Unlock() +- st.clients = append(st.clients, &Client{Session: session}) +-} - -- for k, v := range config.Settings { -- if k == "env" { -- panic("must not provide env via the EditorConfig.Settings field: use the EditorConfig.Env field instead") +-// dropClient removes a client from the set being served. +-func (st *State) dropClient(session *cache.Session) { +- st.mu.Lock() +- defer st.mu.Unlock() +- for i, c := range st.clients { +- if c.Session == session { +- copy(st.clients[i:], st.clients[i+1:]) +- st.clients[len(st.clients)-1] = nil +- st.clients = st.clients[:len(st.clients)-1] +- return - } -- settings[k] = v - } -- -- return settings -} - --func (e *Editor) initialize(ctx context.Context) error { -- config := e.Config() -- -- params := &protocol.ParamInitialize{} -- if e.config.ClientName != "" { -- params.ClientInfo = &protocol.Msg_XInitializeParams_clientInfo{} -- params.ClientInfo.Name = e.config.ClientName -- params.ClientInfo.Version = "v1.0.0" -- } -- params.InitializationOptions = makeSettings(e.sandbox, config) -- params.WorkspaceFolders = makeWorkspaceFolders(e.sandbox, config.WorkspaceFolders) -- -- // Set various client capabilities that are sought by gopls. -- params.Capabilities.Workspace.Configuration = true // support workspace/configuration -- params.Capabilities.Window.WorkDoneProgress = true // support window/workDoneProgress -- params.Capabilities.TextDocument.Completion.CompletionItem.TagSupport.ValueSet = []protocol.CompletionItemTag{protocol.ComplDeprecated} -- params.Capabilities.TextDocument.Completion.CompletionItem.SnippetSupport = true -- params.Capabilities.TextDocument.SemanticTokens.Requests.Full.Value = true -- params.Capabilities.TextDocument.SemanticTokens.TokenTypes = []string{ -- "namespace", "type", "class", "enum", "interface", -- "struct", "typeParameter", "parameter", "variable", "property", "enumMember", -- "event", "function", "method", "macro", "keyword", "modifier", "comment", -- "string", "number", "regexp", "operator", -- } -- params.Capabilities.TextDocument.SemanticTokens.TokenModifiers = []string{ -- "declaration", "definition", "readonly", "static", -- "deprecated", "abstract", "async", "modification", "documentation", "defaultLibrary", -- } -- // The LSP tests have historically enabled this flag, -- // but really we should test both ways for older editors. -- params.Capabilities.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport = true -- // Glob pattern watching is enabled. -- params.Capabilities.Workspace.DidChangeWatchedFiles.DynamicRegistration = true -- // "rename" operations are used for package renaming. -- // -- // TODO(rfindley): add support for other resource operations (create, delete, ...) -- params.Capabilities.Workspace.WorkspaceEdit = &protocol.WorkspaceEditClientCapabilities{ -- ResourceOperations: []protocol.ResourceOperationKind{ -- "rename", -- }, -- } -- // Apply capabilities overlay. -- if config.CapabilitiesJSON != nil { -- if err := json.Unmarshal(config.CapabilitiesJSON, ¶ms.Capabilities); err != nil { -- return fmt.Errorf("unmarshalling EditorConfig.CapabilitiesJSON: %v", err) +-// updateServer updates a server to the set being queried. In practice, there should +-// be at most one remote server. +-func (st *State) updateServer(server *Server) { +- st.mu.Lock() +- defer st.mu.Unlock() +- for i, existing := range st.servers { +- if existing.ID == server.ID { +- // Replace, rather than mutate, to avoid a race. +- newServers := make([]*Server, len(st.servers)) +- copy(newServers, st.servers[:i]) +- newServers[i] = server +- copy(newServers[i+1:], st.servers[i+1:]) +- st.servers = newServers +- return - } - } +- st.servers = append(st.servers, server) +-} - -- trace := protocol.TraceValues("messages") -- params.Trace = &trace -- // TODO: support workspace folders. -- if e.Server != nil { -- resp, err := e.Server.Initialize(ctx, params) -- if err != nil { -- return fmt.Errorf("initialize: %w", err) -- } -- semTokOpts, err := marshalUnmarshal[protocol.SemanticTokensOptions](resp.Capabilities.SemanticTokensProvider) -- if err != nil { -- return fmt.Errorf("unmarshalling semantic tokens options: %v", err) -- } -- e.mu.Lock() -- e.serverCapabilities = resp.Capabilities -- e.semTokOpts = semTokOpts -- e.mu.Unlock() -- -- if err := e.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil { -- return fmt.Errorf("initialized: %w", err) +-// dropServer drops a server from the set being queried. +-func (st *State) dropServer(id string) { +- st.mu.Lock() +- defer st.mu.Unlock() +- for i, s := range st.servers { +- if s.ID == id { +- copy(st.servers[i:], st.servers[i+1:]) +- st.servers[len(st.servers)-1] = nil +- st.servers = st.servers[:len(st.servers)-1] +- return - } - } -- // TODO: await initial configuration here, or expect gopls to manage that? -- return nil -} - --// marshalUnmarshal is a helper to json Marshal and then Unmarshal as a --// different type. Used to work around cases where our protocol types are not --// specific. --func marshalUnmarshal[T any](v any) (T, error) { -- var t T -- data, err := json.Marshal(v) -- if err != nil { -- return t, err -- } -- err = json.Unmarshal(data, &t) -- return t, err +-// an http.ResponseWriter that filters writes +-type filterResponse struct { +- w http.ResponseWriter +- edit func([]byte) []byte -} - --// HasCommand reports whether the connected server supports the command with the given ID. --func (e *Editor) HasCommand(id string) bool { -- for _, command := range e.serverCapabilities.ExecuteCommandProvider.Commands { -- if command == id { -- return true -- } -- } -- return false +-func (c filterResponse) Header() http.Header { +- return c.w.Header() -} - --// makeWorkspaceFolders creates a slice of workspace folders to use for --// this editing session, based on the editor configuration. --func makeWorkspaceFolders(sandbox *Sandbox, paths []string) (folders []protocol.WorkspaceFolder) { -- if len(paths) == 0 { -- paths = []string{string(sandbox.Workdir.RelativeTo)} -- } -- -- for _, path := range paths { -- uri := string(sandbox.Workdir.URI(path)) -- folders = append(folders, protocol.WorkspaceFolder{ -- URI: uri, -- Name: filepath.Base(uri), -- }) -- } -- -- return folders +-func (c filterResponse) Write(buf []byte) (int, error) { +- ans := c.edit(buf) +- return c.w.Write(ans) -} - --// onFileChanges is registered to be called by the Workdir on any writes that --// go through the Workdir API. It is called synchronously by the Workdir. --func (e *Editor) onFileChanges(ctx context.Context, evts []protocol.FileEvent) { -- if e.Server == nil { -- return -- } -- -- // e may be locked when onFileChanges is called, but it is important that we -- // synchronously increment this counter so that we can subsequently assert on -- // the number of expected DidChangeWatchedFiles calls. -- e.callsMu.Lock() -- e.calls.DidChangeWatchedFiles++ -- e.callsMu.Unlock() -- -- // Since e may be locked, we must run this mutation asynchronously. -- go func() { -- e.mu.Lock() -- defer e.mu.Unlock() -- for _, evt := range evts { -- // Always send an on-disk change, even for events that seem useless -- // because they're shadowed by an open buffer. -- path := e.sandbox.Workdir.URIToPath(evt.URI) -- if buf, ok := e.buffers[path]; ok { -- // Following VS Code, don't honor deletions or changes to dirty buffers. -- if buf.dirty || evt.Type == protocol.Deleted { -- continue -- } -- -- content, err := e.sandbox.Workdir.ReadFile(path) -- if err != nil { -- continue // A race with some other operation. -- } -- // No need to update if the buffer content hasn't changed. -- if string(content) == buf.text() { -- continue -- } -- // During shutdown, this call will fail. Ignore the error. -- _ = e.setBufferContentLocked(ctx, path, false, content, nil) -- } -- } -- var matchedEvts []protocol.FileEvent -- for _, evt := range evts { -- filename := filepath.ToSlash(evt.URI.SpanURI().Filename()) -- for _, g := range e.watchPatterns { -- if g.Match(filename) { -- matchedEvts = append(matchedEvts, evt) -- break -- } -- } -- } -- -- // TODO(rfindley): don't send notifications while locked. -- e.Server.DidChangeWatchedFiles(ctx, &protocol.DidChangeWatchedFilesParams{ -- Changes: matchedEvts, -- }) -- }() +-func (c filterResponse) WriteHeader(n int) { +- c.w.WriteHeader(n) -} - --// OpenFile creates a buffer for the given workdir-relative file. --// --// If the file is already open, it is a no-op. --func (e *Editor) OpenFile(ctx context.Context, path string) error { -- if e.HasBuffer(path) { -- return nil -- } -- content, err := e.sandbox.Workdir.ReadFile(path) -- if err != nil { -- return err -- } -- if e.Config().WindowsLineEndings { -- content = toWindowsLineEndings(content) +-// replace annoying nuls by spaces +-func cmdline(w http.ResponseWriter, r *http.Request) { +- fake := filterResponse{ +- w: w, +- edit: func(buf []byte) []byte { +- return bytes.ReplaceAll(buf, []byte{0}, []byte{' '}) +- }, - } -- return e.createBuffer(ctx, path, false, content) +- pprof.Cmdline(fake, r) -} - --// toWindowsLineEndings checks whether content has windows line endings. --// --// If so, it returns content unmodified. If not, it returns a new byte slice modified to use CRLF line endings. --func toWindowsLineEndings(content []byte) []byte { -- abnormal := false -- for i, b := range content { -- if b == '\n' && (i == 0 || content[i-1] != '\r') { -- abnormal = true -- break -- } -- } -- if !abnormal { -- return content -- } -- var buf bytes.Buffer -- for i, b := range content { -- if b == '\n' && (i == 0 || content[i-1] != '\r') { -- buf.WriteByte('\r') -- } -- buf.WriteByte(b) -- } -- return buf.Bytes() +-func (i *Instance) getCache(r *http.Request) interface{} { +- return i.State.Cache(path.Base(r.URL.Path)) -} - --// CreateBuffer creates a new unsaved buffer corresponding to the workdir path, --// containing the given textual content. --func (e *Editor) CreateBuffer(ctx context.Context, path, content string) error { -- return e.createBuffer(ctx, path, true, []byte(content)) +-func (i *Instance) getAnalysis(r *http.Request) interface{} { +- return i.State.Analysis() -} - --func (e *Editor) createBuffer(ctx context.Context, path string, dirty bool, content []byte) error { -- e.mu.Lock() -- -- if _, ok := e.buffers[path]; ok { -- e.mu.Unlock() -- return fmt.Errorf("buffer %q already exists", path) -- } -- -- uri := e.sandbox.Workdir.URI(path).SpanURI() -- buf := buffer{ -- version: 1, -- path: path, -- mapper: protocol.NewMapper(uri, content), -- dirty: dirty, -- } -- e.buffers[path] = buf -- -- item := e.textDocumentItem(buf) -- e.mu.Unlock() -- -- return e.sendDidOpen(ctx, item) +-func (i *Instance) getSession(r *http.Request) interface{} { +- return i.State.Session(path.Base(r.URL.Path)) -} - --// textDocumentItem builds a protocol.TextDocumentItem for the given buffer. --// --// Precondition: e.mu must be held. --func (e *Editor) textDocumentItem(buf buffer) protocol.TextDocumentItem { -- return protocol.TextDocumentItem{ -- URI: e.sandbox.Workdir.URI(buf.path), -- LanguageID: languageID(buf.path, e.config.FileAssociations), -- Version: int32(buf.version), -- Text: buf.text(), -- } +-func (i *Instance) getClient(r *http.Request) interface{} { +- return i.State.Client(path.Base(r.URL.Path)) -} - --func (e *Editor) sendDidOpen(ctx context.Context, item protocol.TextDocumentItem) error { -- if e.Server != nil { -- if err := e.Server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{ -- TextDocument: item, -- }); err != nil { -- return fmt.Errorf("DidOpen: %w", err) +-func (i *Instance) getServer(r *http.Request) interface{} { +- i.State.mu.Lock() +- defer i.State.mu.Unlock() +- id := path.Base(r.URL.Path) +- for _, s := range i.State.servers { +- if s.ID == id { +- return s - } -- e.callsMu.Lock() -- e.calls.DidOpen++ -- e.callsMu.Unlock() - } - return nil -} - --var defaultFileAssociations = map[string]*regexp.Regexp{ -- "go": regexp.MustCompile(`^.*\.go$`), // '$' is important: don't match .gotmpl! -- "go.mod": regexp.MustCompile(`^go\.mod$`), -- "go.sum": regexp.MustCompile(`^go(\.work)?\.sum$`), -- "go.work": regexp.MustCompile(`^go\.work$`), -- "gotmpl": regexp.MustCompile(`^.*tmpl$`), +-func (i *Instance) getView(r *http.Request) interface{} { +- return i.State.View(path.Base(r.URL.Path)) -} - --// languageID returns the language identifier for the path p given the user --// configured fileAssociations. --func languageID(p string, fileAssociations map[string]string) string { -- base := path.Base(p) -- for lang, re := range fileAssociations { -- re := regexp.MustCompile(re) -- if re.MatchString(base) { -- return lang -- } +-func (i *Instance) getFile(r *http.Request) interface{} { +- identifier := path.Base(r.URL.Path) +- sid := path.Base(path.Dir(r.URL.Path)) +- s := i.State.Session(sid) +- if s == nil { +- return nil - } -- for lang, re := range defaultFileAssociations { -- if re.MatchString(base) { -- return lang +- for _, o := range s.Overlays() { +- // TODO(adonovan): understand and document this comparison. +- if o.Identity().Hash.String() == identifier { +- return o - } - } -- return "" +- return nil -} - --// CloseBuffer removes the current buffer (regardless of whether it is saved). --func (e *Editor) CloseBuffer(ctx context.Context, path string) error { -- e.mu.Lock() -- _, ok := e.buffers[path] -- if !ok { -- e.mu.Unlock() -- return ErrUnknownBuffer -- } -- delete(e.buffers, path) -- e.mu.Unlock() -- -- return e.sendDidClose(ctx, e.TextDocumentIdentifier(path)) +-func (i *Instance) getInfo(r *http.Request) interface{} { +- buf := &bytes.Buffer{} +- i.PrintServerInfo(r.Context(), buf) +- return template.HTML(buf.String()) -} - --func (e *Editor) sendDidClose(ctx context.Context, doc protocol.TextDocumentIdentifier) error { -- if e.Server != nil { -- if err := e.Server.DidClose(ctx, &protocol.DidCloseTextDocumentParams{ -- TextDocument: doc, -- }); err != nil { -- return fmt.Errorf("DidClose: %w", err) +-func (i *Instance) AddService(s protocol.Server, session *cache.Session) { +- for _, c := range i.State.clients { +- if c.Session == session { +- c.Service = s +- return - } -- e.callsMu.Lock() -- e.calls.DidClose++ -- e.callsMu.Unlock() - } -- return nil +- stdlog.Printf("unable to find a Client to add the protocol.Server to") -} - --func (e *Editor) TextDocumentIdentifier(path string) protocol.TextDocumentIdentifier { -- return protocol.TextDocumentIdentifier{ -- URI: e.sandbox.Workdir.URI(path), -- } +-func getMemory(_ *http.Request) interface{} { +- var m runtime.MemStats +- runtime.ReadMemStats(&m) +- return m -} - --// SaveBuffer writes the content of the buffer specified by the given path to --// the filesystem. --func (e *Editor) SaveBuffer(ctx context.Context, path string) error { -- if err := e.OrganizeImports(ctx, path); err != nil { -- return fmt.Errorf("organizing imports before save: %w", err) -- } -- if err := e.FormatBuffer(ctx, path); err != nil { -- return fmt.Errorf("formatting before save: %w", err) -- } -- return e.SaveBufferWithoutActions(ctx, path) +-func init() { +- event.SetExporter(makeGlobalExporter(os.Stderr)) -} - --func (e *Editor) SaveBufferWithoutActions(ctx context.Context, path string) error { -- e.mu.Lock() -- defer e.mu.Unlock() -- buf, ok := e.buffers[path] -- if !ok { -- return fmt.Errorf(fmt.Sprintf("unknown buffer: %q", path)) +-func GetInstance(ctx context.Context) *Instance { +- if ctx == nil { +- return nil - } -- content := buf.text() -- includeText := false -- syncOptions, ok := e.serverCapabilities.TextDocumentSync.(protocol.TextDocumentSyncOptions) -- if ok { -- includeText = syncOptions.Save.IncludeText +- v := ctx.Value(instanceKey) +- if v == nil { +- return nil - } +- return v.(*Instance) +-} - -- docID := e.TextDocumentIdentifier(buf.path) -- if e.Server != nil { -- if err := e.Server.WillSave(ctx, &protocol.WillSaveTextDocumentParams{ -- TextDocument: docID, -- Reason: protocol.Manual, -- }); err != nil { -- return fmt.Errorf("WillSave: %w", err) -- } -- } -- if err := e.sandbox.Workdir.WriteFile(ctx, path, content); err != nil { -- return fmt.Errorf("writing %q: %w", path, err) +-// WithInstance creates debug instance ready for use using the supplied +-// configuration and stores it in the returned context. +-func WithInstance(ctx context.Context, agent string) context.Context { +- i := &Instance{ +- StartTime: time.Now(), +- OCAgentConfig: agent, - } +- i.LogWriter = os.Stderr +- ocConfig := ocagent.Discover() +- //TODO: we should not need to adjust the discovered configuration +- ocConfig.Address = i.OCAgentConfig +- i.ocagent = ocagent.Connect(ocConfig) +- i.prometheus = prometheus.New() +- i.rpcs = &Rpcs{} +- i.traces = &traces{} +- i.State = &State{} +- i.exporter = makeInstanceExporter(i) +- return context.WithValue(ctx, instanceKey, i) +-} - -- buf.dirty = false -- e.buffers[path] = buf -- -- if e.Server != nil { -- params := &protocol.DidSaveTextDocumentParams{ -- TextDocument: docID, +-// SetLogFile sets the logfile for use with this instance. +-func (i *Instance) SetLogFile(logfile string, isDaemon bool) (func(), error) { +- // TODO: probably a better solution for deferring closure to the caller would +- // be for the debug instance to itself be closed, but this fixes the +- // immediate bug of logs not being captured. +- closeLog := func() {} +- if logfile != "" { +- if logfile == "auto" { +- if isDaemon { +- logfile = filepath.Join(os.TempDir(), fmt.Sprintf("gopls-daemon-%d.log", os.Getpid())) +- } else { +- logfile = filepath.Join(os.TempDir(), fmt.Sprintf("gopls-%d.log", os.Getpid())) +- } - } -- if includeText { -- params.Text = &content +- f, err := os.Create(logfile) +- if err != nil { +- return nil, fmt.Errorf("unable to create log file: %w", err) - } -- if err := e.Server.DidSave(ctx, params); err != nil { -- return fmt.Errorf("DidSave: %w", err) +- closeLog = func() { +- defer f.Close() - } -- e.callsMu.Lock() -- e.calls.DidSave++ -- e.callsMu.Unlock() +- stdlog.SetOutput(io.MultiWriter(os.Stderr, f)) +- i.LogWriter = f - } -- return nil +- i.Logfile = logfile +- return closeLog, nil -} - --// ErrNoMatch is returned if a regexp search fails. --var ( -- ErrNoMatch = errors.New("no match") -- ErrUnknownBuffer = errors.New("unknown buffer") --) -- --// regexpLocation returns the location of the first occurrence of either re --// or its singular subgroup. It returns ErrNoMatch if the regexp doesn't match. --func regexpLocation(mapper *protocol.Mapper, re string) (protocol.Location, error) { -- var start, end int -- rec, err := regexp.Compile(re) -- if err != nil { -- return protocol.Location{}, err +-// Serve starts and runs a debug server in the background on the given addr. +-// It also logs the port the server starts on, to allow for :0 auto assigned +-// ports. +-func (i *Instance) Serve(ctx context.Context, addr string) (string, error) { +- stdlog.SetFlags(stdlog.Lshortfile) +- if addr == "" { +- return "", nil - } -- indexes := rec.FindSubmatchIndex(mapper.Content) -- if indexes == nil { -- return protocol.Location{}, ErrNoMatch -- } -- switch len(indexes) { -- case 2: -- // no subgroups: return the range of the regexp expression -- start, end = indexes[0], indexes[1] -- case 4: -- // one subgroup: return its range -- start, end = indexes[2], indexes[3] -- default: -- return protocol.Location{}, fmt.Errorf("invalid search regexp %q: expect either 0 or 1 subgroups, got %d", re, len(indexes)/2-1) -- } -- return mapper.OffsetLocation(start, end) --} +- i.serveMu.Lock() +- defer i.serveMu.Unlock() - --// RegexpSearch returns the Location of the first match for re in the buffer --// bufName. For convenience, RegexpSearch supports the following two modes: --// 1. If re has no subgroups, return the position of the match for re itself. --// 2. If re has one subgroup, return the position of the first subgroup. --// --// It returns an error re is invalid, has more than one subgroup, or doesn't --// match the buffer. --func (e *Editor) RegexpSearch(bufName, re string) (protocol.Location, error) { -- e.mu.Lock() -- buf, ok := e.buffers[bufName] -- e.mu.Unlock() -- if !ok { -- return protocol.Location{}, ErrUnknownBuffer +- if i.listenedDebugAddress != "" { +- // Already serving. Return the bound address. +- return i.listenedDebugAddress, nil - } -- return regexpLocation(buf.mapper, re) --} - --// RegexpReplace edits the buffer corresponding to path by replacing the first --// instance of re, or its first subgroup, with the replace text. See --// RegexpSearch for more explanation of these two modes. --// It returns an error if re is invalid, has more than one subgroup, or doesn't --// match the buffer. --func (e *Editor) RegexpReplace(ctx context.Context, path, re, replace string) error { -- e.mu.Lock() -- defer e.mu.Unlock() -- buf, ok := e.buffers[path] -- if !ok { -- return ErrUnknownBuffer -- } -- loc, err := regexpLocation(buf.mapper, re) +- i.debugAddress = addr +- listener, err := net.Listen("tcp", i.debugAddress) - if err != nil { -- return err +- return "", err - } -- edits := []protocol.TextEdit{{ -- Range: loc.Range, -- NewText: replace, -- }} -- patched, err := applyEdits(buf.mapper, edits, e.config.WindowsLineEndings) -- if err != nil { -- return fmt.Errorf("editing %q: %v", path, err) +- i.listenedDebugAddress = listener.Addr().String() +- +- port := listener.Addr().(*net.TCPAddr).Port +- if strings.HasSuffix(i.debugAddress, ":0") { +- stdlog.Printf("debug server listening at http://localhost:%d", port) - } -- return e.setBufferContentLocked(ctx, path, true, patched, edits) --} +- event.Log(ctx, "Debug serving", tag.Port.Of(port)) +- go func() { +- mux := http.NewServeMux() +- mux.HandleFunc("/", render(MainTmpl, func(*http.Request) interface{} { return i })) +- mux.HandleFunc("/debug/", render(DebugTmpl, nil)) +- mux.HandleFunc("/debug/pprof/", pprof.Index) +- mux.HandleFunc("/debug/pprof/cmdline", cmdline) +- mux.HandleFunc("/debug/pprof/profile", pprof.Profile) +- mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) +- mux.HandleFunc("/debug/pprof/trace", pprof.Trace) +- if i.prometheus != nil { +- mux.HandleFunc("/metrics/", i.prometheus.Serve) +- } +- if i.rpcs != nil { +- mux.HandleFunc("/rpc/", render(RPCTmpl, i.rpcs.getData)) +- } +- if i.traces != nil { +- mux.HandleFunc("/trace/", render(TraceTmpl, i.traces.getData)) +- } +- mux.HandleFunc("/analysis/", render(AnalysisTmpl, i.getAnalysis)) +- mux.HandleFunc("/cache/", render(CacheTmpl, i.getCache)) +- mux.HandleFunc("/session/", render(SessionTmpl, i.getSession)) +- mux.HandleFunc("/client/", render(ClientTmpl, i.getClient)) +- mux.HandleFunc("/server/", render(ServerTmpl, i.getServer)) +- mux.HandleFunc("/file/", render(FileTmpl, i.getFile)) +- mux.HandleFunc("/info", render(InfoTmpl, i.getInfo)) +- mux.HandleFunc("/memory", render(MemoryTmpl, getMemory)) - --// EditBuffer applies the given test edits to the buffer identified by path. --func (e *Editor) EditBuffer(ctx context.Context, path string, edits []protocol.TextEdit) error { -- e.mu.Lock() -- defer e.mu.Unlock() -- return e.editBufferLocked(ctx, path, edits) --} +- // Internal debugging helpers. +- mux.HandleFunc("/gc", func(w http.ResponseWriter, r *http.Request) { +- runtime.GC() +- runtime.GC() +- runtime.GC() +- http.Redirect(w, r, "/memory", http.StatusTemporaryRedirect) +- }) +- mux.HandleFunc("/_makeabug", func(w http.ResponseWriter, r *http.Request) { +- bug.Report("bug here") +- http.Error(w, "made a bug", http.StatusOK) +- }) - --func (e *Editor) SetBufferContent(ctx context.Context, path, content string) error { -- e.mu.Lock() -- defer e.mu.Unlock() -- return e.setBufferContentLocked(ctx, path, true, []byte(content), nil) +- if err := http.Serve(listener, mux); err != nil { +- event.Error(ctx, "Debug server failed", err) +- return +- } +- event.Log(ctx, "Debug server finished") +- }() +- return i.listenedDebugAddress, nil -} - --// HasBuffer reports whether the file name is open in the editor. --func (e *Editor) HasBuffer(name string) bool { -- e.mu.Lock() -- defer e.mu.Unlock() -- _, ok := e.buffers[name] -- return ok +-func (i *Instance) DebugAddress() string { +- i.serveMu.Lock() +- defer i.serveMu.Unlock() +- return i.debugAddress -} - --// BufferText returns the content of the buffer with the given name, or "" if --// the file at that path is not open. The second return value reports whether --// the file is open. --func (e *Editor) BufferText(name string) (string, bool) { -- e.mu.Lock() -- defer e.mu.Unlock() -- buf, ok := e.buffers[name] -- if !ok { -- return "", false -- } -- return buf.text(), true +-func (i *Instance) ListenedDebugAddress() string { +- i.serveMu.Lock() +- defer i.serveMu.Unlock() +- return i.listenedDebugAddress -} - --// Mapper returns the protocol.Mapper for the given buffer name, if it is open. --func (e *Editor) Mapper(name string) (*protocol.Mapper, error) { -- e.mu.Lock() -- defer e.mu.Unlock() -- buf, ok := e.buffers[name] -- if !ok { -- return nil, fmt.Errorf("no mapper for %q", name) -- } -- return buf.mapper, nil --} +-func makeGlobalExporter(stderr io.Writer) event.Exporter { +- p := export.Printer{} +- var pMu sync.Mutex +- return func(ctx context.Context, ev core.Event, lm label.Map) context.Context { +- i := GetInstance(ctx) - --// BufferVersion returns the current version of the buffer corresponding to --// name (or 0 if it is not being edited). --func (e *Editor) BufferVersion(name string) int { -- e.mu.Lock() -- defer e.mu.Unlock() -- return e.buffers[name].version +- if event.IsLog(ev) { +- // Don't log context cancellation errors. +- if err := keys.Err.Get(ev); errors.Is(err, context.Canceled) { +- return ctx +- } +- // Make sure any log messages without an instance go to stderr. +- if i == nil { +- pMu.Lock() +- p.WriteEvent(stderr, ev, lm) +- pMu.Unlock() +- } +- level := log.LabeledLevel(lm) +- // Exclude trace logs from LSP logs. +- if level < log.Trace { +- ctx = protocol.LogEvent(ctx, ev, lm, messageType(level)) +- } +- } +- if i == nil { +- return ctx +- } +- return i.exporter(ctx, ev, lm) +- } -} - --func (e *Editor) editBufferLocked(ctx context.Context, path string, edits []protocol.TextEdit) error { -- buf, ok := e.buffers[path] -- if !ok { -- return fmt.Errorf("unknown buffer %q", path) -- } -- content, err := applyEdits(buf.mapper, edits, e.config.WindowsLineEndings) -- if err != nil { -- return fmt.Errorf("editing %q: %v; edits:\n%v", path, err, edits) +-func messageType(l log.Level) protocol.MessageType { +- switch l { +- case log.Error: +- return protocol.Error +- case log.Warning: +- return protocol.Warning +- case log.Debug: +- return protocol.Log - } -- return e.setBufferContentLocked(ctx, path, true, content, edits) +- return protocol.Info -} - --func (e *Editor) setBufferContentLocked(ctx context.Context, path string, dirty bool, content []byte, fromEdits []protocol.TextEdit) error { -- buf, ok := e.buffers[path] -- if !ok { -- return fmt.Errorf("unknown buffer %q", path) -- } -- buf.mapper = protocol.NewMapper(buf.mapper.URI, content) -- buf.version++ -- buf.dirty = dirty -- e.buffers[path] = buf -- // A simple heuristic: if there is only one edit, send it incrementally. -- // Otherwise, send the entire content. -- var evts []protocol.TextDocumentContentChangeEvent -- if len(fromEdits) == 1 { -- evts = append(evts, EditToChangeEvent(fromEdits[0])) -- } else { -- evts = append(evts, protocol.TextDocumentContentChangeEvent{ -- Text: buf.text(), -- }) -- } -- params := &protocol.DidChangeTextDocumentParams{ -- TextDocument: protocol.VersionedTextDocumentIdentifier{ -- Version: int32(buf.version), -- TextDocumentIdentifier: e.TextDocumentIdentifier(buf.path), -- }, -- ContentChanges: evts, -- } -- if e.Server != nil { -- if err := e.Server.DidChange(ctx, params); err != nil { -- return fmt.Errorf("DidChange: %w", err) +-func makeInstanceExporter(i *Instance) event.Exporter { +- exporter := func(ctx context.Context, ev core.Event, lm label.Map) context.Context { +- if i.ocagent != nil { +- ctx = i.ocagent.ProcessEvent(ctx, ev, lm) - } -- e.callsMu.Lock() -- e.calls.DidChange++ -- e.callsMu.Unlock() +- if i.prometheus != nil { +- ctx = i.prometheus.ProcessEvent(ctx, ev, lm) +- } +- if i.rpcs != nil { +- ctx = i.rpcs.ProcessEvent(ctx, ev, lm) +- } +- if i.traces != nil { +- ctx = i.traces.ProcessEvent(ctx, ev, lm) +- } +- if event.IsLog(ev) { +- if s := cache.KeyCreateSession.Get(ev); s != nil { +- i.State.addClient(s) +- } +- if sid := tag.NewServer.Get(ev); sid != "" { +- i.State.updateServer(&Server{ +- ID: sid, +- Logfile: tag.Logfile.Get(ev), +- DebugAddress: tag.DebugAddress.Get(ev), +- GoplsPath: tag.GoplsPath.Get(ev), +- ClientID: tag.ClientID.Get(ev), +- }) +- } +- if s := cache.KeyShutdownSession.Get(ev); s != nil { +- i.State.dropClient(s) +- } +- if sid := tag.EndServer.Get(ev); sid != "" { +- i.State.dropServer(sid) +- } +- if s := cache.KeyUpdateSession.Get(ev); s != nil { +- if c := i.State.Client(s.ID()); c != nil { +- c.DebugAddress = tag.DebugAddress.Get(ev) +- c.Logfile = tag.Logfile.Get(ev) +- c.ServerID = tag.ServerID.Get(ev) +- c.GoplsPath = tag.GoplsPath.Get(ev) +- } +- } +- } +- return ctx - } -- return nil +- // StdTrace must be above export.Spans below (by convention, export +- // middleware applies its wrapped exporter last). +- exporter = StdTrace(exporter) +- metrics := metric.Config{} +- registerMetrics(&metrics) +- exporter = metrics.Exporter(exporter) +- exporter = export.Spans(exporter) +- exporter = export.Labels(exporter) +- return exporter -} - --// GoToDefinition jumps to the definition of the symbol at the given position --// in an open buffer. It returns the location of the resulting jump. --func (e *Editor) Definition(ctx context.Context, loc protocol.Location) (protocol.Location, error) { -- if err := e.checkBufferLocation(loc); err != nil { -- return protocol.Location{}, err -- } -- params := &protocol.DefinitionParams{} -- params.TextDocument.URI = loc.URI -- params.Position = loc.Range.Start +-type dataFunc func(*http.Request) interface{} - -- resp, err := e.Server.Definition(ctx, params) -- if err != nil { -- return protocol.Location{}, fmt.Errorf("definition: %w", err) +-func render(tmpl *template.Template, fun dataFunc) func(http.ResponseWriter, *http.Request) { +- return func(w http.ResponseWriter, r *http.Request) { +- var data interface{} +- if fun != nil { +- data = fun(r) +- } +- if err := tmpl.Execute(w, data); err != nil { +- event.Error(context.Background(), "", err) +- http.Error(w, err.Error(), http.StatusInternalServerError) +- } - } -- return e.extractFirstLocation(ctx, resp) -} - --// TypeDefinition jumps to the type definition of the symbol at the given --// location in an open buffer. --func (e *Editor) TypeDefinition(ctx context.Context, loc protocol.Location) (protocol.Location, error) { -- if err := e.checkBufferLocation(loc); err != nil { -- return protocol.Location{}, err -- } -- params := &protocol.TypeDefinitionParams{} -- params.TextDocument.URI = loc.URI -- params.Position = loc.Range.Start -- -- resp, err := e.Server.TypeDefinition(ctx, params) -- if err != nil { -- return protocol.Location{}, fmt.Errorf("type definition: %w", err) +-func commas(s string) string { +- for i := len(s); i > 3; { +- i -= 3 +- s = s[:i] + "," + s[i:] - } -- return e.extractFirstLocation(ctx, resp) +- return s -} - --// extractFirstLocation returns the first location. --// It opens the file if needed. --func (e *Editor) extractFirstLocation(ctx context.Context, locs []protocol.Location) (protocol.Location, error) { -- if len(locs) == 0 { -- return protocol.Location{}, nil -- } -- -- newPath := e.sandbox.Workdir.URIToPath(locs[0].URI) -- if !e.HasBuffer(newPath) { -- if err := e.OpenFile(ctx, newPath); err != nil { -- return protocol.Location{}, fmt.Errorf("OpenFile: %w", err) -- } -- } -- return locs[0], nil +-func fuint64(v uint64) string { +- return commas(strconv.FormatUint(v, 10)) -} - --// Symbol performs a workspace symbol search using query --func (e *Editor) Symbol(ctx context.Context, query string) ([]protocol.SymbolInformation, error) { -- params := &protocol.WorkspaceSymbolParams{Query: query} -- return e.Server.Symbol(ctx, params) +-func fuint32(v uint32) string { +- return commas(strconv.FormatUint(uint64(v), 10)) -} - --// OrganizeImports requests and performs the source.organizeImports codeAction. --func (e *Editor) OrganizeImports(ctx context.Context, path string) error { -- loc := protocol.Location{URI: e.sandbox.Workdir.URI(path)} // zero Range => whole file -- _, err := e.applyCodeActions(ctx, loc, nil, protocol.SourceOrganizeImports) -- return err +-func fcontent(v []byte) string { +- return string(v) -} - --// RefactorRewrite requests and performs the source.refactorRewrite codeAction. --func (e *Editor) RefactorRewrite(ctx context.Context, loc protocol.Location) error { -- applied, err := e.applyCodeActions(ctx, loc, nil, protocol.RefactorRewrite) -- if err != nil { -- return err -- } -- if applied == 0 { -- return fmt.Errorf("no refactorings were applied") -- } -- return nil +-var BaseTemplate = template.Must(template.New("").Parse(` +- +- +-{{template "title" .}} +- +-{{block "head" .}}{{end}} +- +- +-Main +-Info +-Memory +-Profiling +-Metrics +-RPC +-Trace +-Analysis +-
    +-

    {{template "title" .}}

    +-{{block "body" .}} +-Unknown page +-{{end}} +- +- - --func (e *Editor) applyCodeActions(ctx context.Context, loc protocol.Location, diagnostics []protocol.Diagnostic, only ...protocol.CodeActionKind) (int, error) { -- actions, err := e.getCodeActions(ctx, loc, diagnostics, only...) -- if err != nil { -- return 0, err -- } -- applied := 0 -- for _, action := range actions { -- if action.Title == "" { -- return 0, fmt.Errorf("empty title for code action") -- } -- var match bool -- for _, o := range only { -- if action.Kind == o { -- match = true -- break -- } +-{{define "cachelink"}}Cache {{.}}{{end}} +-{{define "clientlink"}}Client {{.}}{{end}} +-{{define "serverlink"}}Server {{.}}{{end}} +-{{define "sessionlink"}}Session {{.}}{{end}} +-`)).Funcs(template.FuncMap{ +- "fuint64": fuint64, +- "fuint32": fuint32, +- "fcontent": fcontent, +- "localAddress": func(s string) string { +- // Try to translate loopback addresses to localhost, both for cosmetics and +- // because unspecified ipv6 addresses can break links on Windows. +- // +- // TODO(rfindley): In the future, it would be better not to assume the +- // server is running on localhost, and instead construct this address using +- // the remote host. +- host, port, err := net.SplitHostPort(s) +- if err != nil { +- return s - } -- if !match { -- continue +- ip := net.ParseIP(host) +- if ip == nil { +- return s - } -- applied++ -- if err := e.ApplyCodeAction(ctx, action); err != nil { -- return 0, err +- if ip.IsLoopback() || ip.IsUnspecified() { +- return "localhost:" + port - } -- } -- return applied, nil --} -- --func (e *Editor) getCodeActions(ctx context.Context, loc protocol.Location, diagnostics []protocol.Diagnostic, only ...protocol.CodeActionKind) ([]protocol.CodeAction, error) { -- if e.Server == nil { -- return nil, nil -- } -- params := &protocol.CodeActionParams{} -- params.TextDocument.URI = loc.URI -- params.Context.Only = only -- params.Range = loc.Range // may be zero => whole file -- if diagnostics != nil { -- params.Context.Diagnostics = diagnostics -- } -- return e.Server.CodeAction(ctx, params) --} +- return s +- }, +- // TODO(rfindley): re-enable option inspection. +- // "options": func(s *cache.Session) []sessionOption { +- // return showOptions(s.Options()) +- // }, +-}) - --func (e *Editor) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) { -- if e.Server == nil { -- return nil, nil -- } -- var match bool -- if e.serverCapabilities.ExecuteCommandProvider != nil { -- // Ensure that this command was actually listed as a supported command. -- for _, command := range e.serverCapabilities.ExecuteCommandProvider.Commands { -- if command == params.Command { -- match = true -- break -- } -- } -- } -- if !match { -- return nil, fmt.Errorf("unsupported command %q", params.Command) -- } -- result, err := e.Server.ExecuteCommand(ctx, params) -- if err != nil { -- return nil, err -- } -- // Some commands use the go command, which writes directly to disk. -- // For convenience, check for those changes. -- if err := e.sandbox.Workdir.CheckForFileChanges(ctx); err != nil { -- return nil, fmt.Errorf("checking for file changes: %v", err) -- } -- return result, nil --} +-var MainTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` +-{{define "title"}}Gopls server information{{end}} +-{{define "body"}} +-

    Caches

    +-
      {{range .State.Caches}}
    • {{template "cachelink" .ID}}
    • {{end}}
    +-

    Sessions

    +-
      {{range .State.Sessions}}
    • {{template "sessionlink" .ID}} from {{template "cachelink" .Cache.ID}}
    • {{end}}
    +-

    Clients

    +-
      {{range .State.Clients}}
    • {{template "clientlink" .Session.ID}}
    • {{end}}
    +-

    Servers

    +-
      {{range .State.Servers}}
    • {{template "serverlink" .ID}}
    • {{end}}
    +-

    Bug reports

    +-
    {{range .State.Bugs}}
    {{.Key}}
    {{.Description}}
    {{end}}
    +-{{end}} +-`)) - --// FormatBuffer gofmts a Go file. --func (e *Editor) FormatBuffer(ctx context.Context, path string) error { -- if e.Server == nil { -- return nil -- } -- e.mu.Lock() -- version := e.buffers[path].version -- e.mu.Unlock() -- params := &protocol.DocumentFormattingParams{} -- params.TextDocument.URI = e.sandbox.Workdir.URI(path) -- edits, err := e.Server.Formatting(ctx, params) -- if err != nil { -- return fmt.Errorf("textDocument/formatting: %w", err) -- } -- e.mu.Lock() -- defer e.mu.Unlock() -- if versionAfter := e.buffers[path].version; versionAfter != version { -- return fmt.Errorf("before receipt of formatting edits, buffer version changed from %d to %d", version, versionAfter) -- } -- if len(edits) == 0 { -- return nil -- } -- return e.editBufferLocked(ctx, path, edits) --} +-var InfoTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` +-{{define "title"}}Gopls version information{{end}} +-{{define "body"}} +-{{.}} +-{{end}} +-`)) - --func (e *Editor) checkBufferLocation(loc protocol.Location) error { -- e.mu.Lock() -- defer e.mu.Unlock() -- path := e.sandbox.Workdir.URIToPath(loc.URI) -- buf, ok := e.buffers[path] -- if !ok { -- return fmt.Errorf("buffer %q is not open", path) -- } +-var MemoryTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` +-{{define "title"}}Gopls memory usage{{end}} +-{{define "head"}}{{end}} +-{{define "body"}} +-
    +-

    Stats

    +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +- +-
    Allocated bytes{{fuint64 .HeapAlloc}}
    Total allocated bytes{{fuint64 .TotalAlloc}}
    System bytes{{fuint64 .Sys}}
    Heap system bytes{{fuint64 .HeapSys}}
    Malloc calls{{fuint64 .Mallocs}}
    Frees{{fuint64 .Frees}}
    Idle heap bytes{{fuint64 .HeapIdle}}
    In use bytes{{fuint64 .HeapInuse}}
    Released to system bytes{{fuint64 .HeapReleased}}
    Heap object count{{fuint64 .HeapObjects}}
    Stack in use bytes{{fuint64 .StackInuse}}
    Stack from system bytes{{fuint64 .StackSys}}
    Bucket hash bytes{{fuint64 .BuckHashSys}}
    GC metadata bytes{{fuint64 .GCSys}}
    Off heap bytes{{fuint64 .OtherSys}}
    +-

    By size

    +- +- +-{{range .BySize}}{{end}} +-
    SizeMallocsFrees
    {{fuint32 .Size}}{{fuint64 .Mallocs}}{{fuint64 .Frees}}
    +-{{end}} +-`)) - -- _, _, err := buf.mapper.RangeOffsets(loc.Range) -- return err --} +-var DebugTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` +-{{define "title"}}GoPls Debug pages{{end}} +-{{define "body"}} +-Profiling +-{{end}} +-`)) - --// RunGenerate runs `go generate` non-recursively in the workdir-relative dir --// path. It does not report any resulting file changes as a watched file --// change, so must be followed by a call to Workdir.CheckForFileChanges once --// the generate command has completed. --// TODO(rFindley): this shouldn't be necessary anymore. Delete it. --func (e *Editor) RunGenerate(ctx context.Context, dir string) error { -- if e.Server == nil { -- return nil -- } -- absDir := e.sandbox.Workdir.AbsPath(dir) -- cmd, err := command.NewGenerateCommand("", command.GenerateArgs{ -- Dir: protocol.URIFromSpanURI(span.URIFromPath(absDir)), -- Recursive: false, -- }) -- if err != nil { -- return err -- } -- params := &protocol.ExecuteCommandParams{ -- Command: cmd.Command, -- Arguments: cmd.Arguments, -- } -- if _, err := e.ExecuteCommand(ctx, params); err != nil { -- return fmt.Errorf("running generate: %v", err) -- } -- // Unfortunately we can't simply poll the workdir for file changes here, -- // because server-side command may not have completed. In regtests, we can -- // Await this state change, but here we must delegate that responsibility to -- // the caller. -- return nil --} +-var CacheTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` +-{{define "title"}}Cache {{.ID}}{{end}} +-{{define "body"}} +-

    memoize.Store entries

    +-
      {{range $k,$v := .MemStats}}
    • {{$k}} - {{$v}}
    • {{end}}
    +-

    File stats

    +-

    +-{{- $stats := .FileStats -}} +-Total: {{$stats.Total}}
    +-Largest: {{$stats.Largest}}
    +-Errors: {{$stats.Errs}}
    +-

    +-{{end}} +-`)) - --// CodeLens executes a codelens request on the server. --func (e *Editor) CodeLens(ctx context.Context, path string) ([]protocol.CodeLens, error) { -- if e.Server == nil { -- return nil, nil -- } -- e.mu.Lock() -- _, ok := e.buffers[path] -- e.mu.Unlock() -- if !ok { -- return nil, fmt.Errorf("buffer %q is not open", path) -- } -- params := &protocol.CodeLensParams{ -- TextDocument: e.TextDocumentIdentifier(path), -- } -- lens, err := e.Server.CodeLens(ctx, params) -- if err != nil { -- return nil, err -- } -- return lens, nil --} +-var AnalysisTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` +-{{define "title"}}Analysis{{end}} +-{{define "body"}} +-

    Analyzer.Run times

    +-
      {{range .AnalyzerRunTimes}}
    • {{.Duration}} {{.Label}}
    • {{end}}
    +-{{end}} +-`)) - --// Completion executes a completion request on the server. --func (e *Editor) Completion(ctx context.Context, loc protocol.Location) (*protocol.CompletionList, error) { -- if e.Server == nil { -- return nil, nil -- } -- path := e.sandbox.Workdir.URIToPath(loc.URI) -- e.mu.Lock() -- _, ok := e.buffers[path] -- e.mu.Unlock() -- if !ok { -- return nil, fmt.Errorf("buffer %q is not open", path) -- } -- params := &protocol.CompletionParams{ -- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), -- } -- completions, err := e.Server.Completion(ctx, params) -- if err != nil { -- return nil, err -- } -- return completions, nil --} +-var ClientTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` +-{{define "title"}}Client {{.Session.ID}}{{end}} +-{{define "body"}} +-Using session: {{template "sessionlink" .Session.ID}}
    +-{{if .DebugAddress}}Debug this client at: {{localAddress .DebugAddress}}
    {{end}} +-Logfile: {{.Logfile}}
    +-Gopls Path: {{.GoplsPath}}
    +-{{end}} +-`)) - --// AcceptCompletion accepts a completion for the given item at the given --// position. --func (e *Editor) AcceptCompletion(ctx context.Context, loc protocol.Location, item protocol.CompletionItem) error { -- if e.Server == nil { -- return nil -- } -- e.mu.Lock() -- defer e.mu.Unlock() -- path := e.sandbox.Workdir.URIToPath(loc.URI) -- _, ok := e.buffers[path] -- if !ok { -- return fmt.Errorf("buffer %q is not open", path) -- } -- return e.editBufferLocked(ctx, path, append([]protocol.TextEdit{ -- *item.TextEdit, -- }, item.AdditionalTextEdits...)) --} +-var ServerTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` +-{{define "title"}}Server {{.ID}}{{end}} +-{{define "body"}} +-{{if .DebugAddress}}Debug this server at: {{localAddress .DebugAddress}}
    {{end}} +-Logfile: {{.Logfile}}
    +-Gopls Path: {{.GoplsPath}}
    +-{{end}} +-`)) - --// Symbols executes a workspace/symbols request on the server. --func (e *Editor) Symbols(ctx context.Context, sym string) ([]protocol.SymbolInformation, error) { -- if e.Server == nil { -- return nil, nil -- } -- params := &protocol.WorkspaceSymbolParams{Query: sym} -- ans, err := e.Server.Symbol(ctx, params) -- return ans, err --} +-var SessionTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` +-{{define "title"}}Session {{.ID}}{{end}} +-{{define "body"}} +-From: {{template "cachelink" .Cache.ID}}
    +-

    Views

    +-
      {{range .Views}} +-{{- $envOverlay := .EnvOverlay -}} +-
    • ID: {{.ID}}
      +-Type: {{.Type}}
      +-Root: {{.Root}}
      +-{{- if $envOverlay}} +-Env overlay: {{$envOverlay}})
      +-{{end -}} +-Folder: {{.Folder.Name}}:{{.Folder.Dir}}
    • +-{{end}}
    +-

    Overlays

    +-{{$session := .}} +- +-{{end}} +-`)) - --// CodeLens executes a codelens request on the server. --func (e *Editor) InlayHint(ctx context.Context, path string) ([]protocol.InlayHint, error) { -- if e.Server == nil { -- return nil, nil -- } -- e.mu.Lock() -- _, ok := e.buffers[path] -- e.mu.Unlock() -- if !ok { -- return nil, fmt.Errorf("buffer %q is not open", path) -- } -- params := &protocol.InlayHintParams{ -- TextDocument: e.TextDocumentIdentifier(path), -- } -- hints, err := e.Server.InlayHint(ctx, params) -- if err != nil { -- return nil, err -- } -- return hints, nil --} +-var FileTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` +-{{define "title"}}Overlay {{.Identity.Hash}}{{end}} +-{{define "body"}} +-{{with .}} +- URI: {{.URI}}
    +- Identifier: {{.Identity.Hash}}
    +- Version: {{.Version}}
    +- Kind: {{.Kind}}
    +-{{end}} +-

    Contents

    +-
    {{fcontent .Content}}
    +-{{end}} +-`)) +diff -urN a/gopls/internal/debug/template_test.go b/gopls/internal/debug/template_test.go +--- a/gopls/internal/debug/template_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/debug/template_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,156 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// References returns references to the object at loc, as returned by --// the connected LSP server. If no server is connected, it returns (nil, nil). --func (e *Editor) References(ctx context.Context, loc protocol.Location) ([]protocol.Location, error) { -- if e.Server == nil { -- return nil, nil -- } -- path := e.sandbox.Workdir.URIToPath(loc.URI) -- e.mu.Lock() -- _, ok := e.buffers[path] -- e.mu.Unlock() -- if !ok { -- return nil, fmt.Errorf("buffer %q is not open", path) -- } -- params := &protocol.ReferenceParams{ -- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), -- Context: protocol.ReferenceContext{ -- IncludeDeclaration: true, -- }, -- } -- locations, err := e.Server.References(ctx, params) -- if err != nil { -- return nil, err -- } -- return locations, nil +-package debug_test +- +-// Provide 'static type checking' of the templates. This guards against changes in various +-// gopls datastructures causing template execution to fail. The checking is done by +-// the github.com/jba/templatecheck package. Before that is run, the test checks that +-// its list of templates and their arguments corresponds to the arguments in +-// calls to render(). The test assumes that all uses of templates are done through render(). +- +-import ( +- "go/ast" +- "html/template" +- "os" +- "runtime" +- "sort" +- "strings" +- "testing" +- +- "github.com/jba/templatecheck" +- "golang.org/x/tools/go/packages" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/debug" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/internal/testenv" +-) +- +-var templates = map[string]struct { +- tmpl *template.Template +- data interface{} // a value of the needed type +-}{ +- "MainTmpl": {debug.MainTmpl, &debug.Instance{}}, +- "DebugTmpl": {debug.DebugTmpl, nil}, +- "RPCTmpl": {debug.RPCTmpl, &debug.Rpcs{}}, +- "TraceTmpl": {debug.TraceTmpl, debug.TraceResults{}}, +- "CacheTmpl": {debug.CacheTmpl, &cache.Cache{}}, +- "SessionTmpl": {debug.SessionTmpl, &cache.Session{}}, +- "ClientTmpl": {debug.ClientTmpl, &debug.Client{}}, +- "ServerTmpl": {debug.ServerTmpl, &debug.Server{}}, +- "FileTmpl": {debug.FileTmpl, *new(interface { +- file.Handle +- Kind() file.Kind // (overlay files only) +- })}, +- "InfoTmpl": {debug.InfoTmpl, "something"}, +- "MemoryTmpl": {debug.MemoryTmpl, runtime.MemStats{}}, +- "AnalysisTmpl": {debug.AnalysisTmpl, new(debug.State).Analysis()}, -} - --// Rename performs a rename of the object at loc to newName, using the --// connected LSP server. If no server is connected, it returns nil. --func (e *Editor) Rename(ctx context.Context, loc protocol.Location, newName string) error { -- if e.Server == nil { -- return nil -- } -- path := e.sandbox.Workdir.URIToPath(loc.URI) +-func TestTemplates(t *testing.T) { +- testenv.NeedsGoPackages(t) +- testenv.NeedsLocalXTools(t) - -- // Verify that PrepareRename succeeds. -- prepareParams := &protocol.PrepareRenameParams{} -- prepareParams.TextDocument = e.TextDocumentIdentifier(path) -- prepareParams.Position = loc.Range.Start -- if _, err := e.Server.PrepareRename(ctx, prepareParams); err != nil { -- return fmt.Errorf("preparing rename: %v", err) +- cfg := &packages.Config{ +- Mode: packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo, - } +- cfg.Env = os.Environ() +- cfg.Env = append(cfg.Env, +- "GOPACKAGESDRIVER=off", +- "GOWORK=off", // necessary for -mod=mod below +- "GOFLAGS=-mod=mod", +- ) - -- params := &protocol.RenameParams{ -- TextDocument: e.TextDocumentIdentifier(path), -- Position: loc.Range.Start, -- NewName: newName, -- } -- wsEdits, err := e.Server.Rename(ctx, params) +- pkgs, err := packages.Load(cfg, "golang.org/x/tools/gopls/internal/debug") - if err != nil { -- return err +- t.Fatal(err) - } -- for _, change := range wsEdits.DocumentChanges { -- if err := e.applyDocumentChange(ctx, change); err != nil { -- return err -- } +- if len(pkgs) != 1 { +- t.Fatalf("expected a single package, but got %d", len(pkgs)) - } -- return nil --} -- --// Implementations returns implementations for the object at loc, as --// returned by the connected LSP server. If no server is connected, it returns --// (nil, nil). --func (e *Editor) Implementations(ctx context.Context, loc protocol.Location) ([]protocol.Location, error) { -- if e.Server == nil { -- return nil, nil +- p := pkgs[0] +- if len(p.Errors) != 0 { +- t.Fatalf("compiler error, e.g. %v", p.Errors[0]) - } -- path := e.sandbox.Workdir.URIToPath(loc.URI) -- e.mu.Lock() -- _, ok := e.buffers[path] -- e.mu.Unlock() -- if !ok { -- return nil, fmt.Errorf("buffer %q is not open", path) +- // find the calls to render in serve.go +- tree := treeOf(p, "serve.go") +- if tree == nil { +- t.Fatalf("found no syntax tree for %s", "serve.go") - } -- params := &protocol.ImplementationParams{ -- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), +- renders := callsOf(tree, "render") +- if len(renders) == 0 { +- t.Fatalf("found no calls to render") - } -- return e.Server.Implementation(ctx, params) --} -- --func (e *Editor) SignatureHelp(ctx context.Context, loc protocol.Location) (*protocol.SignatureHelp, error) { -- if e.Server == nil { -- return nil, nil +- var found = make(map[string]bool) +- for _, r := range renders { +- if len(r.Args) != 2 { +- // template, func +- t.Fatalf("got %d args, expected 2", len(r.Args)) +- } +- t0, ok := p.TypesInfo.Types[r.Args[0]] +- if !ok || !t0.IsValue() || t0.Type.String() != "*html/template.Template" { +- t.Fatalf("no type info for template") +- } +- if id, ok := r.Args[0].(*ast.Ident); !ok { +- t.Errorf("expected *ast.Ident, got %T", r.Args[0]) +- } else { +- found[id.Name] = true +- } - } -- path := e.sandbox.Workdir.URIToPath(loc.URI) -- e.mu.Lock() -- _, ok := e.buffers[path] -- e.mu.Unlock() -- if !ok { -- return nil, fmt.Errorf("buffer %q is not open", path) +- // make sure found and templates have the same templates +- for k := range found { +- if _, ok := templates[k]; !ok { +- t.Errorf("code has template %s, but test does not", k) +- } - } -- params := &protocol.SignatureHelpParams{ -- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), +- for k := range templates { +- if _, ok := found[k]; !ok { +- t.Errorf("test has template %s, code does not", k) +- } - } -- return e.Server.SignatureHelp(ctx, params) --} -- --func (e *Editor) RenameFile(ctx context.Context, oldPath, newPath string) error { -- closed, opened, err := e.renameBuffers(ctx, oldPath, newPath) -- if err != nil { -- return err +- // now check all the known templates, in alphabetic order, for determinacy +- keys := []string{} +- for k := range templates { +- keys = append(keys, k) - } -- -- for _, c := range closed { -- if err := e.sendDidClose(ctx, c); err != nil { -- return err +- sort.Strings(keys) +- for _, k := range keys { +- v := templates[k] +- // the FuncMap is an annoyance; should not be necessary +- if err := templatecheck.CheckHTML(v.tmpl, v.data); err != nil { +- t.Errorf("%s: %v", k, err) +- continue - } +- t.Logf("%s ok", k) - } -- for _, o := range opened { -- if err := e.sendDidOpen(ctx, o); err != nil { -- return err +-} +- +-func callsOf(tree *ast.File, name string) []*ast.CallExpr { +- var ans []*ast.CallExpr +- f := func(n ast.Node) bool { +- x, ok := n.(*ast.CallExpr) +- if !ok { +- return true +- } +- if y, ok := x.Fun.(*ast.Ident); ok { +- if y.Name == name { +- ans = append(ans, x) +- } - } +- return true - } +- ast.Inspect(tree, f) +- return ans +-} - -- // Finally, perform the renaming on disk. -- if err := e.sandbox.Workdir.RenameFile(ctx, oldPath, newPath); err != nil { -- return fmt.Errorf("renaming sandbox file: %w", err) +-func treeOf(p *packages.Package, fname string) *ast.File { +- for _, tree := range p.Syntax { +- loc := tree.Package +- pos := p.Fset.PositionFor(loc, false) +- if strings.HasSuffix(pos.Filename, fname) { +- return tree +- } - } - return nil -} +diff -urN a/gopls/internal/debug/trace.go b/gopls/internal/debug/trace.go +--- a/gopls/internal/debug/trace.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/debug/trace.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,320 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// renameBuffers renames in-memory buffers affected by the renaming of --// oldPath->newPath, returning the resulting text documents that must be closed --// and opened over the LSP. --func (e *Editor) renameBuffers(ctx context.Context, oldPath, newPath string) (closed []protocol.TextDocumentIdentifier, opened []protocol.TextDocumentItem, _ error) { -- e.mu.Lock() -- defer e.mu.Unlock() -- -- // In case either oldPath or newPath is absolute, convert to absolute paths -- // before checking for containment. -- oldAbs := e.sandbox.Workdir.AbsPath(oldPath) -- newAbs := e.sandbox.Workdir.AbsPath(newPath) -- -- // Collect buffers that are affected by the given file or directory renaming. -- buffersToRename := make(map[string]string) // old path -> new path +-package debug - -- for path := range e.buffers { -- abs := e.sandbox.Workdir.AbsPath(path) -- if oldAbs == abs || source.InDir(oldAbs, abs) { -- rel, err := filepath.Rel(oldAbs, abs) -- if err != nil { -- return nil, nil, fmt.Errorf("filepath.Rel(%q, %q): %v", oldAbs, abs, err) -- } -- nabs := filepath.Join(newAbs, rel) -- newPath := e.sandbox.Workdir.RelPath(nabs) -- buffersToRename[path] = newPath -- } -- } +-import ( +- "bytes" +- "context" +- "fmt" +- "html/template" +- "net/http" +- "runtime/trace" +- "sort" +- "strings" +- "sync" +- "time" - -- // Update buffers, and build protocol changes. -- for old, new := range buffersToRename { -- buf := e.buffers[old] -- delete(e.buffers, old) -- buf.version = 1 -- buf.path = new -- e.buffers[new] = buf +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/core" +- "golang.org/x/tools/internal/event/export" +- "golang.org/x/tools/internal/event/label" +-) - -- closed = append(closed, e.TextDocumentIdentifier(old)) -- opened = append(opened, e.textDocumentItem(buf)) -- } +-// TraceTmpl extends BaseTemplate and renders a TraceResults, e.g. from getData(). +-var TraceTmpl = template.Must(template.Must(BaseTemplate.Clone()).Parse(` +-{{define "title"}}Trace Information{{end}} +-{{define "body"}} +- {{range .Traces}}{{.Name}} last: {{.Last.Duration}}, longest: {{.Longest.Duration}}
    {{end}} +- {{if .Selected}} +-

    {{.Selected.Name}}

    +- {{if .Selected.Last}}

    Last

      {{template "completeSpan" .Selected.Last}}
    {{end}} +- {{if .Selected.Longest}}

    Longest

      {{template "completeSpan" .Selected.Longest}}
    {{end}} +- {{end}} - -- return closed, opened, nil +-

    Recent spans (oldest first)

    +-

    +- A finite number of recent span start/end times are shown below. +- The nesting represents the children of a parent span (and the log events within a span). +- A span may appear twice: chronologically at toplevel, and nested within its parent. +-

    +-
      {{range .Recent}}{{template "spanStartEnd" .}}{{end}}
    +-{{end}} +-{{define "spanStartEnd"}} +- {{if .Start}} +-
  • {{.Span.Header .Start}}
  • +- {{else}} +- {{template "completeSpan" .Span}} +- {{end}} +-{{end}} +-{{define "completeSpan"}} +-
  • {{.Header false}}
  • +- {{if .Events}}
      {{range .Events}}
    • {{.Header}}
    • {{end}}
    {{end}} +- {{if .ChildStartEnd}}
      {{range .ChildStartEnd}}{{template "spanStartEnd" .}}{{end}}
    {{end}} +-{{end}} +-`)) +- +-type traces struct { +- mu sync.Mutex +- sets map[string]*traceSet +- unfinished map[export.SpanContext]*traceSpan +- recent []spanStartEnd +- recentEvictions int -} - --func (e *Editor) applyDocumentChange(ctx context.Context, change protocol.DocumentChanges) error { -- if change.RenameFile != nil { -- oldPath := e.sandbox.Workdir.URIToPath(change.RenameFile.OldURI) -- newPath := e.sandbox.Workdir.URIToPath(change.RenameFile.NewURI) +-// A spanStartEnd records the start or end of a span. +-// If Start, the span may be unfinished, so some fields (e.g. Finish) +-// may be unset and others (e.g. Events) may be being actively populated. +-type spanStartEnd struct { +- Start bool +- Span *traceSpan +-} - -- return e.RenameFile(ctx, oldPath, newPath) -- } -- if change.TextDocumentEdit != nil { -- return e.applyTextDocumentEdit(ctx, *change.TextDocumentEdit) +-func (ev spanStartEnd) Time() time.Time { +- if ev.Start { +- return ev.Span.Start +- } else { +- return ev.Span.Finish - } -- panic("Internal error: one of RenameFile or TextDocumentEdit must be set") -} - --func (e *Editor) applyTextDocumentEdit(ctx context.Context, change protocol.TextDocumentEdit) error { -- path := e.sandbox.Workdir.URIToPath(change.TextDocument.URI) -- if ver := int32(e.BufferVersion(path)); ver != change.TextDocument.Version { -- return fmt.Errorf("buffer versions for %q do not match: have %d, editing %d", path, ver, change.TextDocument.Version) -- } -- if !e.HasBuffer(path) { -- err := e.OpenFile(ctx, path) -- if os.IsNotExist(err) { -- // TODO: it's unclear if this is correct. Here we create the buffer (with -- // version 1), then apply edits. Perhaps we should apply the edits before -- // sending the didOpen notification. -- e.CreateBuffer(ctx, path, "") -- err = nil -- } -- if err != nil { -- return err -- } -- } -- return e.EditBuffer(ctx, path, change.Edits) +-// A TraceResults is the subject for the /trace HTML template. +-type TraceResults struct { // exported for testing +- Traces []*traceSet +- Selected *traceSet +- Recent []spanStartEnd -} - --// Config returns the current editor configuration. --func (e *Editor) Config() EditorConfig { -- e.mu.Lock() -- defer e.mu.Unlock() -- return e.config +-// A traceSet holds two representative spans of a given span name. +-type traceSet struct { +- Name string +- Last *traceSpan +- Longest *traceSpan -} - --func (e *Editor) SetConfig(cfg EditorConfig) { -- e.mu.Lock() -- e.config = cfg -- e.mu.Unlock() +-// A traceSpan holds information about a single span. +-type traceSpan struct { +- TraceID export.TraceID +- SpanID export.SpanID +- ParentID export.SpanID +- Name string +- Start time.Time +- Finish time.Time // set at end +- Duration time.Duration // set at end +- Tags string +- Events []traceEvent // set at end +- ChildStartEnd []spanStartEnd // populated while active +- +- parent *traceSpan -} - --// ChangeConfiguration sets the new editor configuration, and if applicable --// sends a didChangeConfiguration notification. --// --// An error is returned if the change notification failed to send. --func (e *Editor) ChangeConfiguration(ctx context.Context, newConfig EditorConfig) error { -- e.SetConfig(newConfig) -- if e.Server != nil { -- var params protocol.DidChangeConfigurationParams // empty: gopls ignores the Settings field -- if err := e.Server.DidChangeConfiguration(ctx, ¶ms); err != nil { -- return err -- } -- e.callsMu.Lock() -- e.calls.DidChangeConfiguration++ -- e.callsMu.Unlock() +-const timeFormat = "15:04:05.000" +- +-// Header renders the time, name, tags, and (if !start), +-// duration of a span start or end event. +-func (span *traceSpan) Header(start bool) string { +- if start { +- return fmt.Sprintf("%s start %s %s", +- span.Start.Format(timeFormat), span.Name, span.Tags) +- } else { +- return fmt.Sprintf("%s end %s (+%s) %s", +- span.Finish.Format(timeFormat), span.Name, span.Duration, span.Tags) - } -- return nil -} - --// ChangeWorkspaceFolders sets the new workspace folders, and sends a --// didChangeWorkspaceFolders notification to the server. --// --// The given folders must all be unique. --func (e *Editor) ChangeWorkspaceFolders(ctx context.Context, folders []string) error { -- config := e.Config() +-type traceEvent struct { +- Time time.Time +- Offset time.Duration // relative to start of span +- Tags string +-} - -- // capture existing folders so that we can compute the change. -- oldFolders := makeWorkspaceFolders(e.sandbox, config.WorkspaceFolders) -- newFolders := makeWorkspaceFolders(e.sandbox, folders) -- config.WorkspaceFolders = folders -- e.SetConfig(config) +-func (ev traceEvent) Header() string { +- return fmt.Sprintf("%s event (+%s) %s", ev.Time.Format(timeFormat), ev.Offset, ev.Tags) +-} - -- if e.Server == nil { -- return nil +-func StdTrace(exporter event.Exporter) event.Exporter { +- return func(ctx context.Context, ev core.Event, lm label.Map) context.Context { +- span := export.GetSpan(ctx) +- if span == nil { +- return exporter(ctx, ev, lm) +- } +- switch { +- case event.IsStart(ev): +- if span.ParentID.IsValid() { +- region := trace.StartRegion(ctx, span.Name) +- ctx = context.WithValue(ctx, traceKey, region) +- } else { +- var task *trace.Task +- ctx, task = trace.NewTask(ctx, span.Name) +- ctx = context.WithValue(ctx, traceKey, task) +- } +- // Log the start event as it may contain useful labels. +- msg := formatEvent(ev, lm) +- trace.Log(ctx, "start", msg) +- case event.IsLog(ev): +- category := "" +- if event.IsError(ev) { +- category = "error" +- } +- msg := formatEvent(ev, lm) +- trace.Log(ctx, category, msg) +- case event.IsEnd(ev): +- if v := ctx.Value(traceKey); v != nil { +- v.(interface{ End() }).End() +- } +- } +- return exporter(ctx, ev, lm) - } +-} - -- var params protocol.DidChangeWorkspaceFoldersParams +-func formatEvent(ev core.Event, lm label.Map) string { +- buf := &bytes.Buffer{} +- p := export.Printer{} +- p.WriteEvent(buf, ev, lm) +- return buf.String() +-} - -- // Keep track of old workspace folders that must be removed. -- toRemove := make(map[protocol.URI]protocol.WorkspaceFolder) -- for _, folder := range oldFolders { -- toRemove[folder.URI] = folder +-func (t *traces) ProcessEvent(ctx context.Context, ev core.Event, lm label.Map) context.Context { +- span := export.GetSpan(ctx) +- if span == nil { +- return ctx - } - -- // Sanity check: if we see a folder twice the algorithm below doesn't work, -- // so track seen folders to ensure that we panic in that case. -- seen := make(map[protocol.URI]protocol.WorkspaceFolder) -- for _, folder := range newFolders { -- if _, ok := seen[folder.URI]; ok { -- panic(fmt.Sprintf("folder %s seen twice", folder.URI)) +- switch { +- case event.IsStart(ev): +- // Just starting: add it to the unfinished map. +- // Allocate before the critical section. +- td := &traceSpan{ +- TraceID: span.ID.TraceID, +- SpanID: span.ID.SpanID, +- ParentID: span.ParentID, +- Name: span.Name, +- Start: span.Start().At(), +- Tags: renderLabels(span.Start()), - } - -- // If this folder already exists, we don't want to remove it. -- // Otherwise, we need to add it. -- if _, ok := toRemove[folder.URI]; ok { -- delete(toRemove, folder.URI) -- } else { -- params.Event.Added = append(params.Event.Added, folder) +- t.mu.Lock() +- defer t.mu.Unlock() +- +- t.addRecentLocked(td, true) // add start event +- +- if t.sets == nil { +- t.sets = make(map[string]*traceSet) +- t.unfinished = make(map[export.SpanContext]*traceSpan) - } -- } +- t.unfinished[span.ID] = td - -- for _, v := range toRemove { -- params.Event.Removed = append(params.Event.Removed, v) -- } +- // Wire up parents if we have them. +- if span.ParentID.IsValid() { +- parentID := export.SpanContext{TraceID: span.ID.TraceID, SpanID: span.ParentID} +- if parent, ok := t.unfinished[parentID]; ok { +- td.parent = parent +- parent.ChildStartEnd = append(parent.ChildStartEnd, spanStartEnd{true, td}) +- } +- } - -- return e.Server.DidChangeWorkspaceFolders(ctx, ¶ms) --} +- case event.IsEnd(ev): +- // Finishing: must be already in the map. +- // Allocate events before the critical section. +- events := span.Events() +- tdEvents := make([]traceEvent, len(events)) +- for i, event := range events { +- tdEvents[i] = traceEvent{ +- Time: event.At(), +- Tags: renderLabels(event), +- } +- } - --// CodeAction executes a codeAction request on the server. --// If loc.Range is zero, the whole file is implied. --func (e *Editor) CodeAction(ctx context.Context, loc protocol.Location, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) { -- if e.Server == nil { -- return nil, nil -- } -- path := e.sandbox.Workdir.URIToPath(loc.URI) -- e.mu.Lock() -- _, ok := e.buffers[path] -- e.mu.Unlock() -- if !ok { -- return nil, fmt.Errorf("buffer %q is not open", path) -- } -- params := &protocol.CodeActionParams{ -- TextDocument: e.TextDocumentIdentifier(path), -- Context: protocol.CodeActionContext{ -- Diagnostics: diagnostics, -- }, -- Range: loc.Range, // may be zero -- } -- lens, err := e.Server.CodeAction(ctx, params) -- if err != nil { -- return nil, err +- t.mu.Lock() +- defer t.mu.Unlock() +- td, found := t.unfinished[span.ID] +- if !found { +- return ctx // if this happens we are in a bad place +- } +- delete(t.unfinished, span.ID) +- td.Finish = span.Finish().At() +- td.Duration = span.Finish().At().Sub(span.Start().At()) +- td.Events = tdEvents +- t.addRecentLocked(td, false) // add end event +- +- set, ok := t.sets[span.Name] +- if !ok { +- set = &traceSet{Name: span.Name} +- t.sets[span.Name] = set +- } +- set.Last = td +- if set.Longest == nil || set.Last.Duration > set.Longest.Duration { +- set.Longest = set.Last +- } +- if td.parent != nil { +- td.parent.ChildStartEnd = append(td.parent.ChildStartEnd, spanStartEnd{false, td}) +- } else { +- fillOffsets(td, td.Start) +- } - } -- return lens, nil +- return ctx -} - --// Hover triggers a hover at the given position in an open buffer. --func (e *Editor) Hover(ctx context.Context, loc protocol.Location) (*protocol.MarkupContent, protocol.Location, error) { -- if err := e.checkBufferLocation(loc); err != nil { -- return nil, protocol.Location{}, err -- } -- params := &protocol.HoverParams{} -- params.TextDocument.URI = loc.URI -- params.Position = loc.Range.Start +-// addRecentLocked appends a start or end event to the "recent" log, +-// evicting an old entry if necessary. +-func (t *traces) addRecentLocked(span *traceSpan, start bool) { +- t.recent = append(t.recent, spanStartEnd{Start: start, Span: span}) - -- resp, err := e.Server.Hover(ctx, params) -- if err != nil { -- return nil, protocol.Location{}, fmt.Errorf("hover: %w", err) -- } -- if resp == nil { -- return nil, protocol.Location{}, nil -- } -- return &resp.Contents, protocol.Location{URI: loc.URI, Range: resp.Range}, nil --} +- const maxRecent = 100 // number of log entries before eviction +- for len(t.recent) > maxRecent { +- t.recent[0] = spanStartEnd{} // aid GC +- t.recent = t.recent[1:] +- t.recentEvictions++ - --func (e *Editor) DocumentLink(ctx context.Context, path string) ([]protocol.DocumentLink, error) { -- if e.Server == nil { -- return nil, nil +- // Using a slice as a FIFO queue leads to unbounded growth +- // as Go's GC cannot collect the ever-growing unused prefix. +- // So, compact it periodically. +- if t.recentEvictions%maxRecent == 0 { +- t.recent = append([]spanStartEnd(nil), t.recent...) +- } - } -- params := &protocol.DocumentLinkParams{} -- params.TextDocument.URI = e.sandbox.Workdir.URI(path) -- return e.Server.DocumentLink(ctx, params) -} - --func (e *Editor) DocumentHighlight(ctx context.Context, loc protocol.Location) ([]protocol.DocumentHighlight, error) { -- if e.Server == nil { -- return nil, nil -- } -- if err := e.checkBufferLocation(loc); err != nil { -- return nil, err +-// getData returns the TraceResults rendered by TraceTmpl for the /trace[/name] endpoint. +-func (t *traces) getData(req *http.Request) interface{} { +- // TODO(adonovan): the HTTP request doesn't acquire the mutex +- // for t or for each span! Audit and fix. +- +- // Sort last/longest sets by name. +- traces := make([]*traceSet, 0, len(t.sets)) +- for _, set := range t.sets { +- traces = append(traces, set) - } -- params := &protocol.DocumentHighlightParams{} -- params.TextDocument.URI = loc.URI -- params.Position = loc.Range.Start +- sort.Slice(traces, func(i, j int) bool { +- return traces[i].Name < traces[j].Name +- }) - -- return e.Server.DocumentHighlight(ctx, params) +- return TraceResults{ +- Traces: traces, +- Selected: t.sets[strings.TrimPrefix(req.URL.Path, "/trace/")], // may be nil +- Recent: t.recent, +- } -} - --// SemanticTokens invokes textDocument/semanticTokens/full, and interprets its --// result. --func (e *Editor) SemanticTokens(ctx context.Context, path string) ([]SemanticToken, error) { -- p := &protocol.SemanticTokensParams{ -- TextDocument: protocol.TextDocumentIdentifier{ -- URI: e.sandbox.Workdir.URI(path), -- }, -- } -- resp, err := e.Server.SemanticTokensFull(ctx, p) -- if err != nil { -- return nil, err +-func fillOffsets(td *traceSpan, start time.Time) { +- for i := range td.Events { +- td.Events[i].Offset = td.Events[i].Time.Sub(start) - } -- content, ok := e.BufferText(path) -- if !ok { -- return nil, fmt.Errorf("buffer %s is not open", path) +- for _, child := range td.ChildStartEnd { +- if !child.Start { +- fillOffsets(child.Span, start) +- } - } -- return e.interpretTokens(resp.Data, content), nil --} -- --// A SemanticToken is an interpreted semantic token value. --type SemanticToken struct { -- Token string -- TokenType string -- Mod string -} - --// Note: previously this function elided comment, string, and number tokens. --// Instead, filtering of token types should be done by the caller. --func (e *Editor) interpretTokens(x []uint32, contents string) []SemanticToken { -- e.mu.Lock() -- legend := e.semTokOpts.Legend -- e.mu.Unlock() -- lines := strings.Split(contents, "\n") -- ans := []SemanticToken{} -- line, col := 1, 1 -- for i := 0; i < len(x); i += 5 { -- line += int(x[i]) -- col += int(x[i+1]) -- if x[i] != 0 { // new line -- col = int(x[i+1]) + 1 // 1-based column numbers -- } -- sz := x[i+2] -- t := legend.TokenTypes[x[i+3]] -- l := x[i+4] -- var mods []string -- for i, mod := range legend.TokenModifiers { -- if l&(1< time.Hour { +- os.Chtimes(filename, now, now) // ignore error - } -- dataMap[f.Name] = f.Data - } -- return dataMap --} +- touch(indexName) +- touch(casName) - --func validateConfig(config SandboxConfig) error { -- if filepath.IsAbs(config.Workdir) && (len(config.Files) > 0 || config.InGoPath) { -- return errors.New("absolute Workdir cannot be set in conjunction with Files or InGoPath") -- } -- if config.Workdir != "" && config.InGoPath { -- return errors.New("Workdir cannot be set in conjunction with InGoPath") -- } -- if config.GOPROXY != "" && config.ProxyFiles != nil { -- return errors.New("GOPROXY cannot be set in conjunction with ProxyFiles") -- } -- return nil +- memCache.Set(memKey{kind, key}, value, len(value)) +- +- return value, nil -} - --// splitModuleVersionPath extracts module information from files stored in the --// directory structure modulePath@version/suffix. --// For example: --// --// splitModuleVersionPath("mod.com@v1.2.3/package") = ("mod.com", "v1.2.3", "package") --func splitModuleVersionPath(path string) (modulePath, version, suffix string) { -- parts := strings.Split(path, "/") -- var modulePathParts []string -- for i, p := range parts { -- if strings.Contains(p, "@") { -- mv := strings.SplitN(p, "@", 2) -- modulePathParts = append(modulePathParts, mv[0]) -- return strings.Join(modulePathParts, "/"), mv[1], strings.Join(parts[i+1:], "/") -- } -- modulePathParts = append(modulePathParts, p) -- } -- // Default behavior: this is just a module path. -- return path, "", "" --} -- --func (sb *Sandbox) RootDir() string { -- return sb.rootdir --} +-// ErrNotFound is the distinguished error +-// returned by Get when the key is not found. +-var ErrNotFound = fmt.Errorf("not found") - --// GOPATH returns the value of the Sandbox GOPATH. --func (sb *Sandbox) GOPATH() string { -- return sb.gopath --} +-// Set updates the value in the cache. +-func Set(kind string, key [32]byte, value []byte) error { +- memCache.Set(memKey{kind, key}, value, len(value)) - --// GoEnv returns the default environment variables that can be used for --// invoking Go commands in the sandbox. --func (sb *Sandbox) GoEnv() map[string]string { -- vars := map[string]string{ -- "GOPATH": sb.GOPATH(), -- "GOPROXY": sb.goproxy, -- "GO111MODULE": "", -- "GOSUMDB": "off", -- "GOPACKAGESDRIVER": "off", -- } -- if testenv.Go1Point() >= 5 { -- vars["GOMODCACHE"] = "" +- // Set the active event to wake up the GC. +- select { +- case active <- struct{}{}: +- default: - } -- return vars --} - --// goCommandInvocation returns a new gocommand.Invocation initialized with the --// sandbox environment variables and working directory. --func (sb *Sandbox) goCommandInvocation() gocommand.Invocation { -- var vars []string -- for k, v := range sb.GoEnv() { -- vars = append(vars, fmt.Sprintf("%s=%s", k, v)) -- } -- inv := gocommand.Invocation{ -- Env: vars, -- } -- // sb.Workdir may be nil if we exited the constructor with errors (we call -- // Close to clean up any partial state from the constructor, which calls -- // RunGoCommand). -- if sb.Workdir != nil { -- inv.WorkingDir = string(sb.Workdir.RelativeTo) -- } -- return inv --} +- iolimit <- struct{}{} // acquire a token +- defer func() { <-iolimit }() // release a token - --// RunGoCommand executes a go command in the sandbox. If checkForFileChanges is --// true, the sandbox scans the working directory and emits file change events --// for any file changes it finds. --func (sb *Sandbox) RunGoCommand(ctx context.Context, dir, verb string, args, env []string, checkForFileChanges bool) error { -- inv := sb.goCommandInvocation() -- inv.Verb = verb -- inv.Args = args -- inv.Env = append(inv.Env, env...) -- if dir != "" { -- inv.WorkingDir = sb.Workdir.AbsPath(dir) -- } -- stdout, stderr, _, err := sb.goCommandRunner.RunRaw(ctx, inv) +- // First, add the value to the content- +- // addressable store (CAS), if not present. +- hash := sha256.Sum256(value) +- casName, err := filename(casKind, hash) - if err != nil { -- return fmt.Errorf("go command failed (stdout: %s) (stderr: %s): %v", stdout.String(), stderr.String(), err) +- return err - } -- // Since running a go command may result in changes to workspace files, -- // check if we need to send any "watched" file events. -- // -- // TODO(rFindley): this side-effect can impact the usability of the sandbox -- // for benchmarks. Consider refactoring. -- if sb.Workdir != nil && checkForFileChanges { -- if err := sb.Workdir.CheckForFileChanges(ctx); err != nil { -- return fmt.Errorf("checking for file changes: %w", err) +- // Does CAS file exist and have correct (complete) content? +- // TODO(adonovan): opt: use mmap for this check. +- if prev, _ := os.ReadFile(casName); !bytes.Equal(prev, value) { +- if err := os.MkdirAll(filepath.Dir(casName), 0700); err != nil { +- return err +- } +- // Avoiding O_TRUNC here is merely an optimization to avoid +- // cache misses when two threads race to write the same file. +- if err := writeFileNoTrunc(casName, value, 0600); err != nil { +- os.Remove(casName) // ignore error +- return err // e.g. disk full - } - } -- return nil --} -- --// GoVersion checks the version of the go command. --// It returns the X in Go 1.X. --func (sb *Sandbox) GoVersion(ctx context.Context) (int, error) { -- inv := sb.goCommandInvocation() -- return gocommand.GoVersion(ctx, inv, &sb.goCommandRunner) --} - --// Close removes all state associated with the sandbox. --func (sb *Sandbox) Close() error { -- var goCleanErr error -- if sb.gopath != "" { -- goCleanErr = sb.RunGoCommand(context.Background(), "", "clean", []string{"-modcache"}, nil, false) +- // Now write an index entry that refers to the CAS file. +- indexName, err := filename(kind, key) +- if err != nil { +- return err - } -- err := robustio.RemoveAll(sb.rootdir) -- if err != nil || goCleanErr != nil { -- return fmt.Errorf("error(s) cleaning sandbox: cleaning modcache: %v; removing files: %v", goCleanErr, err) +- if err := os.MkdirAll(filepath.Dir(indexName), 0700); err != nil { +- return err +- } +- if err := writeFileNoTrunc(indexName, hash[:], 0600); err != nil { +- os.Remove(indexName) // ignore error +- return err // e.g. disk full - } +- - return nil -} -diff -urN a/gopls/internal/lsp/fake/workdir.go b/gopls/internal/lsp/fake/workdir.go ---- a/gopls/internal/lsp/fake/workdir.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/fake/workdir.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,430 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package fake -- --import ( -- "bytes" -- "context" -- "crypto/sha256" -- "fmt" -- "io/fs" -- "os" -- "path/filepath" -- "runtime" -- "sort" -- "strings" -- "sync" -- "time" -- -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/robustio" --) -- --// RelativeTo is a helper for operations relative to a given directory. --type RelativeTo string - --// AbsPath returns an absolute filesystem path for the workdir-relative path. --func (r RelativeTo) AbsPath(path string) string { -- fp := filepath.FromSlash(path) -- if filepath.IsAbs(fp) { -- return fp -- } -- return filepath.Join(string(r), filepath.FromSlash(path)) --} +-// The active 1-channel is a selectable resettable event +-// indicating recent cache activity. +-var active = make(chan struct{}, 1) - --// RelPath returns a '/'-encoded path relative to the working directory (or an --// absolute path if the file is outside of workdir) --func (r RelativeTo) RelPath(fp string) string { -- root := string(r) -- if rel, err := filepath.Rel(root, fp); err == nil && !strings.HasPrefix(rel, "..") { -- return filepath.ToSlash(rel) +-// writeFileNoTrunc is like os.WriteFile but doesn't truncate until +-// after the write, so that racing writes of the same data are idempotent. +-func writeFileNoTrunc(filename string, data []byte, perm os.FileMode) error { +- f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, perm) +- if err != nil { +- return err - } -- return filepath.ToSlash(fp) --} -- --// writeFileData writes content to the relative path, replacing the special --// token $SANDBOX_WORKDIR with the relative root given by rel. It does not --// trigger any file events. --func writeFileData(path string, content []byte, rel RelativeTo) error { -- content = bytes.ReplaceAll(content, []byte("$SANDBOX_WORKDIR"), []byte(rel)) -- fp := rel.AbsPath(path) -- if err := os.MkdirAll(filepath.Dir(fp), 0755); err != nil { -- return fmt.Errorf("creating nested directory: %w", err) +- _, err = f.Write(data) +- if err == nil { +- err = f.Truncate(int64(len(data))) - } -- backoff := 1 * time.Millisecond -- for { -- err := os.WriteFile(fp, []byte(content), 0644) -- if err != nil { -- // This lock file violation is not handled by the robustio package, as it -- // indicates a real race condition that could be avoided. -- if isWindowsErrLockViolation(err) { -- time.Sleep(backoff) -- backoff *= 2 -- continue -- } -- return fmt.Errorf("writing %q: %w", path, err) -- } -- return nil +- if closeErr := f.Close(); err == nil { +- err = closeErr - } +- return err -} - --// isWindowsErrLockViolation reports whether err is ERROR_LOCK_VIOLATION --// on Windows. --var isWindowsErrLockViolation = func(err error) bool { return false } -- --// Workdir is a temporary working directory for tests. It exposes file --// operations in terms of relative paths, and fakes file watching by triggering --// events on file operations. --type Workdir struct { -- RelativeTo +-// reserved kind strings +-const ( +- casKind = "cas" // content-addressable store files +- bugKind = "bug" // gopls bug reports +-) - -- watcherMu sync.Mutex -- watchers []func(context.Context, []protocol.FileEvent) +-var iolimit = make(chan struct{}, 128) // counting semaphore to limit I/O concurrency in Set. - -- fileMu sync.Mutex -- // File identities we know about, for the purpose of detecting changes. -- // -- // Since files is only used for detecting _changes_, we are tolerant of -- // fileIDs that may have hash and mtime coming from different states of the -- // file: if either are out of sync, then the next poll should detect a -- // discrepancy. It is OK if we detect too many changes, but not OK if we miss -- // changes. -- // -- // For that matter, this mechanism for detecting changes can still be flaky -- // on platforms where mtime is very coarse (such as older versions of WSL). -- // It would be much better to use a proper fs event library, but we can't -- // currently import those into x/tools. -- // -- // TODO(golang/go#52284): replace this polling mechanism with a -- // cross-platform library for filesystem notifications. -- files map[string]fileID --} +-var budget int64 = 1e9 // 1GB - --// NewWorkdir writes the txtar-encoded file data in txt to dir, and returns a --// Workir for operating on these files using --func NewWorkdir(dir string, files map[string][]byte) (*Workdir, error) { -- w := &Workdir{RelativeTo: RelativeTo(dir)} -- for name, data := range files { -- if err := writeFileData(name, data, w.RelativeTo); err != nil { -- return nil, fmt.Errorf("writing to workdir: %w", err) -- } +-// SetBudget sets a soft limit on disk usage of regular files in the +-// cache (in bytes) and returns the previous value. Supplying a +-// negative value queries the current value without changing it. +-// +-// If two gopls processes have different budgets, the one with the +-// lower budget will collect garbage more actively, but both will +-// observe the effect. +-// +-// Even in the steady state, the storage usage reported by the 'du' +-// command may exceed the budget by as much as a factor of 3 due to +-// the overheads of directories and the effects of block quantization, +-// which are especially pronounced for the small index files. +-func SetBudget(new int64) (old int64) { +- if new < 0 { +- return atomic.LoadInt64(&budget) - } -- _, err := w.pollFiles() // poll files to populate the files map. -- return w, err --} -- --// fileID identifies a file version on disk. --type fileID struct { -- mtime time.Time -- hash string // empty if mtime is old enough to be reliable; otherwise a file digest --} -- --func hashFile(data []byte) string { -- return fmt.Sprintf("%x", sha256.Sum256(data)) +- return atomic.SwapInt64(&budget, new) -} - --// RootURI returns the root URI for this working directory of this scratch --// environment. --func (w *Workdir) RootURI() protocol.DocumentURI { -- return toURI(string(w.RelativeTo)) --} +-// --- implementation ---- - --// AddWatcher registers the given func to be called on any file change. --func (w *Workdir) AddWatcher(watcher func(context.Context, []protocol.FileEvent)) { -- w.watcherMu.Lock() -- w.watchers = append(w.watchers, watcher) -- w.watcherMu.Unlock() +-// filename returns the name of the cache file of the specified kind and key. +-// +-// A typical cache file has a name such as: +-// +-// $HOME/Library/Caches / gopls / VVVVVVVV / KK / KKKK...KKKK - kind +-// +-// The portions separated by spaces are as follows: +-// - The user's preferred cache directory; the default value varies by OS. +-// - The constant "gopls". +-// - The "version", 32 bits of the digest of the gopls executable. +-// - The first 8 bits of the key, to avoid huge directories. +-// - The full 256 bits of the key. +-// - The kind or purpose of this cache file (e.g. "analysis"). +-// +-// The kind establishes a namespace for the keys. It is represented as +-// a suffix, not a segment, as this significantly reduces the number +-// of directories created, and thus the storage overhead. +-// +-// Previous iterations of the design aimed for the invariant that once +-// a file is written, its contents are never modified, though it may +-// be atomically replaced or removed. However, not all platforms have +-// an atomic rename operation (our first approach), and file locking +-// (our second) is a notoriously fickle mechanism. +-// +-// The current design instead exploits a trick from the cache +-// implementation used by the go command: writes of small files are in +-// practice atomic (all or nothing) on all platforms. +-// (See GOROOT/src/cmd/go/internal/cache/cache.go.) +-// +-// Russ Cox notes: "all file systems use an rwlock around every file +-// system block, including data blocks, so any writes or reads within +-// the same block are going to be handled atomically by the FS +-// implementation without any need to request file locking explicitly. +-// And since the files are so small, there's only one block. (A block +-// is at minimum 512 bytes, usually much more.)" And: "all modern file +-// systems protect against [partial writes due to power loss] with +-// journals." +-// +-// We use a two-level scheme consisting of an index and a +-// content-addressable store (CAS). A single cache entry consists of +-// two files. The value of a cache entry is written into the file at +-// filename("cas", sha256(value)). Since the value may be arbitrarily +-// large, this write is not atomic. That means we must check the +-// integrity of the contents read back from the CAS to make sure they +-// hash to the expected key. If the CAS file is incomplete or +-// inconsistent, we proceed as if it were missing. +-// +-// Once the CAS file has been written, we write a small fixed-size +-// index file at filename(kind, key), using the values supplied by the +-// caller. The index file contains the hash that identifies the value +-// file in the CAS. (We could add extra metadata to this file, up to +-// 512B, the minimum size of a disk block, if later desired, so long +-// as the total size remains fixed.) Because the index file is small, +-// concurrent writes to it are atomic in practice, even though this is +-// not guaranteed by any OS. The fixed size ensures that readers can't +-// see a palimpsest when a short new file overwrites a longer old one. +-// +-// New versions of gopls are free to reorganize the contents of the +-// version directory as needs evolve. But all versions of gopls must +-// in perpetuity treat the "gopls" directory in a common fashion. +-// +-// In particular, each gopls process attempts to garbage collect +-// the entire gopls directory so that newer binaries can clean up +-// after older ones: in the development cycle especially, new +-// versions may be created frequently. +-func filename(kind string, key [32]byte) (string, error) { +- base := fmt.Sprintf("%x-%s", key, kind) +- dir, err := getCacheDir() +- if err != nil { +- return "", err +- } +- // Keep the BugReports function consistent with this one. +- return filepath.Join(dir, base[:2], base), nil -} - --// URI returns the URI to a the workdir-relative path. --func (w *Workdir) URI(path string) protocol.DocumentURI { -- return toURI(w.AbsPath(path)) --} +-// getCacheDir returns the persistent cache directory of all processes +-// running this version of the gopls executable. +-// +-// It must incorporate the hash of the executable so that we needn't +-// worry about incompatible changes to the file format or changes to +-// the algorithm that produced the index. +-func getCacheDir() (string, error) { +- cacheDirOnce.Do(func() { +- // Use user's preferred cache directory. +- userDir := os.Getenv("GOPLSCACHE") +- if userDir == "" { +- var err error +- userDir, err = os.UserCacheDir() +- if err != nil { +- userDir = os.TempDir() +- } +- } +- goplsDir := filepath.Join(userDir, "gopls") - --// URIToPath converts a uri to a workdir-relative path (or an absolute path, --// if the uri is outside of the workdir). --func (w *Workdir) URIToPath(uri protocol.DocumentURI) string { -- fp := uri.SpanURI().Filename() -- return w.RelPath(fp) --} +- // UserCacheDir may return a nonexistent directory +- // (in which case we must create it, which may fail), +- // or it may return a non-writable directory, in +- // which case we should ideally respect the user's express +- // wishes (e.g. XDG_CACHE_HOME) and not write somewhere else. +- // Sadly UserCacheDir doesn't currently let us distinguish +- // such intent from accidental misconfiguraton such as HOME=/ +- // in a CI builder. So, we check whether the gopls subdirectory +- // can be created (or already exists) and not fall back to /tmp. +- // See also https://github.com/golang/go/issues/57638. +- if os.MkdirAll(goplsDir, 0700) != nil { +- goplsDir = filepath.Join(os.TempDir(), "gopls") +- } - --func toURI(fp string) protocol.DocumentURI { -- return protocol.DocumentURI(span.URIFromPath(fp)) --} +- // Start the garbage collector. +- go gc(goplsDir) - --// ReadFile reads a text file specified by a workdir-relative path. --func (w *Workdir) ReadFile(path string) ([]byte, error) { -- backoff := 1 * time.Millisecond -- for { -- b, err := os.ReadFile(w.AbsPath(path)) +- // Compute the hash of this executable (~20ms) and create a subdirectory. +- hash, err := hashExecutable() - if err != nil { -- if runtime.GOOS == "plan9" && strings.HasSuffix(err.Error(), " exclusive use file already open") { -- // Plan 9 enforces exclusive access to locked files. -- // Give the owner time to unlock it and retry. -- time.Sleep(backoff) -- backoff *= 2 -- continue -- } -- return nil, err +- cacheDirErr = fmt.Errorf("can't hash gopls executable: %v", err) - } -- return b, nil -- } +- // Use only 32 bits of the digest to avoid unwieldy filenames. +- // It's not an adversarial situation. +- cacheDir = filepath.Join(goplsDir, fmt.Sprintf("%x", hash[:4])) +- if err := os.MkdirAll(cacheDir, 0700); err != nil { +- cacheDirErr = fmt.Errorf("can't create cache: %v", err) +- } +- }) +- return cacheDir, cacheDirErr -} - --// RegexpSearch searches the file corresponding to path for the first position --// matching re. --func (w *Workdir) RegexpSearch(path string, re string) (protocol.Location, error) { -- content, err := w.ReadFile(path) +-var ( +- cacheDirOnce sync.Once +- cacheDir string +- cacheDirErr error +-) +- +-func hashExecutable() (hash [32]byte, err error) { +- exe, err := os.Executable() - if err != nil { -- return protocol.Location{}, err +- return hash, err - } -- mapper := protocol.NewMapper(w.URI(path).SpanURI(), content) -- return regexpLocation(mapper, re) --} -- --// RemoveFile removes a workdir-relative file path and notifies watchers of the --// change. --func (w *Workdir) RemoveFile(ctx context.Context, path string) error { -- fp := w.AbsPath(path) -- if err := robustio.RemoveAll(fp); err != nil { -- return fmt.Errorf("removing %q: %w", path, err) +- f, err := os.Open(exe) +- if err != nil { +- return hash, err - } -- -- return w.CheckForFileChanges(ctx) --} -- --// WriteFiles writes the text file content to workdir-relative paths and --// notifies watchers of the changes. --func (w *Workdir) WriteFiles(ctx context.Context, files map[string]string) error { -- for path, content := range files { -- fp := w.AbsPath(path) -- _, err := os.Stat(fp) -- if err != nil && !os.IsNotExist(err) { -- return fmt.Errorf("checking if %q exists: %w", path, err) -- } -- if err := writeFileData(path, []byte(content), w.RelativeTo); err != nil { -- return err -- } +- defer f.Close() +- h := sha256.New() +- if _, err := io.Copy(h, f); err != nil { +- return hash, fmt.Errorf("can't read executable: %w", err) - } -- return w.CheckForFileChanges(ctx) --} -- --// WriteFile writes text file content to a workdir-relative path and notifies --// watchers of the change. --func (w *Workdir) WriteFile(ctx context.Context, path, content string) error { -- return w.WriteFiles(ctx, map[string]string{path: content}) +- h.Sum(hash[:0]) +- return hash, nil -} - --// RenameFile performs an on disk-renaming of the workdir-relative oldPath to --// workdir-relative newPath, and notifies watchers of the changes. +-// gc runs forever, periodically deleting files from the gopls +-// directory until the space budget is no longer exceeded, and also +-// deleting files older than the maximum age, regardless of budget. -// --// oldPath must either be a regular file or in the same directory as newPath. --func (w *Workdir) RenameFile(ctx context.Context, oldPath, newPath string) error { -- oldAbs := w.AbsPath(oldPath) -- newAbs := w.AbsPath(newPath) +-// One gopls process may delete garbage created by a different gopls +-// process, possibly running a different version of gopls, possibly +-// running concurrently. +-func gc(goplsDir string) { +- // period between collections +- // +- // Originally the period was always 1 minute, but this +- // consumed 15% of a CPU core when idle (#61049). +- // +- // The reason for running collections even when idle is so +- // that long lived gopls sessions eventually clean up the +- // caches created by defunct executables. +- const ( +- minPeriod = 5 * time.Minute // when active +- maxPeriod = 6 * time.Hour // when idle +- ) - -- // For os.Rename, “OS-specific restrictions may apply when oldpath and newpath -- // are in different directories.” If that applies here, we may fall back to -- // ReadFile, WriteFile, and RemoveFile to perform the rename non-atomically. +- // Sleep statDelay*batchSize between stats to smooth out I/O. - // -- // However, the fallback path only works for regular files: renaming a -- // directory would be much more complex and isn't needed for our tests. -- fallbackOk := false -- if filepath.Dir(oldAbs) != filepath.Dir(newAbs) { -- fi, err := os.Stat(oldAbs) -- if err == nil && !fi.Mode().IsRegular() { -- return &os.PathError{ -- Op: "RenameFile", -- Path: oldPath, -- Err: fmt.Errorf("%w: file is not regular and not in the same directory as %s", os.ErrInvalid, newPath), -- } -- } -- fallbackOk = true -- } +- // The constants below were chosen using the following heuristics: +- // - 1GB of filecache is on the order of ~100-200k files, in which case +- // 100μs delay per file introduces 10-20s of additional walk time, +- // less than the minPeriod. +- // - Processing batches of stats at once is much more efficient than +- // sleeping after every stat (due to OS optimizations). +- const statDelay = 100 * time.Microsecond // average delay between stats, to smooth out I/O +- const batchSize = 1000 // # of stats to process before sleeping +- const maxAge = 5 * 24 * time.Hour // max time since last access before file is deleted - -- var renameErr error -- const debugFallback = false -- if fallbackOk && debugFallback { -- renameErr = fmt.Errorf("%w: debugging fallback path", os.ErrInvalid) -- } else { -- renameErr = robustio.Rename(oldAbs, newAbs) -- } -- if renameErr != nil { -- if !fallbackOk { -- return renameErr // The OS-specific Rename restrictions do not apply. -- } +- // The macOS filesystem is strikingly slow, at least on some machines. +- // /usr/bin/find achieves only about 25,000 stats per second +- // at full speed (no pause between items), meaning a large +- // cache may take several minutes to scan. +- // We must ensure that short-lived processes (crucially, +- // tests) are able to make progress sweeping garbage. +- // +- // (gopls' caches should never actually get this big in +- // practice: the example mentioned above resulted from a bug +- // that caused filecache to fail to delete any files.) - -- content, err := w.ReadFile(oldPath) -- if err != nil { -- // If we can't even read the file, the error from Rename may be accurate. -- return renameErr +- const debug = false +- +- // Names of all directories found in first pass; nil thereafter. +- dirs := make(map[string]bool) +- +- for { +- // Enumerate all files in the cache. +- type item struct { +- path string +- mtime time.Time +- size int64 - } -- fi, err := os.Stat(newAbs) -- if err == nil { -- if fi.IsDir() { -- // “If newpath already exists and is not a directory, Rename replaces it.” -- // But if it is a directory, maybe not? -- return renameErr +- var files []item +- start := time.Now() +- var total int64 // bytes +- _ = filepath.Walk(goplsDir, func(path string, stat os.FileInfo, err error) error { +- if err != nil { +- return nil // ignore errors - } -- // On most platforms, Rename replaces the named file with a new file, -- // rather than overwriting the existing file it in place. Mimic that -- // behavior here. -- if err := robustio.RemoveAll(newAbs); err != nil { -- // Maybe we don't have permission to replace newPath? -- return renameErr +- if stat.IsDir() { +- // Collect (potentially empty) directories. +- if dirs != nil { +- dirs[path] = true +- } +- } else { +- // Unconditionally delete files we haven't used in ages. +- // (We do this here, not in the second loop, so that we +- // perform age-based collection even in short-lived processes.) +- age := time.Since(stat.ModTime()) +- if age > maxAge { +- if debug { +- log.Printf("age: deleting stale file %s (%dB, age %v)", +- path, stat.Size(), age) +- } +- os.Remove(path) // ignore error +- } else { +- files = append(files, item{path, stat.ModTime(), stat.Size()}) +- total += stat.Size() +- if debug && len(files)%1000 == 0 { +- log.Printf("filecache: checked %d files in %v", len(files), time.Since(start)) +- } +- if len(files)%batchSize == 0 { +- time.Sleep(batchSize * statDelay) +- } +- } - } -- } else if !os.IsNotExist(err) { -- // If the destination path already exists or there is some problem with it, -- // the error from Rename may be accurate. -- return renameErr +- return nil +- }) +- +- // Sort oldest files first. +- sort.Slice(files, func(i, j int) bool { +- return files[i].mtime.Before(files[j].mtime) +- }) +- +- // Delete oldest files until we're under budget. +- budget := atomic.LoadInt64(&budget) +- for _, file := range files { +- if total < budget { +- break +- } +- if debug { +- age := time.Since(file.mtime) +- log.Printf("budget: deleting stale file %s (%dB, age %v)", +- file.path, file.size, age) +- } +- os.Remove(file.path) // ignore error +- total -= file.size - } -- if writeErr := writeFileData(newPath, []byte(content), w.RelativeTo); writeErr != nil { -- // At this point we have tried to actually write the file. -- // If it still doesn't exist, assume that the error from Rename was accurate: -- // for example, maybe we don't have permission to create the new path. -- // Otherwise, return the error from the write, which may indicate some -- // other problem (such as a full disk). -- if _, statErr := os.Stat(newAbs); !os.IsNotExist(statErr) { -- return writeErr +- files = nil // release memory before sleep +- +- // Wait unconditionally for the minimum period. +- time.Sleep(minPeriod) +- +- // Once only, delete all directories. +- // This will succeed only for the empty ones, +- // and ensures that stale directories (whose +- // files have been deleted) are removed eventually. +- // They don't take up much space but they do slow +- // down the traversal. +- // +- // We do this after the sleep to minimize the +- // race against Set, which may create a directory +- // that is momentarily empty. +- // +- // (Test processes don't live that long, so +- // this may not be reached on the CI builders.) +- if dirs != nil { +- dirnames := make([]string, 0, len(dirs)) +- for dir := range dirs { +- dirnames = append(dirnames, dir) +- } +- dirs = nil +- +- // Descending length order => children before parents. +- sort.Slice(dirnames, func(i, j int) bool { +- return len(dirnames[i]) > len(dirnames[j]) +- }) +- var deleted int +- for _, dir := range dirnames { +- if os.Remove(dir) == nil { // ignore error +- deleted++ +- } +- } +- if debug { +- log.Printf("deleted %d empty directories", deleted) - } -- return renameErr - } -- if err := robustio.RemoveAll(oldAbs); err != nil { -- // If we failed to remove the old file, that may explain the Rename error too. -- // Make a best effort to back out the write to the new path. -- robustio.RemoveAll(newAbs) -- return renameErr +- +- // Wait up to the max period, +- // or for Set activity in this process. +- select { +- case <-active: +- case <-time.After(maxPeriod): - } - } -- -- return w.CheckForFileChanges(ctx) -} - --// ListFiles returns a new sorted list of the relative paths of files in dir, --// recursively. --func (w *Workdir) ListFiles(dir string) ([]string, error) { -- absDir := w.AbsPath(dir) -- var paths []string -- if err := filepath.Walk(absDir, func(fp string, info os.FileInfo, err error) error { +-func init() { +- // Register a handler to durably record this process's first +- // assertion failure in the cache so that we can ask users to +- // share this information via the stats command. +- bug.Handle(func(bug bug.Bug) { +- // Wait for cache init (bugs in tests happen early). +- _, _ = getCacheDir() +- +- data, err := json.Marshal(bug) - if err != nil { -- return err -- } -- if info.Mode()&(fs.ModeDir|fs.ModeSymlink) == 0 { -- paths = append(paths, w.RelPath(fp)) +- panic(fmt.Sprintf("error marshalling bug %+v: %v", bug, err)) - } -- return nil -- }); err != nil { -- return nil, err -- } -- sort.Strings(paths) -- return paths, nil --} - --// CheckForFileChanges walks the working directory and checks for any files --// that have changed since the last poll. --func (w *Workdir) CheckForFileChanges(ctx context.Context) error { -- evts, err := w.pollFiles() -- if err != nil { -- return err -- } -- if len(evts) == 0 { -- return nil -- } -- w.watcherMu.Lock() -- watchers := make([]func(context.Context, []protocol.FileEvent), len(w.watchers)) -- copy(watchers, w.watchers) -- w.watcherMu.Unlock() -- for _, w := range watchers { -- w(ctx, evts) -- } -- return nil +- key := sha256.Sum256(data) +- _ = Set(bugKind, key, data) +- }) -} - --// pollFiles updates w.files and calculates FileEvents corresponding to file --// state changes since the last poll. It does not call sendEvents. --func (w *Workdir) pollFiles() ([]protocol.FileEvent, error) { -- w.fileMu.Lock() -- defer w.fileMu.Unlock() +-// BugReports returns a new unordered array of the contents +-// of all cached bug reports produced by this executable. +-// It also returns the location of the cache directory +-// used by this process (or "" on initialization error). +-func BugReports() (string, []bug.Bug) { +- // To test this logic, run: +- // $ TEST_GOPLS_BUG=oops gopls bug # trigger a bug +- // $ gopls stats # list the bugs - -- newFiles := make(map[string]fileID) -- var evts []protocol.FileEvent -- if err := filepath.Walk(string(w.RelativeTo), func(fp string, info os.FileInfo, err error) error { +- dir, err := getCacheDir() +- if err != nil { +- return "", nil // ignore initialization errors +- } +- var result []bug.Bug +- _ = filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error { - if err != nil { -- return err -- } -- // Skip directories and symbolic links (which may be links to directories). -- // -- // The latter matters for repos like Kubernetes, which use symlinks. -- if info.Mode()&(fs.ModeDir|fs.ModeSymlink) != 0 { -- return nil +- return nil // ignore readdir/stat errors - } -- -- // Opt: avoid reading the file if mtime is sufficiently old to be reliable. -- // -- // If mtime is recent, it may not sufficiently identify the file contents: -- // a subsequent write could result in the same mtime. For these cases, we -- // must read the file contents. -- id := fileID{mtime: info.ModTime()} -- if time.Since(info.ModTime()) < 2*time.Second { -- data, err := os.ReadFile(fp) -- if err != nil { -- return err +- // Parse the key from each "XXXX-bug" cache file name. +- if !info.IsDir() && strings.HasSuffix(path, bugKind) { +- var key [32]byte +- n, err := hex.Decode(key[:], []byte(filepath.Base(path)[:len(key)*2])) +- if err != nil || n != len(key) { +- return nil // ignore malformed file names - } -- id.hash = hashFile(data) -- } -- path := w.RelPath(fp) -- newFiles[path] = id -- -- if w.files != nil { -- oldID, ok := w.files[path] -- delete(w.files, path) -- switch { -- case !ok: -- evts = append(evts, protocol.FileEvent{ -- URI: w.URI(path), -- Type: protocol.Created, -- }) -- case oldID != id: -- changed := true -- -- // Check whether oldID and id do not match because oldID was polled at -- // a recent enough to time such as to require hashing. -- // -- // In this case, read the content to check whether the file actually -- // changed. -- if oldID.mtime.Equal(id.mtime) && oldID.hash != "" && id.hash == "" { -- data, err := os.ReadFile(fp) -- if err != nil { -- return err -- } -- if hashFile(data) == oldID.hash { -- changed = false -- } -- } -- if changed { -- evts = append(evts, protocol.FileEvent{ -- URI: w.URI(path), -- Type: protocol.Changed, -- }) +- content, err := Get(bugKind, key) +- if err == nil { // ignore read errors +- var b bug.Bug +- if err := json.Unmarshal(content, &b); err != nil { +- log.Printf("error marshalling bug %q: %v", string(content), err) - } +- result = append(result, b) - } - } -- - return nil -- }); err != nil { -- return nil, err -- } -- -- // Any remaining files must have been deleted. -- for path := range w.files { -- evts = append(evts, protocol.FileEvent{ -- URI: w.URI(path), -- Type: protocol.Deleted, -- }) -- } -- w.files = newFiles -- return evts, nil +- }) +- return dir, result -} -diff -urN a/gopls/internal/lsp/fake/workdir_test.go b/gopls/internal/lsp/fake/workdir_test.go ---- a/gopls/internal/lsp/fake/workdir_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/fake/workdir_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,219 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/filecache/filecache_test.go b/gopls/internal/filecache/filecache_test.go +--- a/gopls/internal/filecache/filecache_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/filecache/filecache_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,265 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package fake +-package filecache_test +- +-// This file defines tests of the API of the filecache package. +-// +-// Some properties (e.g. garbage collection) cannot be exercised +-// through the API, so this test does not attempt to do so. - -import ( -- "context" +- "bytes" +- cryptorand "crypto/rand" +- "fmt" +- "log" +- mathrand "math/rand" - "os" -- "sync" +- "os/exec" +- "strconv" +- "strings" - "testing" - -- "github.com/google/go-cmp/cmp" -- "golang.org/x/tools/gopls/internal/lsp/protocol" +- "golang.org/x/sync/errgroup" +- "golang.org/x/tools/gopls/internal/filecache" +- "golang.org/x/tools/internal/testenv" -) - --const sharedData = ` ---- go.mod -- --go 1.12 ---- nested/README.md -- --Hello World! --` -- --// newWorkdir sets up a temporary Workdir with the given txtar-encoded content. --// It also configures an eventBuffer to receive file event notifications. These --// notifications are sent synchronously for each operation, such that once a --// workdir file operation has returned the caller can expect that any relevant --// file notifications are present in the buffer. --// --// It is the caller's responsibility to call the returned cleanup function. --func newWorkdir(t *testing.T, txt string) (*Workdir, *eventBuffer, func()) { -- t.Helper() +-func TestBasics(t *testing.T) { +- const kind = "TestBasics" +- key := uniqueKey() // never used before +- value := []byte("hello") - -- tmpdir, err := os.MkdirTemp("", "goplstest-workdir-") -- if err != nil { -- t.Fatal(err) -- } -- wd, err := NewWorkdir(tmpdir, UnpackTxt(txt)) -- if err != nil { -- t.Fatal(err) -- } -- cleanup := func() { -- if err := os.RemoveAll(tmpdir); err != nil { -- t.Error(err) +- // Get of a never-seen key returns not found. +- if _, err := filecache.Get(kind, key); err != filecache.ErrNotFound { +- if strings.Contains(err.Error(), "operation not supported") || +- strings.Contains(err.Error(), "not implemented") { +- t.Skipf("skipping: %v", err) - } +- t.Errorf("Get of random key returned err=%q, want not found", err) - } - -- buf := new(eventBuffer) -- wd.AddWatcher(buf.onEvents) -- return wd, buf, cleanup --} -- --// eventBuffer collects events from a file watcher. --type eventBuffer struct { -- mu sync.Mutex -- events []protocol.FileEvent --} -- --// onEvents collects adds events to the buffer; to be used with Workdir.AddWatcher. --func (c *eventBuffer) onEvents(_ context.Context, events []protocol.FileEvent) { -- c.mu.Lock() -- defer c.mu.Unlock() -- -- c.events = append(c.events, events...) --} -- --// take empties the buffer, returning its previous contents. --func (c *eventBuffer) take() []protocol.FileEvent { -- c.mu.Lock() -- defer c.mu.Unlock() -- -- evts := c.events -- c.events = nil -- return evts --} -- --func TestWorkdir_ReadFile(t *testing.T) { -- wd, _, cleanup := newWorkdir(t, sharedData) -- defer cleanup() +- // Set of a never-seen key and a small value succeeds. +- if err := filecache.Set(kind, key, value); err != nil { +- t.Errorf("Set failed: %v", err) +- } - -- got, err := wd.ReadFile("nested/README.md") -- if err != nil { -- t.Fatal(err) +- // Get of the key returns a copy of the value. +- if got, err := filecache.Get(kind, key); err != nil { +- t.Errorf("Get after Set failed: %v", err) +- } else if string(got) != string(value) { +- t.Errorf("Get after Set returned different value: got %q, want %q", got, value) - } -- want := "Hello World!\n" -- if got := string(got); got != want { -- t.Errorf("reading workdir file, got %q, want %q", got, want) +- +- // The kind is effectively part of the key. +- if _, err := filecache.Get("different-kind", key); err != filecache.ErrNotFound { +- t.Errorf("Get with wrong kind returned err=%q, want not found", err) - } -} - --func TestWorkdir_WriteFile(t *testing.T) { -- wd, events, cleanup := newWorkdir(t, sharedData) -- defer cleanup() -- ctx := context.Background() -- -- tests := []struct { -- path string -- wantType protocol.FileChangeType -- }{ -- {"data.txt", protocol.Created}, -- {"nested/README.md", protocol.Changed}, +-// TestConcurrency exercises concurrent access to the same entry. +-func TestConcurrency(t *testing.T) { +- if os.Getenv("GO_BUILDER_NAME") == "plan9-arm" { +- t.Skip(`skipping on plan9-arm builder due to golang/go#58748: failing with 'mount rpc error'`) - } +- const kind = "TestConcurrency" +- key := uniqueKey() +- const N = 100 // concurrency level - -- for _, test := range tests { -- if err := wd.WriteFile(ctx, test.path, "42"); err != nil { -- t.Fatal(err) -- } -- es := events.take() -- if got := len(es); got != 1 { -- t.Fatalf("len(events) = %d, want 1", got) -- } -- path := wd.URIToPath(es[0].URI) -- if path != test.path { -- t.Errorf("event path = %q, want %q", path, test.path) -- } -- if es[0].Type != test.wantType { -- t.Errorf("event type = %v, want %v", es[0].Type, test.wantType) +- // Construct N distinct values, each larger +- // than a typical 4KB OS file buffer page. +- var values [N][8192]byte +- for i := range values { +- if _, err := mathrand.Read(values[i][:]); err != nil { +- t.Fatalf("rand: %v", err) - } -- got, err := wd.ReadFile(test.path) +- } +- +- // get calls Get and verifies that the cache entry +- // matches one of the values passed to Set. +- get := func(mustBeFound bool) error { +- got, err := filecache.Get(kind, key) - if err != nil { -- t.Fatal(err) +- if err == filecache.ErrNotFound && !mustBeFound { +- return nil // not found +- } +- return err - } -- want := "42" -- if got := string(got); got != want { -- t.Errorf("ws.ReadFile(%q) = %q, want %q", test.path, got, want) +- for _, want := range values { +- if bytes.Equal(want[:], got) { +- return nil // a match +- } - } +- return fmt.Errorf("Get returned a value that was never Set") - } --} - --// Test for file notifications following file operations. --func TestWorkdir_FileWatching(t *testing.T) { -- wd, events, cleanup := newWorkdir(t, "") -- defer cleanup() -- ctx := context.Background() -- -- must := func(err error) { -- if err != nil { -- t.Fatal(err) -- } +- // Perform N concurrent calls to Set and Get. +- // All sets must succeed. +- // All gets must return nothing, or one of the Set values; +- // there is no third possibility. +- var group errgroup.Group +- for i := range values { +- i := i +- group.Go(func() error { return filecache.Set(kind, key, values[i][:]) }) +- group.Go(func() error { return get(false) }) - } -- -- type changeMap map[string]protocol.FileChangeType -- checkEvent := func(wantChanges changeMap) { -- gotChanges := make(changeMap) -- for _, e := range events.take() { -- gotChanges[wd.URIToPath(e.URI)] = e.Type -- } -- if diff := cmp.Diff(wantChanges, gotChanges); diff != "" { -- t.Errorf("mismatching file events (-want +got):\n%s", diff) +- if err := group.Wait(); err != nil { +- if strings.Contains(err.Error(), "operation not supported") || +- strings.Contains(err.Error(), "not implemented") { +- t.Skipf("skipping: %v", err) - } +- t.Fatal(err) - } - -- must(wd.WriteFile(ctx, "foo.go", "package foo")) -- checkEvent(changeMap{"foo.go": protocol.Created}) +- // A final Get must report one of the values that was Set. +- if err := get(true); err != nil { +- t.Fatalf("final Get failed: %v", err) +- } +-} - -- must(wd.RenameFile(ctx, "foo.go", "bar.go")) -- checkEvent(changeMap{"foo.go": protocol.Deleted, "bar.go": protocol.Created}) +-const ( +- testIPCKind = "TestIPC" +- testIPCValueA = "hello" +- testIPCValueB = "world" +-) - -- must(wd.RemoveFile(ctx, "bar.go")) -- checkEvent(changeMap{"bar.go": protocol.Deleted}) --} +-// TestIPC exercises interprocess communication through the cache. +-// It calls Set(A) in the parent, { Get(A); Set(B) } in the child +-// process, then Get(B) in the parent. +-func TestIPC(t *testing.T) { +- testenv.NeedsExec(t) - --func TestWorkdir_CheckForFileChanges(t *testing.T) { -- t.Skip("broken on darwin-amd64-10_12") -- wd, events, cleanup := newWorkdir(t, sharedData) -- defer cleanup() -- ctx := context.Background() +- keyA := uniqueKey() +- keyB := uniqueKey() +- value := []byte(testIPCValueA) - -- checkChange := func(wantPath string, wantType protocol.FileChangeType) { -- if err := wd.CheckForFileChanges(ctx); err != nil { -- t.Fatal(err) -- } -- ev := events.take() -- if len(ev) == 0 { -- t.Fatal("no file events received") -- } -- gotEvt := ev[0] -- gotPath := wd.URIToPath(gotEvt.URI) -- // Only check relative path and Type -- if gotPath != wantPath || gotEvt.Type != wantType { -- t.Errorf("file events: got %v, want {Path: %s, Type: %v}", gotEvt, wantPath, wantType) +- // Set keyA. +- if err := filecache.Set(testIPCKind, keyA, value); err != nil { +- if strings.Contains(err.Error(), "operation not supported") { +- t.Skipf("skipping: %v", err) - } +- t.Fatalf("Set: %v", err) - } -- // Sleep some positive amount of time to ensure a distinct mtime. -- if err := writeFileData("go.mod", []byte("module foo.test\n"), wd.RelativeTo); err != nil { +- +- // Call ipcChild in a child process, +- // passing it the keys in the environment +- // (quoted, to avoid NUL termination of C strings). +- // It will Get(A) then Set(B). +- cmd := exec.Command(os.Args[0], os.Args[1:]...) +- cmd.Env = append(os.Environ(), +- "ENTRYPOINT=ipcChild", +- fmt.Sprintf("KEYA=%q", keyA), +- fmt.Sprintf("KEYB=%q", keyB)) +- cmd.Stdout = os.Stderr +- cmd.Stderr = os.Stderr +- if err := cmd.Run(); err != nil { - t.Fatal(err) - } -- checkChange("go.mod", protocol.Changed) -- if err := writeFileData("newFile", []byte("something"), wd.RelativeTo); err != nil { +- +- // Verify keyB. +- got, err := filecache.Get(testIPCKind, keyB) +- if err != nil { - t.Fatal(err) - } -- checkChange("newFile", protocol.Created) -- fp := wd.AbsPath("newFile") -- if err := os.Remove(fp); err != nil { -- t.Fatal(err) +- if string(got) != "world" { +- t.Fatalf("Get(keyB) = %q, want %q", got, "world") - } -- checkChange("newFile", protocol.Deleted) -} - --func TestSplitModuleVersionPath(t *testing.T) { -- tests := []struct { -- path string -- wantModule, wantVersion, wantSuffix string -- }{ -- {"foo.com@v1.2.3/bar", "foo.com", "v1.2.3", "bar"}, -- {"foo.com/module@v1.2.3/bar", "foo.com/module", "v1.2.3", "bar"}, -- {"foo.com@v1.2.3", "foo.com", "v1.2.3", ""}, -- {"std@v1.14.0", "std", "v1.14.0", ""}, -- {"another/module/path", "another/module/path", "", ""}, +-// We define our own main function so that portions of +-// some tests can run in a separate (child) process. +-func TestMain(m *testing.M) { +- switch os.Getenv("ENTRYPOINT") { +- case "ipcChild": +- ipcChild() +- default: +- os.Exit(m.Run()) - } +-} - -- for _, test := range tests { -- module, version, suffix := splitModuleVersionPath(test.path) -- if module != test.wantModule || version != test.wantVersion || suffix != test.wantSuffix { -- t.Errorf("splitModuleVersionPath(%q) =\n\t(%q, %q, %q)\nwant\n\t(%q, %q, %q)", -- test.path, module, version, suffix, test.wantModule, test.wantVersion, test.wantSuffix) +-// ipcChild is the portion of TestIPC that runs in a child process. +-func ipcChild() { +- getenv := func(name string) (key [32]byte) { +- s, _ := strconv.Unquote(os.Getenv(name)) +- copy(key[:], []byte(s)) +- return +- } +- +- // Verify key A. +- got, err := filecache.Get(testIPCKind, getenv("KEYA")) +- if err != nil || string(got) != testIPCValueA { +- log.Fatalf("child: Get(key) = %q, %v; want %q", got, err, testIPCValueA) +- } +- +- // Set key B. +- if err := filecache.Set(testIPCKind, getenv("KEYB"), []byte(testIPCValueB)); err != nil { +- log.Fatalf("child: Set(keyB) failed: %v", err) +- } +-} +- +-// uniqueKey returns a key that has never been used before. +-func uniqueKey() (key [32]byte) { +- if _, err := cryptorand.Read(key[:]); err != nil { +- log.Fatalf("rand: %v", err) +- } +- return +-} +- +-func BenchmarkUncontendedGet(b *testing.B) { +- const kind = "BenchmarkUncontendedGet" +- key := uniqueKey() +- +- var value [8192]byte +- if _, err := mathrand.Read(value[:]); err != nil { +- b.Fatalf("rand: %v", err) +- } +- if err := filecache.Set(kind, key, value[:]); err != nil { +- b.Fatal(err) +- } +- b.ResetTimer() +- b.SetBytes(int64(len(value))) +- +- var group errgroup.Group +- group.SetLimit(50) +- for i := 0; i < b.N; i++ { +- group.Go(func() error { +- _, err := filecache.Get(kind, key) +- return err +- }) +- } +- if err := group.Wait(); err != nil { +- b.Fatal(err) +- } +-} +- +-// These two benchmarks are asymmetric: the one for Get imposes a +-// modest bound on concurrency (50) whereas the one for Set imposes a +-// much higher concurrency (1000) to test the implementation's +-// self-imposed bound. +- +-func BenchmarkUncontendedSet(b *testing.B) { +- const kind = "BenchmarkUncontendedSet" +- key := uniqueKey() +- var value [8192]byte +- +- const P = 1000 // parallelism +- b.SetBytes(P * int64(len(value))) +- +- for i := 0; i < b.N; i++ { +- // Perform P concurrent calls to Set. All must succeed. +- var group errgroup.Group +- for range [P]bool{} { +- group.Go(func() error { +- return filecache.Set(kind, key, value[:]) +- }) +- } +- if err := group.Wait(); err != nil { +- if strings.Contains(err.Error(), "operation not supported") || +- strings.Contains(err.Error(), "not implemented") { +- b.Skipf("skipping: %v", err) +- } +- b.Fatal(err) - } - } -} -diff -urN a/gopls/internal/lsp/fake/workdir_windows.go b/gopls/internal/lsp/fake/workdir_windows.go ---- a/gopls/internal/lsp/fake/workdir_windows.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/fake/workdir_windows.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,21 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/golang/add_import.go b/gopls/internal/golang/add_import.go +--- a/gopls/internal/golang/add_import.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/add_import.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,29 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package fake +-package golang - -import ( -- "errors" -- "syscall" --) +- "context" - --func init() { -- // constants copied from GOROOT/src/internal/syscall/windows/syscall_windows.go -- const ( -- ERROR_LOCK_VIOLATION syscall.Errno = 33 -- ) +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/imports" +-) - -- isWindowsErrLockViolation = func(err error) bool { -- return errors.Is(err, ERROR_LOCK_VIOLATION) +-// AddImport adds a single import statement to the given file +-func AddImport(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, importPath string) ([]protocol.TextEdit, error) { +- pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) +- if err != nil { +- return nil, err - } +- return ComputeOneImportFixEdits(snapshot, pgf, &imports.ImportFix{ +- StmtInfo: imports.ImportInfo{ +- ImportPath: importPath, +- }, +- FixType: imports.AddImport, +- }) -} -diff -urN a/gopls/internal/lsp/filecache/filecache.go b/gopls/internal/lsp/filecache/filecache.go ---- a/gopls/internal/lsp/filecache/filecache.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/filecache/filecache.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,608 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/golang/call_hierarchy.go b/gopls/internal/golang/call_hierarchy.go +--- a/gopls/internal/golang/call_hierarchy.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/call_hierarchy.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,313 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --// The filecache package provides a file-based shared durable blob cache. --// --// The cache is a machine-global mapping from (kind string, key --// [32]byte) to []byte, where kind is an identifier describing the --// namespace or purpose (e.g. "analysis"), and key is a SHA-256 digest --// of the recipe of the value. (It need not be the digest of the value --// itself, so you can query the cache without knowing what value the --// recipe would produce.) --// --// The space budget of the cache can be controlled by [SetBudget]. --// Cache entries may be evicted at any time or in any order. --// Note that "du -sh $GOPLSCACHE" may report a disk usage --// figure that is rather larger (e.g. 50%) than the budget because --// it rounds up partial disk blocks. --// --// The Get and Set operations are concurrency-safe. --package filecache +-package golang - -import ( -- "bytes" -- "crypto/sha256" -- "encoding/hex" -- "encoding/json" +- "context" - "errors" - "fmt" -- "io" -- "io/fs" -- "log" -- "os" +- "go/ast" +- "go/token" +- "go/types" - "path/filepath" -- "sort" -- "strings" -- "sync" -- "sync/atomic" -- "time" - -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/lru" +- "golang.org/x/tools/go/ast/astutil" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/tag" -) - --// Start causes the filecache to initialize and start garbage gollection. --// --// Start is automatically called by the first call to Get, but may be called --// explicitly to pre-initialize the cache. --func Start() { -- go getCacheDir() --} -- --// As an optimization, use a 100MB in-memory LRU cache in front of filecache --// operations. This reduces I/O for operations such as diagnostics or --// implementations that repeatedly access the same cache entries. --var memCache = lru.New(100 * 1e6) -- --type memKey struct { -- kind string -- key [32]byte --} -- --// Get retrieves from the cache and returns the value most recently --// supplied to Set(kind, key), possibly by another process. --// Get returns ErrNotFound if the value was not found. --// --// Callers should not modify the returned array. --func Get(kind string, key [32]byte) ([]byte, error) { -- // First consult the read-through memory cache. -- // Note that memory cache hits do not update the times -- // used for LRU eviction of the file-based cache. -- if value := memCache.Get(memKey{kind, key}); value != nil { -- return value.([]byte), nil -- } -- -- iolimit <- struct{}{} // acquire a token -- defer func() { <-iolimit }() // release a token +-// PrepareCallHierarchy returns an array of CallHierarchyItem for a file and the position within the file. +-func PrepareCallHierarchy(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp protocol.Position) ([]protocol.CallHierarchyItem, error) { +- ctx, done := event.Start(ctx, "golang.PrepareCallHierarchy") +- defer done() - -- // Read the index file, which provides the name of the CAS file. -- indexName, err := filename(kind, key) +- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) - if err != nil { - return nil, err - } -- indexData, err := os.ReadFile(indexName) +- pos, err := pgf.PositionPos(pp) - if err != nil { -- if errors.Is(err, os.ErrNotExist) { -- return nil, ErrNotFound -- } - return nil, err - } -- var valueHash [32]byte -- if copy(valueHash[:], indexData) != len(valueHash) { -- return nil, ErrNotFound // index entry has wrong length +- +- _, obj, _ := referencedObject(pkg, pgf, pos) +- if obj == nil { +- return nil, nil - } - -- // Read the CAS file and check its contents match. -- // -- // This ensures integrity in all cases (corrupt or truncated -- // file, short read, I/O error, wrong length, etc) except an -- // engineered hash collision, which is infeasible. -- casName, err := filename(casKind, valueHash) +- if _, ok := obj.Type().Underlying().(*types.Signature); !ok { +- return nil, nil +- } +- +- declLoc, err := mapPosition(ctx, pkg.FileSet(), snapshot, obj.Pos(), adjustedObjEnd(obj)) - if err != nil { - return nil, err - } -- value, _ := os.ReadFile(casName) // ignore error -- if sha256.Sum256(value) != valueHash { -- return nil, ErrNotFound // CAS file is missing or has wrong contents -- } +- rng := declLoc.Range - -- // Update file times used by LRU eviction. -- // -- // Because this turns a read into a write operation, -- // we follow the approach used in the go command's -- // cache and update the access time only if the -- // existing timestamp is older than one hour. -- // -- // (Traditionally the access time would be updated -- // automatically, but for efficiency most POSIX systems have -- // for many years set the noatime mount option to avoid every -- // open or read operation entailing a metadata write.) -- now := time.Now() -- touch := func(filename string) { -- st, err := os.Stat(filename) -- if err == nil && now.Sub(st.ModTime()) > time.Hour { -- os.Chtimes(filename, now, now) // ignore error -- } +- callHierarchyItem := protocol.CallHierarchyItem{ +- Name: obj.Name(), +- Kind: protocol.Function, +- Tags: []protocol.SymbolTag{}, +- Detail: fmt.Sprintf("%s • %s", obj.Pkg().Path(), filepath.Base(declLoc.URI.Path())), +- URI: declLoc.URI, +- Range: rng, +- SelectionRange: rng, - } -- touch(indexName) -- touch(casName) -- -- memCache.Set(memKey{kind, key}, value, len(value)) -- -- return value, nil +- return []protocol.CallHierarchyItem{callHierarchyItem}, nil -} - --// ErrNotFound is the distinguished error --// returned by Get when the key is not found. --var ErrNotFound = fmt.Errorf("not found") -- --// Set updates the value in the cache. --func Set(kind string, key [32]byte, value []byte) error { -- memCache.Set(memKey{kind, key}, value, len(value)) -- -- // Set the active event to wake up the GC. -- select { -- case active <- struct{}{}: -- default: -- } -- -- iolimit <- struct{}{} // acquire a token -- defer func() { <-iolimit }() // release a token +-// IncomingCalls returns an array of CallHierarchyIncomingCall for a file and the position within the file. +-func IncomingCalls(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pos protocol.Position) ([]protocol.CallHierarchyIncomingCall, error) { +- ctx, done := event.Start(ctx, "golang.IncomingCalls") +- defer done() - -- // First, add the value to the content- -- // addressable store (CAS), if not present. -- hash := sha256.Sum256(value) -- casName, err := filename(casKind, hash) +- refs, err := references(ctx, snapshot, fh, pos, false) - if err != nil { -- return err +- if errors.Is(err, ErrNoIdentFound) || errors.Is(err, errNoObjectFound) { +- return nil, nil +- } +- return nil, err - } -- // Does CAS file exist and have correct (complete) content? -- // TODO(adonovan): opt: use mmap for this check. -- if prev, _ := os.ReadFile(casName); !bytes.Equal(prev, value) { -- if err := os.MkdirAll(filepath.Dir(casName), 0700); err != nil { -- return err +- +- // Group references by their enclosing function declaration. +- incomingCalls := make(map[protocol.Location]*protocol.CallHierarchyIncomingCall) +- for _, ref := range refs { +- callItem, err := enclosingNodeCallItem(ctx, snapshot, ref.pkgPath, ref.location) +- if err != nil { +- event.Error(ctx, "error getting enclosing node", err, tag.Method.Of(string(ref.pkgPath))) +- continue - } -- // Avoiding O_TRUNC here is merely an optimization to avoid -- // cache misses when two threads race to write the same file. -- if err := writeFileNoTrunc(casName, value, 0600); err != nil { -- os.Remove(casName) // ignore error -- return err // e.g. disk full +- loc := protocol.Location{ +- URI: callItem.URI, +- Range: callItem.Range, +- } +- call, ok := incomingCalls[loc] +- if !ok { +- call = &protocol.CallHierarchyIncomingCall{From: callItem} +- incomingCalls[loc] = call - } +- call.FromRanges = append(call.FromRanges, ref.location.Range) - } - -- // Now write an index entry that refers to the CAS file. -- indexName, err := filename(kind, key) -- if err != nil { -- return err -- } -- if err := os.MkdirAll(filepath.Dir(indexName), 0700); err != nil { -- return err -- } -- if err := writeFileNoTrunc(indexName, hash[:], 0600); err != nil { -- os.Remove(indexName) // ignore error -- return err // e.g. disk full +- // Flatten the map of pointers into a slice of values. +- incomingCallItems := make([]protocol.CallHierarchyIncomingCall, 0, len(incomingCalls)) +- for _, callItem := range incomingCalls { +- incomingCallItems = append(incomingCallItems, *callItem) - } -- -- return nil +- return incomingCallItems, nil -} - --// The active 1-channel is a selectable resettable event --// indicating recent cache activity. --var active = make(chan struct{}, 1) -- --// writeFileNoTrunc is like os.WriteFile but doesn't truncate until --// after the write, so that racing writes of the same data are idempotent. --func writeFileNoTrunc(filename string, data []byte, perm os.FileMode) error { -- f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE, perm) +-// enclosingNodeCallItem creates a CallHierarchyItem representing the function call at loc. +-func enclosingNodeCallItem(ctx context.Context, snapshot *cache.Snapshot, pkgPath PackagePath, loc protocol.Location) (protocol.CallHierarchyItem, error) { +- // Parse the file containing the reference. +- fh, err := snapshot.ReadFile(ctx, loc.URI) - if err != nil { -- return err +- return protocol.CallHierarchyItem{}, err - } -- _, err = f.Write(data) -- if err == nil { -- err = f.Truncate(int64(len(data))) +- // TODO(adonovan): opt: before parsing, trim the bodies of functions +- // that don't contain the reference, using either a scanner-based +- // implementation such as https://go.dev/play/p/KUrObH1YkX8 +- // (~31% speedup), or a byte-oriented implementation (2x speedup). +- pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) +- if err != nil { +- return protocol.CallHierarchyItem{}, err - } -- if closeErr := f.Close(); err == nil { -- err = closeErr +- start, end, err := pgf.RangePos(loc.Range) +- if err != nil { +- return protocol.CallHierarchyItem{}, err - } -- return err --} -- --// reserved kind strings --const ( -- casKind = "cas" // content-addressable store files -- bugKind = "bug" // gopls bug reports --) - --var iolimit = make(chan struct{}, 128) // counting semaphore to limit I/O concurrency in Set. +- // Find the enclosing function, if any, and the number of func literals in between. +- var funcDecl *ast.FuncDecl +- var funcLit *ast.FuncLit // innermost function literal +- var litCount int +- path, _ := astutil.PathEnclosingInterval(pgf.File, start, end) +-outer: +- for _, node := range path { +- switch n := node.(type) { +- case *ast.FuncDecl: +- funcDecl = n +- break outer +- case *ast.FuncLit: +- litCount++ +- if litCount > 1 { +- continue +- } +- funcLit = n +- } +- } - --var budget int64 = 1e9 // 1GB +- nameIdent := path[len(path)-1].(*ast.File).Name +- kind := protocol.Package +- if funcDecl != nil { +- nameIdent = funcDecl.Name +- kind = protocol.Function +- } - --// SetBudget sets a soft limit on disk usage of regular files in the --// cache (in bytes) and returns the previous value. Supplying a --// negative value queries the current value without changing it. --// --// If two gopls processes have different budgets, the one with the --// lower budget will collect garbage more actively, but both will --// observe the effect. --// --// Even in the steady state, the storage usage reported by the 'du' --// command may exceed the budget by as much as a factor of 3 due to --// the overheads of directories and the effects of block quantization, --// which are especially pronounced for the small index files. --func SetBudget(new int64) (old int64) { -- if new < 0 { -- return atomic.LoadInt64(&budget) +- nameStart, nameEnd := nameIdent.Pos(), nameIdent.End() +- if funcLit != nil { +- nameStart, nameEnd = funcLit.Type.Func, funcLit.Type.Params.Pos() +- kind = protocol.Function - } -- return atomic.SwapInt64(&budget, new) --} -- --// --- implementation ---- -- --// filename returns the name of the cache file of the specified kind and key. --// --// A typical cache file has a name such as: --// --// $HOME/Library/Caches / gopls / VVVVVVVV / KK / KKKK...KKKK - kind --// --// The portions separated by spaces are as follows: --// - The user's preferred cache directory; the default value varies by OS. --// - The constant "gopls". --// - The "version", 32 bits of the digest of the gopls executable. --// - The first 8 bits of the key, to avoid huge directories. --// - The full 256 bits of the key. --// - The kind or purpose of this cache file (e.g. "analysis"). --// --// The kind establishes a namespace for the keys. It is represented as --// a suffix, not a segment, as this significantly reduces the number --// of directories created, and thus the storage overhead. --// --// Previous iterations of the design aimed for the invariant that once --// a file is written, its contents are never modified, though it may --// be atomically replaced or removed. However, not all platforms have --// an atomic rename operation (our first approach), and file locking --// (our second) is a notoriously fickle mechanism. --// --// The current design instead exploits a trick from the cache --// implementation used by the go command: writes of small files are in --// practice atomic (all or nothing) on all platforms. --// (See GOROOT/src/cmd/go/internal/cache/cache.go.) --// --// Russ Cox notes: "all file systems use an rwlock around every file --// system block, including data blocks, so any writes or reads within --// the same block are going to be handled atomically by the FS --// implementation without any need to request file locking explicitly. --// And since the files are so small, there's only one block. (A block --// is at minimum 512 bytes, usually much more.)" And: "all modern file --// systems protect against [partial writes due to power loss] with --// journals." --// --// We use a two-level scheme consisting of an index and a --// content-addressable store (CAS). A single cache entry consists of --// two files. The value of a cache entry is written into the file at --// filename("cas", sha256(value)). Since the value may be arbitrarily --// large, this write is not atomic. That means we must check the --// integrity of the contents read back from the CAS to make sure they --// hash to the expected key. If the CAS file is incomplete or --// inconsistent, we proceed as if it were missing. --// --// Once the CAS file has been written, we write a small fixed-size --// index file at filename(kind, key), using the values supplied by the --// caller. The index file contains the hash that identifies the value --// file in the CAS. (We could add extra metadata to this file, up to --// 512B, the minimum size of a disk block, if later desired, so long --// as the total size remains fixed.) Because the index file is small, --// concurrent writes to it are atomic in practice, even though this is --// not guaranteed by any OS. The fixed size ensures that readers can't --// see a palimpsest when a short new file overwrites a longer old one. --// --// New versions of gopls are free to reorganize the contents of the --// version directory as needs evolve. But all versions of gopls must --// in perpetuity treat the "gopls" directory in a common fashion. --// --// In particular, each gopls process attempts to garbage collect --// the entire gopls directory so that newer binaries can clean up --// after older ones: in the development cycle especially, new --// versions may be created frequently. --func filename(kind string, key [32]byte) (string, error) { -- base := fmt.Sprintf("%x-%s", key, kind) -- dir, err := getCacheDir() +- rng, err := pgf.PosRange(nameStart, nameEnd) - if err != nil { -- return "", err +- return protocol.CallHierarchyItem{}, err - } -- // Keep the BugReports function consistent with this one. -- return filepath.Join(dir, base[:2], base), nil --} -- --// getCacheDir returns the persistent cache directory of all processes --// running this version of the gopls executable. --// --// It must incorporate the hash of the executable so that we needn't --// worry about incompatible changes to the file format or changes to --// the algorithm that produced the index. --func getCacheDir() (string, error) { -- cacheDirOnce.Do(func() { -- // Use user's preferred cache directory. -- userDir := os.Getenv("GOPLSCACHE") -- if userDir == "" { -- var err error -- userDir, err = os.UserCacheDir() -- if err != nil { -- userDir = os.TempDir() -- } -- } -- goplsDir := filepath.Join(userDir, "gopls") -- -- // UserCacheDir may return a nonexistent directory -- // (in which case we must create it, which may fail), -- // or it may return a non-writable directory, in -- // which case we should ideally respect the user's express -- // wishes (e.g. XDG_CACHE_HOME) and not write somewhere else. -- // Sadly UserCacheDir doesn't currently let us distinguish -- // such intent from accidental misconfiguraton such as HOME=/ -- // in a CI builder. So, we check whether the gopls subdirectory -- // can be created (or already exists) and not fall back to /tmp. -- // See also https://github.com/golang/go/issues/57638. -- if os.MkdirAll(goplsDir, 0700) != nil { -- goplsDir = filepath.Join(os.TempDir(), "gopls") -- } - -- // Start the garbage collector. -- go gc(goplsDir) +- name := nameIdent.Name +- for i := 0; i < litCount; i++ { +- name += ".func()" +- } - -- // Compute the hash of this executable (~20ms) and create a subdirectory. -- hash, err := hashExecutable() -- if err != nil { -- cacheDirErr = fmt.Errorf("can't hash gopls executable: %v", err) -- } -- // Use only 32 bits of the digest to avoid unwieldy filenames. -- // It's not an adversarial situation. -- cacheDir = filepath.Join(goplsDir, fmt.Sprintf("%x", hash[:4])) -- if err := os.MkdirAll(cacheDir, 0700); err != nil { -- cacheDirErr = fmt.Errorf("can't create cache: %v", err) -- } -- }) -- return cacheDir, cacheDirErr +- return protocol.CallHierarchyItem{ +- Name: name, +- Kind: kind, +- Tags: []protocol.SymbolTag{}, +- Detail: fmt.Sprintf("%s • %s", pkgPath, filepath.Base(fh.URI().Path())), +- URI: loc.URI, +- Range: rng, +- SelectionRange: rng, +- }, nil -} - --var ( -- cacheDirOnce sync.Once -- cacheDir string -- cacheDirErr error --) +-// OutgoingCalls returns an array of CallHierarchyOutgoingCall for a file and the position within the file. +-func OutgoingCalls(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp protocol.Position) ([]protocol.CallHierarchyOutgoingCall, error) { +- ctx, done := event.Start(ctx, "golang.OutgoingCalls") +- defer done() - --func hashExecutable() (hash [32]byte, err error) { -- exe, err := os.Executable() +- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) - if err != nil { -- return hash, err +- return nil, err - } -- f, err := os.Open(exe) +- pos, err := pgf.PositionPos(pp) - if err != nil { -- return hash, err +- return nil, err - } -- defer f.Close() -- h := sha256.New() -- if _, err := io.Copy(h, f); err != nil { -- return hash, fmt.Errorf("can't read executable: %w", err) +- +- _, obj, _ := referencedObject(pkg, pgf, pos) +- if obj == nil { +- return nil, nil - } -- h.Sum(hash[:0]) -- return hash, nil --} - --// gc runs forever, periodically deleting files from the gopls --// directory until the space budget is no longer exceeded, and also --// deleting files older than the maximum age, regardless of budget. --// --// One gopls process may delete garbage created by a different gopls --// process, possibly running a different version of gopls, possibly --// running concurrently. --func gc(goplsDir string) { -- // period between collections -- // -- // Originally the period was always 1 minute, but this -- // consumed 15% of a CPU core when idle (#61049). -- // -- // The reason for running collections even when idle is so -- // that long lived gopls sessions eventually clean up the -- // caches created by defunct executables. -- const ( -- minPeriod = 5 * time.Minute // when active -- maxPeriod = 6 * time.Hour // when idle -- ) +- if _, ok := obj.Type().Underlying().(*types.Signature); !ok { +- return nil, nil +- } - -- // Sleep statDelay*batchSize between stats to smooth out I/O. -- // -- // The constants below were chosen using the following heuristics: -- // - 1GB of filecache is on the order of ~100-200k files, in which case -- // 100μs delay per file introduces 10-20s of additional walk time, -- // less than the minPeriod. -- // - Processing batches of stats at once is much more efficient than -- // sleeping after every stat (due to OS optimizations). -- const statDelay = 100 * time.Microsecond // average delay between stats, to smooth out I/O -- const batchSize = 1000 // # of stats to process before sleeping -- const maxAge = 5 * 24 * time.Hour // max time since last access before file is deleted +- // Skip builtins. +- if obj.Pkg() == nil { +- return nil, nil +- } - -- // The macOS filesystem is strikingly slow, at least on some machines. -- // /usr/bin/find achieves only about 25,000 stats per second -- // at full speed (no pause between items), meaning a large -- // cache may take several minutes to scan. -- // We must ensure that short-lived processes (crucially, -- // tests) are able to make progress sweeping garbage. -- // -- // (gopls' caches should never actually get this big in -- // practice: the example mentioned above resulted from a bug -- // that caused filecache to fail to delete any files.) +- if !obj.Pos().IsValid() { +- return nil, bug.Errorf("internal error: object %s.%s missing position", obj.Pkg().Path(), obj.Name()) +- } - -- const debug = false +- declFile := pkg.FileSet().File(obj.Pos()) +- if declFile == nil { +- return nil, bug.Errorf("file not found for %d", obj.Pos()) +- } - -- // Names of all directories found in first pass; nil thereafter. -- dirs := make(map[string]bool) +- uri := protocol.URIFromPath(declFile.Name()) +- offset, err := safetoken.Offset(declFile, obj.Pos()) +- if err != nil { +- return nil, err +- } - -- for { -- // Enumerate all files in the cache. -- type item struct { -- path string -- mtime time.Time -- size int64 -- } -- var files []item -- start := time.Now() -- var total int64 // bytes -- _ = filepath.Walk(goplsDir, func(path string, stat os.FileInfo, err error) error { -- if err != nil { -- return nil // ignore errors -- } -- if stat.IsDir() { -- // Collect (potentially empty) directories. -- if dirs != nil { -- dirs[path] = true -- } -- } else { -- // Unconditionally delete files we haven't used in ages. -- // (We do this here, not in the second loop, so that we -- // perform age-based collection even in short-lived processes.) -- age := time.Since(stat.ModTime()) -- if age > maxAge { -- if debug { -- log.Printf("age: deleting stale file %s (%dB, age %v)", -- path, stat.Size(), age) -- } -- os.Remove(path) // ignore error -- } else { -- files = append(files, item{path, stat.ModTime(), stat.Size()}) -- total += stat.Size() -- if debug && len(files)%1000 == 0 { -- log.Printf("filecache: checked %d files in %v", len(files), time.Since(start)) -- } -- if len(files)%batchSize == 0 { -- time.Sleep(batchSize * statDelay) -- } -- } -- } -- return nil -- }) +- // Use TypecheckFull as we want to inspect the body of the function declaration. +- declPkg, declPGF, err := NarrowestPackageForFile(ctx, snapshot, uri) +- if err != nil { +- return nil, err +- } - -- // Sort oldest files first. -- sort.Slice(files, func(i, j int) bool { -- return files[i].mtime.Before(files[j].mtime) -- }) +- declPos, err := safetoken.Pos(declPGF.Tok, offset) +- if err != nil { +- return nil, err +- } - -- // Delete oldest files until we're under budget. -- budget := atomic.LoadInt64(&budget) -- for _, file := range files { -- if total < budget { -- break -- } -- if debug { -- age := time.Since(file.mtime) -- log.Printf("budget: deleting stale file %s (%dB, age %v)", -- file.path, file.size, age) +- declNode, _, _ := findDeclInfo([]*ast.File{declPGF.File}, declPos) +- if declNode == nil { +- // TODO(rfindley): why don't we return an error here, or even bug.Errorf? +- return nil, nil +- // return nil, bug.Errorf("failed to find declaration for object %s.%s", obj.Pkg().Path(), obj.Name()) +- } +- +- type callRange struct { +- start, end token.Pos +- } +- callRanges := []callRange{} +- ast.Inspect(declNode, func(n ast.Node) bool { +- if call, ok := n.(*ast.CallExpr); ok { +- var start, end token.Pos +- switch n := call.Fun.(type) { +- case *ast.SelectorExpr: +- start, end = n.Sel.NamePos, call.Lparen +- case *ast.Ident: +- start, end = n.NamePos, call.Lparen +- case *ast.FuncLit: +- // while we don't add the function literal as an 'outgoing' call +- // we still want to traverse into it +- return true +- default: +- // ignore any other kind of call expressions +- // for ex: direct function literal calls since that's not an 'outgoing' call +- return false - } -- os.Remove(file.path) // ignore error -- total -= file.size +- callRanges = append(callRanges, callRange{start: start, end: end}) - } -- files = nil // release memory before sleep +- return true +- }) - -- // Wait unconditionally for the minimum period. -- time.Sleep(minPeriod) +- outgoingCalls := map[token.Pos]*protocol.CallHierarchyOutgoingCall{} +- for _, callRange := range callRanges { +- _, obj, _ := referencedObject(declPkg, declPGF, callRange.start) +- if obj == nil { +- continue +- } - -- // Once only, delete all directories. -- // This will succeed only for the empty ones, -- // and ensures that stale directories (whose -- // files have been deleted) are removed eventually. -- // They don't take up much space but they do slow -- // down the traversal. -- // -- // We do this after the sleep to minimize the -- // race against Set, which may create a directory -- // that is momentarily empty. -- // -- // (Test processes don't live that long, so -- // this may not be reached on the CI builders.) -- if dirs != nil { -- dirnames := make([]string, 0, len(dirs)) -- for dir := range dirs { -- dirnames = append(dirnames, dir) -- } -- dirs = nil +- // ignore calls to builtin functions +- if obj.Pkg() == nil { +- continue +- } - -- // Descending length order => children before parents. -- sort.Slice(dirnames, func(i, j int) bool { -- return len(dirnames[i]) > len(dirnames[j]) -- }) -- var deleted int -- for _, dir := range dirnames { -- if os.Remove(dir) == nil { // ignore error -- deleted++ -- } +- outgoingCall, ok := outgoingCalls[obj.Pos()] +- if !ok { +- loc, err := mapPosition(ctx, declPkg.FileSet(), snapshot, obj.Pos(), obj.Pos()+token.Pos(len(obj.Name()))) +- if err != nil { +- return nil, err - } -- if debug { -- log.Printf("deleted %d empty directories", deleted) +- outgoingCall = &protocol.CallHierarchyOutgoingCall{ +- To: protocol.CallHierarchyItem{ +- Name: obj.Name(), +- Kind: protocol.Function, +- Tags: []protocol.SymbolTag{}, +- Detail: fmt.Sprintf("%s • %s", obj.Pkg().Path(), filepath.Base(loc.URI.Path())), +- URI: loc.URI, +- Range: loc.Range, +- SelectionRange: loc.Range, +- }, - } +- outgoingCalls[obj.Pos()] = outgoingCall - } - -- // Wait up to the max period, -- // or for Set activity in this process. -- select { -- case <-active: -- case <-time.After(maxPeriod): -- } -- } --} -- --func init() { -- // Register a handler to durably record this process's first -- // assertion failure in the cache so that we can ask users to -- // share this information via the stats command. -- bug.Handle(func(bug bug.Bug) { -- // Wait for cache init (bugs in tests happen early). -- _, _ = getCacheDir() -- -- data, err := json.Marshal(bug) +- rng, err := declPGF.PosRange(callRange.start, callRange.end) - if err != nil { -- panic(fmt.Sprintf("error marshalling bug %+v: %v", bug, err)) +- return nil, err - } +- outgoingCall.FromRanges = append(outgoingCall.FromRanges, rng) +- } - -- key := sha256.Sum256(data) -- _ = Set(bugKind, key, data) -- }) --} -- --// BugReports returns a new unordered array of the contents --// of all cached bug reports produced by this executable. --// It also returns the location of the cache directory --// used by this process (or "" on initialization error). --func BugReports() (string, []bug.Bug) { -- // To test this logic, run: -- // $ TEST_GOPLS_BUG=oops gopls bug # trigger a bug -- // $ gopls stats # list the bugs -- -- dir, err := getCacheDir() -- if err != nil { -- return "", nil // ignore initialization errors +- outgoingCallItems := make([]protocol.CallHierarchyOutgoingCall, 0, len(outgoingCalls)) +- for _, callItem := range outgoingCalls { +- outgoingCallItems = append(outgoingCallItems, *callItem) - } -- var result []bug.Bug -- _ = filepath.Walk(dir, func(path string, info fs.FileInfo, err error) error { -- if err != nil { -- return nil // ignore readdir/stat errors -- } -- // Parse the key from each "XXXX-bug" cache file name. -- if !info.IsDir() && strings.HasSuffix(path, bugKind) { -- var key [32]byte -- n, err := hex.Decode(key[:], []byte(filepath.Base(path)[:len(key)*2])) -- if err != nil || n != len(key) { -- return nil // ignore malformed file names -- } -- content, err := Get(bugKind, key) -- if err == nil { // ignore read errors -- var b bug.Bug -- if err := json.Unmarshal(content, &b); err != nil { -- log.Printf("error marshalling bug %q: %v", string(content), err) -- } -- result = append(result, b) -- } -- } -- return nil -- }) -- return dir, result +- return outgoingCallItems, nil -} -diff -urN a/gopls/internal/lsp/filecache/filecache_test.go b/gopls/internal/lsp/filecache/filecache_test.go ---- a/gopls/internal/lsp/filecache/filecache_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/filecache/filecache_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,265 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/golang/change_quote.go b/gopls/internal/golang/change_quote.go +--- a/gopls/internal/golang/change_quote.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/change_quote.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,85 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package filecache_test -- --// This file defines tests of the API of the filecache package. --// --// Some properties (e.g. garbage collection) cannot be exercised --// through the API, so this test does not attempt to do so. +-package golang - -import ( -- "bytes" -- cryptorand "crypto/rand" -- "fmt" -- "log" -- mathrand "math/rand" -- "os" -- "os/exec" +- "go/ast" +- "go/token" - "strconv" - "strings" -- "testing" - -- "golang.org/x/sync/errgroup" -- "golang.org/x/tools/gopls/internal/lsp/filecache" -- "golang.org/x/tools/internal/testenv" +- "golang.org/x/tools/go/ast/astutil" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/internal/diff" -) - --func TestBasics(t *testing.T) { -- const kind = "TestBasics" -- key := uniqueKey() // never used before -- value := []byte("hello") +-// ConvertStringLiteral reports whether we can convert between raw and interpreted +-// string literals in the [start, end), along with a CodeAction containing the edits. +-// +-// Only the following conditions are true, the action in result is valid +-// - [start, end) is enclosed by a string literal +-// - if the string is interpreted string, need check whether the convert is allowed +-func ConvertStringLiteral(pgf *parsego.File, fh file.Handle, rng protocol.Range) (protocol.CodeAction, bool) { +- startPos, endPos, err := pgf.RangePos(rng) +- if err != nil { +- return protocol.CodeAction{}, false // e.g. invalid range +- } +- path, _ := astutil.PathEnclosingInterval(pgf.File, startPos, endPos) +- lit, ok := path[0].(*ast.BasicLit) +- if !ok || lit.Kind != token.STRING { +- return protocol.CodeAction{}, false +- } - -- // Get of a never-seen key returns not found. -- if _, err := filecache.Get(kind, key); err != filecache.ErrNotFound { -- if strings.Contains(err.Error(), "operation not supported") || -- strings.Contains(err.Error(), "not implemented") { -- t.Skipf("skipping: %v", err) -- } -- t.Errorf("Get of random key returned err=%q, want not found", err) +- str, err := strconv.Unquote(lit.Value) +- if err != nil { +- return protocol.CodeAction{}, false - } - -- // Set of a never-seen key and a small value succeeds. -- if err := filecache.Set(kind, key, value); err != nil { -- t.Errorf("Set failed: %v", err) +- interpreted := lit.Value[0] == '"' +- // Not all "..." strings can be represented as `...` strings. +- if interpreted && !strconv.CanBackquote(strings.ReplaceAll(str, "\n", "")) { +- return protocol.CodeAction{}, false - } - -- // Get of the key returns a copy of the value. -- if got, err := filecache.Get(kind, key); err != nil { -- t.Errorf("Get after Set failed: %v", err) -- } else if string(got) != string(value) { -- t.Errorf("Get after Set returned different value: got %q, want %q", got, value) -- } -- -- // The kind is effectively part of the key. -- if _, err := filecache.Get("different-kind", key); err != filecache.ErrNotFound { -- t.Errorf("Get with wrong kind returned err=%q, want not found", err) -- } --} -- --// TestConcurrency exercises concurrent access to the same entry. --func TestConcurrency(t *testing.T) { -- if os.Getenv("GO_BUILDER_NAME") == "plan9-arm" { -- t.Skip(`skipping on plan9-arm builder due to golang/go#58748: failing with 'mount rpc error'`) -- } -- const kind = "TestConcurrency" -- key := uniqueKey() -- const N = 100 // concurrency level -- -- // Construct N distinct values, each larger -- // than a typical 4KB OS file buffer page. -- var values [N][8192]byte -- for i := range values { -- if _, err := mathrand.Read(values[i][:]); err != nil { -- t.Fatalf("rand: %v", err) -- } -- } -- -- // get calls Get and verifies that the cache entry -- // matches one of the values passed to Set. -- get := func(mustBeFound bool) error { -- got, err := filecache.Get(kind, key) -- if err != nil { -- if err == filecache.ErrNotFound && !mustBeFound { -- return nil // not found -- } -- return err -- } -- for _, want := range values { -- if bytes.Equal(want[:], got) { -- return nil // a match -- } -- } -- return fmt.Errorf("Get returned a value that was never Set") +- var ( +- title string +- newText string +- ) +- if interpreted { +- title = "Convert to raw string literal" +- newText = "`" + str + "`" +- } else { +- title = "Convert to interpreted string literal" +- newText = strconv.Quote(str) - } - -- // Perform N concurrent calls to Set and Get. -- // All sets must succeed. -- // All gets must return nothing, or one of the Set values; -- // there is no third possibility. -- var group errgroup.Group -- for i := range values { -- i := i -- group.Go(func() error { return filecache.Set(kind, key, values[i][:]) }) -- group.Go(func() error { return get(false) }) +- start, end, err := safetoken.Offsets(pgf.Tok, lit.Pos(), lit.End()) +- if err != nil { +- bug.Reportf("failed to get string literal offset by token.Pos:%v", err) +- return protocol.CodeAction{}, false - } -- if err := group.Wait(); err != nil { -- if strings.Contains(err.Error(), "operation not supported") || -- strings.Contains(err.Error(), "not implemented") { -- t.Skipf("skipping: %v", err) -- } -- t.Fatal(err) +- edits := []diff.Edit{{ +- Start: start, +- End: end, +- New: newText, +- }} +- pedits, err := protocol.EditsFromDiffEdits(pgf.Mapper, edits) +- if err != nil { +- bug.Reportf("failed to convert diff.Edit to protocol.TextEdit:%v", err) +- return protocol.CodeAction{}, false - } - -- // A final Get must report one of the values that was Set. -- if err := get(true); err != nil { -- t.Fatalf("final Get failed: %v", err) -- } +- return protocol.CodeAction{ +- Title: title, +- Kind: protocol.RefactorRewrite, +- Edit: &protocol.WorkspaceEdit{ +- DocumentChanges: documentChanges(fh, pedits), +- }, +- }, true -} +diff -urN a/gopls/internal/golang/change_signature.go b/gopls/internal/golang/change_signature.go +--- a/gopls/internal/golang/change_signature.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/change_signature.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,616 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --const ( -- testIPCKind = "TestIPC" -- testIPCValueA = "hello" -- testIPCValueB = "world" --) +-package golang - --// TestIPC exercises interprocess communication through the cache. --// It calls Set(A) in the parent, { Get(A); Set(B) } in the child --// process, then Get(B) in the parent. --func TestIPC(t *testing.T) { -- testenv.NeedsExec(t) +-import ( +- "bytes" +- "context" +- "fmt" +- "go/ast" +- "go/format" +- "go/parser" +- "go/token" +- "go/types" +- "regexp" - -- keyA := uniqueKey() -- keyB := uniqueKey() -- value := []byte(testIPCValueA) +- "golang.org/x/tools/go/ast/astutil" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/imports" +- internalastutil "golang.org/x/tools/internal/astutil" +- "golang.org/x/tools/internal/diff" +- "golang.org/x/tools/internal/refactor/inline" +- "golang.org/x/tools/internal/tokeninternal" +- "golang.org/x/tools/internal/typesinternal" +- "golang.org/x/tools/internal/versions" +-) - -- // Set keyA. -- if err := filecache.Set(testIPCKind, keyA, value); err != nil { -- if strings.Contains(err.Error(), "operation not supported") { -- t.Skipf("skipping: %v", err) -- } -- t.Fatalf("Set: %v", err) +-// RemoveUnusedParameter computes a refactoring to remove the parameter +-// indicated by the given range, which must be contained within an unused +-// parameter name or field. +-// +-// This operation is a work in progress. Remaining TODO: +-// - Handle function assignment correctly. +-// - Improve the extra newlines in output. +-// - Stream type checking via ForEachPackage. +-// - Avoid unnecessary additional type checking. +-func RemoveUnusedParameter(ctx context.Context, fh file.Handle, rng protocol.Range, snapshot *cache.Snapshot) ([]protocol.DocumentChanges, error) { +- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) +- if err != nil { +- return nil, err - } - -- // Call ipcChild in a child process, -- // passing it the keys in the environment -- // (quoted, to avoid NUL termination of C strings). -- // It will Get(A) then Set(B). -- cmd := exec.Command(os.Args[0], os.Args[1:]...) -- cmd.Env = append(os.Environ(), -- "ENTRYPOINT=ipcChild", -- fmt.Sprintf("KEYA=%q", keyA), -- fmt.Sprintf("KEYB=%q", keyB)) -- cmd.Stdout = os.Stderr -- cmd.Stderr = os.Stderr -- if err := cmd.Run(); err != nil { -- t.Fatal(err) +- // Changes to our heuristics for whether we can remove a parameter must also +- // be reflected in the canRemoveParameter helper. +- if perrors, terrors := pkg.ParseErrors(), pkg.TypeErrors(); len(perrors) > 0 || len(terrors) > 0 { +- var sample string +- if len(perrors) > 0 { +- sample = perrors[0].Error() +- } else { +- sample = terrors[0].Error() +- } +- return nil, fmt.Errorf("can't change signatures for packages with parse or type errors: (e.g. %s)", sample) - } - -- // Verify keyB. -- got, err := filecache.Get(testIPCKind, keyB) +- info, err := FindParam(pgf, rng) - if err != nil { -- t.Fatal(err) +- return nil, err // e.g. invalid range - } -- if string(got) != "world" { -- t.Fatalf("Get(keyB) = %q, want %q", got, "world") +- if info.Field == nil { +- return nil, fmt.Errorf("failed to find field") - } --} - --// We define our own main function so that portions of --// some tests can run in a separate (child) process. --func TestMain(m *testing.M) { -- switch os.Getenv("ENTRYPOINT") { -- case "ipcChild": -- ipcChild() -- default: -- os.Exit(m.Run()) +- // Create the new declaration, which is a copy of the original decl with the +- // unnecessary parameter removed. +- newDecl := internalastutil.CloneNode(info.Decl) +- if info.Name != nil { +- names := remove(newDecl.Type.Params.List[info.FieldIndex].Names, info.NameIndex) +- newDecl.Type.Params.List[info.FieldIndex].Names = names +- } +- if len(newDecl.Type.Params.List[info.FieldIndex].Names) == 0 { +- // Unnamed, or final name was removed: in either case, remove the field. +- newDecl.Type.Params.List = remove(newDecl.Type.Params.List, info.FieldIndex) - } --} - --// ipcChild is the portion of TestIPC that runs in a child process. --func ipcChild() { -- getenv := func(name string) (key [32]byte) { -- s, _ := strconv.Unquote(os.Getenv(name)) -- copy(key[:], []byte(s)) -- return +- // Compute inputs into building a wrapper function around the modified +- // signature. +- var ( +- params = internalastutil.CloneNode(info.Decl.Type.Params) // "_" names will be modified +- args []ast.Expr // arguments to delegate +- variadic = false // whether the signature is variadic +- ) +- { +- allNames := make(map[string]bool) // for renaming blanks +- for _, fld := range params.List { +- for _, n := range fld.Names { +- if n.Name != "_" { +- allNames[n.Name] = true +- } +- } +- } +- blanks := 0 +- for i, fld := range params.List { +- for j, n := range fld.Names { +- if i == info.FieldIndex && j == info.NameIndex { +- continue +- } +- if n.Name == "_" { +- // Create names for blank (_) parameters so the delegating wrapper +- // can refer to them. +- for { +- newName := fmt.Sprintf("blank%d", blanks) +- blanks++ +- if !allNames[newName] { +- n.Name = newName +- break +- } +- } +- } +- args = append(args, &ast.Ident{Name: n.Name}) +- if i == len(params.List)-1 { +- _, variadic = fld.Type.(*ast.Ellipsis) +- } +- } +- } - } - -- // Verify key A. -- got, err := filecache.Get(testIPCKind, getenv("KEYA")) -- if err != nil || string(got) != testIPCValueA { -- log.Fatalf("child: Get(key) = %q, %v; want %q", got, err, testIPCValueA) +- // Rewrite all referring calls. +- newContent, err := rewriteCalls(ctx, signatureRewrite{ +- snapshot: snapshot, +- pkg: pkg, +- pgf: pgf, +- origDecl: info.Decl, +- newDecl: newDecl, +- params: params, +- callArgs: args, +- variadic: variadic, +- }) +- if err != nil { +- return nil, err - } - -- // Set key B. -- if err := filecache.Set(testIPCKind, getenv("KEYB"), []byte(testIPCValueB)); err != nil { -- log.Fatalf("child: Set(keyB) failed: %v", err) +- // Finally, rewrite the original declaration. We do this after inlining all +- // calls, as there may be calls in the same file as the declaration. But none +- // of the inlining should have changed the location of the original +- // declaration. +- { +- idx := findDecl(pgf.File, info.Decl) +- if idx < 0 { +- return nil, bug.Errorf("didn't find original decl") +- } +- +- src, ok := newContent[pgf.URI] +- if !ok { +- src = pgf.Src +- } +- fset := tokeninternal.FileSetFor(pgf.Tok) +- src, err := rewriteSignature(fset, idx, src, newDecl) +- if err != nil { +- return nil, err +- } +- newContent[pgf.URI] = src - } --} - --// uniqueKey returns a key that has never been used before. --func uniqueKey() (key [32]byte) { -- if _, err := cryptorand.Read(key[:]); err != nil { -- log.Fatalf("rand: %v", err) +- // Translate the resulting state into document changes. +- var changes []protocol.DocumentChanges +- for uri, after := range newContent { +- fh, err := snapshot.ReadFile(ctx, uri) +- if err != nil { +- return nil, err +- } +- before, err := fh.Content() +- if err != nil { +- return nil, err +- } +- edits := diff.Bytes(before, after) +- mapper := protocol.NewMapper(uri, before) +- pedits, err := protocol.EditsFromDiffEdits(mapper, edits) +- if err != nil { +- return nil, fmt.Errorf("computing edits for %s: %v", uri, err) +- } +- changes = append(changes, documentChanges(fh, pedits)...) - } -- return +- return changes, nil -} - --func BenchmarkUncontendedGet(b *testing.B) { -- const kind = "BenchmarkUncontendedGet" -- key := uniqueKey() -- -- var value [8192]byte -- if _, err := mathrand.Read(value[:]); err != nil { -- b.Fatalf("rand: %v", err) +-// rewriteSignature rewrites the signature of the declIdx'th declaration in src +-// to use the signature of newDecl (described by fset). +-// +-// TODO(rfindley): I think this operation could be generalized, for example by +-// using a concept of a 'nodepath' to correlate nodes between two related +-// files. +-// +-// Note that with its current application, rewriteSignature is expected to +-// succeed. Separate bug.Errorf calls are used below (rather than one call at +-// the callsite) in order to have greater precision. +-func rewriteSignature(fset *token.FileSet, declIdx int, src0 []byte, newDecl *ast.FuncDecl) ([]byte, error) { +- // Parse the new file0 content, to locate the original params. +- file0, err := parser.ParseFile(fset, "", src0, parser.ParseComments|parser.SkipObjectResolution) +- if err != nil { +- return nil, bug.Errorf("re-parsing declaring file failed: %v", err) - } -- if err := filecache.Set(kind, key, value[:]); err != nil { -- b.Fatal(err) +- decl0, _ := file0.Decls[declIdx].(*ast.FuncDecl) +- // Inlining shouldn't have changed the location of any declarations, but do +- // a sanity check. +- if decl0 == nil || decl0.Name.Name != newDecl.Name.Name { +- return nil, bug.Errorf("inlining affected declaration order: found %v, not func %s", decl0, newDecl.Name.Name) +- } +- opening0, closing0, err := safetoken.Offsets(fset.File(decl0.Pos()), decl0.Type.Params.Opening, decl0.Type.Params.Closing) +- if err != nil { +- return nil, bug.Errorf("can't find params: %v", err) - } -- b.ResetTimer() -- b.SetBytes(int64(len(value))) - -- var group errgroup.Group -- group.SetLimit(50) -- for i := 0; i < b.N; i++ { -- group.Go(func() error { -- _, err := filecache.Get(kind, key) -- return err -- }) +- // Format the modified signature and apply a textual replacement. This +- // minimizes comment disruption. +- formattedType := FormatNode(fset, newDecl.Type) +- expr, err := parser.ParseExprFrom(fset, "", []byte(formattedType), 0) +- if err != nil { +- return nil, bug.Errorf("parsing modified signature: %v", err) - } -- if err := group.Wait(); err != nil { -- b.Fatal(err) +- newType := expr.(*ast.FuncType) +- opening1, closing1, err := safetoken.Offsets(fset.File(newType.Pos()), newType.Params.Opening, newType.Params.Closing) +- if err != nil { +- return nil, bug.Errorf("param offsets: %v", err) - } --} -- --// These two benchmarks are asymmetric: the one for Get imposes a --// modest bound on concurrency (50) whereas the one for Set imposes a --// much higher concurrency (1000) to test the implementation's --// self-imposed bound. -- --func BenchmarkUncontendedSet(b *testing.B) { -- const kind = "BenchmarkUncontendedSet" -- key := uniqueKey() -- var value [8192]byte -- -- const P = 1000 // parallelism -- b.SetBytes(P * int64(len(value))) +- newParams := formattedType[opening1 : closing1+1] - -- for i := 0; i < b.N; i++ { -- // Perform P concurrent calls to Set. All must succeed. -- var group errgroup.Group -- for range [P]bool{} { -- group.Go(func() error { -- return filecache.Set(kind, key, value[:]) -- }) -- } -- if err := group.Wait(); err != nil { -- if strings.Contains(err.Error(), "operation not supported") || -- strings.Contains(err.Error(), "not implemented") { -- b.Skipf("skipping: %v", err) -- } -- b.Fatal(err) +- // Splice. +- var buf bytes.Buffer +- buf.Write(src0[:opening0]) +- buf.WriteString(newParams) +- buf.Write(src0[closing0+1:]) +- newSrc := buf.Bytes() +- if len(file0.Imports) > 0 { +- formatted, err := imports.Process("output", newSrc, nil) +- if err != nil { +- return nil, bug.Errorf("imports.Process failed: %v", err) - } +- newSrc = formatted - } +- return newSrc, nil -} -diff -urN a/gopls/internal/lsp/folding_range.go b/gopls/internal/lsp/folding_range.go ---- a/gopls/internal/lsp/folding_range.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/folding_range.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,46 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package lsp -- --import ( -- "context" -- -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/tag" --) -- --func (s *Server) foldingRange(ctx context.Context, params *protocol.FoldingRangeParams) ([]protocol.FoldingRange, error) { -- ctx, done := event.Start(ctx, "lsp.Server.foldingRange", tag.URI.Of(params.TextDocument.URI)) -- defer done() - -- snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go) -- defer release() -- if !ok { -- return nil, err -- } +-// ParamInfo records information about a param identified by a position. +-type ParamInfo struct { +- Decl *ast.FuncDecl // enclosing func decl (non-nil) +- FieldIndex int // index of Field in Decl.Type.Params, or -1 +- Field *ast.Field // enclosing field of Decl, or nil if range not among parameters +- NameIndex int // index of Name in Field.Names, or nil +- Name *ast.Ident // indicated name (either enclosing, or Field.Names[0] if len(Field.Names) == 1) +-} - -- ranges, err := source.FoldingRange(ctx, snapshot, fh, snapshot.Options().LineFoldingOnly) +-// FindParam finds the parameter information spanned by the given range. +-func FindParam(pgf *parsego.File, rng protocol.Range) (*ParamInfo, error) { +- start, end, err := pgf.RangePos(rng) - if err != nil { - return nil, err - } -- return toProtocolFoldingRanges(ranges) --} - --func toProtocolFoldingRanges(ranges []*source.FoldingRangeInfo) ([]protocol.FoldingRange, error) { -- result := make([]protocol.FoldingRange, 0, len(ranges)) -- for _, info := range ranges { -- rng := info.MappedRange.Range() -- result = append(result, protocol.FoldingRange{ -- StartLine: rng.Start.Line, -- StartCharacter: rng.Start.Character, -- EndLine: rng.End.Line, -- EndCharacter: rng.End.Character, -- Kind: string(info.Kind), -- }) +- path, _ := astutil.PathEnclosingInterval(pgf.File, start, end) +- var ( +- id *ast.Ident +- field *ast.Field +- decl *ast.FuncDecl +- ) +- // Find the outermost enclosing node of each kind, whether or not they match +- // the semantics described in the docstring. +- for _, n := range path { +- switch n := n.(type) { +- case *ast.Ident: +- id = n +- case *ast.Field: +- field = n +- case *ast.FuncDecl: +- decl = n +- } - } -- return result, nil --} -diff -urN a/gopls/internal/lsp/format.go b/gopls/internal/lsp/format.go ---- a/gopls/internal/lsp/format.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/format.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,36 +0,0 @@ --// Copyright 2018 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package lsp -- --import ( -- "context" -- -- "golang.org/x/tools/gopls/internal/lsp/mod" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/lsp/work" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/tag" --) -- --func (s *Server) formatting(ctx context.Context, params *protocol.DocumentFormattingParams) ([]protocol.TextEdit, error) { -- ctx, done := event.Start(ctx, "lsp.Server.formatting", tag.URI.Of(params.TextDocument.URI)) -- defer done() -- -- snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind) -- defer release() -- if !ok { -- return nil, err +- // Check the conditions described in the docstring. +- if decl == nil { +- return nil, fmt.Errorf("range is not within a function declaration") - } -- switch snapshot.FileKind(fh) { -- case source.Mod: -- return mod.Format(ctx, snapshot, fh) -- case source.Go: -- return source.Format(ctx, snapshot, fh) -- case source.Work: -- return work.Format(ctx, snapshot, fh) +- info := &ParamInfo{ +- FieldIndex: -1, +- NameIndex: -1, +- Decl: decl, - } -- return nil, nil +- for fi, f := range decl.Type.Params.List { +- if f == field { +- info.FieldIndex = fi +- info.Field = f +- for ni, n := range f.Names { +- if n == id { +- info.NameIndex = ni +- info.Name = n +- break +- } +- } +- if info.Name == nil && len(info.Field.Names) == 1 { +- info.NameIndex = 0 +- info.Name = info.Field.Names[0] +- } +- break +- } +- } +- return info, nil -} -diff -urN a/gopls/internal/lsp/frob/frob.go b/gopls/internal/lsp/frob/frob.go ---- a/gopls/internal/lsp/frob/frob.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/frob/frob.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,439 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --// Package frob is a fast restricted object encoder/decoder in the --// spirit of encoding/gob. +-// signatureRewrite defines a rewritten function signature. -// --// As with gob, types that recursively contain functions, channels, --// and unsafe.Pointers cannot be encoded, but frob has these --// additional restrictions: +-// See rewriteCalls for more details. +-type signatureRewrite struct { +- snapshot *cache.Snapshot +- pkg *cache.Package +- pgf *parsego.File +- origDecl, newDecl *ast.FuncDecl +- params *ast.FieldList +- callArgs []ast.Expr +- variadic bool +-} +- +-// rewriteCalls returns the document changes required to rewrite the +-// signature of origDecl to that of newDecl. -// --// - Interface values are not supported; this avoids the need for --// the encoding to describe types. +-// This is a rather complicated factoring of the rewrite operation, but is able +-// to describe arbitrary rewrites. Specifically, rewriteCalls creates a +-// synthetic copy of pkg, where the original function declaration is changed to +-// be a trivial wrapper around the new declaration. params and callArgs are +-// used to perform this delegation: params must have the same type as origDecl, +-// but may have renamed parameters (such as is required for delegating blank +-// parameters). callArgs are the arguments of the delegated call (i.e. using +-// params). -// --// - Types that recursively contain private struct fields are not --// permitted. +-// For example, consider removing the unused 'b' parameter below, rewriting -// --// - The encoding is unspecified and subject to change, so the encoder --// and decoder must exactly agree on their implementation and on the --// definitions of the target types. +-// func Foo(a, b, c, _ int) int { +-// return a+c +-// } -// --// - Lengths (of arrays, slices, and maps) are currently assumed to --// fit in 32 bits. +-// To -// --// - There is no error handling. All errors are reported by panicking. +-// func Foo(a, c, _ int) int { +-// return a+c +-// } -// --// - Values are serialized as trees, not graphs, so shared subgraphs --// are encoded repeatedly. +-// In this case, rewriteCalls is parameterized as follows: +-// - origDecl is the original declaration +-// - newDecl is the new declaration, which is a copy of origDecl less the 'b' +-// parameter. +-// - params is a new parameter list (a, b, c, blank0 int) to be used for the +-// new wrapper. +-// - callArgs is the argument list (a, c, blank0), to be used to call the new +-// delegate. -// --// - No attempt is made to detect cyclic data structures. --package frob -- --import ( -- "encoding/binary" -- "fmt" -- "math" -- "reflect" -- "sync" --) +-// rewriting is expressed this way so that rewriteCalls can own the details +-// of *how* this rewriting is performed. For example, as of writing it names +-// the synthetic delegate G_o_p_l_s_foo, but the caller need not know this. +-// +-// By passing an entirely new declaration, rewriteCalls may be used for +-// signature refactorings that may affect the function body, such as removing +-// or adding return values. +-func rewriteCalls(ctx context.Context, rw signatureRewrite) (map[protocol.DocumentURI][]byte, error) { +- // tag is a unique prefix that is added to the delegated declaration. +- // +- // It must have a ~0% probability of causing collisions with existing names. +- const tag = "G_o_p_l_s_" - --// A Codec[T] is an immutable encoder and decoder for values of type T. --type Codec[T any] struct{ frob *frob } +- var ( +- modifiedSrc []byte +- modifiedFile *ast.File +- modifiedDecl *ast.FuncDecl +- ) +- { +- delegate := internalastutil.CloneNode(rw.newDecl) // clone before modifying +- delegate.Name.Name = tag + delegate.Name.Name +- if obj := rw.pkg.Types().Scope().Lookup(delegate.Name.Name); obj != nil { +- return nil, fmt.Errorf("synthetic name %q conflicts with an existing declaration", delegate.Name.Name) +- } - --// CodecFor[T] returns a codec for values of type T. --// It panics if type T is unsuitable. --func CodecFor[T any]() Codec[T] { -- frobsMu.Lock() -- defer frobsMu.Unlock() -- return Codec[T]{frobFor(reflect.TypeOf((*T)(nil)).Elem())} --} +- wrapper := internalastutil.CloneNode(rw.origDecl) +- wrapper.Type.Params = rw.params - --func (codec Codec[T]) Encode(v T) []byte { return codec.frob.Encode(v) } --func (codec Codec[T]) Decode(data []byte, ptr *T) { codec.frob.Decode(data, ptr) } +- // Get the receiver name, creating it if necessary. +- var recv string // nonempty => call is a method call with receiver recv +- if wrapper.Recv.NumFields() > 0 { +- if len(wrapper.Recv.List[0].Names) > 0 { +- recv = wrapper.Recv.List[0].Names[0].Name +- } else { +- // Create unique name for the temporary receiver, which will be inlined away. +- // +- // We use the lexical scope of the original function to avoid conflicts +- // with (e.g.) named result variables. However, since the parameter syntax +- // may have been modified/renamed from the original function, we must +- // reject those names too. +- usedParams := make(map[string]bool) +- for _, fld := range wrapper.Type.Params.List { +- for _, name := range fld.Names { +- usedParams[name.Name] = true +- } +- } +- scope := rw.pkg.TypesInfo().Scopes[rw.origDecl.Type] +- if scope == nil { +- return nil, bug.Errorf("missing function scope for %v", rw.origDecl.Name.Name) +- } +- for i := 0; ; i++ { +- recv = fmt.Sprintf("r%d", i) +- _, obj := scope.LookupParent(recv, token.NoPos) +- if obj == nil && !usedParams[recv] { +- break +- } +- } +- wrapper.Recv.List[0].Names = []*ast.Ident{{Name: recv}} +- } +- } - --var ( -- frobsMu sync.Mutex -- frobs = make(map[reflect.Type]*frob) --) +- name := &ast.Ident{Name: delegate.Name.Name} +- var fun ast.Expr = name +- if recv != "" { +- fun = &ast.SelectorExpr{ +- X: &ast.Ident{Name: recv}, +- Sel: name, +- } +- } +- call := &ast.CallExpr{ +- Fun: fun, +- Args: rw.callArgs, +- } +- if rw.variadic { +- call.Ellipsis = 1 // must not be token.NoPos +- } - --// A frob is an encoder/decoder for a specific type. --type frob struct { -- t reflect.Type -- kind reflect.Kind -- elems []*frob // elem (array/slice/ptr), key+value (map), fields (struct) --} +- var stmt ast.Stmt +- if delegate.Type.Results.NumFields() > 0 { +- stmt = &ast.ReturnStmt{ +- Results: []ast.Expr{call}, +- } +- } else { +- stmt = &ast.ExprStmt{ +- X: call, +- } +- } +- wrapper.Body = &ast.BlockStmt{ +- List: []ast.Stmt{stmt}, +- } - --// frobFor returns the frob for a particular type. --// Precondition: caller holds frobsMu. --func frobFor(t reflect.Type) *frob { -- fr, ok := frobs[t] -- if !ok { -- fr = &frob{t: t, kind: t.Kind()} -- frobs[t] = fr +- fset := tokeninternal.FileSetFor(rw.pgf.Tok) +- var err error +- modifiedSrc, err = replaceFileDecl(rw.pgf, rw.origDecl, delegate) +- if err != nil { +- return nil, err +- } +- // TODO(rfindley): we can probably get away with one fewer parse operations +- // by returning the modified AST from replaceDecl. Investigate if that is +- // accurate. +- modifiedSrc = append(modifiedSrc, []byte("\n\n"+FormatNode(fset, wrapper))...) +- modifiedFile, err = parser.ParseFile(rw.pkg.FileSet(), rw.pgf.URI.Path(), modifiedSrc, parser.ParseComments|parser.SkipObjectResolution) +- if err != nil { +- return nil, err +- } +- modifiedDecl = modifiedFile.Decls[len(modifiedFile.Decls)-1].(*ast.FuncDecl) +- } - -- switch fr.kind { -- case reflect.Bool, -- reflect.Int, -- reflect.Int8, -- reflect.Int16, -- reflect.Int32, -- reflect.Int64, -- reflect.Uint, -- reflect.Uint8, -- reflect.Uint16, -- reflect.Uint32, -- reflect.Uint64, -- reflect.Uintptr, -- reflect.Float32, -- reflect.Float64, -- reflect.Complex64, -- reflect.Complex128, -- reflect.String: +- // Type check pkg again with the modified file, to compute the synthetic +- // callee. +- logf := logger(ctx, "change signature", rw.snapshot.Options().VerboseOutput) +- pkg2, info, err := reTypeCheck(logf, rw.pkg, map[protocol.DocumentURI]*ast.File{rw.pgf.URI: modifiedFile}, false) +- if err != nil { +- return nil, err +- } +- calleeInfo, err := inline.AnalyzeCallee(logf, rw.pkg.FileSet(), pkg2, info, modifiedDecl, modifiedSrc) +- if err != nil { +- return nil, fmt.Errorf("analyzing callee: %v", err) +- } - -- case reflect.Array, -- reflect.Slice, -- reflect.Ptr: // TODO(adonovan): after go1.18, use Pointer -- fr.addElem(fr.t.Elem()) +- post := func(got []byte) []byte { return bytes.ReplaceAll(got, []byte(tag), nil) } +- return inlineAllCalls(ctx, logf, rw.snapshot, rw.pkg, rw.pgf, rw.origDecl, calleeInfo, post) +-} - -- case reflect.Map: -- fr.addElem(fr.t.Key()) -- fr.addElem(fr.t.Elem()) +-// reTypeCheck re-type checks orig with new file contents defined by fileMask. +-// +-// It expects that any newly added imports are already present in the +-// transitive imports of orig. +-// +-// If expectErrors is true, reTypeCheck allows errors in the new package. +-// TODO(rfindley): perhaps this should be a filter to specify which errors are +-// acceptable. +-func reTypeCheck(logf func(string, ...any), orig *cache.Package, fileMask map[protocol.DocumentURI]*ast.File, expectErrors bool) (*types.Package, *types.Info, error) { +- pkg := types.NewPackage(string(orig.Metadata().PkgPath), string(orig.Metadata().Name)) +- info := &types.Info{ +- Types: make(map[ast.Expr]types.TypeAndValue), +- Defs: make(map[*ast.Ident]types.Object), +- Uses: make(map[*ast.Ident]types.Object), +- Implicits: make(map[ast.Node]types.Object), +- Selections: make(map[*ast.SelectorExpr]*types.Selection), +- Scopes: make(map[ast.Node]*types.Scope), +- Instances: make(map[*ast.Ident]types.Instance), +- } +- versions.InitFileVersions(info) +- { +- var files []*ast.File +- for _, pgf := range orig.CompiledGoFiles() { +- if mask, ok := fileMask[pgf.URI]; ok { +- files = append(files, mask) +- } else { +- files = append(files, pgf.File) +- } +- } - -- case reflect.Struct: -- for i := 0; i < fr.t.NumField(); i++ { -- field := fr.t.Field(i) -- if field.PkgPath != "" { -- panic(fmt.Sprintf("unexported field %v", field)) +- // Implement a BFS for imports in the transitive package graph. +- // +- // Note that this only works if any newly added imports are expected to be +- // present among transitive imports. In general we cannot assume this to +- // be the case, but in the special case of removing a parameter it works +- // because any parameter types must be present in export data. +- var importer func(importPath string) (*types.Package, error) +- { +- var ( +- importsByPath = make(map[string]*types.Package) // cached imports +- toSearch = []*types.Package{orig.Types()} // packages to search +- searched = make(map[string]bool) // path -> (false, if present in toSearch; true, if already searched) +- ) +- importer = func(path string) (*types.Package, error) { +- if p, ok := importsByPath[path]; ok { +- return p, nil - } -- fr.addElem(field.Type) +- for len(toSearch) > 0 { +- pkg := toSearch[0] +- toSearch = toSearch[1:] +- searched[pkg.Path()] = true +- for _, p := range pkg.Imports() { +- // TODO(rfindley): this is incorrect: p.Path() is a package path, +- // whereas path is an import path. We can fix this by reporting any +- // newly added imports from inlining, or by using the ImporterFrom +- // interface and package metadata. +- // +- // TODO(rfindley): can't the inliner also be wrong here? It's +- // possible that an import path means different things depending on +- // the location. +- importsByPath[p.Path()] = p +- if _, ok := searched[p.Path()]; !ok { +- searched[p.Path()] = false +- toSearch = append(toSearch, p) +- } +- } +- if p, ok := importsByPath[path]; ok { +- return p, nil +- } +- } +- return nil, fmt.Errorf("missing import") - } +- } +- cfg := &types.Config{ +- Sizes: orig.Metadata().TypesSizes, +- Importer: ImporterFunc(importer), +- } - -- default: -- // chan, func, interface, unsafe.Pointer -- panic(fmt.Sprintf("type %v is not supported by frob", fr.t)) +- // Copied from cache/check.go. +- // TODO(rfindley): factor this out and fix goVersionRx. +- // Set Go dialect. +- if module := orig.Metadata().Module; module != nil && module.GoVersion != "" { +- goVersion := "go" + module.GoVersion +- // types.NewChecker panics if GoVersion is invalid. +- // An unparsable mod file should probably stop us +- // before we get here, but double check just in case. +- if goVersionRx.MatchString(goVersion) { +- cfg.GoVersion = goVersion +- } +- } +- if expectErrors { +- cfg.Error = func(err error) { +- logf("re-type checking: expected error: %v", err) +- } +- } +- typesinternal.SetUsesCgo(cfg) +- checker := types.NewChecker(cfg, orig.FileSet(), pkg, info) +- if err := checker.Files(files); err != nil && !expectErrors { +- return nil, nil, fmt.Errorf("type checking rewritten package: %v", err) - } - } -- return fr +- return pkg, info, nil -} - --func (fr *frob) addElem(t reflect.Type) { -- fr.elems = append(fr.elems, frobFor(t)) --} +-// TODO(golang/go#63472): this looks wrong with the new Go version syntax. +-var goVersionRx = regexp.MustCompile(`^go([1-9][0-9]*)\.(0|[1-9][0-9]*)$`) - --const magic = "frob" +-func remove[T any](s []T, i int) []T { +- return append(s[:i], s[i+1:]...) +-} - --func (fr *frob) Encode(v any) []byte { -- rv := reflect.ValueOf(v) -- if rv.Type() != fr.t { -- panic(fmt.Sprintf("got %v, want %v", rv.Type(), fr.t)) +-// replaceFileDecl replaces old with new in the file described by pgf. +-// +-// TODO(rfindley): generalize, and combine with rewriteSignature. +-func replaceFileDecl(pgf *parsego.File, old, new ast.Decl) ([]byte, error) { +- i := findDecl(pgf.File, old) +- if i == -1 { +- return nil, bug.Errorf("didn't find old declaration") - } -- w := &writer{} -- w.bytes([]byte(magic)) -- fr.encode(w, rv) -- if uint64(len(w.data))>>32 != 0 { -- panic("too large") // includes all cases where len doesn't fit in 32 bits +- start, end, err := safetoken.Offsets(pgf.Tok, old.Pos(), old.End()) +- if err != nil { +- return nil, err - } -- return w.data +- var out bytes.Buffer +- out.Write(pgf.Src[:start]) +- fset := tokeninternal.FileSetFor(pgf.Tok) +- if err := format.Node(&out, fset, new); err != nil { +- return nil, bug.Errorf("formatting new node: %v", err) +- } +- out.Write(pgf.Src[end:]) +- return out.Bytes(), nil -} - --// encode appends the encoding of value v, whose type must be fr.t. --func (fr *frob) encode(out *writer, v reflect.Value) { -- switch fr.kind { -- case reflect.Bool: -- var b byte -- if v.Bool() { -- b = 1 +-// findDecl finds the index of decl in file.Decls. +-// +-// TODO: use slices.Index when it is available. +-func findDecl(file *ast.File, decl ast.Decl) int { +- for i, d := range file.Decls { +- if d == decl { +- return i - } -- out.uint8(b) -- case reflect.Int: -- out.uint64(uint64(v.Int())) -- case reflect.Int8: -- out.uint8(uint8(v.Int())) -- case reflect.Int16: -- out.uint16(uint16(v.Int())) -- case reflect.Int32: -- out.uint32(uint32(v.Int())) -- case reflect.Int64: -- out.uint64(uint64(v.Int())) -- case reflect.Uint: -- out.uint64(v.Uint()) -- case reflect.Uint8: -- out.uint8(uint8(v.Uint())) -- case reflect.Uint16: -- out.uint16(uint16(v.Uint())) -- case reflect.Uint32: -- out.uint32(uint32(v.Uint())) -- case reflect.Uint64: -- out.uint64(v.Uint()) -- case reflect.Uintptr: -- out.uint64(uint64(v.Uint())) -- case reflect.Float32: -- out.uint32(math.Float32bits(float32(v.Float()))) -- case reflect.Float64: -- out.uint64(math.Float64bits(v.Float())) -- case reflect.Complex64: -- z := complex64(v.Complex()) -- out.uint32(uint32(math.Float32bits(real(z)))) -- out.uint32(uint32(math.Float32bits(imag(z)))) -- case reflect.Complex128: -- z := v.Complex() -- out.uint64(math.Float64bits(real(z))) -- out.uint64(math.Float64bits(imag(z))) +- } +- return -1 +-} +diff -urN a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go +--- a/gopls/internal/golang/codeaction.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/codeaction.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,517 +0,0 @@ +-// Copyright 2024 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- case reflect.Array: -- len := v.Type().Len() -- elem := fr.elems[0] -- for i := 0; i < len; i++ { -- elem.encode(out, v.Index(i)) +-package golang +- +-import ( +- "context" +- "encoding/json" +- "fmt" +- "go/ast" +- "strings" +- +- "golang.org/x/tools/go/ast/inspector" +- "golang.org/x/tools/gopls/internal/analysis/fillstruct" +- "golang.org/x/tools/gopls/internal/analysis/fillswitch" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/command" +- "golang.org/x/tools/gopls/internal/settings" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/gopls/internal/util/slices" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/tag" +- "golang.org/x/tools/internal/imports" +-) +- +-// CodeActions returns all code actions (edits and other commands) +-// available for the selected range. +-func CodeActions(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, rng protocol.Range, diagnostics []protocol.Diagnostic, want map[protocol.CodeActionKind]bool) (actions []protocol.CodeAction, _ error) { +- // Only compute quick fixes if there are any diagnostics to fix. +- wantQuickFixes := want[protocol.QuickFix] && len(diagnostics) > 0 +- +- // Code actions requiring syntax information alone. +- if wantQuickFixes || want[protocol.SourceOrganizeImports] || want[protocol.RefactorExtract] { +- pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) +- if err != nil { +- return nil, err - } - -- case reflect.Slice: -- len := v.Len() -- out.uint32(uint32(len)) -- if len > 0 { -- elem := fr.elems[0] -- if elem.kind == reflect.Uint8 { -- // []byte fast path -- out.bytes(v.Bytes()) -- } else { -- for i := 0; i < len; i++ { -- elem.encode(out, v.Index(i)) +- // Process any missing imports and pair them with the diagnostics they fix. +- if wantQuickFixes || want[protocol.SourceOrganizeImports] { +- importEdits, importEditsPerFix, err := allImportsFixes(ctx, snapshot, pgf) +- if err != nil { +- event.Error(ctx, "imports fixes", err, tag.File.Of(fh.URI().Path())) +- importEdits = nil +- importEditsPerFix = nil +- } +- +- // Separate this into a set of codeActions per diagnostic, where +- // each action is the addition, removal, or renaming of one import. +- if wantQuickFixes { +- for _, importFix := range importEditsPerFix { +- fixed := fixedByImportFix(importFix.fix, diagnostics) +- if len(fixed) == 0 { +- continue +- } +- actions = append(actions, protocol.CodeAction{ +- Title: importFixTitle(importFix.fix), +- Kind: protocol.QuickFix, +- Edit: &protocol.WorkspaceEdit{ +- DocumentChanges: documentChanges(fh, importFix.edits), +- }, +- Diagnostics: fixed, +- }) - } - } +- +- // Send all of the import edits as one code action if the file is +- // being organized. +- if want[protocol.SourceOrganizeImports] && len(importEdits) > 0 { +- actions = append(actions, protocol.CodeAction{ +- Title: "Organize Imports", +- Kind: protocol.SourceOrganizeImports, +- Edit: &protocol.WorkspaceEdit{ +- DocumentChanges: documentChanges(fh, importEdits), +- }, +- }) +- } - } - -- case reflect.Map: -- len := v.Len() -- out.uint32(uint32(len)) -- if len > 0 { -- kfrob, vfrob := fr.elems[0], fr.elems[1] -- for iter := v.MapRange(); iter.Next(); { -- kfrob.encode(out, iter.Key()) -- vfrob.encode(out, iter.Value()) +- if want[protocol.RefactorExtract] { +- extractions, err := getExtractCodeActions(pgf, rng, snapshot.Options()) +- if err != nil { +- return nil, err - } +- actions = append(actions, extractions...) - } +- } - -- case reflect.Ptr: // TODO(adonovan): after go1.18, use Pointer -- if v.IsNil() { -- out.uint8(0) -- } else { -- out.uint8(1) -- fr.elems[0].encode(out, v.Elem()) +- // Code actions requiring type information. +- if want[protocol.RefactorRewrite] || +- want[protocol.RefactorInline] || +- want[protocol.GoTest] || +- want[protocol.GoDoc] { +- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) +- if err != nil { +- return nil, err +- } +- if want[protocol.RefactorRewrite] { +- rewrites, err := getRewriteCodeActions(ctx, pkg, snapshot, pgf, fh, rng, snapshot.Options()) +- if err != nil { +- return nil, err +- } +- actions = append(actions, rewrites...) - } - -- case reflect.String: -- len := v.Len() -- out.uint32(uint32(len)) -- if len > 0 { -- out.data = append(out.data, v.String()...) +- if want[protocol.RefactorInline] { +- rewrites, err := getInlineCodeActions(pkg, pgf, rng, snapshot.Options()) +- if err != nil { +- return nil, err +- } +- actions = append(actions, rewrites...) - } - -- case reflect.Struct: -- for i, elem := range fr.elems { -- elem.encode(out, v.Field(i)) +- if want[protocol.GoTest] { +- fixes, err := getGoTestCodeActions(pkg, pgf, rng) +- if err != nil { +- return nil, err +- } +- actions = append(actions, fixes...) - } - -- default: -- panic(fr.t) +- if want[protocol.GoDoc] { +- loc := protocol.Location{URI: pgf.URI, Range: rng} +- cmd, err := command.NewDocCommand("View package documentation", loc) +- if err != nil { +- return nil, err +- } +- actions = append(actions, protocol.CodeAction{ +- Title: cmd.Title, +- Kind: protocol.GoDoc, +- Command: &cmd, +- }) +- } - } +- return actions, nil -} - --func (fr *frob) Decode(data []byte, ptr any) { -- rv := reflect.ValueOf(ptr).Elem() -- if rv.Type() != fr.t { -- panic(fmt.Sprintf("got %v, want %v", rv.Type(), fr.t)) -- } -- rd := &reader{data} -- if string(rd.bytes(4)) != magic { -- panic("not a frob-encoded message") +-func supportsResolveEdits(options *settings.Options) bool { +- return options.CodeActionResolveOptions != nil && slices.Contains(options.CodeActionResolveOptions, "edit") +-} +- +-func importFixTitle(fix *imports.ImportFix) string { +- var str string +- switch fix.FixType { +- case imports.AddImport: +- str = fmt.Sprintf("Add import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath) +- case imports.DeleteImport: +- str = fmt.Sprintf("Delete import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath) +- case imports.SetImportName: +- str = fmt.Sprintf("Rename import: %s %q", fix.StmtInfo.Name, fix.StmtInfo.ImportPath) - } -- fr.decode(rd, rv) -- if len(rd.data) > 0 { -- panic("surplus bytes") +- return str +-} +- +-// fixedByImportFix filters the provided slice of diagnostics to those that +-// would be fixed by the provided imports fix. +-func fixedByImportFix(fix *imports.ImportFix, diagnostics []protocol.Diagnostic) []protocol.Diagnostic { +- var results []protocol.Diagnostic +- for _, diagnostic := range diagnostics { +- switch { +- // "undeclared name: X" may be an unresolved import. +- case strings.HasPrefix(diagnostic.Message, "undeclared name: "): +- ident := strings.TrimPrefix(diagnostic.Message, "undeclared name: ") +- if ident == fix.IdentName { +- results = append(results, diagnostic) +- } +- // "undefined: X" may be an unresolved import at Go 1.20+. +- case strings.HasPrefix(diagnostic.Message, "undefined: "): +- ident := strings.TrimPrefix(diagnostic.Message, "undefined: ") +- if ident == fix.IdentName { +- results = append(results, diagnostic) +- } +- // "could not import: X" may be an invalid import. +- case strings.HasPrefix(diagnostic.Message, "could not import: "): +- ident := strings.TrimPrefix(diagnostic.Message, "could not import: ") +- if ident == fix.IdentName { +- results = append(results, diagnostic) +- } +- // "X imported but not used" is an unused import. +- // "X imported but not used as Y" is an unused import. +- case strings.Contains(diagnostic.Message, " imported but not used"): +- idx := strings.Index(diagnostic.Message, " imported but not used") +- importPath := diagnostic.Message[:idx] +- if importPath == fmt.Sprintf("%q", fix.StmtInfo.ImportPath) { +- results = append(results, diagnostic) +- } +- } - } +- return results -} - --// decode reads from in, decodes a value, and sets addr to it. --// addr must be a zero-initialized addressable variable of type fr.t. --func (fr *frob) decode(in *reader, addr reflect.Value) { -- switch fr.kind { -- case reflect.Bool: -- addr.SetBool(in.uint8() != 0) -- case reflect.Int: -- addr.SetInt(int64(in.uint64())) -- case reflect.Int8: -- addr.SetInt(int64(in.uint8())) -- case reflect.Int16: -- addr.SetInt(int64(in.uint16())) -- case reflect.Int32: -- addr.SetInt(int64(in.uint32())) -- case reflect.Int64: -- addr.SetInt(int64(in.uint64())) -- case reflect.Uint: -- addr.SetUint(in.uint64()) -- case reflect.Uint8: -- addr.SetUint(uint64(in.uint8())) -- case reflect.Uint16: -- addr.SetUint(uint64(in.uint16())) -- case reflect.Uint32: -- addr.SetUint(uint64(in.uint32())) -- case reflect.Uint64: -- addr.SetUint(in.uint64()) -- case reflect.Uintptr: -- addr.SetUint(in.uint64()) -- case reflect.Float32: -- addr.SetFloat(float64(math.Float32frombits(in.uint32()))) -- case reflect.Float64: -- addr.SetFloat(math.Float64frombits(in.uint64())) -- case reflect.Complex64: -- addr.SetComplex(complex128(complex( -- math.Float32frombits(in.uint32()), -- math.Float32frombits(in.uint32()), -- ))) -- case reflect.Complex128: -- addr.SetComplex(complex( -- math.Float64frombits(in.uint64()), -- math.Float64frombits(in.uint64()), -- )) -- -- case reflect.Array: -- len := fr.t.Len() -- for i := 0; i < len; i++ { -- fr.elems[0].decode(in, addr.Index(i)) -- } +-// getExtractCodeActions returns any refactor.extract code actions for the selection. +-func getExtractCodeActions(pgf *parsego.File, rng protocol.Range, options *settings.Options) ([]protocol.CodeAction, error) { +- if rng.Start == rng.End { +- return nil, nil +- } - -- case reflect.Slice: -- len := int(in.uint32()) -- if len > 0 { -- elem := fr.elems[0] -- if elem.kind == reflect.Uint8 { -- // []byte fast path -- // (Not addr.SetBytes: we must make a copy.) -- addr.Set(reflect.AppendSlice(addr, reflect.ValueOf(in.bytes(len)))) -- } else { -- addr.Set(reflect.MakeSlice(fr.t, len, len)) -- for i := 0; i < len; i++ { -- elem.decode(in, addr.Index(i)) -- } -- } +- start, end, err := pgf.RangePos(rng) +- if err != nil { +- return nil, err +- } +- puri := pgf.URI +- var commands []protocol.Command +- if _, ok, methodOk, _ := CanExtractFunction(pgf.Tok, start, end, pgf.Src, pgf.File); ok { +- cmd, err := command.NewApplyFixCommand("Extract function", command.ApplyFixArgs{ +- Fix: fixExtractFunction, +- URI: puri, +- Range: rng, +- ResolveEdits: supportsResolveEdits(options), +- }) +- if err != nil { +- return nil, err - } -- -- case reflect.Map: -- len := int(in.uint32()) -- if len > 0 { -- m := reflect.MakeMapWithSize(fr.t, len) -- addr.Set(m) -- kfrob, vfrob := fr.elems[0], fr.elems[1] -- k := reflect.New(kfrob.t).Elem() -- v := reflect.New(vfrob.t).Elem() -- kzero := reflect.Zero(kfrob.t) -- vzero := reflect.Zero(vfrob.t) -- for i := 0; i < len; i++ { -- // TODO(adonovan): use SetZero from go1.20. -- // k.SetZero() -- // v.SetZero() -- k.Set(kzero) -- v.Set(vzero) -- kfrob.decode(in, k) -- vfrob.decode(in, v) -- m.SetMapIndex(k, v) +- commands = append(commands, cmd) +- if methodOk { +- cmd, err := command.NewApplyFixCommand("Extract method", command.ApplyFixArgs{ +- Fix: fixExtractMethod, +- URI: puri, +- Range: rng, +- ResolveEdits: supportsResolveEdits(options), +- }) +- if err != nil { +- return nil, err - } +- commands = append(commands, cmd) - } -- -- case reflect.Ptr: // TODO(adonovan): after go1.18, use Pointer -- isNil := in.uint8() == 0 -- if !isNil { -- ptr := reflect.New(fr.elems[0].t) -- addr.Set(ptr) -- fr.elems[0].decode(in, ptr.Elem()) -- } -- -- case reflect.String: -- len := int(in.uint32()) -- if len > 0 { -- addr.SetString(string(in.bytes(len))) +- } +- if _, _, ok, _ := CanExtractVariable(start, end, pgf.File); ok { +- cmd, err := command.NewApplyFixCommand("Extract variable", command.ApplyFixArgs{ +- Fix: fixExtractVariable, +- URI: puri, +- Range: rng, +- ResolveEdits: supportsResolveEdits(options), +- }) +- if err != nil { +- return nil, err - } +- commands = append(commands, cmd) +- } +- var actions []protocol.CodeAction +- for i := range commands { +- actions = append(actions, newCodeAction(commands[i].Title, protocol.RefactorExtract, &commands[i], nil, options)) +- } +- return actions, nil +-} - -- case reflect.Struct: -- for i, elem := range fr.elems { -- elem.decode(in, addr.Field(i)) +-func newCodeAction(title string, kind protocol.CodeActionKind, cmd *protocol.Command, diagnostics []protocol.Diagnostic, options *settings.Options) protocol.CodeAction { +- action := protocol.CodeAction{ +- Title: title, +- Kind: kind, +- Diagnostics: diagnostics, +- } +- if !supportsResolveEdits(options) { +- action.Command = cmd +- } else { +- data, err := json.Marshal(cmd) +- if err != nil { +- panic("unable to marshal") - } -- -- default: -- panic(fr.t) +- msg := json.RawMessage(data) +- action.Data = &msg - } +- return action -} - --var le = binary.LittleEndian +-func getRewriteCodeActions(ctx context.Context, pkg *cache.Package, snapshot *cache.Snapshot, pgf *parsego.File, fh file.Handle, rng protocol.Range, options *settings.Options) (_ []protocol.CodeAction, rerr error) { +- // golang/go#61693: code actions were refactored to run outside of the +- // analysis framework, but as a result they lost their panic recovery. +- // +- // These code actions should never fail, but put back the panic recovery as a +- // defensive measure. +- defer func() { +- if r := recover(); r != nil { +- rerr = bug.Errorf("refactor.rewrite code actions panicked: %v", r) +- } +- }() - --type reader struct{ data []byte } +- var actions []protocol.CodeAction - --func (r *reader) uint8() uint8 { -- v := r.data[0] -- r.data = r.data[1:] -- return v --} +- if canRemoveParameter(pkg, pgf, rng) { +- cmd, err := command.NewChangeSignatureCommand("remove unused parameter", command.ChangeSignatureArgs{ +- RemoveParameter: protocol.Location{ +- URI: pgf.URI, +- Range: rng, +- }, +- ResolveEdits: supportsResolveEdits(options), +- }) +- if err != nil { +- return nil, err +- } +- actions = append(actions, newCodeAction("Refactor: remove unused parameter", protocol.RefactorRewrite, &cmd, nil, options)) +- } - --func (r *reader) uint16() uint16 { -- v := le.Uint16(r.data) -- r.data = r.data[2:] -- return v --} +- if action, ok := ConvertStringLiteral(pgf, fh, rng); ok { +- actions = append(actions, action) +- } - --func (r *reader) uint32() uint32 { -- v := le.Uint32(r.data) -- r.data = r.data[4:] -- return v --} +- start, end, err := pgf.RangePos(rng) +- if err != nil { +- return nil, err +- } - --func (r *reader) uint64() uint64 { -- v := le.Uint64(r.data) -- r.data = r.data[8:] -- return v --} +- var commands []protocol.Command +- if _, ok, _ := CanInvertIfCondition(pgf.File, start, end); ok { +- cmd, err := command.NewApplyFixCommand("Invert 'if' condition", command.ApplyFixArgs{ +- Fix: fixInvertIfCondition, +- URI: pgf.URI, +- Range: rng, +- ResolveEdits: supportsResolveEdits(options), +- }) +- if err != nil { +- return nil, err +- } +- commands = append(commands, cmd) +- } - --func (r *reader) bytes(n int) []byte { -- v := r.data[:n] -- r.data = r.data[n:] -- return v --} +- if msg, ok, _ := CanSplitLines(pgf.File, pkg.FileSet(), start, end); ok { +- cmd, err := command.NewApplyFixCommand(msg, command.ApplyFixArgs{ +- Fix: fixSplitLines, +- URI: pgf.URI, +- Range: rng, +- ResolveEdits: supportsResolveEdits(options), +- }) +- if err != nil { +- return nil, err +- } +- commands = append(commands, cmd) +- } - --type writer struct{ data []byte } +- if msg, ok, _ := CanJoinLines(pgf.File, pkg.FileSet(), start, end); ok { +- cmd, err := command.NewApplyFixCommand(msg, command.ApplyFixArgs{ +- Fix: fixJoinLines, +- URI: pgf.URI, +- Range: rng, +- ResolveEdits: supportsResolveEdits(options), +- }) +- if err != nil { +- return nil, err +- } +- commands = append(commands, cmd) +- } - --func (w *writer) uint8(v uint8) { w.data = append(w.data, v) } --func (w *writer) uint16(v uint16) { w.data = appendUint16(w.data, v) } --func (w *writer) uint32(v uint32) { w.data = appendUint32(w.data, v) } --func (w *writer) uint64(v uint64) { w.data = appendUint64(w.data, v) } --func (w *writer) bytes(v []byte) { w.data = append(w.data, v...) } +- // N.B.: an inspector only pays for itself after ~5 passes, which means we're +- // currently not getting a good deal on this inspection. +- // +- // TODO: Consider removing the inspection after convenienceAnalyzers are removed. +- inspect := inspector.New([]*ast.File{pgf.File}) +- for _, diag := range fillstruct.Diagnose(inspect, start, end, pkg.Types(), pkg.TypesInfo()) { +- rng, err := pgf.Mapper.PosRange(pgf.Tok, diag.Pos, diag.End) +- if err != nil { +- return nil, err +- } +- for _, fix := range diag.SuggestedFixes { +- cmd, err := command.NewApplyFixCommand(fix.Message, command.ApplyFixArgs{ +- Fix: diag.Category, +- URI: pgf.URI, +- Range: rng, +- ResolveEdits: supportsResolveEdits(options), +- }) +- if err != nil { +- return nil, err +- } +- commands = append(commands, cmd) +- } +- } - --// TODO(adonovan): delete these as in go1.19 they are methods on LittleEndian: +- for _, diag := range fillswitch.Diagnose(inspect, start, end, pkg.Types(), pkg.TypesInfo()) { +- edits, err := suggestedFixToEdits(ctx, snapshot, pkg.FileSet(), &diag.SuggestedFixes[0]) +- if err != nil { +- return nil, err +- } - --func appendUint16(b []byte, v uint16) []byte { -- return append(b, -- byte(v), -- byte(v>>8), -- ) --} +- changes := []protocol.DocumentChanges{} // must be a slice +- for _, edit := range edits { +- edit := edit +- changes = append(changes, protocol.DocumentChanges{ +- TextDocumentEdit: &edit, +- }) +- } - --func appendUint32(b []byte, v uint32) []byte { -- return append(b, -- byte(v), -- byte(v>>8), -- byte(v>>16), -- byte(v>>24), -- ) --} +- actions = append(actions, protocol.CodeAction{ +- Title: diag.Message, +- Kind: protocol.RefactorRewrite, +- Edit: &protocol.WorkspaceEdit{ +- DocumentChanges: changes, +- }, +- }) +- } +- for i := range commands { +- actions = append(actions, newCodeAction(commands[i].Title, protocol.RefactorRewrite, &commands[i], nil, options)) +- } - --func appendUint64(b []byte, v uint64) []byte { -- return append(b, -- byte(v), -- byte(v>>8), -- byte(v>>16), -- byte(v>>24), -- byte(v>>32), -- byte(v>>40), -- byte(v>>48), -- byte(v>>56), -- ) +- return actions, nil -} -diff -urN a/gopls/internal/lsp/frob/frob_test.go b/gopls/internal/lsp/frob/frob_test.go ---- a/gopls/internal/lsp/frob/frob_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/frob/frob_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,119 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package frob_test +-// canRemoveParameter reports whether we can remove the function parameter +-// indicated by the given [start, end) range. +-// +-// This is true if: +-// - there are no parse or type errors, and +-// - [start, end) is contained within an unused field or parameter name +-// - ... of a non-method function declaration. +-// +-// (Note that the unusedparam analyzer also computes this property, but +-// much more precisely, allowing it to report its findings as diagnostics.) +-func canRemoveParameter(pkg *cache.Package, pgf *parsego.File, rng protocol.Range) bool { +- if perrors, terrors := pkg.ParseErrors(), pkg.TypeErrors(); len(perrors) > 0 || len(terrors) > 0 { +- return false // can't remove parameters from packages with errors +- } +- info, err := FindParam(pgf, rng) +- if err != nil { +- return false // e.g. invalid range +- } +- if info.Field == nil { +- return false // range does not span a parameter +- } +- if info.Decl.Body == nil { +- return false // external function +- } +- if len(info.Field.Names) == 0 { +- return true // no names => field is unused +- } +- if info.Name == nil { +- return false // no name is indicated +- } +- if info.Name.Name == "_" { +- return true // trivially unused +- } - --import ( -- "math" -- "reflect" -- "testing" +- obj := pkg.TypesInfo().Defs[info.Name] +- if obj == nil { +- return false // something went wrong +- } - -- "golang.org/x/tools/gopls/internal/lsp/frob" --) +- used := false +- ast.Inspect(info.Decl.Body, func(node ast.Node) bool { +- if n, ok := node.(*ast.Ident); ok && pkg.TypesInfo().Uses[n] == obj { +- used = true +- } +- return !used // keep going until we find a use +- }) +- return !used +-} - --func TestBasics(t *testing.T) { -- type Basics struct { -- A []*string -- B [2]int -- C *Basics -- D map[string]int -- E []byte -- F []string +-// getInlineCodeActions returns refactor.inline actions available at the specified range. +-func getInlineCodeActions(pkg *cache.Package, pgf *parsego.File, rng protocol.Range, options *settings.Options) ([]protocol.CodeAction, error) { +- start, end, err := pgf.RangePos(rng) +- if err != nil { +- return nil, err - } -- codec := frob.CodecFor[Basics]() - -- s1, s2 := "hello", "world" -- x := Basics{ -- A: []*string{&s1, nil, &s2}, -- B: [...]int{1, 2}, -- C: &Basics{ -- B: [...]int{3, 4}, -- D: map[string]int{"one": 1}, -- }, -- E: []byte("hello"), -- F: []string{s1, s2}, +- // If range is within call expression, offer to inline the call. +- var commands []protocol.Command +- if _, fn, err := EnclosingStaticCall(pkg, pgf, start, end); err == nil { +- cmd, err := command.NewApplyFixCommand(fmt.Sprintf("Inline call to %s", fn.Name()), command.ApplyFixArgs{ +- Fix: fixInlineCall, +- URI: pgf.URI, +- Range: rng, +- ResolveEdits: supportsResolveEdits(options), +- }) +- if err != nil { +- return nil, err +- } +- commands = append(commands, cmd) - } -- var y Basics -- codec.Decode(codec.Encode(x), &y) -- if !reflect.DeepEqual(x, y) { -- t.Fatalf("bad roundtrip: got %#v, want %#v", y, x) +- +- // Convert commands to actions. +- var actions []protocol.CodeAction +- for i := range commands { +- actions = append(actions, newCodeAction(commands[i].Title, protocol.RefactorInline, &commands[i], nil, options)) - } +- return actions, nil -} - --func TestInts(t *testing.T) { -- type Ints struct { -- U uint -- U8 uint8 -- U16 uint16 -- U32 uint32 -- U64 uint64 -- UP uintptr -- I int -- I8 int8 -- I16 int16 -- I32 int32 -- I64 int64 -- F32 float32 -- F64 float64 -- C64 complex64 -- C128 complex128 +-// getGoTestCodeActions returns any "run this test/benchmark" code actions for the selection. +-func getGoTestCodeActions(pkg *cache.Package, pgf *parsego.File, rng protocol.Range) ([]protocol.CodeAction, error) { +- fns, err := TestsAndBenchmarks(pkg, pgf) +- if err != nil { +- return nil, err - } -- codec := frob.CodecFor[Ints]() - -- // maxima -- max1 := Ints{ -- U: math.MaxUint, -- U8: math.MaxUint8, -- U16: math.MaxUint16, -- U32: math.MaxUint32, -- U64: math.MaxUint64, -- UP: math.MaxUint, -- I: math.MaxInt, -- I8: math.MaxInt8, -- I16: math.MaxInt16, -- I32: math.MaxInt32, -- I64: math.MaxInt64, -- F32: math.MaxFloat32, -- F64: math.MaxFloat64, -- C64: complex(math.MaxFloat32, math.MaxFloat32), -- C128: complex(math.MaxFloat64, math.MaxFloat64), +- var tests, benchmarks []string +- for _, fn := range fns.Tests { +- if !protocol.Intersect(fn.Rng, rng) { +- continue +- } +- tests = append(tests, fn.Name) - } -- var max2 Ints -- codec.Decode(codec.Encode(max1), &max2) -- if !reflect.DeepEqual(max1, max2) { -- t.Fatalf("max: bad roundtrip: got %#v, want %#v", max2, max1) +- for _, fn := range fns.Benchmarks { +- if !protocol.Intersect(fn.Rng, rng) { +- continue +- } +- benchmarks = append(benchmarks, fn.Name) - } - -- // minima -- min1 := Ints{ -- I: math.MinInt, -- I8: math.MinInt8, -- I16: math.MinInt16, -- I32: math.MinInt32, -- I64: math.MinInt64, -- F32: -math.MaxFloat32, -- F64: -math.MaxFloat32, -- C64: complex(-math.MaxFloat32, -math.MaxFloat32), -- C128: complex(-math.MaxFloat64, -math.MaxFloat64), -- } -- var min2 Ints -- codec.Decode(codec.Encode(min1), &min2) -- if !reflect.DeepEqual(min1, min2) { -- t.Fatalf("min: bad roundtrip: got %#v, want %#v", min2, min1) +- if len(tests) == 0 && len(benchmarks) == 0 { +- return nil, nil - } - -- // negatives (other than MinInt), to exercise conversions -- neg1 := Ints{ -- I: -1, -- I8: -1, -- I16: -1, -- I32: -1, -- I64: -1, -- } -- var neg2 Ints -- codec.Decode(codec.Encode(neg1), &neg2) -- if !reflect.DeepEqual(neg1, neg2) { -- t.Fatalf("neg: bad roundtrip: got %#v, want %#v", neg2, neg1) +- cmd, err := command.NewTestCommand("Run tests and benchmarks", pgf.URI, tests, benchmarks) +- if err != nil { +- return nil, err - } +- return []protocol.CodeAction{{ +- Title: cmd.Title, +- Kind: protocol.GoTest, +- Command: &cmd, +- }}, nil -} -diff -urN a/gopls/internal/lsp/general.go b/gopls/internal/lsp/general.go ---- a/gopls/internal/lsp/general.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/general.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,707 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. +- +-func documentChanges(fh file.Handle, edits []protocol.TextEdit) []protocol.DocumentChanges { +- return protocol.TextEditsToDocumentChanges(fh.URI(), fh.Version(), edits) +-} +diff -urN a/gopls/internal/golang/code_lens.go b/gopls/internal/golang/code_lens.go +--- a/gopls/internal/golang/code_lens.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/code_lens.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,250 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package lsp +-package golang - -import ( - "context" -- "encoding/json" -- "fmt" -- "go/build" -- "log" -- "os" -- "path" -- "path/filepath" -- "sort" +- "go/ast" +- "go/token" +- "go/types" +- "regexp" - "strings" -- "sync" - -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/debug" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/gopls/internal/telemetry" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/jsonrpc2" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/command" -) - --func (s *Server) initialize(ctx context.Context, params *protocol.ParamInitialize) (*protocol.InitializeResult, error) { -- ctx, done := event.Start(ctx, "lsp.Server.initialize") -- defer done() -- -- telemetry.RecordClientInfo(params) +-type LensFunc func(context.Context, *cache.Snapshot, file.Handle) ([]protocol.CodeLens, error) - -- s.stateMu.Lock() -- if s.state >= serverInitializing { -- defer s.stateMu.Unlock() -- return nil, fmt.Errorf("%w: initialize called while server in %v state", jsonrpc2.ErrInvalidRequest, s.state) +-// LensFuncs returns the supported lensFuncs for Go files. +-func LensFuncs() map[command.Command]LensFunc { +- return map[command.Command]LensFunc{ +- command.Generate: goGenerateCodeLens, +- command.Test: runTestCodeLens, +- command.RegenerateCgo: regenerateCgoLens, +- command.GCDetails: toggleDetailsCodeLens, - } -- s.state = serverInitializing -- s.stateMu.Unlock() +-} - -- // For uniqueness, use the gopls PID rather than params.ProcessID (the client -- // pid). Some clients might start multiple gopls servers, though they -- // probably shouldn't. -- pid := os.Getpid() -- s.tempDir = filepath.Join(os.TempDir(), fmt.Sprintf("gopls-%d.%s", pid, s.session.ID())) -- err := os.Mkdir(s.tempDir, 0700) -- if err != nil { -- // MkdirTemp could fail due to permissions issues. This is a problem with -- // the user's environment, but should not block gopls otherwise behaving. -- // All usage of s.tempDir should be predicated on having a non-empty -- // s.tempDir. -- event.Error(ctx, "creating temp dir", err) -- s.tempDir = "" -- } -- s.progress.SetSupportsWorkDoneProgress(params.Capabilities.Window.WorkDoneProgress) +-var ( +- testRe = regexp.MustCompile(`^Test([^a-z]|$)`) // TestFoo or Test but not Testable +- benchmarkRe = regexp.MustCompile(`^Benchmark([^a-z]|$)`) +-) - -- options := s.Options().Clone() -- // TODO(rfindley): remove the error return from handleOptionResults, and -- // eliminate this defer. -- defer func() { s.SetOptions(options) }() +-func runTestCodeLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) { +- var codeLens []protocol.CodeLens - -- if err := s.handleOptionResults(ctx, source.SetOptions(options, params.InitializationOptions)); err != nil { +- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) +- if err != nil { - return nil, err - } -- options.ForClientCapabilities(params.ClientInfo, params.Capabilities) -- -- if options.ShowBugReports { -- // Report the next bug that occurs on the server. -- bug.Handle(func(b bug.Bug) { -- msg := &protocol.ShowMessageParams{ -- Type: protocol.Error, -- Message: fmt.Sprintf("A bug occurred on the server: %s\nLocation:%s", b.Description, b.Key), -- } -- go func() { -- if err := s.eventuallyShowMessage(context.Background(), msg); err != nil { -- log.Printf("error showing bug: %v", err) -- } -- }() -- }) +- fns, err := TestsAndBenchmarks(pkg, pgf) +- if err != nil { +- return nil, err - } -- -- folders := params.WorkspaceFolders -- if len(folders) == 0 { -- if params.RootURI != "" { -- folders = []protocol.WorkspaceFolder{{ -- URI: string(params.RootURI), -- Name: path.Base(params.RootURI.SpanURI().Filename()), -- }} +- puri := fh.URI() +- for _, fn := range fns.Tests { +- cmd, err := command.NewTestCommand("run test", puri, []string{fn.Name}, nil) +- if err != nil { +- return nil, err - } +- rng := protocol.Range{Start: fn.Rng.Start, End: fn.Rng.Start} +- codeLens = append(codeLens, protocol.CodeLens{Range: rng, Command: &cmd}) - } -- for _, folder := range folders { -- uri := span.URIFromURI(folder.URI) -- if !uri.IsFile() { -- continue +- +- for _, fn := range fns.Benchmarks { +- cmd, err := command.NewTestCommand("run benchmark", puri, nil, []string{fn.Name}) +- if err != nil { +- return nil, err - } -- s.pendingFolders = append(s.pendingFolders, folder) -- } -- // gopls only supports URIs with a file:// scheme, so if we have no -- // workspace folders with a supported scheme, fail to initialize. -- if len(folders) > 0 && len(s.pendingFolders) == 0 { -- return nil, fmt.Errorf("unsupported URI schemes: %v (gopls only supports file URIs)", folders) +- rng := protocol.Range{Start: fn.Rng.Start, End: fn.Rng.Start} +- codeLens = append(codeLens, protocol.CodeLens{Range: rng, Command: &cmd}) - } - -- var codeActionProvider interface{} = true -- if ca := params.Capabilities.TextDocument.CodeAction; len(ca.CodeActionLiteralSupport.CodeActionKind.ValueSet) > 0 { -- // If the client has specified CodeActionLiteralSupport, -- // send the code actions we support. -- // -- // Using CodeActionOptions is only valid if codeActionLiteralSupport is set. -- codeActionProvider = &protocol.CodeActionOptions{ -- CodeActionKinds: s.getSupportedCodeActions(), +- if len(fns.Benchmarks) > 0 { +- pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) +- if err != nil { +- return nil, err - } -- } -- var renameOpts interface{} = true -- if r := params.Capabilities.TextDocument.Rename; r != nil && r.PrepareSupport { -- renameOpts = protocol.RenameOptions{ -- PrepareProvider: r.PrepareSupport, +- // add a code lens to the top of the file which runs all benchmarks in the file +- rng, err := pgf.PosRange(pgf.File.Package, pgf.File.Package) +- if err != nil { +- return nil, err - } -- } -- -- versionInfo := debug.VersionInfo() -- -- // golang/go#45732: Warn users who've installed sergi/go-diff@v1.2.0, since -- // it will corrupt the formatting of their files. -- for _, dep := range versionInfo.Deps { -- if dep.Path == "github.com/sergi/go-diff" && dep.Version == "v1.2.0" { -- if err := s.eventuallyShowMessage(ctx, &protocol.ShowMessageParams{ -- Message: `It looks like you have a bad gopls installation. --Please reinstall gopls by running 'GO111MODULE=on go install golang.org/x/tools/gopls@latest'. --See https://github.com/golang/go/issues/45732 for more information.`, -- Type: protocol.Error, -- }); err != nil { -- return nil, err -- } +- var benches []string +- for _, fn := range fns.Benchmarks { +- benches = append(benches, fn.Name) - } -- } -- -- goplsVersion, err := json.Marshal(versionInfo) -- if err != nil { -- return nil, err -- } -- -- return &protocol.InitializeResult{ -- Capabilities: protocol.ServerCapabilities{ -- CallHierarchyProvider: &protocol.Or_ServerCapabilities_callHierarchyProvider{Value: true}, -- CodeActionProvider: codeActionProvider, -- CodeLensProvider: &protocol.CodeLensOptions{}, // must be non-nil to enable the code lens capability -- CompletionProvider: &protocol.CompletionOptions{ -- TriggerCharacters: []string{"."}, -- }, -- DefinitionProvider: &protocol.Or_ServerCapabilities_definitionProvider{Value: true}, -- TypeDefinitionProvider: &protocol.Or_ServerCapabilities_typeDefinitionProvider{Value: true}, -- ImplementationProvider: &protocol.Or_ServerCapabilities_implementationProvider{Value: true}, -- DocumentFormattingProvider: &protocol.Or_ServerCapabilities_documentFormattingProvider{Value: true}, -- DocumentSymbolProvider: &protocol.Or_ServerCapabilities_documentSymbolProvider{Value: true}, -- WorkspaceSymbolProvider: &protocol.Or_ServerCapabilities_workspaceSymbolProvider{Value: true}, -- ExecuteCommandProvider: &protocol.ExecuteCommandOptions{ -- Commands: nonNilSliceString(options.SupportedCommands), -- }, -- FoldingRangeProvider: &protocol.Or_ServerCapabilities_foldingRangeProvider{Value: true}, -- HoverProvider: &protocol.Or_ServerCapabilities_hoverProvider{Value: true}, -- DocumentHighlightProvider: &protocol.Or_ServerCapabilities_documentHighlightProvider{Value: true}, -- DocumentLinkProvider: &protocol.DocumentLinkOptions{}, -- InlayHintProvider: protocol.InlayHintOptions{}, -- ReferencesProvider: &protocol.Or_ServerCapabilities_referencesProvider{Value: true}, -- RenameProvider: renameOpts, -- SelectionRangeProvider: &protocol.Or_ServerCapabilities_selectionRangeProvider{Value: true}, -- SemanticTokensProvider: protocol.SemanticTokensOptions{ -- Range: &protocol.Or_SemanticTokensOptions_range{Value: true}, -- Full: &protocol.Or_SemanticTokensOptions_full{Value: true}, -- Legend: protocol.SemanticTokensLegend{ -- TokenTypes: nonNilSliceString(options.SemanticTypes), -- TokenModifiers: nonNilSliceString(options.SemanticMods), -- }, -- }, -- SignatureHelpProvider: &protocol.SignatureHelpOptions{ -- TriggerCharacters: []string{"(", ","}, -- }, -- TextDocumentSync: &protocol.TextDocumentSyncOptions{ -- Change: protocol.Incremental, -- OpenClose: true, -- Save: &protocol.SaveOptions{ -- IncludeText: false, -- }, -- }, -- Workspace: &protocol.Workspace6Gn{ -- WorkspaceFolders: &protocol.WorkspaceFolders5Gn{ -- Supported: true, -- ChangeNotifications: "workspace/didChangeWorkspaceFolders", -- }, -- }, -- }, -- ServerInfo: &protocol.PServerInfoMsg_initialize{ -- Name: "gopls", -- Version: string(goplsVersion), -- }, -- }, nil --} -- --func (s *Server) initialized(ctx context.Context, params *protocol.InitializedParams) error { -- ctx, done := event.Start(ctx, "lsp.Server.initialized") -- defer done() -- -- s.stateMu.Lock() -- if s.state >= serverInitialized { -- defer s.stateMu.Unlock() -- return fmt.Errorf("%w: initialized called while server in %v state", jsonrpc2.ErrInvalidRequest, s.state) -- } -- s.state = serverInitialized -- s.stateMu.Unlock() -- -- for _, not := range s.notifications { -- s.client.ShowMessage(ctx, not) -- } -- s.notifications = nil -- -- if err := s.addFolders(ctx, s.pendingFolders); err != nil { -- return err -- } -- s.pendingFolders = nil -- s.checkViewGoVersions() -- -- var registrations []protocol.Registration -- options := s.Options() -- if options.ConfigurationSupported && options.DynamicConfigurationSupported { -- registrations = append(registrations, protocol.Registration{ -- ID: "workspace/didChangeConfiguration", -- Method: "workspace/didChangeConfiguration", -- }) -- } -- if len(registrations) > 0 { -- if err := s.client.RegisterCapability(ctx, &protocol.RegistrationParams{ -- Registrations: registrations, -- }); err != nil { -- return err +- cmd, err := command.NewTestCommand("run file benchmarks", puri, nil, benches) +- if err != nil { +- return nil, err - } +- codeLens = append(codeLens, protocol.CodeLens{Range: rng, Command: &cmd}) - } -- -- // Ask (maybe) about enabling telemetry. Do this asynchronously, as it's OK -- // for users to ignore or dismiss the question. -- go s.maybePromptForTelemetry(ctx, options.TelemetryPrompt) -- -- return nil --} -- --// GoVersionTable maps Go versions to the gopls version in which support will --// be deprecated, and the final gopls version supporting them without warnings. --// Keep this in sync with gopls/README.md. --// --// Must be sorted in ascending order of Go version. --// --// Mutable for testing. --var GoVersionTable = []GoVersionSupport{ -- {12, "", "v0.7.5"}, -- {15, "", "v0.9.5"}, -- {16, "v0.13.0", "v0.11.0"}, -- {17, "v0.13.0", "v0.11.0"}, --} -- --// GoVersionSupport holds information about end-of-life Go version support. --type GoVersionSupport struct { -- GoVersion int -- DeprecatedVersion string // if unset, the version is already deprecated -- InstallGoplsVersion string +- return codeLens, nil -} - --// OldestSupportedGoVersion is the last X in Go 1.X that this version of gopls --// supports. --func OldestSupportedGoVersion() int { -- return GoVersionTable[len(GoVersionTable)-1].GoVersion + 1 +-type TestFn struct { +- Name string +- Rng protocol.Range -} - --// versionMessage returns the warning/error message to display if the user has --// the given Go version, if any. The goVersion variable is the X in Go 1.X. If --// fromBuild is set, the Go version is the version used to build gopls. --// Otherwise, it is the go command version. --// --// If goVersion is invalid (< 0), it returns "", 0. --func versionMessage(goVersion int, fromBuild bool) (string, protocol.MessageType) { -- if goVersion < 0 { -- return "", 0 -- } -- -- for _, v := range GoVersionTable { -- if goVersion <= v.GoVersion { -- var msgBuilder strings.Builder -- -- mType := protocol.Error -- if fromBuild { -- fmt.Fprintf(&msgBuilder, "Gopls was built with Go version 1.%d", goVersion) -- } else { -- fmt.Fprintf(&msgBuilder, "Found Go version 1.%d", goVersion) -- } -- if v.DeprecatedVersion != "" { -- // not deprecated yet, just a warning -- fmt.Fprintf(&msgBuilder, ", which will be unsupported by gopls %s. ", v.DeprecatedVersion) -- mType = protocol.Warning -- } else { -- fmt.Fprint(&msgBuilder, ", which is not supported by this version of gopls. ") -- } -- fmt.Fprintf(&msgBuilder, "Please upgrade to Go 1.%d or later and reinstall gopls. ", OldestSupportedGoVersion()) -- fmt.Fprintf(&msgBuilder, "If you can't upgrade and want this message to go away, please install gopls %s. ", v.InstallGoplsVersion) -- fmt.Fprint(&msgBuilder, "See https://go.dev/s/gopls-support-policy for more details.") -- -- return msgBuilder.String(), mType -- } -- } -- return "", 0 +-type TestFns struct { +- Tests []TestFn +- Benchmarks []TestFn -} - --// checkViewGoVersions checks whether any Go version used by a view is too old, --// raising a showMessage notification if so. --// --// It should be called after views change. --func (s *Server) checkViewGoVersions() { -- oldestVersion, fromBuild := go1Point(), true -- for _, view := range s.session.Views() { -- viewVersion := view.GoVersion() -- if oldestVersion == -1 || viewVersion < oldestVersion { -- oldestVersion, fromBuild = viewVersion, false -- } -- telemetry.RecordViewGoVersion(viewVersion) -- } +-func TestsAndBenchmarks(pkg *cache.Package, pgf *parsego.File) (TestFns, error) { +- var out TestFns - -- if msg, mType := versionMessage(oldestVersion, fromBuild); msg != "" { -- s.eventuallyShowMessage(context.Background(), &protocol.ShowMessageParams{ -- Type: mType, -- Message: msg, -- }) +- if !strings.HasSuffix(pgf.URI.Path(), "_test.go") { +- return out, nil - } --} - --// go1Point returns the x in Go 1.x. If an error occurs extracting the go --// version, it returns -1. --// --// Copied from the testenv package. --func go1Point() int { -- for i := len(build.Default.ReleaseTags) - 1; i >= 0; i-- { -- var version int -- if _, err := fmt.Sscanf(build.Default.ReleaseTags[i], "go1.%d", &version); err != nil { +- for _, d := range pgf.File.Decls { +- fn, ok := d.(*ast.FuncDecl) +- if !ok { - continue - } -- return version -- } -- return -1 --} -- --func (s *Server) addFolders(ctx context.Context, folders []protocol.WorkspaceFolder) error { -- originalViews := len(s.session.Views()) -- viewErrors := make(map[span.URI]error) - -- var ndiagnose sync.WaitGroup // number of unfinished diagnose calls -- if s.Options().VerboseWorkDoneProgress { -- work := s.progress.Start(ctx, DiagnosticWorkTitle(FromInitialWorkspaceLoad), "Calculating diagnostics for initial workspace load...", nil, nil) -- defer func() { -- go func() { -- ndiagnose.Wait() -- work.End(ctx, "Done.") -- }() -- }() -- } -- // Only one view gets to have a workspace. -- var nsnapshots sync.WaitGroup // number of unfinished snapshot initializations -- for _, folder := range folders { -- uri := span.URIFromURI(folder.URI) -- // Ignore non-file URIs. -- if !uri.IsFile() { -- continue -- } -- work := s.progress.Start(ctx, "Setting up workspace", "Loading packages...", nil, nil) -- snapshot, release, err := s.addView(ctx, folder.Name, uri) +- rng, err := pgf.NodeRange(fn) - if err != nil { -- if err == source.ErrViewExists { -- continue -- } -- viewErrors[uri] = err -- work.End(ctx, fmt.Sprintf("Error loading packages: %s", err)) -- continue +- return out, err - } -- // Inv: release() must be called once. -- -- // Initialize snapshot asynchronously. -- initialized := make(chan struct{}) -- nsnapshots.Add(1) -- go func() { -- snapshot.AwaitInitialized(ctx) -- work.End(ctx, "Finished loading packages.") -- nsnapshots.Done() -- close(initialized) // signal -- }() -- -- // Diagnose the newly created view asynchronously. -- ndiagnose.Add(1) -- go func() { -- s.diagnoseSnapshot(snapshot, nil, false, 0) -- <-initialized -- release() -- ndiagnose.Done() -- }() -- } -- -- // Wait for snapshots to be initialized so that all files are known. -- // (We don't need to wait for diagnosis to finish.) -- nsnapshots.Wait() - -- // Register for file watching notifications, if they are supported. -- if err := s.updateWatchedDirectories(ctx); err != nil { -- event.Error(ctx, "failed to register for file watching notifications", err) -- } +- if matchTestFunc(fn, pkg, testRe, "T") { +- out.Tests = append(out.Tests, TestFn{fn.Name.Name, rng}) +- } - -- if len(viewErrors) > 0 { -- errMsg := fmt.Sprintf("Error loading workspace folders (expected %v, got %v)\n", len(folders), len(s.session.Views())-originalViews) -- for uri, err := range viewErrors { -- errMsg += fmt.Sprintf("failed to load view for %s: %v\n", uri, err) +- if matchTestFunc(fn, pkg, benchmarkRe, "B") { +- out.Benchmarks = append(out.Benchmarks, TestFn{fn.Name.Name, rng}) - } -- return s.client.ShowMessage(ctx, &protocol.ShowMessageParams{ -- Type: protocol.Error, -- Message: errMsg, -- }) - } -- return nil --} -- --// updateWatchedDirectories compares the current set of directories to watch --// with the previously registered set of directories. If the set of directories --// has changed, we unregister and re-register for file watching notifications. --// updatedSnapshots is the set of snapshots that have been updated. --func (s *Server) updateWatchedDirectories(ctx context.Context) error { -- patterns := s.session.FileWatchingGlobPatterns(ctx) -- -- s.watchedGlobPatternsMu.Lock() -- defer s.watchedGlobPatternsMu.Unlock() - -- // Nothing to do if the set of workspace directories is unchanged. -- if equalURISet(s.watchedGlobPatterns, patterns) { -- return nil -- } +- return out, nil +-} - -- // If the set of directories to watch has changed, register the updates and -- // unregister the previously watched directories. This ordering avoids a -- // period where no files are being watched. Still, if a user makes on-disk -- // changes before these updates are complete, we may miss them for the new -- // directories. -- prevID := s.watchRegistrationCount - 1 -- if err := s.registerWatchedDirectoriesLocked(ctx, patterns); err != nil { -- return err +-func matchTestFunc(fn *ast.FuncDecl, pkg *cache.Package, nameRe *regexp.Regexp, paramID string) bool { +- // Make sure that the function name matches a test function. +- if !nameRe.MatchString(fn.Name.Name) { +- return false - } -- if prevID >= 0 { -- return s.client.UnregisterCapability(ctx, &protocol.UnregistrationParams{ -- Unregisterations: []protocol.Unregistration{{ -- ID: watchedFilesCapabilityID(prevID), -- Method: "workspace/didChangeWatchedFiles", -- }}, -- }) +- info := pkg.TypesInfo() +- if info == nil { +- return false - } -- return nil --} -- --func watchedFilesCapabilityID(id int) string { -- return fmt.Sprintf("workspace/didChangeWatchedFiles-%d", id) --} -- --func equalURISet(m1, m2 map[string]struct{}) bool { -- if len(m1) != len(m2) { +- obj, ok := info.ObjectOf(fn.Name).(*types.Func) +- if !ok { - return false - } -- for k := range m1 { -- _, ok := m2[k] -- if !ok { -- return false -- } +- sig := obj.Type().(*types.Signature) +- // Test functions should have only one parameter. +- if sig.Params().Len() != 1 { +- return false - } -- return true --} - --// registerWatchedDirectoriesLocked sends the workspace/didChangeWatchedFiles --// registrations to the client and updates s.watchedDirectories. --// The caller must not subsequently mutate patterns. --func (s *Server) registerWatchedDirectoriesLocked(ctx context.Context, patterns map[string]struct{}) error { -- if !s.Options().DynamicWatchedFilesSupported { -- return nil +- // Check the type of the only parameter +- // (We don't Unalias or use typesinternal.ReceiverNamed +- // in the two checks below because "go test" can't see +- // through aliases when enumerating Test* functions; +- // it's syntactic.) +- paramTyp, ok := sig.Params().At(0).Type().(*types.Pointer) +- if !ok { +- return false - } -- s.watchedGlobPatterns = patterns -- watchers := make([]protocol.FileSystemWatcher, 0, len(patterns)) // must be a slice -- val := protocol.WatchChange | protocol.WatchDelete | protocol.WatchCreate -- for pattern := range patterns { -- watchers = append(watchers, protocol.FileSystemWatcher{ -- GlobPattern: pattern, -- Kind: &val, -- }) +- named, ok := paramTyp.Elem().(*types.Named) +- if !ok { +- return false - } -- -- if err := s.client.RegisterCapability(ctx, &protocol.RegistrationParams{ -- Registrations: []protocol.Registration{{ -- ID: watchedFilesCapabilityID(s.watchRegistrationCount), -- Method: "workspace/didChangeWatchedFiles", -- RegisterOptions: protocol.DidChangeWatchedFilesRegistrationOptions{ -- Watchers: watchers, -- }, -- }}, -- }); err != nil { -- return err +- namedObj := named.Obj() +- if namedObj.Pkg().Path() != "testing" { +- return false - } -- s.watchRegistrationCount++ -- return nil --} -- --// Options returns the current server options. --// --// The caller must not modify the result. --func (s *Server) Options() *source.Options { -- s.optionsMu.Lock() -- defer s.optionsMu.Unlock() -- return s.options --} -- --// SetOptions sets the current server options. --// --// The caller must not subsequently modify the options. --func (s *Server) SetOptions(opts *source.Options) { -- s.optionsMu.Lock() -- defer s.optionsMu.Unlock() -- s.options = opts +- return namedObj.Id() == paramID -} - --func (s *Server) fetchFolderOptions(ctx context.Context, folder span.URI) (*source.Options, error) { -- if opts := s.Options(); !opts.ConfigurationSupported { -- return opts, nil -- } -- configs, err := s.client.Configuration(ctx, &protocol.ParamConfiguration{ -- Items: []protocol.ConfigurationItem{{ -- ScopeURI: string(folder), -- Section: "gopls", -- }}, -- }, -- ) +-func goGenerateCodeLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) { +- pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) - if err != nil { -- return nil, fmt.Errorf("failed to get workspace configuration from client (%s): %v", folder, err) +- return nil, err - } +- const ggDirective = "//go:generate" +- for _, c := range pgf.File.Comments { +- for _, l := range c.List { +- if !strings.HasPrefix(l.Text, ggDirective) { +- continue +- } +- rng, err := pgf.PosRange(l.Pos(), l.Pos()+token.Pos(len(ggDirective))) +- if err != nil { +- return nil, err +- } +- dir := fh.URI().Dir() +- nonRecursiveCmd, err := command.NewGenerateCommand("run go generate", command.GenerateArgs{Dir: dir, Recursive: false}) +- if err != nil { +- return nil, err +- } +- recursiveCmd, err := command.NewGenerateCommand("run go generate ./...", command.GenerateArgs{Dir: dir, Recursive: true}) +- if err != nil { +- return nil, err +- } +- return []protocol.CodeLens{ +- {Range: rng, Command: &recursiveCmd}, +- {Range: rng, Command: &nonRecursiveCmd}, +- }, nil - -- folderOpts := s.Options().Clone() -- for _, config := range configs { -- if err := s.handleOptionResults(ctx, source.SetOptions(folderOpts, config)); err != nil { -- return nil, err - } - } -- return folderOpts, nil +- return nil, nil -} - --func (s *Server) eventuallyShowMessage(ctx context.Context, msg *protocol.ShowMessageParams) error { -- s.stateMu.Lock() -- defer s.stateMu.Unlock() -- if s.state == serverInitialized { -- return s.client.ShowMessage(ctx, msg) +-func regenerateCgoLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) { +- pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) +- if err != nil { +- return nil, err - } -- s.notifications = append(s.notifications, msg) -- return nil --} -- --func (s *Server) handleOptionResults(ctx context.Context, results source.OptionResults) error { -- var warnings, errors []string -- for _, result := range results { -- switch result.Error.(type) { -- case nil: -- // nothing to do -- case *source.SoftError: -- warnings = append(warnings, result.Error.Error()) -- default: -- errors = append(errors, result.Error.Error()) +- var c *ast.ImportSpec +- for _, imp := range pgf.File.Imports { +- if imp.Path.Value == `"C"` { +- c = imp - } - } -- -- // Sort messages, but put errors first. -- // -- // Having stable content for the message allows clients to de-duplicate. This -- // matters because we may send duplicate warnings for clients that support -- // dynamic configuration: one for the initial settings, and then more for the -- // individual view settings. -- var msgs []string -- msgType := protocol.Warning -- if len(errors) > 0 { -- msgType = protocol.Error -- sort.Strings(errors) -- msgs = append(msgs, errors...) +- if c == nil { +- return nil, nil - } -- if len(warnings) > 0 { -- sort.Strings(warnings) -- msgs = append(msgs, warnings...) +- rng, err := pgf.NodeRange(c) +- if err != nil { +- return nil, err - } -- -- if len(msgs) > 0 { -- // Settings -- combined := "Invalid settings: " + strings.Join(msgs, "; ") -- params := &protocol.ShowMessageParams{ -- Type: msgType, -- Message: combined, -- } -- return s.eventuallyShowMessage(ctx, params) +- puri := fh.URI() +- cmd, err := command.NewRegenerateCgoCommand("regenerate cgo definitions", command.URIArg{URI: puri}) +- if err != nil { +- return nil, err - } -- -- return nil +- return []protocol.CodeLens{{Range: rng, Command: &cmd}}, nil -} - --// beginFileRequest checks preconditions for a file-oriented request and routes --// it to a snapshot. --// We don't want to return errors for benign conditions like wrong file type, --// so callers should do if !ok { return err } rather than if err != nil. --// The returned cleanup function is non-nil even in case of false/error result. --func (s *Server) beginFileRequest(ctx context.Context, pURI protocol.DocumentURI, expectKind source.FileKind) (source.Snapshot, source.FileHandle, bool, func(), error) { -- uri := pURI.SpanURI() -- if !uri.IsFile() { -- // Not a file URI. Stop processing the request, but don't return an error. -- return nil, nil, false, func() {}, nil -- } -- view, err := s.session.ViewOf(uri) +-func toggleDetailsCodeLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) { +- pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) - if err != nil { -- return nil, nil, false, func() {}, err +- return nil, err - } -- snapshot, release, err := view.Snapshot() -- if err != nil { -- return nil, nil, false, func() {}, err +- if !pgf.File.Package.IsValid() { +- // Without a package name we have nowhere to put the codelens, so give up. +- return nil, nil - } -- fh, err := snapshot.ReadFile(ctx, uri) +- rng, err := pgf.PosRange(pgf.File.Package, pgf.File.Package) - if err != nil { -- release() -- return nil, nil, false, func() {}, err +- return nil, err - } -- if expectKind != source.UnknownKind && snapshot.FileKind(fh) != expectKind { -- // Wrong kind of file. Nothing to do. -- release() -- return nil, nil, false, func() {}, nil +- puri := fh.URI() +- cmd, err := command.NewGCDetailsCommand("Toggle gc annotation details", puri) +- if err != nil { +- return nil, err - } -- return snapshot, fh, true, release, nil +- return []protocol.CodeLens{{Range: rng, Command: &cmd}}, nil -} +diff -urN a/gopls/internal/golang/comment.go b/gopls/internal/golang/comment.go +--- a/gopls/internal/golang/comment.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/comment.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,41 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// shutdown implements the 'shutdown' LSP handler. It releases resources --// associated with the server and waits for all ongoing work to complete. --func (s *Server) shutdown(ctx context.Context) error { -- ctx, done := event.Start(ctx, "lsp.Server.shutdown") -- defer done() +-package golang - -- s.stateMu.Lock() -- defer s.stateMu.Unlock() -- if s.state < serverInitialized { -- event.Log(ctx, "server shutdown without initialization") -- } -- if s.state != serverShutDown { -- // drop all the active views -- s.session.Shutdown(ctx) -- s.state = serverShutDown -- if s.tempDir != "" { -- if err := os.RemoveAll(s.tempDir); err != nil { -- event.Error(ctx, "removing temp dir", err) +-import ( +- "fmt" +- "go/doc/comment" +- +- "golang.org/x/tools/gopls/internal/settings" +-) +- +-// CommentToMarkdown converts comment text to formatted markdown. +-// The comment was prepared by DocReader, +-// so it is known not to have leading, trailing blank lines +-// nor to have trailing spaces at the end of lines. +-// The comment markers have already been removed. +-func CommentToMarkdown(text string, options *settings.Options) string { +- var p comment.Parser +- doc := p.Parse(text) +- var pr comment.Printer +- // The default produces {#Hdr-...} tags for headings. +- // vscode displays thems, which is undesirable. +- // The godoc for comment.Printer says the tags +- // avoid a security problem. +- pr.HeadingID = func(*comment.Heading) string { return "" } +- pr.DocLinkURL = func(link *comment.DocLink) string { +- msg := fmt.Sprintf("https://%s/%s", options.LinkTarget, link.ImportPath) +- if link.Name != "" { +- msg += "#" +- if link.Recv != "" { +- msg += link.Recv + "." - } +- msg += link.Name - } +- return msg - } -- return nil +- easy := pr.Markdown(doc) +- return string(easy) -} +diff -urN a/gopls/internal/golang/completion/builtin.go b/gopls/internal/golang/completion/builtin.go +--- a/gopls/internal/golang/completion/builtin.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/completion/builtin.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,147 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func (s *Server) exit(ctx context.Context) error { -- ctx, done := event.Start(ctx, "lsp.Server.exit") -- defer done() -- -- s.stateMu.Lock() -- defer s.stateMu.Unlock() +-package completion - -- s.client.Close() +-import ( +- "context" +- "go/ast" +- "go/types" +-) - -- if s.state != serverShutDown { -- // TODO: We should be able to do better than this. -- os.Exit(1) +-// builtinArgKind determines the expected object kind for a builtin +-// argument. It attempts to use the AST hints from builtin.go where +-// possible. +-func (c *completer) builtinArgKind(ctx context.Context, obj types.Object, call *ast.CallExpr) objKind { +- builtin, err := c.snapshot.BuiltinFile(ctx) +- if err != nil { +- return 0 - } -- // We don't terminate the process on a normal exit, we just allow it to -- // close naturally if needed after the connection is closed. -- return nil --} +- exprIdx := exprAtPos(c.pos, call.Args) - --// TODO: when we can assume go1.18, replace with generic --// (after retiring support for go1.17) --func nonNilSliceString(x []string) []string { -- if x == nil { -- return []string{} +- builtinObj := builtin.File.Scope.Lookup(obj.Name()) +- if builtinObj == nil { +- return 0 - } -- return x --} --func nonNilSliceTextEdit(x []protocol.TextEdit) []protocol.TextEdit { -- if x == nil { -- return []protocol.TextEdit{} +- decl, ok := builtinObj.Decl.(*ast.FuncDecl) +- if !ok || exprIdx >= len(decl.Type.Params.List) { +- return 0 - } - -- return x --} --func nonNilSliceCompletionItemTag(x []protocol.CompletionItemTag) []protocol.CompletionItemTag { -- if x == nil { -- return []protocol.CompletionItemTag{} -- } -- return x --} --func emptySliceDiagnosticTag(x []protocol.DiagnosticTag) []protocol.DiagnosticTag { -- if x == nil { -- return []protocol.DiagnosticTag{} +- switch ptyp := decl.Type.Params.List[exprIdx].Type.(type) { +- case *ast.ChanType: +- return kindChan +- case *ast.ArrayType: +- return kindSlice +- case *ast.MapType: +- return kindMap +- case *ast.Ident: +- switch ptyp.Name { +- case "Type": +- switch obj.Name() { +- case "make": +- return kindChan | kindSlice | kindMap +- case "len": +- return kindSlice | kindMap | kindArray | kindString | kindChan +- case "cap": +- return kindSlice | kindArray | kindChan +- } +- } - } -- return x +- +- return 0 -} -diff -urN a/gopls/internal/lsp/general_test.go b/gopls/internal/lsp/general_test.go ---- a/gopls/internal/lsp/general_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/general_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,48 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package lsp +-// builtinArgType infers the type of an argument to a builtin +-// function. parentInf is the inferred type info for the builtin +-// call's parent node. +-func (c *completer) builtinArgType(obj types.Object, call *ast.CallExpr, parentInf candidateInference) candidateInference { +- var ( +- exprIdx = exprAtPos(c.pos, call.Args) - --import ( -- "strings" -- "testing" +- // Propagate certain properties from our parent's inference. +- inf = candidateInference{ +- typeName: parentInf.typeName, +- modifiers: parentInf.modifiers, +- } +- ) - -- "golang.org/x/tools/gopls/internal/lsp/protocol" --) +- switch obj.Name() { +- case "append": +- if exprIdx <= 0 { +- // Infer first append() arg type as apparent return type of +- // append(). +- inf.objType = parentInf.objType +- if parentInf.variadic { +- inf.objType = types.NewSlice(inf.objType) +- } +- break +- } - --func TestVersionMessage(t *testing.T) { -- tests := []struct { -- goVersion int -- fromBuild bool -- wantContains []string // string fragments that we expect to see -- wantType protocol.MessageType -- }{ -- {-1, false, nil, 0}, -- {12, false, []string{"1.12", "not supported", "upgrade to Go 1.18", "install gopls v0.7.5"}, protocol.Error}, -- {13, false, []string{"1.13", "not supported", "upgrade to Go 1.18", "install gopls v0.9.5"}, protocol.Error}, -- {15, false, []string{"1.15", "not supported", "upgrade to Go 1.18", "install gopls v0.9.5"}, protocol.Error}, -- {15, true, []string{"Gopls was built with Go version 1.15", "not supported", "upgrade to Go 1.18", "install gopls v0.9.5"}, protocol.Error}, -- {16, false, []string{"1.16", "will be unsupported by gopls v0.13.0", "upgrade to Go 1.18", "install gopls v0.11.0"}, protocol.Warning}, -- {17, false, []string{"1.17", "will be unsupported by gopls v0.13.0", "upgrade to Go 1.18", "install gopls v0.11.0"}, protocol.Warning}, -- {17, true, []string{"Gopls was built with Go version 1.17", "will be unsupported by gopls v0.13.0", "upgrade to Go 1.18", "install gopls v0.11.0"}, protocol.Warning}, -- } +- // For non-initial append() args, infer slice type from the first +- // append() arg, or from parent context. +- if len(call.Args) > 0 { +- inf.objType = c.pkg.TypesInfo().TypeOf(call.Args[0]) +- } +- if inf.objType == nil { +- inf.objType = parentInf.objType +- } +- if inf.objType == nil { +- break +- } - -- for _, test := range tests { -- gotMsg, gotType := versionMessage(test.goVersion, test.fromBuild) +- inf.objType = deslice(inf.objType) - -- if len(test.wantContains) == 0 && gotMsg != "" { -- t.Errorf("versionMessage(%d) = %q, want \"\"", test.goVersion, gotMsg) -- } +- // Check if we are completing the variadic append() param. +- inf.variadic = exprIdx == 1 && len(call.Args) <= 2 - -- for _, want := range test.wantContains { -- if !strings.Contains(gotMsg, want) { -- t.Errorf("versionMessage(%d) = %q, want containing %q", test.goVersion, gotMsg, want) +- // Penalize the first append() argument as a candidate. You +- // don't normally append a slice to itself. +- if sliceChain := objChain(c.pkg.TypesInfo(), call.Args[0]); len(sliceChain) > 0 { +- inf.penalized = append(inf.penalized, penalizedObj{objChain: sliceChain, penalty: 0.9}) +- } +- case "delete": +- if exprIdx > 0 && len(call.Args) > 0 { +- // Try to fill in expected type of map key. +- firstArgType := c.pkg.TypesInfo().TypeOf(call.Args[0]) +- if firstArgType != nil { +- if mt, ok := firstArgType.Underlying().(*types.Map); ok { +- inf.objType = mt.Key() +- } +- } +- } +- case "copy": +- var t1, t2 types.Type +- if len(call.Args) > 0 { +- t1 = c.pkg.TypesInfo().TypeOf(call.Args[0]) +- if len(call.Args) > 1 { +- t2 = c.pkg.TypesInfo().TypeOf(call.Args[1]) - } - } - -- if gotType != test.wantType { -- t.Errorf("versionMessage(%d) = returned message type %d, want %d", test.goVersion, gotType, test.wantType) +- // Fill in expected type of either arg if the other is already present. +- if exprIdx == 1 && t1 != nil { +- inf.objType = t1 +- } else if exprIdx == 0 && t2 != nil { +- inf.objType = t2 +- } +- case "new": +- inf.typeName.wantTypeName = true +- if parentInf.objType != nil { +- // Expected type for "new" is the de-pointered parent type. +- if ptr, ok := parentInf.objType.Underlying().(*types.Pointer); ok { +- inf.objType = ptr.Elem() +- } +- } +- case "make": +- if exprIdx == 0 { +- inf.typeName.wantTypeName = true +- inf.objType = parentInf.objType +- } else { +- inf.objType = types.Typ[types.UntypedInt] - } - } +- +- return inf -} -diff -urN a/gopls/internal/lsp/glob/glob.go b/gopls/internal/lsp/glob/glob.go ---- a/gopls/internal/lsp/glob/glob.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/glob/glob.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,349 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/golang/completion/completion.go b/gopls/internal/golang/completion/completion.go +--- a/gopls/internal/golang/completion/completion.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/completion/completion.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,3356 +0,0 @@ +-// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --// Package glob implements an LSP-compliant glob pattern matcher for testing. --package glob +-// Package completion provides core functionality for code completion in Go +-// editors and tools. +-package completion - -import ( -- "errors" +- "context" - "fmt" +- "go/ast" +- "go/build" +- "go/constant" +- "go/parser" +- "go/printer" +- "go/scanner" +- "go/token" +- "go/types" +- "math" +- "reflect" +- "sort" +- "strconv" - "strings" -- "unicode/utf8" +- "sync" +- "sync/atomic" +- "time" +- "unicode" +- +- "golang.org/x/sync/errgroup" +- "golang.org/x/tools/go/ast/astutil" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/golang/completion/snippet" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/settings" +- goplsastutil "golang.org/x/tools/gopls/internal/util/astutil" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/gopls/internal/util/slices" +- "golang.org/x/tools/gopls/internal/util/typesutil" +- "golang.org/x/tools/internal/aliases" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/fuzzy" +- "golang.org/x/tools/internal/imports" +- "golang.org/x/tools/internal/stdlib" +- "golang.org/x/tools/internal/typeparams" +- "golang.org/x/tools/internal/typesinternal" +- "golang.org/x/tools/internal/versions" -) - --// A Glob is an LSP-compliant glob pattern, as defined by the spec: --// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#documentFilter --// --// NOTE: this implementation is currently only intended for testing. In order --// to make it production ready, we'd need to: --// - verify it against the VS Code implementation --// - add more tests --// - microbenchmark, likely avoiding the element interface --// - resolve the question of what is meant by "character". If it's a UTF-16 --// code (as we suspect) it'll be a bit more work. --// --// Quoting from the spec: --// Glob patterns can have the following syntax: --// - `*` to match one or more characters in a path segment --// - `?` to match on one character in a path segment --// - `**` to match any number of path segments, including none --// - `{}` to group sub patterns into an OR expression. (e.g. `**/*.{ts,js}` --// matches all TypeScript and JavaScript files) --// - `[]` to declare a range of characters to match in a path segment --// (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) --// - `[!...]` to negate a range of characters to match in a path segment --// (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but --// not `example.0`) --// --// Expanding on this: --// - '/' matches one or more literal slashes. --// - any other character matches itself literally. --type Glob struct { -- elems []element // pattern elements --} +-// A CompletionItem represents a possible completion suggested by the algorithm. +-type CompletionItem struct { - --// Parse builds a Glob for the given pattern, returning an error if the pattern --// is invalid. --func Parse(pattern string) (*Glob, error) { -- g, _, err := parse(pattern, false) -- return g, err --} +- // Invariant: CompletionItem does not refer to syntax or types. - --func parse(pattern string, nested bool) (*Glob, string, error) { -- g := new(Glob) -- for len(pattern) > 0 { -- switch pattern[0] { -- case '/': -- pattern = pattern[1:] -- g.elems = append(g.elems, slash{}) +- // Label is the primary text the user sees for this completion item. +- Label string - -- case '*': -- if len(pattern) > 1 && pattern[1] == '*' { -- if (len(g.elems) > 0 && g.elems[len(g.elems)-1] != slash{}) || (len(pattern) > 2 && pattern[2] != '/') { -- return nil, "", errors.New("** may only be adjacent to '/'") -- } -- pattern = pattern[2:] -- g.elems = append(g.elems, starStar{}) -- break -- } -- pattern = pattern[1:] -- g.elems = append(g.elems, star{}) +- // Detail is supplemental information to present to the user. +- // This often contains the type or return type of the completion item. +- Detail string - -- case '?': -- pattern = pattern[1:] -- g.elems = append(g.elems, anyChar{}) +- // InsertText is the text to insert if this item is selected. +- // Any of the prefix that has already been typed is not trimmed. +- // The insert text does not contain snippets. +- InsertText string - -- case '{': -- var gs group -- for pattern[0] != '}' { -- pattern = pattern[1:] -- g, pat, err := parse(pattern, true) -- if err != nil { -- return nil, "", err -- } -- if len(pat) == 0 { -- return nil, "", errors.New("unmatched '{'") -- } -- pattern = pat -- gs = append(gs, g) -- } -- pattern = pattern[1:] -- g.elems = append(g.elems, gs) +- Kind protocol.CompletionItemKind +- Tags []protocol.CompletionItemTag +- Deprecated bool // Deprecated, prefer Tags if available - -- case '}', ',': -- if nested { -- return g, pattern, nil -- } -- pattern = g.parseLiteral(pattern, false) +- // An optional array of additional TextEdits that are applied when +- // selecting this completion. +- // +- // Additional text edits should be used to change text unrelated to the current cursor position +- // (for example adding an import statement at the top of the file if the completion item will +- // insert an unqualified type). +- AdditionalTextEdits []protocol.TextEdit - -- case '[': -- pattern = pattern[1:] -- if len(pattern) == 0 { -- return nil, "", errBadRange -- } -- negate := false -- if pattern[0] == '!' { -- pattern = pattern[1:] -- negate = true -- } -- low, sz, err := readRangeRune(pattern) -- if err != nil { -- return nil, "", err -- } -- pattern = pattern[sz:] -- if len(pattern) == 0 || pattern[0] != '-' { -- return nil, "", errBadRange -- } -- pattern = pattern[1:] -- high, sz, err := readRangeRune(pattern) -- if err != nil { -- return nil, "", err -- } -- pattern = pattern[sz:] -- if len(pattern) == 0 || pattern[0] != ']' { -- return nil, "", errBadRange -- } -- pattern = pattern[1:] -- g.elems = append(g.elems, charRange{negate, low, high}) +- // Depth is how many levels were searched to find this completion. +- // For example when completing "foo<>", "fooBar" is depth 0, and +- // "fooBar.Baz" is depth 1. +- Depth int - -- default: -- pattern = g.parseLiteral(pattern, nested) -- } -- } -- return g, "", nil +- // Score is the internal relevance score. +- // A higher score indicates that this completion item is more relevant. +- Score float64 +- +- // snippet is the LSP snippet for the completion item. The LSP +- // specification contains details about LSP snippets. For example, a +- // snippet for a function with the following signature: +- // +- // func foo(a, b, c int) +- // +- // would be: +- // +- // foo(${1:a int}, ${2: b int}, ${3: c int}) +- // +- // If Placeholders is false in the CompletionOptions, the above +- // snippet would instead be: +- // +- // foo(${1:}) +- snippet *snippet.Builder +- +- // Documentation is the documentation for the completion item. +- Documentation string +- +- // isSlice reports whether the underlying type of the object +- // from which this candidate was derived is a slice. +- // (Used to complete append() calls.) +- isSlice bool -} - --// helper for decoding a rune in range elements, e.g. [a-z] --func readRangeRune(input string) (rune, int, error) { -- r, sz := utf8.DecodeRuneInString(input) -- var err error -- if r == utf8.RuneError { -- // See the documentation for DecodeRuneInString. -- switch sz { -- case 0: -- err = errBadRange -- case 1: -- err = errInvalidUTF8 -- } +-// completionOptions holds completion specific configuration. +-type completionOptions struct { +- unimported bool +- documentation bool +- fullDocumentation bool +- placeholders bool +- snippets bool +- postfix bool +- matcher settings.Matcher +- budget time.Duration +- completeFunctionCalls bool +-} +- +-// Snippet is a convenience returns the snippet if available, otherwise +-// the InsertText. +-// used for an item, depending on if the callee wants placeholders or not. +-func (i *CompletionItem) Snippet() string { +- if i.snippet != nil { +- return i.snippet.String() - } -- return r, sz, err +- return i.InsertText -} - --var ( -- errBadRange = errors.New("'[' patterns must be of the form [x-y]") -- errInvalidUTF8 = errors.New("invalid UTF-8 encoding") +-// Scoring constants are used for weighting the relevance of different candidates. +-const ( +- // stdScore is the base score for all completion items. +- stdScore float64 = 1.0 +- +- // highScore indicates a very relevant completion item. +- highScore float64 = 10.0 +- +- // lowScore indicates an irrelevant or not useful completion item. +- lowScore float64 = 0.01 -) - --func (g *Glob) parseLiteral(pattern string, nested bool) string { -- var specialChars string -- if nested { -- specialChars = "*?{[/}," -- } else { -- specialChars = "*?{[/" -- } -- end := strings.IndexAny(pattern, specialChars) -- if end == -1 { -- end = len(pattern) -- } -- g.elems = append(g.elems, literal(pattern[:end])) -- return pattern[end:] +-// matcher matches a candidate's label against the user input. The +-// returned score reflects the quality of the match. A score of zero +-// indicates no match, and a score of one means a perfect match. +-type matcher interface { +- Score(candidateLabel string) (score float32) -} - --func (g *Glob) String() string { -- var b strings.Builder -- for _, e := range g.elems { -- fmt.Fprint(&b, e) +-// prefixMatcher implements case sensitive prefix matching. +-type prefixMatcher string +- +-func (pm prefixMatcher) Score(candidateLabel string) float32 { +- if strings.HasPrefix(candidateLabel, string(pm)) { +- return 1 - } -- return b.String() +- return -1 -} - --// element holds a glob pattern element, as defined below. --type element fmt.Stringer -- --// element types. --type ( -- slash struct{} // One or more '/' separators -- literal string // string literal, not containing /, *, ?, {}, or [] -- star struct{} // * -- anyChar struct{} // ? -- starStar struct{} // ** -- group []*Glob // {foo, bar, ...} grouping -- charRange struct { // [a-z] character range -- negate bool -- low, high rune -- } --) +-// insensitivePrefixMatcher implements case insensitive prefix matching. +-type insensitivePrefixMatcher string - --func (s slash) String() string { return "/" } --func (l literal) String() string { return string(l) } --func (s star) String() string { return "*" } --func (a anyChar) String() string { return "?" } --func (s starStar) String() string { return "**" } --func (g group) String() string { -- var parts []string -- for _, g := range g { -- parts = append(parts, g.String()) +-func (ipm insensitivePrefixMatcher) Score(candidateLabel string) float32 { +- if strings.HasPrefix(strings.ToLower(candidateLabel), string(ipm)) { +- return 1 - } -- return "{" + strings.Join(parts, ",") + "}" --} --func (r charRange) String() string { -- return "[" + string(r.low) + "-" + string(r.high) + "]" +- return -1 -} - --// Match reports whether the input string matches the glob pattern. --func (g *Glob) Match(input string) bool { -- return match(g.elems, input) --} -- --func match(elems []element, input string) (ok bool) { -- var elem interface{} -- for len(elems) > 0 { -- elem, elems = elems[0], elems[1:] -- switch elem := elem.(type) { -- case slash: -- if len(input) == 0 || input[0] != '/' { -- return false -- } -- for input[0] == '/' { -- input = input[1:] -- } -- -- case starStar: -- // Special cases: -- // - **/a matches "a" -- // - **/ matches everything -- // -- // Note that if ** is followed by anything, it must be '/' (this is -- // enforced by Parse). -- if len(elems) > 0 { -- elems = elems[1:] -- } -- -- // A trailing ** matches anything. -- if len(elems) == 0 { -- return true -- } -- -- // Backtracking: advance pattern segments until the remaining pattern -- // elements match. -- for len(input) != 0 { -- if match(elems, input) { -- return true -- } -- _, input = split(input) -- } -- return false +-// completer contains the necessary information for a single completion request. +-type completer struct { +- snapshot *cache.Snapshot +- pkg *cache.Package +- qf types.Qualifier // for qualifying typed expressions +- mq golang.MetadataQualifier // for syntactic qualifying +- opts *completionOptions - -- case literal: -- if !strings.HasPrefix(input, string(elem)) { -- return false -- } -- input = input[len(elem):] +- // completionContext contains information about the trigger for this +- // completion request. +- completionContext completionContext - -- case star: -- var segInput string -- segInput, input = split(input) +- // fh is a handle to the file associated with this completion request. +- fh file.Handle - -- elemEnd := len(elems) -- for i, e := range elems { -- if e == (slash{}) { -- elemEnd = i -- break -- } -- } -- segElems := elems[:elemEnd] -- elems = elems[elemEnd:] +- // filename is the name of the file associated with this completion request. +- filename string - -- // A trailing * matches the entire segment. -- if len(segElems) == 0 { -- break -- } +- // file is the AST of the file associated with this completion request. +- file *ast.File - -- // Backtracking: advance characters until remaining subpattern elements -- // match. -- matched := false -- for i := range segInput { -- if match(segElems, segInput[i:]) { -- matched = true -- break -- } -- } -- if !matched { -- return false -- } +- // goversion is the version of Go in force in the file, as +- // defined by x/tools/internal/versions. Empty if unknown. +- // TODO(adonovan): with go1.22+ it should always be known. +- goversion string - -- case anyChar: -- if len(input) == 0 || input[0] == '/' { -- return false -- } -- input = input[1:] +- // (tokFile, pos) is the position at which the request was triggered. +- tokFile *token.File +- pos token.Pos - -- case group: -- // Append remaining pattern elements to each group member looking for a -- // match. -- var branch []element -- for _, m := range elem { -- branch = branch[:0] -- branch = append(branch, m.elems...) -- branch = append(branch, elems...) -- if match(branch, input) { -- return true -- } -- } -- return false +- // path is the path of AST nodes enclosing the position. +- path []ast.Node - -- case charRange: -- if len(input) == 0 || input[0] == '/' { -- return false -- } -- c, sz := utf8.DecodeRuneInString(input) -- if c < elem.low || c > elem.high { -- return false -- } -- input = input[sz:] +- // seen is the map that ensures we do not return duplicate results. +- seen map[types.Object]bool - -- default: -- panic(fmt.Sprintf("segment type %T not implemented", elem)) -- } -- } +- // items is the list of completion items returned. +- items []CompletionItem - -- return len(input) == 0 --} +- // completionCallbacks is a list of callbacks to collect completions that +- // require expensive operations. This includes operations where we search +- // through the entire module cache. +- completionCallbacks []func(context.Context, *imports.Options) error - --// split returns the portion before and after the first slash --// (or sequence of consecutive slashes). If there is no slash --// it returns (input, nil). --func split(input string) (first, rest string) { -- i := strings.IndexByte(input, '/') -- if i < 0 { -- return input, "" -- } -- first = input[:i] -- for j := i; j < len(input); j++ { -- if input[j] != '/' { -- return first, input[j:] -- } -- } -- return first, "" --} -diff -urN a/gopls/internal/lsp/glob/glob_test.go b/gopls/internal/lsp/glob/glob_test.go ---- a/gopls/internal/lsp/glob/glob_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/glob/glob_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,118 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +- // surrounding describes the identifier surrounding the position. +- surrounding *Selection - --package glob_test +- // inference contains information we've inferred about ideal +- // candidates such as the candidate's type. +- inference candidateInference - --import ( -- "testing" +- // enclosingFunc contains information about the function enclosing +- // the position. +- enclosingFunc *funcInfo - -- "golang.org/x/tools/gopls/internal/lsp/glob" --) +- // enclosingCompositeLiteral contains information about the composite literal +- // enclosing the position. +- enclosingCompositeLiteral *compLitInfo - --func TestParseErrors(t *testing.T) { -- tests := []string{ -- "***", -- "ab{c", -- "[]", -- "[a-]", -- "ab{c{d}", -- } +- // deepState contains the current state of our deep completion search. +- deepState deepCompletionState - -- for _, test := range tests { -- _, err := glob.Parse(test) -- if err == nil { -- t.Errorf("Parse(%q) succeeded unexpectedly", test) -- } -- } --} +- // matcher matches the candidates against the surrounding prefix. +- matcher matcher - --func TestMatch(t *testing.T) { -- tests := []struct { -- pattern, input string -- want bool -- }{ -- // Basic cases. -- {"", "", true}, -- {"", "a", false}, -- {"", "/", false}, -- {"abc", "abc", true}, +- // methodSetCache caches the types.NewMethodSet call, which is relatively +- // expensive and can be called many times for the same type while searching +- // for deep completions. +- methodSetCache map[methodSetKey]*types.MethodSet - -- // ** behavior -- {"**", "abc", true}, -- {"**/abc", "abc", true}, -- {"**", "abc/def", true}, -- {"{a/**/c,a/**/d}", "a/b/c", true}, -- {"{a/**/c,a/**/d}", "a/b/c/d", true}, -- {"{a/**/c,a/**/e}", "a/b/c/d", false}, -- {"{a/**/c,a/**/e,a/**/d}", "a/b/c/d", true}, -- {"{/a/**/c,a/**/e,a/**/d}", "a/b/c/d", true}, -- {"{/a/**/c,a/**/e,a/**/d}", "/a/b/c/d", false}, -- {"{/a/**/c,a/**/e,a/**/d}", "/a/b/c", true}, -- {"{/a/**/e,a/**/e,a/**/d}", "/a/b/c", false}, +- // tooNewSymbolsCache is a cache of +- // [typesinternal.TooNewStdSymbols], recording for each std +- // package which of its exported symbols are too new for +- // the version of Go in force in the completion file. +- // (The value is the minimum version in the form "go1.%d".) +- tooNewSymbolsCache map[*types.Package]map[types.Object]string - -- // * and ? behavior -- {"/*", "/a", true}, -- {"*", "foo", true}, -- {"*o", "foo", true}, -- {"*o", "foox", false}, -- {"f*o", "foo", true}, -- {"f*o", "fo", true}, -- {"fo?", "foo", true}, -- {"fo?", "fox", true}, -- {"fo?", "fooo", false}, -- {"fo?", "fo", false}, -- {"?", "a", true}, -- {"?", "ab", false}, -- {"?", "", false}, -- {"*?", "", false}, -- {"?b", "ab", true}, -- {"?c", "ab", false}, +- // mapper converts the positions in the file from which the completion originated. +- mapper *protocol.Mapper - -- // {} behavior -- {"ab{c,d}e", "abce", true}, -- {"ab{c,d}e", "abde", true}, -- {"ab{c,d}e", "abxe", false}, -- {"ab{c,d}e", "abe", false}, -- {"{a,b}c", "ac", true}, -- {"{a,b}c", "bc", true}, -- {"{a,b}c", "ab", false}, -- {"a{b,c}", "ab", true}, -- {"a{b,c}", "ac", true}, -- {"a{b,c}", "bc", false}, -- {"ab{c{1,2},d}e", "abc1e", true}, -- {"ab{c{1,2},d}e", "abde", true}, -- {"ab{c{1,2},d}e", "abc1f", false}, -- {"ab{c{1,2},d}e", "abce", false}, -- {"ab{c[}-~]}d", "abc}d", true}, -- {"ab{c[}-~]}d", "abc~d", true}, -- {"ab{c[}-~],y}d", "abcxd", false}, -- {"ab{c[}-~],y}d", "abyd", true}, -- {"ab{c[}-~],y}d", "abd", false}, -- {"{a/b/c,d/e/f}", "a/b/c", true}, -- {"/ab{/c,d}e", "/ab/ce", true}, -- {"/ab{/c,d}e", "/ab/cf", false}, +- // startTime is when we started processing this completion request. It does +- // not include any time the request spent in the queue. +- // +- // Note: in CL 503016, startTime move to *after* type checking, but it was +- // subsequently determined that it was better to keep setting it *before* +- // type checking, so that the completion budget best approximates the user +- // experience. See golang/go#62665 for more details. +- startTime time.Time - -- // [-] behavior -- {"[a-c]", "a", true}, -- {"[a-c]", "b", true}, -- {"[a-c]", "c", true}, -- {"[a-c]", "d", false}, -- {"[a-c]", " ", false}, +- // scopes contains all scopes defined by nodes in our path, +- // including nil values for nodes that don't defined a scope. It +- // also includes our package scope and the universal scope at the +- // end. +- scopes []*types.Scope +-} - -- // Realistic examples. -- {"**/*.{ts,js}", "path/to/foo.ts", true}, -- {"**/*.{ts,js}", "path/to/foo.js", true}, -- {"**/*.{ts,js}", "path/to/foo.go", false}, +-// tooNew reports whether obj is a standard library symbol that is too +-// new for the specified Go version. +-func (c *completer) tooNew(obj types.Object) bool { +- pkg := obj.Pkg() +- if pkg == nil { +- return false // unsafe.Pointer or error.Error - } -- -- for _, test := range tests { -- g, err := glob.Parse(test.pattern) -- if err != nil { -- t.Fatalf("New(%q) failed unexpectedly: %v", test.pattern, err) -- } -- if got := g.Match(test.input); got != test.want { -- t.Errorf("New(%q).Match(%q) = %t, want %t", test.pattern, test.input, got, test.want) -- } +- disallowed, ok := c.tooNewSymbolsCache[pkg] +- if !ok { +- disallowed = typesinternal.TooNewStdSymbols(pkg, c.goversion) +- c.tooNewSymbolsCache[pkg] = disallowed - } +- return disallowed[obj] != "" -} -diff -urN a/gopls/internal/lsp/helper/helper.go b/gopls/internal/lsp/helper/helper.go ---- a/gopls/internal/lsp/helper/helper.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/helper/helper.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,264 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --// The helper command generates the declaration of the concrete --// 'server' type that implements the abstract Server interface defined --// in protocol/tsserver.go (which is itself generated from the LSP --// protocol). --// --// To run, invoke "go generate" in the parent (lsp) directory. --// --// TODO(adonovan): merge this into the main LSP generator. --package main +-// funcInfo holds info about a function object. +-type funcInfo struct { +- // sig is the function declaration enclosing the position. +- sig *types.Signature - --import ( -- "bytes" -- "flag" -- "fmt" -- "go/ast" -- "go/format" -- "go/parser" -- "go/token" -- "log" -- "os" -- "sort" -- "strings" -- "text/template" --) +- // body is the function's body. +- body *ast.BlockStmt +-} - --var ( -- typ = flag.String("t", "Server", "generate code for this type") -- def = flag.String("d", "", "the file the type is defined in") // this relies on punning -- use = flag.String("u", "", "look for uses in this package") -- out = flag.String("o", "", "where to write the generated file") --) +-type compLitInfo struct { +- // cl is the *ast.CompositeLit enclosing the position. +- cl *ast.CompositeLit - --func main() { -- log.SetFlags(log.Lshortfile) -- flag.Parse() -- if *typ == "" || *def == "" || *use == "" || *out == "" { -- flag.PrintDefaults() -- os.Exit(1) -- } -- // read the type definition and see what methods we're looking for -- doTypes() +- // clType is the type of cl. +- clType types.Type - -- // parse the package and see which methods are defined -- doUses() +- // kv is the *ast.KeyValueExpr enclosing the position, if any. +- kv *ast.KeyValueExpr +- +- // inKey is true if we are certain the position is in the key side +- // of a key-value pair. +- inKey bool - -- output() +- // maybeInFieldName is true if inKey is false and it is possible +- // we are completing a struct field name. For example, +- // "SomeStruct{<>}" will be inKey=false, but maybeInFieldName=true +- // because we _could_ be completing a field name. +- maybeInFieldName bool -} - --// replace "\\\n" with nothing before using --var tmpl = `// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-type importInfo struct { +- importPath string +- name string +-} - --package lsp +-type methodSetKey struct { +- typ types.Type +- addressable bool +-} - --// Code generated by gopls/internal/lsp/helper. DO NOT EDIT. +-type completionContext struct { +- // triggerCharacter is the character used to trigger completion at current +- // position, if any. +- triggerCharacter string - --import ( -- "context" +- // triggerKind is information about how a completion was triggered. +- triggerKind protocol.CompletionTriggerKind - -- "golang.org/x/tools/gopls/internal/lsp/protocol" --) +- // commentCompletion is true if we are completing a comment. +- commentCompletion bool - --{{range $key, $v := .Stuff}} --func (s *{{$.Type}}) {{$v.Name}}({{.Param}}) {{.Result}} { -- {{if ne .Found ""}} return s.{{.Internal}}({{.Invoke}})\ -- {{else}}return {{if lt 1 (len .Results)}}nil, {{end}}notImplemented("{{.Name}}"){{end}} +- // packageCompletion is true if we are completing a package name. +- packageCompletion bool -} --{{end}} --` -- --func output() { -- // put in empty param names as needed -- for _, t := range types { -- if t.paramnames == nil { -- t.paramnames = make([]string, len(t.paramtypes)) -- } -- for i, p := range t.paramtypes { -- cm := "" -- if i > 0 { -- cm = ", " -- } -- t.Param += fmt.Sprintf("%s%s %s", cm, t.paramnames[i], p) -- this := t.paramnames[i] -- if this == "_" { -- this = "nil" -- } -- t.Invoke += fmt.Sprintf("%s%s", cm, this) -- } -- if len(t.Results) > 1 { -- t.Result = "(" -- } -- for i, r := range t.Results { -- cm := "" -- if i > 0 { -- cm = ", " -- } -- t.Result += fmt.Sprintf("%s%s", cm, r) -- } -- if len(t.Results) > 1 { -- t.Result += ")" -- } -- } - -- fd, err := os.Create(*out) -- if err != nil { -- log.Fatal(err) -- } -- t, err := template.New("foo").Parse(tmpl) -- if err != nil { -- log.Fatal(err) -- } -- type par struct { -- Type string -- Stuff []*Function -- } -- p := par{*typ, types} -- if false { // debugging the template -- t.Execute(os.Stderr, &p) -- } -- buf := bytes.NewBuffer(nil) -- err = t.Execute(buf, &p) -- if err != nil { -- log.Fatal(err) -- } -- ans, err := format.Source(bytes.Replace(buf.Bytes(), []byte("\\\n"), []byte{}, -1)) -- if err != nil { -- log.Fatal(err) -- } -- fd.Write(ans) +-// A Selection represents the cursor position and surrounding identifier. +-type Selection struct { +- content string +- tokFile *token.File +- start, end, cursor token.Pos // relative to rng.TokFile +- mapper *protocol.Mapper -} - --func doUses() { -- fset := token.NewFileSet() -- pkgs, err := parser.ParseDir(fset, *use, nil, 0) -- if err != nil { -- log.Fatalf("%q:%v", *use, err) -- } -- pkg := pkgs["lsp"] // CHECK -- files := pkg.Files -- for fname, f := range files { -- for _, d := range f.Decls { -- fd, ok := d.(*ast.FuncDecl) -- if !ok { -- continue -- } -- nm := fd.Name.String() -- if ast.IsExported(nm) { -- // we're looking for things like didChange -- continue -- } -- if fx, ok := byname[nm]; ok { -- if fx.Found != "" { -- log.Fatalf("found %s in %s and %s", fx.Internal, fx.Found, fname) -- } -- fx.Found = fname -- // and the Paramnames -- ft := fd.Type -- for _, f := range ft.Params.List { -- nm := "" -- if len(f.Names) > 0 { -- nm = f.Names[0].String() -- if nm == "_" { -- nm = "_gen" -- } -- } -- fx.paramnames = append(fx.paramnames, nm) -- } -- } -- } -- } -- if false { -- for i, f := range types { -- log.Printf("%d %s %s", i, f.Internal, f.Found) -- } -- } +-func (p Selection) Range() (protocol.Range, error) { +- return p.mapper.PosRange(p.tokFile, p.start, p.end) -} - --type Function struct { -- Name string -- Internal string // first letter lower case -- paramtypes []string -- paramnames []string -- Results []string -- Param string -- Result string // do it in code, easier than in a template -- Invoke string -- Found string // file it was found in +-func (p Selection) Prefix() string { +- return p.content[:p.cursor-p.start] -} - --var types []*Function --var byname = map[string]*Function{} // internal names +-func (p Selection) Suffix() string { +- return p.content[p.cursor-p.start:] +-} - --func doTypes() { -- fset := token.NewFileSet() -- f, err := parser.ParseFile(fset, *def, nil, 0) -- if err != nil { -- log.Fatal(err) +-func (c *completer) setSurrounding(ident *ast.Ident) { +- if c.surrounding != nil { +- return - } -- fd, err := os.Create("/tmp/ast") -- if err != nil { -- log.Fatal(err) +- if !(ident.Pos() <= c.pos && c.pos <= ident.End()) { +- return - } -- ast.Fprint(fd, fset, f, ast.NotNilFilter) -- ast.Inspect(f, inter) -- sort.Slice(types, func(i, j int) bool { return types[i].Name < types[j].Name }) -- if false { -- for i, f := range types { -- log.Printf("%d %s(%v) %v", i, f.Name, f.paramtypes, f.Results) -- } +- +- c.surrounding = &Selection{ +- content: ident.Name, +- cursor: c.pos, +- // Overwrite the prefix only. +- tokFile: c.tokFile, +- start: ident.Pos(), +- end: ident.End(), +- mapper: c.mapper, - } +- +- c.setMatcherFromPrefix(c.surrounding.Prefix()) -} - --func inter(n ast.Node) bool { -- x, ok := n.(*ast.TypeSpec) -- if !ok || x.Name.Name != *typ { -- return true -- } -- m := x.Type.(*ast.InterfaceType).Methods.List -- for _, fld := range m { -- fn := fld.Type.(*ast.FuncType) -- p := fn.Params.List -- r := fn.Results.List -- fx := &Function{ -- Name: fld.Names[0].String(), -- } -- fx.Internal = strings.ToLower(fx.Name[:1]) + fx.Name[1:] -- for _, f := range p { -- fx.paramtypes = append(fx.paramtypes, whatis(f.Type)) -- } -- for _, f := range r { -- fx.Results = append(fx.Results, whatis(f.Type)) -- } -- types = append(types, fx) -- byname[fx.Internal] = fx +-func (c *completer) setMatcherFromPrefix(prefix string) { +- switch c.opts.matcher { +- case settings.Fuzzy: +- c.matcher = fuzzy.NewMatcher(prefix) +- case settings.CaseSensitive: +- c.matcher = prefixMatcher(prefix) +- default: +- c.matcher = insensitivePrefixMatcher(strings.ToLower(prefix)) - } -- return false -} - --func whatis(x ast.Expr) string { -- switch n := x.(type) { -- case *ast.SelectorExpr: -- return whatis(n.X) + "." + n.Sel.String() -- case *ast.StarExpr: -- return "*" + whatis(n.X) -- case *ast.Ident: -- if ast.IsExported(n.Name) { -- // these are from package protocol -- return "protocol." + n.Name +-func (c *completer) getSurrounding() *Selection { +- if c.surrounding == nil { +- c.surrounding = &Selection{ +- content: "", +- cursor: c.pos, +- tokFile: c.tokFile, +- start: c.pos, +- end: c.pos, +- mapper: c.mapper, - } -- return n.Name -- case *ast.ArrayType: -- return "[]" + whatis(n.Elt) -- case *ast.InterfaceType: -- return "interface{}" -- default: -- log.Fatalf("Fatal %T", x) -- return fmt.Sprintf("%T", x) - } +- return c.surrounding -} -diff -urN a/gopls/internal/lsp/helper/README.md b/gopls/internal/lsp/helper/README.md ---- a/gopls/internal/lsp/helper/README.md 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/helper/README.md 1970-01-01 00:00:00.000000000 +0000 -@@ -1,35 +0,0 @@ --# Generate server_gen.go -- --`helper` generates the file `../server_gen.go` (in package --`internal/lsp`) which contains stub declarations of server methods. - --To invoke it, run `go generate` in the `gopls/internal/lsp` directory. +-// candidate represents a completion candidate. +-type candidate struct { +- // obj is the types.Object to complete to. +- // TODO(adonovan): eliminate dependence on go/types throughout this struct. +- // See comment in (*completer).selector for explanation. +- obj types.Object - --It is derived from `gopls/internal/lsp/protocol/tsserver.go`, which --itself is generated from the protocol downloaded from VSCode, so be --sure to run `go generate` in the protocol first. Or run `go generate --./...` twice in the gopls directory. +- // score is used to rank candidates. +- score float64 - --It decides what stubs are needed and their signatures --by looking at the `Server` interface (`-t` flag). These all look somewhat like --`Resolve(context.Context, *CompletionItem) (*CompletionItem, error)`. +- // name is the deep object name path, e.g. "foo.bar" +- name string - --It then parses the `lsp` directory (`-u` flag) to see if there is a corresponding --implementation function (which in this case would be named `resolve`). If so --it discovers the parameter names needed, and generates (in `server_gen.go`) code --like +- // detail is additional information about this item. If not specified, +- // defaults to type string for the object. +- detail string - --``` go --func (s *Server) resolve(ctx context.Context, params *protocol.CompletionItem) (*protocol.CompletionItem, error) { -- return s.resolve(ctx, params) --} --``` +- // path holds the path from the search root (excluding the candidate +- // itself) for a deep candidate. +- path []types.Object - --If `resolve` is not defined (and it is not), then the body of the generated function is +- // pathInvokeMask is a bit mask tracking whether each entry in path +- // should be formatted with "()" (i.e. whether it is a function +- // invocation). +- pathInvokeMask uint16 - --```go -- return nil, notImplemented("resolve") --``` +- // mods contains modifications that should be applied to the +- // candidate when inserted. For example, "foo" may be inserted as +- // "*foo" or "foo()". +- mods []typeModKind - --So to add a capability currently not implemented, just define it somewhere in `lsp`. --In this case, just define `func (s *Server) resolve(...)` and re-generate `server_gen.go`. -diff -urN a/gopls/internal/lsp/highlight.go b/gopls/internal/lsp/highlight.go ---- a/gopls/internal/lsp/highlight.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/highlight.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,48 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +- // addressable is true if a pointer can be taken to the candidate. +- addressable bool - --package lsp +- // convertTo is a type that this candidate should be cast to. For +- // example, if convertTo is float64, "foo" should be formatted as +- // "float64(foo)". +- convertTo types.Type - --import ( -- "context" -- -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/lsp/template" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/tag" --) -- --func (s *Server) documentHighlight(ctx context.Context, params *protocol.DocumentHighlightParams) ([]protocol.DocumentHighlight, error) { -- ctx, done := event.Start(ctx, "lsp.Server.documentHighlight", tag.URI.Of(params.TextDocument.URI)) -- defer done() -- -- snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go) -- defer release() -- if !ok { -- return nil, err -- } +- // imp is the import that needs to be added to this package in order +- // for this candidate to be valid. nil if no import needed. +- imp *importInfo +-} - -- if snapshot.FileKind(fh) == source.Tmpl { -- return template.Highlight(ctx, snapshot, fh, params.Position) +-func (c candidate) hasMod(mod typeModKind) bool { +- for _, m := range c.mods { +- if m == mod { +- return true +- } - } +- return false +-} - -- rngs, err := source.Highlight(ctx, snapshot, fh, params.Position) -- if err != nil { -- event.Error(ctx, "no highlight", err) -- } -- return toProtocolHighlight(rngs), nil +-// ErrIsDefinition is an error that informs the user they got no +-// completions because they tried to complete the name of a new object +-// being defined. +-type ErrIsDefinition struct { +- objStr string -} - --func toProtocolHighlight(rngs []protocol.Range) []protocol.DocumentHighlight { -- result := make([]protocol.DocumentHighlight, 0, len(rngs)) -- kind := protocol.Text -- for _, rng := range rngs { -- result = append(result, protocol.DocumentHighlight{ -- Kind: kind, -- Range: rng, -- }) +-func (e ErrIsDefinition) Error() string { +- msg := "this is a definition" +- if e.objStr != "" { +- msg += " of " + e.objStr - } -- return result +- return msg -} -diff -urN a/gopls/internal/lsp/hover.go b/gopls/internal/lsp/hover.go ---- a/gopls/internal/lsp/hover.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/hover.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,45 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package lsp -- --import ( -- "context" - -- "golang.org/x/tools/gopls/internal/lsp/mod" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/lsp/template" -- "golang.org/x/tools/gopls/internal/lsp/work" -- "golang.org/x/tools/gopls/internal/telemetry" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/tag" --) +-// Completion returns a list of possible candidates for completion, given a +-// a file and a position. +-// +-// The selection is computed based on the preceding identifier and can be used by +-// the client to score the quality of the completion. For instance, some clients +-// may tolerate imperfect matches as valid completion results, since users may make typos. +-func Completion(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, protoPos protocol.Position, protoContext protocol.CompletionContext) ([]CompletionItem, *Selection, error) { +- ctx, done := event.Start(ctx, "completion.Completion") +- defer done() - --func (s *Server) hover(ctx context.Context, params *protocol.HoverParams) (_ *protocol.Hover, rerr error) { -- recordLatency := telemetry.StartLatencyTimer("hover") -- defer func() { -- recordLatency(ctx, rerr) -- }() +- startTime := time.Now() - -- ctx, done := event.Start(ctx, "lsp.Server.hover", tag.URI.Of(params.TextDocument.URI)) -- defer done() +- pkg, pgf, err := golang.NarrowestPackageForFile(ctx, snapshot, fh.URI()) +- if err != nil || pgf.File.Package == token.NoPos { +- // If we can't parse this file or find position for the package +- // keyword, it may be missing a package declaration. Try offering +- // suggestions for the package declaration. +- // Note that this would be the case even if the keyword 'package' is +- // present but no package name exists. +- items, surrounding, innerErr := packageClauseCompletions(ctx, snapshot, fh, protoPos) +- if innerErr != nil { +- // return the error for GetParsedFile since it's more relevant in this situation. +- return nil, nil, fmt.Errorf("getting file %s for Completion: %v (package completions: %v)", fh.URI(), err, innerErr) +- } +- return items, surrounding, nil +- } - -- snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind) -- defer release() -- if !ok { -- return nil, err +- pos, err := pgf.PositionPos(protoPos) +- if err != nil { +- return nil, nil, err - } -- switch snapshot.FileKind(fh) { -- case source.Mod: -- return mod.Hover(ctx, snapshot, fh, params.Position) -- case source.Go: -- return source.Hover(ctx, snapshot, fh, params.Position) -- case source.Tmpl: -- return template.Hover(ctx, snapshot, fh, params.Position) -- case source.Work: -- return work.Hover(ctx, snapshot, fh, params.Position) +- // Completion is based on what precedes the cursor. +- // Find the path to the position before pos. +- path, _ := astutil.PathEnclosingInterval(pgf.File, pos-1, pos-1) +- if path == nil { +- return nil, nil, fmt.Errorf("cannot find node enclosing position") - } -- return nil, nil --} -diff -urN a/gopls/internal/lsp/implementation.go b/gopls/internal/lsp/implementation.go ---- a/gopls/internal/lsp/implementation.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/implementation.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,32 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package lsp +- // Check if completion at this position is valid. If not, return early. +- switch n := path[0].(type) { +- case *ast.BasicLit: +- // Skip completion inside literals except for ImportSpec +- if len(path) > 1 { +- if _, ok := path[1].(*ast.ImportSpec); ok { +- break +- } +- } +- return nil, nil, nil +- case *ast.CallExpr: +- if n.Ellipsis.IsValid() && pos > n.Ellipsis && pos <= n.Ellipsis+token.Pos(len("...")) { +- // Don't offer completions inside or directly after "...". For +- // example, don't offer completions at "<>" in "foo(bar...<>"). +- return nil, nil, nil +- } +- case *ast.Ident: +- // reject defining identifiers +- if obj, ok := pkg.TypesInfo().Defs[n]; ok { +- if v, ok := obj.(*types.Var); ok && v.IsField() && v.Embedded() { +- // An anonymous field is also a reference to a type. +- } else if pgf.File.Name == n { +- // Don't skip completions if Ident is for package name. +- break +- } else { +- objStr := "" +- if obj != nil { +- qual := types.RelativeTo(pkg.Types()) +- objStr = types.ObjectString(obj, qual) +- } +- ans, sel := definition(path, obj, pgf) +- if ans != nil { +- sort.Slice(ans, func(i, j int) bool { +- return ans[i].Score > ans[j].Score +- }) +- return ans, sel, nil +- } +- return nil, nil, ErrIsDefinition{objStr: objStr} +- } +- } +- } - --import ( -- "context" +- // Collect all surrounding scopes, innermost first. +- scopes := golang.CollectScopes(pkg.TypesInfo(), path, pos) +- scopes = append(scopes, pkg.Types().Scope(), types.Universe) - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/telemetry" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/tag" --) +- var goversion string // "" => no version check +- // Prior go1.22, the behavior of FileVersion is not useful to us. +- if slices.Contains(build.Default.ReleaseTags, "go1.22") { +- goversion = versions.FileVersion(pkg.TypesInfo(), pgf.File) // may be "" +- } - --func (s *Server) implementation(ctx context.Context, params *protocol.ImplementationParams) (_ []protocol.Location, rerr error) { -- recordLatency := telemetry.StartLatencyTimer("implementation") -- defer func() { -- recordLatency(ctx, rerr) -- }() +- opts := snapshot.Options() +- c := &completer{ +- pkg: pkg, +- snapshot: snapshot, +- qf: typesutil.FileQualifier(pgf.File, pkg.Types(), pkg.TypesInfo()), +- mq: golang.MetadataQualifierForFile(snapshot, pgf.File, pkg.Metadata()), +- completionContext: completionContext{ +- triggerCharacter: protoContext.TriggerCharacter, +- triggerKind: protoContext.TriggerKind, +- }, +- fh: fh, +- filename: fh.URI().Path(), +- tokFile: pgf.Tok, +- file: pgf.File, +- goversion: goversion, +- path: path, +- pos: pos, +- seen: make(map[types.Object]bool), +- enclosingFunc: enclosingFunction(path, pkg.TypesInfo()), +- enclosingCompositeLiteral: enclosingCompositeLiteral(path, pos, pkg.TypesInfo()), +- deepState: deepCompletionState{ +- enabled: opts.DeepCompletion, +- }, +- opts: &completionOptions{ +- matcher: opts.Matcher, +- unimported: opts.CompleteUnimported, +- documentation: opts.CompletionDocumentation && opts.HoverKind != settings.NoDocumentation, +- fullDocumentation: opts.HoverKind == settings.FullDocumentation, +- placeholders: opts.UsePlaceholders, +- budget: opts.CompletionBudget, +- snippets: opts.InsertTextFormat == protocol.SnippetTextFormat, +- postfix: opts.ExperimentalPostfixCompletions, +- completeFunctionCalls: opts.CompleteFunctionCalls, +- }, +- // default to a matcher that always matches +- matcher: prefixMatcher(""), +- methodSetCache: make(map[methodSetKey]*types.MethodSet), +- tooNewSymbolsCache: make(map[*types.Package]map[types.Object]string), +- mapper: pgf.Mapper, +- startTime: startTime, +- scopes: scopes, +- } - -- ctx, done := event.Start(ctx, "lsp.Server.implementation", tag.URI.Of(params.TextDocument.URI)) -- defer done() +- ctx, cancel := context.WithCancel(ctx) +- defer cancel() - -- snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go) -- defer release() -- if !ok { -- return nil, err +- // Compute the deadline for this operation. Deadline is relative to the +- // search operation, not the entire completion RPC, as the work up until this +- // point depends significantly on how long it took to type-check, which in +- // turn depends on the timing of the request relative to other operations on +- // the snapshot. Including that work in the budget leads to inconsistent +- // results (and realistically, if type-checking took 200ms already, the user +- // is unlikely to be significantly more bothered by e.g. another 100ms of +- // search). +- // +- // Don't overload the context with this deadline, as we don't want to +- // conflate user cancellation (=fail the operation) with our time limit +- // (=stop searching and succeed with partial results). +- var deadline *time.Time +- if c.opts.budget > 0 { +- d := startTime.Add(c.opts.budget) +- deadline = &d - } -- return source.Implementation(ctx, snapshot, fh, params.Position) --} -diff -urN a/gopls/internal/lsp/inlay_hint.go b/gopls/internal/lsp/inlay_hint.go ---- a/gopls/internal/lsp/inlay_hint.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/inlay_hint.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,33 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package lsp +- if surrounding := c.containingIdent(pgf.Src); surrounding != nil { +- c.setSurrounding(surrounding) +- } - --import ( -- "context" +- c.inference = expectedCandidate(ctx, c) - -- "golang.org/x/tools/gopls/internal/lsp/mod" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/tag" --) +- err = c.collectCompletions(ctx) +- if err != nil { +- return nil, nil, err +- } - --func (s *Server) inlayHint(ctx context.Context, params *protocol.InlayHintParams) ([]protocol.InlayHint, error) { -- ctx, done := event.Start(ctx, "lsp.Server.inlayHint", tag.URI.Of(params.TextDocument.URI)) -- defer done() +- // Deep search collected candidates and their members for more candidates. +- c.deepSearch(ctx, 1, deadline) - -- snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind) -- defer release() -- if !ok { -- return nil, err -- } -- switch snapshot.FileKind(fh) { -- case source.Mod: -- return mod.InlayHint(ctx, snapshot, fh, params.Range) -- case source.Go: -- return source.InlayHint(ctx, snapshot, fh, params.Range) +- // At this point we have a sufficiently complete set of results, and want to +- // return as close to the completion budget as possible. Previously, we +- // avoided cancelling the context because it could result in partial results +- // for e.g. struct fields. At this point, we have a minimal valid set of +- // candidates, and so truncating due to context cancellation is acceptable. +- if c.opts.budget > 0 { +- timeoutDuration := time.Until(c.startTime.Add(c.opts.budget)) +- ctx, cancel = context.WithTimeout(ctx, timeoutDuration) +- defer cancel() - } -- return nil, nil --} -diff -urN a/gopls/internal/lsp/link.go b/gopls/internal/lsp/link.go ---- a/gopls/internal/lsp/link.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/link.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,280 +0,0 @@ --// Copyright 2018 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package lsp -- --import ( -- "bytes" -- "context" -- "fmt" -- "go/ast" -- "go/token" -- "net/url" -- "regexp" -- "strings" -- "sync" +- for _, callback := range c.completionCallbacks { +- if deadline == nil || time.Now().Before(*deadline) { +- if err := c.snapshot.RunProcessEnvFunc(ctx, callback); err != nil { +- return nil, nil, err +- } +- } +- } - -- "golang.org/x/mod/modfile" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/tag" --) +- // Search candidates populated by expensive operations like +- // unimportedMembers etc. for more completion items. +- c.deepSearch(ctx, 0, deadline) - --func (s *Server) documentLink(ctx context.Context, params *protocol.DocumentLinkParams) (links []protocol.DocumentLink, err error) { -- ctx, done := event.Start(ctx, "lsp.Server.documentLink") -- defer done() +- // Statement candidates offer an entire statement in certain contexts, as +- // opposed to a single object. Add statement candidates last because they +- // depend on other candidates having already been collected. +- c.addStatementCandidates() - -- snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind) -- defer release() -- if !ok { -- return nil, err -- } -- switch snapshot.FileKind(fh) { -- case source.Mod: -- links, err = modLinks(ctx, snapshot, fh) -- case source.Go: -- links, err = goLinks(ctx, snapshot, fh) -- } -- // Don't return errors for document links. -- if err != nil { -- event.Error(ctx, "failed to compute document links", err, tag.URI.Of(fh.URI())) -- return nil, nil -- } -- return links, nil +- c.sortItems() +- return c.items, c.getSurrounding(), nil -} - --func modLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.DocumentLink, error) { -- pm, err := snapshot.ParseMod(ctx, fh) -- if err != nil { -- return nil, err -- } -- -- var links []protocol.DocumentLink -- for _, req := range pm.File.Require { -- if req.Syntax == nil { -- continue -- } -- // See golang/go#36998: don't link to modules matching GOPRIVATE. -- if snapshot.View().IsGoPrivatePath(req.Mod.Path) { +-// collectCompletions adds possible completion candidates to either the deep +-// search queue or completion items directly for different completion contexts. +-func (c *completer) collectCompletions(ctx context.Context) error { +- // Inside import blocks, return completions for unimported packages. +- for _, importSpec := range c.file.Imports { +- if !(importSpec.Path.Pos() <= c.pos && c.pos <= importSpec.Path.End()) { - continue - } -- dep := []byte(req.Mod.Path) -- start, end := req.Syntax.Start.Byte, req.Syntax.End.Byte -- i := bytes.Index(pm.Mapper.Content[start:end], dep) -- if i == -1 { -- continue +- return c.populateImportCompletions(importSpec) +- } +- +- // Inside comments, offer completions for the name of the relevant symbol. +- for _, comment := range c.file.Comments { +- if comment.Pos() < c.pos && c.pos <= comment.End() { +- c.populateCommentCompletions(comment) +- return nil - } -- // Shift the start position to the location of the -- // dependency within the require statement. -- target := source.BuildLink(snapshot.Options().LinkTarget, "mod/"+req.Mod.String(), "") -- l, err := toProtocolLink(pm.Mapper, target, start+i, start+i+len(dep)) -- if err != nil { -- return nil, err +- } +- +- // Struct literals are handled entirely separately. +- if c.wantStructFieldCompletions() { +- // If we are definitely completing a struct field name, deep completions +- // don't make sense. +- if c.enclosingCompositeLiteral.inKey { +- c.deepState.enabled = false - } -- links = append(links, l) +- return c.structLiteralFieldName(ctx) - } -- // TODO(ridersofrohan): handle links for replace and exclude directives. -- if syntax := pm.File.Syntax; syntax == nil { -- return links, nil +- +- if lt := c.wantLabelCompletion(); lt != labelNone { +- c.labels(lt) +- return nil - } - -- // Get all the links that are contained in the comments of the file. -- urlRegexp := snapshot.Options().URLRegexp -- for _, expr := range pm.File.Syntax.Stmt { -- comments := expr.Comment() -- if comments == nil { -- continue -- } -- for _, section := range [][]modfile.Comment{comments.Before, comments.Suffix, comments.After} { -- for _, comment := range section { -- l, err := findLinksInString(urlRegexp, comment.Token, comment.Start.Byte, pm.Mapper) -- if err != nil { -- return nil, err -- } -- links = append(links, l...) -- } +- if c.emptySwitchStmt() { +- // Empty switch statements only admit "default" and "case" keywords. +- c.addKeywordItems(map[string]bool{}, highScore, CASE, DEFAULT) +- return nil +- } +- +- switch n := c.path[0].(type) { +- case *ast.Ident: +- if c.file.Name == n { +- return c.packageNameCompletions(ctx, c.fh.URI(), n) +- } else if sel, ok := c.path[1].(*ast.SelectorExpr); ok && sel.Sel == n { +- // Is this the Sel part of a selector? +- return c.selector(ctx, sel) - } +- return c.lexical(ctx) +- // The function name hasn't been typed yet, but the parens are there: +- // recv.‸(arg) +- case *ast.TypeAssertExpr: +- // Create a fake selector expression. +- // +- // The name "_" is the convention used by go/parser to represent phantom +- // selectors. +- sel := &ast.Ident{NamePos: n.X.End() + token.Pos(len(".")), Name: "_"} +- return c.selector(ctx, &ast.SelectorExpr{X: n.X, Sel: sel}) +- case *ast.SelectorExpr: +- return c.selector(ctx, n) +- // At the file scope, only keywords are allowed. +- case *ast.BadDecl, *ast.File: +- c.addKeywordCompletions() +- default: +- // fallback to lexical completions +- return c.lexical(ctx) - } -- return links, nil --} - --// goLinks returns the set of hyperlink annotations for the specified Go file. --func goLinks(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.DocumentLink, error) { +- return nil +-} - -- pgf, err := snapshot.ParseGo(ctx, fh, source.ParseFull) -- if err != nil { -- return nil, err +-// containingIdent returns the *ast.Ident containing pos, if any. It +-// synthesizes an *ast.Ident to allow completion in the face of +-// certain syntax errors. +-func (c *completer) containingIdent(src []byte) *ast.Ident { +- // In the normal case, our leaf AST node is the identifier being completed. +- if ident, ok := c.path[0].(*ast.Ident); ok { +- return ident - } - -- var links []protocol.DocumentLink +- pos, tkn, lit := c.scanToken(src) +- if !pos.IsValid() { +- return nil +- } - -- // Create links for import specs. -- if snapshot.Options().ImportShortcut.ShowLinks() { +- fakeIdent := &ast.Ident{Name: lit, NamePos: pos} - -- // If links are to pkg.go.dev, append module version suffixes. -- // This requires the import map from the package metadata. Ignore errors. -- var depsByImpPath map[source.ImportPath]source.PackageID -- if strings.ToLower(snapshot.Options().LinkTarget) == "pkg.go.dev" { -- if meta, err := source.NarrowestMetadataForFile(ctx, snapshot, fh.URI()); err == nil { -- depsByImpPath = meta.DepsByImpPath -- } -- } +- if _, isBadDecl := c.path[0].(*ast.BadDecl); isBadDecl { +- // You don't get *ast.Idents at the file level, so look for bad +- // decls and use the manually extracted token. +- return fakeIdent +- } else if c.emptySwitchStmt() { +- // Only keywords are allowed in empty switch statements. +- // *ast.Idents are not parsed, so we must use the manually +- // extracted token. +- return fakeIdent +- } else if tkn.IsKeyword() { +- // Otherwise, manually extract the prefix if our containing token +- // is a keyword. This improves completion after an "accidental +- // keyword", e.g. completing to "variance" in "someFunc(var<>)". +- return fakeIdent +- } - -- for _, imp := range pgf.File.Imports { -- importPath := source.UnquoteImportPath(imp) -- if importPath == "" { -- continue // bad import -- } -- // See golang/go#36998: don't link to modules matching GOPRIVATE. -- if snapshot.View().IsGoPrivatePath(string(importPath)) { -- continue -- } +- return nil +-} - -- urlPath := string(importPath) +-// scanToken scans pgh's contents for the token containing pos. +-func (c *completer) scanToken(contents []byte) (token.Pos, token.Token, string) { +- tok := c.pkg.FileSet().File(c.pos) - -- // For pkg.go.dev, append module version suffix to package import path. -- if m := snapshot.Metadata(depsByImpPath[importPath]); m != nil && m.Module != nil && m.Module.Path != "" && m.Module.Version != "" { -- urlPath = strings.Replace(urlPath, m.Module.Path, m.Module.Path+"@"+m.Module.Version, 1) -- } +- var s scanner.Scanner +- s.Init(tok, contents, nil, 0) +- for { +- tknPos, tkn, lit := s.Scan() +- if tkn == token.EOF || tknPos >= c.pos { +- return token.NoPos, token.ILLEGAL, "" +- } - -- start, end, err := safetoken.Offsets(pgf.Tok, imp.Path.Pos(), imp.Path.End()) -- if err != nil { -- return nil, err -- } -- targetURL := source.BuildLink(snapshot.Options().LinkTarget, urlPath, "") -- // Account for the quotation marks in the positions. -- l, err := toProtocolLink(pgf.Mapper, targetURL, start+len(`"`), end-len(`"`)) -- if err != nil { -- return nil, err -- } -- links = append(links, l) +- if len(lit) > 0 && tknPos <= c.pos && c.pos <= tknPos+token.Pos(len(lit)) { +- return tknPos, tkn, lit - } - } +-} - -- urlRegexp := snapshot.Options().URLRegexp -- -- // Gather links found in string literals. -- var str []*ast.BasicLit -- ast.Inspect(pgf.File, func(node ast.Node) bool { -- switch n := node.(type) { -- case *ast.ImportSpec: -- return false // don't process import strings again -- case *ast.BasicLit: -- if n.Kind == token.STRING { -- str = append(str, n) -- } +-func (c *completer) sortItems() { +- sort.SliceStable(c.items, func(i, j int) bool { +- // Sort by score first. +- if c.items[i].Score != c.items[j].Score { +- return c.items[i].Score > c.items[j].Score - } -- return true +- +- // Then sort by label so order stays consistent. This also has the +- // effect of preferring shorter candidates. +- return c.items[i].Label < c.items[j].Label - }) -- for _, s := range str { -- strOffset, err := safetoken.Offset(pgf.Tok, s.Pos()) -- if err != nil { -- return nil, err -- } -- l, err := findLinksInString(urlRegexp, s.Value, strOffset, pgf.Mapper) -- if err != nil { -- return nil, err -- } -- links = append(links, l...) +-} +- +-// emptySwitchStmt reports whether pos is in an empty switch or select +-// statement. +-func (c *completer) emptySwitchStmt() bool { +- block, ok := c.path[0].(*ast.BlockStmt) +- if !ok || len(block.List) > 0 || len(c.path) == 1 { +- return false - } - -- // Gather links found in comments. -- for _, commentGroup := range pgf.File.Comments { -- for _, comment := range commentGroup.List { -- commentOffset, err := safetoken.Offset(pgf.Tok, comment.Pos()) -- if err != nil { -- return nil, err -- } -- l, err := findLinksInString(urlRegexp, comment.Text, commentOffset, pgf.Mapper) -- if err != nil { -- return nil, err -- } -- links = append(links, l...) +- switch c.path[1].(type) { +- case *ast.SwitchStmt, *ast.TypeSwitchStmt, *ast.SelectStmt: +- return true +- default: +- return false +- } +-} +- +-// populateImportCompletions yields completions for an import path around the cursor. +-// +-// Completions are suggested at the directory depth of the given import path so +-// that we don't overwhelm the user with a large list of possibilities. As an +-// example, a completion for the prefix "golang" results in "golang.org/". +-// Completions for "golang.org/" yield its subdirectories +-// (i.e. "golang.org/x/"). The user is meant to accept completion suggestions +-// until they reach a complete import path. +-func (c *completer) populateImportCompletions(searchImport *ast.ImportSpec) error { +- if !strings.HasPrefix(searchImport.Path.Value, `"`) { +- return nil +- } +- +- // deepSearch is not valuable for import completions. +- c.deepState.enabled = false +- +- importPath := searchImport.Path.Value +- +- // Extract the text between the quotes (if any) in an import spec. +- // prefix is the part of import path before the cursor. +- prefixEnd := c.pos - searchImport.Path.Pos() +- prefix := strings.Trim(importPath[:prefixEnd], `"`) +- +- // The number of directories in the import path gives us the depth at +- // which to search. +- depth := len(strings.Split(prefix, "/")) - 1 +- +- content := importPath +- start, end := searchImport.Path.Pos(), searchImport.Path.End() +- namePrefix, nameSuffix := `"`, `"` +- // If a starting quote is present, adjust surrounding to either after the +- // cursor or after the first slash (/), except if cursor is at the starting +- // quote. Otherwise we provide a completion including the starting quote. +- if strings.HasPrefix(importPath, `"`) && c.pos > searchImport.Path.Pos() { +- content = content[1:] +- start++ +- if depth > 0 { +- // Adjust textEdit start to replacement range. For ex: if current +- // path was "golang.or/x/to<>ols/internal/", where <> is the cursor +- // position, start of the replacement range would be after +- // "golang.org/x/". +- path := strings.SplitAfter(prefix, "/") +- numChars := len(strings.Join(path[:len(path)-1], "")) +- content = content[numChars:] +- start += token.Pos(numChars) - } +- namePrefix = "" - } - -- return links, nil --} +- // We won't provide an ending quote if one is already present, except if +- // cursor is after the ending quote but still in import spec. This is +- // because cursor has to be in our textEdit range. +- if strings.HasSuffix(importPath, `"`) && c.pos < searchImport.Path.End() { +- end-- +- content = content[:len(content)-1] +- nameSuffix = "" +- } - --// acceptedSchemes controls the schemes that URLs must have to be shown to the --// user. Other schemes can't be opened by LSP clients, so linkifying them is --// distracting. See golang/go#43990. --var acceptedSchemes = map[string]bool{ -- "http": true, -- "https": true, --} +- c.surrounding = &Selection{ +- content: content, +- cursor: c.pos, +- tokFile: c.tokFile, +- start: start, +- end: end, +- mapper: c.mapper, +- } - --// urlRegexp is the user-supplied regular expression to match URL. --// srcOffset is the start offset of 'src' within m's file. --func findLinksInString(urlRegexp *regexp.Regexp, src string, srcOffset int, m *protocol.Mapper) ([]protocol.DocumentLink, error) { -- var links []protocol.DocumentLink -- for _, index := range urlRegexp.FindAllIndex([]byte(src), -1) { -- start, end := index[0], index[1] -- link := src[start:end] -- linkURL, err := url.Parse(link) -- // Fallback: Linkify IP addresses as suggested in golang/go#18824. +- seenImports := make(map[string]struct{}) +- for _, importSpec := range c.file.Imports { +- if importSpec.Path.Value == importPath { +- continue +- } +- seenImportPath, err := strconv.Unquote(importSpec.Path.Value) - if err != nil { -- linkURL, err = url.Parse("//" + link) -- // Not all potential links will be valid, so don't return this error. -- if err != nil { -- continue -- } +- return err - } -- // If the URL has no scheme, use https. -- if linkURL.Scheme == "" { -- linkURL.Scheme = "https" +- seenImports[seenImportPath] = struct{}{} +- } +- +- var mu sync.Mutex // guard c.items locally, since searchImports is called in parallel +- seen := make(map[string]struct{}) +- searchImports := func(pkg imports.ImportFix) { +- path := pkg.StmtInfo.ImportPath +- if _, ok := seenImports[path]; ok { +- return - } -- if !acceptedSchemes[linkURL.Scheme] { -- continue +- +- // Any package path containing fewer directories than the search +- // prefix is not a match. +- pkgDirList := strings.Split(path, "/") +- if len(pkgDirList) < depth+1 { +- return - } +- pkgToConsider := strings.Join(pkgDirList[:depth+1], "/") - -- l, err := toProtocolLink(m, linkURL.String(), srcOffset+start, srcOffset+end) -- if err != nil { -- return nil, err +- name := pkgDirList[depth] +- // if we're adding an opening quote to completion too, set name to full +- // package path since we'll need to overwrite that range. +- if namePrefix == `"` { +- name = pkgToConsider - } -- links = append(links, l) -- } -- // Handle golang/go#1234-style links. -- r := getIssueRegexp() -- for _, index := range r.FindAllIndex([]byte(src), -1) { -- start, end := index[0], index[1] -- matches := r.FindStringSubmatch(src) -- if len(matches) < 4 { -- continue +- +- score := pkg.Relevance +- if len(pkgDirList)-1 == depth { +- score *= highScore +- } else { +- // For incomplete package paths, add a terminal slash to indicate that the +- // user should keep triggering completions. +- name += "/" +- pkgToConsider += "/" - } -- org, repo, number := matches[1], matches[2], matches[3] -- targetURL := fmt.Sprintf("https://github.com/%s/%s/issues/%s", org, repo, number) -- l, err := toProtocolLink(m, targetURL, srcOffset+start, srcOffset+end) -- if err != nil { -- return nil, err +- +- if _, ok := seen[pkgToConsider]; ok { +- return - } -- links = append(links, l) +- seen[pkgToConsider] = struct{}{} +- +- mu.Lock() +- defer mu.Unlock() +- +- name = namePrefix + name + nameSuffix +- obj := types.NewPkgName(0, nil, name, types.NewPackage(pkgToConsider, name)) +- c.deepState.enqueue(candidate{ +- obj: obj, +- detail: strconv.Quote(pkgToConsider), +- score: score, +- }) - } -- return links, nil --} - --func getIssueRegexp() *regexp.Regexp { -- once.Do(func() { -- issueRegexp = regexp.MustCompile(`(\w+)/([\w-]+)#([0-9]+)`) +- c.completionCallbacks = append(c.completionCallbacks, func(ctx context.Context, opts *imports.Options) error { +- return imports.GetImportPaths(ctx, searchImports, prefix, c.filename, c.pkg.Types().Name(), opts.Env) - }) -- return issueRegexp +- return nil -} - --var ( -- once sync.Once -- issueRegexp *regexp.Regexp --) +-// populateCommentCompletions yields completions for comments preceding or in declarations. +-func (c *completer) populateCommentCompletions(comment *ast.CommentGroup) { +- // If the completion was triggered by a period, ignore it. These types of +- // completions will not be useful in comments. +- if c.completionContext.triggerCharacter == "." { +- return +- } - --func toProtocolLink(m *protocol.Mapper, targetURL string, start, end int) (protocol.DocumentLink, error) { -- rng, err := m.OffsetRange(start, end) -- if err != nil { -- return protocol.DocumentLink{}, err +- // Using the comment position find the line after +- file := c.pkg.FileSet().File(comment.End()) +- if file == nil { +- return - } -- return protocol.DocumentLink{ -- Range: rng, -- Target: &targetURL, -- }, nil --} -diff -urN a/gopls/internal/lsp/lru/lru_fuzz_test.go b/gopls/internal/lsp/lru/lru_fuzz_test.go ---- a/gopls/internal/lsp/lru/lru_fuzz_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/lru/lru_fuzz_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,41 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --//go:build go1.18 --// +build go1.18 +- // Deep completion doesn't work properly in comments since we don't +- // have a type object to complete further. +- c.deepState.enabled = false +- c.completionContext.commentCompletion = true - --package lru_test +- // Documentation isn't useful in comments, since it might end up being the +- // comment itself. +- c.opts.documentation = false - --import ( -- "testing" +- commentLine := safetoken.Line(file, comment.End()) - -- "golang.org/x/tools/gopls/internal/lsp/lru" --) +- // comment is valid, set surrounding as word boundaries around cursor +- c.setSurroundingForComment(comment) - --// Simple fuzzing test for consistency. --func FuzzCache(f *testing.F) { -- type op struct { -- set bool -- key, value byte -- } -- f.Fuzz(func(t *testing.T, data []byte) { -- var ops []op -- for len(data) >= 3 { -- ops = append(ops, op{data[0]%2 == 0, data[1], data[2]}) -- data = data[3:] -- } -- cache := lru.New(100) -- var reference [256]byte -- for _, op := range ops { -- if op.set { -- reference[op.key] = op.value -- cache.Set(op.key, op.value, 1) -- } else { -- if v := cache.Get(op.key); v != nil && v != reference[op.key] { -- t.Fatalf("cache.Get(%d) = %d, want %d", op.key, v, reference[op.key]) -- } -- } +- // Using the next line pos, grab and parse the exported symbol on that line +- for _, n := range c.file.Decls { +- declLine := safetoken.Line(file, n.Pos()) +- // if the comment is not in, directly above or on the same line as a declaration +- if declLine != commentLine && declLine != commentLine+1 && +- !(n.Pos() <= comment.Pos() && comment.End() <= n.End()) { +- continue - } -- }) --} -diff -urN a/gopls/internal/lsp/lru/lru.go b/gopls/internal/lsp/lru/lru.go ---- a/gopls/internal/lsp/lru/lru.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/lru/lru.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,151 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +- switch node := n.(type) { +- // handle const, vars, and types +- case *ast.GenDecl: +- for _, spec := range node.Specs { +- switch spec := spec.(type) { +- case *ast.ValueSpec: +- for _, name := range spec.Names { +- if name.String() == "_" { +- continue +- } +- obj := c.pkg.TypesInfo().ObjectOf(name) +- c.deepState.enqueue(candidate{obj: obj, score: stdScore}) +- } +- case *ast.TypeSpec: +- // add TypeSpec fields to completion +- switch typeNode := spec.Type.(type) { +- case *ast.StructType: +- c.addFieldItems(typeNode.Fields) +- case *ast.FuncType: +- c.addFieldItems(typeNode.Params) +- c.addFieldItems(typeNode.Results) +- case *ast.InterfaceType: +- c.addFieldItems(typeNode.Methods) +- } - --// The lru package implements a fixed-size in-memory LRU cache. --package lru +- if spec.Name.String() == "_" { +- continue +- } - --import ( -- "container/heap" -- "fmt" -- "sync" --) +- obj := c.pkg.TypesInfo().ObjectOf(spec.Name) +- // Type name should get a higher score than fields but not highScore by default +- // since field near a comment cursor gets a highScore +- score := stdScore * 1.1 +- // If type declaration is on the line after comment, give it a highScore. +- if declLine == commentLine+1 { +- score = highScore +- } - --// A Cache is a fixed-size in-memory LRU cache. --type Cache struct { -- capacity int +- c.deepState.enqueue(candidate{obj: obj, score: score}) +- } +- } +- // handle functions +- case *ast.FuncDecl: +- c.addFieldItems(node.Recv) +- c.addFieldItems(node.Type.Params) +- c.addFieldItems(node.Type.Results) - -- mu sync.Mutex -- used int // used capacity, in user-specified units -- m map[any]*entry // k/v lookup -- lru queue // min-atime priority queue of *entry -- clock int64 // clock time, incremented whenever the cache is updated --} +- // collect receiver struct fields +- if node.Recv != nil { +- sig := c.pkg.TypesInfo().Defs[node.Name].(*types.Func).Type().(*types.Signature) +- _, named := typesinternal.ReceiverNamed(sig.Recv()) // may be nil if ill-typed +- if named != nil { +- if recvStruct, ok := named.Underlying().(*types.Struct); ok { +- for i := 0; i < recvStruct.NumFields(); i++ { +- field := recvStruct.Field(i) +- c.deepState.enqueue(candidate{obj: field, score: lowScore}) +- } +- } +- } +- } - --type entry struct { -- key any -- value any -- size int // caller-specified size -- atime int64 // last access / set time -- index int // index of entry in the heap slice --} +- if node.Name.String() == "_" { +- continue +- } - --// New creates a new Cache with the given capacity, which must be positive. --// --// The cache capacity uses arbitrary units, which are specified during the Set --// operation. --func New(capacity int) *Cache { -- if capacity == 0 { -- panic("zero capacity") -- } +- obj := c.pkg.TypesInfo().ObjectOf(node.Name) +- if obj == nil || obj.Pkg() != nil && obj.Pkg() != c.pkg.Types() { +- continue +- } - -- return &Cache{ -- capacity: capacity, -- m: make(map[any]*entry), +- c.deepState.enqueue(candidate{obj: obj, score: highScore}) +- } - } -} - --// Get retrieves the value for the specified key, or nil if the key is not --// found. --// --// If the key is found, its access time is updated. --func (c *Cache) Get(key any) any { -- c.mu.Lock() -- defer c.mu.Unlock() +-// sets word boundaries surrounding a cursor for a comment +-func (c *completer) setSurroundingForComment(comments *ast.CommentGroup) { +- var cursorComment *ast.Comment +- for _, comment := range comments.List { +- if c.pos >= comment.Pos() && c.pos <= comment.End() { +- cursorComment = comment +- break +- } +- } +- // if cursor isn't in the comment +- if cursorComment == nil { +- return +- } - -- c.clock++ // every access updates the clock +- // index of cursor in comment text +- cursorOffset := int(c.pos - cursorComment.Pos()) +- start, end := cursorOffset, cursorOffset +- for start > 0 && isValidIdentifierChar(cursorComment.Text[start-1]) { +- start-- +- } +- for end < len(cursorComment.Text) && isValidIdentifierChar(cursorComment.Text[end]) { +- end++ +- } - -- if e, ok := c.m[key]; ok { // cache hit -- e.atime = c.clock -- heap.Fix(&c.lru, e.index) -- return e.value +- c.surrounding = &Selection{ +- content: cursorComment.Text[start:end], +- cursor: c.pos, +- tokFile: c.tokFile, +- start: token.Pos(int(cursorComment.Slash) + start), +- end: token.Pos(int(cursorComment.Slash) + end), +- mapper: c.mapper, - } +- c.setMatcherFromPrefix(c.surrounding.Prefix()) +-} - -- return nil +-// isValidIdentifierChar returns true if a byte is a valid go identifier +-// character, i.e. unicode letter or digit or underscore. +-func isValidIdentifierChar(char byte) bool { +- charRune := rune(char) +- return unicode.In(charRune, unicode.Letter, unicode.Digit) || char == '_' -} - --// Set stores a value for the specified key, using its given size to update the --// current cache size, evicting old entries as necessary to fit in the cache --// capacity. --// --// Size must be a non-negative value. If size is larger than the cache --// capacity, the value is not stored and the cache is not modified. --func (c *Cache) Set(key, value any, size int) { -- if size < 0 { -- panic(fmt.Sprintf("size must be non-negative, got %d", size)) -- } -- if size > c.capacity { -- return // uncacheable +-// adds struct fields, interface methods, function declaration fields to completion +-func (c *completer) addFieldItems(fields *ast.FieldList) { +- if fields == nil { +- return - } - -- c.mu.Lock() -- defer c.mu.Unlock() -- -- c.clock++ +- cursor := c.surrounding.cursor +- for _, field := range fields.List { +- for _, name := range field.Names { +- if name.String() == "_" { +- continue +- } +- obj := c.pkg.TypesInfo().ObjectOf(name) +- if obj == nil { +- continue +- } - -- // Remove the existing cache entry for key, if it exists. -- e, ok := c.m[key] -- if ok { -- c.used -= e.size -- heap.Remove(&c.lru, e.index) -- delete(c.m, key) -- } +- // if we're in a field comment/doc, score that field as more relevant +- score := stdScore +- if field.Comment != nil && field.Comment.Pos() <= cursor && cursor <= field.Comment.End() { +- score = highScore +- } else if field.Doc != nil && field.Doc.Pos() <= cursor && cursor <= field.Doc.End() { +- score = highScore +- } - -- // Evict entries until the new value will fit. -- newUsed := c.used + size -- if newUsed < 0 { -- return // integer overflow; return silently -- } -- c.used = newUsed -- for c.used > c.capacity { -- // evict oldest entry -- e = heap.Pop(&c.lru).(*entry) -- c.used -= e.size -- delete(c.m, e.key) +- c.deepState.enqueue(candidate{obj: obj, score: score}) +- } - } +-} - -- // Store the new value. -- // Opt: e is evicted, so it can be reused to reduce allocation. -- if e == nil { -- e = new(entry) +-func (c *completer) wantStructFieldCompletions() bool { +- clInfo := c.enclosingCompositeLiteral +- if clInfo == nil { +- return false - } -- e.key = key -- e.value = value -- e.size = size -- e.atime = c.clock -- c.m[e.key] = e -- heap.Push(&c.lru, e) +- return is[*types.Struct](clInfo.clType) && (clInfo.inKey || clInfo.maybeInFieldName) +-} - -- if len(c.m) != len(c.lru) { -- panic("map and LRU are inconsistent") -- } +-func (c *completer) wantTypeName() bool { +- return !c.completionContext.commentCompletion && c.inference.typeName.wantTypeName -} - --// -- priority queue boilerplate -- +-// See https://golang.org/issue/36001. Unimported completions are expensive. +-const ( +- maxUnimportedPackageNames = 5 +- unimportedMemberTarget = 100 +-) - --// queue is a min-atime priority queue of cache entries. --type queue []*entry +-// selector finds completions for the specified selector expression. +-func (c *completer) selector(ctx context.Context, sel *ast.SelectorExpr) error { +- c.inference.objChain = objChain(c.pkg.TypesInfo(), sel.X) - --func (q queue) Len() int { return len(q) } +- // True selector? +- if tv, ok := c.pkg.TypesInfo().Types[sel.X]; ok { +- c.methodsAndFields(tv.Type, tv.Addressable(), nil, c.deepState.enqueue) +- c.addPostfixSnippetCandidates(ctx, sel) +- return nil +- } - --func (q queue) Less(i, j int) bool { return q[i].atime < q[j].atime } +- id, ok := sel.X.(*ast.Ident) +- if !ok { +- return nil +- } - --func (q queue) Swap(i, j int) { -- q[i], q[j] = q[j], q[i] -- q[i].index = i -- q[j].index = j --} +- // Treat sel as a qualified identifier. +- var filter func(*metadata.Package) bool +- needImport := false +- if pkgName, ok := c.pkg.TypesInfo().Uses[id].(*types.PkgName); ok { +- // Qualified identifier with import declaration. +- imp := pkgName.Imported() - --func (q *queue) Push(x any) { -- e := x.(*entry) -- e.index = len(*q) -- *q = append(*q, e) --} +- // Known direct dependency? Expand using type information. +- if _, ok := c.pkg.Metadata().DepsByPkgPath[golang.PackagePath(imp.Path())]; ok { +- c.packageMembers(imp, stdScore, nil, c.deepState.enqueue) +- return nil +- } - --func (q *queue) Pop() any { -- last := len(*q) - 1 -- e := (*q)[last] -- (*q)[last] = nil // aid GC -- *q = (*q)[:last] -- return e --} -diff -urN a/gopls/internal/lsp/lru/lru_test.go b/gopls/internal/lsp/lru/lru_test.go ---- a/gopls/internal/lsp/lru/lru_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/lru/lru_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,154 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +- // Imported declaration with missing type information. +- // Fall through to shallow completion of unimported package members. +- // Match candidate packages by path. +- filter = func(mp *metadata.Package) bool { +- return strings.TrimPrefix(string(mp.PkgPath), "vendor/") == imp.Path() +- } +- } else { +- // Qualified identifier without import declaration. +- // Match candidate packages by name. +- filter = func(mp *metadata.Package) bool { +- return string(mp.Name) == id.Name +- } +- needImport = true +- } - --package lru_test +- // Search unimported packages. +- if !c.opts.unimported { +- return nil // feature disabled +- } - --import ( -- "bytes" -- cryptorand "crypto/rand" -- "fmt" -- "log" -- mathrand "math/rand" -- "strings" -- "testing" +- // -- completion of symbols in unimported packages -- - -- "golang.org/x/sync/errgroup" -- "golang.org/x/tools/gopls/internal/lsp/lru" --) +- // The deep completion algorithm is exceedingly complex and +- // deeply coupled to the now obsolete notions that all +- // token.Pos values can be interpreted by as a single FileSet +- // belonging to the Snapshot and that all types.Object values +- // are canonicalized by a single types.Importer mapping. +- // These invariants are no longer true now that gopls uses +- // an incremental approach, parsing and type-checking each +- // package separately. +- // +- // Consequently, completion of symbols defined in packages that +- // are not currently imported by the query file cannot use the +- // deep completion machinery which is based on type information. +- // Instead it must use only syntax information from a quick +- // parse of top-level declarations (but not function bodies). +- // +- // TODO(adonovan): rewrite the deep completion machinery to +- // not assume global Pos/Object realms and then use export +- // data instead of the quick parse approach taken here. - --func TestCache(t *testing.T) { -- type get struct { -- key string -- want any +- // First, we search among packages in the forward transitive +- // closure of the workspace. +- // We'll use a fast parse to extract package members +- // from those that match the name/path criterion. +- all, err := c.snapshot.AllMetadata(ctx) +- if err != nil { +- return err - } -- type set struct { -- key, value string +- known := make(map[golang.PackagePath]*metadata.Package) +- for _, mp := range all { +- if mp.Name == "main" { +- continue // not importable +- } +- if mp.IsIntermediateTestVariant() { +- continue +- } +- // The only test variant we admit is "p [p.test]" +- // when we are completing within "p_test [p.test]", +- // as in that case we would like to offer completions +- // of the test variants' additional symbols. +- if mp.ForTest != "" && c.pkg.Metadata().PkgPath != mp.ForTest+"_test" { +- continue +- } +- if !filter(mp) { +- continue +- } +- // Prefer previous entry unless this one is its test variant. +- if mp.ForTest != "" || known[mp.PkgPath] == nil { +- known[mp.PkgPath] = mp +- } - } - -- tests := []struct { -- label string -- steps []any -- }{ -- {"empty cache", []any{ -- get{"a", nil}, -- get{"b", nil}, -- }}, -- {"zero-length string", []any{ -- set{"a", ""}, -- get{"a", ""}, -- }}, -- {"under capacity", []any{ -- set{"a", "123"}, -- set{"b", "456"}, -- get{"a", "123"}, -- get{"b", "456"}, -- }}, -- {"over capacity", []any{ -- set{"a", "123"}, -- set{"b", "456"}, -- set{"c", "78901"}, -- get{"a", nil}, -- get{"b", "456"}, -- get{"c", "78901"}, -- }}, -- {"access ordering", []any{ -- set{"a", "123"}, -- set{"b", "456"}, -- get{"a", "123"}, -- set{"c", "78901"}, -- get{"a", "123"}, -- get{"b", nil}, -- get{"c", "78901"}, -- }}, +- paths := make([]string, 0, len(known)) +- for path := range known { +- paths = append(paths, string(path)) - } - -- for _, test := range tests { -- t.Run(test.label, func(t *testing.T) { -- c := lru.New(10) -- for i, step := range test.steps { -- switch step := step.(type) { -- case get: -- if got := c.Get(step.key); got != step.want { -- t.Errorf("#%d: c.Get(%q) = %q, want %q", i, step.key, got, step.want) -- } -- case set: -- c.Set(step.key, step.value, len(step.value)) -- } -- } +- // Rank import paths as goimports would. +- var relevances map[string]float64 +- if len(paths) > 0 { +- if err := c.snapshot.RunProcessEnvFunc(ctx, func(ctx context.Context, opts *imports.Options) error { +- var err error +- relevances, err = imports.ScoreImportPaths(ctx, opts.Env, paths) +- return err +- }); err != nil { +- return err +- } +- sort.Slice(paths, func(i, j int) bool { +- return relevances[paths[i]] > relevances[paths[j]] - }) - } --} -- --// TestConcurrency exercises concurrent access to the same entry. --// --// It is a copy of TestConcurrency from the filecache package. --func TestConcurrency(t *testing.T) { -- key := uniqueKey() -- const N = 100 // concurrency level - -- // Construct N distinct values, each larger -- // than a typical 4KB OS file buffer page. -- var values [N][8192]byte -- for i := range values { -- if _, err := mathrand.Read(values[i][:]); err != nil { -- t.Fatalf("rand: %v", err) +- // quickParse does a quick parse of a single file of package m, +- // extracts exported package members and adds candidates to c.items. +- // TODO(rfindley): synchronizing access to c here does not feel right. +- // Consider adding a concurrency-safe API for completer. +- var cMu sync.Mutex // guards c.items and c.matcher +- var enough int32 // atomic bool +- quickParse := func(uri protocol.DocumentURI, mp *metadata.Package, tooNew map[string]bool) error { +- if atomic.LoadInt32(&enough) != 0 { +- return nil - } -- } - -- cache := lru.New(100 * 1e6) // 100MB cache +- fh, err := c.snapshot.ReadFile(ctx, uri) +- if err != nil { +- return err +- } +- content, err := fh.Content() +- if err != nil { +- return err +- } +- path := string(mp.PkgPath) +- forEachPackageMember(content, func(tok token.Token, id *ast.Ident, fn *ast.FuncDecl) { +- if atomic.LoadInt32(&enough) != 0 { +- return +- } - -- // get calls Get and verifies that the cache entry -- // matches one of the values passed to Set. -- get := func(mustBeFound bool) error { -- got := cache.Get(key) -- if got == nil { -- if !mustBeFound { -- return nil +- if !id.IsExported() { +- return - } -- return fmt.Errorf("Get did not return a value") -- } -- gotBytes := got.([]byte) -- for _, want := range values { -- if bytes.Equal(want[:], gotBytes) { -- return nil // a match +- +- if tooNew[id.Name] { +- return // symbol too new for requesting file's Go's version - } -- } -- return fmt.Errorf("Get returned a value that was never Set") -- } - -- // Perform N concurrent calls to Set and Get. -- // All sets must succeed. -- // All gets must return nothing, or one of the Set values; -- // there is no third possibility. -- var group errgroup.Group -- for i := range values { -- i := i -- v := values[i][:] -- group.Go(func() error { -- cache.Set(key, v, len(v)) -- return nil -- }) -- group.Go(func() error { return get(false) }) -- } -- if err := group.Wait(); err != nil { -- if strings.Contains(err.Error(), "operation not supported") || -- strings.Contains(err.Error(), "not implemented") { -- t.Skipf("skipping: %v", err) -- } -- t.Fatal(err) -- } +- cMu.Lock() +- score := c.matcher.Score(id.Name) +- cMu.Unlock() - -- // A final Get must report one of the values that was Set. -- if err := get(true); err != nil { -- t.Fatalf("final Get failed: %v", err) -- } --} +- if sel.Sel.Name != "_" && score == 0 { +- return // not a match; avoid constructing the completion item below +- } - --// uniqueKey returns a key that has never been used before. --func uniqueKey() (key [32]byte) { -- if _, err := cryptorand.Read(key[:]); err != nil { -- log.Fatalf("rand: %v", err) -- } -- return --} -diff -urN a/gopls/internal/lsp/lsprpc/autostart_default.go b/gopls/internal/lsp/lsprpc/autostart_default.go ---- a/gopls/internal/lsp/lsprpc/autostart_default.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/lsprpc/autostart_default.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,39 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +- // The only detail is the kind and package: `var (from "example.com/foo")` +- // TODO(adonovan): pretty-print FuncDecl.FuncType or TypeSpec.Type? +- // TODO(adonovan): should this score consider the actual c.matcher.Score +- // of the item? How does this compare with the deepState.enqueue path? +- item := CompletionItem{ +- Label: id.Name, +- Detail: fmt.Sprintf("%s (from %q)", strings.ToLower(tok.String()), mp.PkgPath), +- InsertText: id.Name, +- Score: float64(score) * unimportedScore(relevances[path]), +- } +- switch tok { +- case token.FUNC: +- item.Kind = protocol.FunctionCompletion +- case token.VAR: +- item.Kind = protocol.VariableCompletion +- case token.CONST: +- item.Kind = protocol.ConstantCompletion +- case token.TYPE: +- // Without types, we can't distinguish Class from Interface. +- item.Kind = protocol.ClassCompletion +- } - --package lsprpc +- if needImport { +- imp := &importInfo{importPath: path} +- if imports.ImportPathToAssumedName(path) != string(mp.Name) { +- imp.name = string(mp.Name) +- } +- item.AdditionalTextEdits, _ = c.importEdits(imp) +- } - --import ( -- "fmt" +- // For functions, add a parameter snippet. +- if fn != nil { +- paramList := func(list *ast.FieldList) []string { +- var params []string +- if list != nil { +- var cfg printer.Config // slight overkill +- param := func(name string, typ ast.Expr) { +- var buf strings.Builder +- buf.WriteString(name) +- buf.WriteByte(' ') +- cfg.Fprint(&buf, token.NewFileSet(), typ) +- params = append(params, buf.String()) +- } - -- exec "golang.org/x/sys/execabs" --) +- for _, field := range list.List { +- if field.Names != nil { +- for _, name := range field.Names { +- param(name.Name, field.Type) +- } +- } else { +- param("_", field.Type) +- } +- } +- } +- return params +- } - --var ( -- daemonize = func(*exec.Cmd) {} -- autoNetworkAddress = autoNetworkAddressDefault -- verifyRemoteOwnership = verifyRemoteOwnershipDefault --) +- // Ideally we would eliminate the suffix of type +- // parameters that are redundant with inference +- // from the argument types (#51783), but it's +- // quite fiddly to do using syntax alone. +- // (See inferableTypeParams in format.go.) +- tparams := paramList(fn.Type.TypeParams) +- params := paramList(fn.Type.Params) +- var sn snippet.Builder +- c.functionCallSnippet(id.Name, tparams, params, &sn) +- item.snippet = &sn +- } - --func runRemote(cmd *exec.Cmd) error { -- daemonize(cmd) -- if err := cmd.Start(); err != nil { -- return fmt.Errorf("starting remote gopls: %w", err) +- cMu.Lock() +- c.items = append(c.items, item) +- if len(c.items) >= unimportedMemberTarget { +- atomic.StoreInt32(&enough, 1) +- } +- cMu.Unlock() +- }) +- return nil - } -- return nil --} - --// autoNetworkAddressDefault returns the default network and address for the --// automatically-started gopls remote. See autostart_posix.go for more --// information. --func autoNetworkAddressDefault(goplsPath, id string) (network string, address string) { -- if id != "" { -- panic("identified remotes are not supported on windows") +- var goversion string +- // TODO(adonovan): after go1.21, replace with: +- // goversion = c.pkg.GetTypesInfo().FileVersions[c.file] +- if v := reflect.ValueOf(c.pkg.TypesInfo()).Elem().FieldByName("FileVersions"); v.IsValid() { +- goversion = v.Interface().(map[*ast.File]string)[c.file] // may be "" - } -- return "tcp", "localhost:37374" --} - --func verifyRemoteOwnershipDefault(network, address string) (bool, error) { -- return true, nil --} -diff -urN a/gopls/internal/lsp/lsprpc/autostart_posix.go b/gopls/internal/lsp/lsprpc/autostart_posix.go ---- a/gopls/internal/lsp/lsprpc/autostart_posix.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/lsprpc/autostart_posix.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,97 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +- // Extract the package-level candidates using a quick parse. +- var g errgroup.Group +- for _, path := range paths { +- mp := known[golang.PackagePath(path)] +- +- // For standard packages, build a filter of symbols that +- // are too new for the requesting file's Go version. +- var tooNew map[string]bool +- if syms, ok := stdlib.PackageSymbols[path]; ok && goversion != "" { +- tooNew = make(map[string]bool) +- for _, sym := range syms { +- if versions.Before(goversion, sym.Version.String()) { +- tooNew[sym.Name] = true +- } +- } +- } - --//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris --// +build darwin dragonfly freebsd linux netbsd openbsd solaris +- for _, uri := range mp.CompiledGoFiles { +- uri := uri +- g.Go(func() error { +- return quickParse(uri, mp, tooNew) +- }) +- } +- } +- if err := g.Wait(); err != nil { +- return err +- } - --package lsprpc +- // In addition, we search in the module cache using goimports. +- ctx, cancel := context.WithCancel(ctx) +- var mu sync.Mutex +- add := func(pkgExport imports.PackageExport) { +- if ignoreUnimportedCompletion(pkgExport.Fix) { +- return +- } - --import ( -- "crypto/sha256" -- "errors" -- "fmt" -- "log" -- "os" -- "os/user" -- "path/filepath" -- "strconv" -- "syscall" +- mu.Lock() +- defer mu.Unlock() +- // TODO(adonovan): what if the actual package has a vendor/ prefix? +- if _, ok := known[golang.PackagePath(pkgExport.Fix.StmtInfo.ImportPath)]; ok { +- return // We got this one above. +- } - -- exec "golang.org/x/sys/execabs" --) +- // Continue with untyped proposals. +- pkg := types.NewPackage(pkgExport.Fix.StmtInfo.ImportPath, pkgExport.Fix.IdentName) +- for _, symbol := range pkgExport.Exports { +- if goversion != "" && versions.Before(goversion, symbol.Version.String()) { +- continue // symbol too new for this file +- } +- score := unimportedScore(pkgExport.Fix.Relevance) +- c.deepState.enqueue(candidate{ +- obj: types.NewVar(0, pkg, symbol.Name, nil), +- score: score, +- imp: &importInfo{ +- importPath: pkgExport.Fix.StmtInfo.ImportPath, +- name: pkgExport.Fix.StmtInfo.Name, +- }, +- }) +- } +- if len(c.items) >= unimportedMemberTarget { +- cancel() +- } +- } - --func init() { -- daemonize = daemonizePosix -- autoNetworkAddress = autoNetworkAddressPosix -- verifyRemoteOwnership = verifyRemoteOwnershipPosix +- c.completionCallbacks = append(c.completionCallbacks, func(ctx context.Context, opts *imports.Options) error { +- defer cancel() +- return imports.GetPackageExports(ctx, add, id.Name, c.filename, c.pkg.Types().Name(), opts.Env) +- }) +- return nil -} - --func daemonizePosix(cmd *exec.Cmd) { -- cmd.SysProcAttr = &syscall.SysProcAttr{ -- Setsid: true, -- } +-// unimportedScore returns a score for an unimported package that is generally +-// lower than other candidates. +-func unimportedScore(relevance float64) float64 { +- return (stdScore + .1*relevance) / 2 -} - --// autoNetworkAddressPosix resolves an id on the 'auto' pseduo-network to a --// real network and address. On unix, this uses unix domain sockets. --func autoNetworkAddressPosix(goplsPath, id string) (network string, address string) { -- // Especially when doing local development or testing, it's important that -- // the remote gopls instance we connect to is running the same binary as our -- // forwarder. So we encode a short hash of the binary path into the daemon -- // socket name. If possible, we also include the buildid in this hash, to -- // account for long-running processes where the binary has been subsequently -- // rebuilt. -- h := sha256.New() -- cmd := exec.Command("go", "tool", "buildid", goplsPath) -- cmd.Stdout = h -- var pathHash []byte -- if err := cmd.Run(); err == nil { -- pathHash = h.Sum(nil) -- } else { -- log.Printf("error getting current buildid: %v", err) -- sum := sha256.Sum256([]byte(goplsPath)) -- pathHash = sum[:] -- } -- shortHash := fmt.Sprintf("%x", pathHash)[:6] -- user := os.Getenv("USER") -- if user == "" { -- user = "shared" -- } -- basename := filepath.Base(goplsPath) -- idComponent := "" -- if id != "" { -- idComponent = "-" + id -- } -- runtimeDir := os.TempDir() -- if xdg := os.Getenv("XDG_RUNTIME_DIR"); xdg != "" { -- runtimeDir = xdg +-func (c *completer) packageMembers(pkg *types.Package, score float64, imp *importInfo, cb func(candidate)) { +- scope := pkg.Scope() +- for _, name := range scope.Names() { +- obj := scope.Lookup(name) +- if c.tooNew(obj) { +- continue // std symbol too new for file's Go version +- } +- cb(candidate{ +- obj: obj, +- score: score, +- imp: imp, +- addressable: isVar(obj), +- }) - } -- return "unix", filepath.Join(runtimeDir, fmt.Sprintf("%s-%s-daemon.%s%s", basename, shortHash, user, idComponent)) -} - --func verifyRemoteOwnershipPosix(network, address string) (bool, error) { -- if network != "unix" { -- return true, nil -- } -- fi, err := os.Stat(address) -- if err != nil { -- if os.IsNotExist(err) { -- return true, nil +-// ignoreUnimportedCompletion reports whether an unimported completion +-// resulting in the given import should be ignored. +-func ignoreUnimportedCompletion(fix *imports.ImportFix) bool { +- // golang/go#60062: don't add unimported completion to golang.org/toolchain. +- return fix != nil && strings.HasPrefix(fix.StmtInfo.ImportPath, "golang.org/toolchain") +-} +- +-func (c *completer) methodsAndFields(typ types.Type, addressable bool, imp *importInfo, cb func(candidate)) { +- mset := c.methodSetCache[methodSetKey{typ, addressable}] +- if mset == nil { +- if addressable && !types.IsInterface(typ) && !isPointer(typ) { +- // Add methods of *T, which includes methods with receiver T. +- mset = types.NewMethodSet(types.NewPointer(typ)) +- } else { +- // Add methods of T. +- mset = types.NewMethodSet(typ) - } -- return false, fmt.Errorf("checking socket owner: %w", err) -- } -- stat, ok := fi.Sys().(*syscall.Stat_t) -- if !ok { -- return false, errors.New("fi.Sys() is not a Stat_t") -- } -- user, err := user.Current() -- if err != nil { -- return false, fmt.Errorf("checking current user: %w", err) +- c.methodSetCache[methodSetKey{typ, addressable}] = mset - } -- uid, err := strconv.ParseUint(user.Uid, 10, 32) -- if err != nil { -- return false, fmt.Errorf("parsing current UID: %w", err) +- +- if isStarTestingDotF(typ) && addressable { +- // is that a sufficient test? (or is more care needed?) +- if c.fuzz(mset, imp, cb) { +- return +- } - } -- return stat.Uid == uint32(uid), nil --} -diff -urN a/gopls/internal/lsp/lsprpc/binder.go b/gopls/internal/lsp/lsprpc/binder.go ---- a/gopls/internal/lsp/lsprpc/binder.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/lsprpc/binder.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,148 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package lsprpc +- for i := 0; i < mset.Len(); i++ { +- obj := mset.At(i).Obj() +- // to the other side of the cb() queue? +- if c.tooNew(obj) { +- continue // std method too new for file's Go version +- } +- cb(candidate{ +- obj: mset.At(i).Obj(), +- score: stdScore, +- imp: imp, +- addressable: addressable || isPointer(typ), +- }) +- } - --import ( -- "context" -- "encoding/json" -- "fmt" +- // Add fields of T. +- eachField(typ, func(v *types.Var) { +- if c.tooNew(v) { +- return // std field too new for file's Go version +- } +- cb(candidate{ +- obj: v, +- score: stdScore - 0.01, +- imp: imp, +- addressable: addressable || isPointer(typ), +- }) +- }) +-} - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/internal/event" -- jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" -- "golang.org/x/tools/internal/xcontext" --) +-// isStarTestingDotF reports whether typ is *testing.F. +-func isStarTestingDotF(typ types.Type) bool { +- // No Unalias, since go test doesn't consider +- // types when enumeratinf test funcs, only syntax. +- ptr, _ := typ.(*types.Pointer) +- if ptr == nil { +- return false +- } +- named, _ := ptr.Elem().(*types.Named) +- if named == nil { +- return false +- } +- obj := named.Obj() +- // obj.Pkg is nil for the error type. +- return obj != nil && obj.Pkg() != nil && obj.Pkg().Path() == "testing" && obj.Name() == "F" +-} - --// The BinderFunc type adapts a bind function to implement the jsonrpc2.Binder --// interface. --type BinderFunc func(ctx context.Context, conn *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions +-// lexical finds completions in the lexical environment. +-func (c *completer) lexical(ctx context.Context) error { +- var ( +- builtinIota = types.Universe.Lookup("iota") +- builtinNil = types.Universe.Lookup("nil") - --func (f BinderFunc) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions { -- return f(ctx, conn) --} +- // TODO(rfindley): only allow "comparable" where it is valid (in constraint +- // position or embedded in interface declarations). +- // builtinComparable = types.Universe.Lookup("comparable") +- ) - --// Middleware defines a transformation of jsonrpc2 Binders, that may be --// composed to build jsonrpc2 servers. --type Middleware func(jsonrpc2_v2.Binder) jsonrpc2_v2.Binder +- // Track seen variables to avoid showing completions for shadowed variables. +- // This works since we look at scopes from innermost to outermost. +- seen := make(map[string]struct{}) - --// A ServerFunc is used to construct an LSP server for a given client. --type ServerFunc func(context.Context, protocol.ClientCloser) protocol.Server +- // Process scopes innermost first. +- for i, scope := range c.scopes { +- if scope == nil { +- continue +- } - --// ServerBinder binds incoming connections to a new server. --type ServerBinder struct { -- newServer ServerFunc --} +- Names: +- for _, name := range scope.Names() { +- declScope, obj := scope.LookupParent(name, c.pos) +- if declScope != scope { +- continue // Name was declared in some enclosing scope, or not at all. +- } - --func NewServerBinder(newServer ServerFunc) *ServerBinder { -- return &ServerBinder{newServer: newServer} --} +- // If obj's type is invalid, find the AST node that defines the lexical block +- // containing the declaration of obj. Don't resolve types for packages. +- if !isPkgName(obj) && !typeIsValid(obj.Type()) { +- // Match the scope to its ast.Node. If the scope is the package scope, +- // use the *ast.File as the starting node. +- var node ast.Node +- if i < len(c.path) { +- node = c.path[i] +- } else if i == len(c.path) { // use the *ast.File for package scope +- node = c.path[i-1] +- } +- if node != nil { +- if resolved := resolveInvalid(c.pkg.FileSet(), obj, node, c.pkg.TypesInfo()); resolved != nil { +- obj = resolved +- } +- } +- } - --func (b *ServerBinder) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions { -- client := protocol.ClientDispatcherV2(conn) -- server := b.newServer(ctx, client) -- serverHandler := protocol.ServerHandlerV2(server) -- // Wrap the server handler to inject the client into each request context, so -- // that log events are reflected back to the client. -- wrapped := jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) { -- ctx = protocol.WithClient(ctx, client) -- return serverHandler.Handle(ctx, req) -- }) -- preempter := &canceler{ -- conn: conn, -- } -- return jsonrpc2_v2.ConnectionOptions{ -- Handler: wrapped, -- Preempter: preempter, -- } --} +- // Don't use LHS of decl in RHS. +- for _, ident := range enclosingDeclLHS(c.path) { +- if obj.Pos() == ident.Pos() { +- continue Names +- } +- } - --type canceler struct { -- conn *jsonrpc2_v2.Connection --} +- // Don't suggest "iota" outside of const decls. +- if obj == builtinIota && !c.inConstDecl() { +- continue +- } - --func (c *canceler) Preempt(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) { -- if req.Method != "$/cancelRequest" { -- return nil, jsonrpc2_v2.ErrNotHandled -- } -- var params protocol.CancelParams -- if err := json.Unmarshal(req.Params, ¶ms); err != nil { -- return nil, fmt.Errorf("%w: %v", jsonrpc2_v2.ErrParse, err) -- } -- var id jsonrpc2_v2.ID -- switch raw := params.ID.(type) { -- case float64: -- id = jsonrpc2_v2.Int64ID(int64(raw)) -- case string: -- id = jsonrpc2_v2.StringID(raw) -- default: -- return nil, fmt.Errorf("%w: invalid ID type %T", jsonrpc2_v2.ErrParse, params.ID) -- } -- c.conn.Cancel(id) -- return nil, nil --} +- // Rank outer scopes lower than inner. +- score := stdScore * math.Pow(.99, float64(i)) - --type ForwardBinder struct { -- dialer jsonrpc2_v2.Dialer -- onBind func(*jsonrpc2_v2.Connection) --} +- // Dowrank "nil" a bit so it is ranked below more interesting candidates. +- if obj == builtinNil { +- score /= 2 +- } - --func NewForwardBinder(dialer jsonrpc2_v2.Dialer) *ForwardBinder { -- return &ForwardBinder{ -- dialer: dialer, +- // If we haven't already added a candidate for an object with this name. +- if _, ok := seen[obj.Name()]; !ok { +- seen[obj.Name()] = struct{}{} +- c.deepState.enqueue(candidate{ +- obj: obj, +- score: score, +- addressable: isVar(obj), +- }) +- } +- } - } --} - --func (b *ForwardBinder) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) (opts jsonrpc2_v2.ConnectionOptions) { -- client := protocol.ClientDispatcherV2(conn) -- clientBinder := NewClientBinder(func(context.Context, protocol.Server) protocol.Client { return client }) +- if c.inference.objType != nil { +- if named, ok := aliases.Unalias(typesinternal.Unpointer(c.inference.objType)).(*types.Named); ok { +- // If we expected a named type, check the type's package for +- // completion items. This is useful when the current file hasn't +- // imported the type's package yet. - -- serverConn, err := jsonrpc2_v2.Dial(context.Background(), b.dialer, clientBinder) -- if err != nil { -- return jsonrpc2_v2.ConnectionOptions{ -- Handler: jsonrpc2_v2.HandlerFunc(func(context.Context, *jsonrpc2_v2.Request) (interface{}, error) { -- return nil, fmt.Errorf("%w: %v", jsonrpc2_v2.ErrInternal, err) -- }), +- if named.Obj() != nil && named.Obj().Pkg() != nil { +- pkg := named.Obj().Pkg() +- +- // Make sure the package name isn't already in use by another +- // object, and that this file doesn't import the package yet. +- // TODO(adonovan): what if pkg.Path has vendor/ prefix? +- if _, ok := seen[pkg.Name()]; !ok && pkg != c.pkg.Types() && !alreadyImports(c.file, golang.ImportPath(pkg.Path())) { +- seen[pkg.Name()] = struct{}{} +- obj := types.NewPkgName(0, nil, pkg.Name(), pkg) +- imp := &importInfo{ +- importPath: pkg.Path(), +- } +- if imports.ImportPathToAssumedName(pkg.Path()) != pkg.Name() { +- imp.name = pkg.Name() +- } +- c.deepState.enqueue(candidate{ +- obj: obj, +- score: stdScore, +- imp: imp, +- }) +- } +- } - } - } - -- if b.onBind != nil { -- b.onBind(serverConn) -- } -- server := protocol.ServerDispatcherV2(serverConn) -- preempter := &canceler{ -- conn: conn, -- } -- detached := xcontext.Detach(ctx) -- go func() { -- conn.Wait() -- if err := serverConn.Close(); err != nil { -- event.Log(detached, fmt.Sprintf("closing remote connection: %v", err)) +- if c.opts.unimported { +- if err := c.unimportedPackages(ctx, seen); err != nil { +- return err - } -- }() -- return jsonrpc2_v2.ConnectionOptions{ -- Handler: protocol.ServerHandlerV2(server), -- Preempter: preempter, - } --} - --// A ClientFunc is used to construct an LSP client for a given server. --type ClientFunc func(context.Context, protocol.Server) protocol.Client +- if c.inference.typeName.isTypeParam { +- // If we are completing a type param, offer each structural type. +- // This ensures we suggest "[]int" and "[]float64" for a constraint +- // with type union "[]int | []float64". +- if t, ok := c.inference.objType.(*types.Interface); ok { +- if terms, err := typeparams.InterfaceTermSet(t); err == nil { +- for _, term := range terms { +- c.injectType(ctx, term.Type()) +- } +- } +- } +- } else { +- c.injectType(ctx, c.inference.objType) +- } - --// ClientBinder binds an LSP client to an incoming connection. --type ClientBinder struct { -- newClient ClientFunc --} +- // Add keyword completion items appropriate in the current context. +- c.addKeywordCompletions() - --func NewClientBinder(newClient ClientFunc) *ClientBinder { -- return &ClientBinder{newClient} +- return nil -} - --func (b *ClientBinder) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions { -- server := protocol.ServerDispatcherV2(conn) -- client := b.newClient(ctx, server) -- return jsonrpc2_v2.ConnectionOptions{ -- Handler: protocol.ClientHandlerV2(client), +-// injectType manufactures candidates based on the given type. This is +-// intended for types not discoverable via lexical search, such as +-// composite and/or generic types. For example, if the type is "[]int", +-// this method makes sure you get candidates "[]int{}" and "[]int" +-// (the latter applies when completing a type name). +-func (c *completer) injectType(ctx context.Context, t types.Type) { +- if t == nil { +- return - } --} -diff -urN a/gopls/internal/lsp/lsprpc/binder_test.go b/gopls/internal/lsp/lsprpc/binder_test.go ---- a/gopls/internal/lsp/lsprpc/binder_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/lsprpc/binder_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,147 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package lsprpc_test +- t = typesinternal.Unpointer(t) - --import ( -- "context" -- "regexp" -- "strings" -- "testing" -- "time" +- // If we have an expected type and it is _not_ a named type, handle +- // it specially. Non-named types like "[]int" will never be +- // considered via a lexical search, so we need to directly inject +- // them. Also allow generic types since lexical search does not +- // infer instantiated versions of them. +- if named, ok := aliases.Unalias(t).(*types.Named); !ok || named.TypeParams().Len() > 0 { +- // If our expected type is "[]int", this will add a literal +- // candidate of "[]int{}". +- c.literal(ctx, t, nil) - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" +- if _, isBasic := t.(*types.Basic); !isBasic { +- // If we expect a non-basic type name (e.g. "[]int"), hack up +- // a named type whose name is literally "[]int". This allows +- // us to reuse our object based completion machinery. +- fakeNamedType := candidate{ +- obj: types.NewTypeName(token.NoPos, nil, types.TypeString(t, c.qf), t), +- score: stdScore, +- } +- // Make sure the type name matches before considering +- // candidate. This cuts down on useless candidates. +- if c.matchingTypeName(&fakeNamedType) { +- c.deepState.enqueue(fakeNamedType) +- } +- } +- } +-} - -- . "golang.org/x/tools/gopls/internal/lsp/lsprpc" --) +-func (c *completer) unimportedPackages(ctx context.Context, seen map[string]struct{}) error { +- var prefix string +- if c.surrounding != nil { +- prefix = c.surrounding.Prefix() +- } - --type TestEnv struct { -- Conns []*jsonrpc2_v2.Connection -- Servers []*jsonrpc2_v2.Server --} +- // Don't suggest unimported packages if we have absolutely nothing +- // to go on. +- if prefix == "" { +- return nil +- } - --func (e *TestEnv) Shutdown(t *testing.T) { -- for _, s := range e.Servers { -- s.Shutdown() +- count := 0 +- +- // Search the forward transitive closure of the workspace. +- all, err := c.snapshot.AllMetadata(ctx) +- if err != nil { +- return err - } -- for _, c := range e.Conns { -- if err := c.Close(); err != nil { -- t.Error(err) +- pkgNameByPath := make(map[golang.PackagePath]string) +- var paths []string // actually PackagePaths +- for _, mp := range all { +- if mp.ForTest != "" { +- continue // skip all test variants - } -- } -- for _, s := range e.Servers { -- if err := s.Wait(); err != nil { -- t.Error(err) +- if mp.Name == "main" { +- continue // main is non-importable +- } +- if !strings.HasPrefix(string(mp.Name), prefix) { +- continue // not a match - } +- paths = append(paths, string(mp.PkgPath)) +- pkgNameByPath[mp.PkgPath] = string(mp.Name) - } --} - --func (e *TestEnv) serve(ctx context.Context, t *testing.T, server jsonrpc2_v2.Binder) (jsonrpc2_v2.Listener, *jsonrpc2_v2.Server) { -- l, err := jsonrpc2_v2.NetPipeListener(ctx) -- if err != nil { -- t.Fatal(err) +- // Rank candidates using goimports' algorithm. +- var relevances map[string]float64 +- if len(paths) != 0 { +- if err := c.snapshot.RunProcessEnvFunc(ctx, func(ctx context.Context, opts *imports.Options) error { +- var err error +- relevances, err = imports.ScoreImportPaths(ctx, opts.Env, paths) +- return err +- }); err != nil { +- return err +- } - } -- s := jsonrpc2_v2.NewServer(ctx, l, server) -- e.Servers = append(e.Servers, s) -- return l, s --} +- sort.Slice(paths, func(i, j int) bool { +- if relevances[paths[i]] != relevances[paths[j]] { +- return relevances[paths[i]] > relevances[paths[j]] +- } - --func (e *TestEnv) dial(ctx context.Context, t *testing.T, dialer jsonrpc2_v2.Dialer, client jsonrpc2_v2.Binder, forwarded bool) *jsonrpc2_v2.Connection { -- if forwarded { -- l, _ := e.serve(ctx, t, NewForwardBinder(dialer)) -- dialer = l.Dialer() +- // Fall back to lexical sort to keep truncated set of candidates +- // in a consistent order. +- return paths[i] < paths[j] +- }) +- +- for _, path := range paths { +- name := pkgNameByPath[golang.PackagePath(path)] +- if _, ok := seen[name]; ok { +- continue +- } +- imp := &importInfo{ +- importPath: path, +- } +- if imports.ImportPathToAssumedName(path) != name { +- imp.name = name +- } +- if count >= maxUnimportedPackageNames { +- return nil +- } +- c.deepState.enqueue(candidate{ +- // Pass an empty *types.Package to disable deep completions. +- obj: types.NewPkgName(0, nil, name, types.NewPackage(path, name)), +- score: unimportedScore(relevances[path]), +- imp: imp, +- }) +- count++ - } -- conn, err := jsonrpc2_v2.Dial(ctx, dialer, client) -- if err != nil { -- t.Fatal(err) +- +- var mu sync.Mutex +- add := func(pkg imports.ImportFix) { +- if ignoreUnimportedCompletion(&pkg) { +- return +- } +- mu.Lock() +- defer mu.Unlock() +- if _, ok := seen[pkg.IdentName]; ok { +- return +- } +- if _, ok := relevances[pkg.StmtInfo.ImportPath]; ok { +- return +- } +- +- if count >= maxUnimportedPackageNames { +- return +- } +- +- // Do not add the unimported packages to seen, since we can have +- // multiple packages of the same name as completion suggestions, since +- // only one will be chosen. +- obj := types.NewPkgName(0, nil, pkg.IdentName, types.NewPackage(pkg.StmtInfo.ImportPath, pkg.IdentName)) +- c.deepState.enqueue(candidate{ +- obj: obj, +- score: unimportedScore(pkg.Relevance), +- imp: &importInfo{ +- importPath: pkg.StmtInfo.ImportPath, +- name: pkg.StmtInfo.Name, +- }, +- }) +- count++ - } -- e.Conns = append(e.Conns, conn) -- return conn --} - --func staticClientBinder(client protocol.Client) jsonrpc2_v2.Binder { -- f := func(context.Context, protocol.Server) protocol.Client { return client } -- return NewClientBinder(f) +- c.completionCallbacks = append(c.completionCallbacks, func(ctx context.Context, opts *imports.Options) error { +- return imports.GetAllCandidates(ctx, add, prefix, c.filename, c.pkg.Types().Name(), opts.Env) +- }) +- +- return nil -} - --func staticServerBinder(server protocol.Server) jsonrpc2_v2.Binder { -- f := func(ctx context.Context, client protocol.ClientCloser) protocol.Server { -- return server +-// alreadyImports reports whether f has an import with the specified path. +-func alreadyImports(f *ast.File, path golang.ImportPath) bool { +- for _, s := range f.Imports { +- if metadata.UnquoteImportPath(s) == path { +- return true +- } - } -- return NewServerBinder(f) +- return false -} - --func TestClientLoggingV2(t *testing.T) { -- ctx := context.Background() +-func (c *completer) inConstDecl() bool { +- for _, n := range c.path { +- if decl, ok := n.(*ast.GenDecl); ok && decl.Tok == token.CONST { +- return true +- } +- } +- return false +-} - -- for name, forwarded := range map[string]bool{ -- "forwarded": true, -- "standalone": false, -- } { -- t.Run(name, func(t *testing.T) { -- client := FakeClient{Logs: make(chan string, 10)} -- env := new(TestEnv) -- defer env.Shutdown(t) -- l, _ := env.serve(ctx, t, staticServerBinder(PingServer{})) -- conn := env.dial(ctx, t, l.Dialer(), staticClientBinder(client), forwarded) +-// structLiteralFieldName finds completions for struct field names inside a struct literal. +-func (c *completer) structLiteralFieldName(ctx context.Context) error { +- clInfo := c.enclosingCompositeLiteral - -- if err := protocol.ServerDispatcherV2(conn).DidOpen(ctx, &protocol.DidOpenTextDocumentParams{}); err != nil { -- t.Errorf("DidOpen: %v", err) +- // Mark fields of the composite literal that have already been set, +- // except for the current field. +- addedFields := make(map[*types.Var]bool) +- for _, el := range clInfo.cl.Elts { +- if kvExpr, ok := el.(*ast.KeyValueExpr); ok { +- if clInfo.kv == kvExpr { +- continue - } -- select { -- case got := <-client.Logs: -- want := "ping" -- matched, err := regexp.MatchString(want, got) -- if err != nil { -- t.Fatal(err) -- } -- if !matched { -- t.Errorf("got log %q, want a log containing %q", got, want) +- +- if key, ok := kvExpr.Key.(*ast.Ident); ok { +- if used, ok := c.pkg.TypesInfo().Uses[key]; ok { +- if usedVar, ok := used.(*types.Var); ok { +- addedFields[usedVar] = true +- } - } -- case <-time.After(1 * time.Second): -- t.Error("timeout waiting for client log") - } -- }) +- } - } --} -- --func TestRequestCancellationV2(t *testing.T) { -- ctx := context.Background() - -- for name, forwarded := range map[string]bool{ -- "forwarded": true, -- "standalone": false, -- } { -- t.Run(name, func(t *testing.T) { -- server := WaitableServer{ -- Started: make(chan struct{}), -- Completed: make(chan error), +- // Add struct fields. +- if t, ok := aliases.Unalias(clInfo.clType).(*types.Struct); ok { +- const deltaScore = 0.0001 +- for i := 0; i < t.NumFields(); i++ { +- field := t.Field(i) +- if !addedFields[field] { +- c.deepState.enqueue(candidate{ +- obj: field, +- score: highScore - float64(i)*deltaScore, +- }) - } -- env := new(TestEnv) -- defer env.Shutdown(t) -- l, _ := env.serve(ctx, t, staticServerBinder(server)) -- client := FakeClient{Logs: make(chan string, 10)} -- conn := env.dial(ctx, t, l.Dialer(), staticClientBinder(client), forwarded) +- } - -- sd := protocol.ServerDispatcherV2(conn) -- ctx, cancel := context.WithCancel(ctx) +- // Fall through and add lexical completions if we aren't +- // certain we are in the key part of a key-value pair. +- if !clInfo.maybeInFieldName { +- return nil +- } +- } - -- result := make(chan error) -- go func() { -- _, err := sd.Hover(ctx, &protocol.HoverParams{}) -- result <- err -- }() -- // Wait for the Hover request to start. -- <-server.Started -- cancel() -- if err := <-result; err == nil { -- t.Error("nil error for cancelled Hover(), want non-nil") -- } -- if err := <-server.Completed; err == nil || !strings.Contains(err.Error(), "cancelled hover") { -- t.Errorf("Hover(): unexpected server-side error %v", err) -- } -- }) -- } +- return c.lexical(ctx) -} -diff -urN a/gopls/internal/lsp/lsprpc/commandinterceptor.go b/gopls/internal/lsp/lsprpc/commandinterceptor.go ---- a/gopls/internal/lsp/lsprpc/commandinterceptor.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/lsprpc/commandinterceptor.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,44 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package lsprpc - --import ( -- "context" -- "encoding/json" +-// enclosingCompositeLiteral returns information about the composite literal enclosing the +-// position. +-func enclosingCompositeLiteral(path []ast.Node, pos token.Pos, info *types.Info) *compLitInfo { +- for _, n := range path { +- switch n := n.(type) { +- case *ast.CompositeLit: +- // The enclosing node will be a composite literal if the user has just +- // opened the curly brace (e.g. &x{<>) or the completion request is triggered +- // from an already completed composite literal expression (e.g. &x{foo: 1, <>}) +- // +- // The position is not part of the composite literal unless it falls within the +- // curly braces (e.g. "foo.Foo<>Struct{}"). +- if !(n.Lbrace < pos && pos <= n.Rbrace) { +- // Keep searching since we may yet be inside a composite literal. +- // For example "Foo{B: Ba<>{}}". +- break +- } - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" --) +- tv, ok := info.Types[n] +- if !ok { +- return nil +- } - --// HandlerMiddleware is a middleware that only modifies the jsonrpc2 handler. --type HandlerMiddleware func(jsonrpc2_v2.Handler) jsonrpc2_v2.Handler +- clInfo := compLitInfo{ +- cl: n, +- clType: typesinternal.Unpointer(tv.Type).Underlying(), +- } - --// BindHandler transforms a HandlerMiddleware into a Middleware. --func BindHandler(hmw HandlerMiddleware) Middleware { -- return Middleware(func(binder jsonrpc2_v2.Binder) jsonrpc2_v2.Binder { -- return BinderFunc(func(ctx context.Context, conn *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions { -- opts := binder.Bind(ctx, conn) -- opts.Handler = hmw(opts.Handler) -- return opts -- }) -- }) --} +- var ( +- expr ast.Expr +- hasKeys bool +- ) +- for _, el := range n.Elts { +- // Remember the expression that the position falls in, if any. +- if el.Pos() <= pos && pos <= el.End() { +- expr = el +- } - --func CommandInterceptor(command string, run func(*protocol.ExecuteCommandParams) (interface{}, error)) Middleware { -- return BindHandler(func(delegate jsonrpc2_v2.Handler) jsonrpc2_v2.Handler { -- return jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) { -- if req.Method == "workspace/executeCommand" { -- var params protocol.ExecuteCommandParams -- if err := json.Unmarshal(req.Params, ¶ms); err == nil { -- if params.Command == command { -- return run(¶ms) +- if kv, ok := el.(*ast.KeyValueExpr); ok { +- hasKeys = true +- // If expr == el then we know the position falls in this expression, +- // so also record kv as the enclosing *ast.KeyValueExpr. +- if expr == el { +- clInfo.kv = kv +- break - } - } - } - -- return delegate.Handle(ctx, req) -- }) -- }) --} -diff -urN a/gopls/internal/lsp/lsprpc/commandinterceptor_test.go b/gopls/internal/lsp/lsprpc/commandinterceptor_test.go ---- a/gopls/internal/lsp/lsprpc/commandinterceptor_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/lsprpc/commandinterceptor_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,42 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package lsprpc_test -- --import ( -- "context" -- "testing" -- -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- -- . "golang.org/x/tools/gopls/internal/lsp/lsprpc" --) -- --func TestCommandInterceptor(t *testing.T) { -- const command = "foo" -- caught := false -- intercept := func(_ *protocol.ExecuteCommandParams) (interface{}, error) { -- caught = true -- return map[string]interface{}{}, nil -- } -- -- ctx := context.Background() -- env := new(TestEnv) -- defer env.Shutdown(t) -- mw := CommandInterceptor(command, intercept) -- l, _ := env.serve(ctx, t, mw(noopBinder)) -- conn := env.dial(ctx, t, l.Dialer(), noopBinder, false) -- -- params := &protocol.ExecuteCommandParams{ -- Command: command, -- } -- var res interface{} -- err := conn.Call(ctx, "workspace/executeCommand", params).Await(ctx, &res) -- if err != nil { -- t.Fatal(err) -- } -- if !caught { -- t.Errorf("workspace/executeCommand was not intercepted") -- } --} -diff -urN a/gopls/internal/lsp/lsprpc/dialer.go b/gopls/internal/lsp/lsprpc/dialer.go ---- a/gopls/internal/lsp/lsprpc/dialer.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/lsprpc/dialer.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,114 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package lsprpc -- --import ( -- "context" -- "fmt" -- "io" -- "net" -- "os" -- "time" -- -- exec "golang.org/x/sys/execabs" -- "golang.org/x/tools/internal/event" --) -- --// AutoNetwork is the pseudo network type used to signal that gopls should use --// automatic discovery to resolve a remote address. --const AutoNetwork = "auto" -- --// An AutoDialer is a jsonrpc2 dialer that understands the 'auto' network. --type AutoDialer struct { -- network, addr string // the 'real' network and address -- isAuto bool // whether the server is on the 'auto' network -- -- executable string -- argFunc func(network, addr string) []string --} +- if clInfo.kv != nil { +- // If in a *ast.KeyValueExpr, we know we are in the key if the position +- // is to the left of the colon (e.g. "Foo{F<>: V}". +- clInfo.inKey = pos <= clInfo.kv.Colon +- } else if hasKeys { +- // If we aren't in a *ast.KeyValueExpr but the composite literal has +- // other *ast.KeyValueExprs, we must be on the key side of a new +- // *ast.KeyValueExpr (e.g. "Foo{F: V, <>}"). +- clInfo.inKey = true +- } else { +- switch clInfo.clType.(type) { +- case *types.Struct: +- if len(n.Elts) == 0 { +- // If the struct literal is empty, next could be a struct field +- // name or an expression (e.g. "Foo{<>}" could become "Foo{F:}" +- // or "Foo{someVar}"). +- clInfo.maybeInFieldName = true +- } else if len(n.Elts) == 1 { +- // If there is one expression and the position is in that expression +- // and the expression is an identifier, we may be writing a field +- // name or an expression (e.g. "Foo{F<>}"). +- _, clInfo.maybeInFieldName = expr.(*ast.Ident) +- } +- case *types.Map: +- // If we aren't in a *ast.KeyValueExpr we must be adding a new key +- // to the map. +- clInfo.inKey = true +- } +- } - --func NewAutoDialer(rawAddr string, argFunc func(network, addr string) []string) (*AutoDialer, error) { -- d := AutoDialer{ -- argFunc: argFunc, -- } -- d.network, d.addr = ParseAddr(rawAddr) -- if d.network == AutoNetwork { -- d.isAuto = true -- bin, err := os.Executable() -- if err != nil { -- return nil, fmt.Errorf("getting executable: %w", err) +- return &clInfo +- default: +- if breaksExpectedTypeInference(n, pos) { +- return nil +- } - } -- d.executable = bin -- d.network, d.addr = autoNetworkAddress(bin, d.addr) - } -- return &d, nil --} - --// Dial implements the jsonrpc2.Dialer interface. --func (d *AutoDialer) Dial(ctx context.Context) (io.ReadWriteCloser, error) { -- conn, err := d.dialNet(ctx) -- return conn, err +- return nil -} - --// TODO(rFindley): remove this once we no longer need to integrate with v1 of --// the jsonrpc2 package. --func (d *AutoDialer) dialNet(ctx context.Context) (net.Conn, error) { -- // Attempt to verify that we own the remote. This is imperfect, but if we can -- // determine that the remote is owned by a different user, we should fail. -- ok, err := verifyRemoteOwnership(d.network, d.addr) -- if err != nil { -- // If the ownership check itself failed, we fail open but log an error to -- // the user. -- event.Error(ctx, "unable to check daemon socket owner, failing open", err) -- } else if !ok { -- // We successfully checked that the socket is not owned by us, we fail -- // closed. -- return nil, fmt.Errorf("socket %q is owned by a different user", d.addr) -- } -- const dialTimeout = 1 * time.Second -- // Try dialing our remote once, in case it is already running. -- netConn, err := net.DialTimeout(d.network, d.addr, dialTimeout) -- if err == nil { -- return netConn, nil -- } -- if d.isAuto && d.argFunc != nil { -- if d.network == "unix" { -- // Sometimes the socketfile isn't properly cleaned up when the server -- // shuts down. Since we have already tried and failed to dial this -- // address, it should *usually* be safe to remove the socket before -- // binding to the address. -- // TODO(rfindley): there is probably a race here if multiple server -- // instances are simultaneously starting up. -- if _, err := os.Stat(d.addr); err == nil { -- if err := os.Remove(d.addr); err != nil { -- return nil, fmt.Errorf("removing remote socket file: %w", err) +-// enclosingFunction returns the signature and body of the function +-// enclosing the given position. +-func enclosingFunction(path []ast.Node, info *types.Info) *funcInfo { +- for _, node := range path { +- switch t := node.(type) { +- case *ast.FuncDecl: +- if obj, ok := info.Defs[t.Name]; ok { +- return &funcInfo{ +- sig: obj.Type().(*types.Signature), +- body: t.Body, +- } +- } +- case *ast.FuncLit: +- if typ, ok := info.Types[t]; ok { +- if sig, _ := typ.Type.(*types.Signature); sig == nil { +- // golang/go#49397: it should not be possible, but we somehow arrived +- // here with a non-signature type, most likely due to AST mangling +- // such that node.Type is not a FuncType. +- return nil +- } +- return &funcInfo{ +- sig: typ.Type.(*types.Signature), +- body: t.Body, - } - } -- } -- args := d.argFunc(d.network, d.addr) -- cmd := exec.Command(d.executable, args...) -- if err := runRemote(cmd); err != nil { -- return nil, err - } - } +- return nil +-} - -- const retries = 5 -- // It can take some time for the newly started server to bind to our address, -- // so we retry for a bit. -- for retry := 0; retry < retries; retry++ { -- startDial := time.Now() -- netConn, err = net.DialTimeout(d.network, d.addr, dialTimeout) -- if err == nil { -- return netConn, nil +-func (c *completer) expectedCompositeLiteralType() types.Type { +- clInfo := c.enclosingCompositeLiteral +- switch t := clInfo.clType.(type) { +- case *types.Slice: +- if clInfo.inKey { +- return types.Typ[types.UntypedInt] - } -- event.Log(ctx, fmt.Sprintf("failed attempt #%d to connect to remote: %v\n", retry+2, err)) -- // In case our failure was a fast-failure, ensure we wait at least -- // f.dialTimeout before trying again. -- if retry != retries-1 { -- time.Sleep(dialTimeout - time.Since(startDial)) +- return t.Elem() +- case *types.Array: +- if clInfo.inKey { +- return types.Typ[types.UntypedInt] +- } +- return t.Elem() +- case *types.Map: +- if clInfo.inKey { +- return t.Key() +- } +- return t.Elem() +- case *types.Struct: +- // If we are completing a key (i.e. field name), there is no expected type. +- if clInfo.inKey { +- return nil - } -- } -- return nil, fmt.Errorf("dialing remote: %w", err) --} -diff -urN a/gopls/internal/lsp/lsprpc/goenv.go b/gopls/internal/lsp/lsprpc/goenv.go ---- a/gopls/internal/lsp/lsprpc/goenv.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/lsprpc/goenv.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,96 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package lsprpc -- --import ( -- "context" -- "encoding/json" -- "fmt" -- "os" -- -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/gocommand" -- jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" --) - --func GoEnvMiddleware() (Middleware, error) { -- return BindHandler(func(delegate jsonrpc2_v2.Handler) jsonrpc2_v2.Handler { -- return jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) { -- if req.Method == "initialize" { -- if err := addGoEnvToInitializeRequestV2(ctx, req); err != nil { -- event.Error(ctx, "adding go env to initialize", err) +- // If we are in a key-value pair, but not in the key, then we must be on the +- // value side. The expected type of the value will be determined from the key. +- if clInfo.kv != nil { +- if key, ok := clInfo.kv.Key.(*ast.Ident); ok { +- for i := 0; i < t.NumFields(); i++ { +- if field := t.Field(i); field.Name() == key.Name { +- return field.Type() +- } - } - } -- return delegate.Handle(ctx, req) -- }) -- }), nil --} +- } else { +- // If we aren't in a key-value pair and aren't in the key, we must be using +- // implicit field names. - --func addGoEnvToInitializeRequestV2(ctx context.Context, req *jsonrpc2_v2.Request) error { -- var params protocol.ParamInitialize -- if err := json.Unmarshal(req.Params, ¶ms); err != nil { -- return err -- } -- var opts map[string]interface{} -- switch v := params.InitializationOptions.(type) { -- case nil: -- opts = make(map[string]interface{}) -- case map[string]interface{}: -- opts = v -- default: -- return fmt.Errorf("unexpected type for InitializationOptions: %T", v) -- } -- envOpt, ok := opts["env"] -- if !ok { -- envOpt = make(map[string]interface{}) -- } -- env, ok := envOpt.(map[string]interface{}) -- if !ok { -- return fmt.Errorf("env option is %T, expected a map", envOpt) -- } -- goenv, err := getGoEnv(ctx, env) -- if err != nil { -- return err -- } -- // We don't want to propagate GOWORK unless explicitly set since that could mess with -- // path inference during cmd/go invocations, see golang/go#51825. -- _, goworkSet := os.LookupEnv("GOWORK") -- for govar, value := range goenv { -- if govar == "GOWORK" && !goworkSet { -- continue +- // The order of the literal fields must match the order in the struct definition. +- // Find the element that the position belongs to and suggest that field's type. +- if i := exprAtPos(c.pos, clInfo.cl.Elts); i < t.NumFields() { +- return t.Field(i).Type() +- } - } -- env[govar] = value - } -- opts["env"] = env -- params.InitializationOptions = opts -- raw, err := json.Marshal(params) -- if err != nil { -- return fmt.Errorf("marshaling updated options: %v", err) -- } -- req.Params = json.RawMessage(raw) - return nil -} - --func getGoEnv(ctx context.Context, env map[string]interface{}) (map[string]string, error) { -- var runEnv []string -- for k, v := range env { -- runEnv = append(runEnv, fmt.Sprintf("%s=%s", k, v)) -- } -- runner := gocommand.Runner{} -- output, err := runner.Run(ctx, gocommand.Invocation{ -- Verb: "env", -- Args: []string{"-json"}, -- Env: runEnv, -- }) -- if err != nil { -- return nil, err -- } -- envmap := make(map[string]string) -- if err := json.Unmarshal(output.Bytes(), &envmap); err != nil { -- return nil, err -- } -- return envmap, nil +-// typeMod represents an operator that changes the expected type. +-type typeMod struct { +- mod typeModKind +- arrayLen int64 -} -diff -urN a/gopls/internal/lsp/lsprpc/goenv_test.go b/gopls/internal/lsp/lsprpc/goenv_test.go ---- a/gopls/internal/lsp/lsprpc/goenv_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/lsprpc/goenv_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,68 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package lsprpc_test -- --import ( -- "context" -- "testing" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/internal/testenv" +-type typeModKind int - -- . "golang.org/x/tools/gopls/internal/lsp/lsprpc" +-const ( +- dereference typeModKind = iota // pointer indirection: "*" +- reference // adds level of pointer: "&" for values, "*" for type names +- chanRead // channel read operator: "<-" +- sliceType // make a slice type: "[]" in "[]int" +- arrayType // make an array type: "[2]" in "[2]int" +- invoke // make a function call: "()" in "foo()" +- takeSlice // take slice of array: "[:]" in "foo[:]" +- takeDotDotDot // turn slice into variadic args: "..." in "foo..." +- index // index into slice/array: "[0]" in "foo[0]" -) - --type initServer struct { -- protocol.Server +-type objKind int - -- params *protocol.ParamInitialize --} +-const ( +- kindAny objKind = 0 +- kindArray objKind = 1 << iota +- kindSlice +- kindChan +- kindMap +- kindStruct +- kindString +- kindInt +- kindBool +- kindBytes +- kindPtr +- kindFloat +- kindComplex +- kindError +- kindStringer +- kindFunc +-) - --func (s *initServer) Initialize(ctx context.Context, params *protocol.ParamInitialize) (*protocol.InitializeResult, error) { -- s.params = params -- return &protocol.InitializeResult{}, nil +-// penalizedObj represents an object that should be disfavored as a +-// completion candidate. +-type penalizedObj struct { +- // objChain is the full "chain", e.g. "foo.bar().baz" becomes +- // []types.Object{foo, bar, baz}. +- objChain []types.Object +- // penalty is score penalty in the range (0, 1). +- penalty float64 -} - --func TestGoEnvMiddleware(t *testing.T) { -- testenv.NeedsTool(t, "go") +-// candidateInference holds information we have inferred about a type that can be +-// used at the current position. +-type candidateInference struct { +- // objType is the desired type of an object used at the query position. +- objType types.Type - -- ctx := context.Background() +- // objKind is a mask of expected kinds of types such as "map", "slice", etc. +- objKind objKind - -- server := &initServer{} -- env := new(TestEnv) -- defer env.Shutdown(t) -- l, _ := env.serve(ctx, t, staticServerBinder(server)) -- mw, err := GoEnvMiddleware() -- if err != nil { -- t.Fatal(err) -- } -- binder := mw(NewForwardBinder(l.Dialer())) -- l, _ = env.serve(ctx, t, binder) -- conn := env.dial(ctx, t, l.Dialer(), noopBinder, true) -- dispatch := protocol.ServerDispatcherV2(conn) -- initParams := &protocol.ParamInitialize{} -- initParams.InitializationOptions = map[string]interface{}{ -- "env": map[string]interface{}{ -- "GONOPROXY": "example.com", -- }, -- } -- if _, err := dispatch.Initialize(ctx, initParams); err != nil { -- t.Fatal(err) -- } +- // variadic is true if we are completing the initial variadic +- // parameter. For example: +- // append([]T{}, <>) // objType=T variadic=true +- // append([]T{}, T{}, <>) // objType=T variadic=false +- variadic bool - -- if server.params == nil { -- t.Fatalf("initialize params are unset") -- } -- envOpts := server.params.InitializationOptions.(map[string]interface{})["env"].(map[string]interface{}) +- // modifiers are prefixes such as "*", "&" or "<-" that influence how +- // a candidate type relates to the expected type. +- modifiers []typeMod - -- // Check for an arbitrary Go variable. It should be set. -- if _, ok := envOpts["GOPRIVATE"]; !ok { -- t.Errorf("Go environment variable GOPRIVATE unset in initialization options") -- } -- // Check that the variable present in our user config was not overwritten. -- if got, want := envOpts["GONOPROXY"], "example.com"; got != want { -- t.Errorf("GONOPROXY=%q, want %q", got, want) -- } --} -diff -urN a/gopls/internal/lsp/lsprpc/lsprpc.go b/gopls/internal/lsp/lsprpc/lsprpc.go ---- a/gopls/internal/lsp/lsprpc/lsprpc.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/lsprpc/lsprpc.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,545 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +- // convertibleTo is a type our candidate type must be convertible to. +- convertibleTo types.Type - --// Package lsprpc implements a jsonrpc2.StreamServer that may be used to --// serve the LSP on a jsonrpc2 channel. --package lsprpc +- // typeName holds information about the expected type name at +- // position, if any. +- typeName typeNameInference - --import ( -- "context" -- "encoding/json" -- "fmt" -- "log" -- "net" -- "os" -- "strconv" -- "strings" -- "sync" -- "sync/atomic" -- "time" +- // assignees are the types that would receive a function call's +- // results at the position. For example: +- // +- // foo := 123 +- // foo, bar := <> +- // +- // at "<>", the assignees are [int, ]. +- assignees []types.Type - -- "golang.org/x/tools/gopls/internal/lsp" -- "golang.org/x/tools/gopls/internal/lsp/cache" -- "golang.org/x/tools/gopls/internal/lsp/command" -- "golang.org/x/tools/gopls/internal/lsp/debug" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/tag" -- "golang.org/x/tools/internal/jsonrpc2" --) +- // variadicAssignees is true if we could be completing an inner +- // function call that fills out an outer function call's variadic +- // params. For example: +- // +- // func foo(int, ...string) {} +- // +- // foo(<>) // variadicAssignees=true +- // foo(bar<>) // variadicAssignees=true +- // foo(bar, baz<>) // variadicAssignees=false +- variadicAssignees bool - --// Unique identifiers for client/server. --var serverIndex int64 +- // penalized holds expressions that should be disfavored as +- // candidates. For example, it tracks expressions already used in a +- // switch statement's other cases. Each expression is tracked using +- // its entire object "chain" allowing differentiation between +- // "a.foo" and "b.foo" when "a" and "b" are the same type. +- penalized []penalizedObj - --// The StreamServer type is a jsonrpc2.StreamServer that handles incoming --// streams as a new LSP session, using a shared cache. --type StreamServer struct { -- cache *cache.Cache -- // daemon controls whether or not to log new connections. -- daemon bool +- // objChain contains the chain of objects representing the +- // surrounding *ast.SelectorExpr. For example, if we are completing +- // "foo.bar.ba<>", objChain will contain []types.Object{foo, bar}. +- objChain []types.Object +-} - -- // optionsOverrides is passed to newly created sessions. -- optionsOverrides func(*source.Options) +-// typeNameInference holds information about the expected type name at +-// position. +-type typeNameInference struct { +- // wantTypeName is true if we expect the name of a type. +- wantTypeName bool - -- // serverForTest may be set to a test fake for testing. -- serverForTest protocol.Server --} +- // modifiers are prefixes such as "*", "&" or "<-" that influence how +- // a candidate type relates to the expected type. +- modifiers []typeMod - --// NewStreamServer creates a StreamServer using the shared cache. If --// withTelemetry is true, each session is instrumented with telemetry that --// records RPC statistics. --func NewStreamServer(cache *cache.Cache, daemon bool, optionsFunc func(*source.Options)) *StreamServer { -- return &StreamServer{cache: cache, daemon: daemon, optionsOverrides: optionsFunc} +- // assertableFrom is a type that must be assertable to our candidate type. +- assertableFrom types.Type +- +- // wantComparable is true if we want a comparable type. +- wantComparable bool +- +- // seenTypeSwitchCases tracks types that have already been used by +- // the containing type switch. +- seenTypeSwitchCases []types.Type +- +- // compLitType is true if we are completing a composite literal type +- // name, e.g "foo<>{}". +- compLitType bool +- +- // isTypeParam is true if we are completing a type instantiation parameter +- isTypeParam bool -} - --func (s *StreamServer) Binder() *ServerBinder { -- newServer := func(ctx context.Context, client protocol.ClientCloser) protocol.Server { -- session := cache.NewSession(ctx, s.cache) -- server := s.serverForTest -- if server == nil { -- options := source.DefaultOptions(s.optionsOverrides) -- server = lsp.NewServer(session, client, options) -- if instance := debug.GetInstance(ctx); instance != nil { -- instance.AddService(server, session) +-// expectedCandidate returns information about the expected candidate +-// for an expression at the query position. +-func expectedCandidate(ctx context.Context, c *completer) (inf candidateInference) { +- inf.typeName = expectTypeName(c) +- +- if c.enclosingCompositeLiteral != nil { +- inf.objType = c.expectedCompositeLiteralType() +- } +- +-Nodes: +- for i, node := range c.path { +- switch node := node.(type) { +- case *ast.BinaryExpr: +- // Determine if query position comes from left or right of op. +- e := node.X +- if c.pos < node.OpPos { +- e = node.Y +- } +- if tv, ok := c.pkg.TypesInfo().Types[e]; ok { +- switch node.Op { +- case token.LAND, token.LOR: +- // Don't infer "bool" type for "&&" or "||". Often you want +- // to compose a boolean expression from non-boolean +- // candidates. +- default: +- inf.objType = tv.Type +- } +- break Nodes +- } +- case *ast.AssignStmt: +- // Only rank completions if you are on the right side of the token. +- if c.pos > node.TokPos { +- i := exprAtPos(c.pos, node.Rhs) +- if i >= len(node.Lhs) { +- i = len(node.Lhs) - 1 +- } +- if tv, ok := c.pkg.TypesInfo().Types[node.Lhs[i]]; ok { +- inf.objType = tv.Type +- } +- +- // If we have a single expression on the RHS, record the LHS +- // assignees so we can favor multi-return function calls with +- // matching result values. +- if len(node.Rhs) <= 1 { +- for _, lhs := range node.Lhs { +- inf.assignees = append(inf.assignees, c.pkg.TypesInfo().TypeOf(lhs)) +- } +- } else { +- // Otherwise, record our single assignee, even if its type is +- // not available. We use this info to downrank functions +- // with the wrong number of result values. +- inf.assignees = append(inf.assignees, c.pkg.TypesInfo().TypeOf(node.Lhs[i])) +- } +- } +- return inf +- case *ast.ValueSpec: +- if node.Type != nil && c.pos > node.Type.End() { +- inf.objType = c.pkg.TypesInfo().TypeOf(node.Type) +- } +- return inf +- case *ast.CallExpr: +- // Only consider CallExpr args if position falls between parens. +- if node.Lparen < c.pos && c.pos <= node.Rparen { +- // For type conversions like "int64(foo)" we can only infer our +- // desired type is convertible to int64. +- if typ := typeConversion(node, c.pkg.TypesInfo()); typ != nil { +- inf.convertibleTo = typ +- break Nodes +- } +- +- sig, _ := c.pkg.TypesInfo().Types[node.Fun].Type.(*types.Signature) +- +- if sig != nil && sig.TypeParams().Len() > 0 { +- // If we are completing a generic func call, re-check the call expression. +- // This allows type param inference to work in cases like: +- // +- // func foo[T any](T) {} +- // foo[int](<>) // <- get "int" completions instead of "T" +- // +- // TODO: remove this after https://go.dev/issue/52503 +- info := &types.Info{Types: make(map[ast.Expr]types.TypeAndValue)} +- types.CheckExpr(c.pkg.FileSet(), c.pkg.Types(), node.Fun.Pos(), node.Fun, info) +- sig, _ = info.Types[node.Fun].Type.(*types.Signature) +- } +- +- if sig != nil { +- inf = c.expectedCallParamType(inf, node, sig) +- } +- +- if funIdent, ok := node.Fun.(*ast.Ident); ok { +- obj := c.pkg.TypesInfo().ObjectOf(funIdent) +- +- if obj != nil && obj.Parent() == types.Universe { +- // Defer call to builtinArgType so we can provide it the +- // inferred type from its parent node. +- defer func() { +- inf = c.builtinArgType(obj, node, inf) +- inf.objKind = c.builtinArgKind(ctx, obj, node) +- }() +- +- // The expected type of builtin arguments like append() is +- // the expected type of the builtin call itself. For +- // example: +- // +- // var foo []int = append(<>) +- // +- // To find the expected type at <> we "skip" the append() +- // node and get the expected type one level up, which is +- // []int. +- continue Nodes +- } +- } +- +- return inf +- } +- case *ast.ReturnStmt: +- if c.enclosingFunc != nil { +- sig := c.enclosingFunc.sig +- // Find signature result that corresponds to our return statement. +- if resultIdx := exprAtPos(c.pos, node.Results); resultIdx < len(node.Results) { +- if resultIdx < sig.Results().Len() { +- inf.objType = sig.Results().At(resultIdx).Type() +- } +- } +- } +- return inf +- case *ast.CaseClause: +- if swtch, ok := findSwitchStmt(c.path[i+1:], c.pos, node).(*ast.SwitchStmt); ok { +- if tv, ok := c.pkg.TypesInfo().Types[swtch.Tag]; ok { +- inf.objType = tv.Type +- +- // Record which objects have already been used in the case +- // statements so we don't suggest them again. +- for _, cc := range swtch.Body.List { +- for _, caseExpr := range cc.(*ast.CaseClause).List { +- // Don't record the expression we are currently completing. +- if caseExpr.Pos() < c.pos && c.pos <= caseExpr.End() { +- continue +- } +- +- if objs := objChain(c.pkg.TypesInfo(), caseExpr); len(objs) > 0 { +- inf.penalized = append(inf.penalized, penalizedObj{objChain: objs, penalty: 0.1}) +- } +- } +- } +- } +- } +- return inf +- case *ast.SliceExpr: +- // Make sure position falls within the brackets (e.g. "foo[a:<>]"). +- if node.Lbrack < c.pos && c.pos <= node.Rbrack { +- inf.objType = types.Typ[types.UntypedInt] +- } +- return inf +- case *ast.IndexExpr: +- // Make sure position falls within the brackets (e.g. "foo[<>]"). +- if node.Lbrack < c.pos && c.pos <= node.Rbrack { +- if tv, ok := c.pkg.TypesInfo().Types[node.X]; ok { +- switch t := tv.Type.Underlying().(type) { +- case *types.Map: +- inf.objType = t.Key() +- case *types.Slice, *types.Array: +- inf.objType = types.Typ[types.UntypedInt] +- } +- +- if ct := expectedConstraint(tv.Type, 0); ct != nil { +- inf.objType = ct +- inf.typeName.wantTypeName = true +- inf.typeName.isTypeParam = true +- } +- } +- } +- return inf +- case *ast.IndexListExpr: +- if node.Lbrack < c.pos && c.pos <= node.Rbrack { +- if tv, ok := c.pkg.TypesInfo().Types[node.X]; ok { +- if ct := expectedConstraint(tv.Type, exprAtPos(c.pos, node.Indices)); ct != nil { +- inf.objType = ct +- inf.typeName.wantTypeName = true +- inf.typeName.isTypeParam = true +- } +- } +- } +- return inf +- case *ast.SendStmt: +- // Make sure we are on right side of arrow (e.g. "foo <- <>"). +- if c.pos > node.Arrow+1 { +- if tv, ok := c.pkg.TypesInfo().Types[node.Chan]; ok { +- if ch, ok := tv.Type.Underlying().(*types.Chan); ok { +- inf.objType = ch.Elem() +- } +- } +- } +- return inf +- case *ast.RangeStmt: +- if goplsastutil.NodeContains(node.X, c.pos) { +- inf.objKind |= kindSlice | kindArray | kindMap | kindString +- if node.Value == nil { +- inf.objKind |= kindChan +- } +- } +- return inf +- case *ast.StarExpr: +- inf.modifiers = append(inf.modifiers, typeMod{mod: dereference}) +- case *ast.UnaryExpr: +- switch node.Op { +- case token.AND: +- inf.modifiers = append(inf.modifiers, typeMod{mod: reference}) +- case token.ARROW: +- inf.modifiers = append(inf.modifiers, typeMod{mod: chanRead}) +- } +- case *ast.DeferStmt, *ast.GoStmt: +- inf.objKind |= kindFunc +- return inf +- default: +- if breaksExpectedTypeInference(node, c.pos) { +- return inf - } - } -- return server - } -- return NewServerBinder(newServer) +- +- return inf -} - --// ServeStream implements the jsonrpc2.StreamServer interface, by handling --// incoming streams using a new lsp server. --func (s *StreamServer) ServeStream(ctx context.Context, conn jsonrpc2.Conn) error { -- client := protocol.ClientDispatcher(conn) -- session := cache.NewSession(ctx, s.cache) -- server := s.serverForTest -- if server == nil { -- options := source.DefaultOptions(s.optionsOverrides) -- server = lsp.NewServer(session, client, options) -- if instance := debug.GetInstance(ctx); instance != nil { -- instance.AddService(server, session) -- } +-func (c *completer) expectedCallParamType(inf candidateInference, node *ast.CallExpr, sig *types.Signature) candidateInference { +- numParams := sig.Params().Len() +- if numParams == 0 { +- return inf - } -- // Clients may or may not send a shutdown message. Make sure the server is -- // shut down. -- // TODO(rFindley): this shutdown should perhaps be on a disconnected context. -- defer func() { -- if err := server.Shutdown(ctx); err != nil { -- event.Error(ctx, "error shutting down", err) +- +- exprIdx := exprAtPos(c.pos, node.Args) +- +- // If we have one or zero arg expressions, we may be +- // completing to a function call that returns multiple +- // values, in turn getting passed in to the surrounding +- // call. Record the assignees so we can favor function +- // calls that return matching values. +- if len(node.Args) <= 1 && exprIdx == 0 { +- for i := 0; i < sig.Params().Len(); i++ { +- inf.assignees = append(inf.assignees, sig.Params().At(i).Type()) - } -- }() -- executable, err := os.Executable() -- if err != nil { -- log.Printf("error getting gopls path: %v", err) -- executable = "" -- } -- ctx = protocol.WithClient(ctx, client) -- conn.Go(ctx, -- protocol.Handlers( -- handshaker(session, executable, s.daemon, -- protocol.ServerHandler(server, -- jsonrpc2.MethodNotFound)))) -- if s.daemon { -- log.Printf("Session %s: connected", session.ID()) -- defer log.Printf("Session %s: exited", session.ID()) +- +- // Record that we may be completing into variadic parameters. +- inf.variadicAssignees = sig.Variadic() - } -- <-conn.Done() -- return conn.Err() --} - --// A Forwarder is a jsonrpc2.StreamServer that handles an LSP stream by --// forwarding it to a remote. This is used when the gopls process started by --// the editor is in the `-remote` mode, which means it finds and connects to a --// separate gopls daemon. In these cases, we still want the forwarder gopls to --// be instrumented with telemetry, and want to be able to in some cases hijack --// the jsonrpc2 connection with the daemon. --type Forwarder struct { -- dialer *AutoDialer +- // Make sure not to run past the end of expected parameters. +- if exprIdx >= numParams { +- inf.objType = sig.Params().At(numParams - 1).Type() +- } else { +- inf.objType = sig.Params().At(exprIdx).Type() +- } - -- mu sync.Mutex -- // Hold on to the server connection so that we can redo the handshake if any -- // information changes. -- serverConn jsonrpc2.Conn -- serverID string --} +- if sig.Variadic() && exprIdx >= (numParams-1) { +- // If we are completing a variadic param, deslice the variadic type. +- inf.objType = deslice(inf.objType) +- // Record whether we are completing the initial variadic param. +- inf.variadic = exprIdx == numParams-1 && len(node.Args) <= numParams - --// NewForwarder creates a new Forwarder, ready to forward connections to the --// remote server specified by rawAddr. If provided and rawAddr indicates an --// 'automatic' address (starting with 'auto;'), argFunc may be used to start a --// remote server for the auto-discovered address. --func NewForwarder(rawAddr string, argFunc func(network, address string) []string) (*Forwarder, error) { -- dialer, err := NewAutoDialer(rawAddr, argFunc) -- if err != nil { -- return nil, err +- // Check if we can infer object kind from printf verb. +- inf.objKind |= printfArgKind(c.pkg.TypesInfo(), node, exprIdx) - } -- fwd := &Forwarder{ -- dialer: dialer, +- +- // If our expected type is an uninstantiated generic type param, +- // swap to the constraint which will do a decent job filtering +- // candidates. +- if tp, _ := inf.objType.(*types.TypeParam); tp != nil { +- inf.objType = tp.Constraint() - } -- return fwd, nil +- +- return inf -} - --// QueryServerState queries the server state of the current server. --func QueryServerState(ctx context.Context, addr string) (*ServerState, error) { -- serverConn, err := dialRemote(ctx, addr) -- if err != nil { -- return nil, err +-func expectedConstraint(t types.Type, idx int) types.Type { +- var tp *types.TypeParamList +- if named, _ := t.(*types.Named); named != nil { +- tp = named.TypeParams() +- } else if sig, _ := t.Underlying().(*types.Signature); sig != nil { +- tp = sig.TypeParams() - } -- var state ServerState -- if err := protocol.Call(ctx, serverConn, sessionsMethod, nil, &state); err != nil { -- return nil, fmt.Errorf("querying server state: %w", err) +- if tp == nil || idx >= tp.Len() { +- return nil - } -- return &state, nil +- return tp.At(idx).Constraint() -} - --// dialRemote is used for making calls into the gopls daemon. addr should be a --// URL, possibly on the synthetic 'auto' network (e.g. tcp://..., unix://..., --// or auto://...). --func dialRemote(ctx context.Context, addr string) (jsonrpc2.Conn, error) { -- network, address := ParseAddr(addr) -- if network == AutoNetwork { -- gp, err := os.Executable() -- if err != nil { -- return nil, fmt.Errorf("getting gopls path: %w", err) +-// objChain decomposes e into a chain of objects if possible. For +-// example, "foo.bar().baz" will yield []types.Object{foo, bar, baz}. +-// If any part can't be turned into an object, return nil. +-func objChain(info *types.Info, e ast.Expr) []types.Object { +- var objs []types.Object +- +- for e != nil { +- switch n := e.(type) { +- case *ast.Ident: +- obj := info.ObjectOf(n) +- if obj == nil { +- return nil +- } +- objs = append(objs, obj) +- e = nil +- case *ast.SelectorExpr: +- obj := info.ObjectOf(n.Sel) +- if obj == nil { +- return nil +- } +- objs = append(objs, obj) +- e = n.X +- case *ast.CallExpr: +- if len(n.Args) > 0 { +- return nil +- } +- e = n.Fun +- default: +- return nil - } -- network, address = autoNetworkAddress(gp, address) - } -- netConn, err := net.DialTimeout(network, address, 5*time.Second) -- if err != nil { -- return nil, fmt.Errorf("dialing remote: %w", err) +- +- // Reverse order so the layout matches the syntactic order. +- for i := 0; i < len(objs)/2; i++ { +- objs[i], objs[len(objs)-1-i] = objs[len(objs)-1-i], objs[i] - } -- serverConn := jsonrpc2.NewConn(jsonrpc2.NewHeaderStream(netConn)) -- serverConn.Go(ctx, jsonrpc2.MethodNotFound) -- return serverConn, nil +- +- return objs -} - --func ExecuteCommand(ctx context.Context, addr string, id string, request, result interface{}) error { -- serverConn, err := dialRemote(ctx, addr) -- if err != nil { -- return err -- } -- args, err := command.MarshalArgs(request) -- if err != nil { -- return err -- } -- params := protocol.ExecuteCommandParams{ -- Command: id, -- Arguments: args, +-// applyTypeModifiers applies the list of type modifiers to a type. +-// It returns nil if the modifiers could not be applied. +-func (ci candidateInference) applyTypeModifiers(typ types.Type, addressable bool) types.Type { +- for _, mod := range ci.modifiers { +- switch mod.mod { +- case dereference: +- // For every "*" indirection operator, remove a pointer layer +- // from candidate type. +- if ptr, ok := typ.Underlying().(*types.Pointer); ok { +- typ = ptr.Elem() +- } else { +- return nil +- } +- case reference: +- // For every "&" address operator, add another pointer layer to +- // candidate type, if the candidate is addressable. +- if addressable { +- typ = types.NewPointer(typ) +- } else { +- return nil +- } +- case chanRead: +- // For every "<-" operator, remove a layer of channelness. +- if ch, ok := typ.(*types.Chan); ok { +- typ = ch.Elem() +- } else { +- return nil +- } +- } - } -- return protocol.Call(ctx, serverConn, "workspace/executeCommand", params, result) --} - --// ServeStream dials the forwarder remote and binds the remote to serve the LSP --// on the incoming stream. --func (f *Forwarder) ServeStream(ctx context.Context, clientConn jsonrpc2.Conn) error { -- client := protocol.ClientDispatcher(clientConn) +- return typ +-} - -- netConn, err := f.dialer.dialNet(ctx) -- if err != nil { -- return fmt.Errorf("forwarder: connecting to remote: %w", err) +-// applyTypeNameModifiers applies the list of type modifiers to a type name. +-func (ci candidateInference) applyTypeNameModifiers(typ types.Type) types.Type { +- for _, mod := range ci.typeName.modifiers { +- switch mod.mod { +- case reference: +- typ = types.NewPointer(typ) +- case arrayType: +- typ = types.NewArray(typ, mod.arrayLen) +- case sliceType: +- typ = types.NewSlice(typ) +- } - } -- serverConn := jsonrpc2.NewConn(jsonrpc2.NewHeaderStream(netConn)) -- server := protocol.ServerDispatcher(serverConn) -- -- // Forward between connections. -- serverConn.Go(ctx, -- protocol.Handlers( -- protocol.ClientHandler(client, -- jsonrpc2.MethodNotFound))) +- return typ +-} - -- // Don't run the clientConn yet, so that we can complete the handshake before -- // processing any client messages. -- -- // Do a handshake with the server instance to exchange debug information. -- index := atomic.AddInt64(&serverIndex, 1) -- f.mu.Lock() -- f.serverConn = serverConn -- f.serverID = strconv.FormatInt(index, 10) -- f.mu.Unlock() -- f.handshake(ctx) -- clientConn.Go(ctx, -- protocol.Handlers( -- f.handler( -- protocol.ServerHandler(server, -- jsonrpc2.MethodNotFound)))) -- -- select { -- case <-serverConn.Done(): -- clientConn.Close() -- case <-clientConn.Done(): -- serverConn.Close() -- } -- -- err = nil -- if serverConn.Err() != nil { -- err = fmt.Errorf("remote disconnected: %v", serverConn.Err()) -- } else if clientConn.Err() != nil { -- err = fmt.Errorf("client disconnected: %v", clientConn.Err()) -- } -- event.Log(ctx, fmt.Sprintf("forwarder: exited with error: %v", err)) -- return err +-// matchesVariadic returns true if we are completing a variadic +-// parameter and candType is a compatible slice type. +-func (ci candidateInference) matchesVariadic(candType types.Type) bool { +- return ci.variadic && ci.objType != nil && assignableTo(candType, types.NewSlice(ci.objType)) -} - --// TODO(rfindley): remove this handshaking in favor of middleware. --func (f *Forwarder) handshake(ctx context.Context) { -- // This call to os.Executable is redundant, and will be eliminated by the -- // transition to the V2 API. -- goplsPath, err := os.Executable() -- if err != nil { -- event.Error(ctx, "getting executable for handshake", err) -- goplsPath = "" +-// findSwitchStmt returns an *ast.CaseClause's corresponding *ast.SwitchStmt or +-// *ast.TypeSwitchStmt. path should start from the case clause's first ancestor. +-func findSwitchStmt(path []ast.Node, pos token.Pos, c *ast.CaseClause) ast.Stmt { +- // Make sure position falls within a "case <>:" clause. +- if exprAtPos(pos, c.List) >= len(c.List) { +- return nil - } -- var ( -- hreq = handshakeRequest{ -- ServerID: f.serverID, -- GoplsPath: goplsPath, -- } -- hresp handshakeResponse -- ) -- if di := debug.GetInstance(ctx); di != nil { -- hreq.Logfile = di.Logfile -- hreq.DebugAddr = di.ListenedDebugAddress() +- // A case clause is always nested within a block statement in a switch statement. +- if len(path) < 2 { +- return nil - } -- if err := protocol.Call(ctx, f.serverConn, handshakeMethod, hreq, &hresp); err != nil { -- // TODO(rfindley): at some point in the future we should return an error -- // here. Handshakes have become functional in nature. -- event.Error(ctx, "forwarder: gopls handshake failed", err) +- if _, ok := path[0].(*ast.BlockStmt); !ok { +- return nil - } -- if hresp.GoplsPath != goplsPath { -- event.Error(ctx, "", fmt.Errorf("forwarder: gopls path mismatch: forwarder is %q, remote is %q", goplsPath, hresp.GoplsPath)) +- switch s := path[1].(type) { +- case *ast.SwitchStmt: +- return s +- case *ast.TypeSwitchStmt: +- return s +- default: +- return nil - } -- event.Log(ctx, "New server", -- tag.NewServer.Of(f.serverID), -- tag.Logfile.Of(hresp.Logfile), -- tag.DebugAddress.Of(hresp.DebugAddr), -- tag.GoplsPath.Of(hresp.GoplsPath), -- tag.ClientID.Of(hresp.SessionID), -- ) -} - --func ConnectToRemote(ctx context.Context, addr string) (net.Conn, error) { -- dialer, err := NewAutoDialer(addr, nil) -- if err != nil { -- return nil, err +-// breaksExpectedTypeInference reports if an expression node's type is unrelated +-// to its child expression node types. For example, "Foo{Bar: x.Baz(<>)}" should +-// expect a function argument, not a composite literal value. +-func breaksExpectedTypeInference(n ast.Node, pos token.Pos) bool { +- switch n := n.(type) { +- case *ast.CompositeLit: +- // Doesn't break inference if pos is in type name. +- // For example: "Foo<>{Bar: 123}" +- return n.Type == nil || !goplsastutil.NodeContains(n.Type, pos) +- case *ast.CallExpr: +- // Doesn't break inference if pos is in func name. +- // For example: "Foo<>(123)" +- return !goplsastutil.NodeContains(n.Fun, pos) +- case *ast.FuncLit, *ast.IndexExpr, *ast.SliceExpr: +- return true +- default: +- return false - } -- return dialer.dialNet(ctx) -} - --// handler intercepts messages to the daemon to enrich them with local --// information. --func (f *Forwarder) handler(handler jsonrpc2.Handler) jsonrpc2.Handler { -- return func(ctx context.Context, reply jsonrpc2.Replier, r jsonrpc2.Request) error { -- // Intercept certain messages to add special handling. -- switch r.Method() { -- case "initialize": -- if newr, err := addGoEnvToInitializeRequest(ctx, r); err == nil { -- r = newr -- } else { -- log.Printf("unable to add local env to initialize request: %v", err) +-// expectTypeName returns information about the expected type name at position. +-func expectTypeName(c *completer) typeNameInference { +- var inf typeNameInference +- +-Nodes: +- for i, p := range c.path { +- switch n := p.(type) { +- case *ast.FieldList: +- // Expect a type name if pos is in a FieldList. This applies to +- // FuncType params/results, FuncDecl receiver, StructType, and +- // InterfaceType. We don't need to worry about the field name +- // because completion bails out early if pos is in an *ast.Ident +- // that defines an object. +- inf.wantTypeName = true +- break Nodes +- case *ast.CaseClause: +- // Expect type names in type switch case clauses. +- if swtch, ok := findSwitchStmt(c.path[i+1:], c.pos, n).(*ast.TypeSwitchStmt); ok { +- // The case clause types must be assertable from the type switch parameter. +- ast.Inspect(swtch.Assign, func(n ast.Node) bool { +- if ta, ok := n.(*ast.TypeAssertExpr); ok { +- inf.assertableFrom = c.pkg.TypesInfo().TypeOf(ta.X) +- return false +- } +- return true +- }) +- inf.wantTypeName = true +- +- // Track the types that have already been used in this +- // switch's case statements so we don't recommend them. +- for _, e := range swtch.Body.List { +- for _, typeExpr := range e.(*ast.CaseClause).List { +- // Skip if type expression contains pos. We don't want to +- // count it as already used if the user is completing it. +- if typeExpr.Pos() < c.pos && c.pos <= typeExpr.End() { +- continue +- } +- +- if t := c.pkg.TypesInfo().TypeOf(typeExpr); t != nil { +- inf.seenTypeSwitchCases = append(inf.seenTypeSwitchCases, t) +- } +- } +- } +- +- break Nodes - } -- case "workspace/executeCommand": -- var params protocol.ExecuteCommandParams -- if err := json.Unmarshal(r.Params(), ¶ms); err == nil { -- if params.Command == command.StartDebugging.ID() { -- var args command.DebuggingArgs -- if err := command.UnmarshalArgs(params.Arguments, &args); err == nil { -- reply = f.replyWithDebugAddress(ctx, reply, args) -- } else { -- event.Error(ctx, "unmarshaling debugging args", err) +- return typeNameInference{} +- case *ast.TypeAssertExpr: +- // Expect type names in type assert expressions. +- if n.Lparen < c.pos && c.pos <= n.Rparen { +- // The type in parens must be assertable from the expression type. +- inf.assertableFrom = c.pkg.TypesInfo().TypeOf(n.X) +- inf.wantTypeName = true +- break Nodes +- } +- return typeNameInference{} +- case *ast.StarExpr: +- inf.modifiers = append(inf.modifiers, typeMod{mod: reference}) +- case *ast.CompositeLit: +- // We want a type name if position is in the "Type" part of a +- // composite literal (e.g. "Foo<>{}"). +- if n.Type != nil && n.Type.Pos() <= c.pos && c.pos <= n.Type.End() { +- inf.wantTypeName = true +- inf.compLitType = true +- +- if i < len(c.path)-1 { +- // Track preceding "&" operator. Technically it applies to +- // the composite literal and not the type name, but if +- // affects our type completion nonetheless. +- if u, ok := c.path[i+1].(*ast.UnaryExpr); ok && u.Op == token.AND { +- inf.modifiers = append(inf.modifiers, typeMod{mod: reference}) +- } +- } +- } +- break Nodes +- case *ast.ArrayType: +- // If we are inside the "Elt" part of an array type, we want a type name. +- if n.Elt.Pos() <= c.pos && c.pos <= n.Elt.End() { +- inf.wantTypeName = true +- if n.Len == nil { +- // No "Len" expression means a slice type. +- inf.modifiers = append(inf.modifiers, typeMod{mod: sliceType}) +- } else { +- // Try to get the array type using the constant value of "Len". +- tv, ok := c.pkg.TypesInfo().Types[n.Len] +- if ok && tv.Value != nil && tv.Value.Kind() == constant.Int { +- if arrayLen, ok := constant.Int64Val(tv.Value); ok { +- inf.modifiers = append(inf.modifiers, typeMod{mod: arrayType, arrayLen: arrayLen}) +- } +- } +- } +- +- // ArrayTypes can be nested, so keep going if our parent is an +- // ArrayType. +- if i < len(c.path)-1 { +- if _, ok := c.path[i+1].(*ast.ArrayType); ok { +- continue Nodes - } - } +- +- break Nodes +- } +- case *ast.MapType: +- inf.wantTypeName = true +- if n.Key != nil { +- inf.wantComparable = goplsastutil.NodeContains(n.Key, c.pos) - } else { -- event.Error(ctx, "intercepting executeCommand request", err) +- // If the key is empty, assume we are completing the key if +- // pos is directly after the "map[". +- inf.wantComparable = c.pos == n.Pos()+token.Pos(len("map[")) +- } +- break Nodes +- case *ast.ValueSpec: +- inf.wantTypeName = n.Type != nil && goplsastutil.NodeContains(n.Type, c.pos) +- break Nodes +- case *ast.TypeSpec: +- inf.wantTypeName = goplsastutil.NodeContains(n.Type, c.pos) +- default: +- if breaksExpectedTypeInference(p, c.pos) { +- return typeNameInference{} - } - } -- // The gopls workspace environment defaults to the process environment in -- // which gopls daemon was started. To avoid discrepancies in Go environment -- // between the editor and daemon, inject any unset variables in `go env` -- // into the options sent by initialize. -- // -- // See also golang.org/issue/37830. -- return handler(ctx, reply, r) - } +- +- return inf -} - --// addGoEnvToInitializeRequest builds a new initialize request in which we set --// any environment variables output by `go env` and not already present in the --// request. --// --// It returns an error if r is not an initialize request, or is otherwise --// malformed. --func addGoEnvToInitializeRequest(ctx context.Context, r jsonrpc2.Request) (jsonrpc2.Request, error) { -- var params protocol.ParamInitialize -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return nil, err -- } -- var opts map[string]interface{} -- switch v := params.InitializationOptions.(type) { -- case nil: -- opts = make(map[string]interface{}) -- case map[string]interface{}: -- opts = v -- default: -- return nil, fmt.Errorf("unexpected type for InitializationOptions: %T", v) -- } -- envOpt, ok := opts["env"] -- if !ok { -- envOpt = make(map[string]interface{}) -- } -- env, ok := envOpt.(map[string]interface{}) -- if !ok { -- return nil, fmt.Errorf(`env option is %T, expected a map`, envOpt) -- } -- goenv, err := getGoEnv(ctx, env) -- if err != nil { -- return nil, err -- } -- // We don't want to propagate GOWORK unless explicitly set since that could mess with -- // path inference during cmd/go invocations, see golang/go#51825. -- _, goworkSet := os.LookupEnv("GOWORK") -- for govar, value := range goenv { -- if govar == "GOWORK" && !goworkSet { -- continue -- } -- env[govar] = value -- } -- opts["env"] = env -- params.InitializationOptions = opts -- call, ok := r.(*jsonrpc2.Call) -- if !ok { -- return nil, fmt.Errorf("%T is not a *jsonrpc2.Call", r) -- } -- return jsonrpc2.NewCall(call.ID(), "initialize", params) +-func (c *completer) fakeObj(T types.Type) *types.Var { +- return types.NewVar(token.NoPos, c.pkg.Types(), "", T) -} - --func (f *Forwarder) replyWithDebugAddress(outerCtx context.Context, r jsonrpc2.Replier, args command.DebuggingArgs) jsonrpc2.Replier { -- di := debug.GetInstance(outerCtx) -- if di == nil { -- event.Log(outerCtx, "no debug instance to start") -- return r -- } -- return func(ctx context.Context, result interface{}, outerErr error) error { -- if outerErr != nil { -- return r(ctx, result, outerErr) +-// derivableTypes iterates types you can derive from t. For example, +-// from "foo" we might derive "&foo", and "foo()". +-func derivableTypes(t types.Type, addressable bool, f func(t types.Type, addressable bool, mod typeModKind) bool) bool { +- switch t := t.Underlying().(type) { +- case *types.Signature: +- // If t is a func type with a single result, offer the result type. +- if t.Results().Len() == 1 && f(t.Results().At(0).Type(), false, invoke) { +- return true - } -- // Enrich the result with our own debugging information. Since we're an -- // intermediary, the jsonrpc2 package has deserialized the result into -- // maps, by default. Re-do the unmarshalling. -- raw, err := json.Marshal(result) -- if err != nil { -- event.Error(outerCtx, "marshaling intermediate command result", err) -- return r(ctx, result, err) +- case *types.Array: +- if f(t.Elem(), true, index) { +- return true - } -- var modified command.DebuggingResult -- if err := json.Unmarshal(raw, &modified); err != nil { -- event.Error(outerCtx, "unmarshaling intermediate command result", err) -- return r(ctx, result, err) +- // Try converting array to slice. +- if f(types.NewSlice(t.Elem()), false, takeSlice) { +- return true - } -- addr := args.Addr -- if addr == "" { -- addr = "localhost:0" +- case *types.Pointer: +- if f(t.Elem(), false, dereference) { +- return true - } -- addr, err = di.Serve(outerCtx, addr) -- if err != nil { -- event.Error(outerCtx, "starting debug server", err) -- return r(ctx, result, outerErr) +- case *types.Slice: +- if f(t.Elem(), true, index) { +- return true +- } +- case *types.Map: +- if f(t.Elem(), false, index) { +- return true +- } +- case *types.Chan: +- if f(t.Elem(), false, chanRead) { +- return true - } -- urls := []string{"http://" + addr} -- modified.URLs = append(urls, modified.URLs...) -- go f.handshake(ctx) -- return r(ctx, modified, nil) - } --} -- --// A handshakeRequest identifies a client to the LSP server. --type handshakeRequest struct { -- // ServerID is the ID of the server on the client. This should usually be 0. -- ServerID string `json:"serverID"` -- // Logfile is the location of the clients log file. -- Logfile string `json:"logfile"` -- // DebugAddr is the client debug address. -- DebugAddr string `json:"debugAddr"` -- // GoplsPath is the path to the Gopls binary running the current client -- // process. -- GoplsPath string `json:"goplsPath"` --} - --// A handshakeResponse is returned by the LSP server to tell the LSP client --// information about its session. --type handshakeResponse struct { -- // SessionID is the server session associated with the client. -- SessionID string `json:"sessionID"` -- // Logfile is the location of the server logs. -- Logfile string `json:"logfile"` -- // DebugAddr is the server debug address. -- DebugAddr string `json:"debugAddr"` -- // GoplsPath is the path to the Gopls binary running the current server -- // process. -- GoplsPath string `json:"goplsPath"` --} +- // Check if c is addressable and a pointer to c matches our type inference. +- if addressable && f(types.NewPointer(t), false, reference) { +- return true +- } - --// ClientSession identifies a current client LSP session on the server. Note --// that it looks similar to handshakeResposne, but in fact 'Logfile' and --// 'DebugAddr' now refer to the client. --type ClientSession struct { -- SessionID string `json:"sessionID"` -- Logfile string `json:"logfile"` -- DebugAddr string `json:"debugAddr"` +- return false -} - --// ServerState holds information about the gopls daemon process, including its --// debug information and debug information of all of its current connected --// clients. --type ServerState struct { -- Logfile string `json:"logfile"` -- DebugAddr string `json:"debugAddr"` -- GoplsPath string `json:"goplsPath"` -- CurrentClientID string `json:"currentClientID"` -- Clients []ClientSession `json:"clients"` --} +-// anyCandType reports whether f returns true for any candidate type +-// derivable from c. It searches up to three levels of type +-// modification. For example, given "foo" we could discover "***foo" +-// or "*foo()". +-func (c *candidate) anyCandType(f func(t types.Type, addressable bool) bool) bool { +- if c.obj == nil || c.obj.Type() == nil { +- return false +- } - --const ( -- handshakeMethod = "gopls/handshake" -- sessionsMethod = "gopls/sessions" --) +- const maxDepth = 3 - --func handshaker(session *cache.Session, goplsPath string, logHandshakes bool, handler jsonrpc2.Handler) jsonrpc2.Handler { -- return func(ctx context.Context, reply jsonrpc2.Replier, r jsonrpc2.Request) error { -- switch r.Method() { -- case handshakeMethod: -- // We log.Printf in this handler, rather than event.Log when we want logs -- // to go to the daemon log rather than being reflected back to the -- // client. -- var req handshakeRequest -- if err := json.Unmarshal(r.Params(), &req); err != nil { -- if logHandshakes { -- log.Printf("Error processing handshake for session %s: %v", session.ID(), err) -- } -- sendError(ctx, reply, err) -- return nil -- } -- if logHandshakes { -- log.Printf("Session %s: got handshake. Logfile: %q, Debug addr: %q", session.ID(), req.Logfile, req.DebugAddr) -- } -- event.Log(ctx, "Handshake session update", -- cache.KeyUpdateSession.Of(session), -- tag.DebugAddress.Of(req.DebugAddr), -- tag.Logfile.Of(req.Logfile), -- tag.ServerID.Of(req.ServerID), -- tag.GoplsPath.Of(req.GoplsPath), -- ) -- resp := handshakeResponse{ -- SessionID: session.ID(), -- GoplsPath: goplsPath, -- } -- if di := debug.GetInstance(ctx); di != nil { -- resp.Logfile = di.Logfile -- resp.DebugAddr = di.ListenedDebugAddress() +- var searchTypes func(t types.Type, addressable bool, mods []typeModKind) bool +- searchTypes = func(t types.Type, addressable bool, mods []typeModKind) bool { +- if f(t, addressable) { +- if len(mods) > 0 { +- newMods := make([]typeModKind, len(mods)+len(c.mods)) +- copy(newMods, mods) +- copy(newMods[len(mods):], c.mods) +- c.mods = newMods - } -- return reply(ctx, resp, nil) +- return true +- } - -- case sessionsMethod: -- resp := ServerState{ -- GoplsPath: goplsPath, -- CurrentClientID: session.ID(), -- } -- if di := debug.GetInstance(ctx); di != nil { -- resp.Logfile = di.Logfile -- resp.DebugAddr = di.ListenedDebugAddress() -- for _, c := range di.State.Clients() { -- resp.Clients = append(resp.Clients, ClientSession{ -- SessionID: c.Session.ID(), -- Logfile: c.Logfile, -- DebugAddr: c.DebugAddress, -- }) -- } -- } -- return reply(ctx, resp, nil) +- if len(mods) == maxDepth { +- return false - } -- return handler(ctx, reply, r) +- +- return derivableTypes(t, addressable, func(t types.Type, addressable bool, mod typeModKind) bool { +- return searchTypes(t, addressable, append(mods, mod)) +- }) - } +- +- return searchTypes(c.obj.Type(), c.addressable, make([]typeModKind, 0, maxDepth)) -} - --func sendError(ctx context.Context, reply jsonrpc2.Replier, err error) { -- err = fmt.Errorf("%v: %w", err, jsonrpc2.ErrParse) -- if err := reply(ctx, nil, err); err != nil { -- event.Error(ctx, "", err) +-// matchingCandidate reports whether cand matches our type inferences. +-// It mutates cand's score in certain cases. +-func (c *completer) matchingCandidate(cand *candidate) bool { +- if c.completionContext.commentCompletion { +- return false - } --} - --// ParseAddr parses the address of a gopls remote. --// TODO(rFindley): further document this syntax, and allow URI-style remote --// addresses such as "auto://...". --func ParseAddr(listen string) (network string, address string) { -- // Allow passing just -remote=auto, as a shorthand for using automatic remote -- // resolution. -- if listen == AutoNetwork { -- return AutoNetwork, "" +- // Bail out early if we are completing a field name in a composite literal. +- if v, ok := cand.obj.(*types.Var); ok && v.IsField() && c.wantStructFieldCompletions() { +- return true - } -- if parts := strings.SplitN(listen, ";", 2); len(parts) == 2 { -- return parts[0], parts[1] +- +- if isTypeName(cand.obj) { +- return c.matchingTypeName(cand) +- } else if c.wantTypeName() { +- // If we want a type, a non-type object never matches. +- return false - } -- return "tcp", listen --} -diff -urN a/gopls/internal/lsp/lsprpc/lsprpc_test.go b/gopls/internal/lsp/lsprpc/lsprpc_test.go ---- a/gopls/internal/lsp/lsprpc/lsprpc_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/lsprpc/lsprpc_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,376 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package lsprpc +- if c.inference.candTypeMatches(cand) { +- return true +- } - --import ( -- "context" -- "encoding/json" -- "errors" -- "regexp" -- "strings" -- "testing" -- "time" +- candType := cand.obj.Type() +- if candType == nil { +- return false +- } - -- "golang.org/x/tools/gopls/internal/lsp/cache" -- "golang.org/x/tools/gopls/internal/lsp/debug" -- "golang.org/x/tools/gopls/internal/lsp/fake" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/jsonrpc2" -- "golang.org/x/tools/internal/jsonrpc2/servertest" -- "golang.org/x/tools/internal/testenv" --) +- if sig, ok := candType.Underlying().(*types.Signature); ok { +- if c.inference.assigneesMatch(cand, sig) { +- // Invoke the candidate if its results are multi-assignable. +- cand.mods = append(cand.mods, invoke) +- return true +- } +- } - --type FakeClient struct { -- protocol.Client +- // Default to invoking *types.Func candidates. This is so function +- // completions in an empty statement (or other cases with no expected type) +- // are invoked by default. +- if isFunc(cand.obj) { +- cand.mods = append(cand.mods, invoke) +- } - -- Logs chan string +- return false -} - --func (c FakeClient) LogMessage(ctx context.Context, params *protocol.LogMessageParams) error { -- c.Logs <- params.Message -- return nil --} +-// candTypeMatches reports whether cand makes a good completion +-// candidate given the candidate inference. cand's score may be +-// mutated to downrank the candidate in certain situations. +-func (ci *candidateInference) candTypeMatches(cand *candidate) bool { +- var ( +- expTypes = make([]types.Type, 0, 2) +- variadicType types.Type +- ) +- if ci.objType != nil { +- expTypes = append(expTypes, ci.objType) - --// fakeServer is intended to be embedded in the test fakes below, to trivially --// implement Shutdown. --type fakeServer struct { -- protocol.Server --} +- if ci.variadic { +- variadicType = types.NewSlice(ci.objType) +- expTypes = append(expTypes, variadicType) +- } +- } - --func (fakeServer) Shutdown(ctx context.Context) error { -- return nil --} +- return cand.anyCandType(func(candType types.Type, addressable bool) bool { +- // Take into account any type modifiers on the expected type. +- candType = ci.applyTypeModifiers(candType, addressable) +- if candType == nil { +- return false +- } - --type PingServer struct{ fakeServer } +- if ci.convertibleTo != nil && convertibleTo(candType, ci.convertibleTo) { +- return true +- } - --func (s PingServer) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error { -- event.Log(ctx, "ping") -- return nil --} +- for _, expType := range expTypes { +- if isEmptyInterface(expType) { +- continue +- } - --func TestClientLogging(t *testing.T) { -- ctx, cancel := context.WithCancel(context.Background()) -- defer cancel() +- matches := ci.typeMatches(expType, candType) +- if !matches { +- // If candType doesn't otherwise match, consider if we can +- // convert candType directly to expType. +- if considerTypeConversion(candType, expType, cand.path) { +- cand.convertTo = expType +- // Give a major score penalty so we always prefer directly +- // assignable candidates, all else equal. +- cand.score *= 0.5 +- return true +- } - -- server := PingServer{} -- client := FakeClient{Logs: make(chan string, 10)} +- continue +- } - -- ctx = debug.WithInstance(ctx, "", "") -- ss := NewStreamServer(cache.New(nil), false, nil) -- ss.serverForTest = server -- ts := servertest.NewPipeServer(ss, nil) -- defer checkClose(t, ts.Close) -- cc := ts.Connect(ctx) -- cc.Go(ctx, protocol.ClientHandler(client, jsonrpc2.MethodNotFound)) +- if expType == variadicType { +- cand.mods = append(cand.mods, takeDotDotDot) +- } - -- if err := protocol.ServerDispatcher(cc).DidOpen(ctx, &protocol.DidOpenTextDocumentParams{}); err != nil { -- t.Errorf("DidOpen: %v", err) -- } +- // Lower candidate score for untyped conversions. This avoids +- // ranking untyped constants above candidates with an exact type +- // match. Don't lower score of builtin constants, e.g. "true". +- if isUntyped(candType) && !types.Identical(candType, expType) && cand.obj.Parent() != types.Universe { +- // Bigger penalty for deep completions into other packages to +- // avoid random constants from other packages popping up all +- // the time. +- if len(cand.path) > 0 && isPkgName(cand.path[0]) { +- cand.score *= 0.5 +- } else { +- cand.score *= 0.75 +- } +- } - -- select { -- case got := <-client.Logs: -- want := "ping" -- matched, err := regexp.MatchString(want, got) -- if err != nil { -- t.Fatal(err) +- return true - } -- if !matched { -- t.Errorf("got log %q, want a log containing %q", got, want) +- +- // If we don't have a specific expected type, fall back to coarser +- // object kind checks. +- if ci.objType == nil || isEmptyInterface(ci.objType) { +- // If we were able to apply type modifiers to our candidate type, +- // count that as a match. For example: +- // +- // var foo chan int +- // <-fo<> +- // +- // We were able to apply the "<-" type modifier to "foo", so "foo" +- // matches. +- if len(ci.modifiers) > 0 { +- return true +- } +- +- // If we didn't have an exact type match, check if our object kind +- // matches. +- if ci.kindMatches(candType) { +- if ci.objKind == kindFunc { +- cand.mods = append(cand.mods, invoke) +- } +- return true +- } - } -- case <-time.After(1 * time.Second): -- t.Error("timeout waiting for client log") -- } +- +- return false +- }) -} - --// WaitableServer instruments LSP request so that we can control their timing. --// The requests chosen are arbitrary: we simply needed one that blocks, and --// another that doesn't. --type WaitableServer struct { -- fakeServer +-// considerTypeConversion returns true if we should offer a completion +-// automatically converting "from" to "to". +-func considerTypeConversion(from, to types.Type, path []types.Object) bool { +- // Don't offer to convert deep completions from other packages. +- // Otherwise there are many random package level consts/vars that +- // pop up as candidates all the time. +- if len(path) > 0 && isPkgName(path[0]) { +- return false +- } - -- Started chan struct{} -- Completed chan error --} +- if _, ok := from.(*types.TypeParam); ok { +- return false +- } - --func (s WaitableServer) Hover(ctx context.Context, _ *protocol.HoverParams) (_ *protocol.Hover, err error) { -- s.Started <- struct{}{} -- defer func() { -- s.Completed <- err -- }() -- select { -- case <-ctx.Done(): -- return nil, errors.New("cancelled hover") -- case <-time.After(10 * time.Second): +- if !convertibleTo(from, to) { +- return false - } -- return &protocol.Hover{}, nil --} - --func (s WaitableServer) ResolveCompletionItem(_ context.Context, item *protocol.CompletionItem) (*protocol.CompletionItem, error) { -- return item, nil +- // Don't offer to convert ints to strings since that probably +- // doesn't do what the user wants. +- if isBasicKind(from, types.IsInteger) && isBasicKind(to, types.IsString) { +- return false +- } +- +- return true -} - --func checkClose(t *testing.T, closer func() error) { -- t.Helper() -- if err := closer(); err != nil { -- t.Errorf("closing: %v", err) +-// typeMatches reports whether an object of candType makes a good +-// completion candidate given the expected type expType. +-func (ci *candidateInference) typeMatches(expType, candType types.Type) bool { +- // Handle untyped values specially since AssignableTo gives false negatives +- // for them (see https://golang.org/issue/32146). +- if candBasic, ok := candType.Underlying().(*types.Basic); ok { +- if expBasic, ok := expType.Underlying().(*types.Basic); ok { +- // Note that the candidate and/or the expected can be untyped. +- // In "fo<> == 100" the expected type is untyped, and the +- // candidate could also be an untyped constant. +- +- // Sort by is_untyped and then by is_int to simplify below logic. +- a, b := candBasic.Info(), expBasic.Info() +- if a&types.IsUntyped == 0 || (b&types.IsInteger > 0 && b&types.IsUntyped > 0) { +- a, b = b, a +- } +- +- // If at least one is untyped... +- if a&types.IsUntyped > 0 { +- switch { +- // Untyped integers are compatible with floats. +- case a&types.IsInteger > 0 && b&types.IsFloat > 0: +- return true +- +- // Check if their constant kind (bool|int|float|complex|string) matches. +- // This doesn't take into account the constant value, so there will be some +- // false positives due to integer sign and overflow. +- case a&types.IsConstType == b&types.IsConstType: +- return true +- } +- } +- } - } +- +- // AssignableTo covers the case where the types are equal, but also handles +- // cases like assigning a concrete type to an interface type. +- return assignableTo(candType, expType) -} - --func setupForwarding(ctx context.Context, t *testing.T, s protocol.Server) (direct, forwarded servertest.Connector, cleanup func()) { -- t.Helper() -- serveCtx := debug.WithInstance(ctx, "", "") -- ss := NewStreamServer(cache.New(nil), false, nil) -- ss.serverForTest = s -- tsDirect := servertest.NewTCPServer(serveCtx, ss, nil) +-// kindMatches reports whether candType's kind matches our expected +-// kind (e.g. slice, map, etc.). +-func (ci *candidateInference) kindMatches(candType types.Type) bool { +- return ci.objKind > 0 && ci.objKind&candKind(candType) > 0 +-} - -- forwarder, err := NewForwarder("tcp;"+tsDirect.Addr, nil) -- if err != nil { -- t.Fatal(err) +-// assigneesMatch reports whether an invocation of sig matches the +-// number and type of any assignees. +-func (ci *candidateInference) assigneesMatch(cand *candidate, sig *types.Signature) bool { +- if len(ci.assignees) == 0 { +- return false - } -- tsForwarded := servertest.NewPipeServer(forwarder, nil) -- return tsDirect, tsForwarded, func() { -- checkClose(t, tsDirect.Close) -- checkClose(t, tsForwarded.Close) +- +- // Uniresult functions are always usable and are handled by the +- // normal, non-assignees type matching logic. +- if sig.Results().Len() == 1 { +- return false - } --} - --func TestRequestCancellation(t *testing.T) { -- ctx := context.Background() -- server := WaitableServer{ -- Started: make(chan struct{}), -- Completed: make(chan error), +- // Don't prefer completing into func(...interface{}) calls since all +- // functions would match. +- if ci.variadicAssignees && len(ci.assignees) == 1 && isEmptyInterface(deslice(ci.assignees[0])) { +- return false - } -- tsDirect, tsForwarded, cleanup := setupForwarding(ctx, t, server) -- defer cleanup() -- tests := []struct { -- serverType string -- ts servertest.Connector -- }{ -- {"direct", tsDirect}, -- {"forwarder", tsForwarded}, +- +- var numberOfResultsCouldMatch bool +- if ci.variadicAssignees { +- numberOfResultsCouldMatch = sig.Results().Len() >= len(ci.assignees)-1 +- } else { +- numberOfResultsCouldMatch = sig.Results().Len() == len(ci.assignees) - } - -- for _, test := range tests { -- t.Run(test.serverType, func(t *testing.T) { -- cc := test.ts.Connect(ctx) -- sd := protocol.ServerDispatcher(cc) -- cc.Go(ctx, -- protocol.Handlers( -- jsonrpc2.MethodNotFound)) +- // If our signature doesn't return the right number of values, it's +- // not a match, so downrank it. For example: +- // +- // var foo func() (int, int) +- // a, b, c := <> // downrank "foo()" since it only returns two values +- if !numberOfResultsCouldMatch { +- cand.score /= 2 +- return false +- } - -- ctx := context.Background() -- ctx, cancel := context.WithCancel(ctx) +- // If at least one assignee has a valid type, and all valid +- // assignees match the corresponding sig result value, the signature +- // is a match. +- allMatch := false +- for i := 0; i < sig.Results().Len(); i++ { +- var assignee types.Type - -- result := make(chan error) -- go func() { -- _, err := sd.Hover(ctx, &protocol.HoverParams{}) -- result <- err -- }() -- // Wait for the Hover request to start. -- <-server.Started -- cancel() -- if err := <-result; err == nil { -- t.Error("nil error for cancelled Hover(), want non-nil") -- } -- if err := <-server.Completed; err == nil || !strings.Contains(err.Error(), "cancelled hover") { -- t.Errorf("Hover(): unexpected server-side error %v", err) +- // If we are completing into variadic parameters, deslice the +- // expected variadic type. +- if ci.variadicAssignees && i >= len(ci.assignees)-1 { +- assignee = ci.assignees[len(ci.assignees)-1] +- if elem := deslice(assignee); elem != nil { +- assignee = elem - } -- }) +- } else { +- assignee = ci.assignees[i] +- } +- +- if assignee == nil || assignee == types.Typ[types.Invalid] { +- continue +- } +- +- allMatch = ci.typeMatches(assignee, sig.Results().At(i).Type()) +- if !allMatch { +- break +- } - } +- return allMatch -} - --const exampleProgram = ` ---- go.mod -- --module mod +-func (c *completer) matchingTypeName(cand *candidate) bool { +- if !c.wantTypeName() { +- return false +- } - --go 1.12 ---- main.go -- --package main +- typeMatches := func(candType types.Type) bool { +- // Take into account any type name modifier prefixes. +- candType = c.inference.applyTypeNameModifiers(candType) - --import "fmt" +- if from := c.inference.typeName.assertableFrom; from != nil { +- // Don't suggest the starting type in type assertions. For example, +- // if "foo" is an io.Writer, don't suggest "foo.(io.Writer)". +- if types.Identical(from, candType) { +- return false +- } - --func main() { -- fmt.Println("Hello World.") --}` +- if intf, ok := from.Underlying().(*types.Interface); ok { +- if !types.AssertableTo(intf, candType) { +- return false +- } +- } +- } - --func TestDebugInfoLifecycle(t *testing.T) { -- sb, err := fake.NewSandbox(&fake.SandboxConfig{Files: fake.UnpackTxt(exampleProgram)}) -- if err != nil { -- t.Fatal(err) -- } -- defer func() { -- if err := sb.Close(); err != nil { -- // TODO(golang/go#38490): we can't currently make this an error because -- // it fails on Windows: the workspace directory is still locked by a -- // separate Go process. -- // Once we have a reliable way to wait for proper shutdown, make this an -- // error. -- t.Logf("closing workspace failed: %v", err) +- if c.inference.typeName.wantComparable && !types.Comparable(candType) { +- return false - } -- }() - -- baseCtx, cancel := context.WithCancel(context.Background()) -- defer cancel() -- clientCtx := debug.WithInstance(baseCtx, "", "") -- serverCtx := debug.WithInstance(baseCtx, "", "") +- // Skip this type if it has already been used in another type +- // switch case. +- for _, seen := range c.inference.typeName.seenTypeSwitchCases { +- if types.Identical(candType, seen) { +- return false +- } +- } - -- cache := cache.New(nil) -- ss := NewStreamServer(cache, false, nil) -- tsBackend := servertest.NewTCPServer(serverCtx, ss, nil) +- // We can expect a type name and have an expected type in cases like: +- // +- // var foo []int +- // foo = []i<> +- // +- // Where our expected type is "[]int", and we expect a type name. +- if c.inference.objType != nil { +- return assignableTo(candType, c.inference.objType) +- } - -- forwarder, err := NewForwarder("tcp;"+tsBackend.Addr, nil) -- if err != nil { -- t.Fatal(err) +- // Default to saying any type name is a match. +- return true - } -- tsForwarder := servertest.NewPipeServer(forwarder, nil) - -- const skipApplyEdits = false -- ed1, err := fake.NewEditor(sb, fake.EditorConfig{}).Connect(clientCtx, tsForwarder, fake.ClientHooks{}, skipApplyEdits) -- if err != nil { -- t.Fatal(err) -- } -- defer ed1.Close(clientCtx) -- ed2, err := fake.NewEditor(sb, fake.EditorConfig{}).Connect(baseCtx, tsBackend, fake.ClientHooks{}, skipApplyEdits) -- if err != nil { -- t.Fatal(err) -- } -- defer ed2.Close(baseCtx) +- t := cand.obj.Type() - -- serverDebug := debug.GetInstance(serverCtx) -- if got, want := len(serverDebug.State.Clients()), 2; got != want { -- t.Errorf("len(server:Clients) = %d, want %d", got, want) -- } -- if got, want := len(serverDebug.State.Sessions()), 2; got != want { -- t.Errorf("len(server:Sessions) = %d, want %d", got, want) -- } -- clientDebug := debug.GetInstance(clientCtx) -- if got, want := len(clientDebug.State.Servers()), 1; got != want { -- t.Errorf("len(client:Servers) = %d, want %d", got, want) -- } -- // Close one of the connections to verify that the client and session were -- // dropped. -- if err := ed1.Close(clientCtx); err != nil { -- t.Fatal(err) +- if typeMatches(t) { +- return true - } -- /*TODO: at this point we have verified the editor is closed -- However there is no way currently to wait for all associated go routines to -- go away, and we need to wait for those to trigger the client drop -- for now we just give it a little bit of time, but we need to fix this -- in a principled way -- */ -- start := time.Now() -- delay := time.Millisecond -- const maxWait = time.Second -- for len(serverDebug.State.Clients()) > 1 { -- if time.Since(start) > maxWait { -- break +- +- if !types.IsInterface(t) && typeMatches(types.NewPointer(t)) { +- if c.inference.typeName.compLitType { +- // If we are completing a composite literal type as in +- // "foo<>{}", to make a pointer we must prepend "&". +- cand.mods = append(cand.mods, reference) +- } else { +- // If we are completing a normal type name such as "foo<>", to +- // make a pointer we must prepend "*". +- cand.mods = append(cand.mods, dereference) - } -- time.Sleep(delay) -- delay *= 2 -- } -- if got, want := len(serverDebug.State.Clients()), 1; got != want { -- t.Errorf("len(server:Clients) = %d, want %d", got, want) -- } -- if got, want := len(serverDebug.State.Sessions()), 1; got != want { -- t.Errorf("len(server:Sessions()) = %d, want %d", got, want) +- return true - } --} - --type initServer struct { -- fakeServer -- -- params *protocol.ParamInitialize +- return false -} - --func (s *initServer) Initialize(ctx context.Context, params *protocol.ParamInitialize) (*protocol.InitializeResult, error) { -- s.params = params -- return &protocol.InitializeResult{}, nil --} +-var ( +- // "interface { Error() string }" (i.e. error) +- errorIntf = types.Universe.Lookup("error").Type().Underlying().(*types.Interface) - --func TestEnvForwarding(t *testing.T) { -- testenv.NeedsTool(t, "go") +- // "interface { String() string }" (i.e. fmt.Stringer) +- stringerIntf = types.NewInterfaceType([]*types.Func{ +- types.NewFunc(token.NoPos, nil, "String", types.NewSignature( +- nil, +- nil, +- types.NewTuple(types.NewParam(token.NoPos, nil, "", types.Typ[types.String])), +- false, +- )), +- }, nil).Complete() - -- ctx := context.Background() +- byteType = types.Universe.Lookup("byte").Type() +-) - -- server := &initServer{} -- _, tsForwarded, cleanup := setupForwarding(ctx, t, server) -- defer cleanup() +-// candKind returns the objKind of candType, if any. +-func candKind(candType types.Type) objKind { +- var kind objKind - -- conn := tsForwarded.Connect(ctx) -- conn.Go(ctx, jsonrpc2.MethodNotFound) -- dispatch := protocol.ServerDispatcher(conn) -- initParams := &protocol.ParamInitialize{} -- initParams.InitializationOptions = map[string]interface{}{ -- "env": map[string]interface{}{ -- "GONOPROXY": "example.com", -- }, -- } -- _, err := dispatch.Initialize(ctx, initParams) -- if err != nil { -- t.Fatal(err) -- } -- if server.params == nil { -- t.Fatalf("initialize params are unset") +- switch t := candType.Underlying().(type) { +- case *types.Array: +- kind |= kindArray +- if t.Elem() == byteType { +- kind |= kindBytes +- } +- case *types.Slice: +- kind |= kindSlice +- if t.Elem() == byteType { +- kind |= kindBytes +- } +- case *types.Chan: +- kind |= kindChan +- case *types.Map: +- kind |= kindMap +- case *types.Pointer: +- kind |= kindPtr +- +- // Some builtins handle array pointers as arrays, so just report a pointer +- // to an array as an array. +- if _, isArray := t.Elem().Underlying().(*types.Array); isArray { +- kind |= kindArray +- } +- case *types.Basic: +- switch info := t.Info(); { +- case info&types.IsString > 0: +- kind |= kindString +- case info&types.IsInteger > 0: +- kind |= kindInt +- case info&types.IsFloat > 0: +- kind |= kindFloat +- case info&types.IsComplex > 0: +- kind |= kindComplex +- case info&types.IsBoolean > 0: +- kind |= kindBool +- } +- case *types.Signature: +- return kindFunc - } -- env := server.params.InitializationOptions.(map[string]interface{})["env"].(map[string]interface{}) - -- // Check for an arbitrary Go variable. It should be set. -- if _, ok := env["GOPRIVATE"]; !ok { -- t.Errorf("Go environment variable GOPRIVATE unset in initialization options") +- if types.Implements(candType, errorIntf) { +- kind |= kindError - } -- // Check that the variable present in our user config was not overwritten. -- if v := env["GONOPROXY"]; v != "example.com" { -- t.Errorf("GONOPROXY environment variable was overwritten") +- +- if types.Implements(candType, stringerIntf) { +- kind |= kindStringer - } +- +- return kind -} - --func TestListenParsing(t *testing.T) { -- tests := []struct { -- input, wantNetwork, wantAddr string -- }{ -- {"127.0.0.1:0", "tcp", "127.0.0.1:0"}, -- {"unix;/tmp/sock", "unix", "/tmp/sock"}, -- {"auto", "auto", ""}, -- {"auto;foo", "auto", "foo"}, +-// innermostScope returns the innermost scope for c.pos. +-func (c *completer) innermostScope() *types.Scope { +- for _, s := range c.scopes { +- if s != nil { +- return s +- } - } +- return nil +-} - -- for _, test := range tests { -- gotNetwork, gotAddr := ParseAddr(test.input) -- if gotNetwork != test.wantNetwork { -- t.Errorf("network = %q, want %q", gotNetwork, test.wantNetwork) -- } -- if gotAddr != test.wantAddr { -- t.Errorf("addr = %q, want %q", gotAddr, test.wantAddr) +-// isSlice reports whether the object's underlying type is a slice. +-func isSlice(obj types.Object) bool { +- if obj != nil && obj.Type() != nil { +- if _, ok := obj.Type().Underlying().(*types.Slice); ok { +- return true - } - } +- return false -} - --// For #59479, verify that empty slices are serialized as []. --func TestEmptySlices(t *testing.T) { -- // The LSP would prefer that empty slices be sent as [] rather than null. -- const bad = `{"a":null}` -- const good = `{"a":[]}` -- var x struct { -- A []string `json:"a"` -- } -- buf, _ := json.Marshal(x) -- if string(buf) != bad { -- // uninitialized is ezpected to give null -- t.Errorf("unexpectedly got %s, want %s", buf, bad) -- } -- x.A = make([]string, 0) -- buf, _ = json.Marshal(x) -- if string(buf) != good { -- // expect [] -- t.Errorf("unexpectedly got %s, want %s", buf, good) -- } -- x.A = []string{} -- buf, _ = json.Marshal(x) -- if string(buf) != good { -- // expect [] -- t.Errorf("unexpectedly got %s, want %s", buf, good) +-// forEachPackageMember calls f(tok, id, fn) for each package-level +-// TYPE/VAR/CONST/FUNC declaration in the Go source file, based on a +-// quick partial parse. fn is non-nil only for function declarations. +-// The AST position information is garbage. +-func forEachPackageMember(content []byte, f func(tok token.Token, id *ast.Ident, fn *ast.FuncDecl)) { +- purged := goplsastutil.PurgeFuncBodies(content) +- file, _ := parser.ParseFile(token.NewFileSet(), "", purged, 0) +- for _, decl := range file.Decls { +- switch decl := decl.(type) { +- case *ast.GenDecl: +- for _, spec := range decl.Specs { +- switch spec := spec.(type) { +- case *ast.ValueSpec: // var/const +- for _, id := range spec.Names { +- f(decl.Tok, id, nil) +- } +- case *ast.TypeSpec: +- f(decl.Tok, spec.Name, nil) +- } +- } +- case *ast.FuncDecl: +- if decl.Recv == nil { +- f(token.FUNC, decl.Name, decl) +- } +- } - } -} -diff -urN a/gopls/internal/lsp/lsprpc/middleware.go b/gopls/internal/lsp/lsprpc/middleware.go ---- a/gopls/internal/lsp/lsprpc/middleware.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/lsprpc/middleware.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,142 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. +- +-func is[T any](x any) bool { +- _, ok := x.(T) +- return ok +-} +diff -urN a/gopls/internal/golang/completion/deep_completion.go b/gopls/internal/golang/completion/deep_completion.go +--- a/gopls/internal/golang/completion/deep_completion.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/completion/deep_completion.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,371 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package lsprpc +-package completion - -import ( - "context" -- "encoding/json" -- "fmt" -- "sync" -- -- "golang.org/x/tools/internal/event" -- jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" +- "go/types" +- "strings" +- "time" -) - --// Metadata holds arbitrary data transferred between jsonrpc2 peers. --type Metadata map[string]interface{} +-// MaxDeepCompletions limits deep completion results because in most cases +-// there are too many to be useful. +-const MaxDeepCompletions = 3 - --// PeerInfo holds information about a peering between jsonrpc2 servers. --type PeerInfo struct { -- // RemoteID is the identity of the current server on its peer. -- RemoteID int64 +-// deepCompletionState stores our state as we search for deep completions. +-// "deep completion" refers to searching into objects' fields and methods to +-// find more completion candidates. +-type deepCompletionState struct { +- // enabled indicates whether deep completion is permitted. +- enabled bool - -- // LocalID is the identity of the peer on the server. -- LocalID int64 +- // queueClosed is used to disable adding new sub-fields to search queue +- // once we're running out of our time budget. +- queueClosed bool - -- // IsClient reports whether the peer is a client. If false, the peer is a -- // server. -- IsClient bool +- // thisQueue holds the current breadth first search queue. +- thisQueue []candidate - -- // Metadata holds arbitrary information provided by the peer. -- Metadata Metadata --} +- // nextQueue holds the next breadth first search iteration's queue. +- nextQueue []candidate - --// Handshaker handles both server and client handshaking over jsonrpc2. To --// instrument server-side handshaking, use Handshaker.Middleware. To instrument --// client-side handshaking, call Handshaker.ClientHandshake for any new --// client-side connections. --type Handshaker struct { -- // Metadata will be shared with peers via handshaking. -- Metadata Metadata +- // highScores tracks the highest deep candidate scores we have found +- // so far. This is used to avoid work for low scoring deep candidates. +- highScores [MaxDeepCompletions]float64 - -- mu sync.Mutex -- prevID int64 -- peers map[int64]PeerInfo +- // candidateCount is the count of unique deep candidates encountered +- // so far. +- candidateCount int -} - --// Peers returns the peer info this handshaker knows about by way of either the --// server-side handshake middleware, or client-side handshakes. --func (h *Handshaker) Peers() []PeerInfo { -- h.mu.Lock() -- defer h.mu.Unlock() -- -- var c []PeerInfo -- for _, v := range h.peers { -- c = append(c, v) -- } -- return c +-// enqueue adds a candidate to the search queue. +-func (s *deepCompletionState) enqueue(cand candidate) { +- s.nextQueue = append(s.nextQueue, cand) -} - --// Middleware is a jsonrpc2 middleware function to augment connection binding --// to handle the handshake method, and record disconnections. --func (h *Handshaker) Middleware(inner jsonrpc2_v2.Binder) jsonrpc2_v2.Binder { -- return BinderFunc(func(ctx context.Context, conn *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions { -- opts := inner.Bind(ctx, conn) +-// scorePenalty computes a deep candidate score penalty. A candidate is +-// penalized based on depth to favor shallower candidates. We also give a +-// slight bonus to unexported objects and a slight additional penalty to +-// function objects. +-func (s *deepCompletionState) scorePenalty(cand *candidate) float64 { +- var deepPenalty float64 +- for _, dc := range cand.path { +- deepPenalty++ - -- localID := h.nextID() -- info := &PeerInfo{ -- RemoteID: localID, -- Metadata: h.Metadata, +- if !dc.Exported() { +- deepPenalty -= 0.1 - } - -- // Wrap the delegated handler to accept the handshake. -- delegate := opts.Handler -- opts.Handler = jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) { -- if req.Method == handshakeMethod { -- var peerInfo PeerInfo -- if err := json.Unmarshal(req.Params, &peerInfo); err != nil { -- return nil, fmt.Errorf("%w: unmarshaling client info: %v", jsonrpc2_v2.ErrInvalidParams, err) -- } -- peerInfo.LocalID = localID -- peerInfo.IsClient = true -- h.recordPeer(peerInfo) -- return info, nil -- } -- return delegate.Handle(ctx, req) -- }) -- -- // Record the dropped client. -- go h.cleanupAtDisconnect(conn, localID) -- -- return opts -- }) --} -- --// ClientHandshake performs a client-side handshake with the server at the --// other end of conn, recording the server's peer info and watching for conn's --// disconnection. --func (h *Handshaker) ClientHandshake(ctx context.Context, conn *jsonrpc2_v2.Connection) { -- localID := h.nextID() -- info := &PeerInfo{ -- RemoteID: localID, -- Metadata: h.Metadata, -- } -- -- call := conn.Call(ctx, handshakeMethod, info) -- var serverInfo PeerInfo -- if err := call.Await(ctx, &serverInfo); err != nil { -- event.Error(ctx, "performing handshake", err) -- return +- if _, isSig := dc.Type().Underlying().(*types.Signature); isSig { +- deepPenalty += 0.1 +- } - } -- serverInfo.LocalID = localID -- h.recordPeer(serverInfo) -- -- go h.cleanupAtDisconnect(conn, localID) --} -- --func (h *Handshaker) nextID() int64 { -- h.mu.Lock() -- defer h.mu.Unlock() - -- h.prevID++ -- return h.prevID +- // Normalize penalty to a max depth of 10. +- return deepPenalty / 10 -} - --func (h *Handshaker) cleanupAtDisconnect(conn *jsonrpc2_v2.Connection, peerID int64) { -- conn.Wait() +-// isHighScore returns whether score is among the top MaxDeepCompletions deep +-// candidate scores encountered so far. If so, it adds score to highScores, +-// possibly displacing an existing high score. +-func (s *deepCompletionState) isHighScore(score float64) bool { +- // Invariant: s.highScores is sorted with highest score first. Unclaimed +- // positions are trailing zeros. - -- h.mu.Lock() -- defer h.mu.Unlock() -- delete(h.peers, peerID) --} +- // If we beat an existing score then take its spot. +- for i, deepScore := range s.highScores { +- if score <= deepScore { +- continue +- } - --func (h *Handshaker) recordPeer(info PeerInfo) { -- h.mu.Lock() -- defer h.mu.Unlock() -- if h.peers == nil { -- h.peers = make(map[int64]PeerInfo) +- if deepScore != 0 && i != len(s.highScores)-1 { +- // If this wasn't an empty slot then we need to scooch everyone +- // down one spot. +- copy(s.highScores[i+1:], s.highScores[i:]) +- } +- s.highScores[i] = score +- return true - } -- h.peers[info.LocalID] = info --} -diff -urN a/gopls/internal/lsp/lsprpc/middleware_test.go b/gopls/internal/lsp/lsprpc/middleware_test.go ---- a/gopls/internal/lsp/lsprpc/middleware_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/lsprpc/middleware_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,93 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package lsprpc_test - --import ( -- "context" -- "errors" -- "fmt" -- "testing" -- "time" +- return false +-} - -- . "golang.org/x/tools/gopls/internal/lsp/lsprpc" -- jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" --) +-// newPath returns path from search root for an object following a given +-// candidate. +-func (s *deepCompletionState) newPath(cand candidate, obj types.Object) []types.Object { +- path := make([]types.Object, len(cand.path)+1) +- copy(path, cand.path) +- path[len(path)-1] = obj - --var noopBinder = BinderFunc(func(context.Context, *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions { -- return jsonrpc2_v2.ConnectionOptions{} --}) +- return path +-} - --func TestHandshakeMiddleware(t *testing.T) { -- sh := &Handshaker{ -- Metadata: Metadata{ -- "answer": 42, -- }, -- } -- ctx := context.Background() -- env := new(TestEnv) -- defer env.Shutdown(t) -- l, _ := env.serve(ctx, t, sh.Middleware(noopBinder)) -- conn := env.dial(ctx, t, l.Dialer(), noopBinder, false) -- ch := &Handshaker{ -- Metadata: Metadata{ -- "question": 6 * 9, -- }, -- } +-// deepSearch searches a candidate and its subordinate objects for completion +-// items if deep completion is enabled and adds the valid candidates to +-// completion items. +-func (c *completer) deepSearch(ctx context.Context, minDepth int, deadline *time.Time) { +- defer func() { +- // We can return early before completing the search, so be sure to +- // clear out our queues to not impact any further invocations. +- c.deepState.thisQueue = c.deepState.thisQueue[:0] +- c.deepState.nextQueue = c.deepState.nextQueue[:0] +- }() - -- check := func(connected bool) error { -- clients := sh.Peers() -- servers := ch.Peers() -- want := 0 -- if connected { -- want = 1 -- } -- if got := len(clients); got != want { -- return fmt.Errorf("got %d clients on the server, want %d", got, want) -- } -- if got := len(servers); got != want { -- return fmt.Errorf("got %d servers on the client, want %d", got, want) -- } -- if !connected { -- return nil -- } -- client := clients[0] -- server := servers[0] -- if _, ok := client.Metadata["question"]; !ok { -- return errors.New("no client metadata") -- } -- if _, ok := server.Metadata["answer"]; !ok { -- return errors.New("no server metadata") -- } -- if client.LocalID != server.RemoteID { -- return fmt.Errorf("client.LocalID == %d, server.PeerID == %d", client.LocalID, server.RemoteID) -- } -- if client.RemoteID != server.LocalID { -- return fmt.Errorf("client.PeerID == %d, server.LocalID == %d", client.RemoteID, server.LocalID) +- depth := 0 // current depth being processed +- // Stop reports whether we should stop the search immediately. +- stop := func() bool { +- // Context cancellation indicates that the actual completion operation was +- // cancelled, so ignore minDepth and deadline. +- select { +- case <-ctx.Done(): +- return true +- default: - } -- return nil +- // Otherwise, only stop if we've searched at least minDepth and reached the deadline. +- return depth > minDepth && deadline != nil && time.Now().After(*deadline) - } - -- if err := check(false); err != nil { -- t.Fatalf("before handshake: %v", err) -- } -- ch.ClientHandshake(ctx, conn) -- if err := check(true); err != nil { -- t.Fatalf("after handshake: %v", err) -- } -- conn.Close() -- // Wait for up to ~2s for connections to get cleaned up. -- delay := 25 * time.Millisecond -- for retries := 3; retries >= 0; retries-- { -- time.Sleep(delay) -- err := check(false) -- if err == nil { +- for len(c.deepState.nextQueue) > 0 { +- depth++ +- if stop() { - return - } -- if retries == 0 { -- t.Fatalf("after closing connection: %v", err) -- } -- delay *= 4 -- } --} -diff -urN a/gopls/internal/lsp/lsp_test.go b/gopls/internal/lsp/lsp_test.go ---- a/gopls/internal/lsp/lsp_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/lsp_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,616 +0,0 @@ --// Copyright 2018 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package lsp -- --import ( -- "bytes" -- "context" -- "fmt" -- "os" -- "path/filepath" -- "sort" -- "strings" -- "testing" +- c.deepState.thisQueue, c.deepState.nextQueue = c.deepState.nextQueue, c.deepState.thisQueue[:0] - -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/cache" -- "golang.org/x/tools/gopls/internal/lsp/command" -- "golang.org/x/tools/gopls/internal/lsp/debug" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/lsp/tests" -- "golang.org/x/tools/gopls/internal/lsp/tests/compare" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/testenv" --) +- outer: +- for _, cand := range c.deepState.thisQueue { +- obj := cand.obj - --func TestMain(m *testing.M) { -- bug.PanicOnBugs = true -- testenv.ExitIfSmallMachine() +- if obj == nil { +- continue +- } - -- os.Exit(m.Run()) --} +- // At the top level, dedupe by object. +- if len(cand.path) == 0 { +- if c.seen[obj] { +- continue +- } +- c.seen[obj] = true +- } - --// TestLSP runs the marker tests in files beneath testdata/ using --// implementations of each of the marker operations that make LSP RPCs to a --// gopls server. --func TestLSP(t *testing.T) { -- tests.RunTests(t, "testdata", true, testLSP) --} +- // If obj is not accessible because it lives in another package and is +- // not exported, don't treat it as a completion candidate unless it's +- // a package completion candidate. +- if !c.completionContext.packageCompletion && +- obj.Pkg() != nil && obj.Pkg() != c.pkg.Types() && !obj.Exported() { +- continue +- } - --func testLSP(t *testing.T, datum *tests.Data) { -- ctx := tests.Context(t) +- // If we want a type name, don't offer non-type name candidates. +- // However, do offer package names since they can contain type names, +- // and do offer any candidate without a type since we aren't sure if it +- // is a type name or not (i.e. unimported candidate). +- if c.wantTypeName() && obj.Type() != nil && !isTypeName(obj) && !isPkgName(obj) { +- continue +- } - -- // Setting a debug instance suppresses logging to stderr, but ensures that we -- // still e.g. convert events into runtime/trace/instrumentation. -- // -- // Previously, we called event.SetExporter(nil), which turns off all -- // instrumentation. -- ctx = debug.WithInstance(ctx, "", "off") +- // When searching deep, make sure we don't have a cycle in our chain. +- // We don't dedupe by object because we want to allow both "foo.Baz" +- // and "bar.Baz" even though "Baz" is represented the same types.Object +- // in both. +- for _, seenObj := range cand.path { +- if seenObj == obj { +- continue outer +- } +- } - -- session := cache.NewSession(ctx, cache.New(nil)) -- options := source.DefaultOptions(tests.DefaultOptions) -- options.SetEnvSlice(datum.Config.Env) -- folder := &cache.Folder{ -- Dir: span.URIFromPath(datum.Config.Dir), -- Name: datum.Config.Dir, -- Options: options, -- } -- view, snapshot, release, err := session.NewView(ctx, folder) -- if err != nil { -- t.Fatal(err) -- } +- c.addCandidate(ctx, &cand) - -- defer session.RemoveView(view) +- c.deepState.candidateCount++ +- if c.opts.budget > 0 && c.deepState.candidateCount%100 == 0 { +- if stop() { +- return +- } +- spent := float64(time.Since(c.startTime)) / float64(c.opts.budget) +- // If we are almost out of budgeted time, no further elements +- // should be added to the queue. This ensures remaining time is +- // used for processing current queue. +- if !c.deepState.queueClosed && spent >= 0.85 { +- c.deepState.queueClosed = true +- } +- } - -- // Only run the -modfile specific tests in module mode with Go 1.14 or above. -- datum.ModfileFlagAvailable = len(snapshot.ModFiles()) > 0 && testenv.Go1Point() >= 14 -- release() +- // if deep search is disabled, don't add any more candidates. +- if !c.deepState.enabled || c.deepState.queueClosed { +- continue +- } - -- // Open all files for performance reasons, because gopls only -- // keeps active packages (those with open files) in memory. -- // -- // In practice clients will only send document-oriented requests for open -- // files. -- var modifications []source.FileModification -- for _, module := range datum.Exported.Modules { -- for name := range module.Files { -- filename := datum.Exported.File(module.Name, name) -- if filepath.Ext(filename) != ".go" { +- // Searching members for a type name doesn't make sense. +- if isTypeName(obj) { - continue - } -- content, err := datum.Exported.FileContents(filename) -- if err != nil { -- t.Fatal(err) +- if obj.Type() == nil { +- continue - } -- modifications = append(modifications, source.FileModification{ -- URI: span.URIFromPath(filename), -- Action: source.Open, -- Version: -1, -- Text: content, -- LanguageID: "go", -- }) -- } -- } -- for filename, content := range datum.Config.Overlay { -- if filepath.Ext(filename) != ".go" { -- continue -- } -- modifications = append(modifications, source.FileModification{ -- URI: span.URIFromPath(filename), -- Action: source.Open, -- Version: -1, -- Text: content, -- LanguageID: "go", -- }) -- } -- if err := session.ModifyFiles(ctx, modifications); err != nil { -- t.Fatal(err) -- } -- r := &runner{ -- data: datum, -- ctx: ctx, -- editRecv: make(chan map[span.URI][]byte, 1), -- } -- -- r.server = NewServer(session, testClient{runner: r}, options) -- tests.Run(t, r, datum) --} - --// runner implements tests.Tests by making LSP RPCs to a gopls server. --type runner struct { -- server *Server -- data *tests.Data -- diagnostics map[span.URI][]*source.Diagnostic -- ctx context.Context -- editRecv chan map[span.URI][]byte --} +- // Don't search embedded fields because they were already included in their +- // parent's fields. +- if v, ok := obj.(*types.Var); ok && v.Embedded() { +- continue +- } - --// testClient stubs any client functions that may be called by LSP functions. --type testClient struct { -- protocol.Client -- runner *runner --} +- if sig, ok := obj.Type().Underlying().(*types.Signature); ok { +- // If obj is a function that takes no arguments and returns one +- // value, keep searching across the function call. +- if sig.Params().Len() == 0 && sig.Results().Len() == 1 { +- path := c.deepState.newPath(cand, obj) +- // The result of a function call is not addressable. +- c.methodsAndFields(sig.Results().At(0).Type(), false, cand.imp, func(newCand candidate) { +- newCand.pathInvokeMask = cand.pathInvokeMask | (1 << uint64(len(cand.path))) +- newCand.path = path +- c.deepState.enqueue(newCand) +- }) +- } +- } - --func (c testClient) Close() error { -- return nil +- path := c.deepState.newPath(cand, obj) +- switch obj := obj.(type) { +- case *types.PkgName: +- c.packageMembers(obj.Imported(), stdScore, cand.imp, func(newCand candidate) { +- newCand.pathInvokeMask = cand.pathInvokeMask +- newCand.path = path +- c.deepState.enqueue(newCand) +- }) +- default: +- c.methodsAndFields(obj.Type(), cand.addressable, cand.imp, func(newCand candidate) { +- newCand.pathInvokeMask = cand.pathInvokeMask +- newCand.path = path +- c.deepState.enqueue(newCand) +- }) +- } +- } +- } -} - --// Trivially implement PublishDiagnostics so that we can call --// server.publishReports below to de-dup sent diagnostics. --func (c testClient) PublishDiagnostics(context.Context, *protocol.PublishDiagnosticsParams) error { -- return nil --} +-// addCandidate adds a completion candidate to suggestions, without searching +-// its members for more candidates. +-func (c *completer) addCandidate(ctx context.Context, cand *candidate) { +- obj := cand.obj +- if c.matchingCandidate(cand) { +- cand.score *= highScore - --func (c testClient) ShowMessage(context.Context, *protocol.ShowMessageParams) error { -- return nil --} +- if p := c.penalty(cand); p > 0 { +- cand.score *= (1 - p) +- } +- } else if isTypeName(obj) { +- // If obj is a *types.TypeName that didn't otherwise match, check +- // if a literal object of this type makes a good candidate. - --func (c testClient) ApplyEdit(ctx context.Context, params *protocol.ApplyWorkspaceEditParams) (*protocol.ApplyWorkspaceEditResult, error) { -- res, err := applyTextDocumentEdits(c.runner, params.Edit.DocumentChanges) -- if err != nil { -- return nil, err +- // We only care about named types (i.e. don't want builtin types). +- if _, isNamed := obj.Type().(*types.Named); isNamed { +- c.literal(ctx, obj.Type(), cand.imp) +- } - } -- c.runner.editRecv <- res -- return &protocol.ApplyWorkspaceEditResult{Applied: true}, nil --} - --func (r *runner) CallHierarchy(t *testing.T, spn span.Span, expectedCalls *tests.CallHierarchyResult) { -- mapper, err := r.data.Mapper(spn.URI()) -- if err != nil { -- t.Fatal(err) -- } -- loc, err := mapper.SpanLocation(spn) -- if err != nil { -- t.Fatalf("failed for %v: %v", spn, err) +- // Lower score of method calls so we prefer fields and vars over calls. +- if cand.hasMod(invoke) { +- if sig, ok := obj.Type().Underlying().(*types.Signature); ok && sig.Recv() != nil { +- cand.score *= 0.9 +- } - } - -- params := &protocol.CallHierarchyPrepareParams{ -- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), +- // Prefer private objects over public ones. +- if !obj.Exported() && obj.Parent() != types.Universe { +- cand.score *= 1.1 - } - -- items, err := r.server.PrepareCallHierarchy(r.ctx, params) -- if err != nil { -- t.Fatal(err) -- } -- if len(items) == 0 { -- t.Fatalf("expected call hierarchy item to be returned for identifier at %v\n", loc.Range) +- // Slight penalty for index modifier (e.g. changing "foo" to +- // "foo[]") to curb false positives. +- if cand.hasMod(index) { +- cand.score *= 0.9 - } - -- callLocation := protocol.Location{ -- URI: items[0].URI, -- Range: items[0].Range, -- } -- if callLocation != loc { -- t.Fatalf("expected server.PrepareCallHierarchy to return identifier at %v but got %v\n", loc, callLocation) -- } +- // Favor shallow matches by lowering score according to depth. +- cand.score -= cand.score * c.deepState.scorePenalty(cand) - -- incomingCalls, err := r.server.IncomingCalls(r.ctx, &protocol.CallHierarchyIncomingCallsParams{Item: items[0]}) -- if err != nil { -- t.Error(err) -- } -- var incomingCallItems []protocol.CallHierarchyItem -- for _, item := range incomingCalls { -- incomingCallItems = append(incomingCallItems, item.From) -- } -- msg := tests.DiffCallHierarchyItems(incomingCallItems, expectedCalls.IncomingCalls) -- if msg != "" { -- t.Errorf("incoming calls: %s", msg) +- if cand.score < 0 { +- cand.score = 0 - } - -- outgoingCalls, err := r.server.OutgoingCalls(r.ctx, &protocol.CallHierarchyOutgoingCallsParams{Item: items[0]}) -- if err != nil { -- t.Error(err) -- } -- var outgoingCallItems []protocol.CallHierarchyItem -- for _, item := range outgoingCalls { -- outgoingCallItems = append(outgoingCallItems, item.To) -- } -- msg = tests.DiffCallHierarchyItems(outgoingCallItems, expectedCalls.OutgoingCalls) -- if msg != "" { -- t.Errorf("outgoing calls: %s", msg) +- cand.name = deepCandName(cand) +- if item, err := c.item(ctx, *cand); err == nil { +- c.items = append(c.items, item) - } -} - --func (r *runner) SemanticTokens(t *testing.T, spn span.Span) { -- uri := spn.URI() -- filename := uri.Filename() -- // this is called solely for coverage in semantic.go -- _, err := r.server.semanticTokensFull(r.ctx, &protocol.SemanticTokensParams{ -- TextDocument: protocol.TextDocumentIdentifier{ -- URI: protocol.URIFromSpanURI(uri), -- }, -- }) -- if err != nil { -- t.Errorf("%v for %s", err, filename) -- } -- _, err = r.server.semanticTokensRange(r.ctx, &protocol.SemanticTokensRangeParams{ -- TextDocument: protocol.TextDocumentIdentifier{ -- URI: protocol.URIFromSpanURI(uri), -- }, -- // any legal range. Just to exercise the call. -- Range: protocol.Range{ -- Start: protocol.Position{ -- Line: 0, -- Character: 0, -- }, -- End: protocol.Position{ -- Line: 2, -- Character: 0, -- }, -- }, -- }) -- if err != nil { -- t.Errorf("%v for Range %s", err, filename) +-// deepCandName produces the full candidate name including any +-// ancestor objects. For example, "foo.bar().baz" for candidate "baz". +-func deepCandName(cand *candidate) string { +- totalLen := len(cand.obj.Name()) +- for i, obj := range cand.path { +- totalLen += len(obj.Name()) + 1 +- if cand.pathInvokeMask&(1< 0 { +- totalLen += 2 +- } - } --} - --func (r *runner) SuggestedFix(t *testing.T, spn span.Span, actionKinds []tests.SuggestedFix, expectedActions int) { -- uri := spn.URI() -- view, err := r.server.session.ViewOf(uri) -- if err != nil { -- t.Fatal(err) -- } +- var buf strings.Builder +- buf.Grow(totalLen) - -- m, err := r.data.Mapper(uri) -- if err != nil { -- t.Fatal(err) -- } -- rng, err := m.SpanRange(spn) -- if err != nil { -- t.Fatal(err) -- } -- // Get the diagnostics for this view if we have not done it before. -- r.collectDiagnostics(view) -- var diagnostics []protocol.Diagnostic -- for _, d := range r.diagnostics[uri] { -- // Compare the start positions rather than the entire range because -- // some diagnostics have a range with the same start and end position (8:1-8:1). -- // The current marker functionality prevents us from having a range of 0 length. -- if protocol.ComparePosition(d.Range.Start, rng.Start) == 0 { -- diagnostics = append(diagnostics, toProtocolDiagnostics([]*source.Diagnostic{d})...) -- break +- for i, obj := range cand.path { +- buf.WriteString(obj.Name()) +- if cand.pathInvokeMask&(1< 0 { +- buf.WriteByte('(') +- buf.WriteByte(')') - } +- buf.WriteByte('.') - } -- var codeActionKinds []protocol.CodeActionKind -- for _, k := range actionKinds { -- codeActionKinds = append(codeActionKinds, protocol.CodeActionKind(k.ActionKind)) -- } -- allActions, err := r.server.CodeAction(r.ctx, &protocol.CodeActionParams{ -- TextDocument: protocol.TextDocumentIdentifier{ -- URI: protocol.URIFromSpanURI(uri), -- }, -- Range: rng, -- Context: protocol.CodeActionContext{ -- Only: codeActionKinds, -- Diagnostics: diagnostics, -- }, -- }) -- if err != nil { -- t.Fatalf("CodeAction %s failed: %v", spn, err) -- } -- var actions []protocol.CodeAction -- for _, action := range allActions { -- for _, fix := range actionKinds { -- if strings.Contains(action.Title, fix.Title) { -- actions = append(actions, action) -- break -- } -- } - -- } -- if len(actions) != expectedActions { -- var summaries []string -- for _, a := range actions { -- summaries = append(summaries, fmt.Sprintf("%q (%s)", a.Title, a.Kind)) +- buf.WriteString(cand.obj.Name()) +- +- return buf.String() +-} +- +-// penalty reports a score penalty for cand in the range (0, 1). +-// For example, a candidate is penalized if it has already been used +-// in another switch case statement. +-func (c *completer) penalty(cand *candidate) float64 { +- for _, p := range c.inference.penalized { +- if c.objChainMatches(cand, p.objChain) { +- return p.penalty - } -- t.Fatalf("CodeAction(...): got %d code actions (%v), want %d", len(actions), summaries, expectedActions) - } -- action := actions[0] -- var match bool -- for _, k := range codeActionKinds { -- if action.Kind == k { -- match = true -- break -- } +- +- return 0 +-} +- +-// objChainMatches reports whether cand combined with the surrounding +-// object prefix matches chain. +-func (c *completer) objChainMatches(cand *candidate, chain []types.Object) bool { +- // For example, when completing: +- // +- // foo.ba<> +- // +- // If we are considering the deep candidate "bar.baz", cand is baz, +- // objChain is [foo] and deepChain is [bar]. We would match the +- // chain [foo, bar, baz]. +- if len(chain) != len(c.inference.objChain)+len(cand.path)+1 { +- return false - } -- if !match { -- t.Fatalf("unexpected kind for code action %s, got %v, want one of %v", action.Title, action.Kind, codeActionKinds) +- +- if chain[len(chain)-1] != cand.obj { +- return false - } -- var res map[span.URI][]byte -- if cmd := action.Command; cmd != nil { -- _, err := r.server.ExecuteCommand(r.ctx, &protocol.ExecuteCommandParams{ -- Command: action.Command.Command, -- Arguments: action.Command.Arguments, -- }) -- if err != nil { -- t.Fatalf("error converting command %q to edits: %v", action.Command.Command, err) -- } -- res = <-r.editRecv -- } else { -- res, err = applyTextDocumentEdits(r, action.Edit.DocumentChanges) -- if err != nil { -- t.Fatal(err) +- +- for i, o := range c.inference.objChain { +- if chain[i] != o { +- return false - } - } -- for u, got := range res { -- want := r.data.Golden(t, "suggestedfix_"+tests.SpanName(spn), u.Filename(), func() ([]byte, error) { -- return got, nil -- }) -- if diff := compare.Bytes(want, got); diff != "" { -- t.Errorf("suggested fixes failed for %s:\n%s", u.Filename(), diff) +- +- for i, o := range cand.path { +- if chain[i+len(c.inference.objChain)] != o { +- return false - } - } +- +- return true -} +diff -urN a/gopls/internal/golang/completion/deep_completion_test.go b/gopls/internal/golang/completion/deep_completion_test.go +--- a/gopls/internal/golang/completion/deep_completion_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/completion/deep_completion_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,33 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func (r *runner) InlayHints(t *testing.T, spn span.Span) { -- uri := spn.URI() -- filename := uri.Filename() +-package completion - -- hints, err := r.server.InlayHint(r.ctx, &protocol.InlayHintParams{ -- TextDocument: protocol.TextDocumentIdentifier{ -- URI: protocol.URIFromSpanURI(uri), -- }, -- // TODO: add Range -- }) -- if err != nil { -- t.Fatal(err) +-import ( +- "testing" +-) +- +-func TestDeepCompletionIsHighScore(t *testing.T) { +- // Test that deepCompletionState.isHighScore properly tracks the top +- // N=MaxDeepCompletions scores. +- +- var s deepCompletionState +- +- if !s.isHighScore(1) { +- // No other scores yet, anything is a winner. +- t.Error("1 should be high score") - } - -- // Map inlay hints to text edits. -- edits := make([]protocol.TextEdit, len(hints)) -- for i, hint := range hints { -- var paddingLeft, paddingRight string -- if hint.PaddingLeft { -- paddingLeft = " " -- } -- if hint.PaddingRight { -- paddingRight = " " -- } -- edits[i] = protocol.TextEdit{ -- Range: protocol.Range{Start: hint.Position, End: hint.Position}, -- NewText: fmt.Sprintf("<%s%s%s>", paddingLeft, hint.Label[0].Value, paddingRight), +- // Fill up with higher scores. +- for i := 0; i < MaxDeepCompletions; i++ { +- if !s.isHighScore(10) { +- t.Error("10 should be high score") - } - } - -- m, err := r.data.Mapper(uri) -- if err != nil { -- t.Fatal(err) -- } -- got, _, err := source.ApplyProtocolEdits(m, edits) -- if err != nil { -- t.Error(err) +- // High scores should be filled with 10s so 2 is not a high score. +- if s.isHighScore(2) { +- t.Error("2 shouldn't be high score") - } +-} +diff -urN a/gopls/internal/golang/completion/definition.go b/gopls/internal/golang/completion/definition.go +--- a/gopls/internal/golang/completion/definition.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/completion/definition.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,160 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- withinlayHints := r.data.Golden(t, "inlayHint", filename, func() ([]byte, error) { -- return got, nil -- }) +-package completion - -- if !bytes.Equal(withinlayHints, got) { -- t.Errorf("inlay hints failed for %s, expected:\n%s\ngot:\n%s", filename, withinlayHints, got) -- } --} +-import ( +- "go/ast" +- "go/types" +- "strings" +- "unicode" +- "unicode/utf8" - --func (r *runner) Rename(t *testing.T, spn span.Span, newText string) { -- tag := fmt.Sprintf("%s-rename", newText) +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/golang/completion/snippet" +- "golang.org/x/tools/gopls/internal/protocol" +-) - -- uri := spn.URI() -- filename := uri.Filename() -- sm, err := r.data.Mapper(uri) -- if err != nil { -- t.Fatal(err) +-// some function definitions in test files can be completed +-// So far, TestFoo(t *testing.T), TestMain(m *testing.M) +-// BenchmarkFoo(b *testing.B), FuzzFoo(f *testing.F) +- +-// path[0] is known to be *ast.Ident +-func definition(path []ast.Node, obj types.Object, pgf *parsego.File) ([]CompletionItem, *Selection) { +- if _, ok := obj.(*types.Func); !ok { +- return nil, nil // not a function at all - } -- loc, err := sm.SpanLocation(spn) -- if err != nil { -- t.Fatalf("failed for %v: %v", spn, err) +- if !strings.HasSuffix(pgf.URI.Path(), "_test.go") { +- return nil, nil // not a test file - } - -- wedit, err := r.server.Rename(r.ctx, &protocol.RenameParams{ -- TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, -- Position: loc.Range.Start, -- NewName: newText, -- }) -- if err != nil { -- renamed := string(r.data.Golden(t, tag, filename, func() ([]byte, error) { -- return []byte(err.Error()), nil -- })) -- if err.Error() != renamed { -- t.Errorf("%s: rename failed for %s, expected:\n%v\ngot:\n%v\n", spn, newText, renamed, err) -- } -- return +- name := path[0].(*ast.Ident).Name +- if len(name) == 0 { +- // can't happen +- return nil, nil - } -- res, err := applyTextDocumentEdits(r, wedit.DocumentChanges) -- if err != nil { -- t.Fatal(err) +- start := path[0].Pos() +- end := path[0].End() +- sel := &Selection{ +- content: "", +- cursor: start, +- tokFile: pgf.Tok, +- start: start, +- end: end, +- mapper: pgf.Mapper, - } -- var orderedURIs []string -- for uri := range res { -- orderedURIs = append(orderedURIs, string(uri)) +- var ans []CompletionItem +- var hasParens bool +- n, ok := path[1].(*ast.FuncDecl) +- if !ok { +- return nil, nil // can't happen - } -- sort.Strings(orderedURIs) -- -- // Print the name and content of each modified file, -- // concatenated, and compare against the golden. -- var buf bytes.Buffer -- for i := 0; i < len(res); i++ { -- if i != 0 { -- buf.WriteByte('\n') -- } -- uri := span.URIFromURI(orderedURIs[i]) -- if len(res) > 1 { -- buf.WriteString(filepath.Base(uri.Filename())) -- buf.WriteString(":\n") -- } -- buf.Write(res[uri]) +- if n.Recv != nil { +- return nil, nil // a method, not a function - } -- got := buf.Bytes() -- want := r.data.Golden(t, tag, filename, func() ([]byte, error) { -- return got, nil -- }) -- if diff := compare.Bytes(want, got); diff != "" { -- t.Errorf("rename failed for %s:\n%s", newText, diff) +- t := n.Type.Params +- if t.Closing != t.Opening { +- hasParens = true - } --} - --func applyTextDocumentEdits(r *runner, edits []protocol.DocumentChanges) (map[span.URI][]byte, error) { -- res := make(map[span.URI][]byte) -- for _, docEdits := range edits { -- if docEdits.TextDocumentEdit != nil { -- uri := docEdits.TextDocumentEdit.TextDocument.URI.SpanURI() -- var m *protocol.Mapper -- // If we have already edited this file, we use the edited version (rather than the -- // file in its original state) so that we preserve our initial changes. -- if content, ok := res[uri]; ok { -- m = protocol.NewMapper(uri, content) -- } else { -- var err error -- if m, err = r.data.Mapper(uri); err != nil { -- return nil, err -- } -- } -- patched, _, err := source.ApplyProtocolEdits(m, docEdits.TextDocumentEdit.Edits) -- if err != nil { -- return nil, err -- } -- res[uri] = patched +- // Always suggest TestMain, if possible +- if strings.HasPrefix("TestMain", name) { +- if hasParens { +- ans = append(ans, defItem("TestMain", obj)) +- } else { +- ans = append(ans, defItem("TestMain(m *testing.M)", obj)) - } - } -- return res, nil --} - --func (r *runner) AddImport(t *testing.T, uri span.URI, expectedImport string) { -- cmd, err := command.NewListKnownPackagesCommand("List Known Packages", command.URIArg{ -- URI: protocol.URIFromSpanURI(uri), -- }) -- if err != nil { -- t.Fatal(err) -- } -- resp, err := r.server.executeCommand(r.ctx, &protocol.ExecuteCommandParams{ -- Command: cmd.Command, -- Arguments: cmd.Arguments, -- }) -- if err != nil { -- t.Fatal(err) -- } -- res := resp.(command.ListKnownPackagesResult) -- var hasPkg bool -- for _, p := range res.Packages { -- if p == expectedImport { -- hasPkg = true -- break +- // If a snippet is possible, suggest it +- if strings.HasPrefix("Test", name) { +- if hasParens { +- ans = append(ans, defItem("Test", obj)) +- } else { +- ans = append(ans, defSnippet("Test", "(t *testing.T)", obj)) - } +- return ans, sel +- } else if strings.HasPrefix("Benchmark", name) { +- if hasParens { +- ans = append(ans, defItem("Benchmark", obj)) +- } else { +- ans = append(ans, defSnippet("Benchmark", "(b *testing.B)", obj)) +- } +- return ans, sel +- } else if strings.HasPrefix("Fuzz", name) { +- if hasParens { +- ans = append(ans, defItem("Fuzz", obj)) +- } else { +- ans = append(ans, defSnippet("Fuzz", "(f *testing.F)", obj)) +- } +- return ans, sel - } -- if !hasPkg { -- t.Fatalf("%s: got %v packages\nwant contains %q", command.ListKnownPackages, res.Packages, expectedImport) -- } -- cmd, err = command.NewAddImportCommand("Add Imports", command.AddImportArgs{ -- URI: protocol.URIFromSpanURI(uri), -- ImportPath: expectedImport, -- }) -- if err != nil { -- t.Fatal(err) -- } -- _, err = r.server.executeCommand(r.ctx, &protocol.ExecuteCommandParams{ -- Command: cmd.Command, -- Arguments: cmd.Arguments, -- }) -- if err != nil { -- t.Fatal(err) -- } -- got := (<-r.editRecv)[uri] -- want := r.data.Golden(t, "addimport", uri.Filename(), func() ([]byte, error) { -- return []byte(got), nil -- }) -- if want == nil { -- t.Fatalf("golden file %q not found", uri.Filename()) -- } -- if diff := compare.Bytes(want, got); diff != "" { -- t.Errorf("%s mismatch\n%s", command.AddImport, diff) +- +- // Fill in the argument for what the user has already typed +- if got := defMatches(name, "Test", path, "(t *testing.T)"); got != "" { +- ans = append(ans, defItem(got, obj)) +- } else if got := defMatches(name, "Benchmark", path, "(b *testing.B)"); got != "" { +- ans = append(ans, defItem(got, obj)) +- } else if got := defMatches(name, "Fuzz", path, "(f *testing.F)"); got != "" { +- ans = append(ans, defItem(got, obj)) - } +- return ans, sel -} - --func (r *runner) SelectionRanges(t *testing.T, spn span.Span) { -- uri := spn.URI() -- sm, err := r.data.Mapper(uri) -- if err != nil { -- t.Fatal(err) -- } -- loc, err := sm.SpanLocation(spn) -- if err != nil { -- t.Error(err) +-// defMatches returns text for defItem, never for defSnippet +-func defMatches(name, pat string, path []ast.Node, arg string) string { +- if !strings.HasPrefix(name, pat) { +- return "" - } -- -- ranges, err := r.server.selectionRange(r.ctx, &protocol.SelectionRangeParams{ -- TextDocument: protocol.TextDocumentIdentifier{ -- URI: protocol.URIFromSpanURI(uri), -- }, -- Positions: []protocol.Position{loc.Range.Start}, -- }) -- if err != nil { -- t.Fatal(err) +- c, _ := utf8.DecodeRuneInString(name[len(pat):]) +- if unicode.IsLower(c) { +- return "" - } -- -- sb := &strings.Builder{} -- for i, path := range ranges { -- fmt.Fprintf(sb, "Ranges %d: ", i) -- rng := path -- for { -- s, e, err := sm.RangeOffsets(rng.Range) -- if err != nil { -- t.Error(err) -- } -- -- var snippet string -- if e-s < 30 { -- snippet = string(sm.Content[s:e]) -- } else { -- snippet = string(sm.Content[s:s+15]) + "..." + string(sm.Content[e-15:e]) -- } -- -- fmt.Fprintf(sb, "\n\t%v %q", rng.Range, strings.ReplaceAll(snippet, "\n", "\\n")) -- -- if rng.Parent == nil { -- break -- } -- rng = *rng.Parent -- } -- sb.WriteRune('\n') +- fd, ok := path[1].(*ast.FuncDecl) +- if !ok { +- // we don't know what's going on +- return "" - } -- got := sb.String() -- -- testName := "selectionrange_" + tests.SpanName(spn) -- want := r.data.Golden(t, testName, uri.Filename(), func() ([]byte, error) { -- return []byte(got), nil -- }) -- if want == nil { -- t.Fatalf("golden file %q not found", uri.Filename()) +- fp := fd.Type.Params +- if len(fp.List) > 0 { +- // signature already there, nothing to suggest +- return "" - } -- if diff := compare.Text(got, string(want)); diff != "" { -- t.Errorf("%s mismatch\n%s", testName, diff) +- if fp.Opening != fp.Closing { +- // nothing: completion works on words, not easy to insert arg +- return "" - } +- // suggesting signature too +- return name + arg -} - --func (r *runner) collectDiagnostics(view *cache.View) { -- if r.diagnostics != nil { -- return -- } -- r.diagnostics = make(map[span.URI][]*source.Diagnostic) -- -- snapshot, release, err := view.Snapshot() -- if err != nil { -- panic(err) +-func defSnippet(prefix, suffix string, obj types.Object) CompletionItem { +- var sn snippet.Builder +- sn.WriteText(prefix) +- sn.WritePlaceholder(func(b *snippet.Builder) { b.WriteText("Xxx") }) +- sn.WriteText(suffix + " {\n\t") +- sn.WriteFinalTabstop() +- sn.WriteText("\n}") +- return CompletionItem{ +- Label: prefix + "Xxx" + suffix, +- Detail: "tab, type the rest of the name, then tab", +- Kind: protocol.FunctionCompletion, +- Depth: 0, +- Score: 10, +- snippet: &sn, +- Documentation: prefix + " test function", +- isSlice: isSlice(obj), - } -- defer release() -- -- // Always run diagnostics with analysis. -- r.server.diagnose(r.ctx, snapshot, analyzeEverything) -- for uri, reports := range r.server.diagnostics { -- for _, report := range reports.reports { -- for _, d := range report.diags { -- r.diagnostics[uri] = append(r.diagnostics[uri], d) -- } -- } +-} +-func defItem(val string, obj types.Object) CompletionItem { +- return CompletionItem{ +- Label: val, +- InsertText: val, +- Kind: protocol.FunctionCompletion, +- Depth: 0, +- Score: 9, // prefer the snippets when available +- Documentation: "complete the function name", +- isSlice: isSlice(obj), - } -} -diff -urN a/gopls/internal/lsp/mod/code_lens.go b/gopls/internal/lsp/mod/code_lens.go ---- a/gopls/internal/lsp/mod/code_lens.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/mod/code_lens.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,191 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/golang/completion/format.go b/gopls/internal/golang/completion/format.go +--- a/gopls/internal/golang/completion/format.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/completion/format.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,444 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package mod +-package completion - -import ( - "context" +- "errors" - "fmt" -- "os" -- "path/filepath" -- -- "golang.org/x/mod/modfile" -- "golang.org/x/tools/gopls/internal/lsp/command" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" +- "go/ast" +- "go/doc" +- "go/types" +- "strings" +- +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/golang/completion/snippet" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/internal/aliases" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/imports" -) - --// LensFuncs returns the supported lensFuncs for go.mod files. --func LensFuncs() map[command.Command]source.LensFunc { -- return map[command.Command]source.LensFunc{ -- command.UpgradeDependency: upgradeLenses, -- command.Tidy: tidyLens, -- command.Vendor: vendorLens, -- command.RunGovulncheck: vulncheckLenses, -- } --} +-var ( +- errNoMatch = errors.New("not a surrounding match") +- errLowScore = errors.New("not a high scoring candidate") +-) - --func upgradeLenses(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.CodeLens, error) { -- pm, err := snapshot.ParseMod(ctx, fh) -- if err != nil || pm.File == nil { -- return nil, err -- } -- uri := protocol.URIFromSpanURI(fh.URI()) -- reset, err := command.NewResetGoModDiagnosticsCommand("Reset go.mod diagnostics", command.ResetGoModDiagnosticsArgs{URIArg: command.URIArg{URI: uri}}) -- if err != nil { -- return nil, err -- } -- // Put the `Reset go.mod diagnostics` codelens on the module statement. -- modrng, err := moduleStmtRange(fh, pm) -- if err != nil { -- return nil, err +-// item formats a candidate to a CompletionItem. +-func (c *completer) item(ctx context.Context, cand candidate) (CompletionItem, error) { +- obj := cand.obj +- +- // if the object isn't a valid match against the surrounding, return early. +- matchScore := c.matcher.Score(cand.name) +- if matchScore <= 0 { +- return CompletionItem{}, errNoMatch - } -- lenses := []protocol.CodeLens{{Range: modrng, Command: &reset}} -- if len(pm.File.Require) == 0 { -- // Nothing to upgrade. -- return lenses, nil +- cand.score *= float64(matchScore) +- +- // Ignore deep candidates that won't be in the MaxDeepCompletions anyway. +- if len(cand.path) != 0 && !c.deepState.isHighScore(cand.score) { +- return CompletionItem{}, errLowScore - } -- var requires []string -- for _, req := range pm.File.Require { -- requires = append(requires, req.Mod.Path) +- +- // Handle builtin types separately. +- if obj.Parent() == types.Universe { +- return c.formatBuiltin(ctx, cand) - } -- checkUpgrade, err := command.NewCheckUpgradesCommand("Check for upgrades", command.CheckUpgradesArgs{ -- URI: uri, -- Modules: requires, -- }) -- if err != nil { -- return nil, err +- +- var ( +- label = cand.name +- detail = types.TypeString(obj.Type(), c.qf) +- insert = label +- kind = protocol.TextCompletion +- snip snippet.Builder +- protocolEdits []protocol.TextEdit +- ) +- if obj.Type() == nil { +- detail = "" - } -- upgradeTransitive, err := command.NewUpgradeDependencyCommand("Upgrade transitive dependencies", command.DependencyArgs{ -- URI: uri, -- AddRequire: false, -- GoCmdArgs: []string{"-d", "-u", "-t", "./..."}, -- }) -- if err != nil { -- return nil, err +- if isTypeName(obj) && c.wantTypeParams() { +- x := cand.obj.(*types.TypeName) +- if named, ok := aliases.Unalias(x.Type()).(*types.Named); ok { +- tp := named.TypeParams() +- label += golang.FormatTypeParams(tp) +- insert = label // maintain invariant above (label == insert) +- } - } -- upgradeDirect, err := command.NewUpgradeDependencyCommand("Upgrade direct dependencies", command.DependencyArgs{ -- URI: uri, -- AddRequire: false, -- GoCmdArgs: append([]string{"-d"}, requires...), -- }) -- if err != nil { -- return nil, err +- +- snip.WriteText(insert) +- +- switch obj := obj.(type) { +- case *types.TypeName: +- detail, kind = golang.FormatType(obj.Type(), c.qf) +- case *types.Const: +- kind = protocol.ConstantCompletion +- case *types.Var: +- if _, ok := obj.Type().(*types.Struct); ok { +- detail = "struct{...}" // for anonymous unaliased struct types +- } else if obj.IsField() { +- var err error +- detail, err = golang.FormatVarType(ctx, c.snapshot, c.pkg, obj, c.qf, c.mq) +- if err != nil { +- return CompletionItem{}, err +- } +- } +- if obj.IsField() { +- kind = protocol.FieldCompletion +- c.structFieldSnippet(cand, detail, &snip) +- } else { +- kind = protocol.VariableCompletion +- } +- if obj.Type() == nil { +- break +- } +- case *types.Func: +- if obj.Type().(*types.Signature).Recv() == nil { +- kind = protocol.FunctionCompletion +- } else { +- kind = protocol.MethodCompletion +- } +- case *types.PkgName: +- kind = protocol.ModuleCompletion +- detail = fmt.Sprintf("%q", obj.Imported().Path()) +- case *types.Label: +- kind = protocol.ConstantCompletion +- detail = "label" - } - -- // Put the upgrade code lenses above the first require block or statement. -- rng, err := firstRequireRange(fh, pm) -- if err != nil { -- return nil, err +- var prefix string +- for _, mod := range cand.mods { +- switch mod { +- case reference: +- prefix = "&" + prefix +- case dereference: +- prefix = "*" + prefix +- case chanRead: +- prefix = "<-" + prefix +- } - } - -- return append(lenses, []protocol.CodeLens{ -- {Range: rng, Command: &checkUpgrade}, -- {Range: rng, Command: &upgradeTransitive}, -- {Range: rng, Command: &upgradeDirect}, -- }...), nil --} +- var ( +- suffix string +- funcType = obj.Type() +- ) +-Suffixes: +- for _, mod := range cand.mods { +- switch mod { +- case invoke: +- if sig, ok := funcType.Underlying().(*types.Signature); ok { +- s, err := golang.NewSignature(ctx, c.snapshot, c.pkg, sig, nil, c.qf, c.mq) +- if err != nil { +- return CompletionItem{}, err +- } - --func tidyLens(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.CodeLens, error) { -- pm, err := snapshot.ParseMod(ctx, fh) -- if err != nil || pm.File == nil { -- return nil, err +- tparams := s.TypeParams() +- if len(tparams) > 0 { +- // Eliminate the suffix of type parameters that are +- // likely redundant because they can probably be +- // inferred from the argument types (#51783). +- // +- // We don't bother doing the reverse inference from +- // result types as result-only type parameters are +- // quite unusual. +- free := inferableTypeParams(sig) +- for i := sig.TypeParams().Len() - 1; i >= 0; i-- { +- tparam := sig.TypeParams().At(i) +- if !free[tparam] { +- break +- } +- tparams = tparams[:i] // eliminate +- } +- } +- +- c.functionCallSnippet("", tparams, s.Params(), &snip) +- if sig.Results().Len() == 1 { +- funcType = sig.Results().At(0).Type() +- } +- detail = "func" + s.Format() +- } +- +- if !c.opts.snippets { +- // Without snippets the candidate will not include "()". Don't +- // add further suffixes since they will be invalid. For +- // example, with snippets "foo()..." would become "foo..." +- // without snippets if we added the dotDotDot. +- break Suffixes +- } +- case takeSlice: +- suffix += "[:]" +- case takeDotDotDot: +- suffix += "..." +- case index: +- snip.WriteText("[") +- snip.WritePlaceholder(nil) +- snip.WriteText("]") +- } - } -- uri := protocol.URIFromSpanURI(fh.URI()) -- cmd, err := command.NewTidyCommand("Run go mod tidy", command.URIArgs{URIs: []protocol.DocumentURI{uri}}) -- if err != nil { -- return nil, err +- +- // If this candidate needs an additional import statement, +- // add the additional text edits needed. +- if cand.imp != nil { +- addlEdits, err := c.importEdits(cand.imp) +- +- if err != nil { +- return CompletionItem{}, err +- } +- +- protocolEdits = append(protocolEdits, addlEdits...) +- if kind != protocol.ModuleCompletion { +- if detail != "" { +- detail += " " +- } +- detail += fmt.Sprintf("(from %q)", cand.imp.importPath) +- } - } -- rng, err := moduleStmtRange(fh, pm) -- if err != nil { -- return nil, err +- +- if cand.convertTo != nil { +- typeName := types.TypeString(cand.convertTo, c.qf) +- +- switch t := cand.convertTo.(type) { +- // We need extra parens when casting to these types. For example, +- // we need "(*int)(foo)", not "*int(foo)". +- case *types.Pointer, *types.Signature: +- typeName = "(" + typeName + ")" +- case *types.Basic: +- // If the types are incompatible (as determined by typeMatches), then we +- // must need a conversion here. However, if the target type is untyped, +- // don't suggest converting to e.g. "untyped float" (golang/go#62141). +- if t.Info()&types.IsUntyped != 0 { +- typeName = types.TypeString(types.Default(cand.convertTo), c.qf) +- } +- } +- +- prefix = typeName + "(" + prefix +- suffix = ")" - } -- return []protocol.CodeLens{{ -- Range: rng, -- Command: &cmd, -- }}, nil --} - --func vendorLens(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.CodeLens, error) { -- pm, err := snapshot.ParseMod(ctx, fh) -- if err != nil || pm.File == nil { -- return nil, err +- if prefix != "" { +- // If we are in a selector, add an edit to place prefix before selector. +- if sel := enclosingSelector(c.path, c.pos); sel != nil { +- edits, err := c.editText(sel.Pos(), sel.Pos(), prefix) +- if err != nil { +- return CompletionItem{}, err +- } +- protocolEdits = append(protocolEdits, edits...) +- } else { +- // If there is no selector, just stick the prefix at the start. +- insert = prefix + insert +- snip.PrependText(prefix) +- } - } -- if len(pm.File.Require) == 0 { -- // Nothing to vendor. -- return nil, nil +- +- if suffix != "" { +- insert += suffix +- snip.WriteText(suffix) - } -- rng, err := moduleStmtRange(fh, pm) -- if err != nil { -- return nil, err +- +- detail = strings.TrimPrefix(detail, "untyped ") +- // override computed detail with provided detail, if something is provided. +- if cand.detail != "" { +- detail = cand.detail - } -- title := "Create vendor directory" -- uri := protocol.URIFromSpanURI(fh.URI()) -- cmd, err := command.NewVendorCommand(title, command.URIArg{URI: uri}) -- if err != nil { -- return nil, err +- item := CompletionItem{ +- Label: label, +- InsertText: insert, +- AdditionalTextEdits: protocolEdits, +- Detail: detail, +- Kind: kind, +- Score: cand.score, +- Depth: len(cand.path), +- snippet: &snip, +- isSlice: isSlice(obj), - } -- // Change the message depending on whether or not the module already has a -- // vendor directory. -- vendorDir := filepath.Join(filepath.Dir(fh.URI().Filename()), "vendor") -- if info, _ := os.Stat(vendorDir); info != nil && info.IsDir() { -- title = "Sync vendor directory" +- // If the user doesn't want documentation for completion items. +- if !c.opts.documentation { +- return item, nil - } -- return []protocol.CodeLens{{Range: rng, Command: &cmd}}, nil --} +- pos := safetoken.StartPosition(c.pkg.FileSet(), obj.Pos()) - --func moduleStmtRange(fh source.FileHandle, pm *source.ParsedModule) (protocol.Range, error) { -- if pm.File == nil || pm.File.Module == nil || pm.File.Module.Syntax == nil { -- return protocol.Range{}, fmt.Errorf("no module statement in %s", fh.URI()) +- // We ignore errors here, because some types, like "unsafe" or "error", +- // may not have valid positions that we can use to get documentation. +- if !pos.IsValid() { +- return item, nil - } -- syntax := pm.File.Module.Syntax -- return pm.Mapper.OffsetRange(syntax.Start.Byte, syntax.End.Byte) --} - --// firstRequireRange returns the range for the first "require" in the given --// go.mod file. This is either a require block or an individual require line. --func firstRequireRange(fh source.FileHandle, pm *source.ParsedModule) (protocol.Range, error) { -- if len(pm.File.Require) == 0 { -- return protocol.Range{}, fmt.Errorf("no requires in the file %s", fh.URI()) +- comment, err := golang.HoverDocForObject(ctx, c.snapshot, c.pkg.FileSet(), obj) +- if err != nil { +- event.Error(ctx, fmt.Sprintf("failed to find Hover for %q", obj.Name()), err) +- return item, nil - } -- var start, end modfile.Position -- for _, stmt := range pm.File.Syntax.Stmt { -- if b, ok := stmt.(*modfile.LineBlock); ok && len(b.Token) == 1 && b.Token[0] == "require" { -- start, end = b.Span() -- break +- if c.opts.fullDocumentation { +- item.Documentation = comment.Text() +- } else { +- item.Documentation = doc.Synopsis(comment.Text()) +- } +- // The desired pattern is `^// Deprecated`, but the prefix has been removed +- // TODO(rfindley): It doesn't look like this does the right thing for +- // multi-line comments. +- if strings.HasPrefix(comment.Text(), "Deprecated") { +- if c.snapshot.Options().CompletionTags { +- item.Tags = []protocol.CompletionItemTag{protocol.ComplDeprecated} +- } else if c.snapshot.Options().CompletionDeprecated { +- item.Deprecated = true - } - } - -- firstRequire := pm.File.Require[0].Syntax -- if start.Byte == 0 || firstRequire.Start.Byte < start.Byte { -- start, end = firstRequire.Start, firstRequire.End -- } -- return pm.Mapper.OffsetRange(start.Byte, end.Byte) +- return item, nil -} - --func vulncheckLenses(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.CodeLens, error) { -- pm, err := snapshot.ParseMod(ctx, fh) -- if err != nil || pm.File == nil { -- return nil, err -- } -- // Place the codelenses near the module statement. -- // A module may not have the require block, -- // but vulnerabilities can exist in standard libraries. -- uri := protocol.URIFromSpanURI(fh.URI()) -- rng, err := moduleStmtRange(fh, pm) -- if err != nil { -- return nil, err +-// importEdits produces the text edits necessary to add the given import to the current file. +-func (c *completer) importEdits(imp *importInfo) ([]protocol.TextEdit, error) { +- if imp == nil { +- return nil, nil - } - -- vulncheck, err := command.NewRunGovulncheckCommand("Run govulncheck", command.VulncheckArgs{ -- URI: uri, -- Pattern: "./...", -- }) +- pgf, err := c.pkg.File(protocol.URIFromPath(c.filename)) - if err != nil { - return nil, err - } -- return []protocol.CodeLens{ -- {Range: rng, Command: &vulncheck}, -- }, nil --} -diff -urN a/gopls/internal/lsp/mod/diagnostics.go b/gopls/internal/lsp/mod/diagnostics.go ---- a/gopls/internal/lsp/mod/diagnostics.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/mod/diagnostics.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,559 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --// Package mod provides core features related to go.mod file --// handling for use by Go editors and tools. --package mod -- --import ( -- "context" -- "fmt" -- "runtime" -- "sort" -- "strings" -- "sync" - -- "golang.org/x/mod/modfile" -- "golang.org/x/mod/semver" -- "golang.org/x/sync/errgroup" -- "golang.org/x/tools/gopls/internal/lsp/command" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/gopls/internal/vulncheck/govulncheck" -- "golang.org/x/tools/internal/event" --) -- --// Diagnostics returns diagnostics from parsing the modules in the workspace. --func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[span.URI][]*source.Diagnostic, error) { -- ctx, done := event.Start(ctx, "mod.Diagnostics", source.SnapshotLabels(snapshot)...) -- defer done() -- -- return collectDiagnostics(ctx, snapshot, ModParseDiagnostics) --} -- --// Diagnostics returns diagnostics from running go mod tidy. --func TidyDiagnostics(ctx context.Context, snapshot source.Snapshot) (map[span.URI][]*source.Diagnostic, error) { -- ctx, done := event.Start(ctx, "mod.Diagnostics", source.SnapshotLabels(snapshot)...) -- defer done() -- -- return collectDiagnostics(ctx, snapshot, ModTidyDiagnostics) +- return golang.ComputeOneImportFixEdits(c.snapshot, pgf, &imports.ImportFix{ +- StmtInfo: imports.ImportInfo{ +- ImportPath: imp.importPath, +- Name: imp.name, +- }, +- // IdentName is unused on this path and is difficult to get. +- FixType: imports.AddImport, +- }) -} - --// UpgradeDiagnostics returns upgrade diagnostics for the modules in the --// workspace with known upgrades. --func UpgradeDiagnostics(ctx context.Context, snapshot source.Snapshot) (map[span.URI][]*source.Diagnostic, error) { -- ctx, done := event.Start(ctx, "mod.UpgradeDiagnostics", source.SnapshotLabels(snapshot)...) -- defer done() -- -- return collectDiagnostics(ctx, snapshot, ModUpgradeDiagnostics) +-func (c *completer) formatBuiltin(ctx context.Context, cand candidate) (CompletionItem, error) { +- obj := cand.obj +- item := CompletionItem{ +- Label: obj.Name(), +- InsertText: obj.Name(), +- Score: cand.score, +- } +- switch obj.(type) { +- case *types.Const: +- item.Kind = protocol.ConstantCompletion +- case *types.Builtin: +- item.Kind = protocol.FunctionCompletion +- sig, err := golang.NewBuiltinSignature(ctx, c.snapshot, obj.Name()) +- if err != nil { +- return CompletionItem{}, err +- } +- item.Detail = "func" + sig.Format() +- item.snippet = &snippet.Builder{} +- // The signature inferred for a built-in is instantiated, so TypeParams=∅. +- c.functionCallSnippet(obj.Name(), sig.TypeParams(), sig.Params(), item.snippet) +- case *types.TypeName: +- if types.IsInterface(obj.Type()) { +- item.Kind = protocol.InterfaceCompletion +- } else { +- item.Kind = protocol.ClassCompletion +- } +- case *types.Nil: +- item.Kind = protocol.VariableCompletion +- } +- return item, nil -} - --// VulnerabilityDiagnostics returns vulnerability diagnostics for the active modules in the --// workspace with known vulnerabilities. --func VulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot) (map[span.URI][]*source.Diagnostic, error) { -- ctx, done := event.Start(ctx, "mod.VulnerabilityDiagnostics", source.SnapshotLabels(snapshot)...) -- defer done() -- -- return collectDiagnostics(ctx, snapshot, ModVulnerabilityDiagnostics) +-// decide if the type params (if any) should be part of the completion +-// which only possible for types.Named and types.Signature +-// (so far, only in receivers, e.g.; func (s *GENERIC[K, V])..., which is a types.Named) +-func (c *completer) wantTypeParams() bool { +- // Need to be lexically in a receiver, and a child of an IndexListExpr +- // (but IndexListExpr only exists with go1.18) +- start := c.path[0].Pos() +- for i, nd := range c.path { +- if fd, ok := nd.(*ast.FuncDecl); ok { +- if i > 0 && fd.Recv != nil && start < fd.Recv.End() { +- return true +- } else { +- return false +- } +- } +- } +- return false -} - --func collectDiagnostics(ctx context.Context, snapshot source.Snapshot, diagFn func(context.Context, source.Snapshot, source.FileHandle) ([]*source.Diagnostic, error)) (map[span.URI][]*source.Diagnostic, error) { -- g, ctx := errgroup.WithContext(ctx) -- cpulimit := runtime.GOMAXPROCS(0) -- g.SetLimit(cpulimit) -- -- var mu sync.Mutex -- reports := make(map[span.URI][]*source.Diagnostic) +-// inferableTypeParams returns the set of type parameters +-// of sig that are constrained by (inferred from) the argument types. +-func inferableTypeParams(sig *types.Signature) map[*types.TypeParam]bool { +- free := make(map[*types.TypeParam]bool) - -- for _, uri := range snapshot.ModFiles() { -- uri := uri -- g.Go(func() error { -- fh, err := snapshot.ReadFile(ctx, uri) -- if err != nil { -- return err +- // visit adds to free all the free type parameters of t. +- var visit func(t types.Type) +- visit = func(t types.Type) { +- switch t := t.(type) { +- case *types.Array: +- visit(t.Elem()) +- case *types.Chan: +- visit(t.Elem()) +- case *types.Map: +- visit(t.Key()) +- visit(t.Elem()) +- case *types.Pointer: +- visit(t.Elem()) +- case *types.Slice: +- visit(t.Elem()) +- case *types.Interface: +- for i := 0; i < t.NumExplicitMethods(); i++ { +- visit(t.ExplicitMethod(i).Type()) - } -- diagnostics, err := diagFn(ctx, snapshot, fh) -- if err != nil { -- return err +- for i := 0; i < t.NumEmbeddeds(); i++ { +- visit(t.EmbeddedType(i)) - } -- for _, d := range diagnostics { -- mu.Lock() -- reports[d.URI] = append(reports[fh.URI()], d) -- mu.Unlock() +- case *types.Union: +- for i := 0; i < t.Len(); i++ { +- visit(t.Term(i).Type()) - } -- return nil -- }) +- case *types.Signature: +- if tp := t.TypeParams(); tp != nil { +- // Generic signatures only appear as the type of generic +- // function declarations, so this isn't really reachable. +- for i := 0; i < tp.Len(); i++ { +- visit(tp.At(i).Constraint()) +- } +- } +- visit(t.Params()) +- visit(t.Results()) +- case *types.Tuple: +- for i := 0; i < t.Len(); i++ { +- visit(t.At(i).Type()) +- } +- case *types.Struct: +- for i := 0; i < t.NumFields(); i++ { +- visit(t.Field(i).Type()) +- } +- case *types.TypeParam: +- free[t] = true +- case *aliases.Alias: +- visit(aliases.Unalias(t)) +- case *types.Named: +- targs := t.TypeArgs() +- for i := 0; i < targs.Len(); i++ { +- visit(targs.At(i)) +- } +- case *types.Basic: +- // nop +- default: +- panic(t) +- } - } - -- if err := g.Wait(); err != nil { -- return nil, err -- } -- return reports, nil --} +- visit(sig.Params()) - --// ModParseDiagnostics reports diagnostics from parsing the mod file. --func ModParseDiagnostics(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) (diagnostics []*source.Diagnostic, err error) { -- pm, err := snapshot.ParseMod(ctx, fh) -- if err != nil { -- if pm == nil || len(pm.ParseErrors) == 0 { -- return nil, err +- // Perform induction through constraints. +-restart: +- for i := 0; i < sig.TypeParams().Len(); i++ { +- tp := sig.TypeParams().At(i) +- if free[tp] { +- n := len(free) +- visit(tp.Constraint()) +- if len(free) > n { +- goto restart // iterate until fixed point +- } - } -- return pm.ParseErrors, nil - } -- return nil, nil +- return free -} +diff -urN a/gopls/internal/golang/completion/fuzz.go b/gopls/internal/golang/completion/fuzz.go +--- a/gopls/internal/golang/completion/fuzz.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/completion/fuzz.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,141 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// ModTidyDiagnostics reports diagnostics from running go mod tidy. --func ModTidyDiagnostics(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) (diagnostics []*source.Diagnostic, err error) { -- pm, err := snapshot.ParseMod(ctx, fh) // memoized -- if err != nil { -- return nil, nil // errors reported by ModDiagnostics above -- } +-package completion - -- tidied, err := snapshot.ModTidy(ctx, pm) -- if err != nil && !source.IsNonFatalGoModError(err) { -- event.Error(ctx, fmt.Sprintf("tidy: diagnosing %s", pm.URI), err) -- } -- if err == nil { -- for _, d := range tidied.Diagnostics { -- if d.URI != fh.URI() { +-import ( +- "fmt" +- "go/ast" +- "go/types" +- "strings" +- +- "golang.org/x/tools/gopls/internal/protocol" +-) +- +-// golang/go#51089 +-// *testing.F deserves special treatment as member use is constrained: +-// The arguments to f.Fuzz are determined by the arguments to a previous f.Add +-// Inside f.Fuzz only f.Failed and f.Name are allowed. +-// PJW: are there other packages where we can deduce usage constraints? +- +-// if we find fuzz completions, then return true, as those are the only completions to offer +-func (c *completer) fuzz(mset *types.MethodSet, imp *importInfo, cb func(candidate)) bool { +- // 1. inside f.Fuzz? (only f.Failed and f.Name) +- // 2. possible completing f.Fuzz? +- // [Ident,SelectorExpr,Callexpr,ExprStmt,BlockiStmt,FuncDecl(Fuzz...)] +- // 3. before f.Fuzz, same (for 2., offer choice when looking at an F) +- +- // does the path contain FuncLit as arg to f.Fuzz CallExpr? +- inside := false +-Loop: +- for i, n := range c.path { +- switch v := n.(type) { +- case *ast.CallExpr: +- if len(v.Args) != 1 { +- continue Loop +- } +- if _, ok := v.Args[0].(*ast.FuncLit); !ok { - continue - } -- diagnostics = append(diagnostics, d) +- if s, ok := v.Fun.(*ast.SelectorExpr); !ok || s.Sel.Name != "Fuzz" { +- continue +- } +- if i > 2 { // avoid t.Fuzz itself in tests +- inside = true +- break Loop +- } - } - } -- return diagnostics, nil --} -- --// ModUpgradeDiagnostics adds upgrade quick fixes for individual modules if the upgrades --// are recorded in the view. --func ModUpgradeDiagnostics(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) (upgradeDiagnostics []*source.Diagnostic, err error) { -- pm, err := snapshot.ParseMod(ctx, fh) -- if err != nil { -- // Don't return an error if there are parse error diagnostics to be shown, but also do not -- // continue since we won't be able to show the upgrade diagnostics. -- if pm != nil && len(pm.ParseErrors) != 0 { -- return nil, nil +- if inside { +- for i := 0; i < mset.Len(); i++ { +- o := mset.At(i).Obj() +- if o.Name() == "Failed" || o.Name() == "Name" { +- cb(candidate{ +- obj: o, +- score: stdScore, +- imp: imp, +- addressable: true, +- }) +- } - } -- return nil, err +- return true - } +- // if it could be t.Fuzz, look for the preceding t.Add +- id, ok := c.path[0].(*ast.Ident) +- if ok && strings.HasPrefix("Fuzz", id.Name) { +- var add *ast.CallExpr +- f := func(n ast.Node) bool { +- if n == nil { +- return true +- } +- call, ok := n.(*ast.CallExpr) +- if !ok { +- return true +- } +- s, ok := call.Fun.(*ast.SelectorExpr) +- if !ok { +- return true +- } +- if s.Sel.Name != "Add" { +- return true +- } +- // Sel.X should be of type *testing.F +- got := c.pkg.TypesInfo().Types[s.X] +- if got.Type.String() == "*testing.F" { +- add = call +- } +- return false // because we're done... +- } +- // look at the enclosing FuzzFoo functions +- if len(c.path) < 2 { +- return false +- } +- n := c.path[len(c.path)-2] +- if _, ok := n.(*ast.FuncDecl); !ok { +- // the path should start with ast.File, ast.FuncDecl, ... +- // but it didn't, so give up +- return false +- } +- ast.Inspect(n, f) +- if add == nil { +- // looks like f.Fuzz without a preceding f.Add. +- // let the regular completion handle it. +- return false +- } - -- upgrades := snapshot.View().ModuleUpgrades(fh.URI()) -- for _, req := range pm.File.Require { -- ver, ok := upgrades[req.Mod.Path] -- if !ok || req.Mod.Version == ver { -- continue +- lbl := "Fuzz(func(t *testing.T" +- for i, a := range add.Args { +- info := c.pkg.TypesInfo().TypeOf(a) +- if info == nil { +- return false // How could this happen, but better safe than panic. +- } +- lbl += fmt.Sprintf(", %c %s", 'a'+i, info) - } -- rng, err := pm.Mapper.OffsetRange(req.Syntax.Start.Byte, req.Syntax.End.Byte) -- if err != nil { -- return nil, err +- lbl += ")" +- xx := CompletionItem{ +- Label: lbl, +- InsertText: lbl, +- Kind: protocol.FunctionCompletion, +- Depth: 0, +- Score: 10, // pretty confident the user should see this +- Documentation: "argument types from f.Add", +- isSlice: false, - } -- // Upgrade to the exact version we offer the user, not the most recent. -- title := fmt.Sprintf("%s%v", upgradeCodeActionPrefix, ver) -- cmd, err := command.NewUpgradeDependencyCommand(title, command.DependencyArgs{ -- URI: protocol.URIFromSpanURI(fh.URI()), -- AddRequire: false, -- GoCmdArgs: []string{req.Mod.Path + "@" + ver}, -- }) -- if err != nil { -- return nil, err +- c.items = append(c.items, xx) +- for i := 0; i < mset.Len(); i++ { +- o := mset.At(i).Obj() +- if o.Name() != "Fuzz" { +- cb(candidate{ +- obj: o, +- score: stdScore, +- imp: imp, +- addressable: true, +- }) +- } - } -- upgradeDiagnostics = append(upgradeDiagnostics, &source.Diagnostic{ -- URI: fh.URI(), -- Range: rng, -- Severity: protocol.SeverityInformation, -- Source: source.UpgradeNotification, -- Message: fmt.Sprintf("%v can be upgraded", req.Mod.Path), -- SuggestedFixes: []source.SuggestedFix{source.SuggestedFixFromCommand(cmd, protocol.QuickFix)}, -- }) +- return true // done - } -- -- return upgradeDiagnostics, nil +- // let the standard processing take care of it instead +- return false -} +diff -urN a/gopls/internal/golang/completion/keywords.go b/gopls/internal/golang/completion/keywords.go +--- a/gopls/internal/golang/completion/keywords.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/completion/keywords.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,154 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --const upgradeCodeActionPrefix = "Upgrade to " +-package completion - --// ModVulnerabilityDiagnostics adds diagnostics for vulnerabilities in individual modules --// if the vulnerability is recorded in the view. --func ModVulnerabilityDiagnostics(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) (vulnDiagnostics []*source.Diagnostic, err error) { -- pm, err := snapshot.ParseMod(ctx, fh) -- if err != nil { -- // Don't return an error if there are parse error diagnostics to be shown, but also do not -- // continue since we won't be able to show the vulnerability diagnostics. -- if pm != nil && len(pm.ParseErrors) != 0 { -- return nil, nil -- } -- return nil, err -- } +-import ( +- "go/ast" - -- diagSource := source.Govulncheck -- vs := snapshot.View().Vulnerabilities(fh.URI())[fh.URI()] -- if vs == nil && snapshot.Options().Vulncheck == source.ModeVulncheckImports { -- vs, err = snapshot.ModVuln(ctx, fh.URI()) -- if err != nil { -- return nil, err +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/astutil" +-) +- +-const ( +- BREAK = "break" +- CASE = "case" +- CHAN = "chan" +- CONST = "const" +- CONTINUE = "continue" +- DEFAULT = "default" +- DEFER = "defer" +- ELSE = "else" +- FALLTHROUGH = "fallthrough" +- FOR = "for" +- FUNC = "func" +- GO = "go" +- GOTO = "goto" +- IF = "if" +- IMPORT = "import" +- INTERFACE = "interface" +- MAP = "map" +- PACKAGE = "package" +- RANGE = "range" +- RETURN = "return" +- SELECT = "select" +- STRUCT = "struct" +- SWITCH = "switch" +- TYPE = "type" +- VAR = "var" +-) +- +-// addKeywordCompletions offers keyword candidates appropriate at the position. +-func (c *completer) addKeywordCompletions() { +- seen := make(map[string]bool) +- +- if c.wantTypeName() && c.inference.objType == nil { +- // If we want a type name but don't have an expected obj type, +- // include "interface", "struct", "func", "chan", and "map". +- +- // "interface" and "struct" are more common declaring named types. +- // Give them a higher score if we are in a type declaration. +- structIntf, funcChanMap := stdScore, highScore +- if len(c.path) > 1 { +- if _, namedDecl := c.path[1].(*ast.TypeSpec); namedDecl { +- structIntf, funcChanMap = highScore, stdScore +- } - } -- diagSource = source.Vulncheck -- } -- if vs == nil || len(vs.Findings) == 0 { -- return nil, nil +- +- c.addKeywordItems(seen, structIntf, STRUCT, INTERFACE) +- c.addKeywordItems(seen, funcChanMap, FUNC, CHAN, MAP) - } - -- suggestRunOrResetGovulncheck, err := suggestGovulncheckAction(diagSource == source.Govulncheck, fh.URI()) -- if err != nil { -- // must not happen -- return nil, err // TODO: bug report +- // If we are at the file scope, only offer decl keywords. We don't +- // get *ast.Idents at the file scope because non-keyword identifiers +- // turn into *ast.BadDecl, not *ast.Ident. +- if len(c.path) == 1 || is[*ast.File](c.path[1]) { +- c.addKeywordItems(seen, stdScore, TYPE, CONST, VAR, FUNC, IMPORT) +- return +- } else if _, ok := c.path[0].(*ast.Ident); !ok { +- // Otherwise only offer keywords if the client is completing an identifier. +- return - } -- vulnsByModule := make(map[string][]*govulncheck.Finding) - -- for _, finding := range vs.Findings { -- if vuln, typ := foundVuln(finding); typ == vulnCalled || typ == vulnImported { -- vulnsByModule[vuln.Module] = append(vulnsByModule[vuln.Module], finding) +- if len(c.path) > 2 { +- // Offer "range" if we are in ast.ForStmt.Init. This is what the +- // AST looks like before "range" is typed, e.g. "for i := r<>". +- if loop, ok := c.path[2].(*ast.ForStmt); ok && loop.Init != nil && astutil.NodeContains(loop.Init, c.pos) { +- c.addKeywordItems(seen, stdScore, RANGE) - } - } -- for _, req := range pm.File.Require { -- mod := req.Mod.Path -- findings := vulnsByModule[mod] -- if len(findings) == 0 { -- continue -- } -- // note: req.Syntax is the line corresponding to 'require', which means -- // req.Syntax.Start can point to the beginning of the "require" keyword -- // for a single line require (e.g. "require golang.org/x/mod v0.0.0"). -- start := req.Syntax.Start.Byte -- if len(req.Syntax.Token) == 3 { -- start += len("require ") +- +- // Only suggest keywords if we are beginning a statement. +- switch n := c.path[1].(type) { +- case *ast.BlockStmt, *ast.ExprStmt: +- // OK - our ident must be at beginning of statement. +- case *ast.CommClause: +- // Make sure we aren't in the Comm statement. +- if !n.Colon.IsValid() || c.pos <= n.Colon { +- return - } -- rng, err := pm.Mapper.OffsetRange(start, req.Syntax.End.Byte) -- if err != nil { -- return nil, err +- case *ast.CaseClause: +- // Make sure we aren't in the case List. +- if !n.Colon.IsValid() || c.pos <= n.Colon { +- return - } -- // Map affecting vulns to 'warning' level diagnostics, -- // others to 'info' level diagnostics. -- // Fixes will include only the upgrades for warning level diagnostics. -- var warningFixes, infoFixes []source.SuggestedFix -- var warningSet, infoSet = map[string]bool{}, map[string]bool{} -- for _, finding := range findings { -- // It is possible that the source code was changed since the last -- // govulncheck run and information in the `vulns` info is stale. -- // For example, imagine that a user is in the middle of updating -- // problematic modules detected by the govulncheck run by applying -- // quick fixes. Stale diagnostics can be confusing and prevent the -- // user from quickly locating the next module to fix. -- // Ideally we should rerun the analysis with the updated module -- // dependencies or any other code changes, but we are not yet -- // in the position of automatically triggering the analysis -- // (govulncheck can take a while). We also don't know exactly what -- // part of source code was changed since `vulns` was computed. -- // As a heuristic, we assume that a user upgrades the affecting -- // module to the version with the fix or the latest one, and if the -- // version in the require statement is equal to or higher than the -- // fixed version, skip generating a diagnostic about the vulnerability. -- // Eventually, the user has to rerun govulncheck. -- if finding.FixedVersion != "" && semver.IsValid(req.Mod.Version) && semver.Compare(finding.FixedVersion, req.Mod.Version) <= 0 { -- continue -- } -- switch _, typ := foundVuln(finding); typ { -- case vulnImported: -- infoSet[finding.OSV] = true -- case vulnCalled: -- warningSet[finding.OSV] = true -- } -- // Upgrade to the exact version we offer the user, not the most recent. -- if fixedVersion := finding.FixedVersion; semver.IsValid(fixedVersion) && semver.Compare(req.Mod.Version, fixedVersion) < 0 { -- cmd, err := getUpgradeCodeAction(fh, req, fixedVersion) -- if err != nil { -- return nil, err // TODO: bug report +- default: +- return +- } +- +- // Filter out keywords depending on scope +- // Skip the first one because we want to look at the enclosing scopes +- path := c.path[1:] +- for i, n := range path { +- switch node := n.(type) { +- case *ast.CaseClause: +- // only recommend "fallthrough" and "break" within the bodies of a case clause +- if c.pos > node.Colon { +- c.addKeywordItems(seen, stdScore, BREAK) +- // "fallthrough" is only valid in switch statements. +- // A case clause is always nested within a block statement in a switch statement, +- // that block statement is nested within either a TypeSwitchStmt or a SwitchStmt. +- if i+2 >= len(path) { +- continue - } -- sf := source.SuggestedFixFromCommand(cmd, protocol.QuickFix) -- switch _, typ := foundVuln(finding); typ { -- case vulnImported: -- infoFixes = append(infoFixes, sf) -- case vulnCalled: -- warningFixes = append(warningFixes, sf) +- if _, ok := path[i+2].(*ast.SwitchStmt); ok { +- c.addKeywordItems(seen, stdScore, FALLTHROUGH) - } - } -- } -- -- if len(warningSet) == 0 && len(infoSet) == 0 { -- continue -- } -- // Remove affecting osvs from the non-affecting osv list if any. -- if len(warningSet) > 0 { -- for k := range infoSet { -- if warningSet[k] { -- delete(infoSet, k) -- } +- case *ast.CommClause: +- if c.pos > node.Colon { +- c.addKeywordItems(seen, stdScore, BREAK) +- } +- case *ast.TypeSwitchStmt, *ast.SelectStmt, *ast.SwitchStmt: +- c.addKeywordItems(seen, stdScore, CASE, DEFAULT) +- case *ast.ForStmt, *ast.RangeStmt: +- c.addKeywordItems(seen, stdScore, BREAK, CONTINUE) +- // This is a bit weak, functions allow for many keywords +- case *ast.FuncDecl: +- if node.Body != nil && c.pos > node.Body.Lbrace { +- c.addKeywordItems(seen, stdScore, DEFER, RETURN, FOR, GO, SWITCH, SELECT, IF, ELSE, VAR, CONST, GOTO, TYPE) - } -- } -- // Add an upgrade for module@latest. -- // TODO(suzmue): verify if latest is the same as fixedVersion. -- latest, err := getUpgradeCodeAction(fh, req, "latest") -- if err != nil { -- return nil, err // TODO: bug report -- } -- sf := source.SuggestedFixFromCommand(latest, protocol.QuickFix) -- if len(warningFixes) > 0 { -- warningFixes = append(warningFixes, sf) -- } -- if len(infoFixes) > 0 { -- infoFixes = append(infoFixes, sf) -- } -- if len(warningSet) > 0 { -- warning := sortedKeys(warningSet) -- warningFixes = append(warningFixes, suggestRunOrResetGovulncheck) -- vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{ -- URI: fh.URI(), -- Range: rng, -- Severity: protocol.SeverityWarning, -- Source: diagSource, -- Message: getVulnMessage(req.Mod.Path, warning, true, diagSource == source.Govulncheck), -- SuggestedFixes: warningFixes, -- }) -- } -- if len(infoSet) > 0 { -- info := sortedKeys(infoSet) -- infoFixes = append(infoFixes, suggestRunOrResetGovulncheck) -- vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{ -- URI: fh.URI(), -- Range: rng, -- Severity: protocol.SeverityInformation, -- Source: diagSource, -- Message: getVulnMessage(req.Mod.Path, info, false, diagSource == source.Govulncheck), -- SuggestedFixes: infoFixes, -- }) - } - } +-} - -- // TODO(hyangah): place this diagnostic on the `go` directive or `toolchain` directive -- // after https://go.dev/issue/57001. -- const diagnoseStdLib = false -- -- // If diagnosing the stdlib, add standard library vulnerability diagnostics -- // on the module declaration. -- // -- // Only proceed if we have a valid module declaration on which to position -- // the diagnostics. -- if diagnoseStdLib && pm.File.Module != nil && pm.File.Module.Syntax != nil { -- // Add standard library vulnerabilities. -- stdlibVulns := vulnsByModule["stdlib"] -- if len(stdlibVulns) == 0 { -- return vulnDiagnostics, nil -- } -- -- // Put the standard library diagnostic on the module declaration. -- rng, err := pm.Mapper.OffsetRange(pm.File.Module.Syntax.Start.Byte, pm.File.Module.Syntax.End.Byte) -- if err != nil { -- return vulnDiagnostics, nil // TODO: bug report -- } -- -- var warningSet, infoSet = map[string]bool{}, map[string]bool{} -- for _, finding := range stdlibVulns { -- switch _, typ := foundVuln(finding); typ { -- case vulnImported: -- infoSet[finding.OSV] = true -- case vulnCalled: -- warningSet[finding.OSV] = true -- } +-// addKeywordItems dedupes and adds completion items for the specified +-// keywords with the specified score. +-func (c *completer) addKeywordItems(seen map[string]bool, score float64, kws ...string) { +- for _, kw := range kws { +- if seen[kw] { +- continue - } -- if len(warningSet) > 0 { -- warning := sortedKeys(warningSet) -- fixes := []source.SuggestedFix{suggestRunOrResetGovulncheck} -- vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{ -- URI: fh.URI(), -- Range: rng, -- Severity: protocol.SeverityWarning, -- Source: diagSource, -- Message: getVulnMessage("go", warning, true, diagSource == source.Govulncheck), -- SuggestedFixes: fixes, -- }) +- seen[kw] = true - -- // remove affecting osvs from the non-affecting osv list if any. -- for k := range infoSet { -- if warningSet[k] { -- delete(infoSet, k) -- } -- } -- } -- if len(infoSet) > 0 { -- info := sortedKeys(infoSet) -- fixes := []source.SuggestedFix{suggestRunOrResetGovulncheck} -- vulnDiagnostics = append(vulnDiagnostics, &source.Diagnostic{ -- URI: fh.URI(), -- Range: rng, -- Severity: protocol.SeverityInformation, -- Source: diagSource, -- Message: getVulnMessage("go", info, false, diagSource == source.Govulncheck), -- SuggestedFixes: fixes, +- if matchScore := c.matcher.Score(kw); matchScore > 0 { +- c.items = append(c.items, CompletionItem{ +- Label: kw, +- Kind: protocol.KeywordCompletion, +- InsertText: kw, +- Score: score * float64(matchScore), - }) - } - } -- -- return vulnDiagnostics, nil -} +diff -urN a/gopls/internal/golang/completion/labels.go b/gopls/internal/golang/completion/labels.go +--- a/gopls/internal/golang/completion/labels.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/completion/labels.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,112 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --type vulnFindingType int +-package completion +- +-import ( +- "go/ast" +- "go/token" +- "math" +-) +- +-type labelType int - -const ( -- vulnUnknown vulnFindingType = iota -- vulnCalled -- vulnImported -- vulnRequired +- labelNone labelType = iota +- labelBreak +- labelContinue +- labelGoto -) - --// foundVuln returns the frame info describing discovered vulnerable symbol/package/module --// and how this vulnerability affects the analyzed package or module. --func foundVuln(finding *govulncheck.Finding) (*govulncheck.Frame, vulnFindingType) { -- // finding.Trace is sorted from the imported vulnerable symbol to -- // the entry point in the callstack. -- // If Function is set, then Package must be set. Module will always be set. -- // If Function is set it was found in the call graph, otherwise if Package is set -- // it was found in the import graph, otherwise it was found in the require graph. -- // See the documentation of govulncheck.Finding. -- if len(finding.Trace) == 0 { // this shouldn't happen, but just in case... -- return nil, vulnUnknown -- } -- vuln := finding.Trace[0] -- if vuln.Package == "" { -- return vuln, vulnRequired -- } -- if vuln.Function == "" { -- return vuln, vulnImported +-// wantLabelCompletion returns true if we want (only) label +-// completions at the position. +-func (c *completer) wantLabelCompletion() labelType { +- if _, ok := c.path[0].(*ast.Ident); ok && len(c.path) > 1 { +- // We want a label if we are an *ast.Ident child of a statement +- // that accepts a label, e.g. "break Lo<>". +- return takesLabel(c.path[1]) - } -- return vuln, vulnCalled +- +- return labelNone -} - --func sortedKeys(m map[string]bool) []string { -- ret := make([]string, 0, len(m)) -- for k := range m { -- ret = append(ret, k) -- } -- sort.Strings(ret) -- return ret --} -- --// suggestGovulncheckAction returns a code action that suggests either run govulncheck --// for more accurate investigation (if the present vulncheck diagnostics are based on --// analysis less accurate than govulncheck) or reset the existing govulncheck result --// (if the present vulncheck diagnostics are already based on govulncheck run). --func suggestGovulncheckAction(fromGovulncheck bool, uri span.URI) (source.SuggestedFix, error) { -- if fromGovulncheck { -- resetVulncheck, err := command.NewResetGoModDiagnosticsCommand("Reset govulncheck result", command.ResetGoModDiagnosticsArgs{ -- URIArg: command.URIArg{URI: protocol.DocumentURI(uri)}, -- DiagnosticSource: string(source.Govulncheck), -- }) -- if err != nil { -- return source.SuggestedFix{}, err -- } -- return source.SuggestedFixFromCommand(resetVulncheck, protocol.QuickFix), nil -- } -- vulncheck, err := command.NewRunGovulncheckCommand("Run govulncheck to verify", command.VulncheckArgs{ -- URI: protocol.DocumentURI(uri), -- Pattern: "./...", -- }) -- if err != nil { -- return source.SuggestedFix{}, err -- } -- return source.SuggestedFixFromCommand(vulncheck, protocol.QuickFix), nil --} -- --func getVulnMessage(mod string, vulns []string, used, fromGovulncheck bool) string { -- var b strings.Builder -- if used { -- switch len(vulns) { -- case 1: -- fmt.Fprintf(&b, "%v has a vulnerability used in the code: %v.", mod, vulns[0]) -- default: -- fmt.Fprintf(&b, "%v has vulnerabilities used in the code: %v.", mod, strings.Join(vulns, ", ")) -- } -- } else { -- if fromGovulncheck { -- switch len(vulns) { -- case 1: -- fmt.Fprintf(&b, "%v has a vulnerability %v that is not used in the code.", mod, vulns[0]) -- default: -- fmt.Fprintf(&b, "%v has known vulnerabilities %v that are not used in the code.", mod, strings.Join(vulns, ", ")) -- } -- } else { -- switch len(vulns) { -- case 1: -- fmt.Fprintf(&b, "%v has a vulnerability %v.", mod, vulns[0]) -- default: -- fmt.Fprintf(&b, "%v has known vulnerabilities %v.", mod, strings.Join(vulns, ", ")) -- } +-// takesLabel returns the corresponding labelType if n is a statement +-// that accepts a label, otherwise labelNone. +-func takesLabel(n ast.Node) labelType { +- if bs, ok := n.(*ast.BranchStmt); ok { +- switch bs.Tok { +- case token.BREAK: +- return labelBreak +- case token.CONTINUE: +- return labelContinue +- case token.GOTO: +- return labelGoto - } - } -- return b.String() --} -- --// href returns the url for the vulnerability information. --// Eventually we should retrieve the url embedded in the osv.Entry. --// While vuln.go.dev is under development, this always returns --// the page in pkg.go.dev. --func href(vulnID string) string { -- return fmt.Sprintf("https://pkg.go.dev/vuln/%s", vulnID) +- return labelNone -} - --func getUpgradeCodeAction(fh source.FileHandle, req *modfile.Require, version string) (protocol.Command, error) { -- cmd, err := command.NewUpgradeDependencyCommand(upgradeTitle(version), command.DependencyArgs{ -- URI: protocol.URIFromSpanURI(fh.URI()), -- AddRequire: false, -- GoCmdArgs: []string{req.Mod.Path + "@" + version}, -- }) -- if err != nil { -- return protocol.Command{}, err +-// labels adds completion items for labels defined in the enclosing +-// function. +-func (c *completer) labels(lt labelType) { +- if c.enclosingFunc == nil { +- return - } -- return cmd, nil --} - --func upgradeTitle(fixedVersion string) string { -- title := fmt.Sprintf("%s%v", upgradeCodeActionPrefix, fixedVersion) -- return title --} -- --// SelectUpgradeCodeActions takes a list of code actions for a required module --// and returns a more selective list of upgrade code actions, --// where the code actions have been deduped. Code actions unrelated to upgrade --// are deduplicated by the name. --func SelectUpgradeCodeActions(actions []protocol.CodeAction) []protocol.CodeAction { -- if len(actions) <= 1 { -- return actions // return early if no sorting necessary +- addLabel := func(score float64, l *ast.LabeledStmt) { +- labelObj := c.pkg.TypesInfo().ObjectOf(l.Label) +- if labelObj != nil { +- c.deepState.enqueue(candidate{obj: labelObj, score: score}) +- } - } -- var versionedUpgrade, latestUpgrade, resetAction protocol.CodeAction -- var chosenVersionedUpgrade string -- var selected []protocol.CodeAction - -- seenTitles := make(map[string]bool) +- switch lt { +- case labelBreak, labelContinue: +- // "break" and "continue" only accept labels from enclosing statements. - -- for _, action := range actions { -- if strings.HasPrefix(action.Title, upgradeCodeActionPrefix) { -- if v := getUpgradeVersion(action); v == "latest" && latestUpgrade.Title == "" { -- latestUpgrade = action -- } else if versionedUpgrade.Title == "" || semver.Compare(v, chosenVersionedUpgrade) > 0 { -- chosenVersionedUpgrade = v -- versionedUpgrade = action +- for i, p := range c.path { +- switch p := p.(type) { +- case *ast.FuncLit: +- // Labels are function scoped, so don't continue out of functions. +- return +- case *ast.LabeledStmt: +- switch p.Stmt.(type) { +- case *ast.ForStmt, *ast.RangeStmt: +- // Loop labels can be used for "break" or "continue". +- addLabel(highScore*math.Pow(.99, float64(i)), p) +- case *ast.SwitchStmt, *ast.SelectStmt, *ast.TypeSwitchStmt: +- // Switch and select labels can be used only for "break". +- if lt == labelBreak { +- addLabel(highScore*math.Pow(.99, float64(i)), p) +- } +- } - } -- } else if strings.HasPrefix(action.Title, "Reset govulncheck") { -- resetAction = action -- } else if !seenTitles[action.Command.Title] { -- seenTitles[action.Command.Title] = true -- selected = append(selected, action) - } -- } -- if versionedUpgrade.Title != "" { -- selected = append(selected, versionedUpgrade) -- } -- if latestUpgrade.Title != "" { -- selected = append(selected, latestUpgrade) -- } -- if resetAction.Title != "" { -- selected = append(selected, resetAction) -- } -- return selected --} -- --func getUpgradeVersion(p protocol.CodeAction) string { -- return strings.TrimPrefix(p.Title, upgradeCodeActionPrefix) --} -diff -urN a/gopls/internal/lsp/mod/format.go b/gopls/internal/lsp/mod/format.go ---- a/gopls/internal/lsp/mod/format.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/mod/format.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,30 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package mod -- --import ( -- "context" -- -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/internal/event" --) +- case labelGoto: +- // Goto accepts any label in the same function not in a nested +- // block. It also doesn't take labels that would jump across +- // variable definitions, but ignore that case for now. +- ast.Inspect(c.enclosingFunc.body, func(n ast.Node) bool { +- if n == nil { +- return false +- } - --func Format(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.TextEdit, error) { -- ctx, done := event.Start(ctx, "mod.Format") -- defer done() +- switch n := n.(type) { +- // Only search into block-like nodes enclosing our "goto". +- // This prevents us from finding labels in nested blocks. +- case *ast.BlockStmt, *ast.CommClause, *ast.CaseClause: +- for _, p := range c.path { +- if n == p { +- return true +- } +- } +- return false +- case *ast.LabeledStmt: +- addLabel(highScore, n) +- } - -- pm, err := snapshot.ParseMod(ctx, fh) -- if err != nil { -- return nil, err -- } -- formatted, err := pm.File.Format() -- if err != nil { -- return nil, err +- return true +- }) - } -- // Calculate the edits to be made due to the change. -- diffs := snapshot.Options().ComputeEdits(string(pm.Mapper.Content), string(formatted)) -- return source.ToProtocolEdits(pm.Mapper, diffs) -} -diff -urN a/gopls/internal/lsp/mod/hover.go b/gopls/internal/lsp/mod/hover.go ---- a/gopls/internal/lsp/mod/hover.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/mod/hover.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,378 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/golang/completion/literal.go b/gopls/internal/golang/completion/literal.go +--- a/gopls/internal/golang/completion/literal.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/completion/literal.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,602 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package mod +-package completion - -import ( -- "bytes" - "context" - "fmt" -- "sort" +- "go/types" - "strings" +- "unicode" - -- "golang.org/x/mod/modfile" -- "golang.org/x/mod/semver" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/vulncheck" -- "golang.org/x/tools/gopls/internal/vulncheck/govulncheck" -- "golang.org/x/tools/gopls/internal/vulncheck/osv" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/golang/completion/snippet" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/aliases" - "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/typesinternal" -) - --func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.Hover, error) { -- var found bool -- for _, uri := range snapshot.ModFiles() { -- if fh.URI() == uri { -- found = true -- break -- } -- } -- -- // We only provide hover information for the view's go.mod files. -- if !found { -- return nil, nil +-// literal generates composite literal, function literal, and make() +-// completion items. +-func (c *completer) literal(ctx context.Context, literalType types.Type, imp *importInfo) { +- if !c.opts.snippets { +- return - } - -- ctx, done := event.Start(ctx, "mod.Hover") -- defer done() +- expType := c.inference.objType - -- // Get the position of the cursor. -- pm, err := snapshot.ParseMod(ctx, fh) -- if err != nil { -- return nil, fmt.Errorf("getting modfile handle: %w", err) -- } -- offset, err := pm.Mapper.PositionOffset(position) -- if err != nil { -- return nil, fmt.Errorf("computing cursor position: %w", err) +- if c.inference.matchesVariadic(literalType) { +- // Don't offer literal slice candidates for variadic arguments. +- // For example, don't offer "[]interface{}{}" in "fmt.Print(<>)". +- return - } - -- // If the cursor position is on a module statement -- if hover, ok := hoverOnModuleStatement(ctx, pm, offset, snapshot, fh); ok { -- return hover, nil +- // Avoid literal candidates if the expected type is an empty +- // interface. It isn't very useful to suggest a literal candidate of +- // every possible type. +- if expType != nil && isEmptyInterface(expType) { +- return - } -- return hoverOnRequireStatement(ctx, pm, offset, snapshot, fh) --} - --func hoverOnRequireStatement(ctx context.Context, pm *source.ParsedModule, offset int, snapshot source.Snapshot, fh source.FileHandle) (*protocol.Hover, error) { -- // Confirm that the cursor is at the position of a require statement. -- var req *modfile.Require -- var startOffset, endOffset int -- for _, r := range pm.File.Require { -- dep := []byte(r.Mod.Path) -- s, e := r.Syntax.Start.Byte, r.Syntax.End.Byte -- i := bytes.Index(pm.Mapper.Content[s:e], dep) -- if i == -1 { -- continue -- } -- // Shift the start position to the location of the -- // dependency within the require statement. -- startOffset, endOffset = s+i, e -- if startOffset <= offset && offset <= endOffset { -- req = r -- break -- } -- } -- // TODO(hyangah): find position for info about vulnerabilities in Go +- // We handle unnamed literal completions explicitly before searching +- // for candidates. Avoid named-type literal completions for +- // unnamed-type expected type since that results in duplicate +- // candidates. For example, in +- // +- // type mySlice []int +- // var []int = <> +- // +- // don't offer "mySlice{}" since we have already added a candidate +- // of "[]int{}". - -- // The cursor position is not on a require statement. -- if req == nil { -- return nil, nil -- } +- // TODO(adonovan): think about aliases: +- // they should probably be treated more like Named. +- // Should this use Deref not Unpointer? +- if is[*types.Named](aliases.Unalias(literalType)) && +- expType != nil && +- !is[*types.Named](aliases.Unalias(typesinternal.Unpointer(expType))) { - -- // Get the vulnerability info. -- fromGovulncheck := true -- vs := snapshot.View().Vulnerabilities(fh.URI())[fh.URI()] -- if vs == nil && snapshot.Options().Vulncheck == source.ModeVulncheckImports { -- var err error -- vs, err = snapshot.ModVuln(ctx, fh.URI()) -- if err != nil { -- return nil, err -- } -- fromGovulncheck = false +- return - } -- affecting, nonaffecting, osvs := lookupVulns(vs, req.Mod.Path, req.Mod.Version) - -- // Get the `go mod why` results for the given file. -- why, err := snapshot.ModWhy(ctx, fh) -- if err != nil { -- return nil, err -- } -- explanation, ok := why[req.Mod.Path] -- if !ok { -- return nil, nil +- // Check if an object of type literalType would match our expected type. +- cand := candidate{ +- obj: c.fakeObj(literalType), - } - -- // Get the range to highlight for the hover. -- // TODO(hyangah): adjust the hover range to include the version number -- // to match the diagnostics' range. -- rng, err := pm.Mapper.OffsetRange(startOffset, endOffset) -- if err != nil { -- return nil, err +- switch literalType.Underlying().(type) { +- // These literal types are addressable (e.g. "&[]int{}"), others are +- // not (e.g. can't do "&(func(){})"). +- case *types.Struct, *types.Array, *types.Slice, *types.Map: +- cand.addressable = true - } -- options := snapshot.Options() -- isPrivate := snapshot.View().IsGoPrivatePath(req.Mod.Path) -- header := formatHeader(req.Mod.Path, options) -- explanation = formatExplanation(explanation, req, options, isPrivate) -- vulns := formatVulnerabilities(affecting, nonaffecting, osvs, options, fromGovulncheck) -- -- return &protocol.Hover{ -- Contents: protocol.MarkupContent{ -- Kind: options.PreferredContentFormat, -- Value: header + vulns + explanation, -- }, -- Range: rng, -- }, nil --} - --func hoverOnModuleStatement(ctx context.Context, pm *source.ParsedModule, offset int, snapshot source.Snapshot, fh source.FileHandle) (*protocol.Hover, bool) { -- module := pm.File.Module -- if module == nil { -- return nil, false // no module stmt -- } -- if offset < module.Syntax.Start.Byte || offset > module.Syntax.End.Byte { -- return nil, false // cursor not in module stmt +- if !c.matchingCandidate(&cand) || cand.convertTo != nil { +- return - } - -- rng, err := pm.Mapper.OffsetRange(module.Syntax.Start.Byte, module.Syntax.End.Byte) -- if err != nil { -- return nil, false -- } -- fromGovulncheck := true -- vs := snapshot.View().Vulnerabilities(fh.URI())[fh.URI()] +- var ( +- qf = c.qf +- sel = enclosingSelector(c.path, c.pos) +- ) - -- if vs == nil && snapshot.Options().Vulncheck == source.ModeVulncheckImports { -- vs, err = snapshot.ModVuln(ctx, fh.URI()) -- if err != nil { -- return nil, false -- } -- fromGovulncheck = false +- // Don't qualify the type name if we are in a selector expression +- // since the package name is already present. +- if sel != nil { +- qf = func(_ *types.Package) string { return "" } - } -- modpath := "stdlib" -- goVersion := snapshot.View().GoVersionString() -- affecting, nonaffecting, osvs := lookupVulns(vs, modpath, goVersion) -- options := snapshot.Options() -- vulns := formatVulnerabilities(affecting, nonaffecting, osvs, options, fromGovulncheck) - -- return &protocol.Hover{ -- Contents: protocol.MarkupContent{ -- Kind: options.PreferredContentFormat, -- Value: vulns, -- }, -- Range: rng, -- }, true --} +- snip, typeName := c.typeNameSnippet(literalType, qf) - --func formatHeader(modpath string, options *source.Options) string { -- var b strings.Builder -- // Write the heading as an H3. -- b.WriteString("#### " + modpath) -- if options.PreferredContentFormat == protocol.Markdown { -- b.WriteString("\n\n") -- } else { -- b.WriteRune('\n') +- // A type name of "[]int" doesn't work very will with the matcher +- // since "[" isn't a valid identifier prefix. Here we strip off the +- // slice (and array) prefix yielding just "int". +- matchName := typeName +- switch t := literalType.(type) { +- case *types.Slice: +- matchName = types.TypeString(t.Elem(), qf) +- case *types.Array: +- matchName = types.TypeString(t.Elem(), qf) - } -- return b.String() --} - --func lookupVulns(vulns *vulncheck.Result, modpath, version string) (affecting, nonaffecting []*govulncheck.Finding, osvs map[string]*osv.Entry) { -- if vulns == nil || len(vulns.Entries) == 0 { -- return nil, nil, nil -- } -- for _, finding := range vulns.Findings { -- vuln, typ := foundVuln(finding) -- if vuln.Module != modpath { -- continue -- } -- // It is possible that the source code was changed since the last -- // govulncheck run and information in the `vulns` info is stale. -- // For example, imagine that a user is in the middle of updating -- // problematic modules detected by the govulncheck run by applying -- // quick fixes. Stale diagnostics can be confusing and prevent the -- // user from quickly locating the next module to fix. -- // Ideally we should rerun the analysis with the updated module -- // dependencies or any other code changes, but we are not yet -- // in the position of automatically triggering the analysis -- // (govulncheck can take a while). We also don't know exactly what -- // part of source code was changed since `vulns` was computed. -- // As a heuristic, we assume that a user upgrades the affecting -- // module to the version with the fix or the latest one, and if the -- // version in the require statement is equal to or higher than the -- // fixed version, skip the vulnerability information in the hover. -- // Eventually, the user has to rerun govulncheck. -- if finding.FixedVersion != "" && semver.IsValid(version) && semver.Compare(finding.FixedVersion, version) <= 0 { -- continue -- } -- switch typ { -- case vulnCalled: -- affecting = append(affecting, finding) -- case vulnImported: -- nonaffecting = append(nonaffecting, finding) -- } +- addlEdits, err := c.importEdits(imp) +- if err != nil { +- event.Error(ctx, "error adding import for literal candidate", err) +- return - } - -- // Remove affecting elements from nonaffecting. -- // An OSV entry can appear in both lists if an OSV entry covers -- // multiple packages imported but not all vulnerable symbols are used. -- // The current wording of hover message doesn't clearly -- // present this case well IMO, so let's skip reporting nonaffecting. -- if len(affecting) > 0 && len(nonaffecting) > 0 { -- affectingSet := map[string]bool{} -- for _, f := range affecting { -- affectingSet[f.OSV] = true +- // If prefix matches the type name, client may want a composite literal. +- if score := c.matcher.Score(matchName); score > 0 { +- if cand.hasMod(reference) { +- if sel != nil { +- // If we are in a selector we must place the "&" before the selector. +- // For example, "foo.B<>" must complete to "&foo.Bar{}", not +- // "foo.&Bar{}". +- edits, err := c.editText(sel.Pos(), sel.Pos(), "&") +- if err != nil { +- event.Error(ctx, "error making edit for literal pointer completion", err) +- return +- } +- addlEdits = append(addlEdits, edits...) +- } else { +- // Otherwise we can stick the "&" directly before the type name. +- typeName = "&" + typeName +- snip.PrependText("&") +- } - } -- n := 0 -- for _, v := range nonaffecting { -- if !affectingSet[v.OSV] { -- nonaffecting[n] = v -- n++ +- +- switch t := literalType.Underlying().(type) { +- case *types.Struct, *types.Array, *types.Slice, *types.Map: +- c.compositeLiteral(t, snip.Clone(), typeName, float64(score), addlEdits) +- case *types.Signature: +- // Add a literal completion for a signature type that implements +- // an interface. For example, offer "http.HandlerFunc()" when +- // expected type is "http.Handler". +- if expType != nil && types.IsInterface(expType) { +- c.basicLiteral(t, snip.Clone(), typeName, float64(score), addlEdits) +- } +- case *types.Basic: +- // Add a literal completion for basic types that implement our +- // expected interface (e.g. named string type http.Dir +- // implements http.FileSystem), or are identical to our expected +- // type (i.e. yielding a type conversion such as "float64()"). +- if expType != nil && (types.IsInterface(expType) || types.Identical(expType, literalType)) { +- c.basicLiteral(t, snip.Clone(), typeName, float64(score), addlEdits) - } - } -- nonaffecting = nonaffecting[:n] - } -- sort.Slice(nonaffecting, func(i, j int) bool { return nonaffecting[i].OSV < nonaffecting[j].OSV }) -- sort.Slice(affecting, func(i, j int) bool { return affecting[i].OSV < affecting[j].OSV }) -- return affecting, nonaffecting, vulns.Entries --} - --func fixedVersion(fixed string) string { -- if fixed == "" { -- return "No fix is available." +- // If prefix matches "make", client may want a "make()" +- // invocation. We also include the type name to allow for more +- // flexible fuzzy matching. +- if score := c.matcher.Score("make." + matchName); !cand.hasMod(reference) && score > 0 { +- switch literalType.Underlying().(type) { +- case *types.Slice: +- // The second argument to "make()" for slices is required, so default to "0". +- c.makeCall(snip.Clone(), typeName, "0", float64(score), addlEdits) +- case *types.Map, *types.Chan: +- // Maps and channels don't require the second argument, so omit +- // to keep things simple for now. +- c.makeCall(snip.Clone(), typeName, "", float64(score), addlEdits) +- } - } -- return "Fixed in " + fixed + "." --} - --func formatVulnerabilities(affecting, nonaffecting []*govulncheck.Finding, osvs map[string]*osv.Entry, options *source.Options, fromGovulncheck bool) string { -- if len(osvs) == 0 || (len(affecting) == 0 && len(nonaffecting) == 0) { -- return "" -- } -- byOSV := func(findings []*govulncheck.Finding) map[string][]*govulncheck.Finding { -- m := make(map[string][]*govulncheck.Finding) -- for _, f := range findings { -- m[f.OSV] = append(m[f.OSV], f) +- // If prefix matches "func", client may want a function literal. +- if score := c.matcher.Score("func"); !cand.hasMod(reference) && score > 0 && (expType == nil || !types.IsInterface(expType)) { +- switch t := literalType.Underlying().(type) { +- case *types.Signature: +- c.functionLiteral(ctx, t, float64(score)) - } -- return m - } -- affectingByOSV := byOSV(affecting) -- nonaffectingByOSV := byOSV(nonaffecting) -- -- // TODO(hyangah): can we use go templates to generate hover messages? -- // Then, we can use a different template for markdown case. -- useMarkdown := options.PreferredContentFormat == protocol.Markdown +-} - -- var b strings.Builder +-// literalCandidateScore is the base score for literal candidates. +-// Literal candidates match the expected type so they should be high +-// scoring, but we want them ranked below lexical objects of the +-// correct type, so scale down highScore. +-const literalCandidateScore = highScore / 2 - -- if len(affectingByOSV) > 0 { -- // TODO(hyangah): make the message more eyecatching (icon/codicon/color) -- if len(affectingByOSV) == 1 { -- fmt.Fprintf(&b, "\n**WARNING:** Found %d reachable vulnerability.\n", len(affectingByOSV)) -- } else { -- fmt.Fprintf(&b, "\n**WARNING:** Found %d reachable vulnerabilities.\n", len(affectingByOSV)) +-// functionLiteral adds a function literal completion item for the +-// given signature. +-func (c *completer) functionLiteral(ctx context.Context, sig *types.Signature, matchScore float64) { +- snip := &snippet.Builder{} +- snip.WriteText("func(") +- +- // First we generate names for each param and keep a seen count so +- // we know if we need to uniquify param names. For example, +- // "func(int)" will become "func(i int)", but "func(int, int64)" +- // will become "func(i1 int, i2 int64)". +- var ( +- paramNames = make([]string, sig.Params().Len()) +- paramNameCount = make(map[string]int) +- hasTypeParams bool +- ) +- for i := 0; i < sig.Params().Len(); i++ { +- var ( +- p = sig.Params().At(i) +- name = p.Name() +- ) +- +- if tp, _ := aliases.Unalias(p.Type()).(*types.TypeParam); tp != nil && !c.typeParamInScope(tp) { +- hasTypeParams = true - } -- } -- for id, findings := range affectingByOSV { -- fix := fixedVersion(findings[0].FixedVersion) -- pkgs := vulnerablePkgsInfo(findings, useMarkdown) -- osvEntry := osvs[id] - -- if useMarkdown { -- fmt.Fprintf(&b, "- [**%v**](%v) %v%v\n%v\n", id, href(id), osvEntry.Summary, pkgs, fix) -- } else { -- fmt.Fprintf(&b, " - [%v] %v (%v) %v%v\n", id, osvEntry.Summary, href(id), pkgs, fix) +- if name == "" { +- // If the param has no name in the signature, guess a name based +- // on the type. Use an empty qualifier to ignore the package. +- // For example, we want to name "http.Request" "r", not "hr". +- typeName, err := golang.FormatVarType(ctx, c.snapshot, c.pkg, p, +- func(p *types.Package) string { return "" }, +- func(golang.PackageName, golang.ImportPath, golang.PackagePath) string { return "" }) +- if err != nil { +- // In general, the only error we should encounter while formatting is +- // context cancellation. +- if ctx.Err() == nil { +- event.Error(ctx, "formatting var type", err) +- } +- return +- } +- name = abbreviateTypeName(typeName) +- } +- paramNames[i] = name +- if name != "_" { +- paramNameCount[name]++ - } - } -- if len(nonaffecting) > 0 { -- if fromGovulncheck { -- fmt.Fprintf(&b, "\n**Note:** The project imports packages with known vulnerabilities, but does not call the vulnerable code.\n") +- +- for n, c := range paramNameCount { +- // Any names we saw more than once will need a unique suffix added +- // on. Reset the count to 1 to act as the suffix for the first +- // name. +- if c >= 2 { +- paramNameCount[n] = 1 - } else { -- fmt.Fprintf(&b, "\n**Note:** The project imports packages with known vulnerabilities. Use `govulncheck` to check if the project uses vulnerable symbols.\n") +- delete(paramNameCount, n) - } - } -- for k, findings := range nonaffectingByOSV { -- fix := fixedVersion(findings[0].FixedVersion) -- pkgs := vulnerablePkgsInfo(findings, useMarkdown) -- osvEntry := osvs[k] - -- if useMarkdown { -- fmt.Fprintf(&b, "- [%v](%v) %v%v\n%v\n", k, href(k), osvEntry.Summary, pkgs, fix) +- for i := 0; i < sig.Params().Len(); i++ { +- if hasTypeParams && !c.opts.placeholders { +- // If there are type params in the args then the user must +- // choose the concrete types. If placeholders are disabled just +- // drop them between the parens and let them fill things in. +- snip.WritePlaceholder(nil) +- break +- } +- +- if i > 0 { +- snip.WriteText(", ") +- } +- +- var ( +- p = sig.Params().At(i) +- name = paramNames[i] +- ) +- +- // Uniquify names by adding on an incrementing numeric suffix. +- if idx, found := paramNameCount[name]; found { +- paramNameCount[name]++ +- name = fmt.Sprintf("%s%d", name, idx) +- } +- +- if name != p.Name() && c.opts.placeholders { +- // If we didn't use the signature's param name verbatim then we +- // may have chosen a poor name. Give the user a placeholder so +- // they can easily fix the name. +- snip.WritePlaceholder(func(b *snippet.Builder) { +- b.WriteText(name) +- }) - } else { -- fmt.Fprintf(&b, " - [%v] %v (%v) %v\n%v\n", k, osvEntry.Summary, href(k), pkgs, fix) +- snip.WriteText(name) - } -- } -- b.WriteString("\n") -- return b.String() --} - --func vulnerablePkgsInfo(findings []*govulncheck.Finding, useMarkdown bool) string { -- var b strings.Builder -- seen := map[string]bool{} -- for _, f := range findings { -- p := f.Trace[0].Package -- if !seen[p] { -- seen[p] = true -- if useMarkdown { -- b.WriteString("\n * `") -- } else { -- b.WriteString("\n ") +- // If the following param's type is identical to this one, omit +- // this param's type string. For example, emit "i, j int" instead +- // of "i int, j int". +- if i == sig.Params().Len()-1 || !types.Identical(p.Type(), sig.Params().At(i+1).Type()) { +- snip.WriteText(" ") +- typeStr, err := golang.FormatVarType(ctx, c.snapshot, c.pkg, p, c.qf, c.mq) +- if err != nil { +- // In general, the only error we should encounter while formatting is +- // context cancellation. +- if ctx.Err() == nil { +- event.Error(ctx, "formatting var type", err) +- } +- return - } -- b.WriteString(p) -- if useMarkdown { -- b.WriteString("`") +- if sig.Variadic() && i == sig.Params().Len()-1 { +- typeStr = strings.Replace(typeStr, "[]", "...", 1) +- } +- +- if tp, ok := aliases.Unalias(p.Type()).(*types.TypeParam); ok && !c.typeParamInScope(tp) { +- snip.WritePlaceholder(func(snip *snippet.Builder) { +- snip.WriteText(typeStr) +- }) +- } else { +- snip.WriteText(typeStr) - } - } - } -- return b.String() --} +- snip.WriteText(")") - --func formatExplanation(text string, req *modfile.Require, options *source.Options, isPrivate bool) string { -- text = strings.TrimSuffix(text, "\n") -- splt := strings.Split(text, "\n") -- length := len(splt) +- results := sig.Results() +- if results.Len() > 0 { +- snip.WriteText(" ") +- } - -- var b strings.Builder +- resultsNeedParens := results.Len() > 1 || +- results.Len() == 1 && results.At(0).Name() != "" - -- // If the explanation is 2 lines, then it is of the form: -- // # golang.org/x/text/encoding -- // (main module does not need package golang.org/x/text/encoding) -- if length == 2 { -- b.WriteString(splt[1]) -- return b.String() +- var resultHasTypeParams bool +- for i := 0; i < results.Len(); i++ { +- if tp, ok := aliases.Unalias(results.At(i).Type()).(*types.TypeParam); ok && !c.typeParamInScope(tp) { +- resultHasTypeParams = true +- } - } - -- imp := splt[length-1] // import path -- reference := imp -- // See golang/go#36998: don't link to modules matching GOPRIVATE. -- if !isPrivate && options.PreferredContentFormat == protocol.Markdown { -- target := imp -- if strings.ToLower(options.LinkTarget) == "pkg.go.dev" { -- target = strings.Replace(target, req.Mod.Path, req.Mod.String(), 1) +- if resultsNeedParens { +- snip.WriteText("(") +- } +- for i := 0; i < results.Len(); i++ { +- if resultHasTypeParams && !c.opts.placeholders { +- // Leave an empty tabstop if placeholders are disabled and there +- // are type args that need specificying. +- snip.WritePlaceholder(nil) +- break +- } +- +- if i > 0 { +- snip.WriteText(", ") +- } +- r := results.At(i) +- if name := r.Name(); name != "" { +- snip.WriteText(name + " ") +- } +- +- text, err := golang.FormatVarType(ctx, c.snapshot, c.pkg, r, c.qf, c.mq) +- if err != nil { +- // In general, the only error we should encounter while formatting is +- // context cancellation. +- if ctx.Err() == nil { +- event.Error(ctx, "formatting var type", err) +- } +- return +- } +- if tp, ok := aliases.Unalias(r.Type()).(*types.TypeParam); ok && !c.typeParamInScope(tp) { +- snip.WritePlaceholder(func(snip *snippet.Builder) { +- snip.WriteText(text) +- }) +- } else { +- snip.WriteText(text) - } -- reference = fmt.Sprintf("[%s](%s)", imp, source.BuildLink(options.LinkTarget, target, "")) - } -- b.WriteString("This module is necessary because " + reference + " is imported in") +- if resultsNeedParens { +- snip.WriteText(")") +- } - -- // If the explanation is 3 lines, then it is of the form: -- // # golang.org/x/tools -- // modtest -- // golang.org/x/tools/go/packages -- if length == 3 { -- msg := fmt.Sprintf(" `%s`.", splt[1]) -- b.WriteString(msg) -- return b.String() +- snip.WriteText(" {") +- snip.WriteFinalTabstop() +- snip.WriteText("}") +- +- c.items = append(c.items, CompletionItem{ +- Label: "func(...) {}", +- Score: matchScore * literalCandidateScore, +- Kind: protocol.VariableCompletion, +- snippet: snip, +- }) +-} +- +-// conventionalAcronyms contains conventional acronyms for type names +-// in lower case. For example, "ctx" for "context" and "err" for "error". +-var conventionalAcronyms = map[string]string{ +- "context": "ctx", +- "error": "err", +- "tx": "tx", +- "responsewriter": "w", +-} +- +-// abbreviateTypeName abbreviates type names into acronyms. For +-// example, "fooBar" is abbreviated "fb". Care is taken to ignore +-// non-identifier runes. For example, "[]int" becomes "i", and +-// "struct { i int }" becomes "s". +-func abbreviateTypeName(s string) string { +- var ( +- b strings.Builder +- useNextUpper bool +- ) +- +- // Trim off leading non-letters. We trim everything between "[" and +- // "]" to handle array types like "[someConst]int". +- var inBracket bool +- s = strings.TrimFunc(s, func(r rune) bool { +- if inBracket { +- inBracket = r != ']' +- return true +- } +- +- if r == '[' { +- inBracket = true +- } +- +- return !unicode.IsLetter(r) +- }) +- +- if acr, ok := conventionalAcronyms[strings.ToLower(s)]; ok { +- return acr - } - -- // If the explanation is more than 3 lines, then it is of the form: -- // # golang.org/x/text/language -- // rsc.io/quote -- // rsc.io/sampler -- // golang.org/x/text/language -- b.WriteString(":\n```text") -- dash := "" -- for _, imp := range splt[1 : length-1] { -- dash += "-" -- b.WriteString("\n" + dash + " " + imp) +- for i, r := range s { +- // Stop if we encounter a non-identifier rune. +- if !unicode.IsLetter(r) && !unicode.IsNumber(r) { +- break +- } +- +- if i == 0 { +- b.WriteRune(unicode.ToLower(r)) +- } +- +- if unicode.IsUpper(r) { +- if useNextUpper { +- b.WriteRune(unicode.ToLower(r)) +- useNextUpper = false +- } +- } else { +- useNextUpper = true +- } - } -- b.WriteString("\n```") +- - return b.String() -} -diff -urN a/gopls/internal/lsp/mod/inlayhint.go b/gopls/internal/lsp/mod/inlayhint.go ---- a/gopls/internal/lsp/mod/inlayhint.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/mod/inlayhint.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,100 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. --package mod - --import ( -- "context" -- "fmt" +-// compositeLiteral adds a composite literal completion item for the given typeName. +-// T is an (unnamed, unaliased) struct, array, slice, or map type. +-func (c *completer) compositeLiteral(T types.Type, snip *snippet.Builder, typeName string, matchScore float64, edits []protocol.TextEdit) { +- snip.WriteText("{") +- // Don't put the tab stop inside the composite literal curlies "{}" +- // for structs that have no accessible fields. +- if strct, ok := T.(*types.Struct); !ok || fieldsAccessible(strct, c.pkg.Types()) { +- snip.WriteFinalTabstop() +- } +- snip.WriteText("}") - -- "golang.org/x/mod/modfile" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" --) +- nonSnippet := typeName + "{}" - --func InlayHint(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, rng protocol.Range) ([]protocol.InlayHint, error) { -- // Inlay hints are enabled if the client supports them. -- pm, err := snapshot.ParseMod(ctx, fh) -- if err != nil { -- return nil, err +- c.items = append(c.items, CompletionItem{ +- Label: nonSnippet, +- InsertText: nonSnippet, +- Score: matchScore * literalCandidateScore, +- Kind: protocol.VariableCompletion, +- AdditionalTextEdits: edits, +- snippet: snip, +- }) +-} +- +-// basicLiteral adds a literal completion item for the given basic +-// type name typeName. +-func (c *completer) basicLiteral(T types.Type, snip *snippet.Builder, typeName string, matchScore float64, edits []protocol.TextEdit) { +- // Never give type conversions like "untyped int()". +- if isUntyped(T) { +- return - } - -- // Compare the version of the module used in the snapshot's metadata with the -- // version requested by the module, in both cases, taking replaces into account. -- // Produce an InlayHint when the version is the module is not the one used. +- snip.WriteText("(") +- snip.WriteFinalTabstop() +- snip.WriteText(")") - -- replaces := make(map[string]*modfile.Replace) -- for _, x := range pm.File.Replace { -- replaces[x.Old.Path] = x -- } +- nonSnippet := typeName + "()" - -- requires := make(map[string]*modfile.Require) -- for _, x := range pm.File.Require { -- requires[x.Mod.Path] = x +- c.items = append(c.items, CompletionItem{ +- Label: nonSnippet, +- InsertText: nonSnippet, +- Detail: T.String(), +- Score: matchScore * literalCandidateScore, +- Kind: protocol.VariableCompletion, +- AdditionalTextEdits: edits, +- snippet: snip, +- }) +-} +- +-// makeCall adds a completion item for a "make()" call given a specific type. +-func (c *completer) makeCall(snip *snippet.Builder, typeName string, secondArg string, matchScore float64, edits []protocol.TextEdit) { +- // Keep it simple and don't add any placeholders for optional "make()" arguments. +- +- snip.PrependText("make(") +- if secondArg != "" { +- snip.WriteText(", ") +- snip.WritePlaceholder(func(b *snippet.Builder) { +- if c.opts.placeholders { +- b.WriteText(secondArg) +- } +- }) - } +- snip.WriteText(")") - -- am, err := snapshot.AllMetadata(ctx) -- if err != nil { -- return nil, err +- var nonSnippet strings.Builder +- nonSnippet.WriteString("make(" + typeName) +- if secondArg != "" { +- nonSnippet.WriteString(", ") +- nonSnippet.WriteString(secondArg) - } +- nonSnippet.WriteByte(')') - -- var ans []protocol.InlayHint -- seen := make(map[string]bool) -- for _, meta := range am { -- if meta.Module == nil || seen[meta.Module.Path] { -- continue -- } -- seen[meta.Module.Path] = true -- metaVersion := meta.Module.Version -- if meta.Module.Replace != nil { -- metaVersion = meta.Module.Replace.Version +- c.items = append(c.items, CompletionItem{ +- Label: nonSnippet.String(), +- InsertText: nonSnippet.String(), +- Score: matchScore * literalCandidateScore, +- Kind: protocol.FunctionCompletion, +- AdditionalTextEdits: edits, +- snippet: snip, +- }) +-} +- +-// Create a snippet for a type name where type params become placeholders. +-func (c *completer) typeNameSnippet(literalType types.Type, qf types.Qualifier) (*snippet.Builder, string) { +- var ( +- snip snippet.Builder +- typeName string +- // TODO(adonovan): think more about aliases. +- // They should probably be treated more like Named. +- named, _ = aliases.Unalias(literalType).(*types.Named) +- ) +- +- if named != nil && named.Obj() != nil && named.TypeParams().Len() > 0 && !c.fullyInstantiated(named) { +- // We are not "fully instantiated" meaning we have type params that must be specified. +- if pkg := qf(named.Obj().Pkg()); pkg != "" { +- typeName = pkg + "." - } -- // These versions can be blank, as in gopls/go.mod's local replace -- if oldrepl, ok := replaces[meta.Module.Path]; ok && oldrepl.New.Version != metaVersion { -- ih := genHint(oldrepl.Syntax, oldrepl.New.Version, metaVersion, pm.Mapper) -- if ih != nil { -- ans = append(ans, *ih) -- } -- } else if oldreq, ok := requires[meta.Module.Path]; ok && oldreq.Mod.Version != metaVersion { -- // maybe it was replaced: -- if _, ok := replaces[meta.Module.Path]; ok { -- continue -- } -- ih := genHint(oldreq.Syntax, oldreq.Mod.Version, metaVersion, pm.Mapper) -- if ih != nil { -- ans = append(ans, *ih) +- +- // We do this to get "someType" instead of "someType[T]". +- typeName += named.Obj().Name() +- snip.WriteText(typeName + "[") +- +- if c.opts.placeholders { +- for i := 0; i < named.TypeParams().Len(); i++ { +- if i > 0 { +- snip.WriteText(", ") +- } +- snip.WritePlaceholder(func(snip *snippet.Builder) { +- snip.WriteText(types.TypeString(named.TypeParams().At(i), qf)) +- }) - } +- } else { +- snip.WritePlaceholder(nil) - } +- snip.WriteText("]") +- typeName += "[...]" +- } else { +- // We don't have unspecified type params so use default type formatting. +- typeName = types.TypeString(literalType, qf) +- snip.WriteText(typeName) - } -- return ans, nil +- +- return &snip, typeName -} - --func genHint(mline *modfile.Line, oldVersion, newVersion string, m *protocol.Mapper) *protocol.InlayHint { -- x := mline.End.Byte // the parser has removed trailing whitespace and comments (see modfile_test.go) -- x -= len(mline.Token[len(mline.Token)-1]) -- line, err := m.OffsetPosition(x) -- if err != nil { -- return nil -- } -- part := protocol.InlayHintLabelPart{ -- Value: newVersion, -- Tooltip: &protocol.OrPTooltipPLabel{ -- Value: fmt.Sprintf("used metadata's version %s rather than go.mod's version %s", newVersion, oldVersion), -- }, +-// fullyInstantiated reports whether all of t's type params have +-// specified type args. +-func (c *completer) fullyInstantiated(t *types.Named) bool { +- tps := t.TypeParams() +- tas := t.TypeArgs() +- +- if tps.Len() != tas.Len() { +- return false - } -- rng, err := m.OffsetRange(x, mline.End.Byte) -- if err != nil { -- return nil +- +- for i := 0; i < tas.Len(); i++ { +- // TODO(adonovan) think about generic aliases. +- switch ta := aliases.Unalias(tas.At(i)).(type) { +- case *types.TypeParam: +- // A *TypeParam only counts as specified if it is currently in +- // scope (i.e. we are in a generic definition). +- if !c.typeParamInScope(ta) { +- return false +- } +- case *types.Named: +- if !c.fullyInstantiated(ta) { +- return false +- } +- } - } -- te := protocol.TextEdit{ -- Range: rng, -- NewText: newVersion, +- return true +-} +- +-// typeParamInScope returns whether tp's object is in scope at c.pos. +-// This tells you whether you are in a generic definition and can +-// assume tp has been specified. +-func (c *completer) typeParamInScope(tp *types.TypeParam) bool { +- obj := tp.Obj() +- if obj == nil { +- return false - } -- return &protocol.InlayHint{ -- Position: line, -- Label: []protocol.InlayHintLabelPart{part}, -- Kind: protocol.Parameter, -- PaddingRight: true, -- TextEdits: []protocol.TextEdit{te}, +- +- scope := c.innermostScope() +- if scope == nil { +- return false - } +- +- _, foundObj := scope.LookupParent(obj.Name(), c.pos) +- return obj == foundObj -} -diff -urN a/gopls/internal/lsp/progress/progress.go b/gopls/internal/lsp/progress/progress.go ---- a/gopls/internal/lsp/progress/progress.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/progress/progress.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,283 +0,0 @@ +diff -urN a/gopls/internal/golang/completion/package.go b/gopls/internal/golang/completion/package.go +--- a/gopls/internal/golang/completion/package.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/completion/package.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,353 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package progress +-package completion - -import ( +- "bytes" - "context" +- "errors" - "fmt" -- "math/rand" -- "strconv" +- "go/ast" +- "go/parser" +- "go/scanner" +- "go/token" +- "go/types" +- "path/filepath" - "strings" -- "sync" +- "unicode" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/tag" -- "golang.org/x/tools/internal/xcontext" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/internal/fuzzy" -) - --type Tracker struct { -- client protocol.Client -- supportsWorkDoneProgress bool +-// packageClauseCompletions offers completions for a package declaration when +-// one is not present in the given file. +-func packageClauseCompletions(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) ([]CompletionItem, *Selection, error) { +- // We know that the AST for this file will be empty due to the missing +- // package declaration, but parse it anyway to get a mapper. +- // TODO(adonovan): opt: there's no need to parse just to get a mapper. +- pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) +- if err != nil { +- return nil, nil, err +- } - -- mu sync.Mutex -- inProgress map[protocol.ProgressToken]*WorkDone --} +- offset, err := pgf.Mapper.PositionOffset(position) +- if err != nil { +- return nil, nil, err +- } +- surrounding, err := packageCompletionSurrounding(pgf, offset) +- if err != nil { +- return nil, nil, fmt.Errorf("invalid position for package completion: %w", err) +- } - --func NewTracker(client protocol.Client) *Tracker { -- return &Tracker{ -- client: client, -- inProgress: make(map[protocol.ProgressToken]*WorkDone), +- packageSuggestions, err := packageSuggestions(ctx, snapshot, fh.URI(), "") +- if err != nil { +- return nil, nil, err - } --} - --// SetSupportsWorkDoneProgress sets whether the client supports work done --// progress reporting. It must be set before using the tracker. --// --// TODO(rfindley): fix this broken initialization pattern. --// Also: do we actually need the fall-back progress behavior using ShowMessage? --// Surely ShowMessage notifications are too noisy to be worthwhile. --func (tracker *Tracker) SetSupportsWorkDoneProgress(b bool) { -- tracker.supportsWorkDoneProgress = b --} +- var items []CompletionItem +- for _, pkg := range packageSuggestions { +- insertText := fmt.Sprintf("package %s", pkg.name) +- items = append(items, CompletionItem{ +- Label: insertText, +- Kind: protocol.ModuleCompletion, +- InsertText: insertText, +- Score: pkg.score, +- }) +- } - --// SupportsWorkDoneProgress reports whether the tracker supports work done --// progress reporting. --func (tracker *Tracker) SupportsWorkDoneProgress() bool { -- return tracker.supportsWorkDoneProgress +- return items, surrounding, nil -} - --// Start notifies the client of work being done on the server. It uses either --// ShowMessage RPCs or $/progress messages, depending on the capabilities of --// the client. The returned WorkDone handle may be used to report incremental --// progress, and to report work completion. In particular, it is an error to --// call start and not call end(...) on the returned WorkDone handle. --// --// If token is empty, a token will be randomly generated. --// --// The progress item is considered cancellable if the given cancel func is --// non-nil. In this case, cancel is called when the work done --// --// Example: --// --// func Generate(ctx) (err error) { --// ctx, cancel := context.WithCancel(ctx) --// defer cancel() --// work := s.progress.start(ctx, "generate", "running go generate", cancel) --// defer func() { --// if err != nil { --// work.end(ctx, fmt.Sprintf("generate failed: %v", err)) --// } else { --// work.end(ctx, "done") --// } --// }() --// // Do the work... --// } --func (t *Tracker) Start(ctx context.Context, title, message string, token protocol.ProgressToken, cancel func()) *WorkDone { -- ctx = xcontext.Detach(ctx) // progress messages should not be cancelled -- wd := &WorkDone{ -- client: t.client, -- token: token, -- cancel: cancel, +-// packageCompletionSurrounding returns surrounding for package completion if a +-// package completions can be suggested at a given cursor offset. A valid location +-// for package completion is above any declarations or import statements. +-func packageCompletionSurrounding(pgf *parsego.File, offset int) (*Selection, error) { +- m := pgf.Mapper +- // If the file lacks a package declaration, the parser will return an empty +- // AST. As a work-around, try to parse an expression from the file contents. +- fset := token.NewFileSet() +- expr, _ := parser.ParseExprFrom(fset, m.URI.Path(), pgf.Src, parser.Mode(0)) +- if expr == nil { +- return nil, fmt.Errorf("unparseable file (%s)", m.URI) - } -- if !t.supportsWorkDoneProgress { -- // Previous iterations of this fallback attempted to retain cancellation -- // support by using ShowMessageCommand with a 'Cancel' button, but this is -- // not ideal as the 'Cancel' dialog stays open even after the command -- // completes. -- // -- // Just show a simple message. Clients can implement workDone progress -- // reporting to get cancellation support. -- if err := wd.client.ShowMessage(ctx, &protocol.ShowMessageParams{ -- Type: protocol.Log, -- Message: message, -- }); err != nil { -- event.Error(ctx, "showing start message for "+title, err) +- tok := fset.File(expr.Pos()) +- cursor := tok.Pos(offset) +- +- // If we were able to parse out an identifier as the first expression from +- // the file, it may be the beginning of a package declaration ("pack "). +- // We can offer package completions if the cursor is in the identifier. +- if name, ok := expr.(*ast.Ident); ok { +- if cursor >= name.Pos() && cursor <= name.End() { +- if !strings.HasPrefix(PACKAGE, name.Name) { +- return nil, fmt.Errorf("cursor in non-matching ident") +- } +- return &Selection{ +- content: name.Name, +- cursor: cursor, +- tokFile: tok, +- start: name.Pos(), +- end: name.End(), +- mapper: m, +- }, nil - } -- return wd - } -- if wd.token == nil { -- token = strconv.FormatInt(rand.Int63(), 10) -- err := wd.client.WorkDoneProgressCreate(ctx, &protocol.WorkDoneProgressCreateParams{ -- Token: token, -- }) -- if err != nil { -- wd.err = err -- event.Error(ctx, "starting work for "+title, err) -- return wd +- +- // The file is invalid, but it contains an expression that we were able to +- // parse. We will use this expression to construct the cursor's +- // "surrounding". +- +- // First, consider the possibility that we have a valid "package" keyword +- // with an empty package name ("package "). "package" is parsed as an +- // *ast.BadDecl since it is a keyword. This logic would allow "package" to +- // appear on any line of the file as long as it's the first code expression +- // in the file. +- lines := strings.Split(string(pgf.Src), "\n") +- cursorLine := safetoken.Line(tok, cursor) +- if cursorLine <= 0 || cursorLine > len(lines) { +- return nil, fmt.Errorf("invalid line number") +- } +- if safetoken.StartPosition(fset, expr.Pos()).Line == cursorLine { +- words := strings.Fields(lines[cursorLine-1]) +- if len(words) > 0 && words[0] == PACKAGE { +- content := PACKAGE +- // Account for spaces if there are any. +- if len(words) > 1 { +- content += " " +- } +- +- start := expr.Pos() +- end := token.Pos(int(expr.Pos()) + len(content) + 1) +- // We have verified that we have a valid 'package' keyword as our +- // first expression. Ensure that cursor is in this keyword or +- // otherwise fallback to the general case. +- if cursor >= start && cursor <= end { +- return &Selection{ +- content: content, +- cursor: cursor, +- tokFile: tok, +- start: start, +- end: end, +- mapper: m, +- }, nil +- } - } -- wd.token = token - } -- // At this point we have a token that the client knows about. Store the token -- // before starting work. -- t.mu.Lock() -- t.inProgress[wd.token] = wd -- t.mu.Unlock() -- wd.cleanup = func() { -- t.mu.Lock() -- delete(t.inProgress, token) -- t.mu.Unlock() +- +- // If the cursor is after the start of the expression, no package +- // declaration will be valid. +- if cursor > expr.Pos() { +- return nil, fmt.Errorf("cursor after expression") - } -- err := wd.client.Progress(ctx, &protocol.ProgressParams{ -- Token: wd.token, -- Value: &protocol.WorkDoneProgressBegin{ -- Kind: "begin", -- Cancellable: wd.cancel != nil, -- Message: message, -- Title: title, -- }, -- }) -- if err != nil { -- event.Error(ctx, "progress begin", err) +- +- // If the cursor is in a comment, don't offer any completions. +- if cursorInComment(tok, cursor, m.Content) { +- return nil, fmt.Errorf("cursor in comment") - } -- return wd +- +- // The surrounding range in this case is the cursor. +- return &Selection{ +- content: "", +- tokFile: tok, +- start: cursor, +- end: cursor, +- cursor: cursor, +- mapper: m, +- }, nil -} - --func (t *Tracker) Cancel(token protocol.ProgressToken) error { -- t.mu.Lock() -- defer t.mu.Unlock() -- wd, ok := t.inProgress[token] -- if !ok { -- return fmt.Errorf("token %q not found in progress", token) -- } -- if wd.cancel == nil { -- return fmt.Errorf("work %q is not cancellable", token) +-func cursorInComment(file *token.File, cursor token.Pos, src []byte) bool { +- var s scanner.Scanner +- s.Init(file, src, func(_ token.Position, _ string) {}, scanner.ScanComments) +- for { +- pos, tok, lit := s.Scan() +- if pos <= cursor && cursor <= token.Pos(int(pos)+len(lit)) { +- return tok == token.COMMENT +- } +- if tok == token.EOF { +- break +- } - } -- wd.doCancel() -- return nil +- return false -} - --// WorkDone represents a unit of work that is reported to the client via the --// progress API. --type WorkDone struct { -- client protocol.Client -- // If token is nil, this workDone object uses the ShowMessage API, rather -- // than $/progress. -- token protocol.ProgressToken -- // err is set if progress reporting is broken for some reason (for example, -- // if there was an initial error creating a token). -- err error +-// packageNameCompletions returns name completions for a package clause using +-// the current name as prefix. +-func (c *completer) packageNameCompletions(ctx context.Context, fileURI protocol.DocumentURI, name *ast.Ident) error { +- cursor := int(c.pos - name.NamePos) +- if cursor < 0 || cursor > len(name.Name) { +- return errors.New("cursor is not in package name identifier") +- } - -- cancelMu sync.Mutex -- cancelled bool -- cancel func() +- c.completionContext.packageCompletion = true - -- cleanup func() --} +- prefix := name.Name[:cursor] +- packageSuggestions, err := packageSuggestions(ctx, c.snapshot, fileURI, prefix) +- if err != nil { +- return err +- } - --func (wd *WorkDone) Token() protocol.ProgressToken { -- return wd.token +- for _, pkg := range packageSuggestions { +- c.deepState.enqueue(pkg) +- } +- return nil -} - --func (wd *WorkDone) doCancel() { -- wd.cancelMu.Lock() -- defer wd.cancelMu.Unlock() -- if !wd.cancelled { -- wd.cancel() +-// packageSuggestions returns a list of packages from workspace packages that +-// have the given prefix and are used in the same directory as the given +-// file. This also includes test packages for these packages (_test) and +-// the directory name itself. +-func packageSuggestions(ctx context.Context, snapshot *cache.Snapshot, fileURI protocol.DocumentURI, prefix string) (packages []candidate, err error) { +- active, err := snapshot.WorkspaceMetadata(ctx) +- if err != nil { +- return nil, err - } --} - --// Report reports an update on WorkDone report back to the client. --func (wd *WorkDone) Report(ctx context.Context, message string, percentage float64) { -- ctx = xcontext.Detach(ctx) // progress messages should not be cancelled -- if wd == nil { -- return +- toCandidate := func(name string, score float64) candidate { +- obj := types.NewPkgName(0, nil, name, types.NewPackage("", name)) +- return candidate{obj: obj, name: name, detail: name, score: score} - } -- wd.cancelMu.Lock() -- cancelled := wd.cancelled -- wd.cancelMu.Unlock() -- if cancelled { -- return +- +- matcher := fuzzy.NewMatcher(prefix) +- +- // Always try to suggest a main package +- defer func() { +- if score := float64(matcher.Score("main")); score > 0 { +- packages = append(packages, toCandidate("main", score*lowScore)) +- } +- }() +- +- dirPath := filepath.Dir(fileURI.Path()) +- dirName := filepath.Base(dirPath) +- if !isValidDirName(dirName) { +- return packages, nil - } -- if wd.err != nil || wd.token == nil { -- // Not using the workDone API, so we do nothing. It would be far too spammy -- // to send incremental messages. -- return +- pkgName := convertDirNameToPkgName(dirName) +- +- seenPkgs := make(map[golang.PackageName]struct{}) +- +- // The `go` command by default only allows one package per directory but we +- // support multiple package suggestions since gopls is build system agnostic. +- for _, mp := range active { +- if mp.Name == "main" || mp.Name == "" { +- continue +- } +- if _, ok := seenPkgs[mp.Name]; ok { +- continue +- } +- +- // Only add packages that are previously used in the current directory. +- var relevantPkg bool +- for _, uri := range mp.CompiledGoFiles { +- if filepath.Dir(uri.Path()) == dirPath { +- relevantPkg = true +- break +- } +- } +- if !relevantPkg { +- continue +- } +- +- // Add a found package used in current directory as a high relevance +- // suggestion and the test package for it as a medium relevance +- // suggestion. +- if score := float64(matcher.Score(string(mp.Name))); score > 0 { +- packages = append(packages, toCandidate(string(mp.Name), score*highScore)) +- } +- seenPkgs[mp.Name] = struct{}{} +- +- testPkgName := mp.Name + "_test" +- if _, ok := seenPkgs[testPkgName]; ok || strings.HasSuffix(string(mp.Name), "_test") { +- continue +- } +- if score := float64(matcher.Score(string(testPkgName))); score > 0 { +- packages = append(packages, toCandidate(string(testPkgName), score*stdScore)) +- } +- seenPkgs[testPkgName] = struct{}{} - } -- message = strings.TrimSuffix(message, "\n") -- err := wd.client.Progress(ctx, &protocol.ProgressParams{ -- Token: wd.token, -- Value: &protocol.WorkDoneProgressReport{ -- Kind: "report", -- // Note that in the LSP spec, the value of Cancellable may be changed to -- // control whether the cancel button in the UI is enabled. Since we don't -- // yet use this feature, the value is kept constant here. -- Cancellable: wd.cancel != nil, -- Message: message, -- Percentage: uint32(percentage), -- }, -- }) -- if err != nil { -- event.Error(ctx, "reporting progress", err) +- +- // Add current directory name as a low relevance suggestion. +- if _, ok := seenPkgs[pkgName]; !ok { +- if score := float64(matcher.Score(string(pkgName))); score > 0 { +- packages = append(packages, toCandidate(string(pkgName), score*lowScore)) +- } +- +- testPkgName := pkgName + "_test" +- if score := float64(matcher.Score(string(testPkgName))); score > 0 { +- packages = append(packages, toCandidate(string(testPkgName), score*lowScore)) +- } - } +- +- return packages, nil -} - --// End reports a workdone completion back to the client. --func (wd *WorkDone) End(ctx context.Context, message string) { -- ctx = xcontext.Detach(ctx) // progress messages should not be cancelled -- if wd == nil { -- return -- } -- var err error -- switch { -- case wd.err != nil: -- // There is a prior error. -- case wd.token == nil: -- // We're falling back to message-based reporting. -- err = wd.client.ShowMessage(ctx, &protocol.ShowMessageParams{ -- Type: protocol.Info, -- Message: message, -- }) -- default: -- err = wd.client.Progress(ctx, &protocol.ProgressParams{ -- Token: wd.token, -- Value: &protocol.WorkDoneProgressEnd{ -- Kind: "end", -- Message: message, -- }, -- }) -- } -- if err != nil { -- event.Error(ctx, "ending work", err) +-// isValidDirName checks whether the passed directory name can be used in +-// a package path. Requirements for a package path can be found here: +-// https://golang.org/ref/mod#go-mod-file-ident. +-func isValidDirName(dirName string) bool { +- if dirName == "" { +- return false - } -- if wd.cleanup != nil { -- wd.cleanup() +- +- for i, ch := range dirName { +- if isLetter(ch) || isDigit(ch) { +- continue +- } +- if i == 0 { +- // Directory name can start only with '_'. '.' is not allowed in module paths. +- // '-' and '~' are not allowed because elements of package paths must be +- // safe command-line arguments. +- if ch == '_' { +- continue +- } +- } else { +- // Modules path elements can't end with '.' +- if isAllowedPunctuation(ch) && (i != len(dirName)-1 || ch != '.') { +- continue +- } +- } +- +- return false - } +- return true -} - --// EventWriter writes every incoming []byte to --// event.Print with the operation=generate tag --// to distinguish its logs from others. --type EventWriter struct { -- ctx context.Context -- operation string --} +-// convertDirNameToPkgName converts a valid directory name to a valid package name. +-// It leaves only letters and digits. All letters are mapped to lower case. +-func convertDirNameToPkgName(dirName string) golang.PackageName { +- var buf bytes.Buffer +- for _, ch := range dirName { +- switch { +- case isLetter(ch): +- buf.WriteRune(unicode.ToLower(ch)) - --func NewEventWriter(ctx context.Context, operation string) *EventWriter { -- return &EventWriter{ctx: ctx, operation: operation} +- case buf.Len() != 0 && isDigit(ch): +- buf.WriteRune(ch) +- } +- } +- return golang.PackageName(buf.String()) -} - --func (ew *EventWriter) Write(p []byte) (n int, err error) { -- event.Log(ew.ctx, string(p), tag.Operation.Of(ew.operation)) -- return len(p), nil --} +-// isLetter and isDigit allow only ASCII characters because +-// "Each path element is a non-empty string made of up ASCII letters, +-// ASCII digits, and limited ASCII punctuation" +-// (see https://golang.org/ref/mod#go-mod-file-ident). - --// WorkDoneWriter wraps a workDone handle to provide a Writer interface, --// so that workDone reporting can more easily be hooked into commands. --type WorkDoneWriter struct { -- // In order to implement the io.Writer interface, we must close over ctx. -- ctx context.Context -- wd *WorkDone +-func isLetter(ch rune) bool { +- return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' -} - --func NewWorkDoneWriter(ctx context.Context, wd *WorkDone) *WorkDoneWriter { -- return &WorkDoneWriter{ctx: ctx, wd: wd} +-func isDigit(ch rune) bool { +- return '0' <= ch && ch <= '9' -} - --func (wdw *WorkDoneWriter) Write(p []byte) (n int, err error) { -- wdw.wd.Report(wdw.ctx, string(p), 0) -- // Don't fail just because of a failure to report progress. -- return len(p), nil +-func isAllowedPunctuation(ch rune) bool { +- return ch == '_' || ch == '-' || ch == '~' || ch == '.' -} -diff -urN a/gopls/internal/lsp/progress/progress_test.go b/gopls/internal/lsp/progress/progress_test.go ---- a/gopls/internal/lsp/progress/progress_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/progress/progress_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,161 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/golang/completion/package_test.go b/gopls/internal/golang/completion/package_test.go +--- a/gopls/internal/golang/completion/package_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/completion/package_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,81 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package progress +-package completion - -import ( -- "context" -- "fmt" -- "sync" - "testing" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" +- "golang.org/x/tools/gopls/internal/golang" -) - --type fakeClient struct { -- protocol.Client -- -- token protocol.ProgressToken -- -- mu sync.Mutex -- created, begun, reported, messages, ended int --} -- --func (c *fakeClient) checkToken(token protocol.ProgressToken) { -- if token == nil { -- panic("nil token in progress message") -- } -- if c.token != nil && c.token != token { -- panic(fmt.Errorf("invalid token in progress message: got %v, want %v", token, c.token)) -- } --} -- --func (c *fakeClient) WorkDoneProgressCreate(ctx context.Context, params *protocol.WorkDoneProgressCreateParams) error { -- c.mu.Lock() -- defer c.mu.Unlock() -- c.checkToken(params.Token) -- c.created++ -- return nil --} -- --func (c *fakeClient) Progress(ctx context.Context, params *protocol.ProgressParams) error { -- c.mu.Lock() -- defer c.mu.Unlock() -- c.checkToken(params.Token) -- switch params.Value.(type) { -- case *protocol.WorkDoneProgressBegin: -- c.begun++ -- case *protocol.WorkDoneProgressReport: -- c.reported++ -- case *protocol.WorkDoneProgressEnd: -- c.ended++ -- default: -- panic(fmt.Errorf("unknown progress value %T", params.Value)) +-func TestIsValidDirName(t *testing.T) { +- tests := []struct { +- dirName string +- valid bool +- }{ +- {dirName: "", valid: false}, +- // +- {dirName: "a", valid: true}, +- {dirName: "abcdef", valid: true}, +- {dirName: "AbCdEf", valid: true}, +- // +- {dirName: "1a35", valid: true}, +- {dirName: "a16", valid: true}, +- // +- {dirName: "_a", valid: true}, +- {dirName: "a_", valid: true}, +- // +- {dirName: "~a", valid: false}, +- {dirName: "a~", valid: true}, +- // +- {dirName: "-a", valid: false}, +- {dirName: "a-", valid: true}, +- // +- {dirName: ".a", valid: false}, +- {dirName: "a.", valid: false}, +- // +- {dirName: "a~_b--c.-e", valid: true}, +- {dirName: "~a~_b--c.-e", valid: false}, +- {dirName: "a~_b--c.-e--~", valid: true}, +- {dirName: "a~_b--2134dc42.-e6--~", valid: true}, +- {dirName: "abc`def", valid: false}, +- {dirName: "тест", valid: false}, +- {dirName: "你好", valid: false}, +- } +- for _, tt := range tests { +- valid := isValidDirName(tt.dirName) +- if tt.valid != valid { +- t.Errorf("%s: expected %v, got %v", tt.dirName, tt.valid, valid) +- } - } -- return nil --} -- --func (c *fakeClient) ShowMessage(context.Context, *protocol.ShowMessageParams) error { -- c.mu.Lock() -- defer c.mu.Unlock() -- c.messages++ -- return nil --} -- --func setup(token protocol.ProgressToken) (context.Context, *Tracker, *fakeClient) { -- c := &fakeClient{} -- tracker := NewTracker(c) -- tracker.SetSupportsWorkDoneProgress(true) -- return context.Background(), tracker, c -} - --func TestProgressTracker_Reporting(t *testing.T) { -- for _, test := range []struct { -- name string -- supported bool -- token protocol.ProgressToken -- wantReported, wantCreated, wantBegun, wantEnded int -- wantMessages int +-func TestConvertDirNameToPkgName(t *testing.T) { +- tests := []struct { +- dirName string +- pkgName golang.PackageName - }{ -- { -- name: "unsupported", -- wantMessages: 2, -- }, -- { -- name: "random token", -- supported: true, -- wantCreated: 1, -- wantBegun: 1, -- wantReported: 1, -- wantEnded: 1, -- }, -- { -- name: "string token", -- supported: true, -- token: "token", -- wantBegun: 1, -- wantReported: 1, -- wantEnded: 1, -- }, -- { -- name: "numeric token", -- supported: true, -- token: 1, -- wantReported: 1, -- wantBegun: 1, -- wantEnded: 1, -- }, -- } { -- test := test -- t.Run(test.name, func(t *testing.T) { -- ctx, tracker, client := setup(test.token) -- ctx, cancel := context.WithCancel(ctx) -- defer cancel() -- tracker.supportsWorkDoneProgress = test.supported -- work := tracker.Start(ctx, "work", "message", test.token, nil) -- client.mu.Lock() -- gotCreated, gotBegun := client.created, client.begun -- client.mu.Unlock() -- if gotCreated != test.wantCreated { -- t.Errorf("got %d created tokens, want %d", gotCreated, test.wantCreated) -- } -- if gotBegun != test.wantBegun { -- t.Errorf("got %d work begun, want %d", gotBegun, test.wantBegun) -- } -- // Ignore errors: this is just testing the reporting behavior. -- work.Report(ctx, "report", 50) -- client.mu.Lock() -- gotReported := client.reported -- client.mu.Unlock() -- if gotReported != test.wantReported { -- t.Errorf("got %d progress reports, want %d", gotReported, test.wantCreated) -- } -- work.End(ctx, "done") -- client.mu.Lock() -- gotEnded, gotMessages := client.ended, client.messages -- client.mu.Unlock() -- if gotEnded != test.wantEnded { -- t.Errorf("got %d ended reports, want %d", gotEnded, test.wantEnded) -- } -- if gotMessages != test.wantMessages { -- t.Errorf("got %d messages, want %d", gotMessages, test.wantMessages) -- } -- }) +- {dirName: "a", pkgName: "a"}, +- {dirName: "abcdef", pkgName: "abcdef"}, +- {dirName: "AbCdEf", pkgName: "abcdef"}, +- {dirName: "1a35", pkgName: "a35"}, +- {dirName: "14a35", pkgName: "a35"}, +- {dirName: "a16", pkgName: "a16"}, +- {dirName: "_a", pkgName: "a"}, +- {dirName: "a_", pkgName: "a"}, +- {dirName: "a~", pkgName: "a"}, +- {dirName: "a-", pkgName: "a"}, +- {dirName: "a~_b--c.-e", pkgName: "abce"}, +- {dirName: "a~_b--c.-e--~", pkgName: "abce"}, +- {dirName: "a~_b--2134dc42.-e6--~", pkgName: "ab2134dc42e6"}, - } --} -- --func TestProgressTracker_Cancellation(t *testing.T) { -- for _, token := range []protocol.ProgressToken{nil, 1, "a"} { -- ctx, tracker, _ := setup(token) -- var canceled bool -- cancel := func() { canceled = true } -- work := tracker.Start(ctx, "work", "message", token, cancel) -- if err := tracker.Cancel(work.Token()); err != nil { -- t.Fatal(err) -- } -- if !canceled { -- t.Errorf("tracker.cancel(...): cancel not called") +- for _, tt := range tests { +- pkgName := convertDirNameToPkgName(tt.dirName) +- if tt.pkgName != pkgName { +- t.Errorf("%s: expected %v, got %v", tt.dirName, tt.pkgName, pkgName) +- continue - } - } -} -diff -urN a/gopls/internal/lsp/prompt.go b/gopls/internal/lsp/prompt.go ---- a/gopls/internal/lsp/prompt.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/prompt.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,317 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/golang/completion/postfix_snippets.go b/gopls/internal/golang/completion/postfix_snippets.go +--- a/gopls/internal/golang/completion/postfix_snippets.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/completion/postfix_snippets.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,682 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package lsp +-package completion - -import ( - "context" - "fmt" -- "os" -- "path/filepath" -- "time" +- "go/ast" +- "go/token" +- "go/types" +- "log" +- "reflect" +- "strings" +- "sync" +- "text/template" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/telemetry" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/golang/completion/snippet" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/internal/aliases" - "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/imports" +- "golang.org/x/tools/internal/typesinternal" -) - --// promptTimeout is the amount of time we wait for an ongoing prompt before --// prompting again. This gives the user time to reply. However, at some point --// we must assume that the client is not displaying the prompt, the user is --// ignoring it, or the prompt has been disrupted in some way (e.g. by a gopls --// crash). --const promptTimeout = 24 * time.Hour +-// Postfix snippets are artificial methods that allow the user to +-// compose common operations in an "argument oriented" fashion. For +-// example, instead of "sort.Slice(someSlice, ...)" a user can expand +-// "someSlice.sort!". - --// The following constants are used for testing telemetry integration. --const ( -- TelemetryPromptWorkTitle = "Checking telemetry prompt" // progress notification title, for awaiting in tests -- GoplsConfigDirEnvvar = "GOPLS_CONFIG_DIR" // overridden for testing -- FakeTelemetryModefileEnvvar = "GOPLS_FAKE_TELEMETRY_MODEFILE" // overridden for testing -- TelemetryYes = "Yes, I'd like to help." -- TelemetryNo = "No, thanks." --) +-// postfixTmpl represents a postfix snippet completion candidate. +-type postfixTmpl struct { +- // label is the completion candidate's label presented to the user. +- label string - --// getenv returns the effective environment variable value for the provided --// key, looking up the key in the session environment before falling back on --// the process environment. --func (s *Server) getenv(key string) string { -- if v, ok := s.Options().Env[key]; ok { -- return v +- // details is passed along to the client as the candidate's details. +- details string +- +- // body is the template text. See postfixTmplArgs for details on the +- // facilities available to the template. +- body string +- +- tmpl *template.Template +-} +- +-// postfixTmplArgs are the template execution arguments available to +-// the postfix snippet templates. +-type postfixTmplArgs struct { +- // StmtOK is true if it is valid to replace the selector with a +- // statement. For example: +- // +- // func foo() { +- // bar.sort! // statement okay +- // +- // someMethod(bar.sort!) // statement not okay +- // } +- StmtOK bool +- +- // X is the textual SelectorExpr.X. For example, when completing +- // "foo.bar.print!", "X" is "foo.bar". +- X string +- +- // Obj is the types.Object of SelectorExpr.X, if any. +- Obj types.Object +- +- // Type is the type of "foo.bar" in "foo.bar.print!". +- Type types.Type +- +- // FuncResult are results of the enclosed function +- FuncResults []*types.Var +- +- sel *ast.SelectorExpr +- scope *types.Scope +- snip snippet.Builder +- importIfNeeded func(pkgPath string, scope *types.Scope) (name string, edits []protocol.TextEdit, err error) +- edits []protocol.TextEdit +- qf types.Qualifier +- varNames map[string]bool +- placeholders bool +- currentTabStop int +-} +- +-var postfixTmpls = []postfixTmpl{{ +- label: "sort", +- details: "sort.Slice()", +- body: `{{if and (eq .Kind "slice") .StmtOK -}} +-{{.Import "sort"}}.Slice({{.X}}, func({{.VarName nil "i"}}, {{.VarName nil "j"}} int) bool { +- {{.Cursor}} +-}) +-{{- end}}`, +-}, { +- label: "last", +- details: "s[len(s)-1]", +- body: `{{if and (eq .Kind "slice") .Obj -}} +-{{.X}}[len({{.X}})-1] +-{{- end}}`, +-}, { +- label: "reverse", +- details: "reverse slice", +- body: `{{if and (eq .Kind "slice") .StmtOK -}} +-{{.Import "slices"}}.Reverse({{.X}}) +-{{- end}}`, +-}, { +- label: "range", +- details: "range over slice", +- body: `{{if and (eq .Kind "slice") .StmtOK -}} +-for {{.VarName nil "i" | .Placeholder }}, {{.VarName .ElemType "v" | .Placeholder}} := range {{.X}} { +- {{.Cursor}} +-} +-{{- end}}`, +-}, { +- label: "for", +- details: "range over slice by index", +- body: `{{if and (eq .Kind "slice") .StmtOK -}} +-for {{ .VarName nil "i" | .Placeholder }} := range {{.X}} { +- {{.Cursor}} +-} +-{{- end}}`, +-}, { +- label: "forr", +- details: "range over slice by index and value", +- body: `{{if and (eq .Kind "slice") .StmtOK -}} +-for {{.VarName nil "i" | .Placeholder }}, {{.VarName .ElemType "v" | .Placeholder }} := range {{.X}} { +- {{.Cursor}} +-} +-{{- end}}`, +-}, { +- label: "append", +- details: "append and re-assign slice", +- body: `{{if and (eq .Kind "slice") .StmtOK .Obj -}} +-{{.X}} = append({{.X}}, {{.Cursor}}) +-{{- end}}`, +-}, { +- label: "append", +- details: "append to slice", +- body: `{{if and (eq .Kind "slice") (not .StmtOK) -}} +-append({{.X}}, {{.Cursor}}) +-{{- end}}`, +-}, { +- label: "copy", +- details: "duplicate slice", +- body: `{{if and (eq .Kind "slice") .StmtOK .Obj -}} +-{{$v := (.VarName nil (printf "%sCopy" .X))}}{{$v}} := make([]{{.TypeName .ElemType}}, len({{.X}})) +-copy({{$v}}, {{.X}}) +-{{end}}`, +-}, { +- label: "range", +- details: "range over map", +- body: `{{if and (eq .Kind "map") .StmtOK -}} +-for {{.VarName .KeyType "k" | .Placeholder}}, {{.VarName .ElemType "v" | .Placeholder}} := range {{.X}} { +- {{.Cursor}} +-} +-{{- end}}`, +-}, { +- label: "for", +- details: "range over map by key", +- body: `{{if and (eq .Kind "map") .StmtOK -}} +-for {{.VarName .KeyType "k" | .Placeholder}} := range {{.X}} { +- {{.Cursor}} +-} +-{{- end}}`, +-}, { +- label: "forr", +- details: "range over map by key and value", +- body: `{{if and (eq .Kind "map") .StmtOK -}} +-for {{.VarName .KeyType "k" | .Placeholder}}, {{.VarName .ElemType "v" | .Placeholder}} := range {{.X}} { +- {{.Cursor}} +-} +-{{- end}}`, +-}, { +- label: "clear", +- details: "clear map contents", +- body: `{{if and (eq .Kind "map") .StmtOK -}} +-{{$k := (.VarName .KeyType "k")}}for {{$k}} := range {{.X}} { +- delete({{.X}}, {{$k}}) +-} +-{{end}}`, +-}, { +- label: "keys", +- details: "create slice of keys", +- body: `{{if and (eq .Kind "map") .StmtOK -}} +-{{$keysVar := (.VarName nil "keys")}}{{$keysVar}} := make([]{{.TypeName .KeyType}}, 0, len({{.X}})) +-{{$k := (.VarName .KeyType "k")}}for {{$k}} := range {{.X}} { +- {{$keysVar}} = append({{$keysVar}}, {{$k}}) +-} +-{{end}}`, +-}, { +- label: "range", +- details: "range over channel", +- body: `{{if and (eq .Kind "chan") .StmtOK -}} +-for {{.VarName .ElemType "e" | .Placeholder}} := range {{.X}} { +- {{.Cursor}} +-} +-{{- end}}`, +-}, { +- label: "for", +- details: "range over channel", +- body: `{{if and (eq .Kind "chan") .StmtOK -}} +-for {{.VarName .ElemType "e" | .Placeholder}} := range {{.X}} { +- {{.Cursor}} +-} +-{{- end}}`, +-}, { +- label: "var", +- details: "assign to variables", +- body: `{{if and (eq .Kind "tuple") .StmtOK -}} +-{{$a := .}}{{range $i, $v := .Tuple}}{{if $i}}, {{end}}{{$a.VarName $v.Type $v.Name | $a.Placeholder }}{{end}} := {{.X}} +-{{- end}}`, +-}, { +- label: "var", +- details: "assign to variable", +- body: `{{if and (ne .Kind "tuple") .StmtOK -}} +-{{.VarName .Type "" | .Placeholder }} := {{.X}} +-{{- end}}`, +-}, { +- label: "print", +- details: "print to stdout", +- body: `{{if and (ne .Kind "tuple") .StmtOK -}} +-{{.Import "fmt"}}.Printf("{{.EscapeQuotes .X}}: %v\n", {{.X}}) +-{{- end}}`, +-}, { +- label: "print", +- details: "print to stdout", +- body: `{{if and (eq .Kind "tuple") .StmtOK -}} +-{{.Import "fmt"}}.Println({{.X}}) +-{{- end}}`, +-}, { +- label: "split", +- details: "split string", +- body: `{{if (eq (.TypeName .Type) "string") -}} +-{{.Import "strings"}}.Split({{.X}}, "{{.Cursor}}") +-{{- end}}`, +-}, { +- label: "join", +- details: "join string slice", +- body: `{{if and (eq .Kind "slice") (eq (.TypeName .ElemType) "string") -}} +-{{.Import "strings"}}.Join({{.X}}, "{{.Cursor}}") +-{{- end}}`, +-}, { +- label: "ifnotnil", +- details: "if expr != nil", +- body: `{{if and (or (eq .Kind "pointer") (eq .Kind "chan") (eq .Kind "signature") (eq .Kind "interface") (eq .Kind "map") (eq .Kind "slice")) .StmtOK -}} +-if {{.X}} != nil { +- {{.Cursor}} +-} +-{{- end}}`, +-}, { +- label: "len", +- details: "len(s)", +- body: `{{if (eq .Kind "slice" "map" "array" "chan") -}} +-len({{.X}}) +-{{- end}}`, +-}, { +- label: "iferr", +- details: "check error and return", +- body: `{{if and .StmtOK (eq (.TypeName .Type) "error") -}} +-{{- $errName := (or (and .IsIdent .X) "err") -}} +-if {{if not .IsIdent}}err := {{.X}}; {{end}}{{$errName}} != nil { +- return {{$a := .}}{{range $i, $v := .FuncResults}} +- {{- if $i}}, {{end -}} +- {{- if eq ($a.TypeName $v.Type) "error" -}} +- {{$a.Placeholder $errName}} +- {{- else -}} +- {{$a.Zero $v.Type}} +- {{- end -}} +- {{end}} +-} +-{{end}}`, +-}, { +- label: "iferr", +- details: "check error and return", +- body: `{{if and .StmtOK (eq .Kind "tuple") (len .Tuple) (eq (.TypeName .TupleLast.Type) "error") -}} +-{{- $a := . -}} +-if {{range $i, $v := .Tuple}}{{if $i}}, {{end}}{{if and (eq ($a.TypeName $v.Type) "error") (eq (inc $i) (len $a.Tuple))}}err{{else}}_{{end}}{{end}} := {{.X -}} +-; err != nil { +- return {{range $i, $v := .FuncResults}} +- {{- if $i}}, {{end -}} +- {{- if eq ($a.TypeName $v.Type) "error" -}} +- {{$a.Placeholder "err"}} +- {{- else -}} +- {{$a.Zero $v.Type}} +- {{- end -}} +- {{end}} +-} +-{{end}}`, +-}, { +- // variferr snippets use nested placeholders, as described in +- // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#snippet_syntax, +- // so that users can wrap the returned error without modifying the error +- // variable name. +- label: "variferr", +- details: "assign variables and check error", +- body: `{{if and .StmtOK (eq .Kind "tuple") (len .Tuple) (eq (.TypeName .TupleLast.Type) "error") -}} +-{{- $a := . -}} +-{{- $errName := "err" -}} +-{{- range $i, $v := .Tuple -}} +- {{- if $i}}, {{end -}} +- {{- if and (eq ($a.TypeName $v.Type) "error") (eq (inc $i) (len $a.Tuple)) -}} +- {{$errName | $a.SpecifiedPlaceholder (len $a.Tuple)}} +- {{- else -}} +- {{$a.VarName $v.Type $v.Name | $a.Placeholder}} +- {{- end -}} +-{{- end}} := {{.X}} +-if {{$errName | $a.SpecifiedPlaceholder (len $a.Tuple)}} != nil { +- return {{range $i, $v := .FuncResults}} +- {{- if $i}}, {{end -}} +- {{- if eq ($a.TypeName $v.Type) "error" -}} +- {{$errName | $a.SpecifiedPlaceholder (len $a.Tuple) | +- $a.SpecifiedPlaceholder (inc (len $a.Tuple))}} +- {{- else -}} +- {{$a.Zero $v.Type}} +- {{- end -}} +- {{end}} +-} +-{{end}}`, +-}, { +- label: "variferr", +- details: "assign variables and check error", +- body: `{{if and .StmtOK (eq (.TypeName .Type) "error") -}} +-{{- $a := . -}} +-{{- $errName := .VarName nil "err" -}} +-{{$errName | $a.SpecifiedPlaceholder 1}} := {{.X}} +-if {{$errName | $a.SpecifiedPlaceholder 1}} != nil { +- return {{range $i, $v := .FuncResults}} +- {{- if $i}}, {{end -}} +- {{- if eq ($a.TypeName $v.Type) "error" -}} +- {{$errName | $a.SpecifiedPlaceholder 1 | $a.SpecifiedPlaceholder 2}} +- {{- else -}} +- {{$a.Zero $v.Type}} +- {{- end -}} +- {{end}} +-} +-{{end}}`, +-}} +- +-// Cursor indicates where the client's cursor should end up after the +-// snippet is done. +-func (a *postfixTmplArgs) Cursor() string { +- return "$0" +-} +- +-// Placeholder indicate a tab stop with the placeholder string, the order +-// of tab stops is the same as the order of invocation +-func (a *postfixTmplArgs) Placeholder(placeholder string) string { +- if !a.placeholders { +- placeholder = "" - } -- return os.Getenv(key) +- return fmt.Sprintf("${%d:%s}", a.nextTabStop(), placeholder) -} - --// configDir returns the root of the gopls configuration dir. By default this --// is os.UserConfigDir/gopls, but it may be overridden for tests. --func (s *Server) configDir() (string, error) { -- if d := s.getenv(GoplsConfigDirEnvvar); d != "" { -- return d, nil +-// nextTabStop returns the next tab stop index for a new placeholder. +-func (a *postfixTmplArgs) nextTabStop() int { +- // Tab stops start from 1, so increment before returning. +- a.currentTabStop++ +- return a.currentTabStop +-} +- +-// SpecifiedPlaceholder indicate a specified tab stop with the placeholder string. +-// Sometimes the same tab stop appears in multiple places and their numbers +-// need to be specified. e.g. variferr +-func (a *postfixTmplArgs) SpecifiedPlaceholder(tabStop int, placeholder string) string { +- if !a.placeholders { +- placeholder = "" - } -- userDir, err := os.UserConfigDir() +- return fmt.Sprintf("${%d:%s}", tabStop, placeholder) +-} +- +-// Import makes sure the package corresponding to path is imported, +-// returning the identifier to use to refer to the package. +-func (a *postfixTmplArgs) Import(path string) (string, error) { +- name, edits, err := a.importIfNeeded(path, a.scope) - if err != nil { -- return "", err +- return "", fmt.Errorf("couldn't import %q: %w", path, err) - } -- return filepath.Join(userDir, "gopls"), nil +- a.edits = append(a.edits, edits...) +- +- return name, nil -} - --// telemetryMode returns the current effective telemetry mode. --// By default this is x/telemetry.Mode(), but it may be overridden for tests. --func (s *Server) telemetryMode() string { -- if fake := s.getenv(FakeTelemetryModefileEnvvar); fake != "" { -- if data, err := os.ReadFile(fake); err == nil { -- return string(data) -- } -- return "off" -- } -- return telemetry.Mode() +-func (a *postfixTmplArgs) EscapeQuotes(v string) string { +- return strings.ReplaceAll(v, `"`, `\\"`) -} - --// setTelemetryMode sets the current telemetry mode. --// By default this calls x/telemetry.SetMode, but it may be overridden for --// tests. --func (s *Server) setTelemetryMode(mode string) error { -- if fake := s.getenv(FakeTelemetryModefileEnvvar); fake != "" { -- return os.WriteFile(fake, []byte(mode), 0666) +-// ElemType returns the Elem() type of xType, if applicable. +-func (a *postfixTmplArgs) ElemType() types.Type { +- type hasElem interface{ Elem() types.Type } // Array, Chan, Map, Pointer, Slice +- if e, ok := a.Type.Underlying().(hasElem); ok { +- return e.Elem() - } -- return telemetry.SetMode(mode) +- return nil -} - --// maybePromptForTelemetry checks for the right conditions, and then prompts --// the user to ask if they want to enable Go telemetry uploading. If the user --// responds 'Yes', the telemetry mode is set to "on". --// --// The actual conditions for prompting are defensive, erring on the side of not --// prompting. --// If enabled is false, this will not prompt the user in any condition, --// but will send work progress reports to help testing. --func (s *Server) maybePromptForTelemetry(ctx context.Context, enabled bool) { -- if s.Options().VerboseWorkDoneProgress { -- work := s.progress.Start(ctx, TelemetryPromptWorkTitle, "Checking if gopls should prompt about telemetry...", nil, nil) -- defer work.End(ctx, "Done.") -- } +-// Kind returns the underlying kind of type, e.g. "slice", "struct", +-// etc. +-func (a *postfixTmplArgs) Kind() string { +- t := reflect.TypeOf(a.Type.Underlying()) +- return strings.ToLower(strings.TrimPrefix(t.String(), "*types.")) +-} - -- if !enabled { // check this after the work progress message for testing. -- return // prompt is disabled -- } +-// KeyType returns the type of X's key. KeyType panics if X is not a +-// map. +-func (a *postfixTmplArgs) KeyType() types.Type { +- return a.Type.Underlying().(*types.Map).Key() +-} - -- if s.telemetryMode() == "on" { -- // Telemetry is already on -- nothing to ask about. -- return +-// Tuple returns the tuple result vars if the type of X is tuple. +-func (a *postfixTmplArgs) Tuple() []*types.Var { +- tuple, _ := a.Type.(*types.Tuple) +- if tuple == nil { +- return nil - } - -- errorf := func(format string, args ...any) { -- err := fmt.Errorf(format, args...) -- event.Error(ctx, "telemetry prompt failed", err) +- typs := make([]*types.Var, 0, tuple.Len()) +- for i := 0; i < tuple.Len(); i++ { +- typs = append(typs, tuple.At(i)) - } +- return typs +-} - -- // Only prompt if we can read/write the prompt config file. -- configDir, err := s.configDir() -- if err != nil { -- errorf("unable to determine config dir: %v", err) -- return +-// TupleLast returns the last tuple result vars if the type of X is tuple. +-func (a *postfixTmplArgs) TupleLast() *types.Var { +- tuple, _ := a.Type.(*types.Tuple) +- if tuple == nil { +- return nil - } +- if tuple.Len() == 0 { +- return nil +- } +- return tuple.At(tuple.Len() - 1) +-} - -- var ( -- promptDir = filepath.Join(configDir, "prompt") // prompt configuration directory -- promptFile = filepath.Join(promptDir, "telemetry") // telemetry prompt file -- ) +-// TypeName returns the textual representation of type t. +-func (a *postfixTmplArgs) TypeName(t types.Type) (string, error) { +- if t == nil || t == types.Typ[types.Invalid] { +- return "", fmt.Errorf("invalid type: %v", t) +- } +- return types.TypeString(t, a.qf), nil +-} - -- // prompt states, to be written to the prompt file -- const ( -- pYes = "yes" // user said yes -- pNo = "no" // user said no -- pPending = "pending" // current prompt is still pending -- pFailed = "failed" // prompt was asked but failed -- ) -- validStates := map[string]bool{ -- pYes: true, -- pNo: true, -- pPending: true, -- pFailed: true, +-// Zero return the zero value representation of type t +-func (a *postfixTmplArgs) Zero(t types.Type) string { +- return formatZeroValue(t, a.qf) +-} +- +-func (a *postfixTmplArgs) IsIdent() bool { +- _, ok := a.sel.X.(*ast.Ident) +- return ok +-} +- +-// VarName returns a suitable variable name for the type t. If t +-// implements the error interface, "err" is used. If t is not a named +-// type then nonNamedDefault is used. Otherwise a name is made by +-// abbreviating the type name. If the resultant name is already in +-// scope, an integer is appended to make a unique name. +-func (a *postfixTmplArgs) VarName(t types.Type, nonNamedDefault string) string { +- if t == nil { +- t = types.Typ[types.Invalid] - } - -- // parse the current prompt file -- var ( -- state string -- attempts = 0 // number of times we've asked already -- ) -- if content, err := os.ReadFile(promptFile); err == nil { -- if _, err := fmt.Sscanf(string(content), "%s %d", &state, &attempts); err == nil && validStates[state] { -- if state == pYes || state == pNo { -- // Prompt has been answered. Nothing to do. -- return -- } -- } else { -- state, attempts = "", 0 -- errorf("malformed prompt result %q", string(content)) -- } -- } else if !os.IsNotExist(err) { -- errorf("reading prompt file: %v", err) -- // Something went wrong. Since we don't know how many times we've asked the -- // prompt, err on the side of not spamming. -- return +- var name string +- // go/types predicates are undefined on types.Typ[types.Invalid]. +- if !types.Identical(t, types.Typ[types.Invalid]) && types.Implements(t, errorIntf) { +- name = "err" +- } else if !is[*types.Named](aliases.Unalias(typesinternal.Unpointer(t))) { +- name = nonNamedDefault - } - -- if attempts >= 5 { -- // We've tried asking enough; give up. -- return +- if name == "" { +- name = types.TypeString(t, func(p *types.Package) string { +- return "" +- }) +- name = abbreviateTypeName(name) - } -- if attempts == 0 { -- // First time asking the prompt; we may need to make the prompt dir. -- if err := os.MkdirAll(promptDir, 0777); err != nil { -- errorf("creating prompt dir: %v", err) -- return +- +- if dot := strings.LastIndex(name, "."); dot > -1 { +- name = name[dot+1:] +- } +- +- uniqueName := name +- for i := 2; ; i++ { +- if s, _ := a.scope.LookupParent(uniqueName, token.NoPos); s == nil && !a.varNames[uniqueName] { +- break - } +- uniqueName = fmt.Sprintf("%s%d", name, i) - } - -- // Acquire the lock and write "pending" to the prompt file before actually -- // prompting. -- // -- // This ensures that the prompt file is writeable, and that we increment the -- // attempt counter before we prompt, so that we don't end up in a failure -- // mode where we keep prompting and then failing to record the response. +- a.varNames[uniqueName] = true - -- release, ok, err := acquireLockFile(promptFile) -- if err != nil { -- errorf("acquiring prompt: %v", err) -- return -- } -- if !ok { -- // Another prompt is currently pending. +- return uniqueName +-} +- +-func (c *completer) addPostfixSnippetCandidates(ctx context.Context, sel *ast.SelectorExpr) { +- if !c.opts.postfix { - return - } -- defer release() - -- attempts++ +- initPostfixRules() - -- pendingContent := []byte(fmt.Sprintf("%s %d", pPending, attempts)) -- if err := os.WriteFile(promptFile, pendingContent, 0666); err != nil { -- errorf("writing pending state: %v", err) +- if sel == nil || sel.Sel == nil { - return - } - -- var prompt = `Go telemetry helps us improve Go by periodically sending anonymous metrics and crash reports to the Go team. Learn more at https://telemetry.go.dev/privacy. -- --Would you like to enable Go telemetry? --` -- if s.Options().LinkifyShowMessage { -- prompt = `Go telemetry helps us improve Go by periodically sending anonymous metrics and crash reports to the Go team. Learn more at [telemetry.go.dev/privacy](https://telemetry.go.dev/privacy). -- --Would you like to enable Go telemetry? --` -- } -- // TODO(rfindley): investigate a "tell me more" action in combination with ShowDocument. -- params := &protocol.ShowMessageRequestParams{ -- Type: protocol.Info, -- Message: prompt, -- Actions: []protocol.MessageActionItem{ -- {Title: TelemetryYes}, -- {Title: TelemetryNo}, -- }, +- selType := c.pkg.TypesInfo().TypeOf(sel.X) +- if selType == nil { +- return - } - -- item, err := s.client.ShowMessageRequest(ctx, params) -- if err != nil { -- errorf("ShowMessageRequest failed: %v", err) -- // Defensive: ensure item == nil for the logic below. -- item = nil +- // Skip empty tuples since there is no value to operate on. +- if tuple, ok := selType.(*types.Tuple); ok && tuple == nil { +- return - } - -- message := func(typ protocol.MessageType, msg string) { -- if err := s.client.ShowMessage(ctx, &protocol.ShowMessageParams{ -- Type: typ, -- Message: msg, -- }); err != nil { -- errorf("ShowMessage(unrecognize) failed: %v", err) +- tokFile := c.pkg.FileSet().File(c.pos) +- +- // Only replace sel with a statement if sel is already a statement. +- var stmtOK bool +- for i, n := range c.path { +- if n == sel && i < len(c.path)-1 { +- switch p := c.path[i+1].(type) { +- case *ast.ExprStmt: +- stmtOK = true +- case *ast.AssignStmt: +- // In cases like: +- // +- // foo.<> +- // bar = 123 +- // +- // detect that "foo." makes up the entire statement since the +- // apparent selector spans lines. +- stmtOK = safetoken.Line(tokFile, c.pos) < safetoken.Line(tokFile, p.TokPos) +- } +- break - } - } - -- result := pFailed -- if item == nil { -- // e.g. dialog was dismissed -- errorf("no response") -- } else { -- // Response matches MessageActionItem.Title. -- switch item.Title { -- case TelemetryYes: -- result = pYes -- if err := s.setTelemetryMode("on"); err == nil { -- message(protocol.Info, telemetryOnMessage(s.Options().LinkifyShowMessage)) -- } else { -- errorf("enabling telemetry failed: %v", err) -- msg := fmt.Sprintf("Failed to enable Go telemetry: %v\nTo enable telemetry manually, please run `go run golang.org/x/telemetry/cmd/gotelemetry@latest on`", err) -- message(protocol.Error, msg) +- var funcResults []*types.Var +- if c.enclosingFunc != nil { +- results := c.enclosingFunc.sig.Results() +- if results != nil { +- funcResults = make([]*types.Var, results.Len()) +- for i := 0; i < results.Len(); i++ { +- funcResults[i] = results.At(i) - } -- -- case TelemetryNo: -- result = pNo -- default: -- errorf("unrecognized response %q", item.Title) -- message(protocol.Error, fmt.Sprintf("Unrecognized response %q", item.Title)) - } - } -- resultContent := []byte(fmt.Sprintf("%s %d", result, attempts)) -- if err := os.WriteFile(promptFile, resultContent, 0666); err != nil { -- errorf("error writing result state to prompt file: %v", err) -- } --} -- --func telemetryOnMessage(linkify bool) string { -- format := `Thank you. Telemetry uploading is now enabled. - --To disable telemetry uploading, run %s. --` -- var runCmd = "`go run golang.org/x/telemetry/cmd/gotelemetry@latest off`" -- if linkify { -- runCmd = "[gotelemetry off](https://golang.org/x/telemetry/cmd/gotelemetry)" +- scope := c.pkg.Types().Scope().Innermost(c.pos) +- if scope == nil { +- return - } -- return fmt.Sprintf(format, runCmd) --} - --// acquireLockFile attempts to "acquire a lock" for writing to path. --// --// This is achieved by creating an exclusive lock file at .lock. Lock --// files expire after a period, at which point acquireLockFile will remove and --// recreate the lock file. --// --// acquireLockFile fails if path is in a directory that doesn't exist. --func acquireLockFile(path string) (func(), bool, error) { -- lockpath := path + ".lock" -- fi, err := os.Stat(lockpath) -- if err == nil { -- if time.Since(fi.ModTime()) > promptTimeout { -- _ = os.Remove(lockpath) // ignore error -- } else { -- return nil, false, nil -- } -- } else if !os.IsNotExist(err) { -- return nil, false, fmt.Errorf("statting lockfile: %v", err) -- } +- // afterDot is the position after selector dot, e.g. "|" in +- // "foo.|print". +- afterDot := sel.Sel.Pos() - -- f, err := os.OpenFile(lockpath, os.O_CREATE|os.O_EXCL, 0666) -- if err != nil { -- if os.IsExist(err) { -- return nil, false, nil +- // We must detect dangling selectors such as: +- // +- // foo.<> +- // bar +- // +- // and adjust afterDot so that we don't mistakenly delete the +- // newline thinking "bar" is part of our selector. +- if startLine := safetoken.Line(tokFile, sel.Pos()); startLine != safetoken.Line(tokFile, afterDot) { +- if safetoken.Line(tokFile, c.pos) != startLine { +- return - } -- return nil, false, fmt.Errorf("creating lockfile: %v", err) -- } -- fi, err = f.Stat() -- if err != nil { -- return nil, false, err +- afterDot = c.pos - } -- release := func() { -- _ = f.Close() // ignore error -- fi2, err := os.Stat(lockpath) -- if err == nil && os.SameFile(fi, fi2) { -- // Only clean up the lockfile if it's the same file we created. -- // Otherwise, our lock has expired and something else has the lock. -- // -- // There's a race here, in that the file could have changed since the -- // stat above; but given that we've already waited 24h this is extremely -- // unlikely, and acceptable. -- _ = os.Remove(lockpath) +- +- for _, rule := range postfixTmpls { +- // When completing foo.print<>, "print" is naturally overwritten, +- // but we need to also remove "foo." so the snippet has a clean +- // slate. +- edits, err := c.editText(sel.Pos(), afterDot, "") +- if err != nil { +- event.Error(ctx, "error calculating postfix edits", err) +- return - } -- } -- return release, true, nil --} -diff -urN a/gopls/internal/lsp/prompt_test.go b/gopls/internal/lsp/prompt_test.go ---- a/gopls/internal/lsp/prompt_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/prompt_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,82 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package lsp +- tmplArgs := postfixTmplArgs{ +- X: golang.FormatNode(c.pkg.FileSet(), sel.X), +- StmtOK: stmtOK, +- Obj: exprObj(c.pkg.TypesInfo(), sel.X), +- Type: selType, +- FuncResults: funcResults, +- sel: sel, +- qf: c.qf, +- importIfNeeded: c.importIfNeeded, +- scope: scope, +- varNames: make(map[string]bool), +- placeholders: c.opts.placeholders, +- } - --import ( -- "path/filepath" -- "sync" -- "sync/atomic" -- "testing" --) +- // Feed the template straight into the snippet builder. This +- // allows templates to build snippets as they are executed. +- err = rule.tmpl.Execute(&tmplArgs.snip, &tmplArgs) +- if err != nil { +- event.Error(ctx, "error executing postfix template", err) +- continue +- } - --func TestAcquireFileLock(t *testing.T) { -- name := filepath.Join(t.TempDir(), "config.json") +- if strings.TrimSpace(tmplArgs.snip.String()) == "" { +- continue +- } - -- const concurrency = 100 -- var acquired int32 -- var releasers [concurrency]func() -- defer func() { -- for _, r := range releasers { -- if r != nil { -- r() -- } +- score := c.matcher.Score(rule.label) +- if score <= 0 { +- continue - } -- }() - -- var wg sync.WaitGroup -- for i := range releasers { -- i := i -- wg.Add(1) -- go func() { -- defer wg.Done() +- c.items = append(c.items, CompletionItem{ +- Label: rule.label + "!", +- Detail: rule.details, +- Score: float64(score) * 0.01, +- Kind: protocol.SnippetCompletion, +- snippet: &tmplArgs.snip, +- AdditionalTextEdits: append(edits, tmplArgs.edits...), +- }) +- } +-} - -- release, ok, err := acquireLockFile(name) +-var postfixRulesOnce sync.Once +- +-func initPostfixRules() { +- postfixRulesOnce.Do(func() { +- var idx int +- for _, rule := range postfixTmpls { +- var err error +- rule.tmpl, err = template.New("postfix_snippet").Funcs(template.FuncMap{ +- "inc": inc, +- }).Parse(rule.body) - if err != nil { -- t.Errorf("Acquire failed: %v", err) -- return -- } -- if ok { -- atomic.AddInt32(&acquired, 1) -- releasers[i] = release +- log.Panicf("error parsing postfix snippet template: %v", err) - } -- }() -- } -- -- wg.Wait() +- postfixTmpls[idx] = rule +- idx++ +- } +- postfixTmpls = postfixTmpls[:idx] +- }) +-} - -- if acquired != 1 { -- t.Errorf("Acquire succeeded %d times, expected exactly 1", acquired) -- } +-func inc(i int) int { +- return i + 1 -} - --func TestReleaseAndAcquireFileLock(t *testing.T) { -- name := filepath.Join(t.TempDir(), "config.json") +-// importIfNeeded returns the package identifier and any necessary +-// edits to import package pkgPath. +-func (c *completer) importIfNeeded(pkgPath string, scope *types.Scope) (string, []protocol.TextEdit, error) { +- defaultName := imports.ImportPathToAssumedName(pkgPath) - -- acquire := func() (func(), bool) { -- t.Helper() -- release, ok, err := acquireLockFile(name) -- if err != nil { -- t.Fatal(err) +- // Check if file already imports pkgPath. +- for _, s := range c.file.Imports { +- // TODO(adonovan): what if pkgPath has a vendor/ suffix? +- // This may be the cause of go.dev/issue/56291. +- if string(metadata.UnquoteImportPath(s)) == pkgPath { +- if s.Name == nil { +- return defaultName, nil, nil +- } +- if s.Name.Name != "_" { +- return s.Name.Name, nil, nil +- } - } -- return release, ok - } - -- release, ok := acquire() -- if !ok { -- t.Fatal("failed to Acquire") -- } -- if release2, ok := acquire(); ok { -- release() -- release2() -- t.Fatalf("Acquire succeeded unexpectedly") +- // Give up if the package's name is already in use by another object. +- if _, obj := scope.LookupParent(defaultName, token.NoPos); obj != nil { +- return "", nil, fmt.Errorf("import name %q of %q already in use", defaultName, pkgPath) - } - -- release() -- release3, ok := acquire() -- release3() -- if !ok { -- t.Fatalf("failed to Acquire") +- edits, err := c.importEdits(&importInfo{ +- importPath: pkgPath, +- }) +- if err != nil { +- return "", nil, err - } +- +- return defaultName, edits, nil -} -diff -urN a/gopls/internal/lsp/protocol/codeactionkind.go b/gopls/internal/lsp/protocol/codeactionkind.go ---- a/gopls/internal/lsp/protocol/codeactionkind.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/protocol/codeactionkind.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,11 +0,0 @@ +diff -urN a/gopls/internal/golang/completion/printf.go b/gopls/internal/golang/completion/printf.go +--- a/gopls/internal/golang/completion/printf.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/completion/printf.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,172 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package protocol +-package completion - --// Custom code actions that aren't explicitly stated in LSP --const ( -- GoTest CodeActionKind = "goTest" -- // TODO: Add GoGenerate, RegenerateCgo etc. +-import ( +- "go/ast" +- "go/constant" +- "go/types" +- "strconv" +- "strings" +- "unicode/utf8" -) -diff -urN a/gopls/internal/lsp/protocol/context.go b/gopls/internal/lsp/protocol/context.go ---- a/gopls/internal/lsp/protocol/context.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/protocol/context.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,45 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package protocol +-// printfArgKind returns the expected objKind when completing a +-// printf-like operand. call is the printf-like function call, and +-// argIdx is the index of call.Args being completed. +-func printfArgKind(info *types.Info, call *ast.CallExpr, argIdx int) objKind { +- // Printf-like function name must end in "f". +- fn := exprObj(info, call.Fun) +- if fn == nil || !strings.HasSuffix(fn.Name(), "f") { +- return kindAny +- } - --import ( -- "bytes" -- "context" +- sig, _ := fn.Type().Underlying().(*types.Signature) +- if sig == nil { +- return kindAny +- } - -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/core" -- "golang.org/x/tools/internal/event/export" -- "golang.org/x/tools/internal/event/label" -- "golang.org/x/tools/internal/xcontext" --) +- // Must be variadic and take at least two params. +- numParams := sig.Params().Len() +- if !sig.Variadic() || numParams < 2 || argIdx < numParams-1 { +- return kindAny +- } - --type contextKey int +- // Param preceding variadic args must be a (format) string. +- if !types.Identical(sig.Params().At(numParams-2).Type(), types.Typ[types.String]) { +- return kindAny +- } - --const ( -- clientKey = contextKey(iota) --) +- // Format string must be a constant. +- strArg := info.Types[call.Args[numParams-2]].Value +- if strArg == nil || strArg.Kind() != constant.String { +- return kindAny +- } - --func WithClient(ctx context.Context, client Client) context.Context { -- return context.WithValue(ctx, clientKey, client) +- return formatOperandKind(constant.StringVal(strArg), argIdx-(numParams-1)+1) -} - --func LogEvent(ctx context.Context, ev core.Event, lm label.Map, mt MessageType) context.Context { -- client, ok := ctx.Value(clientKey).(Client) -- if !ok { -- return ctx -- } -- buf := &bytes.Buffer{} -- p := export.Printer{} -- p.WriteEvent(buf, ev, lm) -- msg := &LogMessageParams{Type: mt, Message: buf.String()} -- // Handle messages generated via event.Error, which won't have a level Label. -- if event.IsError(ev) { -- msg.Type = Error +-// formatOperandKind returns the objKind corresponding to format's +-// operandIdx'th operand. +-func formatOperandKind(format string, operandIdx int) objKind { +- var ( +- prevOperandIdx int +- kind = kindAny +- ) +- for { +- i := strings.Index(format, "%") +- if i == -1 { +- break +- } +- +- var operands []formatOperand +- format, operands = parsePrintfVerb(format[i+1:], prevOperandIdx) +- +- // Check if any this verb's operands correspond to our target +- // operandIdx. +- for _, v := range operands { +- if v.idx == operandIdx { +- if kind == kindAny { +- kind = v.kind +- } else if v.kind != kindAny { +- // If multiple verbs refer to the same operand, take the +- // intersection of their kinds. +- kind &= v.kind +- } +- } +- +- prevOperandIdx = v.idx +- } - } -- // TODO(adonovan): the goroutine here could cause log -- // messages to be delivered out of order! Use a queue. -- go client.LogMessage(xcontext.Detach(ctx), msg) -- return ctx +- return kind -} -diff -urN a/gopls/internal/lsp/protocol/doc.go b/gopls/internal/lsp/protocol/doc.go ---- a/gopls/internal/lsp/protocol/doc.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/protocol/doc.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,18 +0,0 @@ --// Copyright 2018 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --//go:generate go run ./generate +-type formatOperand struct { +- // idx is the one-based printf operand index. +- idx int +- // kind is a mask of expected kinds of objects for this operand. +- kind objKind +-} - --// Package protocol contains the structs that map directly to the --// request and response messages of the Language Server Protocol. --// --// It is a literal transcription, with unmodified comments, and only the changes --// required to make it go code. --// Names are uppercased to export them. --// All fields have JSON tags added to correct the names. --// Fields marked with a ? are also marked as "omitempty" --// Fields that are "|| null" are made pointers --// Fields that are string or number are left as string --// Fields that are type "number" are made float64 --package protocol -diff -urN a/gopls/internal/lsp/protocol/enums.go b/gopls/internal/lsp/protocol/enums.go ---- a/gopls/internal/lsp/protocol/enums.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/protocol/enums.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,174 +0,0 @@ --// Copyright 2018 The Go Authors. All rights reserved. +-// parsePrintfVerb parses the leading printf verb in f. The opening +-// "%" must already be trimmed from f. prevIdx is the previous +-// operand's index, or zero if this is the first verb. The format +-// string is returned with the leading verb removed. Multiple operands +-// can be returned in the case of dynamic widths such as "%*.*f". +-func parsePrintfVerb(f string, prevIdx int) (string, []formatOperand) { +- var verbs []formatOperand +- +- addVerb := func(k objKind) { +- verbs = append(verbs, formatOperand{ +- idx: prevIdx + 1, +- kind: k, +- }) +- prevIdx++ +- } +- +- for len(f) > 0 { +- // Trim first rune off of f so we are guaranteed to make progress. +- r, l := utf8.DecodeRuneInString(f) +- f = f[l:] +- +- // We care about three things: +- // 1. The verb, which maps directly to object kind. +- // 2. Explicit operand indices like "%[2]s". +- // 3. Dynamic widths using "*". +- switch r { +- case '%': +- return f, nil +- case '*': +- addVerb(kindInt) +- continue +- case '[': +- // Parse operand index as in "%[2]s". +- i := strings.Index(f, "]") +- if i == -1 { +- return f, nil +- } +- +- idx, err := strconv.Atoi(f[:i]) +- f = f[i+1:] +- if err != nil { +- return f, nil +- } +- +- prevIdx = idx - 1 +- continue +- case 'v', 'T': +- addVerb(kindAny) +- case 't': +- addVerb(kindBool) +- case 'c', 'd', 'o', 'O', 'U': +- addVerb(kindInt) +- case 'e', 'E', 'f', 'F', 'g', 'G': +- addVerb(kindFloat | kindComplex) +- case 'b': +- addVerb(kindInt | kindFloat | kindComplex | kindBytes) +- case 'q', 's': +- addVerb(kindString | kindBytes | kindStringer | kindError) +- case 'x', 'X': +- // Omit kindStringer and kindError though technically allowed. +- addVerb(kindString | kindBytes | kindInt | kindFloat | kindComplex) +- case 'p': +- addVerb(kindPtr | kindSlice) +- case 'w': +- addVerb(kindError) +- case '+', '-', '#', ' ', '.', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': +- // Flag or numeric width/precision value. +- continue +- default: +- // Assume unrecognized rune is a custom fmt.Formatter verb. +- addVerb(kindAny) +- } +- +- if len(verbs) > 0 { +- break +- } +- } +- +- return f, verbs +-} +diff -urN a/gopls/internal/golang/completion/printf_test.go b/gopls/internal/golang/completion/printf_test.go +--- a/gopls/internal/golang/completion/printf_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/completion/printf_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,72 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package protocol +-package completion - -import ( - "fmt" +- "testing" -) - --var ( -- namesTextDocumentSyncKind [int(Incremental) + 1]string -- namesMessageType [int(Log) + 1]string -- namesFileChangeType [int(Deleted) + 1]string -- namesWatchKind [int(WatchDelete) + 1]string -- namesCompletionTriggerKind [int(TriggerForIncompleteCompletions) + 1]string -- namesDiagnosticSeverity [int(SeverityHint) + 1]string -- namesDiagnosticTag [int(Unnecessary) + 1]string -- namesCompletionItemKind [int(TypeParameterCompletion) + 1]string -- namesInsertTextFormat [int(SnippetTextFormat) + 1]string -- namesDocumentHighlightKind [int(Write) + 1]string -- namesSymbolKind [int(TypeParameter) + 1]string -- namesTextDocumentSaveReason [int(FocusOut) + 1]string --) +-func TestFormatOperandKind(t *testing.T) { +- cases := []struct { +- f string +- idx int +- kind objKind +- }{ +- {"", 1, kindAny}, +- {"%", 1, kindAny}, +- {"%%%", 1, kindAny}, +- {"%[1", 1, kindAny}, +- {"%[?%s", 2, kindAny}, +- {"%[abc]v", 1, kindAny}, - --func init() { -- namesTextDocumentSyncKind[int(None)] = "None" -- namesTextDocumentSyncKind[int(Full)] = "Full" -- namesTextDocumentSyncKind[int(Incremental)] = "Incremental" +- {"%v", 1, kindAny}, +- {"%T", 1, kindAny}, +- {"%t", 1, kindBool}, +- {"%d", 1, kindInt}, +- {"%c", 1, kindInt}, +- {"%o", 1, kindInt}, +- {"%O", 1, kindInt}, +- {"%U", 1, kindInt}, +- {"%e", 1, kindFloat | kindComplex}, +- {"%E", 1, kindFloat | kindComplex}, +- {"%f", 1, kindFloat | kindComplex}, +- {"%F", 1, kindFloat | kindComplex}, +- {"%g", 1, kindFloat | kindComplex}, +- {"%G", 1, kindFloat | kindComplex}, +- {"%b", 1, kindInt | kindFloat | kindComplex | kindBytes}, +- {"%q", 1, kindString | kindBytes | kindStringer | kindError}, +- {"%s", 1, kindString | kindBytes | kindStringer | kindError}, +- {"%x", 1, kindString | kindBytes | kindInt | kindFloat | kindComplex}, +- {"%X", 1, kindString | kindBytes | kindInt | kindFloat | kindComplex}, +- {"%p", 1, kindPtr | kindSlice}, +- {"%w", 1, kindError}, - -- namesMessageType[int(Error)] = "Error" -- namesMessageType[int(Warning)] = "Warning" -- namesMessageType[int(Info)] = "Info" -- namesMessageType[int(Log)] = "Log" +- {"%1.2f", 1, kindFloat | kindComplex}, +- {"%*f", 1, kindInt}, +- {"%*f", 2, kindFloat | kindComplex}, +- {"%*.*f", 1, kindInt}, +- {"%*.*f", 2, kindInt}, +- {"%*.*f", 3, kindFloat | kindComplex}, +- {"%[3]*.[2]*[1]f", 1, kindFloat | kindComplex}, +- {"%[3]*.[2]*[1]f", 2, kindInt}, +- {"%[3]*.[2]*[1]f", 3, kindInt}, - -- namesFileChangeType[int(Created)] = "Created" -- namesFileChangeType[int(Changed)] = "Changed" -- namesFileChangeType[int(Deleted)] = "Deleted" +- {"foo %% %d", 1, kindInt}, +- {"%#-12.34f", 1, kindFloat | kindComplex}, +- {"% d", 1, kindInt}, - -- namesWatchKind[int(WatchCreate)] = "WatchCreate" -- namesWatchKind[int(WatchChange)] = "WatchChange" -- namesWatchKind[int(WatchDelete)] = "WatchDelete" +- {"%s %[1]X %d", 1, kindString | kindBytes}, +- {"%s %[1]X %d", 2, kindInt}, +- } - -- namesCompletionTriggerKind[int(Invoked)] = "Invoked" -- namesCompletionTriggerKind[int(TriggerCharacter)] = "TriggerCharacter" -- namesCompletionTriggerKind[int(TriggerForIncompleteCompletions)] = "TriggerForIncompleteCompletions" +- for _, c := range cases { +- t.Run(fmt.Sprintf("%q#%d", c.f, c.idx), func(t *testing.T) { +- if got := formatOperandKind(c.f, c.idx); got != c.kind { +- t.Errorf("expected %d (%[1]b), got %d (%[2]b)", c.kind, got) +- } +- }) +- } +-} +diff -urN a/gopls/internal/golang/completion/snippet/snippet_builder.go b/gopls/internal/golang/completion/snippet/snippet_builder.go +--- a/gopls/internal/golang/completion/snippet/snippet_builder.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/completion/snippet/snippet_builder.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,111 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- namesDiagnosticSeverity[int(SeverityError)] = "Error" -- namesDiagnosticSeverity[int(SeverityWarning)] = "Warning" -- namesDiagnosticSeverity[int(SeverityInformation)] = "Information" -- namesDiagnosticSeverity[int(SeverityHint)] = "Hint" +-// Package snippet implements the specification for the LSP snippet format. +-// +-// Snippets are "tab stop" templates returned as an optional attribute of LSP +-// completion candidates. As the user presses tab, they cycle through a series of +-// tab stops defined in the snippet. Each tab stop can optionally have placeholder +-// text, which can be pre-selected by editors. For a full description of syntax +-// and features, see "Snippet Syntax" at +-// https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_completion. +-// +-// A typical snippet looks like "foo(${1:i int}, ${2:s string})". +-package snippet - -- namesDiagnosticTag[int(Unnecessary)] = "Unnecessary" +-import ( +- "fmt" +- "strings" +-) - -- namesCompletionItemKind[int(TextCompletion)] = "text" -- namesCompletionItemKind[int(MethodCompletion)] = "method" -- namesCompletionItemKind[int(FunctionCompletion)] = "func" -- namesCompletionItemKind[int(ConstructorCompletion)] = "constructor" -- namesCompletionItemKind[int(FieldCompletion)] = "field" -- namesCompletionItemKind[int(VariableCompletion)] = "var" -- namesCompletionItemKind[int(ClassCompletion)] = "type" -- namesCompletionItemKind[int(InterfaceCompletion)] = "interface" -- namesCompletionItemKind[int(ModuleCompletion)] = "package" -- namesCompletionItemKind[int(PropertyCompletion)] = "property" -- namesCompletionItemKind[int(UnitCompletion)] = "unit" -- namesCompletionItemKind[int(ValueCompletion)] = "value" -- namesCompletionItemKind[int(EnumCompletion)] = "enum" -- namesCompletionItemKind[int(KeywordCompletion)] = "keyword" -- namesCompletionItemKind[int(SnippetCompletion)] = "snippet" -- namesCompletionItemKind[int(ColorCompletion)] = "color" -- namesCompletionItemKind[int(FileCompletion)] = "file" -- namesCompletionItemKind[int(ReferenceCompletion)] = "reference" -- namesCompletionItemKind[int(FolderCompletion)] = "folder" -- namesCompletionItemKind[int(EnumMemberCompletion)] = "enumMember" -- namesCompletionItemKind[int(ConstantCompletion)] = "const" -- namesCompletionItemKind[int(StructCompletion)] = "struct" -- namesCompletionItemKind[int(EventCompletion)] = "event" -- namesCompletionItemKind[int(OperatorCompletion)] = "operator" -- namesCompletionItemKind[int(TypeParameterCompletion)] = "typeParam" +-// A Builder is used to build an LSP snippet piecemeal. +-// The zero value is ready to use. Do not copy a non-zero Builder. +-type Builder struct { +- // currentTabStop is the index of the previous tab stop. The +- // next tab stop will be currentTabStop+1. +- currentTabStop int +- sb strings.Builder +-} - -- namesInsertTextFormat[int(PlainTextTextFormat)] = "PlainText" -- namesInsertTextFormat[int(SnippetTextFormat)] = "Snippet" +-// Escape characters defined in https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_completion under "Grammar". +-var replacer = strings.NewReplacer( +- `\`, `\\`, +- `}`, `\}`, +- `$`, `\$`, +-) - -- namesDocumentHighlightKind[int(Text)] = "Text" -- namesDocumentHighlightKind[int(Read)] = "Read" -- namesDocumentHighlightKind[int(Write)] = "Write" +-func (b *Builder) WriteText(s string) { +- replacer.WriteString(&b.sb, s) +-} - -- namesSymbolKind[int(File)] = "File" -- namesSymbolKind[int(Module)] = "Module" -- namesSymbolKind[int(Namespace)] = "Namespace" -- namesSymbolKind[int(Package)] = "Package" -- namesSymbolKind[int(Class)] = "Class" -- namesSymbolKind[int(Method)] = "Method" -- namesSymbolKind[int(Property)] = "Property" -- namesSymbolKind[int(Field)] = "Field" -- namesSymbolKind[int(Constructor)] = "Constructor" -- namesSymbolKind[int(Enum)] = "Enum" -- namesSymbolKind[int(Interface)] = "Interface" -- namesSymbolKind[int(Function)] = "Function" -- namesSymbolKind[int(Variable)] = "Variable" -- namesSymbolKind[int(Constant)] = "Constant" -- namesSymbolKind[int(String)] = "String" -- namesSymbolKind[int(Number)] = "Number" -- namesSymbolKind[int(Boolean)] = "Boolean" -- namesSymbolKind[int(Array)] = "Array" -- namesSymbolKind[int(Object)] = "Object" -- namesSymbolKind[int(Key)] = "Key" -- namesSymbolKind[int(Null)] = "Null" -- namesSymbolKind[int(EnumMember)] = "EnumMember" -- namesSymbolKind[int(Struct)] = "Struct" -- namesSymbolKind[int(Event)] = "Event" -- namesSymbolKind[int(Operator)] = "Operator" -- namesSymbolKind[int(TypeParameter)] = "TypeParameter" +-func (b *Builder) PrependText(s string) { +- rawSnip := b.String() +- b.sb.Reset() +- b.WriteText(s) +- b.sb.WriteString(rawSnip) +-} - -- namesTextDocumentSaveReason[int(Manual)] = "Manual" -- namesTextDocumentSaveReason[int(AfterDelay)] = "AfterDelay" -- namesTextDocumentSaveReason[int(FocusOut)] = "FocusOut" +-func (b *Builder) Write(data []byte) (int, error) { +- return b.sb.Write(data) -} - --func formatEnum(f fmt.State, c rune, i int, names []string, unknown string) { -- s := "" -- if i >= 0 && i < len(names) { -- s = names[i] -- } -- if s != "" { -- fmt.Fprint(f, s) -- } else { -- fmt.Fprintf(f, "%s(%d)", unknown, i) +-// WritePlaceholder writes a tab stop and placeholder value to the Builder. +-// The callback style allows for creating nested placeholders. To write an +-// empty tab stop, provide a nil callback. +-func (b *Builder) WritePlaceholder(fn func(*Builder)) { +- fmt.Fprintf(&b.sb, "${%d:", b.nextTabStop()) +- if fn != nil { +- fn(b) - } +- b.sb.WriteByte('}') -} - --func (e TextDocumentSyncKind) Format(f fmt.State, c rune) { -- formatEnum(f, c, int(e), namesTextDocumentSyncKind[:], "TextDocumentSyncKind") +-// WriteFinalTabstop marks where cursor ends up after the user has +-// cycled through all the normal tab stops. It defaults to the +-// character after the snippet. +-func (b *Builder) WriteFinalTabstop() { +- fmt.Fprint(&b.sb, "$0") -} - --func (e MessageType) Format(f fmt.State, c rune) { -- formatEnum(f, c, int(e), namesMessageType[:], "MessageType") --} +-// In addition to '\', '}', and '$', snippet choices also use '|' and ',' as +-// meta characters, so they must be escaped within the choices. +-var choiceReplacer = strings.NewReplacer( +- `\`, `\\`, +- `}`, `\}`, +- `$`, `\$`, +- `|`, `\|`, +- `,`, `\,`, +-) - --func (e FileChangeType) Format(f fmt.State, c rune) { -- formatEnum(f, c, int(e), namesFileChangeType[:], "FileChangeType") +-// WriteChoice writes a tab stop and list of text choices to the Builder. +-// The user's editor will prompt the user to choose one of the choices. +-func (b *Builder) WriteChoice(choices []string) { +- fmt.Fprintf(&b.sb, "${%d|", b.nextTabStop()) +- for i, c := range choices { +- if i != 0 { +- b.sb.WriteByte(',') +- } +- choiceReplacer.WriteString(&b.sb, c) +- } +- b.sb.WriteString("|}") -} - --func (e CompletionTriggerKind) Format(f fmt.State, c rune) { -- formatEnum(f, c, int(e), namesCompletionTriggerKind[:], "CompletionTriggerKind") +-// String returns the built snippet string. +-func (b *Builder) String() string { +- return b.sb.String() -} - --func (e DiagnosticSeverity) Format(f fmt.State, c rune) { -- formatEnum(f, c, int(e), namesDiagnosticSeverity[:], "DiagnosticSeverity") +-// Clone returns a copy of b. +-func (b *Builder) Clone() *Builder { +- var clone Builder +- clone.sb.WriteString(b.String()) +- return &clone -} - --func (e DiagnosticTag) Format(f fmt.State, c rune) { -- formatEnum(f, c, int(e), namesDiagnosticTag[:], "DiagnosticTag") +-// nextTabStop returns the next tab stop index for a new placeholder. +-func (b *Builder) nextTabStop() int { +- // Tab stops start from 1, so increment before returning. +- b.currentTabStop++ +- return b.currentTabStop -} +diff -urN a/gopls/internal/golang/completion/snippet/snippet_builder_test.go b/gopls/internal/golang/completion/snippet/snippet_builder_test.go +--- a/gopls/internal/golang/completion/snippet/snippet_builder_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/completion/snippet/snippet_builder_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,62 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func (e CompletionItemKind) Format(f fmt.State, c rune) { -- formatEnum(f, c, int(e), namesCompletionItemKind[:], "CompletionItemKind") --} +-package snippet - --func (e InsertTextFormat) Format(f fmt.State, c rune) { -- formatEnum(f, c, int(e), namesInsertTextFormat[:], "InsertTextFormat") --} +-import ( +- "testing" +-) - --func (e DocumentHighlightKind) Format(f fmt.State, c rune) { -- formatEnum(f, c, int(e), namesDocumentHighlightKind[:], "DocumentHighlightKind") --} +-func TestSnippetBuilder(t *testing.T) { +- expect := func(expected string, fn func(*Builder)) { +- t.Helper() - --func (e SymbolKind) Format(f fmt.State, c rune) { -- formatEnum(f, c, int(e), namesSymbolKind[:], "SymbolKind") --} +- var b Builder +- fn(&b) +- if got := b.String(); got != expected { +- t.Errorf("got %q, expected %q", got, expected) +- } +- } - --func (e TextDocumentSaveReason) Format(f fmt.State, c rune) { -- formatEnum(f, c, int(e), namesTextDocumentSaveReason[:], "TextDocumentSaveReason") +- expect("", func(b *Builder) {}) +- +- expect(`hi { \} \$ | " , / \\`, func(b *Builder) { +- b.WriteText(`hi { } $ | " , / \`) +- }) +- +- expect("${1:}", func(b *Builder) { +- b.WritePlaceholder(nil) +- }) +- +- expect("hi ${1:there}", func(b *Builder) { +- b.WriteText("hi ") +- b.WritePlaceholder(func(b *Builder) { +- b.WriteText("there") +- }) +- }) +- +- expect(`${1:id=${2:{your id\}}}`, func(b *Builder) { +- b.WritePlaceholder(func(b *Builder) { +- b.WriteText("id=") +- b.WritePlaceholder(func(b *Builder) { +- b.WriteText("{your id}") +- }) +- }) +- }) +- +- expect(`${1|one,{ \} \$ \| " \, / \\,three|}`, func(b *Builder) { +- b.WriteChoice([]string{"one", `{ } $ | " , / \`, "three"}) +- }) +- +- expect("$0 hello", func(b *Builder) { +- b.WriteFinalTabstop() +- b.WriteText(" hello") +- }) +- +- expect(`prepended \$5 ${1:} hello`, func(b *Builder) { +- b.WritePlaceholder(nil) +- b.WriteText(" hello") +- b.PrependText("prepended $5 ") +- }) -} -diff -urN a/gopls/internal/lsp/protocol/generate/generate.go b/gopls/internal/lsp/protocol/generate/generate.go ---- a/gopls/internal/lsp/protocol/generate/generate.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/protocol/generate/generate.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,121 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/golang/completion/snippet.go b/gopls/internal/golang/completion/snippet.go +--- a/gopls/internal/golang/completion/snippet.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/completion/snippet.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,126 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --//go:build go1.19 --// +build go1.19 -- --package main +-package completion - -import ( -- "bytes" -- "fmt" -- "log" -- "strings" +- "go/ast" +- +- "golang.org/x/tools/gopls/internal/golang/completion/snippet" +- "golang.org/x/tools/gopls/internal/util/safetoken" -) - --// a newType is a type that needs a name and a definition --// These are the various types that the json specification doesn't name --type newType struct { -- name string -- properties Properties // for struct/literal types -- items []*Type // for other types ("and", "tuple") -- line int -- kind string // Or, And, Tuple, Lit, Map -- typ *Type --} +-// structFieldSnippet calculates the snippet for struct literal field names. +-func (c *completer) structFieldSnippet(cand candidate, detail string, snip *snippet.Builder) { +- if !c.wantStructFieldCompletions() { +- return +- } - --func generateDoc(out *bytes.Buffer, doc string) { -- if doc == "" { +- // If we are in a deep completion then we can't be completing a field +- // name (e.g. "Foo{f<>}" completing to "Foo{f.Bar}" should not generate +- // a snippet). +- if len(cand.path) > 0 { - return - } - -- if !strings.Contains(doc, "\n") { -- fmt.Fprintf(out, "// %s\n", doc) +- clInfo := c.enclosingCompositeLiteral +- +- // If we are already in a key-value expression, we don't want a snippet. +- if clInfo.kv != nil { - return - } -- var list bool -- for _, line := range strings.Split(doc, "\n") { -- // Lists in metaModel.json start with a dash. -- // To make a go doc list they have to be preceded -- // by a blank line, and indented. -- // (see type TextDccumentFilter in protocol.go) -- if len(line) > 0 && line[0] == '-' { -- if !list { -- list = true -- fmt.Fprintf(out, "//\n") -- } -- fmt.Fprintf(out, "// %s\n", line) -- } else { -- if len(line) == 0 { -- list = false -- } -- fmt.Fprintf(out, "// %s\n", line) +- +- // A plain snippet turns "Foo{Ba<>" into "Foo{Bar: <>". +- snip.WriteText(": ") +- snip.WritePlaceholder(func(b *snippet.Builder) { +- // A placeholder snippet turns "Foo{Ba<>" into "Foo{Bar: <*int*>". +- if c.opts.placeholders { +- b.WriteText(detail) - } +- }) +- +- fset := c.pkg.FileSet() +- +- // If the cursor position is on a different line from the literal's opening brace, +- // we are in a multiline literal. Ignore line directives. +- if safetoken.StartPosition(fset, c.pos).Line != safetoken.StartPosition(fset, clInfo.cl.Lbrace).Line { +- snip.WriteText(",") - } -} - --// decide if a property is optional, and if it needs a * --// return ",omitempty" if it is optional, and "*" if it needs a pointer --func propStar(name string, t NameType, gotype string) (string, string) { -- var opt, star string -- if t.Optional { -- star = "*" -- opt = ",omitempty" +-// functionCallSnippet calculates the snippet for function calls. +-// +-// Callers should omit the suffix of type parameters that are +-// constrained by the argument types, to avoid offering completions +-// that contain instantiations that are redundant because of type +-// inference, such as f[int](1) for func f[T any](x T). +-func (c *completer) functionCallSnippet(name string, tparams, params []string, snip *snippet.Builder) { +- if !c.opts.completeFunctionCalls { +- snip.WriteText(name) +- return - } -- if strings.HasPrefix(gotype, "[]") || strings.HasPrefix(gotype, "map[") { -- star = "" // passed by reference, so no need for * -- } else { -- switch gotype { -- case "bool", "uint32", "int32", "string", "interface{}": -- star = "" // gopls compatibility if t.Optional -- } -- } -- ostar, oopt := star, opt -- if newStar, ok := goplsStar[prop{name, t.Name}]; ok { -- switch newStar { -- case nothing: -- star, opt = "", "" -- case wantStar: -- star, opt = "*", "" -- case wantOpt: -- star, opt = "", ",omitempty" -- case wantOptStar: -- star, opt = "*", ",omitempty" -- } -- if star == ostar && opt == oopt { // no change -- log.Printf("goplsStar[ {%q, %q} ](%d) useless %s/%s %s/%s", name, t.Name, t.Line, ostar, star, oopt, opt) +- +- // If there is no suffix then we need to reuse existing call parens +- // "()" if present. If there is an identifier suffix then we always +- // need to include "()" since we don't overwrite the suffix. +- if c.surrounding != nil && c.surrounding.Suffix() == "" && len(c.path) > 1 { +- // If we are the left side (i.e. "Fun") part of a call expression, +- // we don't want a snippet since there are already parens present. +- switch n := c.path[1].(type) { +- case *ast.CallExpr: +- // The Lparen != Rparen check detects fudged CallExprs we +- // inserted when fixing the AST. In this case, we do still need +- // to insert the calling "()" parens. +- if n.Fun == c.path[0] && n.Lparen != n.Rparen { +- return +- } +- case *ast.SelectorExpr: +- if len(c.path) > 2 { +- if call, ok := c.path[2].(*ast.CallExpr); ok && call.Fun == c.path[1] && call.Lparen != call.Rparen { +- return +- } +- } - } -- usedGoplsStar[prop{name, t.Name}] = true - } - -- return opt, star --} +- snip.WriteText(name) - --func goName(s string) string { -- // Go naming conventions -- if strings.HasSuffix(s, "Id") { -- s = s[:len(s)-len("Id")] + "ID" -- } else if strings.HasSuffix(s, "Uri") { -- s = s[:len(s)-3] + "URI" -- } else if s == "uri" { -- s = "URI" -- } else if s == "id" { -- s = "ID" +- if len(tparams) > 0 { +- snip.WriteText("[") +- if c.opts.placeholders { +- for i, tp := range tparams { +- if i > 0 { +- snip.WriteText(", ") +- } +- snip.WritePlaceholder(func(b *snippet.Builder) { +- b.WriteText(tp) +- }) +- } +- } else { +- snip.WritePlaceholder(nil) +- } +- snip.WriteText("]") - } - -- // renames for temporary GOPLS compatibility -- if news := goplsType[s]; news != "" { -- usedGoplsType[s] = true -- s = news -- } -- // Names beginning _ are not exported -- if strings.HasPrefix(s, "_") { -- s = strings.Replace(s, "_", "X", 1) -- } -- if s != "string" { // base types are unchanged (textDocuemnt/diagnostic) -- // Title is deprecated, but a) s is only one word, b) replacement is too heavy-weight -- s = strings.Title(s) +- snip.WriteText("(") +- +- if c.opts.placeholders { +- // A placeholder snippet turns "someFun<>" into "someFunc(<*i int*>, *s string*)". +- for i, p := range params { +- if i > 0 { +- snip.WriteText(", ") +- } +- snip.WritePlaceholder(func(b *snippet.Builder) { +- b.WriteText(p) +- }) +- } +- } else { +- // A plain snippet turns "someFun<>" into "someFunc(<>)". +- if len(params) > 0 { +- snip.WritePlaceholder(nil) +- } - } -- return s +- +- snip.WriteText(")") -} -diff -urN a/gopls/internal/lsp/protocol/generate/main.go b/gopls/internal/lsp/protocol/generate/main.go ---- a/gopls/internal/lsp/protocol/generate/main.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/protocol/generate/main.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,390 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/golang/completion/statements.go b/gopls/internal/golang/completion/statements.go +--- a/gopls/internal/golang/completion/statements.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/completion/statements.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,420 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --//go:build go1.19 --// +build go1.19 -- --// The generate command generates Go declarations from VSCode's --// description of the Language Server Protocol. --// --// To run it, type 'go generate' in the parent (protocol) directory. --package main -- --// see https://github.com/golang/go/issues/61217 for discussion of an issue +-package completion - -import ( -- "bytes" -- "encoding/json" -- "flag" - "fmt" -- "go/format" -- "log" -- "os" -- "os/exec" -- "path/filepath" +- "go/ast" +- "go/token" +- "go/types" - "strings" +- +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/golang/completion/snippet" +- "golang.org/x/tools/gopls/internal/protocol" -) - --const vscodeRepo = "https://github.com/microsoft/vscode-languageserver-node" +-// addStatementCandidates adds full statement completion candidates +-// appropriate for the current context. +-func (c *completer) addStatementCandidates() { +- c.addErrCheck() +- c.addAssignAppend() +- c.addReturnZeroValues() +-} - --// lspGitRef names a branch or tag in vscodeRepo. --// It implicitly determines the protocol version of the LSP used by gopls. --// For example, tag release/protocol/3.17.3 of the repo defines protocol version 3.17.0. --// (Point releases are reflected in the git tag version even when they are cosmetic --// and don't change the protocol.) --var lspGitRef = "release/protocol/3.17.4-next.2" +-// addAssignAppend offers a completion candidate of the form: +-// +-// someSlice = append(someSlice, ) +-// +-// It will offer the "append" completion in either of two situations: +-// +-// 1. Position is in RHS of assign, prefix matches "append", and +-// corresponding LHS object is a slice. For example, +-// "foo = ap<>" completes to "foo = append(foo, )". +-// +-// 2. Prefix is an ident or selector in an *ast.ExprStmt (i.e. +-// beginning of statement), and our best matching candidate is a +-// slice. For example: "foo.ba" completes to "foo.bar = append(foo.bar, )". +-func (c *completer) addAssignAppend() { +- if len(c.path) < 3 { +- return +- } - --var ( -- repodir = flag.String("d", "", "directory containing clone of "+vscodeRepo) -- outputdir = flag.String("o", ".", "output directory") -- // PJW: not for real code -- cmpdir = flag.String("c", "", "directory of earlier code") -- doboth = flag.String("b", "", "generate and compare") -- lineNumbers = flag.Bool("l", false, "add line numbers to generated output") --) +- ident, _ := c.path[0].(*ast.Ident) +- if ident == nil { +- return +- } - --func main() { -- log.SetFlags(log.Lshortfile) // log file name and line number, not time -- flag.Parse() +- var ( +- // sliceText is the full name of our slice object, e.g. "s.abc" in +- // "s.abc = app<>". +- sliceText string +- // needsLHS is true if we need to prepend the LHS slice name and +- // "=" to our candidate. +- needsLHS = false +- fset = c.pkg.FileSet() +- ) - -- processinline() --} +- switch n := c.path[1].(type) { +- case *ast.AssignStmt: +- // We are already in an assignment. Make sure our prefix matches "append". +- if c.matcher.Score("append") <= 0 { +- return +- } - --func processinline() { -- // A local repository may be specified during debugging. -- // The default behavior is to download the canonical version. -- if *repodir == "" { -- tmpdir, err := os.MkdirTemp("", "") -- if err != nil { -- log.Fatal(err) +- exprIdx := exprAtPos(c.pos, n.Rhs) +- if exprIdx == len(n.Rhs) || exprIdx > len(n.Lhs)-1 { +- return - } -- defer os.RemoveAll(tmpdir) // ignore error - -- // Clone the repository. -- cmd := exec.Command("git", "clone", "--quiet", "--depth=1", "-c", "advice.detachedHead=false", vscodeRepo, "--branch="+lspGitRef, "--single-branch", tmpdir) -- cmd.Stdout = os.Stderr -- cmd.Stderr = os.Stderr -- if err := cmd.Run(); err != nil { -- log.Fatal(err) +- lhsType := c.pkg.TypesInfo().TypeOf(n.Lhs[exprIdx]) +- if lhsType == nil { +- return - } - -- *repodir = tmpdir -- } else { -- lspGitRef = fmt.Sprintf("(not git, local dir %s)", *repodir) +- // Make sure our corresponding LHS object is a slice. +- if _, isSlice := lhsType.Underlying().(*types.Slice); !isSlice { +- return +- } +- +- // The name or our slice is whatever's in the LHS expression. +- sliceText = golang.FormatNode(fset, n.Lhs[exprIdx]) +- case *ast.SelectorExpr: +- // Make sure we are a selector at the beginning of a statement. +- if _, parentIsExprtStmt := c.path[2].(*ast.ExprStmt); !parentIsExprtStmt { +- return +- } +- +- // So far we only know the first part of our slice name. For +- // example in "s.a<>" we only know our slice begins with "s." +- // since the user could still be typing. +- sliceText = golang.FormatNode(fset, n.X) + "." +- needsLHS = true +- case *ast.ExprStmt: +- needsLHS = true +- default: +- return - } - -- model := parse(filepath.Join(*repodir, "protocol/metaModel.json")) +- var ( +- label string +- snip snippet.Builder +- score = highScore +- ) - -- findTypeNames(model) -- generateOutput(model) +- if needsLHS { +- // Offer the long form assign + append candidate if our best +- // candidate is a slice. +- bestItem := c.topCandidate() +- if bestItem == nil || !bestItem.isSlice { +- return +- } - -- fileHdr = fileHeader(model) +- // Don't rank the full form assign + append candidate above the +- // slice itself. +- score = bestItem.Score - 0.01 - -- // write the files -- writeclient() -- writeserver() -- writeprotocol() -- writejsons() +- // Fill in rest of sliceText now that we have the object name. +- sliceText += bestItem.Label - -- checkTables() --} +- // Fill in the candidate's LHS bits. +- label = fmt.Sprintf("%s = ", bestItem.Label) +- snip.WriteText(label) +- } - --// common file header for output files --var fileHdr string +- snip.WriteText(fmt.Sprintf("append(%s, ", sliceText)) +- snip.WritePlaceholder(nil) +- snip.WriteText(")") - --func writeclient() { -- out := new(bytes.Buffer) -- fmt.Fprintln(out, fileHdr) -- out.WriteString( -- `import ( -- "context" -- "encoding/json" +- c.items = append(c.items, CompletionItem{ +- Label: label + fmt.Sprintf("append(%s, )", sliceText), +- Kind: protocol.FunctionCompletion, +- Score: score, +- snippet: &snip, +- }) +-} - -- "golang.org/x/tools/internal/jsonrpc2" --) --`) -- out.WriteString("type Client interface {\n") -- for _, k := range cdecls.keys() { -- out.WriteString(cdecls[k]) -- } -- out.WriteString("}\n\n") -- out.WriteString("func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier, r jsonrpc2.Request) (bool, error) {\n") -- out.WriteString("\tswitch r.Method() {\n") -- for _, k := range ccases.keys() { -- out.WriteString(ccases[k]) -- } -- out.WriteString(("\tdefault:\n\t\treturn false, nil\n\t}\n}\n\n")) -- for _, k := range cfuncs.keys() { -- out.WriteString(cfuncs[k]) +-// topCandidate returns the strictly highest scoring candidate +-// collected so far. If the top two candidates have the same score, +-// nil is returned. +-func (c *completer) topCandidate() *CompletionItem { +- var bestItem, secondBestItem *CompletionItem +- for i := range c.items { +- if bestItem == nil || c.items[i].Score > bestItem.Score { +- bestItem = &c.items[i] +- } else if secondBestItem == nil || c.items[i].Score > secondBestItem.Score { +- secondBestItem = &c.items[i] +- } - } - -- x, err := format.Source(out.Bytes()) -- if err != nil { -- os.WriteFile("/tmp/a.go", out.Bytes(), 0644) -- log.Fatalf("tsclient.go: %v", err) +- // If secondBestItem has the same score, bestItem isn't +- // the strict best. +- if secondBestItem != nil && secondBestItem.Score == bestItem.Score { +- return nil - } - -- if err := os.WriteFile(filepath.Join(*outputdir, "tsclient.go"), x, 0644); err != nil { -- log.Fatalf("%v writing tsclient.go", err) -- } +- return bestItem -} - --func writeserver() { -- out := new(bytes.Buffer) -- fmt.Fprintln(out, fileHdr) -- out.WriteString( -- `import ( -- "context" -- "encoding/json" -- -- "golang.org/x/tools/internal/jsonrpc2" --) --`) -- out.WriteString("type Server interface {\n") -- for _, k := range sdecls.keys() { -- out.WriteString(sdecls[k]) +-// addErrCheck offers a completion candidate of the form: +-// +-// if err != nil { +-// return nil, err +-// } +-// +-// In the case of test functions, it offers a completion candidate of the form: +-// +-// if err != nil { +-// t.Fatal(err) +-// } +-// +-// The position must be in a function that returns an error, and the +-// statement preceding the position must be an assignment where the +-// final LHS object is an error. addErrCheck will synthesize +-// zero values as necessary to make the return statement valid. +-func (c *completer) addErrCheck() { +- if len(c.path) < 2 || c.enclosingFunc == nil || !c.opts.placeholders { +- return - } -- out.WriteString(` NonstandardRequest(ctx context.Context, method string, params interface{}) (interface{}, error) --} - --func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, r jsonrpc2.Request) (bool, error) { -- switch r.Method() { --`) -- for _, k := range scases.keys() { -- out.WriteString(scases[k]) +- var ( +- errorType = types.Universe.Lookup("error").Type() +- result = c.enclosingFunc.sig.Results() +- testVar = getTestVar(c.enclosingFunc, c.pkg) +- isTest = testVar != "" +- doesNotReturnErr = result.Len() == 0 || !types.Identical(result.At(result.Len()-1).Type(), errorType) +- ) +- // Make sure our enclosing function is a Test func or returns an error. +- if !isTest && doesNotReturnErr { +- return - } -- out.WriteString(("\tdefault:\n\t\treturn false, nil\n\t}\n}\n\n")) -- for _, k := range sfuncs.keys() { -- out.WriteString(sfuncs[k]) +- +- prevLine := prevStmt(c.pos, c.path) +- if prevLine == nil { +- return - } -- out.WriteString(`func (s *serverDispatcher) NonstandardRequest(ctx context.Context, method string, params interface{}) (interface{}, error) { -- var result interface{} -- if err := s.sender.Call(ctx, method, params, &result); err != nil { -- return nil, err +- +- // Make sure our preceding statement was as assignment. +- assign, _ := prevLine.(*ast.AssignStmt) +- if assign == nil || len(assign.Lhs) == 0 { +- return - } -- return result, nil --} --`) - -- x, err := format.Source(out.Bytes()) -- if err != nil { -- os.WriteFile("/tmp/a.go", out.Bytes(), 0644) -- log.Fatalf("tsserver.go: %v", err) +- lastAssignee := assign.Lhs[len(assign.Lhs)-1] +- +- // Make sure the final assignee is an error. +- if !types.Identical(c.pkg.TypesInfo().TypeOf(lastAssignee), errorType) { +- return - } - -- if err := os.WriteFile(filepath.Join(*outputdir, "tsserver.go"), x, 0644); err != nil { -- log.Fatalf("%v writing tsserver.go", err) +- var ( +- // errVar is e.g. "err" in "foo, err := bar()". +- errVar = golang.FormatNode(c.pkg.FileSet(), lastAssignee) +- +- // Whether we need to include the "if" keyword in our candidate. +- needsIf = true +- ) +- +- // If the returned error from the previous statement is "_", it is not a real object. +- // If we don't have an error, and the function signature takes a testing.TB that is either ignored +- // or an "_", then we also can't call t.Fatal(err). +- if errVar == "_" { +- return - } --} - --func writeprotocol() { -- out := new(bytes.Buffer) -- fmt.Fprintln(out, fileHdr) -- out.WriteString("import \"encoding/json\"\n\n") +- // Below we try to detect if the user has already started typing "if +- // err" so we can replace what they've typed with our complete +- // statement. +- switch n := c.path[0].(type) { +- case *ast.Ident: +- switch c.path[1].(type) { +- case *ast.ExprStmt: +- // This handles: +- // +- // f, err := os.Open("foo") +- // i<> - -- // The followiing are unneeded, but make the new code a superset of the old -- hack := func(newer, existing string) { -- if _, ok := types[existing]; !ok { -- log.Fatalf("types[%q] not found", existing) +- // Make sure they are typing "if". +- if c.matcher.Score("if") <= 0 { +- return +- } +- case *ast.IfStmt: +- // This handles: +- // +- // f, err := os.Open("foo") +- // if er<> +- +- // Make sure they are typing the error's name. +- if c.matcher.Score(errVar) <= 0 { +- return +- } +- +- needsIf = false +- default: +- return - } -- types[newer] = strings.Replace(types[existing], existing, newer, 1) -- } -- hack("ConfigurationParams", "ParamConfiguration") -- hack("InitializeParams", "ParamInitialize") -- hack("PreviousResultId", "PreviousResultID") -- hack("WorkspaceFoldersServerCapabilities", "WorkspaceFolders5Gn") -- hack("_InitializeParams", "XInitializeParams") -- // and some aliases to make the new code contain the old -- types["PrepareRename2Gn"] = "type PrepareRename2Gn = Msg_PrepareRename2Gn // (alias) line 13927\n" -- types["PrepareRenameResult"] = "type PrepareRenameResult = Msg_PrepareRename2Gn // (alias) line 13927\n" -- for _, k := range types.keys() { -- if k == "WatchKind" { -- types[k] = "type WatchKind = uint32 // line 13505" // strict gopls compatibility needs the '=' +- case *ast.IfStmt: +- // This handles: +- // +- // f, err := os.Open("foo") +- // if <> +- +- // Avoid false positives by ensuring the if's cond is a bad +- // expression. For example, don't offer the completion in cases +- // like "if <> somethingElse". +- if _, bad := n.Cond.(*ast.BadExpr); !bad { +- return - } -- out.WriteString(types[k]) +- +- // If "if" is our direct prefix, we need to include it in our +- // candidate since the existing "if" will be overwritten. +- needsIf = c.pos == n.Pos()+token.Pos(len("if")) - } - -- out.WriteString("\nconst (\n") -- for _, k := range consts.keys() { -- out.WriteString(consts[k]) +- // Build up a snippet that looks like: +- // +- // if err != nil { +- // return , ..., ${1:err} +- // } +- // +- // We make the error a placeholder so it is easy to alter the error. +- var snip snippet.Builder +- if needsIf { +- snip.WriteText("if ") - } -- out.WriteString(")\n\n") -- x, err := format.Source(out.Bytes()) -- if err != nil { -- os.WriteFile("/tmp/a.go", out.Bytes(), 0644) -- log.Fatalf("tsprotocol.go: %v", err) +- snip.WriteText(fmt.Sprintf("%s != nil {\n\t", errVar)) +- +- var label string +- if isTest { +- snip.WriteText(fmt.Sprintf("%s.Fatal(%s)", testVar, errVar)) +- label = fmt.Sprintf("%[1]s != nil { %[2]s.Fatal(%[1]s) }", errVar, testVar) +- } else { +- snip.WriteText("return ") +- for i := 0; i < result.Len()-1; i++ { +- snip.WriteText(formatZeroValue(result.At(i).Type(), c.qf)) +- snip.WriteText(", ") +- } +- snip.WritePlaceholder(func(b *snippet.Builder) { +- b.WriteText(errVar) +- }) +- label = fmt.Sprintf("%[1]s != nil { return %[1]s }", errVar) - } -- if err := os.WriteFile(filepath.Join(*outputdir, "tsprotocol.go"), x, 0644); err != nil { -- log.Fatalf("%v writing tsprotocol.go", err) +- +- snip.WriteText("\n}") +- +- if needsIf { +- label = "if " + label - } +- +- c.items = append(c.items, CompletionItem{ +- Label: label, +- Kind: protocol.SnippetCompletion, +- Score: highScore, +- snippet: &snip, +- }) -} - --func writejsons() { -- out := new(bytes.Buffer) -- fmt.Fprintln(out, fileHdr) -- out.WriteString("import \"encoding/json\"\n\n") -- out.WriteString("import \"fmt\"\n") +-// getTestVar checks the function signature's input parameters and returns +-// the name of the first parameter that implements "testing.TB". For example, +-// func someFunc(t *testing.T) returns the string "t", func someFunc(b *testing.B) +-// returns "b" etc. An empty string indicates that the function signature +-// does not take a testing.TB parameter or does so but is ignored such +-// as func someFunc(*testing.T). +-func getTestVar(enclosingFunc *funcInfo, pkg *cache.Package) string { +- if enclosingFunc == nil || enclosingFunc.sig == nil { +- return "" +- } - -- out.WriteString(` --// UnmarshalError indicates that a JSON value did not conform to --// one of the expected cases of an LSP union type. --type UnmarshalError struct { -- msg string --} +- var testingPkg *types.Package +- for _, p := range pkg.Types().Imports() { +- if p.Path() == "testing" { +- testingPkg = p +- break +- } +- } +- if testingPkg == nil { +- return "" +- } +- tbObj := testingPkg.Scope().Lookup("TB") +- if tbObj == nil { +- return "" +- } +- iface, ok := tbObj.Type().Underlying().(*types.Interface) +- if !ok { +- return "" +- } - --func (e UnmarshalError) Error() string { -- return e.msg +- sig := enclosingFunc.sig +- for i := 0; i < sig.Params().Len(); i++ { +- param := sig.Params().At(i) +- if param.Name() == "_" { +- continue +- } +- if !types.Implements(param.Type(), iface) { +- continue +- } +- return param.Name() +- } +- +- return "" -} --`) - -- for _, k := range jsons.keys() { -- out.WriteString(jsons[k]) -- } -- x, err := format.Source(out.Bytes()) -- if err != nil { -- os.WriteFile("/tmp/a.go", out.Bytes(), 0644) -- log.Fatalf("tsjson.go: %v", err) +-// addReturnZeroValues offers a snippet candidate on the form: +-// +-// return 0, "", nil +-// +-// Requires a partially or fully written return keyword at position. +-// Requires current position to be in a function with more than +-// zero return parameters. +-func (c *completer) addReturnZeroValues() { +- if len(c.path) < 2 || c.enclosingFunc == nil || !c.opts.placeholders { +- return - } -- if err := os.WriteFile(filepath.Join(*outputdir, "tsjson.go"), x, 0644); err != nil { -- log.Fatalf("%v writing tsjson.go", err) +- result := c.enclosingFunc.sig.Results() +- if result.Len() == 0 { +- return - } --} - --// create the common file header for the output files --func fileHeader(model Model) string { -- fname := filepath.Join(*repodir, ".git", "HEAD") -- buf, err := os.ReadFile(fname) -- if err != nil { -- log.Fatal(err) +- // Offer just less than we expect from return as a keyword. +- var score = stdScore - 0.01 +- switch c.path[0].(type) { +- case *ast.ReturnStmt, *ast.Ident: +- f := c.matcher.Score("return") +- if f <= 0 { +- return +- } +- score *= float64(f) +- default: +- return - } -- buf = bytes.TrimSpace(buf) -- var githash string -- if len(buf) == 40 { -- githash = string(buf[:40]) -- } else if bytes.HasPrefix(buf, []byte("ref: ")) { -- fname = filepath.Join(*repodir, ".git", string(buf[5:])) -- buf, err = os.ReadFile(fname) -- if err != nil { -- log.Fatal(err) +- +- // The snippet will have a placeholder over each return value. +- // The label will not. +- var snip snippet.Builder +- var label strings.Builder +- snip.WriteText("return ") +- fmt.Fprintf(&label, "return ") +- +- for i := 0; i < result.Len(); i++ { +- if i > 0 { +- snip.WriteText(", ") +- fmt.Fprintf(&label, ", ") - } -- githash = string(buf[:40]) -- } else { -- log.Fatalf("githash cannot be recovered from %s", fname) +- +- zero := formatZeroValue(result.At(i).Type(), c.qf) +- snip.WritePlaceholder(func(b *snippet.Builder) { +- b.WriteText(zero) +- }) +- fmt.Fprintf(&label, zero) - } - -- format := `// Copyright 2023 The Go Authors. All rights reserved. +- c.items = append(c.items, CompletionItem{ +- Label: label.String(), +- Kind: protocol.SnippetCompletion, +- Score: score, +- snippet: &snip, +- }) +-} +diff -urN a/gopls/internal/golang/completion/util.go b/gopls/internal/golang/completion/util.go +--- a/gopls/internal/golang/completion/util.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/completion/util.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,334 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --// Code generated for LSP. DO NOT EDIT. -- --package protocol +-package completion - --// Code generated from %[1]s at ref %[2]s (hash %[3]s). --// %[4]s/blob/%[2]s/%[1]s --// LSP metaData.version = %[5]s. +-import ( +- "go/ast" +- "go/token" +- "go/types" - --` -- return fmt.Sprintf(format, -- "protocol/metaModel.json", // 1 -- lspGitRef, // 2 -- githash, // 3 -- vscodeRepo, // 4 -- model.Version.Version) // 5 --} +- "golang.org/x/tools/go/types/typeutil" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/internal/aliases" +- "golang.org/x/tools/internal/diff" +- "golang.org/x/tools/internal/typeparams" +-) - --func parse(fname string) Model { -- buf, err := os.ReadFile(fname) -- if err != nil { -- log.Fatal(err) -- } -- buf = addLineNumbers(buf) -- var model Model -- if err := json.Unmarshal(buf, &model); err != nil { -- log.Fatal(err) +-// exprAtPos returns the index of the expression containing pos. +-func exprAtPos(pos token.Pos, args []ast.Expr) int { +- for i, expr := range args { +- if expr.Pos() <= pos && pos <= expr.End() { +- return i +- } - } -- return model +- return len(args) -} - --// Type.Value has to be treated specially for literals and maps --func (t *Type) UnmarshalJSON(data []byte) error { -- // First unmarshal only the unambiguous fields. -- var x struct { -- Kind string `json:"kind"` -- Items []*Type `json:"items"` -- Element *Type `json:"element"` -- Name string `json:"name"` -- Key *Type `json:"key"` -- Value any `json:"value"` -- Line int `json:"line"` -- } -- if err := json.Unmarshal(data, &x); err != nil { -- return err -- } -- *t = Type{ -- Kind: x.Kind, -- Items: x.Items, -- Element: x.Element, -- Name: x.Name, -- Value: x.Value, -- Line: x.Line, -- } +-// eachField invokes fn for each field that can be selected from a +-// value of type T. +-func eachField(T types.Type, fn func(*types.Var)) { +- // TODO(adonovan): this algorithm doesn't exclude ambiguous +- // selections that match more than one field/method. +- // types.NewSelectionSet should do that for us. - -- // Then unmarshal the 'value' field based on the kind. -- // This depends on Unmarshal ignoring fields it doesn't know about. -- switch x.Kind { -- case "map": -- var x struct { -- Key *Type `json:"key"` -- Value *Type `json:"value"` -- } -- if err := json.Unmarshal(data, &x); err != nil { -- return fmt.Errorf("Type.kind=map: %v", err) -- } -- t.Key = x.Key -- t.Value = x.Value +- // for termination on recursive types +- var seen typeutil.Map - -- case "literal": -- var z struct { -- Value ParseLiteral `json:"value"` -- } +- var visit func(T types.Type) +- visit = func(T types.Type) { +- // T may be a Struct, optionally Named, with an optional +- // Pointer (with optional Aliases at every step!): +- // Consider: type T *struct{ f int }; _ = T(nil).f +- if T, ok := typeparams.Deref(T).Underlying().(*types.Struct); ok { +- if seen.At(T) != nil { +- return +- } - -- if err := json.Unmarshal(data, &z); err != nil { -- return fmt.Errorf("Type.kind=literal: %v", err) +- for i := 0; i < T.NumFields(); i++ { +- f := T.Field(i) +- fn(f) +- if f.Anonymous() { +- seen.Set(T, true) +- visit(f.Type()) +- } +- } - } -- t.Value = z.Value -- -- case "base", "reference", "array", "and", "or", "tuple", -- "stringLiteral": -- // no-op. never seen integerLiteral or booleanLiteral. -- -- default: -- return fmt.Errorf("cannot decode Type.kind %q: %s", x.Kind, data) - } -- return nil +- visit(T) -} - --// which table entries were not used --func checkTables() { -- for k := range disambiguate { -- if !usedDisambiguate[k] { -- log.Printf("disambiguate[%v] unused", k) -- } -- } -- for k := range renameProp { -- if !usedRenameProp[k] { -- log.Printf("renameProp {%q, %q} unused", k[0], k[1]) -- } -- } -- for k := range goplsStar { -- if !usedGoplsStar[k] { -- log.Printf("goplsStar {%q, %q} unused", k[0], k[1]) -- } +-// typeIsValid reports whether typ doesn't contain any Invalid types. +-func typeIsValid(typ types.Type) bool { +- // Check named types separately, because we don't want +- // to call Underlying() on them to avoid problems with recursive types. +- if _, ok := aliases.Unalias(typ).(*types.Named); ok { +- return true - } -- for k := range goplsType { -- if !usedGoplsType[k] { -- log.Printf("unused goplsType[%q]->%s", k, goplsType[k]) +- +- switch typ := typ.Underlying().(type) { +- case *types.Basic: +- return typ.Kind() != types.Invalid +- case *types.Array: +- return typeIsValid(typ.Elem()) +- case *types.Slice: +- return typeIsValid(typ.Elem()) +- case *types.Pointer: +- return typeIsValid(typ.Elem()) +- case *types.Map: +- return typeIsValid(typ.Key()) && typeIsValid(typ.Elem()) +- case *types.Chan: +- return typeIsValid(typ.Elem()) +- case *types.Signature: +- return typeIsValid(typ.Params()) && typeIsValid(typ.Results()) +- case *types.Tuple: +- for i := 0; i < typ.Len(); i++ { +- if !typeIsValid(typ.At(i).Type()) { +- return false +- } - } +- return true +- case *types.Struct, *types.Interface: +- // Don't bother checking structs, interfaces for validity. +- return true +- default: +- return false - } -} -diff -urN a/gopls/internal/lsp/protocol/generate/main_test.go b/gopls/internal/lsp/protocol/generate/main_test.go ---- a/gopls/internal/lsp/protocol/generate/main_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/protocol/generate/main_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,119 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --//go:build go1.19 --// +build go1.19 -- --package main +-// resolveInvalid traverses the node of the AST that defines the scope +-// containing the declaration of obj, and attempts to find a user-friendly +-// name for its invalid type. The resulting Object and its Type are fake. +-func resolveInvalid(fset *token.FileSet, obj types.Object, node ast.Node, info *types.Info) types.Object { +- var resultExpr ast.Expr +- ast.Inspect(node, func(node ast.Node) bool { +- switch n := node.(type) { +- case *ast.ValueSpec: +- for _, name := range n.Names { +- if info.Defs[name] == obj { +- resultExpr = n.Type +- } +- } +- return false +- case *ast.Field: // This case handles parameters and results of a FuncDecl or FuncLit. +- for _, name := range n.Names { +- if info.Defs[name] == obj { +- resultExpr = n.Type +- } +- } +- return false +- default: +- return true +- } +- }) +- // Construct a fake type for the object and return a fake object with this type. +- typename := golang.FormatNode(fset, resultExpr) +- typ := types.NewNamed(types.NewTypeName(token.NoPos, obj.Pkg(), typename, nil), types.Typ[types.Invalid], nil) +- return types.NewVar(obj.Pos(), obj.Pkg(), obj.Name(), typ) +-} - --import ( -- "encoding/json" -- "fmt" -- "log" -- "os" -- "testing" --) +-// TODO(adonovan): inline these. +-func isVar(obj types.Object) bool { return is[*types.Var](obj) } +-func isTypeName(obj types.Object) bool { return is[*types.TypeName](obj) } +-func isFunc(obj types.Object) bool { return is[*types.Func](obj) } +-func isPkgName(obj types.Object) bool { return is[*types.PkgName](obj) } - --// These tests require the result of --//"git clone https://github.com/microsoft/vscode-languageserver-node" in the HOME directory +-// isPointer reports whether T is a Pointer, or an alias of one. +-// It returns false for a Named type whose Underlying is a Pointer. +-// +-// TODO(adonovan): shouldn't this use CoreType(T)? +-func isPointer(T types.Type) bool { return is[*types.Pointer](aliases.Unalias(T)) } - --// this is not a test, but a way to get code coverage, --// (in vscode, just run the test with "go.coverOnSingleTest": true) --func TestAll(t *testing.T) { -- t.Skip("needs vscode-languageserver-node repository") -- *lineNumbers = true -- log.SetFlags(log.Lshortfile) -- main() +-func isEmptyInterface(T types.Type) bool { +- // TODO(adonovan): shouldn't this use Underlying? +- intf, _ := T.(*types.Interface) +- return intf != nil && intf.NumMethods() == 0 && intf.IsMethodSet() -} - --// check that the parsed file includes all the information --// from the json file. This test will fail if the spec --// introduces new fields. (one can test this test by --// commenting out the version field in Model.) --func TestParseContents(t *testing.T) { -- t.Skip("needs vscode-languageserver-node repository") -- log.SetFlags(log.Lshortfile) -- -- // compute our parse of the specification -- dir := os.Getenv("HOME") + "/vscode-languageserver-node" -- fname := dir + "/protocol/metaModel.json" -- v := parse(fname) -- out, err := json.Marshal(v) -- if err != nil { -- t.Fatal(err) -- } -- var our interface{} -- if err := json.Unmarshal(out, &our); err != nil { -- t.Fatal(err) +-func isUntyped(T types.Type) bool { +- if basic, ok := aliases.Unalias(T).(*types.Basic); ok { +- return basic.Info()&types.IsUntyped > 0 - } +- return false +-} - -- // process the json file -- buf, err := os.ReadFile(fname) -- if err != nil { -- t.Fatalf("could not read metaModel.json: %v", err) -- } -- var raw interface{} -- if err := json.Unmarshal(buf, &raw); err != nil { -- t.Fatal(err) +-func deslice(T types.Type) types.Type { +- if slice, ok := T.Underlying().(*types.Slice); ok { +- return slice.Elem() - } +- return nil +-} - -- // convert to strings showing the fields -- them := flatten(raw) -- us := flatten(our) -- -- // everything in them should be in us -- lesser := make(sortedMap[bool]) -- for _, s := range them { -- lesser[s] = true +-// isSelector returns the enclosing *ast.SelectorExpr when pos is in the +-// selector. +-func enclosingSelector(path []ast.Node, pos token.Pos) *ast.SelectorExpr { +- if len(path) == 0 { +- return nil - } -- greater := make(sortedMap[bool]) // set of fields we have -- for _, s := range us { -- greater[s] = true +- +- if sel, ok := path[0].(*ast.SelectorExpr); ok { +- return sel - } -- for _, k := range lesser.keys() { // set if fields they have -- if !greater[k] { -- t.Errorf("missing %s", k) +- +- // TODO(adonovan): consider ast.ParenExpr (e.g. (x).name) +- if _, ok := path[0].(*ast.Ident); ok && len(path) > 1 { +- if sel, ok := path[1].(*ast.SelectorExpr); ok && pos >= sel.Sel.Pos() { +- return sel - } - } +- +- return nil -} - --// flatten(nil) = "nil" --// flatten(v string) = fmt.Sprintf("%q", v) --// flatten(v float64)= fmt.Sprintf("%g", v) --// flatten(v bool) = fmt.Sprintf("%v", v) --// flatten(v []any) = []string{"[0]"flatten(v[0]), "[1]"flatten(v[1]), ...} --// flatten(v map[string]any) = {"key1": flatten(v["key1"]), "key2": flatten(v["key2"]), ...} --func flatten(x any) []string { -- switch v := x.(type) { -- case nil: -- return []string{"nil"} -- case string: -- return []string{fmt.Sprintf("%q", v)} -- case float64: -- return []string{fmt.Sprintf("%g", v)} -- case bool: -- return []string{fmt.Sprintf("%v", v)} -- case []any: -- var ans []string -- for i, x := range v { -- idx := fmt.Sprintf("[%.3d]", i) -- for _, s := range flatten(x) { -- ans = append(ans, idx+s) -- } -- } -- return ans -- case map[string]any: -- var ans []string -- for k, x := range v { -- idx := fmt.Sprintf("%q:", k) -- for _, s := range flatten(x) { -- ans = append(ans, idx+s) +-// enclosingDeclLHS returns LHS idents from containing value spec or +-// assign statement. +-func enclosingDeclLHS(path []ast.Node) []*ast.Ident { +- for _, n := range path { +- switch n := n.(type) { +- case *ast.ValueSpec: +- return n.Names +- case *ast.AssignStmt: +- ids := make([]*ast.Ident, 0, len(n.Lhs)) +- for _, e := range n.Lhs { +- if id, ok := e.(*ast.Ident); ok { +- ids = append(ids, id) +- } - } +- return ids - } -- return ans -- default: -- log.Fatalf("unexpected type %T", x) -- return nil - } +- +- return nil -} -diff -urN a/gopls/internal/lsp/protocol/generate/output.go b/gopls/internal/lsp/protocol/generate/output.go ---- a/gopls/internal/lsp/protocol/generate/output.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/protocol/generate/output.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,427 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --//go:build go1.19 --// +build go1.19 +-// exprObj returns the types.Object associated with the *ast.Ident or +-// *ast.SelectorExpr e. +-func exprObj(info *types.Info, e ast.Expr) types.Object { +- var ident *ast.Ident +- switch expr := e.(type) { +- case *ast.Ident: +- ident = expr +- case *ast.SelectorExpr: +- ident = expr.Sel +- default: +- return nil +- } - --package main +- return info.ObjectOf(ident) +-} - --import ( -- "bytes" -- "fmt" -- "log" -- "sort" -- "strings" --) +-// typeConversion returns the type being converted to if call is a type +-// conversion expression. +-func typeConversion(call *ast.CallExpr, info *types.Info) types.Type { +- // Type conversion (e.g. "float64(foo)"). +- if fun, _ := exprObj(info, call.Fun).(*types.TypeName); fun != nil { +- return fun.Type() +- } - --var ( -- // tsclient.go has 3 sections -- cdecls = make(sortedMap[string]) -- ccases = make(sortedMap[string]) -- cfuncs = make(sortedMap[string]) -- // tsserver.go has 3 sections -- sdecls = make(sortedMap[string]) -- scases = make(sortedMap[string]) -- sfuncs = make(sortedMap[string]) -- // tsprotocol.go has 2 sections -- types = make(sortedMap[string]) -- consts = make(sortedMap[string]) -- // tsjson has 1 section -- jsons = make(sortedMap[string]) --) +- return nil +-} - --func generateOutput(model Model) { -- for _, r := range model.Requests { -- genDecl(r.Method, r.Params, r.Result, r.Direction) -- genCase(r.Method, r.Params, r.Result, r.Direction) -- genFunc(r.Method, r.Params, r.Result, r.Direction, false) -- } -- for _, n := range model.Notifications { -- if n.Method == "$/cancelRequest" { -- continue // handled internally by jsonrpc2 +-// fieldsAccessible returns whether s has at least one field accessible by p. +-func fieldsAccessible(s *types.Struct, p *types.Package) bool { +- for i := 0; i < s.NumFields(); i++ { +- f := s.Field(i) +- if f.Exported() || f.Pkg() == p { +- return true - } -- genDecl(n.Method, n.Params, nil, n.Direction) -- genCase(n.Method, n.Params, nil, n.Direction) -- genFunc(n.Method, n.Params, nil, n.Direction, true) - } -- genStructs(model) -- genAliases(model) -- genGenTypes() // generate the unnamed types -- genConsts(model) -- genMarshal() +- return false -} - --func genDecl(method string, param, result *Type, dir string) { -- fname := methodName(method) -- p := "" -- if notNil(param) { -- p = ", *" + goplsName(param) -- } -- ret := "error" -- if notNil(result) { -- tp := goplsName(result) -- if !hasNilValue(tp) { -- tp = "*" + tp +-// prevStmt returns the statement that precedes the statement containing pos. +-// For example: +-// +-// foo := 1 +-// bar(1 + 2<>) +-// +-// If "<>" is pos, prevStmt returns "foo := 1" +-func prevStmt(pos token.Pos, path []ast.Node) ast.Stmt { +- var blockLines []ast.Stmt +- for i := 0; i < len(path) && blockLines == nil; i++ { +- switch n := path[i].(type) { +- case *ast.BlockStmt: +- blockLines = n.List +- case *ast.CommClause: +- blockLines = n.Body +- case *ast.CaseClause: +- blockLines = n.Body - } -- ret = fmt.Sprintf("(%s, error)", tp) - } -- // special gopls compatibility case (PJW: still needed?) -- switch method { -- case "workspace/configuration": -- // was And_Param_workspace_configuration, but the type substitution doesn't work, -- // as ParamConfiguration is embedded in And_Param_workspace_configuration -- p = ", *ParamConfiguration" -- ret = "([]LSPAny, error)" +- +- for i := len(blockLines) - 1; i >= 0; i-- { +- if blockLines[i].End() < pos { +- return blockLines[i] +- } - } -- msg := fmt.Sprintf("\t%s(context.Context%s) %s // %s\n", fname, p, ret, method) -- switch dir { -- case "clientToServer": -- sdecls[method] = msg -- case "serverToClient": -- cdecls[method] = msg -- case "both": -- sdecls[method] = msg -- cdecls[method] = msg +- +- return nil +-} +- +-// formatZeroValue produces Go code representing the zero value of T. It +-// returns the empty string if T is invalid. +-func formatZeroValue(T types.Type, qf types.Qualifier) string { +- switch u := T.Underlying().(type) { +- case *types.Basic: +- switch { +- case u.Info()&types.IsNumeric > 0: +- return "0" +- case u.Info()&types.IsString > 0: +- return `""` +- case u.Info()&types.IsBoolean > 0: +- return "false" +- default: +- return "" +- } +- case *types.Pointer, *types.Interface, *types.Chan, *types.Map, *types.Slice, *types.Signature: +- return "nil" - default: -- log.Fatalf("impossible direction %q", dir) +- return types.TypeString(T, qf) + "{}" - } -} - --func genCase(method string, param, result *Type, dir string) { -- out := new(bytes.Buffer) -- fmt.Fprintf(out, "\tcase %q:\n", method) -- var p string -- fname := methodName(method) -- if notNil(param) { -- nm := goplsName(param) -- if method == "workspace/configuration" { // gopls compatibility -- // was And_Param_workspace_configuration, which contains ParamConfiguration -- // so renaming the type leads to circular definitions -- nm = "ParamConfiguration" // gopls compatibility -- } -- fmt.Fprintf(out, "\t\tvar params %s\n", nm) -- fmt.Fprintf(out, "\t\tif err := json.Unmarshal(r.Params(), ¶ms); err != nil {\n") -- fmt.Fprintf(out, "\t\t\treturn true, sendParseError(ctx, reply, err)\n\t\t}\n") -- p = ", ¶ms" +-// isBasicKind returns whether t is a basic type of kind k. +-func isBasicKind(t types.Type, k types.BasicInfo) bool { +- b, _ := t.Underlying().(*types.Basic) +- return b != nil && b.Info()&k > 0 +-} +- +-func (c *completer) editText(from, to token.Pos, newText string) ([]protocol.TextEdit, error) { +- start, end, err := safetoken.Offsets(c.tokFile, from, to) +- if err != nil { +- return nil, err // can't happen: from/to came from c - } -- if notNil(result) { -- fmt.Fprintf(out, "\t\tresp, err := %%s.%s(ctx%s)\n", fname, p) -- out.WriteString("\t\tif err != nil {\n") -- out.WriteString("\t\t\treturn true, reply(ctx, nil, err)\n") -- out.WriteString("\t\t}\n") -- out.WriteString("\t\treturn true, reply(ctx, resp, nil)\n") -- } else { -- fmt.Fprintf(out, "\t\terr := %%s.%s(ctx%s)\n", fname, p) -- out.WriteString("\t\treturn true, reply(ctx, nil, err)\n") +- return protocol.EditsFromDiffEdits(c.mapper, []diff.Edit{{ +- Start: start, +- End: end, +- New: newText, +- }}) +-} +- +-// assignableTo is like types.AssignableTo, but returns false if +-// either type is invalid. +-func assignableTo(x, to types.Type) bool { +- if aliases.Unalias(x) == types.Typ[types.Invalid] || +- aliases.Unalias(to) == types.Typ[types.Invalid] { +- return false - } -- msg := out.String() -- switch dir { -- case "clientToServer": -- scases[method] = fmt.Sprintf(msg, "server") -- case "serverToClient": -- ccases[method] = fmt.Sprintf(msg, "client") -- case "both": -- scases[method] = fmt.Sprintf(msg, "server") -- ccases[method] = fmt.Sprintf(msg, "client") -- default: -- log.Fatalf("impossible direction %q", dir) +- +- return types.AssignableTo(x, to) +-} +- +-// convertibleTo is like types.ConvertibleTo, but returns false if +-// either type is invalid. +-func convertibleTo(x, to types.Type) bool { +- if aliases.Unalias(x) == types.Typ[types.Invalid] || +- aliases.Unalias(to) == types.Typ[types.Invalid] { +- return false - } +- +- return types.ConvertibleTo(x, to) -} +diff -urN a/gopls/internal/golang/completion/util_test.go b/gopls/internal/golang/completion/util_test.go +--- a/gopls/internal/golang/completion/util_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/completion/util_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,28 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func genFunc(method string, param, result *Type, dir string, isnotify bool) { -- out := new(bytes.Buffer) -- var p, r string -- var goResult string -- if notNil(param) { -- p = ", params *" + goplsName(param) +-package completion +- +-import ( +- "go/types" +- "testing" +-) +- +-func TestFormatZeroValue(t *testing.T) { +- tests := []struct { +- typ types.Type +- want string +- }{ +- {types.Typ[types.String], `""`}, +- {types.Typ[types.Byte], "0"}, +- {types.Typ[types.Invalid], ""}, +- {types.Universe.Lookup("error").Type(), "nil"}, - } -- if notNil(result) { -- goResult = goplsName(result) -- if !hasNilValue(goResult) { -- goResult = "*" + goResult +- +- for _, test := range tests { +- if got := formatZeroValue(test.typ, nil); got != test.want { +- t.Errorf("formatZeroValue(%v) = %q, want %q", test.typ, got, test.want) - } -- r = fmt.Sprintf("(%s, error)", goResult) -- } else { -- r = "error" - } -- // special gopls compatibility case -- switch method { -- case "workspace/configuration": -- // was And_Param_workspace_configuration, but the type substitution doesn't work, -- // as ParamConfiguration is embedded in And_Param_workspace_configuration -- p = ", params *ParamConfiguration" -- r = "([]LSPAny, error)" -- goResult = "[]LSPAny" +-} +diff -urN a/gopls/internal/golang/definition.go b/gopls/internal/golang/definition.go +--- a/gopls/internal/golang/definition.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/definition.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,312 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package golang +- +-import ( +- "context" +- "errors" +- "fmt" +- "go/ast" +- "go/parser" +- "go/token" +- "go/types" +- +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/internal/event" +-) +- +-// Definition handles the textDocument/definition request for Go files. +-func Definition(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) ([]protocol.Location, error) { +- ctx, done := event.Start(ctx, "golang.Definition") +- defer done() +- +- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) +- if err != nil { +- return nil, err +- } +- pos, err := pgf.PositionPos(position) +- if err != nil { +- return nil, err - } -- fname := methodName(method) -- fmt.Fprintf(out, "func (s *%%sDispatcher) %s(ctx context.Context%s) %s {\n", -- fname, p, r) - -- if !notNil(result) { -- if isnotify { -- if notNil(param) { -- fmt.Fprintf(out, "\treturn s.sender.Notify(ctx, %q, params)\n", method) -- } else { -- fmt.Fprintf(out, "\treturn s.sender.Notify(ctx, %q, nil)\n", method) -- } -- } else { -- if notNil(param) { -- fmt.Fprintf(out, "\treturn s.sender.Call(ctx, %q, params, nil)\n", method) -- } else { -- fmt.Fprintf(out, "\treturn s.sender.Call(ctx, %q, nil, nil)\n", method) +- // Handle the case where the cursor is in an import. +- importLocations, err := importDefinition(ctx, snapshot, pkg, pgf, pos) +- if err != nil { +- return nil, err +- } +- if len(importLocations) > 0 { +- return importLocations, nil +- } +- +- // Handle the case where the cursor is in the package name. +- // We use "<= End" to accept a query immediately after the package name. +- if pgf.File != nil && pgf.File.Name.Pos() <= pos && pos <= pgf.File.Name.End() { +- // If there's no package documentation, just use current file. +- declFile := pgf +- for _, pgf := range pkg.CompiledGoFiles() { +- if pgf.File.Name != nil && pgf.File.Doc != nil { +- declFile = pgf +- break - } - } -- } else { -- fmt.Fprintf(out, "\tvar result %s\n", goResult) -- if isnotify { -- if notNil(param) { -- fmt.Fprintf(out, "\ts.sender.Notify(ctx, %q, params)\n", method) -- } else { -- fmt.Fprintf(out, "\t\tif err := s.sender.Notify(ctx, %q, nil); err != nil {\n", method) -- } -- } else { -- if notNil(param) { -- fmt.Fprintf(out, "\t\tif err := s.sender.Call(ctx, %q, params, &result); err != nil {\n", method) -- } else { -- fmt.Fprintf(out, "\t\tif err := s.sender.Call(ctx, %q, nil, &result); err != nil {\n", method) -- } +- loc, err := declFile.NodeLocation(declFile.File.Name) +- if err != nil { +- return nil, err - } -- fmt.Fprintf(out, "\t\treturn nil, err\n\t}\n\treturn result, nil\n") +- return []protocol.Location{loc}, nil - } -- out.WriteString("}\n") -- msg := out.String() -- switch dir { -- case "clientToServer": -- sfuncs[method] = fmt.Sprintf(msg, "server") -- case "serverToClient": -- cfuncs[method] = fmt.Sprintf(msg, "client") -- case "both": -- sfuncs[method] = fmt.Sprintf(msg, "server") -- cfuncs[method] = fmt.Sprintf(msg, "client") -- default: -- log.Fatalf("impossible direction %q", dir) +- +- // Handle the case where the cursor is in a linkname directive. +- locations, err := LinknameDefinition(ctx, snapshot, pgf.Mapper, position) +- if !errors.Is(err, ErrNoLinkname) { +- return locations, err - } --} - --func genStructs(model Model) { -- structures := make(map[string]*Structure) // for expanding Extends -- for _, s := range model.Structures { -- structures[s.Name] = s +- // Handle the case where the cursor is in an embed directive. +- locations, err = EmbedDefinition(pgf.Mapper, position) +- if !errors.Is(err, ErrNoEmbed) { +- return locations, err - } -- for _, s := range model.Structures { -- out := new(bytes.Buffer) -- generateDoc(out, s.Documentation) -- nm := goName(s.Name) -- if nm == "string" { // an unacceptable strut name -- // a weird case, and needed only so the generated code contains the old gopls code -- nm = "DocumentDiagnosticParams" -- } -- fmt.Fprintf(out, "type %s struct {%s\n", nm, linex(s.Line)) -- // for gpls compatibilitye, embed most extensions, but expand the rest some day -- props := append([]NameType{}, s.Properties...) -- if s.Name == "SymbolInformation" { // but expand this one -- for _, ex := range s.Extends { -- fmt.Fprintf(out, "\t// extends %s\n", ex.Name) -- props = append(props, structures[ex.Name].Properties...) -- } -- genProps(out, props, nm) -- } else { -- genProps(out, props, nm) -- for _, ex := range s.Extends { -- fmt.Fprintf(out, "\t%s\n", goName(ex.Name)) -- } -- } -- for _, ex := range s.Mixins { -- fmt.Fprintf(out, "\t%s\n", goName(ex.Name)) -- } -- out.WriteString("}\n") -- types[nm] = out.String() +- +- // The general case: the cursor is on an identifier. +- _, obj, _ := referencedObject(pkg, pgf, pos) +- if obj == nil { +- return nil, nil - } -- // base types -- types["DocumentURI"] = "type DocumentURI string\n" -- types["URI"] = "type URI = string\n" - -- types["LSPAny"] = "type LSPAny = interface{}\n" -- // A special case, the only previously existing Or type -- types["DocumentDiagnosticReport"] = "type DocumentDiagnosticReport = Or_DocumentDiagnosticReport // (alias) line 13909\n" +- // Handle objects with no position: builtin, unsafe. +- if !obj.Pos().IsValid() { +- return builtinDefinition(ctx, snapshot, obj) +- } - +- // Finally, map the object position. +- loc, err := mapPosition(ctx, pkg.FileSet(), snapshot, obj.Pos(), adjustedObjEnd(obj)) +- if err != nil { +- return nil, err +- } +- return []protocol.Location{loc}, nil -} - --func genProps(out *bytes.Buffer, props []NameType, name string) { -- for _, p := range props { -- tp := goplsName(p.Type) -- if newNm, ok := renameProp[prop{name, p.Name}]; ok { -- usedRenameProp[prop{name, p.Name}] = true -- if tp == newNm { -- log.Printf("renameProp useless {%q, %q} for %s", name, p.Name, tp) -- } -- tp = newNm -- } -- // it's a pointer if it is optional, or for gopls compatibility -- opt, star := propStar(name, p, tp) -- json := fmt.Sprintf(" `json:\"%s%s\"`", p.Name, opt) -- generateDoc(out, p.Documentation) -- fmt.Fprintf(out, "\t%s %s%s %s\n", goName(p.Name), star, tp, json) +-// builtinDefinition returns the location of the fake source +-// declaration of a built-in in {builtin,unsafe}.go. +-func builtinDefinition(ctx context.Context, snapshot *cache.Snapshot, obj types.Object) ([]protocol.Location, error) { +- pgf, decl, err := builtinDecl(ctx, snapshot, obj) +- if err != nil { +- return nil, err - } --} - --func genAliases(model Model) { -- for _, ta := range model.TypeAliases { -- out := new(bytes.Buffer) -- generateDoc(out, ta.Documentation) -- nm := goName(ta.Name) -- if nm != ta.Name { -- continue // renamed the type, e.g., "DocumentDiagnosticReport", an or-type to "string" -- } -- tp := goplsName(ta.Type) -- fmt.Fprintf(out, "type %s = %s // (alias) line %d\n", nm, tp, ta.Line) -- types[nm] = out.String() +- loc, err := pgf.PosLocation(decl.Pos(), decl.Pos()+token.Pos(len(obj.Name()))) +- if err != nil { +- return nil, err - } +- return []protocol.Location{loc}, nil -} - --func genGenTypes() { -- for _, nt := range genTypes { -- out := new(bytes.Buffer) -- nm := goplsName(nt.typ) -- switch nt.kind { -- case "literal": -- fmt.Fprintf(out, "// created for Literal (%s)\n", nt.name) -- fmt.Fprintf(out, "type %s struct {%s\n", nm, linex(nt.line+1)) -- genProps(out, nt.properties, nt.name) // systematic name, not gopls name; is this a good choice? -- case "or": -- if !strings.HasPrefix(nm, "Or") { -- // It was replaced by a narrower type defined elsewhere -- continue -- } -- names := []string{} -- for _, t := range nt.items { -- if notNil(t) { -- names = append(names, goplsName(t)) -- } -- } -- sort.Strings(names) -- fmt.Fprintf(out, "// created for Or %v\n", names) -- fmt.Fprintf(out, "type %s struct {%s\n", nm, linex(nt.line+1)) -- fmt.Fprintf(out, "\tValue interface{} `json:\"value\"`\n") -- case "and": -- fmt.Fprintf(out, "// created for And\n") -- fmt.Fprintf(out, "type %s struct {%s\n", nm, linex(nt.line+1)) -- for _, x := range nt.items { -- nm := goplsName(x) -- fmt.Fprintf(out, "\t%s\n", nm) -- } -- case "tuple": // there's only this one -- nt.name = "UIntCommaUInt" -- fmt.Fprintf(out, "//created for Tuple\ntype %s struct {%s\n", nm, linex(nt.line+1)) -- fmt.Fprintf(out, "\tFld0 uint32 `json:\"fld0\"`\n") -- fmt.Fprintf(out, "\tFld1 uint32 `json:\"fld1\"`\n") -- default: -- log.Fatalf("%s not handled", nt.kind) +-// builtinDecl returns the parsed Go file and node corresponding to a builtin +-// object, which may be a universe object or part of types.Unsafe. +-func builtinDecl(ctx context.Context, snapshot *cache.Snapshot, obj types.Object) (*parsego.File, ast.Node, error) { +- // getDecl returns the file-level declaration of name +- // using legacy (go/ast) object resolution. +- getDecl := func(file *ast.File, name string) (ast.Node, error) { +- astObj := file.Scope.Lookup(name) +- if astObj == nil { +- // Every built-in should have documentation syntax. +- // However, it is possible to reach this statement by +- // commenting out declarations in {builtin,unsafe}.go. +- return nil, fmt.Errorf("internal error: no object for %s", name) - } -- out.WriteString("}\n") -- types[nm] = out.String() -- } --} --func genConsts(model Model) { -- for _, e := range model.Enumerations { -- out := new(bytes.Buffer) -- generateDoc(out, e.Documentation) -- tp := goplsName(e.Type) -- nm := goName(e.Name) -- fmt.Fprintf(out, "type %s %s%s\n", nm, tp, linex(e.Line)) -- types[nm] = out.String() -- vals := new(bytes.Buffer) -- generateDoc(vals, e.Documentation) -- for _, v := range e.Values { -- generateDoc(vals, v.Documentation) -- nm := goName(v.Name) -- more, ok := disambiguate[e.Name] -- if ok { -- usedDisambiguate[e.Name] = true -- nm = more.prefix + nm + more.suffix -- nm = goName(nm) // stringType -- } -- var val string -- switch v := v.Value.(type) { -- case string: -- val = fmt.Sprintf("%q", v) -- case float64: -- val = fmt.Sprintf("%d", int(v)) -- default: -- log.Fatalf("impossible type %T", v) -- } -- fmt.Fprintf(vals, "\t%s %s = %s%s\n", nm, e.Name, val, linex(v.Line)) +- decl, ok := astObj.Decl.(ast.Node) +- if !ok { +- return nil, bug.Errorf("internal error: no declaration for %s", obj.Name()) - } -- consts[nm] = vals.String() +- return decl, nil - } --} --func genMarshal() { -- for _, nt := range genTypes { -- nm := goplsName(nt.typ) -- if !strings.HasPrefix(nm, "Or") { -- continue +- +- var ( +- pgf *parsego.File +- decl ast.Node +- err error +- ) +- if obj.Pkg() == types.Unsafe { +- // package "unsafe": +- // parse $GOROOT/src/unsafe/unsafe.go +- unsafe := snapshot.Metadata("unsafe") +- if unsafe == nil { +- // If the type checker somehow resolved 'unsafe', we must have metadata +- // for it. +- return nil, nil, bug.Errorf("no metadata for package 'unsafe'") +- } +- uri := unsafe.GoFiles[0] +- fh, err := snapshot.ReadFile(ctx, uri) +- if err != nil { +- return nil, nil, err - } -- names := []string{} -- for _, t := range nt.items { -- if notNil(t) { -- names = append(names, goplsName(t)) -- } +- pgf, err = snapshot.ParseGo(ctx, fh, parsego.Full&^parser.SkipObjectResolution) +- if err != nil { +- return nil, nil, err - } -- sort.Strings(names) -- var buf bytes.Buffer -- fmt.Fprintf(&buf, "// from line %d\n", nt.line) -- fmt.Fprintf(&buf, "func (t %s) MarshalJSON() ([]byte, error) {\n", nm) -- buf.WriteString("\tswitch x := t.Value.(type){\n") -- for _, nmx := range names { -- fmt.Fprintf(&buf, "\tcase %s:\n", nmx) -- fmt.Fprintf(&buf, "\t\treturn json.Marshal(x)\n") +- decl, err = getDecl(pgf.File, obj.Name()) +- if err != nil { +- return nil, nil, err - } -- buf.WriteString("\tcase nil:\n\t\treturn []byte(\"null\"), nil\n\t}\n") -- fmt.Fprintf(&buf, "\treturn nil, fmt.Errorf(\"type %%T not one of %v\", t)\n", names) -- buf.WriteString("}\n\n") -- -- fmt.Fprintf(&buf, "func (t *%s) UnmarshalJSON(x []byte) error {\n", nm) -- buf.WriteString("\tif string(x) == \"null\" {\n\t\tt.Value = nil\n\t\t\treturn nil\n\t}\n") -- for i, nmx := range names { -- fmt.Fprintf(&buf, "\tvar h%d %s\n", i, nmx) -- fmt.Fprintf(&buf, "\tif err := json.Unmarshal(x, &h%d); err == nil {\n\t\tt.Value = h%d\n\t\t\treturn nil\n\t\t}\n", i, i) +- } else { +- // pseudo-package "builtin": +- // use parsed $GOROOT/src/builtin/builtin.go +- pgf, err = snapshot.BuiltinFile(ctx) +- if err != nil { +- return nil, nil, err - } -- fmt.Fprintf(&buf, "return &UnmarshalError{\"unmarshal failed to match one of %v\"}", names) -- buf.WriteString("}\n\n") -- jsons[nm] = buf.String() -- } --} - --func linex(n int) string { -- if *lineNumbers { -- return fmt.Sprintf(" // line %d", n) -- } -- return "" --} +- if obj.Parent() == types.Universe { +- // built-in function or type +- decl, err = getDecl(pgf.File, obj.Name()) +- if err != nil { +- return nil, nil, err +- } +- } else if obj.Name() == "Error" { +- // error.Error method +- decl, err = getDecl(pgf.File, "error") +- if err != nil { +- return nil, nil, err +- } +- decl = decl.(*ast.TypeSpec).Type.(*ast.InterfaceType).Methods.List[0] - --func goplsName(t *Type) string { -- nm := typeNames[t] -- // translate systematic name to gopls name -- if newNm, ok := goplsType[nm]; ok { -- usedGoplsType[nm] = true -- nm = newNm +- } else { +- return nil, nil, bug.Errorf("unknown built-in %v", obj) +- } - } -- return nm +- return pgf, decl, nil -} - --func notNil(t *Type) bool { // shutdwon is the special case that needs this -- return t != nil && (t.Kind != "base" || t.Name != "null") --} -- --func hasNilValue(t string) bool { -- // this may be unreliable, and need a supplementary table -- if strings.HasPrefix(t, "[]") || strings.HasPrefix(t, "*") { -- return true +-// referencedObject returns the identifier and object referenced at the +-// specified position, which must be within the file pgf, for the purposes of +-// definition/hover/call hierarchy operations. It returns a nil object if no +-// object was found at the given position. +-// +-// If the returned identifier is a type-switch implicit (i.e. the x in x := +-// e.(type)), the third result will be the type of the expression being +-// switched on (the type of e in the example). This facilitates workarounds for +-// limitations of the go/types API, which does not report an object for the +-// identifier x. +-// +-// For embedded fields, referencedObject returns the type name object rather +-// than the var (field) object. +-// +-// TODO(rfindley): this function exists to preserve the pre-existing behavior +-// of golang.Identifier. Eliminate this helper in favor of sharing +-// functionality with objectsAt, after choosing suitable primitives. +-func referencedObject(pkg *cache.Package, pgf *parsego.File, pos token.Pos) (*ast.Ident, types.Object, types.Type) { +- path := pathEnclosingObjNode(pgf.File, pos) +- if len(path) == 0 { +- return nil, nil, nil - } -- if t == "interface{}" || t == "any" { -- return true +- var obj types.Object +- info := pkg.TypesInfo() +- switch n := path[0].(type) { +- case *ast.Ident: +- obj = info.ObjectOf(n) +- // If n is the var's declaring ident in a type switch +- // [i.e. the x in x := foo.(type)], it will not have an object. In this +- // case, set obj to the first implicit object (if any), and return the type +- // of the expression being switched on. +- // +- // The type switch may have no case clauses and thus no +- // implicit objects; this is a type error ("unused x"), +- if obj == nil { +- if implicits, typ := typeSwitchImplicits(info, path); len(implicits) > 0 { +- return n, implicits[0], typ +- } +- } +- +- // If the original position was an embedded field, we want to jump +- // to the field's type definition, not the field's definition. +- if v, ok := obj.(*types.Var); ok && v.Embedded() { +- // types.Info.Uses contains the embedded field's *types.TypeName. +- if typeName := info.Uses[n]; typeName != nil { +- obj = typeName +- } +- } +- return n, obj, nil - } -- // that's all the cases that occur currently -- return false +- return nil, nil, nil -} -diff -urN a/gopls/internal/lsp/protocol/generate/README.md b/gopls/internal/lsp/protocol/generate/README.md ---- a/gopls/internal/lsp/protocol/generate/README.md 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/protocol/generate/README.md 1970-01-01 00:00:00.000000000 +0000 -@@ -1,144 +0,0 @@ --# LSP Support for gopls -- --## The protocol -- --The LSP protocol exchanges json-encoded messages between the client and the server. --(gopls is the server.) The messages are either Requests, which require Responses, or --Notifications, which generate no response. Each Request or Notification has a method name --such as "textDocument/hover" that indicates its meaning and determines which function in the server will handle it. --The protocol is described in a --[web page](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/), --in words, and in a json file (metaModel.json) available either linked towards the bottom of the --web page, or in the vscode-languageserver-node repository. This code uses the latter so the --exact version can be tied to a githash. By default, the command will download the `github.com/microsoft/vscode-languageserver-node` repository to a temporary directory. -- --The specification has five sections - --1. Requests, which describe the Request and Response types for request methods (e.g., *textDocument/didChange*), --2. Notifications, which describe the Request types for notification methods, --3. Structures, which describe named struct-like types, --4. TypeAliases, which describe type aliases, --5. Enumerations, which describe named constants. +-// importDefinition returns locations defining a package referenced by the +-// import spec containing pos. +-// +-// If pos is not inside an import spec, it returns nil, nil. +-func importDefinition(ctx context.Context, s *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, pos token.Pos) ([]protocol.Location, error) { +- var imp *ast.ImportSpec +- for _, spec := range pgf.File.Imports { +- // We use "<= End" to accept a query immediately after an ImportSpec. +- if spec.Path.Pos() <= pos && pos <= spec.Path.End() { +- imp = spec +- } +- } +- if imp == nil { +- return nil, nil +- } - --Requests and Notifications are tagged with a Method (e.g., `"textDocument/hover"`). --The specification does not specify the names of the functions that handle the messages. These --names are specified by the `methodNames` map. Enumerations generate Go `const`s, but --in Typescript they are scoped to namespaces, while in Go they are scoped to a package, so the Go names --may need to be modified to avoid name collisions. (See the `disambiguate` map, and its use.) +- importPath := metadata.UnquoteImportPath(imp) +- impID := pkg.Metadata().DepsByImpPath[importPath] +- if impID == "" { +- return nil, fmt.Errorf("failed to resolve import %q", importPath) +- } +- impMetadata := s.Metadata(impID) +- if impMetadata == nil { +- return nil, fmt.Errorf("missing information for package %q", impID) +- } - --Finally, the specified types are Typescript types, which are quite different from Go types. +- var locs []protocol.Location +- for _, f := range impMetadata.CompiledGoFiles { +- fh, err := s.ReadFile(ctx, f) +- if err != nil { +- if ctx.Err() != nil { +- return nil, ctx.Err() +- } +- continue +- } +- pgf, err := s.ParseGo(ctx, fh, parsego.Header) +- if err != nil { +- if ctx.Err() != nil { +- return nil, ctx.Err() +- } +- continue +- } +- loc, err := pgf.NodeLocation(pgf.File) +- if err != nil { +- return nil, err +- } +- locs = append(locs, loc) +- } - --### Optionality +- if len(locs) == 0 { +- return nil, fmt.Errorf("package %q has no readable files", impID) // incl. unsafe +- } - --The specification can mark fields in structs as Optional. The client distinguishes between missing --fields and `null` fields in some cases. The Go translation for an optional type --should be making sure the field's value --can be `nil`, and adding the json tag `,omitempty`. The former condition would be satisfied by --adding `*` to the field's type if the type is not a reference type. +- return locs, nil +-} - --### Types +-// TODO(rfindley): avoid the duplicate column mapping here, by associating a +-// column mapper with each file handle. +-func mapPosition(ctx context.Context, fset *token.FileSet, s file.Source, start, end token.Pos) (protocol.Location, error) { +- file := fset.File(start) +- uri := protocol.URIFromPath(file.Name()) +- fh, err := s.ReadFile(ctx, uri) +- if err != nil { +- return protocol.Location{}, err +- } +- content, err := fh.Content() +- if err != nil { +- return protocol.Location{}, err +- } +- m := protocol.NewMapper(fh.URI(), content) +- return m.PosLocation(file, start, end) +-} +diff -urN a/gopls/internal/golang/diagnostics.go b/gopls/internal/golang/diagnostics.go +--- a/gopls/internal/golang/diagnostics.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/diagnostics.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,48 +0,0 @@ +-// Copyright 2018 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --The specification uses a number of different types, only a few of which correspond directly to Go types. --The specification's types are "base", "reference", "map", "literal", "stringLiteral", "tuple", "and", "or". --The "base" types correspond directly to Go types, although some Go types needs to be chosen for `URI` and `DocumentUri`. (The "base" types`RegExp`, `BooleanLiteral`, `NumericLiteral` never occur.) +-package golang - --"reference" types are the struct-like types in the Structures section of the specification. The given --names are suitable for Go to use, except the code needs to change names like `_Initialze` to `XInitialize` so --they are exported for json marshaling and unmarshaling. +-import ( +- "context" - --"map" types are just like Go. (The key type in all of them is `DocumentUri`.) +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/progress" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/settings" +- "golang.org/x/tools/gopls/internal/util/maps" +-) - --"stringLiteral" types are types whose type name and value are a single string. The chosen Go equivalent --is to make the type `string` and the value a constant. (The alternative would be to generate a new --named type, which seemed redundant.) +-// Analyze reports go/analysis-framework diagnostics in the specified package. +-// +-// If the provided tracker is non-nil, it may be used to provide notifications +-// of the ongoing analysis pass. +-func Analyze(ctx context.Context, snapshot *cache.Snapshot, pkgIDs map[PackageID]*metadata.Package, tracker *progress.Tracker) (map[protocol.DocumentURI][]*cache.Diagnostic, error) { +- // Exit early if the context has been canceled. This also protects us +- // from a race on Options, see golang/go#36699. +- if ctx.Err() != nil { +- return nil, ctx.Err() +- } - --"literal" types are like Go anonymous structs, so they have to be given a name. (All instances --of the remaining types have to be given names. One approach is to construct the name from the components --of the type, but this leads to misleading punning, and is unstable if components are added. The other approach --is to construct the name from the context of the definition, that is, from the types it is defined within. --For instance `Lit__InitializeParams_clientInfo` is the "literal" type at the --`clientInfo` field in the `_InitializeParams` --struct. Although this choice is sensitive to the ordering of the components, the code uses this approach, --presuming that reordering components is an unlikely protocol change.) +- options := snapshot.Options() +- categories := []map[string]*settings.Analyzer{ +- options.DefaultAnalyzers, +- options.StaticcheckAnalyzers, +- } - --"tuple" types are generated as Go structs. (There is only one, with two `uint32` fields.) +- var analyzers []*settings.Analyzer +- for _, cat := range categories { +- for _, a := range cat { +- analyzers = append(analyzers, a) +- } +- } - --"and" types are Go structs with embedded type names. (There is only one, `And_Param_workspace_configuration`.) +- analysisDiagnostics, err := snapshot.Analyze(ctx, pkgIDs, analyzers, tracker) +- if err != nil { +- return nil, err +- } +- byURI := func(d *cache.Diagnostic) protocol.DocumentURI { return d.URI } +- return maps.Group(analysisDiagnostics, byURI), nil +-} +diff -urN a/gopls/internal/golang/embeddirective.go b/gopls/internal/golang/embeddirective.go +--- a/gopls/internal/golang/embeddirective.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/embeddirective.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,195 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --"or" types are the most complicated. There are a lot of them and there is no simple Go equivalent. --They are defined as structs with a single `Value interface{}` field and custom json marshaling --and unmarshaling code. Users can assign anything to `Value` but the type will be checked, and --correctly marshaled, by the custom marshaling code. The unmarshaling code checks types, so `Value` --will have one of the permitted types. (`nil` is always allowed.) There are about 40 "or" types that --have a single non-null component, and these are converted to the component type. +-package golang - --## Processing +-import ( +- "errors" +- "fmt" +- "io/fs" +- "path/filepath" +- "strconv" +- "strings" +- "unicode" +- "unicode/utf8" - --The code parses the json specification file, and scans all the types. It assigns names, as described --above, to the types that are unnamed in the specification, and constructs Go equivalents as required. --(Most of this code is in typenames.go.) +- "golang.org/x/tools/gopls/internal/protocol" +-) - --There are four output files. tsclient.go and tsserver.go contain the definition and implementation --of the `protocol.Client` and `protocol.Server` types and the code that dispatches on the Method --of the Request or Notification. tsjson.go contains the custom marshaling and unmarshaling code. --And tsprotocol.go contains the type and const definitions. +-// ErrNoEmbed is returned by EmbedDefinition when no embed +-// directive is found at a particular position. +-// As such it indicates that other definitions could be worth checking. +-var ErrNoEmbed = errors.New("no embed directive found") - --### Accommodating gopls +-var errStopWalk = errors.New("stop walk") - --As the code generates output, mostly in generateoutput.go and main.go, --it makes adjustments so that no changes are required to the existing Go code. --(Organizing the computation this way makes the code's structure simpler, but results in --a lot of unused types.) --There are three major classes of these adjustments, and leftover special cases. +-// EmbedDefinition finds a file matching the embed directive at pos in the mapped file. +-// If there is no embed directive at pos, returns ErrNoEmbed. +-// If multiple files match the embed pattern, one is picked at random. +-func EmbedDefinition(m *protocol.Mapper, pos protocol.Position) ([]protocol.Location, error) { +- pattern, _ := parseEmbedDirective(m, pos) +- if pattern == "" { +- return nil, ErrNoEmbed +- } - --The first major --adjustment is to change generated type names to the ones gopls expects. Some of these don't change the --semantics of the type, just the name. --But for historical reasons a lot of them replace "or" types by a single --component of the type. (Until fairly recently Go only saw or used only one of components.) --The `goplsType` map in tables.go controls this process. +- // Find the first matching file. +- var match string +- dir := filepath.Dir(m.URI.Path()) +- err := filepath.WalkDir(dir, func(abs string, d fs.DirEntry, e error) error { +- if e != nil { +- return e +- } +- rel, err := filepath.Rel(dir, abs) +- if err != nil { +- return err +- } +- ok, err := filepath.Match(pattern, rel) +- if err != nil { +- return err +- } +- if ok && !d.IsDir() { +- match = abs +- return errStopWalk +- } +- return nil +- }) +- if err != nil && !errors.Is(err, errStopWalk) { +- return nil, err +- } +- if match == "" { +- return nil, fmt.Errorf("%q does not match any files in %q", pattern, dir) +- } - --The second major adjustment is to the types of fields of structs, which is done using the --`renameProp` map in tables.go. +- loc := protocol.Location{ +- URI: protocol.URIFromPath(match), +- Range: protocol.Range{ +- Start: protocol.Position{Line: 0, Character: 0}, +- }, +- } +- return []protocol.Location{loc}, nil +-} - --The third major adjustment handles optionality, controlling `*` and `,omitempty` placement when --the default rules don't match what gopls is expecting. (The map is `goplsStar`, also in tables.go) --(If the intermediate components in expressions of the form `A.B.C.S` were optional, the code would need --a lot of useless checking for nils. Typescript has a language construct to avoid most checks.) +-// parseEmbedDirective attempts to parse a go:embed directive argument at pos. +-// If successful it return the directive argument and its range, else zero values are returned. +-func parseEmbedDirective(m *protocol.Mapper, pos protocol.Position) (string, protocol.Range) { +- lineStart, err := m.PositionOffset(protocol.Position{Line: pos.Line, Character: 0}) +- if err != nil { +- return "", protocol.Range{} +- } +- lineEnd, err := m.PositionOffset(protocol.Position{Line: pos.Line + 1, Character: 0}) +- if err != nil { +- return "", protocol.Range{} +- } - --Then there are some additional special cases. There are a few places with adjustments to avoid --recursive types. For instance `LSPArray` is `[]LSPAny`, but `LSPAny` is an "or" type including `LSPArray`. --The solution is to make `LSPAny` an `interface{}`. Another instance is `_InitializeParams.trace` --whose type is an "or" of 3 stringLiterals, which just becomes a `string`. +- text := string(m.Content[lineStart:lineEnd]) +- if !strings.HasPrefix(text, "//go:embed") { +- return "", protocol.Range{} +- } +- text = text[len("//go:embed"):] +- offset := lineStart + len("//go:embed") - --### Checking +- // Find the first pattern in text that covers the offset of the pos we are looking for. +- findOffset, err := m.PositionOffset(pos) +- if err != nil { +- return "", protocol.Range{} +- } +- patterns, err := parseGoEmbed(text, offset) +- if err != nil { +- return "", protocol.Range{} +- } +- for _, p := range patterns { +- if p.startOffset <= findOffset && findOffset <= p.endOffset { +- // Found our match. +- rng, err := m.OffsetRange(p.startOffset, p.endOffset) +- if err != nil { +- return "", protocol.Range{} +- } +- return p.pattern, rng +- } +- } - --`TestAll(t *testing.T)` checks that there are no unexpected fields in the json specification. +- return "", protocol.Range{} +-} - --While the code is executing, it checks that all the entries in the maps in tables.go are used. --It also checks that the entries in `renameProp` and `goplsStar` are not redundant. +-type fileEmbed struct { +- pattern string +- startOffset int +- endOffset int +-} - --As a one-time check on the first release of this code, diff-ing the existing and generated tsclient.go --and tsserver.go code results in only whitespace and comment diffs. The existing and generated --tsprotocol.go differ in whitespace and comments, and in a substantial number of new type definitions --that the older, more heuristic, code did not generate. (And the unused type `_InitializeParams` differs --slightly between the new and the old, and is not worth fixing.) +-// parseGoEmbed patterns that come after the directive. +-// +-// Copied and adapted from go/build/read.go. +-// Replaced token.Position with start/end offset (including quotes if present). +-func parseGoEmbed(args string, offset int) ([]fileEmbed, error) { +- trimBytes := func(n int) { +- offset += n +- args = args[n:] +- } +- trimSpace := func() { +- trim := strings.TrimLeftFunc(args, unicode.IsSpace) +- trimBytes(len(args) - len(trim)) +- } - --### Some history +- var list []fileEmbed +- for trimSpace(); args != ""; trimSpace() { +- var path string +- pathOffset := offset +- Switch: +- switch args[0] { +- default: +- i := len(args) +- for j, c := range args { +- if unicode.IsSpace(c) { +- i = j +- break +- } +- } +- path = args[:i] +- trimBytes(i) - --The original stub code was written by hand, but with the protocol under active development, that --couldn't last. The web page existed before the json specification, but it lagged the implementation --and was hard to process by machine. So the earlier version of the generating code was written in Typescript, and --used the Typescript compiler's API to parse the protocol code in the repository. --It then used a set of heuristics --to pick out the elements of the protocol, and another set of overlapping heuristics to create the Go code. --The output was functional, but idiosyncratic, and the code was fragile and barely maintainable. +- case '`': +- var ok bool +- path, _, ok = strings.Cut(args[1:], "`") +- if !ok { +- return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args) +- } +- trimBytes(1 + len(path) + 1) - --### The future +- case '"': +- i := 1 +- for ; i < len(args); i++ { +- if args[i] == '\\' { +- i++ +- continue +- } +- if args[i] == '"' { +- q, err := strconv.Unquote(args[:i+1]) +- if err != nil { +- return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args[:i+1]) +- } +- path = q +- trimBytes(i + 1) +- break Switch +- } +- } +- if i >= len(args) { +- return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args) +- } +- } - --Most of the adjustments using the maps in tables.go could be removed by making changes, mostly to names, --in the gopls code. Using more "or" types in gopls requires more elaborate, but stereotyped, changes. --But even without all the adjustments, making this its own module would face problems; a number of --dependencies would have to be factored out. And, it is fragile. The custom unmarshaling code knows what --types it expects. A design that return an 'any' on unexpected types would match the json --'ignore unexpected values' philosophy better, but the Go code would need extra checking. -diff -urN a/gopls/internal/lsp/protocol/generate/tables.go b/gopls/internal/lsp/protocol/generate/tables.go ---- a/gopls/internal/lsp/protocol/generate/tables.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/protocol/generate/tables.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,341 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. +- if args != "" { +- r, _ := utf8.DecodeRuneInString(args) +- if !unicode.IsSpace(r) { +- return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args) +- } +- } +- list = append(list, fileEmbed{ +- pattern: path, +- startOffset: pathOffset, +- endOffset: offset, +- }) +- } +- return list, nil +-} +diff -urN a/gopls/internal/golang/extract.go b/gopls/internal/golang/extract.go +--- a/gopls/internal/golang/extract.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/extract.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,1361 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --//go:build go1.19 --// +build go1.19 -- --package main -- --import "log" +-package golang - --// prop combines the name of a property with the name of the structure it is in. --type prop [2]string +-import ( +- "bytes" +- "fmt" +- "go/ast" +- "go/format" +- "go/parser" +- "go/token" +- "go/types" +- "sort" +- "strings" +- "text/scanner" - --const ( -- nothing = iota -- wantStar -- wantOpt -- wantOptStar +- "golang.org/x/tools/go/analysis" +- "golang.org/x/tools/go/ast/astutil" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/internal/analysisinternal" -) - --// goplsStar records the optionality of each field in the protocol. --// The comments are vague hints as to why removing the line is not trivial. --// A.B.C.D means that one of B or C would change to a pointer --// so a test or initialization would be needed --var goplsStar = map[prop]int{ -- {"ClientCapabilities", "textDocument"}: wantOpt, // A.B.C.D at fake/editor.go:255 -- {"ClientCapabilities", "window"}: wantOpt, // regtest failures -- {"ClientCapabilities", "workspace"}: wantOpt, // regtest failures -- {"CodeAction", "kind"}: wantOpt, // A.B.C.D +-func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { +- tokFile := fset.File(file.Pos()) +- expr, path, ok, err := CanExtractVariable(start, end, file) +- if !ok { +- return nil, nil, fmt.Errorf("extractVariable: cannot extract %s: %v", safetoken.StartPosition(fset, start), err) +- } - -- {"CodeActionClientCapabilities", "codeActionLiteralSupport"}: wantOpt, // regtest failures +- // Create new AST node for extracted code. +- var lhsNames []string +- switch expr := expr.(type) { +- // TODO: stricter rules for selectorExpr. +- case *ast.BasicLit, *ast.CompositeLit, *ast.IndexExpr, *ast.SliceExpr, +- *ast.UnaryExpr, *ast.BinaryExpr, *ast.SelectorExpr: +- lhsName, _ := generateAvailableIdentifier(expr.Pos(), path, pkg, info, "x", 0) +- lhsNames = append(lhsNames, lhsName) +- case *ast.CallExpr: +- tup, ok := info.TypeOf(expr).(*types.Tuple) +- if !ok { +- // If the call expression only has one return value, we can treat it the +- // same as our standard extract variable case. +- lhsName, _ := generateAvailableIdentifier(expr.Pos(), path, pkg, info, "x", 0) +- lhsNames = append(lhsNames, lhsName) +- break +- } +- idx := 0 +- for i := 0; i < tup.Len(); i++ { +- // Generate a unique variable for each return value. +- var lhsName string +- lhsName, idx = generateAvailableIdentifier(expr.Pos(), path, pkg, info, "x", idx) +- lhsNames = append(lhsNames, lhsName) +- } +- default: +- return nil, nil, fmt.Errorf("cannot extract %T", expr) +- } - -- {"CompletionClientCapabilities", "completionItem"}: wantOpt, // A.B.C.D -- {"CompletionClientCapabilities", "insertTextMode"}: wantOpt, // A.B.C.D -- {"CompletionItem", "kind"}: wantOpt, // need temporary variables -- {"CompletionParams", "context"}: wantOpt, // needs nil checks +- insertBeforeStmt := analysisinternal.StmtToInsertVarBefore(path) +- if insertBeforeStmt == nil { +- return nil, nil, fmt.Errorf("cannot find location to insert extraction") +- } +- indent, err := calculateIndentation(src, tokFile, insertBeforeStmt) +- if err != nil { +- return nil, nil, err +- } +- newLineIndent := "\n" + indent - -- {"Diagnostic", "severity"}: wantOpt, // nil checks or more careful thought -- {"DidSaveTextDocumentParams", "text"}: wantOptStar, // capabilities_test.go:112 logic -- {"DocumentHighlight", "kind"}: wantOpt, // need temporary variables -- {"Hover", "range"}: wantOpt, // complex expressions -- {"InlayHint", "kind"}: wantOpt, // temporary variables -- -- {"Lit_CompletionClientCapabilities_completionItem", "tagSupport"}: nothing, // A.B.C. -- {"Lit_SemanticTokensClientCapabilities_requests", "full"}: nothing, // A.B.C.D -- {"Lit_SemanticTokensClientCapabilities_requests", "range"}: nothing, // A.B.C.D -- {"Lit_SemanticTokensClientCapabilities_requests_full_Item1", "delta"}: nothing, // A.B.C.D -- {"Lit_SemanticTokensOptions_full_Item1", "delta"}: nothing, // A.B.C. -- -- {"Lit_TextDocumentContentChangeEvent_Item0", "range"}: wantStar, // == nil test +- lhs := strings.Join(lhsNames, ", ") +- assignStmt := &ast.AssignStmt{ +- Lhs: []ast.Expr{ast.NewIdent(lhs)}, +- Tok: token.DEFINE, +- Rhs: []ast.Expr{expr}, +- } +- var buf bytes.Buffer +- if err := format.Node(&buf, fset, assignStmt); err != nil { +- return nil, nil, err +- } +- assignment := strings.ReplaceAll(buf.String(), "\n", newLineIndent) + newLineIndent - -- {"TextDocumentClientCapabilities", "codeAction"}: wantOpt, // A.B.C.D -- {"TextDocumentClientCapabilities", "completion"}: wantOpt, // A.B.C.D -- {"TextDocumentClientCapabilities", "documentSymbol"}: wantOpt, // A.B.C.D -- {"TextDocumentClientCapabilities", "publishDiagnostics"}: wantOpt, //A.B.C.D -- {"TextDocumentClientCapabilities", "semanticTokens"}: wantOpt, // A.B.C.D -- {"TextDocumentSyncOptions", "change"}: wantOpt, // &constant -- {"WorkDoneProgressParams", "workDoneToken"}: wantOpt, // regtest -- {"WorkspaceClientCapabilities", "didChangeConfiguration"}: wantOpt, // A.B.C.D -- {"WorkspaceClientCapabilities", "didChangeWatchedFiles"}: wantOpt, // A.B.C.D +- return fset, &analysis.SuggestedFix{ +- TextEdits: []analysis.TextEdit{ +- { +- Pos: insertBeforeStmt.Pos(), +- End: insertBeforeStmt.Pos(), +- NewText: []byte(assignment), +- }, +- { +- Pos: start, +- End: end, +- NewText: []byte(lhs), +- }, +- }, +- }, nil -} - --// keep track of which entries in goplsStar are used --var usedGoplsStar = make(map[prop]bool) -- --// For gopls compatibility, use a different, typically more restrictive, type for some fields. --var renameProp = map[prop]string{ -- {"CancelParams", "id"}: "interface{}", -- {"Command", "arguments"}: "[]json.RawMessage", -- {"CompletionItem", "textEdit"}: "TextEdit", -- {"Diagnostic", "code"}: "interface{}", -- {"Diagnostic", "data"}: "json.RawMessage", // delay unmarshalling quickfixes -- -- {"DocumentDiagnosticReportPartialResult", "relatedDocuments"}: "map[DocumentURI]interface{}", -- -- {"ExecuteCommandParams", "arguments"}: "[]json.RawMessage", -- {"FoldingRange", "kind"}: "string", -- {"Hover", "contents"}: "MarkupContent", -- {"InlayHint", "label"}: "[]InlayHintLabelPart", -- -- {"RelatedFullDocumentDiagnosticReport", "relatedDocuments"}: "map[DocumentURI]interface{}", -- {"RelatedUnchangedDocumentDiagnosticReport", "relatedDocuments"}: "map[DocumentURI]interface{}", -- -- // PJW: this one is tricky. -- {"ServerCapabilities", "codeActionProvider"}: "interface{}", -- -- {"ServerCapabilities", "inlayHintProvider"}: "interface{}", -- // slightly tricky -- {"ServerCapabilities", "renameProvider"}: "interface{}", -- // slightly tricky -- {"ServerCapabilities", "semanticTokensProvider"}: "interface{}", -- // slightly tricky -- {"ServerCapabilities", "textDocumentSync"}: "interface{}", -- {"TextDocumentEdit", "edits"}: "[]TextEdit", -- {"TextDocumentSyncOptions", "save"}: "SaveOptions", -- {"WorkspaceEdit", "documentChanges"}: "[]DocumentChanges", +-// CanExtractVariable reports whether the code in the given range can be +-// extracted to a variable. +-func CanExtractVariable(start, end token.Pos, file *ast.File) (ast.Expr, []ast.Node, bool, error) { +- if start == end { +- return nil, nil, false, fmt.Errorf("start and end are equal") +- } +- path, _ := astutil.PathEnclosingInterval(file, start, end) +- if len(path) == 0 { +- return nil, nil, false, fmt.Errorf("no path enclosing interval") +- } +- for _, n := range path { +- if _, ok := n.(*ast.ImportSpec); ok { +- return nil, nil, false, fmt.Errorf("cannot extract variable in an import block") +- } +- } +- node := path[0] +- if start != node.Pos() || end != node.End() { +- return nil, nil, false, fmt.Errorf("range does not map to an AST node") +- } +- expr, ok := node.(ast.Expr) +- if !ok { +- return nil, nil, false, fmt.Errorf("node is not an expression") +- } +- switch expr.(type) { +- case *ast.BasicLit, *ast.CompositeLit, *ast.IndexExpr, *ast.CallExpr, +- *ast.SliceExpr, *ast.UnaryExpr, *ast.BinaryExpr, *ast.SelectorExpr: +- return expr, path, true, nil +- } +- return nil, nil, false, fmt.Errorf("cannot extract an %T to a variable", expr) -} - --// which entries of renameProp were used --var usedRenameProp = make(map[prop]bool) -- --type adjust struct { -- prefix, suffix string +-// Calculate indentation for insertion. +-// When inserting lines of code, we must ensure that the lines have consistent +-// formatting (i.e. the proper indentation). To do so, we observe the indentation on the +-// line of code on which the insertion occurs. +-func calculateIndentation(content []byte, tok *token.File, insertBeforeStmt ast.Node) (string, error) { +- line := safetoken.Line(tok, insertBeforeStmt.Pos()) +- lineOffset, stmtOffset, err := safetoken.Offsets(tok, tok.LineStart(line), insertBeforeStmt.Pos()) +- if err != nil { +- return "", err +- } +- return string(content[lineOffset:stmtOffset]), nil -} - --// disambiguate specifies prefixes or suffixes to add to all values of --// some enum types to avoid name conflicts --var disambiguate = map[string]adjust{ -- "CodeActionTriggerKind": {"CodeAction", ""}, -- "CompletionItemKind": {"", "Completion"}, -- "CompletionItemTag": {"Compl", ""}, -- "DiagnosticSeverity": {"Severity", ""}, -- "DocumentDiagnosticReportKind": {"Diagnostic", ""}, -- "FileOperationPatternKind": {"", "Pattern"}, -- "InlineCompletionTriggerKind": {"Inline", ""}, -- "InsertTextFormat": {"", "TextFormat"}, -- "SemanticTokenModifiers": {"Mod", ""}, -- "SemanticTokenTypes": {"", "Type"}, -- "SignatureHelpTriggerKind": {"Sig", ""}, -- "SymbolTag": {"", "Symbol"}, -- "WatchKind": {"Watch", ""}, +-// generateAvailableIdentifier adjusts the new function name until there are no collisions in scope. +-// Possible collisions include other function and variable names. Returns the next index to check for prefix. +-func generateAvailableIdentifier(pos token.Pos, path []ast.Node, pkg *types.Package, info *types.Info, prefix string, idx int) (string, int) { +- scopes := CollectScopes(info, path, pos) +- scopes = append(scopes, pkg.Scope()) +- return generateIdentifier(idx, prefix, func(name string) bool { +- for _, scope := range scopes { +- if scope != nil && scope.Lookup(name) != nil { +- return true +- } +- } +- return false +- }) -} - --// which entries of disambiguate got used --var usedDisambiguate = make(map[string]bool) -- --// for gopls compatibility, replace generated type names with existing ones --var goplsType = map[string]string{ -- "And_RegOpt_textDocument_colorPresentation": "WorkDoneProgressOptionsAndTextDocumentRegistrationOptions", -- "ConfigurationParams": "ParamConfiguration", -- "DocumentDiagnosticParams": "string", -- "DocumentDiagnosticReport": "string", -- "DocumentUri": "DocumentURI", -- "InitializeParams": "ParamInitialize", -- "LSPAny": "interface{}", -- -- "Lit_CodeActionClientCapabilities_codeActionLiteralSupport": "PCodeActionLiteralSupportPCodeAction", -- "Lit_CodeActionClientCapabilities_codeActionLiteralSupport_codeActionKind": "FCodeActionKindPCodeActionLiteralSupport", -- -- "Lit_CodeActionClientCapabilities_resolveSupport": "PResolveSupportPCodeAction", -- "Lit_CodeAction_disabled": "PDisabledMsg_textDocument_codeAction", -- "Lit_CompletionClientCapabilities_completionItem": "PCompletionItemPCompletion", -- "Lit_CompletionClientCapabilities_completionItemKind": "PCompletionItemKindPCompletion", -- -- "Lit_CompletionClientCapabilities_completionItem_insertTextModeSupport": "FInsertTextModeSupportPCompletionItem", -- -- "Lit_CompletionClientCapabilities_completionItem_resolveSupport": "FResolveSupportPCompletionItem", -- "Lit_CompletionClientCapabilities_completionItem_tagSupport": "FTagSupportPCompletionItem", -- -- "Lit_CompletionClientCapabilities_completionList": "PCompletionListPCompletion", -- "Lit_CompletionList_itemDefaults": "PItemDefaultsMsg_textDocument_completion", -- "Lit_CompletionList_itemDefaults_editRange_Item1": "FEditRangePItemDefaults", -- "Lit_CompletionOptions_completionItem": "PCompletionItemPCompletionProvider", -- "Lit_DocumentSymbolClientCapabilities_symbolKind": "PSymbolKindPDocumentSymbol", -- "Lit_DocumentSymbolClientCapabilities_tagSupport": "PTagSupportPDocumentSymbol", -- "Lit_FoldingRangeClientCapabilities_foldingRange": "PFoldingRangePFoldingRange", -- "Lit_FoldingRangeClientCapabilities_foldingRangeKind": "PFoldingRangeKindPFoldingRange", -- "Lit_GeneralClientCapabilities_staleRequestSupport": "PStaleRequestSupportPGeneral", -- "Lit_InitializeResult_serverInfo": "PServerInfoMsg_initialize", -- "Lit_InlayHintClientCapabilities_resolveSupport": "PResolveSupportPInlayHint", -- "Lit_MarkedString_Item1": "Msg_MarkedString", -- "Lit_NotebookDocumentChangeEvent_cells": "PCellsPChange", -- "Lit_NotebookDocumentChangeEvent_cells_structure": "FStructurePCells", -- "Lit_NotebookDocumentFilter_Item0": "Msg_NotebookDocumentFilter", -- -- "Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item0": "PNotebookSelectorPNotebookDocumentSync", -- -- "Lit_PrepareRenameResult_Item1": "Msg_PrepareRename2Gn", -- -- "Lit_PublishDiagnosticsClientCapabilities_tagSupport": "PTagSupportPPublishDiagnostics", -- "Lit_SemanticTokensClientCapabilities_requests": "PRequestsPSemanticTokens", -- "Lit_SemanticTokensClientCapabilities_requests_full_Item1": "FFullPRequests", -- "Lit_SemanticTokensClientCapabilities_requests_range_Item1": "FRangePRequests", -- -- "Lit_SemanticTokensOptions_full_Item1": "PFullESemanticTokensOptions", -- "Lit_SemanticTokensOptions_range_Item1": "PRangeESemanticTokensOptions", -- "Lit_ServerCapabilities_workspace": "Workspace6Gn", -- -- "Lit_ShowMessageRequestClientCapabilities_messageActionItem": "PMessageActionItemPShowMessage", -- "Lit_SignatureHelpClientCapabilities_signatureInformation": "PSignatureInformationPSignatureHelp", -- -- "Lit_SignatureHelpClientCapabilities_signatureInformation_parameterInformation": "FParameterInformationPSignatureInformation", -- -- "Lit_TextDocumentContentChangeEvent_Item0": "Msg_TextDocumentContentChangeEvent", -- "Lit_TextDocumentFilter_Item0": "Msg_TextDocumentFilter", -- "Lit_TextDocumentFilter_Item1": "Msg_TextDocumentFilter", -- "Lit_WorkspaceEditClientCapabilities_changeAnnotationSupport": "PChangeAnnotationSupportPWorkspaceEdit", -- "Lit_WorkspaceSymbolClientCapabilities_resolveSupport": "PResolveSupportPSymbol", -- "Lit_WorkspaceSymbolClientCapabilities_symbolKind": "PSymbolKindPSymbol", -- "Lit_WorkspaceSymbolClientCapabilities_tagSupport": "PTagSupportPSymbol", -- "Lit_WorkspaceSymbol_location_Item1": "PLocationMsg_workspace_symbol", -- "Lit__InitializeParams_clientInfo": "Msg_XInitializeParams_clientInfo", -- "Or_CompletionList_itemDefaults_editRange": "OrFEditRangePItemDefaults", -- "Or_Declaration": "[]Location", -- "Or_DidChangeConfigurationRegistrationOptions_section": "OrPSection_workspace_didChangeConfiguration", -- "Or_GlobPattern": "string", -- "Or_InlayHintLabelPart_tooltip": "OrPTooltipPLabel", -- "Or_InlayHint_tooltip": "OrPTooltip_textDocument_inlayHint", -- "Or_LSPAny": "interface{}", -- "Or_NotebookDocumentFilter": "Msg_NotebookDocumentFilter", -- "Or_NotebookDocumentSyncOptions_notebookSelector_Elem": "PNotebookSelectorPNotebookDocumentSync", -- -- "Or_NotebookDocumentSyncOptions_notebookSelector_Elem_Item0_notebook": "OrFNotebookPNotebookSelector", -- -- "Or_ParameterInformation_documentation": "string", -- "Or_ParameterInformation_label": "string", -- "Or_PrepareRenameResult": "Msg_PrepareRename2Gn", -- "Or_ProgressToken": "interface{}", -- "Or_Result_textDocument_completion": "CompletionList", -- "Or_Result_textDocument_declaration": "Or_textDocument_declaration", -- "Or_Result_textDocument_definition": "[]Location", -- "Or_Result_textDocument_documentSymbol": "[]interface{}", -- "Or_Result_textDocument_implementation": "[]Location", -- "Or_Result_textDocument_semanticTokens_full_delta": "interface{}", -- "Or_Result_textDocument_typeDefinition": "[]Location", -- "Or_Result_workspace_symbol": "[]SymbolInformation", -- "Or_TextDocumentContentChangeEvent": "Msg_TextDocumentContentChangeEvent", -- "Or_TextDocumentFilter": "Msg_TextDocumentFilter", -- "Or_WorkspaceFoldersServerCapabilities_changeNotifications": "string", -- "Or_WorkspaceSymbol_location": "OrPLocation_workspace_symbol", -- "PrepareRenameResult": "PrepareRename2Gn", -- "Tuple_ParameterInformation_label_Item1": "UIntCommaUInt", -- "WorkspaceFoldersServerCapabilities": "WorkspaceFolders5Gn", -- "[]LSPAny": "[]interface{}", -- "[]Or_NotebookDocumentSyncOptions_notebookSelector_Elem": "[]PNotebookSelectorPNotebookDocumentSync", -- "[]Or_Result_textDocument_codeAction_Item0_Elem": "[]CodeAction", -- "[]PreviousResultId": "[]PreviousResultID", -- "[]uinteger": "[]uint32", -- "boolean": "bool", -- "decimal": "float64", -- "integer": "int32", -- "map[DocumentUri][]TextEdit": "map[DocumentURI][]TextEdit", -- "uinteger": "uint32", +-func generateIdentifier(idx int, prefix string, hasCollision func(string) bool) (string, int) { +- name := prefix +- if idx != 0 { +- name += fmt.Sprintf("%d", idx) +- } +- for hasCollision(name) { +- idx++ +- name = fmt.Sprintf("%v%d", prefix, idx) +- } +- return name, idx + 1 -} - --var usedGoplsType = make(map[string]bool) -- --// methodNames is a map from the method to the name of the function that handles it --var methodNames = map[string]string{ -- "$/cancelRequest": "CancelRequest", -- "$/logTrace": "LogTrace", -- "$/progress": "Progress", -- "$/setTrace": "SetTrace", -- "callHierarchy/incomingCalls": "IncomingCalls", -- "callHierarchy/outgoingCalls": "OutgoingCalls", -- "client/registerCapability": "RegisterCapability", -- "client/unregisterCapability": "UnregisterCapability", -- "codeAction/resolve": "ResolveCodeAction", -- "codeLens/resolve": "ResolveCodeLens", -- "completionItem/resolve": "ResolveCompletionItem", -- "documentLink/resolve": "ResolveDocumentLink", -- "exit": "Exit", -- "initialize": "Initialize", -- "initialized": "Initialized", -- "inlayHint/resolve": "Resolve", -- "notebookDocument/didChange": "DidChangeNotebookDocument", -- "notebookDocument/didClose": "DidCloseNotebookDocument", -- "notebookDocument/didOpen": "DidOpenNotebookDocument", -- "notebookDocument/didSave": "DidSaveNotebookDocument", -- "shutdown": "Shutdown", -- "telemetry/event": "Event", -- "textDocument/codeAction": "CodeAction", -- "textDocument/codeLens": "CodeLens", -- "textDocument/colorPresentation": "ColorPresentation", -- "textDocument/completion": "Completion", -- "textDocument/declaration": "Declaration", -- "textDocument/definition": "Definition", -- "textDocument/diagnostic": "Diagnostic", -- "textDocument/didChange": "DidChange", -- "textDocument/didClose": "DidClose", -- "textDocument/didOpen": "DidOpen", -- "textDocument/didSave": "DidSave", -- "textDocument/documentColor": "DocumentColor", -- "textDocument/documentHighlight": "DocumentHighlight", -- "textDocument/documentLink": "DocumentLink", -- "textDocument/documentSymbol": "DocumentSymbol", -- "textDocument/foldingRange": "FoldingRange", -- "textDocument/formatting": "Formatting", -- "textDocument/hover": "Hover", -- "textDocument/implementation": "Implementation", -- "textDocument/inlayHint": "InlayHint", -- "textDocument/inlineCompletion": "InlineCompletion", -- "textDocument/inlineValue": "InlineValue", -- "textDocument/linkedEditingRange": "LinkedEditingRange", -- "textDocument/moniker": "Moniker", -- "textDocument/onTypeFormatting": "OnTypeFormatting", -- "textDocument/prepareCallHierarchy": "PrepareCallHierarchy", -- "textDocument/prepareRename": "PrepareRename", -- "textDocument/prepareTypeHierarchy": "PrepareTypeHierarchy", -- "textDocument/publishDiagnostics": "PublishDiagnostics", -- "textDocument/rangeFormatting": "RangeFormatting", -- "textDocument/rangesFormatting": "RangesFormatting", -- "textDocument/references": "References", -- "textDocument/rename": "Rename", -- "textDocument/selectionRange": "SelectionRange", -- "textDocument/semanticTokens/full": "SemanticTokensFull", -- "textDocument/semanticTokens/full/delta": "SemanticTokensFullDelta", -- "textDocument/semanticTokens/range": "SemanticTokensRange", -- "textDocument/signatureHelp": "SignatureHelp", -- "textDocument/typeDefinition": "TypeDefinition", -- "textDocument/willSave": "WillSave", -- "textDocument/willSaveWaitUntil": "WillSaveWaitUntil", -- "typeHierarchy/subtypes": "Subtypes", -- "typeHierarchy/supertypes": "Supertypes", -- "window/logMessage": "LogMessage", -- "window/showDocument": "ShowDocument", -- "window/showMessage": "ShowMessage", -- "window/showMessageRequest": "ShowMessageRequest", -- "window/workDoneProgress/cancel": "WorkDoneProgressCancel", -- "window/workDoneProgress/create": "WorkDoneProgressCreate", -- "workspace/applyEdit": "ApplyEdit", -- "workspace/codeLens/refresh": "CodeLensRefresh", -- "workspace/configuration": "Configuration", -- "workspace/diagnostic": "DiagnosticWorkspace", -- "workspace/diagnostic/refresh": "DiagnosticRefresh", -- "workspace/didChangeConfiguration": "DidChangeConfiguration", -- "workspace/didChangeWatchedFiles": "DidChangeWatchedFiles", -- "workspace/didChangeWorkspaceFolders": "DidChangeWorkspaceFolders", -- "workspace/didCreateFiles": "DidCreateFiles", -- "workspace/didDeleteFiles": "DidDeleteFiles", -- "workspace/didRenameFiles": "DidRenameFiles", -- "workspace/executeCommand": "ExecuteCommand", -- "workspace/inlayHint/refresh": "InlayHintRefresh", -- "workspace/inlineValue/refresh": "InlineValueRefresh", -- "workspace/semanticTokens/refresh": "SemanticTokensRefresh", -- "workspace/symbol": "Symbol", -- "workspace/willCreateFiles": "WillCreateFiles", -- "workspace/willDeleteFiles": "WillDeleteFiles", -- "workspace/willRenameFiles": "WillRenameFiles", -- "workspace/workspaceFolders": "WorkspaceFolders", -- "workspaceSymbol/resolve": "ResolveWorkspaceSymbol", +-// returnVariable keeps track of the information we need to properly introduce a new variable +-// that we will return in the extracted function. +-type returnVariable struct { +- // name is the identifier that is used on the left-hand side of the call to +- // the extracted function. +- name ast.Expr +- // decl is the declaration of the variable. It is used in the type signature of the +- // extracted function and for variable declarations. +- decl *ast.Field +- // zeroVal is the "zero value" of the type of the variable. It is used in a return +- // statement in the extracted function. +- zeroVal ast.Expr -} - --func methodName(method string) string { -- ans := methodNames[method] -- if ans == "" { -- log.Fatalf("unknown method %q", method) -- } -- return ans +-// extractMethod refactors the selected block of code into a new method. +-func extractMethod(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { +- return extractFunctionMethod(fset, start, end, src, file, pkg, info, true) -} -diff -urN a/gopls/internal/lsp/protocol/generate/typenames.go b/gopls/internal/lsp/protocol/generate/typenames.go ---- a/gopls/internal/lsp/protocol/generate/typenames.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/protocol/generate/typenames.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,184 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --//go:build go1.19 --// +build go1.19 -- --package main - --import ( -- "fmt" -- "log" -- "strings" --) -- --var typeNames = make(map[*Type]string) --var genTypes []*newType +-// extractFunction refactors the selected block of code into a new function. +-func extractFunction(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { +- return extractFunctionMethod(fset, start, end, src, file, pkg, info, false) +-} - --func findTypeNames(model Model) { -- for _, s := range model.Structures { -- for _, e := range s.Extends { -- nameType(e, nil) // all references -- } -- for _, m := range s.Mixins { -- nameType(m, nil) // all references -- } -- for _, p := range s.Properties { -- nameType(p.Type, []string{s.Name, p.Name}) -- } +-// extractFunctionMethod refactors the selected block of code into a new function/method. +-// It also replaces the selected block of code with a call to the extracted +-// function. First, we manually adjust the selection range. We remove trailing +-// and leading whitespace characters to ensure the range is precisely bounded +-// by AST nodes. Next, we determine the variables that will be the parameters +-// and return values of the extracted function/method. Lastly, we construct the call +-// of the function/method and insert this call as well as the extracted function/method into +-// their proper locations. +-func extractFunctionMethod(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, pkg *types.Package, info *types.Info, isMethod bool) (*token.FileSet, *analysis.SuggestedFix, error) { +- errorPrefix := "extractFunction" +- if isMethod { +- errorPrefix = "extractMethod" - } -- for _, t := range model.Enumerations { -- nameType(t.Type, []string{t.Name}) +- +- tok := fset.File(file.Pos()) +- if tok == nil { +- return nil, nil, bug.Errorf("no file for position") - } -- for _, t := range model.TypeAliases { -- nameType(t.Type, []string{t.Name}) +- p, ok, methodOk, err := CanExtractFunction(tok, start, end, src, file) +- if (!ok && !isMethod) || (!methodOk && isMethod) { +- return nil, nil, fmt.Errorf("%s: cannot extract %s: %v", errorPrefix, +- safetoken.StartPosition(fset, start), err) - } -- for _, r := range model.Requests { -- nameType(r.Params, []string{"Param", r.Method}) -- nameType(r.Result, []string{"Result", r.Method}) -- nameType(r.RegistrationOptions, []string{"RegOpt", r.Method}) +- tok, path, start, end, outer, node := p.tok, p.path, p.start, p.end, p.outer, p.node +- fileScope := info.Scopes[file] +- if fileScope == nil { +- return nil, nil, fmt.Errorf("%s: file scope is empty", errorPrefix) - } -- for _, n := range model.Notifications { -- nameType(n.Params, []string{"Param", n.Method}) -- nameType(n.RegistrationOptions, []string{"RegOpt", n.Method}) +- pkgScope := fileScope.Parent() +- if pkgScope == nil { +- return nil, nil, fmt.Errorf("%s: package scope is empty", errorPrefix) - } --} - --// nameType populates typeNames[t] with the computed name of the type. --// path is the list of enclosing constructs in the JSON model. --func nameType(t *Type, path []string) string { -- if t == nil || typeNames[t] != "" { -- return "" -- } -- switch t.Kind { -- case "base": -- typeNames[t] = t.Name -- return t.Name -- case "reference": -- typeNames[t] = t.Name -- return t.Name -- case "array": -- nm := "[]" + nameType(t.Element, append(path, "Elem")) -- typeNames[t] = nm -- return nm -- case "map": -- key := nameType(t.Key, nil) // never a generated type -- value := nameType(t.Value.(*Type), append(path, "Value")) -- nm := "map[" + key + "]" + value -- typeNames[t] = nm -- return nm -- // generated types -- case "and": -- nm := nameFromPath("And", path) -- typeNames[t] = nm -- for _, it := range t.Items { -- nameType(it, append(path, "Item")) +- // A return statement is non-nested if its parent node is equal to the parent node +- // of the first node in the selection. These cases must be handled separately because +- // non-nested return statements are guaranteed to execute. +- var retStmts []*ast.ReturnStmt +- var hasNonNestedReturn bool +- startParent := findParent(outer, node) +- ast.Inspect(outer, func(n ast.Node) bool { +- if n == nil { +- return false - } -- genTypes = append(genTypes, &newType{ -- name: nm, -- typ: t, -- kind: "and", -- items: t.Items, -- line: t.Line, -- }) -- return nm -- case "literal": -- nm := nameFromPath("Lit", path) -- typeNames[t] = nm -- for _, p := range t.Value.(ParseLiteral).Properties { -- nameType(p.Type, append(path, p.Name)) +- if n.Pos() < start || n.End() > end { +- return n.Pos() <= end - } -- genTypes = append(genTypes, &newType{ -- name: nm, -- typ: t, -- kind: "literal", -- properties: t.Value.(ParseLiteral).Properties, -- line: t.Line, -- }) -- return nm -- case "tuple": -- nm := nameFromPath("Tuple", path) -- typeNames[t] = nm -- for _, it := range t.Items { -- nameType(it, append(path, "Item")) +- ret, ok := n.(*ast.ReturnStmt) +- if !ok { +- return true - } -- genTypes = append(genTypes, &newType{ -- name: nm, -- typ: t, -- kind: "tuple", -- items: t.Items, -- line: t.Line, -- }) -- return nm -- case "or": -- nm := nameFromPath("Or", path) -- typeNames[t] = nm -- for i, it := range t.Items { -- // these names depend on the ordering within the "or" type -- nameType(it, append(path, fmt.Sprintf("Item%d", i))) +- if findParent(outer, n) == startParent { +- hasNonNestedReturn = true - } -- // this code handles an "or" of stringLiterals (_InitializeParams.trace) -- names := make(map[string]int) -- msg := "" -- for _, it := range t.Items { -- if line, ok := names[typeNames[it]]; ok { -- // duplicate component names are bad -- msg += fmt.Sprintf("lines %d %d dup, %s for %s\n", line, it.Line, typeNames[it], nm) -- } -- names[typeNames[it]] = t.Line +- retStmts = append(retStmts, ret) +- return false +- }) +- containsReturnStatement := len(retStmts) > 0 +- +- // Now that we have determined the correct range for the selection block, +- // we must determine the signature of the extracted function. We will then replace +- // the block with an assignment statement that calls the extracted function with +- // the appropriate parameters and return values. +- variables, err := collectFreeVars(info, file, fileScope, pkgScope, start, end, path[0]) +- if err != nil { +- return nil, nil, err +- } +- +- var ( +- receiverUsed bool +- receiver *ast.Field +- receiverName string +- receiverObj types.Object +- ) +- if isMethod { +- if outer == nil || outer.Recv == nil || len(outer.Recv.List) == 0 { +- return nil, nil, fmt.Errorf("%s: cannot extract need method receiver", errorPrefix) - } -- // this code handles an "or" of stringLiterals (_InitializeParams.trace) -- if len(names) == 1 { -- var solekey string -- for k := range names { -- solekey = k // the sole name -- } -- if solekey == "string" { // _InitializeParams.trace -- typeNames[t] = "string" -- return "string" -- } -- // otherwise unexpected -- log.Printf("unexpected: single-case 'or' type has non-string key %s: %s", nm, solekey) -- log.Fatal(msg) -- } else if len(names) == 2 { -- // if one of the names is null, just use the other, rather than generating an "or". -- // This removes about 40 types from the generated code. An entry in goplsStar -- // could be added to handle the null case, if necessary. -- newNm := "" -- sawNull := false -- for k := range names { -- if k == "null" { -- sawNull = true -- } else { -- newNm = k -- } -- } -- if sawNull { -- typeNames[t] = newNm -- return newNm -- } +- receiver = outer.Recv.List[0] +- if len(receiver.Names) == 0 || receiver.Names[0] == nil { +- return nil, nil, fmt.Errorf("%s: cannot extract need method receiver name", errorPrefix) - } -- genTypes = append(genTypes, &newType{ -- name: nm, -- typ: t, -- kind: "or", -- items: t.Items, -- line: t.Line, -- }) -- return nm -- case "stringLiteral": // a single type, like 'kind' or 'rename' -- typeNames[t] = "string" -- return "string" -- default: -- log.Fatalf("nameType: %T unexpected, line:%d path:%v", t, t.Line, path) -- panic("unreachable in nameType") +- recvName := receiver.Names[0] +- receiverName = recvName.Name +- receiverObj = info.ObjectOf(recvName) - } --} - --func nameFromPath(prefix string, path []string) string { -- nm := prefix + "_" + strings.Join(path, "_") -- // methods have slashes -- nm = strings.ReplaceAll(nm, "/", "_") -- return nm --} -diff -urN a/gopls/internal/lsp/protocol/generate/types.go b/gopls/internal/lsp/protocol/generate/types.go ---- a/gopls/internal/lsp/protocol/generate/types.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/protocol/generate/types.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,170 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --//go:build go1.19 --// +build go1.19 -- --package main +- var ( +- params, returns []ast.Expr // used when calling the extracted function +- paramTypes, returnTypes []*ast.Field // used in the signature of the extracted function +- uninitialized []types.Object // vars we will need to initialize before the call +- ) - --import ( -- "fmt" -- "sort" --) +- // Avoid duplicates while traversing vars and uninitialized. +- seenVars := make(map[types.Object]ast.Expr) +- seenUninitialized := make(map[types.Object]struct{}) - --// Model contains the parsed version of the spec --type Model struct { -- Version Metadata `json:"metaData"` -- Requests []*Request `json:"requests"` -- Notifications []*Notification `json:"notifications"` -- Structures []*Structure `json:"structures"` -- Enumerations []*Enumeration `json:"enumerations"` -- TypeAliases []*TypeAlias `json:"typeAliases"` -- Line int `json:"line"` --} +- // Some variables on the left-hand side of our assignment statement may be free. If our +- // selection begins in the same scope in which the free variable is defined, we can +- // redefine it in our assignment statement. See the following example, where 'b' and +- // 'err' (both free variables) can be redefined in the second funcCall() while maintaining +- // correctness. +- // +- // +- // Not Redefined: +- // +- // a, err := funcCall() +- // var b int +- // b, err = funcCall() +- // +- // Redefined: +- // +- // a, err := funcCall() +- // b, err := funcCall() +- // +- // We track the number of free variables that can be redefined to maintain our preference +- // of using "x, y, z := fn()" style assignment statements. +- var canRedefineCount int - --// Metadata is information about the version of the spec --type Metadata struct { -- Version string `json:"version"` -- Line int `json:"line"` --} +- // Each identifier in the selected block must become (1) a parameter to the +- // extracted function, (2) a return value of the extracted function, or (3) a local +- // variable in the extracted function. Determine the outcome(s) for each variable +- // based on whether it is free, altered within the selected block, and used outside +- // of the selected block. +- for _, v := range variables { +- if _, ok := seenVars[v.obj]; ok { +- continue +- } +- if v.obj.Name() == "_" { +- // The blank identifier is always a local variable +- continue +- } +- typ := analysisinternal.TypeExpr(file, pkg, v.obj.Type()) +- if typ == nil { +- return nil, nil, fmt.Errorf("nil AST expression for type: %v", v.obj.Name()) +- } +- seenVars[v.obj] = typ +- identifier := ast.NewIdent(v.obj.Name()) +- // An identifier must meet three conditions to become a return value of the +- // extracted function. (1) its value must be defined or reassigned within +- // the selection (isAssigned), (2) it must be used at least once after the +- // selection (isUsed), and (3) its first use after the selection +- // cannot be its own reassignment or redefinition (objOverriden). +- vscope := v.obj.Parent() +- if vscope == nil { +- return nil, nil, fmt.Errorf("parent nil") +- } +- isUsed, firstUseAfter := objUsed(info, end, vscope.End(), v.obj) +- if v.assigned && isUsed && !varOverridden(info, firstUseAfter, v.obj, v.free, outer) { +- returnTypes = append(returnTypes, &ast.Field{Type: typ}) +- returns = append(returns, identifier) +- if !v.free { +- uninitialized = append(uninitialized, v.obj) - --// A Request is the parsed version of an LSP request --type Request struct { -- Documentation string `json:"documentation"` -- ErrorData *Type `json:"errorData"` -- Direction string `json:"messageDirection"` -- Method string `json:"method"` -- Params *Type `json:"params"` -- PartialResult *Type `json:"partialResult"` -- Proposed bool `json:"proposed"` -- RegistrationMethod string `json:"registrationMethod"` -- RegistrationOptions *Type `json:"registrationOptions"` -- Result *Type `json:"result"` -- Since string `json:"since"` -- Line int `json:"line"` --} +- } else { +- // In go1.22, Scope.Pos for function scopes changed (#60752): +- // it used to start at the body ('{'), now it starts at "func". +- // +- // The second condition below handles the case when +- // v's block is the FuncDecl.Body itself. +- if vscope.Pos() == startParent.Pos() || +- startParent == outer.Body && vscope == info.Scopes[outer.Type] { +- canRedefineCount++ +- } +- } +- } +- // An identifier must meet two conditions to become a parameter of the +- // extracted function. (1) it must be free (isFree), and (2) its first +- // use within the selection cannot be its own definition (isDefined). +- if v.free && !v.defined { +- // Skip the selector for a method. +- if isMethod && v.obj == receiverObj { +- receiverUsed = true +- continue +- } +- params = append(params, identifier) +- paramTypes = append(paramTypes, &ast.Field{ +- Names: []*ast.Ident{identifier}, +- Type: typ, +- }) +- } +- } - --// A Notificatin is the parsed version of an LSP notification --type Notification struct { -- Documentation string `json:"documentation"` -- Direction string `json:"messageDirection"` -- Method string `json:"method"` -- Params *Type `json:"params"` -- Proposed bool `json:"proposed"` -- RegistrationMethod string `json:"registrationMethod"` -- RegistrationOptions *Type `json:"registrationOptions"` -- Since string `json:"since"` -- Line int `json:"line"` --} +- reorderParams(params, paramTypes) - --// A Structure is the parsed version of an LSP structure from the spec --type Structure struct { -- Documentation string `json:"documentation"` -- Extends []*Type `json:"extends"` -- Mixins []*Type `json:"mixins"` -- Name string `json:"name"` -- Properties []NameType `json:"properties"` -- Proposed bool `json:"proposed"` -- Since string `json:"since"` -- Line int `json:"line"` --} +- // Find the function literal that encloses the selection. The enclosing function literal +- // may not be the enclosing function declaration (i.e. 'outer'). For example, in the +- // following block: +- // +- // func main() { +- // ast.Inspect(node, func(n ast.Node) bool { +- // v := 1 // this line extracted +- // return true +- // }) +- // } +- // +- // 'outer' is main(). However, the extracted selection most directly belongs to +- // the anonymous function literal, the second argument of ast.Inspect(). We use the +- // enclosing function literal to determine the proper return types for return statements +- // within the selection. We still need the enclosing function declaration because this is +- // the top-level declaration. We inspect the top-level declaration to look for variables +- // as well as for code replacement. +- enclosing := outer.Type +- for _, p := range path { +- if p == enclosing { +- break +- } +- if fl, ok := p.(*ast.FuncLit); ok { +- enclosing = fl.Type +- break +- } +- } - --// An enumeration is the parsed version of an LSP enumeration from the spec --type Enumeration struct { -- Documentation string `json:"documentation"` -- Name string `json:"name"` -- Proposed bool `json:"proposed"` -- Since string `json:"since"` -- SupportsCustomValues bool `json:"supportsCustomValues"` -- Type *Type `json:"type"` -- Values []NameValue `json:"values"` -- Line int `json:"line"` --} +- // We put the selection in a constructed file. We can then traverse and edit +- // the extracted selection without modifying the original AST. +- startOffset, endOffset, err := safetoken.Offsets(tok, start, end) +- if err != nil { +- return nil, nil, err +- } +- selection := src[startOffset:endOffset] +- extractedBlock, err := parseBlockStmt(fset, selection) +- if err != nil { +- return nil, nil, err +- } - --// A TypeAlias is the parsed version of an LSP type alias from the spec --type TypeAlias struct { -- Documentation string `json:"documentation"` -- Deprecated string `json:"deprecated"` -- Name string `json:"name"` -- Proposed bool `json:"proposed"` -- Since string `json:"since"` -- Type *Type `json:"type"` -- Line int `json:"line"` --} +- // We need to account for return statements in the selected block, as they will complicate +- // the logical flow of the extracted function. See the following example, where ** denotes +- // the range to be extracted. +- // +- // Before: +- // +- // func _() int { +- // a := 1 +- // b := 2 +- // **if a == b { +- // return a +- // }** +- // ... +- // } +- // +- // After: +- // +- // func _() int { +- // a := 1 +- // b := 2 +- // cond0, ret0 := x0(a, b) +- // if cond0 { +- // return ret0 +- // } +- // ... +- // } +- // +- // func x0(a int, b int) (bool, int) { +- // if a == b { +- // return true, a +- // } +- // return false, 0 +- // } +- // +- // We handle returns by adding an additional boolean return value to the extracted function. +- // This bool reports whether the original function would have returned. Because the +- // extracted selection contains a return statement, we must also add the types in the +- // return signature of the enclosing function to the return signature of the +- // extracted function. We then add an extra if statement checking this boolean value +- // in the original function. If the condition is met, the original function should +- // return a value, mimicking the functionality of the original return statement(s) +- // in the selection. +- // +- // If there is a return that is guaranteed to execute (hasNonNestedReturns=true), then +- // we don't need to include this additional condition check and can simply return. +- // +- // Before: +- // +- // func _() int { +- // a := 1 +- // b := 2 +- // **if a == b { +- // return a +- // } +- // return b** +- // } +- // +- // After: +- // +- // func _() int { +- // a := 1 +- // b := 2 +- // return x0(a, b) +- // } +- // +- // func x0(a int, b int) int { +- // if a == b { +- // return a +- // } +- // return b +- // } - --// A NameValue describes an enumeration constant --type NameValue struct { -- Documentation string `json:"documentation"` -- Name string `json:"name"` -- Proposed bool `json:"proposed"` -- Since string `json:"since"` -- Value any `json:"value"` // number or string -- Line int `json:"line"` --} +- var retVars []*returnVariable +- var ifReturn *ast.IfStmt +- if containsReturnStatement { +- if !hasNonNestedReturn { +- // The selected block contained return statements, so we have to modify the +- // signature of the extracted function as described above. Adjust all of +- // the return statements in the extracted function to reflect this change in +- // signature. +- if err := adjustReturnStatements(returnTypes, seenVars, file, pkg, extractedBlock); err != nil { +- return nil, nil, err +- } +- } +- // Collect the additional return values and types needed to accommodate return +- // statements in the selection. Update the type signature of the extracted +- // function and construct the if statement that will be inserted in the enclosing +- // function. +- retVars, ifReturn, err = generateReturnInfo(enclosing, pkg, path, file, info, start, hasNonNestedReturn) +- if err != nil { +- return nil, nil, err +- } +- } - --// A Type is the parsed version of an LSP type from the spec, --// or a Type the code constructs --type Type struct { -- Kind string `json:"kind"` // -- which kind goes with which field -- -- Items []*Type `json:"items"` // "and", "or", "tuple" -- Element *Type `json:"element"` // "array" -- Name string `json:"name"` // "base", "reference" -- Key *Type `json:"key"` // "map" -- Value any `json:"value"` // "map", "stringLiteral", "literal" -- Line int `json:"line"` // JSON source line --} +- // Add a return statement to the end of the new function. This return statement must include +- // the values for the types of the original extracted function signature and (if a return +- // statement is present in the selection) enclosing function signature. +- // This only needs to be done if the selections does not have a non-nested return, otherwise +- // it already terminates with a return statement. +- hasReturnValues := len(returns)+len(retVars) > 0 +- if hasReturnValues && !hasNonNestedReturn { +- extractedBlock.List = append(extractedBlock.List, &ast.ReturnStmt{ +- Results: append(returns, getZeroVals(retVars)...), +- }) +- } - --// ParsedLiteral is Type.Value when Type.Kind is "literal" --type ParseLiteral struct { -- Properties `json:"properties"` --} +- // Construct the appropriate call to the extracted function. +- // We must meet two conditions to use ":=" instead of '='. (1) there must be at least +- // one variable on the lhs that is uninitialized (non-free) prior to the assignment. +- // (2) all of the initialized (free) variables on the lhs must be able to be redefined. +- sym := token.ASSIGN +- canDefineCount := len(uninitialized) + canRedefineCount +- canDefine := len(uninitialized)+len(retVars) > 0 && canDefineCount == len(returns) +- if canDefine { +- sym = token.DEFINE +- } +- var name, funName string +- if isMethod { +- name = "newMethod" +- // TODO(suzmue): generate a name that does not conflict for "newMethod". +- funName = name +- } else { +- name = "newFunction" +- funName, _ = generateAvailableIdentifier(start, path, pkg, info, name, 0) +- } +- extractedFunCall := generateFuncCall(hasNonNestedReturn, hasReturnValues, params, +- append(returns, getNames(retVars)...), funName, sym, receiverName) - --// A NameType represents the name and type of a structure element --type NameType struct { -- Name string `json:"name"` -- Type *Type `json:"type"` -- Optional bool `json:"optional"` -- Documentation string `json:"documentation"` -- Deprecated string `json:"deprecated"` -- Since string `json:"since"` -- Proposed bool `json:"proposed"` -- Line int `json:"line"` --} +- // Build the extracted function. +- newFunc := &ast.FuncDecl{ +- Name: ast.NewIdent(funName), +- Type: &ast.FuncType{ +- Params: &ast.FieldList{List: paramTypes}, +- Results: &ast.FieldList{List: append(returnTypes, getDecls(retVars)...)}, +- }, +- Body: extractedBlock, +- } +- if isMethod { +- var names []*ast.Ident +- if receiverUsed { +- names = append(names, ast.NewIdent(receiverName)) +- } +- newFunc.Recv = &ast.FieldList{ +- List: []*ast.Field{{ +- Names: names, +- Type: receiver.Type, +- }}, +- } +- } - --// Properties are the collection of structure fields --type Properties []NameType +- // Create variable declarations for any identifiers that need to be initialized prior to +- // calling the extracted function. We do not manually initialize variables if every return +- // value is uninitialized. We can use := to initialize the variables in this situation. +- var declarations []ast.Stmt +- if canDefineCount != len(returns) { +- declarations = initializeVars(uninitialized, retVars, seenUninitialized, seenVars) +- } - --// addLineNumbers adds a "line" field to each object in the JSON. --func addLineNumbers(buf []byte) []byte { -- var ans []byte -- // In the specification .json file, the delimiter '{' is -- // always followed by a newline. There are other {s embedded in strings. -- // json.Token does not return \n, or :, or , so using it would -- // require parsing the json to reconstruct the missing information. -- for linecnt, i := 1, 0; i < len(buf); i++ { -- ans = append(ans, buf[i]) -- switch buf[i] { -- case '{': -- if buf[i+1] == '\n' { -- ans = append(ans, fmt.Sprintf(`"line": %d, `, linecnt)...) -- // warning: this would fail if the spec file had -- // `"value": {\n}`, but it does not, as comma is a separator. +- var declBuf, replaceBuf, newFuncBuf, ifBuf, commentBuf bytes.Buffer +- if err := format.Node(&declBuf, fset, declarations); err != nil { +- return nil, nil, err +- } +- if err := format.Node(&replaceBuf, fset, extractedFunCall); err != nil { +- return nil, nil, err +- } +- if ifReturn != nil { +- if err := format.Node(&ifBuf, fset, ifReturn); err != nil { +- return nil, nil, err +- } +- } +- if err := format.Node(&newFuncBuf, fset, newFunc); err != nil { +- return nil, nil, err +- } +- // Find all the comments within the range and print them to be put somewhere. +- // TODO(suzmue): print these in the extracted function at the correct place. +- for _, cg := range file.Comments { +- if cg.Pos().IsValid() && cg.Pos() < end && cg.Pos() >= start { +- for _, c := range cg.List { +- fmt.Fprintln(&commentBuf, c.Text) - } -- case '\n': -- linecnt++ - } - } -- return ans --} -- --type sortedMap[T any] map[string]T - --func (s sortedMap[T]) keys() []string { -- var keys []string -- for k := range s { -- keys = append(keys, k) +- // We're going to replace the whole enclosing function, +- // so preserve the text before and after the selected block. +- outerStart, outerEnd, err := safetoken.Offsets(tok, outer.Pos(), outer.End()) +- if err != nil { +- return nil, nil, err - } -- sort.Strings(keys) -- return keys --} -diff -urN a/gopls/internal/lsp/protocol/log.go b/gopls/internal/lsp/protocol/log.go ---- a/gopls/internal/lsp/protocol/log.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/protocol/log.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,136 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package protocol -- --import ( -- "context" -- "fmt" -- "io" -- "strings" -- "sync" -- "time" -- -- "golang.org/x/tools/internal/jsonrpc2" --) +- before := src[outerStart:startOffset] +- after := src[endOffset:outerEnd] +- indent, err := calculateIndentation(src, tok, node) +- if err != nil { +- return nil, nil, err +- } +- newLineIndent := "\n" + indent - --type loggingStream struct { -- stream jsonrpc2.Stream -- logMu sync.Mutex -- log io.Writer --} +- var fullReplacement strings.Builder +- fullReplacement.Write(before) +- if commentBuf.Len() > 0 { +- comments := strings.ReplaceAll(commentBuf.String(), "\n", newLineIndent) +- fullReplacement.WriteString(comments) +- } +- if declBuf.Len() > 0 { // add any initializations, if needed +- initializations := strings.ReplaceAll(declBuf.String(), "\n", newLineIndent) + +- newLineIndent +- fullReplacement.WriteString(initializations) +- } +- fullReplacement.Write(replaceBuf.Bytes()) // call the extracted function +- if ifBuf.Len() > 0 { // add the if statement below the function call, if needed +- ifstatement := newLineIndent + +- strings.ReplaceAll(ifBuf.String(), "\n", newLineIndent) +- fullReplacement.WriteString(ifstatement) +- } +- fullReplacement.Write(after) +- fullReplacement.WriteString("\n\n") // add newlines after the enclosing function +- fullReplacement.Write(newFuncBuf.Bytes()) // insert the extracted function - --// LoggingStream returns a stream that does LSP protocol logging too --func LoggingStream(str jsonrpc2.Stream, w io.Writer) jsonrpc2.Stream { -- return &loggingStream{stream: str, log: w} +- return fset, &analysis.SuggestedFix{ +- TextEdits: []analysis.TextEdit{{ +- Pos: outer.Pos(), +- End: outer.End(), +- NewText: []byte(fullReplacement.String()), +- }}, +- }, nil -} - --func (s *loggingStream) Read(ctx context.Context) (jsonrpc2.Message, int64, error) { -- msg, count, err := s.stream.Read(ctx) -- if err == nil { -- s.logCommon(msg, true) -- } -- return msg, count, err --} -- --func (s *loggingStream) Write(ctx context.Context, msg jsonrpc2.Message) (int64, error) { -- s.logCommon(msg, false) -- count, err := s.stream.Write(ctx, msg) -- return count, err --} -- --func (s *loggingStream) Close() error { -- return s.stream.Close() --} -- --type req struct { -- method string -- start time.Time --} -- --type mapped struct { -- mu sync.Mutex -- clientCalls map[string]req -- serverCalls map[string]req --} -- --var maps = &mapped{ -- sync.Mutex{}, -- make(map[string]req), -- make(map[string]req), --} -- --// these 4 methods are each used exactly once, but it seemed --// better to have the encapsulation rather than ad hoc mutex --// code in 4 places --func (m *mapped) client(id string) req { -- m.mu.Lock() -- defer m.mu.Unlock() -- v := m.clientCalls[id] -- delete(m.clientCalls, id) -- return v --} -- --func (m *mapped) server(id string) req { -- m.mu.Lock() -- defer m.mu.Unlock() -- v := m.serverCalls[id] -- delete(m.serverCalls, id) -- return v --} -- --func (m *mapped) setClient(id string, r req) { -- m.mu.Lock() -- defer m.mu.Unlock() -- m.clientCalls[id] = r --} -- --func (m *mapped) setServer(id string, r req) { -- m.mu.Lock() -- defer m.mu.Unlock() -- m.serverCalls[id] = r --} -- --const eor = "\r\n\r\n\r\n" -- --func (s *loggingStream) logCommon(msg jsonrpc2.Message, isRead bool) { -- s.logMu.Lock() -- defer s.logMu.Unlock() -- direction, pastTense := "Received", "Received" -- get, set := maps.client, maps.setServer -- if isRead { -- direction, pastTense = "Sending", "Sent" -- get, set = maps.server, maps.setClient +-// isSelector reports if e is the selector expr , . +-func isSelector(e ast.Expr, x, sel string) bool { +- selectorExpr, ok := e.(*ast.SelectorExpr) +- if !ok { +- return false - } -- if msg == nil || s.log == nil { -- return +- ident, ok := selectorExpr.X.(*ast.Ident) +- if !ok { +- return false - } -- tm := time.Now() -- tmfmt := tm.Format("15:04:05.000 PM") +- return ident.Name == x && selectorExpr.Sel.Name == sel +-} - -- buf := strings.Builder{} -- fmt.Fprintf(&buf, "[Trace - %s] ", tmfmt) // common beginning -- switch msg := msg.(type) { -- case *jsonrpc2.Call: -- id := fmt.Sprint(msg.ID()) -- fmt.Fprintf(&buf, "%s request '%s - (%s)'.\n", direction, msg.Method(), id) -- fmt.Fprintf(&buf, "Params: %s%s", msg.Params(), eor) -- set(id, req{method: msg.Method(), start: tm}) -- case *jsonrpc2.Notification: -- fmt.Fprintf(&buf, "%s notification '%s'.\n", direction, msg.Method()) -- fmt.Fprintf(&buf, "Params: %s%s", msg.Params(), eor) -- case *jsonrpc2.Response: -- id := fmt.Sprint(msg.ID()) -- if err := msg.Err(); err != nil { -- fmt.Fprintf(s.log, "[Error - %s] %s #%s %s%s", pastTense, tmfmt, id, err, eor) -- return +-// reorderParams reorders the given parameters in-place to follow common Go conventions. +-func reorderParams(params []ast.Expr, paramTypes []*ast.Field) { +- // Move Context parameter (if any) to front. +- for i, t := range paramTypes { +- if isSelector(t.Type, "context", "Context") { +- p, t := params[i], paramTypes[i] +- copy(params[1:], params[:i]) +- copy(paramTypes[1:], paramTypes[:i]) +- params[0], paramTypes[0] = p, t +- break - } -- cc := get(id) -- elapsed := tm.Sub(cc.start) -- fmt.Fprintf(&buf, "%s response '%s - (%s)' in %dms.\n", -- direction, cc.method, id, elapsed/time.Millisecond) -- fmt.Fprintf(&buf, "Result: %s%s", msg.Result(), eor) - } -- s.log.Write([]byte(buf.String())) -} -diff -urN a/gopls/internal/lsp/protocol/mapper.go b/gopls/internal/lsp/protocol/mapper.go ---- a/gopls/internal/lsp/protocol/mapper.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/protocol/mapper.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,529 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package protocol -- --// This file defines Mapper, which wraps a file content buffer --// ([]byte) and provides efficient conversion between every kind of --// position representation. --// --// gopls uses four main representations of position: --// --// 1. byte offsets, e.g. (start, end int), starting from zero. --// --// 2. go/token notation. Use these types when interacting directly --// with the go/* syntax packages: --// --// token.Pos --// token.FileSet --// token.File --// --// Because File.Offset and File.Pos panic on invalid inputs, --// we do not call them directly and instead use the safetoken package --// for these conversions. This is enforced by a static check. --// --// Beware also that the methods of token.File have two bugs for which --// safetoken contains workarounds: --// - #57490, whereby the parser may create ast.Nodes during error --// recovery whose computed positions are out of bounds (EOF+1). --// - #41029, whereby the wrong line number is returned for the EOF position. --// --// 3. the span package. --// --// span.Point = (line, col8, offset). --// span.Span = (uri URI, start, end span.Point) --// --// Line and column are 1-based. --// Columns are measured in bytes (UTF-8 codes). --// All fields are optional. --// --// These types are useful as intermediate conversions of validated --// ranges (though MappedRange is superior as it is self contained --// and universally convertible). Since their fields are optional --// they are also useful for parsing user-provided positions (e.g. in --// the CLI) before we have access to file contents. --// --// 4. protocol, the LSP RPC message format. --// --// protocol.Position = (Line, Character uint32) --// protocol.Range = (start, end Position) --// protocol.Location = (URI, protocol.Range) --// --// Line and Character are 0-based. --// Characters (columns) are measured in UTF-16 codes. --// --// protocol.Mapper holds the (URI, Content) of a file, enabling --// efficient mapping between byte offsets, span ranges, and --// protocol ranges. --// --// protocol.MappedRange holds a protocol.Mapper and valid (start, --// end int) byte offsets, enabling infallible, efficient conversion --// to any other format. -- --import ( -- "bytes" -- "fmt" -- "go/ast" -- "go/token" -- "path/filepath" -- "sort" -- "strings" -- "sync" -- "unicode/utf8" -- -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/gopls/internal/span" --) - --// A Mapper wraps the content of a file and provides mapping --// between byte offsets and notations of position such as: --// --// - (line, col8) pairs, where col8 is a 1-based UTF-8 column number --// (bytes), as used by the go/token and span packages. --// --// - (line, col16) pairs, where col16 is a 1-based UTF-16 column --// number, as used by the LSP protocol. --// --// All conversion methods are named "FromTo", where From and To are the two types. --// For example, the PointPosition method converts from a Point to a Position. +-// adjustRangeForCommentsAndWhiteSpace adjusts the given range to exclude unnecessary leading or +-// trailing whitespace characters from selection as well as leading or trailing comments. +-// In the following example, each line of the if statement is indented once. There are also two +-// extra spaces after the sclosing bracket before the line break and a comment. -// --// Mapper does not intrinsically depend on go/token-based --// representations. Use safetoken to map between token.Pos <=> byte --// offsets, or the convenience methods such as PosPosition, --// NodePosition, or NodeRange. +-// \tif (true) { +-// \t _ = 1 +-// \t} // hello \n -// --// See overview comments at top of this file. --type Mapper struct { -- URI span.URI -- Content []byte -- -- // Line-number information is requested only for a tiny -- // fraction of Mappers, so we compute it lazily. -- // Call initLines() before accessing fields below. -- linesOnce sync.Once -- lineStart []int // byte offset of start of ith line (0-based); last=EOF iff \n-terminated -- nonASCII bool -- -- // TODO(adonovan): adding an extra lineStart entry for EOF -- // might simplify every method that accesses it. Try it out. --} -- --// NewMapper creates a new mapper for the given URI and content. --func NewMapper(uri span.URI, content []byte) *Mapper { -- return &Mapper{URI: uri, Content: content} --} -- --// initLines populates the lineStart table. --func (m *Mapper) initLines() { -- m.linesOnce.Do(func() { -- nlines := bytes.Count(m.Content, []byte("\n")) -- m.lineStart = make([]int, 1, nlines+1) // initially []int{0} -- for offset, b := range m.Content { -- if b == '\n' { -- m.lineStart = append(m.lineStart, offset+1) -- } -- if b >= utf8.RuneSelf { -- m.nonASCII = true -- } -- } +-// By default, a valid range begins at 'if' and ends at the first whitespace character +-// after the '}'. But, users are likely to highlight full lines rather than adjusting +-// their cursors for whitespace. To support this use case, we must manually adjust the +-// ranges to match the correct AST node. In this particular example, we would adjust +-// rng.Start forward to the start of 'if' and rng.End backward to after '}'. +-func adjustRangeForCommentsAndWhiteSpace(tok *token.File, start, end token.Pos, content []byte, file *ast.File) (token.Pos, token.Pos, error) { +- // Adjust the end of the range to after leading whitespace and comments. +- prevStart := token.NoPos +- startComment := sort.Search(len(file.Comments), func(i int) bool { +- // Find the index for the first comment that ends after range start. +- return file.Comments[i].End() > start - }) --} -- --// -- conversions from span (UTF-8) domain -- -- --// SpanLocation converts a (UTF-8) span to a protocol (UTF-16) range. --// Precondition: the URIs of SpanLocation and Mapper match. --func (m *Mapper) SpanLocation(s span.Span) (Location, error) { -- rng, err := m.SpanRange(s) -- if err != nil { -- return Location{}, err -- } -- return m.RangeLocation(rng), nil --} -- --// SpanRange converts a (UTF-8) span to a protocol (UTF-16) range. --// Precondition: the URIs of Span and Mapper match. --func (m *Mapper) SpanRange(s span.Span) (Range, error) { -- // Assert that we aren't using the wrong mapper. -- // We check only the base name, and case insensitively, -- // because we can't assume clean paths, no symbolic links, -- // case-sensitive directories. The authoritative answer -- // requires querying the file system, and we don't want -- // to do that. -- if !strings.EqualFold(filepath.Base(string(m.URI)), filepath.Base(string(s.URI()))) { -- return Range{}, bug.Errorf("mapper is for file %q instead of %q", m.URI, s.URI()) -- } -- start, err := m.PointPosition(s.Start()) -- if err != nil { -- return Range{}, fmt.Errorf("start: %w", err) -- } -- end, err := m.PointPosition(s.End()) -- if err != nil { -- return Range{}, fmt.Errorf("end: %w", err) -- } -- return Range{Start: start, End: end}, nil --} -- --// PointPosition converts a valid span (UTF-8) point to a protocol (UTF-16) position. --func (m *Mapper) PointPosition(p span.Point) (Position, error) { -- if p.HasPosition() { -- line, col8 := p.Line()-1, p.Column()-1 // both 0-based -- m.initLines() -- if line >= len(m.lineStart) { -- return Position{}, fmt.Errorf("line number %d out of range (max %d)", line, len(m.lineStart)) +- for prevStart != start { +- prevStart = start +- // If start is within a comment, move start to the end +- // of the comment group. +- if startComment < len(file.Comments) && file.Comments[startComment].Pos() <= start && start < file.Comments[startComment].End() { +- start = file.Comments[startComment].End() +- startComment++ - } -- offset := m.lineStart[line] -- end := offset + col8 -- -- // Validate column. -- if end > len(m.Content) { -- return Position{}, fmt.Errorf("column is beyond end of file") -- } else if line+1 < len(m.lineStart) && end >= m.lineStart[line+1] { -- return Position{}, fmt.Errorf("column is beyond end of line") +- // Move forwards to find a non-whitespace character. +- offset, err := safetoken.Offset(tok, start) +- if err != nil { +- return 0, 0, err - } -- -- char := UTF16Len(m.Content[offset:end]) -- return Position{Line: uint32(line), Character: uint32(char)}, nil -- } -- if p.HasOffset() { -- return m.OffsetPosition(p.Offset()) -- } -- return Position{}, fmt.Errorf("point has neither offset nor line/column") --} -- --// -- conversions from byte offsets -- -- --// OffsetLocation converts a byte-offset interval to a protocol (UTF-16) location. --func (m *Mapper) OffsetLocation(start, end int) (Location, error) { -- rng, err := m.OffsetRange(start, end) -- if err != nil { -- return Location{}, err -- } -- return m.RangeLocation(rng), nil --} -- --// OffsetRange converts a byte-offset interval to a protocol (UTF-16) range. --func (m *Mapper) OffsetRange(start, end int) (Range, error) { -- if start > end { -- return Range{}, fmt.Errorf("start offset (%d) > end (%d)", start, end) -- } -- startPosition, err := m.OffsetPosition(start) -- if err != nil { -- return Range{}, fmt.Errorf("start: %v", err) -- } -- endPosition, err := m.OffsetPosition(end) -- if err != nil { -- return Range{}, fmt.Errorf("end: %v", err) +- for offset < len(content) && isGoWhiteSpace(content[offset]) { +- offset++ +- } +- start = tok.Pos(offset) - } -- return Range{Start: startPosition, End: endPosition}, nil --} - --// OffsetSpan converts a byte-offset interval to a (UTF-8) span. --// The resulting span contains line, column, and offset information. --func (m *Mapper) OffsetSpan(start, end int) (span.Span, error) { -- if start > end { -- return span.Span{}, fmt.Errorf("start offset (%d) > end (%d)", start, end) -- } -- startPoint, err := m.OffsetPoint(start) -- if err != nil { -- return span.Span{}, fmt.Errorf("start: %v", err) -- } -- endPoint, err := m.OffsetPoint(end) -- if err != nil { -- return span.Span{}, fmt.Errorf("end: %v", err) +- // Adjust the end of the range to before trailing whitespace and comments. +- prevEnd := token.NoPos +- endComment := sort.Search(len(file.Comments), func(i int) bool { +- // Find the index for the first comment that ends after the range end. +- return file.Comments[i].End() >= end +- }) +- // Search will return n if not found, so we need to adjust if there are no +- // comments that would match. +- if endComment == len(file.Comments) { +- endComment = -1 - } -- return span.New(m.URI, startPoint, endPoint), nil --} -- --// OffsetPosition converts a byte offset to a protocol (UTF-16) position. --func (m *Mapper) OffsetPosition(offset int) (Position, error) { -- if !(0 <= offset && offset <= len(m.Content)) { -- return Position{}, fmt.Errorf("invalid offset %d (want 0-%d)", offset, len(m.Content)) +- for prevEnd != end { +- prevEnd = end +- // If end is within a comment, move end to the start +- // of the comment group. +- if endComment >= 0 && file.Comments[endComment].Pos() < end && end <= file.Comments[endComment].End() { +- end = file.Comments[endComment].Pos() +- endComment-- +- } +- // Move backwards to find a non-whitespace character. +- offset, err := safetoken.Offset(tok, end) +- if err != nil { +- return 0, 0, err +- } +- for offset > 0 && isGoWhiteSpace(content[offset-1]) { +- offset-- +- } +- end = tok.Pos(offset) - } -- // No error may be returned after this point, -- // even if the offset does not fall at a rune boundary. -- // (See panic in MappedRange.Range reachable.) -- -- line, col16 := m.lineCol16(offset) -- return Position{Line: uint32(line), Character: uint32(col16)}, nil --} - --// lineCol16 converts a valid byte offset to line and UTF-16 column numbers, both 0-based. --func (m *Mapper) lineCol16(offset int) (int, int) { -- line, start, cr := m.line(offset) -- var col16 int -- if m.nonASCII { -- col16 = UTF16Len(m.Content[start:offset]) -- } else { -- col16 = offset - start -- } -- if cr { -- col16-- // retreat from \r at line end -- } -- return line, col16 +- return start, end, nil -} - --// lineCol8 converts a valid byte offset to line and UTF-8 column numbers, both 0-based. --func (m *Mapper) lineCol8(offset int) (int, int) { -- line, start, cr := m.line(offset) -- col8 := offset - start -- if cr { -- col8-- // retreat from \r at line end -- } -- return line, col8 +-// isGoWhiteSpace returns true if b is a considered white space in +-// Go as defined by scanner.GoWhitespace. +-func isGoWhiteSpace(b byte) bool { +- return uint64(scanner.GoWhitespace)&(1< 0 && m.Content[offset-1] == '\r' -- -- line-- // 0-based -- -- return line, m.lineStart[line], cr --} -- --// OffsetPoint converts a byte offset to a span (UTF-8) point. --// The resulting point contains line, column, and offset information. --func (m *Mapper) OffsetPoint(offset int) (span.Point, error) { -- if !(0 <= offset && offset <= len(m.Content)) { -- return span.Point{}, fmt.Errorf("invalid offset %d (want 0-%d)", offset, len(m.Content)) -- } -- line, col8 := m.lineCol8(offset) -- return span.NewPoint(line+1, col8+1, offset), nil --} -- --// OffsetMappedRange returns a MappedRange for the given byte offsets. --// A MappedRange can be converted to any other form. --func (m *Mapper) OffsetMappedRange(start, end int) (MappedRange, error) { -- if !(0 <= start && start <= end && end <= len(m.Content)) { -- return MappedRange{}, fmt.Errorf("invalid offsets (%d, %d) (file %s has size %d)", start, end, m.URI, len(m.Content)) -- } -- return MappedRange{m, start, end}, nil +- return parent -} - --// -- conversions from protocol (UTF-16) domain -- +-// variable describes the status of a variable within a selection. +-type variable struct { +- obj types.Object - --// LocationSpan converts a protocol (UTF-16) Location to a (UTF-8) span. --// Precondition: the URIs of Location and Mapper match. --func (m *Mapper) LocationSpan(l Location) (span.Span, error) { -- // TODO(adonovan): check that l.URI matches m.URI. -- return m.RangeSpan(l.Range) --} +- // free reports whether the variable is a free variable, meaning it should +- // be a parameter to the extracted function. +- free bool - --// RangeSpan converts a protocol (UTF-16) range to a (UTF-8) span. --// The resulting span has valid Positions and Offsets. --func (m *Mapper) RangeSpan(r Range) (span.Span, error) { -- start, end, err := m.RangeOffsets(r) -- if err != nil { -- return span.Span{}, err -- } -- return m.OffsetSpan(start, end) --} +- // assigned reports whether the variable is assigned to in the selection. +- assigned bool - --// RangeOffsets converts a protocol (UTF-16) range to start/end byte offsets. --func (m *Mapper) RangeOffsets(r Range) (int, int, error) { -- start, err := m.PositionOffset(r.Start) -- if err != nil { -- return 0, 0, err -- } -- end, err := m.PositionOffset(r.End) -- if err != nil { -- return 0, 0, err -- } -- return start, end, nil +- // defined reports whether the variable is defined in the selection. +- defined bool -} - --// PositionOffset converts a protocol (UTF-16) position to a byte offset. --func (m *Mapper) PositionOffset(p Position) (int, error) { -- m.initLines() -- -- // Validate line number. -- if p.Line > uint32(len(m.lineStart)) { -- return 0, fmt.Errorf("line number %d out of range 0-%d", p.Line, len(m.lineStart)) -- } else if p.Line == uint32(len(m.lineStart)) { -- if p.Character == 0 { -- return len(m.Content), nil // EOF +-// collectFreeVars maps each identifier in the given range to whether it is "free." +-// Given a range, a variable in that range is defined as "free" if it is declared +-// outside of the range and neither at the file scope nor package scope. These free +-// variables will be used as arguments in the extracted function. It also returns a +-// list of identifiers that may need to be returned by the extracted function. +-// Some of the code in this function has been adapted from tools/cmd/guru/freevars.go. +-func collectFreeVars(info *types.Info, file *ast.File, fileScope, pkgScope *types.Scope, start, end token.Pos, node ast.Node) ([]*variable, error) { +- // id returns non-nil if n denotes an object that is referenced by the span +- // and defined either within the span or in the lexical environment. The bool +- // return value acts as an indicator for where it was defined. +- id := func(n *ast.Ident) (types.Object, bool) { +- obj := info.Uses[n] +- if obj == nil { +- return info.Defs[n], false - } -- return 0, fmt.Errorf("column is beyond end of file") -- } -- -- offset := m.lineStart[p.Line] -- content := m.Content[offset:] // rest of file from start of enclosing line -- -- // Advance bytes up to the required number of UTF-16 codes. -- col8 := 0 -- for col16 := 0; col16 < int(p.Character); col16++ { -- r, sz := utf8.DecodeRune(content) -- if sz == 0 { -- return 0, fmt.Errorf("column is beyond end of file") +- if obj.Name() == "_" { +- return nil, false // exclude objects denoting '_' - } -- if r == '\n' { -- return 0, fmt.Errorf("column is beyond end of line") +- if _, ok := obj.(*types.PkgName); ok { +- return nil, false // imported package - } -- if sz == 1 && r == utf8.RuneError { -- return 0, fmt.Errorf("buffer contains invalid UTF-8 text") +- if !(file.Pos() <= obj.Pos() && obj.Pos() <= file.End()) { +- return nil, false // not defined in this file - } -- content = content[sz:] -- -- if r >= 0x10000 { -- col16++ // rune was encoded by a pair of surrogate UTF-16 codes +- scope := obj.Parent() +- if scope == nil { +- return nil, false // e.g. interface method, struct field +- } +- if scope == fileScope || scope == pkgScope { +- return nil, false // defined at file or package scope +- } +- if start <= obj.Pos() && obj.Pos() <= end { +- return obj, false // defined within selection => not free +- } +- return obj, true +- } +- // sel returns non-nil if n denotes a selection o.x.y that is referenced by the +- // span and defined either within the span or in the lexical environment. The bool +- // return value acts as an indicator for where it was defined. +- var sel func(n *ast.SelectorExpr) (types.Object, bool) +- sel = func(n *ast.SelectorExpr) (types.Object, bool) { +- switch x := astutil.Unparen(n.X).(type) { +- case *ast.SelectorExpr: +- return sel(x) +- case *ast.Ident: +- return id(x) +- } +- return nil, false +- } +- seen := make(map[types.Object]*variable) +- firstUseIn := make(map[types.Object]token.Pos) +- var vars []types.Object +- ast.Inspect(node, func(n ast.Node) bool { +- if n == nil { +- return false +- } +- if start <= n.Pos() && n.End() <= end { +- var obj types.Object +- var isFree, prune bool +- switch n := n.(type) { +- case *ast.Ident: +- obj, isFree = id(n) +- case *ast.SelectorExpr: +- obj, isFree = sel(n) +- prune = true +- } +- if obj != nil { +- seen[obj] = &variable{ +- obj: obj, +- free: isFree, +- } +- vars = append(vars, obj) +- // Find the first time that the object is used in the selection. +- first, ok := firstUseIn[obj] +- if !ok || n.Pos() < first { +- firstUseIn[obj] = n.Pos() +- } +- if prune { +- return false +- } +- } +- } +- return n.Pos() <= end +- }) - -- if col16 == int(p.Character) { -- break // requested position is in the middle of a rune +- // Find identifiers that are initialized or whose values are altered at some +- // point in the selected block. For example, in a selected block from lines 2-4, +- // variables x, y, and z are included in assigned. However, in a selected block +- // from lines 3-4, only variables y and z are included in assigned. +- // +- // 1: var a int +- // 2: var x int +- // 3: y := 3 +- // 4: z := x + a +- // +- ast.Inspect(node, func(n ast.Node) bool { +- if n == nil { +- return false +- } +- if n.Pos() < start || n.End() > end { +- return n.Pos() <= end +- } +- switch n := n.(type) { +- case *ast.AssignStmt: +- for _, assignment := range n.Lhs { +- lhs, ok := assignment.(*ast.Ident) +- if !ok { +- continue +- } +- obj, _ := id(lhs) +- if obj == nil { +- continue +- } +- if _, ok := seen[obj]; !ok { +- continue +- } +- seen[obj].assigned = true +- if n.Tok != token.DEFINE { +- continue +- } +- // Find identifiers that are defined prior to being used +- // elsewhere in the selection. +- // TODO: Include identifiers that are assigned prior to being +- // used elsewhere in the selection. Then, change the assignment +- // to a definition in the extracted function. +- if firstUseIn[obj] != lhs.Pos() { +- continue +- } +- // Ensure that the object is not used in its own re-definition. +- // For example: +- // var f float64 +- // f, e := math.Frexp(f) +- for _, expr := range n.Rhs { +- if referencesObj(info, expr, obj) { +- continue +- } +- if _, ok := seen[obj]; !ok { +- continue +- } +- seen[obj].defined = true +- break +- } +- } +- return false +- case *ast.DeclStmt: +- gen, ok := n.Decl.(*ast.GenDecl) +- if !ok { +- return false +- } +- for _, spec := range gen.Specs { +- vSpecs, ok := spec.(*ast.ValueSpec) +- if !ok { +- continue +- } +- for _, vSpec := range vSpecs.Names { +- obj, _ := id(vSpec) +- if obj == nil { +- continue +- } +- if _, ok := seen[obj]; !ok { +- continue +- } +- seen[obj].assigned = true +- } +- } +- return false +- case *ast.IncDecStmt: +- if ident, ok := n.X.(*ast.Ident); !ok { +- return false +- } else if obj, _ := id(ident); obj == nil { +- return false +- } else { +- if _, ok := seen[obj]; !ok { +- return false +- } +- seen[obj].assigned = true - } - } -- col8 += sz +- return true +- }) +- var variables []*variable +- for _, obj := range vars { +- v, ok := seen[obj] +- if !ok { +- return nil, fmt.Errorf("no seen types.Object for %v", obj) +- } +- variables = append(variables, v) - } -- return offset + col8, nil +- return variables, nil -} - --// PositionPoint converts a protocol (UTF-16) position to a span (UTF-8) point. --// The resulting point has a valid Position and Offset. --func (m *Mapper) PositionPoint(p Position) (span.Point, error) { -- offset, err := m.PositionOffset(p) -- if err != nil { -- return span.Point{}, err -- } -- line, col8 := m.lineCol8(offset) -- -- return span.NewPoint(line+1, col8+1, offset), nil +-// referencesObj checks whether the given object appears in the given expression. +-func referencesObj(info *types.Info, expr ast.Expr, obj types.Object) bool { +- var hasObj bool +- ast.Inspect(expr, func(n ast.Node) bool { +- if n == nil { +- return false +- } +- ident, ok := n.(*ast.Ident) +- if !ok { +- return true +- } +- objUse := info.Uses[ident] +- if obj == objUse { +- hasObj = true +- return false +- } +- return false +- }) +- return hasObj -} - --// -- go/token domain convenience methods -- -- --// PosPosition converts a token pos to a protocol (UTF-16) position. --func (m *Mapper) PosPosition(tf *token.File, pos token.Pos) (Position, error) { -- offset, err := safetoken.Offset(tf, pos) -- if err != nil { -- return Position{}, err -- } -- return m.OffsetPosition(offset) +-type fnExtractParams struct { +- tok *token.File +- start, end token.Pos +- path []ast.Node +- outer *ast.FuncDecl +- node ast.Node -} - --// PosLocation converts a token range to a protocol (UTF-16) location. --func (m *Mapper) PosLocation(tf *token.File, start, end token.Pos) (Location, error) { -- startOffset, endOffset, err := safetoken.Offsets(tf, start, end) -- if err != nil { -- return Location{}, err +-// CanExtractFunction reports whether the code in the given range can be +-// extracted to a function. +-func CanExtractFunction(tok *token.File, start, end token.Pos, src []byte, file *ast.File) (*fnExtractParams, bool, bool, error) { +- if start == end { +- return nil, false, false, fmt.Errorf("start and end are equal") - } -- rng, err := m.OffsetRange(startOffset, endOffset) +- var err error +- start, end, err = adjustRangeForCommentsAndWhiteSpace(tok, start, end, src, file) - if err != nil { -- return Location{}, err +- return nil, false, false, err - } -- return m.RangeLocation(rng), nil --} -- --// PosRange converts a token range to a protocol (UTF-16) range. --func (m *Mapper) PosRange(tf *token.File, start, end token.Pos) (Range, error) { -- startOffset, endOffset, err := safetoken.Offsets(tf, start, end) -- if err != nil { -- return Range{}, err +- path, _ := astutil.PathEnclosingInterval(file, start, end) +- if len(path) == 0 { +- return nil, false, false, fmt.Errorf("no path enclosing interval") +- } +- // Node that encloses the selection must be a statement. +- // TODO: Support function extraction for an expression. +- _, ok := path[0].(ast.Stmt) +- if !ok { +- return nil, false, false, fmt.Errorf("node is not a statement") - } -- return m.OffsetRange(startOffset, endOffset) --} -- --// NodeRange converts a syntax node range to a protocol (UTF-16) range. --func (m *Mapper) NodeRange(tf *token.File, node ast.Node) (Range, error) { -- return m.PosRange(tf, node.Pos(), node.End()) --} -- --// RangeLocation pairs a protocol Range with its URI, in a Location. --func (m *Mapper) RangeLocation(rng Range) Location { -- return Location{URI: URIFromSpanURI(m.URI), Range: rng} --} - --// PosMappedRange returns a MappedRange for the given token.Pos range. --func (m *Mapper) PosMappedRange(tf *token.File, start, end token.Pos) (MappedRange, error) { -- startOffset, endOffset, err := safetoken.Offsets(tf, start, end) -- if err != nil { -- return MappedRange{}, nil +- // Find the function declaration that encloses the selection. +- var outer *ast.FuncDecl +- for _, p := range path { +- if p, ok := p.(*ast.FuncDecl); ok { +- outer = p +- break +- } +- } +- if outer == nil { +- return nil, false, false, fmt.Errorf("no enclosing function") - } -- return m.OffsetMappedRange(startOffset, endOffset) --} - --// NodeMappedRange returns a MappedRange for the given node range. --func (m *Mapper) NodeMappedRange(tf *token.File, node ast.Node) (MappedRange, error) { -- return m.PosMappedRange(tf, node.Pos(), node.End()) +- // Find the nodes at the start and end of the selection. +- var startNode, endNode ast.Node +- ast.Inspect(outer, func(n ast.Node) bool { +- if n == nil { +- return false +- } +- // Do not override 'start' with a node that begins at the same location +- // but is nested further from 'outer'. +- if startNode == nil && n.Pos() == start && n.End() <= end { +- startNode = n +- } +- if endNode == nil && n.End() == end && n.Pos() >= start { +- endNode = n +- } +- return n.Pos() <= end +- }) +- if startNode == nil || endNode == nil { +- return nil, false, false, fmt.Errorf("range does not map to AST nodes") +- } +- // If the region is a blockStmt, use the first and last nodes in the block +- // statement. +- // { ... } => { ... } +- if blockStmt, ok := startNode.(*ast.BlockStmt); ok { +- if len(blockStmt.List) == 0 { +- return nil, false, false, fmt.Errorf("range maps to empty block statement") +- } +- startNode, endNode = blockStmt.List[0], blockStmt.List[len(blockStmt.List)-1] +- start, end = startNode.Pos(), endNode.End() +- } +- return &fnExtractParams{ +- tok: tok, +- start: start, +- end: end, +- path: path, +- outer: outer, +- node: startNode, +- }, true, outer.Recv != nil, nil -} - --// -- MappedRange -- -- --// A MappedRange represents a valid byte-offset range of a file. --// Through its Mapper it can be converted into other forms such --// as protocol.Range or span.Span. --// --// Construct one by calling Mapper.OffsetMappedRange with start/end offsets. --// From the go/token domain, call safetoken.Offsets first, --// or use a helper such as ParsedGoFile.MappedPosRange. --// --// Two MappedRanges produced the same Mapper are equal if and only if they --// denote the same range. Two MappedRanges produced by different Mappers --// are unequal even when they represent the same range of the same file. --type MappedRange struct { -- Mapper *Mapper -- start, end int // valid byte offsets: 0 <= start <= end <= len(Mapper.Content) +-// objUsed checks if the object is used within the range. It returns the first +-// occurrence of the object in the range, if it exists. +-func objUsed(info *types.Info, start, end token.Pos, obj types.Object) (bool, *ast.Ident) { +- var firstUse *ast.Ident +- for id, objUse := range info.Uses { +- if obj != objUse { +- continue +- } +- if id.Pos() < start || id.End() > end { +- continue +- } +- if firstUse == nil || id.Pos() < firstUse.Pos() { +- firstUse = id +- } +- } +- return firstUse != nil, firstUse -} - --// Offsets returns the (start, end) byte offsets of this range. --func (mr MappedRange) Offsets() (start, end int) { return mr.start, mr.end } -- --// -- convenience functions -- -- --// URI returns the URI of the range's file. --func (mr MappedRange) URI() span.URI { -- return mr.Mapper.URI +-// varOverridden traverses the given AST node until we find the given identifier. Then, we +-// examine the occurrence of the given identifier and check for (1) whether the identifier +-// is being redefined. If the identifier is free, we also check for (2) whether the identifier +-// is being reassigned. We will not include an identifier in the return statement of the +-// extracted function if it meets one of the above conditions. +-func varOverridden(info *types.Info, firstUse *ast.Ident, obj types.Object, isFree bool, node ast.Node) bool { +- var isOverriden bool +- ast.Inspect(node, func(n ast.Node) bool { +- if n == nil { +- return false +- } +- assignment, ok := n.(*ast.AssignStmt) +- if !ok { +- return true +- } +- // A free variable is initialized prior to the selection. We can always reassign +- // this variable after the selection because it has already been defined. +- // Conversely, a non-free variable is initialized within the selection. Thus, we +- // cannot reassign this variable after the selection unless it is initialized and +- // returned by the extracted function. +- if !isFree && assignment.Tok == token.ASSIGN { +- return false +- } +- for _, assigned := range assignment.Lhs { +- ident, ok := assigned.(*ast.Ident) +- // Check if we found the first use of the identifier. +- if !ok || ident != firstUse { +- continue +- } +- objUse := info.Uses[ident] +- if objUse == nil || objUse != obj { +- continue +- } +- // Ensure that the object is not used in its own definition. +- // For example: +- // var f float64 +- // f, e := math.Frexp(f) +- for _, expr := range assignment.Rhs { +- if referencesObj(info, expr, obj) { +- return false +- } +- } +- isOverriden = true +- return false +- } +- return false +- }) +- return isOverriden -} - --// Range returns the range in protocol (UTF-16) form. --func (mr MappedRange) Range() Range { -- rng, err := mr.Mapper.OffsetRange(mr.start, mr.end) +-// parseBlockStmt generates an AST file from the given text. We then return the portion of the +-// file that represents the text. +-func parseBlockStmt(fset *token.FileSet, src []byte) (*ast.BlockStmt, error) { +- text := "package main\nfunc _() { " + string(src) + " }" +- extract, err := parser.ParseFile(fset, "", text, 0) - if err != nil { -- panic(err) // can't happen +- return nil, err - } -- return rng --} -- --// Location returns the range in protocol location (UTF-16) form. --func (mr MappedRange) Location() Location { -- return mr.Mapper.RangeLocation(mr.Range()) +- if len(extract.Decls) == 0 { +- return nil, fmt.Errorf("parsed file does not contain any declarations") +- } +- decl, ok := extract.Decls[0].(*ast.FuncDecl) +- if !ok { +- return nil, fmt.Errorf("parsed file does not contain expected function declaration") +- } +- if decl.Body == nil { +- return nil, fmt.Errorf("extracted function has no body") +- } +- return decl.Body, nil -} - --// Span returns the range in span (UTF-8) form. --func (mr MappedRange) Span() span.Span { -- spn, err := mr.Mapper.OffsetSpan(mr.start, mr.end) -- if err != nil { -- panic(err) // can't happen +-// generateReturnInfo generates the information we need to adjust the return statements and +-// signature of the extracted function. We prepare names, signatures, and "zero values" that +-// represent the new variables. We also use this information to construct the if statement that +-// is inserted below the call to the extracted function. +-func generateReturnInfo(enclosing *ast.FuncType, pkg *types.Package, path []ast.Node, file *ast.File, info *types.Info, pos token.Pos, hasNonNestedReturns bool) ([]*returnVariable, *ast.IfStmt, error) { +- var retVars []*returnVariable +- var cond *ast.Ident +- if !hasNonNestedReturns { +- // Generate information for the added bool value. +- name, _ := generateAvailableIdentifier(pos, path, pkg, info, "shouldReturn", 0) +- cond = &ast.Ident{Name: name} +- retVars = append(retVars, &returnVariable{ +- name: cond, +- decl: &ast.Field{Type: ast.NewIdent("bool")}, +- zeroVal: ast.NewIdent("false"), +- }) +- } +- // Generate information for the values in the return signature of the enclosing function. +- if enclosing.Results != nil { +- idx := 0 +- for _, field := range enclosing.Results.List { +- typ := info.TypeOf(field.Type) +- if typ == nil { +- return nil, nil, fmt.Errorf( +- "failed type conversion, AST expression: %T", field.Type) +- } +- expr := analysisinternal.TypeExpr(file, pkg, typ) +- if expr == nil { +- return nil, nil, fmt.Errorf("nil AST expression") +- } +- var name string +- name, idx = generateAvailableIdentifier(pos, path, pkg, info, "returnValue", idx) +- retVars = append(retVars, &returnVariable{ +- name: ast.NewIdent(name), +- decl: &ast.Field{Type: expr}, +- zeroVal: analysisinternal.ZeroValue(file, pkg, typ), +- }) +- } +- } +- var ifReturn *ast.IfStmt +- if !hasNonNestedReturns { +- // Create the return statement for the enclosing function. We must exclude the variable +- // for the condition of the if statement (cond) from the return statement. +- ifReturn = &ast.IfStmt{ +- Cond: cond, +- Body: &ast.BlockStmt{ +- List: []ast.Stmt{&ast.ReturnStmt{Results: getNames(retVars)[1:]}}, +- }, +- } - } -- return spn +- return retVars, ifReturn, nil -} - --// String formats the range in span (UTF-8) notation. --func (mr MappedRange) String() string { -- return fmt.Sprint(mr.Span()) +-// adjustReturnStatements adds "zero values" of the given types to each return statement +-// in the given AST node. +-func adjustReturnStatements(returnTypes []*ast.Field, seenVars map[types.Object]ast.Expr, file *ast.File, pkg *types.Package, extractedBlock *ast.BlockStmt) error { +- var zeroVals []ast.Expr +- // Create "zero values" for each type. +- for _, returnType := range returnTypes { +- var val ast.Expr +- for obj, typ := range seenVars { +- if typ != returnType.Type { +- continue +- } +- val = analysisinternal.ZeroValue(file, pkg, obj.Type()) +- break +- } +- if val == nil { +- return fmt.Errorf( +- "could not find matching AST expression for %T", returnType.Type) +- } +- zeroVals = append(zeroVals, val) +- } +- // Add "zero values" to each return statement. +- // The bool reports whether the enclosing function should return after calling the +- // extracted function. We set the bool to 'true' because, if these return statements +- // execute, the extracted function terminates early, and the enclosing function must +- // return as well. +- zeroVals = append(zeroVals, ast.NewIdent("true")) +- ast.Inspect(extractedBlock, func(n ast.Node) bool { +- if n == nil { +- return false +- } +- if n, ok := n.(*ast.ReturnStmt); ok { +- n.Results = append(zeroVals, n.Results...) +- return false +- } +- return true +- }) +- return nil -} - --// LocationTextDocumentPositionParams converts its argument to its result. --func LocationTextDocumentPositionParams(loc Location) TextDocumentPositionParams { -- return TextDocumentPositionParams{ -- TextDocument: TextDocumentIdentifier{URI: loc.URI}, -- Position: loc.Range.Start, +-// generateFuncCall constructs a call expression for the extracted function, described by the +-// given parameters and return variables. +-func generateFuncCall(hasNonNestedReturn, hasReturnVals bool, params, returns []ast.Expr, name string, token token.Token, selector string) ast.Node { +- var replace ast.Node +- callExpr := &ast.CallExpr{ +- Fun: ast.NewIdent(name), +- Args: params, - } --} -diff -urN a/gopls/internal/lsp/protocol/mapper_test.go b/gopls/internal/lsp/protocol/mapper_test.go ---- a/gopls/internal/lsp/protocol/mapper_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/protocol/mapper_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,439 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package protocol_test -- --import ( -- "fmt" -- "strings" -- "testing" -- -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/span" --) -- --// This file tests Mapper's logic for converting between --// span.Point and UTF-16 columns. (The strange form attests to an --// earlier abstraction.) -- --// 𐐀 is U+10400 = [F0 90 90 80] in UTF-8, [D801 DC00] in UTF-16. --var funnyString = []byte("𐐀23\n𐐀45") -- --var toUTF16Tests = []struct { -- scenario string -- input []byte -- line int // 1-indexed count -- col int // 1-indexed byte position in line -- offset int // 0-indexed byte offset into input -- resUTF16col int // 1-indexed UTF-16 col number -- pre string // everything before the cursor on the line -- post string // everything from the cursor onwards -- err string // expected error string in call to ToUTF16Column -- issue *bool --}{ -- { -- scenario: "cursor missing content", -- input: nil, -- offset: -1, -- err: "point has neither offset nor line/column", -- }, -- { -- scenario: "cursor missing position", -- input: funnyString, -- line: -1, -- col: -1, -- offset: -1, -- err: "point has neither offset nor line/column", -- }, -- { -- scenario: "zero length input; cursor at first col, first line", -- input: []byte(""), -- line: 1, -- col: 1, -- offset: 0, -- resUTF16col: 1, -- }, -- { -- scenario: "cursor before funny character; first line", -- input: funnyString, -- line: 1, -- col: 1, -- offset: 0, -- resUTF16col: 1, -- pre: "", -- post: "𐐀23", -- }, -- { -- scenario: "cursor after funny character; first line", -- input: funnyString, -- line: 1, -- col: 5, // 4 + 1 (1-indexed) -- offset: 4, // (unused since we have line+col) -- resUTF16col: 3, // 2 + 1 (1-indexed) -- pre: "𐐀", -- post: "23", -- }, -- { -- scenario: "cursor after last character on first line", -- input: funnyString, -- line: 1, -- col: 7, // 4 + 1 + 1 + 1 (1-indexed) -- offset: 6, // 4 + 1 + 1 (unused since we have line+col) -- resUTF16col: 5, // 2 + 1 + 1 + 1 (1-indexed) -- pre: "𐐀23", -- post: "", -- }, -- { -- scenario: "cursor before funny character; second line", -- input: funnyString, -- line: 2, -- col: 1, -- offset: 7, // length of first line (unused since we have line+col) -- resUTF16col: 1, -- pre: "", -- post: "𐐀45", -- }, -- { -- scenario: "cursor after funny character; second line", -- input: funnyString, -- line: 1, -- col: 5, // 4 + 1 (1-indexed) -- offset: 11, // 7 (length of first line) + 4 (unused since we have line+col) -- resUTF16col: 3, // 2 + 1 (1-indexed) -- pre: "𐐀", -- post: "45", -- }, -- { -- scenario: "cursor after last character on second line", -- input: funnyString, -- line: 2, -- col: 7, // 4 + 1 + 1 + 1 (1-indexed) -- offset: 13, // 7 (length of first line) + 4 + 1 + 1 (unused since we have line+col) -- resUTF16col: 5, // 2 + 1 + 1 + 1 (1-indexed) -- pre: "𐐀45", -- post: "", -- }, -- { -- scenario: "cursor beyond end of file", -- input: funnyString, -- line: 2, -- col: 8, // 4 + 1 + 1 + 1 + 1 (1-indexed) -- offset: 14, // 4 + 1 + 1 + 1 (unused since we have line+col) -- err: "column is beyond end of file", -- }, --} -- --var fromUTF16Tests = []struct { -- scenario string -- input []byte -- line int // 1-indexed line number (isn't actually used) -- utf16col int // 1-indexed UTF-16 col number -- resCol int // 1-indexed byte position in line -- resOffset int // 0-indexed byte offset into input -- pre string // everything before the cursor on the line -- post string // everything from the cursor onwards -- err string // expected error string in call to ToUTF16Column --}{ -- { -- scenario: "zero length input; cursor at first col, first line", -- input: []byte(""), -- line: 1, -- utf16col: 1, -- resCol: 1, -- resOffset: 0, -- pre: "", -- post: "", -- }, -- { -- scenario: "cursor before funny character", -- input: funnyString, -- line: 1, -- utf16col: 1, -- resCol: 1, -- resOffset: 0, -- pre: "", -- post: "𐐀23", -- }, -- { -- scenario: "cursor after funny character", -- input: funnyString, -- line: 1, -- utf16col: 3, -- resCol: 5, -- resOffset: 4, -- pre: "𐐀", -- post: "23", -- }, -- { -- scenario: "cursor after last character on line", -- input: funnyString, -- line: 1, -- utf16col: 5, -- resCol: 7, -- resOffset: 6, -- pre: "𐐀23", -- post: "", -- }, -- { -- scenario: "cursor beyond last character on line", -- input: funnyString, -- line: 1, -- utf16col: 6, -- resCol: 7, -- resOffset: 6, -- pre: "𐐀23", -- post: "", -- err: "column is beyond end of line", -- }, -- { -- scenario: "cursor before funny character; second line", -- input: funnyString, -- line: 2, -- utf16col: 1, -- resCol: 1, -- resOffset: 7, -- pre: "", -- post: "𐐀45", -- }, -- { -- scenario: "cursor after funny character; second line", -- input: funnyString, -- line: 2, -- utf16col: 3, // 2 + 1 (1-indexed) -- resCol: 5, // 4 + 1 (1-indexed) -- resOffset: 11, // 7 (length of first line) + 4 -- pre: "𐐀", -- post: "45", -- }, -- { -- scenario: "cursor after last character on second line", -- input: funnyString, -- line: 2, -- utf16col: 5, // 2 + 1 + 1 + 1 (1-indexed) -- resCol: 7, // 4 + 1 + 1 + 1 (1-indexed) -- resOffset: 13, // 7 (length of first line) + 4 + 1 + 1 -- pre: "𐐀45", -- post: "", -- }, -- { -- scenario: "cursor beyond end of file", -- input: funnyString, -- line: 2, -- utf16col: 6, // 2 + 1 + 1 + 1 + 1(1-indexed) -- resCol: 8, // 4 + 1 + 1 + 1 + 1 (1-indexed) -- resOffset: 14, // 7 (length of first line) + 4 + 1 + 1 + 1 -- err: "column is beyond end of file", -- }, --} -- --func TestToUTF16(t *testing.T) { -- for _, e := range toUTF16Tests { -- t.Run(e.scenario, func(t *testing.T) { -- if e.issue != nil && !*e.issue { -- t.Skip("expected to fail") -- } -- p := span.NewPoint(e.line, e.col, e.offset) -- m := protocol.NewMapper("", e.input) -- pos, err := m.PointPosition(p) -- if err != nil { -- if err.Error() != e.err { -- t.Fatalf("expected error %v; got %v", e.err, err) -- } -- return -- } -- if e.err != "" { -- t.Fatalf("unexpected success; wanted %v", e.err) -- } -- got := int(pos.Character) + 1 -- if got != e.resUTF16col { -- t.Fatalf("expected result %v; got %v", e.resUTF16col, got) -- } -- pre, post := getPrePost(e.input, p.Offset()) -- if string(pre) != e.pre { -- t.Fatalf("expected #%d pre %q; got %q", p.Offset(), e.pre, pre) -- } -- if string(post) != e.post { -- t.Fatalf("expected #%d, post %q; got %q", p.Offset(), e.post, post) -- } -- }) +- if selector != "" { +- callExpr = &ast.CallExpr{ +- Fun: &ast.SelectorExpr{ +- X: ast.NewIdent(selector), +- Sel: ast.NewIdent(name), +- }, +- Args: params, +- } - } --} -- --func TestFromUTF16(t *testing.T) { -- for _, e := range fromUTF16Tests { -- t.Run(e.scenario, func(t *testing.T) { -- m := protocol.NewMapper("", []byte(e.input)) -- p, err := m.PositionPoint(protocol.Position{ -- Line: uint32(e.line - 1), -- Character: uint32(e.utf16col - 1), -- }) -- if err != nil { -- if err.Error() != e.err { -- t.Fatalf("expected error %v; got %v", e.err, err) -- } -- return -- } -- if e.err != "" { -- t.Fatalf("unexpected success; wanted %v", e.err) -- } -- if p.Column() != e.resCol { -- t.Fatalf("expected resulting col %v; got %v", e.resCol, p.Column()) -- } -- if p.Offset() != e.resOffset { -- t.Fatalf("expected resulting offset %v; got %v", e.resOffset, p.Offset()) -- } -- pre, post := getPrePost(e.input, p.Offset()) -- if string(pre) != e.pre { -- t.Fatalf("expected #%d pre %q; got %q", p.Offset(), e.pre, pre) +- if hasReturnVals { +- if hasNonNestedReturn { +- // Create a return statement that returns the result of the function call. +- replace = &ast.ReturnStmt{ +- Return: 0, +- Results: []ast.Expr{callExpr}, - } -- if string(post) != e.post { -- t.Fatalf("expected #%d post %q; got %q", p.Offset(), e.post, post) +- } else { +- // Assign the result of the function call. +- replace = &ast.AssignStmt{ +- Lhs: returns, +- Tok: token, +- Rhs: []ast.Expr{callExpr}, - } -- }) -- } --} -- --func getPrePost(content []byte, offset int) (string, string) { -- pre, post := string(content)[:offset], string(content)[offset:] -- if i := strings.LastIndex(pre, "\n"); i >= 0 { -- pre = pre[i+1:] -- } -- if i := strings.IndexRune(post, '\n'); i >= 0 { -- post = post[:i] -- } -- return pre, post --} -- --// -- these are the historical lsppos tests -- -- --type testCase struct { -- content string // input text -- substrOrOffset interface{} // explicit integer offset, or a substring -- wantLine, wantChar int // expected LSP position information --} -- --// offset returns the test case byte offset --func (c testCase) offset() int { -- switch x := c.substrOrOffset.(type) { -- case int: -- return x -- case string: -- i := strings.Index(c.content, x) -- if i < 0 { -- panic(fmt.Sprintf("%q does not contain substring %q", c.content, x)) - } -- return i +- } else { +- replace = callExpr - } -- panic("substrOrIndex must be an integer or string") --} -- --var tests = []testCase{ -- {"a𐐀b", "a", 0, 0}, -- {"a𐐀b", "𐐀", 0, 1}, -- {"a𐐀b", "b", 0, 3}, -- {"a𐐀b\n", "\n", 0, 4}, -- {"a𐐀b\r\n", "\n", 0, 4}, // \r|\n is not a valid position, so we move back to the end of the first line. -- {"a𐐀b\r\nx", "x", 1, 0}, -- {"a𐐀b\r\nx\ny", "y", 2, 0}, -- -- // Testing EOL and EOF positions -- {"", 0, 0, 0}, // 0th position of an empty buffer is (0, 0) -- {"abc", "c", 0, 2}, -- {"abc", 3, 0, 3}, -- {"abc\n", "\n", 0, 3}, -- {"abc\n", 4, 1, 0}, // position after a newline is on the next line +- return replace -} - --func TestLineChar(t *testing.T) { -- for _, test := range tests { -- m := protocol.NewMapper("", []byte(test.content)) -- offset := test.offset() -- posn, _ := m.OffsetPosition(offset) -- gotLine, gotChar := int(posn.Line), int(posn.Character) -- if gotLine != test.wantLine || gotChar != test.wantChar { -- t.Errorf("LineChar(%d) = (%d,%d), want (%d,%d)", offset, gotLine, gotChar, test.wantLine, test.wantChar) +-// initializeVars creates variable declarations, if needed. +-// Our preference is to replace the selected block with an "x, y, z := fn()" style +-// assignment statement. We can use this style when all of the variables in the +-// extracted function's return statement are either not defined prior to the extracted block +-// or can be safely redefined. However, for example, if z is already defined +-// in a different scope, we replace the selected block with: +-// +-// var x int +-// var y string +-// x, y, z = fn() +-func initializeVars(uninitialized []types.Object, retVars []*returnVariable, seenUninitialized map[types.Object]struct{}, seenVars map[types.Object]ast.Expr) []ast.Stmt { +- var declarations []ast.Stmt +- for _, obj := range uninitialized { +- if _, ok := seenUninitialized[obj]; ok { +- continue - } -- } --} -- --func TestInvalidOffset(t *testing.T) { -- content := []byte("a𐐀b\r\nx\ny") -- m := protocol.NewMapper("", content) -- for _, offset := range []int{-1, 100} { -- posn, err := m.OffsetPosition(offset) -- if err == nil { -- t.Errorf("OffsetPosition(%d) = %s, want error", offset, posn) +- seenUninitialized[obj] = struct{}{} +- valSpec := &ast.ValueSpec{ +- Names: []*ast.Ident{ast.NewIdent(obj.Name())}, +- Type: seenVars[obj], +- } +- genDecl := &ast.GenDecl{ +- Tok: token.VAR, +- Specs: []ast.Spec{valSpec}, - } +- declarations = append(declarations, &ast.DeclStmt{Decl: genDecl}) - } --} -- --func TestPosition(t *testing.T) { -- for _, test := range tests { -- m := protocol.NewMapper("", []byte(test.content)) -- offset := test.offset() -- got, err := m.OffsetPosition(offset) -- if err != nil { -- t.Errorf("OffsetPosition(%d) failed: %v", offset, err) -- continue +- // Each variable added from a return statement in the selection +- // must be initialized. +- for i, retVar := range retVars { +- n := retVar.name.(*ast.Ident) +- valSpec := &ast.ValueSpec{ +- Names: []*ast.Ident{n}, +- Type: retVars[i].decl.Type, - } -- want := protocol.Position{Line: uint32(test.wantLine), Character: uint32(test.wantChar)} -- if got != want { -- t.Errorf("Position(%d) = %v, want %v", offset, got, want) +- genDecl := &ast.GenDecl{ +- Tok: token.VAR, +- Specs: []ast.Spec{valSpec}, - } +- declarations = append(declarations, &ast.DeclStmt{Decl: genDecl}) - } +- return declarations -} - --func TestRange(t *testing.T) { -- for _, test := range tests { -- m := protocol.NewMapper("", []byte(test.content)) -- offset := test.offset() -- got, err := m.OffsetRange(0, offset) -- if err != nil { -- t.Fatal(err) -- } -- want := protocol.Range{ -- End: protocol.Position{Line: uint32(test.wantLine), Character: uint32(test.wantChar)}, -- } -- if got != want { -- t.Errorf("Range(%d) = %v, want %v", offset, got, want) -- } +-// getNames returns the names from the given list of returnVariable. +-func getNames(retVars []*returnVariable) []ast.Expr { +- var names []ast.Expr +- for _, retVar := range retVars { +- names = append(names, retVar.name) - } +- return names -} - --func TestBytesOffset(t *testing.T) { -- tests := []struct { -- text string -- pos protocol.Position -- want int -- }{ -- // U+10400 encodes as [F0 90 90 80] in UTF-8 and [D801 DC00] in UTF-16. -- {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 0}, want: 0}, -- {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 1}, want: 1}, -- {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 2}, want: 1}, -- {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 3}, want: 5}, -- {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 4}, want: 6}, -- {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 5}, want: -1}, -- {text: "aaa\nbbb\n", pos: protocol.Position{Line: 0, Character: 3}, want: 3}, -- {text: "aaa\nbbb\n", pos: protocol.Position{Line: 0, Character: 4}, want: -1}, -- {text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 0}, want: 4}, -- {text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 3}, want: 7}, -- {text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 4}, want: -1}, -- {text: "aaa\nbbb\n", pos: protocol.Position{Line: 2, Character: 0}, want: 8}, -- {text: "aaa\nbbb\n", pos: protocol.Position{Line: 2, Character: 1}, want: -1}, -- {text: "aaa\nbbb\n\n", pos: protocol.Position{Line: 2, Character: 0}, want: 8}, +-// getZeroVals returns the "zero values" from the given list of returnVariable. +-func getZeroVals(retVars []*returnVariable) []ast.Expr { +- var zvs []ast.Expr +- for _, retVar := range retVars { +- zvs = append(zvs, retVar.zeroVal) - } +- return zvs +-} - -- for i, test := range tests { -- fname := fmt.Sprintf("test %d", i) -- uri := span.URIFromPath(fname) -- mapper := protocol.NewMapper(uri, []byte(test.text)) -- got, err := mapper.PositionPoint(test.pos) -- if err != nil && test.want != -1 { -- t.Errorf("%d: unexpected error: %v", i, err) -- } -- if err == nil && got.Offset() != test.want { -- t.Errorf("want %d for %q(Line:%d,Character:%d), but got %d", test.want, test.text, int(test.pos.Line), int(test.pos.Character), got.Offset()) -- } +-// getDecls returns the declarations from the given list of returnVariable. +-func getDecls(retVars []*returnVariable) []*ast.Field { +- var decls []*ast.Field +- for _, retVar := range retVars { +- decls = append(decls, retVar.decl) - } +- return decls -} -diff -urN a/gopls/internal/lsp/protocol/protocol.go b/gopls/internal/lsp/protocol/protocol.go ---- a/gopls/internal/lsp/protocol/protocol.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/protocol/protocol.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,284 +0,0 @@ --// Copyright 2018 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/golang/fix.go b/gopls/internal/golang/fix.go +--- a/gopls/internal/golang/fix.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/fix.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,227 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package protocol +-package golang - -import ( - "context" -- "encoding/json" - "fmt" -- "io" +- "go/ast" +- "go/token" +- "go/types" - -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/jsonrpc2" -- jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" -- "golang.org/x/tools/internal/xcontext" +- "golang.org/x/tools/go/analysis" +- "golang.org/x/tools/gopls/internal/analysis/embeddirective" +- "golang.org/x/tools/gopls/internal/analysis/fillstruct" +- "golang.org/x/tools/gopls/internal/analysis/stubmethods" +- "golang.org/x/tools/gopls/internal/analysis/undeclaredname" +- "golang.org/x/tools/gopls/internal/analysis/unusedparams" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/internal/imports" -) - --var ( -- // RequestCancelledError should be used when a request is cancelled early. -- RequestCancelledError = jsonrpc2.NewError(-32800, "JSON RPC cancelled") -- RequestCancelledErrorV2 = jsonrpc2_v2.NewError(-32800, "JSON RPC cancelled") --) +-// A fixer is a function that suggests a fix for a diagnostic produced +-// by the analysis framework. This is done outside of the analyzer Run +-// function so that the construction of expensive fixes can be +-// deferred until they are requested by the user. +-// +-// The actual diagnostic is not provided; only its position, as the +-// triple (pgf, start, end); the resulting SuggestedFix implicitly +-// relates to that file. +-// +-// The supplied token positions (start, end) must belong to +-// pkg.FileSet(), and the returned positions +-// (SuggestedFix.TextEdits[*].{Pos,End}) must belong to the returned +-// FileSet. +-// +-// A fixer may return (nil, nil) if no fix is available. +-type fixer func(ctx context.Context, s *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) - --type ClientCloser interface { -- Client -- io.Closer --} +-// A singleFileFixer is a Fixer that inspects only a single file, +-// and does not depend on data types from the cache package. +-// +-// TODO(adonovan): move fillstruct and undeclaredname into this +-// package, so we can remove the import restriction and push +-// the singleFile wrapper down into each singleFileFixer? +-type singleFileFixer func(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) - --type connSender interface { -- io.Closer +-// singleFile adapts a single-file fixer to a Fixer. +-func singleFile(fixer1 singleFileFixer) fixer { +- return func(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) { +- return fixer1(pkg.FileSet(), start, end, pgf.Src, pgf.File, pkg.Types(), pkg.TypesInfo()) +- } +-} - -- Notify(ctx context.Context, method string, params interface{}) error -- Call(ctx context.Context, method string, params, result interface{}) error --} -- --type clientDispatcher struct { -- sender connSender --} -- --func (c *clientDispatcher) Close() error { -- return c.sender.Close() --} -- --// ClientDispatcher returns a Client that dispatches LSP requests across the --// given jsonrpc2 connection. --func ClientDispatcher(conn jsonrpc2.Conn) ClientCloser { -- return &clientDispatcher{sender: clientConn{conn}} --} -- --type clientConn struct { -- conn jsonrpc2.Conn +-// Names of ApplyFix.Fix created directly by the CodeAction handler. +-const ( +- fixExtractVariable = "extract_variable" +- fixExtractFunction = "extract_function" +- fixExtractMethod = "extract_method" +- fixInlineCall = "inline_call" +- fixInvertIfCondition = "invert_if_condition" +- fixSplitLines = "split_lines" +- fixJoinLines = "join_lines" +-) +- +-// ApplyFix applies the specified kind of suggested fix to the given +-// file and range, returning the resulting edits. +-// +-// A fix kind is either the Category of an analysis.Diagnostic that +-// had a SuggestedFix with no edits; or the name of a fix agreed upon +-// by [CodeActions] and this function. +-// Fix kinds identify fixes in the command protocol. +-// +-// TODO(adonovan): come up with a better mechanism for registering the +-// connection between analyzers, code actions, and fixers. A flaw of +-// the current approach is that the same Category could in theory +-// apply to a Diagnostic with several lazy fixes, making them +-// impossible to distinguish. It would more precise if there was a +-// SuggestedFix.Category field, or some other way to squirrel metadata +-// in the fix. +-func ApplyFix(ctx context.Context, fix string, snapshot *cache.Snapshot, fh file.Handle, rng protocol.Range) ([]protocol.TextDocumentEdit, error) { +- // This can't be expressed as an entry in the fixer table below +- // because it operates in the protocol (not go/{token,ast}) domain. +- // (Sigh; perhaps it was a mistake to factor out the +- // NarrowestPackageForFile/RangePos/suggestedFixToEdits +- // steps.) +- if fix == unusedparams.FixCategory { +- changes, err := RemoveUnusedParameter(ctx, fh, rng, snapshot) +- if err != nil { +- return nil, err +- } +- // Unwrap TextDocumentEdits again! +- var edits []protocol.TextDocumentEdit +- for _, change := range changes { +- edits = append(edits, *change.TextDocumentEdit) +- } +- return edits, nil +- } +- +- fixers := map[string]fixer{ +- // Fixes for analyzer-provided diagnostics. +- // These match the Diagnostic.Category. +- embeddirective.FixCategory: addEmbedImport, +- fillstruct.FixCategory: singleFile(fillstruct.SuggestedFix), +- stubmethods.FixCategory: stubMethodsFixer, +- undeclaredname.FixCategory: singleFile(undeclaredname.SuggestedFix), +- +- // Ad-hoc fixers: these are used when the command is +- // constructed directly by logic in server/code_action. +- fixExtractFunction: singleFile(extractFunction), +- fixExtractMethod: singleFile(extractMethod), +- fixExtractVariable: singleFile(extractVariable), +- fixInlineCall: inlineCall, +- fixInvertIfCondition: singleFile(invertIfCondition), +- fixSplitLines: singleFile(splitLines), +- fixJoinLines: singleFile(joinLines), +- } +- fixer, ok := fixers[fix] +- if !ok { +- return nil, fmt.Errorf("no suggested fix function for %s", fix) +- } +- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) +- if err != nil { +- return nil, err +- } +- start, end, err := pgf.RangePos(rng) +- if err != nil { +- return nil, err +- } +- fixFset, suggestion, err := fixer(ctx, snapshot, pkg, pgf, start, end) +- if err != nil { +- return nil, err +- } +- if suggestion == nil { +- return nil, nil +- } +- return suggestedFixToEdits(ctx, snapshot, fixFset, suggestion) -} - --func (c clientConn) Close() error { -- return c.conn.Close() +-// suggestedFixToEdits converts the suggestion's edits from analysis form into protocol form. +-func suggestedFixToEdits(ctx context.Context, snapshot *cache.Snapshot, fset *token.FileSet, suggestion *analysis.SuggestedFix) ([]protocol.TextDocumentEdit, error) { +- editsPerFile := map[protocol.DocumentURI]*protocol.TextDocumentEdit{} +- for _, edit := range suggestion.TextEdits { +- tokFile := fset.File(edit.Pos) +- if tokFile == nil { +- return nil, bug.Errorf("no file for edit position") +- } +- end := edit.End +- if !end.IsValid() { +- end = edit.Pos +- } +- fh, err := snapshot.ReadFile(ctx, protocol.URIFromPath(tokFile.Name())) +- if err != nil { +- return nil, err +- } +- te, ok := editsPerFile[fh.URI()] +- if !ok { +- te = &protocol.TextDocumentEdit{ +- TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{ +- Version: fh.Version(), +- TextDocumentIdentifier: protocol.TextDocumentIdentifier{ +- URI: fh.URI(), +- }, +- }, +- } +- editsPerFile[fh.URI()] = te +- } +- content, err := fh.Content() +- if err != nil { +- return nil, err +- } +- m := protocol.NewMapper(fh.URI(), content) // TODO(adonovan): opt: memoize in map +- rng, err := m.PosRange(tokFile, edit.Pos, end) +- if err != nil { +- return nil, err +- } +- te.Edits = append(te.Edits, protocol.Or_TextDocumentEdit_edits_Elem{ +- Value: protocol.TextEdit{ +- Range: rng, +- NewText: string(edit.NewText), +- }, +- }) +- } +- var edits []protocol.TextDocumentEdit +- for _, edit := range editsPerFile { +- edits = append(edits, *edit) +- } +- return edits, nil -} - --func (c clientConn) Notify(ctx context.Context, method string, params interface{}) error { -- return c.conn.Notify(ctx, method, params) --} +-// addEmbedImport adds a missing embed "embed" import with blank name. +-func addEmbedImport(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, _, _ token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) { +- // Like golang.AddImport, but with _ as Name and using our pgf. +- protoEdits, err := ComputeOneImportFixEdits(snapshot, pgf, &imports.ImportFix{ +- StmtInfo: imports.ImportInfo{ +- ImportPath: "embed", +- Name: "_", +- }, +- FixType: imports.AddImport, +- }) +- if err != nil { +- return nil, nil, fmt.Errorf("compute edits: %w", err) +- } - --func (c clientConn) Call(ctx context.Context, method string, params interface{}, result interface{}) error { -- id, err := c.conn.Call(ctx, method, params, result) -- if ctx.Err() != nil { -- cancelCall(ctx, c, id) +- var edits []analysis.TextEdit +- for _, e := range protoEdits { +- start, end, err := pgf.RangePos(e.Range) +- if err != nil { +- return nil, nil, err // e.g. invalid range +- } +- edits = append(edits, analysis.TextEdit{ +- Pos: start, +- End: end, +- NewText: []byte(e.NewText), +- }) - } -- return err --} - --func ClientDispatcherV2(conn *jsonrpc2_v2.Connection) ClientCloser { -- return &clientDispatcher{clientConnV2{conn}} +- return pkg.FileSet(), &analysis.SuggestedFix{ +- Message: "Add embed import", +- TextEdits: edits, +- }, nil -} +diff -urN a/gopls/internal/golang/folding_range.go b/gopls/internal/golang/folding_range.go +--- a/gopls/internal/golang/folding_range.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/folding_range.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,197 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --type clientConnV2 struct { -- conn *jsonrpc2_v2.Connection --} +-package golang - --func (c clientConnV2) Close() error { -- return c.conn.Close() --} +-import ( +- "context" +- "go/ast" +- "go/token" +- "sort" +- "strings" - --func (c clientConnV2) Notify(ctx context.Context, method string, params interface{}) error { -- return c.conn.Notify(ctx, method, params) --} +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/gopls/internal/util/safetoken" +-) - --func (c clientConnV2) Call(ctx context.Context, method string, params interface{}, result interface{}) error { -- call := c.conn.Call(ctx, method, params) -- err := call.Await(ctx, result) -- if ctx.Err() != nil { -- detached := xcontext.Detach(ctx) -- c.conn.Notify(detached, "$/cancelRequest", &CancelParams{ID: call.ID().Raw()}) -- } -- return err +-// FoldingRangeInfo holds range and kind info of folding for an ast.Node +-type FoldingRangeInfo struct { +- MappedRange protocol.MappedRange +- Kind protocol.FoldingRangeKind -} - --// ServerDispatcher returns a Server that dispatches LSP requests across the --// given jsonrpc2 connection. --func ServerDispatcher(conn jsonrpc2.Conn) Server { -- return &serverDispatcher{sender: clientConn{conn}} --} +-// FoldingRange gets all of the folding range for f. +-func FoldingRange(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, lineFoldingOnly bool) (ranges []*FoldingRangeInfo, err error) { +- // TODO(suzmue): consider limiting the number of folding ranges returned, and +- // implement a way to prioritize folding ranges in that case. +- pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) +- if err != nil { +- return nil, err +- } - --func ServerDispatcherV2(conn *jsonrpc2_v2.Connection) Server { -- return &serverDispatcher{sender: clientConnV2{conn}} --} +- // With parse errors, we wouldn't be able to produce accurate folding info. +- // LSP protocol (3.16) currently does not have a way to handle this case +- // (https://github.com/microsoft/language-server-protocol/issues/1200). +- // We cannot return an error either because we are afraid some editors +- // may not handle errors nicely. As a workaround, we now return an empty +- // result and let the client handle this case by double check the file +- // contents (i.e. if the file is not empty and the folding range result +- // is empty, raise an internal error). +- if pgf.ParseErr != nil { +- return nil, nil +- } - --type serverDispatcher struct { -- sender connSender --} +- // Get folding ranges for comments separately as they are not walked by ast.Inspect. +- ranges = append(ranges, commentsFoldingRange(pgf)...) - --func ClientHandler(client Client, handler jsonrpc2.Handler) jsonrpc2.Handler { -- return func(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error { -- if ctx.Err() != nil { -- ctx := xcontext.Detach(ctx) -- return reply(ctx, nil, RequestCancelledError) -- } -- handled, err := clientDispatch(ctx, client, reply, req) -- if handled || err != nil { -- return err +- visit := func(n ast.Node) bool { +- rng := foldingRangeFunc(pgf, n, lineFoldingOnly) +- if rng != nil { +- ranges = append(ranges, rng) - } -- return handler(ctx, reply, req) +- return true - } --} +- // Walk the ast and collect folding ranges. +- ast.Inspect(pgf.File, visit) - --func ClientHandlerV2(client Client) jsonrpc2_v2.Handler { -- return jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) { -- if ctx.Err() != nil { -- return nil, RequestCancelledErrorV2 -- } -- req1 := req2to1(req) -- var ( -- result interface{} -- resErr error -- ) -- replier := func(_ context.Context, res interface{}, err error) error { -- if err != nil { -- resErr = err -- return nil -- } -- result = res -- return nil -- } -- _, err := clientDispatch(ctx, client, replier, req1) -- if err != nil { -- return nil, err -- } -- return result, resErr +- sort.Slice(ranges, func(i, j int) bool { +- irng := ranges[i].MappedRange.Range() +- jrng := ranges[j].MappedRange.Range() +- return protocol.CompareRange(irng, jrng) < 0 - }) +- +- return ranges, nil -} - --func ServerHandler(server Server, handler jsonrpc2.Handler) jsonrpc2.Handler { -- return func(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error { -- if ctx.Err() != nil { -- ctx := xcontext.Detach(ctx) -- return reply(ctx, nil, RequestCancelledError) +-// foldingRangeFunc calculates the line folding range for ast.Node n +-func foldingRangeFunc(pgf *parsego.File, n ast.Node, lineFoldingOnly bool) *FoldingRangeInfo { +- // TODO(suzmue): include trailing empty lines before the closing +- // parenthesis/brace. +- var kind protocol.FoldingRangeKind +- var start, end token.Pos +- switch n := n.(type) { +- case *ast.BlockStmt: +- // Fold between positions of or lines between "{" and "}". +- var startList, endList token.Pos +- if num := len(n.List); num != 0 { +- startList, endList = n.List[0].Pos(), n.List[num-1].End() - } -- handled, err := serverDispatch(ctx, server, reply, req) -- if handled || err != nil { -- return err +- start, end = validLineFoldingRange(pgf.Tok, n.Lbrace, n.Rbrace, startList, endList, lineFoldingOnly) +- case *ast.CaseClause: +- // Fold from position of ":" to end. +- start, end = n.Colon+1, n.End() +- case *ast.CommClause: +- // Fold from position of ":" to end. +- start, end = n.Colon+1, n.End() +- case *ast.CallExpr: +- // Fold from position of "(" to position of ")". +- start, end = n.Lparen+1, n.Rparen +- case *ast.FieldList: +- // Fold between positions of or lines between opening parenthesis/brace and closing parenthesis/brace. +- var startList, endList token.Pos +- if num := len(n.List); num != 0 { +- startList, endList = n.List[0].Pos(), n.List[num-1].End() - } -- //TODO: This code is wrong, it ignores handler and assumes non standard -- // request handles everything -- // non standard request should just be a layered handler. -- var params interface{} -- if err := json.Unmarshal(req.Params(), ¶ms); err != nil { -- return sendParseError(ctx, reply, err) +- start, end = validLineFoldingRange(pgf.Tok, n.Opening, n.Closing, startList, endList, lineFoldingOnly) +- case *ast.GenDecl: +- // If this is an import declaration, set the kind to be protocol.Imports. +- if n.Tok == token.IMPORT { +- kind = protocol.Imports - } -- resp, err := server.NonstandardRequest(ctx, req.Method(), params) -- return reply(ctx, resp, err) -- -- } --} -- --func ServerHandlerV2(server Server) jsonrpc2_v2.Handler { -- return jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) { -- if ctx.Err() != nil { -- return nil, RequestCancelledErrorV2 +- // Fold between positions of or lines between "(" and ")". +- var startSpecs, endSpecs token.Pos +- if num := len(n.Specs); num != 0 { +- startSpecs, endSpecs = n.Specs[0].Pos(), n.Specs[num-1].End() - } -- req1 := req2to1(req) -- var ( -- result interface{} -- resErr error -- ) -- replier := func(_ context.Context, res interface{}, err error) error { -- if err != nil { -- resErr = err -- return nil -- } -- result = res -- return nil +- start, end = validLineFoldingRange(pgf.Tok, n.Lparen, n.Rparen, startSpecs, endSpecs, lineFoldingOnly) +- case *ast.BasicLit: +- // Fold raw string literals from position of "`" to position of "`". +- if n.Kind == token.STRING && len(n.Value) >= 2 && n.Value[0] == '`' && n.Value[len(n.Value)-1] == '`' { +- start, end = n.Pos(), n.End() - } -- _, err := serverDispatch(ctx, server, replier, req1) -- if err != nil { -- return nil, err +- case *ast.CompositeLit: +- // Fold between positions of or lines between "{" and "}". +- var startElts, endElts token.Pos +- if num := len(n.Elts); num != 0 { +- startElts, endElts = n.Elts[0].Pos(), n.Elts[num-1].End() - } -- return result, resErr -- }) --} +- start, end = validLineFoldingRange(pgf.Tok, n.Lbrace, n.Rbrace, startElts, endElts, lineFoldingOnly) +- } - --func req2to1(req2 *jsonrpc2_v2.Request) jsonrpc2.Request { -- if req2.ID.IsValid() { -- raw := req2.ID.Raw() -- var idv1 jsonrpc2.ID -- switch v := raw.(type) { -- case int64: -- idv1 = jsonrpc2.NewIntID(v) -- case string: -- idv1 = jsonrpc2.NewStringID(v) -- default: -- panic(fmt.Sprintf("unsupported ID type %T", raw)) -- } -- req1, err := jsonrpc2.NewCall(idv1, req2.Method, req2.Params) -- if err != nil { -- panic(err) -- } -- return req1 +- // Check that folding positions are valid. +- if !start.IsValid() || !end.IsValid() { +- return nil - } -- req1, err := jsonrpc2.NewNotification(req2.Method, req2.Params) +- // in line folding mode, do not fold if the start and end lines are the same. +- if lineFoldingOnly && safetoken.Line(pgf.Tok, start) == safetoken.Line(pgf.Tok, end) { +- return nil +- } +- mrng, err := pgf.PosMappedRange(start, end) - if err != nil { -- panic(err) +- bug.Errorf("%w", err) // can't happen +- } +- return &FoldingRangeInfo{ +- MappedRange: mrng, +- Kind: kind, - } -- return req1 --} -- --func Handlers(handler jsonrpc2.Handler) jsonrpc2.Handler { -- return CancelHandler( -- jsonrpc2.AsyncHandler( -- jsonrpc2.MustReplyHandler(handler))) -} - --func CancelHandler(handler jsonrpc2.Handler) jsonrpc2.Handler { -- handler, canceller := jsonrpc2.CancelHandler(handler) -- return func(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error { -- if req.Method() != "$/cancelRequest" { -- // TODO(iancottrell): See if we can generate a reply for the request to be cancelled -- // at the point of cancellation rather than waiting for gopls to naturally reply. -- // To do that, we need to keep track of whether a reply has been sent already and -- // be careful about racing between the two paths. -- // TODO(iancottrell): Add a test that watches the stream and verifies the response -- // for the cancelled request flows. -- replyWithDetachedContext := func(ctx context.Context, resp interface{}, err error) error { -- // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#cancelRequest -- if ctx.Err() != nil && err == nil { -- err = RequestCancelledError -- } -- ctx = xcontext.Detach(ctx) -- return reply(ctx, resp, err) -- } -- return handler(ctx, replyWithDetachedContext, req) -- } -- var params CancelParams -- if err := json.Unmarshal(req.Params(), ¶ms); err != nil { -- return sendParseError(ctx, reply, err) +-// validLineFoldingRange returns start and end token.Pos for folding range if the range is valid. +-// returns token.NoPos otherwise, which fails token.IsValid check +-func validLineFoldingRange(tokFile *token.File, open, close, start, end token.Pos, lineFoldingOnly bool) (token.Pos, token.Pos) { +- if lineFoldingOnly { +- if !open.IsValid() || !close.IsValid() { +- return token.NoPos, token.NoPos - } -- if n, ok := params.ID.(float64); ok { -- canceller(jsonrpc2.NewIntID(int64(n))) -- } else if s, ok := params.ID.(string); ok { -- canceller(jsonrpc2.NewStringID(s)) -- } else { -- return sendParseError(ctx, reply, fmt.Errorf("request ID %v malformed", params.ID)) +- +- // Don't want to fold if the start/end is on the same line as the open/close +- // as an example, the example below should *not* fold: +- // var x = [2]string{"d", +- // "e" } +- if safetoken.Line(tokFile, open) == safetoken.Line(tokFile, start) || +- safetoken.Line(tokFile, close) == safetoken.Line(tokFile, end) { +- return token.NoPos, token.NoPos - } -- return reply(ctx, nil, nil) -- } --} - --func Call(ctx context.Context, conn jsonrpc2.Conn, method string, params interface{}, result interface{}) error { -- id, err := conn.Call(ctx, method, params, result) -- if ctx.Err() != nil { -- cancelCall(ctx, clientConn{conn}, id) +- return open + 1, end - } -- return err +- return open + 1, close -} - --func cancelCall(ctx context.Context, sender connSender, id jsonrpc2.ID) { -- ctx = xcontext.Detach(ctx) -- ctx, done := event.Start(ctx, "protocol.canceller") -- defer done() -- // Note that only *jsonrpc2.ID implements json.Marshaler. -- sender.Notify(ctx, "$/cancelRequest", &CancelParams{ID: &id}) --} +-// commentsFoldingRange returns the folding ranges for all comment blocks in file. +-// The folding range starts at the end of the first line of the comment block, and ends at the end of the +-// comment block and has kind protocol.Comment. +-func commentsFoldingRange(pgf *parsego.File) (comments []*FoldingRangeInfo) { +- tokFile := pgf.Tok +- for _, commentGrp := range pgf.File.Comments { +- startGrpLine, endGrpLine := safetoken.Line(tokFile, commentGrp.Pos()), safetoken.Line(tokFile, commentGrp.End()) +- if startGrpLine == endGrpLine { +- // Don't fold single line comments. +- continue +- } - --func sendParseError(ctx context.Context, reply jsonrpc2.Replier, err error) error { -- return reply(ctx, nil, fmt.Errorf("%w: %s", jsonrpc2.ErrParse, err)) +- firstComment := commentGrp.List[0] +- startPos, endLinePos := firstComment.Pos(), firstComment.End() +- startCmmntLine, endCmmntLine := safetoken.Line(tokFile, startPos), safetoken.Line(tokFile, endLinePos) +- if startCmmntLine != endCmmntLine { +- // If the first comment spans multiple lines, then we want to have the +- // folding range start at the end of the first line. +- endLinePos = token.Pos(int(startPos) + len(strings.Split(firstComment.Text, "\n")[0])) +- } +- mrng, err := pgf.PosMappedRange(endLinePos, commentGrp.End()) +- if err != nil { +- bug.Errorf("%w", err) // can't happen +- } +- comments = append(comments, &FoldingRangeInfo{ +- // Fold from the end of the first line comment to the end of the comment block. +- MappedRange: mrng, +- Kind: protocol.Comment, +- }) +- } +- return comments -} -diff -urN a/gopls/internal/lsp/protocol/span.go b/gopls/internal/lsp/protocol/span.go ---- a/gopls/internal/lsp/protocol/span.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/protocol/span.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,114 +0,0 @@ +diff -urN a/gopls/internal/golang/format.go b/gopls/internal/golang/format.go +--- a/gopls/internal/golang/format.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/format.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,337 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package protocol +-// Package golang defines the LSP features for navigation, analysis, +-// and refactoring of Go source code. +-package golang - -import ( +- "bytes" +- "context" - "fmt" -- "unicode/utf8" +- "go/ast" +- "go/format" +- "go/parser" +- "go/token" +- "strings" +- "text/scanner" - -- "golang.org/x/tools/gopls/internal/span" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/internal/diff" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/imports" +- "golang.org/x/tools/internal/tokeninternal" -) - --func URIFromSpanURI(uri span.URI) DocumentURI { -- return DocumentURI(uri) // simple conversion --} -- --func URIFromPath(path string) DocumentURI { -- return URIFromSpanURI(span.URIFromPath(path)) // normalizing conversion --} -- --func (u DocumentURI) SpanURI() span.URI { -- return span.URIFromURI(string(u)) // normalizing conversion --} -- --// CompareLocation defines a three-valued comparison over locations, --// lexicographically ordered by (URI, Range). --func CompareLocation(x, y Location) int { -- if x.URI != y.URI { -- if x.URI < y.URI { -- return -1 -- } else { -- return +1 -- } -- } -- return CompareRange(x.Range, y.Range) --} +-// Format formats a file with a given range. +-func Format(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.TextEdit, error) { +- ctx, done := event.Start(ctx, "golang.Format") +- defer done() - --// CompareRange returns -1 if a is before b, 0 if a == b, and 1 if a is after b. --// --// A range a is defined to be 'before' b if a.Start is before b.Start, or --// a.Start == b.Start and a.End is before b.End. --func CompareRange(a, b Range) int { -- if r := ComparePosition(a.Start, b.Start); r != 0 { -- return r +- // Generated files shouldn't be edited. So, don't format them +- if IsGenerated(ctx, snapshot, fh.URI()) { +- return nil, fmt.Errorf("can't format %q: file is generated", fh.URI().Path()) - } -- return ComparePosition(a.End, b.End) --} - --// ComparePosition returns -1 if a is before b, 0 if a == b, and 1 if a is after b. --func ComparePosition(a, b Position) int { -- if a.Line != b.Line { -- if a.Line < b.Line { -- return -1 -- } else { -- return +1 -- } +- pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) +- if err != nil { +- return nil, err - } -- if a.Character != b.Character { -- if a.Character < b.Character { -- return -1 -- } else { -- return +1 +- // Even if this file has parse errors, it might still be possible to format it. +- // Using format.Node on an AST with errors may result in code being modified. +- // Attempt to format the source of this file instead. +- if pgf.ParseErr != nil { +- formatted, err := formatSource(ctx, fh) +- if err != nil { +- return nil, err - } +- return computeTextEdits(ctx, pgf, string(formatted)) - } -- return 0 --} - --func Intersect(a, b Range) bool { -- if a.Start.Line > b.End.Line || a.End.Line < b.Start.Line { -- return false +- // format.Node changes slightly from one release to another, so the version +- // of Go used to build the LSP server will determine how it formats code. +- // This should be acceptable for all users, who likely be prompted to rebuild +- // the LSP server on each Go release. +- buf := &bytes.Buffer{} +- fset := tokeninternal.FileSetFor(pgf.Tok) +- if err := format.Node(buf, fset, pgf.File); err != nil { +- return nil, err - } -- return !((a.Start.Line == b.End.Line) && a.Start.Character > b.End.Character || -- (a.End.Line == b.Start.Line) && a.End.Character < b.Start.Character) --} -- --// Format implements fmt.Formatter. --// --// Note: Formatter is implemented instead of Stringer (presumably) for --// performance reasons, though it is not clear that it matters in practice. --func (r Range) Format(f fmt.State, _ rune) { -- fmt.Fprintf(f, "%v-%v", r.Start, r.End) --} +- formatted := buf.String() - --// Format implements fmt.Formatter. --// --// See Range.Format for discussion of why the Formatter interface is --// implemented rather than Stringer. --func (p Position) Format(f fmt.State, _ rune) { -- fmt.Fprintf(f, "%v:%v", p.Line, p.Character) +- // Apply additional formatting, if any is supported. Currently, the only +- // supported additional formatter is gofumpt. +- if format := snapshot.Options().GofumptFormat; snapshot.Options().Gofumpt && format != nil { +- // gofumpt can customize formatting based on language version and module +- // path, if available. +- // +- // Try to derive this information, but fall-back on the default behavior. +- // +- // TODO: under which circumstances can we fail to find module information? +- // Can this, for example, result in inconsistent formatting across saves, +- // due to pending calls to packages.Load? +- var langVersion, modulePath string +- meta, err := NarrowestMetadataForFile(ctx, snapshot, fh.URI()) +- if err == nil { +- if mi := meta.Module; mi != nil { +- langVersion = mi.GoVersion +- modulePath = mi.Path +- } +- } +- b, err := format(ctx, langVersion, modulePath, buf.Bytes()) +- if err != nil { +- return nil, err +- } +- formatted = string(b) +- } +- return computeTextEdits(ctx, pgf, formatted) -} - --// -- implementation helpers -- -- --// UTF16Len returns the number of codes in the UTF-16 transcoding of s. --func UTF16Len(s []byte) int { -- var n int -- for len(s) > 0 { -- n++ -- -- // Fast path for ASCII. -- if s[0] < 0x80 { -- s = s[1:] -- continue -- } +-func formatSource(ctx context.Context, fh file.Handle) ([]byte, error) { +- _, done := event.Start(ctx, "golang.formatSource") +- defer done() - -- r, size := utf8.DecodeRune(s) -- if r >= 0x10000 { -- n++ // surrogate pair -- } -- s = s[size:] +- data, err := fh.Content() +- if err != nil { +- return nil, err - } -- return n +- return format.Source(data) -} -diff -urN a/gopls/internal/lsp/protocol/tsclient.go b/gopls/internal/lsp/protocol/tsclient.go ---- a/gopls/internal/lsp/protocol/tsclient.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/protocol/tsclient.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,249 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --// Code generated for LSP. DO NOT EDIT. +-type importFix struct { +- fix *imports.ImportFix +- edits []protocol.TextEdit +-} - --package protocol +-// allImportsFixes formats f for each possible fix to the imports. +-// In addition to returning the result of applying all edits, +-// it returns a list of fixes that could be applied to the file, with the +-// corresponding TextEdits that would be needed to apply that fix. +-func allImportsFixes(ctx context.Context, snapshot *cache.Snapshot, pgf *parsego.File) (allFixEdits []protocol.TextEdit, editsPerFix []*importFix, err error) { +- ctx, done := event.Start(ctx, "golang.AllImportsFixes") +- defer done() - --// Code generated from protocol/metaModel.json at ref release/protocol/3.17.4-next.2 (hash 184c8a7f010d335582f24337fe182baa6f2fccdd). --// https://github.com/microsoft/vscode-languageserver-node/blob/release/protocol/3.17.4-next.2/protocol/metaModel.json --// LSP metaData.version = 3.17.0. +- if err := snapshot.RunProcessEnvFunc(ctx, func(ctx context.Context, opts *imports.Options) error { +- allFixEdits, editsPerFix, err = computeImportEdits(ctx, pgf, opts) +- return err +- }); err != nil { +- return nil, nil, fmt.Errorf("AllImportsFixes: %v", err) +- } +- return allFixEdits, editsPerFix, nil +-} - --import ( -- "context" -- "encoding/json" +-// computeImportEdits computes a set of edits that perform one or all of the +-// necessary import fixes. +-func computeImportEdits(ctx context.Context, pgf *parsego.File, options *imports.Options) (allFixEdits []protocol.TextEdit, editsPerFix []*importFix, err error) { +- filename := pgf.URI.Path() - -- "golang.org/x/tools/internal/jsonrpc2" --) +- // Build up basic information about the original file. +- allFixes, err := imports.FixImports(ctx, filename, pgf.Src, options) +- if err != nil { +- return nil, nil, err +- } - --type Client interface { -- LogTrace(context.Context, *LogTraceParams) error // $/logTrace -- Progress(context.Context, *ProgressParams) error // $/progress -- RegisterCapability(context.Context, *RegistrationParams) error // client/registerCapability -- UnregisterCapability(context.Context, *UnregistrationParams) error // client/unregisterCapability -- Event(context.Context, *interface{}) error // telemetry/event -- PublishDiagnostics(context.Context, *PublishDiagnosticsParams) error // textDocument/publishDiagnostics -- LogMessage(context.Context, *LogMessageParams) error // window/logMessage -- ShowDocument(context.Context, *ShowDocumentParams) (*ShowDocumentResult, error) // window/showDocument -- ShowMessage(context.Context, *ShowMessageParams) error // window/showMessage -- ShowMessageRequest(context.Context, *ShowMessageRequestParams) (*MessageActionItem, error) // window/showMessageRequest -- WorkDoneProgressCreate(context.Context, *WorkDoneProgressCreateParams) error // window/workDoneProgress/create -- ApplyEdit(context.Context, *ApplyWorkspaceEditParams) (*ApplyWorkspaceEditResult, error) // workspace/applyEdit -- CodeLensRefresh(context.Context) error // workspace/codeLens/refresh -- Configuration(context.Context, *ParamConfiguration) ([]LSPAny, error) // workspace/configuration -- DiagnosticRefresh(context.Context) error // workspace/diagnostic/refresh -- InlayHintRefresh(context.Context) error // workspace/inlayHint/refresh -- InlineValueRefresh(context.Context) error // workspace/inlineValue/refresh -- SemanticTokensRefresh(context.Context) error // workspace/semanticTokens/refresh -- WorkspaceFolders(context.Context) ([]WorkspaceFolder, error) // workspace/workspaceFolders --} +- allFixEdits, err = computeFixEdits(pgf, options, allFixes) +- if err != nil { +- return nil, nil, err +- } - --func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier, r jsonrpc2.Request) (bool, error) { -- switch r.Method() { -- case "$/logTrace": -- var params LogTraceParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- err := client.LogTrace(ctx, ¶ms) -- return true, reply(ctx, nil, err) -- case "$/progress": -- var params ProgressParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- err := client.Progress(ctx, ¶ms) -- return true, reply(ctx, nil, err) -- case "client/registerCapability": -- var params RegistrationParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- err := client.RegisterCapability(ctx, ¶ms) -- return true, reply(ctx, nil, err) -- case "client/unregisterCapability": -- var params UnregistrationParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- err := client.UnregisterCapability(ctx, ¶ms) -- return true, reply(ctx, nil, err) -- case "telemetry/event": -- var params interface{} -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- err := client.Event(ctx, ¶ms) -- return true, reply(ctx, nil, err) -- case "textDocument/publishDiagnostics": -- var params PublishDiagnosticsParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- err := client.PublishDiagnostics(ctx, ¶ms) -- return true, reply(ctx, nil, err) -- case "window/logMessage": -- var params LogMessageParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- err := client.LogMessage(ctx, ¶ms) -- return true, reply(ctx, nil, err) -- case "window/showDocument": -- var params ShowDocumentParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- resp, err := client.ShowDocument(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) -- } -- return true, reply(ctx, resp, nil) -- case "window/showMessage": -- var params ShowMessageParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- err := client.ShowMessage(ctx, ¶ms) -- return true, reply(ctx, nil, err) -- case "window/showMessageRequest": -- var params ShowMessageRequestParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- resp, err := client.ShowMessageRequest(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) -- } -- return true, reply(ctx, resp, nil) -- case "window/workDoneProgress/create": -- var params WorkDoneProgressCreateParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- err := client.WorkDoneProgressCreate(ctx, ¶ms) -- return true, reply(ctx, nil, err) -- case "workspace/applyEdit": -- var params ApplyWorkspaceEditParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- resp, err := client.ApplyEdit(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) -- } -- return true, reply(ctx, resp, nil) -- case "workspace/codeLens/refresh": -- err := client.CodeLensRefresh(ctx) -- return true, reply(ctx, nil, err) -- case "workspace/configuration": -- var params ParamConfiguration -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- resp, err := client.Configuration(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) -- } -- return true, reply(ctx, resp, nil) -- case "workspace/diagnostic/refresh": -- err := client.DiagnosticRefresh(ctx) -- return true, reply(ctx, nil, err) -- case "workspace/inlayHint/refresh": -- err := client.InlayHintRefresh(ctx) -- return true, reply(ctx, nil, err) -- case "workspace/inlineValue/refresh": -- err := client.InlineValueRefresh(ctx) -- return true, reply(ctx, nil, err) -- case "workspace/semanticTokens/refresh": -- err := client.SemanticTokensRefresh(ctx) -- return true, reply(ctx, nil, err) -- case "workspace/workspaceFolders": -- resp, err := client.WorkspaceFolders(ctx) +- // Apply all of the import fixes to the file. +- // Add the edits for each fix to the result. +- for _, fix := range allFixes { +- edits, err := computeFixEdits(pgf, options, []*imports.ImportFix{fix}) - if err != nil { -- return true, reply(ctx, nil, err) +- return nil, nil, err - } -- return true, reply(ctx, resp, nil) -- default: -- return false, nil +- editsPerFix = append(editsPerFix, &importFix{ +- fix: fix, +- edits: edits, +- }) - } +- return allFixEdits, editsPerFix, nil -} - --func (s *clientDispatcher) LogTrace(ctx context.Context, params *LogTraceParams) error { -- return s.sender.Notify(ctx, "$/logTrace", params) --} --func (s *clientDispatcher) Progress(ctx context.Context, params *ProgressParams) error { -- return s.sender.Notify(ctx, "$/progress", params) --} --func (s *clientDispatcher) RegisterCapability(ctx context.Context, params *RegistrationParams) error { -- return s.sender.Call(ctx, "client/registerCapability", params, nil) --} --func (s *clientDispatcher) UnregisterCapability(ctx context.Context, params *UnregistrationParams) error { -- return s.sender.Call(ctx, "client/unregisterCapability", params, nil) --} --func (s *clientDispatcher) Event(ctx context.Context, params *interface{}) error { -- return s.sender.Notify(ctx, "telemetry/event", params) --} --func (s *clientDispatcher) PublishDiagnostics(ctx context.Context, params *PublishDiagnosticsParams) error { -- return s.sender.Notify(ctx, "textDocument/publishDiagnostics", params) --} --func (s *clientDispatcher) LogMessage(ctx context.Context, params *LogMessageParams) error { -- return s.sender.Notify(ctx, "window/logMessage", params) --} --func (s *clientDispatcher) ShowDocument(ctx context.Context, params *ShowDocumentParams) (*ShowDocumentResult, error) { -- var result *ShowDocumentResult -- if err := s.sender.Call(ctx, "window/showDocument", params, &result); err != nil { -- return nil, err +-// ComputeOneImportFixEdits returns text edits for a single import fix. +-func ComputeOneImportFixEdits(snapshot *cache.Snapshot, pgf *parsego.File, fix *imports.ImportFix) ([]protocol.TextEdit, error) { +- options := &imports.Options{ +- LocalPrefix: snapshot.Options().Local, +- // Defaults. +- AllErrors: true, +- Comments: true, +- Fragment: true, +- FormatOnly: false, +- TabIndent: true, +- TabWidth: 8, - } -- return result, nil +- return computeFixEdits(pgf, options, []*imports.ImportFix{fix}) -} --func (s *clientDispatcher) ShowMessage(ctx context.Context, params *ShowMessageParams) error { -- return s.sender.Notify(ctx, "window/showMessage", params) --} --func (s *clientDispatcher) ShowMessageRequest(ctx context.Context, params *ShowMessageRequestParams) (*MessageActionItem, error) { -- var result *MessageActionItem -- if err := s.sender.Call(ctx, "window/showMessageRequest", params, &result); err != nil { +- +-func computeFixEdits(pgf *parsego.File, options *imports.Options, fixes []*imports.ImportFix) ([]protocol.TextEdit, error) { +- // trim the original data to match fixedData +- left, err := importPrefix(pgf.Src) +- if err != nil { - return nil, err - } -- return result, nil --} --func (s *clientDispatcher) WorkDoneProgressCreate(ctx context.Context, params *WorkDoneProgressCreateParams) error { -- return s.sender.Call(ctx, "window/workDoneProgress/create", params, nil) --} --func (s *clientDispatcher) ApplyEdit(ctx context.Context, params *ApplyWorkspaceEditParams) (*ApplyWorkspaceEditResult, error) { -- var result *ApplyWorkspaceEditResult -- if err := s.sender.Call(ctx, "workspace/applyEdit", params, &result); err != nil { -- return nil, err +- extra := !strings.Contains(left, "\n") // one line may have more than imports +- if extra { +- left = string(pgf.Src) - } -- return result, nil --} --func (s *clientDispatcher) CodeLensRefresh(ctx context.Context) error { -- return s.sender.Call(ctx, "workspace/codeLens/refresh", nil, nil) --} --func (s *clientDispatcher) Configuration(ctx context.Context, params *ParamConfiguration) ([]LSPAny, error) { -- var result []LSPAny -- if err := s.sender.Call(ctx, "workspace/configuration", params, &result); err != nil { -- return nil, err +- if len(left) > 0 && left[len(left)-1] != '\n' { +- left += "\n" - } -- return result, nil --} --func (s *clientDispatcher) DiagnosticRefresh(ctx context.Context) error { -- return s.sender.Call(ctx, "workspace/diagnostic/refresh", nil, nil) --} --func (s *clientDispatcher) InlayHintRefresh(ctx context.Context) error { -- return s.sender.Call(ctx, "workspace/inlayHint/refresh", nil, nil) --} --func (s *clientDispatcher) InlineValueRefresh(ctx context.Context) error { -- return s.sender.Call(ctx, "workspace/inlineValue/refresh", nil, nil) --} --func (s *clientDispatcher) SemanticTokensRefresh(ctx context.Context) error { -- return s.sender.Call(ctx, "workspace/semanticTokens/refresh", nil, nil) --} --func (s *clientDispatcher) WorkspaceFolders(ctx context.Context) ([]WorkspaceFolder, error) { -- var result []WorkspaceFolder -- if err := s.sender.Call(ctx, "workspace/workspaceFolders", nil, &result); err != nil { +- // Apply the fixes and re-parse the file so that we can locate the +- // new imports. +- flags := parser.ImportsOnly +- if extra { +- // used all of origData above, use all of it here too +- flags = 0 +- } +- fixedData, err := imports.ApplyFixes(fixes, "", pgf.Src, options, flags) +- if err != nil { - return nil, err - } -- return result, nil +- if fixedData == nil || fixedData[len(fixedData)-1] != '\n' { +- fixedData = append(fixedData, '\n') // ApplyFixes may miss the newline, go figure. +- } +- edits := diff.Strings(left, string(fixedData)) +- return protocolEditsFromSource([]byte(left), edits) -} -diff -urN a/gopls/internal/lsp/protocol/tsdocument_changes.go b/gopls/internal/lsp/protocol/tsdocument_changes.go ---- a/gopls/internal/lsp/protocol/tsdocument_changes.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/protocol/tsdocument_changes.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,42 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package protocol +-// importPrefix returns the prefix of the given file content through the final +-// import statement. If there are no imports, the prefix is the package +-// statement and any comment groups below it. +-func importPrefix(src []byte) (string, error) { +- fset := token.NewFileSet() +- // do as little parsing as possible +- f, err := parser.ParseFile(fset, "", src, parser.ImportsOnly|parser.ParseComments) +- if err != nil { // This can happen if 'package' is misspelled +- return "", fmt.Errorf("importPrefix: failed to parse: %s", err) +- } +- tok := fset.File(f.Pos()) +- var importEnd int +- for _, d := range f.Decls { +- if x, ok := d.(*ast.GenDecl); ok && x.Tok == token.IMPORT { +- if e, err := safetoken.Offset(tok, d.End()); err != nil { +- return "", fmt.Errorf("importPrefix: %s", err) +- } else if e > importEnd { +- importEnd = e +- } +- } +- } - --import ( -- "encoding/json" -- "fmt" --) +- maybeAdjustToLineEnd := func(pos token.Pos, isCommentNode bool) int { +- offset, err := safetoken.Offset(tok, pos) +- if err != nil { +- return -1 +- } - --// DocumentChanges is a union of a file edit and directory rename operations --// for package renaming feature. At most one field of this struct is non-nil. --type DocumentChanges struct { -- TextDocumentEdit *TextDocumentEdit -- RenameFile *RenameFile +- // Don't go past the end of the file. +- if offset > len(src) { +- offset = len(src) +- } +- // The go/ast package does not account for different line endings, and +- // specifically, in the text of a comment, it will strip out \r\n line +- // endings in favor of \n. To account for these differences, we try to +- // return a position on the next line whenever possible. +- switch line := safetoken.Line(tok, tok.Pos(offset)); { +- case line < tok.LineCount(): +- nextLineOffset, err := safetoken.Offset(tok, tok.LineStart(line+1)) +- if err != nil { +- return -1 +- } +- // If we found a position that is at the end of a line, move the +- // offset to the start of the next line. +- if offset+1 == nextLineOffset { +- offset = nextLineOffset +- } +- case isCommentNode, offset+1 == tok.Size(): +- // If the last line of the file is a comment, or we are at the end +- // of the file, the prefix is the entire file. +- offset = len(src) +- } +- return offset +- } +- if importEnd == 0 { +- pkgEnd := f.Name.End() +- importEnd = maybeAdjustToLineEnd(pkgEnd, false) +- } +- for _, cgroup := range f.Comments { +- for _, c := range cgroup.List { +- if end, err := safetoken.Offset(tok, c.End()); err != nil { +- return "", err +- } else if end > importEnd { +- startLine := safetoken.Position(tok, c.Pos()).Line +- endLine := safetoken.Position(tok, c.End()).Line +- +- // Work around golang/go#41197 by checking if the comment might +- // contain "\r", and if so, find the actual end position of the +- // comment by scanning the content of the file. +- startOffset, err := safetoken.Offset(tok, c.Pos()) +- if err != nil { +- return "", err +- } +- if startLine != endLine && bytes.Contains(src[startOffset:], []byte("\r")) { +- if commentEnd := scanForCommentEnd(src[startOffset:]); commentEnd > 0 { +- end = startOffset + commentEnd +- } +- } +- importEnd = maybeAdjustToLineEnd(tok.Pos(end), true) +- } +- } +- } +- if importEnd > len(src) { +- importEnd = len(src) +- } +- return string(src[:importEnd]), nil -} - --func (d *DocumentChanges) UnmarshalJSON(data []byte) error { -- var m map[string]interface{} +-// scanForCommentEnd returns the offset of the end of the multi-line comment +-// at the start of the given byte slice. +-func scanForCommentEnd(src []byte) int { +- var s scanner.Scanner +- s.Init(bytes.NewReader(src)) +- s.Mode ^= scanner.SkipComments - -- if err := json.Unmarshal(data, &m); err != nil { -- return err +- t := s.Scan() +- if t == scanner.Comment { +- return s.Pos().Offset - } +- return 0 +-} - -- if _, ok := m["textDocument"]; ok { -- d.TextDocumentEdit = new(TextDocumentEdit) -- return json.Unmarshal(data, d.TextDocumentEdit) -- } +-func computeTextEdits(ctx context.Context, pgf *parsego.File, formatted string) ([]protocol.TextEdit, error) { +- _, done := event.Start(ctx, "golang.computeTextEdits") +- defer done() - -- d.RenameFile = new(RenameFile) -- return json.Unmarshal(data, d.RenameFile) +- edits := diff.Strings(string(pgf.Src), formatted) +- return protocol.EditsFromDiffEdits(pgf.Mapper, edits) -} - --func (d *DocumentChanges) MarshalJSON() ([]byte, error) { -- if d.TextDocumentEdit != nil { -- return json.Marshal(d.TextDocumentEdit) -- } else if d.RenameFile != nil { -- return json.Marshal(d.RenameFile) +-// protocolEditsFromSource converts text edits to LSP edits using the original +-// source. +-func protocolEditsFromSource(src []byte, edits []diff.Edit) ([]protocol.TextEdit, error) { +- m := protocol.NewMapper("", src) +- var result []protocol.TextEdit +- for _, edit := range edits { +- rng, err := m.OffsetRange(edit.Start, edit.End) +- if err != nil { +- return nil, err +- } +- +- if rng.Start == rng.End && edit.New == "" { +- // Degenerate case, which may result from a diff tool wanting to delete +- // '\r' in line endings. Filter it out. +- continue +- } +- result = append(result, protocol.TextEdit{ +- Range: rng, +- NewText: edit.New, +- }) - } -- return nil, fmt.Errorf("Empty DocumentChanges union value") +- return result, nil -} -diff -urN a/gopls/internal/lsp/protocol/tsjson.go b/gopls/internal/lsp/protocol/tsjson.go ---- a/gopls/internal/lsp/protocol/tsjson.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/protocol/tsjson.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,2090 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/golang/format_test.go b/gopls/internal/golang/format_test.go +--- a/gopls/internal/golang/format_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/format_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,75 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --// Code generated for LSP. DO NOT EDIT. -- --package protocol -- --// Code generated from protocol/metaModel.json at ref release/protocol/3.17.4-next.2 (hash 184c8a7f010d335582f24337fe182baa6f2fccdd). --// https://github.com/microsoft/vscode-languageserver-node/blob/release/protocol/3.17.4-next.2/protocol/metaModel.json --// LSP metaData.version = 3.17.0. +-package golang - --import "encoding/json" +-import ( +- "strings" +- "testing" - --import "fmt" +- "golang.org/x/tools/gopls/internal/test/compare" +-) - --// UnmarshalError indicates that a JSON value did not conform to --// one of the expected cases of an LSP union type. --type UnmarshalError struct { -- msg string +-func TestImportPrefix(t *testing.T) { +- for i, tt := range []struct { +- input, want string +- }{ +- {"package foo", "package foo"}, +- {"package foo\n", "package foo\n"}, +- {"package foo\n\nfunc f(){}\n", "package foo\n"}, +- {"package foo\n\nimport \"fmt\"\n", "package foo\n\nimport \"fmt\""}, +- {"package foo\nimport (\n\"fmt\"\n)\n", "package foo\nimport (\n\"fmt\"\n)"}, +- {"\n\n\npackage foo\n", "\n\n\npackage foo\n"}, +- {"// hi \n\npackage foo //xx\nfunc _(){}\n", "// hi \n\npackage foo //xx\n"}, +- {"package foo //hi\n", "package foo //hi\n"}, +- {"//hi\npackage foo\n//a\n\n//b\n", "//hi\npackage foo\n//a\n\n//b\n"}, +- { +- "package a\n\nimport (\n \"fmt\"\n)\n//hi\n", +- "package a\n\nimport (\n \"fmt\"\n)\n//hi\n", +- }, +- {`package a /*hi*/`, `package a /*hi*/`}, +- {"package main\r\n\r\nimport \"go/types\"\r\n\r\n/*\r\n\r\n */\r\n", "package main\r\n\r\nimport \"go/types\"\r\n\r\n/*\r\n\r\n */\r\n"}, +- {"package x; import \"os\"; func f() {}\n\n", "package x; import \"os\""}, +- {"package x; func f() {fmt.Println()}\n\n", "package x"}, +- } { +- got, err := importPrefix([]byte(tt.input)) +- if err != nil { +- t.Fatal(err) +- } +- if d := compare.Text(tt.want, got); d != "" { +- t.Errorf("%d: failed for %q:\n%s", i, tt.input, d) +- } +- } -} - --func (e UnmarshalError) Error() string { -- return e.msg --} +-func TestCRLFFile(t *testing.T) { +- for i, tt := range []struct { +- input, want string +- }{ +- { +- input: `package main - --// from line 4964 --func (t OrFEditRangePItemDefaults) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case FEditRangePItemDefaults: -- return json.Marshal(x) -- case Range: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil -- } -- return nil, fmt.Errorf("type %T not one of [FEditRangePItemDefaults Range]", t) +-/* +-Hi description +-*/ +-func Hi() { -} +-`, +- want: `package main - --func (t *OrFEditRangePItemDefaults) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 FEditRangePItemDefaults -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 Range -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +-/* +-Hi description +-*/`, +- }, +- } { +- got, err := importPrefix([]byte(strings.ReplaceAll(tt.input, "\n", "\r\n"))) +- if err != nil { +- t.Fatal(err) +- } +- want := strings.ReplaceAll(tt.want, "\n", "\r\n") +- if d := compare.Text(want, got); d != "" { +- t.Errorf("%d: failed for %q:\n%s", i, tt.input, d) +- } - } -- return &UnmarshalError{"unmarshal failed to match one of [FEditRangePItemDefaults Range]"} -} +diff -urN a/gopls/internal/golang/gc_annotations.go b/gopls/internal/golang/gc_annotations.go +--- a/gopls/internal/golang/gc_annotations.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/gc_annotations.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,208 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// from line 10165 --func (t OrFNotebookPNotebookSelector) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case NotebookDocumentFilter: -- return json.Marshal(x) -- case string: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil -- } -- return nil, fmt.Errorf("type %T not one of [NotebookDocumentFilter string]", t) --} +-package golang - --func (t *OrFNotebookPNotebookSelector) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 NotebookDocumentFilter -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 string -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil -- } -- return &UnmarshalError{"unmarshal failed to match one of [NotebookDocumentFilter string]"} --} +-import ( +- "bytes" +- "context" +- "encoding/json" +- "fmt" +- "os" +- "path/filepath" +- "strings" - --// from line 5715 --func (t OrPLocation_workspace_symbol) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case Location: -- return json.Marshal(x) -- case PLocationMsg_workspace_symbol: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil -- } -- return nil, fmt.Errorf("type %T not one of [Location PLocationMsg_workspace_symbol]", t) --} +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/settings" +- "golang.org/x/tools/internal/gocommand" +-) - --func (t *OrPLocation_workspace_symbol) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil +-func GCOptimizationDetails(ctx context.Context, snapshot *cache.Snapshot, mp *metadata.Package) (map[protocol.DocumentURI][]*cache.Diagnostic, error) { +- if len(mp.CompiledGoFiles) == 0 { +- return nil, nil - } -- var h0 Location -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil +- pkgDir := filepath.Dir(mp.CompiledGoFiles[0].Path()) +- outDir := filepath.Join(os.TempDir(), fmt.Sprintf("gopls-%d.details", os.Getpid())) +- +- if err := os.MkdirAll(outDir, 0700); err != nil { +- return nil, err - } -- var h1 PLocationMsg_workspace_symbol -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +- tmpFile, err := os.CreateTemp(os.TempDir(), "gopls-x") +- if err != nil { +- return nil, err - } -- return &UnmarshalError{"unmarshal failed to match one of [Location PLocationMsg_workspace_symbol]"} --} +- tmpFile.Close() // ignore error +- defer os.Remove(tmpFile.Name()) - --// from line 4358 --func (t OrPSection_workspace_didChangeConfiguration) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case []string: -- return json.Marshal(x) -- case string: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +- outDirURI := protocol.URIFromPath(outDir) +- // GC details doesn't handle Windows URIs in the form of "file:///C:/...", +- // so rewrite them to "file://C:/...". See golang/go#41614. +- if !strings.HasPrefix(outDir, "/") { +- outDirURI = protocol.DocumentURI(strings.Replace(string(outDirURI), "file:///", "file://", 1)) - } -- return nil, fmt.Errorf("type %T not one of [[]string string]", t) --} -- --func (t *OrPSection_workspace_didChangeConfiguration) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil +- inv := &gocommand.Invocation{ +- Verb: "build", +- Args: []string{ +- fmt.Sprintf("-gcflags=-json=0,%s", outDirURI), +- fmt.Sprintf("-o=%s", tmpFile.Name()), +- ".", +- }, +- WorkingDir: pkgDir, - } -- var h0 []string -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil +- _, err = snapshot.RunGoCommandDirect(ctx, cache.Normal, inv) +- if err != nil { +- return nil, err - } -- var h1 string -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +- files, err := findJSONFiles(outDir) +- if err != nil { +- return nil, err - } -- return &UnmarshalError{"unmarshal failed to match one of [[]string string]"} --} -- --// from line 7311 --func (t OrPTooltipPLabel) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case MarkupContent: -- return json.Marshal(x) -- case string: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +- reports := make(map[protocol.DocumentURI][]*cache.Diagnostic) +- opts := snapshot.Options() +- var parseError error +- for _, fn := range files { +- uri, diagnostics, err := parseDetailsFile(fn, opts) +- if err != nil { +- // expect errors for all the files, save 1 +- parseError = err +- } +- fh := snapshot.FindFile(uri) +- if fh == nil { +- continue +- } +- if pkgDir != filepath.Dir(fh.URI().Path()) { +- // https://github.com/golang/go/issues/42198 +- // sometimes the detail diagnostics generated for files +- // outside the package can never be taken back. +- continue +- } +- reports[fh.URI()] = diagnostics - } -- return nil, fmt.Errorf("type %T not one of [MarkupContent string]", t) +- return reports, parseError -} - --func (t *OrPTooltipPLabel) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 MarkupContent -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil +-func parseDetailsFile(filename string, options *settings.Options) (protocol.DocumentURI, []*cache.Diagnostic, error) { +- buf, err := os.ReadFile(filename) +- if err != nil { +- return "", nil, err - } -- var h1 string -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +- var ( +- uri protocol.DocumentURI +- i int +- diagnostics []*cache.Diagnostic +- ) +- type metadata struct { +- File string `json:"file,omitempty"` - } -- return &UnmarshalError{"unmarshal failed to match one of [MarkupContent string]"} --} -- --// from line 3772 --func (t OrPTooltip_textDocument_inlayHint) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case MarkupContent: -- return json.Marshal(x) -- case string: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +- for dec := json.NewDecoder(bytes.NewReader(buf)); dec.More(); { +- // The first element always contains metadata. +- if i == 0 { +- i++ +- m := new(metadata) +- if err := dec.Decode(m); err != nil { +- return "", nil, err +- } +- if !strings.HasSuffix(m.File, ".go") { +- continue // +- } +- uri = protocol.URIFromPath(m.File) +- continue +- } +- d := new(protocol.Diagnostic) +- if err := dec.Decode(d); err != nil { +- return "", nil, err +- } +- d.Tags = []protocol.DiagnosticTag{} // must be an actual slice +- msg := d.Code.(string) +- if msg != "" { +- msg = fmt.Sprintf("%s(%s)", msg, d.Message) +- } +- if !showDiagnostic(msg, d.Source, options) { +- continue +- } +- var related []protocol.DiagnosticRelatedInformation +- for _, ri := range d.RelatedInformation { +- // TODO(rfindley): The compiler uses LSP-like JSON to encode gc details, +- // however the positions it uses are 1-based UTF-8: +- // https://github.com/golang/go/blob/master/src/cmd/compile/internal/logopt/log_opts.go +- // +- // Here, we adjust for 0-based positions, but do not translate UTF-8 to UTF-16. +- related = append(related, protocol.DiagnosticRelatedInformation{ +- Location: protocol.Location{ +- URI: ri.Location.URI, +- Range: zeroIndexedRange(ri.Location.Range), +- }, +- Message: ri.Message, +- }) +- } +- diagnostic := &cache.Diagnostic{ +- URI: uri, +- Range: zeroIndexedRange(d.Range), +- Message: msg, +- Severity: d.Severity, +- Source: cache.OptimizationDetailsError, // d.Source is always "go compiler" as of 1.16, use our own +- Tags: d.Tags, +- Related: related, +- } +- diagnostics = append(diagnostics, diagnostic) +- i++ - } -- return nil, fmt.Errorf("type %T not one of [MarkupContent string]", t) +- return uri, diagnostics, nil -} - --func (t *OrPTooltip_textDocument_inlayHint) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil +-// showDiagnostic reports whether a given diagnostic should be shown to the end +-// user, given the current options. +-func showDiagnostic(msg, source string, o *settings.Options) bool { +- if source != "go compiler" { +- return false - } -- var h0 MarkupContent -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil +- if o.Annotations == nil { +- return true - } -- var h1 string -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +- switch { +- case strings.HasPrefix(msg, "canInline") || +- strings.HasPrefix(msg, "cannotInline") || +- strings.HasPrefix(msg, "inlineCall"): +- return o.Annotations[settings.Inline] +- case strings.HasPrefix(msg, "escape") || msg == "leak": +- return o.Annotations[settings.Escape] +- case strings.HasPrefix(msg, "nilcheck"): +- return o.Annotations[settings.Nil] +- case strings.HasPrefix(msg, "isInBounds") || +- strings.HasPrefix(msg, "isSliceInBounds"): +- return o.Annotations[settings.Bounds] - } -- return &UnmarshalError{"unmarshal failed to match one of [MarkupContent string]"} +- return false -} - --// from line 6420 --func (t Or_CancelParams_id) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case int32: -- return json.Marshal(x) -- case string: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +-// The range produced by the compiler is 1-indexed, so subtract range by 1. +-func zeroIndexedRange(rng protocol.Range) protocol.Range { +- return protocol.Range{ +- Start: protocol.Position{ +- Line: rng.Start.Line - 1, +- Character: rng.Start.Character - 1, +- }, +- End: protocol.Position{ +- Line: rng.End.Line - 1, +- Character: rng.End.Character - 1, +- }, - } -- return nil, fmt.Errorf("type %T not one of [int32 string]", t) -} - --func (t *Or_CancelParams_id) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 int32 -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 string -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 +-func findJSONFiles(dir string) ([]string, error) { +- ans := []string{} +- f := func(path string, fi os.FileInfo, _ error) error { +- if fi.IsDir() { +- return nil +- } +- if strings.HasSuffix(path, ".json") { +- ans = append(ans, path) +- } - return nil - } -- return &UnmarshalError{"unmarshal failed to match one of [int32 string]"} +- err := filepath.Walk(dir, f) +- return ans, err -} +diff -urN a/gopls/internal/golang/highlight.go b/gopls/internal/golang/highlight.go +--- a/gopls/internal/golang/highlight.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/highlight.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,528 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// from line 4777 --func (t Or_CompletionItem_documentation) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case MarkupContent: -- return json.Marshal(x) -- case string: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil -- } -- return nil, fmt.Errorf("type %T not one of [MarkupContent string]", t) --} +-package golang - --func (t *Or_CompletionItem_documentation) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 MarkupContent -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 string -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil -- } -- return &UnmarshalError{"unmarshal failed to match one of [MarkupContent string]"} --} +-import ( +- "context" +- "fmt" +- "go/ast" +- "go/token" +- "go/types" - --// from line 4860 --func (t Or_CompletionItem_textEdit) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case InsertReplaceEdit: -- return json.Marshal(x) -- case TextEdit: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil -- } -- return nil, fmt.Errorf("type %T not one of [InsertReplaceEdit TextEdit]", t) --} +- "golang.org/x/tools/go/ast/astutil" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/typesutil" +- "golang.org/x/tools/internal/event" +-) - --func (t *Or_CompletionItem_textEdit) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 InsertReplaceEdit -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 TextEdit -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil -- } -- return &UnmarshalError{"unmarshal failed to match one of [InsertReplaceEdit TextEdit]"} --} +-func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) ([]protocol.Range, error) { +- ctx, done := event.Start(ctx, "golang.Highlight") +- defer done() - --// from line 14168 --func (t Or_Definition) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case Location: -- return json.Marshal(x) -- case []Location: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +- // We always want fully parsed files for highlight, regardless +- // of whether the file belongs to a workspace package. +- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) +- if err != nil { +- return nil, fmt.Errorf("getting package for Highlight: %w", err) - } -- return nil, fmt.Errorf("type %T not one of [Location []Location]", t) --} - --func (t *Or_Definition) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil +- pos, err := pgf.PositionPos(position) +- if err != nil { +- return nil, err - } -- var h0 Location -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil +- path, _ := astutil.PathEnclosingInterval(pgf.File, pos, pos) +- if len(path) == 0 { +- return nil, fmt.Errorf("no enclosing position found for %v:%v", position.Line, position.Character) - } -- var h1 []Location -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +- // If start == end for astutil.PathEnclosingInterval, the 1-char interval +- // following start is used instead. As a result, we might not get an exact +- // match so we should check the 1-char interval to the left of the passed +- // in position to see if that is an exact match. +- if _, ok := path[0].(*ast.Ident); !ok { +- if p, _ := astutil.PathEnclosingInterval(pgf.File, pos-1, pos-1); p != nil { +- switch p[0].(type) { +- case *ast.Ident, *ast.SelectorExpr: +- path = p // use preceding ident/selector +- } +- } - } -- return &UnmarshalError{"unmarshal failed to match one of [Location []Location]"} --} -- --// from line 8865 --func (t Or_Diagnostic_code) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case int32: -- return json.Marshal(x) -- case string: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +- result, err := highlightPath(path, pgf.File, pkg.TypesInfo()) +- if err != nil { +- return nil, err - } -- return nil, fmt.Errorf("type %T not one of [int32 string]", t) +- var ranges []protocol.Range +- for rng := range result { +- rng, err := pgf.PosRange(rng.start, rng.end) +- if err != nil { +- return nil, err +- } +- ranges = append(ranges, rng) +- } +- return ranges, nil -} - --func (t *Or_Diagnostic_code) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 int32 -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 string -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil -- } -- return &UnmarshalError{"unmarshal failed to match one of [int32 string]"} --} +-// highlightPath returns ranges to highlight for the given enclosing path, +-// which should be the result of astutil.PathEnclosingInterval. +-func highlightPath(path []ast.Node, file *ast.File, info *types.Info) (map[posRange]struct{}, error) { +- result := make(map[posRange]struct{}) +- switch node := path[0].(type) { +- case *ast.BasicLit: +- // Import path string literal? +- if len(path) > 1 { +- if imp, ok := path[1].(*ast.ImportSpec); ok { +- highlight := func(n ast.Node) { +- result[posRange{start: n.Pos(), end: n.End()}] = struct{}{} +- } - --// from line 14300 --func (t Or_DocumentDiagnosticReport) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case RelatedFullDocumentDiagnosticReport: -- return json.Marshal(x) -- case RelatedUnchangedDocumentDiagnosticReport: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil -- } -- return nil, fmt.Errorf("type %T not one of [RelatedFullDocumentDiagnosticReport RelatedUnchangedDocumentDiagnosticReport]", t) --} +- // Highlight the import itself... +- highlight(imp) - --func (t *Or_DocumentDiagnosticReport) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 RelatedFullDocumentDiagnosticReport -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 RelatedUnchangedDocumentDiagnosticReport -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +- // ...and all references to it in the file. +- if pkgname, ok := typesutil.ImportedPkgName(info, imp); ok { +- ast.Inspect(file, func(n ast.Node) bool { +- if id, ok := n.(*ast.Ident); ok && +- info.Uses[id] == pkgname { +- highlight(id) +- } +- return true +- }) +- } +- return result, nil +- } +- } +- highlightFuncControlFlow(path, result) +- case *ast.ReturnStmt, *ast.FuncDecl, *ast.FuncType: +- highlightFuncControlFlow(path, result) +- case *ast.Ident: +- // Check if ident is inside return or func decl. +- highlightFuncControlFlow(path, result) +- highlightIdentifier(node, file, info, result) +- case *ast.ForStmt, *ast.RangeStmt: +- highlightLoopControlFlow(path, info, result) +- case *ast.SwitchStmt, *ast.TypeSwitchStmt: +- highlightSwitchFlow(path, info, result) +- case *ast.BranchStmt: +- // BREAK can exit a loop, switch or select, while CONTINUE exit a loop so +- // these need to be handled separately. They can also be embedded in any +- // other loop/switch/select if they have a label. TODO: add support for +- // GOTO and FALLTHROUGH as well. +- switch node.Tok { +- case token.BREAK: +- if node.Label != nil { +- highlightLabeledFlow(path, info, node, result) +- } else { +- highlightUnlabeledBreakFlow(path, info, result) +- } +- case token.CONTINUE: +- if node.Label != nil { +- highlightLabeledFlow(path, info, node, result) +- } else { +- highlightLoopControlFlow(path, info, result) +- } +- } +- default: +- // If the cursor is in an unidentified area, return empty results. +- return nil, nil - } -- return &UnmarshalError{"unmarshal failed to match one of [RelatedFullDocumentDiagnosticReport RelatedUnchangedDocumentDiagnosticReport]"} +- return result, nil -} - --// from line 3895 --func (t Or_DocumentDiagnosticReportPartialResult_relatedDocuments_Value) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case FullDocumentDiagnosticReport: -- return json.Marshal(x) -- case UnchangedDocumentDiagnosticReport: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil -- } -- return nil, fmt.Errorf("type %T not one of [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport]", t) +-type posRange struct { +- start, end token.Pos -} - --func (t *Or_DocumentDiagnosticReportPartialResult_relatedDocuments_Value) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 FullDocumentDiagnosticReport -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 UnchangedDocumentDiagnosticReport -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil -- } -- return &UnmarshalError{"unmarshal failed to match one of [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport]"} --} +-// highlightFuncControlFlow adds highlight ranges to the result map to +-// associate results and result parameters. +-// +-// Specifically, if the cursor is in a result or result parameter, all +-// results and result parameters with the same index are highlighted. If the +-// cursor is in a 'func' or 'return' keyword, the func keyword as well as all +-// returns from that func are highlighted. +-// +-// As a special case, if the cursor is within a complicated expression, control +-// flow highlighting is disabled, as it would highlight too much. +-func highlightFuncControlFlow(path []ast.Node, result map[posRange]unit) { - --// from line 14510 --func (t Or_DocumentFilter) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case NotebookCellTextDocumentFilter: -- return json.Marshal(x) -- case TextDocumentFilter: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil -- } -- return nil, fmt.Errorf("type %T not one of [NotebookCellTextDocumentFilter TextDocumentFilter]", t) --} +- var ( +- funcType *ast.FuncType // type of enclosing func, or nil +- funcBody *ast.BlockStmt // body of enclosing func, or nil +- returnStmt *ast.ReturnStmt // enclosing ReturnStmt within the func, or nil +- ) - --func (t *Or_DocumentFilter) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 NotebookCellTextDocumentFilter -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 TextDocumentFilter -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil -- } -- return &UnmarshalError{"unmarshal failed to match one of [NotebookCellTextDocumentFilter TextDocumentFilter]"} --} +-findEnclosingFunc: +- for i, n := range path { +- switch n := n.(type) { +- // TODO(rfindley, low priority): these pre-existing cases for KeyValueExpr +- // and CallExpr appear to avoid highlighting when the cursor is in a +- // complicated expression. However, the basis for this heuristic is +- // unclear. Can we formalize a rationale? +- case *ast.KeyValueExpr: +- // If cursor is in a key: value expr, we don't want control flow highlighting. +- return - --// from line 5086 --func (t Or_Hover_contents) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case MarkedString: -- return json.Marshal(x) -- case MarkupContent: -- return json.Marshal(x) -- case []MarkedString: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil -- } -- return nil, fmt.Errorf("type %T not one of [MarkedString MarkupContent []MarkedString]", t) --} +- case *ast.CallExpr: +- // If cursor is an arg in a callExpr, we don't want control flow highlighting. +- if i > 0 { +- for _, arg := range n.Args { +- if arg == path[i-1] { +- return +- } +- } +- } - --func (t *Or_Hover_contents) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 MarkedString -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 MarkupContent -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil -- } -- var h2 []MarkedString -- if err := json.Unmarshal(x, &h2); err == nil { -- t.Value = h2 -- return nil -- } -- return &UnmarshalError{"unmarshal failed to match one of [MarkedString MarkupContent []MarkedString]"} --} +- case *ast.FuncLit: +- funcType = n.Type +- funcBody = n.Body +- break findEnclosingFunc - --// from line 3731 --func (t Or_InlayHint_label) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case []InlayHintLabelPart: -- return json.Marshal(x) -- case string: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil -- } -- return nil, fmt.Errorf("type %T not one of [[]InlayHintLabelPart string]", t) --} +- case *ast.FuncDecl: +- funcType = n.Type +- funcBody = n.Body +- break findEnclosingFunc - --func (t *Or_InlayHint_label) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 []InlayHintLabelPart -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 string -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +- case *ast.ReturnStmt: +- returnStmt = n +- } - } -- return &UnmarshalError{"unmarshal failed to match one of [[]InlayHintLabelPart string]"} --} - --// from line 4163 --func (t Or_InlineCompletionItem_insertText) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case StringValue: -- return json.Marshal(x) -- case string: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +- if funcType == nil { +- return // cursor is not in a function - } -- return nil, fmt.Errorf("type %T not one of [StringValue string]", t) --} - --func (t *Or_InlineCompletionItem_insertText) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 StringValue -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil +- // Helper functions for inspecting the current location. +- var ( +- pos = path[0].Pos() +- inSpan = func(start, end token.Pos) bool { return start <= pos && pos < end } +- inNode = func(n ast.Node) bool { return inSpan(n.Pos(), n.End()) } +- ) +- +- inResults := funcType.Results != nil && inNode(funcType.Results) +- +- // If the cursor is on a "return" or "func" keyword, but not highlighting any +- // specific field or expression, we should highlight all of the exit points +- // of the function, including the "return" and "func" keywords. +- funcEnd := funcType.Func + token.Pos(len("func")) +- highlightAll := path[0] == returnStmt || inSpan(funcType.Func, funcEnd) +- var highlightIndexes map[int]bool +- +- if highlightAll { +- // Add the "func" part of the func declaration. +- result[posRange{ +- start: funcType.Func, +- end: funcEnd, +- }] = unit{} +- } else if returnStmt == nil && !inResults { +- return // nothing to highlight +- } else { +- // If we're not highighting the entire return statement, we need to collect +- // specific result indexes to highlight. This may be more than one index if +- // the cursor is on a multi-name result field, but not in any specific name. +- if !highlightAll { +- highlightIndexes = make(map[int]bool) +- if returnStmt != nil { +- for i, n := range returnStmt.Results { +- if inNode(n) { +- highlightIndexes[i] = true +- break +- } +- } +- } +- +- if funcType.Results != nil { +- // Scan fields, either adding highlights according to the highlightIndexes +- // computed above, or accounting for the cursor position within the result +- // list. +- // (We do both at once to avoid repeating the cumbersome field traversal.) +- i := 0 +- findField: +- for _, field := range funcType.Results.List { +- for j, name := range field.Names { +- if inNode(name) || highlightIndexes[i+j] { +- result[posRange{name.Pos(), name.End()}] = unit{} +- highlightIndexes[i+j] = true +- break findField // found/highlighted the specific name +- } +- } +- // If the cursor is in a field but not in a name (e.g. in the space, or +- // the type), highlight the whole field. +- // +- // Note that this may not be ideal if we're at e.g. +- // +- // (x,‸y int, z int8) +- // +- // ...where it would make more sense to highlight only y. But we don't +- // reach this function if not in a func, return, ident, or basiclit. +- if inNode(field) || highlightIndexes[i] { +- result[posRange{field.Pos(), field.End()}] = unit{} +- highlightIndexes[i] = true +- if inNode(field) { +- for j := range field.Names { +- highlightIndexes[i+j] = true +- } +- } +- break findField // found/highlighted the field +- } +- +- n := len(field.Names) +- if n == 0 { +- n = 1 +- } +- i += n +- } +- } +- } - } -- var h1 string -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +- +- if funcBody != nil { +- ast.Inspect(funcBody, func(n ast.Node) bool { +- switch n := n.(type) { +- case *ast.FuncDecl, *ast.FuncLit: +- // Don't traverse into any functions other than enclosingFunc. +- return false +- case *ast.ReturnStmt: +- if highlightAll { +- // Add the entire return statement. +- result[posRange{n.Pos(), n.End()}] = unit{} +- } else { +- // Add the highlighted indexes. +- for i, expr := range n.Results { +- if highlightIndexes[i] { +- result[posRange{expr.Pos(), expr.End()}] = unit{} +- } +- } +- } +- return false +- +- } +- return true +- }) - } -- return &UnmarshalError{"unmarshal failed to match one of [StringValue string]"} -} - --// from line 14278 --func (t Or_InlineValue) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case InlineValueEvaluatableExpression: -- return json.Marshal(x) -- case InlineValueText: -- return json.Marshal(x) -- case InlineValueVariableLookup: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +-// highlightUnlabeledBreakFlow highlights the innermost enclosing for/range/switch or swlect +-func highlightUnlabeledBreakFlow(path []ast.Node, info *types.Info, result map[posRange]struct{}) { +- // Reverse walk the path until we find closest loop, select, or switch. +- for _, n := range path { +- switch n.(type) { +- case *ast.ForStmt, *ast.RangeStmt: +- highlightLoopControlFlow(path, info, result) +- return // only highlight the innermost statement +- case *ast.SwitchStmt, *ast.TypeSwitchStmt: +- highlightSwitchFlow(path, info, result) +- return +- case *ast.SelectStmt: +- // TODO: add highlight when breaking a select. +- return +- } - } -- return nil, fmt.Errorf("type %T not one of [InlineValueEvaluatableExpression InlineValueText InlineValueVariableLookup]", t) -} - --func (t *Or_InlineValue) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 InlineValueEvaluatableExpression -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 InlineValueText -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +-// highlightLabeledFlow highlights the enclosing labeled for, range, +-// or switch statement denoted by a labeled break or continue stmt. +-func highlightLabeledFlow(path []ast.Node, info *types.Info, stmt *ast.BranchStmt, result map[posRange]struct{}) { +- use := info.Uses[stmt.Label] +- if use == nil { +- return - } -- var h2 InlineValueVariableLookup -- if err := json.Unmarshal(x, &h2); err == nil { -- t.Value = h2 -- return nil +- for _, n := range path { +- if label, ok := n.(*ast.LabeledStmt); ok && info.Defs[label.Label] == use { +- switch label.Stmt.(type) { +- case *ast.ForStmt, *ast.RangeStmt: +- highlightLoopControlFlow([]ast.Node{label.Stmt, label}, info, result) +- case *ast.SwitchStmt, *ast.TypeSwitchStmt: +- highlightSwitchFlow([]ast.Node{label.Stmt, label}, info, result) +- } +- return +- } - } -- return &UnmarshalError{"unmarshal failed to match one of [InlineValueEvaluatableExpression InlineValueText InlineValueVariableLookup]"} -} - --// from line 14475 --func (t Or_MarkedString) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case Msg_MarkedString: -- return json.Marshal(x) -- case string: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +-func labelFor(path []ast.Node) *ast.Ident { +- if len(path) > 1 { +- if n, ok := path[1].(*ast.LabeledStmt); ok { +- return n.Label +- } - } -- return nil, fmt.Errorf("type %T not one of [Msg_MarkedString string]", t) +- return nil -} - --func (t *Or_MarkedString) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil +-func highlightLoopControlFlow(path []ast.Node, info *types.Info, result map[posRange]struct{}) { +- var loop ast.Node +- var loopLabel *ast.Ident +- stmtLabel := labelFor(path) +-Outer: +- // Reverse walk the path till we get to the for loop. +- for i := range path { +- switch n := path[i].(type) { +- case *ast.ForStmt, *ast.RangeStmt: +- loopLabel = labelFor(path[i:]) +- +- if stmtLabel == nil || loopLabel == stmtLabel { +- loop = n +- break Outer +- } +- } - } -- var h0 Msg_MarkedString -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil +- if loop == nil { +- return - } -- var h1 string -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +- +- // Add the for statement. +- rng := posRange{ +- start: loop.Pos(), +- end: loop.Pos() + token.Pos(len("for")), - } -- return &UnmarshalError{"unmarshal failed to match one of [Msg_MarkedString string]"} --} +- result[rng] = struct{}{} - --// from line 10472 --func (t Or_NotebookCellTextDocumentFilter_notebook) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case NotebookDocumentFilter: -- return json.Marshal(x) -- case string: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +- // Traverse AST to find branch statements within the same for-loop. +- ast.Inspect(loop, func(n ast.Node) bool { +- switch n.(type) { +- case *ast.ForStmt, *ast.RangeStmt: +- return loop == n +- case *ast.SwitchStmt, *ast.TypeSwitchStmt, *ast.SelectStmt: +- return false +- } +- b, ok := n.(*ast.BranchStmt) +- if !ok { +- return true +- } +- if b.Label == nil || info.Uses[b.Label] == info.Defs[loopLabel] { +- result[posRange{start: b.Pos(), end: b.End()}] = struct{}{} +- } +- return true +- }) +- +- // Find continue statements in the same loop or switches/selects. +- ast.Inspect(loop, func(n ast.Node) bool { +- switch n.(type) { +- case *ast.ForStmt, *ast.RangeStmt: +- return loop == n +- } +- +- if n, ok := n.(*ast.BranchStmt); ok && n.Tok == token.CONTINUE { +- result[posRange{start: n.Pos(), end: n.End()}] = struct{}{} +- } +- return true +- }) +- +- // We don't need to check other for loops if we aren't looking for labeled statements. +- if loopLabel == nil { +- return - } -- return nil, fmt.Errorf("type %T not one of [NotebookDocumentFilter string]", t) +- +- // Find labeled branch statements in any loop. +- ast.Inspect(loop, func(n ast.Node) bool { +- b, ok := n.(*ast.BranchStmt) +- if !ok { +- return true +- } +- // statement with labels that matches the loop +- if b.Label != nil && info.Uses[b.Label] == info.Defs[loopLabel] { +- result[posRange{start: b.Pos(), end: b.End()}] = struct{}{} +- } +- return true +- }) -} - --func (t *Or_NotebookCellTextDocumentFilter_notebook) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 NotebookDocumentFilter -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil +-func highlightSwitchFlow(path []ast.Node, info *types.Info, result map[posRange]struct{}) { +- var switchNode ast.Node +- var switchNodeLabel *ast.Ident +- stmtLabel := labelFor(path) +-Outer: +- // Reverse walk the path till we get to the switch statement. +- for i := range path { +- switch n := path[i].(type) { +- case *ast.SwitchStmt, *ast.TypeSwitchStmt: +- switchNodeLabel = labelFor(path[i:]) +- if stmtLabel == nil || switchNodeLabel == stmtLabel { +- switchNode = n +- break Outer +- } +- } - } -- var h1 string -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +- // Cursor is not in a switch statement +- if switchNode == nil { +- return - } -- return &UnmarshalError{"unmarshal failed to match one of [NotebookDocumentFilter string]"} --} - --// from line 10211 --func (t Or_NotebookDocumentSyncOptions_notebookSelector_Elem_Item1_notebook) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case NotebookDocumentFilter: -- return json.Marshal(x) -- case string: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +- // Add the switch statement. +- rng := posRange{ +- start: switchNode.Pos(), +- end: switchNode.Pos() + token.Pos(len("switch")), - } -- return nil, fmt.Errorf("type %T not one of [NotebookDocumentFilter string]", t) --} +- result[rng] = struct{}{} - --func (t *Or_NotebookDocumentSyncOptions_notebookSelector_Elem_Item1_notebook) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 NotebookDocumentFilter -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 string -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil -- } -- return &UnmarshalError{"unmarshal failed to match one of [NotebookDocumentFilter string]"} --} +- // Traverse AST to find break statements within the same switch. +- ast.Inspect(switchNode, func(n ast.Node) bool { +- switch n.(type) { +- case *ast.SwitchStmt, *ast.TypeSwitchStmt: +- return switchNode == n +- case *ast.ForStmt, *ast.RangeStmt, *ast.SelectStmt: +- return false +- } - --// from line 7404 --func (t Or_RelatedFullDocumentDiagnosticReport_relatedDocuments_Value) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case FullDocumentDiagnosticReport: -- return json.Marshal(x) -- case UnchangedDocumentDiagnosticReport: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil -- } -- return nil, fmt.Errorf("type %T not one of [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport]", t) --} +- b, ok := n.(*ast.BranchStmt) +- if !ok || b.Tok != token.BREAK { +- return true +- } - --func (t *Or_RelatedFullDocumentDiagnosticReport_relatedDocuments_Value) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 FullDocumentDiagnosticReport -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 UnchangedDocumentDiagnosticReport -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil -- } -- return &UnmarshalError{"unmarshal failed to match one of [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport]"} --} +- if b.Label == nil || info.Uses[b.Label] == info.Defs[switchNodeLabel] { +- result[posRange{start: b.Pos(), end: b.End()}] = struct{}{} +- } +- return true +- }) - --// from line 7443 --func (t Or_RelatedUnchangedDocumentDiagnosticReport_relatedDocuments_Value) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case FullDocumentDiagnosticReport: -- return json.Marshal(x) -- case UnchangedDocumentDiagnosticReport: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +- // We don't need to check other switches if we aren't looking for labeled statements. +- if switchNodeLabel == nil { +- return - } -- return nil, fmt.Errorf("type %T not one of [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport]", t) --} - --func (t *Or_RelatedUnchangedDocumentDiagnosticReport_relatedDocuments_Value) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 FullDocumentDiagnosticReport -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 UnchangedDocumentDiagnosticReport -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil -- } -- return &UnmarshalError{"unmarshal failed to match one of [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport]"} --} +- // Find labeled break statements in any switch +- ast.Inspect(switchNode, func(n ast.Node) bool { +- b, ok := n.(*ast.BranchStmt) +- if !ok || b.Tok != token.BREAK { +- return true +- } - --// from line 11106 --func (t Or_RelativePattern_baseUri) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case URI: -- return json.Marshal(x) -- case WorkspaceFolder: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil -- } -- return nil, fmt.Errorf("type %T not one of [URI WorkspaceFolder]", t) --} +- if b.Label != nil && info.Uses[b.Label] == info.Defs[switchNodeLabel] { +- result[posRange{start: b.Pos(), end: b.End()}] = struct{}{} +- } - --func (t *Or_RelativePattern_baseUri) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 URI -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 WorkspaceFolder -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil -- } -- return &UnmarshalError{"unmarshal failed to match one of [URI WorkspaceFolder]"} +- return true +- }) -} - --// from line 1413 --func (t Or_Result_textDocument_codeAction_Item0_Elem) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case CodeAction: -- return json.Marshal(x) -- case Command: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +-func highlightIdentifier(id *ast.Ident, file *ast.File, info *types.Info, result map[posRange]struct{}) { +- highlight := func(n ast.Node) { +- result[posRange{start: n.Pos(), end: n.End()}] = struct{}{} - } -- return nil, fmt.Errorf("type %T not one of [CodeAction Command]", t) --} - --func (t *Or_Result_textDocument_codeAction_Item0_Elem) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 CodeAction -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 Command -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil -- } -- return &UnmarshalError{"unmarshal failed to match one of [CodeAction Command]"} --} +- // obj may be nil if the Ident is undefined. +- // In this case, the behavior expected by tests is +- // to match other undefined Idents of the same name. +- obj := info.ObjectOf(id) - --// from line 980 --func (t Or_Result_textDocument_inlineCompletion) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case InlineCompletionList: -- return json.Marshal(x) -- case []InlineCompletionItem: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil -- } -- return nil, fmt.Errorf("type %T not one of [InlineCompletionList []InlineCompletionItem]", t) --} +- ast.Inspect(file, func(n ast.Node) bool { +- switch n := n.(type) { +- case *ast.Ident: +- if n.Name == id.Name && info.ObjectOf(n) == obj { +- highlight(n) +- } - --func (t *Or_Result_textDocument_inlineCompletion) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 InlineCompletionList -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 []InlineCompletionItem -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil -- } -- return &UnmarshalError{"unmarshal failed to match one of [InlineCompletionList []InlineCompletionItem]"} +- case *ast.ImportSpec: +- pkgname, ok := typesutil.ImportedPkgName(info, n) +- if ok && pkgname == obj { +- if n.Name != nil { +- highlight(n.Name) +- } else { +- highlight(n) +- } +- } +- } +- return true +- }) -} +diff -urN a/gopls/internal/golang/hover.go b/gopls/internal/golang/hover.go +--- a/gopls/internal/golang/hover.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/hover.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,1411 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// from line 12573 --func (t Or_SemanticTokensClientCapabilities_requests_full) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case FFullPRequests: -- return json.Marshal(x) -- case bool: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil -- } -- return nil, fmt.Errorf("type %T not one of [FFullPRequests bool]", t) --} +-package golang - --func (t *Or_SemanticTokensClientCapabilities_requests_full) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 FFullPRequests -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 bool -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil -- } -- return &UnmarshalError{"unmarshal failed to match one of [FFullPRequests bool]"} --} +-import ( +- "bytes" +- "context" +- "encoding/json" +- "fmt" +- "go/ast" +- "go/constant" +- "go/doc" +- "go/format" +- "go/token" +- "go/types" +- "io/fs" +- "path/filepath" +- "sort" +- "strconv" +- "strings" +- "text/tabwriter" +- "time" +- "unicode/utf8" - --// from line 12553 --func (t Or_SemanticTokensClientCapabilities_requests_range) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case FRangePRequests: -- return json.Marshal(x) -- case bool: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil -- } -- return nil, fmt.Errorf("type %T not one of [FRangePRequests bool]", t) --} +- "golang.org/x/text/unicode/runenames" +- "golang.org/x/tools/go/ast/astutil" +- "golang.org/x/tools/go/types/typeutil" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/settings" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/gopls/internal/util/slices" +- "golang.org/x/tools/gopls/internal/util/typesutil" +- "golang.org/x/tools/internal/aliases" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/tokeninternal" +- "golang.org/x/tools/internal/typeparams" +- "golang.org/x/tools/internal/typesinternal" +-) - --func (t *Or_SemanticTokensClientCapabilities_requests_range) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 FRangePRequests -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 bool -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil -- } -- return &UnmarshalError{"unmarshal failed to match one of [FRangePRequests bool]"} --} +-// hoverJSON contains the structured result of a hover query. It is +-// formatted in one of several formats as determined by the HoverKind +-// setting, one of which is JSON. +-// +-// We believe this is used only by govim. +-// TODO(adonovan): see if we can wean all clients of this interface. +-type hoverJSON struct { +- // Synopsis is a single sentence synopsis of the symbol's documentation. +- Synopsis string `json:"synopsis"` - --// from line 6815 --func (t Or_SemanticTokensOptions_full) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case PFullESemanticTokensOptions: -- return json.Marshal(x) -- case bool: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil -- } -- return nil, fmt.Errorf("type %T not one of [PFullESemanticTokensOptions bool]", t) --} +- // FullDocumentation is the symbol's full documentation. +- FullDocumentation string `json:"fullDocumentation"` - --func (t *Or_SemanticTokensOptions_full) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 PFullESemanticTokensOptions -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 bool -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil -- } -- return &UnmarshalError{"unmarshal failed to match one of [PFullESemanticTokensOptions bool]"} --} +- // Signature is the symbol's signature. +- Signature string `json:"signature"` - --// from line 6795 --func (t Or_SemanticTokensOptions_range) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case PRangeESemanticTokensOptions: -- return json.Marshal(x) -- case bool: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil -- } -- return nil, fmt.Errorf("type %T not one of [PRangeESemanticTokensOptions bool]", t) +- // SingleLine is a single line describing the symbol. +- // This is recommended only for use in clients that show a single line for hover. +- SingleLine string `json:"singleLine"` +- +- // SymbolName is the human-readable name to use for the symbol in links. +- SymbolName string `json:"symbolName"` +- +- // LinkPath is the pkg.go.dev link for the given symbol. +- // For example, the "go/ast" part of "pkg.go.dev/go/ast#Node". +- LinkPath string `json:"linkPath"` +- +- // LinkAnchor is the pkg.go.dev link anchor for the given symbol. +- // For example, the "Node" part of "pkg.go.dev/go/ast#Node". +- LinkAnchor string `json:"linkAnchor"` +- +- // New fields go below, and are unexported. The existing +- // exported fields are underspecified and have already +- // constrained our movements too much. A detailed JSON +- // interface might be nice, but it needs a design and a +- // precise specification. +- +- // typeDecl is the declaration syntax for a type, +- // or "" for a non-type. +- typeDecl string +- +- // methods is the list of descriptions of methods of a type, +- // omitting any that are obvious from typeDecl. +- // It is "" for a non-type. +- methods string +- +- // promotedFields is the list of descriptions of accessible +- // fields of a (struct) type that were promoted through an +- // embedded field. +- promotedFields string -} - --func (t *Or_SemanticTokensOptions_range) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil +-// Hover implements the "textDocument/hover" RPC for Go files. +-func Hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) (*protocol.Hover, error) { +- ctx, done := event.Start(ctx, "golang.Hover") +- defer done() +- +- rng, h, err := hover(ctx, snapshot, fh, position) +- if err != nil { +- return nil, err - } -- var h0 PRangeESemanticTokensOptions -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil +- if h == nil { +- return nil, nil - } -- var h1 bool -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +- hover, err := formatHover(h, snapshot.Options()) +- if err != nil { +- return nil, err - } -- return &UnmarshalError{"unmarshal failed to match one of [PRangeESemanticTokensOptions bool]"} +- return &protocol.Hover{ +- Contents: protocol.MarkupContent{ +- Kind: snapshot.Options().PreferredContentFormat, +- Value: hover, +- }, +- Range: rng, +- }, nil -} - --// from line 8525 --func (t Or_ServerCapabilities_callHierarchyProvider) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case CallHierarchyOptions: -- return json.Marshal(x) -- case CallHierarchyRegistrationOptions: -- return json.Marshal(x) -- case bool: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +-// hover computes hover information at the given position. If we do not support +-// hovering at the position, it returns _, nil, nil: an error is only returned +-// if the position is valid but we fail to compute hover information. +-func hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp protocol.Position) (protocol.Range, *hoverJSON, error) { +- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) +- if err != nil { +- return protocol.Range{}, nil, err +- } +- pos, err := pgf.PositionPos(pp) +- if err != nil { +- return protocol.Range{}, nil, err - } -- return nil, fmt.Errorf("type %T not one of [CallHierarchyOptions CallHierarchyRegistrationOptions bool]", t) --} - --func (t *Or_ServerCapabilities_callHierarchyProvider) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil +- // Handle hovering over import paths, which do not have an associated +- // identifier. +- for _, spec := range pgf.File.Imports { +- // We are inclusive of the end point here to allow hovering when the cursor +- // is just after the import path. +- if spec.Path.Pos() <= pos && pos <= spec.Path.End() { +- return hoverImport(ctx, snapshot, pkg, pgf, spec) +- } - } -- var h0 CallHierarchyOptions -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil +- +- // Handle hovering over the package name, which does not have an associated +- // object. +- // As with import paths, we allow hovering just after the package name. +- if pgf.File.Name != nil && pgf.File.Name.Pos() <= pos && pos <= pgf.File.Name.Pos() { +- return hoverPackageName(pkg, pgf) - } -- var h1 CallHierarchyRegistrationOptions -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +- +- // Handle hovering over (non-import-path) literals. +- if path, _ := astutil.PathEnclosingInterval(pgf.File, pos, pos); len(path) > 0 { +- if lit, _ := path[0].(*ast.BasicLit); lit != nil { +- return hoverLit(pgf, lit, pos) +- } - } -- var h2 bool -- if err := json.Unmarshal(x, &h2); err == nil { -- t.Value = h2 -- return nil +- +- // Handle hovering over embed directive argument. +- pattern, embedRng := parseEmbedDirective(pgf.Mapper, pp) +- if pattern != "" { +- return hoverEmbed(fh, embedRng, pattern) - } -- return &UnmarshalError{"unmarshal failed to match one of [CallHierarchyOptions CallHierarchyRegistrationOptions bool]"} --} - --// from line 8333 --func (t Or_ServerCapabilities_codeActionProvider) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case CodeActionOptions: -- return json.Marshal(x) -- case bool: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +- // Handle linkname directive by overriding what to look for. +- var linkedRange *protocol.Range // range referenced by linkname directive, or nil +- if pkgPath, name, offset := parseLinkname(pgf.Mapper, pp); pkgPath != "" && name != "" { +- // rng covering 2nd linkname argument: pkgPath.name. +- rng, err := pgf.PosRange(pgf.Tok.Pos(offset), pgf.Tok.Pos(offset+len(pkgPath)+len(".")+len(name))) +- if err != nil { +- return protocol.Range{}, nil, fmt.Errorf("range over linkname arg: %w", err) +- } +- linkedRange = &rng +- +- pkg, pgf, pos, err = findLinkname(ctx, snapshot, PackagePath(pkgPath), name) +- if err != nil { +- return protocol.Range{}, nil, fmt.Errorf("find linkname: %w", err) +- } - } -- return nil, fmt.Errorf("type %T not one of [CodeActionOptions bool]", t) --} - --func (t *Or_ServerCapabilities_codeActionProvider) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil +- // The general case: compute hover information for the object referenced by +- // the identifier at pos. +- ident, obj, selectedType := referencedObject(pkg, pgf, pos) +- if obj == nil || ident == nil { +- return protocol.Range{}, nil, nil // no object to hover - } -- var h0 CodeActionOptions -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil +- +- // Unless otherwise specified, rng covers the ident being hovered. +- var rng protocol.Range +- if linkedRange != nil { +- rng = *linkedRange +- } else { +- rng, err = pgf.NodeRange(ident) +- if err != nil { +- return protocol.Range{}, nil, err +- } - } -- var h1 bool -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +- +- // By convention, we qualify hover information relative to the package +- // from which the request originated. +- qf := typesutil.FileQualifier(pgf.File, pkg.Types(), pkg.TypesInfo()) +- +- // Handle type switch identifiers as a special case, since they don't have an +- // object. +- // +- // There's not much useful information to provide. +- if selectedType != nil { +- fakeObj := types.NewVar(obj.Pos(), obj.Pkg(), obj.Name(), selectedType) +- signature := types.ObjectString(fakeObj, qf) +- return rng, &hoverJSON{ +- Signature: signature, +- SingleLine: signature, +- SymbolName: fakeObj.Name(), +- }, nil - } -- return &UnmarshalError{"unmarshal failed to match one of [CodeActionOptions bool]"} --} - --// from line 8369 --func (t Or_ServerCapabilities_colorProvider) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case DocumentColorOptions: -- return json.Marshal(x) -- case DocumentColorRegistrationOptions: -- return json.Marshal(x) -- case bool: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +- // Handle builtins, which don't have a package or position. +- if !obj.Pos().IsValid() { +- h, err := hoverBuiltin(ctx, snapshot, obj) +- return rng, h, err - } -- return nil, fmt.Errorf("type %T not one of [DocumentColorOptions DocumentColorRegistrationOptions bool]", t) --} - --func (t *Or_ServerCapabilities_colorProvider) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil +- // For all other objects, consider the full syntax of their declaration in +- // order to correctly compute their documentation, signature, and link. +- // +- // Beware: decl{PGF,Pos} are not necessarily associated with pkg.FileSet(). +- declPGF, declPos, err := parseFull(ctx, snapshot, pkg.FileSet(), obj.Pos()) +- if err != nil { +- return protocol.Range{}, nil, fmt.Errorf("re-parsing declaration of %s: %v", obj.Name(), err) - } -- var h0 DocumentColorOptions -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 DocumentColorRegistrationOptions -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil -- } -- var h2 bool -- if err := json.Unmarshal(x, &h2); err == nil { -- t.Value = h2 -- return nil -- } -- return &UnmarshalError{"unmarshal failed to match one of [DocumentColorOptions DocumentColorRegistrationOptions bool]"} --} +- decl, spec, field := findDeclInfo([]*ast.File{declPGF.File}, declPos) // may be nil^3 +- comment := chooseDocComment(decl, spec, field) +- docText := comment.Text() - --// from line 8195 --func (t Or_ServerCapabilities_declarationProvider) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case DeclarationOptions: -- return json.Marshal(x) -- case DeclarationRegistrationOptions: -- return json.Marshal(x) -- case bool: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil -- } -- return nil, fmt.Errorf("type %T not one of [DeclarationOptions DeclarationRegistrationOptions bool]", t) --} +- // By default, types.ObjectString provides a reasonable signature. +- signature := objectString(obj, qf, declPos, declPGF.Tok, spec) +- singleLineSignature := signature - --func (t *Or_ServerCapabilities_declarationProvider) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 DeclarationOptions -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 DeclarationRegistrationOptions -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil -- } -- var h2 bool -- if err := json.Unmarshal(x, &h2); err == nil { -- t.Value = h2 -- return nil +- // TODO(rfindley): we could do much better for inferred signatures. +- // TODO(adonovan): fuse the two calls below. +- if inferred := inferredSignature(pkg.TypesInfo(), ident); inferred != nil { +- if s := inferredSignatureString(obj, qf, inferred); s != "" { +- signature = s +- } - } -- return &UnmarshalError{"unmarshal failed to match one of [DeclarationOptions DeclarationRegistrationOptions bool]"} --} - --// from line 8217 --func (t Or_ServerCapabilities_definitionProvider) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case DefinitionOptions: -- return json.Marshal(x) -- case bool: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil -- } -- return nil, fmt.Errorf("type %T not one of [DefinitionOptions bool]", t) --} +- // Compute size information for types, +- // and (size, offset) for struct fields. +- // +- // Also, if a struct type's field ordering is significantly +- // wasteful of space, report its optimal size. +- // +- // This information is useful when debugging crashes or +- // optimizing layout. To reduce distraction, we show it only +- // when hovering over the declaring identifier, +- // but not referring identifiers. +- // +- // Size and alignment vary across OS/ARCH. +- // Gopls will select the appropriate build configuration when +- // viewing a type declaration in a build-tagged file, but will +- // use the default build config for all other types, even +- // if they embed platform-variant types. +- // +- var sizeOffset string // optional size/offset description +- if def, ok := pkg.TypesInfo().Defs[ident]; ok && ident.Pos() == def.Pos() { +- // This is the declaring identifier. +- // (We can't simply use ident.Pos() == obj.Pos() because +- // referencedObject prefers the TypeName for an embedded field). - --func (t *Or_ServerCapabilities_definitionProvider) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 DefinitionOptions -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 bool -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil -- } -- return &UnmarshalError{"unmarshal failed to match one of [DefinitionOptions bool]"} --} +- // format returns the decimal and hex representation of x. +- format := func(x int64) string { +- if x < 10 { +- return fmt.Sprintf("%d", x) +- } +- return fmt.Sprintf("%[1]d (%#[1]x)", x) +- } - --// from line 8682 --func (t Or_ServerCapabilities_diagnosticProvider) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case DiagnosticOptions: -- return json.Marshal(x) -- case DiagnosticRegistrationOptions: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil -- } -- return nil, fmt.Errorf("type %T not one of [DiagnosticOptions DiagnosticRegistrationOptions]", t) --} +- path := pathEnclosingObjNode(pgf.File, pos) - --func (t *Or_ServerCapabilities_diagnosticProvider) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 DiagnosticOptions -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 DiagnosticRegistrationOptions -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +- // Build string of form "size=... (X% wasted), offset=...". +- size, wasted, offset := computeSizeOffsetInfo(pkg, path, obj) +- var buf strings.Builder +- if size >= 0 { +- fmt.Fprintf(&buf, "size=%s", format(size)) +- if wasted >= 20 { // >=20% wasted +- fmt.Fprintf(&buf, " (%d%% wasted)", wasted) +- } +- } +- if offset >= 0 { +- if buf.Len() > 0 { +- buf.WriteString(", ") +- } +- fmt.Fprintf(&buf, "offset=%s", format(offset)) +- } +- sizeOffset = buf.String() - } -- return &UnmarshalError{"unmarshal failed to match one of [DiagnosticOptions DiagnosticRegistrationOptions]"} --} - --// from line 8409 --func (t Or_ServerCapabilities_documentFormattingProvider) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case DocumentFormattingOptions: -- return json.Marshal(x) -- case bool: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil -- } -- return nil, fmt.Errorf("type %T not one of [DocumentFormattingOptions bool]", t) --} +- var typeDecl, methods, fields string - --func (t *Or_ServerCapabilities_documentFormattingProvider) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 DocumentFormattingOptions -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 bool -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil -- } -- return &UnmarshalError{"unmarshal failed to match one of [DocumentFormattingOptions bool]"} --} +- // For "objects defined by a type spec", the signature produced by +- // objectString is insufficient: +- // (1) large structs are formatted poorly, with no newlines +- // (2) we lose inline comments +- // Furthermore, we include a summary of their method set. +- _, isTypeName := obj.(*types.TypeName) +- _, isTypeParam := aliases.Unalias(obj.Type()).(*types.TypeParam) +- if isTypeName && !isTypeParam { +- spec, ok := spec.(*ast.TypeSpec) +- if !ok { +- // We cannot find a TypeSpec for this type or alias declaration +- // (that is not a type parameter or a built-in). +- // This should be impossible even for ill-formed trees; +- // we suspect that AST repair may be creating inconsistent +- // positions. Don't report a bug in that case. (#64241) +- errorf := fmt.Errorf +- if !declPGF.Fixed() { +- errorf = bug.Errorf +- } +- return protocol.Range{}, nil, errorf("type name %q without type spec", obj.Name()) +- } - --// from line 8297 --func (t Or_ServerCapabilities_documentHighlightProvider) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case DocumentHighlightOptions: -- return json.Marshal(x) -- case bool: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil -- } -- return nil, fmt.Errorf("type %T not one of [DocumentHighlightOptions bool]", t) --} +- // Format the type's declaration syntax. +- { +- // Don't duplicate comments. +- spec2 := *spec +- spec2.Doc = nil +- spec2.Comment = nil +- +- var b strings.Builder +- b.WriteString("type ") +- fset := tokeninternal.FileSetFor(declPGF.Tok) +- // TODO(adonovan): use a smarter formatter that omits +- // inaccessible fields (non-exported ones from other packages). +- if err := format.Node(&b, fset, &spec2); err != nil { +- return protocol.Range{}, nil, err +- } +- typeDecl = b.String() +- +- // Splice in size/offset at end of first line. +- // "type T struct { // size=..." +- if sizeOffset != "" { +- nl := strings.IndexByte(typeDecl, '\n') +- if nl < 0 { +- nl = len(typeDecl) +- } +- typeDecl = typeDecl[:nl] + " // " + sizeOffset + typeDecl[nl:] +- } +- } - --func (t *Or_ServerCapabilities_documentHighlightProvider) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 DocumentHighlightOptions -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 bool -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil -- } -- return &UnmarshalError{"unmarshal failed to match one of [DocumentHighlightOptions bool]"} --} +- // Promoted fields +- // +- // Show a table of accessible fields of the (struct) +- // type that may not be visible in the syntax (above) +- // due to promotion through embedded fields. +- // +- // Example: +- // +- // // Embedded fields: +- // foo int // through x.y +- // z string // through x.y +- if prom := promotedFields(obj.Type(), pkg.Types()); len(prom) > 0 { +- var b strings.Builder +- b.WriteString("// Embedded fields:\n") +- w := tabwriter.NewWriter(&b, 0, 8, 1, ' ', 0) +- for _, f := range prom { +- fmt.Fprintf(w, "%s\t%s\t// through %s\t\n", +- f.field.Name(), +- types.TypeString(f.field.Type(), qf), +- f.path) +- } +- w.Flush() +- b.WriteByte('\n') +- fields = b.String() +- } +- +- // -- methods -- +- +- // For an interface type, explicit methods will have +- // already been displayed when the node was formatted +- // above. Don't list these again. +- var skip map[string]bool +- if iface, ok := spec.Type.(*ast.InterfaceType); ok { +- if iface.Methods.List != nil { +- for _, m := range iface.Methods.List { +- if len(m.Names) == 1 { +- if skip == nil { +- skip = make(map[string]bool) +- } +- skip[m.Names[0].Name] = true +- } +- } +- } +- } - --// from line 8427 --func (t Or_ServerCapabilities_documentRangeFormattingProvider) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case DocumentRangeFormattingOptions: -- return json.Marshal(x) -- case bool: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil -- } -- return nil, fmt.Errorf("type %T not one of [DocumentRangeFormattingOptions bool]", t) --} +- // Display all the type's accessible methods, +- // including those that require a pointer receiver, +- // and those promoted from embedded struct fields or +- // embedded interfaces. +- var b strings.Builder +- for _, m := range typeutil.IntuitiveMethodSet(obj.Type(), nil) { +- if !accessibleTo(m.Obj(), pkg.Types()) { +- continue // inaccessible +- } +- if skip[m.Obj().Name()] { +- continue // redundant with format.Node above +- } +- if b.Len() > 0 { +- b.WriteByte('\n') +- } - --func (t *Or_ServerCapabilities_documentRangeFormattingProvider) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 DocumentRangeFormattingOptions -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 bool -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil -- } -- return &UnmarshalError{"unmarshal failed to match one of [DocumentRangeFormattingOptions bool]"} --} +- // Use objectString for its prettier rendering of method receivers. +- b.WriteString(objectString(m.Obj(), qf, token.NoPos, nil, nil)) +- } +- methods = b.String() - --// from line 8315 --func (t Or_ServerCapabilities_documentSymbolProvider) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case DocumentSymbolOptions: -- return json.Marshal(x) -- case bool: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +- signature = typeDecl + "\n" + methods +- } else { +- // Non-types +- if sizeOffset != "" { +- signature += " // " + sizeOffset +- } - } -- return nil, fmt.Errorf("type %T not one of [DocumentSymbolOptions bool]", t) --} - --func (t *Or_ServerCapabilities_documentSymbolProvider) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 DocumentSymbolOptions -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 bool -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +- // Compute link data (on pkg.go.dev or other documentation host). +- // +- // If linkPath is empty, the symbol is not linkable. +- var ( +- linkName string // => link title, always non-empty +- linkPath string // => link path +- anchor string // link anchor +- linkMeta *metadata.Package // metadata for the linked package +- ) +- { +- linkMeta = findFileInDeps(snapshot, pkg.Metadata(), declPGF.URI) +- if linkMeta == nil { +- return protocol.Range{}, nil, bug.Errorf("no package data for %s", declPGF.URI) +- } +- +- // For package names, we simply link to their imported package. +- if pkgName, ok := obj.(*types.PkgName); ok { +- linkName = pkgName.Name() +- linkPath = pkgName.Imported().Path() +- impID := linkMeta.DepsByPkgPath[PackagePath(pkgName.Imported().Path())] +- linkMeta = snapshot.Metadata(impID) +- if linkMeta == nil { +- // Broken imports have fake package paths, so it is not a bug if we +- // don't have metadata. As of writing, there is no way to distinguish +- // broken imports from a true bug where expected metadata is missing. +- return protocol.Range{}, nil, fmt.Errorf("no package data for %s", declPGF.URI) +- } +- } else { +- // For all others, check whether the object is in the package scope, or +- // an exported field or method of an object in the package scope. +- // +- // We try to match pkgsite's heuristics for what is linkable, and what is +- // not. +- var recv types.Object +- switch obj := obj.(type) { +- case *types.Func: +- sig := obj.Type().(*types.Signature) +- if sig.Recv() != nil { +- tname := typeToObject(sig.Recv().Type()) +- if tname != nil { // beware typed nil +- recv = tname +- } +- } +- case *types.Var: +- if obj.IsField() { +- if spec, ok := spec.(*ast.TypeSpec); ok { +- typeName := spec.Name +- scopeObj, _ := obj.Pkg().Scope().Lookup(typeName.Name).(*types.TypeName) +- if scopeObj != nil { +- if st, _ := scopeObj.Type().Underlying().(*types.Struct); st != nil { +- for i := 0; i < st.NumFields(); i++ { +- if obj == st.Field(i) { +- recv = scopeObj +- } +- } +- } +- } +- } +- } +- } +- +- // Even if the object is not available in package documentation, it may +- // be embedded in a documented receiver. Detect this by searching +- // enclosing selector expressions. +- // +- // TODO(rfindley): pkgsite doesn't document fields from embedding, just +- // methods. +- if recv == nil || !recv.Exported() { +- path := pathEnclosingObjNode(pgf.File, pos) +- if enclosing := searchForEnclosing(pkg.TypesInfo(), path); enclosing != nil { +- recv = enclosing +- } else { +- recv = nil // note: just recv = ... could result in a typed nil. +- } +- } +- +- pkg := obj.Pkg() +- if recv != nil { +- linkName = fmt.Sprintf("(%s.%s).%s", pkg.Name(), recv.Name(), obj.Name()) +- if obj.Exported() && recv.Exported() && pkg.Scope().Lookup(recv.Name()) == recv { +- linkPath = pkg.Path() +- anchor = fmt.Sprintf("%s.%s", recv.Name(), obj.Name()) +- } +- } else { +- linkName = fmt.Sprintf("%s.%s", pkg.Name(), obj.Name()) +- if obj.Exported() && pkg.Scope().Lookup(obj.Name()) == obj { +- linkPath = pkg.Path() +- anchor = obj.Name() +- } +- } +- } - } -- return &UnmarshalError{"unmarshal failed to match one of [DocumentSymbolOptions bool]"} --} - --// from line 8472 --func (t Or_ServerCapabilities_foldingRangeProvider) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case FoldingRangeOptions: -- return json.Marshal(x) -- case FoldingRangeRegistrationOptions: -- return json.Marshal(x) -- case bool: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +- if snapshot.IsGoPrivatePath(linkPath) || linkMeta.ForTest != "" { +- linkPath = "" +- } else if linkMeta.Module != nil && linkMeta.Module.Version != "" { +- mod := linkMeta.Module +- linkPath = strings.Replace(linkPath, mod.Path, mod.Path+"@"+mod.Version, 1) - } -- return nil, fmt.Errorf("type %T not one of [FoldingRangeOptions FoldingRangeRegistrationOptions bool]", t) +- +- return rng, &hoverJSON{ +- Synopsis: doc.Synopsis(docText), +- FullDocumentation: docText, +- SingleLine: singleLineSignature, +- SymbolName: linkName, +- Signature: signature, +- LinkPath: linkPath, +- LinkAnchor: anchor, +- typeDecl: typeDecl, +- methods: methods, +- promotedFields: fields, +- }, nil -} - --func (t *Or_ServerCapabilities_foldingRangeProvider) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 FoldingRangeOptions -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil +-// hoverBuiltin computes hover information when hovering over a builtin +-// identifier. +-func hoverBuiltin(ctx context.Context, snapshot *cache.Snapshot, obj types.Object) (*hoverJSON, error) { +- // Special handling for error.Error, which is the only builtin method. +- // +- // TODO(rfindley): can this be unified with the handling below? +- if obj.Name() == "Error" { +- signature := obj.String() +- return &hoverJSON{ +- Signature: signature, +- SingleLine: signature, +- // TODO(rfindley): these are better than the current behavior. +- // SymbolName: "(error).Error", +- // LinkPath: "builtin", +- // LinkAnchor: "error.Error", +- }, nil - } -- var h1 FoldingRangeRegistrationOptions -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +- +- pgf, node, err := builtinDecl(ctx, snapshot, obj) +- if err != nil { +- return nil, err - } -- var h2 bool -- if err := json.Unmarshal(x, &h2); err == nil { -- t.Value = h2 -- return nil +- +- var comment *ast.CommentGroup +- path, _ := astutil.PathEnclosingInterval(pgf.File, node.Pos(), node.End()) +- for _, n := range path { +- switch n := n.(type) { +- case *ast.GenDecl: +- // Separate documentation and signature. +- comment = n.Doc +- node2 := *n +- node2.Doc = nil +- node = &node2 +- case *ast.FuncDecl: +- // Ditto. +- comment = n.Doc +- node2 := *n +- node2.Doc = nil +- node = &node2 +- } - } -- return &UnmarshalError{"unmarshal failed to match one of [FoldingRangeOptions FoldingRangeRegistrationOptions bool]"} +- +- signature := FormatNodeFile(pgf.Tok, node) +- // Replace fake types with their common equivalent. +- // TODO(rfindley): we should instead use obj.Type(), which would have the +- // *actual* types of the builtin call. +- signature = replacer.Replace(signature) +- +- docText := comment.Text() +- return &hoverJSON{ +- Synopsis: doc.Synopsis(docText), +- FullDocumentation: docText, +- Signature: signature, +- SingleLine: obj.String(), +- SymbolName: obj.Name(), +- LinkPath: "builtin", +- LinkAnchor: obj.Name(), +- }, nil -} - --// from line 8168 --func (t Or_ServerCapabilities_hoverProvider) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case HoverOptions: -- return json.Marshal(x) -- case bool: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +-// hoverImport computes hover information when hovering over the import path of +-// imp in the file pgf of pkg. +-// +-// If we do not have metadata for the hovered import, it returns _ +-func hoverImport(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, imp *ast.ImportSpec) (protocol.Range, *hoverJSON, error) { +- rng, err := pgf.NodeRange(imp.Path) +- if err != nil { +- return protocol.Range{}, nil, err - } -- return nil, fmt.Errorf("type %T not one of [HoverOptions bool]", t) --} - --func (t *Or_ServerCapabilities_hoverProvider) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil +- importPath := metadata.UnquoteImportPath(imp) +- if importPath == "" { +- return protocol.Range{}, nil, fmt.Errorf("invalid import path") - } -- var h0 HoverOptions -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil +- impID := pkg.Metadata().DepsByImpPath[importPath] +- if impID == "" { +- return protocol.Range{}, nil, fmt.Errorf("no package data for import %q", importPath) - } -- var h1 bool -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +- impMetadata := snapshot.Metadata(impID) +- if impMetadata == nil { +- return protocol.Range{}, nil, bug.Errorf("failed to resolve import ID %q", impID) - } -- return &UnmarshalError{"unmarshal failed to match one of [HoverOptions bool]"} --} - --// from line 8257 --func (t Or_ServerCapabilities_implementationProvider) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case ImplementationOptions: -- return json.Marshal(x) -- case ImplementationRegistrationOptions: -- return json.Marshal(x) -- case bool: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +- // Find the first file with a package doc comment. +- var comment *ast.CommentGroup +- for _, f := range impMetadata.CompiledGoFiles { +- fh, err := snapshot.ReadFile(ctx, f) +- if err != nil { +- if ctx.Err() != nil { +- return protocol.Range{}, nil, ctx.Err() +- } +- continue +- } +- pgf, err := snapshot.ParseGo(ctx, fh, parsego.Header) +- if err != nil { +- if ctx.Err() != nil { +- return protocol.Range{}, nil, ctx.Err() +- } +- continue +- } +- if pgf.File.Doc != nil { +- comment = pgf.File.Doc +- break +- } - } -- return nil, fmt.Errorf("type %T not one of [ImplementationOptions ImplementationRegistrationOptions bool]", t) +- +- docText := comment.Text() +- return rng, &hoverJSON{ +- Synopsis: doc.Synopsis(docText), +- FullDocumentation: docText, +- }, nil -} - --func (t *Or_ServerCapabilities_implementationProvider) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 ImplementationOptions -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 ImplementationRegistrationOptions -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +-// hoverPackageName computes hover information for the package name of the file +-// pgf in pkg. +-func hoverPackageName(pkg *cache.Package, pgf *parsego.File) (protocol.Range, *hoverJSON, error) { +- var comment *ast.CommentGroup +- for _, pgf := range pkg.CompiledGoFiles() { +- if pgf.File.Doc != nil { +- comment = pgf.File.Doc +- break +- } - } -- var h2 bool -- if err := json.Unmarshal(x, &h2); err == nil { -- t.Value = h2 -- return nil +- rng, err := pgf.NodeRange(pgf.File.Name) +- if err != nil { +- return protocol.Range{}, nil, err - } -- return &UnmarshalError{"unmarshal failed to match one of [ImplementationOptions ImplementationRegistrationOptions bool]"} +- docText := comment.Text() +- return rng, &hoverJSON{ +- Synopsis: doc.Synopsis(docText), +- FullDocumentation: docText, +- // Note: including a signature is redundant, since the cursor is already on the +- // package name. +- }, nil -} - --// from line 8659 --func (t Or_ServerCapabilities_inlayHintProvider) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case InlayHintOptions: -- return json.Marshal(x) -- case InlayHintRegistrationOptions: -- return json.Marshal(x) -- case bool: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +-// hoverLit computes hover information when hovering over the basic literal lit +-// in the file pgf. The provided pos must be the exact position of the cursor, +-// as it is used to extract the hovered rune in strings. +-// +-// For example, hovering over "\u2211" in "foo \u2211 bar" yields: +-// +-// '∑', U+2211, N-ARY SUMMATION +-func hoverLit(pgf *parsego.File, lit *ast.BasicLit, pos token.Pos) (protocol.Range, *hoverJSON, error) { +- var ( +- value string // if non-empty, a constant value to format in hover +- r rune // if non-zero, format a description of this rune in hover +- start, end token.Pos // hover span +- ) +- // Extract a rune from the current position. +- // 'Ω', "...Ω...", or 0x03A9 => 'Ω', U+03A9, GREEK CAPITAL LETTER OMEGA +- switch lit.Kind { +- case token.CHAR: +- s, err := strconv.Unquote(lit.Value) +- if err != nil { +- // If the conversion fails, it's because of an invalid syntax, therefore +- // there is no rune to be found. +- return protocol.Range{}, nil, nil +- } +- r, _ = utf8.DecodeRuneInString(s) +- if r == utf8.RuneError { +- return protocol.Range{}, nil, fmt.Errorf("rune error") +- } +- start, end = lit.Pos(), lit.End() +- +- case token.INT: +- // Short literals (e.g. 99 decimal, 07 octal) are uninteresting. +- if len(lit.Value) < 3 { +- return protocol.Range{}, nil, nil +- } +- +- v := constant.MakeFromLiteral(lit.Value, lit.Kind, 0) +- if v.Kind() != constant.Int { +- return protocol.Range{}, nil, nil +- } +- +- switch lit.Value[:2] { +- case "0x", "0X": +- // As a special case, try to recognize hexadecimal literals as runes if +- // they are within the range of valid unicode values. +- if v, ok := constant.Int64Val(v); ok && v > 0 && v <= utf8.MaxRune && utf8.ValidRune(rune(v)) { +- r = rune(v) +- } +- fallthrough +- case "0o", "0O", "0b", "0B": +- // Format the decimal value of non-decimal literals. +- value = v.ExactString() +- start, end = lit.Pos(), lit.End() +- default: +- return protocol.Range{}, nil, nil +- } +- +- case token.STRING: +- // It's a string, scan only if it contains a unicode escape sequence under or before the +- // current cursor position. +- litOffset, err := safetoken.Offset(pgf.Tok, lit.Pos()) +- if err != nil { +- return protocol.Range{}, nil, err +- } +- offset, err := safetoken.Offset(pgf.Tok, pos) +- if err != nil { +- return protocol.Range{}, nil, err +- } +- for i := offset - litOffset; i > 0; i-- { +- // Start at the cursor position and search backward for the beginning of a rune escape sequence. +- rr, _ := utf8.DecodeRuneInString(lit.Value[i:]) +- if rr == utf8.RuneError { +- return protocol.Range{}, nil, fmt.Errorf("rune error") +- } +- if rr == '\\' { +- // Got the beginning, decode it. +- var tail string +- r, _, tail, err = strconv.UnquoteChar(lit.Value[i:], '"') +- if err != nil { +- // If the conversion fails, it's because of an invalid syntax, +- // therefore is no rune to be found. +- return protocol.Range{}, nil, nil +- } +- // Only the rune escape sequence part of the string has to be highlighted, recompute the range. +- runeLen := len(lit.Value) - (i + len(tail)) +- start = token.Pos(int(lit.Pos()) + i) +- end = token.Pos(int(start) + runeLen) +- break +- } +- } - } -- return nil, fmt.Errorf("type %T not one of [InlayHintOptions InlayHintRegistrationOptions bool]", t) --} - --func (t *Or_ServerCapabilities_inlayHintProvider) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil +- if value == "" && r == 0 { // nothing to format +- return protocol.Range{}, nil, nil - } -- var h0 InlayHintOptions -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil +- +- rng, err := pgf.PosRange(start, end) +- if err != nil { +- return protocol.Range{}, nil, err - } -- var h1 InlayHintRegistrationOptions -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +- +- var b strings.Builder +- if value != "" { +- b.WriteString(value) - } -- var h2 bool -- if err := json.Unmarshal(x, &h2); err == nil { -- t.Value = h2 -- return nil +- if r != 0 { +- runeName := runenames.Name(r) +- if len(runeName) > 0 && runeName[0] == '<' { +- // Check if the rune looks like an HTML tag. If so, trim the surrounding <> +- // characters to work around https://github.com/microsoft/vscode/issues/124042. +- runeName = strings.TrimRight(runeName[1:], ">") +- } +- if b.Len() > 0 { +- b.WriteString(", ") +- } +- if strconv.IsPrint(r) { +- fmt.Fprintf(&b, "'%c', ", r) +- } +- fmt.Fprintf(&b, "U+%04X, %s", r, runeName) - } -- return &UnmarshalError{"unmarshal failed to match one of [InlayHintOptions InlayHintRegistrationOptions bool]"} +- hover := b.String() +- return rng, &hoverJSON{ +- Synopsis: hover, +- FullDocumentation: hover, +- }, nil -} - --// from line 8701 --func (t Or_ServerCapabilities_inlineCompletionProvider) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case InlineCompletionOptions: -- return json.Marshal(x) -- case bool: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil -- } -- return nil, fmt.Errorf("type %T not one of [InlineCompletionOptions bool]", t) --} +-// hoverEmbed computes hover information for a filepath.Match pattern. +-// Assumes that the pattern is relative to the location of fh. +-func hoverEmbed(fh file.Handle, rng protocol.Range, pattern string) (protocol.Range, *hoverJSON, error) { +- s := &strings.Builder{} - --func (t *Or_ServerCapabilities_inlineCompletionProvider) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 InlineCompletionOptions -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 +- dir := filepath.Dir(fh.URI().Path()) +- var matches []string +- err := filepath.WalkDir(dir, func(abs string, d fs.DirEntry, e error) error { +- if e != nil { +- return e +- } +- rel, err := filepath.Rel(dir, abs) +- if err != nil { +- return err +- } +- ok, err := filepath.Match(pattern, rel) +- if err != nil { +- return err +- } +- if ok && !d.IsDir() { +- matches = append(matches, rel) +- } - return nil +- }) +- if err != nil { +- return protocol.Range{}, nil, err - } -- var h1 bool -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +- +- for _, m := range matches { +- // TODO: Renders each file as separate markdown paragraphs. +- // If forcing (a single) newline is possible it might be more clear. +- fmt.Fprintf(s, "%s\n\n", m) - } -- return &UnmarshalError{"unmarshal failed to match one of [InlineCompletionOptions bool]"} --} - --// from line 8636 --func (t Or_ServerCapabilities_inlineValueProvider) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case InlineValueOptions: -- return json.Marshal(x) -- case InlineValueRegistrationOptions: -- return json.Marshal(x) -- case bool: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +- json := &hoverJSON{ +- Signature: fmt.Sprintf("Embedding %q", pattern), +- Synopsis: s.String(), +- FullDocumentation: s.String(), - } -- return nil, fmt.Errorf("type %T not one of [InlineValueOptions InlineValueRegistrationOptions bool]", t) +- return rng, json, nil -} - --func (t *Or_ServerCapabilities_inlineValueProvider) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 InlineValueOptions -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 InlineValueRegistrationOptions -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil -- } -- var h2 bool -- if err := json.Unmarshal(x, &h2); err == nil { -- t.Value = h2 -- return nil +-// inferredSignatureString is a wrapper around the types.ObjectString function +-// that adds more information to inferred signatures. It will return an empty string +-// if the passed types.Object is not a signature. +-func inferredSignatureString(obj types.Object, qf types.Qualifier, inferred *types.Signature) string { +- // If the signature type was inferred, prefer the inferred signature with a +- // comment showing the generic signature. +- if sig, _ := obj.Type().Underlying().(*types.Signature); sig != nil && sig.TypeParams().Len() > 0 && inferred != nil { +- obj2 := types.NewFunc(obj.Pos(), obj.Pkg(), obj.Name(), inferred) +- str := types.ObjectString(obj2, qf) +- // Try to avoid overly long lines. +- if len(str) > 60 { +- str += "\n" +- } else { +- str += " " +- } +- str += "// " + types.TypeString(sig, qf) +- return str - } -- return &UnmarshalError{"unmarshal failed to match one of [InlineValueOptions InlineValueRegistrationOptions bool]"} +- return "" -} - --// from line 8548 --func (t Or_ServerCapabilities_linkedEditingRangeProvider) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case LinkedEditingRangeOptions: -- return json.Marshal(x) -- case LinkedEditingRangeRegistrationOptions: -- return json.Marshal(x) -- case bool: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +-// objectString is a wrapper around the types.ObjectString function. +-// It handles adding more information to the object string. +-// If spec is non-nil, it may be used to format additional declaration +-// syntax, and file must be the token.File describing its positions. +-// +-// Precondition: obj is not a built-in function or method. +-func objectString(obj types.Object, qf types.Qualifier, declPos token.Pos, file *token.File, spec ast.Spec) string { +- str := types.ObjectString(obj, qf) +- +- switch obj := obj.(type) { +- case *types.Func: +- // We fork ObjectString to improve its rendering of methods: +- // specifically, we show the receiver name, +- // and replace the period in (T).f by a space (#62190). +- +- sig := obj.Type().(*types.Signature) +- +- var buf bytes.Buffer +- buf.WriteString("func ") +- if recv := sig.Recv(); recv != nil { +- buf.WriteByte('(') +- if _, ok := recv.Type().(*types.Interface); ok { +- // gcimporter creates abstract methods of +- // named interfaces using the interface type +- // (not the named type) as the receiver. +- // Don't print it in full. +- buf.WriteString("interface") +- } else { +- // Show receiver name (go/types does not). +- name := recv.Name() +- if name != "" && name != "_" { +- buf.WriteString(name) +- buf.WriteString(" ") +- } +- types.WriteType(&buf, recv.Type(), qf) +- } +- buf.WriteByte(')') +- buf.WriteByte(' ') // space (go/types uses a period) +- } else if s := qf(obj.Pkg()); s != "" { +- buf.WriteString(s) +- buf.WriteString(".") +- } +- buf.WriteString(obj.Name()) +- types.WriteSignature(&buf, sig, qf) +- str = buf.String() +- +- case *types.Const: +- // Show value of a constant. +- var ( +- declaration = obj.Val().String() // default formatted declaration +- comment = "" // if non-empty, a clarifying comment +- ) +- +- // Try to use the original declaration. +- switch obj.Val().Kind() { +- case constant.String: +- // Usually the original declaration of a string doesn't carry much information. +- // Also strings can be very long. So, just use the constant's value. +- +- default: +- if spec, _ := spec.(*ast.ValueSpec); spec != nil { +- for i, name := range spec.Names { +- if declPos == name.Pos() { +- if i < len(spec.Values) { +- originalDeclaration := FormatNodeFile(file, spec.Values[i]) +- if originalDeclaration != declaration { +- comment = declaration +- declaration = originalDeclaration +- } +- } +- break +- } +- } +- } +- } +- +- // Special formatting cases. +- switch typ := aliases.Unalias(obj.Type()).(type) { +- case *types.Named: +- // Try to add a formatted duration as an inline comment. +- pkg := typ.Obj().Pkg() +- if pkg.Path() == "time" && typ.Obj().Name() == "Duration" && obj.Val().Kind() == constant.Int { +- if d, ok := constant.Int64Val(obj.Val()); ok { +- comment = time.Duration(d).String() +- } +- } +- } +- if comment == declaration { +- comment = "" +- } +- +- str += " = " + declaration +- if comment != "" { +- str += " // " + comment +- } - } -- return nil, fmt.Errorf("type %T not one of [LinkedEditingRangeOptions LinkedEditingRangeRegistrationOptions bool]", t) +- return str -} - --func (t *Or_ServerCapabilities_linkedEditingRangeProvider) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 LinkedEditingRangeOptions -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 LinkedEditingRangeRegistrationOptions -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil -- } -- var h2 bool -- if err := json.Unmarshal(x, &h2); err == nil { -- t.Value = h2 -- return nil +-// HoverDocForObject returns the best doc comment for obj (for which +-// fset provides file/line information). +-// +-// TODO(rfindley): there appears to be zero(!) tests for this functionality. +-func HoverDocForObject(ctx context.Context, snapshot *cache.Snapshot, fset *token.FileSet, obj types.Object) (*ast.CommentGroup, error) { +- if is[*types.TypeName](obj) && is[*types.TypeParam](obj.Type()) { +- return nil, nil - } -- return &UnmarshalError{"unmarshal failed to match one of [LinkedEditingRangeOptions LinkedEditingRangeRegistrationOptions bool]"} --} - --// from line 8590 --func (t Or_ServerCapabilities_monikerProvider) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case MonikerOptions: -- return json.Marshal(x) -- case MonikerRegistrationOptions: -- return json.Marshal(x) -- case bool: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +- pgf, pos, err := parseFull(ctx, snapshot, fset, obj.Pos()) +- if err != nil { +- return nil, fmt.Errorf("re-parsing: %v", err) - } -- return nil, fmt.Errorf("type %T not one of [MonikerOptions MonikerRegistrationOptions bool]", t) +- +- decl, spec, field := findDeclInfo([]*ast.File{pgf.File}, pos) +- return chooseDocComment(decl, spec, field), nil -} - --func (t *Or_ServerCapabilities_monikerProvider) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 MonikerOptions -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 MonikerRegistrationOptions -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 +-func chooseDocComment(decl ast.Decl, spec ast.Spec, field *ast.Field) *ast.CommentGroup { +- if field != nil { +- if field.Doc != nil { +- return field.Doc +- } +- if field.Comment != nil { +- return field.Comment +- } - return nil - } -- var h2 bool -- if err := json.Unmarshal(x, &h2); err == nil { -- t.Value = h2 -- return nil +- switch decl := decl.(type) { +- case *ast.FuncDecl: +- return decl.Doc +- case *ast.GenDecl: +- switch spec := spec.(type) { +- case *ast.ValueSpec: +- if spec.Doc != nil { +- return spec.Doc +- } +- if decl.Doc != nil { +- return decl.Doc +- } +- return spec.Comment +- case *ast.TypeSpec: +- if spec.Doc != nil { +- return spec.Doc +- } +- if decl.Doc != nil { +- return decl.Doc +- } +- return spec.Comment +- } - } -- return &UnmarshalError{"unmarshal failed to match one of [MonikerOptions MonikerRegistrationOptions bool]"} +- return nil -} - --// from line 8140 --func (t Or_ServerCapabilities_notebookDocumentSync) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case NotebookDocumentSyncOptions: -- return json.Marshal(x) -- case NotebookDocumentSyncRegistrationOptions: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +-// parseFull fully parses the file corresponding to position pos (for +-// which fset provides file/line information). +-// +-// It returns the resulting parsego.File as well as new pos contained +-// in the parsed file. +-// +-// BEWARE: the provided FileSet is used only to interpret the provided +-// pos; the resulting File and Pos may belong to the same or a +-// different FileSet, such as one synthesized by the parser cache, if +-// parse-caching is enabled. +-func parseFull(ctx context.Context, snapshot *cache.Snapshot, fset *token.FileSet, pos token.Pos) (*parsego.File, token.Pos, error) { +- f := fset.File(pos) +- if f == nil { +- return nil, 0, bug.Errorf("internal error: no file for position %d", pos) - } -- return nil, fmt.Errorf("type %T not one of [NotebookDocumentSyncOptions NotebookDocumentSyncRegistrationOptions]", t) --} - --func (t *Or_ServerCapabilities_notebookDocumentSync) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil +- uri := protocol.URIFromPath(f.Name()) +- fh, err := snapshot.ReadFile(ctx, uri) +- if err != nil { +- return nil, 0, err - } -- var h0 NotebookDocumentSyncOptions -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil +- +- pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) +- if err != nil { +- return nil, 0, err - } -- var h1 NotebookDocumentSyncRegistrationOptions -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +- +- offset, err := safetoken.Offset(f, pos) +- if err != nil { +- return nil, 0, bug.Errorf("offset out of bounds in %q", uri) - } -- return &UnmarshalError{"unmarshal failed to match one of [NotebookDocumentSyncOptions NotebookDocumentSyncRegistrationOptions]"} --} - --// from line 8279 --func (t Or_ServerCapabilities_referencesProvider) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case ReferenceOptions: -- return json.Marshal(x) -- case bool: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +- fullPos, err := safetoken.Pos(pgf.Tok, offset) +- if err != nil { +- return nil, 0, err - } -- return nil, fmt.Errorf("type %T not one of [ReferenceOptions bool]", t) +- +- return pgf, fullPos, nil -} - --func (t *Or_ServerCapabilities_referencesProvider) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 ReferenceOptions -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 bool -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +-func formatHover(h *hoverJSON, options *settings.Options) (string, error) { +- maybeMarkdown := func(s string) string { +- if s != "" && options.PreferredContentFormat == protocol.Markdown { +- s = fmt.Sprintf("```go\n%s\n```", strings.Trim(s, "\n")) +- } +- return s - } -- return &UnmarshalError{"unmarshal failed to match one of [ReferenceOptions bool]"} --} - --// from line 8454 --func (t Or_ServerCapabilities_renameProvider) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case RenameOptions: -- return json.Marshal(x) -- case bool: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil -- } -- return nil, fmt.Errorf("type %T not one of [RenameOptions bool]", t) --} +- switch options.HoverKind { +- case settings.SingleLine: +- return h.SingleLine, nil - --func (t *Or_ServerCapabilities_renameProvider) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 RenameOptions -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 bool -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil -- } -- return &UnmarshalError{"unmarshal failed to match one of [RenameOptions bool]"} --} +- case settings.NoDocumentation: +- return maybeMarkdown(h.Signature), nil - --// from line 8494 --func (t Or_ServerCapabilities_selectionRangeProvider) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case SelectionRangeOptions: -- return json.Marshal(x) -- case SelectionRangeRegistrationOptions: -- return json.Marshal(x) -- case bool: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +- case settings.Structured: +- b, err := json.Marshal(h) +- if err != nil { +- return "", err +- } +- return string(b), nil +- +- case settings.SynopsisDocumentation, +- settings.FullDocumentation: +- // For types, we display TypeDecl and Methods, +- // but not Signature, which is redundant (= TypeDecl + "\n" + Methods). +- // For all other symbols, we display Signature; +- // TypeDecl and Methods are empty. +- // (This awkwardness is to preserve JSON compatibility.) +- parts := []string{ +- maybeMarkdown(h.Signature), +- maybeMarkdown(h.typeDecl), +- formatDoc(h, options), +- maybeMarkdown(h.promotedFields), +- maybeMarkdown(h.methods), +- formatLink(h, options), +- } +- if h.typeDecl != "" { +- parts[0] = "" // type: suppress redundant Signature +- } +- parts = slices.Remove(parts, "") +- +- var b strings.Builder +- for i, part := range parts { +- if i > 0 { +- if options.PreferredContentFormat == protocol.Markdown { +- b.WriteString("\n\n") +- } else { +- b.WriteByte('\n') +- } +- } +- b.WriteString(part) +- } +- return b.String(), nil +- +- default: +- return "", fmt.Errorf("invalid HoverKind: %v", options.HoverKind) - } -- return nil, fmt.Errorf("type %T not one of [SelectionRangeOptions SelectionRangeRegistrationOptions bool]", t) -} - --func (t *Or_ServerCapabilities_selectionRangeProvider) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 SelectionRangeOptions -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 SelectionRangeRegistrationOptions -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +-func formatLink(h *hoverJSON, options *settings.Options) string { +- if !options.LinksInHover || options.LinkTarget == "" || h.LinkPath == "" { +- return "" - } -- var h2 bool -- if err := json.Unmarshal(x, &h2); err == nil { -- t.Value = h2 -- return nil +- plainLink := cache.BuildLink(options.LinkTarget, h.LinkPath, h.LinkAnchor) +- switch options.PreferredContentFormat { +- case protocol.Markdown: +- return fmt.Sprintf("[`%s` on %s](%s)", h.SymbolName, options.LinkTarget, plainLink) +- case protocol.PlainText: +- return "" +- default: +- return plainLink - } -- return &UnmarshalError{"unmarshal failed to match one of [SelectionRangeOptions SelectionRangeRegistrationOptions bool]"} -} - --// from line 8571 --func (t Or_ServerCapabilities_semanticTokensProvider) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case SemanticTokensOptions: -- return json.Marshal(x) -- case SemanticTokensRegistrationOptions: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +-func formatDoc(h *hoverJSON, options *settings.Options) string { +- var doc string +- switch options.HoverKind { +- case settings.SynopsisDocumentation: +- doc = h.Synopsis +- case settings.FullDocumentation: +- doc = h.FullDocumentation - } -- return nil, fmt.Errorf("type %T not one of [SemanticTokensOptions SemanticTokensRegistrationOptions]", t) +- if options.PreferredContentFormat == protocol.Markdown { +- return CommentToMarkdown(doc, options) +- } +- return doc -} - --func (t *Or_ServerCapabilities_semanticTokensProvider) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 SemanticTokensOptions -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil +-// findDeclInfo returns the syntax nodes involved in the declaration of the +-// types.Object with position pos, searching the given list of file syntax +-// trees. +-// +-// Pos may be the position of the name-defining identifier in a FuncDecl, +-// ValueSpec, TypeSpec, Field, or as a special case the position of +-// Ellipsis.Elt in an ellipsis field. +-// +-// If found, the resulting decl, spec, and field will be the inner-most +-// instance of each node type surrounding pos. +-// +-// If field is non-nil, pos is the position of a field Var. If field is nil and +-// spec is non-nil, pos is the position of a Var, Const, or TypeName object. If +-// both field and spec are nil and decl is non-nil, pos is the position of a +-// Func object. +-// +-// It returns a nil decl if no object-defining node is found at pos. +-// +-// TODO(rfindley): this function has tricky semantics, and may be worth unit +-// testing and/or refactoring. +-func findDeclInfo(files []*ast.File, pos token.Pos) (decl ast.Decl, spec ast.Spec, field *ast.Field) { +- found := false +- +- // Visit the files in search of the node at pos. +- stack := make([]ast.Node, 0, 20) +- +- // Allocate the closure once, outside the loop. +- f := func(n ast.Node) bool { +- if found { +- return false +- } +- if n != nil { +- stack = append(stack, n) // push +- } else { +- stack = stack[:len(stack)-1] // pop +- return false +- } +- +- // Skip subtrees (incl. files) that don't contain the search point. +- if !(n.Pos() <= pos && pos < n.End()) { +- return false +- } +- +- switch n := n.(type) { +- case *ast.Field: +- findEnclosingDeclAndSpec := func() { +- for i := len(stack) - 1; i >= 0; i-- { +- switch n := stack[i].(type) { +- case ast.Spec: +- spec = n +- case ast.Decl: +- decl = n +- return +- } +- } +- } +- +- // Check each field name since you can have +- // multiple names for the same type expression. +- for _, id := range n.Names { +- if id.Pos() == pos { +- field = n +- findEnclosingDeclAndSpec() +- found = true +- return false +- } +- } +- +- // Check *ast.Field itself. This handles embedded +- // fields which have no associated *ast.Ident name. +- if n.Pos() == pos { +- field = n +- findEnclosingDeclAndSpec() +- found = true +- return false +- } +- +- // Also check "X" in "...X". This makes it easy to format variadic +- // signature params properly. +- // +- // TODO(rfindley): I don't understand this comment. How does finding the +- // field in this case make it easier to format variadic signature params? +- if ell, ok := n.Type.(*ast.Ellipsis); ok && ell.Elt != nil && ell.Elt.Pos() == pos { +- field = n +- findEnclosingDeclAndSpec() +- found = true +- return false +- } +- +- case *ast.FuncDecl: +- if n.Name.Pos() == pos { +- decl = n +- found = true +- return false +- } +- +- case *ast.GenDecl: +- for _, s := range n.Specs { +- switch s := s.(type) { +- case *ast.TypeSpec: +- if s.Name.Pos() == pos { +- decl = n +- spec = s +- found = true +- return false +- } +- case *ast.ValueSpec: +- for _, id := range s.Names { +- if id.Pos() == pos { +- decl = n +- spec = s +- found = true +- return false +- } +- } +- } +- } +- } +- return true - } -- var h1 SemanticTokensRegistrationOptions -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +- for _, file := range files { +- ast.Inspect(file, f) +- if found { +- return decl, spec, field +- } - } -- return &UnmarshalError{"unmarshal failed to match one of [SemanticTokensOptions SemanticTokensRegistrationOptions]"} +- +- return nil, nil, nil -} - --// from line 8122 --func (t Or_ServerCapabilities_textDocumentSync) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case TextDocumentSyncKind: -- return json.Marshal(x) -- case TextDocumentSyncOptions: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil -- } -- return nil, fmt.Errorf("type %T not one of [TextDocumentSyncKind TextDocumentSyncOptions]", t) +-type promotedField struct { +- path string // path (e.g. "x.y" through embedded fields) +- field *types.Var -} - --func (t *Or_ServerCapabilities_textDocumentSync) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 TextDocumentSyncKind -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil +-// promotedFields returns the list of accessible promoted fields of a struct type t. +-// (Logic plundered from x/tools/cmd/guru/describe.go.) +-func promotedFields(t types.Type, from *types.Package) []promotedField { +- wantField := func(f *types.Var) bool { +- if !accessibleTo(f, from) { +- return false +- } +- // Check that the field is not shadowed. +- obj, _, _ := types.LookupFieldOrMethod(t, true, f.Pkg(), f.Name()) +- return obj == f - } -- var h1 TextDocumentSyncOptions -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +- +- var fields []promotedField +- var visit func(t types.Type, stack []*types.Named) +- visit = func(t types.Type, stack []*types.Named) { +- tStruct, ok := typesinternal.Unpointer(t).Underlying().(*types.Struct) +- if !ok { +- return +- } +- fieldloop: +- for i := 0; i < tStruct.NumFields(); i++ { +- f := tStruct.Field(i) +- +- // Handle recursion through anonymous fields. +- if f.Anonymous() { +- if _, named := typesinternal.ReceiverNamed(f); named != nil { +- // If we've already visited this named type +- // on this path, break the cycle. +- for _, x := range stack { +- if x.Origin() == named.Origin() { +- continue fieldloop +- } +- } +- visit(f.Type(), append(stack, named)) +- } +- } +- +- // Save accessible promoted fields. +- if len(stack) > 0 && wantField(f) { +- var path strings.Builder +- for i, t := range stack { +- if i > 0 { +- path.WriteByte('.') +- } +- path.WriteString(t.Obj().Name()) +- } +- fields = append(fields, promotedField{ +- path: path.String(), +- field: f, +- }) +- } +- } - } -- return &UnmarshalError{"unmarshal failed to match one of [TextDocumentSyncKind TextDocumentSyncOptions]"} +- visit(t, nil) +- +- return fields -} - --// from line 8235 --func (t Or_ServerCapabilities_typeDefinitionProvider) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case TypeDefinitionOptions: -- return json.Marshal(x) -- case TypeDefinitionRegistrationOptions: -- return json.Marshal(x) -- case bool: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil -- } -- return nil, fmt.Errorf("type %T not one of [TypeDefinitionOptions TypeDefinitionRegistrationOptions bool]", t) +-func accessibleTo(obj types.Object, pkg *types.Package) bool { +- return obj.Exported() || obj.Pkg() == pkg -} - --func (t *Or_ServerCapabilities_typeDefinitionProvider) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 TypeDefinitionOptions -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 TypeDefinitionRegistrationOptions -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil -- } -- var h2 bool -- if err := json.Unmarshal(x, &h2); err == nil { -- t.Value = h2 -- return nil +-// computeSizeOffsetInfo reports the size of obj (if a type or struct +-// field), its wasted space percentage (if a struct type), and its +-// offset (if a struct field). It returns -1 for undefined components. +-func computeSizeOffsetInfo(pkg *cache.Package, path []ast.Node, obj types.Object) (size, wasted, offset int64) { +- size, wasted, offset = -1, -1, -1 +- +- var free typeparams.Free +- sizes := pkg.TypesSizes() +- +- // size (types and fields) +- if v, ok := obj.(*types.Var); ok && v.IsField() || is[*types.TypeName](obj) { +- // If the field's type has free type parameters, +- // its size cannot be computed. +- if !free.Has(obj.Type()) { +- size = sizes.Sizeof(obj.Type()) +- } +- +- // wasted space (struct types) +- if tStruct, ok := obj.Type().Underlying().(*types.Struct); ok && is[*types.TypeName](obj) && size > 0 { +- var fields []*types.Var +- for i := 0; i < tStruct.NumFields(); i++ { +- fields = append(fields, tStruct.Field(i)) +- } +- if len(fields) > 0 { +- // Sort into descending (most compact) order +- // and recompute size of entire struct. +- sort.Slice(fields, func(i, j int) bool { +- return sizes.Sizeof(fields[i].Type()) > +- sizes.Sizeof(fields[j].Type()) +- }) +- offsets := sizes.Offsetsof(fields) +- compactSize := offsets[len(offsets)-1] + sizes.Sizeof(fields[len(fields)-1].Type()) +- wasted = 100 * (size - compactSize) / size +- } +- } - } -- return &UnmarshalError{"unmarshal failed to match one of [TypeDefinitionOptions TypeDefinitionRegistrationOptions bool]"} --} - --// from line 8613 --func (t Or_ServerCapabilities_typeHierarchyProvider) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case TypeHierarchyOptions: -- return json.Marshal(x) -- case TypeHierarchyRegistrationOptions: -- return json.Marshal(x) -- case bool: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +- // offset (fields) +- if v, ok := obj.(*types.Var); ok && v.IsField() { +- // Find enclosing struct type. +- var tStruct *types.Struct +- for _, n := range path { +- if n, ok := n.(*ast.StructType); ok { +- tStruct = pkg.TypesInfo().TypeOf(n).(*types.Struct) +- break +- } +- } +- if tStruct != nil { +- var fields []*types.Var +- for i := 0; i < tStruct.NumFields(); i++ { +- f := tStruct.Field(i) +- // If any preceding field's type has free type parameters, +- // its offset cannot be computed. +- if free.Has(f.Type()) { +- break +- } +- fields = append(fields, f) +- if f == v { +- offsets := sizes.Offsetsof(fields) +- offset = offsets[len(offsets)-1] +- break +- } +- } +- } - } -- return nil, fmt.Errorf("type %T not one of [TypeHierarchyOptions TypeHierarchyRegistrationOptions bool]", t) +- +- return -} +diff -urN a/gopls/internal/golang/identifier.go b/gopls/internal/golang/identifier.go +--- a/gopls/internal/golang/identifier.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/identifier.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,183 +0,0 @@ +-// Copyright 2018 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func (t *Or_ServerCapabilities_typeHierarchyProvider) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 TypeHierarchyOptions -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 TypeHierarchyRegistrationOptions -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil -- } -- var h2 bool -- if err := json.Unmarshal(x, &h2); err == nil { -- t.Value = h2 -- return nil -- } -- return &UnmarshalError{"unmarshal failed to match one of [TypeHierarchyOptions TypeHierarchyRegistrationOptions bool]"} +-package golang +- +-import ( +- "errors" +- "go/ast" +- "go/types" +- +- "golang.org/x/tools/internal/aliases" +- "golang.org/x/tools/internal/typesinternal" +-) +- +-// ErrNoIdentFound is error returned when no identifier is found at a particular position +-var ErrNoIdentFound = errors.New("no identifier found") +- +-// inferredSignature determines the resolved non-generic signature for an +-// identifier in an instantiation expression. +-// +-// If no such signature exists, it returns nil. +-func inferredSignature(info *types.Info, id *ast.Ident) *types.Signature { +- inst := info.Instances[id] +- sig, _ := aliases.Unalias(inst.Type).(*types.Signature) +- return sig -} - --// from line 8391 --func (t Or_ServerCapabilities_workspaceSymbolProvider) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case WorkspaceSymbolOptions: -- return json.Marshal(x) -- case bool: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +-// searchForEnclosing returns, given the AST path to a SelectorExpr, +-// the exported named type of the innermost implicit field selection. +-// +-// For example, given "new(A).d" where this is (due to embedding) a +-// shorthand for "new(A).b.c.d", it returns the named type of c, +-// if it is exported, otherwise the type of b, or A. +-func searchForEnclosing(info *types.Info, path []ast.Node) *types.TypeName { +- for _, n := range path { +- switch n := n.(type) { +- case *ast.SelectorExpr: +- if sel, ok := info.Selections[n]; ok { +- recv := typesinternal.Unpointer(sel.Recv()) +- +- // Keep track of the last exported type seen. +- var exported *types.TypeName +- if named, ok := aliases.Unalias(recv).(*types.Named); ok && named.Obj().Exported() { +- exported = named.Obj() +- } +- // We don't want the last element, as that's the field or +- // method itself. +- for _, index := range sel.Index()[:len(sel.Index())-1] { +- if r, ok := recv.Underlying().(*types.Struct); ok { +- recv = typesinternal.Unpointer(r.Field(index).Type()) +- if named, ok := aliases.Unalias(recv).(*types.Named); ok && named.Obj().Exported() { +- exported = named.Obj() +- } +- } +- } +- return exported +- } +- } - } -- return nil, fmt.Errorf("type %T not one of [WorkspaceSymbolOptions bool]", t) +- return nil -} - --func (t *Or_ServerCapabilities_workspaceSymbolProvider) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 WorkspaceSymbolOptions -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil -- } -- var h1 bool -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 +-// typeToObject returns the relevant type name for the given type, after +-// unwrapping pointers, arrays, slices, channels, and function signatures with +-// a single non-error result, and ignoring built-in named types. +-func typeToObject(typ types.Type) *types.TypeName { +- switch typ := typ.(type) { +- case *aliases.Alias: +- return typ.Obj() +- case *types.Named: +- // TODO(rfindley): this should use typeparams.NamedTypeOrigin. +- return typ.Obj() +- case *types.Pointer: +- return typeToObject(typ.Elem()) +- case *types.Array: +- return typeToObject(typ.Elem()) +- case *types.Slice: +- return typeToObject(typ.Elem()) +- case *types.Chan: +- return typeToObject(typ.Elem()) +- case *types.Signature: +- // Try to find a return value of a named type. If there's only one +- // such value, jump to its type definition. +- var res *types.TypeName +- +- results := typ.Results() +- for i := 0; i < results.Len(); i++ { +- obj := typeToObject(results.At(i).Type()) +- if obj == nil || hasErrorType(obj) { +- // Skip builtins. TODO(rfindley): should comparable be handled here as well? +- continue +- } +- if res != nil { +- // The function/method must have only one return value of a named type. +- return nil +- } +- +- res = obj +- } +- return res +- default: - return nil - } -- return &UnmarshalError{"unmarshal failed to match one of [WorkspaceSymbolOptions bool]"} -} - --// from line 9159 --func (t Or_SignatureInformation_documentation) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case MarkupContent: -- return json.Marshal(x) -- case string: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil -- } -- return nil, fmt.Errorf("type %T not one of [MarkupContent string]", t) +-func hasErrorType(obj types.Object) bool { +- return types.IsInterface(obj.Type()) && obj.Pkg() == nil && obj.Name() == "error" -} - --func (t *Or_SignatureInformation_documentation) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil +-// typeSwitchImplicits returns all the implicit type switch objects that +-// correspond to the leaf *ast.Ident. It also returns the original type +-// associated with the identifier (outside of a case clause). +-func typeSwitchImplicits(info *types.Info, path []ast.Node) ([]types.Object, types.Type) { +- ident, _ := path[0].(*ast.Ident) +- if ident == nil { +- return nil, nil - } -- var h0 MarkupContent -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil +- +- var ( +- ts *ast.TypeSwitchStmt +- assign *ast.AssignStmt +- cc *ast.CaseClause +- obj = info.ObjectOf(ident) +- ) +- +- // Walk our ancestors to determine if our leaf ident refers to a +- // type switch variable, e.g. the "a" from "switch a := b.(type)". +-Outer: +- for i := 1; i < len(path); i++ { +- switch n := path[i].(type) { +- case *ast.AssignStmt: +- // Check if ident is the "a" in "a := foo.(type)". The "a" in +- // this case has no types.Object, so check for ident equality. +- if len(n.Lhs) == 1 && n.Lhs[0] == ident { +- assign = n +- } +- case *ast.CaseClause: +- // Check if ident is a use of "a" within a case clause. Each +- // case clause implicitly maps "a" to a different types.Object, +- // so check if ident's object is the case clause's implicit +- // object. +- if obj != nil && info.Implicits[n] == obj { +- cc = n +- } +- case *ast.TypeSwitchStmt: +- // Look for the type switch that owns our previously found +- // *ast.AssignStmt or *ast.CaseClause. +- if n.Assign == assign { +- ts = n +- break Outer +- } +- +- for _, stmt := range n.Body.List { +- if stmt == cc { +- ts = n +- break Outer +- } +- } +- } - } -- var h1 string -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +- if ts == nil { +- return nil, nil - } -- return &UnmarshalError{"unmarshal failed to match one of [MarkupContent string]"} --} -- --// from line 6928 --func (t Or_TextDocumentEdit_edits_Elem) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case AnnotatedTextEdit: -- return json.Marshal(x) -- case TextEdit: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +- // Our leaf ident refers to a type switch variable. Fan out to the +- // type switch's implicit case clause objects. +- var objs []types.Object +- for _, cc := range ts.Body.List { +- if ccObj := info.Implicits[cc]; ccObj != nil { +- objs = append(objs, ccObj) +- } - } -- return nil, fmt.Errorf("type %T not one of [AnnotatedTextEdit TextEdit]", t) +- // The right-hand side of a type switch should only have one +- // element, and we need to track its type in order to generate +- // hover information for implicit type switch variables. +- var typ types.Type +- if assign, ok := ts.Assign.(*ast.AssignStmt); ok && len(assign.Rhs) == 1 { +- if rhs := assign.Rhs[0].(*ast.TypeAssertExpr); ok { +- typ = info.TypeOf(rhs.X) +- } +- } +- return objs, typ -} +diff -urN a/gopls/internal/golang/identifier_test.go b/gopls/internal/golang/identifier_test.go +--- a/gopls/internal/golang/identifier_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/identifier_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,107 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func (t *Or_TextDocumentEdit_edits_Elem) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 AnnotatedTextEdit -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil +-package golang +- +-import ( +- "bytes" +- "go/ast" +- "go/parser" +- "go/token" +- "go/types" +- "testing" +- +- "golang.org/x/tools/internal/versions" +-) +- +-func TestSearchForEnclosing(t *testing.T) { +- tests := []struct { +- desc string +- // For convenience, consider the first occurrence of the identifier "X" in +- // src. +- src string +- // By convention, "" means no type found. +- wantTypeName string +- }{ +- { +- // TODO(rFindley): is this correct, or do we want to resolve I2 here? +- desc: "embedded interface in interface", +- src: `package a; var y = i1.X; type i1 interface {I2}; type I2 interface{X()}`, +- wantTypeName: "", +- }, +- { +- desc: "embedded interface in struct", +- src: `package a; var y = t.X; type t struct {I}; type I interface{X()}`, +- wantTypeName: "I", +- }, +- { +- desc: "double embedding", +- src: `package a; var y = t1.X; type t1 struct {t2}; type t2 struct {I}; type I interface{X()}`, +- wantTypeName: "I", +- }, - } -- var h1 TextEdit -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +- +- for _, test := range tests { +- test := test +- t.Run(test.desc, func(t *testing.T) { +- fset := token.NewFileSet() +- file, err := parser.ParseFile(fset, "a.go", test.src, parser.AllErrors) +- if err != nil { +- t.Fatal(err) +- } +- column := 1 + bytes.IndexRune([]byte(test.src), 'X') +- pos := posAt(1, column, fset, "a.go") +- path := pathEnclosingObjNode(file, pos) +- if path == nil { +- t.Fatalf("no ident found at (1, %d)", column) +- } +- info := newInfo() +- if _, err = (*types.Config)(nil).Check("p", fset, []*ast.File{file}, info); err != nil { +- t.Fatal(err) +- } +- obj := searchForEnclosing(info, path) +- if obj == nil { +- if test.wantTypeName != "" { +- t.Errorf("searchForEnclosing(...) = , want %q", test.wantTypeName) +- } +- return +- } +- if got := obj.Name(); got != test.wantTypeName { +- t.Errorf("searchForEnclosing(...) = %q, want %q", got, test.wantTypeName) +- } +- }) - } -- return &UnmarshalError{"unmarshal failed to match one of [AnnotatedTextEdit TextEdit]"} -} - --// from line 10131 --func (t Or_TextDocumentSyncOptions_save) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case SaveOptions: -- return json.Marshal(x) -- case bool: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +-// posAt returns the token.Pos corresponding to the 1-based (line, column) +-// coordinates in the file fname of fset. +-func posAt(line, column int, fset *token.FileSet, fname string) token.Pos { +- var tok *token.File +- fset.Iterate(func(tf *token.File) bool { +- if tf.Name() == fname { +- tok = tf +- return false +- } +- return true +- }) +- if tok == nil { +- return token.NoPos - } -- return nil, fmt.Errorf("type %T not one of [SaveOptions bool]", t) +- start := tok.LineStart(line) +- return start + token.Pos(column-1) -} - --func (t *Or_TextDocumentSyncOptions_save) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil +-// newInfo returns a types.Info with all maps populated. +-func newInfo() *types.Info { +- info := &types.Info{ +- Types: make(map[ast.Expr]types.TypeAndValue), +- Defs: make(map[*ast.Ident]types.Object), +- Uses: make(map[*ast.Ident]types.Object), +- Implicits: make(map[ast.Node]types.Object), +- Selections: make(map[*ast.SelectorExpr]*types.Selection), +- Scopes: make(map[ast.Node]*types.Scope), - } -- var h0 SaveOptions -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil +- versions.InitFileVersions(info) +- return info +-} +diff -urN a/gopls/internal/golang/implementation.go b/gopls/internal/golang/implementation.go +--- a/gopls/internal/golang/implementation.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/implementation.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,497 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package golang +- +-import ( +- "context" +- "errors" +- "fmt" +- "go/ast" +- "go/token" +- "go/types" +- "reflect" +- "sort" +- "strings" +- "sync" +- +- "golang.org/x/sync/errgroup" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/cache/methodsets" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/internal/event" +-) +- +-// This file defines the new implementation of the 'implementation' +-// operator that does not require type-checker data structures for an +-// unbounded number of packages. +-// +-// TODO(adonovan): +-// - Audit to ensure robustness in face of type errors. +-// - Eliminate false positives due to 'tricky' cases of the global algorithm. +-// - Ensure we have test coverage of: +-// type aliases +-// nil, PkgName, Builtin (all errors) +-// any (empty result) +-// method of unnamed interface type (e.g. var x interface { f() }) +-// (the global algorithm may find implementations of this type +-// but will not include it in the index.) +- +-// Implementation returns a new sorted array of locations of +-// declarations of types that implement (or are implemented by) the +-// type referred to at the given position. +-// +-// If the position denotes a method, the computation is applied to its +-// receiver type and then its corresponding methods are returned. +-func Implementation(ctx context.Context, snapshot *cache.Snapshot, f file.Handle, pp protocol.Position) ([]protocol.Location, error) { +- ctx, done := event.Start(ctx, "golang.Implementation") +- defer done() +- +- locs, err := implementations(ctx, snapshot, f, pp) +- if err != nil { +- return nil, err - } -- var h1 bool -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +- +- // Sort and de-duplicate locations. +- sort.Slice(locs, func(i, j int) bool { +- return protocol.CompareLocation(locs[i], locs[j]) < 0 +- }) +- out := locs[:0] +- for _, loc := range locs { +- if len(out) == 0 || out[len(out)-1] != loc { +- out = append(out, loc) +- } - } -- return &UnmarshalError{"unmarshal failed to match one of [SaveOptions bool]"} +- locs = out +- +- return locs, nil -} - --// from line 14401 --func (t Or_WorkspaceDocumentDiagnosticReport) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case WorkspaceFullDocumentDiagnosticReport: -- return json.Marshal(x) -- case WorkspaceUnchangedDocumentDiagnosticReport: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +-func implementations(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp protocol.Position) ([]protocol.Location, error) { +- obj, pkg, err := implementsObj(ctx, snapshot, fh.URI(), pp) +- if err != nil { +- return nil, err - } -- return nil, fmt.Errorf("type %T not one of [WorkspaceFullDocumentDiagnosticReport WorkspaceUnchangedDocumentDiagnosticReport]", t) --} - --func (t *Or_WorkspaceDocumentDiagnosticReport) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil +- var localPkgs []*cache.Package +- if obj.Pos().IsValid() { // no local package for error or error.Error +- declPosn := safetoken.StartPosition(pkg.FileSet(), obj.Pos()) +- // Type-check the declaring package (incl. variants) for use +- // by the "local" search, which uses type information to +- // enumerate all types within the package that satisfy the +- // query type, even those defined local to a function. +- declURI := protocol.URIFromPath(declPosn.Filename) +- declMPs, err := snapshot.MetadataForFile(ctx, declURI) +- if err != nil { +- return nil, err +- } +- metadata.RemoveIntermediateTestVariants(&declMPs) +- if len(declMPs) == 0 { +- return nil, fmt.Errorf("no packages for file %s", declURI) +- } +- ids := make([]PackageID, len(declMPs)) +- for i, mp := range declMPs { +- ids[i] = mp.ID +- } +- localPkgs, err = snapshot.TypeCheck(ctx, ids...) +- if err != nil { +- return nil, err +- } - } -- var h0 WorkspaceFullDocumentDiagnosticReport -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil +- +- // Is the selected identifier a type name or method? +- // (For methods, report the corresponding method names.) +- var queryType types.Type +- var queryMethodID string +- switch obj := obj.(type) { +- case *types.TypeName: +- queryType = obj.Type() +- case *types.Func: +- // For methods, use the receiver type, which may be anonymous. +- if recv := obj.Type().(*types.Signature).Recv(); recv != nil { +- queryType = recv.Type() +- queryMethodID = obj.Id() +- } - } -- var h1 WorkspaceUnchangedDocumentDiagnosticReport -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +- if queryType == nil { +- return nil, bug.Errorf("%s is not a type or method", obj.Name()) // should have been handled by implementsObj - } -- return &UnmarshalError{"unmarshal failed to match one of [WorkspaceFullDocumentDiagnosticReport WorkspaceUnchangedDocumentDiagnosticReport]"} --} - --// from line 3292 --func (t Or_WorkspaceEdit_documentChanges_Elem) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case CreateFile: -- return json.Marshal(x) -- case DeleteFile: -- return json.Marshal(x) -- case RenameFile: -- return json.Marshal(x) -- case TextDocumentEdit: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +- // Compute the method-set fingerprint used as a key to the global search. +- key, hasMethods := methodsets.KeyOf(queryType) +- if !hasMethods { +- // A type with no methods yields an empty result. +- // (No point reporting that every type satisfies 'any'.) +- return nil, nil - } -- return nil, fmt.Errorf("type %T not one of [CreateFile DeleteFile RenameFile TextDocumentEdit]", t) --} - --func (t *Or_WorkspaceEdit_documentChanges_Elem) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 CreateFile -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil +- // The global search needs to look at every package in the +- // forward transitive closure of the workspace; see package +- // ./methodsets. +- // +- // For now we do all the type checking before beginning the search. +- // TODO(adonovan): opt: search in parallel topological order +- // so that we can overlap index lookup with typechecking. +- // I suspect a number of algorithms on the result of TypeCheck could +- // be optimized by being applied as soon as each package is available. +- globalMetas, err := snapshot.AllMetadata(ctx) +- if err != nil { +- return nil, err - } -- var h1 DeleteFile -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +- metadata.RemoveIntermediateTestVariants(&globalMetas) +- globalIDs := make([]PackageID, 0, len(globalMetas)) +- +- var pkgPath PackagePath +- if obj.Pkg() != nil { // nil for error +- pkgPath = PackagePath(obj.Pkg().Path()) - } -- var h2 RenameFile -- if err := json.Unmarshal(x, &h2); err == nil { -- t.Value = h2 -- return nil +- for _, mp := range globalMetas { +- if mp.PkgPath == pkgPath { +- continue // declaring package is handled by local implementation +- } +- globalIDs = append(globalIDs, mp.ID) - } -- var h3 TextDocumentEdit -- if err := json.Unmarshal(x, &h3); err == nil { -- t.Value = h3 -- return nil +- indexes, err := snapshot.MethodSets(ctx, globalIDs...) +- if err != nil { +- return nil, fmt.Errorf("querying method sets: %v", err) - } -- return &UnmarshalError{"unmarshal failed to match one of [CreateFile DeleteFile RenameFile TextDocumentEdit]"} --} - --// from line 248 --func (t Or_textDocument_declaration) MarshalJSON() ([]byte, error) { -- switch x := t.Value.(type) { -- case Declaration: -- return json.Marshal(x) -- case []DeclarationLink: -- return json.Marshal(x) -- case nil: -- return []byte("null"), nil +- // Search local and global packages in parallel. +- var ( +- group errgroup.Group +- locsMu sync.Mutex +- locs []protocol.Location +- ) +- // local search +- for _, localPkg := range localPkgs { +- localPkg := localPkg +- group.Go(func() error { +- localLocs, err := localImplementations(ctx, snapshot, localPkg, queryType, queryMethodID) +- if err != nil { +- return err +- } +- locsMu.Lock() +- locs = append(locs, localLocs...) +- locsMu.Unlock() +- return nil +- }) - } -- return nil, fmt.Errorf("type %T not one of [Declaration []DeclarationLink]", t) +- // global search +- for _, index := range indexes { +- index := index +- group.Go(func() error { +- for _, res := range index.Search(key, queryMethodID) { +- loc := res.Location +- // Map offsets to protocol.Locations in parallel (may involve I/O). +- group.Go(func() error { +- ploc, err := offsetToLocation(ctx, snapshot, loc.Filename, loc.Start, loc.End) +- if err != nil { +- return err +- } +- locsMu.Lock() +- locs = append(locs, ploc) +- locsMu.Unlock() +- return nil +- }) +- } +- return nil +- }) +- } +- if err := group.Wait(); err != nil { +- return nil, err +- } +- +- return locs, nil -} - --func (t *Or_textDocument_declaration) UnmarshalJSON(x []byte) error { -- if string(x) == "null" { -- t.Value = nil -- return nil -- } -- var h0 Declaration -- if err := json.Unmarshal(x, &h0); err == nil { -- t.Value = h0 -- return nil +-// offsetToLocation converts an offset-based position to a protocol.Location, +-// which requires reading the file. +-func offsetToLocation(ctx context.Context, snapshot *cache.Snapshot, filename string, start, end int) (protocol.Location, error) { +- uri := protocol.URIFromPath(filename) +- fh, err := snapshot.ReadFile(ctx, uri) +- if err != nil { +- return protocol.Location{}, err // cancelled, perhaps - } -- var h1 []DeclarationLink -- if err := json.Unmarshal(x, &h1); err == nil { -- t.Value = h1 -- return nil +- content, err := fh.Content() +- if err != nil { +- return protocol.Location{}, err // nonexistent or deleted ("can't happen") - } -- return &UnmarshalError{"unmarshal failed to match one of [Declaration []DeclarationLink]"} +- m := protocol.NewMapper(uri, content) +- return m.OffsetLocation(start, end) -} -diff -urN a/gopls/internal/lsp/protocol/tsprotocol.go b/gopls/internal/lsp/protocol/tsprotocol.go ---- a/gopls/internal/lsp/protocol/tsprotocol.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/protocol/tsprotocol.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,5642 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --// Code generated for LSP. DO NOT EDIT. - --package protocol +-// implementsObj returns the object to query for implementations, which is a +-// type name or method. +-// +-// The returned Package is the narrowest package containing ppos, which is the +-// package using the resulting obj but not necessarily the declaring package. +-func implementsObj(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI, ppos protocol.Position) (types.Object, *cache.Package, error) { +- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, uri) +- if err != nil { +- return nil, nil, err +- } +- pos, err := pgf.PositionPos(ppos) +- if err != nil { +- return nil, nil, err +- } - --// Code generated from protocol/metaModel.json at ref release/protocol/3.17.4-next.2 (hash 184c8a7f010d335582f24337fe182baa6f2fccdd). --// https://github.com/microsoft/vscode-languageserver-node/blob/release/protocol/3.17.4-next.2/protocol/metaModel.json --// LSP metaData.version = 3.17.0. +- // This function inherits the limitation of its predecessor in +- // requiring the selection to be an identifier (of a type or +- // method). But there's no fundamental reason why one could +- // not pose this query about any selected piece of syntax that +- // has a type and thus a method set. +- // (If LSP was more thorough about passing text selections as +- // intervals to queries, you could ask about the method set of a +- // subexpression such as x.f().) - --import "encoding/json" +- // TODO(adonovan): simplify: use objectsAt? +- path := pathEnclosingObjNode(pgf.File, pos) +- if path == nil { +- return nil, nil, ErrNoIdentFound +- } +- id, ok := path[0].(*ast.Ident) +- if !ok { +- return nil, nil, ErrNoIdentFound +- } - --// A special text edit with an additional change annotation. --// --// @since 3.16.0. --type AnnotatedTextEdit struct { -- // The actual identifier of the change annotation -- AnnotationID ChangeAnnotationIdentifier `json:"annotationId"` -- TextEdit --} +- // Is the object a type or method? Reject other kinds. +- obj := pkg.TypesInfo().Uses[id] +- if obj == nil { +- // Check uses first (unlike ObjectOf) so that T in +- // struct{T} is treated as a reference to a type, +- // not a declaration of a field. +- obj = pkg.TypesInfo().Defs[id] +- } +- switch obj := obj.(type) { +- case *types.TypeName: +- // ok +- case *types.Func: +- if obj.Type().(*types.Signature).Recv() == nil { +- return nil, nil, fmt.Errorf("%s is a function, not a method", id.Name) +- } +- case nil: +- return nil, nil, fmt.Errorf("%s denotes unknown object", id.Name) +- default: +- // e.g. *types.Var -> "var". +- kind := strings.ToLower(strings.TrimPrefix(reflect.TypeOf(obj).String(), "*types.")) +- return nil, nil, fmt.Errorf("%s is a %s, not a type", id.Name, kind) +- } - --// The parameters passed via an apply workspace edit request. --type ApplyWorkspaceEditParams struct { -- // An optional label of the workspace edit. This label is -- // presented in the user interface for example on an undo -- // stack to undo the workspace edit. -- Label string `json:"label,omitempty"` -- // The edits to apply. -- Edit WorkspaceEdit `json:"edit"` +- return obj, pkg, nil -} - --// The result returned from the apply workspace edit request. +-// localImplementations searches within pkg for declarations of all +-// types that are assignable to/from the query type, and returns a new +-// unordered array of their locations. -// --// @since 3.17 renamed from ApplyWorkspaceEditResponse --type ApplyWorkspaceEditResult struct { -- // Indicates whether the edit was applied or not. -- Applied bool `json:"applied"` -- // An optional textual description for why the edit was not applied. -- // This may be used by the server for diagnostic logging or to provide -- // a suitable error for a request that triggered the edit. -- FailureReason string `json:"failureReason,omitempty"` -- // Depending on the client's failure handling strategy `failedChange` might -- // contain the index of the change that failed. This property is only available -- // if the client signals a `failureHandlingStrategy` in its client capabilities. -- FailedChange uint32 `json:"failedChange,omitempty"` --} +-// If methodID is non-empty, the function instead returns the location +-// of each type's method (if any) of that ID. +-// +-// ("Local" refers to the search within the same package, but this +-// function's results may include type declarations that are local to +-// a function body. The global search index excludes such types +-// because reliably naming such types is hard.) +-func localImplementations(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, queryType types.Type, methodID string) ([]protocol.Location, error) { +- queryType = methodsets.EnsurePointer(queryType) - --// A base for all symbol information. --type BaseSymbolInformation struct { -- // The name of this symbol. -- Name string `json:"name"` -- // The kind of this symbol. -- Kind SymbolKind `json:"kind"` -- // Tags for this symbol. -- // -- // @since 3.16.0 -- Tags []SymbolTag `json:"tags,omitempty"` -- // The name of the symbol containing this symbol. This information is for -- // user interface purposes (e.g. to render a qualifier in the user interface -- // if necessary). It can't be used to re-infer a hierarchy for the document -- // symbols. -- ContainerName string `json:"containerName,omitempty"` --} +- // Scan through all type declarations in the syntax. +- var locs []protocol.Location +- var methodLocs []methodsets.Location +- for _, pgf := range pkg.CompiledGoFiles() { +- ast.Inspect(pgf.File, func(n ast.Node) bool { +- spec, ok := n.(*ast.TypeSpec) +- if !ok { +- return true // not a type declaration +- } +- def := pkg.TypesInfo().Defs[spec.Name] +- if def == nil { +- return true // "can't happen" for types +- } +- if def.(*types.TypeName).IsAlias() { +- return true // skip type aliases to avoid duplicate reporting +- } +- candidateType := methodsets.EnsurePointer(def.Type()) - --// @since 3.16.0 --type CallHierarchyClientCapabilities struct { -- // Whether implementation supports dynamic registration. If this is set to `true` -- // the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` -- // return value for the corresponding server capability as well. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` --} +- // The historical behavior enshrined by this +- // function rejects cases where both are +- // (nontrivial) interface types? +- // That seems like useful information. +- // TODO(adonovan): UX: report I/I pairs too? +- // The same question appears in the global algorithm (methodsets). +- if !concreteImplementsIntf(candidateType, queryType) { +- return true // not assignable +- } - --// Represents an incoming call, e.g. a caller of a method or constructor. --// --// @since 3.16.0 --type CallHierarchyIncomingCall struct { -- // The item that makes the call. -- From CallHierarchyItem `json:"from"` -- // The ranges at which the calls appear. This is relative to the caller -- // denoted by {@link CallHierarchyIncomingCall.from `this.from`}. -- FromRanges []Range `json:"fromRanges"` --} +- // Ignore types with empty method sets. +- // (No point reporting that every type satisfies 'any'.) +- mset := types.NewMethodSet(candidateType) +- if mset.Len() == 0 { +- return true +- } - --// The parameter of a `callHierarchy/incomingCalls` request. --// --// @since 3.16.0 --type CallHierarchyIncomingCallsParams struct { -- Item CallHierarchyItem `json:"item"` -- WorkDoneProgressParams -- PartialResultParams --} +- if methodID == "" { +- // Found matching type. +- locs = append(locs, mustLocation(pgf, spec.Name)) +- return true +- } - --// Represents programming constructs like functions or constructors in the context --// of call hierarchy. --// --// @since 3.16.0 --type CallHierarchyItem struct { -- // The name of this item. -- Name string `json:"name"` -- // The kind of this item. -- Kind SymbolKind `json:"kind"` -- // Tags for this item. -- Tags []SymbolTag `json:"tags,omitempty"` -- // More detail for this item, e.g. the signature of a function. -- Detail string `json:"detail,omitempty"` -- // The resource identifier of this item. -- URI DocumentURI `json:"uri"` -- // The range enclosing this symbol not including leading/trailing whitespace but everything else, e.g. comments and code. -- Range Range `json:"range"` -- // The range that should be selected and revealed when this symbol is being picked, e.g. the name of a function. -- // Must be contained by the {@link CallHierarchyItem.range `range`}. -- SelectionRange Range `json:"selectionRange"` -- // A data entry field that is preserved between a call hierarchy prepare and -- // incoming calls or outgoing calls requests. -- Data interface{} `json:"data,omitempty"` --} +- // Find corresponding method. +- // +- // We can't use LookupFieldOrMethod because it requires +- // the methodID's types.Package, which we don't know. +- // We could recursively search pkg.Imports for it, +- // but it's easier to walk the method set. +- for i := 0; i < mset.Len(); i++ { +- method := mset.At(i).Obj() +- if method.Id() == methodID { +- posn := safetoken.StartPosition(pkg.FileSet(), method.Pos()) +- methodLocs = append(methodLocs, methodsets.Location{ +- Filename: posn.Filename, +- Start: posn.Offset, +- End: posn.Offset + len(method.Name()), +- }) +- break +- } +- } +- return true +- }) +- } - --// Call hierarchy options used during static registration. --// --// @since 3.16.0 --type CallHierarchyOptions struct { -- WorkDoneProgressOptions --} +- // Finally convert method positions to protocol form by reading the files. +- for _, mloc := range methodLocs { +- loc, err := offsetToLocation(ctx, snapshot, mloc.Filename, mloc.Start, mloc.End) +- if err != nil { +- return nil, err +- } +- locs = append(locs, loc) +- } - --// Represents an outgoing call, e.g. calling a getter from a method or a method from a constructor etc. --// --// @since 3.16.0 --type CallHierarchyOutgoingCall struct { -- // The item that is called. -- To CallHierarchyItem `json:"to"` -- // The range at which this item is called. This is the range relative to the caller, e.g the item -- // passed to {@link CallHierarchyItemProvider.provideCallHierarchyOutgoingCalls `provideCallHierarchyOutgoingCalls`} -- // and not {@link CallHierarchyOutgoingCall.to `this.to`}. -- FromRanges []Range `json:"fromRanges"` --} +- // Special case: for types that satisfy error, report builtin.go (see #59527). +- if types.Implements(queryType, errorInterfaceType) { +- loc, err := errorLocation(ctx, snapshot) +- if err != nil { +- return nil, err +- } +- locs = append(locs, loc) +- } - --// The parameter of a `callHierarchy/outgoingCalls` request. --// --// @since 3.16.0 --type CallHierarchyOutgoingCallsParams struct { -- Item CallHierarchyItem `json:"item"` -- WorkDoneProgressParams -- PartialResultParams +- return locs, nil -} - --// The parameter of a `textDocument/prepareCallHierarchy` request. --// --// @since 3.16.0 --type CallHierarchyPrepareParams struct { -- TextDocumentPositionParams -- WorkDoneProgressParams --} +-var errorInterfaceType = types.Universe.Lookup("error").Type().Underlying().(*types.Interface) - --// Call hierarchy options used during static or dynamic registration. --// --// @since 3.16.0 --type CallHierarchyRegistrationOptions struct { -- TextDocumentRegistrationOptions -- CallHierarchyOptions -- StaticRegistrationOptions --} --type CancelParams struct { -- // The request id to cancel. -- ID interface{} `json:"id"` +-// errorLocation returns the location of the 'error' type in builtin.go. +-func errorLocation(ctx context.Context, snapshot *cache.Snapshot) (protocol.Location, error) { +- pgf, err := snapshot.BuiltinFile(ctx) +- if err != nil { +- return protocol.Location{}, err +- } +- for _, decl := range pgf.File.Decls { +- if decl, ok := decl.(*ast.GenDecl); ok { +- for _, spec := range decl.Specs { +- if spec, ok := spec.(*ast.TypeSpec); ok && spec.Name.Name == "error" { +- return pgf.NodeLocation(spec.Name) +- } +- } +- } +- } +- return protocol.Location{}, fmt.Errorf("built-in error type not found") -} - --// Additional information that describes document changes. --// --// @since 3.16.0 --type ChangeAnnotation struct { -- // A human-readable string describing the actual change. The string -- // is rendered prominent in the user interface. -- Label string `json:"label"` -- // A flag which indicates that user confirmation is needed -- // before applying the change. -- NeedsConfirmation bool `json:"needsConfirmation,omitempty"` -- // A human-readable string which is rendered less prominent in -- // the user interface. -- Description string `json:"description,omitempty"` --} +-// concreteImplementsIntf returns true if a is an interface type implemented by +-// concrete type b, or vice versa. +-func concreteImplementsIntf(a, b types.Type) bool { +- aIsIntf, bIsIntf := types.IsInterface(a), types.IsInterface(b) - --// An identifier to refer to a change annotation stored with a workspace edit. --type ChangeAnnotationIdentifier = string // (alias) line 14391 --// Defines the capabilities provided by the client. --type ClientCapabilities struct { -- // Workspace specific client capabilities. -- Workspace WorkspaceClientCapabilities `json:"workspace,omitempty"` -- // Text document specific client capabilities. -- TextDocument TextDocumentClientCapabilities `json:"textDocument,omitempty"` -- // Capabilities specific to the notebook document support. -- // -- // @since 3.17.0 -- NotebookDocument *NotebookDocumentClientCapabilities `json:"notebookDocument,omitempty"` -- // Window specific client capabilities. -- Window WindowClientCapabilities `json:"window,omitempty"` -- // General client capabilities. -- // -- // @since 3.16.0 -- General *GeneralClientCapabilities `json:"general,omitempty"` -- // Experimental client capabilities. -- Experimental interface{} `json:"experimental,omitempty"` --} +- // Make sure exactly one is an interface type. +- if aIsIntf == bIsIntf { +- return false +- } - --// A code action represents a change that can be performed in code, e.g. to fix a problem or --// to refactor code. --// --// A CodeAction must set either `edit` and/or a `command`. If both are supplied, the `edit` is applied first, then the `command` is executed. --type CodeAction struct { -- // A short, human-readable, title for this code action. -- Title string `json:"title"` -- // The kind of the code action. -- // -- // Used to filter code actions. -- Kind CodeActionKind `json:"kind,omitempty"` -- // The diagnostics that this code action resolves. -- Diagnostics []Diagnostic `json:"diagnostics,omitempty"` -- // Marks this as a preferred action. Preferred actions are used by the `auto fix` command and can be targeted -- // by keybindings. -- // -- // A quick fix should be marked preferred if it properly addresses the underlying error. -- // A refactoring should be marked preferred if it is the most reasonable choice of actions to take. -- // -- // @since 3.15.0 -- IsPreferred bool `json:"isPreferred,omitempty"` -- // Marks that the code action cannot currently be applied. -- // -- // Clients should follow the following guidelines regarding disabled code actions: -- // -- // - Disabled code actions are not shown in automatic [lightbulbs](https://code.visualstudio.com/docs/editor/editingevolved#_code-action) -- // code action menus. -- // -- // - Disabled actions are shown as faded out in the code action menu when the user requests a more specific type -- // of code action, such as refactorings. -- // -- // - If the user has a [keybinding](https://code.visualstudio.com/docs/editor/refactoring#_keybindings-for-code-actions) -- // that auto applies a code action and only disabled code actions are returned, the client should show the user an -- // error message with `reason` in the editor. -- // -- // @since 3.16.0 -- Disabled *PDisabledMsg_textDocument_codeAction `json:"disabled,omitempty"` -- // The workspace edit this code action performs. -- Edit *WorkspaceEdit `json:"edit,omitempty"` -- // A command this code action executes. If a code action -- // provides an edit and a command, first the edit is -- // executed and then the command. -- Command *Command `json:"command,omitempty"` -- // A data entry field that is preserved on a code action between -- // a `textDocument/codeAction` and a `codeAction/resolve` request. -- // -- // @since 3.16.0 -- Data interface{} `json:"data,omitempty"` --} +- // Rearrange if needed so "a" is the concrete type. +- if aIsIntf { +- a, b = b, a +- } - --// The Client Capabilities of a {@link CodeActionRequest}. --type CodeActionClientCapabilities struct { -- // Whether code action supports dynamic registration. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -- // The client support code action literals of type `CodeAction` as a valid -- // response of the `textDocument/codeAction` request. If the property is not -- // set the request can only return `Command` literals. -- // -- // @since 3.8.0 -- CodeActionLiteralSupport PCodeActionLiteralSupportPCodeAction `json:"codeActionLiteralSupport,omitempty"` -- // Whether code action supports the `isPreferred` property. -- // -- // @since 3.15.0 -- IsPreferredSupport bool `json:"isPreferredSupport,omitempty"` -- // Whether code action supports the `disabled` property. -- // -- // @since 3.16.0 -- DisabledSupport bool `json:"disabledSupport,omitempty"` -- // Whether code action supports the `data` property which is -- // preserved between a `textDocument/codeAction` and a -- // `codeAction/resolve` request. -- // -- // @since 3.16.0 -- DataSupport bool `json:"dataSupport,omitempty"` -- // Whether the client supports resolving additional code action -- // properties via a separate `codeAction/resolve` request. -- // -- // @since 3.16.0 -- ResolveSupport *PResolveSupportPCodeAction `json:"resolveSupport,omitempty"` -- // Whether the client honors the change annotations in -- // text edits and resource operations returned via the -- // `CodeAction#edit` property by for example presenting -- // the workspace edit in the user interface and asking -- // for confirmation. -- // -- // @since 3.16.0 -- HonorsChangeAnnotations bool `json:"honorsChangeAnnotations,omitempty"` +- // TODO(adonovan): this should really use GenericAssignableTo +- // to report (e.g.) "ArrayList[T] implements List[T]", but +- // GenericAssignableTo doesn't work correctly on pointers to +- // generic named types. Thus the legacy implementation and the +- // "local" part of implementations fail to report generics. +- // The global algorithm based on subsets does the right thing. +- return types.AssignableTo(a, b) -} - --// Contains additional diagnostic information about the context in which --// a {@link CodeActionProvider.provideCodeActions code action} is run. --type CodeActionContext struct { -- // An array of diagnostics known on the client side overlapping the range provided to the -- // `textDocument/codeAction` request. They are provided so that the server knows which -- // errors are currently presented to the user for the given range. There is no guarantee -- // that these accurately reflect the error state of the resource. The primary parameter -- // to compute code actions is the provided range. -- Diagnostics []Diagnostic `json:"diagnostics"` -- // Requested kind of actions to return. -- // -- // Actions not of this kind are filtered out by the client before being shown. So servers -- // can omit computing them. -- Only []CodeActionKind `json:"only,omitempty"` -- // The reason why code actions were requested. -- // -- // @since 3.17.0 -- TriggerKind *CodeActionTriggerKind `json:"triggerKind,omitempty"` --} +-var ( +- // TODO(adonovan): why do various RPC handlers related to +- // IncomingCalls return (nil, nil) on the protocol in response +- // to this error? That seems like a violation of the protocol. +- // Is it perhaps a workaround for VSCode behavior? +- errNoObjectFound = errors.New("no object found") +-) - --// A set of predefined code action kinds --type CodeActionKind string +-// pathEnclosingObjNode returns the AST path to the object-defining +-// node associated with pos. "Object-defining" means either an +-// *ast.Ident mapped directly to a types.Object or an ast.Node mapped +-// implicitly to a types.Object. +-func pathEnclosingObjNode(f *ast.File, pos token.Pos) []ast.Node { +- var ( +- path []ast.Node +- found bool +- ) - --// Provider options for a {@link CodeActionRequest}. --type CodeActionOptions struct { -- // CodeActionKinds that this server may return. -- // -- // The list of kinds may be generic, such as `CodeActionKind.Refactor`, or the server -- // may list out every specific kind they provide. -- CodeActionKinds []CodeActionKind `json:"codeActionKinds,omitempty"` -- // The server provides support to resolve additional -- // information for a code action. -- // -- // @since 3.16.0 -- ResolveProvider bool `json:"resolveProvider,omitempty"` -- WorkDoneProgressOptions --} +- ast.Inspect(f, func(n ast.Node) bool { +- if found { +- return false +- } - --// The parameters of a {@link CodeActionRequest}. --type CodeActionParams struct { -- // The document in which the command was invoked. -- TextDocument TextDocumentIdentifier `json:"textDocument"` -- // The range for which the command was invoked. -- Range Range `json:"range"` -- // Context carrying additional information. -- Context CodeActionContext `json:"context"` -- WorkDoneProgressParams -- PartialResultParams --} +- if n == nil { +- path = path[:len(path)-1] +- return false +- } - --// Registration options for a {@link CodeActionRequest}. --type CodeActionRegistrationOptions struct { -- TextDocumentRegistrationOptions -- CodeActionOptions --} +- path = append(path, n) - --// The reason why code actions were requested. --// --// @since 3.17.0 --type CodeActionTriggerKind uint32 +- switch n := n.(type) { +- case *ast.Ident: +- // Include the position directly after identifier. This handles +- // the common case where the cursor is right after the +- // identifier the user is currently typing. Previously we +- // handled this by calling astutil.PathEnclosingInterval twice, +- // once for "pos" and once for "pos-1". +- found = n.Pos() <= pos && pos <= n.End() +- case *ast.ImportSpec: +- if n.Path.Pos() <= pos && pos < n.Path.End() { +- found = true +- // If import spec has a name, add name to path even though +- // position isn't in the name. +- if n.Name != nil { +- path = append(path, n.Name) +- } +- } +- case *ast.StarExpr: +- // Follow star expressions to the inner identifier. +- if pos == n.Star { +- pos = n.X.Pos() +- } +- } - --// Structure to capture a description for an error code. --// --// @since 3.16.0 --type CodeDescription struct { -- // An URI to open with more information about the diagnostic error. -- Href URI `json:"href"` --} +- return !found +- }) - --// A code lens represents a {@link Command command} that should be shown along with --// source text, like the number of references, a way to run tests, etc. --// --// A code lens is _unresolved_ when no command is associated to it. For performance --// reasons the creation of a code lens and resolving should be done in two stages. --type CodeLens struct { -- // The range in which this code lens is valid. Should only span a single line. -- Range Range `json:"range"` -- // The command this code lens represents. -- Command *Command `json:"command,omitempty"` -- // A data entry field that is preserved on a code lens item between -- // a {@link CodeLensRequest} and a [CodeLensResolveRequest] -- // (#CodeLensResolveRequest) -- Data interface{} `json:"data,omitempty"` --} +- if len(path) == 0 { +- return nil +- } - --// The client capabilities of a {@link CodeLensRequest}. --type CodeLensClientCapabilities struct { -- // Whether code lens supports dynamic registration. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` --} +- // Reverse path so leaf is first element. +- for i := 0; i < len(path)/2; i++ { +- path[i], path[len(path)-1-i] = path[len(path)-1-i], path[i] +- } - --// Code Lens provider options of a {@link CodeLensRequest}. --type CodeLensOptions struct { -- // Code lens has a resolve provider as well. -- ResolveProvider bool `json:"resolveProvider,omitempty"` -- WorkDoneProgressOptions +- return path -} +diff -urN a/gopls/internal/golang/inlay_hint.go b/gopls/internal/golang/inlay_hint.go +--- a/gopls/internal/golang/inlay_hint.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/inlay_hint.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,396 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// The parameters of a {@link CodeLensRequest}. --type CodeLensParams struct { -- // The document to request code lens for. -- TextDocument TextDocumentIdentifier `json:"textDocument"` -- WorkDoneProgressParams -- PartialResultParams --} +-package golang - --// Registration options for a {@link CodeLensRequest}. --type CodeLensRegistrationOptions struct { -- TextDocumentRegistrationOptions -- CodeLensOptions --} +-import ( +- "context" +- "fmt" +- "go/ast" +- "go/constant" +- "go/token" +- "go/types" +- "strings" - --// @since 3.16.0 --type CodeLensWorkspaceClientCapabilities struct { -- // Whether the client implementation supports a refresh request sent from the -- // server to the client. -- // -- // Note that this event is global and will force the client to refresh all -- // code lenses currently shown. It should be used with absolute care and is -- // useful for situation where a server for example detect a project wide -- // change that requires such a calculation. -- RefreshSupport bool `json:"refreshSupport,omitempty"` --} +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/typesutil" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/typeparams" +- "golang.org/x/tools/internal/typesinternal" +-) - --// Represents a color in RGBA space. --type Color struct { -- // The red component of this color in the range [0-1]. -- Red float64 `json:"red"` -- // The green component of this color in the range [0-1]. -- Green float64 `json:"green"` -- // The blue component of this color in the range [0-1]. -- Blue float64 `json:"blue"` -- // The alpha component of this color in the range [0-1]. -- Alpha float64 `json:"alpha"` --} +-const ( +- maxLabelLength = 28 +-) - --// Represents a color range from a document. --type ColorInformation struct { -- // The range in the document where this color appears. -- Range Range `json:"range"` -- // The actual color value for this color range. -- Color Color `json:"color"` --} --type ColorPresentation struct { -- // The label of this color presentation. It will be shown on the color -- // picker header. By default this is also the text that is inserted when selecting -- // this color presentation. -- Label string `json:"label"` -- // An {@link TextEdit edit} which is applied to a document when selecting -- // this presentation for the color. When `falsy` the {@link ColorPresentation.label label} -- // is used. -- TextEdit *TextEdit `json:"textEdit,omitempty"` -- // An optional array of additional {@link TextEdit text edits} that are applied when -- // selecting this color presentation. Edits must not overlap with the main {@link ColorPresentation.textEdit edit} nor with themselves. -- AdditionalTextEdits []TextEdit `json:"additionalTextEdits,omitempty"` --} +-type InlayHintFunc func(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint - --// Parameters for a {@link ColorPresentationRequest}. --type ColorPresentationParams struct { -- // The text document. -- TextDocument TextDocumentIdentifier `json:"textDocument"` -- // The color to request presentations for. -- Color Color `json:"color"` -- // The range where the color would be inserted. Serves as a context. -- Range Range `json:"range"` -- WorkDoneProgressParams -- PartialResultParams +-type Hint struct { +- Name string +- Doc string +- Run InlayHintFunc -} - --// Represents a reference to a command. Provides a title which --// will be used to represent a command in the UI and, optionally, --// an array of arguments which will be passed to the command handler --// function when invoked. --type Command struct { -- // Title of the command, like `save`. -- Title string `json:"title"` -- // The identifier of the actual command handler. -- Command string `json:"command"` -- // Arguments that the command handler should be -- // invoked with. -- Arguments []json.RawMessage `json:"arguments,omitempty"` --} +-const ( +- ParameterNames = "parameterNames" +- AssignVariableTypes = "assignVariableTypes" +- ConstantValues = "constantValues" +- RangeVariableTypes = "rangeVariableTypes" +- CompositeLiteralTypes = "compositeLiteralTypes" +- CompositeLiteralFieldNames = "compositeLiteralFields" +- FunctionTypeParameters = "functionTypeParameters" +-) - --// Completion client capabilities --type CompletionClientCapabilities struct { -- // Whether completion supports dynamic registration. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -- // The client supports the following `CompletionItem` specific -- // capabilities. -- CompletionItem PCompletionItemPCompletion `json:"completionItem,omitempty"` -- CompletionItemKind *PCompletionItemKindPCompletion `json:"completionItemKind,omitempty"` -- // Defines how the client handles whitespace and indentation -- // when accepting a completion item that uses multi line -- // text in either `insertText` or `textEdit`. -- // -- // @since 3.17.0 -- InsertTextMode InsertTextMode `json:"insertTextMode,omitempty"` -- // The client supports to send additional context information for a -- // `textDocument/completion` request. -- ContextSupport bool `json:"contextSupport,omitempty"` -- // The client supports the following `CompletionList` specific -- // capabilities. -- // -- // @since 3.17.0 -- CompletionList *PCompletionListPCompletion `json:"completionList,omitempty"` +-var AllInlayHints = map[string]*Hint{ +- AssignVariableTypes: { +- Name: AssignVariableTypes, +- Doc: "Enable/disable inlay hints for variable types in assign statements:\n```go\n\ti/* int*/, j/* int*/ := 0, len(r)-1\n```", +- Run: assignVariableTypes, +- }, +- ParameterNames: { +- Name: ParameterNames, +- Doc: "Enable/disable inlay hints for parameter names:\n```go\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)\n```", +- Run: parameterNames, +- }, +- ConstantValues: { +- Name: ConstantValues, +- Doc: "Enable/disable inlay hints for constant values:\n```go\n\tconst (\n\t\tKindNone Kind = iota/* = 0*/\n\t\tKindPrint/* = 1*/\n\t\tKindPrintf/* = 2*/\n\t\tKindErrorf/* = 3*/\n\t)\n```", +- Run: constantValues, +- }, +- RangeVariableTypes: { +- Name: RangeVariableTypes, +- Doc: "Enable/disable inlay hints for variable types in range statements:\n```go\n\tfor k/* int*/, v/* string*/ := range []string{} {\n\t\tfmt.Println(k, v)\n\t}\n```", +- Run: rangeVariableTypes, +- }, +- CompositeLiteralTypes: { +- Name: CompositeLiteralTypes, +- Doc: "Enable/disable inlay hints for composite literal types:\n```go\n\tfor _, c := range []struct {\n\t\tin, want string\n\t}{\n\t\t/*struct{ in string; want string }*/{\"Hello, world\", \"dlrow ,olleH\"},\n\t}\n```", +- Run: compositeLiteralTypes, +- }, +- CompositeLiteralFieldNames: { +- Name: CompositeLiteralFieldNames, +- Doc: "Enable/disable inlay hints for composite literal field names:\n```go\n\t{/*in: */\"Hello, world\", /*want: */\"dlrow ,olleH\"}\n```", +- Run: compositeLiteralFields, +- }, +- FunctionTypeParameters: { +- Name: FunctionTypeParameters, +- Doc: "Enable/disable inlay hints for implicit type parameters on generic functions:\n```go\n\tmyFoo/*[int, string]*/(1, \"hello\")\n```", +- Run: funcTypeParams, +- }, -} - --// Contains additional information about the context in which a completion request is triggered. --type CompletionContext struct { -- // How the completion was triggered. -- TriggerKind CompletionTriggerKind `json:"triggerKind"` -- // The trigger character (a single character) that has trigger code complete. -- // Is undefined if `triggerKind !== CompletionTriggerKind.TriggerCharacter` -- TriggerCharacter string `json:"triggerCharacter,omitempty"` --} +-func InlayHint(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pRng protocol.Range) ([]protocol.InlayHint, error) { +- ctx, done := event.Start(ctx, "golang.InlayHint") +- defer done() - --// A completion item represents a text snippet that is --// proposed to complete text that is being typed. --type CompletionItem struct { -- // The label of this completion item. -- // -- // The label property is also by default the text that -- // is inserted when selecting this completion. -- // -- // If label details are provided the label itself should -- // be an unqualified name of the completion item. -- Label string `json:"label"` -- // Additional details for the label -- // -- // @since 3.17.0 -- LabelDetails *CompletionItemLabelDetails `json:"labelDetails,omitempty"` -- // The kind of this completion item. Based of the kind -- // an icon is chosen by the editor. -- Kind CompletionItemKind `json:"kind,omitempty"` -- // Tags for this completion item. -- // -- // @since 3.15.0 -- Tags []CompletionItemTag `json:"tags,omitempty"` -- // A human-readable string with additional information -- // about this item, like type or symbol information. -- Detail string `json:"detail,omitempty"` -- // A human-readable string that represents a doc-comment. -- Documentation *Or_CompletionItem_documentation `json:"documentation,omitempty"` -- // Indicates if this item is deprecated. -- // @deprecated Use `tags` instead. -- Deprecated bool `json:"deprecated,omitempty"` -- // Select this item when showing. -- // -- // *Note* that only one completion item can be selected and that the -- // tool / client decides which item that is. The rule is that the *first* -- // item of those that match best is selected. -- Preselect bool `json:"preselect,omitempty"` -- // A string that should be used when comparing this item -- // with other items. When `falsy` the {@link CompletionItem.label label} -- // is used. -- SortText string `json:"sortText,omitempty"` -- // A string that should be used when filtering a set of -- // completion items. When `falsy` the {@link CompletionItem.label label} -- // is used. -- FilterText string `json:"filterText,omitempty"` -- // A string that should be inserted into a document when selecting -- // this completion. When `falsy` the {@link CompletionItem.label label} -- // is used. -- // -- // The `insertText` is subject to interpretation by the client side. -- // Some tools might not take the string literally. For example -- // VS Code when code complete is requested in this example -- // `con` and a completion item with an `insertText` of -- // `console` is provided it will only insert `sole`. Therefore it is -- // recommended to use `textEdit` instead since it avoids additional client -- // side interpretation. -- InsertText string `json:"insertText,omitempty"` -- // The format of the insert text. The format applies to both the -- // `insertText` property and the `newText` property of a provided -- // `textEdit`. If omitted defaults to `InsertTextFormat.PlainText`. -- // -- // Please note that the insertTextFormat doesn't apply to -- // `additionalTextEdits`. -- InsertTextFormat *InsertTextFormat `json:"insertTextFormat,omitempty"` -- // How whitespace and indentation is handled during completion -- // item insertion. If not provided the clients default value depends on -- // the `textDocument.completion.insertTextMode` client capability. -- // -- // @since 3.16.0 -- InsertTextMode *InsertTextMode `json:"insertTextMode,omitempty"` -- // An {@link TextEdit edit} which is applied to a document when selecting -- // this completion. When an edit is provided the value of -- // {@link CompletionItem.insertText insertText} is ignored. -- // -- // Most editors support two different operations when accepting a completion -- // item. One is to insert a completion text and the other is to replace an -- // existing text with a completion text. Since this can usually not be -- // predetermined by a server it can report both ranges. Clients need to -- // signal support for `InsertReplaceEdits` via the -- // `textDocument.completion.insertReplaceSupport` client capability -- // property. -- // -- // *Note 1:* The text edit's range as well as both ranges from an insert -- // replace edit must be a [single line] and they must contain the position -- // at which completion has been requested. -- // *Note 2:* If an `InsertReplaceEdit` is returned the edit's insert range -- // must be a prefix of the edit's replace range, that means it must be -- // contained and starting at the same position. -- // -- // @since 3.16.0 additional type `InsertReplaceEdit` -- TextEdit *TextEdit `json:"textEdit,omitempty"` -- // The edit text used if the completion item is part of a CompletionList and -- // CompletionList defines an item default for the text edit range. -- // -- // Clients will only honor this property if they opt into completion list -- // item defaults using the capability `completionList.itemDefaults`. -- // -- // If not provided and a list's default range is provided the label -- // property is used as a text. -- // -- // @since 3.17.0 -- TextEditText string `json:"textEditText,omitempty"` -- // An optional array of additional {@link TextEdit text edits} that are applied when -- // selecting this completion. Edits must not overlap (including the same insert position) -- // with the main {@link CompletionItem.textEdit edit} nor with themselves. -- // -- // Additional text edits should be used to change text unrelated to the current cursor position -- // (for example adding an import statement at the top of the file if the completion item will -- // insert an unqualified type). -- AdditionalTextEdits []TextEdit `json:"additionalTextEdits,omitempty"` -- // An optional set of characters that when pressed while this completion is active will accept it first and -- // then type that character. *Note* that all commit characters should have `length=1` and that superfluous -- // characters will be ignored. -- CommitCharacters []string `json:"commitCharacters,omitempty"` -- // An optional {@link Command command} that is executed *after* inserting this completion. *Note* that -- // additional modifications to the current document should be described with the -- // {@link CompletionItem.additionalTextEdits additionalTextEdits}-property. -- Command *Command `json:"command,omitempty"` -- // A data entry field that is preserved on a completion item between a -- // {@link CompletionRequest} and a {@link CompletionResolveRequest}. -- Data interface{} `json:"data,omitempty"` --} +- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) +- if err != nil { +- return nil, fmt.Errorf("getting file for InlayHint: %w", err) +- } - --// The kind of a completion entry. --type CompletionItemKind uint32 +- // Collect a list of the inlay hints that are enabled. +- inlayHintOptions := snapshot.Options().InlayHintOptions +- var enabledHints []InlayHintFunc +- for hint, enabled := range inlayHintOptions.Hints { +- if !enabled { +- continue +- } +- if h, ok := AllInlayHints[hint]; ok { +- enabledHints = append(enabledHints, h.Run) +- } +- } +- if len(enabledHints) == 0 { +- return nil, nil +- } - --// Additional details for a completion item label. --// --// @since 3.17.0 --type CompletionItemLabelDetails struct { -- // An optional string which is rendered less prominently directly after {@link CompletionItem.label label}, -- // without any spacing. Should be used for function signatures and type annotations. -- Detail string `json:"detail,omitempty"` -- // An optional string which is rendered less prominently after {@link CompletionItem.detail}. Should be used -- // for fully qualified names and file paths. -- Description string `json:"description,omitempty"` --} +- info := pkg.TypesInfo() +- q := typesutil.FileQualifier(pgf.File, pkg.Types(), info) - --// Completion item tags are extra annotations that tweak the rendering of a completion --// item. --// --// @since 3.15.0 --type CompletionItemTag uint32 +- // Set the range to the full file if the range is not valid. +- start, end := pgf.File.Pos(), pgf.File.End() +- if pRng.Start.Line < pRng.End.Line || pRng.Start.Character < pRng.End.Character { +- // Adjust start and end for the specified range. +- var err error +- start, end, err = pgf.RangePos(pRng) +- if err != nil { +- return nil, err +- } +- } - --// Represents a collection of {@link CompletionItem completion items} to be presented --// in the editor. --type CompletionList struct { -- // This list it not complete. Further typing results in recomputing this list. -- // -- // Recomputed lists have all their items replaced (not appended) in the -- // incomplete completion sessions. -- IsIncomplete bool `json:"isIncomplete"` -- // In many cases the items of an actual completion result share the same -- // value for properties like `commitCharacters` or the range of a text -- // edit. A completion list can therefore define item defaults which will -- // be used if a completion item itself doesn't specify the value. -- // -- // If a completion list specifies a default value and a completion item -- // also specifies a corresponding value the one from the item is used. -- // -- // Servers are only allowed to return default values if the client -- // signals support for this via the `completionList.itemDefaults` -- // capability. -- // -- // @since 3.17.0 -- ItemDefaults *PItemDefaultsMsg_textDocument_completion `json:"itemDefaults,omitempty"` -- // The completion items. -- Items []CompletionItem `json:"items"` +- var hints []protocol.InlayHint +- ast.Inspect(pgf.File, func(node ast.Node) bool { +- // If not in range, we can stop looking. +- if node == nil || node.End() < start || node.Pos() > end { +- return false +- } +- for _, fn := range enabledHints { +- hints = append(hints, fn(node, pgf.Mapper, pgf.Tok, info, &q)...) +- } +- return true +- }) +- return hints, nil -} - --// Completion options. --type CompletionOptions struct { -- // Most tools trigger completion request automatically without explicitly requesting -- // it using a keyboard shortcut (e.g. Ctrl+Space). Typically they do so when the user -- // starts to type an identifier. For example if the user types `c` in a JavaScript file -- // code complete will automatically pop up present `console` besides others as a -- // completion item. Characters that make up identifiers don't need to be listed here. -- // -- // If code complete should automatically be trigger on characters not being valid inside -- // an identifier (for example `.` in JavaScript) list them in `triggerCharacters`. -- TriggerCharacters []string `json:"triggerCharacters,omitempty"` -- // The list of all possible characters that commit a completion. This field can be used -- // if clients don't support individual commit characters per completion item. See -- // `ClientCapabilities.textDocument.completion.completionItem.commitCharactersSupport` -- // -- // If a server provides both `allCommitCharacters` and commit characters on an individual -- // completion item the ones on the completion item win. -- // -- // @since 3.2.0 -- AllCommitCharacters []string `json:"allCommitCharacters,omitempty"` -- // The server provides support to resolve additional -- // information for a completion item. -- ResolveProvider bool `json:"resolveProvider,omitempty"` -- // The server supports the following `CompletionItem` specific -- // capabilities. -- // -- // @since 3.17.0 -- CompletionItem *PCompletionItemPCompletionProvider `json:"completionItem,omitempty"` -- WorkDoneProgressOptions --} +-func parameterNames(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { +- callExpr, ok := node.(*ast.CallExpr) +- if !ok { +- return nil +- } +- signature, ok := typeparams.CoreType(info.TypeOf(callExpr.Fun)).(*types.Signature) +- if !ok { +- return nil +- } - --// Completion parameters --type CompletionParams struct { -- // The completion context. This is only available it the client specifies -- // to send this using the client capability `textDocument.completion.contextSupport === true` -- Context CompletionContext `json:"context,omitempty"` -- TextDocumentPositionParams -- WorkDoneProgressParams -- PartialResultParams +- var hints []protocol.InlayHint +- for i, v := range callExpr.Args { +- start, err := m.PosPosition(tf, v.Pos()) +- if err != nil { +- continue +- } +- params := signature.Params() +- // When a function has variadic params, we skip args after +- // params.Len(). +- if i > params.Len()-1 { +- break +- } +- param := params.At(i) +- // param.Name is empty for built-ins like append +- if param.Name() == "" { +- continue +- } +- // Skip the parameter name hint if the arg matches +- // the parameter name. +- if i, ok := v.(*ast.Ident); ok && i.Name == param.Name() { +- continue +- } +- +- label := param.Name() +- if signature.Variadic() && i == params.Len()-1 { +- label = label + "..." +- } +- hints = append(hints, protocol.InlayHint{ +- Position: start, +- Label: buildLabel(label + ":"), +- Kind: protocol.Parameter, +- PaddingRight: true, +- }) +- } +- return hints -} - --// Registration options for a {@link CompletionRequest}. --type CompletionRegistrationOptions struct { -- TextDocumentRegistrationOptions -- CompletionOptions +-func funcTypeParams(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { +- ce, ok := node.(*ast.CallExpr) +- if !ok { +- return nil +- } +- id, ok := ce.Fun.(*ast.Ident) +- if !ok { +- return nil +- } +- inst := info.Instances[id] +- if inst.TypeArgs == nil { +- return nil +- } +- start, err := m.PosPosition(tf, id.End()) +- if err != nil { +- return nil +- } +- var args []string +- for i := 0; i < inst.TypeArgs.Len(); i++ { +- args = append(args, inst.TypeArgs.At(i).String()) +- } +- if len(args) == 0 { +- return nil +- } +- return []protocol.InlayHint{{ +- Position: start, +- Label: buildLabel("[" + strings.Join(args, ", ") + "]"), +- Kind: protocol.Type, +- }} -} - --// How a completion was triggered --type CompletionTriggerKind uint32 --type ConfigurationItem struct { -- // The scope to get the configuration section for. -- ScopeURI string `json:"scopeUri,omitempty"` -- // The configuration section asked for. -- Section string `json:"section,omitempty"` --} +-func assignVariableTypes(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint { +- stmt, ok := node.(*ast.AssignStmt) +- if !ok || stmt.Tok != token.DEFINE { +- return nil +- } - --// The parameters of a configuration request. --type ConfigurationParams struct { -- Items []ConfigurationItem `json:"items"` +- var hints []protocol.InlayHint +- for _, v := range stmt.Lhs { +- if h := variableType(v, m, tf, info, q); h != nil { +- hints = append(hints, *h) +- } +- } +- return hints -} - --// Create file operation. --type CreateFile struct { -- // A create -- Kind string `json:"kind"` -- // The resource to create. -- URI DocumentURI `json:"uri"` -- // Additional options -- Options *CreateFileOptions `json:"options,omitempty"` -- ResourceOperation +-func rangeVariableTypes(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint { +- rStmt, ok := node.(*ast.RangeStmt) +- if !ok { +- return nil +- } +- var hints []protocol.InlayHint +- if h := variableType(rStmt.Key, m, tf, info, q); h != nil { +- hints = append(hints, *h) +- } +- if h := variableType(rStmt.Value, m, tf, info, q); h != nil { +- hints = append(hints, *h) +- } +- return hints -} - --// Options to create a file. --type CreateFileOptions struct { -- // Overwrite existing file. Overwrite wins over `ignoreIfExists` -- Overwrite bool `json:"overwrite,omitempty"` -- // Ignore if exists. -- IgnoreIfExists bool `json:"ignoreIfExists,omitempty"` +-func variableType(e ast.Expr, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) *protocol.InlayHint { +- typ := info.TypeOf(e) +- if typ == nil { +- return nil +- } +- end, err := m.PosPosition(tf, e.End()) +- if err != nil { +- return nil +- } +- return &protocol.InlayHint{ +- Position: end, +- Label: buildLabel(types.TypeString(typ, *q)), +- Kind: protocol.Type, +- PaddingLeft: true, +- } -} - --// The parameters sent in notifications/requests for user-initiated creation of --// files. --// --// @since 3.16.0 --type CreateFilesParams struct { -- // An array of all files/folders created in this operation. -- Files []FileCreate `json:"files"` --} +-func constantValues(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { +- genDecl, ok := node.(*ast.GenDecl) +- if !ok || genDecl.Tok != token.CONST { +- return nil +- } - --// The declaration of a symbol representation as one or many {@link Location locations}. --type Declaration = []Location // (alias) line 14248 --// @since 3.14.0 --type DeclarationClientCapabilities struct { -- // Whether declaration supports dynamic registration. If this is set to `true` -- // the client supports the new `DeclarationRegistrationOptions` return value -- // for the corresponding server capability as well. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -- // The client supports additional metadata in the form of declaration links. -- LinkSupport bool `json:"linkSupport,omitempty"` +- var hints []protocol.InlayHint +- for _, v := range genDecl.Specs { +- spec, ok := v.(*ast.ValueSpec) +- if !ok { +- continue +- } +- end, err := m.PosPosition(tf, v.End()) +- if err != nil { +- continue +- } +- // Show hints when values are missing or at least one value is not +- // a basic literal. +- showHints := len(spec.Values) == 0 +- checkValues := len(spec.Names) == len(spec.Values) +- var values []string +- for i, w := range spec.Names { +- obj, ok := info.ObjectOf(w).(*types.Const) +- if !ok || obj.Val().Kind() == constant.Unknown { +- return nil +- } +- if checkValues { +- switch spec.Values[i].(type) { +- case *ast.BadExpr: +- return nil +- case *ast.BasicLit: +- default: +- if obj.Val().Kind() != constant.Bool { +- showHints = true +- } +- } +- } +- values = append(values, fmt.Sprintf("%v", obj.Val())) +- } +- if !showHints || len(values) == 0 { +- continue +- } +- hints = append(hints, protocol.InlayHint{ +- Position: end, +- Label: buildLabel("= " + strings.Join(values, ", ")), +- PaddingLeft: true, +- }) +- } +- return hints -} - --// Information about where a symbol is declared. --// --// Provides additional metadata over normal {@link Location location} declarations, including the range of --// the declaring symbol. --// --// Servers should prefer returning `DeclarationLink` over `Declaration` if supported --// by the client. --type DeclarationLink = LocationLink // (alias) line 14268 --type DeclarationOptions struct { -- WorkDoneProgressOptions --} --type DeclarationParams struct { -- TextDocumentPositionParams -- WorkDoneProgressParams -- PartialResultParams --} --type DeclarationRegistrationOptions struct { -- DeclarationOptions -- TextDocumentRegistrationOptions -- StaticRegistrationOptions --} +-func compositeLiteralFields(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { +- compLit, ok := node.(*ast.CompositeLit) +- if !ok { +- return nil +- } +- typ := info.TypeOf(compLit) +- if typ == nil { +- return nil +- } +- typ = typesinternal.Unpointer(typ) +- strct, ok := typeparams.CoreType(typ).(*types.Struct) +- if !ok { +- return nil +- } - --// The definition of a symbol represented as one or many {@link Location locations}. --// For most programming languages there is only one location at which a symbol is --// defined. --// --// Servers should prefer returning `DefinitionLink` over `Definition` if supported --// by the client. --type Definition = Or_Definition // (alias) line 14166 --// Client Capabilities for a {@link DefinitionRequest}. --type DefinitionClientCapabilities struct { -- // Whether definition supports dynamic registration. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -- // The client supports additional metadata in the form of definition links. -- // -- // @since 3.14.0 -- LinkSupport bool `json:"linkSupport,omitempty"` +- var hints []protocol.InlayHint +- var allEdits []protocol.TextEdit +- for i, v := range compLit.Elts { +- if _, ok := v.(*ast.KeyValueExpr); !ok { +- start, err := m.PosPosition(tf, v.Pos()) +- if err != nil { +- continue +- } +- if i > strct.NumFields()-1 { +- break +- } +- hints = append(hints, protocol.InlayHint{ +- Position: start, +- Label: buildLabel(strct.Field(i).Name() + ":"), +- Kind: protocol.Parameter, +- PaddingRight: true, +- }) +- allEdits = append(allEdits, protocol.TextEdit{ +- Range: protocol.Range{Start: start, End: start}, +- NewText: strct.Field(i).Name() + ": ", +- }) +- } +- } +- // It is not allowed to have a mix of keyed and unkeyed fields, so +- // have the text edits add keys to all fields. +- for i := range hints { +- hints[i].TextEdits = allEdits +- } +- return hints -} - --// Information about where a symbol is defined. --// --// Provides additional metadata over normal {@link Location location} definitions, including the range of --// the defining symbol --type DefinitionLink = LocationLink // (alias) line 14186 --// Server Capabilities for a {@link DefinitionRequest}. --type DefinitionOptions struct { -- WorkDoneProgressOptions +-func compositeLiteralTypes(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint { +- compLit, ok := node.(*ast.CompositeLit) +- if !ok { +- return nil +- } +- typ := info.TypeOf(compLit) +- if typ == nil { +- return nil +- } +- if compLit.Type != nil { +- return nil +- } +- prefix := "" +- if t, ok := typeparams.CoreType(typ).(*types.Pointer); ok { +- typ = t.Elem() +- prefix = "&" +- } +- // The type for this composite literal is implicit, add an inlay hint. +- start, err := m.PosPosition(tf, compLit.Lbrace) +- if err != nil { +- return nil +- } +- return []protocol.InlayHint{{ +- Position: start, +- Label: buildLabel(fmt.Sprintf("%s%s", prefix, types.TypeString(typ, *q))), +- Kind: protocol.Type, +- }} -} - --// Parameters for a {@link DefinitionRequest}. --type DefinitionParams struct { -- TextDocumentPositionParams -- WorkDoneProgressParams -- PartialResultParams +-func buildLabel(s string) []protocol.InlayHintLabelPart { +- label := protocol.InlayHintLabelPart{ +- Value: s, +- } +- if len(s) > maxLabelLength+len("...") { +- label.Value = s[:maxLabelLength] + "..." +- } +- return []protocol.InlayHintLabelPart{label} -} +diff -urN a/gopls/internal/golang/inline_all.go b/gopls/internal/golang/inline_all.go +--- a/gopls/internal/golang/inline_all.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/inline_all.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,276 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// Registration options for a {@link DefinitionRequest}. --type DefinitionRegistrationOptions struct { -- TextDocumentRegistrationOptions -- DefinitionOptions --} +-package golang - --// Delete file operation --type DeleteFile struct { -- // A delete -- Kind string `json:"kind"` -- // The file to delete. -- URI DocumentURI `json:"uri"` -- // Delete options. -- Options *DeleteFileOptions `json:"options,omitempty"` -- ResourceOperation --} +-import ( +- "context" +- "fmt" +- "go/ast" +- "go/parser" +- "go/types" - --// Delete file options --type DeleteFileOptions struct { -- // Delete the content recursively if a folder is denoted. -- Recursive bool `json:"recursive,omitempty"` -- // Ignore the operation if the file doesn't exist. -- IgnoreIfNotExists bool `json:"ignoreIfNotExists,omitempty"` --} +- "golang.org/x/tools/go/ast/astutil" +- "golang.org/x/tools/go/types/typeutil" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/internal/refactor/inline" +-) - --// The parameters sent in notifications/requests for user-initiated deletes of --// files. +-// inlineAllCalls inlines all calls to the original function declaration +-// described by callee, returning the resulting modified file content. -// --// @since 3.16.0 --type DeleteFilesParams struct { -- // An array of all files/folders deleted in this operation. -- Files []FileDelete `json:"files"` --} -- --// Represents a diagnostic, such as a compiler error or warning. Diagnostic objects --// are only valid in the scope of a resource. --type Diagnostic struct { -- // The range at which the message applies -- Range Range `json:"range"` -- // The diagnostic's severity. Can be omitted. If omitted it is up to the -- // client to interpret diagnostics as error, warning, info or hint. -- Severity DiagnosticSeverity `json:"severity,omitempty"` -- // The diagnostic's code, which usually appear in the user interface. -- Code interface{} `json:"code,omitempty"` -- // An optional property to describe the error code. -- // Requires the code field (above) to be present/not null. -- // -- // @since 3.16.0 -- CodeDescription *CodeDescription `json:"codeDescription,omitempty"` -- // A human-readable string describing the source of this -- // diagnostic, e.g. 'typescript' or 'super lint'. It usually -- // appears in the user interface. -- Source string `json:"source,omitempty"` -- // The diagnostic's message. It usually appears in the user interface -- Message string `json:"message"` -- // Additional metadata about the diagnostic. -- // -- // @since 3.15.0 -- Tags []DiagnosticTag `json:"tags,omitempty"` -- // An array of related diagnostic information, e.g. when symbol-names within -- // a scope collide all definitions can be marked via this property. -- RelatedInformation []DiagnosticRelatedInformation `json:"relatedInformation,omitempty"` -- // A data entry field that is preserved between a `textDocument/publishDiagnostics` -- // notification and `textDocument/codeAction` request. -- // -- // @since 3.16.0 -- Data *json.RawMessage `json:"data,omitempty"` --} -- --// Client capabilities specific to diagnostic pull requests. +-// inlining everything is currently an expensive operation: it involves re-type +-// checking every package that contains a potential call, as reported by +-// References. In cases where there are multiple calls per file, inlineAllCalls +-// must type check repeatedly for each additional call. -// --// @since 3.17.0 --type DiagnosticClientCapabilities struct { -- // Whether implementation supports dynamic registration. If this is set to `true` -- // the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` -- // return value for the corresponding server capability as well. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -- // Whether the clients supports related documents for document diagnostic pulls. -- RelatedDocumentSupport bool `json:"relatedDocumentSupport,omitempty"` --} -- --// Diagnostic options. +-// The provided post processing function is applied to the resulting source +-// after each transformation. This is necessary because we are using this +-// function to inline synthetic wrappers for the purpose of signature +-// rewriting. The delegated function has a fake name that doesn't exist in the +-// snapshot, and so we can't re-type check until we replace this fake name. -// --// @since 3.17.0 --type DiagnosticOptions struct { -- // An optional identifier under which the diagnostics are -- // managed by the client. -- Identifier string `json:"identifier,omitempty"` -- // Whether the language has inter file dependencies meaning that -- // editing code in one file can result in a different diagnostic -- // set in another file. Inter file dependencies are common for -- // most programming languages and typically uncommon for linters. -- InterFileDependencies bool `json:"interFileDependencies"` -- // The server provides support for workspace diagnostics as well. -- WorkspaceDiagnostics bool `json:"workspaceDiagnostics"` -- WorkDoneProgressOptions --} -- --// Diagnostic registration options. +-// TODO(rfindley): this only works because removing a parameter is a very +-// narrow operation. A better solution would be to allow for ad-hoc snapshots +-// that expose the full machinery of real snapshots: minimal invalidation, +-// batched type checking, etc. Then we could actually rewrite the declaring +-// package in this snapshot (and so 'post' would not be necessary), and could +-// robustly re-type check for the purpose of iterative inlining, even if the +-// inlined code pulls in new imports that weren't present in export data. -// --// @since 3.17.0 --type DiagnosticRegistrationOptions struct { -- TextDocumentRegistrationOptions -- DiagnosticOptions -- StaticRegistrationOptions --} -- --// Represents a related message and source code location for a diagnostic. This should be --// used to point to code locations that cause or related to a diagnostics, e.g when duplicating --// a symbol in a scope. --type DiagnosticRelatedInformation struct { -- // The location of this related diagnostic information. -- Location Location `json:"location"` -- // The message of this related diagnostic information. -- Message string `json:"message"` --} +-// The code below notes where are assumptions are made that only hold true in +-// the case of parameter removal (annotated with 'Assumption:') +-func inlineAllCalls(ctx context.Context, logf func(string, ...any), snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, origDecl *ast.FuncDecl, callee *inline.Callee, post func([]byte) []byte) (map[protocol.DocumentURI][]byte, error) { +- // Collect references. +- var refs []protocol.Location +- { +- funcPos, err := pgf.Mapper.PosPosition(pgf.Tok, origDecl.Name.NamePos) +- if err != nil { +- return nil, err +- } +- fh, err := snapshot.ReadFile(ctx, pgf.URI) +- if err != nil { +- return nil, err +- } +- refs, err = References(ctx, snapshot, fh, funcPos, false) +- if err != nil { +- return nil, fmt.Errorf("finding references to rewrite: %v", err) +- } +- } - --// Cancellation data returned from a diagnostic request. --// --// @since 3.17.0 --type DiagnosticServerCancellationData struct { -- RetriggerRequest bool `json:"retriggerRequest"` --} +- // Type-check the narrowest package containing each reference. +- // TODO(rfindley): we should expose forEachPackage in order to operate in +- // parallel and to reduce peak memory for this operation. +- var ( +- pkgForRef = make(map[protocol.Location]PackageID) +- pkgs = make(map[PackageID]*cache.Package) +- ) +- { +- needPkgs := make(map[PackageID]struct{}) +- for _, ref := range refs { +- md, err := NarrowestMetadataForFile(ctx, snapshot, ref.URI) +- if err != nil { +- return nil, fmt.Errorf("finding ref metadata: %v", err) +- } +- pkgForRef[ref] = md.ID +- needPkgs[md.ID] = struct{}{} +- } +- var pkgIDs []PackageID +- for id := range needPkgs { // TODO: use maps.Keys once it is available to us +- pkgIDs = append(pkgIDs, id) +- } - --// The diagnostic's severity. --type DiagnosticSeverity uint32 +- refPkgs, err := snapshot.TypeCheck(ctx, pkgIDs...) +- if err != nil { +- return nil, fmt.Errorf("type checking reference packages: %v", err) +- } - --// The diagnostic tags. --// --// @since 3.15.0 --type DiagnosticTag uint32 +- for _, p := range refPkgs { +- pkgs[p.Metadata().ID] = p +- } +- } - --// Workspace client capabilities specific to diagnostic pull requests. --// --// @since 3.17.0 --type DiagnosticWorkspaceClientCapabilities struct { -- // Whether the client implementation supports a refresh request sent from -- // the server to the client. -- // -- // Note that this event is global and will force the client to refresh all -- // pulled diagnostics currently shown. It should be used with absolute care and -- // is useful for situation where a server for example detects a project wide -- // change that requires such a calculation. -- RefreshSupport bool `json:"refreshSupport,omitempty"` --} --type DidChangeConfigurationClientCapabilities struct { -- // Did change configuration notification supports dynamic registration. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` --} +- // Organize calls by top file declaration. Calls within a single file may +- // affect each other, as the inlining edit may affect the surrounding scope +- // or imports Therefore, when inlining subsequent calls in the same +- // declaration, we must re-type check. - --// The parameters of a change configuration notification. --type DidChangeConfigurationParams struct { -- // The actual changed settings -- Settings interface{} `json:"settings"` --} --type DidChangeConfigurationRegistrationOptions struct { -- Section *OrPSection_workspace_didChangeConfiguration `json:"section,omitempty"` --} +- type fileCalls struct { +- pkg *cache.Package +- pgf *parsego.File +- calls []*ast.CallExpr +- } - --// The params sent in a change notebook document notification. --// --// @since 3.17.0 --type DidChangeNotebookDocumentParams struct { -- // The notebook document that did change. The version number points -- // to the version after all provided changes have been applied. If -- // only the text document content of a cell changes the notebook version -- // doesn't necessarily have to change. -- NotebookDocument VersionedNotebookDocumentIdentifier `json:"notebookDocument"` -- // The actual changes to the notebook document. -- // -- // The changes describe single state changes to the notebook document. -- // So if there are two changes c1 (at array index 0) and c2 (at array -- // index 1) for a notebook in state S then c1 moves the notebook from -- // S to S' and c2 from S' to S''. So c1 is computed on the state S and -- // c2 is computed on the state S'. -- // -- // To mirror the content of a notebook using change events use the following approach: -- // -- // - start with the same initial content -- // - apply the 'notebookDocument/didChange' notifications in the order you receive them. -- // - apply the `NotebookChangeEvent`s in a single notification in the order -- // you receive them. -- Change NotebookDocumentChangeEvent `json:"change"` --} +- refsByFile := make(map[protocol.DocumentURI]*fileCalls) +- for _, ref := range refs { +- refpkg := pkgs[pkgForRef[ref]] +- pgf, err := refpkg.File(ref.URI) +- if err != nil { +- return nil, bug.Errorf("finding %s in %s: %v", ref.URI, refpkg.Metadata().ID, err) +- } +- start, end, err := pgf.RangePos(ref.Range) +- if err != nil { +- return nil, err // e.g. invalid range +- } - --// The change text document notification's parameters. --type DidChangeTextDocumentParams struct { -- // The document that did change. The version number points -- // to the version after all provided content changes have -- // been applied. -- TextDocument VersionedTextDocumentIdentifier `json:"textDocument"` -- // The actual content changes. The content changes describe single state changes -- // to the document. So if there are two content changes c1 (at array index 0) and -- // c2 (at array index 1) for a document in state S then c1 moves the document from -- // S to S' and c2 from S' to S''. So c1 is computed on the state S and c2 is computed -- // on the state S'. -- // -- // To mirror the content of a document using change events use the following approach: -- // -- // - start with the same initial content -- // - apply the 'textDocument/didChange' notifications in the order you receive them. -- // - apply the `TextDocumentContentChangeEvent`s in a single notification in the order -- // you receive them. -- ContentChanges []TextDocumentContentChangeEvent `json:"contentChanges"` --} --type DidChangeWatchedFilesClientCapabilities struct { -- // Did change watched files notification supports dynamic registration. Please note -- // that the current protocol doesn't support static configuration for file changes -- // from the server side. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -- // Whether the client has support for {@link RelativePattern relative pattern} -- // or not. +- // Look for the surrounding call expression. +- var ( +- name *ast.Ident +- call *ast.CallExpr +- ) +- path, _ := astutil.PathEnclosingInterval(pgf.File, start, end) +- name, _ = path[0].(*ast.Ident) +- if _, ok := path[1].(*ast.SelectorExpr); ok { +- call, _ = path[2].(*ast.CallExpr) +- } else { +- call, _ = path[1].(*ast.CallExpr) +- } +- if name == nil || call == nil { +- // TODO(rfindley): handle this case with eta-abstraction: +- // a reference to the target function f in a non-call position +- // use(f) +- // is replaced by +- // use(func(...) { f(...) }) +- return nil, fmt.Errorf("cannot inline: found non-call function reference %v", ref) +- } +- // Sanity check. +- if obj := refpkg.TypesInfo().ObjectOf(name); obj == nil || +- obj.Name() != origDecl.Name.Name || +- obj.Pkg() == nil || +- obj.Pkg().Path() != string(pkg.Metadata().PkgPath) { +- return nil, bug.Errorf("cannot inline: corrupted reference %v", ref) +- } +- +- callInfo, ok := refsByFile[ref.URI] +- if !ok { +- callInfo = &fileCalls{ +- pkg: refpkg, +- pgf: pgf, +- } +- refsByFile[ref.URI] = callInfo +- } +- callInfo.calls = append(callInfo.calls, call) +- } +- +- // Inline each call within the same decl in sequence, re-typechecking after +- // each one. If there is only a single call within the decl, we can avoid +- // additional type checking. - // -- // @since 3.17.0 -- RelativePatternSupport bool `json:"relativePatternSupport,omitempty"` --} +- // Assumption: inlining does not affect the package scope, so we can operate +- // on separate files independently. +- result := make(map[protocol.DocumentURI][]byte) +- for uri, callInfo := range refsByFile { +- var ( +- calls = callInfo.calls +- fset = callInfo.pkg.FileSet() +- tpkg = callInfo.pkg.Types() +- tinfo = callInfo.pkg.TypesInfo() +- file = callInfo.pgf.File +- content = callInfo.pgf.Src +- ) - --// The watched files change notification's parameters. --type DidChangeWatchedFilesParams struct { -- // The actual file events. -- Changes []FileEvent `json:"changes"` --} +- // Check for overlapping calls (such as Foo(Foo())). We can't handle these +- // because inlining may change the source order of the inner call with +- // respect to the inlined outer call, and so the heuristic we use to find +- // the next call (counting from top-to-bottom) does not work. +- for i := range calls { +- if i > 0 && calls[i-1].End() > calls[i].Pos() { +- return nil, fmt.Errorf("%s: can't inline overlapping call %s", uri, types.ExprString(calls[i-1])) +- } +- } - --// Describe options to be used when registered for text document change events. --type DidChangeWatchedFilesRegistrationOptions struct { -- // The watchers to register. -- Watchers []FileSystemWatcher `json:"watchers"` --} +- currentCall := 0 +- for currentCall < len(calls) { +- caller := &inline.Caller{ +- Fset: fset, +- Types: tpkg, +- Info: tinfo, +- File: file, +- Call: calls[currentCall], +- Content: content, +- } +- var err error +- content, err = inline.Inline(logf, caller, callee) +- if err != nil { +- return nil, fmt.Errorf("inlining failed: %v", err) +- } +- if post != nil { +- content = post(content) +- } +- if len(calls) <= 1 { +- // No need to re-type check, as we've inlined all calls. +- break +- } - --// The parameters of a `workspace/didChangeWorkspaceFolders` notification. --type DidChangeWorkspaceFoldersParams struct { -- // The actual workspace folder change event. -- Event WorkspaceFoldersChangeEvent `json:"event"` --} +- // TODO(rfindley): develop a theory of "trivial" inlining, which are +- // inlinings that don't require re-type checking. +- // +- // In principle, if the inlining only involves replacing one call with +- // another, the scope of the caller is unchanged and there is no need to +- // type check again before inlining subsequent calls (edits should not +- // overlap, and should not affect each other semantically). However, it +- // feels sufficiently complicated that, to be safe, this optimization is +- // deferred until later. - --// The params sent in a close notebook document notification. --// --// @since 3.17.0 --type DidCloseNotebookDocumentParams struct { -- // The notebook document that got closed. -- NotebookDocument NotebookDocumentIdentifier `json:"notebookDocument"` -- // The text documents that represent the content -- // of a notebook cell that got closed. -- CellTextDocuments []TextDocumentIdentifier `json:"cellTextDocuments"` --} +- file, err = parser.ParseFile(fset, uri.Path(), content, parser.ParseComments|parser.SkipObjectResolution) +- if err != nil { +- return nil, bug.Errorf("inlined file failed to parse: %v", err) +- } - --// The parameters sent in a close text document notification --type DidCloseTextDocumentParams struct { -- // The document that was closed. -- TextDocument TextDocumentIdentifier `json:"textDocument"` --} +- // After inlining one call with a removed parameter, the package will +- // fail to type check due to "not enough arguments". Therefore, we must +- // allow type errors here. +- // +- // Assumption: the resulting type errors do not affect the correctness of +- // subsequent inlining, because invalid arguments to a call do not affect +- // anything in the surrounding scope. +- // +- // TODO(rfindley): improve this. +- tpkg, tinfo, err = reTypeCheck(logf, callInfo.pkg, map[protocol.DocumentURI]*ast.File{uri: file}, true) +- if err != nil { +- return nil, bug.Errorf("type checking after inlining failed: %v", err) +- } - --// The params sent in an open notebook document notification. --// --// @since 3.17.0 --type DidOpenNotebookDocumentParams struct { -- // The notebook document that got opened. -- NotebookDocument NotebookDocument `json:"notebookDocument"` -- // The text documents that represent the content -- // of a notebook cell. -- CellTextDocuments []TextDocumentItem `json:"cellTextDocuments"` --} +- // Collect calls to the target function in the modified declaration. +- var calls2 []*ast.CallExpr +- ast.Inspect(file, func(n ast.Node) bool { +- if call, ok := n.(*ast.CallExpr); ok { +- fn := typeutil.StaticCallee(tinfo, call) +- if fn != nil && fn.Pkg().Path() == string(pkg.Metadata().PkgPath) && fn.Name() == origDecl.Name.Name { +- calls2 = append(calls2, call) +- } +- } +- return true +- }) - --// The parameters sent in an open text document notification --type DidOpenTextDocumentParams struct { -- // The document that was opened. -- TextDocument TextDocumentItem `json:"textDocument"` --} +- // If the number of calls has increased, this process will never cease. +- // If the number of calls has decreased, assume that inlining removed a +- // call. +- // If the number of calls didn't change, assume that inlining replaced +- // a call, and move on to the next. +- // +- // Assumption: we're inlining a call that has at most one recursive +- // reference (which holds for signature rewrites). +- // +- // TODO(rfindley): this isn't good enough. We should be able to support +- // inlining all existing calls even if they increase calls. How do we +- // correlate the before and after syntax? +- switch { +- case len(calls2) > len(calls): +- return nil, fmt.Errorf("inlining increased calls %d->%d, possible recursive call? content:\n%s", len(calls), len(calls2), content) +- case len(calls2) < len(calls): +- calls = calls2 +- case len(calls2) == len(calls): +- calls = calls2 +- currentCall++ +- } +- } - --// The params sent in a save notebook document notification. --// --// @since 3.17.0 --type DidSaveNotebookDocumentParams struct { -- // The notebook document that got saved. -- NotebookDocument NotebookDocumentIdentifier `json:"notebookDocument"` +- result[callInfo.pgf.URI] = content +- } +- return result, nil -} +diff -urN a/gopls/internal/golang/inline.go b/gopls/internal/golang/inline.go +--- a/gopls/internal/golang/inline.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/inline.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,136 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// The parameters sent in a save text document notification --type DidSaveTextDocumentParams struct { -- // The document that was saved. -- TextDocument TextDocumentIdentifier `json:"textDocument"` -- // Optional the content when saved. Depends on the includeText value -- // when the save notification was requested. -- Text *string `json:"text,omitempty"` --} --type DocumentColorClientCapabilities struct { -- // Whether implementation supports dynamic registration. If this is set to `true` -- // the client supports the new `DocumentColorRegistrationOptions` return value -- // for the corresponding server capability as well. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` --} --type DocumentColorOptions struct { -- WorkDoneProgressOptions --} +-package golang - --// Parameters for a {@link DocumentColorRequest}. --type DocumentColorParams struct { -- // The text document. -- TextDocument TextDocumentIdentifier `json:"textDocument"` -- WorkDoneProgressParams -- PartialResultParams --} --type DocumentColorRegistrationOptions struct { -- TextDocumentRegistrationOptions -- DocumentColorOptions -- StaticRegistrationOptions --} +-// This file defines the refactor.inline code action. - --// Parameters of the document diagnostic request. --// --// @since 3.17.0 --type DocumentDiagnosticParams struct { -- // The text document. -- TextDocument TextDocumentIdentifier `json:"textDocument"` -- // The additional identifier provided during registration. -- Identifier string `json:"identifier,omitempty"` -- // The result id of a previous response if provided. -- PreviousResultID string `json:"previousResultId,omitempty"` -- WorkDoneProgressParams -- PartialResultParams --} --type DocumentDiagnosticReport = Or_DocumentDiagnosticReport // (alias) line 13909 --// The document diagnostic report kinds. --// --// @since 3.17.0 --type DocumentDiagnosticReportKind string +-import ( +- "context" +- "fmt" +- "go/ast" +- "go/token" +- "go/types" - --// A partial result for a document diagnostic report. --// --// @since 3.17.0 --type DocumentDiagnosticReportPartialResult struct { -- RelatedDocuments map[DocumentURI]interface{} `json:"relatedDocuments"` --} +- "golang.org/x/tools/go/analysis" +- "golang.org/x/tools/go/ast/astutil" +- "golang.org/x/tools/go/types/typeutil" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/internal/diff" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/refactor/inline" +-) - --// A document filter describes a top level text document or --// a notebook cell document. --// --// @since 3.17.0 - proposed support for NotebookCellTextDocumentFilter. --type DocumentFilter = Or_DocumentFilter // (alias) line 14508 --// Client capabilities of a {@link DocumentFormattingRequest}. --type DocumentFormattingClientCapabilities struct { -- // Whether formatting supports dynamic registration. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` --} +-// EnclosingStaticCall returns the innermost function call enclosing +-// the selected range, along with the callee. +-func EnclosingStaticCall(pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*ast.CallExpr, *types.Func, error) { +- path, _ := astutil.PathEnclosingInterval(pgf.File, start, end) - --// Provider options for a {@link DocumentFormattingRequest}. --type DocumentFormattingOptions struct { -- WorkDoneProgressOptions +- var call *ast.CallExpr +-loop: +- for _, n := range path { +- switch n := n.(type) { +- case *ast.FuncLit: +- break loop +- case *ast.CallExpr: +- call = n +- break loop +- } +- } +- if call == nil { +- return nil, nil, fmt.Errorf("no enclosing call") +- } +- if safetoken.Line(pgf.Tok, call.Lparen) != safetoken.Line(pgf.Tok, start) { +- return nil, nil, fmt.Errorf("enclosing call is not on this line") +- } +- fn := typeutil.StaticCallee(pkg.TypesInfo(), call) +- if fn == nil { +- return nil, nil, fmt.Errorf("not a static call to a Go function") +- } +- return call, fn, nil -} - --// The parameters of a {@link DocumentFormattingRequest}. --type DocumentFormattingParams struct { -- // The document to format. -- TextDocument TextDocumentIdentifier `json:"textDocument"` -- // The format options. -- Options FormattingOptions `json:"options"` -- WorkDoneProgressParams --} +-func inlineCall(ctx context.Context, snapshot *cache.Snapshot, callerPkg *cache.Package, callerPGF *parsego.File, start, end token.Pos) (_ *token.FileSet, _ *analysis.SuggestedFix, err error) { +- // Find enclosing static call. +- call, fn, err := EnclosingStaticCall(callerPkg, callerPGF, start, end) +- if err != nil { +- return nil, nil, err +- } - --// Registration options for a {@link DocumentFormattingRequest}. --type DocumentFormattingRegistrationOptions struct { -- TextDocumentRegistrationOptions -- DocumentFormattingOptions --} +- // Locate callee by file/line and analyze it. +- calleePosn := safetoken.StartPosition(callerPkg.FileSet(), fn.Pos()) +- calleePkg, calleePGF, err := NarrowestPackageForFile(ctx, snapshot, protocol.URIFromPath(calleePosn.Filename)) +- if err != nil { +- return nil, nil, err +- } +- var calleeDecl *ast.FuncDecl +- for _, decl := range calleePGF.File.Decls { +- if decl, ok := decl.(*ast.FuncDecl); ok { +- posn := safetoken.StartPosition(calleePkg.FileSet(), decl.Name.Pos()) +- if posn.Line == calleePosn.Line && posn.Column == calleePosn.Column { +- calleeDecl = decl +- break +- } +- } +- } +- if calleeDecl == nil { +- return nil, nil, fmt.Errorf("can't find callee") +- } - --// A document highlight is a range inside a text document which deserves --// special attention. Usually a document highlight is visualized by changing --// the background color of its range. --type DocumentHighlight struct { -- // The range this highlight applies to. -- Range Range `json:"range"` -- // The highlight kind, default is {@link DocumentHighlightKind.Text text}. -- Kind DocumentHighlightKind `json:"kind,omitempty"` --} +- // The inliner assumes that input is well-typed, +- // but that is frequently not the case within gopls. +- // Until we are able to harden the inliner, +- // report panics as errors to avoid crashing the server. +- bad := func(p *cache.Package) bool { return len(p.ParseErrors())+len(p.TypeErrors()) > 0 } +- if bad(calleePkg) || bad(callerPkg) { +- defer func() { +- if x := recover(); x != nil { +- err = fmt.Errorf("inlining failed (%q), likely because inputs were ill-typed", x) +- } +- }() +- } - --// Client Capabilities for a {@link DocumentHighlightRequest}. --type DocumentHighlightClientCapabilities struct { -- // Whether document highlight supports dynamic registration. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` --} +- // Users can consult the gopls event log to see +- // why a particular inlining strategy was chosen. +- logf := logger(ctx, "inliner", snapshot.Options().VerboseOutput) - --// A document highlight kind. --type DocumentHighlightKind uint32 +- callee, err := inline.AnalyzeCallee(logf, calleePkg.FileSet(), calleePkg.Types(), calleePkg.TypesInfo(), calleeDecl, calleePGF.Src) +- if err != nil { +- return nil, nil, err +- } - --// Provider options for a {@link DocumentHighlightRequest}. --type DocumentHighlightOptions struct { -- WorkDoneProgressOptions --} +- // Inline the call. +- caller := &inline.Caller{ +- Fset: callerPkg.FileSet(), +- Types: callerPkg.Types(), +- Info: callerPkg.TypesInfo(), +- File: callerPGF.File, +- Call: call, +- Content: callerPGF.Src, +- } - --// Parameters for a {@link DocumentHighlightRequest}. --type DocumentHighlightParams struct { -- TextDocumentPositionParams -- WorkDoneProgressParams -- PartialResultParams --} +- got, err := inline.Inline(logf, caller, callee) +- if err != nil { +- return nil, nil, err +- } - --// Registration options for a {@link DocumentHighlightRequest}. --type DocumentHighlightRegistrationOptions struct { -- TextDocumentRegistrationOptions -- DocumentHighlightOptions +- return callerPkg.FileSet(), &analysis.SuggestedFix{ +- Message: fmt.Sprintf("inline call of %v", callee), +- TextEdits: diffToTextEdits(callerPGF.Tok, diff.Bytes(callerPGF.Src, got)), +- }, nil -} - --// A document link is a range in a text document that links to an internal or external resource, like another --// text document or a web site. --type DocumentLink struct { -- // The range this link applies to. -- Range Range `json:"range"` -- // The uri this link points to. If missing a resolve request is sent later. -- Target *URI `json:"target,omitempty"` -- // The tooltip text when you hover over this link. -- // -- // If a tooltip is provided, is will be displayed in a string that includes instructions on how to -- // trigger the link, such as `{0} (ctrl + click)`. The specific instructions vary depending on OS, -- // user settings, and localization. -- // -- // @since 3.15.0 -- Tooltip string `json:"tooltip,omitempty"` -- // A data entry field that is preserved on a document link between a -- // DocumentLinkRequest and a DocumentLinkResolveRequest. -- Data interface{} `json:"data,omitempty"` +-// TODO(adonovan): change the inliner to instead accept an io.Writer. +-func logger(ctx context.Context, name string, verbose bool) func(format string, args ...any) { +- if verbose { +- return func(format string, args ...any) { +- event.Log(ctx, name+": "+fmt.Sprintf(format, args...)) +- } +- } else { +- return func(string, ...any) {} +- } -} +diff -urN a/gopls/internal/golang/invertifcondition.go b/gopls/internal/golang/invertifcondition.go +--- a/gopls/internal/golang/invertifcondition.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/invertifcondition.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,267 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// The client capabilities of a {@link DocumentLinkRequest}. --type DocumentLinkClientCapabilities struct { -- // Whether document link supports dynamic registration. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -- // Whether the client supports the `tooltip` property on `DocumentLink`. -- // -- // @since 3.15.0 -- TooltipSupport bool `json:"tooltipSupport,omitempty"` --} +-package golang - --// Provider options for a {@link DocumentLinkRequest}. --type DocumentLinkOptions struct { -- // Document links have a resolve provider as well. -- ResolveProvider bool `json:"resolveProvider,omitempty"` -- WorkDoneProgressOptions --} +-import ( +- "fmt" +- "go/ast" +- "go/token" +- "go/types" +- "strings" - --// The parameters of a {@link DocumentLinkRequest}. --type DocumentLinkParams struct { -- // The document to provide document links for. -- TextDocument TextDocumentIdentifier `json:"textDocument"` -- WorkDoneProgressParams -- PartialResultParams --} +- "golang.org/x/tools/go/analysis" +- "golang.org/x/tools/go/ast/astutil" +- "golang.org/x/tools/gopls/internal/util/safetoken" +-) - --// Registration options for a {@link DocumentLinkRequest}. --type DocumentLinkRegistrationOptions struct { -- TextDocumentRegistrationOptions -- DocumentLinkOptions --} +-// invertIfCondition is a singleFileFixFunc that inverts an if/else statement +-func invertIfCondition(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, _ *types.Package, _ *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { +- ifStatement, _, err := CanInvertIfCondition(file, start, end) +- if err != nil { +- return nil, nil, err +- } - --// Client capabilities of a {@link DocumentOnTypeFormattingRequest}. --type DocumentOnTypeFormattingClientCapabilities struct { -- // Whether on type formatting supports dynamic registration. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` --} +- var replaceElse analysis.TextEdit - --// Provider options for a {@link DocumentOnTypeFormattingRequest}. --type DocumentOnTypeFormattingOptions struct { -- // A character on which formatting should be triggered, like `{`. -- FirstTriggerCharacter string `json:"firstTriggerCharacter"` -- // More trigger characters. -- MoreTriggerCharacter []string `json:"moreTriggerCharacter,omitempty"` --} +- endsWithReturn, err := endsWithReturn(ifStatement.Else) +- if err != nil { +- return nil, nil, err +- } - --// The parameters of a {@link DocumentOnTypeFormattingRequest}. --type DocumentOnTypeFormattingParams struct { -- // The document to format. -- TextDocument TextDocumentIdentifier `json:"textDocument"` -- // The position around which the on type formatting should happen. -- // This is not necessarily the exact position where the character denoted -- // by the property `ch` got typed. -- Position Position `json:"position"` -- // The character that has been typed that triggered the formatting -- // on type request. That is not necessarily the last character that -- // got inserted into the document since the client could auto insert -- // characters as well (e.g. like automatic brace completion). -- Ch string `json:"ch"` -- // The formatting options. -- Options FormattingOptions `json:"options"` --} +- if endsWithReturn { +- // Replace the whole else part with an empty line and an unindented +- // version of the original if body +- sourcePos := safetoken.StartPosition(fset, ifStatement.Pos()) - --// Registration options for a {@link DocumentOnTypeFormattingRequest}. --type DocumentOnTypeFormattingRegistrationOptions struct { -- TextDocumentRegistrationOptions -- DocumentOnTypeFormattingOptions --} +- indent := sourcePos.Column - 1 +- if indent < 0 { +- indent = 0 +- } - --// Client capabilities of a {@link DocumentRangeFormattingRequest}. --type DocumentRangeFormattingClientCapabilities struct { -- // Whether range formatting supports dynamic registration. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -- // Whether the client supports formatting multiple ranges at once. -- // -- // @since 3.18.0 -- // @proposed -- RangesSupport bool `json:"rangesSupport,omitempty"` --} +- standaloneBodyText := ifBodyToStandaloneCode(fset, ifStatement.Body, src) +- replaceElse = analysis.TextEdit{ +- Pos: ifStatement.Body.Rbrace + 1, // 1 == len("}") +- End: ifStatement.End(), +- NewText: []byte("\n\n" + strings.Repeat("\t", indent) + standaloneBodyText), +- } +- } else { +- // Replace the else body text with the if body text +- bodyStart := safetoken.StartPosition(fset, ifStatement.Body.Lbrace) +- bodyEnd := safetoken.EndPosition(fset, ifStatement.Body.Rbrace+1) // 1 == len("}") +- bodyText := src[bodyStart.Offset:bodyEnd.Offset] +- replaceElse = analysis.TextEdit{ +- Pos: ifStatement.Else.Pos(), +- End: ifStatement.Else.End(), +- NewText: bodyText, +- } +- } - --// Provider options for a {@link DocumentRangeFormattingRequest}. --type DocumentRangeFormattingOptions struct { -- // Whether the server supports formatting multiple ranges at once. -- // -- // @since 3.18.0 -- // @proposed -- RangesSupport bool `json:"rangesSupport,omitempty"` -- WorkDoneProgressOptions --} +- // Replace the if text with the else text +- elsePosInSource := safetoken.StartPosition(fset, ifStatement.Else.Pos()) +- elseEndInSource := safetoken.EndPosition(fset, ifStatement.Else.End()) +- elseText := src[elsePosInSource.Offset:elseEndInSource.Offset] +- replaceBodyWithElse := analysis.TextEdit{ +- Pos: ifStatement.Body.Pos(), +- End: ifStatement.Body.End(), +- NewText: elseText, +- } - --// The parameters of a {@link DocumentRangeFormattingRequest}. --type DocumentRangeFormattingParams struct { -- // The document to format. -- TextDocument TextDocumentIdentifier `json:"textDocument"` -- // The range to format -- Range Range `json:"range"` -- // The format options -- Options FormattingOptions `json:"options"` -- WorkDoneProgressParams --} +- // Replace the if condition with its inverse +- inverseCondition, err := invertCondition(fset, ifStatement.Cond, src) +- if err != nil { +- return nil, nil, err +- } +- replaceConditionWithInverse := analysis.TextEdit{ +- Pos: ifStatement.Cond.Pos(), +- End: ifStatement.Cond.End(), +- NewText: inverseCondition, +- } - --// Registration options for a {@link DocumentRangeFormattingRequest}. --type DocumentRangeFormattingRegistrationOptions struct { -- TextDocumentRegistrationOptions -- DocumentRangeFormattingOptions +- // Return a SuggestedFix with just that TextEdit in there +- return fset, &analysis.SuggestedFix{ +- TextEdits: []analysis.TextEdit{ +- replaceConditionWithInverse, +- replaceBodyWithElse, +- replaceElse, +- }, +- }, nil -} - --// The parameters of a {@link DocumentRangesFormattingRequest}. --// --// @since 3.18.0 --// @proposed --type DocumentRangesFormattingParams struct { -- // The document to format. -- TextDocument TextDocumentIdentifier `json:"textDocument"` -- // The ranges to format -- Ranges []Range `json:"ranges"` -- // The format options -- Options FormattingOptions `json:"options"` -- WorkDoneProgressParams +-func endsWithReturn(elseBranch ast.Stmt) (bool, error) { +- elseBlock, isBlockStatement := elseBranch.(*ast.BlockStmt) +- if !isBlockStatement { +- return false, fmt.Errorf("unable to figure out whether this ends with return: %T", elseBranch) +- } +- +- if len(elseBlock.List) == 0 { +- // Empty blocks don't end in returns +- return false, nil +- } +- +- lastStatement := elseBlock.List[len(elseBlock.List)-1] +- +- _, lastStatementIsReturn := lastStatement.(*ast.ReturnStmt) +- return lastStatementIsReturn, nil -} - --// A document selector is the combination of one or many document filters. --// --// @sample `let sel:DocumentSelector = [{ language: 'typescript' }, { language: 'json', pattern: '**∕tsconfig.json' }]`; +-// Turn { fmt.Println("Hello") } into just fmt.Println("Hello"), with one less +-// level of indentation. -// --// The use of a string as a document filter is deprecated @since 3.16.0. --type DocumentSelector = []DocumentFilter // (alias) line 14363 --// Represents programming constructs like variables, classes, interfaces etc. --// that appear in a document. Document symbols can be hierarchical and they --// have two ranges: one that encloses its definition and one that points to --// its most interesting range, e.g. the range of an identifier. --type DocumentSymbol struct { -- // The name of this symbol. Will be displayed in the user interface and therefore must not be -- // an empty string or a string only consisting of white spaces. -- Name string `json:"name"` -- // More detail for this symbol, e.g the signature of a function. -- Detail string `json:"detail,omitempty"` -- // The kind of this symbol. -- Kind SymbolKind `json:"kind"` -- // Tags for this document symbol. -- // -- // @since 3.16.0 -- Tags []SymbolTag `json:"tags,omitempty"` -- // Indicates if this symbol is deprecated. -- // -- // @deprecated Use tags instead -- Deprecated bool `json:"deprecated,omitempty"` -- // The range enclosing this symbol not including leading/trailing whitespace but everything else -- // like comments. This information is typically used to determine if the clients cursor is -- // inside the symbol to reveal in the symbol in the UI. -- Range Range `json:"range"` -- // The range that should be selected and revealed when this symbol is being picked, e.g the name of a function. -- // Must be contained by the `range`. -- SelectionRange Range `json:"selectionRange"` -- // Children of this symbol, e.g. properties of a class. -- Children []DocumentSymbol `json:"children,omitempty"` --} +-// The first line of the result will not be indented, but all of the following +-// lines will. +-func ifBodyToStandaloneCode(fset *token.FileSet, ifBody *ast.BlockStmt, src []byte) string { +- // Get the whole body (without the surrounding braces) as a string +- bodyStart := safetoken.StartPosition(fset, ifBody.Lbrace+1) // 1 == len("}") +- bodyEnd := safetoken.EndPosition(fset, ifBody.Rbrace) +- bodyWithoutBraces := string(src[bodyStart.Offset:bodyEnd.Offset]) +- bodyWithoutBraces = strings.TrimSpace(bodyWithoutBraces) - --// Client Capabilities for a {@link DocumentSymbolRequest}. --type DocumentSymbolClientCapabilities struct { -- // Whether document symbol supports dynamic registration. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -- // Specific capabilities for the `SymbolKind` in the -- // `textDocument/documentSymbol` request. -- SymbolKind *PSymbolKindPDocumentSymbol `json:"symbolKind,omitempty"` -- // The client supports hierarchical document symbols. -- HierarchicalDocumentSymbolSupport bool `json:"hierarchicalDocumentSymbolSupport,omitempty"` -- // The client supports tags on `SymbolInformation`. Tags are supported on -- // `DocumentSymbol` if `hierarchicalDocumentSymbolSupport` is set to true. -- // Clients supporting tags have to handle unknown tags gracefully. -- // -- // @since 3.16.0 -- TagSupport *PTagSupportPDocumentSymbol `json:"tagSupport,omitempty"` -- // The client supports an additional label presented in the UI when -- // registering a document symbol provider. -- // -- // @since 3.16.0 -- LabelSupport bool `json:"labelSupport,omitempty"` --} +- // Unindent +- bodyWithoutBraces = strings.ReplaceAll(bodyWithoutBraces, "\n\t", "\n") - --// Provider options for a {@link DocumentSymbolRequest}. --type DocumentSymbolOptions struct { -- // A human-readable string that is shown when multiple outlines trees -- // are shown for the same document. -- // -- // @since 3.16.0 -- Label string `json:"label,omitempty"` -- WorkDoneProgressOptions +- return bodyWithoutBraces -} - --// Parameters for a {@link DocumentSymbolRequest}. --type DocumentSymbolParams struct { -- // The text document. -- TextDocument TextDocumentIdentifier `json:"textDocument"` -- WorkDoneProgressParams -- PartialResultParams --} +-func invertCondition(fset *token.FileSet, cond ast.Expr, src []byte) ([]byte, error) { +- condStart := safetoken.StartPosition(fset, cond.Pos()) +- condEnd := safetoken.EndPosition(fset, cond.End()) +- oldText := string(src[condStart.Offset:condEnd.Offset]) - --// Registration options for a {@link DocumentSymbolRequest}. --type DocumentSymbolRegistrationOptions struct { -- TextDocumentRegistrationOptions -- DocumentSymbolOptions --} --type DocumentURI string +- switch expr := cond.(type) { +- case *ast.Ident, *ast.ParenExpr, *ast.CallExpr, *ast.StarExpr, *ast.IndexExpr, *ast.IndexListExpr, *ast.SelectorExpr: +- newText := "!" + oldText +- if oldText == "true" { +- newText = "false" +- } else if oldText == "false" { +- newText = "true" +- } - --// Predefined error codes. --type ErrorCodes int32 +- return []byte(newText), nil - --// The client capabilities of a {@link ExecuteCommandRequest}. --type ExecuteCommandClientCapabilities struct { -- // Execute command supports dynamic registration. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` --} +- case *ast.UnaryExpr: +- if expr.Op != token.NOT { +- // This should never happen +- return dumbInvert(fset, cond, src), nil +- } - --// The server capabilities of a {@link ExecuteCommandRequest}. --type ExecuteCommandOptions struct { -- // The commands to be executed on the server -- Commands []string `json:"commands"` -- WorkDoneProgressOptions --} +- inverse := expr.X +- if p, isParen := inverse.(*ast.ParenExpr); isParen { +- // We got !(x), remove the parentheses with the ! so we get just "x" +- inverse = p.X - --// The parameters of a {@link ExecuteCommandRequest}. --type ExecuteCommandParams struct { -- // The identifier of the actual command handler. -- Command string `json:"command"` -- // Arguments that the command should be invoked with. -- Arguments []json.RawMessage `json:"arguments,omitempty"` -- WorkDoneProgressParams --} +- start := safetoken.StartPosition(fset, inverse.Pos()) +- end := safetoken.EndPosition(fset, inverse.End()) +- if start.Line != end.Line { +- // The expression is multi-line, so we can't remove the parentheses +- inverse = expr.X +- } +- } - --// Registration options for a {@link ExecuteCommandRequest}. --type ExecuteCommandRegistrationOptions struct { -- ExecuteCommandOptions --} --type ExecutionSummary struct { -- // A strict monotonically increasing value -- // indicating the execution order of a cell -- // inside a notebook. -- ExecutionOrder uint32 `json:"executionOrder"` -- // Whether the execution was successful or -- // not if known by the client. -- Success bool `json:"success,omitempty"` --} +- start := safetoken.StartPosition(fset, inverse.Pos()) +- end := safetoken.EndPosition(fset, inverse.End()) +- textWithoutNot := src[start.Offset:end.Offset] - --// created for Literal (Lit_CodeActionClientCapabilities_codeActionLiteralSupport_codeActionKind) --type FCodeActionKindPCodeActionLiteralSupport struct { -- // The code action kind values the client supports. When this -- // property exists the client also guarantees that it will -- // handle values outside its set gracefully and falls back -- // to a default value when unknown. -- ValueSet []CodeActionKind `json:"valueSet"` --} +- return textWithoutNot, nil - --// created for Literal (Lit_CompletionList_itemDefaults_editRange_Item1) --type FEditRangePItemDefaults struct { -- Insert Range `json:"insert"` -- Replace Range `json:"replace"` --} +- case *ast.BinaryExpr: +- // These inversions are unsound for floating point NaN, but that's ok. +- negations := map[token.Token]string{ +- token.EQL: "!=", +- token.LSS: ">=", +- token.GTR: "<=", +- token.NEQ: "==", +- token.LEQ: ">", +- token.GEQ: "<", +- } - --// created for Literal (Lit_SemanticTokensClientCapabilities_requests_full_Item1) --type FFullPRequests struct { -- // The client will send the `textDocument/semanticTokens/full/delta` request if -- // the server provides a corresponding handler. -- Delta bool `json:"delta"` --} +- negation, negationFound := negations[expr.Op] +- if !negationFound { +- return invertAndOr(fset, expr, src) +- } - --// created for Literal (Lit_CompletionClientCapabilities_completionItem_insertTextModeSupport) --type FInsertTextModeSupportPCompletionItem struct { -- ValueSet []InsertTextMode `json:"valueSet"` --} +- xPosInSource := safetoken.StartPosition(fset, expr.X.Pos()) +- opPosInSource := safetoken.StartPosition(fset, expr.OpPos) +- yPosInSource := safetoken.StartPosition(fset, expr.Y.Pos()) - --// created for Literal (Lit_SignatureHelpClientCapabilities_signatureInformation_parameterInformation) --type FParameterInformationPSignatureInformation struct { -- // The client supports processing label offsets instead of a -- // simple label string. -- // -- // @since 3.14.0 -- LabelOffsetSupport bool `json:"labelOffsetSupport,omitempty"` --} +- textBeforeOp := string(src[xPosInSource.Offset:opPosInSource.Offset]) - --// created for Literal (Lit_SemanticTokensClientCapabilities_requests_range_Item1) --type FRangePRequests struct { --} +- oldOpWithTrailingWhitespace := string(src[opPosInSource.Offset:yPosInSource.Offset]) +- newOpWithTrailingWhitespace := negation + oldOpWithTrailingWhitespace[len(expr.Op.String()):] - --// created for Literal (Lit_CompletionClientCapabilities_completionItem_resolveSupport) --type FResolveSupportPCompletionItem struct { -- // The properties that a client can resolve lazily. -- Properties []string `json:"properties"` --} +- textAfterOp := string(src[yPosInSource.Offset:condEnd.Offset]) - --// created for Literal (Lit_NotebookDocumentChangeEvent_cells_structure) --type FStructurePCells struct { -- // The change to the cell array. -- Array NotebookCellArrayChange `json:"array"` -- // Additional opened cell text documents. -- DidOpen []TextDocumentItem `json:"didOpen,omitempty"` -- // Additional closed cell text documents. -- DidClose []TextDocumentIdentifier `json:"didClose,omitempty"` +- return []byte(textBeforeOp + newOpWithTrailingWhitespace + textAfterOp), nil +- } +- +- return dumbInvert(fset, cond, src), nil -} - --// created for Literal (Lit_CompletionClientCapabilities_completionItem_tagSupport) --type FTagSupportPCompletionItem struct { -- // The tags supported by the client. -- ValueSet []CompletionItemTag `json:"valueSet"` +-// dumbInvert is a fallback, inverting cond into !(cond). +-func dumbInvert(fset *token.FileSet, expr ast.Expr, src []byte) []byte { +- start := safetoken.StartPosition(fset, expr.Pos()) +- end := safetoken.EndPosition(fset, expr.End()) +- text := string(src[start.Offset:end.Offset]) +- return []byte("!(" + text + ")") -} --type FailureHandlingKind string - --// The file event type --type FileChangeType uint32 +-func invertAndOr(fset *token.FileSet, expr *ast.BinaryExpr, src []byte) ([]byte, error) { +- if expr.Op != token.LAND && expr.Op != token.LOR { +- // Neither AND nor OR, don't know how to invert this +- return dumbInvert(fset, expr, src), nil +- } - --// Represents information on a file/folder create. --// --// @since 3.16.0 --type FileCreate struct { -- // A file:// URI for the location of the file/folder being created. -- URI string `json:"uri"` --} +- oppositeOp := "&&" +- if expr.Op == token.LAND { +- oppositeOp = "||" +- } - --// Represents information on a file/folder delete. --// --// @since 3.16.0 --type FileDelete struct { -- // A file:// URI for the location of the file/folder being deleted. -- URI string `json:"uri"` --} +- xEndInSource := safetoken.EndPosition(fset, expr.X.End()) +- opPosInSource := safetoken.StartPosition(fset, expr.OpPos) +- whitespaceAfterBefore := src[xEndInSource.Offset:opPosInSource.Offset] - --// An event describing a file change. --type FileEvent struct { -- // The file's uri. -- URI DocumentURI `json:"uri"` -- // The change type. -- Type FileChangeType `json:"type"` --} +- invertedBefore, err := invertCondition(fset, expr.X, src) +- if err != nil { +- return nil, err +- } - --// Capabilities relating to events from file operations by the user in the client. --// --// These events do not come from the file system, they come from user operations --// like renaming a file in the UI. --// --// @since 3.16.0 --type FileOperationClientCapabilities struct { -- // Whether the client supports dynamic registration for file requests/notifications. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -- // The client has support for sending didCreateFiles notifications. -- DidCreate bool `json:"didCreate,omitempty"` -- // The client has support for sending willCreateFiles requests. -- WillCreate bool `json:"willCreate,omitempty"` -- // The client has support for sending didRenameFiles notifications. -- DidRename bool `json:"didRename,omitempty"` -- // The client has support for sending willRenameFiles requests. -- WillRename bool `json:"willRename,omitempty"` -- // The client has support for sending didDeleteFiles notifications. -- DidDelete bool `json:"didDelete,omitempty"` -- // The client has support for sending willDeleteFiles requests. -- WillDelete bool `json:"willDelete,omitempty"` --} +- invertedAfter, err := invertCondition(fset, expr.Y, src) +- if err != nil { +- return nil, err +- } - --// A filter to describe in which file operation requests or notifications --// the server is interested in receiving. --// --// @since 3.16.0 --type FileOperationFilter struct { -- // A Uri scheme like `file` or `untitled`. -- Scheme string `json:"scheme,omitempty"` -- // The actual file operation pattern. -- Pattern FileOperationPattern `json:"pattern"` --} +- yPosInSource := safetoken.StartPosition(fset, expr.Y.Pos()) - --// Options for notifications/requests for user operations on files. --// --// @since 3.16.0 --type FileOperationOptions struct { -- // The server is interested in receiving didCreateFiles notifications. -- DidCreate *FileOperationRegistrationOptions `json:"didCreate,omitempty"` -- // The server is interested in receiving willCreateFiles requests. -- WillCreate *FileOperationRegistrationOptions `json:"willCreate,omitempty"` -- // The server is interested in receiving didRenameFiles notifications. -- DidRename *FileOperationRegistrationOptions `json:"didRename,omitempty"` -- // The server is interested in receiving willRenameFiles requests. -- WillRename *FileOperationRegistrationOptions `json:"willRename,omitempty"` -- // The server is interested in receiving didDeleteFiles file notifications. -- DidDelete *FileOperationRegistrationOptions `json:"didDelete,omitempty"` -- // The server is interested in receiving willDeleteFiles file requests. -- WillDelete *FileOperationRegistrationOptions `json:"willDelete,omitempty"` --} +- oldOpWithTrailingWhitespace := string(src[opPosInSource.Offset:yPosInSource.Offset]) +- newOpWithTrailingWhitespace := oppositeOp + oldOpWithTrailingWhitespace[len(expr.Op.String()):] - --// A pattern to describe in which file operation requests or notifications --// the server is interested in receiving. --// --// @since 3.16.0 --type FileOperationPattern struct { -- // The glob pattern to match. Glob patterns can have the following syntax: -- // -- // - `*` to match one or more characters in a path segment -- // - `?` to match on one character in a path segment -- // - `**` to match any number of path segments, including none -- // - `{}` to group sub patterns into an OR expression. (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) -- // - `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) -- // - `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) -- Glob string `json:"glob"` -- // Whether to match files or folders with this pattern. -- // -- // Matches both if undefined. -- Matches *FileOperationPatternKind `json:"matches,omitempty"` -- // Additional options used during matching. -- Options *FileOperationPatternOptions `json:"options,omitempty"` +- return []byte(string(invertedBefore) + string(whitespaceAfterBefore) + newOpWithTrailingWhitespace + string(invertedAfter)), nil -} - --// A pattern kind describing if a glob pattern matches a file a folder or --// both. --// --// @since 3.16.0 --type FileOperationPatternKind string +-// CanInvertIfCondition reports whether we can do invert-if-condition on the +-// code in the given range +-func CanInvertIfCondition(file *ast.File, start, end token.Pos) (*ast.IfStmt, bool, error) { +- path, _ := astutil.PathEnclosingInterval(file, start, end) +- for _, node := range path { +- stmt, isIfStatement := node.(*ast.IfStmt) +- if !isIfStatement { +- continue +- } - --// Matching options for the file operation pattern. --// --// @since 3.16.0 --type FileOperationPatternOptions struct { -- // The pattern should be matched ignoring casing. -- IgnoreCase bool `json:"ignoreCase,omitempty"` --} +- if stmt.Else == nil { +- // Can't invert conditions without else clauses +- return nil, false, fmt.Errorf("else clause required") +- } - --// The options to register for file operations. --// --// @since 3.16.0 --type FileOperationRegistrationOptions struct { -- // The actual filters. -- Filters []FileOperationFilter `json:"filters"` --} +- if _, hasElseIf := stmt.Else.(*ast.IfStmt); hasElseIf { +- // Can't invert conditions with else-if clauses, unclear what that +- // would look like +- return nil, false, fmt.Errorf("else-if not supported") +- } - --// Represents information on a file/folder rename. --// --// @since 3.16.0 --type FileRename struct { -- // A file:// URI for the original location of the file/folder being renamed. -- OldURI string `json:"oldUri"` -- // A file:// URI for the new location of the file/folder being renamed. -- NewURI string `json:"newUri"` --} --type FileSystemWatcher struct { -- // The glob pattern to watch. See {@link GlobPattern glob pattern} for more detail. -- // -- // @since 3.17.0 support for relative patterns. -- GlobPattern GlobPattern `json:"globPattern"` -- // The kind of events of interest. If omitted it defaults -- // to WatchKind.Create | WatchKind.Change | WatchKind.Delete -- // which is 7. -- Kind *WatchKind `json:"kind,omitempty"` --} -- --// Represents a folding range. To be valid, start and end line must be bigger than zero and smaller --// than the number of lines in the document. Clients are free to ignore invalid ranges. --type FoldingRange struct { -- // The zero-based start line of the range to fold. The folded area starts after the line's last character. -- // To be valid, the end must be zero or larger and smaller than the number of lines in the document. -- StartLine uint32 `json:"startLine"` -- // The zero-based character offset from where the folded range starts. If not defined, defaults to the length of the start line. -- StartCharacter uint32 `json:"startCharacter,omitempty"` -- // The zero-based end line of the range to fold. The folded area ends with the line's last character. -- // To be valid, the end must be zero or larger and smaller than the number of lines in the document. -- EndLine uint32 `json:"endLine"` -- // The zero-based character offset before the folded range ends. If not defined, defaults to the length of the end line. -- EndCharacter uint32 `json:"endCharacter,omitempty"` -- // Describes the kind of the folding range such as `comment' or 'region'. The kind -- // is used to categorize folding ranges and used by commands like 'Fold all comments'. -- // See {@link FoldingRangeKind} for an enumeration of standardized kinds. -- Kind string `json:"kind,omitempty"` -- // The text that the client should show when the specified range is -- // collapsed. If not defined or not supported by the client, a default -- // will be chosen by the client. -- // -- // @since 3.17.0 -- CollapsedText string `json:"collapsedText,omitempty"` --} --type FoldingRangeClientCapabilities struct { -- // Whether implementation supports dynamic registration for folding range -- // providers. If this is set to `true` the client supports the new -- // `FoldingRangeRegistrationOptions` return value for the corresponding -- // server capability as well. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -- // The maximum number of folding ranges that the client prefers to receive -- // per document. The value serves as a hint, servers are free to follow the -- // limit. -- RangeLimit uint32 `json:"rangeLimit,omitempty"` -- // If set, the client signals that it only supports folding complete lines. -- // If set, client will ignore specified `startCharacter` and `endCharacter` -- // properties in a FoldingRange. -- LineFoldingOnly bool `json:"lineFoldingOnly,omitempty"` -- // Specific options for the folding range kind. -- // -- // @since 3.17.0 -- FoldingRangeKind *PFoldingRangeKindPFoldingRange `json:"foldingRangeKind,omitempty"` -- // Specific options for the folding range. -- // -- // @since 3.17.0 -- FoldingRange *PFoldingRangePFoldingRange `json:"foldingRange,omitempty"` +- return stmt, true, nil +- } +- +- return nil, false, fmt.Errorf("not an if statement") -} +diff -urN a/gopls/internal/golang/known_packages.go b/gopls/internal/golang/known_packages.go +--- a/gopls/internal/golang/known_packages.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/known_packages.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,137 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// A set of predefined range kinds. --type FoldingRangeKind string --type FoldingRangeOptions struct { -- WorkDoneProgressOptions --} +-package golang - --// Parameters for a {@link FoldingRangeRequest}. --type FoldingRangeParams struct { -- // The text document. -- TextDocument TextDocumentIdentifier `json:"textDocument"` -- WorkDoneProgressParams -- PartialResultParams --} --type FoldingRangeRegistrationOptions struct { -- TextDocumentRegistrationOptions -- FoldingRangeOptions -- StaticRegistrationOptions --} +-import ( +- "context" +- "go/parser" +- "go/token" +- "sort" +- "strings" +- "sync" +- "time" - --// Value-object describing what options formatting should use. --type FormattingOptions struct { -- // Size of a tab in spaces. -- TabSize uint32 `json:"tabSize"` -- // Prefer spaces over tabs. -- InsertSpaces bool `json:"insertSpaces"` -- // Trim trailing whitespace on a line. -- // -- // @since 3.15.0 -- TrimTrailingWhitespace bool `json:"trimTrailingWhitespace,omitempty"` -- // Insert a newline character at the end of the file if one does not exist. -- // -- // @since 3.15.0 -- InsertFinalNewline bool `json:"insertFinalNewline,omitempty"` -- // Trim all newlines after the final newline at the end of the file. -- // -- // @since 3.15.0 -- TrimFinalNewlines bool `json:"trimFinalNewlines,omitempty"` --} +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/imports" +-) - --// A diagnostic report with a full set of problems. +-// KnownPackagePaths returns a new list of package paths of all known +-// packages in the package graph that could potentially be imported by +-// the given file. The list is ordered lexicographically, except that +-// all dot-free paths (standard packages) appear before dotful ones. -// --// @since 3.17.0 --type FullDocumentDiagnosticReport struct { -- // A full document diagnostic report. -- Kind string `json:"kind"` -- // An optional result id. If provided it will -- // be sent on the next diagnostic request for the -- // same document. -- ResultID string `json:"resultId,omitempty"` -- // The actual items. -- Items []Diagnostic `json:"items"` --} +-// It is part of the gopls.list_known_packages command. +-func KnownPackagePaths(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]PackagePath, error) { +- // This algorithm is expressed in terms of Metadata, not Packages, +- // so it doesn't cause or wait for type checking. - --// General client capabilities. --// --// @since 3.16.0 --type GeneralClientCapabilities struct { -- // Client capability that signals how the client -- // handles stale requests (e.g. a request -- // for which the client will not process the response -- // anymore since the information is outdated). -- // -- // @since 3.17.0 -- StaleRequestSupport *PStaleRequestSupportPGeneral `json:"staleRequestSupport,omitempty"` -- // Client capabilities specific to regular expressions. -- // -- // @since 3.16.0 -- RegularExpressions *RegularExpressionsClientCapabilities `json:"regularExpressions,omitempty"` -- // Client capabilities specific to the client's markdown parser. -- // -- // @since 3.16.0 -- Markdown *MarkdownClientCapabilities `json:"markdown,omitempty"` -- // The position encodings supported by the client. Client and server -- // have to agree on the same position encoding to ensure that offsets -- // (e.g. character position in a line) are interpreted the same on both -- // sides. -- // -- // To keep the protocol backwards compatible the following applies: if -- // the value 'utf-16' is missing from the array of position encodings -- // servers can assume that the client supports UTF-16. UTF-16 is -- // therefore a mandatory encoding. -- // -- // If omitted it defaults to ['utf-16']. -- // -- // Implementation considerations: since the conversion from one encoding -- // into another requires the content of the file / line the conversion -- // is best done where the file is read which is usually on the server -- // side. -- // -- // @since 3.17.0 -- PositionEncodings []PositionEncodingKind `json:"positionEncodings,omitempty"` +- current, err := NarrowestMetadataForFile(ctx, snapshot, fh.URI()) +- if err != nil { +- return nil, err // e.g. context cancelled +- } +- +- // Parse the file's imports so we can compute which +- // PackagePaths are imported by this specific file. +- src, err := fh.Content() +- if err != nil { +- return nil, err +- } +- file, err := parser.ParseFile(token.NewFileSet(), fh.URI().Path(), src, parser.ImportsOnly) +- if err != nil { +- return nil, err +- } +- imported := make(map[PackagePath]bool) +- for _, imp := range file.Imports { +- if id := current.DepsByImpPath[metadata.UnquoteImportPath(imp)]; id != "" { +- if mp := snapshot.Metadata(id); mp != nil { +- imported[mp.PkgPath] = true +- } +- } +- } +- +- // Now find candidates among all known packages. +- knownPkgs, err := snapshot.AllMetadata(ctx) +- if err != nil { +- return nil, err +- } +- seen := make(map[PackagePath]bool) +- for _, knownPkg := range knownPkgs { +- // package main cannot be imported +- if knownPkg.Name == "main" { +- continue +- } +- // test packages cannot be imported +- if knownPkg.ForTest != "" { +- continue +- } +- // No need to import what the file already imports. +- // This check is based on PackagePath, not PackageID, +- // so that all test variants are filtered out too. +- if imported[knownPkg.PkgPath] { +- continue +- } +- // make sure internal packages are importable by the file +- if !metadata.IsValidImport(current.PkgPath, knownPkg.PkgPath) { +- continue +- } +- // naive check on cyclical imports +- if isDirectlyCyclical(current, knownPkg) { +- continue +- } +- // AllMetadata may have multiple variants of a pkg. +- seen[knownPkg.PkgPath] = true +- } +- +- // Augment the set by invoking the goimports algorithm. +- if err := snapshot.RunProcessEnvFunc(ctx, func(ctx context.Context, o *imports.Options) error { +- ctx, cancel := context.WithTimeout(ctx, time.Millisecond*80) +- defer cancel() +- var seenMu sync.Mutex +- wrapped := func(ifix imports.ImportFix) { +- seenMu.Lock() +- defer seenMu.Unlock() +- // TODO(adonovan): what if the actual package path has a vendor/ prefix? +- seen[PackagePath(ifix.StmtInfo.ImportPath)] = true +- } +- return imports.GetAllCandidates(ctx, wrapped, "", fh.URI().Path(), string(current.Name), o.Env) +- }); err != nil { +- // If goimports failed, proceed with just the candidates from the metadata. +- event.Error(ctx, "imports.GetAllCandidates", err) +- } +- +- // Sort lexicographically, but with std before non-std packages. +- paths := make([]PackagePath, 0, len(seen)) +- for path := range seen { +- paths = append(paths, path) +- } +- sort.Slice(paths, func(i, j int) bool { +- importI, importJ := paths[i], paths[j] +- iHasDot := strings.Contains(string(importI), ".") +- jHasDot := strings.Contains(string(importJ), ".") +- if iHasDot != jHasDot { +- return jHasDot // dot-free paths (standard packages) compare less +- } +- return importI < importJ +- }) +- +- return paths, nil -} - --// The glob pattern. Either a string pattern or a relative pattern. +-// isDirectlyCyclical checks if imported directly imports pkg. +-// It does not (yet) offer a full cyclical check because showing a user +-// a list of importable packages already generates a very large list +-// and having a few false positives in there could be worth the +-// performance snappiness. -// --// @since 3.17.0 --type GlobPattern = string // (alias) line 14542 --// The result of a hover request. --type Hover struct { -- // The hover's content -- Contents MarkupContent `json:"contents"` -- // An optional range inside the text document that is used to -- // visualize the hover, e.g. by changing the background color. -- Range Range `json:"range,omitempty"` --} --type HoverClientCapabilities struct { -- // Whether hover supports dynamic registration. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -- // Client supports the following content formats for the content -- // property. The order describes the preferred format of the client. -- ContentFormat []MarkupKind `json:"contentFormat,omitempty"` +-// TODO(adonovan): ensure that metadata graph is always cyclic! +-// Many algorithms will get confused or even stuck in the +-// presence of cycles. Then replace this function by 'false'. +-func isDirectlyCyclical(pkg, imported *metadata.Package) bool { +- _, ok := imported.DepsByPkgPath[pkg.PkgPath] +- return ok -} +diff -urN a/gopls/internal/golang/lines.go b/gopls/internal/golang/lines.go +--- a/gopls/internal/golang/lines.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/lines.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,261 +0,0 @@ +-// Copyright 2024 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// Hover options. --type HoverOptions struct { -- WorkDoneProgressOptions --} +-package golang - --// Parameters for a {@link HoverRequest}. --type HoverParams struct { -- TextDocumentPositionParams -- WorkDoneProgressParams --} +-// This file defines refactorings for splitting lists of elements +-// (arguments, literals, etc) across multiple lines, and joining +-// them into a single line. - --// Registration options for a {@link HoverRequest}. --type HoverRegistrationOptions struct { -- TextDocumentRegistrationOptions -- HoverOptions --} +-import ( +- "bytes" +- "go/ast" +- "go/token" +- "go/types" +- "sort" +- "strings" - --// @since 3.6.0 --type ImplementationClientCapabilities struct { -- // Whether implementation supports dynamic registration. If this is set to `true` -- // the client supports the new `ImplementationRegistrationOptions` return value -- // for the corresponding server capability as well. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -- // The client supports additional metadata in the form of definition links. -- // -- // @since 3.14.0 -- LinkSupport bool `json:"linkSupport,omitempty"` --} --type ImplementationOptions struct { -- WorkDoneProgressOptions --} --type ImplementationParams struct { -- TextDocumentPositionParams -- WorkDoneProgressParams -- PartialResultParams --} --type ImplementationRegistrationOptions struct { -- TextDocumentRegistrationOptions -- ImplementationOptions -- StaticRegistrationOptions --} +- "golang.org/x/tools/go/analysis" +- "golang.org/x/tools/go/ast/astutil" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/gopls/internal/util/slices" +-) - --// The data type of the ResponseError if the --// initialize request fails. --type InitializeError struct { -- // Indicates whether the client execute the following retry logic: -- // (1) show the message provided by the ResponseError to the user -- // (2) user selects retry or cancel -- // (3) if user selected retry the initialize method is sent again. -- Retry bool `json:"retry"` --} --type InitializeParams struct { -- XInitializeParams -- WorkspaceFoldersInitializeParams --} +-// CanSplitLines checks whether we can split lists of elements inside an enclosing curly bracket/parens into separate +-// lines. +-func CanSplitLines(file *ast.File, fset *token.FileSet, start, end token.Pos) (string, bool, error) { +- itemType, items, comments, _, _, _ := findSplitJoinTarget(fset, file, nil, start, end) +- if itemType == "" { +- return "", false, nil +- } - --// The result returned from an initialize request. --type InitializeResult struct { -- // The capabilities the language server provides. -- Capabilities ServerCapabilities `json:"capabilities"` -- // Information about the server. -- // -- // @since 3.15.0 -- ServerInfo *PServerInfoMsg_initialize `json:"serverInfo,omitempty"` --} --type InitializedParams struct { --} +- if !canSplitJoinLines(items, comments) { +- return "", false, nil +- } - --// Inlay hint information. --// --// @since 3.17.0 --type InlayHint struct { -- // The position of this hint. -- Position Position `json:"position"` -- // The label of this hint. A human readable string or an array of -- // InlayHintLabelPart label parts. -- // -- // *Note* that neither the string nor the label part can be empty. -- Label []InlayHintLabelPart `json:"label"` -- // The kind of this hint. Can be omitted in which case the client -- // should fall back to a reasonable default. -- Kind InlayHintKind `json:"kind,omitempty"` -- // Optional text edits that are performed when accepting this inlay hint. -- // -- // *Note* that edits are expected to change the document so that the inlay -- // hint (or its nearest variant) is now part of the document and the inlay -- // hint itself is now obsolete. -- TextEdits []TextEdit `json:"textEdits,omitempty"` -- // The tooltip text when you hover over this item. -- Tooltip *OrPTooltip_textDocument_inlayHint `json:"tooltip,omitempty"` -- // Render padding before the hint. -- // -- // Note: Padding should use the editor's background color, not the -- // background color of the hint itself. That means padding can be used -- // to visually align/separate an inlay hint. -- PaddingLeft bool `json:"paddingLeft,omitempty"` -- // Render padding after the hint. -- // -- // Note: Padding should use the editor's background color, not the -- // background color of the hint itself. That means padding can be used -- // to visually align/separate an inlay hint. -- PaddingRight bool `json:"paddingRight,omitempty"` -- // A data entry field that is preserved on an inlay hint between -- // a `textDocument/inlayHint` and a `inlayHint/resolve` request. -- Data interface{} `json:"data,omitempty"` --} +- for i := 1; i < len(items); i++ { +- prevLine := safetoken.EndPosition(fset, items[i-1].End()).Line +- curLine := safetoken.StartPosition(fset, items[i].Pos()).Line +- if prevLine == curLine { +- return "Split " + itemType + " into separate lines", true, nil +- } +- } - --// Inlay hint client capabilities. --// --// @since 3.17.0 --type InlayHintClientCapabilities struct { -- // Whether inlay hints support dynamic registration. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -- // Indicates which properties a client can resolve lazily on an inlay -- // hint. -- ResolveSupport *PResolveSupportPInlayHint `json:"resolveSupport,omitempty"` +- return "", false, nil -} - --// Inlay hint kinds. --// --// @since 3.17.0 --type InlayHintKind uint32 +-// CanJoinLines checks whether we can join lists of elements inside an enclosing curly bracket/parens into a single line. +-func CanJoinLines(file *ast.File, fset *token.FileSet, start, end token.Pos) (string, bool, error) { +- itemType, items, comments, _, _, _ := findSplitJoinTarget(fset, file, nil, start, end) +- if itemType == "" { +- return "", false, nil +- } - --// An inlay hint label part allows for interactive and composite labels --// of inlay hints. --// --// @since 3.17.0 --type InlayHintLabelPart struct { -- // The value of this label part. -- Value string `json:"value"` -- // The tooltip text when you hover over this label part. Depending on -- // the client capability `inlayHint.resolveSupport` clients might resolve -- // this property late using the resolve request. -- Tooltip *OrPTooltipPLabel `json:"tooltip,omitempty"` -- // An optional source code location that represents this -- // label part. -- // -- // The editor will use this location for the hover and for code navigation -- // features: This part will become a clickable link that resolves to the -- // definition of the symbol at the given location (not necessarily the -- // location itself), it shows the hover that shows at the given location, -- // and it shows a context menu with further code navigation commands. -- // -- // Depending on the client capability `inlayHint.resolveSupport` clients -- // might resolve this property late using the resolve request. -- Location *Location `json:"location,omitempty"` -- // An optional command for this label part. -- // -- // Depending on the client capability `inlayHint.resolveSupport` clients -- // might resolve this property late using the resolve request. -- Command *Command `json:"command,omitempty"` --} +- if !canSplitJoinLines(items, comments) { +- return "", false, nil +- } - --// Inlay hint options used during static registration. --// --// @since 3.17.0 --type InlayHintOptions struct { -- // The server provides support to resolve additional -- // information for an inlay hint item. -- ResolveProvider bool `json:"resolveProvider,omitempty"` -- WorkDoneProgressOptions --} +- for i := 1; i < len(items); i++ { +- prevLine := safetoken.EndPosition(fset, items[i-1].End()).Line +- curLine := safetoken.StartPosition(fset, items[i].Pos()).Line +- if prevLine != curLine { +- return "Join " + itemType + " into one line", true, nil +- } +- } - --// A parameter literal used in inlay hint requests. --// --// @since 3.17.0 --type InlayHintParams struct { -- // The text document. -- TextDocument TextDocumentIdentifier `json:"textDocument"` -- // The document range for which inlay hints should be computed. -- Range Range `json:"range"` -- WorkDoneProgressParams +- return "", false, nil -} - --// Inlay hint options used during static or dynamic registration. --// --// @since 3.17.0 --type InlayHintRegistrationOptions struct { -- InlayHintOptions -- TextDocumentRegistrationOptions -- StaticRegistrationOptions --} +-// canSplitJoinLines determines whether we should split/join the lines or not. +-func canSplitJoinLines(items []ast.Node, comments []*ast.CommentGroup) bool { +- if len(items) <= 1 { +- return false +- } - --// Client workspace capabilities specific to inlay hints. --// --// @since 3.17.0 --type InlayHintWorkspaceClientCapabilities struct { -- // Whether the client implementation supports a refresh request sent from -- // the server to the client. -- // -- // Note that this event is global and will force the client to refresh all -- // inlay hints currently shown. It should be used with absolute care and -- // is useful for situation where a server for example detects a project wide -- // change that requires such a calculation. -- RefreshSupport bool `json:"refreshSupport,omitempty"` --} +- for _, cg := range comments { +- if !strings.HasPrefix(cg.List[0].Text, "/*") { +- return false // can't split/join lists containing "//" comments +- } +- } - --// Client capabilities specific to inline completions. --// --// @since 3.18.0 --// @proposed --type InlineCompletionClientCapabilities struct { -- // Whether implementation supports dynamic registration for inline completion providers. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +- return true -} - --// Provides information about the context in which an inline completion was requested. --// --// @since 3.18.0 --// @proposed --type InlineCompletionContext struct { -- // Describes how the inline completion was triggered. -- TriggerKind InlineCompletionTriggerKind `json:"triggerKind"` -- // Provides information about the currently selected item in the autocomplete widget if it is visible. -- SelectedCompletionInfo *SelectedCompletionInfo `json:"selectedCompletionInfo,omitempty"` --} +-// splitLines is a singleFile fixer. +-func splitLines(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, _ *types.Package, _ *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { +- itemType, items, comments, indent, braceOpen, braceClose := findSplitJoinTarget(fset, file, src, start, end) +- if itemType == "" { +- return nil, nil, nil // no fix available +- } - --// An inline completion item represents a text snippet that is proposed inline to complete text that is being typed. --// --// @since 3.18.0 --// @proposed --type InlineCompletionItem struct { -- // The text to replace the range with. Must be set. -- InsertText Or_InlineCompletionItem_insertText `json:"insertText"` -- // A text that is used to decide if this inline completion should be shown. When `falsy` the {@link InlineCompletionItem.insertText} is used. -- FilterText string `json:"filterText,omitempty"` -- // The range to replace. Must begin and end on the same line. -- Range *Range `json:"range,omitempty"` -- // An optional {@link Command} that is executed *after* inserting this completion. -- Command *Command `json:"command,omitempty"` +- return fset, processLines(fset, items, comments, src, braceOpen, braceClose, ",\n", "\n", ",\n"+indent, indent+"\t"), nil -} - --// Represents a collection of {@link InlineCompletionItem inline completion items} to be presented in the editor. --// --// @since 3.18.0 --// @proposed --type InlineCompletionList struct { -- // The inline completion items -- Items []InlineCompletionItem `json:"items"` +-// joinLines is a singleFile fixer. +-func joinLines(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, _ *types.Package, _ *types.Info) (*token.FileSet, *analysis.SuggestedFix, error) { +- itemType, items, comments, _, braceOpen, braceClose := findSplitJoinTarget(fset, file, src, start, end) +- if itemType == "" { +- return nil, nil, nil // no fix available +- } +- +- return fset, processLines(fset, items, comments, src, braceOpen, braceClose, ", ", "", "", ""), nil -} - --// Inline completion options used during static registration. --// --// @since 3.18.0 --// @proposed --type InlineCompletionOptions struct { -- WorkDoneProgressOptions --} +-// processLines is the common operation for both split and join lines because this split/join operation is +-// essentially a transformation of the separating whitespace. +-func processLines(fset *token.FileSet, items []ast.Node, comments []*ast.CommentGroup, src []byte, braceOpen, braceClose token.Pos, sep, prefix, suffix, indent string) *analysis.SuggestedFix { +- nodes := slices.Clone(items) - --// A parameter literal used in inline completion requests. --// --// @since 3.18.0 --// @proposed --type InlineCompletionParams struct { -- // Additional information about the context in which inline completions were -- // requested. -- Context InlineCompletionContext `json:"context"` -- TextDocumentPositionParams -- WorkDoneProgressParams --} +- // box *ast.CommentGroup to ast.Node for easier processing later. +- for _, cg := range comments { +- nodes = append(nodes, cg) +- } - --// Inline completion options used during static or dynamic registration. --// --// @since 3.18.0 --// @proposed --type InlineCompletionRegistrationOptions struct { -- InlineCompletionOptions -- TextDocumentRegistrationOptions -- StaticRegistrationOptions --} +- // Sort to interleave comments and nodes. +- sort.Slice(nodes, func(i, j int) bool { +- return nodes[i].Pos() < nodes[j].Pos() +- }) - --// Describes how an {@link InlineCompletionItemProvider inline completion provider} was triggered. --// --// @since 3.18.0 --// @proposed --type InlineCompletionTriggerKind uint32 +- edits := []analysis.TextEdit{ +- { +- Pos: token.Pos(int(braceOpen) + len("{")), +- End: nodes[0].Pos(), +- NewText: []byte(prefix + indent), +- }, +- { +- Pos: nodes[len(nodes)-1].End(), +- End: braceClose, +- NewText: []byte(suffix), +- }, +- } - --// Inline value information can be provided by different means: --// --// - directly as a text value (class InlineValueText). --// - as a name to use for a variable lookup (class InlineValueVariableLookup) --// - as an evaluatable expression (class InlineValueEvaluatableExpression) --// --// The InlineValue types combines all inline value types into one type. --// --// @since 3.17.0 --type InlineValue = Or_InlineValue // (alias) line 14276 --// Client capabilities specific to inline values. --// --// @since 3.17.0 --type InlineValueClientCapabilities struct { -- // Whether implementation supports dynamic registration for inline value providers. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` --} +- for i := 1; i < len(nodes); i++ { +- pos, end := nodes[i-1].End(), nodes[i].Pos() +- if pos > end { +- // this will happen if we have a /*-style comment inside of a Field +- // e.g. `a /*comment here */ int` +- // +- // we will ignore as we only care about finding the field delimiter. +- continue +- } - --// @since 3.17.0 --type InlineValueContext struct { -- // The stack frame (as a DAP Id) where the execution has stopped. -- FrameID int32 `json:"frameId"` -- // The document range where execution has stopped. -- // Typically the end position of the range denotes the line where the inline values are shown. -- StoppedLocation Range `json:"stoppedLocation"` --} +- // at this point, the `,` token in between 2 nodes here must be the field delimiter. +- posOffset := safetoken.EndPosition(fset, pos).Offset +- endOffset := safetoken.StartPosition(fset, end).Offset +- if bytes.IndexByte(src[posOffset:endOffset], ',') == -1 { +- // nodes[i] or nodes[i-1] is a comment hence no delimiter in between +- // in such case, do nothing. +- continue +- } - --// Provide an inline value through an expression evaluation. --// If only a range is specified, the expression will be extracted from the underlying document. --// An optional expression can be used to override the extracted expression. --// --// @since 3.17.0 --type InlineValueEvaluatableExpression struct { -- // The document range for which the inline value applies. -- // The range is used to extract the evaluatable expression from the underlying document. -- Range Range `json:"range"` -- // If specified the expression overrides the extracted expression. -- Expression string `json:"expression,omitempty"` --} +- edits = append(edits, analysis.TextEdit{Pos: pos, End: end, NewText: []byte(sep + indent)}) +- } - --// Inline value options used during static registration. --// --// @since 3.17.0 --type InlineValueOptions struct { -- WorkDoneProgressOptions +- return &analysis.SuggestedFix{TextEdits: edits} -} - --// A parameter literal used in inline value requests. --// --// @since 3.17.0 --type InlineValueParams struct { -- // The text document. -- TextDocument TextDocumentIdentifier `json:"textDocument"` -- // The document range for which inline values should be computed. -- Range Range `json:"range"` -- // Additional information about the context in which inline values were -- // requested. -- Context InlineValueContext `json:"context"` -- WorkDoneProgressParams --} +-// findSplitJoinTarget returns the first curly bracket/parens that encloses the current cursor. +-func findSplitJoinTarget(fset *token.FileSet, file *ast.File, src []byte, start, end token.Pos) (itemType string, items []ast.Node, comments []*ast.CommentGroup, indent string, open, close token.Pos) { +- isCursorInside := func(nodePos, nodeEnd token.Pos) bool { +- return nodePos < start && end < nodeEnd +- } - --// Inline value options used during static or dynamic registration. --// --// @since 3.17.0 --type InlineValueRegistrationOptions struct { -- InlineValueOptions -- TextDocumentRegistrationOptions -- StaticRegistrationOptions --} +- findTarget := func() (targetType string, target ast.Node, open, close token.Pos) { +- path, _ := astutil.PathEnclosingInterval(file, start, end) +- for _, node := range path { +- switch node := node.(type) { +- case *ast.FuncDecl: +- // target struct method declarations. +- // function (...) someMethod(a int, b int, c int) (d int, e, int) {} +- params := node.Type.Params +- if isCursorInside(params.Opening, params.Closing) { +- return "parameters", params, params.Opening, params.Closing +- } - --// Provide inline value as text. --// --// @since 3.17.0 --type InlineValueText struct { -- // The document range for which the inline value applies. -- Range Range `json:"range"` -- // The text of the inline value. -- Text string `json:"text"` --} +- results := node.Type.Results +- if results != nil && isCursorInside(results.Opening, results.Closing) { +- return "return values", results, results.Opening, results.Closing +- } +- case *ast.FuncType: +- // target function signature args and result. +- // type someFunc func (a int, b int, c int) (d int, e int) +- params := node.Params +- if isCursorInside(params.Opening, params.Closing) { +- return "parameters", params, params.Opening, params.Closing +- } - --// Provide inline value through a variable lookup. --// If only a range is specified, the variable name will be extracted from the underlying document. --// An optional variable name can be used to override the extracted name. --// --// @since 3.17.0 --type InlineValueVariableLookup struct { -- // The document range for which the inline value applies. -- // The range is used to extract the variable name from the underlying document. -- Range Range `json:"range"` -- // If specified the name of the variable to look up. -- VariableName string `json:"variableName,omitempty"` -- // How to perform the lookup. -- CaseSensitiveLookup bool `json:"caseSensitiveLookup"` --} +- results := node.Results +- if results != nil && isCursorInside(results.Opening, results.Closing) { +- return "return values", results, results.Opening, results.Closing +- } +- case *ast.CallExpr: +- // target function calls. +- // someFunction(a, b, c) +- if isCursorInside(node.Lparen, node.Rparen) { +- return "parameters", node, node.Lparen, node.Rparen +- } +- case *ast.CompositeLit: +- // target composite lit instantiation (structs, maps, arrays). +- // A{b: 1, c: 2, d: 3} +- if isCursorInside(node.Lbrace, node.Rbrace) { +- return "elements", node, node.Lbrace, node.Rbrace +- } +- } +- } - --// Client workspace capabilities specific to inline values. --// --// @since 3.17.0 --type InlineValueWorkspaceClientCapabilities struct { -- // Whether the client implementation supports a refresh request sent from the -- // server to the client. +- return "", nil, 0, 0 +- } +- +- targetType, targetNode, open, close := findTarget() +- if targetType == "" { +- return "", nil, nil, "", 0, 0 +- } +- +- switch node := targetNode.(type) { +- case *ast.FieldList: +- for _, field := range node.List { +- items = append(items, field) +- } +- case *ast.CallExpr: +- for _, arg := range node.Args { +- items = append(items, arg) +- } +- case *ast.CompositeLit: +- for _, arg := range node.Elts { +- items = append(items, arg) +- } +- } +- +- // preserve comments separately as it's not part of the targetNode AST. +- for _, cg := range file.Comments { +- if open <= cg.Pos() && cg.Pos() < close { +- comments = append(comments, cg) +- } +- } +- +- // indent is the leading whitespace before the opening curly bracket/paren. - // -- // Note that this event is global and will force the client to refresh all -- // inline values currently shown. It should be used with absolute care and is -- // useful for situation where a server for example detects a project wide -- // change that requires such a calculation. -- RefreshSupport bool `json:"refreshSupport,omitempty"` --} +- // in case where we don't have access to src yet i.e. src == nil +- // it's fine to return incorrect indent because we don't need it yet. +- indent = "" +- if len(src) > 0 { +- var pos token.Pos +- switch node := targetNode.(type) { +- case *ast.FieldList: +- pos = node.Opening +- case *ast.CallExpr: +- pos = node.Lparen +- case *ast.CompositeLit: +- pos = node.Lbrace +- } - --// A special text edit to provide an insert and a replace operation. --// --// @since 3.16.0 --type InsertReplaceEdit struct { -- // The string to be inserted. -- NewText string `json:"newText"` -- // The range if the insert is requested -- Insert Range `json:"insert"` -- // The range if the replace is requested. -- Replace Range `json:"replace"` +- split := bytes.Split(src, []byte("\n")) +- targetLineNumber := safetoken.StartPosition(fset, pos).Line +- firstLine := string(split[targetLineNumber-1]) +- trimmed := strings.TrimSpace(string(firstLine)) +- indent = firstLine[:strings.Index(firstLine, trimmed)] +- } +- +- return targetType, items, comments, indent, open, close -} +diff -urN a/gopls/internal/golang/linkname.go b/gopls/internal/golang/linkname.go +--- a/gopls/internal/golang/linkname.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/linkname.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,145 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// Defines whether the insert text in a completion item should be interpreted as --// plain text or a snippet. --type InsertTextFormat uint32 +-package golang - --// How whitespace and indentation is handled during completion --// item insertion. --// --// @since 3.16.0 --type InsertTextMode uint32 --type LSPAny = interface{} +-import ( +- "context" +- "errors" +- "fmt" +- "go/token" +- "strings" - --// LSP arrays. --// @since 3.17.0 --type LSPArray = []interface{} // (alias) line 14194 --type LSPErrorCodes int32 +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/safetoken" +-) - --// LSP object definition. --// @since 3.17.0 --type LSPObject = map[string]LSPAny // (alias) line 14526 --// Client capabilities for the linked editing range request. --// --// @since 3.16.0 --type LinkedEditingRangeClientCapabilities struct { -- // Whether implementation supports dynamic registration. If this is set to `true` -- // the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` -- // return value for the corresponding server capability as well. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` --} --type LinkedEditingRangeOptions struct { -- WorkDoneProgressOptions --} --type LinkedEditingRangeParams struct { -- TextDocumentPositionParams -- WorkDoneProgressParams --} --type LinkedEditingRangeRegistrationOptions struct { -- TextDocumentRegistrationOptions -- LinkedEditingRangeOptions -- StaticRegistrationOptions --} +-// ErrNoLinkname is returned by LinknameDefinition when no linkname +-// directive is found at a particular position. +-// As such it indicates that other definitions could be worth checking. +-var ErrNoLinkname = errors.New("no linkname directive found") - --// The result of a linked editing range request. --// --// @since 3.16.0 --type LinkedEditingRanges struct { -- // A list of ranges that can be edited together. The ranges must have -- // identical length and contain identical text content. The ranges cannot overlap. -- Ranges []Range `json:"ranges"` -- // An optional word pattern (regular expression) that describes valid contents for -- // the given ranges. If no pattern is provided, the client configuration's word -- // pattern will be used. -- WordPattern string `json:"wordPattern,omitempty"` --} +-// LinknameDefinition finds the definition of the linkname directive in m at pos. +-// If there is no linkname directive at pos, returns ErrNoLinkname. +-func LinknameDefinition(ctx context.Context, snapshot *cache.Snapshot, m *protocol.Mapper, from protocol.Position) ([]protocol.Location, error) { +- pkgPath, name, _ := parseLinkname(m, from) +- if pkgPath == "" { +- return nil, ErrNoLinkname +- } - --// created for Literal (Lit_NotebookDocumentChangeEvent_cells_textContent_Elem) --type Lit_NotebookDocumentChangeEvent_cells_textContent_Elem struct { -- Document VersionedTextDocumentIdentifier `json:"document"` -- Changes []TextDocumentContentChangeEvent `json:"changes"` +- _, pgf, pos, err := findLinkname(ctx, snapshot, PackagePath(pkgPath), name) +- if err != nil { +- return nil, fmt.Errorf("find linkname: %w", err) +- } +- loc, err := pgf.PosLocation(pos, pos+token.Pos(len(name))) +- if err != nil { +- return nil, fmt.Errorf("location of linkname: %w", err) +- } +- return []protocol.Location{loc}, nil -} - --// created for Literal (Lit_NotebookDocumentFilter_Item1) --type Lit_NotebookDocumentFilter_Item1 struct { -- // The type of the enclosing notebook. -- NotebookType string `json:"notebookType,omitempty"` -- // A Uri {@link Uri.scheme scheme}, like `file` or `untitled`. -- Scheme string `json:"scheme"` -- // A glob pattern. -- Pattern string `json:"pattern,omitempty"` --} +-// parseLinkname attempts to parse a go:linkname declaration at the given pos. +-// If successful, it returns +-// - package path referenced +-// - object name referenced +-// - byte offset in mapped file of the start of the link target +-// of the linkname directives 2nd argument. +-// +-// If the position is not in the second argument of a go:linkname directive, +-// or parsing fails, it returns "", "", 0. +-func parseLinkname(m *protocol.Mapper, pos protocol.Position) (pkgPath, name string, targetOffset int) { +- lineStart, err := m.PositionOffset(protocol.Position{Line: pos.Line, Character: 0}) +- if err != nil { +- return "", "", 0 +- } +- lineEnd, err := m.PositionOffset(protocol.Position{Line: pos.Line + 1, Character: 0}) +- if err != nil { +- return "", "", 0 +- } - --// created for Literal (Lit_NotebookDocumentFilter_Item2) --type Lit_NotebookDocumentFilter_Item2 struct { -- // The type of the enclosing notebook. -- NotebookType string `json:"notebookType,omitempty"` -- // A Uri {@link Uri.scheme scheme}, like `file` or `untitled`. -- Scheme string `json:"scheme,omitempty"` -- // A glob pattern. -- Pattern string `json:"pattern"` --} +- directive := string(m.Content[lineStart:lineEnd]) +- // (Assumes no leading spaces.) +- if !strings.HasPrefix(directive, "//go:linkname") { +- return "", "", 0 +- } +- // Sometimes source code (typically tests) has another +- // comment after the directive, trim that away. +- if i := strings.LastIndex(directive, "//"); i != 0 { +- directive = strings.TrimSpace(directive[:i]) +- } - --// created for Literal (Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item0_cells_Elem) --type Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item0_cells_Elem struct { -- Language string `json:"language"` --} +- // Looking for pkgpath in '//go:linkname f pkgpath.g'. +- // (We ignore 1-arg linkname directives.) +- parts := strings.Fields(directive) +- if len(parts) != 3 { +- return "", "", 0 +- } - --// created for Literal (Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item1) --type Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item1 struct { -- // The notebook to be synced If a string -- // value is provided it matches against the -- // notebook type. '*' matches every notebook. -- Notebook *Or_NotebookDocumentSyncOptions_notebookSelector_Elem_Item1_notebook `json:"notebook,omitempty"` -- // The cells of the matching notebook to be synced. -- Cells []Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item1_cells_Elem `json:"cells"` --} +- // Inside 2nd arg [start, end]? +- // (Assumes no trailing spaces.) +- offset, err := m.PositionOffset(pos) +- if err != nil { +- return "", "", 0 +- } +- end := lineStart + len(directive) +- start := end - len(parts[2]) +- if !(start <= offset && offset <= end) { +- return "", "", 0 +- } +- linkname := parts[2] - --// created for Literal (Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item1_cells_Elem) --type Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item1_cells_Elem struct { -- Language string `json:"language"` --} +- // Split the pkg path from the name. +- dot := strings.LastIndexByte(linkname, '.') +- if dot < 0 { +- return "", "", 0 +- } - --// created for Literal (Lit_PrepareRenameResult_Item2) --type Lit_PrepareRenameResult_Item2 struct { -- DefaultBehavior bool `json:"defaultBehavior"` +- return linkname[:dot], linkname[dot+1:], start -} - --// created for Literal (Lit_TextDocumentContentChangeEvent_Item1) --type Lit_TextDocumentContentChangeEvent_Item1 struct { -- // The new text of the whole document. -- Text string `json:"text"` --} +-// findLinkname searches dependencies of packages containing fh for an object +-// with linker name matching the given package path and name. +-func findLinkname(ctx context.Context, snapshot *cache.Snapshot, pkgPath PackagePath, name string) (*cache.Package, *parsego.File, token.Pos, error) { +- // Typically the linkname refers to a forward dependency +- // or a reverse dependency, but in general it may refer +- // to any package that is linked with this one. +- var pkgMeta *metadata.Package +- metas, err := snapshot.AllMetadata(ctx) +- if err != nil { +- return nil, nil, token.NoPos, err +- } +- metadata.RemoveIntermediateTestVariants(&metas) +- for _, meta := range metas { +- if meta.PkgPath == pkgPath { +- pkgMeta = meta +- break +- } +- } +- if pkgMeta == nil { +- return nil, nil, token.NoPos, fmt.Errorf("cannot find package %q", pkgPath) +- } - --// created for Literal (Lit_TextDocumentFilter_Item2) --type Lit_TextDocumentFilter_Item2 struct { -- // A language id, like `typescript`. -- Language string `json:"language,omitempty"` -- // A Uri {@link Uri.scheme scheme}, like `file` or `untitled`. -- Scheme string `json:"scheme,omitempty"` -- // A glob pattern, like `*.{ts,js}`. -- Pattern string `json:"pattern"` --} +- // When found, type check the desired package (snapshot.TypeCheck in TypecheckFull mode), +- pkgs, err := snapshot.TypeCheck(ctx, pkgMeta.ID) +- if err != nil { +- return nil, nil, token.NoPos, err +- } +- pkg := pkgs[0] - --// Represents a location inside a resource, such as a line --// inside a text file. --type Location struct { -- URI DocumentURI `json:"uri"` -- Range Range `json:"range"` --} +- obj := pkg.Types().Scope().Lookup(name) +- if obj == nil { +- return nil, nil, token.NoPos, fmt.Errorf("package %q does not define %s", pkgPath, name) +- } - --// Represents the connection of two locations. Provides additional metadata over normal {@link Location locations}, --// including an origin range. --type LocationLink struct { -- // Span of the origin of this link. -- // -- // Used as the underlined span for mouse interaction. Defaults to the word range at -- // the definition position. -- OriginSelectionRange *Range `json:"originSelectionRange,omitempty"` -- // The target resource identifier of this link. -- TargetURI DocumentURI `json:"targetUri"` -- // The full target range of this link. If the target for example is a symbol then target range is the -- // range enclosing this symbol not including leading/trailing whitespace but everything else -- // like comments. This information is typically used to highlight the range in the editor. -- TargetRange Range `json:"targetRange"` -- // The range that should be selected and revealed when this link is being followed, e.g the name of a function. -- // Must be contained by the `targetRange`. See also `DocumentSymbol#range` -- TargetSelectionRange Range `json:"targetSelectionRange"` --} +- objURI := safetoken.StartPosition(pkg.FileSet(), obj.Pos()) +- pgf, err := pkg.File(protocol.URIFromPath(objURI.Filename)) +- if err != nil { +- return nil, nil, token.NoPos, err +- } - --// The log message parameters. --type LogMessageParams struct { -- // The message type. See {@link MessageType} -- Type MessageType `json:"type"` -- // The actual message. -- Message string `json:"message"` --} --type LogTraceParams struct { -- Message string `json:"message"` -- Verbose string `json:"verbose,omitempty"` +- return pkg, pgf, obj.Pos(), nil -} +diff -urN a/gopls/internal/golang/origin.go b/gopls/internal/golang/origin.go +--- a/gopls/internal/golang/origin.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/origin.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,30 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// Client capabilities specific to the used markdown parser. --// --// @since 3.16.0 --type MarkdownClientCapabilities struct { -- // The name of the parser. -- Parser string `json:"parser"` -- // The version of the parser. -- Version string `json:"version,omitempty"` -- // A list of HTML tags that the client allows / supports in -- // Markdown. -- // -- // @since 3.17.0 -- AllowedTags []string `json:"allowedTags,omitempty"` --} +-package golang - --// MarkedString can be used to render human readable text. It is either a markdown string --// or a code-block that provides a language and a code snippet. The language identifier --// is semantically equal to the optional language identifier in fenced code blocks in GitHub --// issues. See https://help.github.com/articles/creating-and-highlighting-code-blocks/#syntax-highlighting --// --// The pair of a language and a value is an equivalent to markdown: --// ```${language} --// ${value} --// ``` --// --// Note that markdown strings will be sanitized - that means html will be escaped. --// @deprecated use MarkupContent instead. --type MarkedString = Or_MarkedString // (alias) line 14473 --// A `MarkupContent` literal represents a string value which content is interpreted base on its --// kind flag. Currently the protocol supports `plaintext` and `markdown` as markup kinds. --// --// If the kind is `markdown` then the value can contain fenced code blocks like in GitHub issues. --// See https://help.github.com/articles/creating-and-highlighting-code-blocks/#syntax-highlighting --// --// Here is an example how such a string can be constructed using JavaScript / TypeScript: --// ```ts --// --// let markdown: MarkdownContent = { --// kind: MarkupKind.Markdown, --// value: [ --// '# Header', --// 'Some text', --// '```typescript', --// 'someCode();', --// '```' --// ].join('\n') --// }; --// --// ``` --// --// *Please Note* that clients might sanitize the return markdown. A client could decide to --// remove HTML from the markdown to avoid script execution. --type MarkupContent struct { -- // The type of the Markup -- Kind MarkupKind `json:"kind"` -- // The content itself -- Value string `json:"value"` +-import "go/types" +- +-// containsOrigin reports whether the provided object set contains an object +-// with the same origin as the provided obj (which may be a synthetic object +-// created during instantiation). +-func containsOrigin(objSet map[types.Object]bool, obj types.Object) bool { +- objOrigin := origin(obj) +- for target := range objSet { +- if origin(target) == objOrigin { +- return true +- } +- } +- return false -} - --// Describes the content type that a client supports in various --// result literals like `Hover`, `ParameterInfo` or `CompletionItem`. --// --// Please note that `MarkupKinds` must not start with a `$`. This kinds --// are reserved for internal usage. --type MarkupKind string --type MessageActionItem struct { -- // A short title like 'Retry', 'Open Log' etc. -- Title string `json:"title"` +-func origin(obj types.Object) types.Object { +- switch obj := obj.(type) { +- case *types.Var: +- return obj.Origin() +- case *types.Func: +- return obj.Origin() +- } +- return obj -} +diff -urN a/gopls/internal/golang/pkgdoc.go b/gopls/internal/golang/pkgdoc.go +--- a/gopls/internal/golang/pkgdoc.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/pkgdoc.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,633 +0,0 @@ +-// Copyright 2024 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// The message type --type MessageType uint32 +-package golang - --// Moniker definition to match LSIF 0.5 moniker definition. +-// This file defines a simple HTML rendering of package documentation +-// in imitation of the style of pkg.go.dev. -// --// @since 3.16.0 --type Moniker struct { -- // The scheme of the moniker. For example tsc or .Net -- Scheme string `json:"scheme"` -- // The identifier of the moniker. The value is opaque in LSIF however -- // schema owners are allowed to define the structure if they want. -- Identifier string `json:"identifier"` -- // The scope in which the moniker is unique -- Unique UniquenessLevel `json:"unique"` -- // The moniker kind if known. -- Kind *MonikerKind `json:"kind,omitempty"` --} -- --// Client capabilities specific to the moniker request. +-// The current implementation is just a starting point and a +-// placeholder for a more sophisticated one. -// --// @since 3.16.0 --type MonikerClientCapabilities struct { -- // Whether moniker supports dynamic registration. If this is set to `true` -- // the client supports the new `MonikerRegistrationOptions` return value -- // for the corresponding server capability as well. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` --} +-// TODO(adonovan): +-// - rewrite using html/template. +-// Or factor with golang.org/x/pkgsite/internal/godoc/dochtml. +-// - emit breadcrumbs for parent + sibling packages. +-// - list promoted methods---we have type information! +-// - gather Example tests, following go/doc and pkgsite. +-// - add option for doc.AllDecls: show non-exported symbols too. +-// - abbreviate long signatures by replacing parameters 4 onwards with "...". +-// - style the
  • bullets in the index as invisible. +-// - add push notifications such as didChange -> reload. +-// - there appears to be a maximum file size beyond which the +-// "source.doc" code action is not offered. Remove that. +-// - modify JS httpGET function to give a transient visual indication +-// when clicking a source link that the editor is being navigated +-// (in case it doesn't raise itself, like VS Code). - --// The moniker kind. --// --// @since 3.16.0 --type MonikerKind string --type MonikerOptions struct { -- WorkDoneProgressOptions --} --type MonikerParams struct { -- TextDocumentPositionParams -- WorkDoneProgressParams -- PartialResultParams --} --type MonikerRegistrationOptions struct { -- TextDocumentRegistrationOptions -- MonikerOptions --} +-import ( +- "bytes" +- "fmt" +- "go/ast" +- "go/doc" +- "go/doc/comment" +- "go/format" +- "go/token" +- "go/types" +- "html" +- "log" +- "path/filepath" - --// created for Literal (Lit_MarkedString_Item1) --type Msg_MarkedString struct { -- Language string `json:"language"` -- Value string `json:"value"` --} +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/astutil" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/gopls/internal/util/slices" +- "golang.org/x/tools/gopls/internal/util/typesutil" +- "golang.org/x/tools/internal/typesinternal" +-) - --// created for Literal (Lit_NotebookDocumentFilter_Item0) --type Msg_NotebookDocumentFilter struct { -- // The type of the enclosing notebook. -- NotebookType string `json:"notebookType"` -- // A Uri {@link Uri.scheme scheme}, like `file` or `untitled`. -- Scheme string `json:"scheme,omitempty"` -- // A glob pattern. -- Pattern string `json:"pattern,omitempty"` --} +-// RenderPackageDoc formats the package documentation page. +-// +-// The posURL function returns a URL that when visited, has the side +-// effect of causing gopls to direct the client editor to navigate to +-// the specified file/line/column position, in UTF-8 coordinates. +-// +-// The pkgURL function returns a URL for the documentation of the +-// specified package and symbol. +-func RenderPackageDoc(pkg *cache.Package, posURL func(filename string, line, col8 int) protocol.URI, pkgURL func(path PackagePath, fragment string) protocol.URI) ([]byte, error) { +- // We can't use doc.NewFromFiles (even with doc.PreserveAST +- // mode) as it calls ast.NewPackage which assumes that each +- // ast.File has an ast.Scope and resolves identifiers to +- // (deprecated) ast.Objects. (This is golang/go#66290.) +- // But doc.New only requires pkg.{Name,Files}, +- // so we just boil it down. +- // +- // The only loss is doc.classifyExamples. +- // TODO(adonovan): simulate that too. +- fileMap := make(map[string]*ast.File) +- for _, f := range pkg.Syntax() { +- fileMap[pkg.FileSet().File(f.Pos()).Name()] = f +- } +- astpkg := &ast.Package{ +- Name: pkg.Types().Name(), +- Files: fileMap, +- } +- // PreserveAST mode only half works (golang/go#66449): it still +- // mutates ASTs when filtering out non-exported symbols. +- // As a workaround, enable AllDecls to suppress filtering, +- // and do it ourselves. +- mode := doc.PreserveAST | doc.AllDecls +- docpkg := doc.New(astpkg, pkg.Types().Path(), mode) +- +- // Discard non-exported symbols. +- // TODO(adonovan): do this conditionally, and expose option in UI. +- const showUnexported = false +- if !showUnexported { +- var ( +- unexported = func(name string) bool { return !token.IsExported(name) } +- filterValues = func(slice *[]*doc.Value) { +- delValue := func(v *doc.Value) bool { +- v.Names = slices.DeleteFunc(v.Names, unexported) +- return len(v.Names) == 0 +- } +- *slice = slices.DeleteFunc(*slice, delValue) +- } +- filterFuncs = func(funcs *[]*doc.Func) { +- *funcs = slices.DeleteFunc(*funcs, func(v *doc.Func) bool { +- return unexported(v.Name) +- }) +- } +- ) +- filterValues(&docpkg.Consts) +- filterValues(&docpkg.Vars) +- filterFuncs(&docpkg.Funcs) +- docpkg.Types = slices.DeleteFunc(docpkg.Types, func(t *doc.Type) bool { +- filterValues(&t.Consts) +- filterValues(&t.Vars) +- filterFuncs(&t.Funcs) +- filterFuncs(&t.Methods) +- return unexported(t.Name) +- }) +- } - --// created for Literal (Lit_PrepareRenameResult_Item1) --type Msg_PrepareRename2Gn struct { -- Range Range `json:"range"` -- Placeholder string `json:"placeholder"` --} +- var docHTML func(comment string) []byte +- { +- // Adapt doc comment parser and printer +- // to our representation of Go packages +- // so that doc links (e.g. "[fmt.Println]") +- // become valid links. +- +- printer := docpkg.Printer() +- printer.DocLinkURL = func(link *comment.DocLink) string { +- path := pkg.Metadata().PkgPath +- if link.ImportPath != "" { +- path = PackagePath(link.ImportPath) +- } +- fragment := link.Name +- if link.Recv != "" { +- fragment = link.Recv + "." + link.Name +- } +- return pkgURL(path, fragment) +- } +- parser := docpkg.Parser() +- parser.LookupPackage = func(name string) (importPath string, ok bool) { +- // Ambiguous: different files in the same +- // package may have different import mappings, +- // but the hook doesn't provide the file context. +- // TODO(adonovan): conspire with docHTML to +- // pass the doc comment's enclosing file through +- // a shared variable, so that we can compute +- // the correct per-file mapping. +- // +- // TODO(adonovan): check for PkgName.Name +- // matches, but also check for +- // PkgName.Imported.Namer matches, since some +- // packages are typically imported under a +- // non-default name (e.g. pathpkg "path") but +- // may be referred to in doc links using their +- // canonical name. +- for _, f := range pkg.Syntax() { +- for _, imp := range f.Imports { +- pkgName, ok := typesutil.ImportedPkgName(pkg.TypesInfo(), imp) +- if ok && pkgName.Name() == name { +- return pkgName.Imported().Path(), true +- } +- } +- } +- return "", false +- } +- parser.LookupSym = func(recv, name string) (ok bool) { +- defer func() { +- log.Printf("LookupSym %q %q = %t ", recv, name, ok) +- }() +- // package-level decl? +- if recv == "" { +- return pkg.Types().Scope().Lookup(name) != nil +- } - --// created for Literal (Lit_TextDocumentContentChangeEvent_Item0) --type Msg_TextDocumentContentChangeEvent struct { -- // The range of the document that changed. -- Range *Range `json:"range"` -- // The optional length of the range that got replaced. -- // -- // @deprecated use range instead. -- RangeLength uint32 `json:"rangeLength,omitempty"` -- // The new text for the provided range. -- Text string `json:"text"` --} +- // method? +- tname, ok := pkg.Types().Scope().Lookup(recv).(*types.TypeName) +- if !ok { +- return false +- } +- m, _, _ := types.LookupFieldOrMethod(tname.Type(), true, pkg.Types(), name) +- return is[*types.Func](m) +- } +- docHTML = func(comment string) []byte { +- return printer.HTML(parser.Parse(comment)) +- } +- } - --// created for Literal (Lit_TextDocumentFilter_Item1) --type Msg_TextDocumentFilter struct { -- // A language id, like `typescript`. -- Language string `json:"language,omitempty"` -- // A Uri {@link Uri.scheme scheme}, like `file` or `untitled`. -- Scheme string `json:"scheme"` -- // A glob pattern, like `*.{ts,js}`. -- Pattern string `json:"pattern,omitempty"` --} +- var buf bytes.Buffer +- buf.WriteString(` +- +- +- +- +- +- +- +-
    Gopls server has terminated. Page is inactive.
    +-`) - --// created for Literal (Lit__InitializeParams_clientInfo) --type Msg_XInitializeParams_clientInfo struct { -- // The name of the client as defined by the client. -- Name string `json:"name"` -- // The client's version as defined by the client. -- Version string `json:"version,omitempty"` --} +- escape := html.EscapeString - --// A notebook cell. --// --// A cell's document URI must be unique across ALL notebook --// cells and can therefore be used to uniquely identify a --// notebook cell or the cell's text document. --// --// @since 3.17.0 --type NotebookCell struct { -- // The cell's kind -- Kind NotebookCellKind `json:"kind"` -- // The URI of the cell's text document -- // content. -- Document DocumentURI `json:"document"` -- // Additional metadata stored with the cell. -- // -- // Note: should always be an object literal (e.g. LSPObject) -- Metadata *LSPObject `json:"metadata,omitempty"` -- // Additional execution summary information -- // if supported by the client. -- ExecutionSummary *ExecutionSummary `json:"executionSummary,omitempty"` --} +- // sourceLink returns HTML for a link to open a file in the client editor. +- sourceLink := func(text, url string) string { +- // The /open URL returns nothing but has the side effect +- // of causing the LSP client to open the requested file. +- // So we use onclick to prevent the browser from navigating. +- // We keep the href attribute as it causes the to render +- // as a link: blue, underlined, with URL hover information. +- return fmt.Sprintf(`%[2]s`, +- escape(url), text) +- } - --// A change describing how to move a `NotebookCell` --// array from state S to S'. --// --// @since 3.17.0 --type NotebookCellArrayChange struct { -- // The start oftest of the cell that changed. -- Start uint32 `json:"start"` -- // The deleted cells -- DeleteCount uint32 `json:"deleteCount"` -- // The new cells, if any -- Cells []NotebookCell `json:"cells,omitempty"` --} +- // objHTML returns HTML for obj.Name(), possibly as a link. +- objHTML := func(obj types.Object) string { +- text := obj.Name() +- if posn := safetoken.StartPosition(pkg.FileSet(), obj.Pos()); posn.IsValid() { +- return sourceLink(text, posURL(posn.Filename, posn.Line, posn.Column)) +- } +- return text +- } - --// A notebook cell kind. --// --// @since 3.17.0 --type NotebookCellKind uint32 +- // nodeHTML returns HTML markup for a syntax tree. +- // It replaces referring identifiers with links, +- // and adds style spans for strings and comments. +- nodeHTML := func(n ast.Node) string { - --// A notebook cell text document filter denotes a cell text --// document by different properties. --// --// @since 3.17.0 --type NotebookCellTextDocumentFilter struct { -- // A filter that matches against the notebook -- // containing the notebook cell. If a string -- // value is provided it matches against the -- // notebook type. '*' matches every notebook. -- Notebook Or_NotebookCellTextDocumentFilter_notebook `json:"notebook"` -- // A language id like `python`. -- // -- // Will be matched against the language id of the -- // notebook cell document. '*' matches every language. -- Language string `json:"language,omitempty"` --} +- // linkify returns the appropriate URL (if any) for an identifier. +- linkify := func(id *ast.Ident) protocol.URI { +- if obj, ok := pkg.TypesInfo().Uses[id]; ok && obj.Pkg() != nil { +- // imported package name? +- if pkgname, ok := obj.(*types.PkgName); ok { +- // TODO(adonovan): do this for Defs of PkgName too. +- return pkgURL(PackagePath(pkgname.Imported().Path()), "") +- } - --// A notebook document. --// --// @since 3.17.0 --type NotebookDocument struct { -- // The notebook document's uri. -- URI URI `json:"uri"` -- // The type of the notebook. -- NotebookType string `json:"notebookType"` -- // The version number of this document (it will increase after each -- // change, including undo/redo). -- Version int32 `json:"version"` -- // Additional metadata stored with the notebook -- // document. -- // -- // Note: should always be an object literal (e.g. LSPObject) -- Metadata *LSPObject `json:"metadata,omitempty"` -- // The cells of a notebook. -- Cells []NotebookCell `json:"cells"` --} +- // package-level symbol? +- if obj.Parent() == obj.Pkg().Scope() { +- if obj.Pkg() == pkg.Types() { +- return "#" + obj.Name() // intra-package ref +- } else { +- return pkgURL(PackagePath(obj.Pkg().Path()), obj.Name()) +- } +- } - --// A change event for a notebook document. --// --// @since 3.17.0 --type NotebookDocumentChangeEvent struct { -- // The changed meta data if any. -- // -- // Note: should always be an object literal (e.g. LSPObject) -- Metadata *LSPObject `json:"metadata,omitempty"` -- // Changes to cells -- Cells *PCellsPChange `json:"cells,omitempty"` --} +- // method of package-level named type? +- if fn, ok := obj.(*types.Func); ok { +- sig := fn.Type().(*types.Signature) +- if sig.Recv() != nil { +- _, named := typesinternal.ReceiverNamed(sig.Recv()) +- if named != nil { +- fragment := named.Obj().Name() + "." + fn.Name() +- return pkgURL(PackagePath(fn.Pkg().Path()), fragment) +- } +- } +- return "" +- } - --// Capabilities specific to the notebook document support. --// --// @since 3.17.0 --type NotebookDocumentClientCapabilities struct { -- // Capabilities specific to notebook document synchronization -- // -- // @since 3.17.0 -- Synchronization NotebookDocumentSyncClientCapabilities `json:"synchronization"` --} +- // TODO(adonovan): field of package-level named struct type. +- // (Requires an index, since there's no way to +- // get from Var to Named.) +- } +- return "" +- } - --// A notebook document filter denotes a notebook document by --// different properties. The properties will be match --// against the notebook's URI (same as with documents) --// --// @since 3.17.0 --type NotebookDocumentFilter = Msg_NotebookDocumentFilter // (alias) line 14669 --// A literal to identify a notebook document in the client. --// --// @since 3.17.0 --type NotebookDocumentIdentifier struct { -- // The notebook document's uri. -- URI URI `json:"uri"` --} +- // Splice spans into HTML-escaped segments of the +- // original source buffer (which is usually but not +- // necessarily formatted). +- // +- // (For expedience we don't use the more sophisticated +- // approach taken by cmd/godoc and pkgsite's render +- // package, which emit the text, spans, and comments +- // in one traversal of the syntax tree.) +- // +- // TODO(adonovan): splice styled spans around comments too. +- // +- // TODO(adonovan): pkgsite prints specs from grouped +- // type decls like "type ( T1; T2 )" to make them +- // appear as separate decls. We should too. +- var buf bytes.Buffer +- for _, file := range pkg.CompiledGoFiles() { +- if astutil.NodeContains(file.File, n.Pos()) { +- pos := n.Pos() +- +- // emit emits source in the interval [pos:to] and updates pos. +- emit := func(to token.Pos) { +- // Ident and BasicLit always have a valid pos. +- // (Failure means the AST has been corrupted.) +- if !to.IsValid() { +- bug.Reportf("invalid Pos") +- } +- start, err := safetoken.Offset(file.Tok, pos) +- if err != nil { +- bug.Reportf("invalid start Pos: %v", err) +- } +- end, err := safetoken.Offset(file.Tok, to) +- if err != nil { +- bug.Reportf("invalid end Pos: %v", err) +- } +- buf.WriteString(escape(string(file.Src[start:end]))) +- pos = to +- } +- ast.Inspect(n, func(n ast.Node) bool { +- switch n := n.(type) { +- case *ast.Ident: +- emit(n.Pos()) +- pos = n.End() +- if url := linkify(n); url != "" { +- fmt.Fprintf(&buf, "%s", url, escape(n.Name)) +- } else { +- buf.WriteString(escape(n.Name)) // plain +- } - --// Notebook specific client capabilities. --// --// @since 3.17.0 --type NotebookDocumentSyncClientCapabilities struct { -- // Whether implementation supports dynamic registration. If this is -- // set to `true` the client supports the new -- // `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` -- // return value for the corresponding server capability as well. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -- // The client supports sending execution summary data per cell. -- ExecutionSummarySupport bool `json:"executionSummarySupport,omitempty"` --} +- case *ast.BasicLit: +- emit(n.Pos()) +- pos = n.End() +- fmt.Fprintf(&buf, "%s", escape(n.Value)) +- } +- return true +- }) +- emit(n.End()) +- return buf.String() +- } +- } - --// Options specific to a notebook plus its cells --// to be synced to the server. --// --// If a selector provides a notebook document --// filter but no cell selector all cells of a --// matching notebook document will be synced. --// --// If a selector provides no notebook document --// filter but only a cell selector all notebook --// document that contain at least one matching --// cell will be synced. --// --// @since 3.17.0 --type NotebookDocumentSyncOptions struct { -- // The notebooks to be synced -- NotebookSelector []PNotebookSelectorPNotebookDocumentSync `json:"notebookSelector"` -- // Whether save notification should be forwarded to -- // the server. Will only be honored if mode === `notebook`. -- Save bool `json:"save,omitempty"` --} +- // Original source not found. +- // Format the node without adornments. +- if err := format.Node(&buf, pkg.FileSet(), n); err != nil { +- // e.g. BadDecl? +- buf.Reset() +- fmt.Fprintf(&buf, "formatting error: %v", err) +- } +- return escape(buf.String()) +- } - --// Registration options specific to a notebook. --// --// @since 3.17.0 --type NotebookDocumentSyncRegistrationOptions struct { -- NotebookDocumentSyncOptions -- StaticRegistrationOptions --} +- // pkgRelative qualifies types by package name alone +- pkgRelative := func(other *types.Package) string { +- if pkg.Types() == other { +- return "" // same package; unqualified +- } +- return other.Name() +- } - --// A text document identifier to optionally denote a specific version of a text document. --type OptionalVersionedTextDocumentIdentifier struct { -- // The version number of this document. If a versioned text document identifier -- // is sent from the server to the client and the file is not open in the editor -- // (the server has not received an open notification before) the server can send -- // `null` to indicate that the version is unknown and the content on disk is the -- // truth (as specified with document content ownership). -- Version int32 `json:"version"` -- TextDocumentIdentifier --} +- // package name +- fmt.Fprintf(&buf, "

    Package %s

    \n", pkg.Types().Name()) - --// created for Or [FEditRangePItemDefaults Range] --type OrFEditRangePItemDefaults struct { -- Value interface{} `json:"value"` --} +- // import path +- fmt.Fprintf(&buf, "
    import %q
    \n", pkg.Types().Path()) - --// created for Or [NotebookDocumentFilter string] --type OrFNotebookPNotebookSelector struct { -- Value interface{} `json:"value"` --} +- // link to same package in pkg.go.dev +- fmt.Fprintf(&buf, "
    \n", +- "https://pkg.go.dev/"+string(pkg.Types().Path())) - --// created for Or [Location PLocationMsg_workspace_symbol] --type OrPLocation_workspace_symbol struct { -- Value interface{} `json:"value"` --} +- // package doc +- fmt.Fprintf(&buf, "
    %s
    \n", docHTML(docpkg.Doc)) - --// created for Or [[]string string] --type OrPSection_workspace_didChangeConfiguration struct { -- Value interface{} `json:"value"` --} +- // symbol index +- fmt.Fprintf(&buf, "

    Index

    \n") +- fmt.Fprintf(&buf, "
      \n") +- if len(docpkg.Consts) > 0 { +- fmt.Fprintf(&buf, "
    • Constants
    • \n") +- } +- if len(docpkg.Vars) > 0 { +- fmt.Fprintf(&buf, "
    • Variables
    • \n") +- } +- scope := pkg.Types().Scope() +- for _, fn := range docpkg.Funcs { +- obj := scope.Lookup(fn.Name).(*types.Func) +- fmt.Fprintf(&buf, "
    • %s
    • \n", +- obj.Name(), +- escape(types.ObjectString(obj, pkgRelative))) +- } +- for _, doctype := range docpkg.Types { +- tname := scope.Lookup(doctype.Name).(*types.TypeName) +- fmt.Fprintf(&buf, "
    • type %[1]s
    • \n", +- tname.Name()) - --// created for Or [MarkupContent string] --type OrPTooltipPLabel struct { -- Value interface{} `json:"value"` --} +- if len(doctype.Funcs)+len(doctype.Methods) > 0 { +- fmt.Fprintf(&buf, "
        \n") - --// created for Or [MarkupContent string] --type OrPTooltip_textDocument_inlayHint struct { -- Value interface{} `json:"value"` --} +- // constructors +- for _, docfn := range doctype.Funcs { +- obj := scope.Lookup(docfn.Name).(*types.Func) +- fmt.Fprintf(&buf, "
      • %s
      • \n", +- docfn.Name, +- escape(types.ObjectString(obj, pkgRelative))) +- } +- // methods +- for _, docmethod := range doctype.Methods { +- method, _, _ := types.LookupFieldOrMethod(tname.Type(), true, tname.Pkg(), docmethod.Name) +- // TODO(adonovan): style: change the . into a space in +- // ObjectString's "func (T).M()", and hide unexported +- // embedded types. +- fmt.Fprintf(&buf, "
      • %s
      • \n", +- doctype.Name, +- docmethod.Name, +- escape(types.ObjectString(method, pkgRelative))) +- } +- fmt.Fprintf(&buf, "
      \n") +- } +- } +- // TODO(adonovan): add index of Examples here. +- fmt.Fprintf(&buf, "
    \n") - --// created for Or [int32 string] --type Or_CancelParams_id struct { -- Value interface{} `json:"value"` --} +- // constants and variables +- values := func(vals []*doc.Value) { +- for _, v := range vals { +- // anchors +- for _, name := range v.Names { +- fmt.Fprintf(&buf, "\n", escape(name)) +- } - --// created for Or [MarkupContent string] --type Or_CompletionItem_documentation struct { -- Value interface{} `json:"value"` --} +- // declaration +- decl2 := *v.Decl // shallow copy +- decl2.Doc = nil +- fmt.Fprintf(&buf, "
    %s
    \n", nodeHTML(&decl2)) - --// created for Or [InsertReplaceEdit TextEdit] --type Or_CompletionItem_textEdit struct { -- Value interface{} `json:"value"` --} +- // comment (if any) +- fmt.Fprintf(&buf, "
    %s
    \n", docHTML(v.Doc)) +- } +- } +- fmt.Fprintf(&buf, "

    Constants

    \n") +- if len(docpkg.Consts) == 0 { +- fmt.Fprintf(&buf, "
    (no constants)
    \n") +- } else { +- values(docpkg.Consts) +- } +- fmt.Fprintf(&buf, "

    Variables

    \n") +- if len(docpkg.Vars) == 0 { +- fmt.Fprintf(&buf, "
    (no variables)
    \n") +- } else { +- values(docpkg.Vars) +- } - --// created for Or [Location []Location] --type Or_Definition struct { -- Value interface{} `json:"value"` --} +- // package-level functions +- fmt.Fprintf(&buf, "

    Functions

    \n") +- // funcs emits a list of package-level functions, +- // possibly organized beneath the type they construct. +- funcs := func(funcs []*doc.Func) { +- for _, docfn := range funcs { +- obj := scope.Lookup(docfn.Name).(*types.Func) +- fmt.Fprintf(&buf, "

    func %s

    \n", +- docfn.Name, objHTML(obj)) - --// created for Or [int32 string] --type Or_Diagnostic_code struct { -- Value interface{} `json:"value"` --} +- // decl: func F(params) results +- fmt.Fprintf(&buf, "
    %s
    \n", +- nodeHTML(docfn.Decl.Type)) - --// created for Or [RelatedFullDocumentDiagnosticReport RelatedUnchangedDocumentDiagnosticReport] --type Or_DocumentDiagnosticReport struct { -- Value interface{} `json:"value"` --} +- // comment (if any) +- fmt.Fprintf(&buf, "
    %s
    \n", docHTML(docfn.Doc)) +- } +- } +- funcs(docpkg.Funcs) - --// created for Or [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport] --type Or_DocumentDiagnosticReportPartialResult_relatedDocuments_Value struct { -- Value interface{} `json:"value"` --} +- // types and their subelements +- fmt.Fprintf(&buf, "

    Types

    \n") +- for _, doctype := range docpkg.Types { +- tname := scope.Lookup(doctype.Name).(*types.TypeName) - --// created for Or [NotebookCellTextDocumentFilter TextDocumentFilter] --type Or_DocumentFilter struct { -- Value interface{} `json:"value"` --} +- // title and source link +- fmt.Fprintf(&buf, "

    type %s

    \n", doctype.Name, objHTML(tname)) - --// created for Or [MarkedString MarkupContent []MarkedString] --type Or_Hover_contents struct { -- Value interface{} `json:"value"` --} +- // declaration +- // TODO(adonovan): excise non-exported struct fields somehow. +- decl2 := *doctype.Decl // shallow copy +- decl2.Doc = nil +- fmt.Fprintf(&buf, "
    %s
    \n", nodeHTML(&decl2)) - --// created for Or [[]InlayHintLabelPart string] --type Or_InlayHint_label struct { -- Value interface{} `json:"value"` --} +- // comment (if any) +- fmt.Fprintf(&buf, "
    %s
    \n", docHTML(doctype.Doc)) - --// created for Or [StringValue string] --type Or_InlineCompletionItem_insertText struct { -- Value interface{} `json:"value"` --} +- // subelements +- values(doctype.Consts) // constants of type T +- values(doctype.Vars) // vars of type T +- funcs(doctype.Funcs) // constructors of T - --// created for Or [InlineValueEvaluatableExpression InlineValueText InlineValueVariableLookup] --type Or_InlineValue struct { -- Value interface{} `json:"value"` --} +- // methods on T +- for _, docmethod := range doctype.Methods { +- method, _, _ := types.LookupFieldOrMethod(tname.Type(), true, tname.Pkg(), docmethod.Name) +- fmt.Fprintf(&buf, "

    func (%s) %s

    \n", +- doctype.Name, docmethod.Name, +- doctype.Name, objHTML(method)) - --// created for Or [Msg_MarkedString string] --type Or_MarkedString struct { -- Value interface{} `json:"value"` --} +- // decl: func (x T) M(params) results +- fmt.Fprintf(&buf, "
    %s
    \n", +- nodeHTML(docmethod.Decl.Type)) - --// created for Or [NotebookDocumentFilter string] --type Or_NotebookCellTextDocumentFilter_notebook struct { -- Value interface{} `json:"value"` --} +- // comment (if any) +- fmt.Fprintf(&buf, "
    %s
    \n", +- docHTML(docmethod.Doc)) +- } +- } - --// created for Or [NotebookDocumentFilter string] --type Or_NotebookDocumentSyncOptions_notebookSelector_Elem_Item1_notebook struct { -- Value interface{} `json:"value"` --} +- // source files +- fmt.Fprintf(&buf, "

    Source files

    \n") +- for _, filename := range docpkg.Filenames { +- fmt.Fprintf(&buf, "
    %s
    \n", +- sourceLink(filepath.Base(filename), posURL(filename, 1, 1))) +- } - --// created for Or [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport] --type Or_RelatedFullDocumentDiagnosticReport_relatedDocuments_Value struct { -- Value interface{} `json:"value"` +- return buf.Bytes(), nil -} - --// created for Or [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport] --type Or_RelatedUnchangedDocumentDiagnosticReport_relatedDocuments_Value struct { -- Value interface{} `json:"value"` +-// (partly taken from pkgsite's typography.css) +-const pkgDocStyle = ` +-body { +- font-family: Helvetica, Arial, sans-serif; +- font-size: 1rem; +- line-height: normal; -} - --// created for Or [URI WorkspaceFolder] --type Or_RelativePattern_baseUri struct { -- Value interface{} `json:"value"` +-h1 { +- font-size: 1.5rem; -} - --// created for Or [CodeAction Command] --type Or_Result_textDocument_codeAction_Item0_Elem struct { -- Value interface{} `json:"value"` +-h2 { +- font-size: 1.375rem; -} - --// created for Or [InlineCompletionList []InlineCompletionItem] --type Or_Result_textDocument_inlineCompletion struct { -- Value interface{} `json:"value"` +-h3 { +- font-size: 1.25rem; -} - --// created for Or [FFullPRequests bool] --type Or_SemanticTokensClientCapabilities_requests_full struct { -- Value interface{} `json:"value"` +-h4 { +- font-size: 1.125rem; -} - --// created for Or [FRangePRequests bool] --type Or_SemanticTokensClientCapabilities_requests_range struct { -- Value interface{} `json:"value"` +-h5 { +- font-size: 1rem; -} - --// created for Or [PFullESemanticTokensOptions bool] --type Or_SemanticTokensOptions_full struct { -- Value interface{} `json:"value"` +-h6 { +- font-size: 0.875rem; -} - --// created for Or [PRangeESemanticTokensOptions bool] --type Or_SemanticTokensOptions_range struct { -- Value interface{} `json:"value"` +-h1, +-h2, +-h3, +-h4 { +- font-weight: 600; +- line-height: 1.25em; +- word-break: break-word; -} - --// created for Or [CallHierarchyOptions CallHierarchyRegistrationOptions bool] --type Or_ServerCapabilities_callHierarchyProvider struct { -- Value interface{} `json:"value"` +-h5, +-h6 { +- font-weight: 500; +- line-height: 1.3em; +- word-break: break-word; -} - --// created for Or [CodeActionOptions bool] --type Or_ServerCapabilities_codeActionProvider struct { -- Value interface{} `json:"value"` +-p { +- font-size: 1rem; +- line-height: 1.5rem; +- max-width: 60rem; -} - --// created for Or [DocumentColorOptions DocumentColorRegistrationOptions bool] --type Or_ServerCapabilities_colorProvider struct { -- Value interface{} `json:"value"` +-strong { +- font-weight: 600; -} - --// created for Or [DeclarationOptions DeclarationRegistrationOptions bool] --type Or_ServerCapabilities_declarationProvider struct { -- Value interface{} `json:"value"` +-code, +-pre, +-textarea.code { +- font-family: Consolas, 'Liberation Mono', Menlo, monospace; +- font-size: 0.875rem; +- line-height: 1.5em; -} - --// created for Or [DefinitionOptions bool] --type Or_ServerCapabilities_definitionProvider struct { -- Value interface{} `json:"value"` +-pre, +-textarea.code { +- background-color: #eee; +- border: 3px; +- border-radius: 3px +- color: black; +- overflow-x: auto; +- padding: 0.625rem; +- tab-size: 4; +- white-space: pre; -} - --// created for Or [DiagnosticOptions DiagnosticRegistrationOptions] --type Or_ServerCapabilities_diagnosticProvider struct { -- Value interface{} `json:"value"` +-button, +-input, +-select, +-textarea { +- font: inherit; -} - --// created for Or [DocumentFormattingOptions bool] --type Or_ServerCapabilities_documentFormattingProvider struct { -- Value interface{} `json:"value"` +-a, +-a:link, +-a:visited { +- color: rgb(0, 125, 156); +- text-decoration: none; -} - --// created for Or [DocumentHighlightOptions bool] --type Or_ServerCapabilities_documentHighlightProvider struct { -- Value interface{} `json:"value"` +-a:hover, +-a:focus { +- color: rgb(0, 125, 156); +- text-decoration: underline; -} - --// created for Or [DocumentRangeFormattingOptions bool] --type Or_ServerCapabilities_documentRangeFormattingProvider struct { -- Value interface{} `json:"value"` +-a:hover > * { +- text-decoration: underline; -} - --// created for Or [DocumentSymbolOptions bool] --type Or_ServerCapabilities_documentSymbolProvider struct { -- Value interface{} `json:"value"` --} +-.lit { color: darkgreen; } - --// created for Or [FoldingRangeOptions FoldingRangeRegistrationOptions bool] --type Or_ServerCapabilities_foldingRangeProvider struct { -- Value interface{} `json:"value"` --} +-#pkgsite { height: 1.5em; } - --// created for Or [HoverOptions bool] --type Or_ServerCapabilities_hoverProvider struct { -- Value interface{} `json:"value"` +-#disconnected { +- position: fixed; +- top: 1em; +- left: 1em; +- display: none; /* initially */ +- background-color: white; +- border: thick solid red; +- padding: 2em; -} +-` +diff -urN a/gopls/internal/golang/references.go b/gopls/internal/golang/references.go +--- a/gopls/internal/golang/references.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/references.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,698 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// created for Or [ImplementationOptions ImplementationRegistrationOptions bool] --type Or_ServerCapabilities_implementationProvider struct { -- Value interface{} `json:"value"` --} +-package golang - --// created for Or [InlayHintOptions InlayHintRegistrationOptions bool] --type Or_ServerCapabilities_inlayHintProvider struct { -- Value interface{} `json:"value"` --} +-// This file defines the 'references' query based on a serializable +-// index constructed during type checking, thus avoiding the need to +-// type-check packages at search time. +-// +-// See the ./xrefs/ subpackage for the index construction and lookup. +-// +-// This implementation does not intermingle objects from distinct +-// calls to TypeCheck. - --// created for Or [InlineCompletionOptions bool] --type Or_ServerCapabilities_inlineCompletionProvider struct { -- Value interface{} `json:"value"` --} +-import ( +- "context" +- "fmt" +- "go/ast" +- "go/token" +- "go/types" +- "sort" +- "strings" +- "sync" - --// created for Or [InlineValueOptions InlineValueRegistrationOptions bool] --type Or_ServerCapabilities_inlineValueProvider struct { -- Value interface{} `json:"value"` --} +- "golang.org/x/sync/errgroup" +- "golang.org/x/tools/go/types/objectpath" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/cache/methodsets" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/internal/event" +-) - --// created for Or [LinkedEditingRangeOptions LinkedEditingRangeRegistrationOptions bool] --type Or_ServerCapabilities_linkedEditingRangeProvider struct { -- Value interface{} `json:"value"` +-// References returns a list of all references (sorted with +-// definitions before uses) to the object denoted by the identifier at +-// the given file/position, searching the entire workspace. +-func References(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pp protocol.Position, includeDeclaration bool) ([]protocol.Location, error) { +- references, err := references(ctx, snapshot, fh, pp, includeDeclaration) +- if err != nil { +- return nil, err +- } +- locations := make([]protocol.Location, len(references)) +- for i, ref := range references { +- locations[i] = ref.location +- } +- return locations, nil -} - --// created for Or [MonikerOptions MonikerRegistrationOptions bool] --type Or_ServerCapabilities_monikerProvider struct { -- Value interface{} `json:"value"` +-// A reference describes an identifier that refers to the same +-// object as the subject of a References query. +-type reference struct { +- isDeclaration bool +- location protocol.Location +- pkgPath PackagePath // of declaring package (same for all elements of the slice) -} - --// created for Or [NotebookDocumentSyncOptions NotebookDocumentSyncRegistrationOptions] --type Or_ServerCapabilities_notebookDocumentSync struct { -- Value interface{} `json:"value"` --} +-// references returns a list of all references (sorted with +-// definitions before uses) to the object denoted by the identifier at +-// the given file/position, searching the entire workspace. +-func references(ctx context.Context, snapshot *cache.Snapshot, f file.Handle, pp protocol.Position, includeDeclaration bool) ([]reference, error) { +- ctx, done := event.Start(ctx, "golang.references") +- defer done() - --// created for Or [ReferenceOptions bool] --type Or_ServerCapabilities_referencesProvider struct { -- Value interface{} `json:"value"` --} +- // Is the cursor within the package name declaration? +- _, inPackageName, err := parsePackageNameDecl(ctx, snapshot, f, pp) +- if err != nil { +- return nil, err +- } - --// created for Or [RenameOptions bool] --type Or_ServerCapabilities_renameProvider struct { -- Value interface{} `json:"value"` --} +- var refs []reference +- if inPackageName { +- refs, err = packageReferences(ctx, snapshot, f.URI()) +- } else { +- refs, err = ordinaryReferences(ctx, snapshot, f.URI(), pp) +- } +- if err != nil { +- return nil, err +- } - --// created for Or [SelectionRangeOptions SelectionRangeRegistrationOptions bool] --type Or_ServerCapabilities_selectionRangeProvider struct { -- Value interface{} `json:"value"` --} +- sort.Slice(refs, func(i, j int) bool { +- x, y := refs[i], refs[j] +- if x.isDeclaration != y.isDeclaration { +- return x.isDeclaration // decls < refs +- } +- return protocol.CompareLocation(x.location, y.location) < 0 +- }) - --// created for Or [SemanticTokensOptions SemanticTokensRegistrationOptions] --type Or_ServerCapabilities_semanticTokensProvider struct { -- Value interface{} `json:"value"` --} +- // De-duplicate by location, and optionally remove declarations. +- out := refs[:0] +- for _, ref := range refs { +- if !includeDeclaration && ref.isDeclaration { +- continue +- } +- if len(out) == 0 || out[len(out)-1].location != ref.location { +- out = append(out, ref) +- } +- } +- refs = out - --// created for Or [TextDocumentSyncKind TextDocumentSyncOptions] --type Or_ServerCapabilities_textDocumentSync struct { -- Value interface{} `json:"value"` +- return refs, nil -} - --// created for Or [TypeDefinitionOptions TypeDefinitionRegistrationOptions bool] --type Or_ServerCapabilities_typeDefinitionProvider struct { -- Value interface{} `json:"value"` --} +-// packageReferences returns a list of references to the package +-// declaration of the specified name and uri by searching among the +-// import declarations of all packages that directly import the target +-// package. +-func packageReferences(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI) ([]reference, error) { +- metas, err := snapshot.MetadataForFile(ctx, uri) +- if err != nil { +- return nil, err +- } +- if len(metas) == 0 { +- return nil, fmt.Errorf("found no package containing %s", uri) +- } - --// created for Or [TypeHierarchyOptions TypeHierarchyRegistrationOptions bool] --type Or_ServerCapabilities_typeHierarchyProvider struct { -- Value interface{} `json:"value"` --} +- var refs []reference - --// created for Or [WorkspaceSymbolOptions bool] --type Or_ServerCapabilities_workspaceSymbolProvider struct { -- Value interface{} `json:"value"` --} +- // Find external references to the package declaration +- // from each direct import of the package. +- // +- // The narrowest package is the most broadly imported, +- // so we choose it for the external references. +- // +- // But if the file ends with _test.go then we need to +- // find the package it is testing; there's no direct way +- // to do that, so pick a file from the same package that +- // doesn't end in _test.go and start over. +- narrowest := metas[0] +- if narrowest.ForTest != "" && strings.HasSuffix(string(uri), "_test.go") { +- for _, f := range narrowest.CompiledGoFiles { +- if !strings.HasSuffix(string(f), "_test.go") { +- return packageReferences(ctx, snapshot, f) +- } +- } +- // This package has no non-test files. +- // Skip the search for external references. +- // (Conceivably one could blank-import an empty package, but why?) +- } else { +- rdeps, err := snapshot.ReverseDependencies(ctx, narrowest.ID, false) // direct +- if err != nil { +- return nil, err +- } - --// created for Or [MarkupContent string] --type Or_SignatureInformation_documentation struct { -- Value interface{} `json:"value"` --} +- // Restrict search to workspace packages. +- workspace, err := snapshot.WorkspaceMetadata(ctx) +- if err != nil { +- return nil, err +- } +- workspaceMap := make(map[PackageID]*metadata.Package, len(workspace)) +- for _, mp := range workspace { +- workspaceMap[mp.ID] = mp +- } - --// created for Or [AnnotatedTextEdit TextEdit] --type Or_TextDocumentEdit_edits_Elem struct { -- Value interface{} `json:"value"` --} +- for _, rdep := range rdeps { +- if _, ok := workspaceMap[rdep.ID]; !ok { +- continue +- } +- for _, uri := range rdep.CompiledGoFiles { +- fh, err := snapshot.ReadFile(ctx, uri) +- if err != nil { +- return nil, err +- } +- f, err := snapshot.ParseGo(ctx, fh, parsego.Header) +- if err != nil { +- return nil, err +- } +- for _, imp := range f.File.Imports { +- if rdep.DepsByImpPath[metadata.UnquoteImportPath(imp)] == narrowest.ID { +- refs = append(refs, reference{ +- isDeclaration: false, +- location: mustLocation(f, imp), +- pkgPath: narrowest.PkgPath, +- }) +- } +- } +- } +- } +- } - --// created for Or [SaveOptions bool] --type Or_TextDocumentSyncOptions_save struct { -- Value interface{} `json:"value"` --} +- // Find internal "references" to the package from +- // of each package declaration in the target package itself. +- // +- // The widest package (possibly a test variant) has the +- // greatest number of files and thus we choose it for the +- // "internal" references. +- widest := metas[len(metas)-1] // may include _test.go files +- for _, uri := range widest.CompiledGoFiles { +- fh, err := snapshot.ReadFile(ctx, uri) +- if err != nil { +- return nil, err +- } +- f, err := snapshot.ParseGo(ctx, fh, parsego.Header) +- if err != nil { +- return nil, err +- } +- // golang/go#66250: don't crash if the package file lacks a name. +- if f.File.Name.Pos().IsValid() { +- refs = append(refs, reference{ +- isDeclaration: true, // (one of many) +- location: mustLocation(f, f.File.Name), +- pkgPath: widest.PkgPath, +- }) +- } +- } - --// created for Or [WorkspaceFullDocumentDiagnosticReport WorkspaceUnchangedDocumentDiagnosticReport] --type Or_WorkspaceDocumentDiagnosticReport struct { -- Value interface{} `json:"value"` +- return refs, nil -} - --// created for Or [CreateFile DeleteFile RenameFile TextDocumentEdit] --type Or_WorkspaceEdit_documentChanges_Elem struct { -- Value interface{} `json:"value"` --} +-// ordinaryReferences computes references for all ordinary objects (not package declarations). +-func ordinaryReferences(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI, pp protocol.Position) ([]reference, error) { +- // Strategy: use the reference information computed by the +- // type checker to find the declaration. First type-check this +- // package to find the declaration, then type check the +- // declaring package (which may be different), plus variants, +- // to find local (in-package) references. +- // Global references are satisfied by the index. - --// created for Or [Declaration []DeclarationLink] --type Or_textDocument_declaration struct { -- Value interface{} `json:"value"` --} +- // Strictly speaking, a wider package could provide a different +- // declaration (e.g. because the _test.go files can change the +- // meaning of a field or method selection), but the narrower +- // package reports the more broadly referenced object. +- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, uri) +- if err != nil { +- return nil, err +- } - --// created for Literal (Lit_NotebookDocumentChangeEvent_cells) --type PCellsPChange struct { -- // Changes to the cell structure to add or -- // remove cells. -- Structure *FStructurePCells `json:"structure,omitempty"` -- // Changes to notebook cells properties like its -- // kind, execution summary or metadata. -- Data []NotebookCell `json:"data,omitempty"` -- // Changes to the text content of notebook cells. -- TextContent []Lit_NotebookDocumentChangeEvent_cells_textContent_Elem `json:"textContent,omitempty"` --} +- // Find the selected object (declaration or reference). +- // For struct{T}, we choose the field (Def) over the type (Use). +- pos, err := pgf.PositionPos(pp) +- if err != nil { +- return nil, err +- } +- candidates, _, err := objectsAt(pkg.TypesInfo(), pgf.File, pos) +- if err != nil { +- return nil, err +- } - --// created for Literal (Lit_WorkspaceEditClientCapabilities_changeAnnotationSupport) --type PChangeAnnotationSupportPWorkspaceEdit struct { -- // Whether the client groups edits with equal labels into tree nodes, -- // for instance all edits labelled with "Changes in Strings" would -- // be a tree node. -- GroupsOnLabel bool `json:"groupsOnLabel,omitempty"` --} +- // Pick first object arbitrarily. +- // The case variables of a type switch have different +- // types but that difference is immaterial here. +- var obj types.Object +- for obj = range candidates { +- break +- } +- if obj == nil { +- return nil, ErrNoIdentFound // can't happen +- } - --// created for Literal (Lit_CodeActionClientCapabilities_codeActionLiteralSupport) --type PCodeActionLiteralSupportPCodeAction struct { -- // The code action kind is support with the following value -- // set. -- CodeActionKind FCodeActionKindPCodeActionLiteralSupport `json:"codeActionKind"` --} +- // nil, error, error.Error, iota, or other built-in? +- if obj.Pkg() == nil { +- return nil, fmt.Errorf("references to builtin %q are not supported", obj.Name()) +- } +- if !obj.Pos().IsValid() { +- if obj.Pkg().Path() != "unsafe" { +- bug.Reportf("references: object %v has no position", obj) +- } +- return nil, fmt.Errorf("references to unsafe.%s are not supported", obj.Name()) +- } - --// created for Literal (Lit_CompletionClientCapabilities_completionItemKind) --type PCompletionItemKindPCompletion struct { -- // The completion item kind values the client supports. When this -- // property exists the client also guarantees that it will -- // handle values outside its set gracefully and falls back -- // to a default value when unknown. -- // -- // If this property is not present the client only supports -- // the completion items kinds from `Text` to `Reference` as defined in -- // the initial version of the protocol. -- ValueSet []CompletionItemKind `json:"valueSet,omitempty"` --} +- // Find metadata of all packages containing the object's defining file. +- // This may include the query pkg, and possibly other variants. +- declPosn := safetoken.StartPosition(pkg.FileSet(), obj.Pos()) +- declURI := protocol.URIFromPath(declPosn.Filename) +- variants, err := snapshot.MetadataForFile(ctx, declURI) +- if err != nil { +- return nil, err +- } +- if len(variants) == 0 { +- return nil, fmt.Errorf("no packages for file %q", declURI) // can't happen +- } +- // (variants must include ITVs for reverse dependency computation below.) - --// created for Literal (Lit_CompletionClientCapabilities_completionItem) --type PCompletionItemPCompletion struct { -- // Client supports snippets as insert text. -- // -- // A snippet can define tab stops and placeholders with `$1`, `$2` -- // and `${3:foo}`. `$0` defines the final tab stop, it defaults to -- // the end of the snippet. Placeholders with equal identifiers are linked, -- // that is typing in one will update others too. -- SnippetSupport bool `json:"snippetSupport,omitempty"` -- // Client supports commit characters on a completion item. -- CommitCharactersSupport bool `json:"commitCharactersSupport,omitempty"` -- // Client supports the following content formats for the documentation -- // property. The order describes the preferred format of the client. -- DocumentationFormat []MarkupKind `json:"documentationFormat,omitempty"` -- // Client supports the deprecated property on a completion item. -- DeprecatedSupport bool `json:"deprecatedSupport,omitempty"` -- // Client supports the preselect property on a completion item. -- PreselectSupport bool `json:"preselectSupport,omitempty"` -- // Client supports the tag property on a completion item. Clients supporting -- // tags have to handle unknown tags gracefully. Clients especially need to -- // preserve unknown tags when sending a completion item back to the server in -- // a resolve call. -- // -- // @since 3.15.0 -- TagSupport FTagSupportPCompletionItem `json:"tagSupport"` -- // Client support insert replace edit to control different behavior if a -- // completion item is inserted in the text or should replace text. -- // -- // @since 3.16.0 -- InsertReplaceSupport bool `json:"insertReplaceSupport,omitempty"` -- // Indicates which properties a client can resolve lazily on a completion -- // item. Before version 3.16.0 only the predefined properties `documentation` -- // and `details` could be resolved lazily. -- // -- // @since 3.16.0 -- ResolveSupport *FResolveSupportPCompletionItem `json:"resolveSupport,omitempty"` -- // The client supports the `insertTextMode` property on -- // a completion item to override the whitespace handling mode -- // as defined by the client (see `insertTextMode`). -- // -- // @since 3.16.0 -- InsertTextModeSupport *FInsertTextModeSupportPCompletionItem `json:"insertTextModeSupport,omitempty"` -- // The client has support for completion item label -- // details (see also `CompletionItemLabelDetails`). -- // -- // @since 3.17.0 -- LabelDetailsSupport bool `json:"labelDetailsSupport,omitempty"` --} +- // Is object exported? +- // If so, compute scope and targets of the global search. +- var ( +- globalScope = make(map[PackageID]*metadata.Package) // (excludes ITVs) +- globalTargets map[PackagePath]map[objectpath.Path]unit +- expansions = make(map[PackageID]unit) // packages that caused search expansion +- ) +- // TODO(adonovan): what about generic functions? Need to consider both +- // uninstantiated and instantiated. The latter have no objectpath. Use Origin? +- if path, err := objectpath.For(obj); err == nil && obj.Exported() { +- pkgPath := variants[0].PkgPath // (all variants have same package path) +- globalTargets = map[PackagePath]map[objectpath.Path]unit{ +- pkgPath: {path: {}}, // primary target +- } - --// created for Literal (Lit_CompletionOptions_completionItem) --type PCompletionItemPCompletionProvider struct { -- // The server has support for completion item label -- // details (see also `CompletionItemLabelDetails`) when -- // receiving a completion item in a resolve call. -- // -- // @since 3.17.0 -- LabelDetailsSupport bool `json:"labelDetailsSupport,omitempty"` --} +- // Compute set of (non-ITV) workspace packages. +- // We restrict references to this subset. +- workspace, err := snapshot.WorkspaceMetadata(ctx) +- if err != nil { +- return nil, err +- } +- workspaceMap := make(map[PackageID]*metadata.Package, len(workspace)) +- workspaceIDs := make([]PackageID, 0, len(workspace)) +- for _, mp := range workspace { +- workspaceMap[mp.ID] = mp +- workspaceIDs = append(workspaceIDs, mp.ID) +- } - --// created for Literal (Lit_CompletionClientCapabilities_completionList) --type PCompletionListPCompletion struct { -- // The client supports the following itemDefaults on -- // a completion list. -- // -- // The value lists the supported property names of the -- // `CompletionList.itemDefaults` object. If omitted -- // no properties are supported. -- // -- // @since 3.17.0 -- ItemDefaults []string `json:"itemDefaults,omitempty"` --} +- // addRdeps expands the global scope to include the +- // reverse dependencies of the specified package. +- addRdeps := func(id PackageID, transitive bool) error { +- rdeps, err := snapshot.ReverseDependencies(ctx, id, transitive) +- if err != nil { +- return err +- } +- for rdepID, rdep := range rdeps { +- // Skip non-workspace packages. +- // +- // This means we also skip any expansion of the +- // search that might be caused by a non-workspace +- // package, possibly causing us to miss references +- // to the expanded target set from workspace packages. +- // +- // TODO(adonovan): don't skip those expansions. +- // The challenge is how to so without type-checking +- // a lot of non-workspace packages not covered by +- // the initial workspace load. +- if _, ok := workspaceMap[rdepID]; !ok { +- continue +- } - --// created for Literal (Lit_CodeAction_disabled) --type PDisabledMsg_textDocument_codeAction struct { -- // Human readable description of why the code action is currently disabled. -- // -- // This is displayed in the code actions UI. -- Reason string `json:"reason"` --} +- globalScope[rdepID] = rdep +- } +- return nil +- } - --// created for Literal (Lit_FoldingRangeClientCapabilities_foldingRangeKind) --type PFoldingRangeKindPFoldingRange struct { -- // The folding range kind values the client supports. When this -- // property exists the client also guarantees that it will -- // handle values outside its set gracefully and falls back -- // to a default value when unknown. -- ValueSet []FoldingRangeKind `json:"valueSet,omitempty"` --} +- // How far need we search? +- // For package-level objects, we need only search the direct importers. +- // For fields and methods, we must search transitively. +- transitive := obj.Pkg().Scope().Lookup(obj.Name()) != obj - --// created for Literal (Lit_FoldingRangeClientCapabilities_foldingRange) --type PFoldingRangePFoldingRange struct { -- // If set, the client signals that it supports setting collapsedText on -- // folding ranges to display custom labels instead of the default text. -- // -- // @since 3.17.0 -- CollapsedText bool `json:"collapsedText,omitempty"` --} +- // The scope is the union of rdeps of each variant. +- // (Each set is disjoint so there's no benefit to +- // combining the metadata graph traversals.) +- for _, mp := range variants { +- if err := addRdeps(mp.ID, transitive); err != nil { +- return nil, err +- } +- } - --// created for Literal (Lit_SemanticTokensOptions_full_Item1) --type PFullESemanticTokensOptions struct { -- // The server supports deltas for full documents. -- Delta bool `json:"delta"` --} +- // Is object a method? +- // +- // If so, expand the search so that the targets include +- // all methods that correspond to it through interface +- // satisfaction, and the scope includes the rdeps of +- // the package that declares each corresponding type. +- // +- // 'expansions' records the packages that declared +- // such types. +- if recv := effectiveReceiver(obj); recv != nil { +- if err := expandMethodSearch(ctx, snapshot, workspaceIDs, obj.(*types.Func), recv, addRdeps, globalTargets, expansions); err != nil { +- return nil, err +- } +- } +- } - --// created for Literal (Lit_CompletionList_itemDefaults) --type PItemDefaultsMsg_textDocument_completion struct { -- // A default commit character set. -- // -- // @since 3.17.0 -- CommitCharacters []string `json:"commitCharacters,omitempty"` -- // A default edit range. -- // -- // @since 3.17.0 -- EditRange *OrFEditRangePItemDefaults `json:"editRange,omitempty"` -- // A default insert text format. -- // -- // @since 3.17.0 -- InsertTextFormat *InsertTextFormat `json:"insertTextFormat,omitempty"` -- // A default insert text mode. +- // The search functions will call report(loc) for each hit. +- var ( +- refsMu sync.Mutex +- refs []reference +- ) +- report := func(loc protocol.Location, isDecl bool) { +- ref := reference{ +- isDeclaration: isDecl, +- location: loc, +- pkgPath: pkg.Metadata().PkgPath, +- } +- refsMu.Lock() +- refs = append(refs, ref) +- refsMu.Unlock() +- } +- +- // Loop over the variants of the declaring package, +- // and perform both the local (in-package) and global +- // (cross-package) searches, in parallel. - // -- // @since 3.17.0 -- InsertTextMode *InsertTextMode `json:"insertTextMode,omitempty"` -- // A default data value. +- // TODO(adonovan): opt: support LSP reference streaming. See: +- // - https://github.com/microsoft/vscode-languageserver-node/pull/164 +- // - https://github.com/microsoft/language-server-protocol/pull/182 - // -- // @since 3.17.0 -- Data interface{} `json:"data,omitempty"` --} +- // Careful: this goroutine must not return before group.Wait. +- var group errgroup.Group - --// created for Literal (Lit_WorkspaceSymbol_location_Item1) --type PLocationMsg_workspace_symbol struct { -- URI DocumentURI `json:"uri"` --} +- // Compute local references for each variant. +- // The target objects are identified by (URI, offset). +- for _, mp := range variants { +- // We want the ordinary importable package, +- // plus any test-augmented variants, since +- // declarations in _test.go files may change +- // the reference of a selection, or even a +- // field into a method or vice versa. +- // +- // But we don't need intermediate test variants, +- // as their local references will be covered +- // already by other variants. +- if mp.IsIntermediateTestVariant() { +- continue +- } +- mp := mp +- group.Go(func() error { +- // TODO(adonovan): opt: batch these TypeChecks. +- pkgs, err := snapshot.TypeCheck(ctx, mp.ID) +- if err != nil { +- return err +- } +- pkg := pkgs[0] - --// created for Literal (Lit_ShowMessageRequestClientCapabilities_messageActionItem) --type PMessageActionItemPShowMessage struct { -- // Whether the client supports additional attributes which -- // are preserved and send back to the server in the -- // request's response. -- AdditionalPropertiesSupport bool `json:"additionalPropertiesSupport,omitempty"` --} +- // Find the declaration of the corresponding +- // object in this package based on (URI, offset). +- pgf, err := pkg.File(declURI) +- if err != nil { +- return err +- } +- pos, err := safetoken.Pos(pgf.Tok, declPosn.Offset) +- if err != nil { +- return err +- } +- objects, _, err := objectsAt(pkg.TypesInfo(), pgf.File, pos) +- if err != nil { +- return err // unreachable? (probably caught earlier) +- } - --// created for Literal (Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item0) --type PNotebookSelectorPNotebookDocumentSync struct { -- // The notebook to be synced If a string -- // value is provided it matches against the -- // notebook type. '*' matches every notebook. -- Notebook OrFNotebookPNotebookSelector `json:"notebook"` -- // The cells of the matching notebook to be synced. -- Cells []Lit_NotebookDocumentSyncOptions_notebookSelector_Elem_Item0_cells_Elem `json:"cells,omitempty"` --} +- // Report the locations of the declaration(s). +- // TODO(adonovan): what about for corresponding methods? Add tests. +- for _, node := range objects { +- report(mustLocation(pgf, node), true) +- } - --// created for Literal (Lit_SemanticTokensOptions_range_Item1) --type PRangeESemanticTokensOptions struct { --} +- // Convert targets map to set. +- targets := make(map[types.Object]bool) +- for obj := range objects { +- targets[obj] = true +- } - --// created for Literal (Lit_SemanticTokensClientCapabilities_requests) --type PRequestsPSemanticTokens struct { -- // The client will send the `textDocument/semanticTokens/range` request if -- // the server provides a corresponding handler. -- Range Or_SemanticTokensClientCapabilities_requests_range `json:"range"` -- // The client will send the `textDocument/semanticTokens/full` request if -- // the server provides a corresponding handler. -- Full Or_SemanticTokensClientCapabilities_requests_full `json:"full"` --} +- return localReferences(pkg, targets, true, report) +- }) +- } - --// created for Literal (Lit_CodeActionClientCapabilities_resolveSupport) --type PResolveSupportPCodeAction struct { -- // The properties that a client can resolve lazily. -- Properties []string `json:"properties"` --} +- // Also compute local references within packages that declare +- // corresponding methods (see above), which expand the global search. +- // The target objects are identified by (PkgPath, objectpath). +- for id := range expansions { +- id := id +- group.Go(func() error { +- // TODO(adonovan): opt: batch these TypeChecks. +- pkgs, err := snapshot.TypeCheck(ctx, id) +- if err != nil { +- return err +- } +- pkg := pkgs[0] - --// created for Literal (Lit_InlayHintClientCapabilities_resolveSupport) --type PResolveSupportPInlayHint struct { -- // The properties that a client can resolve lazily. -- Properties []string `json:"properties"` --} +- targets := make(map[types.Object]bool) +- for objpath := range globalTargets[pkg.Metadata().PkgPath] { +- obj, err := objectpath.Object(pkg.Types(), objpath) +- if err != nil { +- // No such object, because it was +- // declared only in the test variant. +- continue +- } +- targets[obj] = true +- } - --// created for Literal (Lit_WorkspaceSymbolClientCapabilities_resolveSupport) --type PResolveSupportPSymbol struct { -- // The properties that a client can resolve lazily. Usually -- // `location.range` -- Properties []string `json:"properties"` --} +- // Don't include corresponding types or methods +- // since expansions did that already, and we don't +- // want (e.g.) concrete -> interface -> concrete. +- const correspond = false +- return localReferences(pkg, targets, correspond, report) +- }) +- } - --// created for Literal (Lit_InitializeResult_serverInfo) --type PServerInfoMsg_initialize struct { -- // The name of the server as defined by the server. -- Name string `json:"name"` -- // The server's version as defined by the server. -- Version string `json:"version,omitempty"` --} +- // Compute global references for selected reverse dependencies. +- group.Go(func() error { +- var globalIDs []PackageID +- for id := range globalScope { +- globalIDs = append(globalIDs, id) +- } +- indexes, err := snapshot.References(ctx, globalIDs...) +- if err != nil { +- return err +- } +- for _, index := range indexes { +- for _, loc := range index.Lookup(globalTargets) { +- report(loc, false) +- } +- } +- return nil +- }) - --// created for Literal (Lit_SignatureHelpClientCapabilities_signatureInformation) --type PSignatureInformationPSignatureHelp struct { -- // Client supports the following content formats for the documentation -- // property. The order describes the preferred format of the client. -- DocumentationFormat []MarkupKind `json:"documentationFormat,omitempty"` -- // Client capabilities specific to parameter information. -- ParameterInformation *FParameterInformationPSignatureInformation `json:"parameterInformation,omitempty"` -- // The client supports the `activeParameter` property on `SignatureInformation` -- // literal. -- // -- // @since 3.16.0 -- ActiveParameterSupport bool `json:"activeParameterSupport,omitempty"` +- if err := group.Wait(); err != nil { +- return nil, err +- } +- return refs, nil -} - --// created for Literal (Lit_GeneralClientCapabilities_staleRequestSupport) --type PStaleRequestSupportPGeneral struct { -- // The client will actively cancel the request. -- Cancel bool `json:"cancel"` -- // The list of requests for which the client -- // will retry the request if it receives a -- // response with error code `ContentModified` -- RetryOnContentModified []string `json:"retryOnContentModified"` --} +-// expandMethodSearch expands the scope and targets of a global search +-// for an exported method to include all methods in the workspace +-// that correspond to it through interface satisfaction. +-// +-// Each package that declares a corresponding type is added to +-// expansions so that we can also find local references to the type +-// within the package, which of course requires type checking. +-// +-// The scope is expanded by a sequence of calls (not concurrent) to addRdeps. +-// +-// recv is the method's effective receiver type, for method-set computations. +-func expandMethodSearch(ctx context.Context, snapshot *cache.Snapshot, workspaceIDs []PackageID, method *types.Func, recv types.Type, addRdeps func(id PackageID, transitive bool) error, targets map[PackagePath]map[objectpath.Path]unit, expansions map[PackageID]unit) error { +- // Compute the method-set fingerprint used as a key to the global search. +- key, hasMethods := methodsets.KeyOf(recv) +- if !hasMethods { +- return bug.Errorf("KeyOf(%s)={} yet %s is a method", recv, method) +- } +- // Search the methodset index of each package in the workspace. +- indexes, err := snapshot.MethodSets(ctx, workspaceIDs...) +- if err != nil { +- return err +- } +- var mu sync.Mutex // guards addRdeps, targets, expansions +- var group errgroup.Group +- for i, index := range indexes { +- i := i +- index := index +- group.Go(func() error { +- // Consult index for matching methods. +- results := index.Search(key, method.Name()) +- if len(results) == 0 { +- return nil +- } - --// created for Literal (Lit_DocumentSymbolClientCapabilities_symbolKind) --type PSymbolKindPDocumentSymbol struct { -- // The symbol kind values the client supports. When this -- // property exists the client also guarantees that it will -- // handle values outside its set gracefully and falls back -- // to a default value when unknown. -- // -- // If this property is not present the client only supports -- // the symbol kinds from `File` to `Array` as defined in -- // the initial version of the protocol. -- ValueSet []SymbolKind `json:"valueSet,omitempty"` --} +- // We have discovered one or more corresponding types. +- id := workspaceIDs[i] - --// created for Literal (Lit_WorkspaceSymbolClientCapabilities_symbolKind) --type PSymbolKindPSymbol struct { -- // The symbol kind values the client supports. When this -- // property exists the client also guarantees that it will -- // handle values outside its set gracefully and falls back -- // to a default value when unknown. -- // -- // If this property is not present the client only supports -- // the symbol kinds from `File` to `Array` as defined in -- // the initial version of the protocol. -- ValueSet []SymbolKind `json:"valueSet,omitempty"` --} +- mu.Lock() +- defer mu.Unlock() - --// created for Literal (Lit_DocumentSymbolClientCapabilities_tagSupport) --type PTagSupportPDocumentSymbol struct { -- // The tags supported by the client. -- ValueSet []SymbolTag `json:"valueSet"` --} +- // Expand global search scope to include rdeps of this pkg. +- if err := addRdeps(id, true); err != nil { +- return err +- } - --// created for Literal (Lit_PublishDiagnosticsClientCapabilities_tagSupport) --type PTagSupportPPublishDiagnostics struct { -- // The tags supported by the client. -- ValueSet []DiagnosticTag `json:"valueSet"` --} +- // Mark this package so that we search within it for +- // local references to the additional types/methods. +- expansions[id] = unit{} - --// created for Literal (Lit_WorkspaceSymbolClientCapabilities_tagSupport) --type PTagSupportPSymbol struct { -- // The tags supported by the client. -- ValueSet []SymbolTag `json:"valueSet"` +- // Add each corresponding method the to set of global search targets. +- for _, res := range results { +- methodPkg := PackagePath(res.PkgPath) +- opaths, ok := targets[methodPkg] +- if !ok { +- opaths = make(map[objectpath.Path]unit) +- targets[methodPkg] = opaths +- } +- opaths[res.ObjectPath] = unit{} +- } +- return nil +- }) +- } +- return group.Wait() -} - --// The parameters of a configuration request. --type ParamConfiguration struct { -- Items []ConfigurationItem `json:"items"` --} --type ParamInitialize struct { -- XInitializeParams -- WorkspaceFoldersInitializeParams --} +-// localReferences traverses syntax and reports each reference to one +-// of the target objects, or (if correspond is set) an object that +-// corresponds to one of them via interface satisfaction. +-func localReferences(pkg *cache.Package, targets map[types.Object]bool, correspond bool, report func(loc protocol.Location, isDecl bool)) error { +- // If we're searching for references to a method optionally +- // broaden the search to include references to corresponding +- // methods of mutually assignable receiver types. +- // (We use a slice, but objectsAt never returns >1 methods.) +- var methodRecvs []types.Type +- var methodName string // name of an arbitrary target, iff a method +- if correspond { +- for obj := range targets { +- if t := effectiveReceiver(obj); t != nil { +- methodRecvs = append(methodRecvs, t) +- methodName = obj.Name() +- } +- } +- } - --// Represents a parameter of a callable-signature. A parameter can --// have a label and a doc-comment. --type ParameterInformation struct { -- // The label of this parameter information. -- // -- // Either a string or an inclusive start and exclusive end offsets within its containing -- // signature label. (see SignatureInformation.label). The offsets are based on a UTF-16 -- // string representation as `Position` and `Range` does. -- // -- // *Note*: a label of type string should be a substring of its containing signature label. -- // Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`. -- Label string `json:"label"` -- // The human-readable doc-comment of this parameter. Will be shown -- // in the UI but can be omitted. -- Documentation string `json:"documentation,omitempty"` +- // matches reports whether obj either is or corresponds to a target. +- // (Correspondence is defined as usual for interface methods.) +- matches := func(obj types.Object) bool { +- if containsOrigin(targets, obj) { +- return true +- } +- if methodRecvs != nil && obj.Name() == methodName { +- if orecv := effectiveReceiver(obj); orecv != nil { +- for _, mrecv := range methodRecvs { +- if concreteImplementsIntf(orecv, mrecv) { +- return true +- } +- } +- } +- } +- return false +- } +- +- // Scan through syntax looking for uses of one of the target objects. +- for _, pgf := range pkg.CompiledGoFiles() { +- ast.Inspect(pgf.File, func(n ast.Node) bool { +- if id, ok := n.(*ast.Ident); ok { +- if obj, ok := pkg.TypesInfo().Uses[id]; ok && matches(obj) { +- report(mustLocation(pgf, id), false) +- } +- } +- return true +- }) +- } +- return nil -} --type PartialResultParams struct { -- // An optional token that a server can use to report partial results (e.g. streaming) to -- // the client. -- PartialResultToken *ProgressToken `json:"partialResultToken,omitempty"` +- +-// effectiveReceiver returns the effective receiver type for method-set +-// comparisons for obj, if it is a method, or nil otherwise. +-func effectiveReceiver(obj types.Object) types.Type { +- if fn, ok := obj.(*types.Func); ok { +- if recv := fn.Type().(*types.Signature).Recv(); recv != nil { +- return methodsets.EnsurePointer(recv.Type()) +- } +- } +- return nil -} - --// The glob pattern to watch relative to the base path. Glob patterns can have the following syntax: --// --// - `*` to match one or more characters in a path segment --// - `?` to match on one character in a path segment --// - `**` to match any number of path segments, including none --// - `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) --// - `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) --// - `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) +-// objectsAt returns the non-empty set of objects denoted (def or use) +-// by the specified position within a file syntax tree, or an error if +-// none were found. -// --// @since 3.17.0 --type Pattern = string // (alias) line 14778 --// Position in a text document expressed as zero-based line and character --// offset. Prior to 3.17 the offsets were always based on a UTF-16 string --// representation. So a string of the form `a𐐀b` the character offset of the --// character `a` is 0, the character offset of `𐐀` is 1 and the character --// offset of b is 3 since `𐐀` is represented using two code units in UTF-16. --// Since 3.17 clients and servers can agree on a different string encoding --// representation (e.g. UTF-8). The client announces it's supported encoding --// via the client capability [`general.positionEncodings`](#clientCapabilities). --// The value is an array of position encodings the client supports, with --// decreasing preference (e.g. the encoding at index `0` is the most preferred --// one). To stay backwards compatible the only mandatory encoding is UTF-16 --// represented via the string `utf-16`. The server can pick one of the --// encodings offered by the client and signals that encoding back to the --// client via the initialize result's property --// [`capabilities.positionEncoding`](#serverCapabilities). If the string value --// `utf-16` is missing from the client's capability `general.positionEncodings` --// servers can safely assume that the client supports UTF-16. If the server --// omits the position encoding in its initialize result the encoding defaults --// to the string value `utf-16`. Implementation considerations: since the --// conversion from one encoding into another requires the content of the --// file / line the conversion is best done where the file is read which is --// usually on the server side. +-// The result may contain more than one element because all case +-// variables of a type switch appear to be declared at the same +-// position. -// --// Positions are line end character agnostic. So you can not specify a position --// that denotes `\r|\n` or `\n|` where `|` represents the character offset. +-// Each object is mapped to the syntax node that was treated as an +-// identifier, which is not always an ast.Ident. The second component +-// of the result is the innermost node enclosing pos. -// --// @since 3.17.0 - support for negotiated position encoding. --type Position struct { -- // Line position in a document (zero-based). -- // -- // If a line number is greater than the number of lines in a document, it defaults back to the number of lines in the document. -- // If a line number is negative, it defaults to 0. -- Line uint32 `json:"line"` -- // Character offset on a line in a document (zero-based). -- // -- // The meaning of this offset is determined by the negotiated -- // `PositionEncodingKind`. -- // -- // If the character value is greater than the line length it defaults back to the -- // line length. -- Character uint32 `json:"character"` --} +-// TODO(adonovan): factor in common with referencedObject. +-func objectsAt(info *types.Info, file *ast.File, pos token.Pos) (map[types.Object]ast.Node, ast.Node, error) { +- path := pathEnclosingObjNode(file, pos) +- if path == nil { +- return nil, nil, ErrNoIdentFound +- } - --// A set of predefined position encoding kinds. --// --// @since 3.17.0 --type PositionEncodingKind string --type PrepareRename2Gn = Msg_PrepareRename2Gn // (alias) line 13927 --type PrepareRenameParams struct { -- TextDocumentPositionParams -- WorkDoneProgressParams --} --type PrepareRenameResult = Msg_PrepareRename2Gn // (alias) line 13927 --type PrepareSupportDefaultBehavior uint32 +- targets := make(map[types.Object]ast.Node) - --// A previous result id in a workspace pull request. --// --// @since 3.17.0 --type PreviousResultID struct { -- // The URI for which the client knowns a -- // result id. -- URI DocumentURI `json:"uri"` -- // The value of the previous result id. -- Value string `json:"value"` +- switch leaf := path[0].(type) { +- case *ast.Ident: +- // If leaf represents an implicit type switch object or the type +- // switch "assign" variable, expand to all of the type switch's +- // implicit objects. +- if implicits, _ := typeSwitchImplicits(info, path); len(implicits) > 0 { +- for _, obj := range implicits { +- targets[obj] = leaf +- } +- } else { +- // Note: prior to go1.21, go/types issue #60372 causes the position +- // a field Var T created for struct{*p.T} to be recorded at the +- // start of the field type ("*") not the location of the T. +- // This affects references and other gopls operations (issue #60369). +- // TODO(adonovan): delete this comment when we drop support for go1.20. +- +- // For struct{T}, we prefer the defined field Var over the used TypeName. +- obj := info.ObjectOf(leaf) +- if obj == nil { +- return nil, nil, fmt.Errorf("%w for %q", errNoObjectFound, leaf.Name) +- } +- targets[obj] = leaf +- } +- case *ast.ImportSpec: +- // Look up the implicit *types.PkgName. +- obj := info.Implicits[leaf] +- if obj == nil { +- return nil, nil, fmt.Errorf("%w for import %s", errNoObjectFound, metadata.UnquoteImportPath(leaf)) +- } +- targets[obj] = leaf +- } +- +- if len(targets) == 0 { +- return nil, nil, fmt.Errorf("objectAt: internal error: no targets") // can't happen +- } +- return targets, path[0], nil -} - --// A previous result id in a workspace pull request. +-// mustLocation reports the location interval a syntax node, +-// which must belong to m.File. -// --// @since 3.17.0 --type PreviousResultId struct { -- // The URI for which the client knowns a -- // result id. -- URI DocumentURI `json:"uri"` -- // The value of the previous result id. -- Value string `json:"value"` --} --type ProgressParams struct { -- // The progress token provided by the client or server. -- Token ProgressToken `json:"token"` -- // The progress data. -- Value interface{} `json:"value"` --} --type ProgressToken = interface{} // (alias) line 14375 --// The publish diagnostic client capabilities. --type PublishDiagnosticsClientCapabilities struct { -- // Whether the clients accepts diagnostics with related information. -- RelatedInformation bool `json:"relatedInformation,omitempty"` -- // Client supports the tag property to provide meta data about a diagnostic. -- // Clients supporting tags have to handle unknown tags gracefully. -- // -- // @since 3.15.0 -- TagSupport *PTagSupportPPublishDiagnostics `json:"tagSupport,omitempty"` -- // Whether the client interprets the version property of the -- // `textDocument/publishDiagnostics` notification's parameter. -- // -- // @since 3.15.0 -- VersionSupport bool `json:"versionSupport,omitempty"` -- // Client supports a codeDescription property -- // -- // @since 3.16.0 -- CodeDescriptionSupport bool `json:"codeDescriptionSupport,omitempty"` -- // Whether code action supports the `data` property which is -- // preserved between a `textDocument/publishDiagnostics` and -- // `textDocument/codeAction` request. -- // -- // @since 3.16.0 -- DataSupport bool `json:"dataSupport,omitempty"` +-// Safe for use only by references and implementations. +-func mustLocation(pgf *parsego.File, n ast.Node) protocol.Location { +- loc, err := pgf.NodeLocation(n) +- if err != nil { +- panic(err) // can't happen in references or implementations +- } +- return loc -} +diff -urN a/gopls/internal/golang/rename_check.go b/gopls/internal/golang/rename_check.go +--- a/gopls/internal/golang/rename_check.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/rename_check.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,959 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +-// +-// Taken from golang.org/x/tools/refactor/rename. - --// The publish diagnostic notification's parameters. --type PublishDiagnosticsParams struct { -- // The URI for which diagnostic information is reported. -- URI DocumentURI `json:"uri"` -- // Optional the version number of the document the diagnostics are published for. -- // -- // @since 3.15.0 -- Version int32 `json:"version,omitempty"` -- // An array of diagnostic information items. -- Diagnostics []Diagnostic `json:"diagnostics"` --} +-package golang - --// A range in a text document expressed as (zero-based) start and end positions. +-// This file defines the conflict-checking portion of the rename operation. -// --// If you want to specify a range that contains a line including the line ending --// character(s) then use an end position denoting the start of the next line. --// For example: --// ```ts +-// The renamer works on a single package of type-checked syntax, and +-// is called in parallel for all necessary packages in the workspace, +-// possibly up to the transitive reverse dependencies of the +-// declaration. Finally the union of all edits and errors is computed. -// --// { --// start: { line: 5, character: 23 } --// end : { line 6, character : 0 } --// } +-// Renaming one object may entail renaming of others. For example: -// --// ``` --type Range struct { -- // The range's start position. -- Start Position `json:"start"` -- // The range's end position. -- End Position `json:"end"` --} -- --// Client Capabilities for a {@link ReferencesRequest}. --type ReferenceClientCapabilities struct { -- // Whether references supports dynamic registration. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` --} +-// - An embedded field couples a Var (field) and a TypeName. +-// So, renaming either one requires renaming the other. +-// If the initial object is an embedded field, we must add its +-// TypeName (and its enclosing package) to the renaming set; +-// this is easily discovered at the outset. +-// +-// Conversely, if the initial object is a TypeName, we must observe +-// whether any of its references (from directly importing packages) +-// is coincident with an embedded field Var and, if so, initiate a +-// renaming of it. +-// +-// - A method of an interface type is coupled to all corresponding +-// methods of types that are assigned to the interface (as +-// discovered by the 'satisfy' pass). As a matter of usability, we +-// require that such renamings be initiated from the interface +-// method, not the concrete method. - --// Value-object that contains additional information when --// requesting references. --type ReferenceContext struct { -- // Include the declaration of the current symbol. -- IncludeDeclaration bool `json:"includeDeclaration"` --} +-import ( +- "fmt" +- "go/ast" +- "go/token" +- "go/types" +- "path/filepath" +- "reflect" +- "strings" +- "unicode" - --// Reference options. --type ReferenceOptions struct { -- WorkDoneProgressOptions --} +- "golang.org/x/tools/go/ast/astutil" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/internal/aliases" +- "golang.org/x/tools/internal/typeparams" +- "golang.org/x/tools/internal/typesinternal" +- "golang.org/x/tools/refactor/satisfy" +-) - --// Parameters for a {@link ReferencesRequest}. --type ReferenceParams struct { -- Context ReferenceContext `json:"context"` -- TextDocumentPositionParams -- WorkDoneProgressParams -- PartialResultParams --} -- --// Registration options for a {@link ReferencesRequest}. --type ReferenceRegistrationOptions struct { -- TextDocumentRegistrationOptions -- ReferenceOptions --} +-// errorf reports an error (e.g. conflict) and prevents file modification. +-func (r *renamer) errorf(pos token.Pos, format string, args ...interface{}) { +- // Conflict error messages in the old gorename tool (whence this +- // logic originated) contain rich information associated with +- // multiple source lines, such as: +- // +- // p/a.go:1:2: renaming "x" to "y" here +- // p/b.go:3:4: \t would cause this reference to "y" +- // p/c.go:5:5: \t to become shadowed by this intervening declaration. +- // +- // Unfortunately LSP provides no means to transmit the +- // structure of this error, so we format the positions briefly +- // using dir/file.go where dir is the base name of the parent +- // directory. - --// General parameters to register for a notification or to register a provider. --type Registration struct { -- // The id used to register the request. The id can be used to deregister -- // the request again. -- ID string `json:"id"` -- // The method / capability to register for. -- Method string `json:"method"` -- // Options necessary for the registration. -- RegisterOptions interface{} `json:"registerOptions,omitempty"` --} --type RegistrationParams struct { -- Registrations []Registration `json:"registrations"` --} +- var conflict strings.Builder - --// Client capabilities specific to regular expressions. --// --// @since 3.16.0 --type RegularExpressionsClientCapabilities struct { -- // The engine's name. -- Engine string `json:"engine"` -- // The engine's version. -- Version string `json:"version,omitempty"` --} +- // Add prefix of (truncated) position. +- if pos != token.NoPos { +- // TODO(adonovan): skip position of first error if it is +- // on the same line as the renaming itself. +- posn := safetoken.StartPosition(r.pkg.FileSet(), pos).String() +- segments := strings.Split(filepath.ToSlash(posn), "/") +- if n := len(segments); n > 2 { +- segments = segments[n-2:] +- } +- posn = strings.Join(segments, "/") +- fmt.Fprintf(&conflict, "%s:", posn) - --// A full diagnostic report with a set of related documents. --// --// @since 3.17.0 --type RelatedFullDocumentDiagnosticReport struct { -- // Diagnostics of related documents. This information is useful -- // in programming languages where code in a file A can generate -- // diagnostics in a file B which A depends on. An example of -- // such a language is C/C++ where marco definitions in a file -- // a.cpp and result in errors in a header file b.hpp. -- // -- // @since 3.17.0 -- RelatedDocuments map[DocumentURI]interface{} `json:"relatedDocuments,omitempty"` -- FullDocumentDiagnosticReport --} +- if !strings.HasPrefix(format, "\t") { +- conflict.WriteByte(' ') +- } +- } - --// An unchanged diagnostic report with a set of related documents. --// --// @since 3.17.0 --type RelatedUnchangedDocumentDiagnosticReport struct { -- // Diagnostics of related documents. This information is useful -- // in programming languages where code in a file A can generate -- // diagnostics in a file B which A depends on. An example of -- // such a language is C/C++ where marco definitions in a file -- // a.cpp and result in errors in a header file b.hpp. -- // -- // @since 3.17.0 -- RelatedDocuments map[DocumentURI]interface{} `json:"relatedDocuments,omitempty"` -- UnchangedDocumentDiagnosticReport +- fmt.Fprintf(&conflict, format, args...) +- r.conflicts = append(r.conflicts, conflict.String()) -} - --// A relative pattern is a helper to construct glob patterns that are matched --// relatively to a base URI. The common value for a `baseUri` is a workspace --// folder root, but it can be another absolute URI as well. --// --// @since 3.17.0 --type RelativePattern struct { -- // A workspace folder or a base URI to which this pattern will be matched -- // against relatively. -- BaseURI Or_RelativePattern_baseUri `json:"baseUri"` -- // The actual glob pattern; -- Pattern Pattern `json:"pattern"` --} --type RenameClientCapabilities struct { -- // Whether rename supports dynamic registration. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -- // Client supports testing for validity of rename operations -- // before execution. -- // -- // @since 3.12.0 -- PrepareSupport bool `json:"prepareSupport,omitempty"` -- // Client supports the default behavior result. -- // -- // The value indicates the default behavior used by the -- // client. -- // -- // @since 3.16.0 -- PrepareSupportDefaultBehavior *PrepareSupportDefaultBehavior `json:"prepareSupportDefaultBehavior,omitempty"` -- // Whether the client honors the change annotations in -- // text edits and resource operations returned via the -- // rename request's workspace edit by for example presenting -- // the workspace edit in the user interface and asking -- // for confirmation. -- // -- // @since 3.16.0 -- HonorsChangeAnnotations bool `json:"honorsChangeAnnotations,omitempty"` --} +-// check performs safety checks of the renaming of the 'from' object to r.to. +-func (r *renamer) check(from types.Object) { +- if r.objsToUpdate[from] { +- return +- } +- r.objsToUpdate[from] = true - --// Rename file operation --type RenameFile struct { -- // A rename -- Kind string `json:"kind"` -- // The old (existing) location. -- OldURI DocumentURI `json:"oldUri"` -- // The new location. -- NewURI DocumentURI `json:"newUri"` -- // Rename options. -- Options *RenameFileOptions `json:"options,omitempty"` -- ResourceOperation +- // NB: order of conditions is important. +- if from_, ok := from.(*types.PkgName); ok { +- r.checkInFileBlock(from_) +- } else if from_, ok := from.(*types.Label); ok { +- r.checkLabel(from_) +- } else if isPackageLevel(from) { +- r.checkInPackageBlock(from) +- } else if v, ok := from.(*types.Var); ok && v.IsField() { +- r.checkStructField(v) +- } else if f, ok := from.(*types.Func); ok && recv(f) != nil { +- r.checkMethod(f) +- } else if isLocal(from) { +- r.checkInLexicalScope(from) +- } else { +- r.errorf(from.Pos(), "unexpected %s object %q (please report a bug)\n", +- objectKind(from), from) +- } -} - --// Rename file options --type RenameFileOptions struct { -- // Overwrite target if existing. Overwrite wins over `ignoreIfExists` -- Overwrite bool `json:"overwrite,omitempty"` -- // Ignores if target exists. -- IgnoreIfExists bool `json:"ignoreIfExists,omitempty"` --} +-// checkInFileBlock performs safety checks for renames of objects in the file block, +-// i.e. imported package names. +-func (r *renamer) checkInFileBlock(from *types.PkgName) { +- // Check import name is not "init". +- if r.to == "init" { +- r.errorf(from.Pos(), "%q is not a valid imported package name", r.to) +- } - --// The parameters sent in notifications/requests for user-initiated renames of --// files. --// --// @since 3.16.0 --type RenameFilesParams struct { -- // An array of all files/folders renamed in this operation. When a folder is renamed, only -- // the folder will be included, and not its children. -- Files []FileRename `json:"files"` --} +- // Check for conflicts between file and package block. +- if prev := from.Pkg().Scope().Lookup(r.to); prev != nil { +- r.errorf(from.Pos(), "renaming this %s %q to %q would conflict", +- objectKind(from), from.Name(), r.to) +- r.errorf(prev.Pos(), "\twith this package member %s", +- objectKind(prev)) +- return // since checkInPackageBlock would report redundant errors +- } - --// Provider options for a {@link RenameRequest}. --type RenameOptions struct { -- // Renames should be checked and tested before being executed. -- // -- // @since version 3.12.0 -- PrepareProvider bool `json:"prepareProvider,omitempty"` -- WorkDoneProgressOptions +- // Check for conflicts in lexical scope. +- r.checkInLexicalScope(from) -} - --// The parameters of a {@link RenameRequest}. --type RenameParams struct { -- // The document to rename. -- TextDocument TextDocumentIdentifier `json:"textDocument"` -- // The position at which this request was sent. -- Position Position `json:"position"` -- // The new name of the symbol. If the given name is not valid the -- // request must return a {@link ResponseError} with an -- // appropriate message set. -- NewName string `json:"newName"` -- WorkDoneProgressParams --} +-// checkInPackageBlock performs safety checks for renames of +-// func/var/const/type objects in the package block. +-func (r *renamer) checkInPackageBlock(from types.Object) { +- // Check that there are no references to the name from another +- // package if the renaming would make it unexported. +- if typ := r.pkg.Types(); typ != from.Pkg() && ast.IsExported(r.from) && !ast.IsExported(r.to) { +- if id := someUse(r.pkg.TypesInfo(), from); id != nil { +- r.checkExport(id, typ, from) +- } +- } - --// Registration options for a {@link RenameRequest}. --type RenameRegistrationOptions struct { -- TextDocumentRegistrationOptions -- RenameOptions --} +- // Check that in the package block, "init" is a function, and never referenced. +- if r.to == "init" { +- kind := objectKind(from) +- if kind == "func" { +- // Reject if intra-package references to it exist. +- for id, obj := range r.pkg.TypesInfo().Uses { +- if obj == from { +- r.errorf(from.Pos(), +- "renaming this func %q to %q would make it a package initializer", +- from.Name(), r.to) +- r.errorf(id.Pos(), "\tbut references to it exist") +- break +- } +- } +- } else { +- r.errorf(from.Pos(), "you cannot have a %s at package level named %q", +- kind, r.to) +- } +- } - --// A generic resource operation. --type ResourceOperation struct { -- // The resource operation kind. -- Kind string `json:"kind"` -- // An optional annotation identifier describing the operation. -- // -- // @since 3.16.0 -- AnnotationID *ChangeAnnotationIdentifier `json:"annotationId,omitempty"` --} --type ResourceOperationKind string +- // Check for conflicts between package block and all file blocks. +- for _, f := range r.pkg.Syntax() { +- fileScope := r.pkg.TypesInfo().Scopes[f] +- b, prev := fileScope.LookupParent(r.to, token.NoPos) +- if b == fileScope { +- r.errorf(from.Pos(), "renaming this %s %q to %q would conflict", objectKind(from), from.Name(), r.to) +- var prevPos token.Pos +- if prev != nil { +- prevPos = prev.Pos() +- } +- r.errorf(prevPos, "\twith this %s", objectKind(prev)) +- return // since checkInPackageBlock would report redundant errors +- } +- } - --// Save options. --type SaveOptions struct { -- // The client is supposed to include the content on save. -- IncludeText bool `json:"includeText,omitempty"` +- // Check for conflicts in lexical scope. +- r.checkInLexicalScope(from) -} - --// Describes the currently selected completion item. +-// checkInLexicalScope performs safety checks that a renaming does not +-// change the lexical reference structure of the specified package. -// --// @since 3.18.0 --// @proposed --type SelectedCompletionInfo struct { -- // The range that will be replaced if this completion item is accepted. -- Range Range `json:"range"` -- // The text the range will be replaced with if this completion is accepted. -- Text string `json:"text"` --} +-// For objects in lexical scope, there are three kinds of conflicts: +-// same-, sub-, and super-block conflicts. We will illustrate all three +-// using this example: +-// +-// var x int +-// var z int +-// +-// func f(y int) { +-// print(x) +-// print(y) +-// } +-// +-// Renaming x to z encounters a "same-block conflict", because an object +-// with the new name already exists, defined in the same lexical block +-// as the old object. +-// +-// Renaming x to y encounters a "sub-block conflict", because there exists +-// a reference to x from within (what would become) a hole in its scope. +-// The definition of y in an (inner) sub-block would cast a shadow in +-// the scope of the renamed variable. +-// +-// Renaming y to x encounters a "super-block conflict". This is the +-// converse situation: there is an existing definition of the new name +-// (x) in an (enclosing) super-block, and the renaming would create a +-// hole in its scope, within which there exist references to it. The +-// new name shadows the existing definition of x in the super-block. +-// +-// Removing the old name (and all references to it) is always safe, and +-// requires no checks. +-func (r *renamer) checkInLexicalScope(from types.Object) { +- b := from.Parent() // the block defining the 'from' object +- if b != nil { +- toBlock, to := b.LookupParent(r.to, from.Parent().End()) +- if toBlock == b { +- // same-block conflict +- r.errorf(from.Pos(), "renaming this %s %q to %q", +- objectKind(from), from.Name(), r.to) +- r.errorf(to.Pos(), "\tconflicts with %s in same block", +- objectKind(to)) +- return +- } else if toBlock != nil { +- // Check for super-block conflict. +- // The name r.to is defined in a superblock. +- // Is that name referenced from within this block? +- forEachLexicalRef(r.pkg, to, func(id *ast.Ident, block *types.Scope) bool { +- _, obj := block.LookupParent(from.Name(), id.Pos()) +- if obj == from { +- // super-block conflict +- r.errorf(from.Pos(), "renaming this %s %q to %q", +- objectKind(from), from.Name(), r.to) +- r.errorf(id.Pos(), "\twould shadow this reference") +- r.errorf(to.Pos(), "\tto the %s declared here", +- objectKind(to)) +- return false // stop +- } +- return true +- }) +- } +- } +- // Check for sub-block conflict. +- // Is there an intervening definition of r.to between +- // the block defining 'from' and some reference to it? +- forEachLexicalRef(r.pkg, from, func(id *ast.Ident, block *types.Scope) bool { +- // Find the block that defines the found reference. +- // It may be an ancestor. +- fromBlock, _ := block.LookupParent(from.Name(), id.Pos()) +- // See what r.to would resolve to in the same scope. +- toBlock, to := block.LookupParent(r.to, id.Pos()) +- if to != nil { +- // sub-block conflict +- if deeper(toBlock, fromBlock) { +- r.errorf(from.Pos(), "renaming this %s %q to %q", +- objectKind(from), from.Name(), r.to) +- r.errorf(id.Pos(), "\twould cause this reference to become shadowed") +- r.errorf(to.Pos(), "\tby this intervening %s definition", +- objectKind(to)) +- return false // stop +- } +- } +- return true +- }) - --// A selection range represents a part of a selection hierarchy. A selection range --// may have a parent selection range that contains it. --type SelectionRange struct { -- // The {@link Range range} of this selection range. -- Range Range `json:"range"` -- // The parent selection range containing this range. Therefore `parent.range` must contain `this.range`. -- Parent *SelectionRange `json:"parent,omitempty"` --} --type SelectionRangeClientCapabilities struct { -- // Whether implementation supports dynamic registration for selection range providers. If this is set to `true` -- // the client supports the new `SelectionRangeRegistrationOptions` return value for the corresponding server -- // capability as well. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` --} --type SelectionRangeOptions struct { -- WorkDoneProgressOptions +- // Renaming a type that is used as an embedded field +- // requires renaming the field too. e.g. +- // type T int // if we rename this to U.. +- // var s struct {T} +- // print(s.T) // ...this must change too +- if _, ok := from.(*types.TypeName); ok { +- for id, obj := range r.pkg.TypesInfo().Uses { +- if obj == from { +- if field := r.pkg.TypesInfo().Defs[id]; field != nil { +- r.check(field) +- } +- } +- } +- } -} - --// A parameter literal used in selection range requests. --type SelectionRangeParams struct { -- // The text document. -- TextDocument TextDocumentIdentifier `json:"textDocument"` -- // The positions inside the text document. -- Positions []Position `json:"positions"` -- WorkDoneProgressParams -- PartialResultParams --} --type SelectionRangeRegistrationOptions struct { -- SelectionRangeOptions -- TextDocumentRegistrationOptions -- StaticRegistrationOptions +-// deeper reports whether block x is lexically deeper than y. +-func deeper(x, y *types.Scope) bool { +- if x == y || x == nil { +- return false +- } else if y == nil { +- return true +- } else { +- return deeper(x.Parent(), y.Parent()) +- } -} - --// A set of predefined token modifiers. This set is not fixed --// an clients can specify additional token types via the --// corresponding client capabilities. +-// Scope and Position -// --// @since 3.16.0 --type SemanticTokenModifiers string -- --// A set of predefined token types. This set is not fixed --// an clients can specify additional token types via the --// corresponding client capabilities. +-// Consider a function f declared as: -// --// @since 3.16.0 --type SemanticTokenTypes string +-// func f[T *U, U *T](p, q T) (r, s U) { var ( v T; w = v ); type (t *t; u t) } +-// ^ ^ ^ ^ ^ ^ +-/// {T,U} {p,q,r,s} v w t u +-// +-// All objects {T, U, p, q, r, s, local} belong to the same lexical +-// block, the function scope, which is found in types.Info.Scopes +-// for f's FuncType. (A function body's BlockStmt does not have +-// an associated scope; only nested BlockStmts do.) +-// +-// The effective scope of each object is different: +-// +-// - The type parameters T and U, whose constraints may refer to each +-// other, all have a scope that starts at the beginning of the +-// FuncDecl.Type.Func token. +-// +-// - The parameter and result variables {p,q,r,s} can reference the +-// type parameters but not each other, so their scopes all start at +-// the end of the FuncType. +-// (Prior to go1.22 it was--incorrectly--unset; see #64295). +-// Beware also that Scope.Innermost does not currently work correctly for +-// type parameters: it returns the scope of the package, not the function. +-// +-// - Each const or var {v,w} declared within the function body has a +-// scope that begins at the end of its ValueSpec, or after the +-// AssignStmt for a var declared by ":=". +-// +-// - Each type {t,u} in the body has a scope that that begins at +-// the start of the TypeSpec, so they can be self-recursive +-// but--unlike package-level types--not mutually recursive. - --// @since 3.16.0 --type SemanticTokens struct { -- // An optional result id. If provided and clients support delta updating -- // the client will include the result id in the next semantic token request. -- // A server can then instead of computing all semantic tokens again simply -- // send a delta. -- ResultID string `json:"resultId,omitempty"` -- // The actual tokens. -- Data []uint32 `json:"data"` --} +-// forEachLexicalRef calls fn(id, block) for each identifier id in package +-// pkg that is a reference to obj in lexical scope. block is the +-// lexical block enclosing the reference. If fn returns false the +-// iteration is terminated and findLexicalRefs returns false. +-func forEachLexicalRef(pkg *cache.Package, obj types.Object, fn func(id *ast.Ident, block *types.Scope) bool) bool { +- ok := true +- var stack []ast.Node - --// @since 3.16.0 --type SemanticTokensClientCapabilities struct { -- // Whether implementation supports dynamic registration. If this is set to `true` -- // the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` -- // return value for the corresponding server capability as well. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -- // Which requests the client supports and might send to the server -- // depending on the server's capability. Please note that clients might not -- // show semantic tokens or degrade some of the user experience if a range -- // or full request is advertised by the client but not provided by the -- // server. If for example the client capability `requests.full` and -- // `request.range` are both set to true but the server only provides a -- // range provider the client might not render a minimap correctly or might -- // even decide to not show any semantic tokens at all. -- Requests PRequestsPSemanticTokens `json:"requests"` -- // The token types that the client supports. -- TokenTypes []string `json:"tokenTypes"` -- // The token modifiers that the client supports. -- TokenModifiers []string `json:"tokenModifiers"` -- // The token formats the clients supports. -- Formats []TokenFormat `json:"formats"` -- // Whether the client supports tokens that can overlap each other. -- OverlappingTokenSupport bool `json:"overlappingTokenSupport,omitempty"` -- // Whether the client supports tokens that can span multiple lines. -- MultilineTokenSupport bool `json:"multilineTokenSupport,omitempty"` -- // Whether the client allows the server to actively cancel a -- // semantic token request, e.g. supports returning -- // LSPErrorCodes.ServerCancelled. If a server does the client -- // needs to retrigger the request. -- // -- // @since 3.17.0 -- ServerCancelSupport bool `json:"serverCancelSupport,omitempty"` -- // Whether the client uses semantic tokens to augment existing -- // syntax tokens. If set to `true` client side created syntax -- // tokens and semantic tokens are both used for colorization. If -- // set to `false` the client only uses the returned semantic tokens -- // for colorization. -- // -- // If the value is `undefined` then the client behavior is not -- // specified. -- // -- // @since 3.17.0 -- AugmentsSyntaxTokens bool `json:"augmentsSyntaxTokens,omitempty"` --} +- var visit func(n ast.Node) bool +- visit = func(n ast.Node) bool { +- if n == nil { +- stack = stack[:len(stack)-1] // pop +- return false +- } +- if !ok { +- return false // bail out +- } - --// @since 3.16.0 --type SemanticTokensDelta struct { -- ResultID string `json:"resultId,omitempty"` -- // The semantic token edits to transform a previous result into a new result. -- Edits []SemanticTokensEdit `json:"edits"` --} +- stack = append(stack, n) // push +- switch n := n.(type) { +- case *ast.Ident: +- if pkg.TypesInfo().Uses[n] == obj { +- block := enclosingBlock(pkg.TypesInfo(), stack) +- if !fn(n, block) { +- ok = false +- } +- } +- return visit(nil) // pop stack - --// @since 3.16.0 --type SemanticTokensDeltaParams struct { -- // The text document. -- TextDocument TextDocumentIdentifier `json:"textDocument"` -- // The result id of a previous response. The result Id can either point to a full response -- // or a delta response depending on what was received last. -- PreviousResultID string `json:"previousResultId"` -- WorkDoneProgressParams -- PartialResultParams --} -- --// @since 3.16.0 --type SemanticTokensDeltaPartialResult struct { -- Edits []SemanticTokensEdit `json:"edits"` --} -- --// @since 3.16.0 --type SemanticTokensEdit struct { -- // The start offset of the edit. -- Start uint32 `json:"start"` -- // The count of elements to remove. -- DeleteCount uint32 `json:"deleteCount"` -- // The elements to insert. -- Data []uint32 `json:"data,omitempty"` --} -- --// @since 3.16.0 --type SemanticTokensLegend struct { -- // The token types a server uses. -- TokenTypes []string `json:"tokenTypes"` -- // The token modifiers a server uses. -- TokenModifiers []string `json:"tokenModifiers"` --} +- case *ast.SelectorExpr: +- // don't visit n.Sel +- ast.Inspect(n.X, visit) +- return visit(nil) // pop stack, don't descend - --// @since 3.16.0 --type SemanticTokensOptions struct { -- // The legend used by the server -- Legend SemanticTokensLegend `json:"legend"` -- // Server supports providing semantic tokens for a specific range -- // of a document. -- Range *Or_SemanticTokensOptions_range `json:"range,omitempty"` -- // Server supports providing semantic tokens for a full document. -- Full *Or_SemanticTokensOptions_full `json:"full,omitempty"` -- WorkDoneProgressOptions --} +- case *ast.CompositeLit: +- // Handle recursion ourselves for struct literals +- // so we don't visit field identifiers. +- tv, ok := pkg.TypesInfo().Types[n] +- if !ok { +- return visit(nil) // pop stack, don't descend +- } +- if is[*types.Struct](typeparams.CoreType(typeparams.Deref(tv.Type))) { +- if n.Type != nil { +- ast.Inspect(n.Type, visit) +- } +- for _, elt := range n.Elts { +- if kv, ok := elt.(*ast.KeyValueExpr); ok { +- ast.Inspect(kv.Value, visit) +- } else { +- ast.Inspect(elt, visit) +- } +- } +- return visit(nil) // pop stack, don't descend +- } +- } +- return true +- } - --// @since 3.16.0 --type SemanticTokensParams struct { -- // The text document. -- TextDocument TextDocumentIdentifier `json:"textDocument"` -- WorkDoneProgressParams -- PartialResultParams +- for _, f := range pkg.Syntax() { +- ast.Inspect(f, visit) +- if len(stack) != 0 { +- panic(stack) +- } +- if !ok { +- break +- } +- } +- return ok -} - --// @since 3.16.0 --type SemanticTokensPartialResult struct { -- Data []uint32 `json:"data"` +-// enclosingBlock returns the innermost block logically enclosing the +-// specified AST node (an ast.Ident), specified in the form of a path +-// from the root of the file, [file...n]. +-func enclosingBlock(info *types.Info, stack []ast.Node) *types.Scope { +- for i := range stack { +- n := stack[len(stack)-1-i] +- // For some reason, go/types always associates a +- // function's scope with its FuncType. +- // See comments about scope above. +- switch f := n.(type) { +- case *ast.FuncDecl: +- n = f.Type +- case *ast.FuncLit: +- n = f.Type +- } +- if b := info.Scopes[n]; b != nil { +- return b +- } +- } +- panic("no Scope for *ast.File") -} - --// @since 3.16.0 --type SemanticTokensRangeParams struct { -- // The text document. -- TextDocument TextDocumentIdentifier `json:"textDocument"` -- // The range the semantic tokens are requested for. -- Range Range `json:"range"` -- WorkDoneProgressParams -- PartialResultParams +-func (r *renamer) checkLabel(label *types.Label) { +- // Check there are no identical labels in the function's label block. +- // (Label blocks don't nest, so this is easy.) +- if prev := label.Parent().Lookup(r.to); prev != nil { +- r.errorf(label.Pos(), "renaming this label %q to %q", label.Name(), prev.Name()) +- r.errorf(prev.Pos(), "\twould conflict with this one") +- } -} - --// @since 3.16.0 --type SemanticTokensRegistrationOptions struct { -- TextDocumentRegistrationOptions -- SemanticTokensOptions -- StaticRegistrationOptions --} +-// checkStructField checks that the field renaming will not cause +-// conflicts at its declaration, or ambiguity or changes to any selection. +-func (r *renamer) checkStructField(from *types.Var) { - --// @since 3.16.0 --type SemanticTokensWorkspaceClientCapabilities struct { -- // Whether the client implementation supports a refresh request sent from -- // the server to the client. +- // If this is the declaring package, check that the struct +- // declaration is free of field conflicts, and field/method +- // conflicts. - // -- // Note that this event is global and will force the client to refresh all -- // semantic tokens currently shown. It should be used with absolute care -- // and is useful for situation where a server for example detects a project -- // wide change that requires such a calculation. -- RefreshSupport bool `json:"refreshSupport,omitempty"` --} +- // go/types offers no easy way to get from a field (or interface +- // method) to its declaring struct (or interface), so we must +- // ascend the AST. +- if pgf, ok := enclosingFile(r.pkg, from.Pos()); ok { +- path, _ := astutil.PathEnclosingInterval(pgf.File, from.Pos(), from.Pos()) +- // path matches this pattern: +- // [Ident SelectorExpr? StarExpr? Field FieldList StructType ParenExpr* ... File] - --// Defines the capabilities provided by a language --// server. --type ServerCapabilities struct { -- // The position encoding the server picked from the encodings offered -- // by the client via the client capability `general.positionEncodings`. -- // -- // If the client didn't provide any position encodings the only valid -- // value that a server can return is 'utf-16'. -- // -- // If omitted it defaults to 'utf-16'. -- // -- // @since 3.17.0 -- PositionEncoding *PositionEncodingKind `json:"positionEncoding,omitempty"` -- // Defines how text documents are synced. Is either a detailed structure -- // defining each notification or for backwards compatibility the -- // TextDocumentSyncKind number. -- TextDocumentSync interface{} `json:"textDocumentSync,omitempty"` -- // Defines how notebook documents are synced. -- // -- // @since 3.17.0 -- NotebookDocumentSync *Or_ServerCapabilities_notebookDocumentSync `json:"notebookDocumentSync,omitempty"` -- // The server provides completion support. -- CompletionProvider *CompletionOptions `json:"completionProvider,omitempty"` -- // The server provides hover support. -- HoverProvider *Or_ServerCapabilities_hoverProvider `json:"hoverProvider,omitempty"` -- // The server provides signature help support. -- SignatureHelpProvider *SignatureHelpOptions `json:"signatureHelpProvider,omitempty"` -- // The server provides Goto Declaration support. -- DeclarationProvider *Or_ServerCapabilities_declarationProvider `json:"declarationProvider,omitempty"` -- // The server provides goto definition support. -- DefinitionProvider *Or_ServerCapabilities_definitionProvider `json:"definitionProvider,omitempty"` -- // The server provides Goto Type Definition support. -- TypeDefinitionProvider *Or_ServerCapabilities_typeDefinitionProvider `json:"typeDefinitionProvider,omitempty"` -- // The server provides Goto Implementation support. -- ImplementationProvider *Or_ServerCapabilities_implementationProvider `json:"implementationProvider,omitempty"` -- // The server provides find references support. -- ReferencesProvider *Or_ServerCapabilities_referencesProvider `json:"referencesProvider,omitempty"` -- // The server provides document highlight support. -- DocumentHighlightProvider *Or_ServerCapabilities_documentHighlightProvider `json:"documentHighlightProvider,omitempty"` -- // The server provides document symbol support. -- DocumentSymbolProvider *Or_ServerCapabilities_documentSymbolProvider `json:"documentSymbolProvider,omitempty"` -- // The server provides code actions. CodeActionOptions may only be -- // specified if the client states that it supports -- // `codeActionLiteralSupport` in its initial `initialize` request. -- CodeActionProvider interface{} `json:"codeActionProvider,omitempty"` -- // The server provides code lens. -- CodeLensProvider *CodeLensOptions `json:"codeLensProvider,omitempty"` -- // The server provides document link support. -- DocumentLinkProvider *DocumentLinkOptions `json:"documentLinkProvider,omitempty"` -- // The server provides color provider support. -- ColorProvider *Or_ServerCapabilities_colorProvider `json:"colorProvider,omitempty"` -- // The server provides workspace symbol support. -- WorkspaceSymbolProvider *Or_ServerCapabilities_workspaceSymbolProvider `json:"workspaceSymbolProvider,omitempty"` -- // The server provides document formatting. -- DocumentFormattingProvider *Or_ServerCapabilities_documentFormattingProvider `json:"documentFormattingProvider,omitempty"` -- // The server provides document range formatting. -- DocumentRangeFormattingProvider *Or_ServerCapabilities_documentRangeFormattingProvider `json:"documentRangeFormattingProvider,omitempty"` -- // The server provides document formatting on typing. -- DocumentOnTypeFormattingProvider *DocumentOnTypeFormattingOptions `json:"documentOnTypeFormattingProvider,omitempty"` -- // The server provides rename support. RenameOptions may only be -- // specified if the client states that it supports -- // `prepareSupport` in its initial `initialize` request. -- RenameProvider interface{} `json:"renameProvider,omitempty"` -- // The server provides folding provider support. -- FoldingRangeProvider *Or_ServerCapabilities_foldingRangeProvider `json:"foldingRangeProvider,omitempty"` -- // The server provides selection range support. -- SelectionRangeProvider *Or_ServerCapabilities_selectionRangeProvider `json:"selectionRangeProvider,omitempty"` -- // The server provides execute command support. -- ExecuteCommandProvider *ExecuteCommandOptions `json:"executeCommandProvider,omitempty"` -- // The server provides call hierarchy support. -- // -- // @since 3.16.0 -- CallHierarchyProvider *Or_ServerCapabilities_callHierarchyProvider `json:"callHierarchyProvider,omitempty"` -- // The server provides linked editing range support. -- // -- // @since 3.16.0 -- LinkedEditingRangeProvider *Or_ServerCapabilities_linkedEditingRangeProvider `json:"linkedEditingRangeProvider,omitempty"` -- // The server provides semantic tokens support. -- // -- // @since 3.16.0 -- SemanticTokensProvider interface{} `json:"semanticTokensProvider,omitempty"` -- // The server provides moniker support. -- // -- // @since 3.16.0 -- MonikerProvider *Or_ServerCapabilities_monikerProvider `json:"monikerProvider,omitempty"` -- // The server provides type hierarchy support. -- // -- // @since 3.17.0 -- TypeHierarchyProvider *Or_ServerCapabilities_typeHierarchyProvider `json:"typeHierarchyProvider,omitempty"` -- // The server provides inline values. -- // -- // @since 3.17.0 -- InlineValueProvider *Or_ServerCapabilities_inlineValueProvider `json:"inlineValueProvider,omitempty"` -- // The server provides inlay hints. -- // -- // @since 3.17.0 -- InlayHintProvider interface{} `json:"inlayHintProvider,omitempty"` -- // The server has support for pull model diagnostics. -- // -- // @since 3.17.0 -- DiagnosticProvider *Or_ServerCapabilities_diagnosticProvider `json:"diagnosticProvider,omitempty"` -- // Inline completion options used during static registration. -- // -- // @since 3.18.0 -- // @proposed -- InlineCompletionProvider *Or_ServerCapabilities_inlineCompletionProvider `json:"inlineCompletionProvider,omitempty"` -- // Workspace specific server capabilities. -- Workspace *Workspace6Gn `json:"workspace,omitempty"` -- // Experimental server capabilities. -- Experimental interface{} `json:"experimental,omitempty"` --} --type SetTraceParams struct { -- Value TraceValues `json:"value"` --} +- // Ascend to FieldList. +- var i int +- for { +- if _, ok := path[i].(*ast.FieldList); ok { +- break +- } +- i++ +- } +- i++ +- tStruct := path[i].(*ast.StructType) +- i++ +- // Ascend past parens (unlikely). +- for { +- _, ok := path[i].(*ast.ParenExpr) +- if !ok { +- break +- } +- i++ +- } +- if spec, ok := path[i].(*ast.TypeSpec); ok { +- // This struct is also a named type. +- // We must check for direct (non-promoted) field/field +- // and method/field conflicts. +- named := r.pkg.TypesInfo().Defs[spec.Name].Type() +- prev, indices, _ := types.LookupFieldOrMethod(named, true, r.pkg.Types(), r.to) +- if len(indices) == 1 { +- r.errorf(from.Pos(), "renaming this field %q to %q", +- from.Name(), r.to) +- r.errorf(prev.Pos(), "\twould conflict with this %s", +- objectKind(prev)) +- return // skip checkSelections to avoid redundant errors +- } +- } else { +- // This struct is not a named type. +- // We need only check for direct (non-promoted) field/field conflicts. +- T := r.pkg.TypesInfo().Types[tStruct].Type.Underlying().(*types.Struct) +- for i := 0; i < T.NumFields(); i++ { +- if prev := T.Field(i); prev.Name() == r.to { +- r.errorf(from.Pos(), "renaming this field %q to %q", +- from.Name(), r.to) +- r.errorf(prev.Pos(), "\twould conflict with this field") +- return // skip checkSelections to avoid redundant errors +- } +- } +- } +- } - --// Client capabilities for the showDocument request. --// --// @since 3.16.0 --type ShowDocumentClientCapabilities struct { -- // The client has support for the showDocument -- // request. -- Support bool `json:"support"` --} +- // Renaming an anonymous field requires renaming the type too. e.g. +- // print(s.T) // if we rename T to U, +- // type T int // this and +- // var s struct {T} // this must change too. +- if from.Anonymous() { +- if named, ok := from.Type().(*types.Named); ok { +- r.check(named.Obj()) +- } else if named, ok := aliases.Unalias(typesinternal.Unpointer(from.Type())).(*types.Named); ok { +- r.check(named.Obj()) +- } +- } - --// Params to show a resource in the UI. --// --// @since 3.16.0 --type ShowDocumentParams struct { -- // The uri to show. -- URI URI `json:"uri"` -- // Indicates to show the resource in an external program. -- // To show, for example, `https://code.visualstudio.com/` -- // in the default WEB browser set `external` to `true`. -- External bool `json:"external,omitempty"` -- // An optional property to indicate whether the editor -- // showing the document should take focus or not. -- // Clients might ignore this property if an external -- // program is started. -- TakeFocus bool `json:"takeFocus,omitempty"` -- // An optional selection range if the document is a text -- // document. Clients might ignore the property if an -- // external program is started or the file is not a text -- // file. -- Selection *Range `json:"selection,omitempty"` +- // Check integrity of existing (field and method) selections. +- r.checkSelections(from) -} - --// The result of a showDocument request. --// --// @since 3.16.0 --type ShowDocumentResult struct { -- // A boolean indicating if the show was successful. -- Success bool `json:"success"` --} +-// checkSelections checks that all uses and selections that resolve to +-// the specified object would continue to do so after the renaming. +-func (r *renamer) checkSelections(from types.Object) { +- pkg := r.pkg +- typ := pkg.Types() +- { +- if id := someUse(pkg.TypesInfo(), from); id != nil { +- if !r.checkExport(id, typ, from) { +- return +- } +- } - --// The parameters of a notification message. --type ShowMessageParams struct { -- // The message type. See {@link MessageType} -- Type MessageType `json:"type"` -- // The actual message. -- Message string `json:"message"` --} +- for syntax, sel := range pkg.TypesInfo().Selections { +- // There may be extant selections of only the old +- // name or only the new name, so we must check both. +- // (If neither, the renaming is sound.) +- // +- // In both cases, we wish to compare the lengths +- // of the implicit field path (Selection.Index) +- // to see if the renaming would change it. +- // +- // If a selection that resolves to 'from', when renamed, +- // would yield a path of the same or shorter length, +- // this indicates ambiguity or a changed referent, +- // analogous to same- or sub-block lexical conflict. +- // +- // If a selection using the name 'to' would +- // yield a path of the same or shorter length, +- // this indicates ambiguity or shadowing, +- // analogous to same- or super-block lexical conflict. - --// Show message request client capabilities --type ShowMessageRequestClientCapabilities struct { -- // Capabilities specific to the `MessageActionItem` type. -- MessageActionItem *PMessageActionItemPShowMessage `json:"messageActionItem,omitempty"` --} --type ShowMessageRequestParams struct { -- // The message type. See {@link MessageType} -- Type MessageType `json:"type"` -- // The actual message. -- Message string `json:"message"` -- // The message action items to present. -- Actions []MessageActionItem `json:"actions,omitempty"` --} +- // TODO(adonovan): fix: derive from Types[syntax.X].Mode +- // TODO(adonovan): test with pointer, value, addressable value. +- isAddressable := true - --// Signature help represents the signature of something --// callable. There can be multiple signature but only one --// active and only one active parameter. --type SignatureHelp struct { -- // One or more signatures. -- Signatures []SignatureInformation `json:"signatures"` -- // The active signature. If omitted or the value lies outside the -- // range of `signatures` the value defaults to zero or is ignored if -- // the `SignatureHelp` has no signatures. -- // -- // Whenever possible implementors should make an active decision about -- // the active signature and shouldn't rely on a default value. -- // -- // In future version of the protocol this property might become -- // mandatory to better express this. -- ActiveSignature uint32 `json:"activeSignature,omitempty"` -- // The active parameter of the active signature. If omitted or the value -- // lies outside the range of `signatures[activeSignature].parameters` -- // defaults to 0 if the active signature has parameters. If -- // the active signature has no parameters it is ignored. -- // In future version of the protocol this property might become -- // mandatory to better express the active parameter if the -- // active signature does have any. -- ActiveParameter uint32 `json:"activeParameter,omitempty"` +- if sel.Obj() == from { +- if obj, indices, _ := types.LookupFieldOrMethod(sel.Recv(), isAddressable, from.Pkg(), r.to); obj != nil { +- // Renaming this existing selection of +- // 'from' may block access to an existing +- // type member named 'to'. +- delta := len(indices) - len(sel.Index()) +- if delta > 0 { +- continue // no ambiguity +- } +- r.selectionConflict(from, delta, syntax, obj) +- return +- } +- } else if sel.Obj().Name() == r.to { +- if obj, indices, _ := types.LookupFieldOrMethod(sel.Recv(), isAddressable, from.Pkg(), from.Name()); obj == from { +- // Renaming 'from' may cause this existing +- // selection of the name 'to' to change +- // its meaning. +- delta := len(indices) - len(sel.Index()) +- if delta > 0 { +- continue // no ambiguity +- } +- r.selectionConflict(from, -delta, syntax, sel.Obj()) +- return +- } +- } +- } +- } -} - --// Client Capabilities for a {@link SignatureHelpRequest}. --type SignatureHelpClientCapabilities struct { -- // Whether signature help supports dynamic registration. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -- // The client supports the following `SignatureInformation` -- // specific properties. -- SignatureInformation *PSignatureInformationPSignatureHelp `json:"signatureInformation,omitempty"` -- // The client supports to send additional context information for a -- // `textDocument/signatureHelp` request. A client that opts into -- // contextSupport will also support the `retriggerCharacters` on -- // `SignatureHelpOptions`. -- // -- // @since 3.15.0 -- ContextSupport bool `json:"contextSupport,omitempty"` +-func (r *renamer) selectionConflict(from types.Object, delta int, syntax *ast.SelectorExpr, obj types.Object) { +- r.errorf(from.Pos(), "renaming this %s %q to %q", +- objectKind(from), from.Name(), r.to) +- +- switch { +- case delta < 0: +- // analogous to sub-block conflict +- r.errorf(syntax.Sel.Pos(), +- "\twould change the referent of this selection") +- r.errorf(obj.Pos(), "\tof this %s", objectKind(obj)) +- case delta == 0: +- // analogous to same-block conflict +- r.errorf(syntax.Sel.Pos(), +- "\twould make this reference ambiguous") +- r.errorf(obj.Pos(), "\twith this %s", objectKind(obj)) +- case delta > 0: +- // analogous to super-block conflict +- r.errorf(syntax.Sel.Pos(), +- "\twould shadow this selection") +- r.errorf(obj.Pos(), "\tof the %s declared here", +- objectKind(obj)) +- } -} - --// Additional information about the context in which a signature help request was triggered. +-// checkMethod performs safety checks for renaming a method. +-// There are three hazards: +-// - declaration conflicts +-// - selection ambiguity/changes +-// - entailed renamings of assignable concrete/interface types. -// --// @since 3.15.0 --type SignatureHelpContext struct { -- // Action that caused signature help to be triggered. -- TriggerKind SignatureHelpTriggerKind `json:"triggerKind"` -- // Character that caused signature help to be triggered. -- // -- // This is undefined when `triggerKind !== SignatureHelpTriggerKind.TriggerCharacter` -- TriggerCharacter string `json:"triggerCharacter,omitempty"` -- // `true` if signature help was already showing when it was triggered. -- // -- // Retriggers occurs when the signature help is already active and can be caused by actions such as -- // typing a trigger character, a cursor move, or document content changes. -- IsRetrigger bool `json:"isRetrigger"` -- // The currently active `SignatureHelp`. -- // -- // The `activeSignatureHelp` has its `SignatureHelp.activeSignature` field updated based on -- // the user navigating through available signatures. -- ActiveSignatureHelp *SignatureHelp `json:"activeSignatureHelp,omitempty"` --} +-// We reject renamings initiated at concrete methods if it would +-// change the assignability relation. For renamings of abstract +-// methods, we rename all methods transitively coupled to it via +-// assignability. +-func (r *renamer) checkMethod(from *types.Func) { +- // e.g. error.Error +- if from.Pkg() == nil { +- r.errorf(from.Pos(), "you cannot rename built-in method %s", from) +- return +- } - --// Server Capabilities for a {@link SignatureHelpRequest}. --type SignatureHelpOptions struct { -- // List of characters that trigger signature help automatically. -- TriggerCharacters []string `json:"triggerCharacters,omitempty"` -- // List of characters that re-trigger signature help. -- // -- // These trigger characters are only active when signature help is already showing. All trigger characters -- // are also counted as re-trigger characters. -- // -- // @since 3.15.0 -- RetriggerCharacters []string `json:"retriggerCharacters,omitempty"` -- WorkDoneProgressOptions --} +- // ASSIGNABILITY: We reject renamings of concrete methods that +- // would break a 'satisfy' constraint; but renamings of abstract +- // methods are allowed to proceed, and we rename affected +- // concrete and abstract methods as necessary. It is the +- // initial method that determines the policy. - --// Parameters for a {@link SignatureHelpRequest}. --type SignatureHelpParams struct { -- // The signature help context. This is only available if the client specifies -- // to send this using the client capability `textDocument.signatureHelp.contextSupport === true` -- // -- // @since 3.15.0 -- Context *SignatureHelpContext `json:"context,omitempty"` -- TextDocumentPositionParams -- WorkDoneProgressParams --} +- // Check for conflict at point of declaration. +- // Check to ensure preservation of assignability requirements. +- R := recv(from).Type() +- if types.IsInterface(R) { +- // Abstract method - --// Registration options for a {@link SignatureHelpRequest}. --type SignatureHelpRegistrationOptions struct { -- TextDocumentRegistrationOptions -- SignatureHelpOptions --} +- // declaration +- prev, _, _ := types.LookupFieldOrMethod(R, false, from.Pkg(), r.to) +- if prev != nil { +- r.errorf(from.Pos(), "renaming this interface method %q to %q", +- from.Name(), r.to) +- r.errorf(prev.Pos(), "\twould conflict with this method") +- return +- } - --// How a signature help was triggered. --// --// @since 3.15.0 --type SignatureHelpTriggerKind uint32 +- // Check all interfaces that embed this one for +- // declaration conflicts too. +- { +- // Start with named interface types (better errors) +- for _, obj := range r.pkg.TypesInfo().Defs { +- if obj, ok := obj.(*types.TypeName); ok && types.IsInterface(obj.Type()) { +- f, _, _ := types.LookupFieldOrMethod( +- obj.Type(), false, from.Pkg(), from.Name()) +- if f == nil { +- continue +- } +- t, _, _ := types.LookupFieldOrMethod( +- obj.Type(), false, from.Pkg(), r.to) +- if t == nil { +- continue +- } +- r.errorf(from.Pos(), "renaming this interface method %q to %q", +- from.Name(), r.to) +- r.errorf(t.Pos(), "\twould conflict with this method") +- r.errorf(obj.Pos(), "\tin named interface type %q", obj.Name()) +- } +- } - --// Represents the signature of something callable. A signature --// can have a label, like a function-name, a doc-comment, and --// a set of parameters. --type SignatureInformation struct { -- // The label of this signature. Will be shown in -- // the UI. -- Label string `json:"label"` -- // The human-readable doc-comment of this signature. Will be shown -- // in the UI but can be omitted. -- Documentation *Or_SignatureInformation_documentation `json:"documentation,omitempty"` -- // The parameters of this signature. -- Parameters []ParameterInformation `json:"parameters,omitempty"` -- // The index of the active parameter. -- // -- // If provided, this is used in place of `SignatureHelp.activeParameter`. -- // -- // @since 3.16.0 -- ActiveParameter uint32 `json:"activeParameter,omitempty"` --} +- // Now look at all literal interface types (includes named ones again). +- for e, tv := range r.pkg.TypesInfo().Types { +- if e, ok := e.(*ast.InterfaceType); ok { +- _ = e +- _ = tv.Type.(*types.Interface) +- // TODO(adonovan): implement same check as above. +- } +- } +- } - --// Static registration options to be returned in the initialize --// request. --type StaticRegistrationOptions struct { -- // The id used to register the request. The id can be used to deregister -- // the request again. See also Registration#id. -- ID string `json:"id,omitempty"` --} +- // assignability +- // +- // Find the set of concrete or abstract methods directly +- // coupled to abstract method 'from' by some +- // satisfy.Constraint, and rename them too. +- for key := range r.satisfy() { +- // key = (lhs, rhs) where lhs is always an interface. - --// A string value used as a snippet is a template which allows to insert text --// and to control the editor cursor when insertion happens. --// --// A snippet can define tab stops and placeholders with `$1`, `$2` --// and `${3:foo}`. `$0` defines the final tab stop, it defaults to --// the end of the snippet. Variables are defined with `$name` and --// `${name:default value}`. --// --// @since 3.18.0 --// @proposed --type StringValue struct { -- // The kind of string value. -- Kind string `json:"kind"` -- // The snippet string. -- Value string `json:"value"` --} +- lsel := r.msets.MethodSet(key.LHS).Lookup(from.Pkg(), from.Name()) +- if lsel == nil { +- continue +- } +- rmethods := r.msets.MethodSet(key.RHS) +- rsel := rmethods.Lookup(from.Pkg(), from.Name()) +- if rsel == nil { +- continue +- } - --// Represents information about programming constructs like variables, classes, --// interfaces etc. --type SymbolInformation struct { -- // extends BaseSymbolInformation -- // Indicates if this symbol is deprecated. -- // -- // @deprecated Use tags instead -- Deprecated bool `json:"deprecated,omitempty"` -- // The location of this symbol. The location's range is used by a tool -- // to reveal the location in the editor. If the symbol is selected in the -- // tool the range's start information is used to position the cursor. So -- // the range usually spans more than the actual symbol's name and does -- // normally include things like visibility modifiers. -- // -- // The range doesn't have to denote a node range in the sense of an abstract -- // syntax tree. It can therefore not be used to re-construct a hierarchy of -- // the symbols. -- Location Location `json:"location"` -- // The name of this symbol. -- Name string `json:"name"` -- // The kind of this symbol. -- Kind SymbolKind `json:"kind"` -- // Tags for this symbol. -- // -- // @since 3.16.0 -- Tags []SymbolTag `json:"tags,omitempty"` -- // The name of the symbol containing this symbol. This information is for -- // user interface purposes (e.g. to render a qualifier in the user interface -- // if necessary). It can't be used to re-infer a hierarchy for the document -- // symbols. -- ContainerName string `json:"containerName,omitempty"` --} +- // If both sides have a method of this name, +- // and one of them is m, the other must be coupled. +- var coupled *types.Func +- switch from { +- case lsel.Obj(): +- coupled = rsel.Obj().(*types.Func) +- case rsel.Obj(): +- coupled = lsel.Obj().(*types.Func) +- default: +- continue +- } - --// A symbol kind. --type SymbolKind uint32 +- // We must treat concrete-to-interface +- // constraints like an implicit selection C.f of +- // each interface method I.f, and check that the +- // renaming leaves the selection unchanged and +- // unambiguous. +- // +- // Fun fact: the implicit selection of C.f +- // type I interface{f()} +- // type C struct{I} +- // func (C) g() +- // var _ I = C{} // here +- // yields abstract method I.f. This can make error +- // messages less than obvious. +- // +- if !types.IsInterface(key.RHS) { +- // The logic below was derived from checkSelections. - --// Symbol tags are extra annotations that tweak the rendering of a symbol. --// --// @since 3.16 --type SymbolTag uint32 +- rtosel := rmethods.Lookup(from.Pkg(), r.to) +- if rtosel != nil { +- rto := rtosel.Obj().(*types.Func) +- delta := len(rsel.Index()) - len(rtosel.Index()) +- if delta < 0 { +- continue // no ambiguity +- } - --// Describe options to be used when registered for text document change events. --type TextDocumentChangeRegistrationOptions struct { -- // How documents are synced to the server. -- SyncKind TextDocumentSyncKind `json:"syncKind"` -- TextDocumentRegistrationOptions --} +- // TODO(adonovan): record the constraint's position. +- keyPos := token.NoPos - --// Text document specific client capabilities. --type TextDocumentClientCapabilities struct { -- // Defines which synchronization capabilities the client supports. -- Synchronization *TextDocumentSyncClientCapabilities `json:"synchronization,omitempty"` -- // Capabilities specific to the `textDocument/completion` request. -- Completion CompletionClientCapabilities `json:"completion,omitempty"` -- // Capabilities specific to the `textDocument/hover` request. -- Hover *HoverClientCapabilities `json:"hover,omitempty"` -- // Capabilities specific to the `textDocument/signatureHelp` request. -- SignatureHelp *SignatureHelpClientCapabilities `json:"signatureHelp,omitempty"` -- // Capabilities specific to the `textDocument/declaration` request. -- // -- // @since 3.14.0 -- Declaration *DeclarationClientCapabilities `json:"declaration,omitempty"` -- // Capabilities specific to the `textDocument/definition` request. -- Definition *DefinitionClientCapabilities `json:"definition,omitempty"` -- // Capabilities specific to the `textDocument/typeDefinition` request. -- // -- // @since 3.6.0 -- TypeDefinition *TypeDefinitionClientCapabilities `json:"typeDefinition,omitempty"` -- // Capabilities specific to the `textDocument/implementation` request. -- // -- // @since 3.6.0 -- Implementation *ImplementationClientCapabilities `json:"implementation,omitempty"` -- // Capabilities specific to the `textDocument/references` request. -- References *ReferenceClientCapabilities `json:"references,omitempty"` -- // Capabilities specific to the `textDocument/documentHighlight` request. -- DocumentHighlight *DocumentHighlightClientCapabilities `json:"documentHighlight,omitempty"` -- // Capabilities specific to the `textDocument/documentSymbol` request. -- DocumentSymbol DocumentSymbolClientCapabilities `json:"documentSymbol,omitempty"` -- // Capabilities specific to the `textDocument/codeAction` request. -- CodeAction CodeActionClientCapabilities `json:"codeAction,omitempty"` -- // Capabilities specific to the `textDocument/codeLens` request. -- CodeLens *CodeLensClientCapabilities `json:"codeLens,omitempty"` -- // Capabilities specific to the `textDocument/documentLink` request. -- DocumentLink *DocumentLinkClientCapabilities `json:"documentLink,omitempty"` -- // Capabilities specific to the `textDocument/documentColor` and the -- // `textDocument/colorPresentation` request. -- // -- // @since 3.6.0 -- ColorProvider *DocumentColorClientCapabilities `json:"colorProvider,omitempty"` -- // Capabilities specific to the `textDocument/formatting` request. -- Formatting *DocumentFormattingClientCapabilities `json:"formatting,omitempty"` -- // Capabilities specific to the `textDocument/rangeFormatting` request. -- RangeFormatting *DocumentRangeFormattingClientCapabilities `json:"rangeFormatting,omitempty"` -- // Capabilities specific to the `textDocument/onTypeFormatting` request. -- OnTypeFormatting *DocumentOnTypeFormattingClientCapabilities `json:"onTypeFormatting,omitempty"` -- // Capabilities specific to the `textDocument/rename` request. -- Rename *RenameClientCapabilities `json:"rename,omitempty"` -- // Capabilities specific to the `textDocument/foldingRange` request. -- // -- // @since 3.10.0 -- FoldingRange *FoldingRangeClientCapabilities `json:"foldingRange,omitempty"` -- // Capabilities specific to the `textDocument/selectionRange` request. -- // -- // @since 3.15.0 -- SelectionRange *SelectionRangeClientCapabilities `json:"selectionRange,omitempty"` -- // Capabilities specific to the `textDocument/publishDiagnostics` notification. -- PublishDiagnostics PublishDiagnosticsClientCapabilities `json:"publishDiagnostics,omitempty"` -- // Capabilities specific to the various call hierarchy requests. -- // -- // @since 3.16.0 -- CallHierarchy *CallHierarchyClientCapabilities `json:"callHierarchy,omitempty"` -- // Capabilities specific to the various semantic token request. -- // -- // @since 3.16.0 -- SemanticTokens SemanticTokensClientCapabilities `json:"semanticTokens,omitempty"` -- // Capabilities specific to the `textDocument/linkedEditingRange` request. -- // -- // @since 3.16.0 -- LinkedEditingRange *LinkedEditingRangeClientCapabilities `json:"linkedEditingRange,omitempty"` -- // Client capabilities specific to the `textDocument/moniker` request. -- // -- // @since 3.16.0 -- Moniker *MonikerClientCapabilities `json:"moniker,omitempty"` -- // Capabilities specific to the various type hierarchy requests. -- // -- // @since 3.17.0 -- TypeHierarchy *TypeHierarchyClientCapabilities `json:"typeHierarchy,omitempty"` -- // Capabilities specific to the `textDocument/inlineValue` request. -- // -- // @since 3.17.0 -- InlineValue *InlineValueClientCapabilities `json:"inlineValue,omitempty"` -- // Capabilities specific to the `textDocument/inlayHint` request. -- // -- // @since 3.17.0 -- InlayHint *InlayHintClientCapabilities `json:"inlayHint,omitempty"` -- // Capabilities specific to the diagnostic pull model. -- // -- // @since 3.17.0 -- Diagnostic *DiagnosticClientCapabilities `json:"diagnostic,omitempty"` -- // Client capabilities specific to inline completions. -- // -- // @since 3.18.0 -- // @proposed -- InlineCompletion *InlineCompletionClientCapabilities `json:"inlineCompletion,omitempty"` --} +- r.errorf(from.Pos(), "renaming this method %q to %q", +- from.Name(), r.to) +- if delta == 0 { +- // analogous to same-block conflict +- r.errorf(keyPos, "\twould make the %s method of %s invoked via interface %s ambiguous", +- r.to, key.RHS, key.LHS) +- r.errorf(rto.Pos(), "\twith (%s).%s", +- recv(rto).Type(), r.to) +- } else { +- // analogous to super-block conflict +- r.errorf(keyPos, "\twould change the %s method of %s invoked via interface %s", +- r.to, key.RHS, key.LHS) +- r.errorf(coupled.Pos(), "\tfrom (%s).%s", +- recv(coupled).Type(), r.to) +- r.errorf(rto.Pos(), "\tto (%s).%s", +- recv(rto).Type(), r.to) +- } +- return // one error is enough +- } +- } - --// An event describing a change to a text document. If only a text is provided --// it is considered to be the full content of the document. --type TextDocumentContentChangeEvent = Msg_TextDocumentContentChangeEvent // (alias) line 14417 --// Describes textual changes on a text document. A TextDocumentEdit describes all changes --// on a document version Si and after they are applied move the document to version Si+1. --// So the creator of a TextDocumentEdit doesn't need to sort the array of edits or do any --// kind of ordering. However the edits must be non overlapping. --type TextDocumentEdit struct { -- // The text document to change. -- TextDocument OptionalVersionedTextDocumentIdentifier `json:"textDocument"` -- // The edits to be applied. -- // -- // @since 3.16.0 - support for AnnotatedTextEdit. This is guarded using a -- // client capability. -- Edits []TextEdit `json:"edits"` --} +- if !r.changeMethods { +- // This should be unreachable. +- r.errorf(from.Pos(), "internal error: during renaming of abstract method %s", from) +- r.errorf(coupled.Pos(), "\tchangedMethods=false, coupled method=%s", coupled) +- r.errorf(from.Pos(), "\tPlease file a bug report") +- return +- } - --// A document filter denotes a document by different properties like --// the {@link TextDocument.languageId language}, the {@link Uri.scheme scheme} of --// its resource, or a glob-pattern that is applied to the {@link TextDocument.fileName path}. --// --// Glob patterns can have the following syntax: --// --// - `*` to match one or more characters in a path segment --// - `?` to match on one character in a path segment --// - `**` to match any number of path segments, including none --// - `{}` to group sub patterns into an OR expression. (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) --// - `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) --// - `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) --// --// @sample A language filter that applies to typescript files on disk: `{ language: 'typescript', scheme: 'file' }` --// @sample A language filter that applies to all package.json paths: `{ language: 'json', pattern: '**package.json' }` --// --// @since 3.17.0 --type TextDocumentFilter = Msg_TextDocumentFilter // (alias) line 14560 --// A literal to identify a text document in the client. --type TextDocumentIdentifier struct { -- // The text document's uri. -- URI DocumentURI `json:"uri"` --} +- // Rename the coupled method to preserve assignability. +- r.check(coupled) +- } +- } else { +- // Concrete method - --// An item to transfer a text document from the client to the --// server. --type TextDocumentItem struct { -- // The text document's uri. -- URI DocumentURI `json:"uri"` -- // The text document's language identifier. -- LanguageID string `json:"languageId"` -- // The version number of this document (it will increase after each -- // change, including undo/redo). -- Version int32 `json:"version"` -- // The content of the opened text document. -- Text string `json:"text"` --} +- // declaration +- prev, indices, _ := types.LookupFieldOrMethod(R, true, from.Pkg(), r.to) +- if prev != nil && len(indices) == 1 { +- r.errorf(from.Pos(), "renaming this method %q to %q", +- from.Name(), r.to) +- r.errorf(prev.Pos(), "\twould conflict with this %s", +- objectKind(prev)) +- return +- } - --// A parameter literal used in requests to pass a text document and a position inside that --// document. --type TextDocumentPositionParams struct { -- // The text document. -- TextDocument TextDocumentIdentifier `json:"textDocument"` -- // The position inside the text document. -- Position Position `json:"position"` --} +- // assignability +- // +- // Find the set of abstract methods coupled to concrete +- // method 'from' by some satisfy.Constraint, and rename +- // them too. +- // +- // Coupling may be indirect, e.g. I.f <-> C.f via type D. +- // +- // type I interface {f()} +- // type C int +- // type (C) f() +- // type D struct{C} +- // var _ I = D{} +- // +- for key := range r.satisfy() { +- // key = (lhs, rhs) where lhs is always an interface. +- if types.IsInterface(key.RHS) { +- continue +- } +- rsel := r.msets.MethodSet(key.RHS).Lookup(from.Pkg(), from.Name()) +- if rsel == nil || rsel.Obj() != from { +- continue // rhs does not have the method +- } +- lsel := r.msets.MethodSet(key.LHS).Lookup(from.Pkg(), from.Name()) +- if lsel == nil { +- continue +- } +- imeth := lsel.Obj().(*types.Func) - --// General text document registration options. --type TextDocumentRegistrationOptions struct { -- // A document selector to identify the scope of the registration. If set to null -- // the document selector provided on the client side will be used. -- DocumentSelector DocumentSelector `json:"documentSelector"` --} +- // imeth is the abstract method (e.g. I.f) +- // and key.RHS is the concrete coupling type (e.g. D). +- if !r.changeMethods { +- r.errorf(from.Pos(), "renaming this method %q to %q", +- from.Name(), r.to) +- var pos token.Pos +- var iface string - --// Represents reasons why a text document is saved. --type TextDocumentSaveReason uint32 +- I := recv(imeth).Type() +- if named, ok := aliases.Unalias(I).(*types.Named); ok { +- pos = named.Obj().Pos() +- iface = "interface " + named.Obj().Name() +- } else { +- pos = from.Pos() +- iface = I.String() +- } +- r.errorf(pos, "\twould make %s no longer assignable to %s", +- key.RHS, iface) +- r.errorf(imeth.Pos(), "\t(rename %s.%s if you intend to change both types)", +- I, from.Name()) +- return // one error is enough +- } - --// Save registration options. --type TextDocumentSaveRegistrationOptions struct { -- TextDocumentRegistrationOptions -- SaveOptions --} --type TextDocumentSyncClientCapabilities struct { -- // Whether text document synchronization supports dynamic registration. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -- // The client supports sending will save notifications. -- WillSave bool `json:"willSave,omitempty"` -- // The client supports sending a will save request and -- // waits for a response providing text edits which will -- // be applied to the document before it is saved. -- WillSaveWaitUntil bool `json:"willSaveWaitUntil,omitempty"` -- // The client supports did save notifications. -- DidSave bool `json:"didSave,omitempty"` --} +- // Rename the coupled interface method to preserve assignability. +- r.check(imeth) +- } +- } - --// Defines how the host (editor) should sync --// document changes to the language server. --type TextDocumentSyncKind uint32 --type TextDocumentSyncOptions struct { -- // Open and close notifications are sent to the server. If omitted open close notification should not -- // be sent. -- OpenClose bool `json:"openClose,omitempty"` -- // Change notifications are sent to the server. See TextDocumentSyncKind.None, TextDocumentSyncKind.Full -- // and TextDocumentSyncKind.Incremental. If omitted it defaults to TextDocumentSyncKind.None. -- Change TextDocumentSyncKind `json:"change,omitempty"` -- // If present will save notifications are sent to the server. If omitted the notification should not be -- // sent. -- WillSave bool `json:"willSave,omitempty"` -- // If present will save wait until requests are sent to the server. If omitted the request should not be -- // sent. -- WillSaveWaitUntil bool `json:"willSaveWaitUntil,omitempty"` -- // If present save notifications are sent to the server. If omitted the notification should not be -- // sent. -- Save *SaveOptions `json:"save,omitempty"` +- // Check integrity of existing (field and method) selections. +- // We skip this if there were errors above, to avoid redundant errors. +- r.checkSelections(from) -} - --// A text edit applicable to a text document. --type TextEdit struct { -- // The range of the text document to be manipulated. To insert -- // text into a document create a range where start === end. -- Range Range `json:"range"` -- // The string to be inserted. For delete operations use an -- // empty string. -- NewText string `json:"newText"` +-func (r *renamer) checkExport(id *ast.Ident, pkg *types.Package, from types.Object) bool { +- // Reject cross-package references if r.to is unexported. +- // (Such references may be qualified identifiers or field/method +- // selections.) +- if !ast.IsExported(r.to) && pkg != from.Pkg() { +- r.errorf(from.Pos(), +- "renaming %q to %q would make it unexported", +- from.Name(), r.to) +- r.errorf(id.Pos(), "\tbreaking references from packages such as %q", +- pkg.Path()) +- return false +- } +- return true -} --type TokenFormat string --type TraceValues string - --// Since 3.6.0 --type TypeDefinitionClientCapabilities struct { -- // Whether implementation supports dynamic registration. If this is set to `true` -- // the client supports the new `TypeDefinitionRegistrationOptions` return value -- // for the corresponding server capability as well. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -- // The client supports additional metadata in the form of definition links. -- // -- // Since 3.14.0 -- LinkSupport bool `json:"linkSupport,omitempty"` --} --type TypeDefinitionOptions struct { -- WorkDoneProgressOptions --} --type TypeDefinitionParams struct { -- TextDocumentPositionParams -- WorkDoneProgressParams -- PartialResultParams --} --type TypeDefinitionRegistrationOptions struct { -- TextDocumentRegistrationOptions -- TypeDefinitionOptions -- StaticRegistrationOptions +-// satisfy returns the set of interface satisfaction constraints. +-func (r *renamer) satisfy() map[satisfy.Constraint]bool { +- if r.satisfyConstraints == nil { +- // Compute on demand: it's expensive. +- var f satisfy.Finder +- pkg := r.pkg +- { +- // From satisfy.Finder documentation: +- // +- // The package must be free of type errors, and +- // info.{Defs,Uses,Selections,Types} must have been populated by the +- // type-checker. +- // +- // Only proceed if all packages have no errors. +- if len(pkg.ParseErrors()) > 0 || len(pkg.TypeErrors()) > 0 { +- r.errorf(token.NoPos, // we don't have a position for this error. +- "renaming %q to %q not possible because %q has errors", +- r.from, r.to, pkg.Metadata().PkgPath) +- return nil +- } +- f.Find(pkg.TypesInfo(), pkg.Syntax()) +- } +- r.satisfyConstraints = f.Result +- } +- return r.satisfyConstraints -} - --// @since 3.17.0 --type TypeHierarchyClientCapabilities struct { -- // Whether implementation supports dynamic registration. If this is set to `true` -- // the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` -- // return value for the corresponding server capability as well. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` --} +-// -- helpers ---------------------------------------------------------- - --// @since 3.17.0 --type TypeHierarchyItem struct { -- // The name of this item. -- Name string `json:"name"` -- // The kind of this item. -- Kind SymbolKind `json:"kind"` -- // Tags for this item. -- Tags []SymbolTag `json:"tags,omitempty"` -- // More detail for this item, e.g. the signature of a function. -- Detail string `json:"detail,omitempty"` -- // The resource identifier of this item. -- URI DocumentURI `json:"uri"` -- // The range enclosing this symbol not including leading/trailing whitespace -- // but everything else, e.g. comments and code. -- Range Range `json:"range"` -- // The range that should be selected and revealed when this symbol is being -- // picked, e.g. the name of a function. Must be contained by the -- // {@link TypeHierarchyItem.range `range`}. -- SelectionRange Range `json:"selectionRange"` -- // A data entry field that is preserved between a type hierarchy prepare and -- // supertypes or subtypes requests. It could also be used to identify the -- // type hierarchy in the server, helping improve the performance on -- // resolving supertypes and subtypes. -- Data interface{} `json:"data,omitempty"` +-// recv returns the method's receiver. +-func recv(meth *types.Func) *types.Var { +- return meth.Type().(*types.Signature).Recv() -} - --// Type hierarchy options used during static registration. --// --// @since 3.17.0 --type TypeHierarchyOptions struct { -- WorkDoneProgressOptions +-// someUse returns an arbitrary use of obj within info. +-func someUse(info *types.Info, obj types.Object) *ast.Ident { +- for id, o := range info.Uses { +- if o == obj { +- return id +- } +- } +- return nil -} - --// The parameter of a `textDocument/prepareTypeHierarchy` request. --// --// @since 3.17.0 --type TypeHierarchyPrepareParams struct { -- TextDocumentPositionParams -- WorkDoneProgressParams +-func objectKind(obj types.Object) string { +- if obj == nil { +- return "nil object" +- } +- switch obj := obj.(type) { +- case *types.PkgName: +- return "imported package name" +- case *types.TypeName: +- return "type" +- case *types.Var: +- if obj.IsField() { +- return "field" +- } +- case *types.Func: +- if recv(obj) != nil { +- return "method" +- } +- } +- // label, func, var, const +- return strings.ToLower(strings.TrimPrefix(reflect.TypeOf(obj).String(), "*types.")) -} - --// Type hierarchy options used during static or dynamic registration. --// --// @since 3.17.0 --type TypeHierarchyRegistrationOptions struct { -- TextDocumentRegistrationOptions -- TypeHierarchyOptions -- StaticRegistrationOptions +-// NB: for renamings, blank is not considered valid. +-func isValidIdentifier(id string) bool { +- if id == "" || id == "_" { +- return false +- } +- for i, r := range id { +- if !isLetter(r) && (i == 0 || !isDigit(r)) { +- return false +- } +- } +- return token.Lookup(id) == token.IDENT -} - --// The parameter of a `typeHierarchy/subtypes` request. --// --// @since 3.17.0 --type TypeHierarchySubtypesParams struct { -- Item TypeHierarchyItem `json:"item"` -- WorkDoneProgressParams -- PartialResultParams +-// isLocal reports whether obj is local to some function. +-// Precondition: not a struct field or interface method. +-func isLocal(obj types.Object) bool { +- // [... 5=stmt 4=func 3=file 2=pkg 1=universe] +- var depth int +- for scope := obj.Parent(); scope != nil; scope = scope.Parent() { +- depth++ +- } +- return depth >= 4 -} - --// The parameter of a `typeHierarchy/supertypes` request. --// --// @since 3.17.0 --type TypeHierarchySupertypesParams struct { -- Item TypeHierarchyItem `json:"item"` -- WorkDoneProgressParams -- PartialResultParams +-func isPackageLevel(obj types.Object) bool { +- if obj == nil { +- return false +- } +- return obj.Pkg().Scope().Lookup(obj.Name()) == obj -} - --// created for Tuple --type UIntCommaUInt struct { -- Fld0 uint32 `json:"fld0"` -- Fld1 uint32 `json:"fld1"` +-// -- Plundered from go/scanner: --------------------------------------- +- +-func isLetter(ch rune) bool { +- return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' || ch >= 0x80 && unicode.IsLetter(ch) -} --type URI = string - --// A diagnostic report indicating that the last returned --// report is still accurate. --// --// @since 3.17.0 --type UnchangedDocumentDiagnosticReport struct { -- // A document diagnostic report indicating -- // no changes to the last result. A server can -- // only return `unchanged` if result ids are -- // provided. -- Kind string `json:"kind"` -- // A result id which will be sent on the next -- // diagnostic request for the same document. -- ResultID string `json:"resultId"` +-func isDigit(ch rune) bool { +- return '0' <= ch && ch <= '9' || ch >= 0x80 && unicode.IsDigit(ch) -} +diff -urN a/gopls/internal/golang/rename.go b/gopls/internal/golang/rename.go +--- a/gopls/internal/golang/rename.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/rename.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,1438 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// Moniker uniqueness level to define scope of the moniker. +-package golang +- +-// TODO(adonovan): -// --// @since 3.16.0 --type UniquenessLevel string +-// - method of generic concrete type -> arbitrary instances of same +-// +-// - make satisfy work across packages. +-// +-// - tests, tests, tests: +-// - play with renamings in the k8s tree. +-// - generics +-// - error cases (e.g. conflicts) +-// - renaming a symbol declared in the module cache +-// (currently proceeds with half of the renaming!) +-// - make sure all tests have both a local and a cross-package analogue. +-// - look at coverage +-// - special cases: embedded fields, interfaces, test variants, +-// function-local things with uppercase names; +-// packages with type errors (currently 'satisfy' rejects them), +-// package with missing imports; +-// +-// - measure performance in k8s. +-// +-// - The original gorename tool assumed well-typedness, but the gopls feature +-// does no such check (which actually makes it much more useful). +-// Audit to ensure it is safe on ill-typed code. +-// +-// - Generics support was no doubt buggy before but incrementalization +-// may have exacerbated it. If the problem were just about objects, +-// defs and uses it would be fairly simple, but type assignability +-// comes into play in the 'satisfy' check for method renamings. +-// De-instantiating Vector[int] to Vector[T] changes its type. +-// We need to come up with a theory for the satisfy check that +-// works with generics, and across packages. We currently have no +-// simple way to pass types between packages (think: objectpath for +-// types), though presumably exportdata could be pressed into service. +-// +-// - FileID-based de-duplication of edits to different URIs for the same file. - --// General parameters to unregister a request or notification. --type Unregistration struct { -- // The id used to unregister the request or notification. Usually an id -- // provided during the register request. -- ID string `json:"id"` -- // The method to unregister for. -- Method string `json:"method"` +-import ( +- "context" +- "errors" +- "fmt" +- "go/ast" +- "go/token" +- "go/types" +- "path" +- "path/filepath" +- "regexp" +- "sort" +- "strconv" +- "strings" +- +- "golang.org/x/mod/modfile" +- "golang.org/x/tools/go/ast/astutil" +- "golang.org/x/tools/go/types/objectpath" +- "golang.org/x/tools/go/types/typeutil" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/internal/aliases" +- "golang.org/x/tools/internal/diff" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/typesinternal" +- "golang.org/x/tools/refactor/satisfy" +-) +- +-// A renamer holds state of a single call to renameObj, which renames +-// an object (or several coupled objects) within a single type-checked +-// syntax package. +-type renamer struct { +- pkg *cache.Package // the syntax package in which the renaming is applied +- objsToUpdate map[types.Object]bool // records progress of calls to check +- conflicts []string +- from, to string +- satisfyConstraints map[satisfy.Constraint]bool +- msets typeutil.MethodSetCache +- changeMethods bool -} --type UnregistrationParams struct { -- Unregisterations []Unregistration `json:"unregisterations"` +- +-// A PrepareItem holds the result of a "prepare rename" operation: +-// the source range and value of a selected identifier. +-type PrepareItem struct { +- Range protocol.Range +- Text string -} - --// A versioned notebook document identifier. +-// PrepareRename searches for a valid renaming at position pp. -// --// @since 3.17.0 --type VersionedNotebookDocumentIdentifier struct { -- // The version number of this notebook document. -- Version int32 `json:"version"` -- // The notebook document's uri. -- URI URI `json:"uri"` --} +-// The returned usererr is intended to be displayed to the user to explain why +-// the prepare fails. Probably we could eliminate the redundancy in returning +-// two errors, but for now this is done defensively. +-func PrepareRename(ctx context.Context, snapshot *cache.Snapshot, f file.Handle, pp protocol.Position) (_ *PrepareItem, usererr, err error) { +- ctx, done := event.Start(ctx, "golang.PrepareRename") +- defer done() - --// A text document identifier to denote a specific version of a text document. --type VersionedTextDocumentIdentifier struct { -- // The version number of this document. -- Version int32 `json:"version"` -- TextDocumentIdentifier --} --type WatchKind = uint32 // line 13505// The parameters sent in a will save text document notification. --type WillSaveTextDocumentParams struct { -- // The document that will be saved. -- TextDocument TextDocumentIdentifier `json:"textDocument"` -- // The 'TextDocumentSaveReason'. -- Reason TextDocumentSaveReason `json:"reason"` --} --type WindowClientCapabilities struct { -- // It indicates whether the client supports server initiated -- // progress using the `window/workDoneProgress/create` request. -- // -- // The capability also controls Whether client supports handling -- // of progress notifications. If set servers are allowed to report a -- // `workDoneProgress` property in the request specific server -- // capabilities. -- // -- // @since 3.15.0 -- WorkDoneProgress bool `json:"workDoneProgress,omitempty"` -- // Capabilities specific to the showMessage request. -- // -- // @since 3.16.0 -- ShowMessage *ShowMessageRequestClientCapabilities `json:"showMessage,omitempty"` -- // Capabilities specific to the showDocument request. -- // -- // @since 3.16.0 -- ShowDocument *ShowDocumentClientCapabilities `json:"showDocument,omitempty"` --} --type WorkDoneProgressBegin struct { -- Kind string `json:"kind"` -- // Mandatory title of the progress operation. Used to briefly inform about -- // the kind of operation being performed. -- // -- // Examples: "Indexing" or "Linking dependencies". -- Title string `json:"title"` -- // Controls if a cancel button should show to allow the user to cancel the -- // long running operation. Clients that don't support cancellation are allowed -- // to ignore the setting. -- Cancellable bool `json:"cancellable,omitempty"` -- // Optional, more detailed associated progress message. Contains -- // complementary information to the `title`. -- // -- // Examples: "3/25 files", "project/src/module2", "node_modules/some_dep". -- // If unset, the previous progress message (if any) is still valid. -- Message string `json:"message,omitempty"` -- // Optional progress percentage to display (value 100 is considered 100%). -- // If not provided infinite progress is assumed and clients are allowed -- // to ignore the `percentage` value in subsequent in report notifications. -- // -- // The value should be steadily rising. Clients are free to ignore values -- // that are not following this rule. The value range is [0, 100]. -- Percentage uint32 `json:"percentage,omitempty"` --} --type WorkDoneProgressCancelParams struct { -- // The token to be used to report progress. -- Token ProgressToken `json:"token"` --} --type WorkDoneProgressCreateParams struct { -- // The token to be used to report progress. -- Token ProgressToken `json:"token"` --} --type WorkDoneProgressEnd struct { -- Kind string `json:"kind"` -- // Optional, a final message indicating to for example indicate the outcome -- // of the operation. -- Message string `json:"message,omitempty"` --} --type WorkDoneProgressOptions struct { -- WorkDoneProgress bool `json:"workDoneProgress,omitempty"` --} +- // Is the cursor within the package name declaration? +- if pgf, inPackageName, err := parsePackageNameDecl(ctx, snapshot, f, pp); err != nil { +- return nil, err, err +- } else if inPackageName { +- item, err := prepareRenamePackageName(ctx, snapshot, pgf) +- return item, err, err +- } - --// created for And --type WorkDoneProgressOptionsAndTextDocumentRegistrationOptions struct { -- WorkDoneProgressOptions -- TextDocumentRegistrationOptions --} --type WorkDoneProgressParams struct { -- // An optional token that a server can use to report work done progress. -- WorkDoneToken ProgressToken `json:"workDoneToken,omitempty"` --} --type WorkDoneProgressReport struct { -- Kind string `json:"kind"` -- // Controls enablement state of a cancel button. -- // -- // Clients that don't support cancellation or don't support controlling the button's -- // enablement state are allowed to ignore the property. -- Cancellable bool `json:"cancellable,omitempty"` -- // Optional, more detailed associated progress message. Contains -- // complementary information to the `title`. +- // Ordinary (non-package) renaming. - // -- // Examples: "3/25 files", "project/src/module2", "node_modules/some_dep". -- // If unset, the previous progress message (if any) is still valid. -- Message string `json:"message,omitempty"` -- // Optional progress percentage to display (value 100 is considered 100%). -- // If not provided infinite progress is assumed and clients are allowed -- // to ignore the `percentage` value in subsequent in report notifications. +- // Type-check the current package, locate the reference at the position, +- // validate the object, and report its name and range. - // -- // The value should be steadily rising. Clients are free to ignore values -- // that are not following this rule. The value range is [0, 100] -- Percentage uint32 `json:"percentage,omitempty"` +- // TODO(adonovan): in all cases below, we return usererr=nil, +- // which means we return (nil, nil) at the protocol +- // layer. This seems like a bug, or at best an exploitation of +- // knowledge of VSCode-specific behavior. Can we avoid that? +- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, f.URI()) +- if err != nil { +- return nil, nil, err +- } +- pos, err := pgf.PositionPos(pp) +- if err != nil { +- return nil, nil, err +- } +- targets, node, err := objectsAt(pkg.TypesInfo(), pgf.File, pos) +- if err != nil { +- return nil, nil, err +- } +- var obj types.Object +- for obj = range targets { +- break // pick one arbitrarily +- } +- if err := checkRenamable(obj); err != nil { +- return nil, nil, err +- } +- rng, err := pgf.NodeRange(node) +- if err != nil { +- return nil, nil, err +- } +- if _, isImport := node.(*ast.ImportSpec); isImport { +- // We're not really renaming the import path. +- rng.End = rng.Start +- } +- return &PrepareItem{ +- Range: rng, +- Text: obj.Name(), +- }, nil, nil -} - --// created for Literal (Lit_ServerCapabilities_workspace) --type Workspace6Gn struct { -- // The server supports workspace folder. -- // -- // @since 3.6.0 -- WorkspaceFolders *WorkspaceFolders5Gn `json:"workspaceFolders,omitempty"` -- // The server is interested in notifications/requests for operations on files. -- // -- // @since 3.16.0 -- FileOperations *FileOperationOptions `json:"fileOperations,omitempty"` +-func prepareRenamePackageName(ctx context.Context, snapshot *cache.Snapshot, pgf *parsego.File) (*PrepareItem, error) { +- // Does the client support file renaming? +- fileRenameSupported := false +- for _, op := range snapshot.Options().SupportedResourceOperations { +- if op == protocol.Rename { +- fileRenameSupported = true +- break +- } +- } +- if !fileRenameSupported { +- return nil, errors.New("can't rename package: LSP client does not support file renaming") +- } +- +- // Check validity of the metadata for the file's containing package. +- meta, err := NarrowestMetadataForFile(ctx, snapshot, pgf.URI) +- if err != nil { +- return nil, err +- } +- if meta.Name == "main" { +- return nil, fmt.Errorf("can't rename package \"main\"") +- } +- if strings.HasSuffix(string(meta.Name), "_test") { +- return nil, fmt.Errorf("can't rename x_test packages") +- } +- if meta.Module == nil { +- return nil, fmt.Errorf("can't rename package: missing module information for package %q", meta.PkgPath) +- } +- if meta.Module.Path == string(meta.PkgPath) { +- return nil, fmt.Errorf("can't rename package: package path %q is the same as module path %q", meta.PkgPath, meta.Module.Path) +- } +- +- // Return the location of the package declaration. +- rng, err := pgf.NodeRange(pgf.File.Name) +- if err != nil { +- return nil, err +- } +- return &PrepareItem{ +- Range: rng, +- Text: string(meta.Name), +- }, nil -} - --// Workspace specific client capabilities. --type WorkspaceClientCapabilities struct { -- // The client supports applying batch edits -- // to the workspace by supporting the request -- // 'workspace/applyEdit' -- ApplyEdit bool `json:"applyEdit,omitempty"` -- // Capabilities specific to `WorkspaceEdit`s. -- WorkspaceEdit *WorkspaceEditClientCapabilities `json:"workspaceEdit,omitempty"` -- // Capabilities specific to the `workspace/didChangeConfiguration` notification. -- DidChangeConfiguration DidChangeConfigurationClientCapabilities `json:"didChangeConfiguration,omitempty"` -- // Capabilities specific to the `workspace/didChangeWatchedFiles` notification. -- DidChangeWatchedFiles DidChangeWatchedFilesClientCapabilities `json:"didChangeWatchedFiles,omitempty"` -- // Capabilities specific to the `workspace/symbol` request. -- Symbol *WorkspaceSymbolClientCapabilities `json:"symbol,omitempty"` -- // Capabilities specific to the `workspace/executeCommand` request. -- ExecuteCommand *ExecuteCommandClientCapabilities `json:"executeCommand,omitempty"` -- // The client has support for workspace folders. -- // -- // @since 3.6.0 -- WorkspaceFolders bool `json:"workspaceFolders,omitempty"` -- // The client supports `workspace/configuration` requests. -- // -- // @since 3.6.0 -- Configuration bool `json:"configuration,omitempty"` -- // Capabilities specific to the semantic token requests scoped to the -- // workspace. -- // -- // @since 3.16.0. -- SemanticTokens *SemanticTokensWorkspaceClientCapabilities `json:"semanticTokens,omitempty"` -- // Capabilities specific to the code lens requests scoped to the -- // workspace. -- // -- // @since 3.16.0. -- CodeLens *CodeLensWorkspaceClientCapabilities `json:"codeLens,omitempty"` -- // The client has support for file notifications/requests for user operations on files. -- // -- // Since 3.16.0 -- FileOperations *FileOperationClientCapabilities `json:"fileOperations,omitempty"` -- // Capabilities specific to the inline values requests scoped to the -- // workspace. -- // -- // @since 3.17.0. -- InlineValue *InlineValueWorkspaceClientCapabilities `json:"inlineValue,omitempty"` -- // Capabilities specific to the inlay hint requests scoped to the -- // workspace. -- // -- // @since 3.17.0. -- InlayHint *InlayHintWorkspaceClientCapabilities `json:"inlayHint,omitempty"` -- // Capabilities specific to the diagnostic requests scoped to the -- // workspace. -- // -- // @since 3.17.0. -- Diagnostics *DiagnosticWorkspaceClientCapabilities `json:"diagnostics,omitempty"` +-func checkRenamable(obj types.Object) error { +- switch obj := obj.(type) { +- case *types.Var: +- if obj.Embedded() { +- return fmt.Errorf("can't rename embedded fields: rename the type directly or name the field") +- } +- case *types.Builtin, *types.Nil: +- return fmt.Errorf("%s is built in and cannot be renamed", obj.Name()) +- } +- if obj.Pkg() == nil || obj.Pkg().Path() == "unsafe" { +- // e.g. error.Error, unsafe.Pointer +- return fmt.Errorf("%s is built in and cannot be renamed", obj.Name()) +- } +- if obj.Name() == "_" { +- return errors.New("can't rename \"_\"") +- } +- return nil -} - --// Parameters of the workspace diagnostic request. --// --// @since 3.17.0 --type WorkspaceDiagnosticParams struct { -- // The additional identifier provided during registration. -- Identifier string `json:"identifier,omitempty"` -- // The currently known diagnostic reports with their -- // previous result ids. -- PreviousResultIds []PreviousResultID `json:"previousResultIds"` -- WorkDoneProgressParams -- PartialResultParams --} +-// Rename returns a map of TextEdits for each file modified when renaming a +-// given identifier within a package and a boolean value of true for renaming +-// package and false otherwise. +-func Rename(ctx context.Context, snapshot *cache.Snapshot, f file.Handle, pp protocol.Position, newName string) (map[protocol.DocumentURI][]protocol.TextEdit, bool, error) { +- ctx, done := event.Start(ctx, "golang.Rename") +- defer done() - --// A workspace diagnostic report. --// --// @since 3.17.0 --type WorkspaceDiagnosticReport struct { -- Items []WorkspaceDocumentDiagnosticReport `json:"items"` --} +- if !isValidIdentifier(newName) { +- return nil, false, fmt.Errorf("invalid identifier to rename: %q", newName) +- } - --// A partial result for a workspace diagnostic report. --// --// @since 3.17.0 --type WorkspaceDiagnosticReportPartialResult struct { -- Items []WorkspaceDocumentDiagnosticReport `json:"items"` --} +- // Cursor within package name declaration? +- _, inPackageName, err := parsePackageNameDecl(ctx, snapshot, f, pp) +- if err != nil { +- return nil, false, err +- } - --// A workspace diagnostic document report. --// --// @since 3.17.0 --type WorkspaceDocumentDiagnosticReport = Or_WorkspaceDocumentDiagnosticReport // (alias) line 14399 --// A workspace edit represents changes to many resources managed in the workspace. The edit --// should either provide `changes` or `documentChanges`. If documentChanges are present --// they are preferred over `changes` if the client can handle versioned document edits. --// --// Since version 3.13.0 a workspace edit can contain resource operations as well. If resource --// operations are present clients need to execute the operations in the order in which they --// are provided. So a workspace edit for example can consist of the following two changes: --// (1) a create file a.txt and (2) a text document edit which insert text into file a.txt. --// --// An invalid sequence (e.g. (1) delete file a.txt and (2) insert text into file a.txt) will --// cause failure of the operation. How the client recovers from the failure is described by --// the client capability: `workspace.workspaceEdit.failureHandling` --type WorkspaceEdit struct { -- // Holds changes to existing resources. -- Changes map[DocumentURI][]TextEdit `json:"changes,omitempty"` -- // Depending on the client capability `workspace.workspaceEdit.resourceOperations` document changes -- // are either an array of `TextDocumentEdit`s to express changes to n different text documents -- // where each text document edit addresses a specific version of a text document. Or it can contain -- // above `TextDocumentEdit`s mixed with create, rename and delete file / folder operations. -- // -- // Whether a client supports versioned document edits is expressed via -- // `workspace.workspaceEdit.documentChanges` client capability. -- // -- // If a client neither supports `documentChanges` nor `workspace.workspaceEdit.resourceOperations` then -- // only plain `TextEdit`s using the `changes` property are supported. -- DocumentChanges []DocumentChanges `json:"documentChanges,omitempty"` -- // A map of change annotations that can be referenced in `AnnotatedTextEdit`s or create, rename and -- // delete file / folder operations. -- // -- // Whether clients honor this property depends on the client capability `workspace.changeAnnotationSupport`. -- // -- // @since 3.16.0 -- ChangeAnnotations map[ChangeAnnotationIdentifier]ChangeAnnotation `json:"changeAnnotations,omitempty"` --} --type WorkspaceEditClientCapabilities struct { -- // The client supports versioned document changes in `WorkspaceEdit`s -- DocumentChanges bool `json:"documentChanges,omitempty"` -- // The resource operations the client supports. Clients should at least -- // support 'create', 'rename' and 'delete' files and folders. -- // -- // @since 3.13.0 -- ResourceOperations []ResourceOperationKind `json:"resourceOperations,omitempty"` -- // The failure handling strategy of a client if applying the workspace edit -- // fails. -- // -- // @since 3.13.0 -- FailureHandling *FailureHandlingKind `json:"failureHandling,omitempty"` -- // Whether the client normalizes line endings to the client specific -- // setting. -- // If set to `true` the client will normalize line ending characters -- // in a workspace edit to the client-specified new line -- // character. -- // -- // @since 3.16.0 -- NormalizesLineEndings bool `json:"normalizesLineEndings,omitempty"` -- // Whether the client in general supports change annotations on text edits, -- // create file, rename file and delete file changes. -- // -- // @since 3.16.0 -- ChangeAnnotationSupport *PChangeAnnotationSupportPWorkspaceEdit `json:"changeAnnotationSupport,omitempty"` --} +- var editMap map[protocol.DocumentURI][]diff.Edit +- if inPackageName { +- editMap, err = renamePackageName(ctx, snapshot, f, PackageName(newName)) +- } else { +- editMap, err = renameOrdinary(ctx, snapshot, f, pp, newName) +- } +- if err != nil { +- return nil, false, err +- } - --// A workspace folder inside a client. --type WorkspaceFolder struct { -- // The associated URI for this workspace folder. -- URI URI `json:"uri"` -- // The name of the workspace folder. Used to refer to this -- // workspace folder in the user interface. -- Name string `json:"name"` --} --type WorkspaceFolders5Gn struct { -- // The server has support for workspace folders -- Supported bool `json:"supported,omitempty"` -- // Whether the server wants to receive workspace folder -- // change notifications. -- // -- // If a string is provided the string is treated as an ID -- // under which the notification is registered on the client -- // side. The ID can be used to unregister for these events -- // using the `client/unregisterCapability` request. -- ChangeNotifications string `json:"changeNotifications,omitempty"` --} +- // Convert edits to protocol form. +- result := make(map[protocol.DocumentURI][]protocol.TextEdit) +- for uri, edits := range editMap { +- // Sort and de-duplicate edits. +- // +- // Overlapping edits may arise in local renamings (due +- // to type switch implicits) and globals ones (due to +- // processing multiple package variants). +- // +- // We assume renaming produces diffs that are all +- // replacements (no adjacent insertions that might +- // become reordered) and that are either identical or +- // non-overlapping. +- diff.SortEdits(edits) +- filtered := edits[:0] +- for i, edit := range edits { +- if i == 0 || edit != filtered[len(filtered)-1] { +- filtered = append(filtered, edit) +- } +- } +- edits = filtered - --// The workspace folder change event. --type WorkspaceFoldersChangeEvent struct { -- // The array of added workspace folders -- Added []WorkspaceFolder `json:"added"` -- // The array of the removed workspace folders -- Removed []WorkspaceFolder `json:"removed"` +- // TODO(adonovan): the logic above handles repeat edits to the +- // same file URI (e.g. as a member of package p and p_test) but +- // is not sufficient to handle file-system level aliasing arising +- // from symbolic or hard links. For that, we should use a +- // robustio-FileID-keyed map. +- // See https://go.dev/cl/457615 for example. +- // This really occurs in practice, e.g. kubernetes has +- // vendor/k8s.io/kubectl -> ../../staging/src/k8s.io/kubectl. +- fh, err := snapshot.ReadFile(ctx, uri) +- if err != nil { +- return nil, false, err +- } +- data, err := fh.Content() +- if err != nil { +- return nil, false, err +- } +- m := protocol.NewMapper(uri, data) +- protocolEdits, err := protocol.EditsFromDiffEdits(m, edits) +- if err != nil { +- return nil, false, err +- } +- result[uri] = protocolEdits +- } +- +- return result, inPackageName, nil -} --type WorkspaceFoldersInitializeParams struct { -- // The workspace folders configured in the client when the server starts. +- +-// renameOrdinary renames an ordinary (non-package) name throughout the workspace. +-func renameOrdinary(ctx context.Context, snapshot *cache.Snapshot, f file.Handle, pp protocol.Position, newName string) (map[protocol.DocumentURI][]diff.Edit, error) { +- // Type-check the referring package and locate the object(s). - // -- // This property is only available if the client supports workspace folders. -- // It can be `null` if the client supports workspace folders but none are -- // configured. +- // Unlike NarrowestPackageForFile, this operation prefers the +- // widest variant as, for non-exported identifiers, it is the +- // only package we need. (In case you're wondering why +- // 'references' doesn't also want the widest variant: it +- // computes the union across all variants.) +- var targets map[types.Object]ast.Node +- var pkg *cache.Package +- { +- mps, err := snapshot.MetadataForFile(ctx, f.URI()) +- if err != nil { +- return nil, err +- } +- metadata.RemoveIntermediateTestVariants(&mps) +- if len(mps) == 0 { +- return nil, fmt.Errorf("no package metadata for file %s", f.URI()) +- } +- widest := mps[len(mps)-1] // widest variant may include _test.go files +- pkgs, err := snapshot.TypeCheck(ctx, widest.ID) +- if err != nil { +- return nil, err +- } +- pkg = pkgs[0] +- pgf, err := pkg.File(f.URI()) +- if err != nil { +- return nil, err // "can't happen" +- } +- pos, err := pgf.PositionPos(pp) +- if err != nil { +- return nil, err +- } +- objects, _, err := objectsAt(pkg.TypesInfo(), pgf.File, pos) +- if err != nil { +- return nil, err +- } +- targets = objects +- } +- +- // Pick a representative object arbitrarily. +- // (All share the same name, pos, and kind.) +- var obj types.Object +- for obj = range targets { +- break +- } +- if obj.Name() == newName { +- return nil, fmt.Errorf("old and new names are the same: %s", newName) +- } +- if err := checkRenamable(obj); err != nil { +- return nil, err +- } +- +- // Find objectpath, if object is exported ("" otherwise). +- var declObjPath objectpath.Path +- if obj.Exported() { +- // objectpath.For requires the origin of a generic function or type, not an +- // instantiation (a bug?). +- // +- // Note that unlike Funcs, TypeNames are always canonical (they are "left" +- // of the type parameters, unlike methods). +- switch obj.(type) { // avoid "obj :=" since cases reassign the var +- case *types.TypeName: +- if _, ok := aliases.Unalias(obj.Type()).(*types.TypeParam); ok { +- // As with capitalized function parameters below, type parameters are +- // local. +- goto skipObjectPath +- } +- case *types.Func: +- obj = obj.(*types.Func).Origin() +- case *types.Var: +- // TODO(adonovan): do vars need the origin treatment too? (issue #58462) +- +- // Function parameter and result vars that are (unusually) +- // capitalized are technically exported, even though they +- // cannot be referenced, because they may affect downstream +- // error messages. But we can safely treat them as local. +- // +- // This is not merely an optimization: the renameExported +- // operation gets confused by such vars. It finds them from +- // objectpath, the classifies them as local vars, but as +- // they came from export data they lack syntax and the +- // correct scope tree (issue #61294). +- if !obj.(*types.Var).IsField() && !isPackageLevel(obj) { +- goto skipObjectPath +- } +- } +- if path, err := objectpath.For(obj); err == nil { +- declObjPath = path +- } +- skipObjectPath: +- } +- +- // Nonexported? Search locally. +- if declObjPath == "" { +- var objects []types.Object +- for obj := range targets { +- objects = append(objects, obj) +- } +- editMap, _, err := renameObjects(newName, pkg, objects...) +- return editMap, err +- } +- +- // Exported: search globally. - // -- // @since 3.6.0 -- WorkspaceFolders []WorkspaceFolder `json:"workspaceFolders,omitempty"` --} --type WorkspaceFoldersServerCapabilities struct { -- // The server has support for workspace folders -- Supported bool `json:"supported,omitempty"` -- // Whether the server wants to receive workspace folder -- // change notifications. +- // For exported package-level var/const/func/type objects, the +- // search scope is just the direct importers. - // -- // If a string is provided the string is treated as an ID -- // under which the notification is registered on the client -- // side. The ID can be used to unregister for these events -- // using the `client/unregisterCapability` request. -- ChangeNotifications string `json:"changeNotifications,omitempty"` --} +- // For exported fields and methods, the scope is the +- // transitive rdeps. (The exportedness of the field's struct +- // or method's receiver is irrelevant.) +- transitive := false +- switch obj := obj.(type) { +- case *types.TypeName: +- // Renaming an exported package-level type +- // requires us to inspect all transitive rdeps +- // in the event that the type is embedded. +- // +- // TODO(adonovan): opt: this is conservative +- // but inefficient. Instead, expand the scope +- // of the search only if we actually encounter +- // an embedding of the type, and only then to +- // the rdeps of the embedding package. +- if obj.Parent() == obj.Pkg().Scope() { +- transitive = true +- } - --// A full document diagnostic report for a workspace diagnostic result. --// --// @since 3.17.0 --type WorkspaceFullDocumentDiagnosticReport struct { -- // The URI for which diagnostic information is reported. -- URI DocumentURI `json:"uri"` -- // The version number for which the diagnostics are reported. -- // If the document is not marked as open `null` can be provided. -- Version int32 `json:"version"` -- FullDocumentDiagnosticReport +- case *types.Var: +- if obj.IsField() { +- transitive = true // field +- } +- +- // TODO(adonovan): opt: process only packages that +- // contain a reference (xrefs) to the target field. +- +- case *types.Func: +- if obj.Type().(*types.Signature).Recv() != nil { +- transitive = true // method +- } +- +- // It's tempting to optimize by skipping +- // packages that don't contain a reference to +- // the method in the xrefs index, but we still +- // need to apply the satisfy check to those +- // packages to find assignment statements that +- // might expands the scope of the renaming. +- } +- +- // Type-check all the packages to inspect. +- declURI := protocol.URIFromPath(pkg.FileSet().File(obj.Pos()).Name()) +- pkgs, err := typeCheckReverseDependencies(ctx, snapshot, declURI, transitive) +- if err != nil { +- return nil, err +- } +- +- // Apply the renaming to the (initial) object. +- declPkgPath := PackagePath(obj.Pkg().Path()) +- return renameExported(pkgs, declPkgPath, declObjPath, newName) -} - --// A special workspace symbol that supports locations without a range. +-// typeCheckReverseDependencies returns the type-checked packages for +-// the reverse dependencies of all packages variants containing +-// file declURI. The packages are in some topological order. -// --// See also SymbolInformation. +-// It includes all variants (even intermediate test variants) for the +-// purposes of computing reverse dependencies, but discards ITVs for +-// the actual renaming work. -// --// @since 3.17.0 --type WorkspaceSymbol struct { -- // The location of the symbol. Whether a server is allowed to -- // return a location without a range depends on the client -- // capability `workspace.symbol.resolveSupport`. -- // -- // See SymbolInformation#location for more details. -- Location OrPLocation_workspace_symbol `json:"location"` -- // A data entry field that is preserved on a workspace symbol between a -- // workspace symbol request and a workspace symbol resolve request. -- Data interface{} `json:"data,omitempty"` -- BaseSymbolInformation --} +-// (This neglects obscure edge cases where a _test.go file changes the +-// selectors used only in an ITV, but life is short. Also sin must be +-// punished.) +-func typeCheckReverseDependencies(ctx context.Context, snapshot *cache.Snapshot, declURI protocol.DocumentURI, transitive bool) ([]*cache.Package, error) { +- variants, err := snapshot.MetadataForFile(ctx, declURI) +- if err != nil { +- return nil, err +- } +- // variants must include ITVs for the reverse dependency +- // computation, but they are filtered out before we typecheck. +- allRdeps := make(map[PackageID]*metadata.Package) +- for _, variant := range variants { +- rdeps, err := snapshot.ReverseDependencies(ctx, variant.ID, transitive) +- if err != nil { +- return nil, err +- } +- allRdeps[variant.ID] = variant // include self +- for id, meta := range rdeps { +- allRdeps[id] = meta +- } +- } +- var ids []PackageID +- for id, meta := range allRdeps { +- if meta.IsIntermediateTestVariant() { +- continue +- } +- ids = append(ids, id) +- } - --// Client capabilities for a {@link WorkspaceSymbolRequest}. --type WorkspaceSymbolClientCapabilities struct { -- // Symbol request supports dynamic registration. -- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -- // Specific capabilities for the `SymbolKind` in the `workspace/symbol` request. -- SymbolKind *PSymbolKindPSymbol `json:"symbolKind,omitempty"` -- // The client supports tags on `SymbolInformation`. -- // Clients supporting tags have to handle unknown tags gracefully. +- // Sort the packages into some topological order of the +- // (unfiltered) metadata graph. +- metadata.SortPostOrder(snapshot, ids) +- +- // Dependencies must be visited first since they can expand +- // the search set. Ideally we would process the (filtered) set +- // of packages in the parallel postorder of the snapshot's +- // (unfiltered) metadata graph, but this is quite tricky +- // without a good graph abstraction. - // -- // @since 3.16.0 -- TagSupport *PTagSupportPSymbol `json:"tagSupport,omitempty"` -- // The client support partial workspace symbols. The client will send the -- // request `workspaceSymbol/resolve` to the server to resolve additional -- // properties. +- // For now, we visit packages sequentially in order of +- // ascending height, like an inverted breadth-first search. - // -- // @since 3.17.0 -- ResolveSupport *PResolveSupportPSymbol `json:"resolveSupport,omitempty"` +- // Type checking is by far the dominant cost, so +- // overlapping it with renaming may not be worthwhile. +- return snapshot.TypeCheck(ctx, ids...) -} - --// Server capabilities for a {@link WorkspaceSymbolRequest}. --type WorkspaceSymbolOptions struct { -- // The server provides support to resolve additional -- // information for a workspace symbol. -- // -- // @since 3.17.0 -- ResolveProvider bool `json:"resolveProvider,omitempty"` -- WorkDoneProgressOptions --} +-// renameExported renames the object denoted by (pkgPath, objPath) +-// within the specified packages, along with any other objects that +-// must be renamed as a consequence. The slice of packages must be +-// topologically ordered. +-func renameExported(pkgs []*cache.Package, declPkgPath PackagePath, declObjPath objectpath.Path, newName string) (map[protocol.DocumentURI][]diff.Edit, error) { - --// The parameters of a {@link WorkspaceSymbolRequest}. --type WorkspaceSymbolParams struct { -- // A query string to filter symbols by. Clients may send an empty -- // string here to request all symbols. -- Query string `json:"query"` -- WorkDoneProgressParams -- PartialResultParams --} +- // A target is a name for an object that is stable across types.Packages. +- type target struct { +- pkg PackagePath +- obj objectpath.Path +- } - --// Registration options for a {@link WorkspaceSymbolRequest}. --type WorkspaceSymbolRegistrationOptions struct { -- WorkspaceSymbolOptions --} +- // Populate the initial set of target objects. +- // This set may grow as we discover the consequences of each renaming. +- // +- // TODO(adonovan): strictly, each cone of reverse dependencies +- // of a single variant should have its own target map that +- // monotonically expands as we go up the import graph, because +- // declarations in test files can alter the set of +- // package-level names and change the meaning of field and +- // method selectors. So if we parallelize the graph +- // visitation (see above), we should also compute the targets +- // as a union of dependencies. +- // +- // Or we could decide that the logic below is fast enough not +- // to need parallelism. In small measurements so far the +- // type-checking step is about 95% and the renaming only 5%. +- targets := map[target]bool{{declPkgPath, declObjPath}: true} - --// An unchanged document diagnostic report for a workspace diagnostic result. --// --// @since 3.17.0 --type WorkspaceUnchangedDocumentDiagnosticReport struct { -- // The URI for which diagnostic information is reported. -- URI DocumentURI `json:"uri"` -- // The version number for which the diagnostics are reported. -- // If the document is not marked as open `null` can be provided. -- Version int32 `json:"version"` -- UnchangedDocumentDiagnosticReport +- // Apply the renaming operation to each package. +- allEdits := make(map[protocol.DocumentURI][]diff.Edit) +- for _, pkg := range pkgs { +- +- // Resolved target objects within package pkg. +- var objects []types.Object +- for t := range targets { +- p := pkg.DependencyTypes(t.pkg) +- if p == nil { +- continue // indirect dependency of no consequence +- } +- obj, err := objectpath.Object(p, t.obj) +- if err != nil { +- // Possibly a method or an unexported type +- // that is not reachable through export data? +- // See https://github.com/golang/go/issues/60789. +- // +- // TODO(adonovan): it seems unsatisfactory that Object +- // should return an error for a "valid" path. Perhaps +- // we should define such paths as invalid and make +- // objectpath.For compute reachability? +- // Would that be a compatible change? +- continue +- } +- objects = append(objects, obj) +- } +- if len(objects) == 0 { +- continue // no targets of consequence to this package +- } +- +- // Apply the renaming. +- editMap, moreObjects, err := renameObjects(newName, pkg, objects...) +- if err != nil { +- return nil, err +- } +- +- // It is safe to concatenate the edits as they are non-overlapping +- // (or identical, in which case they will be de-duped by Rename). +- for uri, edits := range editMap { +- allEdits[uri] = append(allEdits[uri], edits...) +- } +- +- // Expand the search set? +- for obj := range moreObjects { +- objpath, err := objectpath.For(obj) +- if err != nil { +- continue // not exported +- } +- target := target{PackagePath(obj.Pkg().Path()), objpath} +- targets[target] = true +- +- // TODO(adonovan): methods requires dynamic +- // programming of the product targets x +- // packages as any package might add a new +- // target (from a forward dep) as a +- // consequence, and any target might imply a +- // new set of rdeps. See golang/go#58461. +- } +- } +- +- return allEdits, nil -} - --// The initialize parameters --type XInitializeParams struct { -- // The process Id of the parent process that started -- // the server. -- // -- // Is `null` if the process has not been started by another process. -- // If the parent process is not alive then the server should exit. -- ProcessID int32 `json:"processId"` -- // Information about the client -- // -- // @since 3.15.0 -- ClientInfo *Msg_XInitializeParams_clientInfo `json:"clientInfo,omitempty"` -- // The locale the client is currently showing the user interface -- // in. This must not necessarily be the locale of the operating -- // system. -- // -- // Uses IETF language tags as the value's syntax -- // (See https://en.wikipedia.org/wiki/IETF_language_tag) -- // -- // @since 3.16.0 -- Locale string `json:"locale,omitempty"` -- // The rootPath of the workspace. Is null -- // if no folder is open. -- // -- // @deprecated in favour of rootUri. -- RootPath string `json:"rootPath,omitempty"` -- // The rootUri of the workspace. Is null if no -- // folder is open. If both `rootPath` and `rootUri` are set -- // `rootUri` wins. +-// renamePackageName renames package declarations, imports, and go.mod files. +-func renamePackageName(ctx context.Context, s *cache.Snapshot, f file.Handle, newName PackageName) (map[protocol.DocumentURI][]diff.Edit, error) { +- // Rename the package decl and all imports. +- renamingEdits, err := renamePackage(ctx, s, f, newName) +- if err != nil { +- return nil, err +- } +- +- // Update the last component of the file's enclosing directory. +- oldBase := filepath.Dir(f.URI().Path()) +- newPkgDir := filepath.Join(filepath.Dir(oldBase), string(newName)) +- +- // Update any affected replace directives in go.mod files. +- // TODO(adonovan): extract into its own function. - // -- // @deprecated in favour of workspaceFolders. -- RootURI DocumentURI `json:"rootUri"` -- // The capabilities provided by the client (editor or tool) -- Capabilities ClientCapabilities `json:"capabilities"` -- // User provided initialization options. -- InitializationOptions interface{} `json:"initializationOptions,omitempty"` -- // The initial trace setting. If omitted trace is disabled ('off'). -- Trace *TraceValues `json:"trace,omitempty"` -- WorkDoneProgressParams +- // Get all workspace modules. +- // TODO(adonovan): should this operate on all go.mod files, +- // irrespective of whether they are included in the workspace? +- modFiles := s.View().ModFiles() +- for _, m := range modFiles { +- fh, err := s.ReadFile(ctx, m) +- if err != nil { +- return nil, err +- } +- pm, err := s.ParseMod(ctx, fh) +- if err != nil { +- return nil, err +- } +- +- modFileDir := filepath.Dir(pm.URI.Path()) +- affectedReplaces := []*modfile.Replace{} +- +- // Check if any replace directives need to be fixed +- for _, r := range pm.File.Replace { +- if !strings.HasPrefix(r.New.Path, "/") && !strings.HasPrefix(r.New.Path, "./") && !strings.HasPrefix(r.New.Path, "../") { +- continue +- } +- +- replacedPath := r.New.Path +- if strings.HasPrefix(r.New.Path, "./") || strings.HasPrefix(r.New.Path, "../") { +- replacedPath = filepath.Join(modFileDir, r.New.Path) +- } +- +- // TODO: Is there a risk of converting a '\' delimited replacement to a '/' delimited replacement? +- if !strings.HasPrefix(filepath.ToSlash(replacedPath)+"/", filepath.ToSlash(oldBase)+"/") { +- continue // not affected by the package renaming +- } +- +- affectedReplaces = append(affectedReplaces, r) +- } +- +- if len(affectedReplaces) == 0 { +- continue +- } +- copied, err := modfile.Parse("", pm.Mapper.Content, nil) +- if err != nil { +- return nil, err +- } +- +- for _, r := range affectedReplaces { +- replacedPath := r.New.Path +- if strings.HasPrefix(r.New.Path, "./") || strings.HasPrefix(r.New.Path, "../") { +- replacedPath = filepath.Join(modFileDir, r.New.Path) +- } +- +- suffix := strings.TrimPrefix(replacedPath, oldBase) +- +- newReplacedPath, err := filepath.Rel(modFileDir, newPkgDir+suffix) +- if err != nil { +- return nil, err +- } +- +- newReplacedPath = filepath.ToSlash(newReplacedPath) +- +- if !strings.HasPrefix(newReplacedPath, "/") && !strings.HasPrefix(newReplacedPath, "../") { +- newReplacedPath = "./" + newReplacedPath +- } +- +- if err := copied.AddReplace(r.Old.Path, "", newReplacedPath, ""); err != nil { +- return nil, err +- } +- } +- +- copied.Cleanup() +- newContent, err := copied.Format() +- if err != nil { +- return nil, err +- } +- +- // Calculate the edits to be made due to the change. +- edits := diff.Bytes(pm.Mapper.Content, newContent) +- renamingEdits[pm.URI] = append(renamingEdits[pm.URI], edits...) +- } +- +- return renamingEdits, nil -} - --// The initialize parameters --type _InitializeParams struct { -- // The process Id of the parent process that started -- // the server. -- // -- // Is `null` if the process has not been started by another process. -- // If the parent process is not alive then the server should exit. -- ProcessID int32 `json:"processId"` -- // Information about the client -- // -- // @since 3.15.0 -- ClientInfo *Msg_XInitializeParams_clientInfo `json:"clientInfo,omitempty"` -- // The locale the client is currently showing the user interface -- // in. This must not necessarily be the locale of the operating -- // system. -- // -- // Uses IETF language tags as the value's syntax -- // (See https://en.wikipedia.org/wiki/IETF_language_tag) -- // -- // @since 3.16.0 -- Locale string `json:"locale,omitempty"` -- // The rootPath of the workspace. Is null -- // if no folder is open. -- // -- // @deprecated in favour of rootUri. -- RootPath string `json:"rootPath,omitempty"` -- // The rootUri of the workspace. Is null if no -- // folder is open. If both `rootPath` and `rootUri` are set -- // `rootUri` wins. -- // -- // @deprecated in favour of workspaceFolders. -- RootURI DocumentURI `json:"rootUri"` -- // The capabilities provided by the client (editor or tool) -- Capabilities ClientCapabilities `json:"capabilities"` -- // User provided initialization options. -- InitializationOptions interface{} `json:"initializationOptions,omitempty"` -- // The initial trace setting. If omitted trace is disabled ('off'). -- Trace *TraceValues `json:"trace,omitempty"` -- WorkDoneProgressParams +-// renamePackage computes all workspace edits required to rename the package +-// described by the given metadata, to newName, by renaming its package +-// directory. +-// +-// It updates package clauses and import paths for the renamed package as well +-// as any other packages affected by the directory renaming among all packages +-// known to the snapshot. +-func renamePackage(ctx context.Context, s *cache.Snapshot, f file.Handle, newName PackageName) (map[protocol.DocumentURI][]diff.Edit, error) { +- if strings.HasSuffix(string(newName), "_test") { +- return nil, fmt.Errorf("cannot rename to _test package") +- } +- +- // We need metadata for the relevant package and module paths. +- // These should be the same for all packages containing the file. +- meta, err := NarrowestMetadataForFile(ctx, s, f.URI()) +- if err != nil { +- return nil, err +- } +- +- oldPkgPath := meta.PkgPath +- if meta.Module == nil { +- return nil, fmt.Errorf("cannot rename package: missing module information for package %q", meta.PkgPath) +- } +- modulePath := PackagePath(meta.Module.Path) +- if modulePath == oldPkgPath { +- return nil, fmt.Errorf("cannot rename package: module path %q is the same as the package path, so renaming the package directory would have no effect", modulePath) +- } +- +- newPathPrefix := path.Join(path.Dir(string(oldPkgPath)), string(newName)) +- +- // We must inspect all packages, not just direct importers, +- // because we also rename subpackages, which may be unrelated. +- // (If the renamed package imports a subpackage it may require +- // edits to both its package and import decls.) +- allMetadata, err := s.AllMetadata(ctx) +- if err != nil { +- return nil, err +- } +- +- // Rename package and import declarations in all relevant packages. +- edits := make(map[protocol.DocumentURI][]diff.Edit) +- for _, mp := range allMetadata { +- // Special case: x_test packages for the renamed package will not have the +- // package path as a dir prefix, but still need their package clauses +- // renamed. +- if mp.PkgPath == oldPkgPath+"_test" { +- if err := renamePackageClause(ctx, mp, s, newName+"_test", edits); err != nil { +- return nil, err +- } +- continue +- } +- +- // Subtle: check this condition before checking for valid module info +- // below, because we should not fail this operation if unrelated packages +- // lack module info. +- if !strings.HasPrefix(string(mp.PkgPath)+"/", string(oldPkgPath)+"/") { +- continue // not affected by the package renaming +- } +- +- if mp.Module == nil { +- // This check will always fail under Bazel. +- return nil, fmt.Errorf("cannot rename package: missing module information for package %q", mp.PkgPath) +- } +- +- if modulePath != PackagePath(mp.Module.Path) { +- continue // don't edit imports if nested package and renaming package have different module paths +- } +- +- // Renaming a package consists of changing its import path and package name. +- suffix := strings.TrimPrefix(string(mp.PkgPath), string(oldPkgPath)) +- newPath := newPathPrefix + suffix +- +- pkgName := mp.Name +- if mp.PkgPath == oldPkgPath { +- pkgName = newName +- +- if err := renamePackageClause(ctx, mp, s, newName, edits); err != nil { +- return nil, err +- } +- } +- +- imp := ImportPath(newPath) // TODO(adonovan): what if newPath has vendor/ prefix? +- if err := renameImports(ctx, s, mp, imp, pkgName, edits); err != nil { +- return nil, err +- } +- } +- +- return edits, nil -} - --const ( -- // A set of predefined code action kinds -- // Empty kind. -- Empty CodeActionKind = "" -- // Base kind for quickfix actions: 'quickfix' -- QuickFix CodeActionKind = "quickfix" -- // Base kind for refactoring actions: 'refactor' -- Refactor CodeActionKind = "refactor" -- // Base kind for refactoring extraction actions: 'refactor.extract' -- // -- // Example extract actions: -- // -- // -- // - Extract method -- // - Extract function -- // - Extract variable -- // - Extract interface from class -- // - ... -- RefactorExtract CodeActionKind = "refactor.extract" -- // Base kind for refactoring inline actions: 'refactor.inline' -- // -- // Example inline actions: -- // -- // -- // - Inline function -- // - Inline variable -- // - Inline constant -- // - ... -- RefactorInline CodeActionKind = "refactor.inline" -- // Base kind for refactoring rewrite actions: 'refactor.rewrite' -- // -- // Example rewrite actions: -- // -- // -- // - Convert JavaScript function to class -- // - Add or remove parameter -- // - Encapsulate field -- // - Make method static -- // - Move method to base class -- // - ... -- RefactorRewrite CodeActionKind = "refactor.rewrite" -- // Base kind for source actions: `source` -- // -- // Source code actions apply to the entire file. -- Source CodeActionKind = "source" -- // Base kind for an organize imports source action: `source.organizeImports` -- SourceOrganizeImports CodeActionKind = "source.organizeImports" -- // Base kind for auto-fix source actions: `source.fixAll`. -- // -- // Fix all actions automatically fix errors that have a clear fix that do not require user input. -- // They should not suppress errors or perform unsafe fixes such as generating new types or classes. -- // -- // @since 3.15.0 -- SourceFixAll CodeActionKind = "source.fixAll" -- // The reason why code actions were requested. -- // -- // @since 3.17.0 -- // Code actions were explicitly requested by the user or by an extension. -- CodeActionInvoked CodeActionTriggerKind = 1 -- // Code actions were requested automatically. -- // -- // This typically happens when current selection in a file changes, but can -- // also be triggered when file content changes. -- CodeActionAutomatic CodeActionTriggerKind = 2 -- // The kind of a completion entry. -- TextCompletion CompletionItemKind = 1 -- MethodCompletion CompletionItemKind = 2 -- FunctionCompletion CompletionItemKind = 3 -- ConstructorCompletion CompletionItemKind = 4 -- FieldCompletion CompletionItemKind = 5 -- VariableCompletion CompletionItemKind = 6 -- ClassCompletion CompletionItemKind = 7 -- InterfaceCompletion CompletionItemKind = 8 -- ModuleCompletion CompletionItemKind = 9 -- PropertyCompletion CompletionItemKind = 10 -- UnitCompletion CompletionItemKind = 11 -- ValueCompletion CompletionItemKind = 12 -- EnumCompletion CompletionItemKind = 13 -- KeywordCompletion CompletionItemKind = 14 -- SnippetCompletion CompletionItemKind = 15 -- ColorCompletion CompletionItemKind = 16 -- FileCompletion CompletionItemKind = 17 -- ReferenceCompletion CompletionItemKind = 18 -- FolderCompletion CompletionItemKind = 19 -- EnumMemberCompletion CompletionItemKind = 20 -- ConstantCompletion CompletionItemKind = 21 -- StructCompletion CompletionItemKind = 22 -- EventCompletion CompletionItemKind = 23 -- OperatorCompletion CompletionItemKind = 24 -- TypeParameterCompletion CompletionItemKind = 25 -- // Completion item tags are extra annotations that tweak the rendering of a completion -- // item. -- // -- // @since 3.15.0 -- // Render a completion as obsolete, usually using a strike-out. -- ComplDeprecated CompletionItemTag = 1 -- // How a completion was triggered -- // Completion was triggered by typing an identifier (24x7 code -- // complete), manual invocation (e.g Ctrl+Space) or via API. -- Invoked CompletionTriggerKind = 1 -- // Completion was triggered by a trigger character specified by -- // the `triggerCharacters` properties of the `CompletionRegistrationOptions`. -- TriggerCharacter CompletionTriggerKind = 2 -- // Completion was re-triggered as current completion list is incomplete -- TriggerForIncompleteCompletions CompletionTriggerKind = 3 -- // The diagnostic's severity. -- // Reports an error. -- SeverityError DiagnosticSeverity = 1 -- // Reports a warning. -- SeverityWarning DiagnosticSeverity = 2 -- // Reports an information. -- SeverityInformation DiagnosticSeverity = 3 -- // Reports a hint. -- SeverityHint DiagnosticSeverity = 4 -- // The diagnostic tags. -- // -- // @since 3.15.0 -- // Unused or unnecessary code. -- // -- // Clients are allowed to render diagnostics with this tag faded out instead of having -- // an error squiggle. -- Unnecessary DiagnosticTag = 1 -- // Deprecated or obsolete code. -- // -- // Clients are allowed to rendered diagnostics with this tag strike through. -- Deprecated DiagnosticTag = 2 -- // The document diagnostic report kinds. -- // -- // @since 3.17.0 -- // A diagnostic report with a full -- // set of problems. -- DiagnosticFull DocumentDiagnosticReportKind = "full" -- // A report indicating that the last -- // returned report is still accurate. -- DiagnosticUnchanged DocumentDiagnosticReportKind = "unchanged" -- // A document highlight kind. -- // A textual occurrence. -- Text DocumentHighlightKind = 1 -- // Read-access of a symbol, like reading a variable. -- Read DocumentHighlightKind = 2 -- // Write-access of a symbol, like writing to a variable. -- Write DocumentHighlightKind = 3 -- // Predefined error codes. -- ParseError ErrorCodes = -32700 -- InvalidRequest ErrorCodes = -32600 -- MethodNotFound ErrorCodes = -32601 -- InvalidParams ErrorCodes = -32602 -- InternalError ErrorCodes = -32603 -- // Error code indicating that a server received a notification or -- // request before the server has received the `initialize` request. -- ServerNotInitialized ErrorCodes = -32002 -- UnknownErrorCode ErrorCodes = -32001 -- // Applying the workspace change is simply aborted if one of the changes provided -- // fails. All operations executed before the failing operation stay executed. -- Abort FailureHandlingKind = "abort" -- // All operations are executed transactional. That means they either all -- // succeed or no changes at all are applied to the workspace. -- Transactional FailureHandlingKind = "transactional" -- // If the workspace edit contains only textual file changes they are executed transactional. -- // If resource changes (create, rename or delete file) are part of the change the failure -- // handling strategy is abort. -- TextOnlyTransactional FailureHandlingKind = "textOnlyTransactional" -- // The client tries to undo the operations already executed. But there is no -- // guarantee that this is succeeding. -- Undo FailureHandlingKind = "undo" -- // The file event type -- // The file got created. -- Created FileChangeType = 1 -- // The file got changed. -- Changed FileChangeType = 2 -- // The file got deleted. -- Deleted FileChangeType = 3 -- // A pattern kind describing if a glob pattern matches a file a folder or -- // both. -- // -- // @since 3.16.0 -- // The pattern matches a file only. -- FilePattern FileOperationPatternKind = "file" -- // The pattern matches a folder only. -- FolderPattern FileOperationPatternKind = "folder" -- // A set of predefined range kinds. -- // Folding range for a comment -- Comment FoldingRangeKind = "comment" -- // Folding range for an import or include -- Imports FoldingRangeKind = "imports" -- // Folding range for a region (e.g. `#region`) -- Region FoldingRangeKind = "region" -- // Inlay hint kinds. -- // -- // @since 3.17.0 -- // An inlay hint that for a type annotation. -- Type InlayHintKind = 1 -- // An inlay hint that is for a parameter. -- Parameter InlayHintKind = 2 -- // Describes how an {@link InlineCompletionItemProvider inline completion provider} was triggered. -- // -- // @since 3.18.0 -- // @proposed -- // Completion was triggered explicitly by a user gesture. -- InlineInvoked InlineCompletionTriggerKind = 0 -- // Completion was triggered automatically while editing. -- InlineAutomatic InlineCompletionTriggerKind = 1 -- // Defines whether the insert text in a completion item should be interpreted as -- // plain text or a snippet. -- // The primary text to be inserted is treated as a plain string. -- PlainTextTextFormat InsertTextFormat = 1 -- // The primary text to be inserted is treated as a snippet. -- // -- // A snippet can define tab stops and placeholders with `$1`, `$2` -- // and `${3:foo}`. `$0` defines the final tab stop, it defaults to -- // the end of the snippet. Placeholders with equal identifiers are linked, -- // that is typing in one will update others too. -- // -- // See also: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#snippet_syntax -- SnippetTextFormat InsertTextFormat = 2 -- // How whitespace and indentation is handled during completion -- // item insertion. -- // -- // @since 3.16.0 -- // The insertion or replace strings is taken as it is. If the -- // value is multi line the lines below the cursor will be -- // inserted using the indentation defined in the string value. -- // The client will not apply any kind of adjustments to the -- // string. -- AsIs InsertTextMode = 1 -- // The editor adjusts leading whitespace of new lines so that -- // they match the indentation up to the cursor of the line for -- // which the item is accepted. -- // -- // Consider a line like this: <2tabs><3tabs>foo. Accepting a -- // multi line completion item is indented using 2 tabs and all -- // following lines inserted will be indented using 2 tabs as well. -- AdjustIndentation InsertTextMode = 2 -- // A request failed but it was syntactically correct, e.g the -- // method name was known and the parameters were valid. The error -- // message should contain human readable information about why -- // the request failed. -- // -- // @since 3.17.0 -- RequestFailed LSPErrorCodes = -32803 -- // The server cancelled the request. This error code should -- // only be used for requests that explicitly support being -- // server cancellable. -- // -- // @since 3.17.0 -- ServerCancelled LSPErrorCodes = -32802 -- // The server detected that the content of a document got -- // modified outside normal conditions. A server should -- // NOT send this error code if it detects a content change -- // in it unprocessed messages. The result even computed -- // on an older state might still be useful for the client. -- // -- // If a client decides that a result is not of any use anymore -- // the client should cancel the request. -- ContentModified LSPErrorCodes = -32801 -- // The client has canceled a request and a server as detected -- // the cancel. -- RequestCancelled LSPErrorCodes = -32800 -- // Describes the content type that a client supports in various -- // result literals like `Hover`, `ParameterInfo` or `CompletionItem`. -- // -- // Please note that `MarkupKinds` must not start with a `$`. This kinds -- // are reserved for internal usage. -- // Plain text is supported as a content format -- PlainText MarkupKind = "plaintext" -- // Markdown is supported as a content format -- Markdown MarkupKind = "markdown" -- // The message type -- // An error message. -- Error MessageType = 1 -- // A warning message. -- Warning MessageType = 2 -- // An information message. -- Info MessageType = 3 -- // A log message. -- Log MessageType = 4 -- // The moniker kind. -- // -- // @since 3.16.0 -- // The moniker represent a symbol that is imported into a project -- Import MonikerKind = "import" -- // The moniker represents a symbol that is exported from a project -- Export MonikerKind = "export" -- // The moniker represents a symbol that is local to a project (e.g. a local -- // variable of a function, a class not visible outside the project, ...) -- Local MonikerKind = "local" -- // A notebook cell kind. -- // -- // @since 3.17.0 -- // A markup-cell is formatted source that is used for display. -- Markup NotebookCellKind = 1 -- // A code-cell is source code. -- Code NotebookCellKind = 2 -- // A set of predefined position encoding kinds. -- // -- // @since 3.17.0 -- // Character offsets count UTF-8 code units (e.g. bytes). -- UTF8 PositionEncodingKind = "utf-8" -- // Character offsets count UTF-16 code units. -- // -- // This is the default and must always be supported -- // by servers -- UTF16 PositionEncodingKind = "utf-16" -- // Character offsets count UTF-32 code units. -- // -- // Implementation note: these are the same as Unicode codepoints, -- // so this `PositionEncodingKind` may also be used for an -- // encoding-agnostic representation of character offsets. -- UTF32 PositionEncodingKind = "utf-32" -- // The client's default behavior is to select the identifier -- // according the to language's syntax rule. -- Identifier PrepareSupportDefaultBehavior = 1 -- // Supports creating new files and folders. -- Create ResourceOperationKind = "create" -- // Supports renaming existing files and folders. -- Rename ResourceOperationKind = "rename" -- // Supports deleting existing files and folders. -- Delete ResourceOperationKind = "delete" -- // A set of predefined token modifiers. This set is not fixed -- // an clients can specify additional token types via the -- // corresponding client capabilities. -- // -- // @since 3.16.0 -- ModDeclaration SemanticTokenModifiers = "declaration" -- ModDefinition SemanticTokenModifiers = "definition" -- ModReadonly SemanticTokenModifiers = "readonly" -- ModStatic SemanticTokenModifiers = "static" -- ModDeprecated SemanticTokenModifiers = "deprecated" -- ModAbstract SemanticTokenModifiers = "abstract" -- ModAsync SemanticTokenModifiers = "async" -- ModModification SemanticTokenModifiers = "modification" -- ModDocumentation SemanticTokenModifiers = "documentation" -- ModDefaultLibrary SemanticTokenModifiers = "defaultLibrary" -- // A set of predefined token types. This set is not fixed -- // an clients can specify additional token types via the -- // corresponding client capabilities. -- // -- // @since 3.16.0 -- NamespaceType SemanticTokenTypes = "namespace" -- // Represents a generic type. Acts as a fallback for types which can't be mapped to -- // a specific type like class or enum. -- TypeType SemanticTokenTypes = "type" -- ClassType SemanticTokenTypes = "class" -- EnumType SemanticTokenTypes = "enum" -- InterfaceType SemanticTokenTypes = "interface" -- StructType SemanticTokenTypes = "struct" -- TypeParameterType SemanticTokenTypes = "typeParameter" -- ParameterType SemanticTokenTypes = "parameter" -- VariableType SemanticTokenTypes = "variable" -- PropertyType SemanticTokenTypes = "property" -- EnumMemberType SemanticTokenTypes = "enumMember" -- EventType SemanticTokenTypes = "event" -- FunctionType SemanticTokenTypes = "function" -- MethodType SemanticTokenTypes = "method" -- MacroType SemanticTokenTypes = "macro" -- KeywordType SemanticTokenTypes = "keyword" -- ModifierType SemanticTokenTypes = "modifier" -- CommentType SemanticTokenTypes = "comment" -- StringType SemanticTokenTypes = "string" -- NumberType SemanticTokenTypes = "number" -- RegexpType SemanticTokenTypes = "regexp" -- OperatorType SemanticTokenTypes = "operator" -- // @since 3.17.0 -- DecoratorType SemanticTokenTypes = "decorator" -- // How a signature help was triggered. -- // -- // @since 3.15.0 -- // Signature help was invoked manually by the user or by a command. -- SigInvoked SignatureHelpTriggerKind = 1 -- // Signature help was triggered by a trigger character. -- SigTriggerCharacter SignatureHelpTriggerKind = 2 -- // Signature help was triggered by the cursor moving or by the document content changing. -- SigContentChange SignatureHelpTriggerKind = 3 -- // A symbol kind. -- File SymbolKind = 1 -- Module SymbolKind = 2 -- Namespace SymbolKind = 3 -- Package SymbolKind = 4 -- Class SymbolKind = 5 -- Method SymbolKind = 6 -- Property SymbolKind = 7 -- Field SymbolKind = 8 -- Constructor SymbolKind = 9 -- Enum SymbolKind = 10 -- Interface SymbolKind = 11 -- Function SymbolKind = 12 -- Variable SymbolKind = 13 -- Constant SymbolKind = 14 -- String SymbolKind = 15 -- Number SymbolKind = 16 -- Boolean SymbolKind = 17 -- Array SymbolKind = 18 -- Object SymbolKind = 19 -- Key SymbolKind = 20 -- Null SymbolKind = 21 -- EnumMember SymbolKind = 22 -- Struct SymbolKind = 23 -- Event SymbolKind = 24 -- Operator SymbolKind = 25 -- TypeParameter SymbolKind = 26 -- // Symbol tags are extra annotations that tweak the rendering of a symbol. -- // -- // @since 3.16 -- // Render a symbol as obsolete, usually using a strike-out. -- DeprecatedSymbol SymbolTag = 1 -- // Represents reasons why a text document is saved. -- // Manually triggered, e.g. by the user pressing save, by starting debugging, -- // or by an API call. -- Manual TextDocumentSaveReason = 1 -- // Automatic after a delay. -- AfterDelay TextDocumentSaveReason = 2 -- // When the editor lost focus. -- FocusOut TextDocumentSaveReason = 3 -- // Defines how the host (editor) should sync -- // document changes to the language server. -- // Documents should not be synced at all. -- None TextDocumentSyncKind = 0 -- // Documents are synced by always sending the full content -- // of the document. -- Full TextDocumentSyncKind = 1 -- // Documents are synced by sending the full content on open. -- // After that only incremental updates to the document are -- // send. -- Incremental TextDocumentSyncKind = 2 -- Relative TokenFormat = "relative" -- // Turn tracing off. -- Off TraceValues = "off" -- // Trace messages only. -- Messages TraceValues = "messages" -- // Verbose message tracing. -- Verbose TraceValues = "verbose" -- // Moniker uniqueness level to define scope of the moniker. -- // -- // @since 3.16.0 -- // The moniker is only unique inside a document -- Document UniquenessLevel = "document" -- // The moniker is unique inside a project for which a dump got created -- Project UniquenessLevel = "project" -- // The moniker is unique inside the group to which a project belongs -- Group UniquenessLevel = "group" -- // The moniker is unique inside the moniker scheme. -- Scheme UniquenessLevel = "scheme" -- // The moniker is globally unique -- Global UniquenessLevel = "global" -- // Interested in create events. -- WatchCreate WatchKind = 1 -- // Interested in change events -- WatchChange WatchKind = 2 -- // Interested in delete events -- WatchDelete WatchKind = 4 --) -diff -urN a/gopls/internal/lsp/protocol/tsserver.go b/gopls/internal/lsp/protocol/tsserver.go ---- a/gopls/internal/lsp/protocol/tsserver.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/protocol/tsserver.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,1196 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --// Code generated for LSP. DO NOT EDIT. -- --package protocol -- --// Code generated from protocol/metaModel.json at ref release/protocol/3.17.4-next.2 (hash 184c8a7f010d335582f24337fe182baa6f2fccdd). --// https://github.com/microsoft/vscode-languageserver-node/blob/release/protocol/3.17.4-next.2/protocol/metaModel.json --// LSP metaData.version = 3.17.0. -- --import ( -- "context" -- "encoding/json" -- -- "golang.org/x/tools/internal/jsonrpc2" --) -- --type Server interface { -- Progress(context.Context, *ProgressParams) error // $/progress -- SetTrace(context.Context, *SetTraceParams) error // $/setTrace -- IncomingCalls(context.Context, *CallHierarchyIncomingCallsParams) ([]CallHierarchyIncomingCall, error) // callHierarchy/incomingCalls -- OutgoingCalls(context.Context, *CallHierarchyOutgoingCallsParams) ([]CallHierarchyOutgoingCall, error) // callHierarchy/outgoingCalls -- ResolveCodeAction(context.Context, *CodeAction) (*CodeAction, error) // codeAction/resolve -- ResolveCodeLens(context.Context, *CodeLens) (*CodeLens, error) // codeLens/resolve -- ResolveCompletionItem(context.Context, *CompletionItem) (*CompletionItem, error) // completionItem/resolve -- ResolveDocumentLink(context.Context, *DocumentLink) (*DocumentLink, error) // documentLink/resolve -- Exit(context.Context) error // exit -- Initialize(context.Context, *ParamInitialize) (*InitializeResult, error) // initialize -- Initialized(context.Context, *InitializedParams) error // initialized -- Resolve(context.Context, *InlayHint) (*InlayHint, error) // inlayHint/resolve -- DidChangeNotebookDocument(context.Context, *DidChangeNotebookDocumentParams) error // notebookDocument/didChange -- DidCloseNotebookDocument(context.Context, *DidCloseNotebookDocumentParams) error // notebookDocument/didClose -- DidOpenNotebookDocument(context.Context, *DidOpenNotebookDocumentParams) error // notebookDocument/didOpen -- DidSaveNotebookDocument(context.Context, *DidSaveNotebookDocumentParams) error // notebookDocument/didSave -- Shutdown(context.Context) error // shutdown -- CodeAction(context.Context, *CodeActionParams) ([]CodeAction, error) // textDocument/codeAction -- CodeLens(context.Context, *CodeLensParams) ([]CodeLens, error) // textDocument/codeLens -- ColorPresentation(context.Context, *ColorPresentationParams) ([]ColorPresentation, error) // textDocument/colorPresentation -- Completion(context.Context, *CompletionParams) (*CompletionList, error) // textDocument/completion -- Declaration(context.Context, *DeclarationParams) (*Or_textDocument_declaration, error) // textDocument/declaration -- Definition(context.Context, *DefinitionParams) ([]Location, error) // textDocument/definition -- Diagnostic(context.Context, *string) (*string, error) // textDocument/diagnostic -- DidChange(context.Context, *DidChangeTextDocumentParams) error // textDocument/didChange -- DidClose(context.Context, *DidCloseTextDocumentParams) error // textDocument/didClose -- DidOpen(context.Context, *DidOpenTextDocumentParams) error // textDocument/didOpen -- DidSave(context.Context, *DidSaveTextDocumentParams) error // textDocument/didSave -- DocumentColor(context.Context, *DocumentColorParams) ([]ColorInformation, error) // textDocument/documentColor -- DocumentHighlight(context.Context, *DocumentHighlightParams) ([]DocumentHighlight, error) // textDocument/documentHighlight -- DocumentLink(context.Context, *DocumentLinkParams) ([]DocumentLink, error) // textDocument/documentLink -- DocumentSymbol(context.Context, *DocumentSymbolParams) ([]interface{}, error) // textDocument/documentSymbol -- FoldingRange(context.Context, *FoldingRangeParams) ([]FoldingRange, error) // textDocument/foldingRange -- Formatting(context.Context, *DocumentFormattingParams) ([]TextEdit, error) // textDocument/formatting -- Hover(context.Context, *HoverParams) (*Hover, error) // textDocument/hover -- Implementation(context.Context, *ImplementationParams) ([]Location, error) // textDocument/implementation -- InlayHint(context.Context, *InlayHintParams) ([]InlayHint, error) // textDocument/inlayHint -- InlineCompletion(context.Context, *InlineCompletionParams) (*Or_Result_textDocument_inlineCompletion, error) // textDocument/inlineCompletion -- InlineValue(context.Context, *InlineValueParams) ([]InlineValue, error) // textDocument/inlineValue -- LinkedEditingRange(context.Context, *LinkedEditingRangeParams) (*LinkedEditingRanges, error) // textDocument/linkedEditingRange -- Moniker(context.Context, *MonikerParams) ([]Moniker, error) // textDocument/moniker -- OnTypeFormatting(context.Context, *DocumentOnTypeFormattingParams) ([]TextEdit, error) // textDocument/onTypeFormatting -- PrepareCallHierarchy(context.Context, *CallHierarchyPrepareParams) ([]CallHierarchyItem, error) // textDocument/prepareCallHierarchy -- PrepareRename(context.Context, *PrepareRenameParams) (*PrepareRename2Gn, error) // textDocument/prepareRename -- PrepareTypeHierarchy(context.Context, *TypeHierarchyPrepareParams) ([]TypeHierarchyItem, error) // textDocument/prepareTypeHierarchy -- RangeFormatting(context.Context, *DocumentRangeFormattingParams) ([]TextEdit, error) // textDocument/rangeFormatting -- RangesFormatting(context.Context, *DocumentRangesFormattingParams) ([]TextEdit, error) // textDocument/rangesFormatting -- References(context.Context, *ReferenceParams) ([]Location, error) // textDocument/references -- Rename(context.Context, *RenameParams) (*WorkspaceEdit, error) // textDocument/rename -- SelectionRange(context.Context, *SelectionRangeParams) ([]SelectionRange, error) // textDocument/selectionRange -- SemanticTokensFull(context.Context, *SemanticTokensParams) (*SemanticTokens, error) // textDocument/semanticTokens/full -- SemanticTokensFullDelta(context.Context, *SemanticTokensDeltaParams) (interface{}, error) // textDocument/semanticTokens/full/delta -- SemanticTokensRange(context.Context, *SemanticTokensRangeParams) (*SemanticTokens, error) // textDocument/semanticTokens/range -- SignatureHelp(context.Context, *SignatureHelpParams) (*SignatureHelp, error) // textDocument/signatureHelp -- TypeDefinition(context.Context, *TypeDefinitionParams) ([]Location, error) // textDocument/typeDefinition -- WillSave(context.Context, *WillSaveTextDocumentParams) error // textDocument/willSave -- WillSaveWaitUntil(context.Context, *WillSaveTextDocumentParams) ([]TextEdit, error) // textDocument/willSaveWaitUntil -- Subtypes(context.Context, *TypeHierarchySubtypesParams) ([]TypeHierarchyItem, error) // typeHierarchy/subtypes -- Supertypes(context.Context, *TypeHierarchySupertypesParams) ([]TypeHierarchyItem, error) // typeHierarchy/supertypes -- WorkDoneProgressCancel(context.Context, *WorkDoneProgressCancelParams) error // window/workDoneProgress/cancel -- DiagnosticWorkspace(context.Context, *WorkspaceDiagnosticParams) (*WorkspaceDiagnosticReport, error) // workspace/diagnostic -- DidChangeConfiguration(context.Context, *DidChangeConfigurationParams) error // workspace/didChangeConfiguration -- DidChangeWatchedFiles(context.Context, *DidChangeWatchedFilesParams) error // workspace/didChangeWatchedFiles -- DidChangeWorkspaceFolders(context.Context, *DidChangeWorkspaceFoldersParams) error // workspace/didChangeWorkspaceFolders -- DidCreateFiles(context.Context, *CreateFilesParams) error // workspace/didCreateFiles -- DidDeleteFiles(context.Context, *DeleteFilesParams) error // workspace/didDeleteFiles -- DidRenameFiles(context.Context, *RenameFilesParams) error // workspace/didRenameFiles -- ExecuteCommand(context.Context, *ExecuteCommandParams) (interface{}, error) // workspace/executeCommand -- Symbol(context.Context, *WorkspaceSymbolParams) ([]SymbolInformation, error) // workspace/symbol -- WillCreateFiles(context.Context, *CreateFilesParams) (*WorkspaceEdit, error) // workspace/willCreateFiles -- WillDeleteFiles(context.Context, *DeleteFilesParams) (*WorkspaceEdit, error) // workspace/willDeleteFiles -- WillRenameFiles(context.Context, *RenameFilesParams) (*WorkspaceEdit, error) // workspace/willRenameFiles -- ResolveWorkspaceSymbol(context.Context, *WorkspaceSymbol) (*WorkspaceSymbol, error) // workspaceSymbol/resolve -- NonstandardRequest(ctx context.Context, method string, params interface{}) (interface{}, error) --} -- --func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, r jsonrpc2.Request) (bool, error) { -- switch r.Method() { -- case "$/progress": -- var params ProgressParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- err := server.Progress(ctx, ¶ms) -- return true, reply(ctx, nil, err) -- case "$/setTrace": -- var params SetTraceParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- err := server.SetTrace(ctx, ¶ms) -- return true, reply(ctx, nil, err) -- case "callHierarchy/incomingCalls": -- var params CallHierarchyIncomingCallsParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- resp, err := server.IncomingCalls(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) -- } -- return true, reply(ctx, resp, nil) -- case "callHierarchy/outgoingCalls": -- var params CallHierarchyOutgoingCallsParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- resp, err := server.OutgoingCalls(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) -- } -- return true, reply(ctx, resp, nil) -- case "codeAction/resolve": -- var params CodeAction -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- resp, err := server.ResolveCodeAction(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) -- } -- return true, reply(ctx, resp, nil) -- case "codeLens/resolve": -- var params CodeLens -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- resp, err := server.ResolveCodeLens(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) -- } -- return true, reply(ctx, resp, nil) -- case "completionItem/resolve": -- var params CompletionItem -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- resp, err := server.ResolveCompletionItem(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) -- } -- return true, reply(ctx, resp, nil) -- case "documentLink/resolve": -- var params DocumentLink -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- resp, err := server.ResolveDocumentLink(ctx, ¶ms) +-// renamePackageClause computes edits renaming the package clause of files in +-// the package described by the given metadata, to newName. +-// +-// Edits are written into the edits map. +-func renamePackageClause(ctx context.Context, mp *metadata.Package, snapshot *cache.Snapshot, newName PackageName, edits map[protocol.DocumentURI][]diff.Edit) error { +- // Rename internal references to the package in the renaming package. +- for _, uri := range mp.CompiledGoFiles { +- fh, err := snapshot.ReadFile(ctx, uri) - if err != nil { -- return true, reply(ctx, nil, err) -- } -- return true, reply(ctx, resp, nil) -- case "exit": -- err := server.Exit(ctx) -- return true, reply(ctx, nil, err) -- case "initialize": -- var params ParamInitialize -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- return err - } -- resp, err := server.Initialize(ctx, ¶ms) +- f, err := snapshot.ParseGo(ctx, fh, parsego.Header) - if err != nil { -- return true, reply(ctx, nil, err) -- } -- return true, reply(ctx, resp, nil) -- case "initialized": -- var params InitializedParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- return err - } -- err := server.Initialized(ctx, ¶ms) -- return true, reply(ctx, nil, err) -- case "inlayHint/resolve": -- var params InlayHint -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- if f.File.Name == nil { +- continue // no package declaration - } -- resp, err := server.Resolve(ctx, ¶ms) +- +- edit, err := posEdit(f.Tok, f.File.Name.Pos(), f.File.Name.End(), string(newName)) - if err != nil { -- return true, reply(ctx, nil, err) +- return err - } -- return true, reply(ctx, resp, nil) -- case "notebookDocument/didChange": -- var params DidChangeNotebookDocumentParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- edits[f.URI] = append(edits[f.URI], edit) +- } +- +- return nil +-} +- +-// renameImports computes the set of edits to imports resulting from renaming +-// the package described by the given metadata, to a package with import path +-// newPath and name newName. +-// +-// Edits are written into the edits map. +-func renameImports(ctx context.Context, snapshot *cache.Snapshot, mp *metadata.Package, newPath ImportPath, newName PackageName, allEdits map[protocol.DocumentURI][]diff.Edit) error { +- rdeps, err := snapshot.ReverseDependencies(ctx, mp.ID, false) // find direct importers +- if err != nil { +- return err +- } +- +- // Pass 1: rename import paths in import declarations. +- needsTypeCheck := make(map[PackageID][]protocol.DocumentURI) +- for _, rdep := range rdeps { +- if rdep.IsIntermediateTestVariant() { +- continue // for renaming, these variants are redundant - } -- err := server.DidChangeNotebookDocument(ctx, ¶ms) -- return true, reply(ctx, nil, err) -- case "notebookDocument/didClose": -- var params DidCloseNotebookDocumentParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- +- for _, uri := range rdep.CompiledGoFiles { +- fh, err := snapshot.ReadFile(ctx, uri) +- if err != nil { +- return err +- } +- f, err := snapshot.ParseGo(ctx, fh, parsego.Header) +- if err != nil { +- return err +- } +- if f.File.Name == nil { +- continue // no package declaration +- } +- for _, imp := range f.File.Imports { +- if rdep.DepsByImpPath[metadata.UnquoteImportPath(imp)] != mp.ID { +- continue // not the import we're looking for +- } +- +- // If the import does not explicitly specify +- // a local name, then we need to invoke the +- // type checker to locate references to update. +- // +- // TODO(adonovan): is this actually true? +- // Renaming an import with a local name can still +- // cause conflicts: shadowing of built-ins, or of +- // package-level decls in the same or another file. +- if imp.Name == nil { +- needsTypeCheck[rdep.ID] = append(needsTypeCheck[rdep.ID], uri) +- } +- +- // Create text edit for the import path (string literal). +- edit, err := posEdit(f.Tok, imp.Path.Pos(), imp.Path.End(), strconv.Quote(string(newPath))) +- if err != nil { +- return err +- } +- allEdits[uri] = append(allEdits[uri], edit) +- } - } -- err := server.DidCloseNotebookDocument(ctx, ¶ms) -- return true, reply(ctx, nil, err) -- case "notebookDocument/didOpen": -- var params DidOpenNotebookDocumentParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- err := server.DidOpenNotebookDocument(ctx, ¶ms) -- return true, reply(ctx, nil, err) -- case "notebookDocument/didSave": -- var params DidSaveNotebookDocumentParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- err := server.DidSaveNotebookDocument(ctx, ¶ms) -- return true, reply(ctx, nil, err) -- case "shutdown": -- err := server.Shutdown(ctx) -- return true, reply(ctx, nil, err) -- case "textDocument/codeAction": -- var params CodeActionParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- resp, err := server.CodeAction(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) -- } -- return true, reply(ctx, resp, nil) -- case "textDocument/codeLens": -- var params CodeLensParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- } +- +- // If the imported package's name hasn't changed, +- // we don't need to rename references within each file. +- if newName == mp.Name { +- return nil +- } +- +- // Pass 2: rename local name (types.PkgName) of imported +- // package throughout one or more files of the package. +- ids := make([]PackageID, 0, len(needsTypeCheck)) +- for id := range needsTypeCheck { +- ids = append(ids, id) +- } +- pkgs, err := snapshot.TypeCheck(ctx, ids...) +- if err != nil { +- return err +- } +- for i, id := range ids { +- pkg := pkgs[i] +- for _, uri := range needsTypeCheck[id] { +- f, err := pkg.File(uri) +- if err != nil { +- return err +- } +- for _, imp := range f.File.Imports { +- if imp.Name != nil { +- continue // has explicit local name +- } +- if rdeps[id].DepsByImpPath[metadata.UnquoteImportPath(imp)] != mp.ID { +- continue // not the import we're looking for +- } +- +- pkgname := pkg.TypesInfo().Implicits[imp].(*types.PkgName) +- +- pkgScope := pkg.Types().Scope() +- fileScope := pkg.TypesInfo().Scopes[f.File] +- +- localName := string(newName) +- try := 0 +- +- // Keep trying with fresh names until one succeeds. +- // +- // TODO(adonovan): fix: this loop is not sufficient to choose a name +- // that is guaranteed to be conflict-free; renameObj may still fail. +- // So the retry loop should be around renameObj, and we shouldn't +- // bother with scopes here. +- for fileScope.Lookup(localName) != nil || pkgScope.Lookup(localName) != nil { +- try++ +- localName = fmt.Sprintf("%s%d", newName, try) +- } +- +- // renameObj detects various conflicts, including: +- // - new name conflicts with a package-level decl in this file; +- // - new name hides a package-level decl in another file that +- // is actually referenced in this file; +- // - new name hides a built-in that is actually referenced +- // in this file; +- // - a reference in this file to the old package name would +- // become shadowed by an intervening declaration that +- // uses the new name. +- // It returns the edits if no conflict was detected. +- editMap, _, err := renameObjects(localName, pkg, pkgname) +- if err != nil { +- return err +- } +- +- // If the chosen local package name matches the package's +- // new name, delete the change that would have inserted +- // an explicit local name, which is always the lexically +- // first change. +- if localName == string(newName) { +- edits, ok := editMap[uri] +- if !ok { +- return fmt.Errorf("internal error: no changes for %s", uri) +- } +- diff.SortEdits(edits) +- editMap[uri] = edits[1:] +- } +- for uri, edits := range editMap { +- allEdits[uri] = append(allEdits[uri], edits...) +- } +- } - } -- resp, err := server.CodeLens(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- } +- return nil +-} +- +-// renameObjects computes the edits to the type-checked syntax package pkg +-// required to rename a set of target objects to newName. +-// +-// It also returns the set of objects that were found (due to +-// corresponding methods and embedded fields) to require renaming as a +-// consequence of the requested renamings. +-// +-// It returns an error if the renaming would cause a conflict. +-func renameObjects(newName string, pkg *cache.Package, targets ...types.Object) (map[protocol.DocumentURI][]diff.Edit, map[types.Object]bool, error) { +- r := renamer{ +- pkg: pkg, +- objsToUpdate: make(map[types.Object]bool), +- from: targets[0].Name(), +- to: newName, +- } +- +- // A renaming initiated at an interface method indicates the +- // intention to rename abstract and concrete methods as needed +- // to preserve assignability. +- // TODO(adonovan): pull this into the caller. +- for _, obj := range targets { +- if obj, ok := obj.(*types.Func); ok { +- recv := obj.Type().(*types.Signature).Recv() +- if recv != nil && types.IsInterface(recv.Type().Underlying()) { +- r.changeMethods = true +- break +- } - } -- return true, reply(ctx, resp, nil) -- case "textDocument/colorPresentation": -- var params ColorPresentationParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- } +- +- // Check that the renaming of the identifier is ok. +- for _, obj := range targets { +- r.check(obj) +- if len(r.conflicts) > 0 { +- // Stop at first error. +- return nil, nil, fmt.Errorf("%s", strings.Join(r.conflicts, "\n")) - } -- resp, err := server.ColorPresentation(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- } +- +- editMap, err := r.update() +- if err != nil { +- return nil, nil, err +- } +- +- // Remove initial targets so that only 'consequences' remain. +- for _, obj := range targets { +- delete(r.objsToUpdate, obj) +- } +- return editMap, r.objsToUpdate, nil +-} +- +-// Rename all references to the target objects. +-func (r *renamer) update() (map[protocol.DocumentURI][]diff.Edit, error) { +- result := make(map[protocol.DocumentURI][]diff.Edit) +- +- // shouldUpdate reports whether obj is one of (or an +- // instantiation of one of) the target objects. +- shouldUpdate := func(obj types.Object) bool { +- return containsOrigin(r.objsToUpdate, obj) +- } +- +- // Find all identifiers in the package that define or use a +- // renamed object. We iterate over info as it is more efficient +- // than calling ast.Inspect for each of r.pkg.CompiledGoFiles(). +- type item struct { +- node ast.Node // Ident, ImportSpec (obj=PkgName), or CaseClause (obj=Var) +- obj types.Object +- isDef bool +- } +- var items []item +- info := r.pkg.TypesInfo() +- for id, obj := range info.Uses { +- if shouldUpdate(obj) { +- items = append(items, item{id, obj, false}) - } -- return true, reply(ctx, resp, nil) -- case "textDocument/completion": -- var params CompletionParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- } +- for id, obj := range info.Defs { +- if shouldUpdate(obj) { +- items = append(items, item{id, obj, true}) - } -- resp, err := server.Completion(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- } +- for node, obj := range info.Implicits { +- if shouldUpdate(obj) { +- switch node.(type) { +- case *ast.ImportSpec, *ast.CaseClause: +- items = append(items, item{node, obj, true}) +- } - } -- return true, reply(ctx, resp, nil) -- case "textDocument/declaration": -- var params DeclarationParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- } +- sort.Slice(items, func(i, j int) bool { +- return items[i].node.Pos() < items[j].node.Pos() +- }) +- +- // Update each identifier, and its doc comment if it is a declaration. +- for _, item := range items { +- pgf, ok := enclosingFile(r.pkg, item.node.Pos()) +- if !ok { +- bug.Reportf("edit does not belong to syntax of package %q", r.pkg) +- continue - } -- resp, err := server.Declaration(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- +- // Renaming a types.PkgName may result in the addition or removal of an identifier, +- // so we deal with this separately. +- if pkgName, ok := item.obj.(*types.PkgName); ok && item.isDef { +- edit, err := r.updatePkgName(pgf, pkgName) +- if err != nil { +- return nil, err +- } +- result[pgf.URI] = append(result[pgf.URI], edit) +- continue - } -- return true, reply(ctx, resp, nil) -- case "textDocument/definition": -- var params DefinitionParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- +- // Workaround the unfortunate lack of a Var object +- // for x in "switch x := expr.(type) {}" by adjusting +- // the case clause to the switch ident. +- // This may result in duplicate edits, but we de-dup later. +- if _, ok := item.node.(*ast.CaseClause); ok { +- path, _ := astutil.PathEnclosingInterval(pgf.File, item.obj.Pos(), item.obj.Pos()) +- item.node = path[0].(*ast.Ident) - } -- resp, err := server.Definition(ctx, ¶ms) +- +- // Replace the identifier with r.to. +- edit, err := posEdit(pgf.Tok, item.node.Pos(), item.node.End(), r.to) - if err != nil { -- return true, reply(ctx, nil, err) +- return nil, err - } -- return true, reply(ctx, resp, nil) -- case "textDocument/diagnostic": -- var params string -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- +- result[pgf.URI] = append(result[pgf.URI], edit) +- +- if !item.isDef { // uses do not have doc comments to update. +- continue - } -- resp, err := server.Diagnostic(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- +- doc := docComment(pgf, item.node.(*ast.Ident)) +- if doc == nil { +- continue - } -- return true, reply(ctx, resp, nil) -- case "textDocument/didChange": -- var params DidChangeTextDocumentParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- +- // Perform the rename in doc comments declared in the original package. +- // go/parser strips out \r\n returns from the comment text, so go +- // line-by-line through the comment text to get the correct positions. +- docRegexp := regexp.MustCompile(`\b` + r.from + `\b`) // valid identifier => valid regexp +- for _, comment := range doc.List { +- if isDirective(comment.Text) { +- continue +- } +- // TODO(adonovan): why are we looping over lines? +- // Just run the loop body once over the entire multiline comment. +- lines := strings.Split(comment.Text, "\n") +- tokFile := pgf.Tok +- commentLine := safetoken.Line(tokFile, comment.Pos()) +- uri := protocol.URIFromPath(tokFile.Name()) +- for i, line := range lines { +- lineStart := comment.Pos() +- if i > 0 { +- lineStart = tokFile.LineStart(commentLine + i) +- } +- for _, locs := range docRegexp.FindAllIndex([]byte(line), -1) { +- edit, err := posEdit(tokFile, lineStart+token.Pos(locs[0]), lineStart+token.Pos(locs[1]), r.to) +- if err != nil { +- return nil, err // can't happen +- } +- result[uri] = append(result[uri], edit) +- } +- } - } -- err := server.DidChange(ctx, ¶ms) -- return true, reply(ctx, nil, err) -- case "textDocument/didClose": -- var params DidCloseTextDocumentParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- } +- +- docLinkEdits, err := r.updateCommentDocLinks() +- if err != nil { +- return nil, err +- } +- for uri, edits := range docLinkEdits { +- result[uri] = append(result[uri], edits...) +- } +- +- return result, nil +-} +- +-// updateCommentDocLinks updates each doc comment in the package +-// that refers to one of the renamed objects using a doc link +-// (https://golang.org/doc/comment#doclinks) such as "[pkg.Type.Method]". +-func (r *renamer) updateCommentDocLinks() (map[protocol.DocumentURI][]diff.Edit, error) { +- result := make(map[protocol.DocumentURI][]diff.Edit) +- var docRenamers []*docLinkRenamer +- for obj := range r.objsToUpdate { +- if _, ok := obj.(*types.PkgName); ok { +- // The dot package name will not be referenced +- if obj.Name() == "." { +- continue +- } +- +- docRenamers = append(docRenamers, &docLinkRenamer{ +- isDep: false, +- isPkgOrType: true, +- file: r.pkg.FileSet().File(obj.Pos()), +- regexp: docLinkPattern("", "", obj.Name(), true), +- to: r.to, +- }) +- continue - } -- err := server.DidClose(ctx, ¶ms) -- return true, reply(ctx, nil, err) -- case "textDocument/didOpen": -- var params DidOpenTextDocumentParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- if !obj.Exported() { +- continue - } -- err := server.DidOpen(ctx, ¶ms) -- return true, reply(ctx, nil, err) -- case "textDocument/didSave": -- var params DidSaveTextDocumentParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- recvName := "" +- // Doc links can reference only exported package-level objects +- // and methods of exported package-level named types. +- if !isPackageLevel(obj) { +- obj, isFunc := obj.(*types.Func) +- if !isFunc { +- continue +- } +- recv := obj.Type().(*types.Signature).Recv() +- if recv == nil { +- continue +- } +- _, named := typesinternal.ReceiverNamed(recv) +- if named == nil { +- continue +- } +- // Doc links can't reference interface methods. +- if types.IsInterface(named.Underlying()) { +- continue +- } +- name := named.Origin().Obj() +- if !name.Exported() || !isPackageLevel(name) { +- continue +- } +- recvName = name.Name() - } -- err := server.DidSave(ctx, ¶ms) -- return true, reply(ctx, nil, err) -- case "textDocument/documentColor": -- var params DocumentColorParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- +- // Qualify objects from other packages. +- pkgName := "" +- if r.pkg.Types() != obj.Pkg() { +- pkgName = obj.Pkg().Name() - } -- resp, err := server.DocumentColor(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- _, isTypeName := obj.(*types.TypeName) +- docRenamers = append(docRenamers, &docLinkRenamer{ +- isDep: r.pkg.Types() != obj.Pkg(), +- isPkgOrType: isTypeName, +- packagePath: obj.Pkg().Path(), +- packageName: pkgName, +- recvName: recvName, +- objName: obj.Name(), +- regexp: docLinkPattern(pkgName, recvName, obj.Name(), isTypeName), +- to: r.to, +- }) +- } +- for _, pgf := range r.pkg.CompiledGoFiles() { +- for _, d := range docRenamers { +- edits, err := d.update(pgf) +- if err != nil { +- return nil, err +- } +- if len(edits) > 0 { +- result[pgf.URI] = append(result[pgf.URI], edits...) +- } - } -- return true, reply(ctx, resp, nil) -- case "textDocument/documentHighlight": -- var params DocumentHighlightParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- } +- return result, nil +-} +- +-// docLinkPattern returns a regular expression that matches doclinks in comments. +-// It has one submatch that indicates the symbol to be updated. +-func docLinkPattern(pkgName, recvName, objName string, isPkgOrType bool) *regexp.Regexp { +- // The doc link may contain a leading star, e.g. [*bytes.Buffer]. +- pattern := `\[\*?` +- if pkgName != "" { +- pattern += pkgName + `\.` +- } +- if recvName != "" { +- pattern += recvName + `\.` +- } +- // The first submatch is object name. +- pattern += `(` + objName + `)` +- // If the object is a *types.TypeName or *types.PkgName, also need +- // match the objects referenced by them, so add `(\.\w+)*`. +- if isPkgOrType { +- pattern += `(?:\.\w+)*` +- } +- // There are two type of link in comments: +- // 1. url link. e.g. [text]: url +- // 2. doc link. e.g. [pkg.Name] +- // in order to only match the doc link, add `([^:]|$)` in the end. +- pattern += `\](?:[^:]|$)` +- +- return regexp.MustCompile(pattern) +-} +- +-// A docLinkRenamer renames doc links of forms such as these: +-// +-// [Func] +-// [pkg.Func] +-// [RecvType.Method] +-// [*Type] +-// [*pkg.Type] +-// [*pkg.RecvType.Method] +-type docLinkRenamer struct { +- isDep bool // object is from a dependency package +- isPkgOrType bool // object is *types.PkgName or *types.TypeName +- packagePath string +- packageName string // e.g. "pkg" +- recvName string // e.g. "RecvType" +- objName string // e.g. "Func", "Type", "Method" +- to string // new name +- regexp *regexp.Regexp +- +- file *token.File // enclosing file, if renaming *types.PkgName +-} +- +-// update updates doc links in the package level comments. +-func (r *docLinkRenamer) update(pgf *parsego.File) (result []diff.Edit, err error) { +- if r.file != nil && r.file != pgf.Tok { +- return nil, nil +- } +- pattern := r.regexp +- // If the object is in dependency package, +- // the imported name in the file may be different from the original package name +- if r.isDep { +- for _, spec := range pgf.File.Imports { +- importPath, _ := strconv.Unquote(spec.Path.Value) +- if importPath == r.packagePath { +- // Ignore blank imports +- if spec.Name == nil || spec.Name.Name == "_" || spec.Name.Name == "." { +- continue +- } +- if spec.Name.Name != r.packageName { +- pattern = docLinkPattern(spec.Name.Name, r.recvName, r.objName, r.isPkgOrType) +- } +- break +- } - } -- resp, err := server.DocumentHighlight(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- } +- +- var edits []diff.Edit +- updateDocLinks := func(doc *ast.CommentGroup) error { +- if doc != nil { +- for _, c := range doc.List { +- for _, locs := range pattern.FindAllStringSubmatchIndex(c.Text, -1) { +- // The first submatch is the object name, so the locs[2:4] is the index of object name. +- edit, err := posEdit(pgf.Tok, c.Pos()+token.Pos(locs[2]), c.Pos()+token.Pos(locs[3]), r.to) +- if err != nil { +- return err +- } +- edits = append(edits, edit) +- } +- } - } -- return true, reply(ctx, resp, nil) -- case "textDocument/documentLink": -- var params DocumentLinkParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- return nil +- } +- +- // Update package doc comments. +- err = updateDocLinks(pgf.File.Doc) +- if err != nil { +- return nil, err +- } +- for _, decl := range pgf.File.Decls { +- var doc *ast.CommentGroup +- switch decl := decl.(type) { +- case *ast.GenDecl: +- doc = decl.Doc +- case *ast.FuncDecl: +- doc = decl.Doc - } -- resp, err := server.DocumentLink(ctx, ¶ms) +- err = updateDocLinks(doc) - if err != nil { -- return true, reply(ctx, nil, err) -- } -- return true, reply(ctx, resp, nil) -- case "textDocument/documentSymbol": -- var params DocumentSymbolParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- return nil, err - } -- resp, err := server.DocumentSymbol(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- } +- return edits, nil +-} +- +-// docComment returns the doc for an identifier within the specified file. +-func docComment(pgf *parsego.File, id *ast.Ident) *ast.CommentGroup { +- nodes, _ := astutil.PathEnclosingInterval(pgf.File, id.Pos(), id.End()) +- for _, node := range nodes { +- switch decl := node.(type) { +- case *ast.FuncDecl: +- return decl.Doc +- case *ast.Field: +- return decl.Doc +- case *ast.GenDecl: +- return decl.Doc +- // For {Type,Value}Spec, if the doc on the spec is absent, +- // search for the enclosing GenDecl +- case *ast.TypeSpec: +- if decl.Doc != nil { +- return decl.Doc +- } +- case *ast.ValueSpec: +- if decl.Doc != nil { +- return decl.Doc +- } +- case *ast.Ident: +- case *ast.AssignStmt: +- // *ast.AssignStmt doesn't have an associated comment group. +- // So, we try to find a comment just before the identifier. +- +- // Try to find a comment group only for short variable declarations (:=). +- if decl.Tok != token.DEFINE { +- return nil +- } +- +- identLine := safetoken.Line(pgf.Tok, id.Pos()) +- for _, comment := range nodes[len(nodes)-1].(*ast.File).Comments { +- if comment.Pos() > id.Pos() { +- // Comment is after the identifier. +- continue +- } +- +- lastCommentLine := safetoken.Line(pgf.Tok, comment.End()) +- if lastCommentLine+1 == identLine { +- return comment +- } +- } +- default: +- return nil - } -- return true, reply(ctx, resp, nil) -- case "textDocument/foldingRange": -- var params FoldingRangeParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- } +- return nil +-} +- +-// updatePkgName returns the updates to rename a pkgName in the import spec by +-// only modifying the package name portion of the import declaration. +-func (r *renamer) updatePkgName(pgf *parsego.File, pkgName *types.PkgName) (diff.Edit, error) { +- // Modify ImportSpec syntax to add or remove the Name as needed. +- path, _ := astutil.PathEnclosingInterval(pgf.File, pkgName.Pos(), pkgName.Pos()) +- if len(path) < 2 { +- return diff.Edit{}, fmt.Errorf("no path enclosing interval for %s", pkgName.Name()) +- } +- spec, ok := path[1].(*ast.ImportSpec) +- if !ok { +- return diff.Edit{}, fmt.Errorf("failed to update PkgName for %s", pkgName.Name()) +- } +- +- newText := "" +- if pkgName.Imported().Name() != r.to { +- newText = r.to + " " +- } +- +- // Replace the portion (possibly empty) of the spec before the path: +- // local "path" or "path" +- // -> <- -><- +- return posEdit(pgf.Tok, spec.Pos(), spec.Path.Pos(), newText) +-} +- +-// parsePackageNameDecl is a convenience function that parses and +-// returns the package name declaration of file fh, and reports +-// whether the position ppos lies within it. +-// +-// Note: also used by references. +-func parsePackageNameDecl(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, ppos protocol.Position) (*parsego.File, bool, error) { +- pgf, err := snapshot.ParseGo(ctx, fh, parsego.Header) +- if err != nil { +- return nil, false, err +- } +- // Careful: because we used parsego.Header, +- // pgf.Pos(ppos) may be beyond EOF => (0, err). +- pos, _ := pgf.PositionPos(ppos) +- return pgf, pgf.File.Name.Pos() <= pos && pos <= pgf.File.Name.End(), nil +-} +- +-// enclosingFile returns the CompiledGoFile of pkg that contains the specified position. +-func enclosingFile(pkg *cache.Package, pos token.Pos) (*parsego.File, bool) { +- for _, pgf := range pkg.CompiledGoFiles() { +- if pgf.File.Pos() <= pos && pos <= pgf.File.End() { +- return pgf, true - } -- resp, err := server.FoldingRange(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) -- } -- return true, reply(ctx, resp, nil) -- case "textDocument/formatting": -- var params DocumentFormattingParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- resp, err := server.Formatting(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) -- } -- return true, reply(ctx, resp, nil) -- case "textDocument/hover": -- var params HoverParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- resp, err := server.Hover(ctx, ¶ms) +- } +- return nil, false +-} +- +-// posEdit returns an edit to replace the (start, end) range of tf with 'new'. +-func posEdit(tf *token.File, start, end token.Pos, new string) (diff.Edit, error) { +- startOffset, endOffset, err := safetoken.Offsets(tf, start, end) +- if err != nil { +- return diff.Edit{}, err +- } +- return diff.Edit{Start: startOffset, End: endOffset, New: new}, nil +-} +diff -urN a/gopls/internal/golang/semtok.go b/gopls/internal/golang/semtok.go +--- a/gopls/internal/golang/semtok.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/semtok.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,956 +0,0 @@ +-// Copyright 2024 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package golang +- +-// This file defines the Semantic Tokens operation for Go source. +- +-import ( +- "bytes" +- "context" +- "errors" +- "fmt" +- "go/ast" +- "go/token" +- "go/types" +- "log" +- "path/filepath" +- "regexp" +- "strings" +- "time" +- +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/semtok" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/gopls/internal/util/typesutil" +- "golang.org/x/tools/internal/aliases" +- "golang.org/x/tools/internal/event" +-) +- +-// semDebug enables comprehensive logging of decisions +-// (gopls semtok foo.go > /dev/null shows log output). +-// It should never be true in checked-in code. +-const semDebug = false +- +-func SemanticTokens(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, rng *protocol.Range) (*protocol.SemanticTokens, error) { +- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) +- if err != nil { +- return nil, err +- } +- +- // Select range. +- var start, end token.Pos +- if rng != nil { +- var err error +- start, end, err = pgf.RangePos(*rng) - if err != nil { -- return true, reply(ctx, nil, err) -- } -- return true, reply(ctx, resp, nil) -- case "textDocument/implementation": -- var params ImplementationParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- return nil, err // e.g. invalid range - } -- resp, err := server.Implementation(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- } else { +- tok := pgf.Tok +- start, end = tok.Pos(0), tok.Pos(tok.Size()) // entire file +- } +- +- // Reject full semantic token requests for large files. +- // +- // The LSP says that errors for the semantic token requests +- // should only be returned for exceptions (a word not +- // otherwise defined). This code treats a too-large file as an +- // exception. On parse errors, the code does what it can. +- const maxFullFileSize = 100000 +- if int(end-start) > maxFullFileSize { +- return nil, fmt.Errorf("semantic tokens: range %s too large (%d > %d)", +- fh.URI().Path(), end-start, maxFullFileSize) +- } +- +- tv := tokenVisitor{ +- ctx: ctx, +- metadataSource: snapshot, +- metadata: pkg.Metadata(), +- info: pkg.TypesInfo(), +- fset: pkg.FileSet(), +- pkg: pkg, +- pgf: pgf, +- start: start, +- end: end, +- } +- tv.visit() +- return &protocol.SemanticTokens{ +- Data: semtok.Encode( +- tv.tokens, +- snapshot.Options().NoSemanticString, +- snapshot.Options().NoSemanticNumber, +- snapshot.Options().SemanticTypes, +- snapshot.Options().SemanticMods), +- ResultID: time.Now().String(), // for delta requests, but we've never seen any +- }, nil +-} +- +-type tokenVisitor struct { +- // inputs +- ctx context.Context // for event logging +- metadataSource metadata.Source // used to resolve imports +- metadata *metadata.Package +- info *types.Info +- fset *token.FileSet +- pkg *cache.Package +- pgf *parsego.File +- start, end token.Pos // range of interest +- +- // working state +- stack []ast.Node // path from root of the syntax tree +- tokens []semtok.Token // computed sequence of semantic tokens +-} +- +-func (tv *tokenVisitor) visit() { +- f := tv.pgf.File +- // may not be in range, but harmless +- tv.token(f.Package, len("package"), semtok.TokKeyword, nil) +- if f.Name != nil { +- tv.token(f.Name.NamePos, len(f.Name.Name), semtok.TokNamespace, nil) +- } +- for _, decl := range f.Decls { +- // Only look at the decls that overlap the range. +- if decl.End() <= tv.start || decl.Pos() >= tv.end { +- continue - } -- return true, reply(ctx, resp, nil) -- case "textDocument/inlayHint": -- var params InlayHintParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- ast.Inspect(decl, tv.inspect) +- } +- +- // Scan all files for imported pkgs, ignore the ambiguous pkg. +- // This is to be consistent with the behavior in [go/doc]: https://pkg.go.dev/pkg/go/doc. +- importByName := make(map[string]*types.PkgName) +- for _, pgf := range tv.pkg.CompiledGoFiles() { +- for _, imp := range pgf.File.Imports { +- if obj, _ := typesutil.ImportedPkgName(tv.pkg.TypesInfo(), imp); obj != nil { +- if old, ok := importByName[obj.Name()]; ok { +- if old != nil && old.Imported() != obj.Imported() { +- importByName[obj.Name()] = nil // nil => ambiguous across files +- } +- continue +- } +- importByName[obj.Name()] = obj +- } - } -- resp, err := server.InlayHint(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- } +- +- for _, cg := range f.Comments { +- for _, c := range cg.List { +- tv.comment(c, importByName) - } -- return true, reply(ctx, resp, nil) -- case "textDocument/inlineCompletion": -- var params InlineCompletionParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- } +-} +- +-// Matches (for example) "[F]", "[*p.T]", "[p.T.M]" +-// unless followed by a colon (exclude url link, e.g. "[go]: https://go.dev"). +-// The first group is reference name. e.g. The first group of "[*p.T.M]" is "p.T.M". +-var docLinkRegex = regexp.MustCompile(`\[\*?([\pL_][\pL_0-9]*(\.[\pL_][\pL_0-9]*){0,2})](?:[^:]|$)`) +- +-// comment emits semantic tokens for a comment. +-// If the comment contains doc links or "go:" directives, +-// it emits a separate token for each link or directive and +-// each comment portion between them. +-func (tv *tokenVisitor) comment(c *ast.Comment, importByName map[string]*types.PkgName) { +- if strings.HasPrefix(c.Text, "//go:") { +- tv.godirective(c) +- return +- } +- +- pkgScope := tv.pkg.Types().Scope() +- // lookupObjects interprets the name in various forms +- // (X, p.T, p.T.M, etc) and return the list of symbols +- // denoted by each identifier in the dotted list. +- lookupObjects := func(name string) (objs []types.Object) { +- scope := pkgScope +- if pkg, suffix, ok := strings.Cut(name, "."); ok { +- if obj, _ := importByName[pkg]; obj != nil { +- objs = append(objs, obj) +- scope = obj.Imported().Scope() +- name = suffix +- } - } -- resp, err := server.InlineCompletion(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- +- if recv, method, ok := strings.Cut(name, "."); ok { +- obj, ok := scope.Lookup(recv).(*types.TypeName) +- if !ok { +- return nil +- } +- objs = append(objs, obj) +- t, ok := obj.Type().(*types.Named) +- if !ok { +- return nil +- } +- m, _, _ := types.LookupFieldOrMethod(t, true, tv.pkg.Types(), method) +- if m == nil { +- return nil +- } +- objs = append(objs, m) +- return objs +- } else { +- obj := scope.Lookup(name) +- if obj == nil { +- return nil +- } +- if _, ok := obj.(*types.PkgName); !ok && !obj.Exported() { +- return nil +- } +- objs = append(objs, obj) +- return objs +- - } -- return true, reply(ctx, resp, nil) -- case "textDocument/inlineValue": -- var params InlineValueParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- } +- +- tokenTypeByObject := func(obj types.Object) semtok.TokenType { +- switch obj.(type) { +- case *types.PkgName: +- return semtok.TokNamespace +- case *types.Func: +- return semtok.TokFunction +- case *types.TypeName: +- return semtok.TokType +- case *types.Const, *types.Var: +- return semtok.TokVariable +- default: +- return semtok.TokComment - } -- resp, err := server.InlineValue(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- } +- +- pos := c.Pos() +- for _, line := range strings.Split(c.Text, "\n") { +- last := 0 +- +- for _, idx := range docLinkRegex.FindAllStringSubmatchIndex(line, -1) { +- // The first group is the reference name. e.g. "X", "p.T", "p.T.M". +- name := line[idx[2]:idx[3]] +- if objs := lookupObjects(name); len(objs) > 0 { +- if last < idx[2] { +- tv.token(pos+token.Pos(last), idx[2]-last, semtok.TokComment, nil) +- } +- offset := pos + token.Pos(idx[2]) +- for i, obj := range objs { +- if i > 0 { +- tv.token(offset, len("."), semtok.TokComment, nil) +- offset += token.Pos(len(".")) +- } +- id, rest, _ := strings.Cut(name, ".") +- name = rest +- tv.token(offset, len(id), tokenTypeByObject(obj), nil) +- offset += token.Pos(len(id)) +- } +- last = idx[3] +- } - } -- return true, reply(ctx, resp, nil) -- case "textDocument/linkedEditingRange": -- var params LinkedEditingRangeParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- if last != len(c.Text) { +- tv.token(pos+token.Pos(last), len(line)-last, semtok.TokComment, nil) - } -- resp, err := server.LinkedEditingRange(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- pos += token.Pos(len(line) + 1) +- } +-} +- +-// token emits a token of the specified extent and semantics. +-func (tv *tokenVisitor) token(start token.Pos, length int, typ semtok.TokenType, modifiers []string) { +- if length <= 0 { +- return // vscode doesn't like 0-length Tokens +- } +- if !start.IsValid() { +- // This is not worth reporting. TODO(pjw): does it still happen? +- return +- } +- end := start + token.Pos(length) +- if start >= tv.end || end <= tv.start { +- return +- } +- // want a line and column from start (in LSP coordinates). Ignore line directives. +- rng, err := tv.pgf.PosRange(start, end) +- if err != nil { +- event.Error(tv.ctx, "failed to convert to range", err) +- return +- } +- if rng.End.Line != rng.Start.Line { +- // this happens if users are typing at the end of the file, but report nothing +- return +- } +- tv.tokens = append(tv.tokens, semtok.Token{ +- Line: rng.Start.Line, +- Start: rng.Start.Character, +- Len: rng.End.Character - rng.Start.Character, // (on same line) +- Type: typ, +- Modifiers: modifiers, +- }) +-} +- +-// strStack converts the stack to a string, for debugging and error messages. +-func (tv *tokenVisitor) strStack() string { +- msg := []string{"["} +- for i := len(tv.stack) - 1; i >= 0; i-- { +- n := tv.stack[i] +- msg = append(msg, strings.TrimPrefix(fmt.Sprintf("%T", n), "*ast.")) +- } +- if len(tv.stack) > 0 { +- pos := tv.stack[len(tv.stack)-1].Pos() +- if _, err := safetoken.Offset(tv.pgf.Tok, pos); err != nil { +- msg = append(msg, fmt.Sprintf("invalid position %v for %s", pos, tv.pgf.URI)) +- } else { +- posn := safetoken.Position(tv.pgf.Tok, pos) +- msg = append(msg, fmt.Sprintf("(%s:%d,col:%d)", +- filepath.Base(posn.Filename), posn.Line, posn.Column)) - } -- return true, reply(ctx, resp, nil) -- case "textDocument/moniker": -- var params MonikerParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- } +- msg = append(msg, "]") +- return strings.Join(msg, " ") +-} +- +-// srcLine returns the source text for n (truncated at first newline). +-func (tv *tokenVisitor) srcLine(n ast.Node) string { +- file := tv.pgf.Tok +- line := safetoken.Line(file, n.Pos()) +- start, err := safetoken.Offset(file, file.LineStart(line)) +- if err != nil { +- return "" +- } +- end := start +- for ; end < len(tv.pgf.Src) && tv.pgf.Src[end] != '\n'; end++ { +- +- } +- return string(tv.pgf.Src[start:end]) +-} +- +-func (tv *tokenVisitor) inspect(n ast.Node) (descend bool) { +- if n == nil { +- tv.stack = tv.stack[:len(tv.stack)-1] // pop +- return true +- } +- tv.stack = append(tv.stack, n) // push +- defer func() { +- if !descend { +- tv.stack = tv.stack[:len(tv.stack)-1] // pop - } -- resp, err := server.Moniker(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- }() +- +- switch n := n.(type) { +- case *ast.ArrayType: +- case *ast.AssignStmt: +- tv.token(n.TokPos, len(n.Tok.String()), semtok.TokOperator, nil) +- case *ast.BasicLit: +- if strings.Contains(n.Value, "\n") { +- // has to be a string. +- tv.multiline(n.Pos(), n.End(), semtok.TokString) +- break - } -- return true, reply(ctx, resp, nil) -- case "textDocument/onTypeFormatting": -- var params DocumentOnTypeFormattingParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- what := semtok.TokNumber +- if n.Kind == token.STRING { +- what = semtok.TokString - } -- resp, err := server.OnTypeFormatting(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- tv.token(n.Pos(), len(n.Value), what, nil) +- case *ast.BinaryExpr: +- tv.token(n.OpPos, len(n.Op.String()), semtok.TokOperator, nil) +- case *ast.BlockStmt: +- case *ast.BranchStmt: +- tv.token(n.TokPos, len(n.Tok.String()), semtok.TokKeyword, nil) +- if n.Label != nil { +- tv.token(n.Label.Pos(), len(n.Label.Name), semtok.TokLabel, nil) - } -- return true, reply(ctx, resp, nil) -- case "textDocument/prepareCallHierarchy": -- var params CallHierarchyPrepareParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- case *ast.CallExpr: +- if n.Ellipsis.IsValid() { +- tv.token(n.Ellipsis, len("..."), semtok.TokOperator, nil) - } -- resp, err := server.PrepareCallHierarchy(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- case *ast.CaseClause: +- iam := "case" +- if n.List == nil { +- iam = "default" - } -- return true, reply(ctx, resp, nil) -- case "textDocument/prepareRename": -- var params PrepareRenameParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- tv.token(n.Case, len(iam), semtok.TokKeyword, nil) +- case *ast.ChanType: +- // chan | chan <- | <- chan +- switch { +- case n.Arrow == token.NoPos: +- tv.token(n.Begin, len("chan"), semtok.TokKeyword, nil) +- case n.Arrow == n.Begin: +- tv.token(n.Arrow, 2, semtok.TokOperator, nil) +- pos := tv.findKeyword("chan", n.Begin+2, n.Value.Pos()) +- tv.token(pos, len("chan"), semtok.TokKeyword, nil) +- case n.Arrow != n.Begin: +- tv.token(n.Begin, len("chan"), semtok.TokKeyword, nil) +- tv.token(n.Arrow, 2, semtok.TokOperator, nil) - } -- resp, err := server.PrepareRename(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- case *ast.CommClause: +- length := len("case") +- if n.Comm == nil { +- length = len("default") - } -- return true, reply(ctx, resp, nil) -- case "textDocument/prepareTypeHierarchy": -- var params TypeHierarchyPrepareParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- tv.token(n.Case, length, semtok.TokKeyword, nil) +- case *ast.CompositeLit: +- case *ast.DeclStmt: +- case *ast.DeferStmt: +- tv.token(n.Defer, len("defer"), semtok.TokKeyword, nil) +- case *ast.Ellipsis: +- tv.token(n.Ellipsis, len("..."), semtok.TokOperator, nil) +- case *ast.EmptyStmt: +- case *ast.ExprStmt: +- case *ast.Field: +- case *ast.FieldList: +- case *ast.ForStmt: +- tv.token(n.For, len("for"), semtok.TokKeyword, nil) +- case *ast.FuncDecl: +- case *ast.FuncLit: +- case *ast.FuncType: +- if n.Func != token.NoPos { +- tv.token(n.Func, len("func"), semtok.TokKeyword, nil) - } -- resp, err := server.PrepareTypeHierarchy(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- case *ast.GenDecl: +- tv.token(n.TokPos, len(n.Tok.String()), semtok.TokKeyword, nil) +- case *ast.GoStmt: +- tv.token(n.Go, len("go"), semtok.TokKeyword, nil) +- case *ast.Ident: +- tv.ident(n) +- case *ast.IfStmt: +- tv.token(n.If, len("if"), semtok.TokKeyword, nil) +- if n.Else != nil { +- // x.Body.End() or x.Body.End()+1, not that it matters +- pos := tv.findKeyword("else", n.Body.End(), n.Else.Pos()) +- tv.token(pos, len("else"), semtok.TokKeyword, nil) - } -- return true, reply(ctx, resp, nil) -- case "textDocument/rangeFormatting": -- var params DocumentRangeFormattingParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- case *ast.ImportSpec: +- tv.importSpec(n) +- return false +- case *ast.IncDecStmt: +- tv.token(n.TokPos, len(n.Tok.String()), semtok.TokOperator, nil) +- case *ast.IndexExpr: +- case *ast.IndexListExpr: +- case *ast.InterfaceType: +- tv.token(n.Interface, len("interface"), semtok.TokKeyword, nil) +- case *ast.KeyValueExpr: +- case *ast.LabeledStmt: +- tv.token(n.Label.Pos(), len(n.Label.Name), semtok.TokLabel, []string{"definition"}) +- case *ast.MapType: +- tv.token(n.Map, len("map"), semtok.TokKeyword, nil) +- case *ast.ParenExpr: +- case *ast.RangeStmt: +- tv.token(n.For, len("for"), semtok.TokKeyword, nil) +- // x.TokPos == token.NoPos is legal (for range foo {}) +- offset := n.TokPos +- if offset == token.NoPos { +- offset = n.For - } -- resp, err := server.RangeFormatting(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- pos := tv.findKeyword("range", offset, n.X.Pos()) +- tv.token(pos, len("range"), semtok.TokKeyword, nil) +- case *ast.ReturnStmt: +- tv.token(n.Return, len("return"), semtok.TokKeyword, nil) +- case *ast.SelectStmt: +- tv.token(n.Select, len("select"), semtok.TokKeyword, nil) +- case *ast.SelectorExpr: +- case *ast.SendStmt: +- tv.token(n.Arrow, len("<-"), semtok.TokOperator, nil) +- case *ast.SliceExpr: +- case *ast.StarExpr: +- tv.token(n.Star, len("*"), semtok.TokOperator, nil) +- case *ast.StructType: +- tv.token(n.Struct, len("struct"), semtok.TokKeyword, nil) +- case *ast.SwitchStmt: +- tv.token(n.Switch, len("switch"), semtok.TokKeyword, nil) +- case *ast.TypeAssertExpr: +- if n.Type == nil { +- pos := tv.findKeyword("type", n.Lparen, n.Rparen) +- tv.token(pos, len("type"), semtok.TokKeyword, nil) - } -- return true, reply(ctx, resp, nil) -- case "textDocument/rangesFormatting": -- var params DocumentRangesFormattingParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- case *ast.TypeSpec: +- case *ast.TypeSwitchStmt: +- tv.token(n.Switch, len("switch"), semtok.TokKeyword, nil) +- case *ast.UnaryExpr: +- tv.token(n.OpPos, len(n.Op.String()), semtok.TokOperator, nil) +- case *ast.ValueSpec: +- // things only seen with parsing or type errors, so ignore them +- case *ast.BadDecl, *ast.BadExpr, *ast.BadStmt: +- return false +- // not going to see these +- case *ast.File, *ast.Package: +- tv.errorf("implement %T %s", n, safetoken.Position(tv.pgf.Tok, n.Pos())) +- // other things we knowingly ignore +- case *ast.Comment, *ast.CommentGroup: +- return false +- default: +- tv.errorf("failed to implement %T", n) +- } +- return true +-} +- +-func (tv *tokenVisitor) ident(id *ast.Ident) { +- var obj types.Object +- +- // emit emits a token for the identifier's extent. +- emit := func(tok semtok.TokenType, modifiers ...string) { +- tv.token(id.Pos(), len(id.Name), tok, modifiers) +- if semDebug { +- q := "nil" +- if obj != nil { +- q = fmt.Sprintf("%T", obj.Type()) // e.g. "*types.Map" +- } +- log.Printf(" use %s/%T/%s got %s %v (%s)", +- id.Name, obj, q, tok, modifiers, tv.strStack()) - } -- resp, err := server.RangesFormatting(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- } +- +- // definition? +- obj = tv.info.Defs[id] +- if obj != nil { +- if tok, modifiers := tv.definitionFor(id, obj); tok != "" { +- emit(tok, modifiers...) +- } else if semDebug { +- log.Printf(" for %s/%T/%T got '' %v (%s)", +- id.Name, obj, obj.Type(), modifiers, tv.strStack()) - } -- return true, reply(ctx, resp, nil) -- case "textDocument/references": -- var params ReferenceParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- return +- } +- +- // use? +- obj = tv.info.Uses[id] +- switch obj := obj.(type) { +- case *types.Builtin: +- emit(semtok.TokFunction, "defaultLibrary") +- case *types.Const: +- if is[*types.Named](obj.Type()) && +- (id.Name == "iota" || id.Name == "true" || id.Name == "false") { +- emit(semtok.TokVariable, "readonly", "defaultLibrary") +- } else { +- emit(semtok.TokVariable, "readonly") - } -- resp, err := server.References(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- case *types.Func: +- emit(semtok.TokFunction) +- case *types.Label: +- // Labels are reliably covered by the syntax traversal. +- case *types.Nil: +- // nil is a predeclared identifier +- emit(semtok.TokVariable, "readonly", "defaultLibrary") +- case *types.PkgName: +- emit(semtok.TokNamespace) +- case *types.TypeName: // could be a TypeParam +- if is[*types.TypeParam](aliases.Unalias(obj.Type())) { +- emit(semtok.TokTypeParam) +- } else if is[*types.Basic](obj.Type()) { +- emit(semtok.TokType, "defaultLibrary") +- } else { +- emit(semtok.TokType) - } -- return true, reply(ctx, resp, nil) -- case "textDocument/rename": -- var params RenameParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- case *types.Var: +- if is[*types.Signature](aliases.Unalias(obj.Type())) { +- emit(semtok.TokFunction) +- } else if tv.isParam(obj.Pos()) { +- // variable, unless use.pos is the pos of a Field in an ancestor FuncDecl +- // or FuncLit and then it's a parameter +- emit(semtok.TokParameter) +- } else { +- emit(semtok.TokVariable) - } -- resp, err := server.Rename(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- case nil: +- if tok, modifiers := tv.unkIdent(id); tok != "" { +- emit(tok, modifiers...) - } -- return true, reply(ctx, resp, nil) -- case "textDocument/selectionRange": -- var params SelectionRangeParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- default: +- panic(obj) +- } +-} +- +-// isParam reports whether the position is that of a parameter name of +-// an enclosing function. +-func (tv *tokenVisitor) isParam(pos token.Pos) bool { +- for i := len(tv.stack) - 1; i >= 0; i-- { +- switch n := tv.stack[i].(type) { +- case *ast.FuncDecl: +- for _, f := range n.Type.Params.List { +- for _, id := range f.Names { +- if id.Pos() == pos { +- return true +- } +- } +- } +- case *ast.FuncLit: +- for _, f := range n.Type.Params.List { +- for _, id := range f.Names { +- if id.Pos() == pos { +- return true +- } +- } +- } - } -- resp, err := server.SelectionRange(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- } +- return false +-} +- +-// unkIdent handles identifiers with no types.Object (neither use nor +-// def), use the parse stack. +-// A lot of these only happen when the package doesn't compile, +-// but in that case it is all best-effort from the parse tree. +-func (tv *tokenVisitor) unkIdent(id *ast.Ident) (semtok.TokenType, []string) { +- def := []string{"definition"} +- n := len(tv.stack) - 2 // parent of Ident; stack is [File ... Ident] +- if n < 0 { +- tv.errorf("no stack") // can't happen +- return "", nil +- } +- switch parent := tv.stack[n].(type) { +- case *ast.BinaryExpr, *ast.UnaryExpr, *ast.ParenExpr, *ast.StarExpr, +- *ast.IncDecStmt, *ast.SliceExpr, *ast.ExprStmt, *ast.IndexExpr, +- *ast.ReturnStmt, *ast.ChanType, *ast.SendStmt, +- *ast.ForStmt, // possibly incomplete +- *ast.IfStmt, /* condition */ +- *ast.KeyValueExpr, // either key or value +- *ast.IndexListExpr: +- return semtok.TokVariable, nil +- case *ast.Ellipsis: +- return semtok.TokType, nil +- case *ast.CaseClause: +- if n-2 >= 0 && is[ast.TypeSwitchStmt](tv.stack[n-2]) { +- return semtok.TokType, nil - } -- return true, reply(ctx, resp, nil) -- case "textDocument/semanticTokens/full": -- var params SemanticTokensParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- return semtok.TokVariable, nil +- case *ast.ArrayType: +- if id == parent.Len { +- // or maybe a Type Param, but we can't just from the parse tree +- return semtok.TokVariable, nil +- } else { +- return semtok.TokType, nil - } -- resp, err := server.SemanticTokensFull(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- case *ast.MapType: +- return semtok.TokType, nil +- case *ast.CallExpr: +- if id == parent.Fun { +- return semtok.TokFunction, nil - } -- return true, reply(ctx, resp, nil) -- case "textDocument/semanticTokens/full/delta": -- var params SemanticTokensDeltaParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- return semtok.TokVariable, nil +- case *ast.SwitchStmt: +- return semtok.TokVariable, nil +- case *ast.TypeAssertExpr: +- if id == parent.X { +- return semtok.TokVariable, nil +- } else if id == parent.Type { +- return semtok.TokType, nil - } -- resp, err := server.SemanticTokensFullDelta(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- case *ast.ValueSpec: +- for _, p := range parent.Names { +- if p == id { +- return semtok.TokVariable, def +- } - } -- return true, reply(ctx, resp, nil) -- case "textDocument/semanticTokens/range": -- var params SemanticTokensRangeParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- for _, p := range parent.Values { +- if p == id { +- return semtok.TokVariable, nil +- } - } -- resp, err := server.SemanticTokensRange(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- return semtok.TokType, nil +- case *ast.SelectorExpr: // e.ti.Selections[nd] is nil, so no help +- if n-1 >= 0 { +- if ce, ok := tv.stack[n-1].(*ast.CallExpr); ok { +- // ... CallExpr SelectorExpr Ident (_.x()) +- if ce.Fun == parent && parent.Sel == id { +- return semtok.TokFunction, nil +- } +- } - } -- return true, reply(ctx, resp, nil) -- case "textDocument/signatureHelp": -- var params SignatureHelpParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- return semtok.TokVariable, nil +- case *ast.AssignStmt: +- for _, p := range parent.Lhs { +- // x := ..., or x = ... +- if p == id { +- if parent.Tok != token.DEFINE { +- def = nil +- } +- return semtok.TokVariable, def // '_' in _ = ... +- } - } -- resp, err := server.SignatureHelp(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- // RHS, = x +- return semtok.TokVariable, nil +- case *ast.TypeSpec: // it's a type if it is either the Name or the Type +- if id == parent.Type { +- def = nil - } -- return true, reply(ctx, resp, nil) -- case "textDocument/typeDefinition": -- var params TypeDefinitionParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- return semtok.TokType, def +- case *ast.Field: +- // ident could be type in a field, or a method in an interface type, or a variable +- if id == parent.Type { +- return semtok.TokType, nil - } -- resp, err := server.TypeDefinition(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- if n > 2 && +- is[*ast.InterfaceType](tv.stack[n-2]) && +- is[*ast.FieldList](tv.stack[n-1]) { +- +- return semtok.TokMethod, def - } -- return true, reply(ctx, resp, nil) -- case "textDocument/willSave": -- var params WillSaveTextDocumentParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- return semtok.TokVariable, nil +- case *ast.LabeledStmt: +- if id == parent.Label { +- return semtok.TokLabel, def - } -- err := server.WillSave(ctx, ¶ms) -- return true, reply(ctx, nil, err) -- case "textDocument/willSaveWaitUntil": -- var params WillSaveTextDocumentParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- case *ast.BranchStmt: +- if id == parent.Label { +- return semtok.TokLabel, nil - } -- resp, err := server.WillSaveWaitUntil(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- case *ast.CompositeLit: +- if parent.Type == id { +- return semtok.TokType, nil - } -- return true, reply(ctx, resp, nil) -- case "typeHierarchy/subtypes": -- var params TypeHierarchySubtypesParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) +- return semtok.TokVariable, nil +- case *ast.RangeStmt: +- if parent.Tok != token.DEFINE { +- def = nil - } -- resp, err := server.Subtypes(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) +- return semtok.TokVariable, def +- case *ast.FuncDecl: +- return semtok.TokFunction, def +- default: +- tv.errorf("%T unexpected: %s %s%q", parent, id.Name, tv.strStack(), tv.srcLine(id)) +- } +- return "", nil +-} +- +-func isDeprecated(n *ast.CommentGroup) bool { +- if n != nil { +- for _, c := range n.List { +- if strings.HasPrefix(c.Text, "// Deprecated") { +- return true +- } - } -- return true, reply(ctx, resp, nil) -- case "typeHierarchy/supertypes": -- var params TypeHierarchySupertypesParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- resp, err := server.Supertypes(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) -- } -- return true, reply(ctx, resp, nil) -- case "window/workDoneProgress/cancel": -- var params WorkDoneProgressCancelParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- err := server.WorkDoneProgressCancel(ctx, ¶ms) -- return true, reply(ctx, nil, err) -- case "workspace/diagnostic": -- var params WorkspaceDiagnosticParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- resp, err := server.DiagnosticWorkspace(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) -- } -- return true, reply(ctx, resp, nil) -- case "workspace/didChangeConfiguration": -- var params DidChangeConfigurationParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- err := server.DidChangeConfiguration(ctx, ¶ms) -- return true, reply(ctx, nil, err) -- case "workspace/didChangeWatchedFiles": -- var params DidChangeWatchedFilesParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- err := server.DidChangeWatchedFiles(ctx, ¶ms) -- return true, reply(ctx, nil, err) -- case "workspace/didChangeWorkspaceFolders": -- var params DidChangeWorkspaceFoldersParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- err := server.DidChangeWorkspaceFolders(ctx, ¶ms) -- return true, reply(ctx, nil, err) -- case "workspace/didCreateFiles": -- var params CreateFilesParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- err := server.DidCreateFiles(ctx, ¶ms) -- return true, reply(ctx, nil, err) -- case "workspace/didDeleteFiles": -- var params DeleteFilesParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- err := server.DidDeleteFiles(ctx, ¶ms) -- return true, reply(ctx, nil, err) -- case "workspace/didRenameFiles": -- var params RenameFilesParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- err := server.DidRenameFiles(ctx, ¶ms) -- return true, reply(ctx, nil, err) -- case "workspace/executeCommand": -- var params ExecuteCommandParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- resp, err := server.ExecuteCommand(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) -- } -- return true, reply(ctx, resp, nil) -- case "workspace/symbol": -- var params WorkspaceSymbolParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- resp, err := server.Symbol(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) -- } -- return true, reply(ctx, resp, nil) -- case "workspace/willCreateFiles": -- var params CreateFilesParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- resp, err := server.WillCreateFiles(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) -- } -- return true, reply(ctx, resp, nil) -- case "workspace/willDeleteFiles": -- var params DeleteFilesParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- resp, err := server.WillDeleteFiles(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) -- } -- return true, reply(ctx, resp, nil) -- case "workspace/willRenameFiles": -- var params RenameFilesParams -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- resp, err := server.WillRenameFiles(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) -- } -- return true, reply(ctx, resp, nil) -- case "workspaceSymbol/resolve": -- var params WorkspaceSymbol -- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { -- return true, sendParseError(ctx, reply, err) -- } -- resp, err := server.ResolveWorkspaceSymbol(ctx, ¶ms) -- if err != nil { -- return true, reply(ctx, nil, err) -- } -- return true, reply(ctx, resp, nil) -- default: -- return false, nil - } +- return false -} - --func (s *serverDispatcher) Progress(ctx context.Context, params *ProgressParams) error { -- return s.sender.Notify(ctx, "$/progress", params) --} --func (s *serverDispatcher) SetTrace(ctx context.Context, params *SetTraceParams) error { -- return s.sender.Notify(ctx, "$/setTrace", params) --} --func (s *serverDispatcher) IncomingCalls(ctx context.Context, params *CallHierarchyIncomingCallsParams) ([]CallHierarchyIncomingCall, error) { -- var result []CallHierarchyIncomingCall -- if err := s.sender.Call(ctx, "callHierarchy/incomingCalls", params, &result); err != nil { -- return nil, err -- } -- return result, nil --} --func (s *serverDispatcher) OutgoingCalls(ctx context.Context, params *CallHierarchyOutgoingCallsParams) ([]CallHierarchyOutgoingCall, error) { -- var result []CallHierarchyOutgoingCall -- if err := s.sender.Call(ctx, "callHierarchy/outgoingCalls", params, &result); err != nil { -- return nil, err -- } -- return result, nil --} --func (s *serverDispatcher) ResolveCodeAction(ctx context.Context, params *CodeAction) (*CodeAction, error) { -- var result *CodeAction -- if err := s.sender.Call(ctx, "codeAction/resolve", params, &result); err != nil { -- return nil, err -- } -- return result, nil --} --func (s *serverDispatcher) ResolveCodeLens(ctx context.Context, params *CodeLens) (*CodeLens, error) { -- var result *CodeLens -- if err := s.sender.Call(ctx, "codeLens/resolve", params, &result); err != nil { -- return nil, err -- } -- return result, nil --} --func (s *serverDispatcher) ResolveCompletionItem(ctx context.Context, params *CompletionItem) (*CompletionItem, error) { -- var result *CompletionItem -- if err := s.sender.Call(ctx, "completionItem/resolve", params, &result); err != nil { -- return nil, err +-// definitionFor handles a defining identifier. +-func (tv *tokenVisitor) definitionFor(id *ast.Ident, obj types.Object) (semtok.TokenType, []string) { +- // The definition of a types.Label cannot be found by +- // ascending the syntax tree, and doing so will reach the +- // FuncDecl, causing us to misinterpret the label as a +- // parameter (#65494). +- // +- // However, labels are reliably covered by the syntax +- // traversal, so we don't need to use type information. +- if is[*types.Label](obj) { +- return "", nil - } -- return result, nil --} --func (s *serverDispatcher) ResolveDocumentLink(ctx context.Context, params *DocumentLink) (*DocumentLink, error) { -- var result *DocumentLink -- if err := s.sender.Call(ctx, "documentLink/resolve", params, &result); err != nil { -- return nil, err +- +- // PJW: look into replacing these syntactic tests with types more generally +- modifiers := []string{"definition"} +- for i := len(tv.stack) - 1; i >= 0; i-- { +- switch ancestor := tv.stack[i].(type) { +- case *ast.AssignStmt, *ast.RangeStmt: +- if id.Name == "_" { +- return "", nil // not really a variable +- } +- return semtok.TokVariable, modifiers +- case *ast.GenDecl: +- if isDeprecated(ancestor.Doc) { +- modifiers = append(modifiers, "deprecated") +- } +- if ancestor.Tok == token.CONST { +- modifiers = append(modifiers, "readonly") +- } +- return semtok.TokVariable, modifiers +- case *ast.FuncDecl: +- // If x is immediately under a FuncDecl, it is a function or method +- if i == len(tv.stack)-2 { +- if isDeprecated(ancestor.Doc) { +- modifiers = append(modifiers, "deprecated") +- } +- if ancestor.Recv != nil { +- return semtok.TokMethod, modifiers +- } +- return semtok.TokFunction, modifiers +- } +- // if x < ... < FieldList < FuncDecl, this is the receiver, a variable +- // PJW: maybe not. it might be a typeparameter in the type of the receiver +- if is[*ast.FieldList](tv.stack[i+1]) { +- if is[*types.TypeName](obj) { +- return semtok.TokTypeParam, modifiers +- } +- return semtok.TokVariable, nil +- } +- // if x < ... < FieldList < FuncType < FuncDecl, this is a param +- return semtok.TokParameter, modifiers +- case *ast.FuncType: +- if isTypeParam(id, ancestor) { +- return semtok.TokTypeParam, modifiers +- } +- return semtok.TokParameter, modifiers +- case *ast.InterfaceType: +- return semtok.TokMethod, modifiers +- case *ast.TypeSpec: +- // GenDecl/Typespec/FuncType/FieldList/Field/Ident +- // (type A func(b uint64)) (err error) +- // b and err should not be semtok.TokType, but semtok.TokVariable +- // and in GenDecl/TpeSpec/StructType/FieldList/Field/Ident +- // (type A struct{b uint64} +- // but on type B struct{C}), C is a type, but is not being defined. +- // GenDecl/TypeSpec/FieldList/Field/Ident is a typeParam +- if is[*ast.FieldList](tv.stack[i+1]) { +- return semtok.TokTypeParam, modifiers +- } +- fldm := tv.stack[len(tv.stack)-2] +- if fld, ok := fldm.(*ast.Field); ok { +- // if len(fld.names) == 0 this is a semtok.TokType, being used +- if len(fld.Names) == 0 { +- return semtok.TokType, nil +- } +- return semtok.TokVariable, modifiers +- } +- return semtok.TokType, modifiers +- } - } -- return result, nil --} --func (s *serverDispatcher) Exit(ctx context.Context) error { -- return s.sender.Notify(ctx, "exit", nil) +- // can't happen +- tv.errorf("failed to find the decl for %s", safetoken.Position(tv.pgf.Tok, id.Pos())) +- return "", nil -} --func (s *serverDispatcher) Initialize(ctx context.Context, params *ParamInitialize) (*InitializeResult, error) { -- var result *InitializeResult -- if err := s.sender.Call(ctx, "initialize", params, &result); err != nil { -- return nil, err +- +-func isTypeParam(id *ast.Ident, t *ast.FuncType) bool { +- if tp := t.TypeParams; tp != nil { +- for _, p := range tp.List { +- for _, n := range p.Names { +- if id == n { +- return true +- } +- } +- } - } -- return result, nil --} --func (s *serverDispatcher) Initialized(ctx context.Context, params *InitializedParams) error { -- return s.sender.Notify(ctx, "initialized", params) +- return false -} --func (s *serverDispatcher) Resolve(ctx context.Context, params *InlayHint) (*InlayHint, error) { -- var result *InlayHint -- if err := s.sender.Call(ctx, "inlayHint/resolve", params, &result); err != nil { -- return nil, err +- +-// multiline emits a multiline token (`string` or /*comment*/). +-func (tv *tokenVisitor) multiline(start, end token.Pos, tok semtok.TokenType) { +- // TODO(adonovan): test with non-ASCII. +- +- f := tv.fset.File(start) +- // the hard part is finding the lengths of lines. include the \n +- length := func(line int) int { +- n := f.LineStart(line) +- if line >= f.LineCount() { +- return f.Size() - int(n) +- } +- return int(f.LineStart(line+1) - n) - } -- return result, nil --} --func (s *serverDispatcher) DidChangeNotebookDocument(ctx context.Context, params *DidChangeNotebookDocumentParams) error { -- return s.sender.Notify(ctx, "notebookDocument/didChange", params) --} --func (s *serverDispatcher) DidCloseNotebookDocument(ctx context.Context, params *DidCloseNotebookDocumentParams) error { -- return s.sender.Notify(ctx, "notebookDocument/didClose", params) --} --func (s *serverDispatcher) DidOpenNotebookDocument(ctx context.Context, params *DidOpenNotebookDocumentParams) error { -- return s.sender.Notify(ctx, "notebookDocument/didOpen", params) --} --func (s *serverDispatcher) DidSaveNotebookDocument(ctx context.Context, params *DidSaveNotebookDocumentParams) error { -- return s.sender.Notify(ctx, "notebookDocument/didSave", params) --} --func (s *serverDispatcher) Shutdown(ctx context.Context) error { -- return s.sender.Call(ctx, "shutdown", nil, nil) --} --func (s *serverDispatcher) CodeAction(ctx context.Context, params *CodeActionParams) ([]CodeAction, error) { -- var result []CodeAction -- if err := s.sender.Call(ctx, "textDocument/codeAction", params, &result); err != nil { -- return nil, err +- spos := safetoken.StartPosition(tv.fset, start) +- epos := safetoken.EndPosition(tv.fset, end) +- sline := spos.Line +- eline := epos.Line +- // first line is from spos.Column to end +- tv.token(start, length(sline)-spos.Column, tok, nil) // leng(sline)-1 - (spos.Column-1) +- for i := sline + 1; i < eline; i++ { +- // intermediate lines are from 1 to end +- tv.token(f.LineStart(i), length(i)-1, tok, nil) // avoid the newline - } -- return result, nil +- // last line is from 1 to epos.Column +- tv.token(f.LineStart(eline), epos.Column-1, tok, nil) // columns are 1-based -} --func (s *serverDispatcher) CodeLens(ctx context.Context, params *CodeLensParams) ([]CodeLens, error) { -- var result []CodeLens -- if err := s.sender.Call(ctx, "textDocument/codeLens", params, &result); err != nil { -- return nil, err +- +-// findKeyword returns the position of a keyword by searching within +-// the specified range, for when its cannot be exactly known from the AST. +-func (tv *tokenVisitor) findKeyword(keyword string, start, end token.Pos) token.Pos { +- // TODO(adonovan): use safetoken.Offset. +- offset := int(start) - tv.pgf.Tok.Base() +- last := int(end) - tv.pgf.Tok.Base() +- buf := tv.pgf.Src +- idx := bytes.Index(buf[offset:last], []byte(keyword)) +- if idx != -1 { +- return start + token.Pos(idx) - } -- return result, nil +- //(in unparsable programs: type _ <-<-chan int) +- tv.errorf("not found:%s %v", keyword, safetoken.StartPosition(tv.fset, start)) +- return token.NoPos -} --func (s *serverDispatcher) ColorPresentation(ctx context.Context, params *ColorPresentationParams) ([]ColorPresentation, error) { -- var result []ColorPresentation -- if err := s.sender.Call(ctx, "textDocument/colorPresentation", params, &result); err != nil { -- return nil, err +- +-func (tv *tokenVisitor) importSpec(spec *ast.ImportSpec) { +- // a local package name or the last component of the Path +- if spec.Name != nil { +- name := spec.Name.String() +- if name != "_" && name != "." { +- tv.token(spec.Name.Pos(), len(name), semtok.TokNamespace, nil) +- } +- return // don't mark anything for . or _ - } -- return result, nil --} --func (s *serverDispatcher) Completion(ctx context.Context, params *CompletionParams) (*CompletionList, error) { -- var result *CompletionList -- if err := s.sender.Call(ctx, "textDocument/completion", params, &result); err != nil { -- return nil, err +- importPath := metadata.UnquoteImportPath(spec) +- if importPath == "" { +- return - } -- return result, nil --} --func (s *serverDispatcher) Declaration(ctx context.Context, params *DeclarationParams) (*Or_textDocument_declaration, error) { -- var result *Or_textDocument_declaration -- if err := s.sender.Call(ctx, "textDocument/declaration", params, &result); err != nil { -- return nil, err +- // Import strings are implementation defined. Try to match with parse information. +- depID := tv.metadata.DepsByImpPath[importPath] +- if depID == "" { +- return - } -- return result, nil --} --func (s *serverDispatcher) Definition(ctx context.Context, params *DefinitionParams) ([]Location, error) { -- var result []Location -- if err := s.sender.Call(ctx, "textDocument/definition", params, &result); err != nil { -- return nil, err +- depMD := tv.metadataSource.Metadata(depID) +- if depMD == nil { +- // unexpected, but impact is that maybe some import is not colored +- return - } -- return result, nil --} --func (s *serverDispatcher) Diagnostic(ctx context.Context, params *string) (*string, error) { -- var result *string -- if err := s.sender.Call(ctx, "textDocument/diagnostic", params, &result); err != nil { -- return nil, err +- // Check whether the original literal contains the package's declared name. +- j := strings.LastIndex(spec.Path.Value, string(depMD.Name)) +- if j < 0 { +- // Package name does not match import path, so there is nothing to report. +- return - } -- return result, nil --} --func (s *serverDispatcher) DidChange(ctx context.Context, params *DidChangeTextDocumentParams) error { -- return s.sender.Notify(ctx, "textDocument/didChange", params) --} --func (s *serverDispatcher) DidClose(ctx context.Context, params *DidCloseTextDocumentParams) error { -- return s.sender.Notify(ctx, "textDocument/didClose", params) +- // Report virtual declaration at the position of the substring. +- start := spec.Path.Pos() + token.Pos(j) +- tv.token(start, len(depMD.Name), semtok.TokNamespace, nil) -} --func (s *serverDispatcher) DidOpen(ctx context.Context, params *DidOpenTextDocumentParams) error { -- return s.sender.Notify(ctx, "textDocument/didOpen", params) +- +-// errorf logs an error and reports a bug. +-func (tv *tokenVisitor) errorf(format string, args ...any) { +- msg := fmt.Sprintf(format, args...) +- bug.Report(msg) +- event.Error(tv.ctx, tv.strStack(), errors.New(msg)) -} --func (s *serverDispatcher) DidSave(ctx context.Context, params *DidSaveTextDocumentParams) error { -- return s.sender.Notify(ctx, "textDocument/didSave", params) +- +-var godirectives = map[string]struct{}{ +- // https://pkg.go.dev/cmd/compile +- "noescape": {}, +- "uintptrescapes": {}, +- "noinline": {}, +- "norace": {}, +- "nosplit": {}, +- "linkname": {}, +- +- // https://pkg.go.dev/go/build +- "build": {}, +- "binary-only-package": {}, +- "embed": {}, -} --func (s *serverDispatcher) DocumentColor(ctx context.Context, params *DocumentColorParams) ([]ColorInformation, error) { -- var result []ColorInformation -- if err := s.sender.Call(ctx, "textDocument/documentColor", params, &result); err != nil { -- return nil, err +- +-// Tokenize godirective at the start of the comment c, if any, and the surrounding comment. +-// If there is any failure, emits the entire comment as a TokComment token. +-// Directives are highlighted as-is, even if used incorrectly. Typically there are +-// dedicated analyzers that will warn about misuse. +-func (tv *tokenVisitor) godirective(c *ast.Comment) { +- // First check if '//go:directive args...' is a valid directive. +- directive, args, _ := strings.Cut(c.Text, " ") +- kind, _ := stringsCutPrefix(directive, "//go:") +- if _, ok := godirectives[kind]; !ok { +- // Unknown 'go:' directive. +- tv.token(c.Pos(), len(c.Text), semtok.TokComment, nil) +- return - } -- return result, nil --} --func (s *serverDispatcher) DocumentHighlight(ctx context.Context, params *DocumentHighlightParams) ([]DocumentHighlight, error) { -- var result []DocumentHighlight -- if err := s.sender.Call(ctx, "textDocument/documentHighlight", params, &result); err != nil { -- return nil, err +- +- // Make the 'go:directive' part stand out, the rest is comments. +- tv.token(c.Pos(), len("//"), semtok.TokComment, nil) +- +- directiveStart := c.Pos() + token.Pos(len("//")) +- tv.token(directiveStart, len(directive[len("//"):]), semtok.TokNamespace, nil) +- +- if len(args) > 0 { +- tailStart := c.Pos() + token.Pos(len(directive)+len(" ")) +- tv.token(tailStart, len(args), semtok.TokComment, nil) - } -- return result, nil -} --func (s *serverDispatcher) DocumentLink(ctx context.Context, params *DocumentLinkParams) ([]DocumentLink, error) { -- var result []DocumentLink -- if err := s.sender.Call(ctx, "textDocument/documentLink", params, &result); err != nil { -- return nil, err +- +-// Go 1.20 strings.CutPrefix. +-func stringsCutPrefix(s, prefix string) (after string, found bool) { +- if !strings.HasPrefix(s, prefix) { +- return s, false - } -- return result, nil +- return s[len(prefix):], true -} --func (s *serverDispatcher) DocumentSymbol(ctx context.Context, params *DocumentSymbolParams) ([]interface{}, error) { -- var result []interface{} -- if err := s.sender.Call(ctx, "textDocument/documentSymbol", params, &result); err != nil { -- return nil, err -- } -- return result, nil +- +-func is[T any](x any) bool { +- _, ok := x.(T) +- return ok -} --func (s *serverDispatcher) FoldingRange(ctx context.Context, params *FoldingRangeParams) ([]FoldingRange, error) { -- var result []FoldingRange -- if err := s.sender.Call(ctx, "textDocument/foldingRange", params, &result); err != nil { -- return nil, err +diff -urN a/gopls/internal/golang/signature_help.go b/gopls/internal/golang/signature_help.go +--- a/gopls/internal/golang/signature_help.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/signature_help.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,204 +0,0 @@ +-// Copyright 2018 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package golang +- +-import ( +- "context" +- "fmt" +- "go/ast" +- "go/token" +- "go/types" +- "strings" +- +- "golang.org/x/tools/go/ast/astutil" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/settings" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/gopls/internal/util/typesutil" +- "golang.org/x/tools/internal/event" +-) +- +-func SignatureHelp(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) (*protocol.SignatureInformation, int, error) { +- ctx, done := event.Start(ctx, "golang.SignatureHelp") +- defer done() +- +- // We need full type-checking here, as we must type-check function bodies in +- // order to provide signature help at the requested position. +- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) +- if err != nil { +- return nil, 0, fmt.Errorf("getting file for SignatureHelp: %w", err) - } -- return result, nil --} --func (s *serverDispatcher) Formatting(ctx context.Context, params *DocumentFormattingParams) ([]TextEdit, error) { -- var result []TextEdit -- if err := s.sender.Call(ctx, "textDocument/formatting", params, &result); err != nil { -- return nil, err +- pos, err := pgf.PositionPos(position) +- if err != nil { +- return nil, 0, err - } -- return result, nil --} --func (s *serverDispatcher) Hover(ctx context.Context, params *HoverParams) (*Hover, error) { -- var result *Hover -- if err := s.sender.Call(ctx, "textDocument/hover", params, &result); err != nil { -- return nil, err +- // Find a call expression surrounding the query position. +- var callExpr *ast.CallExpr +- path, _ := astutil.PathEnclosingInterval(pgf.File, pos, pos) +- if path == nil { +- return nil, 0, fmt.Errorf("cannot find node enclosing position") - } -- return result, nil --} --func (s *serverDispatcher) Implementation(ctx context.Context, params *ImplementationParams) ([]Location, error) { -- var result []Location -- if err := s.sender.Call(ctx, "textDocument/implementation", params, &result); err != nil { -- return nil, err +-FindCall: +- for _, node := range path { +- switch node := node.(type) { +- case *ast.CallExpr: +- if pos >= node.Lparen && pos <= node.Rparen { +- callExpr = node +- break FindCall +- } +- case *ast.FuncLit, *ast.FuncType: +- // The user is within an anonymous function, +- // which may be the parameter to the *ast.CallExpr. +- // Don't show signature help in this case. +- return nil, 0, fmt.Errorf("no signature help within a function declaration") +- case *ast.BasicLit: +- if node.Kind == token.STRING { +- return nil, 0, fmt.Errorf("no signature help within a string literal") +- } +- } +- - } -- return result, nil --} --func (s *serverDispatcher) InlayHint(ctx context.Context, params *InlayHintParams) ([]InlayHint, error) { -- var result []InlayHint -- if err := s.sender.Call(ctx, "textDocument/inlayHint", params, &result); err != nil { -- return nil, err +- if callExpr == nil || callExpr.Fun == nil { +- return nil, 0, fmt.Errorf("cannot find an enclosing function") - } -- return result, nil --} --func (s *serverDispatcher) InlineCompletion(ctx context.Context, params *InlineCompletionParams) (*Or_Result_textDocument_inlineCompletion, error) { -- var result *Or_Result_textDocument_inlineCompletion -- if err := s.sender.Call(ctx, "textDocument/inlineCompletion", params, &result); err != nil { -- return nil, err +- +- info := pkg.TypesInfo() +- +- // Get the type information for the function being called. +- var sig *types.Signature +- if tv, ok := info.Types[callExpr.Fun]; !ok { +- return nil, 0, fmt.Errorf("cannot get type for Fun %[1]T (%[1]v)", callExpr.Fun) +- } else if tv.IsType() { +- return nil, 0, fmt.Errorf("this is a conversion to %s, not a call", tv.Type) +- } else if sig, ok = tv.Type.Underlying().(*types.Signature); !ok { +- return nil, 0, fmt.Errorf("cannot find signature for Fun %[1]T (%[1]v)", callExpr.Fun) - } -- return result, nil --} --func (s *serverDispatcher) InlineValue(ctx context.Context, params *InlineValueParams) ([]InlineValue, error) { -- var result []InlineValue -- if err := s.sender.Call(ctx, "textDocument/inlineValue", params, &result); err != nil { -- return nil, err +- // Inv: sig != nil +- +- qf := typesutil.FileQualifier(pgf.File, pkg.Types(), info) +- +- // Get the object representing the function, if available. +- // There is no object in certain cases such as calling a function returned by +- // a function (e.g. "foo()()"). +- var obj types.Object +- switch t := callExpr.Fun.(type) { +- case *ast.Ident: +- obj = info.ObjectOf(t) +- case *ast.SelectorExpr: +- obj = info.ObjectOf(t.Sel) - } -- return result, nil --} --func (s *serverDispatcher) LinkedEditingRange(ctx context.Context, params *LinkedEditingRangeParams) (*LinkedEditingRanges, error) { -- var result *LinkedEditingRanges -- if err := s.sender.Call(ctx, "textDocument/linkedEditingRange", params, &result); err != nil { -- return nil, err +- +- // Call to built-in? +- if obj != nil && !obj.Pos().IsValid() { +- // function? +- if obj, ok := obj.(*types.Builtin); ok { +- return builtinSignature(ctx, snapshot, callExpr, obj.Name(), pos) +- } +- +- // method (only error.Error)? +- if fn, ok := obj.(*types.Func); ok && fn.Name() == "Error" { +- return &protocol.SignatureInformation{ +- Label: "Error()", +- Documentation: stringToSigInfoDocumentation("Error returns the error message.", snapshot.Options()), +- }, 0, nil +- } +- +- return nil, 0, bug.Errorf("call to unexpected built-in %v (%T)", obj, obj) - } -- return result, nil --} --func (s *serverDispatcher) Moniker(ctx context.Context, params *MonikerParams) ([]Moniker, error) { -- var result []Moniker -- if err := s.sender.Call(ctx, "textDocument/moniker", params, &result); err != nil { -- return nil, err -- } -- return result, nil --} --func (s *serverDispatcher) OnTypeFormatting(ctx context.Context, params *DocumentOnTypeFormattingParams) ([]TextEdit, error) { -- var result []TextEdit -- if err := s.sender.Call(ctx, "textDocument/onTypeFormatting", params, &result); err != nil { -- return nil, err -- } -- return result, nil --} --func (s *serverDispatcher) PrepareCallHierarchy(ctx context.Context, params *CallHierarchyPrepareParams) ([]CallHierarchyItem, error) { -- var result []CallHierarchyItem -- if err := s.sender.Call(ctx, "textDocument/prepareCallHierarchy", params, &result); err != nil { -- return nil, err -- } -- return result, nil --} --func (s *serverDispatcher) PrepareRename(ctx context.Context, params *PrepareRenameParams) (*PrepareRename2Gn, error) { -- var result *PrepareRename2Gn -- if err := s.sender.Call(ctx, "textDocument/prepareRename", params, &result); err != nil { -- return nil, err -- } -- return result, nil --} --func (s *serverDispatcher) PrepareTypeHierarchy(ctx context.Context, params *TypeHierarchyPrepareParams) ([]TypeHierarchyItem, error) { -- var result []TypeHierarchyItem -- if err := s.sender.Call(ctx, "textDocument/prepareTypeHierarchy", params, &result); err != nil { -- return nil, err -- } -- return result, nil --} --func (s *serverDispatcher) RangeFormatting(ctx context.Context, params *DocumentRangeFormattingParams) ([]TextEdit, error) { -- var result []TextEdit -- if err := s.sender.Call(ctx, "textDocument/rangeFormatting", params, &result); err != nil { -- return nil, err -- } -- return result, nil --} --func (s *serverDispatcher) RangesFormatting(ctx context.Context, params *DocumentRangesFormattingParams) ([]TextEdit, error) { -- var result []TextEdit -- if err := s.sender.Call(ctx, "textDocument/rangesFormatting", params, &result); err != nil { -- return nil, err -- } -- return result, nil --} --func (s *serverDispatcher) References(ctx context.Context, params *ReferenceParams) ([]Location, error) { -- var result []Location -- if err := s.sender.Call(ctx, "textDocument/references", params, &result); err != nil { -- return nil, err -- } -- return result, nil --} --func (s *serverDispatcher) Rename(ctx context.Context, params *RenameParams) (*WorkspaceEdit, error) { -- var result *WorkspaceEdit -- if err := s.sender.Call(ctx, "textDocument/rename", params, &result); err != nil { -- return nil, err -- } -- return result, nil --} --func (s *serverDispatcher) SelectionRange(ctx context.Context, params *SelectionRangeParams) ([]SelectionRange, error) { -- var result []SelectionRange -- if err := s.sender.Call(ctx, "textDocument/selectionRange", params, &result); err != nil { -- return nil, err -- } -- return result, nil --} --func (s *serverDispatcher) SemanticTokensFull(ctx context.Context, params *SemanticTokensParams) (*SemanticTokens, error) { -- var result *SemanticTokens -- if err := s.sender.Call(ctx, "textDocument/semanticTokens/full", params, &result); err != nil { -- return nil, err -- } -- return result, nil --} --func (s *serverDispatcher) SemanticTokensFullDelta(ctx context.Context, params *SemanticTokensDeltaParams) (interface{}, error) { -- var result interface{} -- if err := s.sender.Call(ctx, "textDocument/semanticTokens/full/delta", params, &result); err != nil { -- return nil, err -- } -- return result, nil --} --func (s *serverDispatcher) SemanticTokensRange(ctx context.Context, params *SemanticTokensRangeParams) (*SemanticTokens, error) { -- var result *SemanticTokens -- if err := s.sender.Call(ctx, "textDocument/semanticTokens/range", params, &result); err != nil { -- return nil, err -- } -- return result, nil --} --func (s *serverDispatcher) SignatureHelp(ctx context.Context, params *SignatureHelpParams) (*SignatureHelp, error) { -- var result *SignatureHelp -- if err := s.sender.Call(ctx, "textDocument/signatureHelp", params, &result); err != nil { -- return nil, err -- } -- return result, nil --} --func (s *serverDispatcher) TypeDefinition(ctx context.Context, params *TypeDefinitionParams) ([]Location, error) { -- var result []Location -- if err := s.sender.Call(ctx, "textDocument/typeDefinition", params, &result); err != nil { -- return nil, err -- } -- return result, nil --} --func (s *serverDispatcher) WillSave(ctx context.Context, params *WillSaveTextDocumentParams) error { -- return s.sender.Notify(ctx, "textDocument/willSave", params) --} --func (s *serverDispatcher) WillSaveWaitUntil(ctx context.Context, params *WillSaveTextDocumentParams) ([]TextEdit, error) { -- var result []TextEdit -- if err := s.sender.Call(ctx, "textDocument/willSaveWaitUntil", params, &result); err != nil { -- return nil, err -- } -- return result, nil --} --func (s *serverDispatcher) Subtypes(ctx context.Context, params *TypeHierarchySubtypesParams) ([]TypeHierarchyItem, error) { -- var result []TypeHierarchyItem -- if err := s.sender.Call(ctx, "typeHierarchy/subtypes", params, &result); err != nil { -- return nil, err +- +- activeParam := activeParameter(callExpr, sig.Params().Len(), sig.Variadic(), pos) +- +- var ( +- name string +- comment *ast.CommentGroup +- ) +- if obj != nil { +- d, err := HoverDocForObject(ctx, snapshot, pkg.FileSet(), obj) +- if err != nil { +- return nil, 0, err +- } +- name = obj.Name() +- comment = d +- } else { +- name = "func" - } -- return result, nil --} --func (s *serverDispatcher) Supertypes(ctx context.Context, params *TypeHierarchySupertypesParams) ([]TypeHierarchyItem, error) { -- var result []TypeHierarchyItem -- if err := s.sender.Call(ctx, "typeHierarchy/supertypes", params, &result); err != nil { -- return nil, err +- mq := MetadataQualifierForFile(snapshot, pgf.File, pkg.Metadata()) +- s, err := NewSignature(ctx, snapshot, pkg, sig, comment, qf, mq) +- if err != nil { +- return nil, 0, err - } -- return result, nil --} --func (s *serverDispatcher) WorkDoneProgressCancel(ctx context.Context, params *WorkDoneProgressCancelParams) error { -- return s.sender.Notify(ctx, "window/workDoneProgress/cancel", params) --} --func (s *serverDispatcher) DiagnosticWorkspace(ctx context.Context, params *WorkspaceDiagnosticParams) (*WorkspaceDiagnosticReport, error) { -- var result *WorkspaceDiagnosticReport -- if err := s.sender.Call(ctx, "workspace/diagnostic", params, &result); err != nil { -- return nil, err +- paramInfo := make([]protocol.ParameterInformation, 0, len(s.params)) +- for _, p := range s.params { +- paramInfo = append(paramInfo, protocol.ParameterInformation{Label: p}) - } -- return result, nil --} --func (s *serverDispatcher) DidChangeConfiguration(ctx context.Context, params *DidChangeConfigurationParams) error { -- return s.sender.Notify(ctx, "workspace/didChangeConfiguration", params) --} --func (s *serverDispatcher) DidChangeWatchedFiles(ctx context.Context, params *DidChangeWatchedFilesParams) error { -- return s.sender.Notify(ctx, "workspace/didChangeWatchedFiles", params) --} --func (s *serverDispatcher) DidChangeWorkspaceFolders(ctx context.Context, params *DidChangeWorkspaceFoldersParams) error { -- return s.sender.Notify(ctx, "workspace/didChangeWorkspaceFolders", params) --} --func (s *serverDispatcher) DidCreateFiles(ctx context.Context, params *CreateFilesParams) error { -- return s.sender.Notify(ctx, "workspace/didCreateFiles", params) --} --func (s *serverDispatcher) DidDeleteFiles(ctx context.Context, params *DeleteFilesParams) error { -- return s.sender.Notify(ctx, "workspace/didDeleteFiles", params) --} --func (s *serverDispatcher) DidRenameFiles(ctx context.Context, params *RenameFilesParams) error { -- return s.sender.Notify(ctx, "workspace/didRenameFiles", params) +- return &protocol.SignatureInformation{ +- Label: name + s.Format(), +- Documentation: stringToSigInfoDocumentation(s.doc, snapshot.Options()), +- Parameters: paramInfo, +- }, activeParam, nil -} --func (s *serverDispatcher) ExecuteCommand(ctx context.Context, params *ExecuteCommandParams) (interface{}, error) { -- var result interface{} -- if err := s.sender.Call(ctx, "workspace/executeCommand", params, &result); err != nil { -- return nil, err +- +-func builtinSignature(ctx context.Context, snapshot *cache.Snapshot, callExpr *ast.CallExpr, name string, pos token.Pos) (*protocol.SignatureInformation, int, error) { +- sig, err := NewBuiltinSignature(ctx, snapshot, name) +- if err != nil { +- return nil, 0, err - } -- return result, nil --} --func (s *serverDispatcher) Symbol(ctx context.Context, params *WorkspaceSymbolParams) ([]SymbolInformation, error) { -- var result []SymbolInformation -- if err := s.sender.Call(ctx, "workspace/symbol", params, &result); err != nil { -- return nil, err +- paramInfo := make([]protocol.ParameterInformation, 0, len(sig.params)) +- for _, p := range sig.params { +- paramInfo = append(paramInfo, protocol.ParameterInformation{Label: p}) - } -- return result, nil +- activeParam := activeParameter(callExpr, len(sig.params), sig.variadic, pos) +- return &protocol.SignatureInformation{ +- Label: sig.name + sig.Format(), +- Documentation: stringToSigInfoDocumentation(sig.doc, snapshot.Options()), +- Parameters: paramInfo, +- }, activeParam, nil -} --func (s *serverDispatcher) WillCreateFiles(ctx context.Context, params *CreateFilesParams) (*WorkspaceEdit, error) { -- var result *WorkspaceEdit -- if err := s.sender.Call(ctx, "workspace/willCreateFiles", params, &result); err != nil { -- return nil, err +- +-func activeParameter(callExpr *ast.CallExpr, numParams int, variadic bool, pos token.Pos) (activeParam int) { +- if len(callExpr.Args) == 0 { +- return 0 - } -- return result, nil --} --func (s *serverDispatcher) WillDeleteFiles(ctx context.Context, params *DeleteFilesParams) (*WorkspaceEdit, error) { -- var result *WorkspaceEdit -- if err := s.sender.Call(ctx, "workspace/willDeleteFiles", params, &result); err != nil { -- return nil, err +- // First, check if the position is even in the range of the arguments. +- start, end := callExpr.Lparen, callExpr.Rparen +- if !(start <= pos && pos <= end) { +- return 0 - } -- return result, nil --} --func (s *serverDispatcher) WillRenameFiles(ctx context.Context, params *RenameFilesParams) (*WorkspaceEdit, error) { -- var result *WorkspaceEdit -- if err := s.sender.Call(ctx, "workspace/willRenameFiles", params, &result); err != nil { -- return nil, err +- for _, expr := range callExpr.Args { +- if start == token.NoPos { +- start = expr.Pos() +- } +- end = expr.End() +- if start <= pos && pos <= end { +- break +- } +- // Don't advance the active parameter for the last parameter of a variadic function. +- if !variadic || activeParam < numParams-1 { +- activeParam++ +- } +- start = expr.Pos() + 1 // to account for commas - } -- return result, nil +- return activeParam -} --func (s *serverDispatcher) ResolveWorkspaceSymbol(ctx context.Context, params *WorkspaceSymbol) (*WorkspaceSymbol, error) { -- var result *WorkspaceSymbol -- if err := s.sender.Call(ctx, "workspaceSymbol/resolve", params, &result); err != nil { -- return nil, err +- +-func stringToSigInfoDocumentation(s string, options *settings.Options) *protocol.Or_SignatureInformation_documentation { +- v := s +- k := protocol.PlainText +- if options.PreferredContentFormat == protocol.Markdown { +- v = CommentToMarkdown(s, options) +- // whether or not content is newline terminated may not matter for LSP clients, +- // but our tests expect trailing newlines to be stripped. +- v = strings.TrimSuffix(v, "\n") // TODO(pjw): change the golden files +- k = protocol.Markdown - } -- return result, nil --} --func (s *serverDispatcher) NonstandardRequest(ctx context.Context, method string, params interface{}) (interface{}, error) { -- var result interface{} -- if err := s.sender.Call(ctx, method, params, &result); err != nil { -- return nil, err +- return &protocol.Or_SignatureInformation_documentation{ +- Value: protocol.MarkupContent{ +- Kind: k, +- Value: v, +- }, - } -- return result, nil -} -diff -urN a/gopls/internal/lsp/README.md b/gopls/internal/lsp/README.md ---- a/gopls/internal/lsp/README.md 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/README.md 1970-01-01 00:00:00.000000000 +0000 -@@ -1,7 +0,0 @@ --# lsp -- --internal/lsp provides much of the Language Server Protocol (lsp) implementation --for gopls. -- --Documentation for users and contributors can be found in the --[`gopls/doc`](../../gopls/doc) directory. -diff -urN a/gopls/internal/lsp/references.go b/gopls/internal/lsp/references.go ---- a/gopls/internal/lsp/references.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/references.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,36 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/golang/snapshot.go b/gopls/internal/golang/snapshot.go +--- a/gopls/internal/golang/snapshot.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/snapshot.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,98 +0,0 @@ +-// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package lsp +-package golang - -import ( - "context" +- "fmt" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/lsp/template" -- "golang.org/x/tools/gopls/internal/telemetry" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/tag" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/protocol" -) - --func (s *Server) references(ctx context.Context, params *protocol.ReferenceParams) (_ []protocol.Location, rerr error) { -- recordLatency := telemetry.StartLatencyTimer("references") -- defer func() { -- recordLatency(ctx, rerr) -- }() -- -- ctx, done := event.Start(ctx, "lsp.Server.references", tag.URI.Of(params.TextDocument.URI)) -- defer done() -- -- snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind) -- defer release() -- if !ok { +-// NarrowestMetadataForFile returns metadata for the narrowest package +-// (the one with the fewest files) that encloses the specified file. +-// The result may be a test variant, but never an intermediate test variant. +-func NarrowestMetadataForFile(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI) (*metadata.Package, error) { +- mps, err := snapshot.MetadataForFile(ctx, uri) +- if err != nil { - return nil, err - } -- if snapshot.FileKind(fh) == source.Tmpl { -- return template.References(ctx, snapshot, fh, params) +- metadata.RemoveIntermediateTestVariants(&mps) +- if len(mps) == 0 { +- return nil, fmt.Errorf("no package metadata for file %s", uri) - } -- return source.References(ctx, snapshot, fh, params.Position, params.Context.IncludeDeclaration) +- return mps[0], nil -} -diff -urN a/gopls/internal/lsp/regtest/doc.go b/gopls/internal/lsp/regtest/doc.go ---- a/gopls/internal/lsp/regtest/doc.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/regtest/doc.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,157 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --// Package regtest provides a framework for writing gopls regression tests. --// --// User reported regressions are often expressed in terms of editor --// interactions. For example: "When I open my editor in this directory, --// navigate to this file, and change this line, I get a diagnostic that doesn't --// make sense". In these cases reproducing, diagnosing, and writing a test to --// protect against this regression can be difficult. --// --// The regtest package provides an API for developers to express these types of --// user interactions in ordinary Go tests, validate them, and run them in a --// variety of execution modes. --// --// # Test package setup --// --// The regression test package uses a couple of uncommon patterns to reduce --// boilerplate in test bodies. First, it is intended to be imported as "." so --// that helpers do not need to be qualified. Second, it requires some setup --// that is currently implemented in the regtest.Main function, which must be --// invoked by TestMain. Therefore, a minimal regtest testing package looks --// like this: --// --// package lsptests --// --// import ( --// "fmt" --// "testing" --// --// "golang.org/x/tools/gopls/internal/hooks" --// . "golang.org/x/tools/gopls/internal/lsp/regtest" --// ) --// --// func TestMain(m *testing.M) { --// Main(m, hooks.Options) --// } --// --// # Writing a simple regression test --// --// To run a regression test use the regtest.Run function, which accepts a --// txtar-encoded archive defining the initial workspace state. This function --// sets up the workspace in a temporary directory, creates a fake text editor, --// starts gopls, and initializes an LSP session. It then invokes the provided --// test function with an *Env handle encapsulating the newly created --// environment. Because gopls may be run in various modes (as a sidecar or --// daemon process, with different settings), the test runner may perform this --// process multiple times, re-running the test function each time with a new --// environment. --// --// func TestOpenFile(t *testing.T) { --// const files = ` --// -- go.mod -- --// module mod.com --// --// go 1.12 --// -- foo.go -- --// package foo --// ` --// Run(t, files, func(t *testing.T, env *Env) { --// env.OpenFile("foo.go") --// }) --// } --// --// # Configuring Regtest Execution --// --// The regtest package exposes several options that affect the setup process --// described above. To use these options, use the WithOptions function: --// --// WithOptions(opts...).Run(...) --// --// See options.go for a full list of available options. --// --// # Operating on editor state --// --// To operate on editor state within the test body, the Env type provides --// access to the workspace directory (Env.SandBox), text editor (Env.Editor), --// LSP server (Env.Server), and 'awaiter' (Env.Awaiter). --// --// In most cases, operations on these primitive building blocks of the --// regression test environment expect a Context (which should be a child of --// env.Ctx), and return an error. To avoid boilerplate, the Env exposes a set --// of wrappers in wrappers.go for use in scripting: --// --// env.CreateBuffer("c/c.go", "") --// env.EditBuffer("c/c.go", fake.Edit{ --// Text: `package c`, --// }) --// --// These wrappers thread through Env.Ctx, and call t.Fatal on any errors. --// --// # Expressing expectations --// --// The general pattern for a regression test is to script interactions with the --// fake editor and sandbox, and assert that gopls behaves correctly after each --// state change. Unfortunately, this is complicated by the fact that state --// changes are communicated to gopls via unidirectional client->server --// notifications (didOpen, didChange, etc.), and resulting gopls behavior such --// as diagnostics, logs, or messages is communicated back via server->client --// notifications. Therefore, within regression tests we must be able to say "do --// this, and then eventually gopls should do that". To achieve this, the --// regtest package provides a framework for expressing conditions that must --// eventually be met, in terms of the Expectation type. --// --// To express the assertion that "eventually gopls must meet these --// expectations", use env.Await(...): --// --// env.RegexpReplace("x/x.go", `package x`, `package main`) --// env.Await(env.DiagnosticAtRegexp("x/main.go", `fmt`)) --// --// Await evaluates the provided expectations atomically, whenever the client --// receives a state-changing notification from gopls. See expectation.go for a --// full list of available expectations. --// --// A fundamental problem with this model is that if gopls never meets the --// provided expectations, the test runner will hang until the test timeout --// (which defaults to 10m). There are two ways to work around this poor --// behavior: +-// NarrowestPackageForFile is a convenience function that selects the narrowest +-// non-ITV package to which this file belongs, type-checks it in the requested +-// mode (full or workspace), and returns it, along with the parse tree of that +-// file. -// --// 1. Use a precondition to define precisely when we expect conditions to be --// met. Gopls provides the OnceMet(precondition, expectations...) pattern --// to express ("once this precondition is met, the following expectations --// must all hold"). To instrument preconditions, gopls uses verbose --// progress notifications to inform the client about ongoing work (see --// CompletedWork). The most common precondition is to wait for gopls to be --// done processing all change notifications, for which the regtest package --// provides the AfterChange helper. For example: +-// The "narrowest" package is the one with the fewest number of files that +-// includes the given file. This solves the problem of test variants, as the +-// test will have more files than the non-test package. -// --// // We expect diagnostics to be cleared after gopls is done processing the --// // didSave notification. --// env.SaveBuffer("a/go.mod") --// env.AfterChange(EmptyDiagnostics("a/go.mod")) +-// An intermediate test variant (ITV) package has identical source to a regular +-// package but resolves imports differently. gopls should never need to +-// type-check them. -// --// 2. Set a shorter timeout during development, if you expect to be breaking --// tests. By setting the environment variable GOPLS_REGTEST_TIMEOUT=5s, --// regression tests will time out after 5 seconds. +-// Type-checking is expensive. Call snapshot.ParseGo if all you need is a parse +-// tree, or snapshot.MetadataForFile if you only need metadata. +-func NarrowestPackageForFile(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI) (*cache.Package, *parsego.File, error) { +- return selectPackageForFile(ctx, snapshot, uri, func(metas []*metadata.Package) *metadata.Package { return metas[0] }) +-} +- +-// WidestPackageForFile is a convenience function that selects the widest +-// non-ITV package to which this file belongs, type-checks it in the requested +-// mode (full or workspace), and returns it, along with the parse tree of that +-// file. -// --// # Tips & Tricks +-// The "widest" package is the one with the most number of files that includes +-// the given file. Which is the test variant if one exists. -// --// Here are some tips and tricks for working with regression tests: +-// An intermediate test variant (ITV) package has identical source to a regular +-// package but resolves imports differently. gopls should never need to +-// type-check them. -// --// 1. Set the environment variable GOPLS_REGTEST_TIMEOUT=5s during development. --// 2. Run tests with -short. This will only run regression tests in the --// default gopls execution mode. --// 3. Use capture groups to narrow regexp positions. All regular-expression --// based positions (such as DiagnosticAtRegexp) will match the position of --// the first capture group, if any are provided. This can be used to --// identify a specific position in the code for a pattern that may occur in --// multiple places. For example `var (mu) sync.Mutex` matches the position --// of "mu" within the variable declaration. --// 4. Read diagnostics into a variable to implement more complicated --// assertions about diagnostic state in the editor. To do this, use the --// pattern OnceMet(precondition, ReadDiagnostics("file.go", &d)) to capture --// the current diagnostics as soon as the precondition is met. This is --// preferable to accessing the diagnostics directly, as it avoids races. --package regtest -diff -urN a/gopls/internal/lsp/regtest/env.go b/gopls/internal/lsp/regtest/env.go ---- a/gopls/internal/lsp/regtest/env.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/regtest/env.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,403 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +-// Type-checking is expensive. Call snapshot.ParseGo if all you need is a parse +-// tree, or snapshot.MetadataForFile if you only need metadata. +-func WidestPackageForFile(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI) (*cache.Package, *parsego.File, error) { +- return selectPackageForFile(ctx, snapshot, uri, func(metas []*metadata.Package) *metadata.Package { return metas[len(metas)-1] }) +-} +- +-func selectPackageForFile(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI, selector func([]*metadata.Package) *metadata.Package) (*cache.Package, *parsego.File, error) { +- mps, err := snapshot.MetadataForFile(ctx, uri) +- if err != nil { +- return nil, nil, err +- } +- metadata.RemoveIntermediateTestVariants(&mps) +- if len(mps) == 0 { +- return nil, nil, fmt.Errorf("no package metadata for file %s", uri) +- } +- mp := selector(mps) +- pkgs, err := snapshot.TypeCheck(ctx, mp.ID) +- if err != nil { +- return nil, nil, err +- } +- pkg := pkgs[0] +- pgf, err := pkg.File(uri) +- if err != nil { +- return nil, nil, err // "can't happen" +- } +- return pkg, pgf, err +-} +- +-type ( +- PackageID = metadata.PackageID +- PackagePath = metadata.PackagePath +- PackageName = metadata.PackageName +- ImportPath = metadata.ImportPath +-) +- +-type unit = struct{} +diff -urN a/gopls/internal/golang/stub.go b/gopls/internal/golang/stub.go +--- a/gopls/internal/golang/stub.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/stub.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,334 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package regtest +-package golang - -import ( +- "bytes" - "context" - "fmt" +- "go/format" +- "go/parser" +- "go/token" +- "go/types" +- "io" +- pathpkg "path" - "strings" -- "sync" -- "testing" - -- "golang.org/x/tools/gopls/internal/lsp/fake" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/internal/jsonrpc2/servertest" +- "golang.org/x/tools/go/analysis" +- "golang.org/x/tools/go/ast/astutil" +- "golang.org/x/tools/gopls/internal/analysis/stubmethods" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/internal/diff" +- "golang.org/x/tools/internal/tokeninternal" -) - --// Env holds the building blocks of an editor testing environment, providing --// wrapper methods that hide the boilerplate of plumbing contexts and checking --// errors. --type Env struct { -- T testing.TB // TODO(rfindley): rename to TB -- Ctx context.Context +-// stubMethodsFixer returns a suggested fix to declare the missing +-// methods of the concrete type that is assigned to an interface type +-// at the cursor position. +-func stubMethodsFixer(ctx context.Context, snapshot *cache.Snapshot, pkg *cache.Package, pgf *parsego.File, start, end token.Pos) (*token.FileSet, *analysis.SuggestedFix, error) { +- nodes, _ := astutil.PathEnclosingInterval(pgf.File, start, end) +- si := stubmethods.GetStubInfo(pkg.FileSet(), pkg.TypesInfo(), nodes, start) +- if si == nil { +- return nil, nil, fmt.Errorf("nil interface request") +- } - -- // Most tests should not need to access the scratch area, editor, server, or -- // connection, but they are available if needed. -- Sandbox *fake.Sandbox -- Server servertest.Connector +- // A function-local type cannot be stubbed +- // since there's nowhere to put the methods. +- conc := si.Concrete.Obj() +- if conc.Parent() != conc.Pkg().Scope() { +- return nil, nil, fmt.Errorf("local type %q cannot be stubbed", conc.Name()) +- } - -- // Editor is owned by the Env, and shut down -- Editor *fake.Editor +- // Parse the file declaring the concrete type. +- // +- // Beware: declPGF is not necessarily covered by pkg.FileSet() or si.Fset. +- declPGF, _, err := parseFull(ctx, snapshot, si.Fset, conc.Pos()) +- if err != nil { +- return nil, nil, fmt.Errorf("failed to parse file %q declaring implementation type: %w", declPGF.URI, err) +- } +- if declPGF.Fixed() { +- return nil, nil, fmt.Errorf("file contains parse errors: %s", declPGF.URI) +- } - -- Awaiter *Awaiter --} +- // Find metadata for the concrete type's declaring package +- // as we'll need its import mapping. +- declMeta := findFileInDeps(snapshot, pkg.Metadata(), declPGF.URI) +- if declMeta == nil { +- return nil, nil, bug.Errorf("can't find metadata for file %s among dependencies of %s", declPGF.URI, pkg) +- } - --// An Awaiter keeps track of relevant LSP state, so that it may be asserted --// upon with Expectations. --// --// Wire it into a fake.Editor using Awaiter.Hooks(). --// --// TODO(rfindley): consider simply merging Awaiter with the fake.Editor. It --// probably is not worth its own abstraction. --type Awaiter struct { -- workdir *fake.Workdir +- // Record all direct methods of the current object +- concreteFuncs := make(map[string]struct{}) +- for i := 0; i < si.Concrete.NumMethods(); i++ { +- concreteFuncs[si.Concrete.Method(i).Name()] = struct{}{} +- } - -- mu sync.Mutex -- // For simplicity, each waiter gets a unique ID. -- nextWaiterID int -- state State -- waiters map[int]*condition --} +- // Find subset of interface methods that the concrete type lacks. +- ifaceType := si.Interface.Type().Underlying().(*types.Interface) - --func NewAwaiter(workdir *fake.Workdir) *Awaiter { -- return &Awaiter{ -- workdir: workdir, -- state: State{ -- diagnostics: make(map[string]*protocol.PublishDiagnosticsParams), -- work: make(map[protocol.ProgressToken]*workProgress), -- }, -- waiters: make(map[int]*condition), +- type missingFn struct { +- fn *types.Func +- needSubtle string - } --} - --// Hooks returns LSP client hooks required for awaiting asynchronous expectations. --func (a *Awaiter) Hooks() fake.ClientHooks { -- return fake.ClientHooks{ -- OnDiagnostics: a.onDiagnostics, -- OnLogMessage: a.onLogMessage, -- OnWorkDoneProgressCreate: a.onWorkDoneProgressCreate, -- OnProgress: a.onProgress, -- OnShowDocument: a.onShowDocument, -- OnShowMessage: a.onShowMessage, -- OnShowMessageRequest: a.onShowMessageRequest, -- OnRegisterCapability: a.onRegisterCapability, -- OnUnregisterCapability: a.onUnregisterCapability, -- OnApplyEdit: a.onApplyEdit, -- } --} +- var ( +- missing []missingFn +- concreteStruct, isStruct = si.Concrete.Origin().Underlying().(*types.Struct) +- ) - --// State encapsulates the server state TODO: explain more --type State struct { -- // diagnostics are a map of relative path->diagnostics params -- diagnostics map[string]*protocol.PublishDiagnosticsParams -- logs []*protocol.LogMessageParams -- showDocument []*protocol.ShowDocumentParams -- showMessage []*protocol.ShowMessageParams -- showMessageRequest []*protocol.ShowMessageRequestParams +- for i := 0; i < ifaceType.NumMethods(); i++ { +- imethod := ifaceType.Method(i) +- cmethod, index, _ := types.LookupFieldOrMethod(si.Concrete, si.Pointer, imethod.Pkg(), imethod.Name()) +- if cmethod == nil { +- missing = append(missing, missingFn{fn: imethod}) +- continue +- } - -- registrations []*protocol.RegistrationParams -- registeredCapabilities map[string]protocol.Registration -- unregistrations []*protocol.UnregistrationParams -- documentChanges []protocol.DocumentChanges // collected from ApplyEdit downcalls +- if _, ok := cmethod.(*types.Var); ok { +- // len(LookupFieldOrMethod.index) = 1 => conflict, >1 => shadow. +- return nil, nil, fmt.Errorf("adding method %s.%s would conflict with (or shadow) existing field", +- conc.Name(), imethod.Name()) +- } - -- // outstandingWork is a map of token->work summary. All tokens are assumed to -- // be string, though the spec allows for numeric tokens as well. When work -- // completes, it is deleted from this map. -- work map[protocol.ProgressToken]*workProgress --} +- if _, exist := concreteFuncs[imethod.Name()]; exist { +- if !types.Identical(cmethod.Type(), imethod.Type()) { +- return nil, nil, fmt.Errorf("method %s.%s already exists but has the wrong type: got %s, want %s", +- conc.Name(), imethod.Name(), cmethod.Type(), imethod.Type()) +- } +- continue +- } - --// outstandingWork counts started but not complete work items by title. --func (s State) outstandingWork() map[string]uint64 { -- outstanding := make(map[string]uint64) -- for _, work := range s.work { -- if !work.complete { -- outstanding[work.title]++ +- mf := missingFn{fn: imethod} +- if isStruct && len(index) > 0 { +- field := concreteStruct.Field(index[0]) +- +- fn := field.Name() +- if is[*types.Pointer](field.Type()) { +- fn = "*" + fn +- } +- +- mf.needSubtle = fmt.Sprintf("// Subtle: this method shadows the method (%s).%s of %s.%s.\n", fn, imethod.Name(), si.Concrete.Obj().Name(), field.Name()) - } +- +- missing = append(missing, mf) +- } +- if len(missing) == 0 { +- return nil, nil, fmt.Errorf("no missing methods found") - } -- return outstanding --} - --// completedWork counts complete work items by title. --func (s State) completedWork() map[string]uint64 { -- completed := make(map[string]uint64) -- for _, work := range s.work { -- if work.complete { -- completed[work.title]++ +- // Build import environment for the declaring file. +- // (typesutil.FileQualifier works only for complete +- // import mappings, and requires types.) +- importEnv := make(map[ImportPath]string) // value is local name +- for _, imp := range declPGF.File.Imports { +- importPath := metadata.UnquoteImportPath(imp) +- var name string +- if imp.Name != nil { +- name = imp.Name.Name +- if name == "_" { +- continue +- } else if name == "." { +- name = "" // see types.Qualifier +- } +- } else { +- // Use the correct name from the metadata of the imported +- // package---not a guess based on the import path. +- mp := snapshot.Metadata(declMeta.DepsByImpPath[importPath]) +- if mp == nil { +- continue // can't happen? +- } +- name = string(mp.Name) - } +- importEnv[importPath] = name // latest alias wins - } -- return completed --} - --// startedWork counts started (and possibly complete) work items. --func (s State) startedWork() map[string]uint64 { -- started := make(map[string]uint64) -- for _, work := range s.work { -- started[work.title]++ +- // Create a package name qualifier that uses the +- // locally appropriate imported package name. +- // It records any needed new imports. +- // TODO(adonovan): factor with golang.FormatVarType? +- // +- // Prior to CL 469155 this logic preserved any renaming +- // imports from the file that declares the interface +- // method--ostensibly the preferred name for imports of +- // frequently renamed packages such as protobufs. +- // Now we use the package's declared name. If this turns out +- // to be a mistake, then use parseHeader(si.iface.Pos()). +- // +- type newImport struct{ name, importPath string } +- var newImports []newImport // for AddNamedImport +- qual := func(pkg *types.Package) string { +- // TODO(adonovan): don't ignore vendor prefix. +- // +- // Ignore the current package import. +- if pkg.Path() == conc.Pkg().Path() { +- return "" +- } +- +- importPath := ImportPath(pkg.Path()) +- name, ok := importEnv[importPath] +- if !ok { +- // Insert new import using package's declared name. +- // +- // TODO(adonovan): resolve conflict between declared +- // name and existing file-level (declPGF.File.Imports) +- // or package-level (si.Concrete.Pkg.Scope) decls by +- // generating a fresh name. +- name = pkg.Name() +- importEnv[importPath] = name +- new := newImport{importPath: string(importPath)} +- // For clarity, use a renaming import whenever the +- // local name does not match the path's last segment. +- if name != pathpkg.Base(trimVersionSuffix(new.importPath)) { +- new.name = name +- } +- newImports = append(newImports, new) +- } +- return name - } -- return started --} - --type workProgress struct { -- title, msg, endMsg string -- percent float64 -- complete bool // seen 'end'. --} +- // Format interface name (used only in a comment). +- iface := si.Interface.Name() +- if ipkg := si.Interface.Pkg(); ipkg != nil && ipkg != conc.Pkg() { +- iface = ipkg.Name() + "." + iface +- } - --// This method, provided for debugging, accesses mutable fields without a lock, --// so it must not be called concurrent with any State mutation. --func (s State) String() string { -- var b strings.Builder -- b.WriteString("#### log messages (see RPC logs for full text):\n") -- for _, msg := range s.logs { -- summary := fmt.Sprintf("%v: %q", msg.Type, msg.Message) -- if len(summary) > 60 { -- summary = summary[:57] + "..." -- } -- // Some logs are quite long, and since they should be reproduced in the RPC -- // logs on any failure we include here just a short summary. -- fmt.Fprint(&b, "\t"+summary+"\n") +- // Pointer receiver? +- var star string +- if si.Pointer { +- star = "*" - } -- b.WriteString("\n") -- b.WriteString("#### diagnostics:\n") -- for name, params := range s.diagnostics { -- fmt.Fprintf(&b, "\t%s (version %d):\n", name, int(params.Version)) -- for _, d := range params.Diagnostics { -- fmt.Fprintf(&b, "\t\t(%d, %d) [%s]: %s\n", int(d.Range.Start.Line), int(d.Range.Start.Character), d.Source, d.Message) +- +- // If there are any that have named receiver, choose the first one. +- // Otherwise, use lowercase for the first letter of the object. +- rn := strings.ToLower(si.Concrete.Obj().Name()[0:1]) +- for i := 0; i < si.Concrete.NumMethods(); i++ { +- if recv := si.Concrete.Method(i).Type().(*types.Signature).Recv(); recv.Name() != "" { +- rn = recv.Name() +- break - } - } -- b.WriteString("\n") -- b.WriteString("#### outstanding work:\n") -- for token, state := range s.work { -- if state.complete { -- continue -- } -- name := state.title -- if name == "" { -- name = fmt.Sprintf("!NO NAME(token: %s)", token) +- +- // Check for receiver name conflicts +- checkRecvName := func(tuple *types.Tuple) bool { +- for i := 0; i < tuple.Len(); i++ { +- if rn == tuple.At(i).Name() { +- return true +- } - } -- fmt.Fprintf(&b, "\t%s: %.2f\n", name, state.percent) -- } -- b.WriteString("#### completed work:\n") -- for name, count := range s.completedWork() { -- fmt.Fprintf(&b, "\t%s: %d\n", name, count) +- return false - } -- return b.String() --} - --// A condition is satisfied when all expectations are simultaneously --// met. At that point, the 'met' channel is closed. On any failure, err is set --// and the failed channel is closed. --type condition struct { -- expectations []Expectation -- verdict chan Verdict --} +- // Format the new methods. +- var newMethods bytes.Buffer - --func (a *Awaiter) onApplyEdit(_ context.Context, params *protocol.ApplyWorkspaceEditParams) error { -- a.mu.Lock() -- defer a.mu.Unlock() +- for index := range missing { +- mrn := rn + " " +- sig := missing[index].fn.Type().(*types.Signature) +- if checkRecvName(sig.Params()) || checkRecvName(sig.Results()) { +- mrn = "" +- } - -- a.state.documentChanges = append(a.state.documentChanges, params.Edit.DocumentChanges...) -- a.checkConditionsLocked() -- return nil +- fmt.Fprintf(&newMethods, `// %s implements %s. +-%sfunc (%s%s%s%s) %s%s { +- panic("unimplemented") -} +-`, +- missing[index].fn.Name(), +- iface, +- missing[index].needSubtle, +- mrn, +- star, +- si.Concrete.Obj().Name(), +- FormatTypeParams(si.Concrete.TypeParams()), +- missing[index].fn.Name(), +- strings.TrimPrefix(types.TypeString(missing[index].fn.Type(), qual), "func")) +- } - --func (a *Awaiter) onDiagnostics(_ context.Context, d *protocol.PublishDiagnosticsParams) error { -- a.mu.Lock() -- defer a.mu.Unlock() +- // Compute insertion point for new methods: +- // after the top-level declaration enclosing the (package-level) type. +- insertOffset, err := safetoken.Offset(declPGF.Tok, declPGF.File.End()) +- if err != nil { +- return nil, nil, bug.Errorf("internal error: end position outside file bounds: %v", err) +- } +- concOffset, err := safetoken.Offset(si.Fset.File(conc.Pos()), conc.Pos()) +- if err != nil { +- return nil, nil, bug.Errorf("internal error: finding type decl offset: %v", err) +- } +- for _, decl := range declPGF.File.Decls { +- declEndOffset, err := safetoken.Offset(declPGF.Tok, decl.End()) +- if err != nil { +- return nil, nil, bug.Errorf("internal error: finding decl offset: %v", err) +- } +- if declEndOffset > concOffset { +- insertOffset = declEndOffset +- break +- } +- } - -- pth := a.workdir.URIToPath(d.URI) -- a.state.diagnostics[pth] = d -- a.checkConditionsLocked() -- return nil --} +- // Splice the new methods into the file content. +- var buf bytes.Buffer +- input := declPGF.Mapper.Content // unfixed content of file +- buf.Write(input[:insertOffset]) +- buf.WriteByte('\n') +- io.Copy(&buf, &newMethods) +- buf.Write(input[insertOffset:]) - --func (a *Awaiter) onShowDocument(_ context.Context, params *protocol.ShowDocumentParams) error { -- a.mu.Lock() -- defer a.mu.Unlock() +- // Re-parse the file. +- fset := token.NewFileSet() +- newF, err := parser.ParseFile(fset, declPGF.URI.Path(), buf.Bytes(), parser.ParseComments) +- if err != nil { +- return nil, nil, fmt.Errorf("could not reparse file: %w", err) +- } - -- a.state.showDocument = append(a.state.showDocument, params) -- a.checkConditionsLocked() -- return nil --} +- // Splice the new imports into the syntax tree. +- for _, imp := range newImports { +- astutil.AddNamedImport(fset, newF, imp.name, imp.importPath) +- } - --func (a *Awaiter) onShowMessage(_ context.Context, m *protocol.ShowMessageParams) error { -- a.mu.Lock() -- defer a.mu.Unlock() +- // Pretty-print. +- var output bytes.Buffer +- if err := format.Node(&output, fset, newF); err != nil { +- return nil, nil, fmt.Errorf("format.Node: %w", err) +- } - -- a.state.showMessage = append(a.state.showMessage, m) -- a.checkConditionsLocked() -- return nil +- // Report the diff. +- diffs := diff.Bytes(input, output.Bytes()) +- return tokeninternal.FileSetFor(declPGF.Tok), // edits use declPGF.Tok +- &analysis.SuggestedFix{TextEdits: diffToTextEdits(declPGF.Tok, diffs)}, +- nil -} - --func (a *Awaiter) onShowMessageRequest(_ context.Context, m *protocol.ShowMessageRequestParams) error { -- a.mu.Lock() -- defer a.mu.Unlock() +-// diffToTextEdits converts diff (offset-based) edits to analysis (token.Pos) form. +-func diffToTextEdits(tok *token.File, diffs []diff.Edit) []analysis.TextEdit { +- edits := make([]analysis.TextEdit, 0, len(diffs)) +- for _, edit := range diffs { +- edits = append(edits, analysis.TextEdit{ +- Pos: tok.Pos(edit.Start), +- End: tok.Pos(edit.End), +- NewText: []byte(edit.New), +- }) +- } +- return edits +-} - -- a.state.showMessageRequest = append(a.state.showMessageRequest, m) -- a.checkConditionsLocked() -- return nil +-// trimVersionSuffix removes a trailing "/v2" (etc) suffix from a module path. +-// +-// This is only a heuristic as to the package's declared name, and +-// should only be used for stylistic decisions, such as whether it +-// would be clearer to use an explicit local name in the import +-// because the declared name differs from the result of this function. +-// When the name matters for correctness, look up the imported +-// package's Metadata.Name. +-func trimVersionSuffix(path string) string { +- dir, base := pathpkg.Split(path) +- if len(base) > 1 && base[0] == 'v' && strings.Trim(base[1:], "0123456789") == "" { +- return dir // sans "/v2" +- } +- return path -} +diff -urN a/gopls/internal/golang/symbols.go b/gopls/internal/golang/symbols.go +--- a/gopls/internal/golang/symbols.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/symbols.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,230 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func (a *Awaiter) onLogMessage(_ context.Context, m *protocol.LogMessageParams) error { -- a.mu.Lock() -- defer a.mu.Unlock() +-package golang - -- a.state.logs = append(a.state.logs, m) -- a.checkConditionsLocked() -- return nil --} +-import ( +- "context" +- "fmt" +- "go/ast" +- "go/token" +- "go/types" - --func (a *Awaiter) onWorkDoneProgressCreate(_ context.Context, m *protocol.WorkDoneProgressCreateParams) error { -- a.mu.Lock() -- defer a.mu.Unlock() +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/event" +-) - -- a.state.work[m.Token] = &workProgress{} -- return nil --} +-func DocumentSymbols(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.DocumentSymbol, error) { +- ctx, done := event.Start(ctx, "golang.DocumentSymbols") +- defer done() - --func (a *Awaiter) onProgress(_ context.Context, m *protocol.ProgressParams) error { -- a.mu.Lock() -- defer a.mu.Unlock() -- work, ok := a.state.work[m.Token] -- if !ok { -- panic(fmt.Sprintf("got progress report for unknown report %v: %v", m.Token, m)) +- pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) +- if err != nil { +- return nil, fmt.Errorf("getting file for DocumentSymbols: %w", err) - } -- v := m.Value.(map[string]interface{}) -- switch kind := v["kind"]; kind { -- case "begin": -- work.title = v["title"].(string) -- if msg, ok := v["message"]; ok { -- work.msg = msg.(string) -- } -- case "report": -- if pct, ok := v["percentage"]; ok { -- work.percent = pct.(float64) -- } -- if msg, ok := v["message"]; ok { -- work.msg = msg.(string) -- } -- case "end": -- work.complete = true -- if msg, ok := v["message"]; ok { -- work.endMsg = msg.(string) +- +- // Build symbols for file declarations. When encountering a declaration with +- // errors (typically because positions are invalid), we skip the declaration +- // entirely. VS Code fails to show any symbols if one of the top-level +- // symbols is missing position information. +- var symbols []protocol.DocumentSymbol +- for _, decl := range pgf.File.Decls { +- switch decl := decl.(type) { +- case *ast.FuncDecl: +- if decl.Name.Name == "_" { +- continue +- } +- fs, err := funcSymbol(pgf.Mapper, pgf.Tok, decl) +- if err == nil { +- // If function is a method, prepend the type of the method. +- if decl.Recv != nil && len(decl.Recv.List) > 0 { +- fs.Name = fmt.Sprintf("(%s).%s", types.ExprString(decl.Recv.List[0].Type), fs.Name) +- } +- symbols = append(symbols, fs) +- } +- case *ast.GenDecl: +- for _, spec := range decl.Specs { +- switch spec := spec.(type) { +- case *ast.TypeSpec: +- if spec.Name.Name == "_" { +- continue +- } +- ts, err := typeSymbol(pgf.Mapper, pgf.Tok, spec) +- if err == nil { +- symbols = append(symbols, ts) +- } +- case *ast.ValueSpec: +- for _, name := range spec.Names { +- if name.Name == "_" { +- continue +- } +- vs, err := varSymbol(pgf.Mapper, pgf.Tok, spec, name, decl.Tok == token.CONST) +- if err == nil { +- symbols = append(symbols, vs) +- } +- } +- } +- } - } - } -- a.checkConditionsLocked() -- return nil +- return symbols, nil -} - --func (a *Awaiter) onRegisterCapability(_ context.Context, m *protocol.RegistrationParams) error { -- a.mu.Lock() -- defer a.mu.Unlock() -- -- a.state.registrations = append(a.state.registrations, m) -- if a.state.registeredCapabilities == nil { -- a.state.registeredCapabilities = make(map[string]protocol.Registration) +-func funcSymbol(m *protocol.Mapper, tf *token.File, decl *ast.FuncDecl) (protocol.DocumentSymbol, error) { +- s := protocol.DocumentSymbol{ +- Name: decl.Name.Name, +- Kind: protocol.Function, - } -- for _, reg := range m.Registrations { -- a.state.registeredCapabilities[reg.Method] = reg +- if decl.Recv != nil { +- s.Kind = protocol.Method - } -- a.checkConditionsLocked() -- return nil +- var err error +- s.Range, err = m.NodeRange(tf, decl) +- if err != nil { +- return protocol.DocumentSymbol{}, err +- } +- s.SelectionRange, err = m.NodeRange(tf, decl.Name) +- if err != nil { +- return protocol.DocumentSymbol{}, err +- } +- s.Detail = types.ExprString(decl.Type) +- return s, nil -} - --func (a *Awaiter) onUnregisterCapability(_ context.Context, m *protocol.UnregistrationParams) error { -- a.mu.Lock() -- defer a.mu.Unlock() -- -- a.state.unregistrations = append(a.state.unregistrations, m) -- a.checkConditionsLocked() -- return nil +-func typeSymbol(m *protocol.Mapper, tf *token.File, spec *ast.TypeSpec) (protocol.DocumentSymbol, error) { +- s := protocol.DocumentSymbol{ +- Name: spec.Name.Name, +- } +- var err error +- s.Range, err = m.NodeRange(tf, spec) +- if err != nil { +- return protocol.DocumentSymbol{}, err +- } +- s.SelectionRange, err = m.NodeRange(tf, spec.Name) +- if err != nil { +- return protocol.DocumentSymbol{}, err +- } +- s.Kind, s.Detail, s.Children = typeDetails(m, tf, spec.Type) +- return s, nil -} - --func (a *Awaiter) checkConditionsLocked() { -- for id, condition := range a.waiters { -- if v, _ := checkExpectations(a.state, condition.expectations); v != Unmet { -- delete(a.waiters, id) -- condition.verdict <- v +-func typeDetails(m *protocol.Mapper, tf *token.File, typExpr ast.Expr) (kind protocol.SymbolKind, detail string, children []protocol.DocumentSymbol) { +- switch typExpr := typExpr.(type) { +- case *ast.StructType: +- kind = protocol.Struct +- children = fieldListSymbols(m, tf, typExpr.Fields, protocol.Field) +- if len(children) > 0 { +- detail = "struct{...}" +- } else { +- detail = "struct{}" - } -- } --} - --// takeDocumentChanges returns any accumulated document changes (from --// server ApplyEdit RPC downcalls) and resets the list. --func (a *Awaiter) takeDocumentChanges() []protocol.DocumentChanges { -- a.mu.Lock() -- defer a.mu.Unlock() +- // Find interface methods and embedded types. +- case *ast.InterfaceType: +- kind = protocol.Interface +- children = fieldListSymbols(m, tf, typExpr.Methods, protocol.Method) +- if len(children) > 0 { +- detail = "interface{...}" +- } else { +- detail = "interface{}" +- } - -- res := a.state.documentChanges -- a.state.documentChanges = nil -- return res --} +- case *ast.FuncType: +- kind = protocol.Function +- detail = types.ExprString(typExpr) - --// checkExpectations reports whether s meets all expectations. --func checkExpectations(s State, expectations []Expectation) (Verdict, string) { -- finalVerdict := Met -- var summary strings.Builder -- for _, e := range expectations { -- v := e.Check(s) -- if v > finalVerdict { -- finalVerdict = v -- } -- fmt.Fprintf(&summary, "%v: %s\n", v, e.Description) +- default: +- kind = protocol.Class // catch-all, for cases where we don't know the kind syntactically +- detail = types.ExprString(typExpr) - } -- return finalVerdict, summary.String() +- return -} - --// Await blocks until the given expectations are all simultaneously met. --// --// Generally speaking Await should be avoided because it blocks indefinitely if --// gopls ends up in a state where the expectations are never going to be met. --// Use AfterChange or OnceMet instead, so that the runner knows when to stop --// waiting. --func (e *Env) Await(expectations ...Expectation) { -- e.T.Helper() -- if err := e.Awaiter.Await(e.Ctx, expectations...); err != nil { -- e.T.Fatal(err) +-func fieldListSymbols(m *protocol.Mapper, tf *token.File, fields *ast.FieldList, fieldKind protocol.SymbolKind) []protocol.DocumentSymbol { +- if fields == nil { +- return nil - } --} - --// OnceMet blocks until the precondition is met by the state or becomes --// unmeetable. If it was met, OnceMet checks that the state meets all --// expectations in mustMeets. --func (e *Env) OnceMet(precondition Expectation, mustMeets ...Expectation) { -- e.T.Helper() -- e.Await(OnceMet(precondition, mustMeets...)) +- var symbols []protocol.DocumentSymbol +- for _, field := range fields.List { +- detail, children := "", []protocol.DocumentSymbol(nil) +- if field.Type != nil { +- _, detail, children = typeDetails(m, tf, field.Type) +- } +- if len(field.Names) == 0 { // embedded interface or struct field +- // By default, use the formatted type details as the name of this field. +- // This handles potentially invalid syntax, as well as type embeddings in +- // interfaces. +- child := protocol.DocumentSymbol{ +- Name: detail, +- Kind: protocol.Field, // consider all embeddings to be fields +- Children: children, +- } +- +- // If the field is a valid embedding, promote the type name to field +- // name. +- selection := field.Type +- if id := embeddedIdent(field.Type); id != nil { +- child.Name = id.Name +- child.Detail = detail +- selection = id +- } +- +- if rng, err := m.NodeRange(tf, field.Type); err == nil { +- child.Range = rng +- } +- if rng, err := m.NodeRange(tf, selection); err == nil { +- child.SelectionRange = rng +- } +- +- symbols = append(symbols, child) +- } else { +- for _, name := range field.Names { +- child := protocol.DocumentSymbol{ +- Name: name.Name, +- Kind: fieldKind, +- Detail: detail, +- Children: children, +- } +- +- if rng, err := m.NodeRange(tf, field); err == nil { +- child.Range = rng +- } +- if rng, err := m.NodeRange(tf, name); err == nil { +- child.SelectionRange = rng +- } +- +- symbols = append(symbols, child) +- } +- } +- +- } +- return symbols -} - --// Await waits for all expectations to simultaneously be met. It should only be --// called from the main test goroutine. --func (a *Awaiter) Await(ctx context.Context, expectations ...Expectation) error { -- a.mu.Lock() -- // Before adding the waiter, we check if the condition is currently met or -- // failed to avoid a race where the condition was realized before Await was -- // called. -- switch verdict, summary := checkExpectations(a.state, expectations); verdict { -- case Met: -- a.mu.Unlock() -- return nil -- case Unmeetable: -- err := fmt.Errorf("unmeetable expectations:\n%s\nstate:\n%v", summary, a.state) -- a.mu.Unlock() -- return err +-func varSymbol(m *protocol.Mapper, tf *token.File, spec *ast.ValueSpec, name *ast.Ident, isConst bool) (protocol.DocumentSymbol, error) { +- s := protocol.DocumentSymbol{ +- Name: name.Name, +- Kind: protocol.Variable, - } -- cond := &condition{ -- expectations: expectations, -- verdict: make(chan Verdict), +- if isConst { +- s.Kind = protocol.Constant - } -- a.waiters[a.nextWaiterID] = cond -- a.nextWaiterID++ -- a.mu.Unlock() -- - var err error -- select { -- case <-ctx.Done(): -- err = ctx.Err() -- case v := <-cond.verdict: -- if v != Met { -- err = fmt.Errorf("condition has final verdict %v", v) -- } +- s.Range, err = m.NodeRange(tf, spec) +- if err != nil { +- return protocol.DocumentSymbol{}, err - } -- a.mu.Lock() -- defer a.mu.Unlock() -- _, summary := checkExpectations(a.state, expectations) -- -- // Debugging an unmet expectation can be tricky, so we put some effort into -- // nicely formatting the failure. +- s.SelectionRange, err = m.NodeRange(tf, name) - if err != nil { -- return fmt.Errorf("waiting on:\n%s\nerr:%v\n\nstate:\n%v", summary, err, a.state) +- return protocol.DocumentSymbol{}, err - } -- return nil +- if spec.Type != nil { // type may be missing from the syntax +- _, s.Detail, s.Children = typeDetails(m, tf, spec.Type) +- } +- return s, nil -} -diff -urN a/gopls/internal/lsp/regtest/env_test.go b/gopls/internal/lsp/regtest/env_test.go ---- a/gopls/internal/lsp/regtest/env_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/regtest/env_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,66 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/golang/type_definition.go b/gopls/internal/golang/type_definition.go +--- a/gopls/internal/golang/type_definition.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/type_definition.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,59 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package regtest +-package golang - -import ( - "context" -- "encoding/json" -- "testing" +- "fmt" +- "go/token" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/internal/event" -) - --func TestProgressUpdating(t *testing.T) { -- a := &Awaiter{ -- state: State{ -- work: make(map[protocol.ProgressToken]*workProgress), -- }, +-// TypeDefinition handles the textDocument/typeDefinition request for Go files. +-func TypeDefinition(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) ([]protocol.Location, error) { +- ctx, done := event.Start(ctx, "golang.TypeDefinition") +- defer done() +- +- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) +- if err != nil { +- return nil, err - } -- ctx := context.Background() -- if err := a.onWorkDoneProgressCreate(ctx, &protocol.WorkDoneProgressCreateParams{ -- Token: "foo", -- }); err != nil { -- t.Fatal(err) +- pos, err := pgf.PositionPos(position) +- if err != nil { +- return nil, err - } -- if err := a.onWorkDoneProgressCreate(ctx, &protocol.WorkDoneProgressCreateParams{ -- Token: "bar", -- }); err != nil { -- t.Fatal(err) +- +- // TODO(rfindley): handle type switch implicits correctly here: if the user +- // jumps to the type definition of x in x := y.(type), it makes sense to jump +- // to the type of y. +- _, obj, _ := referencedObject(pkg, pgf, pos) +- if obj == nil { +- return nil, nil - } -- updates := []struct { -- token string -- value interface{} -- }{ -- {"foo", protocol.WorkDoneProgressBegin{Kind: "begin", Title: "foo work"}}, -- {"bar", protocol.WorkDoneProgressBegin{Kind: "begin", Title: "bar work"}}, -- {"foo", protocol.WorkDoneProgressEnd{Kind: "end"}}, -- {"bar", protocol.WorkDoneProgressReport{Kind: "report", Percentage: 42}}, +- +- tname := typeToObject(obj.Type()) +- if tname == nil { +- return nil, fmt.Errorf("no type definition for %s", obj.Name()) - } -- for _, update := range updates { -- params := &protocol.ProgressParams{ -- Token: update.token, -- Value: update.value, -- } -- data, err := json.Marshal(params) -- if err != nil { -- t.Fatal(err) -- } -- var unmarshaled protocol.ProgressParams -- if err := json.Unmarshal(data, &unmarshaled); err != nil { -- t.Fatal(err) -- } -- if err := a.onProgress(ctx, &unmarshaled); err != nil { -- t.Fatal(err) +- +- if !tname.Pos().IsValid() { +- // The only defined types with no position are error and comparable. +- if tname.Name() != "error" && tname.Name() != "comparable" { +- bug.Reportf("unexpected type name with no position: %s", tname) - } +- return nil, nil - } -- if !a.state.work["foo"].complete { -- t.Error("work entry \"foo\" is incomplete, want complete") -- } -- got := *a.state.work["bar"] -- want := workProgress{title: "bar work", percent: 42} -- if got != want { -- t.Errorf("work progress for \"bar\": %v, want %v", got, want) +- +- loc, err := mapPosition(ctx, pkg.FileSet(), snapshot, tname.Pos(), tname.Pos()+token.Pos(len(tname.Name()))) +- if err != nil { +- return nil, err - } +- return []protocol.Location{loc}, nil -} -diff -urN a/gopls/internal/lsp/regtest/expectation.go b/gopls/internal/lsp/regtest/expectation.go ---- a/gopls/internal/lsp/regtest/expectation.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/regtest/expectation.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,807 +0,0 @@ +diff -urN a/gopls/internal/golang/types_format.go b/gopls/internal/golang/types_format.go +--- a/gopls/internal/golang/types_format.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/types_format.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,525 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package regtest +-package golang - -import ( +- "bytes" +- "context" - "fmt" -- "regexp" -- "sort" +- "go/ast" +- "go/doc" +- "go/printer" +- "go/token" +- "go/types" - "strings" - -- "github.com/google/go-cmp/cmp" -- "golang.org/x/tools/gopls/internal/lsp" -- "golang.org/x/tools/gopls/internal/lsp/protocol" --) -- --var ( -- // InitialWorkspaceLoad is an expectation that the workspace initial load has -- // completed. It is verified via workdone reporting. -- InitialWorkspaceLoad = CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromInitialWorkspaceLoad), 1, false) --) -- --// A Verdict is the result of checking an expectation against the current --// editor state. --type Verdict int -- --// Order matters for the following constants: verdicts are sorted in order of --// decisiveness. --const ( -- // Met indicates that an expectation is satisfied by the current state. -- Met Verdict = iota -- // Unmet indicates that an expectation is not currently met, but could be met -- // in the future. -- Unmet -- // Unmeetable indicates that an expectation cannot be satisfied in the -- // future. -- Unmeetable +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/settings" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/tag" +- "golang.org/x/tools/internal/tokeninternal" +- "golang.org/x/tools/internal/typeparams" -) - --func (v Verdict) String() string { -- switch v { -- case Met: -- return "Met" -- case Unmet: -- return "Unmet" -- case Unmeetable: -- return "Unmeetable" +-// FormatType returns the detail and kind for a types.Type. +-func FormatType(typ types.Type, qf types.Qualifier) (detail string, kind protocol.CompletionItemKind) { +- typ = typ.Underlying() +- if types.IsInterface(typ) { +- detail = "interface{...}" +- kind = protocol.InterfaceCompletion +- } else if _, ok := typ.(*types.Struct); ok { +- detail = "struct{...}" +- kind = protocol.StructCompletion +- } else { +- detail = types.TypeString(typ, qf) +- kind = protocol.ClassCompletion - } -- return fmt.Sprintf("unrecognized verdict %d", v) +- return detail, kind -} - --// An Expectation is an expected property of the state of the LSP client. --// The Check function reports whether the property is met. --// --// Expectations are combinators. By composing them, tests may express --// complex expectations in terms of simpler ones. --// --// TODO(rfindley): as expectations are combined, it becomes harder to identify --// why they failed. A better signature for Check would be --// --// func(State) (Verdict, string) --// --// returning a reason for the verdict that can be composed similarly to --// descriptions. --type Expectation struct { -- Check func(State) Verdict -- -- // Description holds a noun-phrase identifying what the expectation checks. -- // -- // TODO(rfindley): revisit existing descriptions to ensure they compose nicely. -- Description string +-type signature struct { +- name, doc string +- typeParams, params, results []string +- variadic bool +- needResultParens bool -} - --// OnceMet returns an Expectation that, once the precondition is met, asserts --// that mustMeet is met. --func OnceMet(precondition Expectation, mustMeets ...Expectation) Expectation { -- check := func(s State) Verdict { -- switch pre := precondition.Check(s); pre { -- case Unmeetable: -- return Unmeetable -- case Met: -- for _, mustMeet := range mustMeets { -- verdict := mustMeet.Check(s) -- if verdict != Met { -- return Unmeetable -- } -- } -- return Met -- default: -- return Unmet +-func (s *signature) Format() string { +- var b strings.Builder +- b.WriteByte('(') +- for i, p := range s.params { +- if i > 0 { +- b.WriteString(", ") - } +- b.WriteString(p) - } -- description := describeExpectations(mustMeets...) -- return Expectation{ -- Check: check, -- Description: fmt.Sprintf("once %q is met, must have:\n%s", precondition.Description, description), -- } --} +- b.WriteByte(')') - --func describeExpectations(expectations ...Expectation) string { -- var descriptions []string -- for _, e := range expectations { -- descriptions = append(descriptions, e.Description) +- // Add space between parameters and results. +- if len(s.results) > 0 { +- b.WriteByte(' ') - } -- return strings.Join(descriptions, "\n") --} -- --// Not inverts the sense of an expectation: a met expectation is unmet, and an --// unmet expectation is met. --func Not(e Expectation) Expectation { -- check := func(s State) Verdict { -- switch v := e.Check(s); v { -- case Met: -- return Unmet -- case Unmet, Unmeetable: -- return Met -- default: -- panic(fmt.Sprintf("unexpected verdict %v", v)) +- if s.needResultParens { +- b.WriteByte('(') +- } +- for i, r := range s.results { +- if i > 0 { +- b.WriteString(", ") - } +- b.WriteString(r) - } -- description := describeExpectations(e) -- return Expectation{ -- Check: check, -- Description: fmt.Sprintf("not: %s", description), +- if s.needResultParens { +- b.WriteByte(')') - } +- return b.String() -} - --// AnyOf returns an expectation that is satisfied when any of the given --// expectations is met. --func AnyOf(anyOf ...Expectation) Expectation { -- check := func(s State) Verdict { -- for _, e := range anyOf { -- verdict := e.Check(s) -- if verdict == Met { -- return Met -- } +-func (s *signature) TypeParams() []string { +- return s.typeParams +-} +- +-func (s *signature) Params() []string { +- return s.params +-} +- +-// NewBuiltinSignature returns signature for the builtin object with a given +-// name, if a builtin object with the name exists. +-func NewBuiltinSignature(ctx context.Context, s *cache.Snapshot, name string) (*signature, error) { +- builtin, err := s.BuiltinFile(ctx) +- if err != nil { +- return nil, err +- } +- obj := builtin.File.Scope.Lookup(name) +- if obj == nil { +- return nil, fmt.Errorf("no builtin object for %s", name) +- } +- decl, ok := obj.Decl.(*ast.FuncDecl) +- if !ok { +- return nil, fmt.Errorf("no function declaration for builtin: %s", name) +- } +- if decl.Type == nil { +- return nil, fmt.Errorf("no type for builtin decl %s", decl.Name) +- } +- var variadic bool +- if decl.Type.Params.List != nil { +- numParams := len(decl.Type.Params.List) +- lastParam := decl.Type.Params.List[numParams-1] +- if _, ok := lastParam.Type.(*ast.Ellipsis); ok { +- variadic = true - } -- return Unmet - } -- description := describeExpectations(anyOf...) -- return Expectation{ -- Check: check, -- Description: fmt.Sprintf("Any of:\n%s", description), +- fset := tokeninternal.FileSetFor(builtin.Tok) +- params, _ := formatFieldList(ctx, fset, decl.Type.Params, variadic) +- results, needResultParens := formatFieldList(ctx, fset, decl.Type.Results, false) +- d := decl.Doc.Text() +- switch s.Options().HoverKind { +- case settings.SynopsisDocumentation: +- d = doc.Synopsis(d) +- case settings.NoDocumentation: +- d = "" - } +- return &signature{ +- doc: d, +- name: name, +- needResultParens: needResultParens, +- params: params, +- results: results, +- variadic: variadic, +- }, nil -} - --// AllOf expects that all given expectations are met. --// --// TODO(rfindley): the problem with these types of combinators (OnceMet, AnyOf --// and AllOf) is that we lose the information of *why* they failed: the Awaiter --// is not smart enough to look inside. --// --// Refactor the API such that the Check function is responsible for explaining --// why an expectation failed. This should allow us to significantly improve --// test output: we won't need to summarize state at all, as the verdict --// explanation itself should describe clearly why the expectation not met. --func AllOf(allOf ...Expectation) Expectation { -- check := func(s State) Verdict { -- verdict := Met -- for _, e := range allOf { -- if v := e.Check(s); v > verdict { -- verdict = v +-// replacer replaces some synthetic "type classes" used in the builtin file +-// with their most common constituent type. +-var replacer = strings.NewReplacer( +- `ComplexType`, `complex128`, +- `FloatType`, `float64`, +- `IntegerType`, `int`, +-) +- +-func formatFieldList(ctx context.Context, fset *token.FileSet, list *ast.FieldList, variadic bool) ([]string, bool) { +- if list == nil { +- return nil, false +- } +- var writeResultParens bool +- var result []string +- for i := 0; i < len(list.List); i++ { +- if i >= 1 { +- writeResultParens = true +- } +- p := list.List[i] +- cfg := printer.Config{Mode: printer.UseSpaces | printer.TabIndent, Tabwidth: 4} +- b := &bytes.Buffer{} +- if err := cfg.Fprint(b, fset, p.Type); err != nil { +- event.Error(ctx, "unable to print type", nil, tag.Type.Of(p.Type)) +- continue +- } +- typ := replacer.Replace(b.String()) +- if len(p.Names) == 0 { +- result = append(result, typ) +- } +- for _, name := range p.Names { +- if name.Name != "" { +- if i == 0 { +- writeResultParens = true +- } +- result = append(result, fmt.Sprintf("%s %s", name.Name, typ)) +- } else { +- result = append(result, typ) - } - } -- return verdict - } -- description := describeExpectations(allOf...) -- return Expectation{ -- Check: check, -- Description: fmt.Sprintf("All of:\n%s", description), +- if variadic { +- result[len(result)-1] = strings.Replace(result[len(result)-1], "[]", "...", 1) - } +- return result, writeResultParens -} - --// ReadDiagnostics is an Expectation that stores the current diagnostics for --// fileName in into, whenever it is evaluated. --// --// It can be used in combination with OnceMet or AfterChange to capture the --// state of diagnostics when other expectations are satisfied. --func ReadDiagnostics(fileName string, into *protocol.PublishDiagnosticsParams) Expectation { -- check := func(s State) Verdict { -- diags, ok := s.diagnostics[fileName] -- if !ok { -- return Unmeetable -- } -- *into = *diags -- return Met +-// FormatTypeParams turns TypeParamList into its Go representation, such as: +-// [T, Y]. Note that it does not print constraints as this is mainly used for +-// formatting type params in method receivers. +-func FormatTypeParams(tparams *types.TypeParamList) string { +- if tparams == nil || tparams.Len() == 0 { +- return "" - } -- return Expectation{ -- Check: check, -- Description: fmt.Sprintf("read diagnostics for %q", fileName), +- var buf bytes.Buffer +- buf.WriteByte('[') +- for i := 0; i < tparams.Len(); i++ { +- if i > 0 { +- buf.WriteString(", ") +- } +- buf.WriteString(tparams.At(i).Obj().Name()) - } +- buf.WriteByte(']') +- return buf.String() -} - --// ReadAllDiagnostics is an expectation that stores all published diagnostics --// into the provided map, whenever it is evaluated. --// --// It can be used in combination with OnceMet or AfterChange to capture the --// state of diagnostics when other expectations are satisfied. --func ReadAllDiagnostics(into *map[string]*protocol.PublishDiagnosticsParams) Expectation { -- check := func(s State) Verdict { -- allDiags := make(map[string]*protocol.PublishDiagnosticsParams) -- for name, diags := range s.diagnostics { -- allDiags[name] = diags -- } -- *into = allDiags -- return Met +-// NewSignature returns formatted signature for a types.Signature struct. +-func NewSignature(ctx context.Context, s *cache.Snapshot, pkg *cache.Package, sig *types.Signature, comment *ast.CommentGroup, qf types.Qualifier, mq MetadataQualifier) (*signature, error) { +- var tparams []string +- tpList := sig.TypeParams() +- for i := 0; i < tpList.Len(); i++ { +- tparam := tpList.At(i) +- // TODO: is it possible to reuse the logic from FormatVarType here? +- s := tparam.Obj().Name() + " " + tparam.Constraint().String() +- tparams = append(tparams, s) - } -- return Expectation{ -- Check: check, -- Description: "read all diagnostics", +- +- params := make([]string, 0, sig.Params().Len()) +- for i := 0; i < sig.Params().Len(); i++ { +- el := sig.Params().At(i) +- typ, err := FormatVarType(ctx, s, pkg, el, qf, mq) +- if err != nil { +- return nil, err +- } +- p := typ +- if el.Name() != "" { +- p = el.Name() + " " + typ +- } +- params = append(params, p) - } --} - --// ShownDocument asserts that the client has received a --// ShowDocumentRequest for the given URI. --func ShownDocument(uri protocol.URI) Expectation { -- check := func(s State) Verdict { -- for _, params := range s.showDocument { -- if params.URI == uri { -- return Met +- var needResultParens bool +- results := make([]string, 0, sig.Results().Len()) +- for i := 0; i < sig.Results().Len(); i++ { +- if i >= 1 { +- needResultParens = true +- } +- el := sig.Results().At(i) +- typ, err := FormatVarType(ctx, s, pkg, el, qf, mq) +- if err != nil { +- return nil, err +- } +- if el.Name() == "" { +- results = append(results, typ) +- } else { +- if i == 0 { +- needResultParens = true - } +- results = append(results, el.Name()+" "+typ) - } -- return Unmet - } -- return Expectation{ -- Check: check, -- Description: fmt.Sprintf("received window/showDocument for URI %s", uri), +- var d string +- if comment != nil { +- d = comment.Text() +- } +- switch s.Options().HoverKind { +- case settings.SynopsisDocumentation: +- d = doc.Synopsis(d) +- case settings.NoDocumentation: +- d = "" - } +- return &signature{ +- doc: d, +- typeParams: tparams, +- params: params, +- results: results, +- variadic: sig.Variadic(), +- needResultParens: needResultParens, +- }, nil -} - --// NoShownMessage asserts that the editor has not received a ShowMessage. --func NoShownMessage(subString string) Expectation { -- check := func(s State) Verdict { -- for _, m := range s.showMessage { -- if strings.Contains(m.Message, subString) { -- return Unmeetable -- } -- } -- return Met +-// FormatVarType formats a *types.Var, accounting for type aliases. +-// To do this, it looks in the AST of the file in which the object is declared. +-// On any errors, it always falls back to types.TypeString. +-// +-// TODO(rfindley): this function could return the actual name used in syntax, +-// for better parameter names. +-func FormatVarType(ctx context.Context, snapshot *cache.Snapshot, srcpkg *cache.Package, obj *types.Var, qf types.Qualifier, mq MetadataQualifier) (string, error) { +- // TODO(rfindley): This looks wrong. The previous comment said: +- // "If the given expr refers to a type parameter, then use the +- // object's Type instead of the type parameter declaration. This helps +- // format the instantiated type as opposed to the original undeclared +- // generic type". +- // +- // But of course, if obj is a type param, we are formatting a generic type +- // and not an instantiated type. Handling for instantiated types must be done +- // at a higher level. +- // +- // Left this during refactoring in order to preserve pre-existing logic. +- if typeparams.IsTypeParam(obj.Type()) { +- return types.TypeString(obj.Type(), qf), nil - } -- return Expectation{ -- Check: check, -- Description: fmt.Sprintf("no ShowMessage received containing %q", subString), +- +- if obj.Pkg() == nil || !obj.Pos().IsValid() { +- // This is defensive, though it is extremely unlikely we'll ever have a +- // builtin var. +- return types.TypeString(obj.Type(), qf), nil - } --} - --// ShownMessage asserts that the editor has received a ShowMessageRequest --// containing the given substring. --func ShownMessage(containing string) Expectation { -- check := func(s State) Verdict { -- for _, m := range s.showMessage { -- if strings.Contains(m.Message, containing) { -- return Met -- } -- } -- return Unmet +- // TODO(rfindley): parsing to produce candidates can be costly; consider +- // using faster methods. +- targetpgf, pos, err := parseFull(ctx, snapshot, srcpkg.FileSet(), obj.Pos()) +- if err != nil { +- return "", err // e.g. ctx cancelled - } -- return Expectation{ -- Check: check, -- Description: fmt.Sprintf("received window/showMessage containing %q", containing), +- +- targetMeta := findFileInDeps(snapshot, srcpkg.Metadata(), targetpgf.URI) +- if targetMeta == nil { +- // If we have an object from type-checking, it should exist in a file in +- // the forward transitive closure. +- return "", bug.Errorf("failed to find file %q in deps of %q", targetpgf.URI, srcpkg.Metadata().ID) - } --} - --// ShownMessageRequest asserts that the editor has received a --// ShowMessageRequest with message matching the given regular expression. --func ShownMessageRequest(messageRegexp string) Expectation { -- msgRE := regexp.MustCompile(messageRegexp) -- check := func(s State) Verdict { -- if len(s.showMessageRequest) == 0 { -- return Unmet +- decl, spec, field := findDeclInfo([]*ast.File{targetpgf.File}, pos) +- +- // We can't handle type parameters correctly, so we fall back on TypeString +- // for parameterized decls. +- if decl, _ := decl.(*ast.FuncDecl); decl != nil { +- if decl.Type.TypeParams.NumFields() > 0 { +- return types.TypeString(obj.Type(), qf), nil // in generic function - } -- for _, m := range s.showMessageRequest { -- if msgRE.MatchString(m.Message) { -- return Met +- if decl.Recv != nil && len(decl.Recv.List) > 0 { +- rtype := decl.Recv.List[0].Type +- if e, ok := rtype.(*ast.StarExpr); ok { +- rtype = e.X +- } +- if x, _, _, _ := typeparams.UnpackIndexExpr(rtype); x != nil { +- return types.TypeString(obj.Type(), qf), nil // in method of generic type - } - } -- return Unmet -- } -- return Expectation{ -- Check: check, -- Description: fmt.Sprintf("ShowMessageRequest matching %q", messageRegexp), - } --} -- --// DoneDiagnosingChanges expects that diagnostics are complete from common --// change notifications: didOpen, didChange, didSave, didChangeWatchedFiles, --// and didClose. --// --// This can be used when multiple notifications may have been sent, such as --// when a didChange is immediately followed by a didSave. It is insufficient to --// simply await NoOutstandingWork, because the LSP client has no control over --// when the server starts processing a notification. Therefore, we must keep --// track of --func (e *Env) DoneDiagnosingChanges() Expectation { -- stats := e.Editor.Stats() -- statsBySource := map[lsp.ModificationSource]uint64{ -- lsp.FromDidOpen: stats.DidOpen, -- lsp.FromDidChange: stats.DidChange, -- lsp.FromDidSave: stats.DidSave, -- lsp.FromDidChangeWatchedFiles: stats.DidChangeWatchedFiles, -- lsp.FromDidClose: stats.DidClose, -- lsp.FromDidChangeConfiguration: stats.DidChangeConfiguration, +- if spec, _ := spec.(*ast.TypeSpec); spec != nil && spec.TypeParams.NumFields() > 0 { +- return types.TypeString(obj.Type(), qf), nil // in generic type decl - } - -- var expected []lsp.ModificationSource -- for k, v := range statsBySource { -- if v > 0 { -- expected = append(expected, k) -- } +- if field == nil { +- // TODO(rfindley): we should never reach here from an ordinary var, so +- // should probably return an error here. +- return types.TypeString(obj.Type(), qf), nil - } +- expr := field.Type - -- // Sort for stability. -- sort.Slice(expected, func(i, j int) bool { -- return expected[i] < expected[j] -- }) +- rq := requalifier(snapshot, targetpgf.File, targetMeta, mq) - -- var all []Expectation -- for _, source := range expected { -- all = append(all, CompletedWork(lsp.DiagnosticWorkTitle(source), statsBySource[source], true)) -- } +- // The type names in the AST may not be correctly qualified. +- // Determine the package name to use based on the package that originated +- // the query and the package in which the type is declared. +- // We then qualify the value by cloning the AST node and editing it. +- expr = qualifyTypeExpr(expr, rq) - -- return AllOf(all...) +- // If the request came from a different package than the one in which the +- // types are defined, we may need to modify the qualifiers. +- return FormatNodeFile(targetpgf.Tok, expr), nil -} - --// AfterChange expects that the given expectations will be met after all --// state-changing notifications have been processed by the server. +-// qualifyTypeExpr clones the type expression expr after re-qualifying type +-// names using the given function, which accepts the current syntactic +-// qualifier (possibly "" for unqualified idents), and returns a new qualifier +-// (again, possibly "" if the identifier should be unqualified). -// --// It awaits the completion of all anticipated work before checking the given --// expectations. --func (e *Env) AfterChange(expectations ...Expectation) { -- e.T.Helper() -- e.OnceMet( -- e.DoneDiagnosingChanges(), -- expectations..., -- ) --} +-// The resulting expression may be inaccurate: without type-checking we don't +-// properly account for "." imported identifiers or builtins. +-// +-// TODO(rfindley): add many more tests for this function. +-func qualifyTypeExpr(expr ast.Expr, qf func(string) string) ast.Expr { +- switch expr := expr.(type) { +- case *ast.ArrayType: +- return &ast.ArrayType{ +- Lbrack: expr.Lbrack, +- Elt: qualifyTypeExpr(expr.Elt, qf), +- Len: expr.Len, +- } - --// DoneWithOpen expects all didOpen notifications currently sent by the editor --// to be completely processed. --func (e *Env) DoneWithOpen() Expectation { -- opens := e.Editor.Stats().DidOpen -- return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidOpen), opens, true) --} +- case *ast.BinaryExpr: +- if expr.Op != token.OR { +- return expr +- } +- return &ast.BinaryExpr{ +- X: qualifyTypeExpr(expr.X, qf), +- OpPos: expr.OpPos, +- Op: expr.Op, +- Y: qualifyTypeExpr(expr.Y, qf), +- } - --// StartedChange expects that the server has at least started processing all --// didChange notifications sent from the client. --func (e *Env) StartedChange() Expectation { -- changes := e.Editor.Stats().DidChange -- return StartedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), changes) --} +- case *ast.ChanType: +- return &ast.ChanType{ +- Arrow: expr.Arrow, +- Begin: expr.Begin, +- Dir: expr.Dir, +- Value: qualifyTypeExpr(expr.Value, qf), +- } - --// DoneWithChange expects all didChange notifications currently sent by the --// editor to be completely processed. --func (e *Env) DoneWithChange() Expectation { -- changes := e.Editor.Stats().DidChange -- return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChange), changes, true) --} +- case *ast.Ellipsis: +- return &ast.Ellipsis{ +- Ellipsis: expr.Ellipsis, +- Elt: qualifyTypeExpr(expr.Elt, qf), +- } - --// DoneWithSave expects all didSave notifications currently sent by the editor --// to be completely processed. --func (e *Env) DoneWithSave() Expectation { -- saves := e.Editor.Stats().DidSave -- return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidSave), saves, true) --} +- case *ast.FuncType: +- return &ast.FuncType{ +- Func: expr.Func, +- Params: qualifyFieldList(expr.Params, qf), +- Results: qualifyFieldList(expr.Results, qf), +- } - --// StartedChangeWatchedFiles expects that the server has at least started --// processing all didChangeWatchedFiles notifications sent from the client. --func (e *Env) StartedChangeWatchedFiles() Expectation { -- changes := e.Editor.Stats().DidChangeWatchedFiles -- return StartedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), changes) --} +- case *ast.Ident: +- // Unqualified type (builtin, package local, or dot-imported). - --// DoneWithChangeWatchedFiles expects all didChangeWatchedFiles notifications --// currently sent by the editor to be completely processed. --func (e *Env) DoneWithChangeWatchedFiles() Expectation { -- changes := e.Editor.Stats().DidChangeWatchedFiles -- return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidChangeWatchedFiles), changes, true) --} +- // Don't qualify names that look like builtins. +- // +- // Without type-checking this may be inaccurate. It could be made accurate +- // by doing syntactic object resolution for the entire package, but that +- // does not seem worthwhile and we generally want to avoid using +- // ast.Object, which may be inaccurate. +- if obj := types.Universe.Lookup(expr.Name); obj != nil { +- return expr +- } - --// DoneWithClose expects all didClose notifications currently sent by the --// editor to be completely processed. --func (e *Env) DoneWithClose() Expectation { -- changes := e.Editor.Stats().DidClose -- return CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromDidClose), changes, true) --} +- newName := qf("") +- if newName != "" { +- return &ast.SelectorExpr{ +- X: &ast.Ident{ +- NamePos: expr.Pos(), +- Name: newName, +- }, +- Sel: expr, +- } +- } +- return expr - --// StartedWork expect a work item to have been started >= atLeast times. --// --// See CompletedWork. --func StartedWork(title string, atLeast uint64) Expectation { -- check := func(s State) Verdict { -- if s.startedWork()[title] >= atLeast { -- return Met +- case *ast.IndexExpr: +- return &ast.IndexExpr{ +- X: qualifyTypeExpr(expr.X, qf), +- Lbrack: expr.Lbrack, +- Index: qualifyTypeExpr(expr.Index, qf), +- Rbrack: expr.Rbrack, - } -- return Unmet -- } -- return Expectation{ -- Check: check, -- Description: fmt.Sprintf("started work %q at least %d time(s)", title, atLeast), -- } --} - --// CompletedWork expects a work item to have been completed >= atLeast times. --// --// Since the Progress API doesn't include any hidden metadata, we must use the --// progress notification title to identify the work we expect to be completed. --func CompletedWork(title string, count uint64, atLeast bool) Expectation { -- check := func(s State) Verdict { -- completed := s.completedWork() -- if completed[title] == count || atLeast && completed[title] > count { -- return Met +- case *ast.IndexListExpr: +- indices := make([]ast.Expr, len(expr.Indices)) +- for i, idx := range expr.Indices { +- indices[i] = qualifyTypeExpr(idx, qf) +- } +- return &ast.IndexListExpr{ +- X: qualifyTypeExpr(expr.X, qf), +- Lbrack: expr.Lbrack, +- Indices: indices, +- Rbrack: expr.Rbrack, - } -- return Unmet -- } -- desc := fmt.Sprintf("completed work %q %v times", title, count) -- if atLeast { -- desc = fmt.Sprintf("completed work %q at least %d time(s)", title, count) -- } -- return Expectation{ -- Check: check, -- Description: desc, -- } --} - --type WorkStatus struct { -- // Last seen message from either `begin` or `report` progress. -- Msg string -- // Message sent with `end` progress message. -- EndMsg string --} +- case *ast.InterfaceType: +- return &ast.InterfaceType{ +- Interface: expr.Interface, +- Methods: qualifyFieldList(expr.Methods, qf), +- Incomplete: expr.Incomplete, +- } - --// CompletedProgress expects that workDone progress is complete for the given --// progress token. When non-nil WorkStatus is provided, it will be filled --// when the expectation is met. --// --// If the token is not a progress token that the client has seen, this --// expectation is Unmeetable. --func CompletedProgress(token protocol.ProgressToken, into *WorkStatus) Expectation { -- check := func(s State) Verdict { -- work, ok := s.work[token] -- if !ok { -- return Unmeetable // TODO(rfindley): refactor to allow the verdict to explain this result +- case *ast.MapType: +- return &ast.MapType{ +- Map: expr.Map, +- Key: qualifyTypeExpr(expr.Key, qf), +- Value: qualifyTypeExpr(expr.Value, qf), - } -- if work.complete { -- if into != nil { -- into.Msg = work.msg -- into.EndMsg = work.endMsg -- } -- return Met +- +- case *ast.ParenExpr: +- return &ast.ParenExpr{ +- Lparen: expr.Lparen, +- Rparen: expr.Rparen, +- X: qualifyTypeExpr(expr.X, qf), - } -- return Unmet -- } -- desc := fmt.Sprintf("completed work for token %v", token) -- return Expectation{ -- Check: check, -- Description: desc, -- } --} - --// OutstandingWork expects a work item to be outstanding. The given title must --// be an exact match, whereas the given msg must only be contained in the work --// item's message. --func OutstandingWork(title, msg string) Expectation { -- check := func(s State) Verdict { -- for _, work := range s.work { -- if work.complete { -- continue +- case *ast.SelectorExpr: +- if id, ok := expr.X.(*ast.Ident); ok { +- // qualified type +- newName := qf(id.Name) +- if newName == "" { +- return expr.Sel - } -- if work.title == title && strings.Contains(work.msg, msg) { -- return Met +- return &ast.SelectorExpr{ +- X: &ast.Ident{ +- NamePos: id.NamePos, +- Name: newName, +- }, +- Sel: expr.Sel, - } - } -- return Unmet -- } -- return Expectation{ -- Check: check, -- Description: fmt.Sprintf("outstanding work: %q containing %q", title, msg), +- return expr +- +- case *ast.StarExpr: +- return &ast.StarExpr{ +- Star: expr.Star, +- X: qualifyTypeExpr(expr.X, qf), +- } +- +- case *ast.StructType: +- return &ast.StructType{ +- Struct: expr.Struct, +- Fields: qualifyFieldList(expr.Fields, qf), +- Incomplete: expr.Incomplete, +- } +- +- default: +- return expr - } -} - --// NoOutstandingWork asserts that there is no work initiated using the LSP --// $/progress API that has not completed. --// --// If non-nil, the ignore func is used to ignore certain work items for the --// purpose of this check. --// --// TODO(rfindley): consider refactoring to treat outstanding work the same way --// we treat diagnostics: with an algebra of filters. --func NoOutstandingWork(ignore func(title, msg string) bool) Expectation { -- check := func(s State) Verdict { -- for _, w := range s.work { -- if w.complete { -- continue -- } -- if w.title == "" { -- // A token that has been created but not yet used. -- // -- // TODO(rfindley): this should be separated in the data model: until -- // the "begin" notification, work should not be in progress. -- continue -- } -- if ignore(w.title, w.msg) { -- continue -- } -- return Unmet +-func qualifyFieldList(fl *ast.FieldList, qf func(string) string) *ast.FieldList { +- if fl == nil { +- return nil +- } +- if fl.List == nil { +- return &ast.FieldList{ +- Closing: fl.Closing, +- Opening: fl.Opening, - } -- return Met - } -- return Expectation{ -- Check: check, -- Description: "no outstanding work", +- list := make([]*ast.Field, 0, len(fl.List)) +- for _, f := range fl.List { +- list = append(list, &ast.Field{ +- Comment: f.Comment, +- Doc: f.Doc, +- Names: f.Names, +- Tag: f.Tag, +- Type: qualifyTypeExpr(f.Type, qf), +- }) +- } +- return &ast.FieldList{ +- Closing: fl.Closing, +- Opening: fl.Opening, +- List: list, - } -} +diff -urN a/gopls/internal/golang/util.go b/gopls/internal/golang/util.go +--- a/gopls/internal/golang/util.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/util.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,366 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// IgnoreTelemetryPromptWork may be used in conjunction with NoOutStandingWork --// to ignore the telemetry prompt. --func IgnoreTelemetryPromptWork(title, msg string) bool { -- return title == lsp.TelemetryPromptWorkTitle --} +-package golang - --// NoErrorLogs asserts that the client has not received any log messages of --// error severity. --func NoErrorLogs() Expectation { -- return NoLogMatching(protocol.Error, "") --} +-import ( +- "context" +- "go/ast" +- "go/printer" +- "go/token" +- "go/types" +- "regexp" +- "strings" - --// LogMatching asserts that the client has received a log message --// of type typ matching the regexp re a certain number of times. --// --// The count argument specifies the expected number of matching logs. If --// atLeast is set, this is a lower bound, otherwise there must be exactly count --// matching logs. +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/astutil" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/internal/tokeninternal" +-) +- +-// IsGenerated gets and reads the file denoted by uri and reports +-// whether it contains a "generated file" comment as described at +-// https://golang.org/s/generatedcode. -// --// Logs are asynchronous to other LSP messages, so this expectation should not --// be used with combinators such as OnceMet or AfterChange that assert on --// ordering with respect to other operations. --func LogMatching(typ protocol.MessageType, re string, count int, atLeast bool) Expectation { -- rec, err := regexp.Compile(re) +-// TODO(adonovan): opt: this function does too much. +-// Move snapshot.ReadFile into the caller (most of which have already done it). +-func IsGenerated(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI) bool { +- fh, err := snapshot.ReadFile(ctx, uri) - if err != nil { -- panic(err) +- return false - } -- check := func(state State) Verdict { -- var found int -- for _, msg := range state.logs { -- if msg.Type == typ && rec.Match([]byte(msg.Message)) { -- found++ +- pgf, err := snapshot.ParseGo(ctx, fh, parsego.Header) +- if err != nil { +- return false +- } +- for _, commentGroup := range pgf.File.Comments { +- for _, comment := range commentGroup.List { +- if matched := generatedRx.MatchString(comment.Text); matched { +- // Check if comment is at the beginning of the line in source. +- if safetoken.Position(pgf.Tok, comment.Slash).Column == 1 { +- return true +- } - } - } -- // Check for an exact or "at least" match. -- if found == count || (found >= count && atLeast) { -- return Met -- } -- // If we require an exact count, and have received more than expected, the -- // expectation can never be met. -- if found > count && !atLeast { -- return Unmeetable +- } +- return false +-} +- +-// adjustedObjEnd returns the end position of obj, possibly modified for +-// package names. +-// +-// TODO(rfindley): eliminate this function, by inlining it at callsites where +-// it makes sense. +-func adjustedObjEnd(obj types.Object) token.Pos { +- nameLen := len(obj.Name()) +- if pkgName, ok := obj.(*types.PkgName); ok { +- // An imported Go package has a package-local, unqualified name. +- // When the name matches the imported package name, there is no +- // identifier in the import spec with the local package name. +- // +- // For example: +- // import "go/ast" // name "ast" matches package name +- // import a "go/ast" // name "a" does not match package name +- // +- // When the identifier does not appear in the source, have the range +- // of the object be the import path, including quotes. +- if pkgName.Imported().Name() == pkgName.Name() { +- nameLen = len(pkgName.Imported().Path()) + len(`""`) - } -- return Unmet - } -- desc := fmt.Sprintf("log message matching %q expected %v times", re, count) -- if atLeast { -- desc = fmt.Sprintf("log message matching %q expected at least %v times", re, count) +- return obj.Pos() + token.Pos(nameLen) +-} +- +-// Matches cgo generated comment as well as the proposed standard: +-// +-// https://golang.org/s/generatedcode +-var generatedRx = regexp.MustCompile(`// .*DO NOT EDIT\.?`) +- +-// nodeAtPos returns the index and the node whose position is contained inside +-// the node list. +-func nodeAtPos(nodes []ast.Node, pos token.Pos) (ast.Node, int) { +- if nodes == nil { +- return nil, -1 - } -- return Expectation{ -- Check: check, -- Description: desc, +- for i, node := range nodes { +- if node.Pos() <= pos && pos <= node.End() { +- return node, i +- } - } +- return nil, -1 -} - --// NoLogMatching asserts that the client has not received a log message --// of type typ matching the regexp re. If re is an empty string, any log --// message is considered a match. --func NoLogMatching(typ protocol.MessageType, re string) Expectation { -- var r *regexp.Regexp -- if re != "" { -- var err error -- r, err = regexp.Compile(re) -- if err != nil { -- panic(err) -- } +-// FormatNode returns the "pretty-print" output for an ast node. +-func FormatNode(fset *token.FileSet, n ast.Node) string { +- var buf strings.Builder +- if err := printer.Fprint(&buf, fset, n); err != nil { +- // TODO(rfindley): we should use bug.Reportf here. +- // We encounter this during completion.resolveInvalid. +- return "" - } -- check := func(state State) Verdict { -- for _, msg := range state.logs { -- if msg.Type != typ { +- return buf.String() +-} +- +-// FormatNodeFile is like FormatNode, but requires only the token.File for the +-// syntax containing the given ast node. +-func FormatNodeFile(file *token.File, n ast.Node) string { +- fset := tokeninternal.FileSetFor(file) +- return FormatNode(fset, n) +-} +- +-// findFileInDeps finds package metadata containing URI in the transitive +-// dependencies of m. When using the Go command, the answer is unique. +-func findFileInDeps(s metadata.Source, mp *metadata.Package, uri protocol.DocumentURI) *metadata.Package { +- seen := make(map[PackageID]bool) +- var search func(*metadata.Package) *metadata.Package +- search = func(mp *metadata.Package) *metadata.Package { +- if seen[mp.ID] { +- return nil +- } +- seen[mp.ID] = true +- for _, cgf := range mp.CompiledGoFiles { +- if cgf == uri { +- return mp +- } +- } +- for _, dep := range mp.DepsByPkgPath { +- mp := s.Metadata(dep) +- if mp == nil { +- bug.Reportf("nil metadata for %q", dep) - continue - } -- if r == nil || r.Match([]byte(msg.Message)) { -- return Unmeetable +- if found := search(mp); found != nil { +- return found - } - } -- return Met -- } -- return Expectation{ -- Check: check, -- Description: fmt.Sprintf("no log message matching %q", re), +- return nil - } +- return search(mp) -} - --// FileWatchMatching expects that a file registration matches re. --func FileWatchMatching(re string) Expectation { -- return Expectation{ -- Check: checkFileWatch(re, Met, Unmet), -- Description: fmt.Sprintf("file watch matching %q", re), +-// CollectScopes returns all scopes in an ast path, ordered as innermost scope +-// first. +-func CollectScopes(info *types.Info, path []ast.Node, pos token.Pos) []*types.Scope { +- // scopes[i], where i import path mapping. +- inverseDeps := make(map[PackageID]PackagePath) +- for path, id := range mp.DepsByPkgPath { +- inverseDeps[id] = path - } -- return Expectation{ -- Check: check, -- Description: "any diagnostics " + strings.Join(descs, ", "), +- importsByPkgPath := make(map[PackagePath]ImportPath) // best import paths by pkgPath +- for impPath, id := range mp.DepsByImpPath { +- if id == "" { +- continue +- } +- pkgPath := inverseDeps[id] +- _, hasPath := importsByPkgPath[pkgPath] +- _, hasImp := localNames[impPath] +- // In rare cases, there may be multiple import paths with the same package +- // path. In such scenarios, prefer an import path that already exists in +- // the file. +- if !hasPath || hasImp { +- importsByPkgPath[pkgPath] = impPath +- } - } --} - --// NoDiagnostics asserts that there are no diagnostics matching the given --// filters. Notably, if no filters are supplied this assertion checks that --// there are no diagnostics at all, for any file. --func NoDiagnostics(filters ...DiagnosticFilter) Expectation { -- check := func(s State) Verdict { -- diags := flattenDiagnostics(s) -- for _, filter := range filters { -- var filtered []flatDiagnostic -- for _, d := range diags { -- if filter.check(d.name, d.diag) { -- filtered = append(filtered, d) -- } +- return func(pkgName PackageName, impPath ImportPath, pkgPath PackagePath) string { +- // If supplied, translate the package path to an import path in the source +- // package. +- if pkgPath != "" { +- if srcImp := importsByPkgPath[pkgPath]; srcImp != "" { +- impPath = srcImp +- } +- if pkgPath == mp.PkgPath { +- return "" - } -- diags = filtered - } -- if len(diags) > 0 { -- return Unmet +- if localName, ok := localNames[impPath]; ok && impPath != "" { +- return localName - } -- return Met -- } -- var descs []string -- for _, filter := range filters { -- descs = append(descs, filter.desc) -- } -- return Expectation{ -- Check: check, -- Description: "no diagnostics " + strings.Join(descs, ", "), -- } --} -- --type flatDiagnostic struct { -- name string -- diag protocol.Diagnostic --} -- --func flattenDiagnostics(state State) []flatDiagnostic { -- var result []flatDiagnostic -- for name, diags := range state.diagnostics { -- for _, diag := range diags.Diagnostics { -- result = append(result, flatDiagnostic{name, diag}) +- if pkgName != "" { +- return string(pkgName) - } +- idx := strings.LastIndexByte(string(impPath), '/') +- return string(impPath[idx+1:]) - } -- return result -} - --// -- Diagnostic filters -- -- --// A DiagnosticFilter filters the set of diagnostics, for assertion with --// Diagnostics or NoDiagnostics. --type DiagnosticFilter struct { -- desc string -- check func(name string, _ protocol.Diagnostic) bool --} +-// importInfo collects information about the import specified by imp, +-// extracting its file-local name, package name, import path, and package path. +-// +-// If metadata is missing for the import, the resulting package name and +-// package path may be empty, and the file local name may be guessed based on +-// the import path. +-// +-// Note: previous versions of this helper used a PackageID->PackagePath map +-// extracted from m, for extracting package path even in the case where +-// metadata for a dep was missing. This should not be necessary, as we should +-// always have metadata for IDs contained in DepsByPkgPath. +-func importInfo(s metadata.Source, imp *ast.ImportSpec, mp *metadata.Package) (string, PackageName, ImportPath, PackagePath) { +- var ( +- name string // local name +- pkgName PackageName +- impPath = metadata.UnquoteImportPath(imp) +- pkgPath PackagePath +- ) - --// ForFile filters to diagnostics matching the sandbox-relative file name. --func ForFile(name string) DiagnosticFilter { -- return DiagnosticFilter{ -- desc: fmt.Sprintf("for file %q", name), -- check: func(diagName string, _ protocol.Diagnostic) bool { -- return diagName == name -- }, +- // If the import has a local name, use it. +- if imp.Name != nil { +- name = imp.Name.Name - } --} - --// FromSource filters to diagnostics matching the given diagnostics source. --func FromSource(source string) DiagnosticFilter { -- return DiagnosticFilter{ -- desc: fmt.Sprintf("with source %q", source), -- check: func(_ string, d protocol.Diagnostic) bool { -- return d.Source == source -- }, +- // Try to find metadata for the import. If successful and there is no local +- // name, the package name is the local name. +- if depID := mp.DepsByImpPath[impPath]; depID != "" { +- if depMP := s.Metadata(depID); depMP != nil { +- if name == "" { +- name = string(depMP.Name) +- } +- pkgName = depMP.Name +- pkgPath = depMP.PkgPath +- } - } --} - --// AtRegexp filters to diagnostics in the file with sandbox-relative path name, --// at the first position matching the given regexp pattern. --// --// TODO(rfindley): pass in the editor to expectations, so that they may depend --// on editor state and AtRegexp can be a function rather than a method. --func (e *Env) AtRegexp(name, pattern string) DiagnosticFilter { -- loc := e.RegexpSearch(name, pattern) -- return DiagnosticFilter{ -- desc: fmt.Sprintf("at the first position matching %#q in %q", pattern, name), -- check: func(diagName string, d protocol.Diagnostic) bool { -- return diagName == name && d.Range.Start == loc.Range.Start -- }, +- // If the local name is still unknown, guess it based on the import path. +- if name == "" { +- idx := strings.LastIndexByte(string(impPath), '/') +- name = string(impPath[idx+1:]) - } +- return name, pkgName, impPath, pkgPath -} - --// AtPosition filters to diagnostics at location name:line:character, for a --// sandbox-relative path name. --// --// Line and character are 0-based, and character measures UTF-16 codes. +-// isDirective reports whether c is a comment directive. -// --// Note: prefer the more readable AtRegexp. --func AtPosition(name string, line, character uint32) DiagnosticFilter { -- pos := protocol.Position{Line: line, Character: character} -- return DiagnosticFilter{ -- desc: fmt.Sprintf("at %s:%d:%d", name, line, character), -- check: func(diagName string, d protocol.Diagnostic) bool { -- return diagName == name && d.Range.Start == pos -- }, +-// Copied and adapted from go/src/go/ast/ast.go. +-func isDirective(c string) bool { +- if len(c) < 3 { +- return false +- } +- if c[1] != '/' { +- return false +- } +- //-style comment (no newline at the end) +- c = c[2:] +- if len(c) == 0 { +- // empty line +- return false +- } +- // "//line " is a line directive. +- // (The // has been removed.) +- if strings.HasPrefix(c, "line ") { +- return true - } --} - --// WithMessage filters to diagnostics whose message contains the given --// substring. --func WithMessage(substring string) DiagnosticFilter { -- return DiagnosticFilter{ -- desc: fmt.Sprintf("with message containing %q", substring), -- check: func(_ string, d protocol.Diagnostic) bool { -- return strings.Contains(d.Message, substring) -- }, +- // "//[a-z0-9]+:[a-z0-9]" +- // (The // has been removed.) +- colon := strings.Index(c, ":") +- if colon <= 0 || colon+1 >= len(c) { +- return false +- } +- for i := 0; i <= colon+1; i++ { +- if i == colon { +- continue +- } +- b := c[i] +- if !('a' <= b && b <= 'z' || '0' <= b && b <= '9') { +- return false +- } - } +- return true -} - --// WithSeverityTags filters to diagnostics whose severity and tags match --// the given expectation. --func WithSeverityTags(diagName string, severity protocol.DiagnosticSeverity, tags []protocol.DiagnosticTag) DiagnosticFilter { -- return DiagnosticFilter{ -- desc: fmt.Sprintf("with diagnostic %q with severity %q and tag %#q", diagName, severity, tags), -- check: func(_ string, d protocol.Diagnostic) bool { -- return d.Source == diagName && d.Severity == severity && cmp.Equal(d.Tags, tags) -- }, +-// embeddedIdent returns the type name identifier for an embedding x, if x in a +-// valid embedding. Otherwise, it returns nil. +-// +-// Spec: An embedded field must be specified as a type name T or as a pointer +-// to a non-interface type name *T +-func embeddedIdent(x ast.Expr) *ast.Ident { +- if star, ok := x.(*ast.StarExpr); ok { +- x = star.X +- } +- switch ix := x.(type) { // check for instantiated receivers +- case *ast.IndexExpr: +- x = ix.X +- case *ast.IndexListExpr: +- x = ix.X +- } +- switch x := x.(type) { +- case *ast.Ident: +- return x +- case *ast.SelectorExpr: +- if _, ok := x.X.(*ast.Ident); ok { +- return x.Sel +- } - } +- return nil -} -diff -urN a/gopls/internal/lsp/regtest/marker.go b/gopls/internal/lsp/regtest/marker.go ---- a/gopls/internal/lsp/regtest/marker.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/regtest/marker.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,2366 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. +- +-// An importFunc is an implementation of the single-method +-// types.Importer interface based on a function value. +-type ImporterFunc func(path string) (*types.Package, error) +- +-func (f ImporterFunc) Import(path string) (*types.Package, error) { return f(path) } +diff -urN a/gopls/internal/golang/workspace_symbol.go b/gopls/internal/golang/workspace_symbol.go +--- a/gopls/internal/golang/workspace_symbol.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/workspace_symbol.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,526 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package regtest +-package golang - -import ( -- "bytes" - "context" -- "encoding/json" -- "flag" - "fmt" -- "go/token" -- "go/types" -- "io/fs" -- "log" -- "os" -- "path" - "path/filepath" -- "reflect" -- "regexp" - "runtime" - "sort" - "strings" -- "testing" +- "unicode" - -- "github.com/google/go-cmp/cmp" -- "golang.org/x/tools/go/expect" -- "golang.org/x/tools/gopls/internal/hooks" -- "golang.org/x/tools/gopls/internal/lsp/cache" -- "golang.org/x/tools/gopls/internal/lsp/debug" -- "golang.org/x/tools/gopls/internal/lsp/fake" -- "golang.org/x/tools/gopls/internal/lsp/lsprpc" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/lsp/tests" -- "golang.org/x/tools/gopls/internal/lsp/tests/compare" -- "golang.org/x/tools/internal/diff" -- "golang.org/x/tools/internal/jsonrpc2" -- "golang.org/x/tools/internal/jsonrpc2/servertest" -- "golang.org/x/tools/internal/testenv" -- "golang.org/x/tools/txtar" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/settings" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/fuzzy" -) - --var update = flag.Bool("update", false, "if set, update test data during marker tests") +-// maxSymbols defines the maximum number of symbol results that should ever be +-// sent in response to a client. +-const maxSymbols = 100 - --// RunMarkerTests runs "marker" tests in the given test data directory. --// (In practice: ../../regtest/marker/testdata) --// --// Use this command to run the tests: --// --// $ go test ./gopls/internal/regtest/marker [-update] --// --// A marker test uses the '//@' marker syntax of the x/tools/go/expect package --// to annotate source code with various information such as locations and --// arguments of LSP operations to be executed by the test. The syntax following --// '@' is parsed as a comma-separated list of ordinary Go function calls, for --// example --// --// //@foo(a, "b", 3),bar(0) --// --// and delegates to a corresponding function to perform LSP-related operations. --// See the Marker types documentation below for a list of supported markers. --// --// Each call argument is converted to the type of the corresponding parameter of --// the designated function. The conversion logic may use the surrounding context, --// such as the position or nearby text. See the Argument conversion section below --// for the full set of special conversions. As a special case, the blank --// identifier '_' is treated as the zero value of the parameter type. --// --// The test runner collects test cases by searching the given directory for --// files with the .txt extension. Each file is interpreted as a txtar archive, --// which is extracted to a temporary directory. The relative path to the .txt --// file is used as the subtest name. The preliminary section of the file --// (before the first archive entry) is a free-form comment. +-// WorkspaceSymbols matches symbols across all views using the given query, +-// according to the match semantics parameterized by matcherType and style. -// --// These tests were inspired by (and in many places copied from) a previous --// iteration of the marker tests built on top of the packagestest framework. --// Key design decisions motivating this reimplementation are as follows: --// - The old tests had a single global session, causing interaction at a --// distance and several awkward workarounds. --// - The old tests could not be safely parallelized, because certain tests --// manipulated the server options --// - Relatedly, the old tests did not have a logic grouping of assertions into --// a single unit, resulting in clusters of files serving clusters of --// entangled assertions. --// - The old tests used locations in the source as test names and as the --// identity of golden content, meaning that a single edit could change the --// name of an arbitrary number of subtests, and making it difficult to --// manually edit golden content. --// - The old tests did not hew closely to LSP concepts, resulting in, for --// example, each marker implementation doing its own position --// transformations, and inventing its own mechanism for configuration. --// - The old tests had an ad-hoc session initialization process. The regtest --// environment has had more time devoted to its initialization, and has a --// more convenient API. --// - The old tests lacked documentation, and often had failures that were hard --// to understand. By starting from scratch, we can revisit these aspects. +-// The workspace symbol method is defined in the spec as follows: -// --// # Special files --// --// There are several types of file within the test archive that are given special --// treatment by the test runner: --// - "skip": the presence of this file causes the test to be skipped, with --// the file content used as the skip message. --// - "flags": this file is treated as a whitespace-separated list of flags --// that configure the MarkerTest instance. Supported flags: --// -min_go=go1.18 sets the minimum Go version for the test; --// -cgo requires that CGO_ENABLED is set and the cgo tool is available --// -write_sumfile=a,b,c instructs the test runner to generate go.sum files --// in these directories before running the test. --// -skip_goos=a,b,c instructs the test runner to skip the test for the --// listed GOOS values. --// -ignore_extra_diags suppresses errors for unmatched diagnostics --// TODO(rfindley): using build constraint expressions for -skip_goos would --// be clearer. --// -filter_builtins=false disables the filtering of builtins from --// completion results. --// -filter_keywords=false disables the filtering of keywords from --// completion results. --// TODO(rfindley): support flag values containing whitespace. --// - "settings.json": this file is parsed as JSON, and used as the --// session configuration (see gopls/doc/settings.md) --// - "capabilities.json": this file is parsed as JSON client capabilities, --// and applied as an overlay over the default editor client capabilities. --// see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#clientCapabilities --// for more details. --// - "env": this file is parsed as a list of VAR=VALUE fields specifying the --// editor environment. --// - Golden files: Within the archive, file names starting with '@' are --// treated as "golden" content, and are not written to disk, but instead are --// made available to test methods expecting an argument of type *Golden, --// using the identifier following '@'. For example, if the first parameter of --// Foo were of type *Golden, the test runner would convert the identifier a --// in the call @foo(a, "b", 3) into a *Golden by collecting golden file --// data starting with "@a/". --// - proxy files: any file starting with proxy/ is treated as a Go proxy --// file. If present, these files are written to a separate temporary --// directory and GOPROXY is set to file://. --// --// # Marker types --// --// Markers are of two kinds. A few are "value markers" (e.g. @item), which are --// processed in a first pass and each computes a value that may be referred to --// by name later. Most are "action markers", which are processed in a second --// pass and take some action such as testing an LSP operation; they may refer --// to values computed by value markers. --// --// The following markers are supported within marker tests: --// --// - acceptcompletion(location, label, golden): specifies that accepting the --// completion candidate produced at the given location with provided label --// results in the given golden state. --// --// - codeaction(start, end, kind, golden, ...titles): specifies a code action --// to request for the given range. To support multi-line ranges, the range --// is defined to be between start.Start and end.End. The golden directory --// contains changed file content after the code action is applied. --// If titles are provided, they are used to filter the matching code --// action. --// --// TODO(rfindley): consolidate with codeactionedit, via a @loc2 marker that --// allows binding multi-line locations. --// --// - codeactionedit(range, kind, golden, ...titles): a shorter form of --// codeaction. Invokes a code action of the given kind for the given --// in-line range, and compares the resulting formatted unified *edits* --// (notably, not the full file content) with the golden directory. --// --// - codeactionerr(start, end, kind, wantError): specifies a codeaction that --// fails with an error that matches the expectation. --// --// - codelens(location, title): specifies that a codelens is expected at the --// given location, with given title. Must be used in conjunction with --// @codelenses. --// --// - codelenses(): specifies that textDocument/codeLens should be run for the --// current document, with results compared to the @codelens annotations in --// the current document. --// --// - complete(location, ...items): specifies expected completion results at --// the given location. Must be used in conjunction with @item. --// --// - diag(location, regexp): specifies an expected diagnostic matching the --// given regexp at the given location. The test runner requires --// a 1:1 correspondence between observed diagnostics and diag annotations. --// The diagnostics source and kind fields are ignored, to reduce fuss. --// --// The specified location must match the start position of the diagnostic, --// but end positions are ignored. --// --// TODO(adonovan): in the older marker framework, the annotation asserted --// two additional fields (source="compiler", kind="error"). Restore them? --// --// - def(src, dst location): perform a textDocument/definition request at --// the src location, and check the result points to the dst location. --// --// - documentLink(golden): asserts that textDocument/documentLink returns --// links as described by the golden file. --// --// - foldingrange(golden): perform a textDocument/foldingRange for the --// current document, and compare with the golden content, which is the --// original source annotated with numbered tags delimiting the resulting --// ranges (e.g. <1 kind="..."> ... ). --// --// - format(golden): perform a textDocument/format request for the enclosing --// file, and compare against the named golden file. If the formatting --// request succeeds, the golden file must contain the resulting formatted --// source. If the formatting request fails, the golden file must contain --// the error message. --// --// - highlight(src location, dsts ...location): makes a --// textDocument/highlight request at the given src location, which should --// highlight the provided dst locations. --// --// - hover(src, dst location, g Golden): perform a textDocument/hover at the --// src location, and checks that the result is the dst location, with hover --// content matching "hover.md" in the golden data g. --// --// - implementations(src location, want ...location): makes a --// textDocument/implementation query at the src location and --// checks that the resulting set of locations matches want. --// --// - item(label, details, kind): defines a completion item with the provided --// fields. This information is not positional, and therefore @item markers --// may occur anywhere in the source. Used in conjunction with @complete, --// snippet, or rank. --// --// TODO(rfindley): rethink whether floating @item annotations are the best --// way to specify completion results. --// --// - loc(name, location): specifies the name for a location in the source. These --// locations may be referenced by other markers. --// --// - preparerename(src, spn, placeholder): asserts that a textDocument/prepareRename --// request at the src location expands to the spn location, with given --// placeholder. If placeholder is "", this is treated as a negative --// assertion and prepareRename should return nil. --// --// - rename(location, new, golden): specifies a renaming of the --// identifier at the specified location to the new name. --// The golden directory contains the transformed files. --// --// - renameerr(location, new, wantError): specifies a renaming that --// fails with an error that matches the expectation. --// --// - signature(location, label, active): specifies that --// signatureHelp at the given location should match the provided string, with --// the active parameter (an index) highlighted. --// --// - suggestedfix(location, regexp, kind, golden): like diag, the location and --// regexp identify an expected diagnostic. This diagnostic must --// to have exactly one associated code action of the specified kind. --// This action is executed for its editing effects on the source files. --// Like rename, the golden directory contains the expected transformed files. --// --// - rank(location, ...completionItem): executes a textDocument/completion --// request at the given location, and verifies that each expected --// completion item occurs in the results, in the expected order. Other --// unexpected completion items may occur in the results. --// TODO(rfindley): this exists for compatibility with the old marker tests. --// Replace this with rankl, and rename. --// --// - rankl(location, ...label): like rank, but only cares about completion --// item labels. --// --// - refs(location, want ...location): executes a textDocument/references --// request at the first location and asserts that the result is the set of --// 'want' locations. The first want location must be the declaration --// (assumedly unique). --// --// - snippet(location, completionItem, snippet): executes a --// textDocument/completion request at the location, and searches for a --// result with label matching that of the provided completion item --// (TODO(rfindley): accept a label rather than a completion item). Check --// the the result snippet matches the provided snippet. --// --// - symbol(golden): makes a textDocument/documentSymbol request --// for the enclosing file, formats the response with one symbol --// per line, sorts it, and compares against the named golden file. --// Each line is of the form: --// --// dotted.symbol.name kind "detail" +n lines --// --// where the "+n lines" part indicates that the declaration spans --// several lines. The test otherwise makes no attempt to check --// location information. There is no point to using more than one --// @symbol marker in a given file. --// --// - workspacesymbol(query, golden): makes a workspace/symbol request for the --// given query, formats the response with one symbol per line, and compares --// against the named golden file. As workspace symbols are by definition a --// workspace-wide request, the location of the workspace symbol marker does --// not matter. Each line is of the form: --// --// location name kind --// --// # Argument conversion --// --// Marker arguments are first parsed by the go/expect package, which accepts --// the following tokens as defined by the Go spec: --// - string, int64, float64, and rune literals --// - true and false --// - nil --// - identifiers (type expect.Identifier) --// - regular expressions, denoted the two tokens re"abc" (type *regexp.Regexp) --// --// These values are passed as arguments to the corresponding parameter of the --// test function. Additional value conversions may occur for these argument -> --// parameter type pairs: --// - string->regexp: the argument is parsed as a regular expressions. --// - string->location: the argument is converted to the location of the first --// instance of the argument in the partial line preceding the note. --// - regexp->location: the argument is converted to the location of the first --// match for the argument in the partial line preceding the note. If the --// regular expression contains exactly one subgroup, the position of the --// subgroup is used rather than the position of the submatch. --// - name->location: the argument is replaced by the named location. --// - name->Golden: the argument is used to look up golden content prefixed by --// @. --// - {string,regexp,identifier}->wantError: a wantError type specifies --// an expected error message, either in the form of a substring that --// must be present, a regular expression that it must match, or an --// identifier (e.g. foo) such that the archive entry @foo --// exists and contains the exact expected error. --// --// # Example --// --// Here is a complete example: --// --// -- a.go -- --// package a --// --// const abc = 0x2a //@hover("b", "abc", abc),hover(" =", "abc", abc) --// -- @abc/hover.md -- --// ```go --// const abc untyped int = 42 --// ``` --// --// @hover("b", "abc", abc),hover(" =", "abc", abc) --// --// In this example, the @hover annotation tells the test runner to run the --// hoverMarker function, which has parameters: --// --// (mark marker, src, dsc protocol.Location, g *Golden). --// --// The first argument holds the test context, including fake editor with open --// files, and sandboxed directory. --// --// Argument converters translate the "b" and "abc" arguments into locations by --// interpreting each one as a regular expression and finding the location of --// its first match on the preceding portion of the line, and the abc identifier --// into a dictionary of golden content containing "hover.md". Then the --// hoverMarker method executes a textDocument/hover LSP request at the src --// position, and ensures the result spans "abc", with the markdown content from --// hover.md. (Note that the markdown content includes the expect annotation as --// the doc comment.) --// --// The next hover on the same line asserts the same result, but initiates the --// hover immediately after "abc" in the source. This tests that we find the --// preceding identifier when hovering. --// --// # Updating golden files --// --// To update golden content in the test archive, it is easier to regenerate --// content automatically rather than edit it by hand. To do this, run the --// tests with the -update flag. Only tests that actually run will be updated. --// --// In some cases, golden content will vary by Go version (for example, gopls --// produces different markdown at Go versions before the 1.19 go/doc update). --// By convention, the golden content in test archives should match the output --// at Go tip. Each test function can normalize golden content for older Go --// versions. --// --// Note that -update does not cause missing @diag or @loc markers to be added. +-// The workspace symbol request is sent from the client to the server to +-// list project-wide symbols matching the query string. -// --// # TODO +-// It is unclear what "project-wide" means here, but given the parameters of +-// workspace/symbol do not include any workspace identifier, then it has to be +-// assumed that "project-wide" means "across all workspaces". Hence why +-// WorkspaceSymbols receives the views []View. -// --// This API is a work-in-progress, as we migrate existing marker tests from --// internal/lsp/tests. --// --// Remaining TODO: --// - reorganize regtest packages (and rename to just 'test'?) --// - Rename the files .txtar. --// - Provide some means by which locations in the standard library --// (or builtin.go) can be named, so that, for example, we can we --// can assert that MyError implements the built-in error type. --// - If possible, improve handling for optional arguments. Rather than have --// multiple variations of a marker, it would be nice to support a more --// flexible signature: can codeaction, codeactionedit, codeactionerr, and --// suggestedfix be consolidated? --// --// Existing marker tests (in ../testdata) to port: --// - CallHierarchy --// - SemanticTokens --// - SuggestedFixes --// - InlayHints --// - Renames --// - SelectionRanges --func RunMarkerTests(t *testing.T, dir string) { -- // The marker tests must be able to run go/packages.Load. -- testenv.NeedsGoPackages(t) +-// However, it then becomes unclear what it would mean to call WorkspaceSymbols +-// with a different configured SymbolMatcher per View. Therefore we assume that +-// Session level configuration will define the SymbolMatcher to be used for the +-// WorkspaceSymbols method. +-func WorkspaceSymbols(ctx context.Context, matcher settings.SymbolMatcher, style settings.SymbolStyle, snapshots []*cache.Snapshot, query string) ([]protocol.SymbolInformation, error) { +- ctx, done := event.Start(ctx, "golang.WorkspaceSymbols") +- defer done() +- if query == "" { +- return nil, nil +- } - -- tests, err := loadMarkerTests(dir) -- if err != nil { -- t.Fatal(err) +- var s symbolizer +- switch style { +- case settings.DynamicSymbols: +- s = dynamicSymbolMatch +- case settings.FullyQualifiedSymbols: +- s = fullyQualifiedSymbolMatch +- case settings.PackageQualifiedSymbols: +- s = packageSymbolMatch +- default: +- panic(fmt.Errorf("unknown symbol style: %v", style)) - } - -- // Opt: use a shared cache. -- cache := cache.New(nil) +- return collectSymbols(ctx, snapshots, matcher, s, query) +-} - -- for _, test := range tests { -- test := test -- t.Run(test.name, func(t *testing.T) { -- t.Parallel() -- if test.skipReason != "" { -- t.Skip(test.skipReason) -- } -- for _, goos := range test.skipGOOS { -- if runtime.GOOS == goos { -- t.Skipf("skipping on %s due to -skip_goos", runtime.GOOS) -- } -- } +-// A matcherFunc returns the index and score of a symbol match. +-// +-// See the comment for symbolCollector for more information. +-type matcherFunc func(chunks []string) (int, float64) - -- // TODO(rfindley): it may be more useful to have full support for build -- // constraints. -- if test.minGoVersion != "" { -- var go1point int -- if _, err := fmt.Sscanf(test.minGoVersion, "go1.%d", &go1point); err != nil { -- t.Fatalf("parsing -min_go version: %v", err) -- } -- testenv.NeedsGo1Point(t, go1point) -- } -- if test.cgo { -- testenv.NeedsTool(t, "cgo") -- } -- config := fake.EditorConfig{ -- Settings: test.settings, -- CapabilitiesJSON: test.capabilities, -- Env: test.env, -- } -- if _, ok := config.Settings["diagnosticsDelay"]; !ok { -- if config.Settings == nil { -- config.Settings = make(map[string]any) -- } -- config.Settings["diagnosticsDelay"] = "10ms" -- } -- // inv: config.Settings != nil +-// A symbolizer returns the best symbol match for a name with pkg, according to +-// some heuristic. The symbol name is passed as the slice nameParts of logical +-// name pieces. For example, for myType.field the caller can pass either +-// []string{"myType.field"} or []string{"myType.", "field"}. +-// +-// See the comment for symbolCollector for more information. +-// +-// The space argument is an empty slice with spare capacity that may be used +-// to allocate the result. +-type symbolizer func(space []string, name string, pkg *metadata.Package, m matcherFunc) ([]string, float64) - -- run := &markerTestRun{ -- test: test, -- env: newEnv(t, cache, test.files, test.proxyFiles, test.writeGoSum, config), -- settings: config.Settings, -- values: make(map[expect.Identifier]any), -- diags: make(map[protocol.Location][]protocol.Diagnostic), -- extraNotes: make(map[protocol.DocumentURI]map[string][]*expect.Note), -- } -- // TODO(rfindley): make it easier to clean up the regtest environment. -- defer run.env.Editor.Shutdown(context.Background()) // ignore error -- defer run.env.Sandbox.Close() // ignore error +-func fullyQualifiedSymbolMatch(space []string, name string, pkg *metadata.Package, matcher matcherFunc) ([]string, float64) { +- if _, score := dynamicSymbolMatch(space, name, pkg, matcher); score > 0 { +- return append(space, string(pkg.PkgPath), ".", name), score +- } +- return nil, 0 +-} - -- // Open all files so that we operate consistently with LSP clients, and -- // (pragmatically) so that we have a Mapper available via the fake -- // editor. -- // -- // This also allows avoiding mutating the editor state in tests. -- for file := range test.files { -- run.env.OpenFile(file) -- } -- // Wait for the didOpen notifications to be processed, then collect -- // diagnostics. -- var diags map[string]*protocol.PublishDiagnosticsParams -- run.env.AfterChange(ReadAllDiagnostics(&diags)) -- for path, params := range diags { -- uri := run.env.Sandbox.Workdir.URI(path) -- for _, diag := range params.Diagnostics { -- loc := protocol.Location{ -- URI: uri, -- Range: protocol.Range{ -- Start: diag.Range.Start, -- End: diag.Range.Start, // ignore end positions -- }, -- } -- run.diags[loc] = append(run.diags[loc], diag) -- } -- } +-func dynamicSymbolMatch(space []string, name string, pkg *metadata.Package, matcher matcherFunc) ([]string, float64) { +- if metadata.IsCommandLineArguments(pkg.ID) { +- // command-line-arguments packages have a non-sensical package path, so +- // just use their package name. +- return packageSymbolMatch(space, name, pkg, matcher) +- } - -- var markers []marker -- for _, note := range test.notes { -- mark := marker{run: run, note: note} -- if fn, ok := valueMarkerFuncs[note.Name]; ok { -- fn(mark) -- } else if _, ok := actionMarkerFuncs[note.Name]; ok { -- markers = append(markers, mark) // save for later -- } else { -- uri := mark.uri() -- if run.extraNotes[uri] == nil { -- run.extraNotes[uri] = make(map[string][]*expect.Note) -- } -- run.extraNotes[uri][note.Name] = append(run.extraNotes[uri][note.Name], note) -- } -- } +- var score float64 - -- // Invoke each remaining marker in the test. -- for _, mark := range markers { -- actionMarkerFuncs[mark.note.Name](mark) -- } +- endsInPkgName := strings.HasSuffix(string(pkg.PkgPath), string(pkg.Name)) - -- // Any remaining (un-eliminated) diagnostics are an error. -- if !test.ignoreExtraDiags { -- for loc, diags := range run.diags { -- for _, diag := range diags { -- t.Errorf("%s: unexpected diagnostic: %q", run.fmtLoc(loc), diag.Message) -- } -- } +- // If the package path does not end in the package name, we need to check the +- // package-qualified symbol as an extra pass first. +- if !endsInPkgName { +- pkgQualified := append(space, string(pkg.Name), ".", name) +- idx, score := matcher(pkgQualified) +- nameStart := len(pkg.Name) + 1 +- if score > 0 { +- // If our match is contained entirely within the unqualified portion, +- // just return that. +- if idx >= nameStart { +- return append(space, name), score - } +- // Lower the score for matches that include the package name. +- return pkgQualified, score * 0.8 +- } +- } - -- // TODO(rfindley): use these for whole-file marker tests. -- for uri, extras := range run.extraNotes { -- for name, extra := range extras { -- if len(extra) > 0 { -- t.Errorf("%s: %d unused %q markers", run.env.Sandbox.Workdir.URIToPath(uri), len(extra), name) -- } -- } -- } +- // Now try matching the fully qualified symbol. +- fullyQualified := append(space, string(pkg.PkgPath), ".", name) +- idx, score := matcher(fullyQualified) - -- formatted, err := formatTest(test) -- if err != nil { -- t.Errorf("formatTest: %v", err) -- } else if *update { -- filename := filepath.Join(dir, test.name) -- if err := os.WriteFile(filename, formatted, 0644); err != nil { -- t.Error(err) -- } -- } else { -- // On go 1.19 and later, verify that the testdata has not changed. -- // -- // On earlier Go versions, the golden test data varies due to different -- // markdown escaping. -- // -- // Only check this if the test hasn't already failed, otherwise we'd -- // report duplicate mismatches of golden data. -- if testenv.Go1Point() >= 19 && !t.Failed() { -- // Otherwise, verify that formatted content matches. -- if diff := compare.NamedText("formatted", "on-disk", string(formatted), string(test.content)); diff != "" { -- t.Errorf("formatted test does not match on-disk content:\n%s", diff) -- } -- } -- } -- }) +- // As above, check if we matched just the unqualified symbol name. +- nameStart := len(pkg.PkgPath) + 1 +- if idx >= nameStart { +- return append(space, name), score - } - -- if abs, err := filepath.Abs(dir); err == nil && t.Failed() { -- t.Logf("(Filenames are relative to %s.)", abs) +- // If our package path ends in the package name, we'll have skipped the +- // initial pass above, so check if we matched just the package-qualified +- // name. +- if endsInPkgName && idx >= 0 { +- pkgStart := len(pkg.PkgPath) - len(pkg.Name) +- if idx >= pkgStart { +- return append(space, string(pkg.Name), ".", name), score +- } - } +- +- // Our match was not contained within the unqualified or package qualified +- // symbol. Return the fully qualified symbol but discount the score. +- return fullyQualified, score * 0.6 -} - --// A marker holds state for the execution of a single @marker --// annotation in the source. --type marker struct { -- run *markerTestRun -- note *expect.Note +-func packageSymbolMatch(space []string, name string, pkg *metadata.Package, matcher matcherFunc) ([]string, float64) { +- qualified := append(space, string(pkg.Name), ".", name) +- if _, s := matcher(qualified); s > 0 { +- return qualified, s +- } +- return nil, 0 -} - --// server returns the LSP server for the marker test run. --func (m marker) server() protocol.Server { -- return m.run.env.Editor.Server +-func buildMatcher(matcher settings.SymbolMatcher, query string) matcherFunc { +- switch matcher { +- case settings.SymbolFuzzy: +- return parseQuery(query, newFuzzyMatcher) +- case settings.SymbolFastFuzzy: +- return parseQuery(query, func(query string) matcherFunc { +- return fuzzy.NewSymbolMatcher(query).Match +- }) +- case settings.SymbolCaseSensitive: +- return matchExact(query) +- case settings.SymbolCaseInsensitive: +- q := strings.ToLower(query) +- exact := matchExact(q) +- wrapper := []string{""} +- return func(chunks []string) (int, float64) { +- s := strings.Join(chunks, "") +- wrapper[0] = strings.ToLower(s) +- return exact(wrapper) +- } +- } +- panic(fmt.Errorf("unknown symbol matcher: %v", matcher)) -} - --// errorf reports an error with a prefix indicating the position of the marker note. --// --// It formats the error message using mark.sprintf. --func (mark marker) errorf(format string, args ...any) { -- msg := mark.sprintf(format, args...) -- // TODO(adonovan): consider using fmt.Fprintf(os.Stderr)+t.Fail instead of -- // t.Errorf to avoid reporting uninteresting positions in the Go source of -- // the driver. However, this loses the order of stderr wrt "FAIL: TestFoo" -- // subtest dividers. -- mark.run.env.T.Errorf("%s: %s", mark.run.fmtPos(mark.note.Pos), msg) +-func newFuzzyMatcher(query string) matcherFunc { +- fm := fuzzy.NewMatcher(query) +- return func(chunks []string) (int, float64) { +- score := float64(fm.ScoreChunks(chunks)) +- ranges := fm.MatchedRanges() +- if len(ranges) > 0 { +- return ranges[0], score +- } +- return -1, score +- } -} - --// valueMarkerFunc returns a wrapper around a function that allows it to be --// called during the processing of value markers (e.g. @value(v, 123)) with marker --// arguments converted to function parameters. The provided function's first --// parameter must be of type 'marker', and it must return a value. --// --// Unlike action markers, which are executed for actions such as test --// assertions, value markers are all evaluated first, and each computes --// a value that is recorded by its identifier, which is the marker's first --// argument. These values may be referred to from an action marker by --// this identifier, e.g. @action(... , v, ...). --// --// For example, given a fn with signature --// --// func(mark marker, label, details, kind string) CompletionItem +-// parseQuery parses a field-separated symbol query, extracting the special +-// characters listed below, and returns a matcherFunc corresponding to the AND +-// of all field queries. -// --// The result of valueMarkerFunc can associated with @item notes, and invoked --// as follows: +-// Special characters: -// --// //@item(FooCompletion, "Foo", "func() int", "func") +-// ^ match exact prefix +-// $ match exact suffix +-// ' match exact -// --// The provided fn should not mutate the test environment. --func valueMarkerFunc(fn any) func(marker) { -- ftype := reflect.TypeOf(fn) -- if ftype.NumIn() == 0 || ftype.In(0) != markerType { -- panic(fmt.Sprintf("value marker function %#v must accept marker as its first argument", ftype)) +-// In all three of these special queries, matches are 'smart-cased', meaning +-// they are case sensitive if the symbol query contains any upper-case +-// characters, and case insensitive otherwise. +-func parseQuery(q string, newMatcher func(string) matcherFunc) matcherFunc { +- fields := strings.Fields(q) +- if len(fields) == 0 { +- return func([]string) (int, float64) { return -1, 0 } - } -- if ftype.NumOut() != 1 { -- panic(fmt.Sprintf("value marker function %#v must have exactly 1 result", ftype)) +- var funcs []matcherFunc +- for _, field := range fields { +- var f matcherFunc +- switch { +- case strings.HasPrefix(field, "^"): +- prefix := field[1:] +- f = smartCase(prefix, func(chunks []string) (int, float64) { +- s := strings.Join(chunks, "") +- if strings.HasPrefix(s, prefix) { +- return 0, 1 +- } +- return -1, 0 +- }) +- case strings.HasPrefix(field, "'"): +- exact := field[1:] +- f = smartCase(exact, matchExact(exact)) +- case strings.HasSuffix(field, "$"): +- suffix := field[0 : len(field)-1] +- f = smartCase(suffix, func(chunks []string) (int, float64) { +- s := strings.Join(chunks, "") +- if strings.HasSuffix(s, suffix) { +- return len(s) - len(suffix), 1 +- } +- return -1, 0 +- }) +- default: +- f = newMatcher(field) +- } +- funcs = append(funcs, f) +- } +- if len(funcs) == 1 { +- return funcs[0] - } +- return comboMatcher(funcs).match +-} - -- return func(mark marker) { -- if len(mark.note.Args) == 0 || !is[expect.Identifier](mark.note.Args[0]) { -- mark.errorf("first argument to a value marker function must be an identifier") -- return -- } -- id := mark.note.Args[0].(expect.Identifier) -- if alt, ok := mark.run.values[id]; ok { -- mark.errorf("%s already declared as %T", id, alt) -- return -- } -- args := append([]any{mark}, mark.note.Args[1:]...) -- argValues, err := convertArgs(mark, ftype, args) -- if err != nil { -- mark.errorf("converting args: %v", err) -- return +-func matchExact(exact string) matcherFunc { +- return func(chunks []string) (int, float64) { +- s := strings.Join(chunks, "") +- if idx := strings.LastIndex(s, exact); idx >= 0 { +- return idx, 1 - } -- results := reflect.ValueOf(fn).Call(argValues) -- mark.run.values[id] = results[0].Interface() +- return -1, 0 - } -} - --// actionMarkerFunc returns a wrapper around a function that allows it to be --// called during the processing of action markers (e.g. @action("abc", 123)) --// with marker arguments converted to function parameters. The provided --// function's first parameter must be of type 'marker', and it must not return --// any values. --// --// The provided fn should not mutate the test environment. --func actionMarkerFunc(fn any) func(marker) { -- ftype := reflect.TypeOf(fn) -- if ftype.NumIn() == 0 || ftype.In(0) != markerType { -- panic(fmt.Sprintf("action marker function %#v must accept marker as its first argument", ftype)) -- } -- if ftype.NumOut() != 0 { -- panic(fmt.Sprintf("action marker function %#v cannot have results", ftype)) +-// smartCase returns a matcherFunc that is case-sensitive if q contains any +-// upper-case characters, and case-insensitive otherwise. +-func smartCase(q string, m matcherFunc) matcherFunc { +- insensitive := strings.ToLower(q) == q +- wrapper := []string{""} +- return func(chunks []string) (int, float64) { +- s := strings.Join(chunks, "") +- if insensitive { +- s = strings.ToLower(s) +- } +- wrapper[0] = s +- return m(wrapper) - } +-} - -- return func(mark marker) { -- args := append([]any{mark}, mark.note.Args...) -- argValues, err := convertArgs(mark, ftype, args) -- if err != nil { -- mark.errorf("converting args: %v", err) -- return +-type comboMatcher []matcherFunc +- +-func (c comboMatcher) match(chunks []string) (int, float64) { +- score := 1.0 +- first := 0 +- for _, f := range c { +- idx, s := f(chunks) +- if idx < first { +- first = idx - } -- reflect.ValueOf(fn).Call(argValues) +- score *= s - } +- return first, score -} - --func convertArgs(mark marker, ftype reflect.Type, args []any) ([]reflect.Value, error) { -- var ( -- argValues []reflect.Value -- pnext int // next param index -- p reflect.Type // current param -- ) -- for i, arg := range args { -- if i < ftype.NumIn() { -- p = ftype.In(pnext) -- pnext++ -- } else if p == nil || !ftype.IsVariadic() { -- // The actual number of arguments expected by the mark varies, depending -- // on whether this is a value marker or an action marker. -- // -- // Since this error indicates a bug, probably OK to have an imprecise -- // error message here. -- return nil, fmt.Errorf("too many arguments to %s", mark.note.Name) +-// collectSymbols calls snapshot.Symbols to walk the syntax trees of +-// all files in the views' current snapshots, and returns a sorted, +-// scored list of symbols that best match the parameters. +-// +-// How it matches symbols is parameterized by two interfaces: +-// - A matcherFunc determines how well a string symbol matches a query. It +-// returns a non-negative score indicating the quality of the match. A score +-// of zero indicates no match. +-// - A symbolizer determines how we extract the symbol for an object. This +-// enables the 'symbolStyle' configuration option. +-func collectSymbols(ctx context.Context, snapshots []*cache.Snapshot, matcherType settings.SymbolMatcher, symbolizer symbolizer, query string) ([]protocol.SymbolInformation, error) { +- // Extract symbols from all files. +- var work []symbolFile +- var roots []string +- seen := make(map[protocol.DocumentURI]bool) +- // TODO(adonovan): opt: parallelize this loop? How often is len > 1? +- for _, snapshot := range snapshots { +- // Use the root view URIs for determining (lexically) +- // whether a URI is in any open workspace. +- folderURI := snapshot.Folder() +- roots = append(roots, strings.TrimRight(string(folderURI), "/")) +- +- filters := snapshot.Options().DirectoryFilters +- filterer := cache.NewFilterer(filters) +- folder := filepath.ToSlash(folderURI.Path()) +- +- workspaceOnly := true +- if snapshot.Options().SymbolScope == settings.AllSymbolScope { +- workspaceOnly = false - } -- elemType := p -- if ftype.IsVariadic() && pnext == ftype.NumIn() { -- elemType = p.Elem() +- symbols, err := snapshot.Symbols(ctx, workspaceOnly) +- if err != nil { +- return nil, err - } -- var v reflect.Value -- if id, ok := arg.(expect.Identifier); ok && id == "_" { -- v = reflect.Zero(elemType) -- } else { -- a, err := convert(mark, arg, elemType) +- +- for uri, syms := range symbols { +- norm := filepath.ToSlash(uri.Path()) +- nm := strings.TrimPrefix(norm, folder) +- if filterer.Disallow(nm) { +- continue +- } +- // Only scan each file once. +- if seen[uri] { +- continue +- } +- meta, err := NarrowestMetadataForFile(ctx, snapshot, uri) - if err != nil { -- return nil, err +- event.Error(ctx, fmt.Sprintf("missing metadata for %q", uri), err) +- continue - } -- v = reflect.ValueOf(a) +- seen[uri] = true +- work = append(work, symbolFile{uri, meta, syms}) - } -- argValues = append(argValues, v) -- } -- // Check that we have sufficient arguments. If the function is variadic, we -- // do not need arguments for the final parameter. -- if pnext < ftype.NumIn()-1 || pnext == ftype.NumIn()-1 && !ftype.IsVariadic() { -- // Same comment as above: OK to be vague here. -- return nil, fmt.Errorf("not enough arguments to %s", mark.note.Name) - } -- return argValues, nil --} -- --// is reports whether arg is a T. --func is[T any](arg any) bool { -- _, ok := arg.(T) -- return ok --} - --// Supported value marker functions. See [valueMarkerFunc] for more details. --var valueMarkerFuncs = map[string]func(marker){ -- "loc": valueMarkerFunc(locMarker), -- "item": valueMarkerFunc(completionItemMarker), --} -- --// Supported action marker functions. See [actionMarkerFunc] for more details. --var actionMarkerFuncs = map[string]func(marker){ -- "acceptcompletion": actionMarkerFunc(acceptCompletionMarker), -- "codeaction": actionMarkerFunc(codeActionMarker), -- "codeactionedit": actionMarkerFunc(codeActionEditMarker), -- "codeactionerr": actionMarkerFunc(codeActionErrMarker), -- "codelenses": actionMarkerFunc(codeLensesMarker), -- "complete": actionMarkerFunc(completeMarker), -- "def": actionMarkerFunc(defMarker), -- "diag": actionMarkerFunc(diagMarker), -- "documentlink": actionMarkerFunc(documentLinkMarker), -- "foldingrange": actionMarkerFunc(foldingRangeMarker), -- "format": actionMarkerFunc(formatMarker), -- "highlight": actionMarkerFunc(highlightMarker), -- "hover": actionMarkerFunc(hoverMarker), -- "implementation": actionMarkerFunc(implementationMarker), -- "preparerename": actionMarkerFunc(prepareRenameMarker), -- "rank": actionMarkerFunc(rankMarker), -- "rankl": actionMarkerFunc(ranklMarker), -- "refs": actionMarkerFunc(refsMarker), -- "rename": actionMarkerFunc(renameMarker), -- "renameerr": actionMarkerFunc(renameErrMarker), -- "signature": actionMarkerFunc(signatureMarker), -- "snippet": actionMarkerFunc(snippetMarker), -- "suggestedfix": actionMarkerFunc(suggestedfixMarker), -- "symbol": actionMarkerFunc(symbolMarker), -- "typedef": actionMarkerFunc(typedefMarker), -- "workspacesymbol": actionMarkerFunc(workspaceSymbolMarker), --} -- --// markerTest holds all the test data extracted from a test txtar archive. --// --// See the documentation for RunMarkerTests for more information on the archive --// format. --type markerTest struct { -- name string // relative path to the txtar file in the testdata dir -- fset *token.FileSet // fileset used for parsing notes -- content []byte // raw test content -- archive *txtar.Archive // original test archive -- settings map[string]any // gopls settings -- capabilities []byte // content of capabilities.json file -- env map[string]string // editor environment -- proxyFiles map[string][]byte // proxy content -- files map[string][]byte // data files from the archive (excluding special files) -- notes []*expect.Note // extracted notes from data files -- golden map[expect.Identifier]*Golden // extracted golden content, by identifier name -- -- skipReason string // the skip reason extracted from the "skip" archive file -- flags []string // flags extracted from the special "flags" archive file. -- -- // Parsed flags values. -- minGoVersion string -- cgo bool -- writeGoSum []string // comma separated dirs to write go sum for -- skipGOOS []string // comma separated GOOS values to skip -- ignoreExtraDiags bool -- filterBuiltins bool -- filterKeywords bool --} -- --// flagSet returns the flagset used for parsing the special "flags" file in the --// test archive. --func (t *markerTest) flagSet() *flag.FlagSet { -- flags := flag.NewFlagSet(t.name, flag.ContinueOnError) -- flags.StringVar(&t.minGoVersion, "min_go", "", "if set, the minimum go1.X version required for this test") -- flags.BoolVar(&t.cgo, "cgo", false, "if set, requires cgo (both the cgo tool and CGO_ENABLED=1)") -- flags.Var((*stringListValue)(&t.writeGoSum), "write_sumfile", "if set, write the sumfile for these directories") -- flags.Var((*stringListValue)(&t.skipGOOS), "skip_goos", "if set, skip this test on these GOOS values") -- flags.BoolVar(&t.ignoreExtraDiags, "ignore_extra_diags", false, "if set, suppress errors for unmatched diagnostics") -- flags.BoolVar(&t.filterBuiltins, "filter_builtins", true, "if set, filter builtins from completion results") -- flags.BoolVar(&t.filterKeywords, "filter_keywords", true, "if set, filter keywords from completion results") -- return flags --} -- --// stringListValue implements flag.Value. --type stringListValue []string +- // Match symbols in parallel. +- // Each worker has its own symbolStore, +- // which we merge at the end. +- nmatchers := runtime.GOMAXPROCS(-1) // matching is CPU bound +- results := make(chan *symbolStore) +- for i := 0; i < nmatchers; i++ { +- go func(i int) { +- matcher := buildMatcher(matcherType, query) +- store := new(symbolStore) +- // Assign files to workers in round-robin fashion. +- for j := i; j < len(work); j += nmatchers { +- matchFile(store, symbolizer, matcher, roots, work[j]) +- } +- results <- store +- }(i) +- } - --func (l *stringListValue) Set(s string) error { -- if s != "" { -- for _, d := range strings.Split(s, ",") { -- *l = append(*l, strings.TrimSpace(d)) +- // Gather and merge results as they arrive. +- var unified symbolStore +- for i := 0; i < nmatchers; i++ { +- store := <-results +- for _, syms := range store.res { +- unified.store(syms) - } - } -- return nil --} -- --func (l stringListValue) String() string { -- return strings.Join([]string(l), ",") +- return unified.results(), nil -} - --func (t *markerTest) getGolden(id expect.Identifier) *Golden { -- golden, ok := t.golden[id] -- // If there was no golden content for this identifier, we must create one -- // to handle the case where -update is set: we need a place to store -- // the updated content. -- if !ok { -- golden = &Golden{id: id} -- -- // TODO(adonovan): the separation of markerTest (the -- // static aspects) from markerTestRun (the dynamic -- // ones) is evidently bogus because here we modify -- // markerTest during execution. Let's merge the two. -- t.golden[id] = golden -- } -- return golden +-// symbolFile holds symbol information for a single file. +-type symbolFile struct { +- uri protocol.DocumentURI +- mp *metadata.Package +- syms []cache.Symbol -} - --// Golden holds extracted golden content for a single @ prefix. --// --// When -update is set, golden captures the updated golden contents for later --// writing. --type Golden struct { -- id expect.Identifier -- data map[string][]byte // key "" => @id itself -- updated map[string][]byte --} +-// matchFile scans a symbol file and adds matching symbols to the store. +-func matchFile(store *symbolStore, symbolizer symbolizer, matcher matcherFunc, roots []string, i symbolFile) { +- space := make([]string, 0, 3) +- for _, sym := range i.syms { +- symbolParts, score := symbolizer(space, sym.Name, i.mp, matcher) - --// Get returns golden content for the given name, which corresponds to the --// relative path following the golden prefix @/. For example, to access --// the content of @foo/path/to/result.json from the Golden associated with --// @foo, name should be "path/to/result.json". --// --// If -update is set, the given update function will be called to get the --// updated golden content that should be written back to testdata. --// --// Marker functions must use this method instead of accessing data entries --// directly otherwise the -update operation will delete those entries. --// --// TODO(rfindley): rethink the logic here. We may want to separate Get and Set, --// and not delete golden content that isn't set. --func (g *Golden) Get(t testing.TB, name string, updated []byte) ([]byte, bool) { -- if existing, ok := g.updated[name]; ok { -- // Multiple tests may reference the same golden data, but if they do they -- // must agree about its expected content. -- if diff := compare.NamedText("existing", "updated", string(existing), string(updated)); diff != "" { -- t.Errorf("conflicting updates for golden data %s/%s:\n%s", g.id, name, diff) +- // Check if the score is too low before applying any downranking. +- if store.tooLow(score) { +- continue - } -- } -- if g.updated == nil { -- g.updated = make(map[string][]byte) -- } -- g.updated[name] = updated -- if *update { -- return updated, true -- } - -- res, ok := g.data[name] -- return res, ok --} +- // Factors to apply to the match score for the purpose of downranking +- // results. +- // +- // These numbers were crudely calibrated based on trial-and-error using a +- // small number of sample queries. Adjust as necessary. +- // +- // All factors are multiplicative, meaning if more than one applies they are +- // multiplied together. +- const ( +- // nonWorkspaceFactor is applied to symbols outside the workspace. +- // Developers are less likely to want to jump to code that they +- // are not actively working on. +- nonWorkspaceFactor = 0.5 +- // nonWorkspaceUnexportedFactor is applied to unexported symbols outside +- // the workspace. Since one wouldn't usually jump to unexported +- // symbols to understand a package API, they are particularly irrelevant. +- nonWorkspaceUnexportedFactor = 0.5 +- // every field or method nesting level to access the field decreases +- // the score by a factor of 1.0 - depth*depthFactor, up to a depth of +- // 3. +- // +- // Use a small constant here, as this exists mostly to break ties +- // (e.g. given a type Foo and a field x.Foo, prefer Foo). +- depthFactor = 0.01 +- ) - --// loadMarkerTests walks the given dir looking for .txt files, which it --// interprets as a txtar archive. --// --// See the documentation for RunMarkerTests for more details on the test data --// archive. --func loadMarkerTests(dir string) ([]*markerTest, error) { -- var tests []*markerTest -- err := filepath.WalkDir(dir, func(path string, _ fs.DirEntry, err error) error { -- if strings.HasSuffix(path, ".txt") { -- content, err := os.ReadFile(path) -- if err != nil { -- return err +- startWord := true +- exported := true +- depth := 0.0 +- for _, r := range sym.Name { +- if startWord && !unicode.IsUpper(r) { +- exported = false - } -- -- name := strings.TrimPrefix(path, dir+string(filepath.Separator)) -- test, err := loadMarkerTest(name, content) -- if err != nil { -- return fmt.Errorf("%s: %v", path, err) +- if r == '.' { +- startWord = true +- depth++ +- } else { +- startWord = false - } -- tests = append(tests, test) - } -- return err -- }) -- return tests, err --} -- --func loadMarkerTest(name string, content []byte) (*markerTest, error) { -- archive := txtar.Parse(content) -- if len(archive.Files) == 0 { -- return nil, fmt.Errorf("txtar file has no '-- filename --' sections") -- } -- if bytes.Contains(archive.Comment, []byte("\n-- ")) { -- // This check is conservative, but the comment is only a comment. -- return nil, fmt.Errorf("ill-formed '-- filename --' header in comment") -- } -- test := &markerTest{ -- name: name, -- fset: token.NewFileSet(), -- content: content, -- archive: archive, -- files: make(map[string][]byte), -- golden: make(map[expect.Identifier]*Golden), -- } -- for _, file := range archive.Files { -- switch { -- case file.Name == "skip": -- reason := strings.ReplaceAll(string(file.Data), "\n", " ") -- reason = strings.TrimSpace(reason) -- test.skipReason = reason -- -- case file.Name == "flags": -- test.flags = strings.Fields(string(file.Data)) -- -- case file.Name == "settings.json": -- if err := json.Unmarshal(file.Data, &test.settings); err != nil { -- return nil, err -- } -- -- case file.Name == "capabilities.json": -- test.capabilities = file.Data // lazily unmarshalled by the editor -- -- case file.Name == "env": -- test.env = make(map[string]string) -- fields := strings.Fields(string(file.Data)) -- for _, field := range fields { -- key, value, ok := strings.Cut(field, "=") -- if !ok { -- return nil, fmt.Errorf("env vars must be formatted as var=value, got %q", field) -- } -- test.env[key] = value -- } -- -- case strings.HasPrefix(file.Name, "@"): // golden content -- idstring, name, _ := strings.Cut(file.Name[len("@"):], "/") -- id := expect.Identifier(idstring) -- // Note that a file.Name of just "@id" gives (id, name) = ("id", ""). -- if _, ok := test.golden[id]; !ok { -- test.golden[id] = &Golden{ -- id: id, -- data: make(map[string][]byte), -- } -- } -- test.golden[id].data[name] = file.Data -- -- case strings.HasPrefix(file.Name, "proxy/"): -- name := file.Name[len("proxy/"):] -- if test.proxyFiles == nil { -- test.proxyFiles = make(map[string][]byte) -- } -- test.proxyFiles[name] = file.Data - -- default: // ordinary file content -- notes, err := expect.Parse(test.fset, file.Name, file.Data) -- if err != nil { -- return nil, fmt.Errorf("parsing notes in %q: %v", file.Name, err) +- // TODO(rfindley): use metadata to determine if the file is in a workspace +- // package, rather than this heuristic. +- inWorkspace := false +- for _, root := range roots { +- if strings.HasPrefix(string(i.uri), root) { +- inWorkspace = true +- break - } +- } - -- // Reject common misspelling: "// @mark". -- // TODO(adonovan): permit "// @" within a string. Detect multiple spaces. -- if i := bytes.Index(file.Data, []byte("// @")); i >= 0 { -- line := 1 + bytes.Count(file.Data[:i], []byte("\n")) -- return nil, fmt.Errorf("%s:%d: unwanted space before marker (// @)", file.Name, line) +- // Apply downranking based on workspace position. +- if !inWorkspace { +- score *= nonWorkspaceFactor +- if !exported { +- score *= nonWorkspaceUnexportedFactor - } +- } - -- test.notes = append(test.notes, notes...) -- test.files[file.Name] = file.Data +- // Apply downranking based on symbol depth. +- if depth > 3 { +- depth = 3 - } +- score *= 1.0 - depth*depthFactor - -- // Print a warning if we see what looks like "-- filename --" -- // without the second "--". It's not necessarily wrong, -- // but it should almost never appear in our test inputs. -- if bytes.Contains(file.Data, []byte("\n-- ")) { -- log.Printf("ill-formed '-- filename --' header in %s?", file.Name) +- if store.tooLow(score) { +- continue - } -- } - -- // Parse flags after loading files, as they may have been set by the "flags" -- // file. -- if err := test.flagSet().Parse(test.flags); err != nil { -- return nil, fmt.Errorf("parsing flags: %v", err) +- si := symbolInformation{ +- score: score, +- symbol: strings.Join(symbolParts, ""), +- kind: sym.Kind, +- uri: i.uri, +- rng: sym.Range, +- container: string(i.mp.PkgPath), +- } +- store.store(si) - } +-} - -- return test, nil +-type symbolStore struct { +- res [maxSymbols]symbolInformation -} - --// formatTest formats the test as a txtar archive. --func formatTest(test *markerTest) ([]byte, error) { -- arch := &txtar.Archive{ -- Comment: test.archive.Comment, +-// store inserts si into the sorted results, if si has a high enough score. +-func (sc *symbolStore) store(si symbolInformation) { +- if sc.tooLow(si.score) { +- return - } -- -- updatedGolden := make(map[string][]byte) -- for id, g := range test.golden { -- for name, data := range g.updated { -- filename := "@" + path.Join(string(id), name) // name may be "" -- updatedGolden[filename] = data +- insertAt := sort.Search(len(sc.res), func(i int) bool { +- // Sort by score, then symbol length, and finally lexically. +- if sc.res[i].score != si.score { +- return sc.res[i].score < si.score - } -- } -- -- // Preserve the original ordering of archive files. -- for _, file := range test.archive.Files { -- switch file.Name { -- // Preserve configuration files exactly as they were. They must have parsed -- // if we got this far. -- case "skip", "flags", "settings.json", "capabilities.json", "env": -- arch.Files = append(arch.Files, file) -- default: -- if _, ok := test.files[file.Name]; ok { // ordinary file -- arch.Files = append(arch.Files, file) -- } else if strings.HasPrefix(file.Name, "proxy/") { // proxy file -- arch.Files = append(arch.Files, file) -- } else if data, ok := updatedGolden[file.Name]; ok { // golden file -- arch.Files = append(arch.Files, txtar.File{Name: file.Name, Data: data}) -- delete(updatedGolden, file.Name) -- } +- if len(sc.res[i].symbol) != len(si.symbol) { +- return len(sc.res[i].symbol) > len(si.symbol) - } -- } -- -- // ...followed by any new golden files. -- var newGoldenFiles []txtar.File -- for filename, data := range updatedGolden { -- // TODO(rfindley): it looks like this implicitly removes trailing newlines -- // from golden content. Is there any way to fix that? Perhaps we should -- // just make the diff tolerant of missing newlines? -- newGoldenFiles = append(newGoldenFiles, txtar.File{Name: filename, Data: data}) -- } -- // Sort new golden files lexically. -- sort.Slice(newGoldenFiles, func(i, j int) bool { -- return newGoldenFiles[i].Name < newGoldenFiles[j].Name +- return sc.res[i].symbol > si.symbol - }) -- arch.Files = append(arch.Files, newGoldenFiles...) -- -- return txtar.Format(arch), nil +- if insertAt < len(sc.res)-1 { +- copy(sc.res[insertAt+1:], sc.res[insertAt:len(sc.res)-1]) +- } +- sc.res[insertAt] = si -} - --// newEnv creates a new environment for a marker test. --// --// TODO(rfindley): simplify and refactor the construction of testing --// environments across regtests, marker tests, and benchmarks. --func newEnv(t *testing.T, cache *cache.Cache, files, proxyFiles map[string][]byte, writeGoSum []string, config fake.EditorConfig) *Env { -- sandbox, err := fake.NewSandbox(&fake.SandboxConfig{ -- RootDir: t.TempDir(), -- Files: files, -- ProxyFiles: proxyFiles, -- }) -- if err != nil { -- t.Fatal(err) -- } +-func (sc *symbolStore) tooLow(score float64) bool { +- return score <= sc.res[len(sc.res)-1].score +-} - -- for _, dir := range writeGoSum { -- if err := sandbox.RunGoCommand(context.Background(), dir, "list", []string{"-mod=mod", "..."}, []string{"GOWORK=off"}, true); err != nil { -- t.Fatal(err) +-func (sc *symbolStore) results() []protocol.SymbolInformation { +- var res []protocol.SymbolInformation +- for _, si := range sc.res { +- if si.score <= 0 { +- return res - } +- res = append(res, si.asProtocolSymbolInformation()) - } +- return res +-} - -- // Put a debug instance in the context to prevent logging to stderr. -- // See associated TODO in runner.go: we should revisit this pattern. -- ctx := context.Background() -- ctx = debug.WithInstance(ctx, "", "off") +-// symbolInformation is a cut-down version of protocol.SymbolInformation that +-// allows struct values of this type to be used as map keys. +-type symbolInformation struct { +- score float64 +- symbol string +- container string +- kind protocol.SymbolKind +- uri protocol.DocumentURI +- rng protocol.Range +-} - -- awaiter := NewAwaiter(sandbox.Workdir) -- ss := lsprpc.NewStreamServer(cache, false, hooks.Options) -- server := servertest.NewPipeServer(ss, jsonrpc2.NewRawStream) -- const skipApplyEdits = true // capture edits but don't apply them -- editor, err := fake.NewEditor(sandbox, config).Connect(ctx, server, awaiter.Hooks(), skipApplyEdits) -- if err != nil { -- sandbox.Close() // ignore error -- t.Fatal(err) -- } -- if err := awaiter.Await(ctx, InitialWorkspaceLoad); err != nil { -- sandbox.Close() // ignore error -- t.Fatal(err) -- } -- return &Env{ -- T: t, -- Ctx: ctx, -- Editor: editor, -- Sandbox: sandbox, -- Awaiter: awaiter, +-// asProtocolSymbolInformation converts s to a protocol.SymbolInformation value. +-// +-// TODO: work out how to handle tags if/when they are needed. +-func (s symbolInformation) asProtocolSymbolInformation() protocol.SymbolInformation { +- return protocol.SymbolInformation{ +- Name: s.symbol, +- Kind: s.kind, +- Location: protocol.Location{ +- URI: s.uri, +- Range: s.rng, +- }, +- ContainerName: s.container, - } -} +diff -urN a/gopls/internal/golang/workspace_symbol_test.go b/gopls/internal/golang/workspace_symbol_test.go +--- a/gopls/internal/golang/workspace_symbol_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/golang/workspace_symbol_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,138 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// A markerTestRun holds the state of one run of a marker test archive. --type markerTestRun struct { -- test *markerTest -- env *Env -- settings map[string]any +-package golang - -- // Collected information. -- // Each @diag/@suggestedfix marker eliminates an entry from diags. -- values map[expect.Identifier]any -- diags map[protocol.Location][]protocol.Diagnostic // diagnostics by position; location end == start +-import ( +- "testing" - -- // Notes that weren't associated with a top-level marker func. They may be -- // consumed by another marker (e.g. @codelenses collects @codelens markers). -- // Any notes that aren't consumed are flagged as an error. -- extraNotes map[protocol.DocumentURI]map[string][]*expect.Note --} +- "golang.org/x/tools/gopls/internal/cache" +-) - --// sprintf returns a formatted string after applying pre-processing to --// arguments of the following types: --// - token.Pos: formatted using (*markerTestRun).fmtPos --// - protocol.Location: formatted using (*markerTestRun).fmtLoc --func (c *marker) sprintf(format string, args ...any) string { -- if false { -- _ = fmt.Sprintf(format, args...) // enable vet printf checker -- } -- var args2 []any -- for _, arg := range args { -- switch arg := arg.(type) { -- case token.Pos: -- args2 = append(args2, c.run.fmtPos(arg)) -- case protocol.Location: -- args2 = append(args2, c.run.fmtLoc(arg)) -- default: -- args2 = append(args2, arg) -- } +-func TestParseQuery(t *testing.T) { +- tests := []struct { +- query, s string +- wantMatch bool +- }{ +- {"", "anything", false}, +- {"any", "anything", true}, +- {"any$", "anything", false}, +- {"ing$", "anything", true}, +- {"ing$", "anythinG", true}, +- {"inG$", "anything", false}, +- {"^any", "anything", true}, +- {"^any", "Anything", true}, +- {"^Any", "anything", false}, +- {"at", "anything", true}, +- // TODO: this appears to be a bug in the fuzzy matching algorithm. 'At' +- // should cause a case-sensitive match. +- // {"At", "anything", false}, +- {"At", "Anything", true}, +- {"'yth", "Anything", true}, +- {"'yti", "Anything", false}, +- {"'any 'thing", "Anything", true}, +- {"anythn nythg", "Anything", true}, +- {"ntx", "Anything", false}, +- {"anythn", "anything", true}, +- {"ing", "anything", true}, +- {"anythn nythgx", "anything", false}, - } -- return fmt.Sprintf(format, args2...) --} -- --// uri returns the URI of the file containing the marker. --func (mark marker) uri() protocol.DocumentURI { -- return mark.run.env.Sandbox.Workdir.URI(mark.run.test.fset.File(mark.note.Pos).Name()) --} -- --// path returns the relative path to the file containing the marker. --func (mark marker) path() string { -- return mark.run.env.Sandbox.Workdir.RelPath(mark.run.test.fset.File(mark.note.Pos).Name()) --} - --// fmtLoc formats the given pos in the context of the test, using --// archive-relative paths for files and including the line number in the full --// archive file. --func (run *markerTestRun) fmtPos(pos token.Pos) string { -- file := run.test.fset.File(pos) -- if file == nil { -- run.env.T.Errorf("position %d not in test fileset", pos) -- return "" -- } -- m, err := run.env.Editor.Mapper(file.Name()) -- if err != nil { -- run.env.T.Errorf("%s", err) -- return "" -- } -- loc, err := m.PosLocation(file, pos, pos) -- if err != nil { -- run.env.T.Errorf("Mapper(%s).PosLocation failed: %v", file.Name(), err) +- for _, test := range tests { +- matcher := parseQuery(test.query, newFuzzyMatcher) +- if _, score := matcher([]string{test.s}); score > 0 != test.wantMatch { +- t.Errorf("parseQuery(%q) match for %q: %.2g, want match: %t", test.query, test.s, score, test.wantMatch) +- } - } -- return run.fmtLoc(loc) -} - --// fmtLoc formats the given location in the context of the test, using --// archive-relative paths for files and including the line number in the full --// archive file. --func (run *markerTestRun) fmtLoc(loc protocol.Location) string { -- formatted := run.fmtLocDetails(loc, true) -- if formatted == "" { -- run.env.T.Errorf("unable to find %s in test archive", loc) -- return "" +-func TestFiltererDisallow(t *testing.T) { +- tests := []struct { +- filters []string +- included []string +- excluded []string +- }{ +- { +- []string{"+**/c.go"}, +- []string{"a/c.go", "a/b/c.go"}, +- []string{}, +- }, +- { +- []string{"+a/**/c.go"}, +- []string{"a/b/c.go", "a/b/d/c.go", "a/c.go"}, +- []string{}, +- }, +- { +- []string{"-a/c.go", "+a/**"}, +- []string{"a/c.go"}, +- []string{}, +- }, +- { +- []string{"+a/**/c.go", "-**/c.go"}, +- []string{}, +- []string{"a/b/c.go"}, +- }, +- { +- []string{"+a/**/c.go", "-a/**"}, +- []string{}, +- []string{"a/b/c.go"}, +- }, +- { +- []string{"+**/c.go", "-a/**/c.go"}, +- []string{}, +- []string{"a/b/c.go"}, +- }, +- { +- []string{"+foobar", "-foo"}, +- []string{"foobar", "foobar/a"}, +- []string{"foo", "foo/a"}, +- }, +- { +- []string{"+", "-"}, +- []string{}, +- []string{"foobar", "foobar/a", "foo", "foo/a"}, +- }, +- { +- []string{"-", "+"}, +- []string{"foobar", "foobar/a", "foo", "foo/a"}, +- []string{}, +- }, +- { +- []string{"-a/**/b/**/c.go"}, +- []string{}, +- []string{"a/x/y/z/b/f/g/h/c.go"}, +- }, +- // tests for unsupported glob operators +- { +- []string{"+**/c.go", "-a/*/c.go"}, +- []string{"a/b/c.go"}, +- []string{}, +- }, +- { +- []string{"+**/c.go", "-a/?/c.go"}, +- []string{"a/b/c.go"}, +- []string{}, +- }, +- { +- []string{"-b"}, // should only filter paths prefixed with the "b" directory +- []string{"a/b/c.go", "bb"}, +- []string{"b/c/d.go", "b"}, +- }, - } -- return formatted --} - --// See fmtLoc. If includeTxtPos is not set, the position in the full archive --// file is omitted. --// --// If the location cannot be found within the archive, fmtLocDetails returns "". --func (run *markerTestRun) fmtLocDetails(loc protocol.Location, includeTxtPos bool) string { -- if loc == (protocol.Location{}) { -- return "" -- } -- lines := bytes.Count(run.test.archive.Comment, []byte("\n")) -- var name string -- for _, f := range run.test.archive.Files { -- lines++ // -- separator -- -- uri := run.env.Sandbox.Workdir.URI(f.Name) -- if uri == loc.URI { -- name = f.Name -- break +- for _, test := range tests { +- filterer := cache.NewFilterer(test.filters) +- for _, inc := range test.included { +- if filterer.Disallow(inc) { +- t.Errorf("Filters %v excluded %v, wanted included", test.filters, inc) +- } - } -- lines += bytes.Count(f.Data, []byte("\n")) -- } -- if name == "" { -- return "" -- } -- m, err := run.env.Editor.Mapper(name) -- if err != nil { -- run.env.T.Errorf("internal error: %v", err) -- return "" -- } -- s, err := m.LocationSpan(loc) -- if err != nil { -- run.env.T.Errorf("error formatting location %s: %v", loc, err) -- return "" -- } - -- innerSpan := fmt.Sprintf("%d:%d", s.Start().Line(), s.Start().Column()) // relative to the embedded file -- outerSpan := fmt.Sprintf("%d:%d", lines+s.Start().Line(), s.Start().Column()) // relative to the archive file -- if s.Start() != s.End() { -- if s.End().Line() == s.Start().Line() { -- innerSpan += fmt.Sprintf("-%d", s.End().Column()) -- outerSpan += fmt.Sprintf("-%d", s.End().Column()) -- } else { -- innerSpan += fmt.Sprintf("-%d:%d", s.End().Line(), s.End().Column()) -- innerSpan += fmt.Sprintf("-%d:%d", lines+s.End().Line(), s.End().Column()) +- for _, exc := range test.excluded { +- if !filterer.Disallow(exc) { +- t.Errorf("Filters %v included %v, wanted excluded", test.filters, exc) +- } - } - } +-} +diff -urN a/gopls/internal/hooks/analysis_119.go b/gopls/internal/hooks/analysis_119.go +--- a/gopls/internal/hooks/analysis_119.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/hooks/analysis_119.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,14 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- if includeTxtPos { -- return fmt.Sprintf("%s:%s (%s:%s)", name, innerSpan, run.test.name, outerSpan) -- } else { -- return fmt.Sprintf("%s:%s", name, innerSpan) -- } +-//go:build !go1.20 +-// +build !go1.20 +- +-package hooks +- +-import "golang.org/x/tools/gopls/internal/settings" +- +-func updateAnalyzers(options *settings.Options) { +- options.StaticcheckSupported = false -} +diff -urN a/gopls/internal/hooks/analysis_120.go b/gopls/internal/hooks/analysis_120.go +--- a/gopls/internal/hooks/analysis_120.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/hooks/analysis_120.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,62 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// ---- converters ---- +-//go:build go1.20 +-// +build go1.20 - --// converter is the signature of argument converters. --// A converter should return an error rather than calling marker.errorf(). --type converter func(marker, any) (any, error) +-package hooks - --// Types with special conversions. --var ( -- goldenType = reflect.TypeOf(&Golden{}) -- locationType = reflect.TypeOf(protocol.Location{}) -- markerType = reflect.TypeOf(marker{}) -- regexpType = reflect.TypeOf(®exp.Regexp{}) -- wantErrorType = reflect.TypeOf(wantError{}) +-import ( +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/settings" +- "honnef.co/go/tools/analysis/lint" +- "honnef.co/go/tools/quickfix" +- "honnef.co/go/tools/simple" +- "honnef.co/go/tools/staticcheck" +- "honnef.co/go/tools/stylecheck" -) - --func convert(mark marker, arg any, paramType reflect.Type) (any, error) { -- if paramType == goldenType { -- id, ok := arg.(expect.Identifier) -- if !ok { -- return nil, fmt.Errorf("invalid input type %T: golden key must be an identifier", arg) +-func updateAnalyzers(options *settings.Options) { +- options.StaticcheckSupported = true +- +- mapSeverity := func(severity lint.Severity) protocol.DiagnosticSeverity { +- switch severity { +- case lint.SeverityError: +- return protocol.SeverityError +- case lint.SeverityDeprecated: +- // TODO(dh): in LSP, deprecated is a tag, not a severity. +- // We'll want to support this once we enable SA5011. +- return protocol.SeverityWarning +- case lint.SeverityWarning: +- return protocol.SeverityWarning +- case lint.SeverityInfo: +- return protocol.SeverityInformation +- case lint.SeverityHint: +- return protocol.SeverityHint +- default: +- return protocol.SeverityWarning - } -- return mark.run.test.getGolden(id), nil - } -- if id, ok := arg.(expect.Identifier); ok { -- if arg, ok := mark.run.values[id]; ok { -- if !reflect.TypeOf(arg).AssignableTo(paramType) { -- return nil, fmt.Errorf("cannot convert %v (%T) to %s", arg, arg, paramType) +- add := func(analyzers []*lint.Analyzer, skip map[string]struct{}) { +- for _, a := range analyzers { +- if _, ok := skip[a.Analyzer.Name]; ok { +- continue - } -- return arg, nil -- } -- } -- if reflect.TypeOf(arg).AssignableTo(paramType) { -- return arg, nil // no conversion required -- } -- switch paramType { -- case locationType: -- return convertLocation(mark, arg) -- case wantErrorType: -- return convertWantError(mark, arg) -- default: -- return nil, fmt.Errorf("cannot convert %v (%T) to %s", arg, arg, paramType) -- } --} - --// convertLocation converts a string or regexp argument into the protocol --// location corresponding to the first position of the string (or first match --// of the regexp) in the line preceding the note. --func convertLocation(mark marker, arg any) (protocol.Location, error) { -- switch arg := arg.(type) { -- case string: -- startOff, preceding, m, err := linePreceding(mark.run, mark.note.Pos) -- if err != nil { -- return protocol.Location{}, err -- } -- idx := bytes.Index(preceding, []byte(arg)) -- if idx < 0 { -- return protocol.Location{}, fmt.Errorf("substring %q not found in %q", arg, preceding) +- enabled := !a.Doc.NonDefault +- options.AddStaticcheckAnalyzer(a.Analyzer, enabled, mapSeverity(a.Doc.Severity)) - } -- off := startOff + idx -- return m.OffsetLocation(off, off+len(arg)) -- case *regexp.Regexp: -- return findRegexpInLine(mark.run, mark.note.Pos, arg) -- default: -- return protocol.Location{}, fmt.Errorf("cannot convert argument type %T to location (must be a string to match the preceding line)", arg) - } +- +- add(simple.Analyzers, nil) +- add(staticcheck.Analyzers, map[string]struct{}{ +- // This check conflicts with the vet printf check (golang/go#34494). +- "SA5009": {}, +- // This check relies on facts from dependencies, which +- // we don't currently compute. +- "SA5011": {}, +- }) +- add(stylecheck.Analyzers, nil) +- add(quickfix.Analyzers, nil) -} +diff -urN a/gopls/internal/hooks/gen-licenses.sh b/gopls/internal/hooks/gen-licenses.sh +--- a/gopls/internal/hooks/gen-licenses.sh 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/hooks/gen-licenses.sh 1970-01-01 00:00:00.000000000 +0000 +@@ -1,38 +0,0 @@ +-#!/bin/bash -eu - --// findRegexpInLine searches the partial line preceding pos for a match for the --// regular expression re, returning a location spanning the first match. If re --// contains exactly one subgroup, the position of this subgroup match is --// returned rather than the position of the full match. --func findRegexpInLine(run *markerTestRun, pos token.Pos, re *regexp.Regexp) (protocol.Location, error) { -- startOff, preceding, m, err := linePreceding(run, pos) -- if err != nil { -- return protocol.Location{}, err -- } +-# Copyright 2020 The Go Authors. All rights reserved. +-# Use of this source code is governed by a BSD-style +-# license that can be found in the LICENSE file. - -- matches := re.FindSubmatchIndex(preceding) -- if len(matches) == 0 { -- return protocol.Location{}, fmt.Errorf("no match for regexp %q found in %q", re, string(preceding)) -- } -- var start, end int -- switch len(matches) { -- case 2: -- // no subgroups: return the range of the regexp expression -- start, end = matches[0], matches[1] -- case 4: -- // one subgroup: return its range -- start, end = matches[2], matches[3] -- default: -- return protocol.Location{}, fmt.Errorf("invalid location regexp %q: expect either 0 or 1 subgroups, got %d", re, len(matches)/2-1) -- } +-set -o pipefail - -- return m.OffsetLocation(start+startOff, end+startOff) --} +-output=$1 +-tempfile=$(mktemp) +-cd $(dirname $0) - --func linePreceding(run *markerTestRun, pos token.Pos) (int, []byte, *protocol.Mapper, error) { -- file := run.test.fset.File(pos) -- posn := safetoken.Position(file, pos) -- lineStart := file.LineStart(posn.Line) -- startOff, endOff, err := safetoken.Offsets(file, lineStart, pos) -- if err != nil { -- return 0, nil, nil, err -- } -- m, err := run.env.Editor.Mapper(file.Name()) -- if err != nil { -- return 0, nil, nil, err -- } -- return startOff, m.Content[startOff:endOff], m, nil --} +-cat > $tempfile <> $tempfile +- echo >> $tempfile +- sed 's/^-- / &/' $dir/$license >> $tempfile +- echo >> $tempfile +-done - --// check asserts that 'err' matches the wantError's expectations. --func (we wantError) check(mark marker, err error) { -- if err == nil { -- mark.errorf("@%s succeeded unexpectedly, want %v", mark.note.Name, we) -- return -- } -- got := err.Error() +-echo "\`" >> $tempfile +-mv $tempfile $output +\ No newline at end of file +diff -urN a/gopls/internal/hooks/gofumpt_119.go b/gopls/internal/hooks/gofumpt_119.go +--- a/gopls/internal/hooks/gofumpt_119.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/hooks/gofumpt_119.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,13 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- if we.golden != nil { -- // Error message must match @id golden file. -- wantBytes, ok := we.golden.Get(mark.run.env.T, "", []byte(got)) -- if !ok { -- mark.errorf("@%s: missing @%s entry", mark.note.Name, we.golden.id) -- return -- } -- want := strings.TrimSpace(string(wantBytes)) -- if got != want { -- // (ignore leading/trailing space) -- mark.errorf("@%s failed with wrong error: got:\n%s\nwant:\n%s\ndiff:\n%s", -- mark.note.Name, got, want, compare.Text(want, got)) -- } +-//go:build !go1.20 +-// +build !go1.20 - -- } else if we.pattern != nil { -- // Error message must match regular expression pattern. -- if !we.pattern.MatchString(got) { -- mark.errorf("got error %q, does not match pattern %#q", got, we.pattern) -- } +-package hooks - -- } else if !strings.Contains(got, we.substr) { -- // Error message must contain expected substring. -- mark.errorf("got error %q, want substring %q", got, we.substr) -- } +-import "golang.org/x/tools/gopls/internal/settings" +- +-func updateGofumpt(options *settings.Options) { -} +diff -urN a/gopls/internal/hooks/gofumpt_120.go b/gopls/internal/hooks/gofumpt_120.go +--- a/gopls/internal/hooks/gofumpt_120.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/hooks/gofumpt_120.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,78 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// checkChangedFiles compares the files changed by an operation with their expected (golden) state. --func checkChangedFiles(mark marker, changed map[string][]byte, golden *Golden) { -- // Check changed files match expectations. -- for filename, got := range changed { -- if want, ok := golden.Get(mark.run.env.T, filename, got); !ok { -- mark.errorf("%s: unexpected change to file %s; got:\n%s", -- mark.note.Name, filename, got) +-//go:build go1.20 +-// +build go1.20 - -- } else if string(got) != string(want) { -- mark.errorf("%s: wrong file content for %s: got:\n%s\nwant:\n%s\ndiff:\n%s", -- mark.note.Name, filename, got, want, -- compare.Bytes(want, got)) -- } -- } +-package hooks - -- // Report unmet expectations. -- for filename := range golden.data { -- if _, ok := changed[filename]; !ok { -- want, _ := golden.Get(mark.run.env.T, filename, nil) -- mark.errorf("%s: missing change to file %s; want:\n%s", -- mark.note.Name, filename, want) -- } -- } --} +-import ( +- "context" +- "fmt" - --// checkDiffs computes unified diffs for each changed file, and compares with --// the diff content stored in the given golden directory. --func checkDiffs(mark marker, changed map[string][]byte, golden *Golden) { -- diffs := make(map[string]string) -- for name, after := range changed { -- before := mark.run.env.FileContent(name) -- edits := diff.Strings(before, string(after)) -- d, err := diff.ToUnified("before", "after", before, edits, 0) +- "golang.org/x/tools/gopls/internal/settings" +- "mvdan.cc/gofumpt/format" +-) +- +-func updateGofumpt(options *settings.Options) { +- options.GofumptFormat = func(ctx context.Context, langVersion, modulePath string, src []byte) ([]byte, error) { +- fixedVersion, err := fixLangVersion(langVersion) - if err != nil { -- // Can't happen: edits are consistent. -- log.Fatalf("internal error in diff.ToUnified: %v", err) +- return nil, err - } -- diffs[name] = d +- return format.Source(src, format.Options{ +- LangVersion: fixedVersion, +- ModulePath: modulePath, +- }) - } -- // Check changed files match expectations. -- for filename, got := range diffs { -- if want, ok := golden.Get(mark.run.env.T, filename, []byte(got)); !ok { -- mark.errorf("%s: unexpected change to file %s; got diff:\n%s", -- mark.note.Name, filename, got) +-} - -- } else if got != string(want) { -- mark.errorf("%s: wrong diff for %s:\n\ngot:\n%s\n\nwant:\n%s\n", -- mark.note.Name, filename, got, want) +-// fixLangVersion function cleans the input so that gofumpt doesn't panic. It is +-// rather permissive, and accepts version strings that aren't technically valid +-// in a go.mod file. +-// +-// More specifically, it looks for an optional 'v' followed by 1-3 +-// '.'-separated numbers. The resulting string is stripped of any suffix beyond +-// this expected version number pattern. +-// +-// See also golang/go#61692: gofumpt does not accept the new language versions +-// appearing in go.mod files (e.g. go1.21rc3). +-func fixLangVersion(input string) (string, error) { +- bad := func() (string, error) { +- return "", fmt.Errorf("invalid language version syntax %q", input) +- } +- if input == "" { +- return input, nil +- } +- i := 0 +- if input[0] == 'v' { // be flexible about 'v' +- i++ +- } +- // takeDigits consumes ascii numerals 0-9 and reports if at least one was +- // consumed. +- takeDigits := func() bool { +- found := false +- for ; i < len(input) && '0' <= input[i] && input[i] <= '9'; i++ { +- found = true - } +- return found - } -- // Report unmet expectations. -- for filename := range golden.data { -- if _, ok := changed[filename]; !ok { -- want, _ := golden.Get(mark.run.env.T, filename, nil) -- mark.errorf("%s: missing change to file %s; want:\n%s", -- mark.note.Name, filename, want) +- if !takeDigits() { // versions must start with at least one number +- return bad() +- } +- +- // Accept optional minor and patch versions. +- for n := 0; n < 2; n++ { +- if i < len(input) && input[i] == '.' { +- // Look for minor/patch version. +- i++ +- if !takeDigits() { +- i-- +- break +- } - } - } +- // Accept any suffix. +- return input[:i], nil -} +diff -urN a/gopls/internal/hooks/gofumpt_120_test.go b/gopls/internal/hooks/gofumpt_120_test.go +--- a/gopls/internal/hooks/gofumpt_120_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/hooks/gofumpt_120_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,53 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// ---- marker functions ---- -- --// TODO(rfindley): consolidate documentation of these markers. They are already --// documented above, so much of the documentation here is redundant. -- --// completionItem is a simplified summary of a completion item. --type completionItem struct { -- Label, Detail, Kind, Documentation string --} +-//go:build go1.20 +-// +build go1.20 - --func completionItemMarker(mark marker, label string, other ...string) completionItem { -- if len(other) > 3 { -- mark.errorf("too many arguments to @item: expect at most 4") -- } -- item := completionItem{ -- Label: label, -- } -- if len(other) > 0 { -- item.Detail = other[0] -- } -- if len(other) > 1 { -- item.Kind = other[1] -- } -- if len(other) > 2 { -- item.Documentation = other[2] -- } -- return item --} +-package hooks - --func rankMarker(mark marker, src protocol.Location, items ...completionItem) { -- list := mark.run.env.Completion(src) -- var got []string -- // Collect results that are present in items, preserving their order. -- for _, g := range list.Items { -- for _, w := range items { -- if g.Label == w.Label { -- got = append(got, g.Label) -- break -- } -- } -- } -- var want []string -- for _, w := range items { -- want = append(want, w.Label) -- } -- if diff := cmp.Diff(want, got); diff != "" { -- mark.errorf("completion rankings do not match (-want +got):\n%s", diff) -- } --} +-import "testing" - --func ranklMarker(mark marker, src protocol.Location, labels ...string) { -- list := mark.run.env.Completion(src) -- var got []string -- // Collect results that are present in items, preserving their order. -- for _, g := range list.Items { -- for _, label := range labels { -- if g.Label == label { -- got = append(got, g.Label) -- break -- } -- } -- } -- if diff := cmp.Diff(labels, got); diff != "" { -- mark.errorf("completion rankings do not match (-want +got):\n%s", diff) -- } --} +-func TestFixLangVersion(t *testing.T) { +- tests := []struct { +- input, want string +- wantErr bool +- }{ +- {"", "", false}, +- {"1.18", "1.18", false}, +- {"v1.18", "v1.18", false}, +- {"1.21", "1.21", false}, +- {"1.21rc3", "1.21", false}, +- {"1.21.0", "1.21.0", false}, +- {"1.21.1", "1.21.1", false}, +- {"v1.21.1", "v1.21.1", false}, +- {"v1.21.0rc1", "v1.21.0", false}, // not technically valid, but we're flexible +- {"v1.21.0.0", "v1.21.0", false}, // also technically invalid +- {"1.1", "1.1", false}, +- {"v1", "v1", false}, +- {"1", "1", false}, +- {"v1.21.", "v1.21", false}, // also invalid +- {"1.21.", "1.21", false}, - --func snippetMarker(mark marker, src protocol.Location, item completionItem, want string) { -- list := mark.run.env.Completion(src) -- var ( -- found bool -- got string -- all []string // for errors -- ) -- items := filterBuiltinsAndKeywords(mark, list.Items) -- for _, i := range items { -- all = append(all, i.Label) -- if i.Label == item.Label { -- found = true -- if i.TextEdit != nil { -- got = i.TextEdit.NewText -- } -- break -- } -- } -- if !found { -- mark.errorf("no completion item found matching %s (got: %v)", item.Label, all) -- return -- } -- if got != want { -- mark.errorf("snippets do not match: got %q, want %q", got, want) +- // Error cases. +- {"rc1", "", true}, +- {"x1.2.3", "", true}, - } --} - --// completeMarker implements the @complete marker, running --// textDocument/completion at the given src location and asserting that the --// results match the expected results. --func completeMarker(mark marker, src protocol.Location, want ...completionItem) { -- list := mark.run.env.Completion(src) -- items := filterBuiltinsAndKeywords(mark, list.Items) -- var got []completionItem -- for i, item := range items { -- simplified := completionItem{ -- Label: item.Label, -- Detail: item.Detail, -- Kind: fmt.Sprint(item.Kind), -- } -- if item.Documentation != nil { -- switch v := item.Documentation.Value.(type) { -- case string: -- simplified.Documentation = v -- case protocol.MarkupContent: -- simplified.Documentation = strings.TrimSpace(v.Value) // trim newlines -- } -- } -- // Support short-hand notation: if Detail, Kind, or Documentation are omitted from the -- // item, don't match them. -- if i < len(want) { -- if want[i].Detail == "" { -- simplified.Detail = "" -- } -- if want[i].Kind == "" { -- simplified.Kind = "" -- } -- if want[i].Documentation == "" { -- simplified.Documentation = "" +- for _, test := range tests { +- got, err := fixLangVersion(test.input) +- if test.wantErr { +- if err == nil { +- t.Errorf("fixLangVersion(%q) succeeded unexpectedly", test.input) - } -- } -- got = append(got, simplified) -- } -- if len(want) == 0 { -- want = nil // got is nil if empty -- } -- if diff := cmp.Diff(want, got); diff != "" { -- mark.errorf("Completion(...) returned unexpect results (-want +got):\n%s", diff) -- } --} -- --// filterBuiltinsAndKeywords filters out builtins and keywords from completion --// results. --// --// It over-approximates, and does not detect if builtins are shadowed. --func filterBuiltinsAndKeywords(mark marker, items []protocol.CompletionItem) []protocol.CompletionItem { -- keep := 0 -- for _, item := range items { -- if mark.run.test.filterKeywords && item.Kind == protocol.KeywordCompletion { - continue - } -- if mark.run.test.filterBuiltins && types.Universe.Lookup(item.Label) != nil { -- continue +- if err != nil { +- t.Fatalf("fixLangVersion(%q) failed: %v", test.input, err) - } -- items[keep] = item -- keep++ -- } -- return items[:keep] --} -- --// acceptCompletionMarker implements the @acceptCompletion marker, running --// textDocument/completion at the given src location and accepting the --// candidate with the given label. The resulting source must match the provided --// golden content. --func acceptCompletionMarker(mark marker, src protocol.Location, label string, golden *Golden) { -- list := mark.run.env.Completion(src) -- var selected *protocol.CompletionItem -- for _, item := range list.Items { -- if item.Label == label { -- selected = &item -- break +- if got != test.want { +- t.Errorf("fixLangVersion(%q) = %s, want %s", test.input, got, test.want) - } - } -- if selected == nil { -- mark.errorf("Completion(...) did not return an item labeled %q", label) -- return -- } -- filename := mark.path() -- mapper, err := mark.run.env.Editor.Mapper(filename) -- if err != nil { -- mark.errorf("Editor.Mapper(%s) failed: %v", filename, err) -- return -- } +-} +diff -urN a/gopls/internal/hooks/hooks.go b/gopls/internal/hooks/hooks.go +--- a/gopls/internal/hooks/hooks.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/hooks/hooks.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,20 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- patched, _, err := source.ApplyProtocolEdits(mapper, append([]protocol.TextEdit{ -- *selected.TextEdit, -- }, selected.AdditionalTextEdits...)) +-// Package hooks adds all the standard gopls implementations. +-// This can be used in tests without needing to use the gopls main, and is +-// also the place to edit for custom builds of gopls. +-package hooks // import "golang.org/x/tools/gopls/internal/hooks" - -- if err != nil { -- mark.errorf("ApplyProtocolEdits failed: %v", err) -- return -- } -- changes := map[string][]byte{filename: patched} -- // Check the file state. -- checkChangedFiles(mark, changes, golden) --} +-import ( +- "golang.org/x/tools/gopls/internal/settings" +- "mvdan.cc/xurls/v2" +-) - --// defMarker implements the @def marker, running textDocument/definition at --// the given src location and asserting that there is exactly one resulting --// location, matching dst. --// --// TODO(rfindley): support a variadic destination set. --func defMarker(mark marker, src, dst protocol.Location) { -- got := mark.run.env.GoToDefinition(src) -- if got != dst { -- mark.errorf("definition location does not match:\n\tgot: %s\n\twant %s", -- mark.run.fmtLoc(got), mark.run.fmtLoc(dst)) -- } +-func Options(options *settings.Options) { +- options.LicensesText = licensesText +- options.URLRegexp = xurls.Relaxed() +- updateAnalyzers(options) +- updateGofumpt(options) -} +diff -urN a/gopls/internal/hooks/licenses.go b/gopls/internal/hooks/licenses.go +--- a/gopls/internal/hooks/licenses.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/hooks/licenses.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,146 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func typedefMarker(mark marker, src, dst protocol.Location) { -- got := mark.run.env.TypeDefinition(src) -- if got != dst { -- mark.errorf("type definition location does not match:\n\tgot: %s\n\twant %s", -- mark.run.fmtLoc(got), mark.run.fmtLoc(dst)) -- } --} +-//go:generate ./gen-licenses.sh licenses.go +-package hooks - --func foldingRangeMarker(mark marker, g *Golden) { -- env := mark.run.env -- ranges, err := mark.server().FoldingRange(env.Ctx, &protocol.FoldingRangeParams{ -- TextDocument: protocol.TextDocumentIdentifier{URI: mark.uri()}, -- }) -- if err != nil { -- mark.errorf("foldingRange failed: %v", err) -- return -- } -- var edits []protocol.TextEdit -- insert := func(line, char uint32, text string) { -- pos := protocol.Position{Line: line, Character: char} -- edits = append(edits, protocol.TextEdit{ -- Range: protocol.Range{ -- Start: pos, -- End: pos, -- }, -- NewText: text, -- }) -- } -- for i, rng := range ranges { -- insert(rng.StartLine, rng.StartCharacter, fmt.Sprintf("<%d kind=%q>", i, rng.Kind)) -- insert(rng.EndLine, rng.EndCharacter, fmt.Sprintf("", i)) -- } -- filename := mark.path() -- mapper, err := env.Editor.Mapper(filename) -- if err != nil { -- mark.errorf("Editor.Mapper(%s) failed: %v", filename, err) -- return -- } -- got, _, err := source.ApplyProtocolEdits(mapper, edits) -- if err != nil { -- mark.errorf("ApplyProtocolEdits failed: %v", err) -- return -- } -- want, _ := g.Get(mark.run.env.T, "", got) -- if diff := compare.Bytes(want, got); diff != "" { -- mark.errorf("foldingRange mismatch:\n%s", diff) -- } --} +-const licensesText = ` +--- github.com/BurntSushi/toml COPYING -- - --// formatMarker implements the @format marker. --func formatMarker(mark marker, golden *Golden) { -- edits, err := mark.server().Formatting(mark.run.env.Ctx, &protocol.DocumentFormattingParams{ -- TextDocument: protocol.TextDocumentIdentifier{URI: mark.uri()}, -- }) -- var got []byte -- if err != nil { -- got = []byte(err.Error() + "\n") // all golden content is newline terminated -- } else { -- env := mark.run.env -- filename := mark.path() -- mapper, err := env.Editor.Mapper(filename) -- if err != nil { -- mark.errorf("Editor.Mapper(%s) failed: %v", filename, err) -- } +-The MIT License (MIT) - -- got, _, err = source.ApplyProtocolEdits(mapper, edits) -- if err != nil { -- mark.errorf("ApplyProtocolEdits failed: %v", err) -- return -- } -- } +-Copyright (c) 2013 TOML authors - -- compareGolden(mark, "format", got, golden) --} +-Permission is hereby granted, free of charge, to any person obtaining a copy +-of this software and associated documentation files (the "Software"), to deal +-in the Software without restriction, including without limitation the rights +-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +-copies of the Software, and to permit persons to whom the Software is +-furnished to do so, subject to the following conditions: - --func highlightMarker(mark marker, src protocol.Location, dsts ...protocol.Location) { -- highlights := mark.run.env.DocumentHighlight(src) -- var got []protocol.Range -- for _, h := range highlights { -- got = append(got, h.Range) -- } +-The above copyright notice and this permission notice shall be included in +-all copies or substantial portions of the Software. - -- var want []protocol.Range -- for _, d := range dsts { -- want = append(want, d.Range) -- } +-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +-THE SOFTWARE. - -- sortRanges := func(s []protocol.Range) { -- sort.Slice(s, func(i, j int) bool { -- return protocol.CompareRange(s[i], s[j]) < 0 -- }) -- } +--- github.com/google/go-cmp LICENSE -- - -- sortRanges(got) -- sortRanges(want) +-Copyright (c) 2017 The Go Authors. All rights reserved. - -- if diff := cmp.Diff(want, got); diff != "" { -- mark.errorf("DocumentHighlight(%v) mismatch (-want +got):\n%s", src, diff) -- } --} +-Redistribution and use in source and binary forms, with or without +-modification, are permitted provided that the following conditions are +-met: - --// hoverMarker implements the @hover marker, running textDocument/hover at the --// given src location and asserting that the resulting hover is over the dst --// location (typically a span surrounding src), and that the markdown content --// matches the golden content. --func hoverMarker(mark marker, src, dst protocol.Location, golden *Golden) { -- content, gotDst := mark.run.env.Hover(src) -- if gotDst != dst { -- mark.errorf("hover location does not match:\n\tgot: %s\n\twant %s)", mark.run.fmtLoc(gotDst), mark.run.fmtLoc(dst)) -- } -- gotMD := "" -- if content != nil { -- gotMD = content.Value -- } -- wantMD := "" -- if golden != nil { -- wantBytes, _ := golden.Get(mark.run.env.T, "hover.md", []byte(gotMD)) -- wantMD = string(wantBytes) -- } -- // Normalize newline termination: archive files can't express non-newline -- // terminated files. -- if strings.HasSuffix(wantMD, "\n") && !strings.HasSuffix(gotMD, "\n") { -- gotMD += "\n" -- } -- if diff := tests.DiffMarkdown(wantMD, gotMD); diff != "" { -- mark.errorf("hover markdown mismatch (-want +got):\n%s", diff) -- } --} +- * Redistributions of source code must retain the above copyright +-notice, this list of conditions and the following disclaimer. +- * Redistributions in binary form must reproduce the above +-copyright notice, this list of conditions and the following disclaimer +-in the documentation and/or other materials provided with the +-distribution. +- * Neither the name of Google Inc. nor the names of its +-contributors may be used to endorse or promote products derived from +-this software without specific prior written permission. - --// locMarker implements the @loc marker. It is executed before other --// markers, so that locations are available. --func locMarker(mark marker, loc protocol.Location) protocol.Location { return loc } +-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +-OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - --// diagMarker implements the @diag marker. It eliminates diagnostics from --// the observed set in mark.test. --func diagMarker(mark marker, loc protocol.Location, re *regexp.Regexp) { -- if _, ok := removeDiagnostic(mark, loc, re); !ok { -- mark.errorf("no diagnostic at %v matches %q", loc, re) -- } --} +--- honnef.co/go/tools LICENSE -- - --// removeDiagnostic looks for a diagnostic matching loc at the given position. --// --// If found, it returns (diag, true), and eliminates the matched diagnostic --// from the unmatched set. --// --// If not found, it returns (protocol.Diagnostic{}, false). --func removeDiagnostic(mark marker, loc protocol.Location, re *regexp.Regexp) (protocol.Diagnostic, bool) { -- loc.Range.End = loc.Range.Start // diagnostics ignore end position. -- diags := mark.run.diags[loc] -- for i, diag := range diags { -- if re.MatchString(diag.Message) { -- mark.run.diags[loc] = append(diags[:i], diags[i+1:]...) -- return diag, true -- } -- } -- return protocol.Diagnostic{}, false --} +-Copyright (c) 2016 Dominik Honnef - --// renameMarker implements the @rename(location, new, golden) marker. --func renameMarker(mark marker, loc protocol.Location, newName string, golden *Golden) { -- changed, err := rename(mark.run.env, loc, newName) -- if err != nil { -- mark.errorf("rename failed: %v. (Use @renameerr for expected errors.)", err) -- return -- } -- checkChangedFiles(mark, changed, golden) --} +-Permission is hereby granted, free of charge, to any person obtaining +-a copy of this software and associated documentation files (the +-"Software"), to deal in the Software without restriction, including +-without limitation the rights to use, copy, modify, merge, publish, +-distribute, sublicense, and/or sell copies of the Software, and to +-permit persons to whom the Software is furnished to do so, subject to +-the following conditions: - --// renameErrMarker implements the @renamererr(location, new, error) marker. --func renameErrMarker(mark marker, loc protocol.Location, newName string, wantErr wantError) { -- _, err := rename(mark.run.env, loc, newName) -- wantErr.check(mark, err) --} +-The above copyright notice and this permission notice shall be +-included in all copies or substantial portions of the Software. - --func signatureMarker(mark marker, src protocol.Location, label string, active int64) { -- got := mark.run.env.SignatureHelp(src) -- if label == "" { -- if got != nil && len(got.Signatures) > 0 { -- mark.errorf("signatureHelp = %v, want 0 signatures", got) -- } -- return -- } -- if got == nil || len(got.Signatures) != 1 { -- mark.errorf("signatureHelp = %v, want exactly 1 signature", got) -- return -- } -- if got := got.Signatures[0].Label; got != label { -- mark.errorf("signatureHelp: got label %q, want %q", got, label) -- } -- if got := int64(got.ActiveParameter); got != active { -- mark.errorf("signatureHelp: got active parameter %d, want %d", got, active) -- } --} +-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +-NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +-LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +-OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +-WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - --// rename returns the new contents of the files that would be modified --// by renaming the identifier at loc to newName. --func rename(env *Env, loc protocol.Location, newName string) (map[string][]byte, error) { -- // We call Server.Rename directly, instead of -- // env.Editor.Rename(env.Ctx, loc, newName) -- // to isolate Rename from PrepareRename, and because we don't -- // want to modify the file system in a scenario with multiple -- // @rename markers. +--- mvdan.cc/gofumpt LICENSE -- - -- editMap, err := env.Editor.Server.Rename(env.Ctx, &protocol.RenameParams{ -- TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, -- Position: loc.Range.Start, -- NewName: string(newName), -- }) -- if err != nil { -- return nil, err -- } +-Copyright (c) 2019, Daniel Martí. All rights reserved. - -- fileChanges := make(map[string][]byte) -- if err := applyDocumentChanges(env, editMap.DocumentChanges, fileChanges); err != nil { -- return nil, fmt.Errorf("applying document changes: %v", err) -- } -- return fileChanges, nil --} +-Redistribution and use in source and binary forms, with or without +-modification, are permitted provided that the following conditions are +-met: - --// applyDocumentChanges applies the given document changes to the editor buffer --// content, recording the resulting contents in the fileChanges map. It is an --// error for a change to an edit a file that is already present in the --// fileChanges map. --func applyDocumentChanges(env *Env, changes []protocol.DocumentChanges, fileChanges map[string][]byte) error { -- getMapper := func(path string) (*protocol.Mapper, error) { -- if _, ok := fileChanges[path]; ok { -- return nil, fmt.Errorf("internal error: %s is already edited", path) -- } -- return env.Editor.Mapper(path) -- } +- * Redistributions of source code must retain the above copyright +-notice, this list of conditions and the following disclaimer. +- * Redistributions in binary form must reproduce the above +-copyright notice, this list of conditions and the following disclaimer +-in the documentation and/or other materials provided with the +-distribution. +- * Neither the name of the copyright holder nor the names of its +-contributors may be used to endorse or promote products derived from +-this software without specific prior written permission. - -- for _, change := range changes { -- if change.RenameFile != nil { -- // rename -- oldFile := env.Sandbox.Workdir.URIToPath(change.RenameFile.OldURI) -- mapper, err := getMapper(oldFile) -- if err != nil { -- return err -- } -- newFile := env.Sandbox.Workdir.URIToPath(change.RenameFile.NewURI) -- fileChanges[newFile] = mapper.Content -- } else { -- // edit -- filename := env.Sandbox.Workdir.URIToPath(change.TextDocumentEdit.TextDocument.URI) -- mapper, err := getMapper(filename) -- if err != nil { -- return err -- } -- patched, _, err := source.ApplyProtocolEdits(mapper, change.TextDocumentEdit.Edits) -- if err != nil { -- return err -- } -- fileChanges[filename] = patched -- } -- } +-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +-OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -- return nil --} +--- mvdan.cc/xurls/v2 LICENSE -- - --func codeActionMarker(mark marker, start, end protocol.Location, actionKind string, g *Golden, titles ...string) { -- // Request the range from start.Start to end.End. -- loc := start -- loc.Range.End = end.Range.End +-Copyright (c) 2015, Daniel Martí. All rights reserved. - -- // Apply the fix it suggests. -- changed, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil, titles) -- if err != nil { -- mark.errorf("codeAction failed: %v", err) -- return -- } +-Redistribution and use in source and binary forms, with or without +-modification, are permitted provided that the following conditions are +-met: - -- // Check the file state. -- checkChangedFiles(mark, changed, g) --} +- * Redistributions of source code must retain the above copyright +-notice, this list of conditions and the following disclaimer. +- * Redistributions in binary form must reproduce the above +-copyright notice, this list of conditions and the following disclaimer +-in the documentation and/or other materials provided with the +-distribution. +- * Neither the name of the copyright holder nor the names of its +-contributors may be used to endorse or promote products derived from +-this software without specific prior written permission. - --func codeActionEditMarker(mark marker, loc protocol.Location, actionKind string, g *Golden, titles ...string) { -- changed, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil, titles) -- if err != nil { -- mark.errorf("codeAction failed: %v", err) -- return -- } +-THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +-"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +-LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +-A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +-OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +-SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +-LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +-DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +-THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +-OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -- checkDiffs(mark, changed, g) --} +-` +diff -urN a/gopls/internal/hooks/licenses_test.go b/gopls/internal/hooks/licenses_test.go +--- a/gopls/internal/hooks/licenses_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/hooks/licenses_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,47 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func codeActionErrMarker(mark marker, start, end protocol.Location, actionKind string, wantErr wantError) { -- loc := start -- loc.Range.End = end.Range.End -- _, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil, nil) -- wantErr.check(mark, err) --} +-package hooks - --// codeLensesMarker runs the @codelenses() marker, collecting @codelens marks --// in the current file and comparing with the result of the --// textDocument/codeLens RPC. --func codeLensesMarker(mark marker) { -- type codeLens struct { -- Range protocol.Range -- Title string -- } +-import ( +- "bytes" +- "os" +- "os/exec" +- "runtime" +- "testing" - -- lenses := mark.run.env.CodeLens(mark.path()) -- var got []codeLens -- for _, lens := range lenses { -- title := "" -- if lens.Command != nil { -- title = lens.Command.Title -- } -- got = append(got, codeLens{lens.Range, title}) -- } +- "golang.org/x/tools/internal/testenv" +-) - -- var want []codeLens -- mark.consumeExtraNotes("codelens", actionMarkerFunc(func(_ marker, loc protocol.Location, title string) { -- want = append(want, codeLens{loc.Range, title}) -- })) +-func TestLicenses(t *testing.T) { +- // License text differs for older Go versions because staticcheck or gofumpt +- // isn't supported for those versions, and this fails for unknown, unrelated +- // reasons on Kokoro legacy CI. +- testenv.NeedsGo1Point(t, 21) - -- for _, s := range [][]codeLens{got, want} { -- sort.Slice(s, func(i, j int) bool { -- li, lj := s[i], s[j] -- if c := protocol.CompareRange(li.Range, lj.Range); c != 0 { -- return c < 0 -- } -- return li.Title < lj.Title -- }) +- if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { +- t.Skip("generating licenses only works on Unixes") - } -- -- if diff := cmp.Diff(want, got); diff != "" { -- mark.errorf("codelenses: unexpected diff (-want +got):\n%s", diff) +- tmp, err := os.CreateTemp("", "") +- if err != nil { +- t.Fatal(err) - } --} +- tmp.Close() - --func documentLinkMarker(mark marker, g *Golden) { -- var b bytes.Buffer -- links := mark.run.env.DocumentLink(mark.path()) -- for _, l := range links { -- if l.Target == nil { -- mark.errorf("%s: nil link target", l.Range) -- continue -- } -- loc := protocol.Location{URI: mark.uri(), Range: l.Range} -- fmt.Fprintln(&b, mark.run.fmtLocDetails(loc, false), *l.Target) +- if out, err := exec.Command("./gen-licenses.sh", tmp.Name()).CombinedOutput(); err != nil { +- t.Fatalf("generating licenses failed: %q, %v", out, err) - } - -- compareGolden(mark, "documentLink", b.Bytes(), g) +- got, err := os.ReadFile(tmp.Name()) +- if err != nil { +- t.Fatal(err) +- } +- want, err := os.ReadFile("licenses.go") +- if err != nil { +- t.Fatal(err) +- } +- if !bytes.Equal(got, want) { +- t.Error("combined license text needs updating. Run: `go generate ./internal/hooks` from the gopls module.") +- } -} +diff -urN a/gopls/internal/lsprpc/autostart_default.go b/gopls/internal/lsprpc/autostart_default.go +--- a/gopls/internal/lsprpc/autostart_default.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/lsprpc/autostart_default.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,38 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// consumeExtraNotes runs the provided func for each extra note with the given --// name, and deletes all matching notes. --func (mark marker) consumeExtraNotes(name string, f func(marker)) { -- uri := mark.uri() -- notes := mark.run.extraNotes[uri][name] -- delete(mark.run.extraNotes[uri], name) +-package lsprpc - -- for _, note := range notes { -- f(marker{run: mark.run, note: note}) +-import ( +- "fmt" +- "os/exec" +-) +- +-var ( +- daemonize = func(*exec.Cmd) {} +- autoNetworkAddress = autoNetworkAddressDefault +- verifyRemoteOwnership = verifyRemoteOwnershipDefault +-) +- +-func runRemote(cmd *exec.Cmd) error { +- daemonize(cmd) +- if err := cmd.Start(); err != nil { +- return fmt.Errorf("starting remote gopls: %w", err) - } +- return nil -} - --// suggestedfixMarker implements the @suggestedfix(location, regexp, --// kind, golden) marker. It acts like @diag(location, regexp), to set --// the expectation of a diagnostic, but then it applies the first code --// action of the specified kind suggested by the matched diagnostic. --func suggestedfixMarker(mark marker, loc protocol.Location, re *regexp.Regexp, golden *Golden) { -- loc.Range.End = loc.Range.Start // diagnostics ignore end position. -- // Find and remove the matching diagnostic. -- diag, ok := removeDiagnostic(mark, loc, re) -- if !ok { -- mark.errorf("no diagnostic at %v matches %q", loc, re) -- return +-// autoNetworkAddressDefault returns the default network and address for the +-// automatically-started gopls remote. See autostart_posix.go for more +-// information. +-func autoNetworkAddressDefault(goplsPath, id string) (network string, address string) { +- if id != "" { +- panic("identified remotes are not supported on windows") - } +- return "tcp", "localhost:37374" +-} - -- // Apply the fix it suggests. -- changed, err := codeAction(mark.run.env, loc.URI, diag.Range, "quickfix", &diag, nil) -- if err != nil { -- mark.errorf("suggestedfix failed: %v. (Use @suggestedfixerr for expected errors.)", err) -- return -- } +-func verifyRemoteOwnershipDefault(network, address string) (bool, error) { +- return true, nil +-} +diff -urN a/gopls/internal/lsprpc/autostart_posix.go b/gopls/internal/lsprpc/autostart_posix.go +--- a/gopls/internal/lsprpc/autostart_posix.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/lsprpc/autostart_posix.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,96 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- // Check the file state. -- checkDiffs(mark, changed, golden) +-//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris +-// +build darwin dragonfly freebsd linux netbsd openbsd solaris +- +-package lsprpc +- +-import ( +- "crypto/sha256" +- "errors" +- "fmt" +- "log" +- "os" +- "os/exec" +- "os/user" +- "path/filepath" +- "strconv" +- "syscall" +-) +- +-func init() { +- daemonize = daemonizePosix +- autoNetworkAddress = autoNetworkAddressPosix +- verifyRemoteOwnership = verifyRemoteOwnershipPosix -} - --// codeAction executes a textDocument/codeAction request for the specified --// location and kind. If diag is non-nil, it is used as the code action --// context. --// --// The resulting map contains resulting file contents after the code action is --// applied. Currently, this function does not support code actions that return --// edits directly; it only supports code action commands. --func codeAction(env *Env, uri protocol.DocumentURI, rng protocol.Range, actionKind string, diag *protocol.Diagnostic, titles []string) (map[string][]byte, error) { -- changes, err := codeActionChanges(env, uri, rng, actionKind, diag, titles) -- if err != nil { -- return nil, err -- } -- fileChanges := make(map[string][]byte) -- if err := applyDocumentChanges(env, changes, fileChanges); err != nil { -- return nil, fmt.Errorf("applying document changes: %v", err) +-func daemonizePosix(cmd *exec.Cmd) { +- cmd.SysProcAttr = &syscall.SysProcAttr{ +- Setsid: true, - } -- return fileChanges, nil -} - --// codeActionChanges executes a textDocument/codeAction request for the --// specified location and kind, and captures the resulting document changes. --// If diag is non-nil, it is used as the code action context. --// If titles is non-empty, the code action title must be present among the provided titles. --func codeActionChanges(env *Env, uri protocol.DocumentURI, rng protocol.Range, actionKind string, diag *protocol.Diagnostic, titles []string) ([]protocol.DocumentChanges, error) { -- // Request all code actions that apply to the diagnostic. -- // (The protocol supports filtering using Context.Only={actionKind} -- // but we can give a better error if we don't filter.) -- params := &protocol.CodeActionParams{ -- TextDocument: protocol.TextDocumentIdentifier{URI: uri}, -- Range: rng, -- Context: protocol.CodeActionContext{ -- Only: nil, // => all kinds -- }, +-// autoNetworkAddressPosix resolves an id on the 'auto' pseduo-network to a +-// real network and address. On unix, this uses unix domain sockets. +-func autoNetworkAddressPosix(goplsPath, id string) (network string, address string) { +- // Especially when doing local development or testing, it's important that +- // the remote gopls instance we connect to is running the same binary as our +- // forwarder. So we encode a short hash of the binary path into the daemon +- // socket name. If possible, we also include the buildid in this hash, to +- // account for long-running processes where the binary has been subsequently +- // rebuilt. +- h := sha256.New() +- cmd := exec.Command("go", "tool", "buildid", goplsPath) +- cmd.Stdout = h +- var pathHash []byte +- if err := cmd.Run(); err == nil { +- pathHash = h.Sum(nil) +- } else { +- log.Printf("error getting current buildid: %v", err) +- sum := sha256.Sum256([]byte(goplsPath)) +- pathHash = sum[:] - } -- if diag != nil { -- params.Context.Diagnostics = []protocol.Diagnostic{*diag} +- shortHash := fmt.Sprintf("%x", pathHash)[:6] +- user := os.Getenv("USER") +- if user == "" { +- user = "shared" - } -- -- actions, err := env.Editor.Server.CodeAction(env.Ctx, params) -- if err != nil { -- return nil, err +- basename := filepath.Base(goplsPath) +- idComponent := "" +- if id != "" { +- idComponent = "-" + id +- } +- runtimeDir := os.TempDir() +- if xdg := os.Getenv("XDG_RUNTIME_DIR"); xdg != "" { +- runtimeDir = xdg - } +- return "unix", filepath.Join(runtimeDir, fmt.Sprintf("%s-%s-daemon.%s%s", basename, shortHash, user, idComponent)) +-} - -- // Find the sole candidates CodeAction of the specified kind (e.g. refactor.rewrite). -- var candidates []protocol.CodeAction -- for _, act := range actions { -- if act.Kind == protocol.CodeActionKind(actionKind) { -- if len(titles) > 0 { -- for _, f := range titles { -- if act.Title == f { -- candidates = append(candidates, act) -- break -- } -- } -- } else { -- candidates = append(candidates, act) -- } -- } +-func verifyRemoteOwnershipPosix(network, address string) (bool, error) { +- if network != "unix" { +- return true, nil - } -- if len(candidates) != 1 { -- for _, act := range actions { -- env.T.Logf("found CodeAction Kind=%s Title=%q", act.Kind, act.Title) +- fi, err := os.Stat(address) +- if err != nil { +- if os.IsNotExist(err) { +- return true, nil - } -- return nil, fmt.Errorf("found %d CodeActions of kind %s matching filters %v for this diagnostic, want 1", len(candidates), actionKind, titles) +- return false, fmt.Errorf("checking socket owner: %w", err) - } -- action := candidates[0] -- -- // Apply the codeAction. -- // -- // Spec: -- // "If a code action provides an edit and a command, first the edit is -- // executed and then the command." -- // An action may specify an edit and/or a command, to be -- // applied in that order. But since applyDocumentChanges(env, -- // action.Edit.DocumentChanges) doesn't compose, for now we -- // assert that actions return one or the other. -- if action.Edit != nil { -- if action.Edit.Changes != nil { -- env.T.Errorf("internal error: discarding unexpected CodeAction{Kind=%s, Title=%q}.Edit.Changes", action.Kind, action.Title) -- } -- if action.Edit.DocumentChanges != nil { -- if action.Command != nil { -- env.T.Errorf("internal error: discarding unexpected CodeAction{Kind=%s, Title=%q}.Command", action.Kind, action.Title) -- } -- return action.Edit.DocumentChanges, nil -- } +- stat, ok := fi.Sys().(*syscall.Stat_t) +- if !ok { +- return false, errors.New("fi.Sys() is not a Stat_t") +- } +- user, err := user.Current() +- if err != nil { +- return false, fmt.Errorf("checking current user: %w", err) +- } +- uid, err := strconv.ParseUint(user.Uid, 10, 32) +- if err != nil { +- return false, fmt.Errorf("parsing current UID: %w", err) - } +- return stat.Uid == uint32(uid), nil +-} +diff -urN a/gopls/internal/lsprpc/binder.go b/gopls/internal/lsprpc/binder.go +--- a/gopls/internal/lsprpc/binder.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/lsprpc/binder.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,5 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- if action.Command != nil { -- // This is a typical CodeAction command: -- // -- // Title: "Implement error" -- // Command: gopls.apply_fix -- // Arguments: [{"Fix":"stub_methods","URI":".../a.go","Range":...}}] -- // -- // The client makes an ExecuteCommand RPC to the server, -- // which dispatches it to the ApplyFix handler. -- // ApplyFix dispatches to the "stub_methods" suggestedfix hook (the meat). -- // The server then makes an ApplyEdit RPC to the client, -- // whose Awaiter hook gathers the edits instead of applying them. +-package lsprpc +diff -urN a/gopls/internal/lsprpc/binder_test.go b/gopls/internal/lsprpc/binder_test.go +--- a/gopls/internal/lsprpc/binder_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/lsprpc/binder_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,199 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- _ = env.Awaiter.takeDocumentChanges() // reset (assuming Env is confined to this thread) +-package lsprpc_test - -- if _, err := env.Editor.Server.ExecuteCommand(env.Ctx, &protocol.ExecuteCommandParams{ -- Command: action.Command.Command, -- Arguments: action.Command.Arguments, -- }); err != nil { -- return nil, err -- } -- return env.Awaiter.takeDocumentChanges(), nil -- } +-import ( +- "context" +- "regexp" +- "strings" +- "testing" +- "time" - -- return nil, nil --} +- "golang.org/x/tools/gopls/internal/protocol" +- jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" - --// TODO(adonovan): suggestedfixerr +- . "golang.org/x/tools/gopls/internal/lsprpc" +-) - --// refsMarker implements the @refs marker. --func refsMarker(mark marker, src protocol.Location, want ...protocol.Location) { -- refs := func(includeDeclaration bool, want []protocol.Location) error { -- got, err := mark.server().References(mark.run.env.Ctx, &protocol.ReferenceParams{ -- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(src), -- Context: protocol.ReferenceContext{ -- IncludeDeclaration: includeDeclaration, -- }, -- }) -- if err != nil { -- return err -- } +-// ServerBinder binds incoming connections to a new server. +-type ServerBinder struct { +- newServer ServerFunc +-} - -- return compareLocations(mark, got, want) -- } +-func NewServerBinder(newServer ServerFunc) *ServerBinder { +- return &ServerBinder{newServer: newServer} +-} - -- for _, includeDeclaration := range []bool{false, true} { -- // Ignore first 'want' location if we didn't request the declaration. -- // TODO(adonovan): don't assume a single declaration: -- // there may be >1 if corresponding methods are considered. -- want := want -- if !includeDeclaration && len(want) > 0 { -- want = want[1:] -- } -- if err := refs(includeDeclaration, want); err != nil { -- mark.errorf("refs(includeDeclaration=%t) failed: %v", -- includeDeclaration, err) +-// streamServer used to have this method, but it was never used. +-// TODO(adonovan): figure out whether we need any of this machinery +-// and, if not, delete it. In the meantime, it's better that it sit +-// in the test package with all the other mothballed machinery +-// than in the production code where it would couple streamServer +-// and ServerBinder. +-/* +-func (s *streamServer) Binder() *ServerBinder { +- newServer := func(ctx context.Context, client protocol.ClientCloser) protocol.Server { +- session := cache.NewSession(ctx, s.cache) +- svr := s.serverForTest +- if svr == nil { +- options := settings.DefaultOptions(s.optionsOverrides) +- svr = server.New(session, client, options) +- if instance := debug.GetInstance(ctx); instance != nil { +- instance.AddService(svr, session) +- } - } +- return svr - } +- return NewServerBinder(newServer) -} +-*/ - --// implementationMarker implements the @implementation marker. --func implementationMarker(mark marker, src protocol.Location, want ...protocol.Location) { -- got, err := mark.server().Implementation(mark.run.env.Ctx, &protocol.ImplementationParams{ -- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(src), +-func (b *ServerBinder) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions { +- client := protocol.ClientDispatcherV2(conn) +- server := b.newServer(ctx, client) +- serverHandler := protocol.ServerHandlerV2(server) +- // Wrap the server handler to inject the client into each request context, so +- // that log events are reflected back to the client. +- wrapped := jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) { +- ctx = protocol.WithClient(ctx, client) +- return serverHandler.Handle(ctx, req) - }) -- if err != nil { -- mark.errorf("implementation at %s failed: %v", src, err) -- return +- preempter := &Canceler{ +- Conn: conn, - } -- if err := compareLocations(mark, got, want); err != nil { -- mark.errorf("implementation: %v", err) +- return jsonrpc2_v2.ConnectionOptions{ +- Handler: wrapped, +- Preempter: preempter, - } -} - --func prepareRenameMarker(mark marker, src, spn protocol.Location, placeholder string) { -- params := &protocol.PrepareRenameParams{ -- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(src), -- } -- got, err := mark.run.env.Editor.Server.PrepareRename(mark.run.env.Ctx, params) -- if err != nil { -- mark.run.env.T.Fatal(err) +-type TestEnv struct { +- Conns []*jsonrpc2_v2.Connection +- Servers []*jsonrpc2_v2.Server +-} +- +-func (e *TestEnv) Shutdown(t *testing.T) { +- for _, s := range e.Servers { +- s.Shutdown() - } -- if placeholder == "" { -- if got != nil { -- mark.errorf("PrepareRename(...) = %v, want nil", got) +- for _, c := range e.Conns { +- if err := c.Close(); err != nil { +- t.Error(err) - } -- return - } -- want := &protocol.PrepareRename2Gn{Range: spn.Range, Placeholder: placeholder} -- if diff := cmp.Diff(want, got); diff != "" { -- mark.errorf("mismatching PrepareRename result:\n%s", diff) +- for _, s := range e.Servers { +- if err := s.Wait(); err != nil { +- t.Error(err) +- } - } -} - --// symbolMarker implements the @symbol marker. --func symbolMarker(mark marker, golden *Golden) { -- // Retrieve information about all symbols in this file. -- symbols, err := mark.server().DocumentSymbol(mark.run.env.Ctx, &protocol.DocumentSymbolParams{ -- TextDocument: protocol.TextDocumentIdentifier{URI: mark.uri()}, -- }) -- if err != nil { -- mark.errorf("DocumentSymbol request failed: %v", err) -- return +-func (e *TestEnv) serve(ctx context.Context, t *testing.T, server jsonrpc2_v2.Binder) (jsonrpc2_v2.Listener, *jsonrpc2_v2.Server) { +- l, err := jsonrpc2_v2.NetPipeListener(ctx) +- if err != nil { +- t.Fatal(err) - } +- s := jsonrpc2_v2.NewServer(ctx, l, server) +- e.Servers = append(e.Servers, s) +- return l, s +-} - -- // Format symbols one per line, sorted (in effect) by first column, a dotted name. -- var lines []string -- for _, symbol := range symbols { -- // Each result element is a union of (legacy) -- // SymbolInformation and (new) DocumentSymbol, -- // so we ascertain which one and then transcode. -- data, err := json.Marshal(symbol) -- if err != nil { -- mark.run.env.T.Fatal(err) -- } -- if _, ok := symbol.(map[string]any)["location"]; ok { -- // This case is not reached because Editor initialization -- // enables HierarchicalDocumentSymbolSupport. -- // TODO(adonovan): test this too. -- var sym protocol.SymbolInformation -- if err := json.Unmarshal(data, &sym); err != nil { -- mark.run.env.T.Fatal(err) -- } -- mark.errorf("fake Editor doesn't support SymbolInformation") -- -- } else { -- var sym protocol.DocumentSymbol // new hierarchical hotness -- if err := json.Unmarshal(data, &sym); err != nil { -- mark.run.env.T.Fatal(err) -- } -- -- // Print each symbol in the response tree. -- var visit func(sym protocol.DocumentSymbol, prefix []string) -- visit = func(sym protocol.DocumentSymbol, prefix []string) { -- var out strings.Builder -- out.WriteString(strings.Join(prefix, ".")) -- fmt.Fprintf(&out, " %q", sym.Detail) -- if delta := sym.Range.End.Line - sym.Range.Start.Line; delta > 0 { -- fmt.Fprintf(&out, " +%d lines", delta) -- } -- lines = append(lines, out.String()) -- -- for _, child := range sym.Children { -- visit(child, append(prefix, child.Name)) -- } -- } -- visit(sym, []string{sym.Name}) -- } +-func (e *TestEnv) dial(ctx context.Context, t *testing.T, dialer jsonrpc2_v2.Dialer, client jsonrpc2_v2.Binder, forwarded bool) *jsonrpc2_v2.Connection { +- if forwarded { +- l, _ := e.serve(ctx, t, NewForwardBinder(dialer)) +- dialer = l.Dialer() - } -- sort.Strings(lines) -- lines = append(lines, "") // match trailing newline in .txtar file -- got := []byte(strings.Join(lines, "\n")) -- -- // Compare with golden. -- want, ok := golden.Get(mark.run.env.T, "", got) -- if !ok { -- mark.errorf("%s: missing golden file @%s", mark.note.Name, golden.id) -- } else if diff := cmp.Diff(string(got), string(want)); diff != "" { -- mark.errorf("%s: unexpected output: got:\n%s\nwant:\n%s\ndiff:\n%s", -- mark.note.Name, got, want, diff) +- conn, err := jsonrpc2_v2.Dial(ctx, dialer, client) +- if err != nil { +- t.Fatal(err) - } +- e.Conns = append(e.Conns, conn) +- return conn -} - --// compareLocations returns an error message if got and want are not --// the same set of locations. The marker is used only for fmtLoc. --func compareLocations(mark marker, got, want []protocol.Location) error { -- toStrings := func(locs []protocol.Location) []string { -- strs := make([]string, len(locs)) -- for i, loc := range locs { -- strs[i] = mark.run.fmtLoc(loc) -- } -- sort.Strings(strs) -- return strs -- } -- if diff := cmp.Diff(toStrings(want), toStrings(got)); diff != "" { -- return fmt.Errorf("incorrect result locations: (got %d, want %d):\n%s", -- len(got), len(want), diff) -- } -- return nil +-func staticClientBinder(client protocol.Client) jsonrpc2_v2.Binder { +- f := func(context.Context, protocol.Server) protocol.Client { return client } +- return NewClientBinder(f) -} - --func workspaceSymbolMarker(mark marker, query string, golden *Golden) { -- params := &protocol.WorkspaceSymbolParams{ -- Query: query, +-func staticServerBinder(server protocol.Server) jsonrpc2_v2.Binder { +- f := func(ctx context.Context, client protocol.ClientCloser) protocol.Server { +- return server - } +- return NewServerBinder(f) +-} - -- gotSymbols, err := mark.server().Symbol(mark.run.env.Ctx, params) -- if err != nil { -- mark.errorf("Symbol(%q) failed: %v", query, err) -- return -- } -- var got bytes.Buffer -- for _, s := range gotSymbols { -- // Omit the txtar position of the symbol location; otherwise edits to the -- // txtar archive lead to unexpected failures. -- loc := mark.run.fmtLocDetails(s.Location, false) -- // TODO(rfindley): can we do better here, by detecting if the location is -- // relative to GOROOT? -- if loc == "" { -- loc = "" -- } -- fmt.Fprintf(&got, "%s %s %s\n", loc, s.Name, s.Kind) -- } +-func TestClientLoggingV2(t *testing.T) { +- ctx := context.Background() - -- compareGolden(mark, fmt.Sprintf("Symbol(%q)", query), got.Bytes(), golden) --} +- for name, forwarded := range map[string]bool{ +- "forwarded": true, +- "standalone": false, +- } { +- t.Run(name, func(t *testing.T) { +- client := FakeClient{Logs: make(chan string, 10)} +- env := new(TestEnv) +- defer env.Shutdown(t) +- l, _ := env.serve(ctx, t, staticServerBinder(PingServer{})) +- conn := env.dial(ctx, t, l.Dialer(), staticClientBinder(client), forwarded) - --// compareGolden compares the content of got with that of g.Get(""), reporting --// errors on any mismatch. --// --// TODO(rfindley): use this helper in more places. --func compareGolden(mark marker, op string, got []byte, g *Golden) { -- want, ok := g.Get(mark.run.env.T, "", got) -- if !ok { -- mark.errorf("missing golden file @%s", g.id) -- return +- if err := protocol.ServerDispatcherV2(conn).DidOpen(ctx, &protocol.DidOpenTextDocumentParams{}); err != nil { +- t.Errorf("DidOpen: %v", err) +- } +- select { +- case got := <-client.Logs: +- want := "ping" +- matched, err := regexp.MatchString(want, got) +- if err != nil { +- t.Fatal(err) +- } +- if !matched { +- t.Errorf("got log %q, want a log containing %q", got, want) +- } +- case <-time.After(1 * time.Second): +- t.Error("timeout waiting for client log") +- } +- }) - } -- if diff := compare.Bytes(want, got); diff != "" { -- mark.errorf("%s mismatch:\n%s", op, diff) +-} +- +-func TestRequestCancellationV2(t *testing.T) { +- ctx := context.Background() +- +- for name, forwarded := range map[string]bool{ +- "forwarded": true, +- "standalone": false, +- } { +- t.Run(name, func(t *testing.T) { +- server := WaitableServer{ +- Started: make(chan struct{}), +- Completed: make(chan error), +- } +- env := new(TestEnv) +- defer env.Shutdown(t) +- l, _ := env.serve(ctx, t, staticServerBinder(server)) +- client := FakeClient{Logs: make(chan string, 10)} +- conn := env.dial(ctx, t, l.Dialer(), staticClientBinder(client), forwarded) +- +- sd := protocol.ServerDispatcherV2(conn) +- ctx, cancel := context.WithCancel(ctx) +- +- result := make(chan error) +- go func() { +- _, err := sd.Hover(ctx, &protocol.HoverParams{}) +- result <- err +- }() +- // Wait for the Hover request to start. +- <-server.Started +- cancel() +- if err := <-result; err == nil { +- t.Error("nil error for cancelled Hover(), want non-nil") +- } +- if err := <-server.Completed; err == nil || !strings.Contains(err.Error(), "cancelled hover") { +- t.Errorf("Hover(): unexpected server-side error %v", err) +- } +- }) - } -} -diff -urN a/gopls/internal/lsp/regtest/options.go b/gopls/internal/lsp/regtest/options.go ---- a/gopls/internal/lsp/regtest/options.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/regtest/options.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,134 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/lsprpc/commandinterceptor_test.go b/gopls/internal/lsprpc/commandinterceptor_test.go +--- a/gopls/internal/lsprpc/commandinterceptor_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/lsprpc/commandinterceptor_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,61 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package regtest +-package lsprpc_test - -import ( -- "golang.org/x/tools/gopls/internal/lsp/fake" -- "golang.org/x/tools/gopls/internal/lsp/protocol" +- "context" +- "encoding/json" +- "testing" +- +- "golang.org/x/tools/gopls/internal/protocol" +- jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" +- +- . "golang.org/x/tools/gopls/internal/lsprpc" -) - --type runConfig struct { -- editor fake.EditorConfig -- sandbox fake.SandboxConfig -- modes Mode -- skipHooks bool +-func CommandInterceptor(command string, run func(*protocol.ExecuteCommandParams) (interface{}, error)) Middleware { +- return BindHandler(func(delegate jsonrpc2_v2.Handler) jsonrpc2_v2.Handler { +- return jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) { +- if req.Method == "workspace/executeCommand" { +- var params protocol.ExecuteCommandParams +- if err := json.Unmarshal(req.Params, ¶ms); err == nil { +- if params.Command == command { +- return run(¶ms) +- } +- } +- } +- +- return delegate.Handle(ctx, req) +- }) +- }) -} - --func defaultConfig() runConfig { -- return runConfig{ -- editor: fake.EditorConfig{ -- Settings: map[string]interface{}{ -- // Shorten the diagnostic delay to speed up test execution (else we'd add -- // the default delay to each assertion about diagnostics) -- "diagnosticsDelay": "10ms", -- }, -- }, +-func TestCommandInterceptor(t *testing.T) { +- const command = "foo" +- caught := false +- intercept := func(_ *protocol.ExecuteCommandParams) (interface{}, error) { +- caught = true +- return map[string]interface{}{}, nil - } --} - --// A RunOption augments the behavior of the test runner. --type RunOption interface { -- set(*runConfig) +- ctx := context.Background() +- env := new(TestEnv) +- defer env.Shutdown(t) +- mw := CommandInterceptor(command, intercept) +- l, _ := env.serve(ctx, t, mw(noopBinder)) +- conn := env.dial(ctx, t, l.Dialer(), noopBinder, false) +- +- params := &protocol.ExecuteCommandParams{ +- Command: command, +- } +- var res interface{} +- err := conn.Call(ctx, "workspace/executeCommand", params).Await(ctx, &res) +- if err != nil { +- t.Fatal(err) +- } +- if !caught { +- t.Errorf("workspace/executeCommand was not intercepted") +- } -} +diff -urN a/gopls/internal/lsprpc/dialer.go b/gopls/internal/lsprpc/dialer.go +--- a/gopls/internal/lsprpc/dialer.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/lsprpc/dialer.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,114 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --type optionSetter func(*runConfig) +-package lsprpc - --func (f optionSetter) set(opts *runConfig) { -- f(opts) --} +-import ( +- "context" +- "fmt" +- "io" +- "net" +- "os" +- "os/exec" +- "time" - --// ProxyFiles configures a file proxy using the given txtar-encoded string. --func ProxyFiles(txt string) RunOption { -- return optionSetter(func(opts *runConfig) { -- opts.sandbox.ProxyFiles = fake.UnpackTxt(txt) -- }) --} +- "golang.org/x/tools/internal/event" +-) - --// Modes configures the execution modes that the test should run in. --// --// By default, modes are configured by the test runner. If this option is set, --// it overrides the set of default modes and the test runs in exactly these --// modes. --func Modes(modes Mode) RunOption { -- return optionSetter(func(opts *runConfig) { -- if opts.modes != 0 { -- panic("modes set more than once") -- } -- opts.modes = modes -- }) --} +-// autoNetwork is the pseudo network type used to signal that gopls should use +-// automatic discovery to resolve a remote address. +-const autoNetwork = "auto" - --// WindowsLineEndings configures the editor to use windows line endings. --func WindowsLineEndings() RunOption { -- return optionSetter(func(opts *runConfig) { -- opts.editor.WindowsLineEndings = true -- }) --} +-// An autoDialer is a jsonrpc2 dialer that understands the 'auto' network. +-type autoDialer struct { +- network, addr string // the 'real' network and address +- isAuto bool // whether the server is on the 'auto' network - --// ClientName sets the LSP client name. --func ClientName(name string) RunOption { -- return optionSetter(func(opts *runConfig) { -- opts.editor.ClientName = name -- }) +- executable string +- argFunc func(network, addr string) []string -} - --// Settings sets user-provided configuration for the LSP server. --// --// As a special case, the env setting must not be provided via Settings: use --// EnvVars instead. --type Settings map[string]interface{} -- --func (s Settings) set(opts *runConfig) { -- if opts.editor.Settings == nil { -- opts.editor.Settings = make(map[string]interface{}) +-func newAutoDialer(rawAddr string, argFunc func(network, addr string) []string) (*autoDialer, error) { +- d := autoDialer{ +- argFunc: argFunc, - } -- for k, v := range s { -- opts.editor.Settings[k] = v +- d.network, d.addr = ParseAddr(rawAddr) +- if d.network == autoNetwork { +- d.isAuto = true +- bin, err := os.Executable() +- if err != nil { +- return nil, fmt.Errorf("getting executable: %w", err) +- } +- d.executable = bin +- d.network, d.addr = autoNetworkAddress(bin, d.addr) - } +- return &d, nil -} - --// WorkspaceFolders configures the workdir-relative workspace folders to send --// to the LSP server. By default the editor sends a single workspace folder --// corresponding to the workdir root. To explicitly configure no workspace --// folders, use WorkspaceFolders with no arguments. --func WorkspaceFolders(relFolders ...string) RunOption { -- if len(relFolders) == 0 { -- // Use an empty non-nil slice to signal explicitly no folders. -- relFolders = []string{} -- } -- return optionSetter(func(opts *runConfig) { -- opts.editor.WorkspaceFolders = relFolders -- }) +-// Dial implements the jsonrpc2.Dialer interface. +-func (d *autoDialer) Dial(ctx context.Context) (io.ReadWriteCloser, error) { +- conn, err := d.dialNet(ctx) +- return conn, err -} - --// EnvVars sets environment variables for the LSP session. When applying these --// variables to the session, the special string $SANDBOX_WORKDIR is replaced by --// the absolute path to the sandbox working directory. --type EnvVars map[string]string -- --func (e EnvVars) set(opts *runConfig) { -- if opts.editor.Env == nil { -- opts.editor.Env = make(map[string]string) +-// TODO(rFindley): remove this once we no longer need to integrate with v1 of +-// the jsonrpc2 package. +-func (d *autoDialer) dialNet(ctx context.Context) (net.Conn, error) { +- // Attempt to verify that we own the remote. This is imperfect, but if we can +- // determine that the remote is owned by a different user, we should fail. +- ok, err := verifyRemoteOwnership(d.network, d.addr) +- if err != nil { +- // If the ownership check itself failed, we fail open but log an error to +- // the user. +- event.Error(ctx, "unable to check daemon socket owner, failing open", err) +- } else if !ok { +- // We successfully checked that the socket is not owned by us, we fail +- // closed. +- return nil, fmt.Errorf("socket %q is owned by a different user", d.addr) - } -- for k, v := range e { -- opts.editor.Env[k] = v +- const dialTimeout = 1 * time.Second +- // Try dialing our remote once, in case it is already running. +- netConn, err := net.DialTimeout(d.network, d.addr, dialTimeout) +- if err == nil { +- return netConn, nil +- } +- if d.isAuto && d.argFunc != nil { +- if d.network == "unix" { +- // Sometimes the socketfile isn't properly cleaned up when the server +- // shuts down. Since we have already tried and failed to dial this +- // address, it should *usually* be safe to remove the socket before +- // binding to the address. +- // TODO(rfindley): there is probably a race here if multiple server +- // instances are simultaneously starting up. +- if _, err := os.Stat(d.addr); err == nil { +- if err := os.Remove(d.addr); err != nil { +- return nil, fmt.Errorf("removing remote socket file: %w", err) +- } +- } +- } +- args := d.argFunc(d.network, d.addr) +- cmd := exec.Command(d.executable, args...) +- if err := runRemote(cmd); err != nil { +- return nil, err +- } - } --} -- --// InGOPATH configures the workspace working directory to be GOPATH, rather --// than a separate working directory for use with modules. --func InGOPATH() RunOption { -- return optionSetter(func(opts *runConfig) { -- opts.sandbox.InGoPath = true -- }) --} - --// MessageResponder configures the editor to respond to --// window/showMessageRequest messages using the provided function. --func MessageResponder(f func(*protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error)) RunOption { -- return optionSetter(func(opts *runConfig) { -- opts.editor.MessageResponder = f -- }) +- const retries = 5 +- // It can take some time for the newly started server to bind to our address, +- // so we retry for a bit. +- for retry := 0; retry < retries; retry++ { +- startDial := time.Now() +- netConn, err = net.DialTimeout(d.network, d.addr, dialTimeout) +- if err == nil { +- return netConn, nil +- } +- event.Log(ctx, fmt.Sprintf("failed attempt #%d to connect to remote: %v\n", retry+2, err)) +- // In case our failure was a fast-failure, ensure we wait at least +- // f.dialTimeout before trying again. +- if retry != retries-1 { +- time.Sleep(dialTimeout - time.Since(startDial)) +- } +- } +- return nil, fmt.Errorf("dialing remote: %w", err) -} -diff -urN a/gopls/internal/lsp/regtest/regtest.go b/gopls/internal/lsp/regtest/regtest.go ---- a/gopls/internal/lsp/regtest/regtest.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/regtest/regtest.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,156 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/lsprpc/export_test.go b/gopls/internal/lsprpc/export_test.go +--- a/gopls/internal/lsprpc/export_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/lsprpc/export_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,142 +0,0 @@ +-// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package regtest +-package lsprpc +- +-// This file defines things (and opens backdoors) needed only by tests. - -import ( - "context" -- "flag" +- "encoding/json" - "fmt" -- "os" -- "runtime" -- "testing" -- "time" -- -- "golang.org/x/tools/gopls/internal/lsp/cmd" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/internal/gocommand" -- "golang.org/x/tools/internal/memoize" -- "golang.org/x/tools/internal/testenv" -- "golang.org/x/tools/internal/tool" --) - --var ( -- runSubprocessTests = flag.Bool("enable_gopls_subprocess_tests", false, "run regtests against a gopls subprocess") -- goplsBinaryPath = flag.String("gopls_test_binary", "", "path to the gopls binary for use as a remote, for use with the -enable_gopls_subprocess_tests flag") -- regtestTimeout = flag.Duration("regtest_timeout", defaultRegtestTimeout(), "if nonzero, default timeout for each regtest; defaults to GOPLS_REGTEST_TIMEOUT") -- skipCleanup = flag.Bool("regtest_skip_cleanup", false, "whether to skip cleaning up temp directories") -- printGoroutinesOnFailure = flag.Bool("regtest_print_goroutines", false, "whether to print goroutines info on failure") -- printLogs = flag.Bool("regtest_print_logs", false, "whether to print LSP logs") +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/event" +- jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" +- "golang.org/x/tools/internal/xcontext" -) - --func defaultRegtestTimeout() time.Duration { -- s := os.Getenv("GOPLS_REGTEST_TIMEOUT") -- if s == "" { -- return 0 -- } -- d, err := time.ParseDuration(s) -- if err != nil { -- fmt.Fprintf(os.Stderr, "invalid GOPLS_REGTEST_TIMEOUT %q: %v\n", s, err) -- os.Exit(2) -- } -- return d --} -- --var runner *Runner +-const HandshakeMethod = handshakeMethod - --type regtestRunner interface { -- Run(t *testing.T, files string, f TestFunc) --} +-// A ServerFunc is used to construct an LSP server for a given client. +-type ServerFunc func(context.Context, protocol.ClientCloser) protocol.Server - --func Run(t *testing.T, files string, f TestFunc) { -- runner.Run(t, files, f) +-type Canceler struct { +- Conn *jsonrpc2_v2.Connection -} - --func WithOptions(opts ...RunOption) configuredRunner { -- return configuredRunner{opts: opts} +-func (c *Canceler) Preempt(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) { +- if req.Method != "$/cancelRequest" { +- return nil, jsonrpc2_v2.ErrNotHandled +- } +- var params protocol.CancelParams +- if err := json.Unmarshal(req.Params, ¶ms); err != nil { +- return nil, fmt.Errorf("%w: %v", jsonrpc2_v2.ErrParse, err) +- } +- var id jsonrpc2_v2.ID +- switch raw := params.ID.(type) { +- case float64: +- id = jsonrpc2_v2.Int64ID(int64(raw)) +- case string: +- id = jsonrpc2_v2.StringID(raw) +- default: +- return nil, fmt.Errorf("%w: invalid ID type %T", jsonrpc2_v2.ErrParse, params.ID) +- } +- c.Conn.Cancel(id) +- return nil, nil -} - --type configuredRunner struct { -- opts []RunOption +-type ForwardBinder struct { +- dialer jsonrpc2_v2.Dialer +- onBind func(*jsonrpc2_v2.Connection) -} - --func (r configuredRunner) Run(t *testing.T, files string, f TestFunc) { -- runner.Run(t, files, f, r.opts...) +-func NewForwardBinder(dialer jsonrpc2_v2.Dialer) *ForwardBinder { +- return &ForwardBinder{ +- dialer: dialer, +- } -} - --type RunMultiple []struct { -- Name string -- Runner regtestRunner --} +-func (b *ForwardBinder) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) (opts jsonrpc2_v2.ConnectionOptions) { +- client := protocol.ClientDispatcherV2(conn) +- clientBinder := NewClientBinder(func(context.Context, protocol.Server) protocol.Client { return client }) - --func (r RunMultiple) Run(t *testing.T, files string, f TestFunc) { -- for _, runner := range r { -- t.Run(runner.Name, func(t *testing.T) { -- runner.Runner.Run(t, files, f) -- }) +- serverConn, err := jsonrpc2_v2.Dial(context.Background(), b.dialer, clientBinder) +- if err != nil { +- return jsonrpc2_v2.ConnectionOptions{ +- Handler: jsonrpc2_v2.HandlerFunc(func(context.Context, *jsonrpc2_v2.Request) (interface{}, error) { +- return nil, fmt.Errorf("%w: %v", jsonrpc2_v2.ErrInternal, err) +- }), +- } - } --} - --// DefaultModes returns the default modes to run for each regression test (they --// may be reconfigured by the tests themselves). --func DefaultModes() Mode { -- modes := Default -- if !testing.Short() { -- modes |= Experimental | Forwarded +- if b.onBind != nil { +- b.onBind(serverConn) - } -- if *runSubprocessTests { -- modes |= SeparateProcess +- server := protocol.ServerDispatcherV2(serverConn) +- preempter := &Canceler{ +- Conn: conn, +- } +- detached := xcontext.Detach(ctx) +- go func() { +- conn.Wait() +- if err := serverConn.Close(); err != nil { +- event.Log(detached, fmt.Sprintf("closing remote connection: %v", err)) +- } +- }() +- return jsonrpc2_v2.ConnectionOptions{ +- Handler: protocol.ServerHandlerV2(server), +- Preempter: preempter, - } -- return modes -} - --// Main sets up and tears down the shared regtest state. --func Main(m *testing.M, hook func(*source.Options)) { -- // golang/go#54461: enable additional debugging around hanging Go commands. -- gocommand.DebugHangingGoCommands = true +-func NewClientBinder(newClient ClientFunc) *clientBinder { +- return &clientBinder{newClient} +-} - -- // If this magic environment variable is set, run gopls instead of the test -- // suite. See the documentation for runTestAsGoplsEnvvar for more details. -- if os.Getenv(runTestAsGoplsEnvvar) == "true" { -- tool.Main(context.Background(), cmd.New("gopls", "", nil, hook), os.Args[1:]) -- os.Exit(0) -- } +-// A ClientFunc is used to construct an LSP client for a given server. +-type ClientFunc func(context.Context, protocol.Server) protocol.Client - -- if !testenv.HasExec() { -- fmt.Printf("skipping all tests: exec not supported on %s/%s\n", runtime.GOOS, runtime.GOARCH) -- os.Exit(0) +-// clientBinder binds an LSP client to an incoming connection. +-type clientBinder struct { +- newClient ClientFunc +-} +- +-func (b *clientBinder) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions { +- server := protocol.ServerDispatcherV2(conn) +- client := b.newClient(ctx, server) +- return jsonrpc2_v2.ConnectionOptions{ +- Handler: protocol.ClientHandlerV2(client), - } -- testenv.ExitIfSmallMachine() +-} - -- // Disable GOPACKAGESDRIVER, as it can cause spurious test failures. -- os.Setenv("GOPACKAGESDRIVER", "off") +-// HandlerMiddleware is a middleware that only modifies the jsonrpc2 handler. +-type HandlerMiddleware func(jsonrpc2_v2.Handler) jsonrpc2_v2.Handler - -- flag.Parse() +-// BindHandler transforms a HandlerMiddleware into a Middleware. +-func BindHandler(hmw HandlerMiddleware) Middleware { +- return Middleware(func(binder jsonrpc2_v2.Binder) jsonrpc2_v2.Binder { +- return BinderFunc(func(ctx context.Context, conn *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions { +- opts := binder.Bind(ctx, conn) +- opts.Handler = hmw(opts.Handler) +- return opts +- }) +- }) +-} - -- runner = &Runner{ -- DefaultModes: DefaultModes(), -- Timeout: *regtestTimeout, -- PrintGoroutinesOnFailure: *printGoroutinesOnFailure, -- SkipCleanup: *skipCleanup, -- OptionsHook: hook, -- store: memoize.NewStore(memoize.NeverEvict), -- } +-// The BinderFunc type adapts a bind function to implement the jsonrpc2.Binder +-// interface. +-type BinderFunc func(ctx context.Context, conn *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions - -- runner.goplsPath = *goplsBinaryPath -- if runner.goplsPath == "" { -- var err error -- runner.goplsPath, err = os.Executable() -- if err != nil { -- panic(fmt.Sprintf("finding test binary path: %v", err)) -- } -- } +-func (f BinderFunc) Bind(ctx context.Context, conn *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions { +- return f(ctx, conn) +-} - -- dir, err := os.MkdirTemp("", "gopls-regtest-") -- if err != nil { -- panic(fmt.Errorf("creating regtest temp directory: %v", err)) -- } -- runner.tempDir = dir +-// Middleware defines a transformation of jsonrpc2 Binders, that may be +-// composed to build jsonrpc2 servers. +-type Middleware func(jsonrpc2_v2.Binder) jsonrpc2_v2.Binder - -- var code int -- defer func() { -- if err := runner.Close(); err != nil { -- fmt.Fprintf(os.Stderr, "closing test runner: %v\n", err) -- // Regtest cleanup is broken in go1.12 and earlier, and sometimes flakes on -- // Windows due to file locking, but this is OK for our CI. -- // -- // Fail on go1.13+, except for windows and android which have shutdown problems. -- if testenv.Go1Point() >= 13 && runtime.GOOS != "windows" && runtime.GOOS != "android" { -- os.Exit(1) -- } -- } -- os.Exit(code) -- }() -- code = m.Run() --} -diff -urN a/gopls/internal/lsp/regtest/runner.go b/gopls/internal/lsp/regtest/runner.go ---- a/gopls/internal/lsp/regtest/runner.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/regtest/runner.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,436 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +-var GetGoEnv = getGoEnv +- +-type StreamServer = streamServer +diff -urN a/gopls/internal/lsprpc/goenv.go b/gopls/internal/lsprpc/goenv.go +--- a/gopls/internal/lsprpc/goenv.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/lsprpc/goenv.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,34 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package regtest +-package lsprpc - -import ( -- "bytes" - "context" +- "encoding/json" - "fmt" -- "io" -- "net" -- "os" -- "path/filepath" -- "runtime" -- "runtime/pprof" -- "strings" -- "sync" -- "testing" -- "time" - -- exec "golang.org/x/sys/execabs" -- -- "golang.org/x/tools/gopls/internal/lsp/cache" -- "golang.org/x/tools/gopls/internal/lsp/debug" -- "golang.org/x/tools/gopls/internal/lsp/fake" -- "golang.org/x/tools/gopls/internal/lsp/lsprpc" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/internal/jsonrpc2" -- "golang.org/x/tools/internal/jsonrpc2/servertest" -- "golang.org/x/tools/internal/memoize" -- "golang.org/x/tools/internal/testenv" -- "golang.org/x/tools/internal/xcontext" +- "golang.org/x/tools/internal/gocommand" -) - --// Mode is a bitmask that defines for which execution modes a test should run. --// --// Each mode controls several aspects of gopls' configuration: --// - Which server options to use for gopls sessions --// - Whether to use a shared cache --// - Whether to use a shared server --// - Whether to run the server in-process or in a separate process --// --// The behavior of each mode with respect to these aspects is summarized below. --// TODO(rfindley, cleanup): rather than using arbitrary names for these modes, --// we can compose them explicitly out of the features described here, allowing --// individual tests more freedom in constructing problematic execution modes. --// For example, a test could assert on a certain behavior when running with --// experimental options on a separate process. Moreover, we could unify 'Modes' --// with 'Options', and use RunMultiple rather than a hard-coded loop through --// modes. --// --// Mode | Options | Shared Cache? | Shared Server? | In-process? --// --------------------------------------------------------------------------- --// Default | Default | Y | N | Y --// Forwarded | Default | Y | Y | Y --// SeparateProcess | Default | Y | Y | N --// Experimental | Experimental | N | N | Y --type Mode int +-func getGoEnv(ctx context.Context, env map[string]interface{}) (map[string]string, error) { +- var runEnv []string +- for k, v := range env { +- runEnv = append(runEnv, fmt.Sprintf("%s=%s", k, v)) +- } +- runner := gocommand.Runner{} +- output, err := runner.Run(ctx, gocommand.Invocation{ +- Verb: "env", +- Args: []string{"-json"}, +- Env: runEnv, +- }) +- if err != nil { +- return nil, err +- } +- envmap := make(map[string]string) +- if err := json.Unmarshal(output.Bytes(), &envmap); err != nil { +- return nil, err +- } +- return envmap, nil +-} +diff -urN a/gopls/internal/lsprpc/goenv_test.go b/gopls/internal/lsprpc/goenv_test.go +--- a/gopls/internal/lsprpc/goenv_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/lsprpc/goenv_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,133 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --const ( -- // Default mode runs gopls with the default options, communicating over pipes -- // to emulate the lsp sidecar execution mode, which communicates over -- // stdin/stdout. -- // -- // It uses separate servers for each test, but a shared cache, to avoid -- // duplicating work when processing GOROOT. -- Default Mode = 1 << iota +-package lsprpc_test - -- // Forwarded uses the default options, but forwards connections to a shared -- // in-process gopls server. -- Forwarded +-import ( +- "context" +- "encoding/json" +- "fmt" +- "os" +- "testing" - -- // SeparateProcess uses the default options, but forwards connection to an -- // external gopls daemon. -- // -- // Only supported on GOOS=linux. -- SeparateProcess +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/event" +- jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" +- "golang.org/x/tools/internal/testenv" - -- // Experimental enables all of the experimental configurations that are -- // being developed, and runs gopls in sidecar mode. -- // -- // It uses a separate cache for each test, to exercise races that may only -- // appear with cache misses. -- Experimental +- . "golang.org/x/tools/gopls/internal/lsprpc" -) - --func (m Mode) String() string { -- switch m { -- case Default: -- return "default" -- case Forwarded: -- return "forwarded" -- case SeparateProcess: -- return "separate process" -- case Experimental: -- return "experimental" +-func GoEnvMiddleware() (Middleware, error) { +- return BindHandler(func(delegate jsonrpc2_v2.Handler) jsonrpc2_v2.Handler { +- return jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) { +- if req.Method == "initialize" { +- if err := addGoEnvToInitializeRequestV2(ctx, req); err != nil { +- event.Error(ctx, "adding go env to initialize", err) +- } +- } +- return delegate.Handle(ctx, req) +- }) +- }), nil +-} +- +-// This function is almost identical to addGoEnvToInitializeRequest in lsprpc.go. +-// Make changes in parallel. +-func addGoEnvToInitializeRequestV2(ctx context.Context, req *jsonrpc2_v2.Request) error { +- var params protocol.ParamInitialize +- if err := json.Unmarshal(req.Params, ¶ms); err != nil { +- return err +- } +- var opts map[string]interface{} +- switch v := params.InitializationOptions.(type) { +- case nil: +- opts = make(map[string]interface{}) +- case map[string]interface{}: +- opts = v - default: -- return "unknown mode" +- return fmt.Errorf("unexpected type for InitializationOptions: %T", v) +- } +- envOpt, ok := opts["env"] +- if !ok { +- envOpt = make(map[string]interface{}) +- } +- env, ok := envOpt.(map[string]interface{}) +- if !ok { +- return fmt.Errorf("env option is %T, expected a map", envOpt) +- } +- goenv, err := GetGoEnv(ctx, env) +- if err != nil { +- return err +- } +- // We don't want to propagate GOWORK unless explicitly set since that could mess with +- // path inference during cmd/go invocations, see golang/go#51825. +- _, goworkSet := os.LookupEnv("GOWORK") +- for govar, value := range goenv { +- if govar == "GOWORK" && !goworkSet { +- continue +- } +- env[govar] = value +- } +- opts["env"] = env +- params.InitializationOptions = opts +- raw, err := json.Marshal(params) +- if err != nil { +- return fmt.Errorf("marshaling updated options: %v", err) - } +- req.Params = json.RawMessage(raw) +- return nil -} - --// A Runner runs tests in gopls execution environments, as specified by its --// modes. For modes that share state (for example, a shared cache or common --// remote), any tests that execute on the same Runner will share the same --// state. --type Runner struct { -- // Configuration -- DefaultModes Mode // modes to run for each test -- Timeout time.Duration // per-test timeout, if set -- PrintGoroutinesOnFailure bool // whether to dump goroutines on test failure -- SkipCleanup bool // if set, don't delete test data directories when the test exits -- OptionsHook func(*source.Options) // if set, use these options when creating gopls sessions -- -- // Immutable state shared across test invocations -- goplsPath string // path to the gopls executable (for SeparateProcess mode) -- tempDir string // shared parent temp directory -- store *memoize.Store // shared store +-type initServer struct { +- protocol.Server - -- // Lazily allocated resources -- tsOnce sync.Once -- ts *servertest.TCPServer // shared in-process test server ("forwarded" mode) +- params *protocol.ParamInitialize +-} - -- startRemoteOnce sync.Once -- remoteSocket string // unix domain socket for shared daemon ("separate process" mode) -- remoteErr error -- cancelRemote func() +-func (s *initServer) Initialize(ctx context.Context, params *protocol.ParamInitialize) (*protocol.InitializeResult, error) { +- s.params = params +- return &protocol.InitializeResult{}, nil -} - --type TestFunc func(t *testing.T, env *Env) +-func TestGoEnvMiddleware(t *testing.T) { +- testenv.NeedsTool(t, "go") - --// Run executes the test function in the default configured gopls execution --// modes. For each a test run, a new workspace is created containing the --// un-txtared files specified by filedata. --func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOption) { -- // TODO(rfindley): this function has gotten overly complicated, and warrants -- // refactoring. -- t.Helper() -- checkBuilder(t) -- testenv.NeedsGoPackages(t) +- ctx := context.Background() - -- tests := []struct { -- name string -- mode Mode -- getServer func(func(*source.Options)) jsonrpc2.StreamServer -- }{ -- {"default", Default, r.defaultServer}, -- {"forwarded", Forwarded, r.forwardedServer}, -- {"separate_process", SeparateProcess, r.separateProcessServer}, -- {"experimental", Experimental, r.experimentalServer}, +- server := &initServer{} +- env := new(TestEnv) +- defer env.Shutdown(t) +- l, _ := env.serve(ctx, t, staticServerBinder(server)) +- mw, err := GoEnvMiddleware() +- if err != nil { +- t.Fatal(err) +- } +- binder := mw(NewForwardBinder(l.Dialer())) +- l, _ = env.serve(ctx, t, binder) +- conn := env.dial(ctx, t, l.Dialer(), noopBinder, true) +- dispatch := protocol.ServerDispatcherV2(conn) +- initParams := &protocol.ParamInitialize{} +- initParams.InitializationOptions = map[string]interface{}{ +- "env": map[string]interface{}{ +- "GONOPROXY": "example.com", +- }, +- } +- if _, err := dispatch.Initialize(ctx, initParams); err != nil { +- t.Fatal(err) - } - -- for _, tc := range tests { -- tc := tc -- config := defaultConfig() -- for _, opt := range opts { -- opt.set(&config) -- } -- modes := r.DefaultModes -- if config.modes != 0 { -- modes = config.modes -- } -- if modes&tc.mode == 0 { -- continue -- } +- if server.params == nil { +- t.Fatalf("initialize params are unset") +- } +- envOpts := server.params.InitializationOptions.(map[string]interface{})["env"].(map[string]interface{}) - -- t.Run(tc.name, func(t *testing.T) { -- // TODO(rfindley): once jsonrpc2 shutdown is fixed, we should not leak -- // goroutines in this test function. -- // stacktest.NoLeak(t) +- // Check for an arbitrary Go variable. It should be set. +- if _, ok := envOpts["GOPRIVATE"]; !ok { +- t.Errorf("Go environment variable GOPRIVATE unset in initialization options") +- } +- // Check that the variable present in our user config was not overwritten. +- if got, want := envOpts["GONOPROXY"], "example.com"; got != want { +- t.Errorf("GONOPROXY=%q, want %q", got, want) +- } +-} +diff -urN a/gopls/internal/lsprpc/lsprpc.go b/gopls/internal/lsprpc/lsprpc.go +--- a/gopls/internal/lsprpc/lsprpc.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/lsprpc/lsprpc.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,533 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- ctx := context.Background() -- if r.Timeout != 0 { -- var cancel context.CancelFunc -- ctx, cancel = context.WithTimeout(ctx, r.Timeout) -- defer cancel() -- } else if d, ok := testenv.Deadline(t); ok { -- timeout := time.Until(d) * 19 / 20 // Leave an arbitrary 5% for cleanup. -- var cancel context.CancelFunc -- ctx, cancel = context.WithTimeout(ctx, timeout) -- defer cancel() -- } +-// Package lsprpc implements a jsonrpc2.StreamServer that may be used to +-// serve the LSP on a jsonrpc2 channel. +-package lsprpc - -- // TODO(rfindley): do we need an instance at all? Can it be removed? -- ctx = debug.WithInstance(ctx, "", "off") +-import ( +- "context" +- "encoding/json" +- "fmt" +- "log" +- "net" +- "os" +- "strconv" +- "strings" +- "sync" +- "sync/atomic" +- "time" - -- rootDir := filepath.Join(r.tempDir, filepath.FromSlash(t.Name())) -- if err := os.MkdirAll(rootDir, 0755); err != nil { -- t.Fatal(err) -- } +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/debug" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/command" +- "golang.org/x/tools/gopls/internal/server" +- "golang.org/x/tools/gopls/internal/settings" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/tag" +- "golang.org/x/tools/internal/jsonrpc2" +-) - -- files := fake.UnpackTxt(files) -- if config.editor.WindowsLineEndings { -- for name, data := range files { -- files[name] = bytes.ReplaceAll(data, []byte("\n"), []byte("\r\n")) -- } -- } -- config.sandbox.Files = files -- config.sandbox.RootDir = rootDir -- sandbox, err := fake.NewSandbox(&config.sandbox) -- if err != nil { -- t.Fatal(err) -- } -- defer func() { -- if !r.SkipCleanup { -- if err := sandbox.Close(); err != nil { -- pprof.Lookup("goroutine").WriteTo(os.Stderr, 1) -- t.Errorf("closing the sandbox: %v", err) -- } -- } -- }() +-// Unique identifiers for client/server. +-var serverIndex int64 - -- ss := tc.getServer(r.OptionsHook) +-// The streamServer type is a jsonrpc2.streamServer that handles incoming +-// streams as a new LSP session, using a shared cache. +-type streamServer struct { +- cache *cache.Cache +- // daemon controls whether or not to log new connections. +- daemon bool - -- framer := jsonrpc2.NewRawStream -- ls := &loggingFramer{} -- framer = ls.framer(jsonrpc2.NewRawStream) -- ts := servertest.NewPipeServer(ss, framer) +- // optionsOverrides is passed to newly created sessions. +- optionsOverrides func(*settings.Options) - -- awaiter := NewAwaiter(sandbox.Workdir) -- const skipApplyEdits = false -- editor, err := fake.NewEditor(sandbox, config.editor).Connect(ctx, ts, awaiter.Hooks(), skipApplyEdits) -- if err != nil { -- t.Fatal(err) -- } -- env := &Env{ -- T: t, -- Ctx: ctx, -- Sandbox: sandbox, -- Editor: editor, -- Server: ts, -- Awaiter: awaiter, -- } -- defer func() { -- if t.Failed() && r.PrintGoroutinesOnFailure { -- pprof.Lookup("goroutine").WriteTo(os.Stderr, 1) -- } -- if t.Failed() || *printLogs { -- ls.printBuffers(t.Name(), os.Stderr) -- } -- // For tests that failed due to a timeout, don't fail to shutdown -- // because ctx is done. -- // -- // There is little point to setting an arbitrary timeout for closing -- // the editor: in general we want to clean up before proceeding to the -- // next test, and if there is a deadlock preventing closing it will -- // eventually be handled by the `go test` timeout. -- if err := editor.Close(xcontext.Detach(ctx)); err != nil { -- t.Errorf("closing editor: %v", err) -- } -- }() -- // Always await the initial workspace load. -- env.Await(InitialWorkspaceLoad) -- test(t, env) -- }) -- } +- // serverForTest may be set to a test fake for testing. +- serverForTest protocol.Server -} - --// longBuilders maps builders that are skipped when -short is set to a --// (possibly empty) justification. --var longBuilders = map[string]string{ -- "openbsd-amd64-64": "golang.org/issues/42789", -- "openbsd-386-64": "golang.org/issues/42789", -- "openbsd-386-68": "golang.org/issues/42789", -- "openbsd-amd64-68": "golang.org/issues/42789", -- "darwin-amd64-10_12": "", -- "freebsd-amd64-race": "", -- "illumos-amd64": "", -- "netbsd-arm-bsiegert": "", -- "solaris-amd64-oraclerel": "", -- "windows-arm-zx2c4": "", +-// NewStreamServer creates a StreamServer using the shared cache. If +-// withTelemetry is true, each session is instrumented with telemetry that +-// records RPC statistics. +-func NewStreamServer(cache *cache.Cache, daemon bool, optionsFunc func(*settings.Options)) jsonrpc2.StreamServer { +- return &streamServer{cache: cache, daemon: daemon, optionsOverrides: optionsFunc} -} - --func checkBuilder(t *testing.T) { -- t.Helper() -- builder := os.Getenv("GO_BUILDER_NAME") -- if reason, ok := longBuilders[builder]; ok && testing.Short() { -- if reason != "" { -- t.Skipf("Skipping %s with -short due to %s", builder, reason) -- } else { -- t.Skipf("Skipping %s with -short", builder) +-// ServeStream implements the jsonrpc2.StreamServer interface, by handling +-// incoming streams using a new lsp server. +-func (s *streamServer) ServeStream(ctx context.Context, conn jsonrpc2.Conn) error { +- client := protocol.ClientDispatcher(conn) +- session := cache.NewSession(ctx, s.cache) +- svr := s.serverForTest +- if svr == nil { +- options := settings.DefaultOptions(s.optionsOverrides) +- svr = server.New(session, client, options) +- if instance := debug.GetInstance(ctx); instance != nil { +- instance.AddService(svr, session) - } - } --} -- --type loggingFramer struct { -- mu sync.Mutex -- buf *safeBuffer --} -- --// safeBuffer is a threadsafe buffer for logs. --type safeBuffer struct { -- mu sync.Mutex -- buf bytes.Buffer --} -- --func (b *safeBuffer) Write(p []byte) (int, error) { -- b.mu.Lock() -- defer b.mu.Unlock() -- return b.buf.Write(p) --} -- --func (s *loggingFramer) framer(f jsonrpc2.Framer) jsonrpc2.Framer { -- return func(nc net.Conn) jsonrpc2.Stream { -- s.mu.Lock() -- framed := false -- if s.buf == nil { -- s.buf = &safeBuffer{buf: bytes.Buffer{}} -- framed = true -- } -- s.mu.Unlock() -- stream := f(nc) -- if framed { -- return protocol.LoggingStream(stream, s.buf) +- // Clients may or may not send a shutdown message. Make sure the server is +- // shut down. +- // TODO(rFindley): this shutdown should perhaps be on a disconnected context. +- defer func() { +- if err := svr.Shutdown(ctx); err != nil { +- event.Error(ctx, "error shutting down", err) - } -- return stream +- }() +- executable, err := os.Executable() +- if err != nil { +- log.Printf("error getting gopls path: %v", err) +- executable = "" - } --} -- --func (s *loggingFramer) printBuffers(testname string, w io.Writer) { -- s.mu.Lock() -- defer s.mu.Unlock() -- -- if s.buf == nil { -- return +- ctx = protocol.WithClient(ctx, client) +- conn.Go(ctx, +- protocol.Handlers( +- handshaker(session, executable, s.daemon, +- protocol.ServerHandler(svr, +- jsonrpc2.MethodNotFound)))) +- if s.daemon { +- log.Printf("Session %s: connected", session.ID()) +- defer log.Printf("Session %s: exited", session.ID()) - } -- fmt.Fprintf(os.Stderr, "#### Start Gopls Test Logs for %q\n", testname) -- s.buf.mu.Lock() -- io.Copy(w, &s.buf.buf) -- s.buf.mu.Unlock() -- fmt.Fprintf(os.Stderr, "#### End Gopls Test Logs for %q\n", testname) +- <-conn.Done() +- return conn.Err() -} - --// defaultServer handles the Default execution mode. --func (r *Runner) defaultServer(optsHook func(*source.Options)) jsonrpc2.StreamServer { -- return lsprpc.NewStreamServer(cache.New(r.store), false, optsHook) --} +-// A forwarder is a jsonrpc2.StreamServer that handles an LSP stream by +-// forwarding it to a remote. This is used when the gopls process started by +-// the editor is in the `-remote` mode, which means it finds and connects to a +-// separate gopls daemon. In these cases, we still want the forwarder gopls to +-// be instrumented with telemetry, and want to be able to in some cases hijack +-// the jsonrpc2 connection with the daemon. +-type forwarder struct { +- dialer *autoDialer - --// experimentalServer handles the Experimental execution mode. --func (r *Runner) experimentalServer(optsHook func(*source.Options)) jsonrpc2.StreamServer { -- options := func(o *source.Options) { -- optsHook(o) -- o.EnableAllExperiments() -- } -- return lsprpc.NewStreamServer(cache.New(nil), false, options) +- mu sync.Mutex +- // Hold on to the server connection so that we can redo the handshake if any +- // information changes. +- serverConn jsonrpc2.Conn +- serverID string -} - --// forwardedServer handles the Forwarded execution mode. --func (r *Runner) forwardedServer(optsHook func(*source.Options)) jsonrpc2.StreamServer { -- r.tsOnce.Do(func() { -- ctx := context.Background() -- ctx = debug.WithInstance(ctx, "", "off") -- ss := lsprpc.NewStreamServer(cache.New(nil), false, optsHook) -- r.ts = servertest.NewTCPServer(ctx, ss, nil) -- }) -- return newForwarder("tcp", r.ts.Addr) +-// NewForwarder creates a new forwarder (a [jsonrpc2.StreamServer]), +-// ready to forward connections to the +-// remote server specified by rawAddr. If provided and rawAddr indicates an +-// 'automatic' address (starting with 'auto;'), argFunc may be used to start a +-// remote server for the auto-discovered address. +-func NewForwarder(rawAddr string, argFunc func(network, address string) []string) (jsonrpc2.StreamServer, error) { +- dialer, err := newAutoDialer(rawAddr, argFunc) +- if err != nil { +- return nil, err +- } +- fwd := &forwarder{ +- dialer: dialer, +- } +- return fwd, nil -} - --// runTestAsGoplsEnvvar triggers TestMain to run gopls instead of running --// tests. It's a trick to allow tests to find a binary to use to start a gopls --// subprocess. --const runTestAsGoplsEnvvar = "_GOPLS_TEST_BINARY_RUN_AS_GOPLS" -- --// separateProcessServer handles the SeparateProcess execution mode. --func (r *Runner) separateProcessServer(optsHook func(*source.Options)) jsonrpc2.StreamServer { -- if runtime.GOOS != "linux" { -- panic("separate process execution mode is only supported on linux") +-// QueryServerState returns a JSON-encodable struct describing the state of the named server. +-func QueryServerState(ctx context.Context, addr string) (any, error) { +- serverConn, err := dialRemote(ctx, addr) +- if err != nil { +- return nil, err - } +- var state serverState +- if err := protocol.Call(ctx, serverConn, sessionsMethod, nil, &state); err != nil { +- return nil, fmt.Errorf("querying server state: %w", err) +- } +- return &state, nil +-} - -- r.startRemoteOnce.Do(func() { -- socketDir, err := os.MkdirTemp(r.tempDir, "gopls-regtest-socket") +-// dialRemote is used for making calls into the gopls daemon. addr should be a +-// URL, possibly on the synthetic 'auto' network (e.g. tcp://..., unix://..., +-// or auto://...). +-func dialRemote(ctx context.Context, addr string) (jsonrpc2.Conn, error) { +- network, address := ParseAddr(addr) +- if network == autoNetwork { +- gp, err := os.Executable() - if err != nil { -- r.remoteErr = err -- return -- } -- r.remoteSocket = filepath.Join(socketDir, "gopls-test-daemon") -- -- // The server should be killed by when the test runner exits, but to be -- // conservative also set a listen timeout. -- args := []string{"serve", "-listen", "unix;" + r.remoteSocket, "-listen.timeout", "1m"} -- -- ctx, cancel := context.WithCancel(context.Background()) -- cmd := exec.CommandContext(ctx, r.goplsPath, args...) -- cmd.Env = append(os.Environ(), runTestAsGoplsEnvvar+"=true") -- -- // Start the external gopls process. This is still somewhat racy, as we -- // don't know when gopls binds to the socket, but the gopls forwarder -- // client has built-in retry behavior that should mostly mitigate this -- // problem (and if it doesn't, we probably want to improve the retry -- // behavior). -- if err := cmd.Start(); err != nil { -- cancel() -- r.remoteSocket = "" -- r.remoteErr = err -- } else { -- r.cancelRemote = cancel -- // Spin off a goroutine to wait, so that we free up resources when the -- // server exits. -- go cmd.Wait() +- return nil, fmt.Errorf("getting gopls path: %w", err) - } -- }) -- -- return newForwarder("unix", r.remoteSocket) --} -- --func newForwarder(network, address string) *lsprpc.Forwarder { -- server, err := lsprpc.NewForwarder(network+";"+address, nil) +- network, address = autoNetworkAddress(gp, address) +- } +- netConn, err := net.DialTimeout(network, address, 5*time.Second) - if err != nil { -- // This should never happen, as we are passing an explicit address. -- panic(fmt.Sprintf("internal error: unable to create forwarder: %v", err)) +- return nil, fmt.Errorf("dialing remote: %w", err) - } -- return server +- serverConn := jsonrpc2.NewConn(jsonrpc2.NewHeaderStream(netConn)) +- serverConn.Go(ctx, jsonrpc2.MethodNotFound) +- return serverConn, nil -} - --// Close cleans up resource that have been allocated to this workspace. --func (r *Runner) Close() error { -- var errmsgs []string -- if r.ts != nil { -- if err := r.ts.Close(); err != nil { -- errmsgs = append(errmsgs, err.Error()) -- } -- } -- if r.cancelRemote != nil { -- r.cancelRemote() +-// ExecuteCommand connects to the named server, sends it a +-// workspace/executeCommand request (with command 'id' and arguments +-// JSON encoded in 'request'), and populates the result variable. +-func ExecuteCommand(ctx context.Context, addr string, id string, request, result any) error { +- serverConn, err := dialRemote(ctx, addr) +- if err != nil { +- return err - } -- if !r.SkipCleanup { -- if err := os.RemoveAll(r.tempDir); err != nil { -- errmsgs = append(errmsgs, err.Error()) -- } +- args, err := command.MarshalArgs(request) +- if err != nil { +- return err - } -- if len(errmsgs) > 0 { -- return fmt.Errorf("errors closing the test runner:\n\t%s", strings.Join(errmsgs, "\n\t")) +- params := protocol.ExecuteCommandParams{ +- Command: id, +- Arguments: args, - } -- return nil +- return protocol.Call(ctx, serverConn, "workspace/executeCommand", params, result) -} -diff -urN a/gopls/internal/lsp/regtest/wrappers.go b/gopls/internal/lsp/regtest/wrappers.go ---- a/gopls/internal/lsp/regtest/wrappers.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/regtest/wrappers.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,556 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package regtest +-// ServeStream dials the forwarder remote and binds the remote to serve the LSP +-// on the incoming stream. +-func (f *forwarder) ServeStream(ctx context.Context, clientConn jsonrpc2.Conn) error { +- client := protocol.ClientDispatcher(clientConn) - --import ( -- "encoding/json" -- "path" +- netConn, err := f.dialer.dialNet(ctx) +- if err != nil { +- return fmt.Errorf("forwarder: connecting to remote: %w", err) +- } +- serverConn := jsonrpc2.NewConn(jsonrpc2.NewHeaderStream(netConn)) +- server := protocol.ServerDispatcher(serverConn) - -- "golang.org/x/tools/gopls/internal/lsp/command" -- "golang.org/x/tools/gopls/internal/lsp/fake" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/internal/xcontext" --) +- // Forward between connections. +- serverConn.Go(ctx, +- protocol.Handlers( +- protocol.ClientHandler(client, +- jsonrpc2.MethodNotFound))) - --// RemoveWorkspaceFile deletes a file on disk but does nothing in the --// editor. It calls t.Fatal on any error. --func (e *Env) RemoveWorkspaceFile(name string) { -- e.T.Helper() -- if err := e.Sandbox.Workdir.RemoveFile(e.Ctx, name); err != nil { -- e.T.Fatal(err) -- } --} +- // Don't run the clientConn yet, so that we can complete the handshake before +- // processing any client messages. - --// ReadWorkspaceFile reads a file from the workspace, calling t.Fatal on any --// error. --func (e *Env) ReadWorkspaceFile(name string) string { -- e.T.Helper() -- content, err := e.Sandbox.Workdir.ReadFile(name) -- if err != nil { -- e.T.Fatal(err) -- } -- return string(content) --} +- // Do a handshake with the server instance to exchange debug information. +- index := atomic.AddInt64(&serverIndex, 1) +- f.mu.Lock() +- f.serverConn = serverConn +- f.serverID = strconv.FormatInt(index, 10) +- f.mu.Unlock() +- f.handshake(ctx) +- clientConn.Go(ctx, +- protocol.Handlers( +- f.handler( +- protocol.ServerHandler(server, +- jsonrpc2.MethodNotFound)))) - --// WriteWorkspaceFile writes a file to disk but does nothing in the editor. --// It calls t.Fatal on any error. --func (e *Env) WriteWorkspaceFile(name, content string) { -- e.T.Helper() -- if err := e.Sandbox.Workdir.WriteFile(e.Ctx, name, content); err != nil { -- e.T.Fatal(err) +- select { +- case <-serverConn.Done(): +- clientConn.Close() +- case <-clientConn.Done(): +- serverConn.Close() - } --} - --// WriteWorkspaceFiles deletes a file on disk but does nothing in the --// editor. It calls t.Fatal on any error. --func (e *Env) WriteWorkspaceFiles(files map[string]string) { -- e.T.Helper() -- if err := e.Sandbox.Workdir.WriteFiles(e.Ctx, files); err != nil { -- e.T.Fatal(err) +- err = nil +- if serverConn.Err() != nil { +- err = fmt.Errorf("remote disconnected: %v", serverConn.Err()) +- } else if clientConn.Err() != nil { +- err = fmt.Errorf("client disconnected: %v", clientConn.Err()) - } +- event.Log(ctx, fmt.Sprintf("forwarder: exited with error: %v", err)) +- return err -} - --// ListFiles lists relative paths to files in the given directory. --// It calls t.Fatal on any error. --func (e *Env) ListFiles(dir string) []string { -- e.T.Helper() -- paths, err := e.Sandbox.Workdir.ListFiles(dir) +-// TODO(rfindley): remove this handshaking in favor of middleware. +-func (f *forwarder) handshake(ctx context.Context) { +- // This call to os.Executable is redundant, and will be eliminated by the +- // transition to the V2 API. +- goplsPath, err := os.Executable() - if err != nil { -- e.T.Fatal(err) +- event.Error(ctx, "getting executable for handshake", err) +- goplsPath = "" - } -- return paths --} -- --// OpenFile opens a file in the editor, calling t.Fatal on any error. --func (e *Env) OpenFile(name string) { -- e.T.Helper() -- if err := e.Editor.OpenFile(e.Ctx, name); err != nil { -- e.T.Fatal(err) +- var ( +- hreq = handshakeRequest{ +- ServerID: f.serverID, +- GoplsPath: goplsPath, +- } +- hresp handshakeResponse +- ) +- if di := debug.GetInstance(ctx); di != nil { +- hreq.Logfile = di.Logfile +- hreq.DebugAddr = di.ListenedDebugAddress() - } --} -- --// CreateBuffer creates a buffer in the editor, calling t.Fatal on any error. --func (e *Env) CreateBuffer(name string, content string) { -- e.T.Helper() -- if err := e.Editor.CreateBuffer(e.Ctx, name, content); err != nil { -- e.T.Fatal(err) +- if err := protocol.Call(ctx, f.serverConn, handshakeMethod, hreq, &hresp); err != nil { +- // TODO(rfindley): at some point in the future we should return an error +- // here. Handshakes have become functional in nature. +- event.Error(ctx, "forwarder: gopls handshake failed", err) - } --} -- --// BufferText returns the current buffer contents for the file with the given --// relative path, calling t.Fatal if the file is not open in a buffer. --func (e *Env) BufferText(name string) string { -- e.T.Helper() -- text, ok := e.Editor.BufferText(name) -- if !ok { -- e.T.Fatalf("buffer %q is not open", name) +- if hresp.GoplsPath != goplsPath { +- event.Error(ctx, "", fmt.Errorf("forwarder: gopls path mismatch: forwarder is %q, remote is %q", goplsPath, hresp.GoplsPath)) - } -- return text +- event.Log(ctx, "New server", +- tag.NewServer.Of(f.serverID), +- tag.Logfile.Of(hresp.Logfile), +- tag.DebugAddress.Of(hresp.DebugAddr), +- tag.GoplsPath.Of(hresp.GoplsPath), +- tag.ClientID.Of(hresp.SessionID), +- ) -} - --// CloseBuffer closes an editor buffer without saving, calling t.Fatal on any --// error. --func (e *Env) CloseBuffer(name string) { -- e.T.Helper() -- if err := e.Editor.CloseBuffer(e.Ctx, name); err != nil { -- e.T.Fatal(err) +-func ConnectToRemote(ctx context.Context, addr string) (net.Conn, error) { +- dialer, err := newAutoDialer(addr, nil) +- if err != nil { +- return nil, err - } +- return dialer.dialNet(ctx) -} - --// EditBuffer applies edits to an editor buffer, calling t.Fatal on any error. --func (e *Env) EditBuffer(name string, edits ...protocol.TextEdit) { -- e.T.Helper() -- if err := e.Editor.EditBuffer(e.Ctx, name, edits); err != nil { -- e.T.Fatal(err) +-// handler intercepts messages to the daemon to enrich them with local +-// information. +-func (f *forwarder) handler(handler jsonrpc2.Handler) jsonrpc2.Handler { +- return func(ctx context.Context, reply jsonrpc2.Replier, r jsonrpc2.Request) error { +- // Intercept certain messages to add special handling. +- switch r.Method() { +- case "initialize": +- if newr, err := addGoEnvToInitializeRequest(ctx, r); err == nil { +- r = newr +- } else { +- log.Printf("unable to add local env to initialize request: %v", err) +- } +- case "workspace/executeCommand": +- var params protocol.ExecuteCommandParams +- if err := json.Unmarshal(r.Params(), ¶ms); err == nil { +- if params.Command == command.StartDebugging.ID() { +- var args command.DebuggingArgs +- if err := command.UnmarshalArgs(params.Arguments, &args); err == nil { +- reply = f.replyWithDebugAddress(ctx, reply, args) +- } else { +- event.Error(ctx, "unmarshaling debugging args", err) +- } +- } +- } else { +- event.Error(ctx, "intercepting executeCommand request", err) +- } +- } +- // The gopls workspace environment defaults to the process environment in +- // which gopls daemon was started. To avoid discrepancies in Go environment +- // between the editor and daemon, inject any unset variables in `go env` +- // into the options sent by initialize. +- // +- // See also golang.org/issue/37830. +- return handler(ctx, reply, r) - } -} - --func (e *Env) SetBufferContent(name string, content string) { -- e.T.Helper() -- if err := e.Editor.SetBufferContent(e.Ctx, name, content); err != nil { -- e.T.Fatal(err) +-// addGoEnvToInitializeRequest builds a new initialize request in which we set +-// any environment variables output by `go env` and not already present in the +-// request. +-// +-// It returns an error if r is not an initialize request, or is otherwise +-// malformed. +-func addGoEnvToInitializeRequest(ctx context.Context, r jsonrpc2.Request) (jsonrpc2.Request, error) { +- var params protocol.ParamInitialize +- if err := json.Unmarshal(r.Params(), ¶ms); err != nil { +- return nil, err - } --} -- --// ReadFile returns the file content for name that applies to the current --// editing session: if the file is open, it returns its buffer content, --// otherwise it returns on disk content. --func (e *Env) FileContent(name string) string { -- e.T.Helper() -- text, ok := e.Editor.BufferText(name) -- if ok { -- return text +- var opts map[string]interface{} +- switch v := params.InitializationOptions.(type) { +- case nil: +- opts = make(map[string]interface{}) +- case map[string]interface{}: +- opts = v +- default: +- return nil, fmt.Errorf("unexpected type for InitializationOptions: %T", v) - } -- return e.ReadWorkspaceFile(name) --} -- --// RegexpSearch returns the starting position of the first match for re in the --// buffer specified by name, calling t.Fatal on any error. It first searches --// for the position in open buffers, then in workspace files. --func (e *Env) RegexpSearch(name, re string) protocol.Location { -- e.T.Helper() -- loc, err := e.Editor.RegexpSearch(name, re) -- if err == fake.ErrUnknownBuffer { -- loc, err = e.Sandbox.Workdir.RegexpSearch(name, re) +- envOpt, ok := opts["env"] +- if !ok { +- envOpt = make(map[string]interface{}) +- } +- env, ok := envOpt.(map[string]interface{}) +- if !ok { +- return nil, fmt.Errorf(`env option is %T, expected a map`, envOpt) - } +- goenv, err := getGoEnv(ctx, env) - if err != nil { -- e.T.Fatalf("RegexpSearch: %v, %v for %q", name, err, re) +- return nil, err - } -- return loc --} -- --// RegexpReplace replaces the first group in the first match of regexpStr with --// the replace text, calling t.Fatal on any error. --func (e *Env) RegexpReplace(name, regexpStr, replace string) { -- e.T.Helper() -- if err := e.Editor.RegexpReplace(e.Ctx, name, regexpStr, replace); err != nil { -- e.T.Fatalf("RegexpReplace: %v", err) +- // We don't want to propagate GOWORK unless explicitly set since that could mess with +- // path inference during cmd/go invocations, see golang/go#51825. +- _, goworkSet := os.LookupEnv("GOWORK") +- for govar, value := range goenv { +- if govar == "GOWORK" && !goworkSet { +- continue +- } +- env[govar] = value - } --} -- --// SaveBuffer saves an editor buffer, calling t.Fatal on any error. --func (e *Env) SaveBuffer(name string) { -- e.T.Helper() -- if err := e.Editor.SaveBuffer(e.Ctx, name); err != nil { -- e.T.Fatal(err) +- opts["env"] = env +- params.InitializationOptions = opts +- call, ok := r.(*jsonrpc2.Call) +- if !ok { +- return nil, fmt.Errorf("%T is not a *jsonrpc2.Call", r) - } +- return jsonrpc2.NewCall(call.ID(), "initialize", params) -} - --func (e *Env) SaveBufferWithoutActions(name string) { -- e.T.Helper() -- if err := e.Editor.SaveBufferWithoutActions(e.Ctx, name); err != nil { -- e.T.Fatal(err) +-func (f *forwarder) replyWithDebugAddress(outerCtx context.Context, r jsonrpc2.Replier, args command.DebuggingArgs) jsonrpc2.Replier { +- di := debug.GetInstance(outerCtx) +- if di == nil { +- event.Log(outerCtx, "no debug instance to start") +- return r +- } +- return func(ctx context.Context, result interface{}, outerErr error) error { +- if outerErr != nil { +- return r(ctx, result, outerErr) +- } +- // Enrich the result with our own debugging information. Since we're an +- // intermediary, the jsonrpc2 package has deserialized the result into +- // maps, by default. Re-do the unmarshalling. +- raw, err := json.Marshal(result) +- if err != nil { +- event.Error(outerCtx, "marshaling intermediate command result", err) +- return r(ctx, result, err) +- } +- var modified command.DebuggingResult +- if err := json.Unmarshal(raw, &modified); err != nil { +- event.Error(outerCtx, "unmarshaling intermediate command result", err) +- return r(ctx, result, err) +- } +- addr := args.Addr +- if addr == "" { +- addr = "localhost:0" +- } +- addr, err = di.Serve(outerCtx, addr) +- if err != nil { +- event.Error(outerCtx, "starting debug server", err) +- return r(ctx, result, outerErr) +- } +- urls := []string{"http://" + addr} +- modified.URLs = append(urls, modified.URLs...) +- go f.handshake(ctx) +- return r(ctx, modified, nil) - } -} - --// GoToDefinition goes to definition in the editor, calling t.Fatal on any --// error. It returns the path and position of the resulting jump. --// --// TODO(rfindley): rename this to just 'Definition'. --func (e *Env) GoToDefinition(loc protocol.Location) protocol.Location { -- e.T.Helper() -- loc, err := e.Editor.Definition(e.Ctx, loc) -- if err != nil { -- e.T.Fatal(err) -- } -- return loc +-// A handshakeRequest identifies a client to the LSP server. +-type handshakeRequest struct { +- // ServerID is the ID of the server on the client. This should usually be 0. +- ServerID string `json:"serverID"` +- // Logfile is the location of the clients log file. +- Logfile string `json:"logfile"` +- // DebugAddr is the client debug address. +- DebugAddr string `json:"debugAddr"` +- // GoplsPath is the path to the Gopls binary running the current client +- // process. +- GoplsPath string `json:"goplsPath"` -} - --func (e *Env) TypeDefinition(loc protocol.Location) protocol.Location { -- e.T.Helper() -- loc, err := e.Editor.TypeDefinition(e.Ctx, loc) -- if err != nil { -- e.T.Fatal(err) -- } -- return loc +-// A handshakeResponse is returned by the LSP server to tell the LSP client +-// information about its session. +-type handshakeResponse struct { +- // SessionID is the server session associated with the client. +- SessionID string `json:"sessionID"` +- // Logfile is the location of the server logs. +- Logfile string `json:"logfile"` +- // DebugAddr is the server debug address. +- DebugAddr string `json:"debugAddr"` +- // GoplsPath is the path to the Gopls binary running the current server +- // process. +- GoplsPath string `json:"goplsPath"` -} - --// FormatBuffer formats the editor buffer, calling t.Fatal on any error. --func (e *Env) FormatBuffer(name string) { -- e.T.Helper() -- if err := e.Editor.FormatBuffer(e.Ctx, name); err != nil { -- e.T.Fatal(err) -- } +-// clientSession identifies a current client LSP session on the server. Note +-// that it looks similar to handshakeResposne, but in fact 'Logfile' and +-// 'DebugAddr' now refer to the client. +-type clientSession struct { +- SessionID string `json:"sessionID"` +- Logfile string `json:"logfile"` +- DebugAddr string `json:"debugAddr"` -} - --// OrganizeImports processes the source.organizeImports codeAction, calling --// t.Fatal on any error. --func (e *Env) OrganizeImports(name string) { -- e.T.Helper() -- if err := e.Editor.OrganizeImports(e.Ctx, name); err != nil { -- e.T.Fatal(err) -- } +-// serverState holds information about the gopls daemon process, including its +-// debug information and debug information of all of its current connected +-// clients. +-type serverState struct { +- Logfile string `json:"logfile"` +- DebugAddr string `json:"debugAddr"` +- GoplsPath string `json:"goplsPath"` +- CurrentClientID string `json:"currentClientID"` +- Clients []clientSession `json:"clients"` -} - --// ApplyQuickFixes processes the quickfix codeAction, calling t.Fatal on any error. --func (e *Env) ApplyQuickFixes(path string, diagnostics []protocol.Diagnostic) { -- e.T.Helper() -- loc := protocol.Location{URI: e.Sandbox.Workdir.URI(path)} // zero Range => whole file -- if err := e.Editor.ApplyQuickFixes(e.Ctx, loc, diagnostics); err != nil { -- e.T.Fatal(err) -- } --} +-const ( +- handshakeMethod = "gopls/handshake" +- sessionsMethod = "gopls/sessions" +-) - --// ApplyCodeAction applies the given code action. --func (e *Env) ApplyCodeAction(action protocol.CodeAction) { -- e.T.Helper() -- if err := e.Editor.ApplyCodeAction(e.Ctx, action); err != nil { -- e.T.Fatal(err) -- } --} +-func handshaker(session *cache.Session, goplsPath string, logHandshakes bool, handler jsonrpc2.Handler) jsonrpc2.Handler { +- return func(ctx context.Context, reply jsonrpc2.Replier, r jsonrpc2.Request) error { +- switch r.Method() { +- case handshakeMethod: +- // We log.Printf in this handler, rather than event.Log when we want logs +- // to go to the daemon log rather than being reflected back to the +- // client. +- var req handshakeRequest +- if err := json.Unmarshal(r.Params(), &req); err != nil { +- if logHandshakes { +- log.Printf("Error processing handshake for session %s: %v", session.ID(), err) +- } +- sendError(ctx, reply, err) +- return nil +- } +- if logHandshakes { +- log.Printf("Session %s: got handshake. Logfile: %q, Debug addr: %q", session.ID(), req.Logfile, req.DebugAddr) +- } +- event.Log(ctx, "Handshake session update", +- cache.KeyUpdateSession.Of(session), +- tag.DebugAddress.Of(req.DebugAddr), +- tag.Logfile.Of(req.Logfile), +- tag.ServerID.Of(req.ServerID), +- tag.GoplsPath.Of(req.GoplsPath), +- ) +- resp := handshakeResponse{ +- SessionID: session.ID(), +- GoplsPath: goplsPath, +- } +- if di := debug.GetInstance(ctx); di != nil { +- resp.Logfile = di.Logfile +- resp.DebugAddr = di.ListenedDebugAddress() +- } +- return reply(ctx, resp, nil) - --// GetQuickFixes returns the available quick fix code actions. --func (e *Env) GetQuickFixes(path string, diagnostics []protocol.Diagnostic) []protocol.CodeAction { -- e.T.Helper() -- loc := protocol.Location{URI: e.Sandbox.Workdir.URI(path)} // zero Range => whole file -- actions, err := e.Editor.GetQuickFixes(e.Ctx, loc, diagnostics) -- if err != nil { -- e.T.Fatal(err) +- case sessionsMethod: +- resp := serverState{ +- GoplsPath: goplsPath, +- CurrentClientID: session.ID(), +- } +- if di := debug.GetInstance(ctx); di != nil { +- resp.Logfile = di.Logfile +- resp.DebugAddr = di.ListenedDebugAddress() +- for _, c := range di.State.Clients() { +- resp.Clients = append(resp.Clients, clientSession{ +- SessionID: c.Session.ID(), +- Logfile: c.Logfile, +- DebugAddr: c.DebugAddress, +- }) +- } +- } +- return reply(ctx, resp, nil) +- } +- return handler(ctx, reply, r) - } -- return actions -} - --// Hover in the editor, calling t.Fatal on any error. --func (e *Env) Hover(loc protocol.Location) (*protocol.MarkupContent, protocol.Location) { -- e.T.Helper() -- c, loc, err := e.Editor.Hover(e.Ctx, loc) -- if err != nil { -- e.T.Fatal(err) +-func sendError(ctx context.Context, reply jsonrpc2.Replier, err error) { +- err = fmt.Errorf("%v: %w", err, jsonrpc2.ErrParse) +- if err := reply(ctx, nil, err); err != nil { +- event.Error(ctx, "", err) - } -- return c, loc -} - --func (e *Env) DocumentLink(name string) []protocol.DocumentLink { -- e.T.Helper() -- links, err := e.Editor.DocumentLink(e.Ctx, name) -- if err != nil { -- e.T.Fatal(err) +-// ParseAddr parses the address of a gopls remote. +-// TODO(rFindley): further document this syntax, and allow URI-style remote +-// addresses such as "auto://...". +-func ParseAddr(listen string) (network string, address string) { +- // Allow passing just -remote=auto, as a shorthand for using automatic remote +- // resolution. +- if listen == autoNetwork { +- return autoNetwork, "" - } -- return links --} -- --func (e *Env) DocumentHighlight(loc protocol.Location) []protocol.DocumentHighlight { -- e.T.Helper() -- highlights, err := e.Editor.DocumentHighlight(e.Ctx, loc) -- if err != nil { -- e.T.Fatal(err) +- if parts := strings.SplitN(listen, ";", 2); len(parts) == 2 { +- return parts[0], parts[1] - } -- return highlights +- return "tcp", listen -} +diff -urN a/gopls/internal/lsprpc/lsprpc_test.go b/gopls/internal/lsprpc/lsprpc_test.go +--- a/gopls/internal/lsprpc/lsprpc_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/lsprpc/lsprpc_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,376 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// RunGenerate runs "go generate" in the given dir, calling t.Fatal on any error. --// It waits for the generate command to complete and checks for file changes --// before returning. --func (e *Env) RunGenerate(dir string) { -- e.T.Helper() -- if err := e.Editor.RunGenerate(e.Ctx, dir); err != nil { -- e.T.Fatal(err) -- } -- e.Await(NoOutstandingWork(IgnoreTelemetryPromptWork)) -- // Ideally the fake.Workspace would handle all synthetic file watching, but -- // we help it out here as we need to wait for the generate command to -- // complete before checking the filesystem. -- e.CheckForFileChanges() --} +-package lsprpc - --// RunGoCommand runs the given command in the sandbox's default working --// directory. --func (e *Env) RunGoCommand(verb string, args ...string) { -- e.T.Helper() -- if err := e.Sandbox.RunGoCommand(e.Ctx, "", verb, args, nil, true); err != nil { -- e.T.Fatal(err) -- } --} +-import ( +- "context" +- "encoding/json" +- "errors" +- "regexp" +- "strings" +- "testing" +- "time" - --// RunGoCommandInDir is like RunGoCommand, but executes in the given --// relative directory of the sandbox. --func (e *Env) RunGoCommandInDir(dir, verb string, args ...string) { -- e.T.Helper() -- if err := e.Sandbox.RunGoCommand(e.Ctx, dir, verb, args, nil, true); err != nil { -- e.T.Fatal(err) -- } --} +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/debug" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/test/integration/fake" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/jsonrpc2" +- "golang.org/x/tools/internal/jsonrpc2/servertest" +- "golang.org/x/tools/internal/testenv" +-) - --// RunGoCommandInDirWithEnv is like RunGoCommand, but executes in the given --// relative directory of the sandbox with the given additional environment variables. --func (e *Env) RunGoCommandInDirWithEnv(dir string, env []string, verb string, args ...string) { -- e.T.Helper() -- if err := e.Sandbox.RunGoCommand(e.Ctx, dir, verb, args, env, true); err != nil { -- e.T.Fatal(err) -- } --} +-type FakeClient struct { +- protocol.Client - --// GoVersion checks the version of the go command. --// It returns the X in Go 1.X. --func (e *Env) GoVersion() int { -- e.T.Helper() -- v, err := e.Sandbox.GoVersion(e.Ctx) -- if err != nil { -- e.T.Fatal(err) -- } -- return v +- Logs chan string -} - --// DumpGoSum prints the correct go.sum contents for dir in txtar format, --// for use in creating regtests. --func (e *Env) DumpGoSum(dir string) { -- e.T.Helper() -- -- if err := e.Sandbox.RunGoCommand(e.Ctx, dir, "list", []string{"-mod=mod", "..."}, nil, true); err != nil { -- e.T.Fatal(err) -- } -- sumFile := path.Join(dir, "/go.sum") -- e.T.Log("\n\n-- " + sumFile + " --\n" + e.ReadWorkspaceFile(sumFile)) -- e.T.Fatal("see contents above") +-func (c FakeClient) LogMessage(ctx context.Context, params *protocol.LogMessageParams) error { +- c.Logs <- params.Message +- return nil -} - --// CheckForFileChanges triggers a manual poll of the workspace for any file --// changes since creation, or since last polling. It is a workaround for the --// lack of true file watching support in the fake workspace. --func (e *Env) CheckForFileChanges() { -- e.T.Helper() -- if err := e.Sandbox.Workdir.CheckForFileChanges(e.Ctx); err != nil { -- e.T.Fatal(err) -- } +-// fakeServer is intended to be embedded in the test fakes below, to trivially +-// implement Shutdown. +-type fakeServer struct { +- protocol.Server -} - --// CodeLens calls textDocument/codeLens for the given path, calling t.Fatal on --// any error. --func (e *Env) CodeLens(path string) []protocol.CodeLens { -- e.T.Helper() -- lens, err := e.Editor.CodeLens(e.Ctx, path) -- if err != nil { -- e.T.Fatal(err) -- } -- return lens +-func (fakeServer) Shutdown(ctx context.Context) error { +- return nil -} - --// ExecuteCodeLensCommand executes the command for the code lens matching the --// given command name. --func (e *Env) ExecuteCodeLensCommand(path string, cmd command.Command, result interface{}) { -- e.T.Helper() -- lenses := e.CodeLens(path) -- var lens protocol.CodeLens -- var found bool -- for _, l := range lenses { -- if l.Command.Command == cmd.ID() { -- lens = l -- found = true -- } -- } -- if !found { -- e.T.Fatalf("found no command with the ID %s", cmd.ID()) -- } -- e.ExecuteCommand(&protocol.ExecuteCommandParams{ -- Command: lens.Command.Command, -- Arguments: lens.Command.Arguments, -- }, result) --} +-type PingServer struct{ fakeServer } - --func (e *Env) ExecuteCommand(params *protocol.ExecuteCommandParams, result interface{}) { -- e.T.Helper() -- response, err := e.Editor.ExecuteCommand(e.Ctx, params) -- if err != nil { -- e.T.Fatal(err) -- } -- if result == nil { -- return -- } -- // Hack: The result of an executeCommand request will be unmarshaled into -- // maps. Re-marshal and unmarshal into the type we expect. -- // -- // This could be improved by generating a jsonrpc2 command client from the -- // command.Interface, but that should only be done if we're consolidating -- // this part of the tsprotocol generation. -- data, err := json.Marshal(response) -- if err != nil { -- e.T.Fatal(err) -- } -- if err := json.Unmarshal(data, result); err != nil { -- e.T.Fatal(err) -- } +-func (s PingServer) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error { +- event.Log(ctx, "ping") +- return nil -} - --// StartProfile starts a CPU profile with the given name, using the --// gopls.start_profile custom command. It calls t.Fatal on any error. --// --// The resulting stop function must be called to stop profiling (using the --// gopls.stop_profile custom command). --func (e *Env) StartProfile() (stop func() string) { -- // TODO(golang/go#61217): revisit the ergonomics of these command APIs. -- // -- // This would be a lot simpler if we generated params constructors. -- args, err := command.MarshalArgs(command.StartProfileArgs{}) -- if err != nil { -- e.T.Fatal(err) -- } -- params := &protocol.ExecuteCommandParams{ -- Command: command.StartProfile.ID(), -- Arguments: args, +-func TestClientLogging(t *testing.T) { +- ctx, cancel := context.WithCancel(context.Background()) +- defer cancel() +- +- server := PingServer{} +- client := FakeClient{Logs: make(chan string, 10)} +- +- ctx = debug.WithInstance(ctx, "") +- ss := NewStreamServer(cache.New(nil), false, nil).(*StreamServer) +- ss.serverForTest = server +- ts := servertest.NewPipeServer(ss, nil) +- defer checkClose(t, ts.Close) +- cc := ts.Connect(ctx) +- cc.Go(ctx, protocol.ClientHandler(client, jsonrpc2.MethodNotFound)) +- +- if err := protocol.ServerDispatcher(cc).DidOpen(ctx, &protocol.DidOpenTextDocumentParams{}); err != nil { +- t.Errorf("DidOpen: %v", err) - } -- var result command.StartProfileResult -- e.ExecuteCommand(params, &result) - -- return func() string { -- stopArgs, err := command.MarshalArgs(command.StopProfileArgs{}) +- select { +- case got := <-client.Logs: +- want := "ping" +- matched, err := regexp.MatchString(want, got) - if err != nil { -- e.T.Fatal(err) +- t.Fatal(err) - } -- stopParams := &protocol.ExecuteCommandParams{ -- Command: command.StopProfile.ID(), -- Arguments: stopArgs, +- if !matched { +- t.Errorf("got log %q, want a log containing %q", got, want) - } -- var result command.StopProfileResult -- e.ExecuteCommand(stopParams, &result) -- return result.File +- case <-time.After(1 * time.Second): +- t.Error("timeout waiting for client log") - } -} - --// InlayHints calls textDocument/inlayHints for the given path, calling t.Fatal on --// any error. --func (e *Env) InlayHints(path string) []protocol.InlayHint { -- e.T.Helper() -- hints, err := e.Editor.InlayHint(e.Ctx, path) -- if err != nil { -- e.T.Fatal(err) -- } -- return hints --} +-// WaitableServer instruments LSP request so that we can control their timing. +-// The requests chosen are arbitrary: we simply needed one that blocks, and +-// another that doesn't. +-type WaitableServer struct { +- fakeServer - --// Symbol calls workspace/symbol --func (e *Env) Symbol(query string) []protocol.SymbolInformation { -- e.T.Helper() -- ans, err := e.Editor.Symbols(e.Ctx, query) -- if err != nil { -- e.T.Fatal(err) -- } -- return ans +- Started chan struct{} +- Completed chan error -} - --// References wraps Editor.References, calling t.Fatal on any error. --func (e *Env) References(loc protocol.Location) []protocol.Location { -- e.T.Helper() -- locations, err := e.Editor.References(e.Ctx, loc) -- if err != nil { -- e.T.Fatal(err) +-func (s WaitableServer) Hover(ctx context.Context, _ *protocol.HoverParams) (_ *protocol.Hover, err error) { +- s.Started <- struct{}{} +- defer func() { +- s.Completed <- err +- }() +- select { +- case <-ctx.Done(): +- return nil, errors.New("cancelled hover") +- case <-time.After(10 * time.Second): - } -- return locations +- return &protocol.Hover{}, nil -} - --// Rename wraps Editor.Rename, calling t.Fatal on any error. --func (e *Env) Rename(loc protocol.Location, newName string) { -- e.T.Helper() -- if err := e.Editor.Rename(e.Ctx, loc, newName); err != nil { -- e.T.Fatal(err) -- } +-func (s WaitableServer) ResolveCompletionItem(_ context.Context, item *protocol.CompletionItem) (*protocol.CompletionItem, error) { +- return item, nil -} - --// Implementations wraps Editor.Implementations, calling t.Fatal on any error. --func (e *Env) Implementations(loc protocol.Location) []protocol.Location { -- e.T.Helper() -- locations, err := e.Editor.Implementations(e.Ctx, loc) -- if err != nil { -- e.T.Fatal(err) +-func checkClose(t *testing.T, closer func() error) { +- t.Helper() +- if err := closer(); err != nil { +- t.Errorf("closing: %v", err) - } -- return locations -} - --// RenameFile wraps Editor.RenameFile, calling t.Fatal on any error. --func (e *Env) RenameFile(oldPath, newPath string) { -- e.T.Helper() -- if err := e.Editor.RenameFile(e.Ctx, oldPath, newPath); err != nil { -- e.T.Fatal(err) -- } --} +-func setupForwarding(ctx context.Context, t *testing.T, s protocol.Server) (direct, forwarded servertest.Connector, cleanup func()) { +- t.Helper() +- serveCtx := debug.WithInstance(ctx, "") +- ss := NewStreamServer(cache.New(nil), false, nil).(*StreamServer) +- ss.serverForTest = s +- tsDirect := servertest.NewTCPServer(serveCtx, ss, nil) - --// SignatureHelp wraps Editor.SignatureHelp, calling t.Fatal on error --func (e *Env) SignatureHelp(loc protocol.Location) *protocol.SignatureHelp { -- e.T.Helper() -- sighelp, err := e.Editor.SignatureHelp(e.Ctx, loc) +- forwarder, err := NewForwarder("tcp;"+tsDirect.Addr, nil) - if err != nil { -- e.T.Fatal(err) +- t.Fatal(err) - } -- return sighelp --} -- --// Completion executes a completion request on the server. --func (e *Env) Completion(loc protocol.Location) *protocol.CompletionList { -- e.T.Helper() -- completions, err := e.Editor.Completion(e.Ctx, loc) -- if err != nil { -- e.T.Fatal(err) +- tsForwarded := servertest.NewPipeServer(forwarder, nil) +- return tsDirect, tsForwarded, func() { +- checkClose(t, tsDirect.Close) +- checkClose(t, tsForwarded.Close) - } -- return completions -} - --// AcceptCompletion accepts a completion for the given item at the given --// position. --func (e *Env) AcceptCompletion(loc protocol.Location, item protocol.CompletionItem) { -- e.T.Helper() -- if err := e.Editor.AcceptCompletion(e.Ctx, loc, item); err != nil { -- e.T.Fatal(err) +-func TestRequestCancellation(t *testing.T) { +- ctx := context.Background() +- server := WaitableServer{ +- Started: make(chan struct{}), +- Completed: make(chan error), - } --} -- --// CodeAction calls testDocument/codeAction for the given path, and calls --// t.Fatal if there are errors. --func (e *Env) CodeAction(path string, diagnostics []protocol.Diagnostic) []protocol.CodeAction { -- e.T.Helper() -- loc := protocol.Location{URI: e.Sandbox.Workdir.URI(path)} // no Range => whole file -- actions, err := e.Editor.CodeAction(e.Ctx, loc, diagnostics) -- if err != nil { -- e.T.Fatal(err) +- tsDirect, tsForwarded, cleanup := setupForwarding(ctx, t, server) +- defer cleanup() +- tests := []struct { +- serverType string +- ts servertest.Connector +- }{ +- {"direct", tsDirect}, +- {"forwarder", tsForwarded}, - } -- return actions --} - --// ChangeConfiguration updates the editor config, calling t.Fatal on any error. --func (e *Env) ChangeConfiguration(newConfig fake.EditorConfig) { -- e.T.Helper() -- if err := e.Editor.ChangeConfiguration(e.Ctx, newConfig); err != nil { -- e.T.Fatal(err) +- for _, test := range tests { +- t.Run(test.serverType, func(t *testing.T) { +- cc := test.ts.Connect(ctx) +- sd := protocol.ServerDispatcher(cc) +- cc.Go(ctx, +- protocol.Handlers( +- jsonrpc2.MethodNotFound)) +- +- ctx := context.Background() +- ctx, cancel := context.WithCancel(ctx) +- +- result := make(chan error) +- go func() { +- _, err := sd.Hover(ctx, &protocol.HoverParams{}) +- result <- err +- }() +- // Wait for the Hover request to start. +- <-server.Started +- cancel() +- if err := <-result; err == nil { +- t.Error("nil error for cancelled Hover(), want non-nil") +- } +- if err := <-server.Completed; err == nil || !strings.Contains(err.Error(), "cancelled hover") { +- t.Errorf("Hover(): unexpected server-side error %v", err) +- } +- }) - } -} - --// ChangeWorkspaceFolders updates the editor workspace folders, calling t.Fatal --// on any error. --func (e *Env) ChangeWorkspaceFolders(newFolders ...string) { -- e.T.Helper() -- if err := e.Editor.ChangeWorkspaceFolders(e.Ctx, newFolders); err != nil { -- e.T.Fatal(err) -- } --} +-const exampleProgram = ` +--- go.mod -- +-module mod - --// Close shuts down the editor session and cleans up the sandbox directory, --// calling t.Error on any error. --func (e *Env) Close() { -- ctx := xcontext.Detach(e.Ctx) -- if err := e.Editor.Close(ctx); err != nil { -- e.T.Errorf("closing editor: %v", err) -- } -- if err := e.Sandbox.Close(); err != nil { -- e.T.Errorf("cleaning up sandbox: %v", err) -- } --} -diff -urN a/gopls/internal/lsp/rename.go b/gopls/internal/lsp/rename.go ---- a/gopls/internal/lsp/rename.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/rename.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,86 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-go 1.12 +--- main.go -- +-package main - --package lsp +-import "fmt" - --import ( -- "context" -- "path/filepath" +-func main() { +- fmt.Println("Hello World.") +-}` - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/tag" --) +-func TestDebugInfoLifecycle(t *testing.T) { +- sb, err := fake.NewSandbox(&fake.SandboxConfig{Files: fake.UnpackTxt(exampleProgram)}) +- if err != nil { +- t.Fatal(err) +- } +- defer func() { +- if err := sb.Close(); err != nil { +- // TODO(golang/go#38490): we can't currently make this an error because +- // it fails on Windows: the workspace directory is still locked by a +- // separate Go process. +- // Once we have a reliable way to wait for proper shutdown, make this an +- // error. +- t.Logf("closing workspace failed: %v", err) +- } +- }() - --func (s *Server) rename(ctx context.Context, params *protocol.RenameParams) (*protocol.WorkspaceEdit, error) { -- ctx, done := event.Start(ctx, "lsp.Server.rename", tag.URI.Of(params.TextDocument.URI)) -- defer done() +- baseCtx, cancel := context.WithCancel(context.Background()) +- defer cancel() +- clientCtx := debug.WithInstance(baseCtx, "") +- serverCtx := debug.WithInstance(baseCtx, "") - -- snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go) -- defer release() -- if !ok { -- return nil, err +- cache := cache.New(nil) +- ss := NewStreamServer(cache, false, nil) +- tsBackend := servertest.NewTCPServer(serverCtx, ss, nil) +- +- forwarder, err := NewForwarder("tcp;"+tsBackend.Addr, nil) +- if err != nil { +- t.Fatal(err) - } -- // Because we don't handle directory renaming within source.Rename, source.Rename returns -- // boolean value isPkgRenaming to determine whether an DocumentChanges of type RenameFile should -- // be added to the return protocol.WorkspaceEdit value. -- edits, isPkgRenaming, err := source.Rename(ctx, snapshot, fh, params.Position, params.NewName) +- tsForwarder := servertest.NewPipeServer(forwarder, nil) +- +- const skipApplyEdits = false +- ed1, err := fake.NewEditor(sb, fake.EditorConfig{}).Connect(clientCtx, tsForwarder, fake.ClientHooks{}, skipApplyEdits) - if err != nil { -- return nil, err +- t.Fatal(err) +- } +- defer ed1.Close(clientCtx) +- ed2, err := fake.NewEditor(sb, fake.EditorConfig{}).Connect(baseCtx, tsBackend, fake.ClientHooks{}, skipApplyEdits) +- if err != nil { +- t.Fatal(err) - } +- defer ed2.Close(baseCtx) - -- docChanges := []protocol.DocumentChanges{} // must be a slice -- for uri, e := range edits { -- fh, err := snapshot.ReadFile(ctx, uri) -- if err != nil { -- return nil, err +- serverDebug := debug.GetInstance(serverCtx) +- if got, want := len(serverDebug.State.Clients()), 2; got != want { +- t.Errorf("len(server:Clients) = %d, want %d", got, want) +- } +- if got, want := len(serverDebug.State.Sessions()), 2; got != want { +- t.Errorf("len(server:Sessions) = %d, want %d", got, want) +- } +- clientDebug := debug.GetInstance(clientCtx) +- if got, want := len(clientDebug.State.Servers()), 1; got != want { +- t.Errorf("len(client:Servers) = %d, want %d", got, want) +- } +- // Close one of the connections to verify that the client and session were +- // dropped. +- if err := ed1.Close(clientCtx); err != nil { +- t.Fatal(err) +- } +- /*TODO: at this point we have verified the editor is closed +- However there is no way currently to wait for all associated go routines to +- go away, and we need to wait for those to trigger the client drop +- for now we just give it a little bit of time, but we need to fix this +- in a principled way +- */ +- start := time.Now() +- delay := time.Millisecond +- const maxWait = time.Second +- for len(serverDebug.State.Clients()) > 1 { +- if time.Since(start) > maxWait { +- break - } -- docChanges = append(docChanges, documentChanges(fh, e)...) +- time.Sleep(delay) +- delay *= 2 - } -- if isPkgRenaming { -- // Update the last component of the file's enclosing directory. -- oldBase := filepath.Dir(fh.URI().Filename()) -- newURI := filepath.Join(filepath.Dir(oldBase), params.NewName) -- docChanges = append(docChanges, protocol.DocumentChanges{ -- RenameFile: &protocol.RenameFile{ -- Kind: "rename", -- OldURI: protocol.URIFromPath(oldBase), -- NewURI: protocol.URIFromPath(newURI), -- }, -- }) +- if got, want := len(serverDebug.State.Clients()), 1; got != want { +- t.Errorf("len(server:Clients) = %d, want %d", got, want) +- } +- if got, want := len(serverDebug.State.Sessions()), 1; got != want { +- t.Errorf("len(server:Sessions()) = %d, want %d", got, want) - } -- return &protocol.WorkspaceEdit{ -- DocumentChanges: docChanges, -- }, nil -} - --// prepareRename implements the textDocument/prepareRename handler. It may --// return (nil, nil) if there is no rename at the cursor position, but it is --// not desirable to display an error to the user. --// --// TODO(rfindley): why wouldn't we want to show an error to the user, if the --// user initiated a rename request at the cursor? --func (s *Server) prepareRename(ctx context.Context, params *protocol.PrepareRenameParams) (*protocol.PrepareRename2Gn, error) { -- ctx, done := event.Start(ctx, "lsp.Server.prepareRename", tag.URI.Of(params.TextDocument.URI)) -- defer done() +-type initServer struct { +- fakeServer - -- snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go) -- defer release() -- if !ok { -- return nil, err -- } -- // Do not return errors here, as it adds clutter. -- // Returning a nil result means there is not a valid rename. -- item, usererr, err := source.PrepareRename(ctx, snapshot, fh, params.Position) -- if err != nil { -- // Return usererr here rather than err, to avoid cluttering the UI with -- // internal error details. -- return nil, usererr -- } -- return &protocol.PrepareRename2Gn{ -- Range: item.Range, -- Placeholder: item.Text, -- }, nil +- params *protocol.ParamInitialize -} -diff -urN a/gopls/internal/lsp/reset_golden.sh b/gopls/internal/lsp/reset_golden.sh ---- a/gopls/internal/lsp/reset_golden.sh 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/reset_golden.sh 1970-01-01 00:00:00.000000000 +0000 -@@ -1,30 +0,0 @@ --#!/bin/bash --# --# Copyright 2022 The Go Authors. All rights reserved. --# Use of this source code is governed by a BSD-style --# license that can be found in the LICENSE file. --# --# Updates the *.golden files ... to match the tests' current behavior. -- --set -eu -- --GO117BIN="go1.17.9" -- --command -v $GO117BIN >/dev/null 2>&1 || { -- go install golang.org/dl/$GO117BIN@latest -- $GO117BIN download --} -- --find ./internal/lsp/testdata -name *.golden ! -name summary*.txt.golden -delete --# Here we intentionally do not run the ./internal/lsp/source tests with --# -golden. Eventually these tests will be deleted, and in the meantime they are --# redundant with the ./internal/lsp tests. --# --# Note: go1.17.9 tests must be run *before* go tests, as by convention the --# golden output should match the output of gopls built with the most recent --# version of Go. If output differs at 1.17, tests must be tolerant of the 1.17 --# output. --$GO117BIN test ./internal/lsp -golden --go test ./internal/lsp -golden --$GO117BIN test ./test -golden --go test ./test -golden -diff -urN a/gopls/internal/lsp/safetoken/safetoken.go b/gopls/internal/lsp/safetoken/safetoken.go ---- a/gopls/internal/lsp/safetoken/safetoken.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/safetoken/safetoken.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,127 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --// Package safetoken provides wrappers around methods in go/token, --// that return errors rather than panicking. --// --// It also provides a central place for workarounds in the underlying --// packages. The use of this package's functions instead of methods of --// token.File (such as Offset, Position, and PositionFor) is mandatory --// throughout the gopls codebase and enforced by a static check. --package safetoken +-func (s *initServer) Initialize(ctx context.Context, params *protocol.ParamInitialize) (*protocol.InitializeResult, error) { +- s.params = params +- return &protocol.InitializeResult{}, nil +-} - --import ( -- "fmt" -- "go/token" --) +-func TestEnvForwarding(t *testing.T) { +- testenv.NeedsTool(t, "go") - --// Offset returns f.Offset(pos), but first checks that the file --// contains the pos. --// --// The definition of "contains" here differs from that of token.File --// in order to work around a bug in the parser (issue #57490): during --// error recovery, the parser may create syntax nodes whose computed --// End position is 1 byte beyond EOF, which would cause --// token.File.Offset to panic. The workaround is that this function --// accepts a Pos that is exactly 1 byte beyond EOF and maps it to the --// EOF offset. --func Offset(f *token.File, pos token.Pos) (int, error) { -- if !inRange(f, pos) { -- // Accept a Pos that is 1 byte beyond EOF, -- // and map it to the EOF offset. -- // (Workaround for #57490.) -- if int(pos) == f.Base()+f.Size()+1 { -- return f.Size(), nil -- } +- ctx := context.Background() - -- return -1, fmt.Errorf("pos %d is not in range [%d:%d] of file %s", -- pos, f.Base(), f.Base()+f.Size(), f.Name()) -- } -- return int(pos) - f.Base(), nil --} +- server := &initServer{} +- _, tsForwarded, cleanup := setupForwarding(ctx, t, server) +- defer cleanup() - --// Offsets returns Offset(start) and Offset(end). --func Offsets(f *token.File, start, end token.Pos) (int, int, error) { -- startOffset, err := Offset(f, start) -- if err != nil { -- return 0, 0, fmt.Errorf("start: %v", err) +- conn := tsForwarded.Connect(ctx) +- conn.Go(ctx, jsonrpc2.MethodNotFound) +- dispatch := protocol.ServerDispatcher(conn) +- initParams := &protocol.ParamInitialize{} +- initParams.InitializationOptions = map[string]interface{}{ +- "env": map[string]interface{}{ +- "GONOPROXY": "example.com", +- }, - } -- endOffset, err := Offset(f, end) +- _, err := dispatch.Initialize(ctx, initParams) - if err != nil { -- return 0, 0, fmt.Errorf("end: %v", err) +- t.Fatal(err) - } -- return startOffset, endOffset, nil --} -- --// Pos returns f.Pos(offset), but first checks that the offset is --// non-negative and not larger than the size of the file. --func Pos(f *token.File, offset int) (token.Pos, error) { -- if !(0 <= offset && offset <= f.Size()) { -- return token.NoPos, fmt.Errorf("offset %d is not in range for file %s of size %d", offset, f.Name(), f.Size()) +- if server.params == nil { +- t.Fatalf("initialize params are unset") - } -- return token.Pos(f.Base() + offset), nil --} -- --// inRange reports whether file f contains position pos, --// according to the invariants of token.File. --// --// This function is not public because of the ambiguity it would --// create w.r.t. the definition of "contains". Use Offset instead. --func inRange(f *token.File, pos token.Pos) bool { -- return token.Pos(f.Base()) <= pos && pos <= token.Pos(f.Base()+f.Size()) --} +- env := server.params.InitializationOptions.(map[string]interface{})["env"].(map[string]interface{}) - --// Position returns the Position for the pos value in the given file. --// --// p must be NoPos, a valid Pos in the range of f, or exactly 1 byte --// beyond the end of f. (See [Offset] for explanation.) --// Any other value causes a panic. --// --// Line directives (//line comments) are ignored. --func Position(f *token.File, pos token.Pos) token.Position { -- // Work around issue #57490. -- if int(pos) == f.Base()+f.Size()+1 { -- pos-- +- // Check for an arbitrary Go variable. It should be set. +- if _, ok := env["GOPRIVATE"]; !ok { +- t.Errorf("Go environment variable GOPRIVATE unset in initialization options") +- } +- // Check that the variable present in our user config was not overwritten. +- if v := env["GONOPROXY"]; v != "example.com" { +- t.Errorf("GONOPROXY environment variable was overwritten") - } -- -- // TODO(adonovan): centralize the workaround for -- // golang/go#41029 (newline at EOF) here too. -- -- return f.PositionFor(pos, false) -} - --// Line returns the line number for the given offset in the given file. --func Line(f *token.File, pos token.Pos) int { -- return Position(f, pos).Line --} +-func TestListenParsing(t *testing.T) { +- tests := []struct { +- input, wantNetwork, wantAddr string +- }{ +- {"127.0.0.1:0", "tcp", "127.0.0.1:0"}, +- {"unix;/tmp/sock", "unix", "/tmp/sock"}, +- {"auto", "auto", ""}, +- {"auto;foo", "auto", "foo"}, +- } - --// StartPosition converts a start Pos in the FileSet into a Position. --// --// Call this function only if start represents the start of a token or --// parse tree, such as the result of Node.Pos(). If start is the end of --// an interval, such as Node.End(), call EndPosition instead, as it --// may need the correction described at [Position]. --func StartPosition(fset *token.FileSet, start token.Pos) (_ token.Position) { -- if f := fset.File(start); f != nil { -- return Position(f, start) +- for _, test := range tests { +- gotNetwork, gotAddr := ParseAddr(test.input) +- if gotNetwork != test.wantNetwork { +- t.Errorf("network = %q, want %q", gotNetwork, test.wantNetwork) +- } +- if gotAddr != test.wantAddr { +- t.Errorf("addr = %q, want %q", gotAddr, test.wantAddr) +- } - } -- return -} - --// EndPosition converts an end Pos in the FileSet into a Position. --// --// Call this function only if pos represents the end of --// a non-empty interval, such as the result of Node.End(). --func EndPosition(fset *token.FileSet, end token.Pos) (_ token.Position) { -- if f := fset.File(end); f != nil && int(end) > f.Base() { -- return Position(f, end) +-// For #59479, verify that empty slices are serialized as []. +-func TestEmptySlices(t *testing.T) { +- // The LSP would prefer that empty slices be sent as [] rather than null. +- const bad = `{"a":null}` +- const good = `{"a":[]}` +- var x struct { +- A []string `json:"a"` - } -- -- // Work around issue #57490. -- if f := fset.File(end - 1); f != nil { -- return Position(f, end) +- buf, _ := json.Marshal(x) +- if string(buf) != bad { +- // uninitialized is ezpected to give null +- t.Errorf("unexpectedly got %s, want %s", buf, bad) +- } +- x.A = make([]string, 0) +- buf, _ = json.Marshal(x) +- if string(buf) != good { +- // expect [] +- t.Errorf("unexpectedly got %s, want %s", buf, good) +- } +- x.A = []string{} +- buf, _ = json.Marshal(x) +- if string(buf) != good { +- // expect [] +- t.Errorf("unexpectedly got %s, want %s", buf, good) - } -- -- return -} -diff -urN a/gopls/internal/lsp/safetoken/safetoken_test.go b/gopls/internal/lsp/safetoken/safetoken_test.go ---- a/gopls/internal/lsp/safetoken/safetoken_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/safetoken/safetoken_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,132 +0,0 @@ +diff -urN a/gopls/internal/lsprpc/middleware_test.go b/gopls/internal/lsprpc/middleware_test.go +--- a/gopls/internal/lsprpc/middleware_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/lsprpc/middleware_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,223 +0,0 @@ -// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package safetoken_test +-package lsprpc_test - -import ( +- "context" +- "encoding/json" +- "errors" - "fmt" -- "go/parser" -- "go/token" -- "go/types" -- "os" +- "sync" - "testing" +- "time" - -- "golang.org/x/tools/go/packages" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/internal/testenv" +- . "golang.org/x/tools/gopls/internal/lsprpc" +- "golang.org/x/tools/internal/event" +- jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" -) - --func TestWorkaroundIssue57490(t *testing.T) { -- // During error recovery the parser synthesizes various close -- // tokens at EOF, causing the End position of incomplete -- // syntax nodes, computed as Rbrace+len("}"), to be beyond EOF. -- src := `package p; func f() { var x struct` -- fset := token.NewFileSet() -- file, _ := parser.ParseFile(fset, "a.go", src, 0) -- tf := fset.File(file.Pos()) -- -- // Add another file to the FileSet. -- file2, _ := parser.ParseFile(fset, "b.go", "package q", 0) +-var noopBinder = BinderFunc(func(context.Context, *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions { +- return jsonrpc2_v2.ConnectionOptions{} +-}) - -- // This is the ambiguity of #57490... -- if file.End() != file2.Pos() { -- t.Errorf("file.End() %d != %d file2.Pos()", file.End(), file2.Pos()) -- } -- // ...which causes these statements to panic. -- if false { -- tf.Offset(file.End()) // panic: invalid Pos value 36 (should be in [1, 35]) -- tf.Position(file.End()) // panic: invalid Pos value 36 (should be in [1, 35]) +-func TestHandshakeMiddleware(t *testing.T) { +- sh := &Handshaker{ +- metadata: metadata{ +- "answer": 42, +- }, - } -- -- // The offset of the EOF position is the file size. -- offset, err := safetoken.Offset(tf, file.End()-1) -- if err != nil || offset != tf.Size() { -- t.Errorf("Offset(EOF) = (%d, %v), want token.File.Size %d", offset, err, tf.Size()) +- ctx := context.Background() +- env := new(TestEnv) +- defer env.Shutdown(t) +- l, _ := env.serve(ctx, t, sh.Middleware(noopBinder)) +- conn := env.dial(ctx, t, l.Dialer(), noopBinder, false) +- ch := &Handshaker{ +- metadata: metadata{ +- "question": 6 * 9, +- }, - } - -- // The offset of the file.End() position, 1 byte beyond EOF, -- // is also the size of the file. -- offset, err = safetoken.Offset(tf, file.End()) -- if err != nil || offset != tf.Size() { -- t.Errorf("Offset(ast.File.End()) = (%d, %v), want token.File.Size %d", offset, err, tf.Size()) +- check := func(connected bool) error { +- clients := sh.Peers() +- servers := ch.Peers() +- want := 0 +- if connected { +- want = 1 +- } +- if got := len(clients); got != want { +- return fmt.Errorf("got %d clients on the server, want %d", got, want) +- } +- if got := len(servers); got != want { +- return fmt.Errorf("got %d servers on the client, want %d", got, want) +- } +- if !connected { +- return nil +- } +- client := clients[0] +- server := servers[0] +- if _, ok := client.Metadata["question"]; !ok { +- return errors.New("no client metadata") +- } +- if _, ok := server.Metadata["answer"]; !ok { +- return errors.New("no server metadata") +- } +- if client.LocalID != server.RemoteID { +- return fmt.Errorf("client.LocalID == %d, server.PeerID == %d", client.LocalID, server.RemoteID) +- } +- if client.RemoteID != server.LocalID { +- return fmt.Errorf("client.PeerID == %d, server.LocalID == %d", client.RemoteID, server.LocalID) +- } +- return nil - } - -- if got, want := safetoken.Position(tf, file.End()).String(), "a.go:1:35"; got != want { -- t.Errorf("Position(ast.File.End()) = %s, want %s", got, want) +- if err := check(false); err != nil { +- t.Fatalf("before handshake: %v", err) - } -- -- if got, want := safetoken.EndPosition(fset, file.End()).String(), "a.go:1:35"; got != want { -- t.Errorf("EndPosition(ast.File.End()) = %s, want %s", got, want) +- ch.ClientHandshake(ctx, conn) +- if err := check(true); err != nil { +- t.Fatalf("after handshake: %v", err) - } -- -- // Note that calling StartPosition on an end may yield the wrong file: -- if got, want := safetoken.StartPosition(fset, file.End()).String(), "b.go:1:1"; got != want { -- t.Errorf("StartPosition(ast.File.End()) = %s, want %s", got, want) +- conn.Close() +- // Wait for up to ~2s for connections to get cleaned up. +- delay := 25 * time.Millisecond +- for retries := 3; retries >= 0; retries-- { +- time.Sleep(delay) +- err := check(false) +- if err == nil { +- return +- } +- if retries == 0 { +- t.Fatalf("after closing connection: %v", err) +- } +- delay *= 4 - } -} - --// To reduce the risk of panic, or bugs for which this package --// provides a workaround, this test statically reports references to --// forbidden methods of token.File or FileSet throughout gopls and --// suggests alternatives. --func TestGoplsSourceDoesNotCallTokenFileMethods(t *testing.T) { -- testenv.NeedsGoPackages(t) -- testenv.NeedsGo1Point(t, 18) -- testenv.NeedsLocalXTools(t) +-// Handshaker handles both server and client handshaking over jsonrpc2 v2. +-// To instrument server-side handshaking, use Handshaker.Middleware. +-// To instrument client-side handshaking, call +-// Handshaker.ClientHandshake for any new client-side connections. +-type Handshaker struct { +- // metadata will be shared with peers via handshaking. +- metadata metadata - -- cfg := &packages.Config{ -- Mode: packages.NeedName | packages.NeedModule | packages.NeedCompiledGoFiles | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedImports | packages.NeedDeps, -- } -- cfg.Env = os.Environ() -- cfg.Env = append(cfg.Env, -- "GOPACKAGESDRIVER=off", -- "GOWORK=off", // necessary for -mod=mod below -- "GOFLAGS=-mod=mod", -- ) +- mu sync.Mutex +- prevID int64 +- peers map[int64]PeerInfo +-} - -- pkgs, err := packages.Load(cfg, "go/token", "golang.org/x/tools/gopls/...") -- if err != nil { -- t.Fatal(err) -- } -- var tokenPkg *packages.Package -- for _, pkg := range pkgs { -- if pkg.PkgPath == "go/token" { -- tokenPkg = pkg -- break -- } -- } -- if tokenPkg == nil { -- t.Fatal("missing package go/token") -- } +-// metadata holds arbitrary data transferred between jsonrpc2 peers. +-type metadata map[string]any - -- File := tokenPkg.Types.Scope().Lookup("File") -- FileSet := tokenPkg.Types.Scope().Lookup("FileSet") +-// PeerInfo holds information about a peering between jsonrpc2 servers. +-type PeerInfo struct { +- // RemoteID is the identity of the current server on its peer. +- RemoteID int64 - -- alternative := make(map[types.Object]string) -- setAlternative := func(recv types.Object, old, new string) { -- oldMethod, _, _ := types.LookupFieldOrMethod(recv.Type(), true, recv.Pkg(), old) -- alternative[oldMethod] = new -- } -- setAlternative(File, "Line", "safetoken.Line") -- setAlternative(File, "Offset", "safetoken.Offset") -- setAlternative(File, "Position", "safetoken.Position") -- setAlternative(File, "PositionFor", "safetoken.Position") -- setAlternative(FileSet, "Position", "safetoken.StartPosition or EndPosition") -- setAlternative(FileSet, "PositionFor", "safetoken.StartPosition or EndPosition") +- // LocalID is the identity of the peer on the server. +- LocalID int64 - -- for _, pkg := range pkgs { -- switch pkg.PkgPath { -- case "go/token", "golang.org/x/tools/gopls/internal/lsp/safetoken": -- continue // allow calls within these packages -- } +- // IsClient reports whether the peer is a client. If false, the peer is a +- // server. +- IsClient bool - -- for ident, obj := range pkg.TypesInfo.Uses { -- if alt, ok := alternative[obj]; ok { -- posn := safetoken.StartPosition(pkg.Fset, ident.Pos()) -- fmt.Fprintf(os.Stderr, "%s: forbidden use of %v; use %s instead.\n", posn, obj, alt) -- t.Fail() -- } -- } +- // Metadata holds arbitrary information provided by the peer. +- Metadata metadata +-} +- +-// Peers returns the peer info this handshaker knows about by way of either the +-// server-side handshake middleware, or client-side handshakes. +-func (h *Handshaker) Peers() []PeerInfo { +- h.mu.Lock() +- defer h.mu.Unlock() +- +- var c []PeerInfo +- for _, v := range h.peers { +- c = append(c, v) - } +- return c -} -diff -urN a/gopls/internal/lsp/selection_range.go b/gopls/internal/lsp/selection_range.go ---- a/gopls/internal/lsp/selection_range.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/selection_range.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,69 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package lsp +-// Middleware is a jsonrpc2 middleware function to augment connection binding +-// to handle the handshake method, and record disconnections. +-func (h *Handshaker) Middleware(inner jsonrpc2_v2.Binder) jsonrpc2_v2.Binder { +- return BinderFunc(func(ctx context.Context, conn *jsonrpc2_v2.Connection) jsonrpc2_v2.ConnectionOptions { +- opts := inner.Bind(ctx, conn) - --import ( -- "context" +- localID := h.nextID() +- info := &PeerInfo{ +- RemoteID: localID, +- Metadata: h.metadata, +- } - -- "golang.org/x/tools/go/ast/astutil" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/internal/event" --) +- // Wrap the delegated handler to accept the handshake. +- delegate := opts.Handler +- opts.Handler = jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) { +- if req.Method == HandshakeMethod { +- var peerInfo PeerInfo +- if err := json.Unmarshal(req.Params, &peerInfo); err != nil { +- return nil, fmt.Errorf("%w: unmarshaling client info: %v", jsonrpc2_v2.ErrInvalidParams, err) +- } +- peerInfo.LocalID = localID +- peerInfo.IsClient = true +- h.recordPeer(peerInfo) +- return info, nil +- } +- return delegate.Handle(ctx, req) +- }) - --// selectionRange defines the textDocument/selectionRange feature, --// which, given a list of positions within a file, --// reports a linked list of enclosing syntactic blocks, innermost first. --// --// See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_selectionRange. --// --// This feature can be used by a client to implement "expand selection" in a --// language-aware fashion. Multiple input positions are supported to allow --// for multiple cursors, and the entire path up to the whole document is --// returned for each cursor to avoid multiple round-trips when the user is --// likely to issue this command multiple times in quick succession. --func (s *Server) selectionRange(ctx context.Context, params *protocol.SelectionRangeParams) ([]protocol.SelectionRange, error) { -- ctx, done := event.Start(ctx, "lsp.Server.selectionRange") -- defer done() +- // Record the dropped client. +- go h.cleanupAtDisconnect(conn, localID) - -- snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind) -- defer release() -- if !ok { -- return nil, err +- return opts +- }) +-} +- +-// ClientHandshake performs a client-side handshake with the server at the +-// other end of conn, recording the server's peer info and watching for conn's +-// disconnection. +-func (h *Handshaker) ClientHandshake(ctx context.Context, conn *jsonrpc2_v2.Connection) { +- localID := h.nextID() +- info := &PeerInfo{ +- RemoteID: localID, +- Metadata: h.metadata, - } - -- pgf, err := snapshot.ParseGo(ctx, fh, source.ParseFull) -- if err != nil { -- return nil, err +- call := conn.Call(ctx, HandshakeMethod, info) +- var serverInfo PeerInfo +- if err := call.Await(ctx, &serverInfo); err != nil { +- event.Error(ctx, "performing handshake", err) +- return - } +- serverInfo.LocalID = localID +- h.recordPeer(serverInfo) - -- result := make([]protocol.SelectionRange, len(params.Positions)) -- for i, protocolPos := range params.Positions { -- pos, err := pgf.PositionPos(protocolPos) -- if err != nil { -- return nil, err -- } +- go h.cleanupAtDisconnect(conn, localID) +-} - -- path, _ := astutil.PathEnclosingInterval(pgf.File, pos, pos) +-func (h *Handshaker) nextID() int64 { +- h.mu.Lock() +- defer h.mu.Unlock() - -- tail := &result[i] // tail of the Parent linked list, built head first +- h.prevID++ +- return h.prevID +-} - -- for j, node := range path { -- rng, err := pgf.NodeRange(node) -- if err != nil { -- return nil, err -- } +-func (h *Handshaker) cleanupAtDisconnect(conn *jsonrpc2_v2.Connection, peerID int64) { +- conn.Wait() - -- // Add node to tail. -- if j > 0 { -- tail.Parent = &protocol.SelectionRange{} -- tail = tail.Parent -- } -- tail.Range = rng -- } -- } +- h.mu.Lock() +- defer h.mu.Unlock() +- delete(h.peers, peerID) +-} - -- return result, nil +-func (h *Handshaker) recordPeer(info PeerInfo) { +- h.mu.Lock() +- defer h.mu.Unlock() +- if h.peers == nil { +- h.peers = make(map[int64]PeerInfo) +- } +- h.peers[info.LocalID] = info -} -diff -urN a/gopls/internal/lsp/semantic.go b/gopls/internal/lsp/semantic.go ---- a/gopls/internal/lsp/semantic.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/semantic.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,1052 +0,0 @@ +diff -urN a/gopls/internal/mod/code_lens.go b/gopls/internal/mod/code_lens.go +--- a/gopls/internal/mod/code_lens.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/mod/code_lens.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,193 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package lsp +-package mod - -import ( -- "bytes" - "context" -- "errors" - "fmt" -- "go/ast" -- "go/token" -- "go/types" -- "log" +- "os" - "path/filepath" -- "sort" -- "strings" -- "time" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/lsp/template" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/tag" -- "golang.org/x/tools/internal/typeparams" +- "golang.org/x/mod/modfile" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/command" -) - --// The LSP says that errors for the semantic token requests should only be returned --// for exceptions (a word not otherwise defined). This code treats a too-large file --// as an exception. On parse errors, the code does what it can. -- --// reject full semantic token requests for large files --const maxFullFileSize int = 100000 -- --// to control comprehensive logging of decisions (gopls semtok foo.go > /dev/null shows log output) --// semDebug should NEVER be true in checked-in code --const semDebug = false -- --func (s *Server) semanticTokensFull(ctx context.Context, params *protocol.SemanticTokensParams) (*protocol.SemanticTokens, error) { -- ctx, done := event.Start(ctx, "lsp.Server.semanticTokensFull", tag.URI.Of(params.TextDocument.URI)) -- defer done() -- -- ret, err := s.computeSemanticTokens(ctx, params.TextDocument, nil) -- return ret, err +-// LensFuncs returns the supported lensFuncs for go.mod files. +-func LensFuncs() map[command.Command]golang.LensFunc { +- return map[command.Command]golang.LensFunc{ +- command.UpgradeDependency: upgradeLenses, +- command.Tidy: tidyLens, +- command.Vendor: vendorLens, +- command.RunGovulncheck: vulncheckLenses, +- } -} - --func (s *Server) semanticTokensRange(ctx context.Context, params *protocol.SemanticTokensRangeParams) (*protocol.SemanticTokens, error) { -- ctx, done := event.Start(ctx, "lsp.Server.semanticTokensRange", tag.URI.Of(params.TextDocument.URI)) -- defer done() +-func upgradeLenses(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) { +- pm, err := snapshot.ParseMod(ctx, fh) +- if err != nil || pm.File == nil { +- return nil, err +- } +- uri := fh.URI() +- reset, err := command.NewResetGoModDiagnosticsCommand("Reset go.mod diagnostics", command.ResetGoModDiagnosticsArgs{URIArg: command.URIArg{URI: uri}}) +- if err != nil { +- return nil, err +- } +- // Put the `Reset go.mod diagnostics` codelens on the module statement. +- modrng, err := moduleStmtRange(fh, pm) +- if err != nil { +- return nil, err +- } +- lenses := []protocol.CodeLens{{Range: modrng, Command: &reset}} +- if len(pm.File.Require) == 0 { +- // Nothing to upgrade. +- return lenses, nil +- } +- var requires []string +- for _, req := range pm.File.Require { +- requires = append(requires, req.Mod.Path) +- } +- checkUpgrade, err := command.NewCheckUpgradesCommand("Check for upgrades", command.CheckUpgradesArgs{ +- URI: uri, +- Modules: requires, +- }) +- if err != nil { +- return nil, err +- } +- upgradeTransitive, err := command.NewUpgradeDependencyCommand("Upgrade transitive dependencies", command.DependencyArgs{ +- URI: uri, +- AddRequire: false, +- GoCmdArgs: []string{"-d", "-u", "-t", "./..."}, +- }) +- if err != nil { +- return nil, err +- } +- upgradeDirect, err := command.NewUpgradeDependencyCommand("Upgrade direct dependencies", command.DependencyArgs{ +- URI: uri, +- AddRequire: false, +- GoCmdArgs: append([]string{"-d"}, requires...), +- }) +- if err != nil { +- return nil, err +- } - -- ret, err := s.computeSemanticTokens(ctx, params.TextDocument, ¶ms.Range) -- return ret, err +- // Put the upgrade code lenses above the first require block or statement. +- rng, err := firstRequireRange(fh, pm) +- if err != nil { +- return nil, err +- } +- +- return append(lenses, []protocol.CodeLens{ +- {Range: rng, Command: &checkUpgrade}, +- {Range: rng, Command: &upgradeTransitive}, +- {Range: rng, Command: &upgradeDirect}, +- }...), nil -} - --func (s *Server) computeSemanticTokens(ctx context.Context, td protocol.TextDocumentIdentifier, rng *protocol.Range) (*protocol.SemanticTokens, error) { -- ans := protocol.SemanticTokens{ -- Data: []uint32{}, +-func tidyLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) { +- pm, err := snapshot.ParseMod(ctx, fh) +- if err != nil || pm.File == nil { +- return nil, err - } -- snapshot, fh, ok, release, err := s.beginFileRequest(ctx, td.URI, source.UnknownKind) -- defer release() -- if !ok { +- uri := fh.URI() +- cmd, err := command.NewTidyCommand("Run go mod tidy", command.URIArgs{URIs: []protocol.DocumentURI{uri}}) +- if err != nil { - return nil, err - } -- if !snapshot.Options().SemanticTokens { -- // return an error, so if the option changes -- // the client won't remember the wrong answer -- return nil, fmt.Errorf("semantictokens are disabled") +- rng, err := moduleStmtRange(fh, pm) +- if err != nil { +- return nil, err - } -- kind := snapshot.FileKind(fh) -- if kind == source.Tmpl { -- // this is a little cumbersome to avoid both exporting 'encoded' and its methods -- // and to avoid import cycles -- e := &encoded{ -- ctx: ctx, -- metadataSource: snapshot, -- rng: rng, -- tokTypes: snapshot.Options().SemanticTypes, -- tokMods: snapshot.Options().SemanticMods, -- } -- add := func(line, start uint32, len uint32) { -- e.add(line, start, len, tokMacro, nil) -- } -- data := func() []uint32 { -- return e.Data() -- } -- return template.SemanticTokens(ctx, snapshot, fh.URI(), add, data) +- return []protocol.CodeLens{{ +- Range: rng, +- Command: &cmd, +- }}, nil +-} +- +-func vendorLens(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) { +- pm, err := snapshot.ParseMod(ctx, fh) +- if err != nil || pm.File == nil { +- return nil, err - } -- if kind != source.Go { +- if len(pm.File.Require) == 0 { +- // Nothing to vendor. - return nil, nil - } -- pkg, pgf, err := source.NarrowestPackageForFile(ctx, snapshot, fh.URI()) +- rng, err := moduleStmtRange(fh, pm) - if err != nil { - return nil, err - } -- -- if rng == nil && len(pgf.Src) > maxFullFileSize { -- err := fmt.Errorf("semantic tokens: file %s too large for full (%d>%d)", -- fh.URI().Filename(), len(pgf.Src), maxFullFileSize) +- title := "Create vendor directory" +- uri := fh.URI() +- cmd, err := command.NewVendorCommand(title, command.URIArg{URI: uri}) +- if err != nil { - return nil, err - } -- e := &encoded{ -- ctx: ctx, -- metadataSource: snapshot, -- pgf: pgf, -- rng: rng, -- ti: pkg.GetTypesInfo(), -- pkg: pkg, -- fset: pkg.FileSet(), -- tokTypes: snapshot.Options().SemanticTypes, -- tokMods: snapshot.Options().SemanticMods, -- noStrings: snapshot.Options().NoSemanticString, -- noNumbers: snapshot.Options().NoSemanticNumber, -- } -- if err := e.init(); err != nil { -- // e.init should never return an error, unless there's some -- // seemingly impossible race condition -- return nil, err +- // Change the message depending on whether or not the module already has a +- // vendor directory. +- vendorDir := filepath.Join(filepath.Dir(fh.URI().Path()), "vendor") +- if info, _ := os.Stat(vendorDir); info != nil && info.IsDir() { +- title = "Sync vendor directory" - } -- e.semantics() -- ans.Data = e.Data() -- // For delta requests, but we've never seen any. -- ans.ResultID = fmt.Sprintf("%v", time.Now()) -- return &ans, nil +- return []protocol.CodeLens{{Range: rng, Command: &cmd}}, nil -} - --func (e *encoded) semantics() { -- f := e.pgf.File -- // may not be in range, but harmless -- e.token(f.Package, len("package"), tokKeyword, nil) -- e.token(f.Name.NamePos, len(f.Name.Name), tokNamespace, nil) -- inspect := func(n ast.Node) bool { -- return e.inspector(n) +-func moduleStmtRange(fh file.Handle, pm *cache.ParsedModule) (protocol.Range, error) { +- if pm.File == nil || pm.File.Module == nil || pm.File.Module.Syntax == nil { +- return protocol.Range{}, fmt.Errorf("no module statement in %s", fh.URI()) - } -- for _, d := range f.Decls { -- // only look at the decls that overlap the range -- start, end := d.Pos(), d.End() -- if end <= e.start || start >= e.end { -- continue -- } -- ast.Inspect(d, inspect) +- syntax := pm.File.Module.Syntax +- return pm.Mapper.OffsetRange(syntax.Start.Byte, syntax.End.Byte) +-} +- +-// firstRequireRange returns the range for the first "require" in the given +-// go.mod file. This is either a require block or an individual require line. +-func firstRequireRange(fh file.Handle, pm *cache.ParsedModule) (protocol.Range, error) { +- if len(pm.File.Require) == 0 { +- return protocol.Range{}, fmt.Errorf("no requires in the file %s", fh.URI()) - } -- for _, cg := range f.Comments { -- for _, c := range cg.List { -- if strings.HasPrefix(c.Text, "//go:") { -- e.godirective(c) -- continue -- } -- if !strings.Contains(c.Text, "\n") { -- e.token(c.Pos(), len(c.Text), tokComment, nil) -- continue -- } -- e.multiline(c.Pos(), c.End(), c.Text, tokComment) +- var start, end modfile.Position +- for _, stmt := range pm.File.Syntax.Stmt { +- if b, ok := stmt.(*modfile.LineBlock); ok && len(b.Token) == 1 && b.Token[0] == "require" { +- start, end = b.Span() +- break - } - } --} -- --type tokenType string - --const ( -- tokNamespace tokenType = "namespace" -- tokType tokenType = "type" -- tokInterface tokenType = "interface" -- tokTypeParam tokenType = "typeParameter" -- tokParameter tokenType = "parameter" -- tokVariable tokenType = "variable" -- tokMethod tokenType = "method" -- tokFunction tokenType = "function" -- tokKeyword tokenType = "keyword" -- tokComment tokenType = "comment" -- tokString tokenType = "string" -- tokNumber tokenType = "number" -- tokOperator tokenType = "operator" -- -- tokMacro tokenType = "macro" // for templates --) -- --func (e *encoded) token(start token.Pos, leng int, typ tokenType, mods []string) { -- if !start.IsValid() { -- // This is not worth reporting. TODO(pjw): does it still happen? -- return +- firstRequire := pm.File.Require[0].Syntax +- if start.Byte == 0 || firstRequire.Start.Byte < start.Byte { +- start, end = firstRequire.Start, firstRequire.End - } -- if start >= e.end || start+token.Pos(leng) <= e.start { -- return +- return pm.Mapper.OffsetRange(start.Byte, end.Byte) +-} +- +-func vulncheckLenses(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.CodeLens, error) { +- pm, err := snapshot.ParseMod(ctx, fh) +- if err != nil || pm.File == nil { +- return nil, err - } -- // want a line and column from start (in LSP coordinates). Ignore line directives. -- lspRange, err := e.pgf.PosRange(start, start+token.Pos(leng)) +- // Place the codelenses near the module statement. +- // A module may not have the require block, +- // but vulnerabilities can exist in standard libraries. +- uri := fh.URI() +- rng, err := moduleStmtRange(fh, pm) - if err != nil { -- event.Error(e.ctx, "failed to convert to range", err) -- return +- return nil, err - } -- if lspRange.End.Line != lspRange.Start.Line { -- // this happens if users are typing at the end of the file, but report nothing -- return +- +- vulncheck, err := command.NewRunGovulncheckCommand("Run govulncheck", command.VulncheckArgs{ +- URI: uri, +- Pattern: "./...", +- }) +- if err != nil { +- return nil, err - } -- // token is all on one line -- length := lspRange.End.Character - lspRange.Start.Character -- e.add(lspRange.Start.Line, lspRange.Start.Character, length, typ, mods) +- return []protocol.CodeLens{ +- {Range: rng, Command: &vulncheck}, +- }, nil -} +diff -urN a/gopls/internal/mod/diagnostics.go b/gopls/internal/mod/diagnostics.go +--- a/gopls/internal/mod/diagnostics.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/mod/diagnostics.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,560 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-// Package mod provides core features related to go.mod file +-// handling for use by Go editors and tools. +-package mod +- +-import ( +- "context" +- "fmt" +- "runtime" +- "sort" +- "strings" +- "sync" +- +- "golang.org/x/mod/modfile" +- "golang.org/x/mod/semver" +- "golang.org/x/sync/errgroup" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/command" +- "golang.org/x/tools/gopls/internal/settings" +- "golang.org/x/tools/gopls/internal/vulncheck/govulncheck" +- "golang.org/x/tools/internal/event" +-) +- +-// ParseDiagnostics returns diagnostics from parsing the go.mod files in the workspace. +-func ParseDiagnostics(ctx context.Context, snapshot *cache.Snapshot) (map[protocol.DocumentURI][]*cache.Diagnostic, error) { +- ctx, done := event.Start(ctx, "mod.Diagnostics", snapshot.Labels()...) +- defer done() - --func (e *encoded) add(line, start uint32, len uint32, tok tokenType, mod []string) { -- x := semItem{line, start, len, tok, mod} -- e.items = append(e.items, x) +- return collectDiagnostics(ctx, snapshot, ModParseDiagnostics) -} - --// semItem represents a token found walking the parse tree --type semItem struct { -- line, start uint32 -- len uint32 -- typeStr tokenType -- mods []string +-// Diagnostics returns diagnostics from running go mod tidy. +-func TidyDiagnostics(ctx context.Context, snapshot *cache.Snapshot) (map[protocol.DocumentURI][]*cache.Diagnostic, error) { +- ctx, done := event.Start(ctx, "mod.Diagnostics", snapshot.Labels()...) +- defer done() +- +- return collectDiagnostics(ctx, snapshot, ModTidyDiagnostics) -} - --type encoded struct { -- // the generated data -- items []semItem +-// UpgradeDiagnostics returns upgrade diagnostics for the modules in the +-// workspace with known upgrades. +-func UpgradeDiagnostics(ctx context.Context, snapshot *cache.Snapshot) (map[protocol.DocumentURI][]*cache.Diagnostic, error) { +- ctx, done := event.Start(ctx, "mod.UpgradeDiagnostics", snapshot.Labels()...) +- defer done() - -- noStrings bool -- noNumbers bool +- return collectDiagnostics(ctx, snapshot, ModUpgradeDiagnostics) +-} - -- ctx context.Context -- // metadataSource is used to resolve imports -- metadataSource source.MetadataSource -- tokTypes, tokMods []string -- pgf *source.ParsedGoFile -- rng *protocol.Range -- ti *types.Info -- pkg source.Package -- fset *token.FileSet -- // allowed starting and ending token.Pos, set by init -- // used to avoid looking at declarations not in range -- start, end token.Pos -- // path from the root of the parse tree, used for debugging -- stack []ast.Node +-// VulnerabilityDiagnostics returns vulnerability diagnostics for the active modules in the +-// workspace with known vulnerabilities. +-func VulnerabilityDiagnostics(ctx context.Context, snapshot *cache.Snapshot) (map[protocol.DocumentURI][]*cache.Diagnostic, error) { +- ctx, done := event.Start(ctx, "mod.VulnerabilityDiagnostics", snapshot.Labels()...) +- defer done() +- +- return collectDiagnostics(ctx, snapshot, ModVulnerabilityDiagnostics) -} - --// convert the stack to a string, for debugging --func (e *encoded) strStack() string { -- msg := []string{"["} -- for i := len(e.stack) - 1; i >= 0; i-- { -- s := e.stack[i] -- msg = append(msg, fmt.Sprintf("%T", s)[5:]) -- } -- if len(e.stack) > 0 { -- loc := e.stack[len(e.stack)-1].Pos() -- if _, err := safetoken.Offset(e.pgf.Tok, loc); err != nil { -- msg = append(msg, fmt.Sprintf("invalid position %v for %s", loc, e.pgf.URI)) -- } else { -- add := safetoken.Position(e.pgf.Tok, loc) -- nm := filepath.Base(add.Filename) -- msg = append(msg, fmt.Sprintf("(%s:%d,col:%d)", nm, add.Line, add.Column)) +-func collectDiagnostics(ctx context.Context, snapshot *cache.Snapshot, diagFn func(context.Context, *cache.Snapshot, file.Handle) ([]*cache.Diagnostic, error)) (map[protocol.DocumentURI][]*cache.Diagnostic, error) { +- g, ctx := errgroup.WithContext(ctx) +- cpulimit := runtime.GOMAXPROCS(0) +- g.SetLimit(cpulimit) +- +- var mu sync.Mutex +- reports := make(map[protocol.DocumentURI][]*cache.Diagnostic) +- +- for _, uri := range snapshot.View().ModFiles() { +- uri := uri +- g.Go(func() error { +- fh, err := snapshot.ReadFile(ctx, uri) +- if err != nil { +- return err +- } +- diagnostics, err := diagFn(ctx, snapshot, fh) +- if err != nil { +- return err +- } +- for _, d := range diagnostics { +- mu.Lock() +- reports[d.URI] = append(reports[fh.URI()], d) +- mu.Unlock() +- } +- return nil +- }) +- } +- +- if err := g.Wait(); err != nil { +- return nil, err +- } +- return reports, nil +-} +- +-// ModParseDiagnostics reports diagnostics from parsing the mod file. +-func ModParseDiagnostics(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) (diagnostics []*cache.Diagnostic, err error) { +- pm, err := snapshot.ParseMod(ctx, fh) +- if err != nil { +- if pm == nil || len(pm.ParseErrors) == 0 { +- return nil, err - } +- return pm.ParseErrors, nil - } -- msg = append(msg, "]") -- return strings.Join(msg, " ") +- return nil, nil -} - --// find the line in the source --func (e *encoded) srcLine(x ast.Node) string { -- file := e.pgf.Tok -- line := safetoken.Line(file, x.Pos()) -- start, err := safetoken.Offset(file, file.LineStart(line)) +-// ModTidyDiagnostics reports diagnostics from running go mod tidy. +-func ModTidyDiagnostics(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]*cache.Diagnostic, error) { +- pm, err := snapshot.ParseMod(ctx, fh) // memoized - if err != nil { -- return "" +- return nil, nil // errors reported by ModDiagnostics above - } -- end := start -- for ; end < len(e.pgf.Src) && e.pgf.Src[end] != '\n'; end++ { - +- tidied, err := snapshot.ModTidy(ctx, pm) +- if err != nil { +- if err != cache.ErrNoModOnDisk { +- // TODO(rfindley): the check for ErrNoModOnDisk was historically determined +- // to be benign, but may date back to the time when the Go command did not +- // have overlay support. +- // +- // See if we can pass the overlay to the Go command, and eliminate this guard.. +- event.Error(ctx, fmt.Sprintf("tidy: diagnosing %s", pm.URI), err) +- } +- return nil, nil - } -- ans := e.pgf.Src[start:end] -- return string(ans) +- return tidied.Diagnostics, nil -} - --func (e *encoded) inspector(n ast.Node) bool { -- pop := func() { -- e.stack = e.stack[:len(e.stack)-1] -- } -- if n == nil { -- pop() -- return true -- } -- e.stack = append(e.stack, n) -- switch x := n.(type) { -- case *ast.ArrayType: -- case *ast.AssignStmt: -- e.token(x.TokPos, len(x.Tok.String()), tokOperator, nil) -- case *ast.BasicLit: -- if strings.Contains(x.Value, "\n") { -- // has to be a string. -- e.multiline(x.Pos(), x.End(), x.Value, tokString) -- break +-// ModUpgradeDiagnostics adds upgrade quick fixes for individual modules if the upgrades +-// are recorded in the view. +-func ModUpgradeDiagnostics(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) (upgradeDiagnostics []*cache.Diagnostic, err error) { +- pm, err := snapshot.ParseMod(ctx, fh) +- if err != nil { +- // Don't return an error if there are parse error diagnostics to be shown, but also do not +- // continue since we won't be able to show the upgrade diagnostics. +- if pm != nil && len(pm.ParseErrors) != 0 { +- return nil, nil - } -- ln := len(x.Value) -- what := tokNumber -- if x.Kind == token.STRING { -- what = tokString +- return nil, err +- } +- +- upgrades := snapshot.ModuleUpgrades(fh.URI()) +- for _, req := range pm.File.Require { +- ver, ok := upgrades[req.Mod.Path] +- if !ok || req.Mod.Version == ver { +- continue - } -- e.token(x.Pos(), ln, what, nil) -- case *ast.BinaryExpr: -- e.token(x.OpPos, len(x.Op.String()), tokOperator, nil) -- case *ast.BlockStmt: -- case *ast.BranchStmt: -- e.token(x.TokPos, len(x.Tok.String()), tokKeyword, nil) -- // There's no semantic encoding for labels -- case *ast.CallExpr: -- if x.Ellipsis != token.NoPos { -- e.token(x.Ellipsis, len("..."), tokOperator, nil) +- rng, err := pm.Mapper.OffsetRange(req.Syntax.Start.Byte, req.Syntax.End.Byte) +- if err != nil { +- return nil, err - } -- case *ast.CaseClause: -- iam := "case" -- if x.List == nil { -- iam = "default" +- // Upgrade to the exact version we offer the user, not the most recent. +- title := fmt.Sprintf("%s%v", upgradeCodeActionPrefix, ver) +- cmd, err := command.NewUpgradeDependencyCommand(title, command.DependencyArgs{ +- URI: fh.URI(), +- AddRequire: false, +- GoCmdArgs: []string{req.Mod.Path + "@" + ver}, +- }) +- if err != nil { +- return nil, err - } -- e.token(x.Case, len(iam), tokKeyword, nil) -- case *ast.ChanType: -- // chan | chan <- | <- chan -- switch { -- case x.Arrow == token.NoPos: -- e.token(x.Begin, len("chan"), tokKeyword, nil) -- case x.Arrow == x.Begin: -- e.token(x.Arrow, 2, tokOperator, nil) -- pos := e.findKeyword("chan", x.Begin+2, x.Value.Pos()) -- e.token(pos, len("chan"), tokKeyword, nil) -- case x.Arrow != x.Begin: -- e.token(x.Begin, len("chan"), tokKeyword, nil) -- e.token(x.Arrow, 2, tokOperator, nil) +- upgradeDiagnostics = append(upgradeDiagnostics, &cache.Diagnostic{ +- URI: fh.URI(), +- Range: rng, +- Severity: protocol.SeverityInformation, +- Source: cache.UpgradeNotification, +- Message: fmt.Sprintf("%v can be upgraded", req.Mod.Path), +- SuggestedFixes: []cache.SuggestedFix{cache.SuggestedFixFromCommand(cmd, protocol.QuickFix)}, +- }) +- } +- +- return upgradeDiagnostics, nil +-} +- +-const upgradeCodeActionPrefix = "Upgrade to " +- +-// ModVulnerabilityDiagnostics adds diagnostics for vulnerabilities in individual modules +-// if the vulnerability is recorded in the view. +-func ModVulnerabilityDiagnostics(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) (vulnDiagnostics []*cache.Diagnostic, err error) { +- pm, err := snapshot.ParseMod(ctx, fh) +- if err != nil { +- // Don't return an error if there are parse error diagnostics to be shown, but also do not +- // continue since we won't be able to show the vulnerability diagnostics. +- if pm != nil && len(pm.ParseErrors) != 0 { +- return nil, nil - } -- case *ast.CommClause: -- iam := len("case") -- if x.Comm == nil { -- iam = len("default") +- return nil, err +- } +- +- diagSource := cache.Govulncheck +- vs := snapshot.Vulnerabilities(fh.URI())[fh.URI()] +- if vs == nil && snapshot.Options().Vulncheck == settings.ModeVulncheckImports { +- vs, err = snapshot.ModVuln(ctx, fh.URI()) +- if err != nil { +- return nil, err - } -- e.token(x.Case, iam, tokKeyword, nil) -- case *ast.CompositeLit: -- case *ast.DeclStmt: -- case *ast.DeferStmt: -- e.token(x.Defer, len("defer"), tokKeyword, nil) -- case *ast.Ellipsis: -- e.token(x.Ellipsis, len("..."), tokOperator, nil) -- case *ast.EmptyStmt: -- case *ast.ExprStmt: -- case *ast.Field: -- case *ast.FieldList: -- case *ast.ForStmt: -- e.token(x.For, len("for"), tokKeyword, nil) -- case *ast.FuncDecl: -- case *ast.FuncLit: -- case *ast.FuncType: -- if x.Func != token.NoPos { -- e.token(x.Func, len("func"), tokKeyword, nil) -- } -- case *ast.GenDecl: -- e.token(x.TokPos, len(x.Tok.String()), tokKeyword, nil) -- case *ast.GoStmt: -- e.token(x.Go, len("go"), tokKeyword, nil) -- case *ast.Ident: -- e.ident(x) -- case *ast.IfStmt: -- e.token(x.If, len("if"), tokKeyword, nil) -- if x.Else != nil { -- // x.Body.End() or x.Body.End()+1, not that it matters -- pos := e.findKeyword("else", x.Body.End(), x.Else.Pos()) -- e.token(pos, len("else"), tokKeyword, nil) -- } -- case *ast.ImportSpec: -- e.importSpec(x) -- pop() -- return false -- case *ast.IncDecStmt: -- e.token(x.TokPos, len(x.Tok.String()), tokOperator, nil) -- case *ast.IndexExpr: -- case *typeparams.IndexListExpr: -- case *ast.InterfaceType: -- e.token(x.Interface, len("interface"), tokKeyword, nil) -- case *ast.KeyValueExpr: -- case *ast.LabeledStmt: -- case *ast.MapType: -- e.token(x.Map, len("map"), tokKeyword, nil) -- case *ast.ParenExpr: -- case *ast.RangeStmt: -- e.token(x.For, len("for"), tokKeyword, nil) -- // x.TokPos == token.NoPos is legal (for range foo {}) -- offset := x.TokPos -- if offset == token.NoPos { -- offset = x.For -- } -- pos := e.findKeyword("range", offset, x.X.Pos()) -- e.token(pos, len("range"), tokKeyword, nil) -- case *ast.ReturnStmt: -- e.token(x.Return, len("return"), tokKeyword, nil) -- case *ast.SelectStmt: -- e.token(x.Select, len("select"), tokKeyword, nil) -- case *ast.SelectorExpr: -- case *ast.SendStmt: -- e.token(x.Arrow, len("<-"), tokOperator, nil) -- case *ast.SliceExpr: -- case *ast.StarExpr: -- e.token(x.Star, len("*"), tokOperator, nil) -- case *ast.StructType: -- e.token(x.Struct, len("struct"), tokKeyword, nil) -- case *ast.SwitchStmt: -- e.token(x.Switch, len("switch"), tokKeyword, nil) -- case *ast.TypeAssertExpr: -- if x.Type == nil { -- pos := e.findKeyword("type", x.Lparen, x.Rparen) -- e.token(pos, len("type"), tokKeyword, nil) -- } -- case *ast.TypeSpec: -- case *ast.TypeSwitchStmt: -- e.token(x.Switch, len("switch"), tokKeyword, nil) -- case *ast.UnaryExpr: -- e.token(x.OpPos, len(x.Op.String()), tokOperator, nil) -- case *ast.ValueSpec: -- // things only seen with parsing or type errors, so ignore them -- case *ast.BadDecl, *ast.BadExpr, *ast.BadStmt: -- return true -- // not going to see these -- case *ast.File, *ast.Package: -- e.unexpected(fmt.Sprintf("implement %T %s", x, safetoken.Position(e.pgf.Tok, x.Pos()))) -- // other things we knowingly ignore -- case *ast.Comment, *ast.CommentGroup: -- pop() -- return false -- default: -- e.unexpected(fmt.Sprintf("failed to implement %T", x)) +- diagSource = cache.Vulncheck +- } +- if vs == nil || len(vs.Findings) == 0 { +- return nil, nil - } -- return true --} - --func (e *encoded) ident(x *ast.Ident) { -- if e.ti == nil { -- what, mods := e.unkIdent(x) -- if what != "" { -- e.token(x.Pos(), len(x.String()), what, mods) -- } -- if semDebug { -- log.Printf(" nil %s/nil/nil %q %v %s", x.String(), what, mods, e.strStack()) -- } -- return +- suggestRunOrResetGovulncheck, err := suggestGovulncheckAction(diagSource == cache.Govulncheck, fh.URI()) +- if err != nil { +- // must not happen +- return nil, err // TODO: bug report - } -- def := e.ti.Defs[x] -- if def != nil { -- what, mods := e.definitionFor(x, def) -- if what != "" { -- e.token(x.Pos(), len(x.String()), what, mods) -- } -- if semDebug { -- log.Printf(" for %s/%T/%T got %s %v (%s)", x.String(), def, def.Type(), what, mods, e.strStack()) +- vulnsByModule := make(map[string][]*govulncheck.Finding) +- +- for _, finding := range vs.Findings { +- if vuln, typ := foundVuln(finding); typ == vulnCalled || typ == vulnImported { +- vulnsByModule[vuln.Module] = append(vulnsByModule[vuln.Module], finding) - } -- return - } -- use := e.ti.Uses[x] -- tok := func(pos token.Pos, lng int, tok tokenType, mods []string) { -- e.token(pos, lng, tok, mods) -- q := "nil" -- if use != nil { -- q = fmt.Sprintf("%T", use.Type()) -- } -- if semDebug { -- log.Printf(" use %s/%T/%s got %s %v (%s)", x.String(), use, q, tok, mods, e.strStack()) +- for _, req := range pm.File.Require { +- mod := req.Mod.Path +- findings := vulnsByModule[mod] +- if len(findings) == 0 { +- continue - } -- } -- -- switch y := use.(type) { -- case nil: -- what, mods := e.unkIdent(x) -- if what != "" { -- tok(x.Pos(), len(x.String()), what, mods) -- } else if semDebug { -- // tok() wasn't called, so didn't log -- log.Printf(" nil %s/%T/nil %q %v (%s)", x.String(), use, what, mods, e.strStack()) +- // note: req.Syntax is the line corresponding to 'require', which means +- // req.Syntax.Start can point to the beginning of the "require" keyword +- // for a single line require (e.g. "require golang.org/x/mod v0.0.0"). +- start := req.Syntax.Start.Byte +- if len(req.Syntax.Token) == 3 { +- start += len("require ") - } -- return -- case *types.Builtin: -- tok(x.NamePos, len(x.Name), tokFunction, []string{"defaultLibrary"}) -- case *types.Const: -- mods := []string{"readonly"} -- tt := y.Type() -- if _, ok := tt.(*types.Basic); ok { -- tok(x.Pos(), len(x.String()), tokVariable, mods) -- break +- rng, err := pm.Mapper.OffsetRange(start, req.Syntax.End.Byte) +- if err != nil { +- return nil, err - } -- if ttx, ok := tt.(*types.Named); ok { -- if x.String() == "iota" { -- e.unexpected(fmt.Sprintf("iota:%T", ttx)) +- // Map affecting vulns to 'warning' level diagnostics, +- // others to 'info' level diagnostics. +- // Fixes will include only the upgrades for warning level diagnostics. +- var warningFixes, infoFixes []cache.SuggestedFix +- var warningSet, infoSet = map[string]bool{}, map[string]bool{} +- for _, finding := range findings { +- // It is possible that the source code was changed since the last +- // govulncheck run and information in the `vulns` info is stale. +- // For example, imagine that a user is in the middle of updating +- // problematic modules detected by the govulncheck run by applying +- // quick fixes. Stale diagnostics can be confusing and prevent the +- // user from quickly locating the next module to fix. +- // Ideally we should rerun the analysis with the updated module +- // dependencies or any other code changes, but we are not yet +- // in the position of automatically triggering the analysis +- // (govulncheck can take a while). We also don't know exactly what +- // part of source code was changed since `vulns` was computed. +- // As a heuristic, we assume that a user upgrades the affecting +- // module to the version with the fix or the latest one, and if the +- // version in the require statement is equal to or higher than the +- // fixed version, skip generating a diagnostic about the vulnerability. +- // Eventually, the user has to rerun govulncheck. +- if finding.FixedVersion != "" && semver.IsValid(req.Mod.Version) && semver.Compare(finding.FixedVersion, req.Mod.Version) <= 0 { +- continue - } -- if _, ok := ttx.Underlying().(*types.Basic); ok { -- tok(x.Pos(), len(x.String()), tokVariable, mods) -- break +- switch _, typ := foundVuln(finding); typ { +- case vulnImported: +- infoSet[finding.OSV] = true +- case vulnCalled: +- warningSet[finding.OSV] = true - } -- e.unexpected(fmt.Sprintf("%q/%T", x.String(), tt)) -- } -- // can this happen? Don't think so -- e.unexpected(fmt.Sprintf("%s %T %#v", x.String(), tt, tt)) -- case *types.Func: -- tok(x.Pos(), len(x.Name), tokFunction, nil) -- case *types.Label: -- // nothing to map it to -- case *types.Nil: -- // nil is a predeclared identifier -- tok(x.Pos(), len("nil"), tokVariable, []string{"readonly", "defaultLibrary"}) -- case *types.PkgName: -- tok(x.Pos(), len(x.Name), tokNamespace, nil) -- case *types.TypeName: // could be a tokTpeParam -- var mods []string -- if _, ok := y.Type().(*types.Basic); ok { -- mods = []string{"defaultLibrary"} -- } else if _, ok := y.Type().(*typeparams.TypeParam); ok { -- tok(x.Pos(), len(x.String()), tokTypeParam, mods) -- break -- } -- tok(x.Pos(), len(x.String()), tokType, mods) -- case *types.Var: -- if isSignature(y) { -- tok(x.Pos(), len(x.Name), tokFunction, nil) -- } else if e.isParam(use.Pos()) { -- // variable, unless use.pos is the pos of a Field in an ancestor FuncDecl -- // or FuncLit and then it's a parameter -- tok(x.Pos(), len(x.Name), tokParameter, nil) -- } else { -- tok(x.Pos(), len(x.Name), tokVariable, nil) -- } -- -- default: -- // can't happen -- if use == nil { -- msg := fmt.Sprintf("%#v %#v %#v", x, e.ti.Defs[x], e.ti.Uses[x]) -- e.unexpected(msg) -- } -- if use.Type() != nil { -- e.unexpected(fmt.Sprintf("%s %T/%T,%#v", x.String(), use, use.Type(), use)) -- } else { -- e.unexpected(fmt.Sprintf("%s %T", x.String(), use)) -- } -- } --} -- --func (e *encoded) isParam(pos token.Pos) bool { -- for i := len(e.stack) - 1; i >= 0; i-- { -- switch n := e.stack[i].(type) { -- case *ast.FuncDecl: -- for _, f := range n.Type.Params.List { -- for _, id := range f.Names { -- if id.Pos() == pos { -- return true -- } +- // Upgrade to the exact version we offer the user, not the most recent. +- if fixedVersion := finding.FixedVersion; semver.IsValid(fixedVersion) && semver.Compare(req.Mod.Version, fixedVersion) < 0 { +- cmd, err := getUpgradeCodeAction(fh, req, fixedVersion) +- if err != nil { +- return nil, err // TODO: bug report - } -- } -- case *ast.FuncLit: -- for _, f := range n.Type.Params.List { -- for _, id := range f.Names { -- if id.Pos() == pos { -- return true -- } +- sf := cache.SuggestedFixFromCommand(cmd, protocol.QuickFix) +- switch _, typ := foundVuln(finding); typ { +- case vulnImported: +- infoFixes = append(infoFixes, sf) +- case vulnCalled: +- warningFixes = append(warningFixes, sf) - } - } - } -- } -- return false --} -- --func isSignature(use types.Object) bool { -- if _, ok := use.(*types.Var); !ok { -- return false -- } -- v := use.Type() -- if v == nil { -- return false -- } -- if _, ok := v.(*types.Signature); ok { -- return true -- } -- return false --} - --// both e.ti.Defs and e.ti.Uses are nil. use the parse stack. --// a lot of these only happen when the package doesn't compile --// but in that case it is all best-effort from the parse tree --func (e *encoded) unkIdent(x *ast.Ident) (tokenType, []string) { -- def := []string{"definition"} -- n := len(e.stack) - 2 // parent of Ident -- if n < 0 { -- e.unexpected("no stack?") -- return "", nil -- } -- switch nd := e.stack[n].(type) { -- case *ast.BinaryExpr, *ast.UnaryExpr, *ast.ParenExpr, *ast.StarExpr, -- *ast.IncDecStmt, *ast.SliceExpr, *ast.ExprStmt, *ast.IndexExpr, -- *ast.ReturnStmt, *ast.ChanType, *ast.SendStmt, -- *ast.ForStmt, // possibly incomplete -- *ast.IfStmt, /* condition */ -- *ast.KeyValueExpr: // either key or value -- return tokVariable, nil -- case *typeparams.IndexListExpr: -- return tokVariable, nil -- case *ast.Ellipsis: -- return tokType, nil -- case *ast.CaseClause: -- if n-2 >= 0 { -- if _, ok := e.stack[n-2].(*ast.TypeSwitchStmt); ok { -- return tokType, nil -- } -- } -- return tokVariable, nil -- case *ast.ArrayType: -- if x == nd.Len { -- // or maybe a Type Param, but we can't just from the parse tree -- return tokVariable, nil -- } else { -- return tokType, nil -- } -- case *ast.MapType: -- return tokType, nil -- case *ast.CallExpr: -- if x == nd.Fun { -- return tokFunction, nil -- } -- return tokVariable, nil -- case *ast.SwitchStmt: -- return tokVariable, nil -- case *ast.TypeAssertExpr: -- if x == nd.X { -- return tokVariable, nil -- } else if x == nd.Type { -- return tokType, nil -- } -- case *ast.ValueSpec: -- for _, p := range nd.Names { -- if p == x { -- return tokVariable, def -- } -- } -- for _, p := range nd.Values { -- if p == x { -- return tokVariable, nil -- } -- } -- return tokType, nil -- case *ast.SelectorExpr: // e.ti.Selections[nd] is nil, so no help -- if n-1 >= 0 { -- if ce, ok := e.stack[n-1].(*ast.CallExpr); ok { -- // ... CallExpr SelectorExpr Ident (_.x()) -- if ce.Fun == nd && nd.Sel == x { -- return tokFunction, nil -- } -- } +- if len(warningSet) == 0 && len(infoSet) == 0 { +- continue - } -- return tokVariable, nil -- case *ast.AssignStmt: -- for _, p := range nd.Lhs { -- // x := ..., or x = ... -- if p == x { -- if nd.Tok != token.DEFINE { -- def = nil +- // Remove affecting osvs from the non-affecting osv list if any. +- if len(warningSet) > 0 { +- for k := range infoSet { +- if warningSet[k] { +- delete(infoSet, k) - } -- return tokVariable, def // '_' in _ = ... - } - } -- // RHS, = x -- return tokVariable, nil -- case *ast.TypeSpec: // it's a type if it is either the Name or the Type -- if x == nd.Type { -- def = nil +- // Add an upgrade for module@latest. +- // TODO(suzmue): verify if latest is the same as fixedVersion. +- latest, err := getUpgradeCodeAction(fh, req, "latest") +- if err != nil { +- return nil, err // TODO: bug report - } -- return tokType, def -- case *ast.Field: -- // ident could be type in a field, or a method in an interface type, or a variable -- if x == nd.Type { -- return tokType, nil +- sf := cache.SuggestedFixFromCommand(latest, protocol.QuickFix) +- if len(warningFixes) > 0 { +- warningFixes = append(warningFixes, sf) - } -- if n-2 >= 0 { -- _, okit := e.stack[n-2].(*ast.InterfaceType) -- _, okfl := e.stack[n-1].(*ast.FieldList) -- if okit && okfl { -- return tokMethod, def -- } +- if len(infoFixes) > 0 { +- infoFixes = append(infoFixes, sf) - } -- return tokVariable, nil -- case *ast.LabeledStmt, *ast.BranchStmt: -- // nothing to report -- case *ast.CompositeLit: -- if nd.Type == x { -- return tokType, nil +- if len(warningSet) > 0 { +- warning := sortedKeys(warningSet) +- warningFixes = append(warningFixes, suggestRunOrResetGovulncheck) +- vulnDiagnostics = append(vulnDiagnostics, &cache.Diagnostic{ +- URI: fh.URI(), +- Range: rng, +- Severity: protocol.SeverityWarning, +- Source: diagSource, +- Message: getVulnMessage(req.Mod.Path, warning, true, diagSource == cache.Govulncheck), +- SuggestedFixes: warningFixes, +- }) - } -- return tokVariable, nil -- case *ast.RangeStmt: -- if nd.Tok != token.DEFINE { -- def = nil +- if len(infoSet) > 0 { +- info := sortedKeys(infoSet) +- infoFixes = append(infoFixes, suggestRunOrResetGovulncheck) +- vulnDiagnostics = append(vulnDiagnostics, &cache.Diagnostic{ +- URI: fh.URI(), +- Range: rng, +- Severity: protocol.SeverityInformation, +- Source: diagSource, +- Message: getVulnMessage(req.Mod.Path, info, false, diagSource == cache.Govulncheck), +- SuggestedFixes: infoFixes, +- }) - } -- return tokVariable, def -- case *ast.FuncDecl: -- return tokFunction, def -- default: -- msg := fmt.Sprintf("%T undexpected: %s %s%q", nd, x.Name, e.strStack(), e.srcLine(x)) -- e.unexpected(msg) - } -- return "", nil --} - --func isDeprecated(n *ast.CommentGroup) bool { -- if n == nil { -- return false -- } -- for _, c := range n.List { -- if strings.HasPrefix(c.Text, "// Deprecated") { -- return true +- // TODO(hyangah): place this diagnostic on the `go` directive or `toolchain` directive +- // after https://go.dev/issue/57001. +- const diagnoseStdLib = false +- +- // If diagnosing the stdlib, add standard library vulnerability diagnostics +- // on the module declaration. +- // +- // Only proceed if we have a valid module declaration on which to position +- // the diagnostics. +- if diagnoseStdLib && pm.File.Module != nil && pm.File.Module.Syntax != nil { +- // Add standard library vulnerabilities. +- stdlibVulns := vulnsByModule["stdlib"] +- if len(stdlibVulns) == 0 { +- return vulnDiagnostics, nil - } -- } -- return false --} - --func (e *encoded) definitionFor(x *ast.Ident, def types.Object) (tokenType, []string) { -- // PJW: def == types.Label? probably a nothing -- // PJW: look into replacing these syntactic tests with types more generally -- mods := []string{"definition"} -- for i := len(e.stack) - 1; i >= 0; i-- { -- s := e.stack[i] -- switch y := s.(type) { -- case *ast.AssignStmt, *ast.RangeStmt: -- if x.Name == "_" { -- return "", nil // not really a variable -- } -- return tokVariable, mods -- case *ast.GenDecl: -- if isDeprecated(y.Doc) { -- mods = append(mods, "deprecated") -- } -- if y.Tok == token.CONST { -- mods = append(mods, "readonly") -- } -- return tokVariable, mods -- case *ast.FuncDecl: -- // If x is immediately under a FuncDecl, it is a function or method -- if i == len(e.stack)-2 { -- if isDeprecated(y.Doc) { -- mods = append(mods, "deprecated") -- } -- if y.Recv != nil { -- return tokMethod, mods -- } -- return tokFunction, mods -- } -- // if x < ... < FieldList < FuncDecl, this is the receiver, a variable -- // PJW: maybe not. it might be a typeparameter in the type of the receiver -- if _, ok := e.stack[i+1].(*ast.FieldList); ok { -- if _, ok := def.(*types.TypeName); ok { -- return tokTypeParam, mods -- } -- return tokVariable, nil -- } -- // if x < ... < FieldList < FuncType < FuncDecl, this is a param -- return tokParameter, mods -- case *ast.FuncType: // is it in the TypeParams? -- if isTypeParam(x, y) { -- return tokTypeParam, mods -- } -- return tokParameter, mods -- case *ast.InterfaceType: -- return tokMethod, mods -- case *ast.TypeSpec: -- // GenDecl/Typespec/FuncType/FieldList/Field/Ident -- // (type A func(b uint64)) (err error) -- // b and err should not be tokType, but tokVaraible -- // and in GenDecl/TpeSpec/StructType/FieldList/Field/Ident -- // (type A struct{b uint64} -- // but on type B struct{C}), C is a type, but is not being defined. -- // GenDecl/TypeSpec/FieldList/Field/Ident is a typeParam -- if _, ok := e.stack[i+1].(*ast.FieldList); ok { -- return tokTypeParam, mods -- } -- fldm := e.stack[len(e.stack)-2] -- if fld, ok := fldm.(*ast.Field); ok { -- // if len(fld.names) == 0 this is a tokType, being used -- if len(fld.Names) == 0 { -- return tokType, nil -- } -- return tokVariable, mods +- // Put the standard library diagnostic on the module declaration. +- rng, err := pm.Mapper.OffsetRange(pm.File.Module.Syntax.Start.Byte, pm.File.Module.Syntax.End.Byte) +- if err != nil { +- return vulnDiagnostics, nil // TODO: bug report +- } +- +- var warningSet, infoSet = map[string]bool{}, map[string]bool{} +- for _, finding := range stdlibVulns { +- switch _, typ := foundVuln(finding); typ { +- case vulnImported: +- infoSet[finding.OSV] = true +- case vulnCalled: +- warningSet[finding.OSV] = true - } -- return tokType, mods - } -- } -- // can't happen -- msg := fmt.Sprintf("failed to find the decl for %s", safetoken.Position(e.pgf.Tok, x.Pos())) -- e.unexpected(msg) -- return "", []string{""} --} +- if len(warningSet) > 0 { +- warning := sortedKeys(warningSet) +- fixes := []cache.SuggestedFix{suggestRunOrResetGovulncheck} +- vulnDiagnostics = append(vulnDiagnostics, &cache.Diagnostic{ +- URI: fh.URI(), +- Range: rng, +- Severity: protocol.SeverityWarning, +- Source: diagSource, +- Message: getVulnMessage("go", warning, true, diagSource == cache.Govulncheck), +- SuggestedFixes: fixes, +- }) - --func isTypeParam(x *ast.Ident, y *ast.FuncType) bool { -- tp := typeparams.ForFuncType(y) -- if tp == nil { -- return false -- } -- for _, p := range tp.List { -- for _, n := range p.Names { -- if x == n { -- return true +- // remove affecting osvs from the non-affecting osv list if any. +- for k := range infoSet { +- if warningSet[k] { +- delete(infoSet, k) +- } - } - } +- if len(infoSet) > 0 { +- info := sortedKeys(infoSet) +- fixes := []cache.SuggestedFix{suggestRunOrResetGovulncheck} +- vulnDiagnostics = append(vulnDiagnostics, &cache.Diagnostic{ +- URI: fh.URI(), +- Range: rng, +- Severity: protocol.SeverityInformation, +- Source: diagSource, +- Message: getVulnMessage("go", info, false, diagSource == cache.Govulncheck), +- SuggestedFixes: fixes, +- }) +- } - } -- return false +- +- return vulnDiagnostics, nil -} - --func (e *encoded) multiline(start, end token.Pos, val string, tok tokenType) { -- f := e.fset.File(start) -- // the hard part is finding the lengths of lines. include the \n -- leng := func(line int) int { -- n := f.LineStart(line) -- if line >= f.LineCount() { -- return f.Size() - int(n) -- } -- return int(f.LineStart(line+1) - n) +-type vulnFindingType int +- +-const ( +- vulnUnknown vulnFindingType = iota +- vulnCalled +- vulnImported +- vulnRequired +-) +- +-// foundVuln returns the frame info describing discovered vulnerable symbol/package/module +-// and how this vulnerability affects the analyzed package or module. +-func foundVuln(finding *govulncheck.Finding) (*govulncheck.Frame, vulnFindingType) { +- // finding.Trace is sorted from the imported vulnerable symbol to +- // the entry point in the callstack. +- // If Function is set, then Package must be set. Module will always be set. +- // If Function is set it was found in the call graph, otherwise if Package is set +- // it was found in the import graph, otherwise it was found in the require graph. +- // See the documentation of govulncheck.Finding. +- if len(finding.Trace) == 0 { // this shouldn't happen, but just in case... +- return nil, vulnUnknown - } -- spos := safetoken.StartPosition(e.fset, start) -- epos := safetoken.EndPosition(e.fset, end) -- sline := spos.Line -- eline := epos.Line -- // first line is from spos.Column to end -- e.token(start, leng(sline)-spos.Column, tok, nil) // leng(sline)-1 - (spos.Column-1) -- for i := sline + 1; i < eline; i++ { -- // intermediate lines are from 1 to end -- e.token(f.LineStart(i), leng(i)-1, tok, nil) // avoid the newline +- vuln := finding.Trace[0] +- if vuln.Package == "" { +- return vuln, vulnRequired - } -- // last line is from 1 to epos.Column -- e.token(f.LineStart(eline), epos.Column-1, tok, nil) // columns are 1-based +- if vuln.Function == "" { +- return vuln, vulnImported +- } +- return vuln, vulnCalled -} - --// findKeyword finds a keyword rather than guessing its location --func (e *encoded) findKeyword(keyword string, start, end token.Pos) token.Pos { -- offset := int(start) - e.pgf.Tok.Base() -- last := int(end) - e.pgf.Tok.Base() -- buf := e.pgf.Src -- idx := bytes.Index(buf[offset:last], []byte(keyword)) -- if idx != -1 { -- return start + token.Pos(idx) +-func sortedKeys(m map[string]bool) []string { +- ret := make([]string, 0, len(m)) +- for k := range m { +- ret = append(ret, k) - } -- //(in unparsable programs: type _ <-<-chan int) -- e.unexpected(fmt.Sprintf("not found:%s %v", keyword, safetoken.StartPosition(e.fset, start))) -- return token.NoPos +- sort.Strings(ret) +- return ret -} - --func (e *encoded) init() error { -- if e.rng != nil { -- var err error -- e.start, e.end, err = e.pgf.RangePos(*e.rng) +-// suggestGovulncheckAction returns a code action that suggests either run govulncheck +-// for more accurate investigation (if the present vulncheck diagnostics are based on +-// analysis less accurate than govulncheck) or reset the existing govulncheck result +-// (if the present vulncheck diagnostics are already based on govulncheck run). +-func suggestGovulncheckAction(fromGovulncheck bool, uri protocol.DocumentURI) (cache.SuggestedFix, error) { +- if fromGovulncheck { +- resetVulncheck, err := command.NewResetGoModDiagnosticsCommand("Reset govulncheck result", command.ResetGoModDiagnosticsArgs{ +- URIArg: command.URIArg{URI: uri}, +- DiagnosticSource: string(cache.Govulncheck), +- }) - if err != nil { -- return fmt.Errorf("range span (%w) error for %s", err, e.pgf.File.Name) +- return cache.SuggestedFix{}, err - } -- } else { -- tok := e.pgf.Tok -- e.start, e.end = tok.Pos(0), tok.Pos(tok.Size()) // entire file +- return cache.SuggestedFixFromCommand(resetVulncheck, protocol.QuickFix), nil - } -- return nil +- vulncheck, err := command.NewRunGovulncheckCommand("Run govulncheck to verify", command.VulncheckArgs{ +- URI: uri, +- Pattern: "./...", +- }) +- if err != nil { +- return cache.SuggestedFix{}, err +- } +- return cache.SuggestedFixFromCommand(vulncheck, protocol.QuickFix), nil -} - --func (e *encoded) Data() []uint32 { -- // binary operators, at least, will be out of order -- sort.Slice(e.items, func(i, j int) bool { -- if e.items[i].line != e.items[j].line { -- return e.items[i].line < e.items[j].line -- } -- return e.items[i].start < e.items[j].start -- }) -- typeMap, modMap := e.maps() -- // each semantic token needs five values -- // (see Integer Encoding for Tokens in the LSP spec) -- x := make([]uint32, 5*len(e.items)) -- var j int -- var last semItem -- for i := 0; i < len(e.items); i++ { -- item := e.items[i] -- typ, ok := typeMap[item.typeStr] -- if !ok { -- continue // client doesn't want typeStr -- } -- if item.typeStr == tokString && e.noStrings { -- continue -- } -- if item.typeStr == tokNumber && e.noNumbers { -- continue +-func getVulnMessage(mod string, vulns []string, used, fromGovulncheck bool) string { +- var b strings.Builder +- if used { +- switch len(vulns) { +- case 1: +- fmt.Fprintf(&b, "%v has a vulnerability used in the code: %v.", mod, vulns[0]) +- default: +- fmt.Fprintf(&b, "%v has vulnerabilities used in the code: %v.", mod, strings.Join(vulns, ", ")) - } -- if j == 0 { -- x[0] = e.items[0].line +- } else { +- if fromGovulncheck { +- switch len(vulns) { +- case 1: +- fmt.Fprintf(&b, "%v has a vulnerability %v that is not used in the code.", mod, vulns[0]) +- default: +- fmt.Fprintf(&b, "%v has known vulnerabilities %v that are not used in the code.", mod, strings.Join(vulns, ", ")) +- } - } else { -- x[j] = item.line - last.line -- } -- x[j+1] = item.start -- if j > 0 && x[j] == 0 { -- x[j+1] = item.start - last.start -- } -- x[j+2] = item.len -- x[j+3] = uint32(typ) -- mask := 0 -- for _, s := range item.mods { -- // modMap[s] is 0 if the client doesn't want this modifier -- mask |= modMap[s] +- switch len(vulns) { +- case 1: +- fmt.Fprintf(&b, "%v has a vulnerability %v.", mod, vulns[0]) +- default: +- fmt.Fprintf(&b, "%v has known vulnerabilities %v.", mod, strings.Join(vulns, ", ")) +- } - } -- x[j+4] = uint32(mask) -- j += 5 -- last = item - } -- return x[:j] +- return b.String() -} - --func (e *encoded) importSpec(d *ast.ImportSpec) { -- // a local package name or the last component of the Path -- if d.Name != nil { -- nm := d.Name.String() -- if nm != "_" && nm != "." { -- e.token(d.Name.Pos(), len(nm), tokNamespace, nil) -- } -- return // don't mark anything for . or _ -- } -- importPath := source.UnquoteImportPath(d) -- if importPath == "" { -- return -- } -- // Import strings are implementation defined. Try to match with parse information. -- depID := e.pkg.Metadata().DepsByImpPath[importPath] -- if depID == "" { -- return -- } -- depMD := e.metadataSource.Metadata(depID) -- if depMD == nil { -- // unexpected, but impact is that maybe some import is not colored -- return -- } -- // Check whether the original literal contains the package's declared name. -- j := strings.LastIndex(d.Path.Value, string(depMD.Name)) -- if j == -1 { -- // Package name does not match import path, so there is nothing to report. -- return -- } -- // Report virtual declaration at the position of the substring. -- start := d.Path.Pos() + token.Pos(j) -- e.token(start, len(depMD.Name), tokNamespace, nil) +-// href returns the url for the vulnerability information. +-// Eventually we should retrieve the url embedded in the osv.Entry. +-// While vuln.go.dev is under development, this always returns +-// the page in pkg.go.dev. +-func href(vulnID string) string { +- return fmt.Sprintf("https://pkg.go.dev/vuln/%s", vulnID) -} - --// log unexpected state --func (e *encoded) unexpected(msg string) { -- if semDebug { -- panic(msg) +-func getUpgradeCodeAction(fh file.Handle, req *modfile.Require, version string) (protocol.Command, error) { +- cmd, err := command.NewUpgradeDependencyCommand(upgradeTitle(version), command.DependencyArgs{ +- URI: fh.URI(), +- AddRequire: false, +- GoCmdArgs: []string{req.Mod.Path + "@" + version}, +- }) +- if err != nil { +- return protocol.Command{}, err - } -- event.Error(e.ctx, e.strStack(), errors.New(msg)) +- return cmd, nil -} - --// SemType returns a string equivalent of the type, for gopls semtok --func SemType(n int) string { -- tokTypes := SemanticTypes() -- tokMods := SemanticModifiers() -- if n >= 0 && n < len(tokTypes) { -- return tokTypes[n] -- } -- // not found for some reason -- return fmt.Sprintf("?%d[%d,%d]?", n, len(tokTypes), len(tokMods)) +-func upgradeTitle(fixedVersion string) string { +- title := fmt.Sprintf("%s%v", upgradeCodeActionPrefix, fixedVersion) +- return title -} - --// SemMods returns the []string equivalent of the mods, for gopls semtok. --func SemMods(n int) []string { -- tokMods := SemanticModifiers() -- mods := []string{} -- for i := 0; i < len(tokMods); i++ { -- if (n & (1 << uint(i))) != 0 { -- mods = append(mods, tokMods[i]) -- } +-// SelectUpgradeCodeActions takes a list of code actions for a required module +-// and returns a more selective list of upgrade code actions, +-// where the code actions have been deduped. Code actions unrelated to upgrade +-// are deduplicated by the name. +-func SelectUpgradeCodeActions(actions []protocol.CodeAction) []protocol.CodeAction { +- if len(actions) <= 1 { +- return actions // return early if no sorting necessary - } -- return mods --} +- var versionedUpgrade, latestUpgrade, resetAction protocol.CodeAction +- var chosenVersionedUpgrade string +- var selected []protocol.CodeAction - --func (e *encoded) maps() (map[tokenType]int, map[string]int) { -- tmap := make(map[tokenType]int) -- mmap := make(map[string]int) -- for i, t := range e.tokTypes { -- tmap[tokenType(t)] = i +- seenTitles := make(map[string]bool) +- +- for _, action := range actions { +- if strings.HasPrefix(action.Title, upgradeCodeActionPrefix) { +- if v := getUpgradeVersion(action); v == "latest" && latestUpgrade.Title == "" { +- latestUpgrade = action +- } else if versionedUpgrade.Title == "" || semver.Compare(v, chosenVersionedUpgrade) > 0 { +- chosenVersionedUpgrade = v +- versionedUpgrade = action +- } +- } else if strings.HasPrefix(action.Title, "Reset govulncheck") { +- resetAction = action +- } else if !seenTitles[action.Command.Title] { +- seenTitles[action.Command.Title] = true +- selected = append(selected, action) +- } - } -- for i, m := range e.tokMods { -- mmap[m] = 1 << uint(i) // go 1.12 compatibility +- if versionedUpgrade.Title != "" { +- selected = append(selected, versionedUpgrade) - } -- return tmap, mmap --} -- --// SemanticTypes to use in case there is no client, as in the command line, or tests --func SemanticTypes() []string { -- return semanticTypes[:] --} -- --// SemanticModifiers to use in case there is no client. --func SemanticModifiers() []string { -- return semanticModifiers[:] --} -- --var ( -- semanticTypes = [...]string{ -- "namespace", "type", "class", "enum", "interface", -- "struct", "typeParameter", "parameter", "variable", "property", "enumMember", -- "event", "function", "method", "macro", "keyword", "modifier", "comment", -- "string", "number", "regexp", "operator", +- if latestUpgrade.Title != "" { +- selected = append(selected, latestUpgrade) - } -- semanticModifiers = [...]string{ -- "declaration", "definition", "readonly", "static", -- "deprecated", "abstract", "async", "modification", "documentation", "defaultLibrary", +- if resetAction.Title != "" { +- selected = append(selected, resetAction) - } --) -- --var godirectives = map[string]struct{}{ -- // https://pkg.go.dev/cmd/compile -- "noescape": {}, -- "uintptrescapes": {}, -- "noinline": {}, -- "norace": {}, -- "nosplit": {}, -- "linkname": {}, +- return selected +-} - -- // https://pkg.go.dev/go/build -- "build": {}, -- "binary-only-package": {}, -- "embed": {}, +-func getUpgradeVersion(p protocol.CodeAction) string { +- return strings.TrimPrefix(p.Title, upgradeCodeActionPrefix) -} +diff -urN a/gopls/internal/mod/format.go b/gopls/internal/mod/format.go +--- a/gopls/internal/mod/format.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/mod/format.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,32 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// Tokenize godirective at the start of the comment c, if any, and the surrounding comment. --// If there is any failure, emits the entire comment as a tokComment token. --// Directives are highlighted as-is, even if used incorrectly. Typically there are --// dedicated analyzers that will warn about misuse. --func (e *encoded) godirective(c *ast.Comment) { -- // First check if '//go:directive args...' is a valid directive. -- directive, args, _ := strings.Cut(c.Text, " ") -- kind, _ := stringsCutPrefix(directive, "//go:") -- if _, ok := godirectives[kind]; !ok { -- // Unknown go: directive. -- e.token(c.Pos(), len(c.Text), tokComment, nil) -- return -- } +-package mod - -- // Make the 'go:directive' part stand out, the rest is comments. -- e.token(c.Pos(), len("//"), tokComment, nil) +-import ( +- "context" - -- directiveStart := c.Pos() + token.Pos(len("//")) -- e.token(directiveStart, len(directive[len("//"):]), tokNamespace, nil) +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/diff" +- "golang.org/x/tools/internal/event" +-) - -- if len(args) > 0 { -- tailStart := c.Pos() + token.Pos(len(directive)+len(" ")) -- e.token(tailStart, len(args), tokComment, nil) -- } --} +-func Format(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.TextEdit, error) { +- ctx, done := event.Start(ctx, "mod.Format") +- defer done() - --// Go 1.20 strings.CutPrefix. --func stringsCutPrefix(s, prefix string) (after string, found bool) { -- if !strings.HasPrefix(s, prefix) { -- return s, false +- pm, err := snapshot.ParseMod(ctx, fh) +- if err != nil { +- return nil, err - } -- return s[len(prefix):], true +- formatted, err := pm.File.Format() +- if err != nil { +- return nil, err +- } +- // Calculate the edits to be made due to the change. +- diffs := diff.Bytes(pm.Mapper.Content, formatted) +- return protocol.EditsFromDiffEdits(pm.Mapper, diffs) -} -diff -urN a/gopls/internal/lsp/server_gen.go b/gopls/internal/lsp/server_gen.go ---- a/gopls/internal/lsp/server_gen.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/server_gen.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,309 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/mod/hover.go b/gopls/internal/mod/hover.go +--- a/gopls/internal/mod/hover.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/mod/hover.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,380 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package lsp -- --// Code generated by gopls/internal/lsp/helper. DO NOT EDIT. +-package mod - -import ( +- "bytes" - "context" +- "fmt" +- "sort" +- "strings" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" +- "golang.org/x/mod/modfile" +- "golang.org/x/mod/semver" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/settings" +- "golang.org/x/tools/gopls/internal/vulncheck" +- "golang.org/x/tools/gopls/internal/vulncheck/govulncheck" +- "golang.org/x/tools/gopls/internal/vulncheck/osv" +- "golang.org/x/tools/internal/event" -) - --func (s *Server) CodeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) { -- return s.codeAction(ctx, params) --} -- --func (s *Server) CodeLens(ctx context.Context, params *protocol.CodeLensParams) ([]protocol.CodeLens, error) { -- return s.codeLens(ctx, params) --} +-func Hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) (*protocol.Hover, error) { +- var found bool +- for _, uri := range snapshot.View().ModFiles() { +- if fh.URI() == uri { +- found = true +- break +- } +- } - --func (s *Server) ColorPresentation(context.Context, *protocol.ColorPresentationParams) ([]protocol.ColorPresentation, error) { -- return nil, notImplemented("ColorPresentation") --} +- // We only provide hover information for the view's go.mod files. +- if !found { +- return nil, nil +- } - --func (s *Server) Completion(ctx context.Context, params *protocol.CompletionParams) (*protocol.CompletionList, error) { -- return s.completion(ctx, params) --} +- ctx, done := event.Start(ctx, "mod.Hover") +- defer done() - --func (s *Server) Declaration(context.Context, *protocol.DeclarationParams) (*protocol.Or_textDocument_declaration, error) { -- return nil, notImplemented("Declaration") --} +- // Get the position of the cursor. +- pm, err := snapshot.ParseMod(ctx, fh) +- if err != nil { +- return nil, fmt.Errorf("getting modfile handle: %w", err) +- } +- offset, err := pm.Mapper.PositionOffset(position) +- if err != nil { +- return nil, fmt.Errorf("computing cursor position: %w", err) +- } - --func (s *Server) Definition(ctx context.Context, params *protocol.DefinitionParams) ([]protocol.Location, error) { -- return s.definition(ctx, params) +- // If the cursor position is on a module statement +- if hover, ok := hoverOnModuleStatement(ctx, pm, offset, snapshot, fh); ok { +- return hover, nil +- } +- return hoverOnRequireStatement(ctx, pm, offset, snapshot, fh) -} - --func (s *Server) Diagnostic(context.Context, *string) (*string, error) { -- return nil, notImplemented("Diagnostic") --} +-func hoverOnRequireStatement(ctx context.Context, pm *cache.ParsedModule, offset int, snapshot *cache.Snapshot, fh file.Handle) (*protocol.Hover, error) { +- // Confirm that the cursor is at the position of a require statement. +- var req *modfile.Require +- var startOffset, endOffset int +- for _, r := range pm.File.Require { +- dep := []byte(r.Mod.Path) +- s, e := r.Syntax.Start.Byte, r.Syntax.End.Byte +- i := bytes.Index(pm.Mapper.Content[s:e], dep) +- if i == -1 { +- continue +- } +- // Shift the start position to the location of the +- // dependency within the require statement. +- startOffset, endOffset = s+i, e +- if startOffset <= offset && offset <= endOffset { +- req = r +- break +- } +- } +- // TODO(hyangah): find position for info about vulnerabilities in Go - --func (s *Server) DiagnosticWorkspace(context.Context, *protocol.WorkspaceDiagnosticParams) (*protocol.WorkspaceDiagnosticReport, error) { -- return nil, notImplemented("DiagnosticWorkspace") --} +- // The cursor position is not on a require statement. +- if req == nil { +- return nil, nil +- } - --func (s *Server) DidChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error { -- return s.didChange(ctx, params) --} +- // Get the vulnerability info. +- fromGovulncheck := true +- vs := snapshot.Vulnerabilities(fh.URI())[fh.URI()] +- if vs == nil && snapshot.Options().Vulncheck == settings.ModeVulncheckImports { +- var err error +- vs, err = snapshot.ModVuln(ctx, fh.URI()) +- if err != nil { +- return nil, err +- } +- fromGovulncheck = false +- } +- affecting, nonaffecting, osvs := lookupVulns(vs, req.Mod.Path, req.Mod.Version) - --func (s *Server) DidChangeConfiguration(ctx context.Context, _gen *protocol.DidChangeConfigurationParams) error { -- return s.didChangeConfiguration(ctx, _gen) --} +- // Get the `go mod why` results for the given file. +- why, err := snapshot.ModWhy(ctx, fh) +- if err != nil { +- return nil, err +- } +- explanation, ok := why[req.Mod.Path] +- if !ok { +- return nil, nil +- } - --func (s *Server) DidChangeNotebookDocument(context.Context, *protocol.DidChangeNotebookDocumentParams) error { -- return notImplemented("DidChangeNotebookDocument") --} +- // Get the range to highlight for the hover. +- // TODO(hyangah): adjust the hover range to include the version number +- // to match the diagnostics' range. +- rng, err := pm.Mapper.OffsetRange(startOffset, endOffset) +- if err != nil { +- return nil, err +- } +- options := snapshot.Options() +- isPrivate := snapshot.IsGoPrivatePath(req.Mod.Path) +- header := formatHeader(req.Mod.Path, options) +- explanation = formatExplanation(explanation, req, options, isPrivate) +- vulns := formatVulnerabilities(affecting, nonaffecting, osvs, options, fromGovulncheck) - --func (s *Server) DidChangeWatchedFiles(ctx context.Context, params *protocol.DidChangeWatchedFilesParams) error { -- return s.didChangeWatchedFiles(ctx, params) +- return &protocol.Hover{ +- Contents: protocol.MarkupContent{ +- Kind: options.PreferredContentFormat, +- Value: header + vulns + explanation, +- }, +- Range: rng, +- }, nil -} - --func (s *Server) DidChangeWorkspaceFolders(ctx context.Context, params *protocol.DidChangeWorkspaceFoldersParams) error { -- return s.didChangeWorkspaceFolders(ctx, params) --} +-func hoverOnModuleStatement(ctx context.Context, pm *cache.ParsedModule, offset int, snapshot *cache.Snapshot, fh file.Handle) (*protocol.Hover, bool) { +- module := pm.File.Module +- if module == nil { +- return nil, false // no module stmt +- } +- if offset < module.Syntax.Start.Byte || offset > module.Syntax.End.Byte { +- return nil, false // cursor not in module stmt +- } - --func (s *Server) DidClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error { -- return s.didClose(ctx, params) --} +- rng, err := pm.Mapper.OffsetRange(module.Syntax.Start.Byte, module.Syntax.End.Byte) +- if err != nil { +- return nil, false +- } +- fromGovulncheck := true +- vs := snapshot.Vulnerabilities(fh.URI())[fh.URI()] - --func (s *Server) DidCloseNotebookDocument(context.Context, *protocol.DidCloseNotebookDocumentParams) error { -- return notImplemented("DidCloseNotebookDocument") --} +- if vs == nil && snapshot.Options().Vulncheck == settings.ModeVulncheckImports { +- vs, err = snapshot.ModVuln(ctx, fh.URI()) +- if err != nil { +- return nil, false +- } +- fromGovulncheck = false +- } +- modpath := "stdlib" +- goVersion := snapshot.View().GoVersionString() +- affecting, nonaffecting, osvs := lookupVulns(vs, modpath, goVersion) +- options := snapshot.Options() +- vulns := formatVulnerabilities(affecting, nonaffecting, osvs, options, fromGovulncheck) - --func (s *Server) DidCreateFiles(context.Context, *protocol.CreateFilesParams) error { -- return notImplemented("DidCreateFiles") +- return &protocol.Hover{ +- Contents: protocol.MarkupContent{ +- Kind: options.PreferredContentFormat, +- Value: vulns, +- }, +- Range: rng, +- }, true -} - --func (s *Server) DidDeleteFiles(context.Context, *protocol.DeleteFilesParams) error { -- return notImplemented("DidDeleteFiles") +-func formatHeader(modpath string, options *settings.Options) string { +- var b strings.Builder +- // Write the heading as an H3. +- b.WriteString("#### " + modpath) +- if options.PreferredContentFormat == protocol.Markdown { +- b.WriteString("\n\n") +- } else { +- b.WriteRune('\n') +- } +- return b.String() -} - --func (s *Server) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error { -- return s.didOpen(ctx, params) --} +-func lookupVulns(vulns *vulncheck.Result, modpath, version string) (affecting, nonaffecting []*govulncheck.Finding, osvs map[string]*osv.Entry) { +- if vulns == nil || len(vulns.Entries) == 0 { +- return nil, nil, nil +- } +- for _, finding := range vulns.Findings { +- vuln, typ := foundVuln(finding) +- if vuln.Module != modpath { +- continue +- } +- // It is possible that the source code was changed since the last +- // govulncheck run and information in the `vulns` info is stale. +- // For example, imagine that a user is in the middle of updating +- // problematic modules detected by the govulncheck run by applying +- // quick fixes. Stale diagnostics can be confusing and prevent the +- // user from quickly locating the next module to fix. +- // Ideally we should rerun the analysis with the updated module +- // dependencies or any other code changes, but we are not yet +- // in the position of automatically triggering the analysis +- // (govulncheck can take a while). We also don't know exactly what +- // part of source code was changed since `vulns` was computed. +- // As a heuristic, we assume that a user upgrades the affecting +- // module to the version with the fix or the latest one, and if the +- // version in the require statement is equal to or higher than the +- // fixed version, skip the vulnerability information in the hover. +- // Eventually, the user has to rerun govulncheck. +- if finding.FixedVersion != "" && semver.IsValid(version) && semver.Compare(finding.FixedVersion, version) <= 0 { +- continue +- } +- switch typ { +- case vulnCalled: +- affecting = append(affecting, finding) +- case vulnImported: +- nonaffecting = append(nonaffecting, finding) +- } +- } - --func (s *Server) DidOpenNotebookDocument(context.Context, *protocol.DidOpenNotebookDocumentParams) error { -- return notImplemented("DidOpenNotebookDocument") +- // Remove affecting elements from nonaffecting. +- // An OSV entry can appear in both lists if an OSV entry covers +- // multiple packages imported but not all vulnerable symbols are used. +- // The current wording of hover message doesn't clearly +- // present this case well IMO, so let's skip reporting nonaffecting. +- if len(affecting) > 0 && len(nonaffecting) > 0 { +- affectingSet := map[string]bool{} +- for _, f := range affecting { +- affectingSet[f.OSV] = true +- } +- n := 0 +- for _, v := range nonaffecting { +- if !affectingSet[v.OSV] { +- nonaffecting[n] = v +- n++ +- } +- } +- nonaffecting = nonaffecting[:n] +- } +- sort.Slice(nonaffecting, func(i, j int) bool { return nonaffecting[i].OSV < nonaffecting[j].OSV }) +- sort.Slice(affecting, func(i, j int) bool { return affecting[i].OSV < affecting[j].OSV }) +- return affecting, nonaffecting, vulns.Entries -} - --func (s *Server) DidRenameFiles(context.Context, *protocol.RenameFilesParams) error { -- return notImplemented("DidRenameFiles") +-func fixedVersion(fixed string) string { +- if fixed == "" { +- return "No fix is available." +- } +- return "Fixed in " + fixed + "." -} - --func (s *Server) DidSave(ctx context.Context, params *protocol.DidSaveTextDocumentParams) error { -- return s.didSave(ctx, params) --} +-func formatVulnerabilities(affecting, nonaffecting []*govulncheck.Finding, osvs map[string]*osv.Entry, options *settings.Options, fromGovulncheck bool) string { +- if len(osvs) == 0 || (len(affecting) == 0 && len(nonaffecting) == 0) { +- return "" +- } +- byOSV := func(findings []*govulncheck.Finding) map[string][]*govulncheck.Finding { +- m := make(map[string][]*govulncheck.Finding) +- for _, f := range findings { +- m[f.OSV] = append(m[f.OSV], f) +- } +- return m +- } +- affectingByOSV := byOSV(affecting) +- nonaffectingByOSV := byOSV(nonaffecting) - --func (s *Server) DidSaveNotebookDocument(context.Context, *protocol.DidSaveNotebookDocumentParams) error { -- return notImplemented("DidSaveNotebookDocument") --} +- // TODO(hyangah): can we use go templates to generate hover messages? +- // Then, we can use a different template for markdown case. +- useMarkdown := options.PreferredContentFormat == protocol.Markdown - --func (s *Server) DocumentColor(context.Context, *protocol.DocumentColorParams) ([]protocol.ColorInformation, error) { -- return nil, notImplemented("DocumentColor") --} +- var b strings.Builder - --func (s *Server) DocumentHighlight(ctx context.Context, params *protocol.DocumentHighlightParams) ([]protocol.DocumentHighlight, error) { -- return s.documentHighlight(ctx, params) --} +- if len(affectingByOSV) > 0 { +- // TODO(hyangah): make the message more eyecatching (icon/codicon/color) +- if len(affectingByOSV) == 1 { +- fmt.Fprintf(&b, "\n**WARNING:** Found %d reachable vulnerability.\n", len(affectingByOSV)) +- } else { +- fmt.Fprintf(&b, "\n**WARNING:** Found %d reachable vulnerabilities.\n", len(affectingByOSV)) +- } +- } +- for id, findings := range affectingByOSV { +- fix := fixedVersion(findings[0].FixedVersion) +- pkgs := vulnerablePkgsInfo(findings, useMarkdown) +- osvEntry := osvs[id] - --func (s *Server) DocumentLink(ctx context.Context, params *protocol.DocumentLinkParams) ([]protocol.DocumentLink, error) { -- return s.documentLink(ctx, params) --} +- if useMarkdown { +- fmt.Fprintf(&b, "- [**%v**](%v) %v%v\n%v\n", id, href(id), osvEntry.Summary, pkgs, fix) +- } else { +- fmt.Fprintf(&b, " - [%v] %v (%v) %v%v\n", id, osvEntry.Summary, href(id), pkgs, fix) +- } +- } +- if len(nonaffecting) > 0 { +- if fromGovulncheck { +- fmt.Fprintf(&b, "\n**Note:** The project imports packages with known vulnerabilities, but does not call the vulnerable code.\n") +- } else { +- fmt.Fprintf(&b, "\n**Note:** The project imports packages with known vulnerabilities. Use `govulncheck` to check if the project uses vulnerable symbols.\n") +- } +- } +- for k, findings := range nonaffectingByOSV { +- fix := fixedVersion(findings[0].FixedVersion) +- pkgs := vulnerablePkgsInfo(findings, useMarkdown) +- osvEntry := osvs[k] - --func (s *Server) DocumentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) ([]interface{}, error) { -- return s.documentSymbol(ctx, params) +- if useMarkdown { +- fmt.Fprintf(&b, "- [%v](%v) %v%v\n%v\n", k, href(k), osvEntry.Summary, pkgs, fix) +- } else { +- fmt.Fprintf(&b, " - [%v] %v (%v) %v\n%v\n", k, osvEntry.Summary, href(k), pkgs, fix) +- } +- } +- b.WriteString("\n") +- return b.String() -} - --func (s *Server) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) { -- return s.executeCommand(ctx, params) +-func vulnerablePkgsInfo(findings []*govulncheck.Finding, useMarkdown bool) string { +- var b strings.Builder +- seen := map[string]bool{} +- for _, f := range findings { +- p := f.Trace[0].Package +- if !seen[p] { +- seen[p] = true +- if useMarkdown { +- b.WriteString("\n * `") +- } else { +- b.WriteString("\n ") +- } +- b.WriteString(p) +- if useMarkdown { +- b.WriteString("`") +- } +- } +- } +- return b.String() -} - --func (s *Server) Exit(ctx context.Context) error { -- return s.exit(ctx) --} +-func formatExplanation(text string, req *modfile.Require, options *settings.Options, isPrivate bool) string { +- text = strings.TrimSuffix(text, "\n") +- splt := strings.Split(text, "\n") +- length := len(splt) - --func (s *Server) FoldingRange(ctx context.Context, params *protocol.FoldingRangeParams) ([]protocol.FoldingRange, error) { -- return s.foldingRange(ctx, params) --} +- var b strings.Builder - --func (s *Server) Formatting(ctx context.Context, params *protocol.DocumentFormattingParams) ([]protocol.TextEdit, error) { -- return s.formatting(ctx, params) --} +- // If the explanation is 2 lines, then it is of the form: +- // # golang.org/x/text/encoding +- // (main module does not need package golang.org/x/text/encoding) +- if length == 2 { +- b.WriteString(splt[1]) +- return b.String() +- } - --func (s *Server) Hover(ctx context.Context, params *protocol.HoverParams) (*protocol.Hover, error) { -- return s.hover(ctx, params) --} +- imp := splt[length-1] // import path +- reference := imp +- // See golang/go#36998: don't link to modules matching GOPRIVATE. +- if !isPrivate && options.PreferredContentFormat == protocol.Markdown { +- target := imp +- if strings.ToLower(options.LinkTarget) == "pkg.go.dev" { +- target = strings.Replace(target, req.Mod.Path, req.Mod.String(), 1) +- } +- reference = fmt.Sprintf("[%s](%s)", imp, cache.BuildLink(options.LinkTarget, target, "")) +- } +- b.WriteString("This module is necessary because " + reference + " is imported in") - --func (s *Server) Implementation(ctx context.Context, params *protocol.ImplementationParams) ([]protocol.Location, error) { -- return s.implementation(ctx, params) --} +- // If the explanation is 3 lines, then it is of the form: +- // # golang.org/x/tools +- // modtest +- // golang.org/x/tools/go/packages +- if length == 3 { +- msg := fmt.Sprintf(" `%s`.", splt[1]) +- b.WriteString(msg) +- return b.String() +- } - --func (s *Server) IncomingCalls(ctx context.Context, params *protocol.CallHierarchyIncomingCallsParams) ([]protocol.CallHierarchyIncomingCall, error) { -- return s.incomingCalls(ctx, params) +- // If the explanation is more than 3 lines, then it is of the form: +- // # golang.org/x/text/language +- // rsc.io/quote +- // rsc.io/sampler +- // golang.org/x/text/language +- b.WriteString(":\n```text") +- dash := "" +- for _, imp := range splt[1 : length-1] { +- dash += "-" +- b.WriteString("\n" + dash + " " + imp) +- } +- b.WriteString("\n```") +- return b.String() -} +diff -urN a/gopls/internal/mod/inlayhint.go b/gopls/internal/mod/inlayhint.go +--- a/gopls/internal/mod/inlayhint.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/mod/inlayhint.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,104 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +-package mod - --func (s *Server) Initialize(ctx context.Context, params *protocol.ParamInitialize) (*protocol.InitializeResult, error) { -- return s.initialize(ctx, params) --} +-import ( +- "context" +- "fmt" - --func (s *Server) Initialized(ctx context.Context, params *protocol.InitializedParams) error { -- return s.initialized(ctx, params) --} +- "golang.org/x/mod/modfile" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +-) - --func (s *Server) InlayHint(ctx context.Context, params *protocol.InlayHintParams) ([]protocol.InlayHint, error) { -- return s.inlayHint(ctx, params) --} +-func InlayHint(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, _ protocol.Range) ([]protocol.InlayHint, error) { +- // Inlay hints are enabled if the client supports them. +- pm, err := snapshot.ParseMod(ctx, fh) +- if err != nil { +- return nil, err +- } - --func (s *Server) InlineCompletion(context.Context, *protocol.InlineCompletionParams) (*protocol.Or_Result_textDocument_inlineCompletion, error) { -- return nil, notImplemented("InlineCompletion") --} +- // Compare the version of the module used in the snapshot's +- // metadata (i.e. the solution to the MVS constraints computed +- // by go list) with the version requested by the module, in +- // both cases, taking replaces into account. Produce an +- // InlayHint when the version of the module is not the one +- // used. - --func (s *Server) InlineValue(context.Context, *protocol.InlineValueParams) ([]protocol.InlineValue, error) { -- return nil, notImplemented("InlineValue") --} +- replaces := make(map[string]*modfile.Replace) +- for _, x := range pm.File.Replace { +- replaces[x.Old.Path] = x +- } - --func (s *Server) LinkedEditingRange(context.Context, *protocol.LinkedEditingRangeParams) (*protocol.LinkedEditingRanges, error) { -- return nil, notImplemented("LinkedEditingRange") --} +- requires := make(map[string]*modfile.Require) +- for _, x := range pm.File.Require { +- requires[x.Mod.Path] = x +- } - --func (s *Server) Moniker(context.Context, *protocol.MonikerParams) ([]protocol.Moniker, error) { -- return nil, notImplemented("Moniker") --} +- am, err := snapshot.AllMetadata(ctx) +- if err != nil { +- return nil, err +- } - --func (s *Server) NonstandardRequest(ctx context.Context, method string, params interface{}) (interface{}, error) { -- return s.nonstandardRequest(ctx, method, params) +- var ans []protocol.InlayHint +- seen := make(map[string]bool) +- for _, meta := range am { +- if meta.Module == nil || seen[meta.Module.Path] { +- continue +- } +- seen[meta.Module.Path] = true +- metaVersion := meta.Module.Version +- if meta.Module.Replace != nil { +- metaVersion = meta.Module.Replace.Version +- } +- // These versions can be blank, as in gopls/go.mod's local replace +- if oldrepl, ok := replaces[meta.Module.Path]; ok && oldrepl.New.Version != metaVersion { +- ih := genHint(oldrepl.Syntax, oldrepl.New.Version, metaVersion, pm.Mapper) +- if ih != nil { +- ans = append(ans, *ih) +- } +- } else if oldreq, ok := requires[meta.Module.Path]; ok && oldreq.Mod.Version != metaVersion { +- // maybe it was replaced: +- if _, ok := replaces[meta.Module.Path]; ok { +- continue +- } +- ih := genHint(oldreq.Syntax, oldreq.Mod.Version, metaVersion, pm.Mapper) +- if ih != nil { +- ans = append(ans, *ih) +- } +- } +- } +- return ans, nil -} - --func (s *Server) OnTypeFormatting(context.Context, *protocol.DocumentOnTypeFormattingParams) ([]protocol.TextEdit, error) { -- return nil, notImplemented("OnTypeFormatting") +-func genHint(mline *modfile.Line, oldVersion, newVersion string, m *protocol.Mapper) *protocol.InlayHint { +- x := mline.End.Byte // the parser has removed trailing whitespace and comments (see modfile_test.go) +- x -= len(mline.Token[len(mline.Token)-1]) +- line, err := m.OffsetPosition(x) +- if err != nil { +- return nil +- } +- part := protocol.InlayHintLabelPart{ +- Value: newVersion, +- Tooltip: &protocol.OrPTooltipPLabel{ +- Value: fmt.Sprintf("The build selects version %s rather than go.mod's version %s.", newVersion, oldVersion), +- }, +- } +- rng, err := m.OffsetRange(x, mline.End.Byte) +- if err != nil { +- return nil +- } +- te := protocol.TextEdit{ +- Range: rng, +- NewText: newVersion, +- } +- return &protocol.InlayHint{ +- Position: line, +- Label: []protocol.InlayHintLabelPart{part}, +- Kind: protocol.Parameter, +- PaddingRight: true, +- TextEdits: []protocol.TextEdit{te}, +- } -} +diff -urN a/gopls/internal/progress/progress.go b/gopls/internal/progress/progress.go +--- a/gopls/internal/progress/progress.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/progress/progress.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,292 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func (s *Server) OutgoingCalls(ctx context.Context, params *protocol.CallHierarchyOutgoingCallsParams) ([]protocol.CallHierarchyOutgoingCall, error) { -- return s.outgoingCalls(ctx, params) --} +-// The progress package defines utilities for reporting the progress +-// of long-running operations using features of the LSP client +-// interface such as Progress and ShowMessage. +-package progress - --func (s *Server) PrepareCallHierarchy(ctx context.Context, params *protocol.CallHierarchyPrepareParams) ([]protocol.CallHierarchyItem, error) { -- return s.prepareCallHierarchy(ctx, params) --} +-import ( +- "context" +- "fmt" +- "io" +- "math/rand" +- "strconv" +- "strings" +- "sync" - --func (s *Server) PrepareRename(ctx context.Context, params *protocol.PrepareRenameParams) (*protocol.PrepareRename2Gn, error) { -- return s.prepareRename(ctx, params) --} +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/tag" +- "golang.org/x/tools/internal/xcontext" +-) - --func (s *Server) PrepareTypeHierarchy(context.Context, *protocol.TypeHierarchyPrepareParams) ([]protocol.TypeHierarchyItem, error) { -- return nil, notImplemented("PrepareTypeHierarchy") +-// NewTracker returns a new Tracker that reports progress to the +-// specified client. +-func NewTracker(client protocol.Client) *Tracker { +- return &Tracker{ +- client: client, +- inProgress: make(map[protocol.ProgressToken]*WorkDone), +- } -} - --func (s *Server) Progress(context.Context, *protocol.ProgressParams) error { -- return notImplemented("Progress") --} +-// A Tracker reports the progress of a long-running operation to an LSP client. +-type Tracker struct { +- client protocol.Client +- supportsWorkDoneProgress bool - --func (s *Server) RangeFormatting(context.Context, *protocol.DocumentRangeFormattingParams) ([]protocol.TextEdit, error) { -- return nil, notImplemented("RangeFormatting") +- mu sync.Mutex +- inProgress map[protocol.ProgressToken]*WorkDone -} - --func (s *Server) RangesFormatting(context.Context, *protocol.DocumentRangesFormattingParams) ([]protocol.TextEdit, error) { -- return nil, notImplemented("RangesFormatting") +-// SetSupportsWorkDoneProgress sets whether the client supports "work done" +-// progress reporting. It must be set before using the tracker. +-// +-// TODO(rfindley): fix this broken initialization pattern. +-// Also: do we actually need the fall-back progress behavior using ShowMessage? +-// Surely ShowMessage notifications are too noisy to be worthwhile. +-func (t *Tracker) SetSupportsWorkDoneProgress(b bool) { +- t.supportsWorkDoneProgress = b -} - --func (s *Server) References(ctx context.Context, params *protocol.ReferenceParams) ([]protocol.Location, error) { -- return s.references(ctx, params) +-// SupportsWorkDoneProgress reports whether the tracker supports work done +-// progress reporting. +-func (t *Tracker) SupportsWorkDoneProgress() bool { +- return t.supportsWorkDoneProgress -} - --func (s *Server) Rename(ctx context.Context, params *protocol.RenameParams) (*protocol.WorkspaceEdit, error) { -- return s.rename(ctx, params) +-// Start notifies the client of work being done on the server. It uses either +-// ShowMessage RPCs or $/progress messages, depending on the capabilities of +-// the client. The returned WorkDone handle may be used to report incremental +-// progress, and to report work completion. In particular, it is an error to +-// call start and not call end(...) on the returned WorkDone handle. +-// +-// If token is empty, a token will be randomly generated. +-// +-// The progress item is considered cancellable if the given cancel func is +-// non-nil. In this case, cancel is called when the work done +-// +-// Example: +-// +-// func Generate(ctx) (err error) { +-// ctx, cancel := context.WithCancel(ctx) +-// defer cancel() +-// work := s.progress.start(ctx, "generate", "running go generate", cancel) +-// defer func() { +-// if err != nil { +-// work.end(ctx, fmt.Sprintf("generate failed: %v", err)) +-// } else { +-// work.end(ctx, "done") +-// } +-// }() +-// // Do the work... +-// } +-func (t *Tracker) Start(ctx context.Context, title, message string, token protocol.ProgressToken, cancel func()) *WorkDone { +- ctx = xcontext.Detach(ctx) // progress messages should not be cancelled +- wd := &WorkDone{ +- client: t.client, +- token: token, +- cancel: cancel, +- } +- if !t.supportsWorkDoneProgress { +- // Previous iterations of this fallback attempted to retain cancellation +- // support by using ShowMessageCommand with a 'Cancel' button, but this is +- // not ideal as the 'Cancel' dialog stays open even after the command +- // completes. +- // +- // Just show a simple message. Clients can implement workDone progress +- // reporting to get cancellation support. +- if err := wd.client.ShowMessage(ctx, &protocol.ShowMessageParams{ +- Type: protocol.Log, +- Message: message, +- }); err != nil { +- event.Error(ctx, "showing start message for "+title, err) +- } +- return wd +- } +- if wd.token == nil { +- token = strconv.FormatInt(rand.Int63(), 10) +- err := wd.client.WorkDoneProgressCreate(ctx, &protocol.WorkDoneProgressCreateParams{ +- Token: token, +- }) +- if err != nil { +- wd.err = err +- event.Error(ctx, "starting work for "+title, err) +- return wd +- } +- wd.token = token +- } +- // At this point we have a token that the client knows about. Store the token +- // before starting work. +- t.mu.Lock() +- t.inProgress[wd.token] = wd +- t.mu.Unlock() +- wd.cleanup = func() { +- t.mu.Lock() +- delete(t.inProgress, token) +- t.mu.Unlock() +- } +- err := wd.client.Progress(ctx, &protocol.ProgressParams{ +- Token: wd.token, +- Value: &protocol.WorkDoneProgressBegin{ +- Kind: "begin", +- Cancellable: wd.cancel != nil, +- Message: message, +- Title: title, +- }, +- }) +- if err != nil { +- event.Error(ctx, "progress begin", err) +- } +- return wd -} - --func (s *Server) Resolve(context.Context, *protocol.InlayHint) (*protocol.InlayHint, error) { -- return nil, notImplemented("Resolve") +-func (t *Tracker) Cancel(token protocol.ProgressToken) error { +- t.mu.Lock() +- defer t.mu.Unlock() +- wd, ok := t.inProgress[token] +- if !ok { +- return fmt.Errorf("token %q not found in progress", token) +- } +- if wd.cancel == nil { +- return fmt.Errorf("work %q is not cancellable", token) +- } +- wd.doCancel() +- return nil -} - --func (s *Server) ResolveCodeAction(context.Context, *protocol.CodeAction) (*protocol.CodeAction, error) { -- return nil, notImplemented("ResolveCodeAction") --} +-// WorkDone represents a unit of work that is reported to the client via the +-// progress API. +-type WorkDone struct { +- client protocol.Client +- // If token is nil, this workDone object uses the ShowMessage API, rather +- // than $/progress. +- token protocol.ProgressToken +- // err is set if progress reporting is broken for some reason (for example, +- // if there was an initial error creating a token). +- err error - --func (s *Server) ResolveCodeLens(context.Context, *protocol.CodeLens) (*protocol.CodeLens, error) { -- return nil, notImplemented("ResolveCodeLens") --} +- cancelMu sync.Mutex +- cancelled bool +- cancel func() - --func (s *Server) ResolveCompletionItem(context.Context, *protocol.CompletionItem) (*protocol.CompletionItem, error) { -- return nil, notImplemented("ResolveCompletionItem") +- cleanup func() -} - --func (s *Server) ResolveDocumentLink(context.Context, *protocol.DocumentLink) (*protocol.DocumentLink, error) { -- return nil, notImplemented("ResolveDocumentLink") +-func (wd *WorkDone) Token() protocol.ProgressToken { +- return wd.token -} - --func (s *Server) ResolveWorkspaceSymbol(context.Context, *protocol.WorkspaceSymbol) (*protocol.WorkspaceSymbol, error) { -- return nil, notImplemented("ResolveWorkspaceSymbol") +-func (wd *WorkDone) doCancel() { +- wd.cancelMu.Lock() +- defer wd.cancelMu.Unlock() +- if !wd.cancelled { +- wd.cancel() +- } -} - --func (s *Server) SelectionRange(ctx context.Context, params *protocol.SelectionRangeParams) ([]protocol.SelectionRange, error) { -- return s.selectionRange(ctx, params) +-// Report reports an update on WorkDone report back to the client. +-func (wd *WorkDone) Report(ctx context.Context, message string, percentage float64) { +- ctx = xcontext.Detach(ctx) // progress messages should not be cancelled +- if wd == nil { +- return +- } +- wd.cancelMu.Lock() +- cancelled := wd.cancelled +- wd.cancelMu.Unlock() +- if cancelled { +- return +- } +- if wd.err != nil || wd.token == nil { +- // Not using the workDone API, so we do nothing. It would be far too spammy +- // to send incremental messages. +- return +- } +- message = strings.TrimSuffix(message, "\n") +- err := wd.client.Progress(ctx, &protocol.ProgressParams{ +- Token: wd.token, +- Value: &protocol.WorkDoneProgressReport{ +- Kind: "report", +- // Note that in the LSP spec, the value of Cancellable may be changed to +- // control whether the cancel button in the UI is enabled. Since we don't +- // yet use this feature, the value is kept constant here. +- Cancellable: wd.cancel != nil, +- Message: message, +- Percentage: uint32(percentage), +- }, +- }) +- if err != nil { +- event.Error(ctx, "reporting progress", err) +- } -} - --func (s *Server) SemanticTokensFull(ctx context.Context, params *protocol.SemanticTokensParams) (*protocol.SemanticTokens, error) { -- return s.semanticTokensFull(ctx, params) +-// End reports a workdone completion back to the client. +-func (wd *WorkDone) End(ctx context.Context, message string) { +- ctx = xcontext.Detach(ctx) // progress messages should not be cancelled +- if wd == nil { +- return +- } +- var err error +- switch { +- case wd.err != nil: +- // There is a prior error. +- case wd.token == nil: +- // We're falling back to message-based reporting. +- err = wd.client.ShowMessage(ctx, &protocol.ShowMessageParams{ +- Type: protocol.Info, +- Message: message, +- }) +- default: +- err = wd.client.Progress(ctx, &protocol.ProgressParams{ +- Token: wd.token, +- Value: &protocol.WorkDoneProgressEnd{ +- Kind: "end", +- Message: message, +- }, +- }) +- } +- if err != nil { +- event.Error(ctx, "ending work", err) +- } +- if wd.cleanup != nil { +- wd.cleanup() +- } -} - --func (s *Server) SemanticTokensFullDelta(context.Context, *protocol.SemanticTokensDeltaParams) (interface{}, error) { -- return nil, notImplemented("SemanticTokensFullDelta") +-// NewEventWriter returns an [io.Writer] that calls the context's +-// event printer for each data payload, wrapping it with the +-// operation=generate tag to distinguish its logs from others. +-func NewEventWriter(ctx context.Context, operation string) io.Writer { +- return &eventWriter{ctx: ctx, operation: operation} -} - --func (s *Server) SemanticTokensRange(ctx context.Context, params *protocol.SemanticTokensRangeParams) (*protocol.SemanticTokens, error) { -- return s.semanticTokensRange(ctx, params) +-type eventWriter struct { +- ctx context.Context +- operation string -} - --func (s *Server) SetTrace(context.Context, *protocol.SetTraceParams) error { -- return notImplemented("SetTrace") +-func (ew *eventWriter) Write(p []byte) (n int, err error) { +- event.Log(ew.ctx, string(p), tag.Operation.Of(ew.operation)) +- return len(p), nil -} - --func (s *Server) Shutdown(ctx context.Context) error { -- return s.shutdown(ctx) +-// NewWorkDoneWriter wraps a WorkDone handle to provide a Writer interface, +-// so that workDone reporting can more easily be hooked into commands. +-func NewWorkDoneWriter(ctx context.Context, wd *WorkDone) io.Writer { +- return &workDoneWriter{ctx: ctx, wd: wd} -} - --func (s *Server) SignatureHelp(ctx context.Context, params *protocol.SignatureHelpParams) (*protocol.SignatureHelp, error) { -- return s.signatureHelp(ctx, params) +-// workDoneWriter wraps a workDone handle to provide a Writer interface, +-// so that workDone reporting can more easily be hooked into commands. +-type workDoneWriter struct { +- // In order to implement the io.Writer interface, we must close over ctx. +- ctx context.Context +- wd *WorkDone -} - --func (s *Server) Subtypes(context.Context, *protocol.TypeHierarchySubtypesParams) ([]protocol.TypeHierarchyItem, error) { -- return nil, notImplemented("Subtypes") +-func (wdw *workDoneWriter) Write(p []byte) (n int, err error) { +- wdw.wd.Report(wdw.ctx, string(p), 0) +- // Don't fail just because of a failure to report progress. +- return len(p), nil -} +diff -urN a/gopls/internal/progress/progress_test.go b/gopls/internal/progress/progress_test.go +--- a/gopls/internal/progress/progress_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/progress/progress_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,161 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func (s *Server) Supertypes(context.Context, *protocol.TypeHierarchySupertypesParams) ([]protocol.TypeHierarchyItem, error) { -- return nil, notImplemented("Supertypes") --} +-package progress +- +-import ( +- "context" +- "fmt" +- "sync" +- "testing" +- +- "golang.org/x/tools/gopls/internal/protocol" +-) +- +-type fakeClient struct { +- protocol.Client - --func (s *Server) Symbol(ctx context.Context, params *protocol.WorkspaceSymbolParams) ([]protocol.SymbolInformation, error) { -- return s.symbol(ctx, params) +- token protocol.ProgressToken +- +- mu sync.Mutex +- created, begun, reported, messages, ended int -} - --func (s *Server) TypeDefinition(ctx context.Context, params *protocol.TypeDefinitionParams) ([]protocol.Location, error) { -- return s.typeDefinition(ctx, params) +-func (c *fakeClient) checkToken(token protocol.ProgressToken) { +- if token == nil { +- panic("nil token in progress message") +- } +- if c.token != nil && c.token != token { +- panic(fmt.Errorf("invalid token in progress message: got %v, want %v", token, c.token)) +- } -} - --func (s *Server) WillCreateFiles(context.Context, *protocol.CreateFilesParams) (*protocol.WorkspaceEdit, error) { -- return nil, notImplemented("WillCreateFiles") +-func (c *fakeClient) WorkDoneProgressCreate(ctx context.Context, params *protocol.WorkDoneProgressCreateParams) error { +- c.mu.Lock() +- defer c.mu.Unlock() +- c.checkToken(params.Token) +- c.created++ +- return nil -} - --func (s *Server) WillDeleteFiles(context.Context, *protocol.DeleteFilesParams) (*protocol.WorkspaceEdit, error) { -- return nil, notImplemented("WillDeleteFiles") +-func (c *fakeClient) Progress(ctx context.Context, params *protocol.ProgressParams) error { +- c.mu.Lock() +- defer c.mu.Unlock() +- c.checkToken(params.Token) +- switch params.Value.(type) { +- case *protocol.WorkDoneProgressBegin: +- c.begun++ +- case *protocol.WorkDoneProgressReport: +- c.reported++ +- case *protocol.WorkDoneProgressEnd: +- c.ended++ +- default: +- panic(fmt.Errorf("unknown progress value %T", params.Value)) +- } +- return nil -} - --func (s *Server) WillRenameFiles(context.Context, *protocol.RenameFilesParams) (*protocol.WorkspaceEdit, error) { -- return nil, notImplemented("WillRenameFiles") +-func (c *fakeClient) ShowMessage(context.Context, *protocol.ShowMessageParams) error { +- c.mu.Lock() +- defer c.mu.Unlock() +- c.messages++ +- return nil -} - --func (s *Server) WillSave(context.Context, *protocol.WillSaveTextDocumentParams) error { -- return notImplemented("WillSave") +-func setup() (context.Context, *Tracker, *fakeClient) { +- c := &fakeClient{} +- tracker := NewTracker(c) +- tracker.SetSupportsWorkDoneProgress(true) +- return context.Background(), tracker, c -} - --func (s *Server) WillSaveWaitUntil(context.Context, *protocol.WillSaveTextDocumentParams) ([]protocol.TextEdit, error) { -- return nil, notImplemented("WillSaveWaitUntil") +-func TestProgressTracker_Reporting(t *testing.T) { +- for _, test := range []struct { +- name string +- supported bool +- token protocol.ProgressToken +- wantReported, wantCreated, wantBegun, wantEnded int +- wantMessages int +- }{ +- { +- name: "unsupported", +- wantMessages: 2, +- }, +- { +- name: "random token", +- supported: true, +- wantCreated: 1, +- wantBegun: 1, +- wantReported: 1, +- wantEnded: 1, +- }, +- { +- name: "string token", +- supported: true, +- token: "token", +- wantBegun: 1, +- wantReported: 1, +- wantEnded: 1, +- }, +- { +- name: "numeric token", +- supported: true, +- token: 1, +- wantReported: 1, +- wantBegun: 1, +- wantEnded: 1, +- }, +- } { +- test := test +- t.Run(test.name, func(t *testing.T) { +- ctx, tracker, client := setup() +- ctx, cancel := context.WithCancel(ctx) +- defer cancel() +- tracker.supportsWorkDoneProgress = test.supported +- work := tracker.Start(ctx, "work", "message", test.token, nil) +- client.mu.Lock() +- gotCreated, gotBegun := client.created, client.begun +- client.mu.Unlock() +- if gotCreated != test.wantCreated { +- t.Errorf("got %d created tokens, want %d", gotCreated, test.wantCreated) +- } +- if gotBegun != test.wantBegun { +- t.Errorf("got %d work begun, want %d", gotBegun, test.wantBegun) +- } +- // Ignore errors: this is just testing the reporting behavior. +- work.Report(ctx, "report", 50) +- client.mu.Lock() +- gotReported := client.reported +- client.mu.Unlock() +- if gotReported != test.wantReported { +- t.Errorf("got %d progress reports, want %d", gotReported, test.wantCreated) +- } +- work.End(ctx, "done") +- client.mu.Lock() +- gotEnded, gotMessages := client.ended, client.messages +- client.mu.Unlock() +- if gotEnded != test.wantEnded { +- t.Errorf("got %d ended reports, want %d", gotEnded, test.wantEnded) +- } +- if gotMessages != test.wantMessages { +- t.Errorf("got %d messages, want %d", gotMessages, test.wantMessages) +- } +- }) +- } -} - --func (s *Server) WorkDoneProgressCancel(ctx context.Context, params *protocol.WorkDoneProgressCancelParams) error { -- return s.workDoneProgressCancel(ctx, params) +-func TestProgressTracker_Cancellation(t *testing.T) { +- for _, token := range []protocol.ProgressToken{nil, 1, "a"} { +- ctx, tracker, _ := setup() +- var canceled bool +- cancel := func() { canceled = true } +- work := tracker.Start(ctx, "work", "message", token, cancel) +- if err := tracker.Cancel(work.Token()); err != nil { +- t.Fatal(err) +- } +- if !canceled { +- t.Errorf("tracker.cancel(...): cancel not called") +- } +- } -} -diff -urN a/gopls/internal/lsp/server.go b/gopls/internal/lsp/server.go ---- a/gopls/internal/lsp/server.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/server.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,205 +0,0 @@ --// Copyright 2018 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/protocol/codeactionkind.go b/gopls/internal/protocol/codeactionkind.go +--- a/gopls/internal/protocol/codeactionkind.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/codeactionkind.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,13 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --//go:generate go run ./helper -d protocol/tsserver.go -o server_gen.go -u . -- --// Package lsp implements LSP for gopls. --package lsp +-package protocol - --import ( -- "context" -- "fmt" -- "os" -- "sync" +-// Custom code actions that aren't explicitly stated in LSP +-const ( +- GoTest CodeActionKind = "goTest" +- // TODO: Add GoGenerate, RegenerateCgo etc. - -- "golang.org/x/tools/gopls/internal/lsp/cache" -- "golang.org/x/tools/gopls/internal/lsp/progress" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/jsonrpc2" +- GoDoc CodeActionKind = "source.doc" -) +diff -urN a/gopls/internal/protocol/command/command_gen.go b/gopls/internal/protocol/command/command_gen.go +--- a/gopls/internal/protocol/command/command_gen.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/command/command_gen.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,700 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --const concurrentAnalyses = 1 -- --// NewServer creates an LSP server and binds it to handle incoming client --// messages on the supplied stream. --func NewServer(session *cache.Session, client protocol.ClientCloser, options *source.Options) *Server { -- return &Server{ -- diagnostics: map[span.URI]*fileReports{}, -- gcOptimizationDetails: make(map[source.PackageID]struct{}), -- watchedGlobPatterns: nil, // empty -- changedFiles: make(map[span.URI]struct{}), -- session: session, -- client: client, -- diagnosticsSema: make(chan struct{}, concurrentAnalyses), -- progress: progress.NewTracker(client), -- options: options, -- } --} +-// Don't include this file during code generation, or it will break the build +-// if existing interface methods have been modified. +-//go:build !generate +-// +build !generate - --type serverState int +-// Code generated by gen.go. DO NOT EDIT. - --const ( -- serverCreated = serverState(iota) -- serverInitializing // set once the server has received "initialize" request -- serverInitialized // set once the server has received "initialized" request -- serverShutDown --) +-package command - --func (s serverState) String() string { -- switch s { -- case serverCreated: -- return "created" -- case serverInitializing: -- return "initializing" -- case serverInitialized: -- return "initialized" -- case serverShutDown: -- return "shutDown" -- } -- return fmt.Sprintf("(unknown state: %d)", int(s)) --} +-import ( +- "context" +- "fmt" - --// Server implements the protocol.Server interface. --type Server struct { -- client protocol.ClientCloser +- "golang.org/x/tools/gopls/internal/protocol" +-) - -- stateMu sync.Mutex -- state serverState -- // notifications generated before serverInitialized -- notifications []*protocol.ShowMessageParams -- -- session *cache.Session -- -- tempDir string -- -- // changedFiles tracks files for which there has been a textDocument/didChange. -- changedFilesMu sync.Mutex -- changedFiles map[span.URI]struct{} -- -- // folders is only valid between initialize and initialized, and holds the -- // set of folders to build views for when we are ready -- pendingFolders []protocol.WorkspaceFolder -- -- // watchedGlobPatterns is the set of glob patterns that we have requested -- // the client watch on disk. It will be updated as the set of directories -- // that the server should watch changes. -- // The map field may be reassigned but the map is immutable. -- watchedGlobPatternsMu sync.Mutex -- watchedGlobPatterns map[string]struct{} -- watchRegistrationCount int -- -- diagnosticsMu sync.Mutex -- diagnostics map[span.URI]*fileReports -- -- // gcOptimizationDetails describes the packages for which we want -- // optimization details to be included in the diagnostics. The key is the -- // ID of the package. -- gcOptimizationDetailsMu sync.Mutex -- gcOptimizationDetails map[source.PackageID]struct{} -- -- // diagnosticsSema limits the concurrency of diagnostics runs, which can be -- // expensive. -- diagnosticsSema chan struct{} -- -- progress *progress.Tracker -- -- // When the workspace fails to load, we show its status through a progress -- // report with an error message. -- criticalErrorStatusMu sync.Mutex -- criticalErrorStatus *progress.WorkDone -- -- // Track an ongoing CPU profile created with the StartProfile command and -- // terminated with the StopProfile command. -- ongoingProfileMu sync.Mutex -- ongoingProfile *os.File // if non-nil, an ongoing profile is writing to this file -- -- // Track most recently requested options. -- optionsMu sync.Mutex -- options *source.Options --} -- --func (s *Server) workDoneProgressCancel(ctx context.Context, params *protocol.WorkDoneProgressCancelParams) error { -- ctx, done := event.Start(ctx, "lsp.Server.workDoneProgressCancel") -- defer done() +-// Symbolic names for gopls commands, excluding "gopls." prefix. +-// These commands may be requested by ExecuteCommand, CodeLens, +-// CodeAction, and other LSP requests. +-const ( +- AddDependency Command = "add_dependency" +- AddImport Command = "add_import" +- AddTelemetryCounters Command = "add_telemetry_counters" +- ApplyFix Command = "apply_fix" +- ChangeSignature Command = "change_signature" +- CheckUpgrades Command = "check_upgrades" +- DiagnoseFiles Command = "diagnose_files" +- Doc Command = "doc" +- EditGoDirective Command = "edit_go_directive" +- FetchVulncheckResult Command = "fetch_vulncheck_result" +- GCDetails Command = "gc_details" +- Generate Command = "generate" +- GoGetPackage Command = "go_get_package" +- ListImports Command = "list_imports" +- ListKnownPackages Command = "list_known_packages" +- MaybePromptForTelemetry Command = "maybe_prompt_for_telemetry" +- MemStats Command = "mem_stats" +- RegenerateCgo Command = "regenerate_cgo" +- RemoveDependency Command = "remove_dependency" +- ResetGoModDiagnostics Command = "reset_go_mod_diagnostics" +- RunGoWorkCommand Command = "run_go_work_command" +- RunGovulncheck Command = "run_govulncheck" +- RunTests Command = "run_tests" +- StartDebugging Command = "start_debugging" +- StartProfile Command = "start_profile" +- StopProfile Command = "stop_profile" +- Test Command = "test" +- Tidy Command = "tidy" +- ToggleGCDetails Command = "toggle_gc_details" +- UpdateGoSum Command = "update_go_sum" +- UpgradeDependency Command = "upgrade_dependency" +- Vendor Command = "vendor" +- Views Command = "views" +- WorkspaceStats Command = "workspace_stats" +-) - -- return s.progress.Cancel(params.Token) +-var Commands = []Command{ +- AddDependency, +- AddImport, +- AddTelemetryCounters, +- ApplyFix, +- ChangeSignature, +- CheckUpgrades, +- DiagnoseFiles, +- Doc, +- EditGoDirective, +- FetchVulncheckResult, +- GCDetails, +- Generate, +- GoGetPackage, +- ListImports, +- ListKnownPackages, +- MaybePromptForTelemetry, +- MemStats, +- RegenerateCgo, +- RemoveDependency, +- ResetGoModDiagnostics, +- RunGoWorkCommand, +- RunGovulncheck, +- RunTests, +- StartDebugging, +- StartProfile, +- StopProfile, +- Test, +- Tidy, +- ToggleGCDetails, +- UpdateGoSum, +- UpgradeDependency, +- Vendor, +- Views, +- WorkspaceStats, -} - --func (s *Server) nonstandardRequest(ctx context.Context, method string, params interface{}) (interface{}, error) { -- ctx, done := event.Start(ctx, "lsp.Server.nonstandardRequest") -- defer done() -- -- switch method { -- case "gopls/diagnoseFiles": -- paramMap := params.(map[string]interface{}) -- // TODO(adonovan): opt: parallelize FileDiagnostics(URI...), either -- // by calling it in multiple goroutines or, better, by making -- // the relevant APIs accept a set of URIs/packages. -- for _, file := range paramMap["files"].([]interface{}) { -- snapshot, fh, ok, release, err := s.beginFileRequest(ctx, protocol.DocumentURI(file.(string)), source.UnknownKind) -- defer release() -- if !ok { -- return nil, err -- } -- -- fileID, diagnostics, err := s.diagnoseFile(ctx, snapshot, fh.URI()) -- if err != nil { -- return nil, err -- } -- if err := s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{ -- URI: protocol.URIFromSpanURI(fh.URI()), -- Diagnostics: toProtocolDiagnostics(diagnostics), -- Version: fileID.Version(), -- }); err != nil { -- return nil, err -- } +-func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Interface) (interface{}, error) { +- switch params.Command { +- case "gopls.add_dependency": +- var a0 DependencyArgs +- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { +- return nil, err - } -- if err := s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{ -- URI: "gopls://diagnostics-done", -- }); err != nil { +- return nil, s.AddDependency(ctx, a0) +- case "gopls.add_import": +- var a0 AddImportArgs +- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { +- return nil, err +- } +- return nil, s.AddImport(ctx, a0) +- case "gopls.add_telemetry_counters": +- var a0 AddTelemetryCountersArgs +- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { +- return nil, err +- } +- return nil, s.AddTelemetryCounters(ctx, a0) +- case "gopls.apply_fix": +- var a0 ApplyFixArgs +- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { +- return nil, err +- } +- return s.ApplyFix(ctx, a0) +- case "gopls.change_signature": +- var a0 ChangeSignatureArgs +- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { +- return nil, err +- } +- return s.ChangeSignature(ctx, a0) +- case "gopls.check_upgrades": +- var a0 CheckUpgradesArgs +- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { +- return nil, err +- } +- return nil, s.CheckUpgrades(ctx, a0) +- case "gopls.diagnose_files": +- var a0 DiagnoseFilesArgs +- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { +- return nil, err +- } +- return nil, s.DiagnoseFiles(ctx, a0) +- case "gopls.doc": +- var a0 protocol.Location +- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { +- return nil, err +- } +- return nil, s.Doc(ctx, a0) +- case "gopls.edit_go_directive": +- var a0 EditGoDirectiveArgs +- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { +- return nil, err +- } +- return nil, s.EditGoDirective(ctx, a0) +- case "gopls.fetch_vulncheck_result": +- var a0 URIArg +- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { +- return nil, err +- } +- return s.FetchVulncheckResult(ctx, a0) +- case "gopls.gc_details": +- var a0 protocol.DocumentURI +- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { +- return nil, err +- } +- return nil, s.GCDetails(ctx, a0) +- case "gopls.generate": +- var a0 GenerateArgs +- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { +- return nil, err +- } +- return nil, s.Generate(ctx, a0) +- case "gopls.go_get_package": +- var a0 GoGetPackageArgs +- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { +- return nil, err +- } +- return nil, s.GoGetPackage(ctx, a0) +- case "gopls.list_imports": +- var a0 URIArg +- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { +- return nil, err +- } +- return s.ListImports(ctx, a0) +- case "gopls.list_known_packages": +- var a0 URIArg +- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { +- return nil, err +- } +- return s.ListKnownPackages(ctx, a0) +- case "gopls.maybe_prompt_for_telemetry": +- return nil, s.MaybePromptForTelemetry(ctx) +- case "gopls.mem_stats": +- return s.MemStats(ctx) +- case "gopls.regenerate_cgo": +- var a0 URIArg +- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { +- return nil, err +- } +- return nil, s.RegenerateCgo(ctx, a0) +- case "gopls.remove_dependency": +- var a0 RemoveDependencyArgs +- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { +- return nil, err +- } +- return nil, s.RemoveDependency(ctx, a0) +- case "gopls.reset_go_mod_diagnostics": +- var a0 ResetGoModDiagnosticsArgs +- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { +- return nil, err +- } +- return nil, s.ResetGoModDiagnostics(ctx, a0) +- case "gopls.run_go_work_command": +- var a0 RunGoWorkArgs +- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { +- return nil, err +- } +- return nil, s.RunGoWorkCommand(ctx, a0) +- case "gopls.run_govulncheck": +- var a0 VulncheckArgs +- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { +- return nil, err +- } +- return s.RunGovulncheck(ctx, a0) +- case "gopls.run_tests": +- var a0 RunTestsArgs +- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { +- return nil, err +- } +- return nil, s.RunTests(ctx, a0) +- case "gopls.start_debugging": +- var a0 DebuggingArgs +- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { +- return nil, err +- } +- return s.StartDebugging(ctx, a0) +- case "gopls.start_profile": +- var a0 StartProfileArgs +- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { +- return nil, err +- } +- return s.StartProfile(ctx, a0) +- case "gopls.stop_profile": +- var a0 StopProfileArgs +- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { +- return nil, err +- } +- return s.StopProfile(ctx, a0) +- case "gopls.test": +- var a0 protocol.DocumentURI +- var a1 []string +- var a2 []string +- if err := UnmarshalArgs(params.Arguments, &a0, &a1, &a2); err != nil { +- return nil, err +- } +- return nil, s.Test(ctx, a0, a1, a2) +- case "gopls.tidy": +- var a0 URIArgs +- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { +- return nil, err +- } +- return nil, s.Tidy(ctx, a0) +- case "gopls.toggle_gc_details": +- var a0 URIArg +- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { +- return nil, err +- } +- return nil, s.ToggleGCDetails(ctx, a0) +- case "gopls.update_go_sum": +- var a0 URIArgs +- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { +- return nil, err +- } +- return nil, s.UpdateGoSum(ctx, a0) +- case "gopls.upgrade_dependency": +- var a0 DependencyArgs +- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { +- return nil, err +- } +- return nil, s.UpgradeDependency(ctx, a0) +- case "gopls.vendor": +- var a0 URIArg +- if err := UnmarshalArgs(params.Arguments, &a0); err != nil { - return nil, err - } -- return struct{}{}, nil +- return nil, s.Vendor(ctx, a0) +- case "gopls.views": +- return s.Views(ctx) +- case "gopls.workspace_stats": +- return s.WorkspaceStats(ctx) - } -- return nil, notImplemented(method) +- return nil, fmt.Errorf("unsupported command %q", params.Command) -} - --// fileDiagnostics reports diagnostics in the specified file, --// as used by the "gopls check" or "gopls fix" commands. --// --// TODO(adonovan): opt: this function is called in a loop from the --// "gopls/diagnoseFiles" nonstandard request handler. It would be more --// efficient to compute the set of packages and TypeCheck and --// Analyze them all at once. Or instead support textDocument/diagnostic --// (golang/go#60122). --func (s *Server) diagnoseFile(ctx context.Context, snapshot source.Snapshot, uri span.URI) (source.FileHandle, []*source.Diagnostic, error) { -- fh, err := snapshot.ReadFile(ctx, uri) +-func NewAddDependencyCommand(title string, a0 DependencyArgs) (protocol.Command, error) { +- args, err := MarshalArgs(a0) - if err != nil { -- return nil, nil, err +- return protocol.Command{}, err - } -- pkg, _, err := source.NarrowestPackageForFile(ctx, snapshot, uri) +- return protocol.Command{ +- Title: title, +- Command: "gopls.add_dependency", +- Arguments: args, +- }, nil +-} +- +-func NewAddImportCommand(title string, a0 AddImportArgs) (protocol.Command, error) { +- args, err := MarshalArgs(a0) - if err != nil { -- return nil, nil, err +- return protocol.Command{}, err - } -- pkgDiags, err := pkg.DiagnosticsForFile(ctx, snapshot, uri) +- return protocol.Command{ +- Title: title, +- Command: "gopls.add_import", +- Arguments: args, +- }, nil +-} +- +-func NewAddTelemetryCountersCommand(title string, a0 AddTelemetryCountersArgs) (protocol.Command, error) { +- args, err := MarshalArgs(a0) - if err != nil { -- return nil, nil, err +- return protocol.Command{}, err - } -- adiags, err := source.Analyze(ctx, snapshot, map[source.PackageID]unit{pkg.Metadata().ID: {}}, nil /* progress tracker */) +- return protocol.Command{ +- Title: title, +- Command: "gopls.add_telemetry_counters", +- Arguments: args, +- }, nil +-} +- +-func NewApplyFixCommand(title string, a0 ApplyFixArgs) (protocol.Command, error) { +- args, err := MarshalArgs(a0) - if err != nil { -- return nil, nil, err +- return protocol.Command{}, err - } -- var td, ad []*source.Diagnostic // combine load/parse/type + analysis diagnostics -- source.CombineDiagnostics(pkgDiags, adiags[uri], &td, &ad) -- s.storeDiagnostics(snapshot, uri, typeCheckSource, td, true) -- s.storeDiagnostics(snapshot, uri, analysisSource, ad, true) -- return fh, append(td, ad...), nil +- return protocol.Command{ +- Title: title, +- Command: "gopls.apply_fix", +- Arguments: args, +- }, nil -} - --func notImplemented(method string) error { -- return fmt.Errorf("%w: %q not yet implemented", jsonrpc2.ErrMethodNotFound, method) +-func NewChangeSignatureCommand(title string, a0 ChangeSignatureArgs) (protocol.Command, error) { +- args, err := MarshalArgs(a0) +- if err != nil { +- return protocol.Command{}, err +- } +- return protocol.Command{ +- Title: title, +- Command: "gopls.change_signature", +- Arguments: args, +- }, nil -} -diff -urN a/gopls/internal/lsp/signature_help.go b/gopls/internal/lsp/signature_help.go ---- a/gopls/internal/lsp/signature_help.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/signature_help.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,34 +0,0 @@ --// Copyright 2018 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package lsp -- --import ( -- "context" -- -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/tag" --) -- --func (s *Server) signatureHelp(ctx context.Context, params *protocol.SignatureHelpParams) (*protocol.SignatureHelp, error) { -- ctx, done := event.Start(ctx, "lsp.Server.signatureHelp", tag.URI.Of(params.TextDocument.URI)) -- defer done() - -- snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.Go) -- defer release() -- if !ok { -- return nil, err -- } -- info, activeParameter, err := source.SignatureHelp(ctx, snapshot, fh, params.Position) +-func NewCheckUpgradesCommand(title string, a0 CheckUpgradesArgs) (protocol.Command, error) { +- args, err := MarshalArgs(a0) - if err != nil { -- event.Error(ctx, "no signature help", err, tag.Position.Of(params.Position)) -- return nil, nil // sic? There could be many reasons for failure. +- return protocol.Command{}, err - } -- return &protocol.SignatureHelp{ -- Signatures: []protocol.SignatureInformation{*info}, -- ActiveParameter: uint32(activeParameter), +- return protocol.Command{ +- Title: title, +- Command: "gopls.check_upgrades", +- Arguments: args, - }, nil -} -diff -urN a/gopls/internal/lsp/snippet/snippet_builder.go b/gopls/internal/lsp/snippet/snippet_builder.go ---- a/gopls/internal/lsp/snippet/snippet_builder.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/snippet/snippet_builder.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,111 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --// Package snippet implements the specification for the LSP snippet format. --// --// Snippets are "tab stop" templates returned as an optional attribute of LSP --// completion candidates. As the user presses tab, they cycle through a series of --// tab stops defined in the snippet. Each tab stop can optionally have placeholder --// text, which can be pre-selected by editors. For a full description of syntax --// and features, see "Snippet Syntax" at --// https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_completion. --// --// A typical snippet looks like "foo(${1:i int}, ${2:s string})". --package snippet - --import ( -- "fmt" -- "strings" --) -- --// A Builder is used to build an LSP snippet piecemeal. --// The zero value is ready to use. Do not copy a non-zero Builder. --type Builder struct { -- // currentTabStop is the index of the previous tab stop. The -- // next tab stop will be currentTabStop+1. -- currentTabStop int -- sb strings.Builder +-func NewDiagnoseFilesCommand(title string, a0 DiagnoseFilesArgs) (protocol.Command, error) { +- args, err := MarshalArgs(a0) +- if err != nil { +- return protocol.Command{}, err +- } +- return protocol.Command{ +- Title: title, +- Command: "gopls.diagnose_files", +- Arguments: args, +- }, nil -} - --// Escape characters defined in https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_completion under "Grammar". --var replacer = strings.NewReplacer( -- `\`, `\\`, -- `}`, `\}`, -- `$`, `\$`, --) -- --func (b *Builder) WriteText(s string) { -- replacer.WriteString(&b.sb, s) +-func NewDocCommand(title string, a0 protocol.Location) (protocol.Command, error) { +- args, err := MarshalArgs(a0) +- if err != nil { +- return protocol.Command{}, err +- } +- return protocol.Command{ +- Title: title, +- Command: "gopls.doc", +- Arguments: args, +- }, nil -} - --func (b *Builder) PrependText(s string) { -- rawSnip := b.String() -- b.sb.Reset() -- b.WriteText(s) -- b.sb.WriteString(rawSnip) +-func NewEditGoDirectiveCommand(title string, a0 EditGoDirectiveArgs) (protocol.Command, error) { +- args, err := MarshalArgs(a0) +- if err != nil { +- return protocol.Command{}, err +- } +- return protocol.Command{ +- Title: title, +- Command: "gopls.edit_go_directive", +- Arguments: args, +- }, nil -} - --func (b *Builder) Write(data []byte) (int, error) { -- return b.sb.Write(data) +-func NewFetchVulncheckResultCommand(title string, a0 URIArg) (protocol.Command, error) { +- args, err := MarshalArgs(a0) +- if err != nil { +- return protocol.Command{}, err +- } +- return protocol.Command{ +- Title: title, +- Command: "gopls.fetch_vulncheck_result", +- Arguments: args, +- }, nil -} - --// WritePlaceholder writes a tab stop and placeholder value to the Builder. --// The callback style allows for creating nested placeholders. To write an --// empty tab stop, provide a nil callback. --func (b *Builder) WritePlaceholder(fn func(*Builder)) { -- fmt.Fprintf(&b.sb, "${%d:", b.nextTabStop()) -- if fn != nil { -- fn(b) +-func NewGCDetailsCommand(title string, a0 protocol.DocumentURI) (protocol.Command, error) { +- args, err := MarshalArgs(a0) +- if err != nil { +- return protocol.Command{}, err - } -- b.sb.WriteByte('}') +- return protocol.Command{ +- Title: title, +- Command: "gopls.gc_details", +- Arguments: args, +- }, nil -} - --// WriteFinalTabstop marks where cursor ends up after the user has --// cycled through all the normal tab stops. It defaults to the --// character after the snippet. --func (b *Builder) WriteFinalTabstop() { -- fmt.Fprint(&b.sb, "$0") +-func NewGenerateCommand(title string, a0 GenerateArgs) (protocol.Command, error) { +- args, err := MarshalArgs(a0) +- if err != nil { +- return protocol.Command{}, err +- } +- return protocol.Command{ +- Title: title, +- Command: "gopls.generate", +- Arguments: args, +- }, nil -} - --// In addition to '\', '}', and '$', snippet choices also use '|' and ',' as --// meta characters, so they must be escaped within the choices. --var choiceReplacer = strings.NewReplacer( -- `\`, `\\`, -- `}`, `\}`, -- `$`, `\$`, -- `|`, `\|`, -- `,`, `\,`, --) +-func NewGoGetPackageCommand(title string, a0 GoGetPackageArgs) (protocol.Command, error) { +- args, err := MarshalArgs(a0) +- if err != nil { +- return protocol.Command{}, err +- } +- return protocol.Command{ +- Title: title, +- Command: "gopls.go_get_package", +- Arguments: args, +- }, nil +-} - --// WriteChoice writes a tab stop and list of text choices to the Builder. --// The user's editor will prompt the user to choose one of the choices. --func (b *Builder) WriteChoice(choices []string) { -- fmt.Fprintf(&b.sb, "${%d|", b.nextTabStop()) -- for i, c := range choices { -- if i != 0 { -- b.sb.WriteByte(',') -- } -- choiceReplacer.WriteString(&b.sb, c) +-func NewListImportsCommand(title string, a0 URIArg) (protocol.Command, error) { +- args, err := MarshalArgs(a0) +- if err != nil { +- return protocol.Command{}, err - } -- b.sb.WriteString("|}") +- return protocol.Command{ +- Title: title, +- Command: "gopls.list_imports", +- Arguments: args, +- }, nil -} - --// String returns the built snippet string. --func (b *Builder) String() string { -- return b.sb.String() +-func NewListKnownPackagesCommand(title string, a0 URIArg) (protocol.Command, error) { +- args, err := MarshalArgs(a0) +- if err != nil { +- return protocol.Command{}, err +- } +- return protocol.Command{ +- Title: title, +- Command: "gopls.list_known_packages", +- Arguments: args, +- }, nil -} - --// Clone returns a copy of b. --func (b *Builder) Clone() *Builder { -- var clone Builder -- clone.sb.WriteString(b.String()) -- return &clone +-func NewMaybePromptForTelemetryCommand(title string) (protocol.Command, error) { +- args, err := MarshalArgs() +- if err != nil { +- return protocol.Command{}, err +- } +- return protocol.Command{ +- Title: title, +- Command: "gopls.maybe_prompt_for_telemetry", +- Arguments: args, +- }, nil -} - --// nextTabStop returns the next tab stop index for a new placeholder. --func (b *Builder) nextTabStop() int { -- // Tab stops start from 1, so increment before returning. -- b.currentTabStop++ -- return b.currentTabStop +-func NewMemStatsCommand(title string) (protocol.Command, error) { +- args, err := MarshalArgs() +- if err != nil { +- return protocol.Command{}, err +- } +- return protocol.Command{ +- Title: title, +- Command: "gopls.mem_stats", +- Arguments: args, +- }, nil -} -diff -urN a/gopls/internal/lsp/snippet/snippet_builder_test.go b/gopls/internal/lsp/snippet/snippet_builder_test.go ---- a/gopls/internal/lsp/snippet/snippet_builder_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/snippet/snippet_builder_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,62 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package snippet +-func NewRegenerateCgoCommand(title string, a0 URIArg) (protocol.Command, error) { +- args, err := MarshalArgs(a0) +- if err != nil { +- return protocol.Command{}, err +- } +- return protocol.Command{ +- Title: title, +- Command: "gopls.regenerate_cgo", +- Arguments: args, +- }, nil +-} - --import ( -- "testing" --) +-func NewRemoveDependencyCommand(title string, a0 RemoveDependencyArgs) (protocol.Command, error) { +- args, err := MarshalArgs(a0) +- if err != nil { +- return protocol.Command{}, err +- } +- return protocol.Command{ +- Title: title, +- Command: "gopls.remove_dependency", +- Arguments: args, +- }, nil +-} - --func TestSnippetBuilder(t *testing.T) { -- expect := func(expected string, fn func(*Builder)) { -- t.Helper() +-func NewResetGoModDiagnosticsCommand(title string, a0 ResetGoModDiagnosticsArgs) (protocol.Command, error) { +- args, err := MarshalArgs(a0) +- if err != nil { +- return protocol.Command{}, err +- } +- return protocol.Command{ +- Title: title, +- Command: "gopls.reset_go_mod_diagnostics", +- Arguments: args, +- }, nil +-} - -- var b Builder -- fn(&b) -- if got := b.String(); got != expected { -- t.Errorf("got %q, expected %q", got, expected) -- } +-func NewRunGoWorkCommandCommand(title string, a0 RunGoWorkArgs) (protocol.Command, error) { +- args, err := MarshalArgs(a0) +- if err != nil { +- return protocol.Command{}, err - } +- return protocol.Command{ +- Title: title, +- Command: "gopls.run_go_work_command", +- Arguments: args, +- }, nil +-} - -- expect("", func(b *Builder) {}) +-func NewRunGovulncheckCommand(title string, a0 VulncheckArgs) (protocol.Command, error) { +- args, err := MarshalArgs(a0) +- if err != nil { +- return protocol.Command{}, err +- } +- return protocol.Command{ +- Title: title, +- Command: "gopls.run_govulncheck", +- Arguments: args, +- }, nil +-} - -- expect(`hi { \} \$ | " , / \\`, func(b *Builder) { -- b.WriteText(`hi { } $ | " , / \`) -- }) +-func NewRunTestsCommand(title string, a0 RunTestsArgs) (protocol.Command, error) { +- args, err := MarshalArgs(a0) +- if err != nil { +- return protocol.Command{}, err +- } +- return protocol.Command{ +- Title: title, +- Command: "gopls.run_tests", +- Arguments: args, +- }, nil +-} - -- expect("${1:}", func(b *Builder) { -- b.WritePlaceholder(nil) -- }) +-func NewStartDebuggingCommand(title string, a0 DebuggingArgs) (protocol.Command, error) { +- args, err := MarshalArgs(a0) +- if err != nil { +- return protocol.Command{}, err +- } +- return protocol.Command{ +- Title: title, +- Command: "gopls.start_debugging", +- Arguments: args, +- }, nil +-} - -- expect("hi ${1:there}", func(b *Builder) { -- b.WriteText("hi ") -- b.WritePlaceholder(func(b *Builder) { -- b.WriteText("there") -- }) -- }) +-func NewStartProfileCommand(title string, a0 StartProfileArgs) (protocol.Command, error) { +- args, err := MarshalArgs(a0) +- if err != nil { +- return protocol.Command{}, err +- } +- return protocol.Command{ +- Title: title, +- Command: "gopls.start_profile", +- Arguments: args, +- }, nil +-} - -- expect(`${1:id=${2:{your id\}}}`, func(b *Builder) { -- b.WritePlaceholder(func(b *Builder) { -- b.WriteText("id=") -- b.WritePlaceholder(func(b *Builder) { -- b.WriteText("{your id}") -- }) -- }) -- }) +-func NewStopProfileCommand(title string, a0 StopProfileArgs) (protocol.Command, error) { +- args, err := MarshalArgs(a0) +- if err != nil { +- return protocol.Command{}, err +- } +- return protocol.Command{ +- Title: title, +- Command: "gopls.stop_profile", +- Arguments: args, +- }, nil +-} - -- expect(`${1|one,{ \} \$ \| " \, / \\,three|}`, func(b *Builder) { -- b.WriteChoice([]string{"one", `{ } $ | " , / \`, "three"}) -- }) +-func NewTestCommand(title string, a0 protocol.DocumentURI, a1 []string, a2 []string) (protocol.Command, error) { +- args, err := MarshalArgs(a0, a1, a2) +- if err != nil { +- return protocol.Command{}, err +- } +- return protocol.Command{ +- Title: title, +- Command: "gopls.test", +- Arguments: args, +- }, nil +-} - -- expect("$0 hello", func(b *Builder) { -- b.WriteFinalTabstop() -- b.WriteText(" hello") -- }) +-func NewTidyCommand(title string, a0 URIArgs) (protocol.Command, error) { +- args, err := MarshalArgs(a0) +- if err != nil { +- return protocol.Command{}, err +- } +- return protocol.Command{ +- Title: title, +- Command: "gopls.tidy", +- Arguments: args, +- }, nil +-} - -- expect(`prepended \$5 ${1:} hello`, func(b *Builder) { -- b.WritePlaceholder(nil) -- b.WriteText(" hello") -- b.PrependText("prepended $5 ") -- }) +-func NewToggleGCDetailsCommand(title string, a0 URIArg) (protocol.Command, error) { +- args, err := MarshalArgs(a0) +- if err != nil { +- return protocol.Command{}, err +- } +- return protocol.Command{ +- Title: title, +- Command: "gopls.toggle_gc_details", +- Arguments: args, +- }, nil -} -diff -urN a/gopls/internal/lsp/source/add_import.go b/gopls/internal/lsp/source/add_import.go ---- a/gopls/internal/lsp/source/add_import.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/add_import.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,26 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +- +-func NewUpdateGoSumCommand(title string, a0 URIArgs) (protocol.Command, error) { +- args, err := MarshalArgs(a0) +- if err != nil { +- return protocol.Command{}, err +- } +- return protocol.Command{ +- Title: title, +- Command: "gopls.update_go_sum", +- Arguments: args, +- }, nil +-} +- +-func NewUpgradeDependencyCommand(title string, a0 DependencyArgs) (protocol.Command, error) { +- args, err := MarshalArgs(a0) +- if err != nil { +- return protocol.Command{}, err +- } +- return protocol.Command{ +- Title: title, +- Command: "gopls.upgrade_dependency", +- Arguments: args, +- }, nil +-} +- +-func NewVendorCommand(title string, a0 URIArg) (protocol.Command, error) { +- args, err := MarshalArgs(a0) +- if err != nil { +- return protocol.Command{}, err +- } +- return protocol.Command{ +- Title: title, +- Command: "gopls.vendor", +- Arguments: args, +- }, nil +-} +- +-func NewViewsCommand(title string) (protocol.Command, error) { +- args, err := MarshalArgs() +- if err != nil { +- return protocol.Command{}, err +- } +- return protocol.Command{ +- Title: title, +- Command: "gopls.views", +- Arguments: args, +- }, nil +-} +- +-func NewWorkspaceStatsCommand(title string) (protocol.Command, error) { +- args, err := MarshalArgs() +- if err != nil { +- return protocol.Command{}, err +- } +- return protocol.Command{ +- Title: title, +- Command: "gopls.workspace_stats", +- Arguments: args, +- }, nil +-} +diff -urN a/gopls/internal/protocol/command/commandmeta/meta.go b/gopls/internal/protocol/command/commandmeta/meta.go +--- a/gopls/internal/protocol/command/commandmeta/meta.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/command/commandmeta/meta.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,260 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package source +-// Package commandmeta provides metadata about LSP commands, by analyzing the +-// command.Interface type. +-package commandmeta - -import ( -- "context" +- "fmt" +- "go/ast" +- "go/token" +- "go/types" +- "reflect" +- "strings" +- "unicode" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/internal/imports" +- "golang.org/x/tools/go/ast/astutil" +- "golang.org/x/tools/go/packages" +- "golang.org/x/tools/gopls/internal/protocol/command" +- "golang.org/x/tools/internal/aliases" -) - --// AddImport adds a single import statement to the given file --func AddImport(ctx context.Context, snapshot Snapshot, fh FileHandle, importPath string) ([]protocol.TextEdit, error) { -- pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) +-type Command struct { +- MethodName string +- Name string +- // TODO(rFindley): I think Title can actually be eliminated. In all cases +- // where we use it, there is probably a more appropriate contextual title. +- Title string +- Doc string +- Args []*Field +- Result *Field +-} +- +-func (c *Command) ID() string { +- return command.ID(c.Name) +-} +- +-type Field struct { +- Name string +- Doc string +- JSONTag string +- Type types.Type +- FieldMod string +- // In some circumstances, we may want to recursively load additional field +- // descriptors for fields of struct types, documenting their internals. +- Fields []*Field +-} +- +-func Load() (*packages.Package, []*Command, error) { +- pkgs, err := packages.Load( +- &packages.Config{ +- Mode: packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedImports | packages.NeedDeps, +- BuildFlags: []string{"-tags=generate"}, +- }, +- "golang.org/x/tools/gopls/internal/protocol/command", +- ) +- if err != nil { +- return nil, nil, fmt.Errorf("packages.Load: %v", err) +- } +- pkg := pkgs[0] +- if len(pkg.Errors) > 0 { +- return pkg, nil, pkg.Errors[0] +- } +- +- // For a bit of type safety, use reflection to get the interface name within +- // the package scope. +- it := reflect.TypeOf((*command.Interface)(nil)).Elem() +- obj := pkg.Types.Scope().Lookup(it.Name()).Type().Underlying().(*types.Interface) +- +- // Load command metadata corresponding to each interface method. +- var commands []*Command +- loader := fieldLoader{make(map[types.Object]*Field)} +- for i := 0; i < obj.NumMethods(); i++ { +- m := obj.Method(i) +- c, err := loader.loadMethod(pkg, m) +- if err != nil { +- return nil, nil, fmt.Errorf("loading %s: %v", m.Name(), err) +- } +- commands = append(commands, c) +- } +- return pkg, commands, nil +-} +- +-// fieldLoader loads field information, memoizing results to prevent infinite +-// recursion. +-type fieldLoader struct { +- loaded map[types.Object]*Field +-} +- +-var universeError = types.Universe.Lookup("error").Type() +- +-func (l *fieldLoader) loadMethod(pkg *packages.Package, m *types.Func) (*Command, error) { +- node, err := findField(pkg, m.Pos()) - if err != nil { - return nil, err - } -- return ComputeOneImportFixEdits(snapshot, pgf, &imports.ImportFix{ -- StmtInfo: imports.ImportInfo{ -- ImportPath: importPath, -- }, -- FixType: imports.AddImport, -- }) +- title, doc := splitDoc(node.Doc.Text()) +- c := &Command{ +- MethodName: m.Name(), +- Name: lspName(m.Name()), +- Doc: doc, +- Title: title, +- } +- sig := m.Type().Underlying().(*types.Signature) +- rlen := sig.Results().Len() +- if rlen > 2 || rlen == 0 { +- return nil, fmt.Errorf("must have 1 or 2 returns, got %d", rlen) +- } +- finalResult := sig.Results().At(rlen - 1) +- if !types.Identical(finalResult.Type(), universeError) { +- return nil, fmt.Errorf("final return must be error") +- } +- if rlen == 2 { +- obj := sig.Results().At(0) +- c.Result, err = l.loadField(pkg, obj, "", "") +- if err != nil { +- return nil, err +- } +- } +- for i := 0; i < sig.Params().Len(); i++ { +- obj := sig.Params().At(i) +- fld, err := l.loadField(pkg, obj, "", "") +- if err != nil { +- return nil, err +- } +- if i == 0 { +- // Lazy check that the first argument is a context. We could relax this, +- // but then the generated code gets more complicated. +- if named, ok := aliases.Unalias(fld.Type).(*types.Named); !ok || named.Obj().Name() != "Context" || named.Obj().Pkg().Path() != "context" { +- return nil, fmt.Errorf("first method parameter must be context.Context") +- } +- // Skip the context argument, as it is implied. +- continue +- } +- c.Args = append(c.Args, fld) +- } +- return c, nil -} -diff -urN a/gopls/internal/lsp/source/api_json.go b/gopls/internal/lsp/source/api_json.go ---- a/gopls/internal/lsp/source/api_json.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/api_json.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,1288 +0,0 @@ --// Code generated by "golang.org/x/tools/gopls/doc/generate"; DO NOT EDIT. - --package source +-func (l *fieldLoader) loadField(pkg *packages.Package, obj *types.Var, doc, tag string) (*Field, error) { +- if existing, ok := l.loaded[obj]; ok { +- return existing, nil +- } +- fld := &Field{ +- Name: obj.Name(), +- Doc: strings.TrimSpace(doc), +- Type: obj.Type(), +- JSONTag: reflect.StructTag(tag).Get("json"), +- } +- under := fld.Type.Underlying() +- // Quick-and-dirty handling for various underlying types. +- switch p := under.(type) { +- case *types.Pointer: +- under = p.Elem().Underlying() +- case *types.Array: +- under = p.Elem().Underlying() +- fld.FieldMod = fmt.Sprintf("[%d]", p.Len()) +- case *types.Slice: +- under = p.Elem().Underlying() +- fld.FieldMod = "[]" +- } - --var GeneratedAPIJSON = &APIJSON{ -- Options: map[string][]*OptionJSON{ -- "User": { -- { -- Name: "buildFlags", -- Type: "[]string", -- Doc: "buildFlags is the set of flags passed on to the build system when invoked.\nIt is applied to queries like `go list`, which is used when discovering files.\nThe most common use is to set `-tags`.\n", -- Default: "[]", -- Hierarchy: "build", -- }, -- { -- Name: "env", -- Type: "map[string]string", -- Doc: "env adds environment variables to external commands run by `gopls`, most notably `go list`.\n", -- Default: "{}", -- Hierarchy: "build", -- }, -- { -- Name: "directoryFilters", -- Type: "[]string", -- Doc: "directoryFilters can be used to exclude unwanted directories from the\nworkspace. By default, all directories are included. Filters are an\noperator, `+` to include and `-` to exclude, followed by a path prefix\nrelative to the workspace folder. They are evaluated in order, and\nthe last filter that applies to a path controls whether it is included.\nThe path prefix can be empty, so an initial `-` excludes everything.\n\nDirectoryFilters also supports the `**` operator to match 0 or more directories.\n\nExamples:\n\nExclude node_modules at current depth: `-node_modules`\n\nExclude node_modules at any depth: `-**/node_modules`\n\nInclude only project_a: `-` (exclude everything), `+project_a`\n\nInclude only project_a, but not node_modules inside it: `-`, `+project_a`, `-project_a/node_modules`\n", -- Default: "[\"-**/node_modules\"]", -- Hierarchy: "build", -- }, -- { -- Name: "templateExtensions", -- Type: "[]string", -- Doc: "templateExtensions gives the extensions of file names that are treateed\nas template files. (The extension\nis the part of the file name after the final dot.)\n", -- Default: "[]", -- Hierarchy: "build", -- }, -- { -- Name: "memoryMode", -- Type: "enum", -- Doc: "memoryMode controls the tradeoff `gopls` makes between memory usage and\ncorrectness.\n\nValues other than `Normal` are untested and may break in surprising ways.\n", -- EnumValues: []EnumValue{ -- { -- Value: "\"DegradeClosed\"", -- Doc: "`\"DegradeClosed\"`: In DegradeClosed mode, `gopls` will collect less information about\npackages without open files. As a result, features like Find\nReferences and Rename will miss results in such packages.\n", -- }, -- {Value: "\"Normal\""}, -- }, -- Default: "\"Normal\"", -- Status: "experimental", -- Hierarchy: "build", -- }, -- { -- Name: "expandWorkspaceToModule", -- Type: "bool", -- Doc: "expandWorkspaceToModule instructs `gopls` to adjust the scope of the\nworkspace to find the best available module root. `gopls` first looks for\na go.mod file in any parent directory of the workspace folder, expanding\nthe scope to that directory if it exists. If no viable parent directory is\nfound, gopls will check if there is exactly one child directory containing\na go.mod file, narrowing the scope to that directory if it exists.\n", -- Default: "true", -- Status: "experimental", -- Hierarchy: "build", -- }, -- { -- Name: "allowModfileModifications", -- Type: "bool", -- Doc: "allowModfileModifications disables -mod=readonly, allowing imports from\nout-of-scope modules. This option will eventually be removed.\n", -- Default: "false", -- Status: "experimental", -- Hierarchy: "build", -- }, -- { -- Name: "allowImplicitNetworkAccess", -- Type: "bool", -- Doc: "allowImplicitNetworkAccess disables GOPROXY=off, allowing implicit module\ndownloads rather than requiring user action. This option will eventually\nbe removed.\n", -- Default: "false", -- Status: "experimental", -- Hierarchy: "build", -- }, -- { -- Name: "standaloneTags", -- Type: "[]string", -- Doc: "standaloneTags specifies a set of build constraints that identify\nindividual Go source files that make up the entire main package of an\nexecutable.\n\nA common example of standalone main files is the convention of using the\ndirective `//go:build ignore` to denote files that are not intended to be\nincluded in any package, for example because they are invoked directly by\nthe developer using `go run`.\n\nGopls considers a file to be a standalone main file if and only if it has\npackage name \"main\" and has a build directive of the exact form\n\"//go:build tag\" or \"// +build tag\", where tag is among the list of tags\nconfigured by this setting. Notably, if the build constraint is more\ncomplicated than a simple tag (such as the composite constraint\n`//go:build tag && go1.18`), the file is not considered to be a standalone\nmain file.\n\nThis setting is only supported when gopls is built with Go 1.16 or later.\n", -- Default: "[\"ignore\"]", -- Hierarchy: "build", -- }, -- { -- Name: "hoverKind", -- Type: "enum", -- Doc: "hoverKind controls the information that appears in the hover text.\nSingleLine and Structured are intended for use only by authors of editor plugins.\n", -- EnumValues: []EnumValue{ -- {Value: "\"FullDocumentation\""}, -- {Value: "\"NoDocumentation\""}, -- {Value: "\"SingleLine\""}, -- { -- Value: "\"Structured\"", -- Doc: "`\"Structured\"` is an experimental setting that returns a structured hover format.\nThis format separates the signature from the documentation, so that the client\ncan do more manipulation of these fields.\n\nThis should only be used by clients that support this behavior.\n", -- }, -- {Value: "\"SynopsisDocumentation\""}, -- }, -- Default: "\"FullDocumentation\"", -- Hierarchy: "ui.documentation", -- }, -- { -- Name: "linkTarget", -- Type: "string", -- Doc: "linkTarget controls where documentation links go.\nIt might be one of:\n\n* `\"godoc.org\"`\n* `\"pkg.go.dev\"`\n\nIf company chooses to use its own `godoc.org`, its address can be used as well.\n\nModules matching the GOPRIVATE environment variable will not have\ndocumentation links in hover.\n", -- Default: "\"pkg.go.dev\"", -- Hierarchy: "ui.documentation", -- }, -- { -- Name: "linksInHover", -- Type: "bool", -- Doc: "linksInHover toggles the presence of links to documentation in hover.\n", -- Default: "true", -- Hierarchy: "ui.documentation", -- }, -- { -- Name: "usePlaceholders", -- Type: "bool", -- Doc: "placeholders enables placeholders for function parameters or struct\nfields in completion responses.\n", -- Default: "false", -- Hierarchy: "ui.completion", -- }, -- { -- Name: "completionBudget", -- Type: "time.Duration", -- Doc: "completionBudget is the soft latency goal for completion requests. Most\nrequests finish in a couple milliseconds, but in some cases deep\ncompletions can take much longer. As we use up our budget we\ndynamically reduce the search scope to ensure we return timely\nresults. Zero means unlimited.\n", -- Default: "\"100ms\"", -- Status: "debug", -- Hierarchy: "ui.completion", -- }, -- { -- Name: "matcher", -- Type: "enum", -- Doc: "matcher sets the algorithm that is used when calculating completion\ncandidates.\n", -- EnumValues: []EnumValue{ -- {Value: "\"CaseInsensitive\""}, -- {Value: "\"CaseSensitive\""}, -- {Value: "\"Fuzzy\""}, -- }, -- Default: "\"Fuzzy\"", -- Status: "advanced", -- Hierarchy: "ui.completion", -- }, -- { -- Name: "experimentalPostfixCompletions", -- Type: "bool", -- Doc: "experimentalPostfixCompletions enables artificial method snippets\nsuch as \"someSlice.sort!\".\n", -- Default: "true", -- Status: "experimental", -- Hierarchy: "ui.completion", -- }, -- { -- Name: "completeFunctionCalls", -- Type: "bool", -- Doc: "completeFunctionCalls enables function call completion.\n\nWhen completing a statement, or when a function return type matches the\nexpected of the expression being completed, completion may suggest call\nexpressions (i.e. may include parentheses).\n", -- Default: "true", -- Hierarchy: "ui.completion", -- }, -- { -- Name: "importShortcut", -- Type: "enum", -- Doc: "importShortcut specifies whether import statements should link to\ndocumentation or go to definitions.\n", -- EnumValues: []EnumValue{ -- {Value: "\"Both\""}, -- {Value: "\"Definition\""}, -- {Value: "\"Link\""}, -- }, -- Default: "\"Both\"", -- Hierarchy: "ui.navigation", -- }, -- { -- Name: "symbolMatcher", -- Type: "enum", -- Doc: "symbolMatcher sets the algorithm that is used when finding workspace symbols.\n", -- EnumValues: []EnumValue{ -- {Value: "\"CaseInsensitive\""}, -- {Value: "\"CaseSensitive\""}, -- {Value: "\"FastFuzzy\""}, -- {Value: "\"Fuzzy\""}, -- }, -- Default: "\"FastFuzzy\"", -- Status: "advanced", -- Hierarchy: "ui.navigation", -- }, -- { -- Name: "symbolStyle", -- Type: "enum", -- Doc: "symbolStyle controls how symbols are qualified in symbol responses.\n\nExample Usage:\n\n```json5\n\"gopls\": {\n...\n \"symbolStyle\": \"Dynamic\",\n...\n}\n```\n", -- EnumValues: []EnumValue{ -- { -- Value: "\"Dynamic\"", -- Doc: "`\"Dynamic\"` uses whichever qualifier results in the highest scoring\nmatch for the given symbol query. Here a \"qualifier\" is any \"/\" or \".\"\ndelimited suffix of the fully qualified symbol. i.e. \"to/pkg.Foo.Field\" or\njust \"Foo.Field\".\n", -- }, -- { -- Value: "\"Full\"", -- Doc: "`\"Full\"` is fully qualified symbols, i.e.\n\"path/to/pkg.Foo.Field\".\n", -- }, -- { -- Value: "\"Package\"", -- Doc: "`\"Package\"` is package qualified symbols i.e.\n\"pkg.Foo.Field\".\n", -- }, -- }, -- Default: "\"Dynamic\"", -- Status: "advanced", -- Hierarchy: "ui.navigation", -- }, -- { -- Name: "symbolScope", -- Type: "enum", -- Doc: "symbolScope controls which packages are searched for workspace/symbol\nrequests. The default value, \"workspace\", searches only workspace\npackages. The legacy behavior, \"all\", causes all loaded packages to be\nsearched, including dependencies; this is more expensive and may return\nunwanted results.\n", -- EnumValues: []EnumValue{ -- { -- Value: "\"all\"", -- Doc: "`\"all\"` matches symbols in any loaded package, including\ndependencies.\n", -- }, -- { -- Value: "\"workspace\"", -- Doc: "`\"workspace\"` matches symbols in workspace packages only.\n", -- }, -- }, -- Default: "\"all\"", -- Hierarchy: "ui.navigation", -- }, -- { -- Name: "analyses", -- Type: "map[string]bool", -- Doc: "analyses specify analyses that the user would like to enable or disable.\nA map of the names of analysis passes that should be enabled/disabled.\nA full list of analyzers that gopls uses can be found in\n[analyzers.md](https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md).\n\nExample Usage:\n\n```json5\n...\n\"analyses\": {\n \"unreachable\": false, // Disable the unreachable analyzer.\n \"unusedparams\": true // Enable the unusedparams analyzer.\n}\n...\n```\n", -- EnumKeys: EnumKeys{ -- ValueType: "bool", -- Keys: []EnumKey{ -- { -- Name: "\"appends\"", -- Doc: "check for missing values after append\n\nThis checker reports calls to append that pass\nno values to be appended to the slice.\n\n\ts := []string{\"a\", \"b\", \"c\"}\n\t_ = append(s)\n\nSuch calls are always no-ops and often indicate an\nunderlying mistake.", -- Default: "true", -- }, -- { -- Name: "\"asmdecl\"", -- Doc: "report mismatches between assembly files and Go declarations", -- Default: "true", -- }, -- { -- Name: "\"assign\"", -- Doc: "check for useless assignments\n\nThis checker reports assignments of the form x = x or a[i] = a[i].\nThese are almost always useless, and even when they aren't they are\nusually a mistake.", -- Default: "true", -- }, -- { -- Name: "\"atomic\"", -- Doc: "check for common mistakes using the sync/atomic package\n\nThe atomic checker looks for assignment statements of the form:\n\n\tx = atomic.AddUint64(&x, 1)\n\nwhich are not atomic.", -- Default: "true", -- }, -- { -- Name: "\"atomicalign\"", -- Doc: "check for non-64-bits-aligned arguments to sync/atomic functions", -- Default: "true", -- }, -- { -- Name: "\"bools\"", -- Doc: "check for common mistakes involving boolean operators", -- Default: "true", -- }, -- { -- Name: "\"buildtag\"", -- Doc: "check //go:build and // +build directives", -- Default: "true", -- }, -- { -- Name: "\"cgocall\"", -- Doc: "detect some violations of the cgo pointer passing rules\n\nCheck for invalid cgo pointer passing.\nThis looks for code that uses cgo to call C code passing values\nwhose types are almost always invalid according to the cgo pointer\nsharing rules.\nSpecifically, it warns about attempts to pass a Go chan, map, func,\nor slice to C, either directly, or via a pointer, array, or struct.", -- Default: "true", -- }, -- { -- Name: "\"composites\"", -- Doc: "check for unkeyed composite literals\n\nThis analyzer reports a diagnostic for composite literals of struct\ntypes imported from another package that do not use the field-keyed\nsyntax. Such literals are fragile because the addition of a new field\n(even if unexported) to the struct will cause compilation to fail.\n\nAs an example,\n\n\terr = &net.DNSConfigError{err}\n\nshould be replaced by:\n\n\terr = &net.DNSConfigError{Err: err}\n", -- Default: "true", -- }, -- { -- Name: "\"copylocks\"", -- Doc: "check for locks erroneously passed by value\n\nInadvertently copying a value containing a lock, such as sync.Mutex or\nsync.WaitGroup, may cause both copies to malfunction. Generally such\nvalues should be referred to through a pointer.", -- Default: "true", -- }, -- { -- Name: "\"deepequalerrors\"", -- Doc: "check for calls of reflect.DeepEqual on error values\n\nThe deepequalerrors checker looks for calls of the form:\n\n reflect.DeepEqual(err1, err2)\n\nwhere err1 and err2 are errors. Using reflect.DeepEqual to compare\nerrors is discouraged.", -- Default: "true", -- }, -- { -- Name: "\"defers\"", -- Doc: "report common mistakes in defer statements\n\nThe defers analyzer reports a diagnostic when a defer statement would\nresult in a non-deferred call to time.Since, as experience has shown\nthat this is nearly always a mistake.\n\nFor example:\n\n\tstart := time.Now()\n\t...\n\tdefer recordLatency(time.Since(start)) // error: call to time.Since is not deferred\n\nThe correct code is:\n\n\tdefer func() { recordLatency(time.Since(start)) }()", -- Default: "true", -- }, -- { -- Name: "\"deprecated\"", -- Doc: "check for use of deprecated identifiers\n\nThe deprecated analyzer looks for deprecated symbols and package imports.\n\nSee https://go.dev/wiki/Deprecated to learn about Go's convention\nfor documenting and signaling deprecated identifiers.", -- Default: "true", -- }, -- { -- Name: "\"directive\"", -- Doc: "check Go toolchain directives such as //go:debug\n\nThis analyzer checks for problems with known Go toolchain directives\nin all Go source files in a package directory, even those excluded by\n//go:build constraints, and all non-Go source files too.\n\nFor //go:debug (see https://go.dev/doc/godebug), the analyzer checks\nthat the directives are placed only in Go source files, only above the\npackage comment, and only in package main or *_test.go files.\n\nSupport for other known directives may be added in the future.\n\nThis analyzer does not check //go:build, which is handled by the\nbuildtag analyzer.\n", -- Default: "true", -- }, -- { -- Name: "\"embed\"", -- Doc: "check //go:embed directive usage\n\nThis analyzer checks that the embed package is imported if //go:embed\ndirectives are present, providing a suggested fix to add the import if\nit is missing.\n\nThis analyzer also checks that //go:embed directives precede the\ndeclaration of a single variable.", -- Default: "true", -- }, -- { -- Name: "\"errorsas\"", -- Doc: "report passing non-pointer or non-error values to errors.As\n\nThe errorsas analysis reports calls to errors.As where the type\nof the second argument is not a pointer to a type implementing error.", -- Default: "true", -- }, -- { -- Name: "\"fieldalignment\"", -- Doc: "find structs that would use less memory if their fields were sorted\n\nThis analyzer find structs that can be rearranged to use less memory, and provides\na suggested edit with the most compact order.\n\nNote that there are two different diagnostics reported. One checks struct size,\nand the other reports \"pointer bytes\" used. Pointer bytes is how many bytes of the\nobject that the garbage collector has to potentially scan for pointers, for example:\n\n\tstruct { uint32; string }\n\nhave 16 pointer bytes because the garbage collector has to scan up through the string's\ninner pointer.\n\n\tstruct { string; *uint32 }\n\nhas 24 pointer bytes because it has to scan further through the *uint32.\n\n\tstruct { string; uint32 }\n\nhas 8 because it can stop immediately after the string pointer.\n\nBe aware that the most compact order is not always the most efficient.\nIn rare cases it may cause two variables each updated by its own goroutine\nto occupy the same CPU cache line, inducing a form of memory contention\nknown as \"false sharing\" that slows down both goroutines.\n", -- Default: "false", -- }, -- { -- Name: "\"httpresponse\"", -- Doc: "check for mistakes using HTTP responses\n\nA common mistake when using the net/http package is to defer a function\ncall to close the http.Response Body before checking the error that\ndetermines whether the response is valid:\n\n\tresp, err := http.Head(url)\n\tdefer resp.Body.Close()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\t// (defer statement belongs here)\n\nThis checker helps uncover latent nil dereference bugs by reporting a\ndiagnostic for such mistakes.", -- Default: "true", -- }, -- { -- Name: "\"ifaceassert\"", -- Doc: "detect impossible interface-to-interface type assertions\n\nThis checker flags type assertions v.(T) and corresponding type-switch cases\nin which the static type V of v is an interface that cannot possibly implement\nthe target interface T. This occurs when V and T contain methods with the same\nname but different signatures. Example:\n\n\tvar v interface {\n\t\tRead()\n\t}\n\t_ = v.(io.Reader)\n\nThe Read method in v has a different signature than the Read method in\nio.Reader, so this assertion cannot succeed.", -- Default: "true", -- }, -- { -- Name: "\"loopclosure\"", -- Doc: "check references to loop variables from within nested functions\n\nThis analyzer reports places where a function literal references the\niteration variable of an enclosing loop, and the loop calls the function\nin such a way (e.g. with go or defer) that it may outlive the loop\niteration and possibly observe the wrong value of the variable.\n\nIn this example, all the deferred functions run after the loop has\ncompleted, so all observe the final value of v.\n\n\tfor _, v := range list {\n\t defer func() {\n\t use(v) // incorrect\n\t }()\n\t}\n\nOne fix is to create a new variable for each iteration of the loop:\n\n\tfor _, v := range list {\n\t v := v // new var per iteration\n\t defer func() {\n\t use(v) // ok\n\t }()\n\t}\n\nThe next example uses a go statement and has a similar problem.\nIn addition, it has a data race because the loop updates v\nconcurrent with the goroutines accessing it.\n\n\tfor _, v := range elem {\n\t go func() {\n\t use(v) // incorrect, and a data race\n\t }()\n\t}\n\nA fix is the same as before. The checker also reports problems\nin goroutines started by golang.org/x/sync/errgroup.Group.\nA hard-to-spot variant of this form is common in parallel tests:\n\n\tfunc Test(t *testing.T) {\n\t for _, test := range tests {\n\t t.Run(test.name, func(t *testing.T) {\n\t t.Parallel()\n\t use(test) // incorrect, and a data race\n\t })\n\t }\n\t}\n\nThe t.Parallel() call causes the rest of the function to execute\nconcurrent with the loop.\n\nThe analyzer reports references only in the last statement,\nas it is not deep enough to understand the effects of subsequent\nstatements that might render the reference benign.\n(\"Last statement\" is defined recursively in compound\nstatements such as if, switch, and select.)\n\nSee: https://golang.org/doc/go_faq.html#closures_and_goroutines", -- Default: "true", -- }, -- { -- Name: "\"lostcancel\"", -- Doc: "check cancel func returned by context.WithCancel is called\n\nThe cancellation function returned by context.WithCancel, WithTimeout,\nand WithDeadline must be called or the new context will remain live\nuntil its parent context is cancelled.\n(The background context is never cancelled.)", -- Default: "true", -- }, -- { -- Name: "\"nilfunc\"", -- Doc: "check for useless comparisons between functions and nil\n\nA useless comparison is one like f == nil as opposed to f() == nil.", -- Default: "true", -- }, -- { -- Name: "\"nilness\"", -- Doc: "check for redundant or impossible nil comparisons\n\nThe nilness checker inspects the control-flow graph of each function in\na package and reports nil pointer dereferences, degenerate nil\npointers, and panics with nil values. A degenerate comparison is of the form\nx==nil or x!=nil where x is statically known to be nil or non-nil. These are\noften a mistake, especially in control flow related to errors. Panics with nil\nvalues are checked because they are not detectable by\n\n\tif r := recover(); r != nil {\n\nThis check reports conditions such as:\n\n\tif f == nil { // impossible condition (f is a function)\n\t}\n\nand:\n\n\tp := &v\n\t...\n\tif p != nil { // tautological condition\n\t}\n\nand:\n\n\tif p == nil {\n\t\tprint(*p) // nil dereference\n\t}\n\nand:\n\n\tif p == nil {\n\t\tpanic(p)\n\t}", -- Default: "true", -- }, -- { -- Name: "\"printf\"", -- Doc: "check consistency of Printf format strings and arguments\n\nThe check applies to calls of the formatting functions such as\n[fmt.Printf] and [fmt.Sprintf], as well as any detected wrappers of\nthose functions.\n\nIn this example, the %d format operator requires an integer operand:\n\n\tfmt.Printf(\"%d\", \"hello\") // fmt.Printf format %d has arg \"hello\" of wrong type string\n\nSee the documentation of the fmt package for the complete set of\nformat operators and their operand types.\n\nTo enable printf checking on a function that is not found by this\nanalyzer's heuristics (for example, because control is obscured by\ndynamic method calls), insert a bogus call:\n\n\tfunc MyPrintf(format string, args ...any) {\n\t\tif false {\n\t\t\t_ = fmt.Sprintf(format, args...) // enable printf checker\n\t\t}\n\t\t...\n\t}\n\nThe -funcs flag specifies a comma-separated list of names of additional\nknown formatting functions or methods. If the name contains a period,\nit must denote a specific function using one of the following forms:\n\n\tdir/pkg.Function\n\tdir/pkg.Type.Method\n\t(*dir/pkg.Type).Method\n\nOtherwise the name is interpreted as a case-insensitive unqualified\nidentifier such as \"errorf\". Either way, if a listed name ends in f, the\nfunction is assumed to be Printf-like, taking a format string before the\nargument list. Otherwise it is assumed to be Print-like, taking a list\nof arguments with no format string.", -- Default: "true", -- }, -- { -- Name: "\"shadow\"", -- Doc: "check for possible unintended shadowing of variables\n\nThis analyzer check for shadowed variables.\nA shadowed variable is a variable declared in an inner scope\nwith the same name and type as a variable in an outer scope,\nand where the outer variable is mentioned after the inner one\nis declared.\n\n(This definition can be refined; the module generates too many\nfalse positives and is not yet enabled by default.)\n\nFor example:\n\n\tfunc BadRead(f *os.File, buf []byte) error {\n\t\tvar err error\n\t\tfor {\n\t\t\tn, err := f.Read(buf) // shadows the function variable 'err'\n\t\t\tif err != nil {\n\t\t\t\tbreak // causes return of wrong value\n\t\t\t}\n\t\t\tfoo(buf)\n\t\t}\n\t\treturn err\n\t}", -- Default: "false", -- }, -- { -- Name: "\"shift\"", -- Doc: "check for shifts that equal or exceed the width of the integer", -- Default: "true", -- }, -- { -- Name: "\"simplifycompositelit\"", -- Doc: "check for composite literal simplifications\n\nAn array, slice, or map composite literal of the form:\n\t[]T{T{}, T{}}\nwill be simplified to:\n\t[]T{{}, {}}\n\nThis is one of the simplifications that \"gofmt -s\" applies.", -- Default: "true", -- }, -- { -- Name: "\"simplifyrange\"", -- Doc: "check for range statement simplifications\n\nA range of the form:\n\tfor x, _ = range v {...}\nwill be simplified to:\n\tfor x = range v {...}\n\nA range of the form:\n\tfor _ = range v {...}\nwill be simplified to:\n\tfor range v {...}\n\nThis is one of the simplifications that \"gofmt -s\" applies.", -- Default: "true", -- }, -- { -- Name: "\"simplifyslice\"", -- Doc: "check for slice simplifications\n\nA slice expression of the form:\n\ts[a:len(s)]\nwill be simplified to:\n\ts[a:]\n\nThis is one of the simplifications that \"gofmt -s\" applies.", -- Default: "true", -- }, -- { -- Name: "\"slog\"", -- Doc: "check for invalid structured logging calls\n\nThe slog checker looks for calls to functions from the log/slog\npackage that take alternating key-value pairs. It reports calls\nwhere an argument in a key position is neither a string nor a\nslog.Attr, and where a final key is missing its value.\nFor example,it would report\n\n\tslog.Warn(\"message\", 11, \"k\") // slog.Warn arg \"11\" should be a string or a slog.Attr\n\nand\n\n\tslog.Info(\"message\", \"k1\", v1, \"k2\") // call to slog.Info missing a final value", -- Default: "true", -- }, -- { -- Name: "\"sortslice\"", -- Doc: "check the argument type of sort.Slice\n\nsort.Slice requires an argument of a slice type. Check that\nthe interface{} value passed to sort.Slice is actually a slice.", -- Default: "true", -- }, -- { -- Name: "\"stdmethods\"", -- Doc: "check signature of methods of well-known interfaces\n\nSometimes a type may be intended to satisfy an interface but may fail to\ndo so because of a mistake in its method signature.\nFor example, the result of this WriteTo method should be (int64, error),\nnot error, to satisfy io.WriterTo:\n\n\ttype myWriterTo struct{...}\n\tfunc (myWriterTo) WriteTo(w io.Writer) error { ... }\n\nThis check ensures that each method whose name matches one of several\nwell-known interface methods from the standard library has the correct\nsignature for that interface.\n\nChecked method names include:\n\n\tFormat GobEncode GobDecode MarshalJSON MarshalXML\n\tPeek ReadByte ReadFrom ReadRune Scan Seek\n\tUnmarshalJSON UnreadByte UnreadRune WriteByte\n\tWriteTo", -- Default: "true", -- }, -- { -- Name: "\"stringintconv\"", -- Doc: "check for string(int) conversions\n\nThis checker flags conversions of the form string(x) where x is an integer\n(but not byte or rune) type. Such conversions are discouraged because they\nreturn the UTF-8 representation of the Unicode code point x, and not a decimal\nstring representation of x as one might expect. Furthermore, if x denotes an\ninvalid code point, the conversion cannot be statically rejected.\n\nFor conversions that intend on using the code point, consider replacing them\nwith string(rune(x)). Otherwise, strconv.Itoa and its equivalents return the\nstring representation of the value in the desired base.", -- Default: "true", -- }, -- { -- Name: "\"structtag\"", -- Doc: "check that struct field tags conform to reflect.StructTag.Get\n\nAlso report certain struct tags (json, xml) used with unexported fields.", -- Default: "true", -- }, -- { -- Name: "\"testinggoroutine\"", -- Doc: "report calls to (*testing.T).Fatal from goroutines started by a test.\n\nFunctions that abruptly terminate a test, such as the Fatal, Fatalf, FailNow, and\nSkip{,f,Now} methods of *testing.T, must be called from the test goroutine itself.\nThis checker detects calls to these functions that occur within a goroutine\nstarted by the test. For example:\n\n\tfunc TestFoo(t *testing.T) {\n\t go func() {\n\t t.Fatal(\"oops\") // error: (*T).Fatal called from non-test goroutine\n\t }()\n\t}", -- Default: "true", -- }, -- { -- Name: "\"tests\"", -- Doc: "check for common mistaken usages of tests and examples\n\nThe tests checker walks Test, Benchmark, Fuzzing and Example functions checking\nmalformed names, wrong signatures and examples documenting non-existent\nidentifiers.\n\nPlease see the documentation for package testing in golang.org/pkg/testing\nfor the conventions that are enforced for Tests, Benchmarks, and Examples.", -- Default: "true", -- }, -- { -- Name: "\"timeformat\"", -- Doc: "check for calls of (time.Time).Format or time.Parse with 2006-02-01\n\nThe timeformat checker looks for time formats with the 2006-02-01 (yyyy-dd-mm)\nformat. Internationally, \"yyyy-dd-mm\" does not occur in common calendar date\nstandards, and so it is more likely that 2006-01-02 (yyyy-mm-dd) was intended.", -- Default: "true", -- }, -- { -- Name: "\"unmarshal\"", -- Doc: "report passing non-pointer or non-interface values to unmarshal\n\nThe unmarshal analysis reports calls to functions such as json.Unmarshal\nin which the argument type is not a pointer or an interface.", -- Default: "true", -- }, -- { -- Name: "\"unreachable\"", -- Doc: "check for unreachable code\n\nThe unreachable analyzer finds statements that execution can never reach\nbecause they are preceded by an return statement, a call to panic, an\ninfinite loop, or similar constructs.", -- Default: "true", -- }, -- { -- Name: "\"unsafeptr\"", -- Doc: "check for invalid conversions of uintptr to unsafe.Pointer\n\nThe unsafeptr analyzer reports likely incorrect uses of unsafe.Pointer\nto convert integers to pointers. A conversion from uintptr to\nunsafe.Pointer is invalid if it implies that there is a uintptr-typed\nword in memory that holds a pointer value, because that word will be\ninvisible to stack copying and to the garbage collector.", -- Default: "true", -- }, -- { -- Name: "\"unusedparams\"", -- Doc: "check for unused parameters of functions\n\nThe unusedparams analyzer checks functions to see if there are\nany parameters that are not being used.\n\nTo reduce false positives it ignores:\n- methods\n- parameters that do not have a name or have the name '_' (the blank identifier)\n- functions in test files\n- functions with empty bodies or those with just a return stmt", -- Default: "false", -- }, -- { -- Name: "\"unusedresult\"", -- Doc: "check for unused results of calls to some functions\n\nSome functions like fmt.Errorf return a result and have no side\neffects, so it is always a mistake to discard the result. Other\nfunctions may return an error that must not be ignored, or a cleanup\noperation that must be called. This analyzer reports calls to\nfunctions like these when the result of the call is ignored.\n\nThe set of functions may be controlled using flags.", -- Default: "true", -- }, -- { -- Name: "\"unusedwrite\"", -- Doc: "checks for unused writes\n\nThe analyzer reports instances of writes to struct fields and\narrays that are never read. Specifically, when a struct object\nor an array is copied, its elements are copied implicitly by\nthe compiler, and any element write to this copy does nothing\nwith the original object.\n\nFor example:\n\n\ttype T struct { x int }\n\n\tfunc f(input []T) {\n\t\tfor i, v := range input { // v is a copy\n\t\t\tv.x = i // unused write to field x\n\t\t}\n\t}\n\nAnother example is about non-pointer receiver:\n\n\ttype T struct { x int }\n\n\tfunc (t T) f() { // t is a copy\n\t\tt.x = i // unused write to field x\n\t}", -- Default: "false", -- }, -- { -- Name: "\"useany\"", -- Doc: "check for constraints that could be simplified to \"any\"", -- Default: "false", -- }, -- { -- Name: "\"fillreturns\"", -- Doc: "suggest fixes for errors due to an incorrect number of return values\n\nThis checker provides suggested fixes for type errors of the\ntype \"wrong number of return values (want %d, got %d)\". For example:\n\tfunc m() (int, string, *bool, error) {\n\t\treturn\n\t}\nwill turn into\n\tfunc m() (int, string, *bool, error) {\n\t\treturn 0, \"\", nil, nil\n\t}\n\nThis functionality is similar to https://github.com/sqs/goreturns.\n", -- Default: "true", -- }, -- { -- Name: "\"nonewvars\"", -- Doc: "suggested fixes for \"no new vars on left side of :=\"\n\nThis checker provides suggested fixes for type errors of the\ntype \"no new vars on left side of :=\". For example:\n\tz := 1\n\tz := 2\nwill turn into\n\tz := 1\n\tz = 2\n", -- Default: "true", -- }, -- { -- Name: "\"noresultvalues\"", -- Doc: "suggested fixes for unexpected return values\n\nThis checker provides suggested fixes for type errors of the\ntype \"no result values expected\" or \"too many return values\".\nFor example:\n\tfunc z() { return nil }\nwill turn into\n\tfunc z() { return }\n", -- Default: "true", -- }, -- { -- Name: "\"undeclaredname\"", -- Doc: "suggested fixes for \"undeclared name: <>\"\n\nThis checker provides suggested fixes for type errors of the\ntype \"undeclared name: <>\". It will either insert a new statement,\nsuch as:\n\n\"<> := \"\n\nor a new function declaration, such as:\n\nfunc <>(inferred parameters) {\n\tpanic(\"implement me!\")\n}\n", -- Default: "true", -- }, -- { -- Name: "\"unusedvariable\"", -- Doc: "check for unused variables\n\nThe unusedvariable analyzer suggests fixes for unused variables errors.\n", -- Default: "false", -- }, -- { -- Name: "\"fillstruct\"", -- Doc: "note incomplete struct initializations\n\nThis analyzer provides diagnostics for any struct literals that do not have\nany fields initialized. Because the suggested fix for this analysis is\nexpensive to compute, callers should compute it separately, using the\nSuggestedFix function below.\n", -- Default: "true", -- }, -- { -- Name: "\"infertypeargs\"", -- Doc: "check for unnecessary type arguments in call expressions\n\nExplicit type arguments may be omitted from call expressions if they can be\ninferred from function arguments, or from other type arguments:\n\n\tfunc f[T any](T) {}\n\t\n\tfunc _() {\n\t\tf[string](\"foo\") // string could be inferred\n\t}\n", -- Default: "true", -- }, -- { -- Name: "\"stubmethods\"", -- Doc: "stub methods analyzer\n\nThis analyzer generates method stubs for concrete types\nin order to implement a target interface", -- Default: "true", -- }, -- }, -- }, -- Default: "{}", -- Hierarchy: "ui.diagnostic", -- }, -- { -- Name: "staticcheck", -- Type: "bool", -- Doc: "staticcheck enables additional analyses from staticcheck.io.\nThese analyses are documented on\n[Staticcheck's website](https://staticcheck.io/docs/checks/).\n", -- Default: "false", -- Status: "experimental", -- Hierarchy: "ui.diagnostic", -- }, -- { -- Name: "annotations", -- Type: "map[string]bool", -- Doc: "annotations specifies the various kinds of optimization diagnostics\nthat should be reported by the gc_details command.\n", -- EnumKeys: EnumKeys{ -- ValueType: "bool", -- Keys: []EnumKey{ -- { -- Name: "\"bounds\"", -- Doc: "`\"bounds\"` controls bounds checking diagnostics.\n", -- Default: "true", -- }, -- { -- Name: "\"escape\"", -- Doc: "`\"escape\"` controls diagnostics about escape choices.\n", -- Default: "true", -- }, -- { -- Name: "\"inline\"", -- Doc: "`\"inline\"` controls diagnostics about inlining choices.\n", -- Default: "true", -- }, -- { -- Name: "\"nil\"", -- Doc: "`\"nil\"` controls nil checks.\n", -- Default: "true", -- }, -- }, -- }, -- Default: "{\"bounds\":true,\"escape\":true,\"inline\":true,\"nil\":true}", -- Status: "experimental", -- Hierarchy: "ui.diagnostic", -- }, -- { -- Name: "vulncheck", -- Type: "enum", -- Doc: "vulncheck enables vulnerability scanning.\n", -- EnumValues: []EnumValue{ -- { -- Value: "\"Imports\"", -- Doc: "`\"Imports\"`: In Imports mode, `gopls` will report vulnerabilities that affect packages\ndirectly and indirectly used by the analyzed main module.\n", -- }, -- { -- Value: "\"Off\"", -- Doc: "`\"Off\"`: Disable vulnerability analysis.\n", -- }, -- }, -- Default: "\"Off\"", -- Status: "experimental", -- Hierarchy: "ui.diagnostic", -- }, -- { -- Name: "diagnosticsDelay", -- Type: "time.Duration", -- Doc: "diagnosticsDelay controls the amount of time that gopls waits\nafter the most recent file modification before computing deep diagnostics.\nSimple diagnostics (parsing and type-checking) are always run immediately\non recently modified packages.\n\nThis option must be set to a valid duration string, for example `\"250ms\"`.\n", -- Default: "\"1s\"", -- Status: "advanced", -- Hierarchy: "ui.diagnostic", -- }, -- { -- Name: "diagnosticsTrigger", -- Type: "enum", -- Doc: "diagnosticsTrigger controls when to run diagnostics.\n", -- EnumValues: []EnumValue{ -- { -- Value: "\"Edit\"", -- Doc: "`\"Edit\"`: Trigger diagnostics on file edit and save. (default)\n", -- }, -- { -- Value: "\"Save\"", -- Doc: "`\"Save\"`: Trigger diagnostics only on file save. Events like initial workspace load\nor configuration change will still trigger diagnostics.\n", -- }, -- }, -- Default: "\"Edit\"", -- Status: "experimental", -- Hierarchy: "ui.diagnostic", -- }, -- { -- Name: "analysisProgressReporting", -- Type: "bool", -- Doc: "analysisProgressReporting controls whether gopls sends progress\nnotifications when construction of its index of analysis facts is taking a\nlong time. Cancelling these notifications will cancel the indexing task,\nthough it will restart after the next change in the workspace.\n\nWhen a package is opened for the first time and heavyweight analyses such as\nstaticcheck are enabled, it can take a while to construct the index of\nanalysis facts for all its dependencies. The index is cached in the\nfilesystem, so subsequent analysis should be faster.\n", -- Default: "true", -- Hierarchy: "ui.diagnostic", -- }, -- { -- Name: "hints", -- Type: "map[string]bool", -- Doc: "hints specify inlay hints that users want to see. A full list of hints\nthat gopls uses can be found in\n[inlayHints.md](https://github.com/golang/tools/blob/master/gopls/doc/inlayHints.md).\n", -- EnumKeys: EnumKeys{Keys: []EnumKey{ -- { -- Name: "\"assignVariableTypes\"", -- Doc: "Enable/disable inlay hints for variable types in assign statements:\n```go\n\ti/* int*/, j/* int*/ := 0, len(r)-1\n```", -- Default: "false", -- }, -- { -- Name: "\"compositeLiteralFields\"", -- Doc: "Enable/disable inlay hints for composite literal field names:\n```go\n\t{/*in: */\"Hello, world\", /*want: */\"dlrow ,olleH\"}\n```", -- Default: "false", -- }, -- { -- Name: "\"compositeLiteralTypes\"", -- Doc: "Enable/disable inlay hints for composite literal types:\n```go\n\tfor _, c := range []struct {\n\t\tin, want string\n\t}{\n\t\t/*struct{ in string; want string }*/{\"Hello, world\", \"dlrow ,olleH\"},\n\t}\n```", -- Default: "false", -- }, -- { -- Name: "\"constantValues\"", -- Doc: "Enable/disable inlay hints for constant values:\n```go\n\tconst (\n\t\tKindNone Kind = iota/* = 0*/\n\t\tKindPrint/* = 1*/\n\t\tKindPrintf/* = 2*/\n\t\tKindErrorf/* = 3*/\n\t)\n```", -- Default: "false", -- }, -- { -- Name: "\"functionTypeParameters\"", -- Doc: "Enable/disable inlay hints for implicit type parameters on generic functions:\n```go\n\tmyFoo/*[int, string]*/(1, \"hello\")\n```", -- Default: "false", -- }, -- { -- Name: "\"parameterNames\"", -- Doc: "Enable/disable inlay hints for parameter names:\n```go\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)\n```", -- Default: "false", -- }, -- { -- Name: "\"rangeVariableTypes\"", -- Doc: "Enable/disable inlay hints for variable types in range statements:\n```go\n\tfor k/* int*/, v/* string*/ := range []string{} {\n\t\tfmt.Println(k, v)\n\t}\n```", -- Default: "false", -- }, -- }}, -- Default: "{}", -- Status: "experimental", -- Hierarchy: "ui.inlayhint", -- }, -- { -- Name: "codelenses", -- Type: "map[string]bool", -- Doc: "codelenses overrides the enabled/disabled state of code lenses. See the\n\"Code Lenses\" section of the\n[Settings page](https://github.com/golang/tools/blob/master/gopls/doc/settings.md#code-lenses)\nfor the list of supported lenses.\n\nExample Usage:\n\n```json5\n\"gopls\": {\n...\n \"codelenses\": {\n \"generate\": false, // Don't show the `go generate` lens.\n \"gc_details\": true // Show a code lens toggling the display of gc's choices.\n }\n...\n}\n```\n", -- EnumKeys: EnumKeys{ -- ValueType: "bool", -- Keys: []EnumKey{ -- { -- Name: "\"gc_details\"", -- Doc: "Toggle the calculation of gc annotations.", -- Default: "false", -- }, -- { -- Name: "\"generate\"", -- Doc: "Runs `go generate` for a given directory.", -- Default: "true", -- }, -- { -- Name: "\"regenerate_cgo\"", -- Doc: "Regenerates cgo definitions.", -- Default: "true", -- }, -- { -- Name: "\"run_govulncheck\"", -- Doc: "Run vulnerability check (`govulncheck`).", -- Default: "false", -- }, -- { -- Name: "\"test\"", -- Doc: "Runs `go test` for a specific set of test or benchmark functions.", -- Default: "false", -- }, -- { -- Name: "\"tidy\"", -- Doc: "Runs `go mod tidy` for a module.", -- Default: "true", -- }, -- { -- Name: "\"upgrade_dependency\"", -- Doc: "Upgrades a dependency in the go.mod file for a module.", -- Default: "true", -- }, -- { -- Name: "\"vendor\"", -- Doc: "Runs `go mod vendor` for a module.", -- Default: "true", -- }, -- }, -- }, -- Default: "{\"gc_details\":false,\"generate\":true,\"regenerate_cgo\":true,\"tidy\":true,\"upgrade_dependency\":true,\"vendor\":true}", -- Hierarchy: "ui", -- }, -- { -- Name: "semanticTokens", -- Type: "bool", -- Doc: "semanticTokens controls whether the LSP server will send\nsemantic tokens to the client.\n", -- Default: "false", -- Status: "experimental", -- Hierarchy: "ui", -- }, -- { -- Name: "noSemanticString", -- Type: "bool", -- Doc: "noSemanticString turns off the sending of the semantic token 'string'\n", -- Default: "false", -- Status: "experimental", -- Hierarchy: "ui", -- }, -- { -- Name: "noSemanticNumber", -- Type: "bool", -- Doc: "noSemanticNumber turns off the sending of the semantic token 'number'\n", -- Default: "false", -- Status: "experimental", -- Hierarchy: "ui", -- }, -- { -- Name: "local", -- Type: "string", -- Doc: "local is the equivalent of the `goimports -local` flag, which puts\nimports beginning with this string after third-party packages. It should\nbe the prefix of the import path whose imports should be grouped\nseparately.\n", -- Default: "\"\"", -- Hierarchy: "formatting", -- }, -- { -- Name: "gofumpt", -- Type: "bool", -- Doc: "gofumpt indicates if we should run gofumpt formatting.\n", -- Default: "false", -- Hierarchy: "formatting", +- if s, ok := under.(*types.Struct); ok { +- for i := 0; i < s.NumFields(); i++ { +- obj2 := s.Field(i) +- pkg2 := pkg +- if obj2.Pkg() != pkg2.Types { +- pkg2, ok = pkg.Imports[obj2.Pkg().Path()] +- if !ok { +- return nil, fmt.Errorf("missing import for %q: %q", pkg.ID, obj2.Pkg().Path()) +- } +- } +- node, err := findField(pkg2, obj2.Pos()) +- if err != nil { +- return nil, err +- } +- tag := s.Tag(i) +- structField, err := l.loadField(pkg2, obj2, node.Doc.Text(), tag) +- if err != nil { +- return nil, err +- } +- fld.Fields = append(fld.Fields, structField) +- } +- } +- return fld, nil +-} +- +-// splitDoc parses a command doc string to separate the title from normal +-// documentation. +-// +-// The doc comment should be of the form: "MethodName: Title\nDocumentation" +-func splitDoc(text string) (title, doc string) { +- docParts := strings.SplitN(text, "\n", 2) +- titleParts := strings.SplitN(docParts[0], ":", 2) +- if len(titleParts) > 1 { +- title = strings.TrimSpace(titleParts[1]) +- } +- if len(docParts) > 1 { +- doc = strings.TrimSpace(docParts[1]) +- } +- return title, doc +-} +- +-// lspName returns the normalized command name to use in the LSP. +-func lspName(methodName string) string { +- words := splitCamel(methodName) +- for i := range words { +- words[i] = strings.ToLower(words[i]) +- } +- return strings.Join(words, "_") +-} +- +-// splitCamel splits s into words, according to camel-case word boundaries. +-// Initialisms are grouped as a single word. +-// +-// For example: +-// +-// "RunTests" -> []string{"Run", "Tests"} +-// "GCDetails" -> []string{"GC", "Details"} +-func splitCamel(s string) []string { +- var words []string +- for len(s) > 0 { +- last := strings.LastIndexFunc(s, unicode.IsUpper) +- if last < 0 { +- last = 0 +- } +- if last == len(s)-1 { +- // Group initialisms as a single word. +- last = 1 + strings.LastIndexFunc(s[:last], func(r rune) bool { return !unicode.IsUpper(r) }) +- } +- words = append(words, s[last:]) +- s = s[:last] +- } +- for i := 0; i < len(words)/2; i++ { +- j := len(words) - i - 1 +- words[i], words[j] = words[j], words[i] +- } +- return words +-} +- +-// findField finds the struct field or interface method positioned at pos, +-// within the AST. +-func findField(pkg *packages.Package, pos token.Pos) (*ast.Field, error) { +- fset := pkg.Fset +- var file *ast.File +- for _, f := range pkg.Syntax { +- if fset.File(f.Pos()).Name() == fset.File(pos).Name() { +- file = f +- break +- } +- } +- if file == nil { +- return nil, fmt.Errorf("no file for pos %v", pos) +- } +- path, _ := astutil.PathEnclosingInterval(file, pos, pos) +- // This is fragile, but in the cases we care about, the field will be in +- // path[1]. +- return path[1].(*ast.Field), nil +-} +diff -urN a/gopls/internal/protocol/command/gen/gen.go b/gopls/internal/protocol/command/gen/gen.go +--- a/gopls/internal/protocol/command/gen/gen.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/command/gen/gen.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,161 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-// Package gen is used to generate command bindings from the gopls command +-// interface. +-package gen +- +-import ( +- "bytes" +- "fmt" +- "go/types" +- "text/template" +- +- "golang.org/x/tools/gopls/internal/protocol/command/commandmeta" +- "golang.org/x/tools/internal/imports" +-) +- +-const src = `// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-// Don't include this file during code generation, or it will break the build +-// if existing interface methods have been modified. +-//go:build !generate +-// +build !generate +- +-// Code generated by gen.go. DO NOT EDIT. +- +-package command +- +-import ( +- {{range $k, $v := .Imports -}} +- "{{$k}}" +- {{end}} +-) +- +-// Symbolic names for gopls commands, excluding "gopls." prefix. +-// These commands may be requested by ExecuteCommand, CodeLens, +-// CodeAction, and other LSP requests. +-const ( +-{{- range .Commands}} +- {{.MethodName}} Command = "{{.Name}}" +-{{- end}} +-) +- +-var Commands = []Command { +-{{- range .Commands}} +- {{.MethodName}}, +-{{- end}} +-} +- +-func Dispatch(ctx context.Context, params *protocol.ExecuteCommandParams, s Interface) (interface{}, error) { +- switch params.Command { +- {{- range .Commands}} +- case "{{.ID}}": +- {{- if .Args -}} +- {{- range $i, $v := .Args}} +- var a{{$i}} {{typeString $v.Type}} +- {{- end}} +- if err := UnmarshalArgs(params.Arguments{{range $i, $v := .Args}}, &a{{$i}}{{end}}); err != nil { +- return nil, err +- } +- {{end -}} +- return {{if not .Result}}nil, {{end}}s.{{.MethodName}}(ctx{{range $i, $v := .Args}}, a{{$i}}{{end}}) +- {{- end}} +- } +- return nil, fmt.Errorf("unsupported command %q", params.Command) +-} +-{{- range .Commands}} +- +-func New{{.MethodName}}Command(title string, {{range $i, $v := .Args}}{{if $i}}, {{end}}a{{$i}} {{typeString $v.Type}}{{end}}) (protocol.Command, error) { +- args, err := MarshalArgs({{range $i, $v := .Args}}{{if $i}}, {{end}}a{{$i}}{{end}}) +- if err != nil { +- return protocol.Command{}, err +- } +- return protocol.Command{ +- Title: title, +- Command: "{{.ID}}", +- Arguments: args, +- }, nil +-} +-{{end}} +-` +- +-type data struct { +- Imports map[string]bool +- Commands []*commandmeta.Command +-} +- +-func Generate() ([]byte, error) { +- pkg, cmds, err := commandmeta.Load() +- if err != nil { +- return nil, fmt.Errorf("loading command data: %v", err) +- } +- qf := func(p *types.Package) string { +- if p == pkg.Types { +- return "" +- } +- return p.Name() +- } +- tmpl, err := template.New("").Funcs(template.FuncMap{ +- "typeString": func(t types.Type) string { +- return types.TypeString(t, qf) +- }, +- }).Parse(src) +- if err != nil { +- return nil, err +- } +- d := data{ +- Commands: cmds, +- Imports: map[string]bool{ +- "context": true, +- "fmt": true, +- "golang.org/x/tools/gopls/internal/protocol": true, +- }, +- } +- const thispkg = "golang.org/x/tools/gopls/internal/protocol/command" +- for _, c := range d.Commands { +- for _, arg := range c.Args { +- pth := pkgPath(arg.Type) +- if pth != "" && pth != thispkg { +- d.Imports[pth] = true +- } +- } +- if c.Result != nil { +- pth := pkgPath(c.Result.Type) +- if pth != "" && pth != thispkg { +- d.Imports[pth] = true +- } +- } +- } +- +- var buf bytes.Buffer +- if err := tmpl.Execute(&buf, d); err != nil { +- return nil, fmt.Errorf("executing: %v", err) +- } +- +- opts := &imports.Options{ +- AllErrors: true, +- FormatOnly: true, +- Comments: true, +- } +- content, err := imports.Process("", buf.Bytes(), opts) +- if err != nil { +- return nil, fmt.Errorf("goimports: %v", err) +- } +- return content, nil +-} +- +-func pkgPath(t types.Type) string { +- type hasTypeName interface { // *Named or *Alias (or *TypeParam) +- Obj() *types.TypeName +- } +- if t, ok := t.(hasTypeName); ok { +- if pkg := t.Obj().Pkg(); pkg != nil { +- return pkg.Path() +- } +- } +- return "" +-} +diff -urN a/gopls/internal/protocol/command/generate.go b/gopls/internal/protocol/command/generate.go +--- a/gopls/internal/protocol/command/generate.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/command/generate.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,25 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-//go:build ignore +-// +build ignore +- +-package main +- +-import ( +- "log" +- "os" +- +- "golang.org/x/tools/gopls/internal/protocol/command/gen" +-) +- +-func main() { +- content, err := gen.Generate() +- if err != nil { +- log.Fatal(err) +- } +- if err := os.WriteFile("command_gen.go", content, 0644); err != nil { +- log.Fatal(err) +- } +-} +diff -urN a/gopls/internal/protocol/command/interface.go b/gopls/internal/protocol/command/interface.go +--- a/gopls/internal/protocol/command/interface.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/command/interface.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,534 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-//go:generate go run -tags=generate generate.go +- +-// Package command defines the interface provided by gopls for the +-// workspace/executeCommand LSP request. +-// +-// This interface is fully specified by the Interface type, provided it +-// conforms to the restrictions outlined in its doc string. +-// +-// Bindings for server-side command dispatch and client-side serialization are +-// also provided by this package, via code generation. +-package command +- +-import ( +- "context" +- +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/vulncheck" +-) +- +-// Interface defines the interface gopls exposes for the +-// workspace/executeCommand request. +-// +-// This interface is used to generate marshaling/unmarshaling code, dispatch, +-// and documentation, and so has some additional restrictions: +-// 1. All method arguments must be JSON serializable. +-// 2. Methods must return either error or (T, error), where T is a +-// JSON serializable type. +-// 3. The first line of the doc string is special. Everything after the colon +-// is considered the command 'Title'. +-// TODO(rFindley): reconsider this -- Title may be unnecessary. +-// +-// The doc comment on each method is eventually published at +-// https://github.com/golang/tools/blob/master/gopls/doc/commands.md, +-// so please be consistent in using this form: +-// +-// Command: Capitalized verb phrase with no period +-// +-// Longer description here... +-type Interface interface { +- // ApplyFix: Apply a fix +- // +- // Applies a fix to a region of source code. +- ApplyFix(context.Context, ApplyFixArgs) (*protocol.WorkspaceEdit, error) +- +- // Test: Run test(s) (legacy) +- // +- // Runs `go test` for a specific set of test or benchmark functions. +- Test(context.Context, protocol.DocumentURI, []string, []string) error +- +- // TODO: deprecate Test in favor of RunTests below. +- +- // Test: Run test(s) +- // +- // Runs `go test` for a specific set of test or benchmark functions. +- RunTests(context.Context, RunTestsArgs) error +- +- // Generate: Run go generate +- // +- // Runs `go generate` for a given directory. +- Generate(context.Context, GenerateArgs) error +- +- // Doc: View package documentation. +- // +- // Opens the Go package documentation page for the current +- // package in a browser. +- Doc(context.Context, protocol.Location) error +- +- // RegenerateCgo: Regenerate cgo +- // +- // Regenerates cgo definitions. +- RegenerateCgo(context.Context, URIArg) error +- +- // Tidy: Run go mod tidy +- // +- // Runs `go mod tidy` for a module. +- Tidy(context.Context, URIArgs) error +- +- // Vendor: Run go mod vendor +- // +- // Runs `go mod vendor` for a module. +- Vendor(context.Context, URIArg) error +- +- // EditGoDirective: Run go mod edit -go=version +- // +- // Runs `go mod edit -go=version` for a module. +- EditGoDirective(context.Context, EditGoDirectiveArgs) error +- +- // UpdateGoSum: Update go.sum +- // +- // Updates the go.sum file for a module. +- UpdateGoSum(context.Context, URIArgs) error +- +- // CheckUpgrades: Check for upgrades +- // +- // Checks for module upgrades. +- CheckUpgrades(context.Context, CheckUpgradesArgs) error +- +- // AddDependency: Add a dependency +- // +- // Adds a dependency to the go.mod file for a module. +- AddDependency(context.Context, DependencyArgs) error +- +- // UpgradeDependency: Upgrade a dependency +- // +- // Upgrades a dependency in the go.mod file for a module. +- UpgradeDependency(context.Context, DependencyArgs) error +- +- // RemoveDependency: Remove a dependency +- // +- // Removes a dependency from the go.mod file of a module. +- RemoveDependency(context.Context, RemoveDependencyArgs) error +- +- // ResetGoModDiagnostics: Reset go.mod diagnostics +- // +- // Reset diagnostics in the go.mod file of a module. +- ResetGoModDiagnostics(context.Context, ResetGoModDiagnosticsArgs) error +- +- // GoGetPackage: 'go get' a package +- // +- // Runs `go get` to fetch a package. +- GoGetPackage(context.Context, GoGetPackageArgs) error +- +- // GCDetails: Toggle gc_details +- // +- // Toggle the calculation of gc annotations. +- GCDetails(context.Context, protocol.DocumentURI) error +- +- // TODO: deprecate GCDetails in favor of ToggleGCDetails below. +- +- // ToggleGCDetails: Toggle gc_details +- // +- // Toggle the calculation of gc annotations. +- ToggleGCDetails(context.Context, URIArg) error +- +- // ListKnownPackages: List known packages +- // +- // Retrieve a list of packages that are importable from the given URI. +- ListKnownPackages(context.Context, URIArg) (ListKnownPackagesResult, error) +- +- // ListImports: List imports of a file and its package +- // +- // Retrieve a list of imports in the given Go file, and the package it +- // belongs to. +- ListImports(context.Context, URIArg) (ListImportsResult, error) +- +- // AddImport: Add an import +- // +- // Ask the server to add an import path to a given Go file. The method will +- // call applyEdit on the client so that clients don't have to apply the edit +- // themselves. +- AddImport(context.Context, AddImportArgs) error +- +- // StartDebugging: Start the gopls debug server +- // +- // Start the gopls debug server if it isn't running, and return the debug +- // address. +- StartDebugging(context.Context, DebuggingArgs) (DebuggingResult, error) +- +- // StartProfile: Start capturing a profile of gopls' execution +- // +- // Start a new pprof profile. Before using the resulting file, profiling must +- // be stopped with a corresponding call to StopProfile. +- // +- // This command is intended for internal use only, by the gopls benchmark +- // runner. +- StartProfile(context.Context, StartProfileArgs) (StartProfileResult, error) +- +- // StopProfile: Stop an ongoing profile +- // +- // This command is intended for internal use only, by the gopls benchmark +- // runner. +- StopProfile(context.Context, StopProfileArgs) (StopProfileResult, error) +- +- // RunGovulncheck: Run vulncheck +- // +- // Run vulnerability check (`govulncheck`). +- RunGovulncheck(context.Context, VulncheckArgs) (RunVulncheckResult, error) +- +- // FetchVulncheckResult: Get known vulncheck result +- // +- // Fetch the result of latest vulnerability check (`govulncheck`). +- FetchVulncheckResult(context.Context, URIArg) (map[protocol.DocumentURI]*vulncheck.Result, error) +- +- // MemStats: Fetch memory statistics +- // +- // Call runtime.GC multiple times and return memory statistics as reported by +- // runtime.MemStats. +- // +- // This command is used for benchmarking, and may change in the future. +- MemStats(context.Context) (MemStatsResult, error) +- +- // WorkspaceStats: Fetch workspace statistics +- // +- // Query statistics about workspace builds, modules, packages, and files. +- // +- // This command is intended for internal use only, by the gopls stats +- // command. +- WorkspaceStats(context.Context) (WorkspaceStatsResult, error) +- +- // RunGoWorkCommand: Run `go work [args...]`, and apply the resulting go.work +- // edits to the current go.work file +- RunGoWorkCommand(context.Context, RunGoWorkArgs) error +- +- // AddTelemetryCounters: Update the given telemetry counters +- // +- // Gopls will prepend "fwd/" to all the counters updated using this command +- // to avoid conflicts with other counters gopls collects. +- AddTelemetryCounters(context.Context, AddTelemetryCountersArgs) error +- +- // MaybePromptForTelemetry: Prompt user to enable telemetry +- // +- // Checks for the right conditions, and then prompts the user +- // to ask if they want to enable Go telemetry uploading. If +- // the user responds 'Yes', the telemetry mode is set to "on". +- MaybePromptForTelemetry(context.Context) error +- +- // ChangeSignature: Perform a "change signature" refactoring +- // +- // This command is experimental, currently only supporting parameter removal. +- // Its signature will certainly change in the future (pun intended). +- ChangeSignature(context.Context, ChangeSignatureArgs) (*protocol.WorkspaceEdit, error) +- +- // DiagnoseFiles: Cause server to publish diagnostics for the specified files. +- // +- // This command is needed by the 'gopls {check,fix}' CLI subcommands. +- DiagnoseFiles(context.Context, DiagnoseFilesArgs) error +- +- // Views: List current Views on the server. +- // +- // This command is intended for use by gopls tests only. +- Views(context.Context) ([]View, error) +-} +- +-type RunTestsArgs struct { +- // The test file containing the tests to run. +- URI protocol.DocumentURI +- +- // Specific test names to run, e.g. TestFoo. +- Tests []string +- +- // Specific benchmarks to run, e.g. BenchmarkFoo. +- Benchmarks []string +-} +- +-type GenerateArgs struct { +- // URI for the directory to generate. +- Dir protocol.DocumentURI +- +- // Whether to generate recursively (go generate ./...) +- Recursive bool +-} +- +-// TODO(rFindley): document the rest of these once the docgen is fleshed out. +- +-type ApplyFixArgs struct { +- // The name of the fix to apply. +- // +- // For fixes suggested by analyzers, this is a string constant +- // advertised by the analyzer that matches the Category of +- // the analysis.Diagnostic with a SuggestedFix containing no edits. +- // +- // For fixes suggested by code actions, this is a string agreed +- // upon by the code action and golang.ApplyFix. +- Fix string +- +- // The file URI for the document to fix. +- URI protocol.DocumentURI +- // The document range to scan for fixes. +- Range protocol.Range +- // Whether to resolve and return the edits. +- ResolveEdits bool +-} +- +-type URIArg struct { +- // The file URI. +- URI protocol.DocumentURI +-} +- +-type URIArgs struct { +- // The file URIs. +- URIs []protocol.DocumentURI +-} +- +-type CheckUpgradesArgs struct { +- // The go.mod file URI. +- URI protocol.DocumentURI +- // The modules to check. +- Modules []string +-} +- +-type DependencyArgs struct { +- // The go.mod file URI. +- URI protocol.DocumentURI +- // Additional args to pass to the go command. +- GoCmdArgs []string +- // Whether to add a require directive. +- AddRequire bool +-} +- +-type RemoveDependencyArgs struct { +- // The go.mod file URI. +- URI protocol.DocumentURI +- // The module path to remove. +- ModulePath string +- // If the module is tidied apart from the one unused diagnostic, we can +- // run `go get module@none`, and then run `go mod tidy`. Otherwise, we +- // must make textual edits. +- OnlyDiagnostic bool +-} +- +-type EditGoDirectiveArgs struct { +- // Any document URI within the relevant module. +- URI protocol.DocumentURI +- // The version to pass to `go mod edit -go`. +- Version string +-} +- +-type GoGetPackageArgs struct { +- // Any document URI within the relevant module. +- URI protocol.DocumentURI +- // The package to go get. +- Pkg string +- AddRequire bool +-} +- +-type AddImportArgs struct { +- // ImportPath is the target import path that should +- // be added to the URI file +- ImportPath string +- // URI is the file that the ImportPath should be +- // added to +- URI protocol.DocumentURI +-} +- +-type ListKnownPackagesResult struct { +- // Packages is a list of packages relative +- // to the URIArg passed by the command request. +- // In other words, it omits paths that are already +- // imported or cannot be imported due to compiler +- // restrictions. +- Packages []string +-} +- +-type ListImportsResult struct { +- // Imports is a list of imports in the requested file. +- Imports []FileImport +- +- // PackageImports is a list of all imports in the requested file's package. +- PackageImports []PackageImport +-} +- +-type FileImport struct { +- // Path is the import path of the import. +- Path string +- // Name is the name of the import, e.g. `foo` in `import foo "strings"`. +- Name string +-} +- +-type PackageImport struct { +- // Path is the import path of the import. +- Path string +-} +- +-type DebuggingArgs struct { +- // Optional: the address (including port) for the debug server to listen on. +- // If not provided, the debug server will bind to "localhost:0", and the +- // full debug URL will be contained in the result. +- // +- // If there is more than one gopls instance along the serving path (i.e. you +- // are using a daemon), each gopls instance will attempt to start debugging. +- // If Addr specifies a port, only the daemon will be able to bind to that +- // port, and each intermediate gopls instance will fail to start debugging. +- // For this reason it is recommended not to specify a port (or equivalently, +- // to specify ":0"). +- // +- // If the server was already debugging this field has no effect, and the +- // result will contain the previously configured debug URL(s). +- Addr string +-} +- +-type DebuggingResult struct { +- // The URLs to use to access the debug servers, for all gopls instances in +- // the serving path. For the common case of a single gopls instance (i.e. no +- // daemon), this will be exactly one address. +- // +- // In the case of one or more gopls instances forwarding the LSP to a daemon, +- // URLs will contain debug addresses for each server in the serving path, in +- // serving order. The daemon debug address will be the last entry in the +- // slice. If any intermediate gopls instance fails to start debugging, no +- // error will be returned but the debug URL for that server in the URLs slice +- // will be empty. +- URLs []string +-} +- +-// StartProfileArgs holds the arguments to the StartProfile command. +-// +-// It is a placeholder for future compatibility. +-type StartProfileArgs struct { +-} +- +-// StartProfileResult holds the result of the StartProfile command. +-// +-// It is a placeholder for future compatibility. +-type StartProfileResult struct { +-} +- +-// StopProfileArgs holds the arguments to the StopProfile command. +-// +-// It is a placeholder for future compatibility. +-type StopProfileArgs struct { +-} +- +-// StopProfileResult holds the result to the StopProfile command. +-type StopProfileResult struct { +- // File is the profile file name. +- File string +-} +- +-type ResetGoModDiagnosticsArgs struct { +- URIArg +- +- // Optional: source of the diagnostics to reset. +- // If not set, all resettable go.mod diagnostics will be cleared. +- DiagnosticSource string +-} +- +-type VulncheckArgs struct { +- // Any document in the directory from which govulncheck will run. +- URI protocol.DocumentURI +- +- // Package pattern. E.g. "", ".", "./...". +- Pattern string +- +- // TODO: -tests +-} +- +-// RunVulncheckResult holds the result of asynchronously starting the vulncheck +-// command. +-type RunVulncheckResult struct { +- // Token holds the progress token for LSP workDone reporting of the vulncheck +- // invocation. +- Token protocol.ProgressToken +-} +- +-// CallStack models a trace of function calls starting +-// with a client function or method and ending with a +-// call to a vulnerable symbol. +-type CallStack []StackEntry +- +-// StackEntry models an element of a call stack. +-type StackEntry struct { +- // See golang.org/x/exp/vulncheck.StackEntry. +- +- // User-friendly representation of function/method names. +- // e.g. package.funcName, package.(recvType).methodName, ... +- Name string +- URI protocol.DocumentURI +- Pos protocol.Position // Start position. (0-based. Column is always 0) +-} +- +-// MemStatsResult holds selected fields from runtime.MemStats. +-type MemStatsResult struct { +- HeapAlloc uint64 +- HeapInUse uint64 +- TotalAlloc uint64 +-} +- +-// WorkspaceStatsResult returns information about the size and shape of the +-// workspace. +-type WorkspaceStatsResult struct { +- Files FileStats // file stats for the cache +- Views []ViewStats // stats for each view in the session +-} +- +-// FileStats holds information about a set of files. +-type FileStats struct { +- Total int // total number of files +- Largest int // number of bytes in the largest file +- Errs int // number of files that could not be read +-} +- +-// ViewStats holds information about a single View in the session. +-type ViewStats struct { +- GoCommandVersion string // version of the Go command resolved for this view +- AllPackages PackageStats // package info for all packages (incl. dependencies) +- WorkspacePackages PackageStats // package info for workspace packages +- Diagnostics int // total number of diagnostics in the workspace +-} +- +-// PackageStats holds information about a collection of packages. +-type PackageStats struct { +- Packages int // total number of packages +- LargestPackage int // number of files in the largest package +- CompiledGoFiles int // total number of compiled Go files across all packages +- Modules int // total number of unique modules +-} +- +-type RunGoWorkArgs struct { +- ViewID string // ID of the view to run the command from +- InitFirst bool // Whether to run `go work init` first +- Args []string // Args to pass to `go work` +-} +- +-// AddTelemetryCountersArgs holds the arguments to the AddCounters command +-// that updates the telemetry counters. +-type AddTelemetryCountersArgs struct { +- // Names and Values must have the same length. +- Names []string // Name of counters. +- Values []int64 // Values added to the corresponding counters. Must be non-negative. +-} +- +-// ChangeSignatureArgs specifies a "change signature" refactoring to perform. +-type ChangeSignatureArgs struct { +- RemoveParameter protocol.Location +- // Whether to resolve and return the edits. +- ResolveEdits bool +-} +- +-// DiagnoseFilesArgs specifies a set of files for which diagnostics are wanted. +-type DiagnoseFilesArgs struct { +- Files []protocol.DocumentURI +-} +- +-// A View holds summary information about a cache.View. +-type View struct { +- Type string // view type (via cache.ViewType.String) +- Root protocol.DocumentURI // root dir of the view (e.g. containing go.mod or go.work) +- Folder protocol.DocumentURI // workspace folder associated with the view +- EnvOverlay []string // environment variable overrides +-} +diff -urN a/gopls/internal/protocol/command/interface_test.go b/gopls/internal/protocol/command/interface_test.go +--- a/gopls/internal/protocol/command/interface_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/command/interface_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,32 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package command_test +- +-import ( +- "os" +- "testing" +- +- "github.com/google/go-cmp/cmp" +- "golang.org/x/tools/gopls/internal/protocol/command/gen" +- "golang.org/x/tools/internal/testenv" +-) +- +-func TestGenerated(t *testing.T) { +- testenv.NeedsGoPackages(t) +- testenv.NeedsLocalXTools(t) +- +- onDisk, err := os.ReadFile("command_gen.go") +- if err != nil { +- t.Fatal(err) +- } +- +- generated, err := gen.Generate() +- if err != nil { +- t.Fatal(err) +- } +- if diff := cmp.Diff(string(generated), string(onDisk)); diff != "" { +- t.Errorf("command_gen.go is stale -- regenerate (-generated +on disk)\n%s", diff) +- } +-} +diff -urN a/gopls/internal/protocol/command/util.go b/gopls/internal/protocol/command/util.go +--- a/gopls/internal/protocol/command/util.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/command/util.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,64 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package command +- +-import ( +- "encoding/json" +- "fmt" +-) +- +-// ID returns the command name for use in the LSP. +-func ID(name string) string { +- return "gopls." + name +-} +- +-type Command string +- +-// ID returns the command identifier to use in the executeCommand request. +-func (c Command) ID() string { +- return ID(string(c)) +-} +- +-// MarshalArgs encodes the given arguments to json.RawMessages. This function +-// is used to construct arguments to a protocol.Command. +-// +-// Example usage: +-// +-// jsonArgs, err := MarshalArgs(1, "hello", true, StructuredArg{42, 12.6}) +-func MarshalArgs(args ...interface{}) ([]json.RawMessage, error) { +- var out []json.RawMessage +- for _, arg := range args { +- argJSON, err := json.Marshal(arg) +- if err != nil { +- return nil, err +- } +- out = append(out, argJSON) +- } +- return out, nil +-} +- +-// UnmarshalArgs decodes the given json.RawMessages to the variables provided +-// by args. Each element of args should be a pointer. +-// +-// Example usage: +-// +-// var ( +-// num int +-// str string +-// bul bool +-// structured StructuredArg +-// ) +-// err := UnmarshalArgs(args, &num, &str, &bul, &structured) +-func UnmarshalArgs(jsonArgs []json.RawMessage, args ...interface{}) error { +- if len(args) != len(jsonArgs) { +- return fmt.Errorf("DecodeArgs: expected %d input arguments, got %d JSON arguments", len(args), len(jsonArgs)) +- } +- for i, arg := range args { +- if err := json.Unmarshal(jsonArgs[i], arg); err != nil { +- return err +- } +- } +- return nil +-} +diff -urN a/gopls/internal/protocol/context.go b/gopls/internal/protocol/context.go +--- a/gopls/internal/protocol/context.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/context.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,65 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package protocol +- +-import ( +- "bytes" +- "context" +- "sync" +- +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/core" +- "golang.org/x/tools/internal/event/export" +- "golang.org/x/tools/internal/event/label" +- "golang.org/x/tools/internal/xcontext" +-) +- +-type contextKey int +- +-const ( +- clientKey = contextKey(iota) +-) +- +-func WithClient(ctx context.Context, client Client) context.Context { +- return context.WithValue(ctx, clientKey, client) +-} +- +-func LogEvent(ctx context.Context, ev core.Event, lm label.Map, mt MessageType) context.Context { +- client, ok := ctx.Value(clientKey).(Client) +- if !ok { +- return ctx +- } +- buf := &bytes.Buffer{} +- p := export.Printer{} +- p.WriteEvent(buf, ev, lm) +- msg := &LogMessageParams{Type: mt, Message: buf.String()} +- // Handle messages generated via event.Error, which won't have a level Label. +- if event.IsError(ev) { +- msg.Type = Error +- } +- +- // The background goroutine lives forever once started, +- // and ensures log messages are sent in order (#61216). +- startLogSenderOnce.Do(func() { +- go func() { +- for f := range logQueue { +- f() +- } +- }() +- }) +- +- // Add the log item to a queue, rather than sending a +- // window/logMessage request to the client synchronously, +- // which would slow down this thread. +- ctx2 := xcontext.Detach(ctx) +- logQueue <- func() { client.LogMessage(ctx2, msg) } +- +- return ctx +-} +- +-var ( +- startLogSenderOnce sync.Once +- logQueue = make(chan func(), 100) // big enough for a large transient burst +-) +diff -urN a/gopls/internal/protocol/doc.go b/gopls/internal/protocol/doc.go +--- a/gopls/internal/protocol/doc.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/doc.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,18 +0,0 @@ +-// Copyright 2018 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-//go:generate go run ./generate +- +-// Package protocol contains the structs that map directly to the +-// request and response messages of the Language Server Protocol. +-// +-// It is a literal transcription, with unmodified comments, and only the changes +-// required to make it go code. +-// Names are uppercased to export them. +-// All fields have JSON tags added to correct the names. +-// Fields marked with a ? are also marked as "omitempty" +-// Fields that are "|| null" are made pointers +-// Fields that are string or number are left as string +-// Fields that are type "number" are made float64 +-package protocol +diff -urN a/gopls/internal/protocol/edits.go b/gopls/internal/protocol/edits.go +--- a/gopls/internal/protocol/edits.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/edits.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,128 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package protocol +- +-import ( +- "fmt" +- +- "golang.org/x/tools/internal/diff" +-) +- +-// EditsFromDiffEdits converts diff.Edits to a non-nil slice of LSP TextEdits. +-// See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textEditArray +-func EditsFromDiffEdits(m *Mapper, edits []diff.Edit) ([]TextEdit, error) { +- // LSP doesn't require TextEditArray to be sorted: +- // this is the receiver's concern. But govim, and perhaps +- // other clients have historically relied on the order. +- edits = append([]diff.Edit(nil), edits...) +- diff.SortEdits(edits) +- +- result := make([]TextEdit, len(edits)) +- for i, edit := range edits { +- rng, err := m.OffsetRange(edit.Start, edit.End) +- if err != nil { +- return nil, err +- } +- result[i] = TextEdit{ +- Range: rng, +- NewText: edit.New, +- } +- } +- return result, nil +-} +- +-// EditsToDiffEdits converts LSP TextEdits to diff.Edits. +-// See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textEditArray +-func EditsToDiffEdits(m *Mapper, edits []TextEdit) ([]diff.Edit, error) { +- if edits == nil { +- return nil, nil +- } +- result := make([]diff.Edit, len(edits)) +- for i, edit := range edits { +- start, end, err := m.RangeOffsets(edit.Range) +- if err != nil { +- return nil, err +- } +- result[i] = diff.Edit{ +- Start: start, +- End: end, +- New: edit.NewText, +- } +- } +- return result, nil +-} +- +-// ApplyEdits applies the patch (edits) to m.Content and returns the result. +-// It also returns the edits converted to diff-package form. +-func ApplyEdits(m *Mapper, edits []TextEdit) ([]byte, []diff.Edit, error) { +- diffEdits, err := EditsToDiffEdits(m, edits) +- if err != nil { +- return nil, nil, err +- } +- out, err := diff.ApplyBytes(m.Content, diffEdits) +- return out, diffEdits, err +-} +- +-// AsTextEdits converts a slice possibly containing AnnotatedTextEdits +-// to a slice of TextEdits. +-func AsTextEdits(edits []Or_TextDocumentEdit_edits_Elem) []TextEdit { +- var result []TextEdit +- for _, e := range edits { +- var te TextEdit +- if x, ok := e.Value.(AnnotatedTextEdit); ok { +- te = x.TextEdit +- } else if x, ok := e.Value.(TextEdit); ok { +- te = x +- } else { +- panic(fmt.Sprintf("unexpected type %T, expected AnnotatedTextEdit or TextEdit", e.Value)) +- } +- result = append(result, te) +- } +- return result +-} +- +-// AsAnnotatedTextEdits converts a slice of TextEdits +-// to a slice of Or_TextDocumentEdit_edits_Elem. +-// (returning a typed nil is required in server: in code_action.go and command.go)) +-func AsAnnotatedTextEdits(edits []TextEdit) []Or_TextDocumentEdit_edits_Elem { +- if edits == nil { +- return []Or_TextDocumentEdit_edits_Elem{} +- } +- var result []Or_TextDocumentEdit_edits_Elem +- for _, e := range edits { +- result = append(result, Or_TextDocumentEdit_edits_Elem{ +- Value: TextEdit{ +- Range: e.Range, +- NewText: e.NewText, - }, -- { -- Name: "verboseOutput", -- Type: "bool", -- Doc: "verboseOutput enables additional debug logging.\n", -- Default: "false", -- Status: "debug", +- }) +- } +- return result +-} +- +-// TextEditsToDocumentChanges converts a set of edits within the +-// specified (versioned) file to a singleton list of DocumentChanges +-// (as required for a WorkspaceEdit). +-func TextEditsToDocumentChanges(uri DocumentURI, version int32, edits []TextEdit) []DocumentChanges { +- return []DocumentChanges{{ +- TextDocumentEdit: &TextDocumentEdit{ +- TextDocument: OptionalVersionedTextDocumentIdentifier{ +- Version: version, +- TextDocumentIdentifier: TextDocumentIdentifier{URI: uri}, - }, +- Edits: AsAnnotatedTextEdits(edits), - }, +- }} +-} +- +-// TextDocumentEditsToDocumentChanges wraps each TextDocumentEdit in a DocumentChange. +-func TextDocumentEditsToDocumentChanges(edits []TextDocumentEdit) []DocumentChanges { +- changes := []DocumentChanges{} // non-nil +- for _, edit := range edits { +- edit := edit +- changes = append(changes, DocumentChanges{TextDocumentEdit: &edit}) +- } +- return changes +-} +diff -urN a/gopls/internal/protocol/enums.go b/gopls/internal/protocol/enums.go +--- a/gopls/internal/protocol/enums.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/enums.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,174 +0,0 @@ +-// Copyright 2018 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package protocol +- +-import ( +- "fmt" +-) +- +-var ( +- namesTextDocumentSyncKind [int(Incremental) + 1]string +- namesMessageType [int(Log) + 1]string +- namesFileChangeType [int(Deleted) + 1]string +- namesWatchKind [int(WatchDelete) + 1]string +- namesCompletionTriggerKind [int(TriggerForIncompleteCompletions) + 1]string +- namesDiagnosticSeverity [int(SeverityHint) + 1]string +- namesDiagnosticTag [int(Unnecessary) + 1]string +- namesCompletionItemKind [int(TypeParameterCompletion) + 1]string +- namesInsertTextFormat [int(SnippetTextFormat) + 1]string +- namesDocumentHighlightKind [int(Write) + 1]string +- namesSymbolKind [int(TypeParameter) + 1]string +- namesTextDocumentSaveReason [int(FocusOut) + 1]string +-) +- +-func init() { +- namesTextDocumentSyncKind[int(None)] = "None" +- namesTextDocumentSyncKind[int(Full)] = "Full" +- namesTextDocumentSyncKind[int(Incremental)] = "Incremental" +- +- namesMessageType[int(Error)] = "Error" +- namesMessageType[int(Warning)] = "Warning" +- namesMessageType[int(Info)] = "Info" +- namesMessageType[int(Log)] = "Log" +- +- namesFileChangeType[int(Created)] = "Created" +- namesFileChangeType[int(Changed)] = "Changed" +- namesFileChangeType[int(Deleted)] = "Deleted" +- +- namesWatchKind[int(WatchCreate)] = "WatchCreate" +- namesWatchKind[int(WatchChange)] = "WatchChange" +- namesWatchKind[int(WatchDelete)] = "WatchDelete" +- +- namesCompletionTriggerKind[int(Invoked)] = "Invoked" +- namesCompletionTriggerKind[int(TriggerCharacter)] = "TriggerCharacter" +- namesCompletionTriggerKind[int(TriggerForIncompleteCompletions)] = "TriggerForIncompleteCompletions" +- +- namesDiagnosticSeverity[int(SeverityError)] = "Error" +- namesDiagnosticSeverity[int(SeverityWarning)] = "Warning" +- namesDiagnosticSeverity[int(SeverityInformation)] = "Information" +- namesDiagnosticSeverity[int(SeverityHint)] = "Hint" +- +- namesDiagnosticTag[int(Unnecessary)] = "Unnecessary" +- +- namesCompletionItemKind[int(TextCompletion)] = "text" +- namesCompletionItemKind[int(MethodCompletion)] = "method" +- namesCompletionItemKind[int(FunctionCompletion)] = "func" +- namesCompletionItemKind[int(ConstructorCompletion)] = "constructor" +- namesCompletionItemKind[int(FieldCompletion)] = "field" +- namesCompletionItemKind[int(VariableCompletion)] = "var" +- namesCompletionItemKind[int(ClassCompletion)] = "type" +- namesCompletionItemKind[int(InterfaceCompletion)] = "interface" +- namesCompletionItemKind[int(ModuleCompletion)] = "package" +- namesCompletionItemKind[int(PropertyCompletion)] = "property" +- namesCompletionItemKind[int(UnitCompletion)] = "unit" +- namesCompletionItemKind[int(ValueCompletion)] = "value" +- namesCompletionItemKind[int(EnumCompletion)] = "enum" +- namesCompletionItemKind[int(KeywordCompletion)] = "keyword" +- namesCompletionItemKind[int(SnippetCompletion)] = "snippet" +- namesCompletionItemKind[int(ColorCompletion)] = "color" +- namesCompletionItemKind[int(FileCompletion)] = "file" +- namesCompletionItemKind[int(ReferenceCompletion)] = "reference" +- namesCompletionItemKind[int(FolderCompletion)] = "folder" +- namesCompletionItemKind[int(EnumMemberCompletion)] = "enumMember" +- namesCompletionItemKind[int(ConstantCompletion)] = "const" +- namesCompletionItemKind[int(StructCompletion)] = "struct" +- namesCompletionItemKind[int(EventCompletion)] = "event" +- namesCompletionItemKind[int(OperatorCompletion)] = "operator" +- namesCompletionItemKind[int(TypeParameterCompletion)] = "typeParam" +- +- namesInsertTextFormat[int(PlainTextTextFormat)] = "PlainText" +- namesInsertTextFormat[int(SnippetTextFormat)] = "Snippet" +- +- namesDocumentHighlightKind[int(Text)] = "Text" +- namesDocumentHighlightKind[int(Read)] = "Read" +- namesDocumentHighlightKind[int(Write)] = "Write" +- +- namesSymbolKind[int(File)] = "File" +- namesSymbolKind[int(Module)] = "Module" +- namesSymbolKind[int(Namespace)] = "Namespace" +- namesSymbolKind[int(Package)] = "Package" +- namesSymbolKind[int(Class)] = "Class" +- namesSymbolKind[int(Method)] = "Method" +- namesSymbolKind[int(Property)] = "Property" +- namesSymbolKind[int(Field)] = "Field" +- namesSymbolKind[int(Constructor)] = "Constructor" +- namesSymbolKind[int(Enum)] = "Enum" +- namesSymbolKind[int(Interface)] = "Interface" +- namesSymbolKind[int(Function)] = "Function" +- namesSymbolKind[int(Variable)] = "Variable" +- namesSymbolKind[int(Constant)] = "Constant" +- namesSymbolKind[int(String)] = "String" +- namesSymbolKind[int(Number)] = "Number" +- namesSymbolKind[int(Boolean)] = "Boolean" +- namesSymbolKind[int(Array)] = "Array" +- namesSymbolKind[int(Object)] = "Object" +- namesSymbolKind[int(Key)] = "Key" +- namesSymbolKind[int(Null)] = "Null" +- namesSymbolKind[int(EnumMember)] = "EnumMember" +- namesSymbolKind[int(Struct)] = "Struct" +- namesSymbolKind[int(Event)] = "Event" +- namesSymbolKind[int(Operator)] = "Operator" +- namesSymbolKind[int(TypeParameter)] = "TypeParameter" +- +- namesTextDocumentSaveReason[int(Manual)] = "Manual" +- namesTextDocumentSaveReason[int(AfterDelay)] = "AfterDelay" +- namesTextDocumentSaveReason[int(FocusOut)] = "FocusOut" +-} +- +-func formatEnum(f fmt.State, i int, names []string, unknown string) { +- s := "" +- if i >= 0 && i < len(names) { +- s = names[i] +- } +- if s != "" { +- fmt.Fprint(f, s) +- } else { +- fmt.Fprintf(f, "%s(%d)", unknown, i) +- } +-} +- +-func (e TextDocumentSyncKind) Format(f fmt.State, c rune) { +- formatEnum(f, int(e), namesTextDocumentSyncKind[:], "TextDocumentSyncKind") +-} +- +-func (e MessageType) Format(f fmt.State, c rune) { +- formatEnum(f, int(e), namesMessageType[:], "MessageType") +-} +- +-func (e FileChangeType) Format(f fmt.State, c rune) { +- formatEnum(f, int(e), namesFileChangeType[:], "FileChangeType") +-} +- +-func (e CompletionTriggerKind) Format(f fmt.State, c rune) { +- formatEnum(f, int(e), namesCompletionTriggerKind[:], "CompletionTriggerKind") +-} +- +-func (e DiagnosticSeverity) Format(f fmt.State, c rune) { +- formatEnum(f, int(e), namesDiagnosticSeverity[:], "DiagnosticSeverity") +-} +- +-func (e DiagnosticTag) Format(f fmt.State, c rune) { +- formatEnum(f, int(e), namesDiagnosticTag[:], "DiagnosticTag") +-} +- +-func (e CompletionItemKind) Format(f fmt.State, c rune) { +- formatEnum(f, int(e), namesCompletionItemKind[:], "CompletionItemKind") +-} +- +-func (e InsertTextFormat) Format(f fmt.State, c rune) { +- formatEnum(f, int(e), namesInsertTextFormat[:], "InsertTextFormat") +-} +- +-func (e DocumentHighlightKind) Format(f fmt.State, c rune) { +- formatEnum(f, int(e), namesDocumentHighlightKind[:], "DocumentHighlightKind") +-} +- +-func (e SymbolKind) Format(f fmt.State, c rune) { +- formatEnum(f, int(e), namesSymbolKind[:], "SymbolKind") +-} +- +-func (e TextDocumentSaveReason) Format(f fmt.State, c rune) { +- formatEnum(f, int(e), namesTextDocumentSaveReason[:], "TextDocumentSaveReason") +-} +diff -urN a/gopls/internal/protocol/generate/generate.go b/gopls/internal/protocol/generate/generate.go +--- a/gopls/internal/protocol/generate/generate.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/generate/generate.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,118 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package main +- +-import ( +- "bytes" +- "fmt" +- "log" +- "strings" +-) +- +-// a newType is a type that needs a name and a definition +-// These are the various types that the json specification doesn't name +-type newType struct { +- name string +- properties Properties // for struct/literal types +- items []*Type // for other types ("and", "tuple") +- line int +- kind string // Or, And, Tuple, Lit, Map +- typ *Type +-} +- +-func generateDoc(out *bytes.Buffer, doc string) { +- if doc == "" { +- return +- } +- +- if !strings.Contains(doc, "\n") { +- fmt.Fprintf(out, "// %s\n", doc) +- return +- } +- var list bool +- for _, line := range strings.Split(doc, "\n") { +- // Lists in metaModel.json start with a dash. +- // To make a go doc list they have to be preceded +- // by a blank line, and indented. +- // (see type TextDccumentFilter in protocol.go) +- if len(line) > 0 && line[0] == '-' { +- if !list { +- list = true +- fmt.Fprintf(out, "//\n") +- } +- fmt.Fprintf(out, "// %s\n", line) +- } else { +- if len(line) == 0 { +- list = false +- } +- fmt.Fprintf(out, "// %s\n", line) +- } +- } +-} +- +-// decide if a property is optional, and if it needs a * +-// return ",omitempty" if it is optional, and "*" if it needs a pointer +-func propStar(name string, t NameType, gotype string) (string, string) { +- var opt, star string +- if t.Optional { +- star = "*" +- opt = ",omitempty" +- } +- if strings.HasPrefix(gotype, "[]") || strings.HasPrefix(gotype, "map[") { +- star = "" // passed by reference, so no need for * +- } else { +- switch gotype { +- case "bool", "uint32", "int32", "string", "interface{}": +- star = "" // gopls compatibility if t.Optional +- } +- } +- ostar, oopt := star, opt +- if newStar, ok := goplsStar[prop{name, t.Name}]; ok { +- switch newStar { +- case nothing: +- star, opt = "", "" +- case wantStar: +- star, opt = "*", "" +- case wantOpt: +- star, opt = "", ",omitempty" +- case wantOptStar: +- star, opt = "*", ",omitempty" +- } +- if star == ostar && opt == oopt { // no change +- log.Printf("goplsStar[ {%q, %q} ](%d) useless %s/%s %s/%s", name, t.Name, t.Line, ostar, star, oopt, opt) +- } +- usedGoplsStar[prop{name, t.Name}] = true +- } +- +- return opt, star +-} +- +-func goName(s string) string { +- // Go naming conventions +- if strings.HasSuffix(s, "Id") { +- s = s[:len(s)-len("Id")] + "ID" +- } else if strings.HasSuffix(s, "Uri") { +- s = s[:len(s)-3] + "URI" +- } else if s == "uri" { +- s = "URI" +- } else if s == "id" { +- s = "ID" +- } +- +- // renames for temporary GOPLS compatibility +- if news := goplsType[s]; news != "" { +- usedGoplsType[s] = true +- s = news +- } +- // Names beginning _ are not exported +- if strings.HasPrefix(s, "_") { +- s = strings.Replace(s, "_", "X", 1) +- } +- if s != "string" { // base types are unchanged (textDocuemnt/diagnostic) +- // Title is deprecated, but a) s is only one word, b) replacement is too heavy-weight +- s = strings.Title(s) +- } +- return s +-} +diff -urN a/gopls/internal/protocol/generate/main.go b/gopls/internal/protocol/generate/main.go +--- a/gopls/internal/protocol/generate/main.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/generate/main.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,377 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-// The generate command generates Go declarations from VSCode's +-// description of the Language Server Protocol. +-// +-// To run it, type 'go generate' in the parent (protocol) directory. +-package main +- +-// see https://github.com/golang/go/issues/61217 for discussion of an issue +- +-import ( +- "bytes" +- "encoding/json" +- "flag" +- "fmt" +- "go/format" +- "log" +- "os" +- "os/exec" +- "path/filepath" +- "strings" +-) +- +-const vscodeRepo = "https://github.com/microsoft/vscode-languageserver-node" +- +-// lspGitRef names a branch or tag in vscodeRepo. +-// It implicitly determines the protocol version of the LSP used by gopls. +-// For example, tag release/protocol/3.17.3 of the repo defines protocol version 3.17.0. +-// (Point releases are reflected in the git tag version even when they are cosmetic +-// and don't change the protocol.) +-var lspGitRef = "release/protocol/3.17.6-next.2" +- +-var ( +- repodir = flag.String("d", "", "directory containing clone of "+vscodeRepo) +- outputdir = flag.String("o", ".", "output directory") +- // PJW: not for real code +- cmpdir = flag.String("c", "", "directory of earlier code") +- doboth = flag.String("b", "", "generate and compare") +- lineNumbers = flag.Bool("l", false, "add line numbers to generated output") +-) +- +-func main() { +- log.SetFlags(log.Lshortfile) // log file name and line number, not time +- flag.Parse() +- +- processinline() +-} +- +-func processinline() { +- // A local repository may be specified during debugging. +- // The default behavior is to download the canonical version. +- if *repodir == "" { +- tmpdir, err := os.MkdirTemp("", "") +- if err != nil { +- log.Fatal(err) +- } +- defer os.RemoveAll(tmpdir) // ignore error +- +- // Clone the repository. +- cmd := exec.Command("git", "clone", "--quiet", "--depth=1", "-c", "advice.detachedHead=false", vscodeRepo, "--branch="+lspGitRef, "--single-branch", tmpdir) +- cmd.Stdout = os.Stderr +- cmd.Stderr = os.Stderr +- if err := cmd.Run(); err != nil { +- log.Fatal(err) +- } +- +- *repodir = tmpdir +- } else { +- lspGitRef = fmt.Sprintf("(not git, local dir %s)", *repodir) +- } +- +- model := parse(filepath.Join(*repodir, "protocol/metaModel.json")) +- +- findTypeNames(model) +- generateOutput(model) +- +- fileHdr = fileHeader(model) +- +- // write the files +- writeclient() +- writeserver() +- writeprotocol() +- writejsons() +- +- checkTables() +-} +- +-// common file header for output files +-var fileHdr string +- +-func writeclient() { +- out := new(bytes.Buffer) +- fmt.Fprintln(out, fileHdr) +- out.WriteString( +- `import ( +- "context" +- +- "golang.org/x/tools/internal/jsonrpc2" +-) +-`) +- out.WriteString("type Client interface {\n") +- for _, k := range cdecls.keys() { +- out.WriteString(cdecls[k]) +- } +- out.WriteString("}\n\n") +- out.WriteString(`func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier, r jsonrpc2.Request) (bool, error) { +- defer recoverHandlerPanic(r.Method()) +- switch r.Method() { +-`) +- for _, k := range ccases.keys() { +- out.WriteString(ccases[k]) +- } +- out.WriteString(("\tdefault:\n\t\treturn false, nil\n\t}\n}\n\n")) +- for _, k := range cfuncs.keys() { +- out.WriteString(cfuncs[k]) +- } +- +- x, err := format.Source(out.Bytes()) +- if err != nil { +- os.WriteFile("/tmp/a.go", out.Bytes(), 0644) +- log.Fatalf("tsclient.go: %v", err) +- } +- +- if err := os.WriteFile(filepath.Join(*outputdir, "tsclient.go"), x, 0644); err != nil { +- log.Fatalf("%v writing tsclient.go", err) +- } +-} +- +-func writeserver() { +- out := new(bytes.Buffer) +- fmt.Fprintln(out, fileHdr) +- out.WriteString( +- `import ( +- "context" +- +- "golang.org/x/tools/internal/jsonrpc2" +-) +-`) +- out.WriteString("type Server interface {\n") +- for _, k := range sdecls.keys() { +- out.WriteString(sdecls[k]) +- } +- out.WriteString(` +-} +- +-func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, r jsonrpc2.Request) (bool, error) { +- defer recoverHandlerPanic(r.Method()) +- switch r.Method() { +-`) +- for _, k := range scases.keys() { +- out.WriteString(scases[k]) +- } +- out.WriteString(("\tdefault:\n\t\treturn false, nil\n\t}\n}\n\n")) +- for _, k := range sfuncs.keys() { +- out.WriteString(sfuncs[k]) +- } +- x, err := format.Source(out.Bytes()) +- if err != nil { +- os.WriteFile("/tmp/a.go", out.Bytes(), 0644) +- log.Fatalf("tsserver.go: %v", err) +- } +- +- if err := os.WriteFile(filepath.Join(*outputdir, "tsserver.go"), x, 0644); err != nil { +- log.Fatalf("%v writing tsserver.go", err) +- } +-} +- +-func writeprotocol() { +- out := new(bytes.Buffer) +- fmt.Fprintln(out, fileHdr) +- out.WriteString("import \"encoding/json\"\n\n") +- +- // The followiing are unneeded, but make the new code a superset of the old +- hack := func(newer, existing string) { +- if _, ok := types[existing]; !ok { +- log.Fatalf("types[%q] not found", existing) +- } +- types[newer] = strings.Replace(types[existing], existing, newer, 1) +- } +- hack("ConfigurationParams", "ParamConfiguration") +- hack("InitializeParams", "ParamInitialize") +- hack("PreviousResultId", "PreviousResultID") +- hack("WorkspaceFoldersServerCapabilities", "WorkspaceFolders5Gn") +- hack("_InitializeParams", "XInitializeParams") +- +- for _, k := range types.keys() { +- if k == "WatchKind" { +- types[k] = "type WatchKind = uint32" // strict gopls compatibility needs the '=' +- } +- out.WriteString(types[k]) +- } +- +- out.WriteString("\nconst (\n") +- for _, k := range consts.keys() { +- out.WriteString(consts[k]) +- } +- out.WriteString(")\n\n") +- x, err := format.Source(out.Bytes()) +- if err != nil { +- os.WriteFile("/tmp/a.go", out.Bytes(), 0644) +- log.Fatalf("tsprotocol.go: %v", err) +- } +- if err := os.WriteFile(filepath.Join(*outputdir, "tsprotocol.go"), x, 0644); err != nil { +- log.Fatalf("%v writing tsprotocol.go", err) +- } +-} +- +-func writejsons() { +- out := new(bytes.Buffer) +- fmt.Fprintln(out, fileHdr) +- out.WriteString("import \"encoding/json\"\n\n") +- out.WriteString("import \"fmt\"\n") +- +- out.WriteString(` +-// UnmarshalError indicates that a JSON value did not conform to +-// one of the expected cases of an LSP union type. +-type UnmarshalError struct { +- msg string +-} +- +-func (e UnmarshalError) Error() string { +- return e.msg +-} +-`) +- +- for _, k := range jsons.keys() { +- out.WriteString(jsons[k]) +- } +- x, err := format.Source(out.Bytes()) +- if err != nil { +- os.WriteFile("/tmp/a.go", out.Bytes(), 0644) +- log.Fatalf("tsjson.go: %v", err) +- } +- if err := os.WriteFile(filepath.Join(*outputdir, "tsjson.go"), x, 0644); err != nil { +- log.Fatalf("%v writing tsjson.go", err) +- } +-} +- +-// create the common file header for the output files +-func fileHeader(model Model) string { +- fname := filepath.Join(*repodir, ".git", "HEAD") +- buf, err := os.ReadFile(fname) +- if err != nil { +- log.Fatal(err) +- } +- buf = bytes.TrimSpace(buf) +- var githash string +- if len(buf) == 40 { +- githash = string(buf[:40]) +- } else if bytes.HasPrefix(buf, []byte("ref: ")) { +- fname = filepath.Join(*repodir, ".git", string(buf[5:])) +- buf, err = os.ReadFile(fname) +- if err != nil { +- log.Fatal(err) +- } +- githash = string(buf[:40]) +- } else { +- log.Fatalf("githash cannot be recovered from %s", fname) +- } +- +- format := `// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-// Code generated for LSP. DO NOT EDIT. +- +-package protocol +- +-// Code generated from %[1]s at ref %[2]s (hash %[3]s). +-// %[4]s/blob/%[2]s/%[1]s +-// LSP metaData.version = %[5]s. +- +-` +- return fmt.Sprintf(format, +- "protocol/metaModel.json", // 1 +- lspGitRef, // 2 +- githash, // 3 +- vscodeRepo, // 4 +- model.Version.Version) // 5 +-} +- +-func parse(fname string) Model { +- buf, err := os.ReadFile(fname) +- if err != nil { +- log.Fatal(err) +- } +- buf = addLineNumbers(buf) +- var model Model +- if err := json.Unmarshal(buf, &model); err != nil { +- log.Fatal(err) +- } +- return model +-} +- +-// Type.Value has to be treated specially for literals and maps +-func (t *Type) UnmarshalJSON(data []byte) error { +- // First unmarshal only the unambiguous fields. +- var x struct { +- Kind string `json:"kind"` +- Items []*Type `json:"items"` +- Element *Type `json:"element"` +- Name string `json:"name"` +- Key *Type `json:"key"` +- Value any `json:"value"` +- Line int `json:"line"` +- } +- if err := json.Unmarshal(data, &x); err != nil { +- return err +- } +- *t = Type{ +- Kind: x.Kind, +- Items: x.Items, +- Element: x.Element, +- Name: x.Name, +- Value: x.Value, +- Line: x.Line, +- } +- +- // Then unmarshal the 'value' field based on the kind. +- // This depends on Unmarshal ignoring fields it doesn't know about. +- switch x.Kind { +- case "map": +- var x struct { +- Key *Type `json:"key"` +- Value *Type `json:"value"` +- } +- if err := json.Unmarshal(data, &x); err != nil { +- return fmt.Errorf("Type.kind=map: %v", err) +- } +- t.Key = x.Key +- t.Value = x.Value +- +- case "literal": +- var z struct { +- Value ParseLiteral `json:"value"` +- } +- +- if err := json.Unmarshal(data, &z); err != nil { +- return fmt.Errorf("Type.kind=literal: %v", err) +- } +- t.Value = z.Value +- +- case "base", "reference", "array", "and", "or", "tuple", +- "stringLiteral": +- // no-op. never seen integerLiteral or booleanLiteral. +- +- default: +- return fmt.Errorf("cannot decode Type.kind %q: %s", x.Kind, data) +- } +- return nil +-} +- +-// which table entries were not used +-func checkTables() { +- for k := range disambiguate { +- if !usedDisambiguate[k] { +- log.Printf("disambiguate[%v] unused", k) +- } +- } +- for k := range renameProp { +- if !usedRenameProp[k] { +- log.Printf("renameProp {%q, %q} unused", k[0], k[1]) +- } +- } +- for k := range goplsStar { +- if !usedGoplsStar[k] { +- log.Printf("goplsStar {%q, %q} unused", k[0], k[1]) +- } +- } +- for k := range goplsType { +- if !usedGoplsType[k] { +- log.Printf("unused goplsType[%q]->%s", k, goplsType[k]) +- } +- } +-} +diff -urN a/gopls/internal/protocol/generate/main_test.go b/gopls/internal/protocol/generate/main_test.go +--- a/gopls/internal/protocol/generate/main_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/generate/main_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,116 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package main +- +-import ( +- "encoding/json" +- "fmt" +- "log" +- "os" +- "testing" +-) +- +-// These tests require the result of +-//"git clone https://github.com/microsoft/vscode-languageserver-node" in the HOME directory +- +-// this is not a test, but a way to get code coverage, +-// (in vscode, just run the test with "go.coverOnSingleTest": true) +-func TestAll(t *testing.T) { +- t.Skip("needs vscode-languageserver-node repository") +- *lineNumbers = true +- log.SetFlags(log.Lshortfile) +- main() +-} +- +-// check that the parsed file includes all the information +-// from the json file. This test will fail if the spec +-// introduces new fields. (one can test this test by +-// commenting out the version field in Model.) +-func TestParseContents(t *testing.T) { +- t.Skip("needs vscode-languageserver-node repository") +- log.SetFlags(log.Lshortfile) +- +- // compute our parse of the specification +- dir := os.Getenv("HOME") + "/vscode-languageserver-node" +- fname := dir + "/protocol/metaModel.json" +- v := parse(fname) +- out, err := json.Marshal(v) +- if err != nil { +- t.Fatal(err) +- } +- var our interface{} +- if err := json.Unmarshal(out, &our); err != nil { +- t.Fatal(err) +- } +- +- // process the json file +- buf, err := os.ReadFile(fname) +- if err != nil { +- t.Fatalf("could not read metaModel.json: %v", err) +- } +- var raw interface{} +- if err := json.Unmarshal(buf, &raw); err != nil { +- t.Fatal(err) +- } +- +- // convert to strings showing the fields +- them := flatten(raw) +- us := flatten(our) +- +- // everything in them should be in us +- lesser := make(sortedMap[bool]) +- for _, s := range them { +- lesser[s] = true +- } +- greater := make(sortedMap[bool]) // set of fields we have +- for _, s := range us { +- greater[s] = true +- } +- for _, k := range lesser.keys() { // set if fields they have +- if !greater[k] { +- t.Errorf("missing %s", k) +- } +- } +-} +- +-// flatten(nil) = "nil" +-// flatten(v string) = fmt.Sprintf("%q", v) +-// flatten(v float64)= fmt.Sprintf("%g", v) +-// flatten(v bool) = fmt.Sprintf("%v", v) +-// flatten(v []any) = []string{"[0]"flatten(v[0]), "[1]"flatten(v[1]), ...} +-// flatten(v map[string]any) = {"key1": flatten(v["key1"]), "key2": flatten(v["key2"]), ...} +-func flatten(x any) []string { +- switch v := x.(type) { +- case nil: +- return []string{"nil"} +- case string: +- return []string{fmt.Sprintf("%q", v)} +- case float64: +- return []string{fmt.Sprintf("%g", v)} +- case bool: +- return []string{fmt.Sprintf("%v", v)} +- case []any: +- var ans []string +- for i, x := range v { +- idx := fmt.Sprintf("[%.3d]", i) +- for _, s := range flatten(x) { +- ans = append(ans, idx+s) +- } +- } +- return ans +- case map[string]any: +- var ans []string +- for k, x := range v { +- idx := fmt.Sprintf("%q:", k) +- for _, s := range flatten(x) { +- ans = append(ans, idx+s) +- } +- } +- return ans +- default: +- log.Fatalf("unexpected type %T", x) +- return nil +- } +-} +diff -urN a/gopls/internal/protocol/generate/output.go b/gopls/internal/protocol/generate/output.go +--- a/gopls/internal/protocol/generate/output.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/generate/output.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,423 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package main +- +-import ( +- "bytes" +- "fmt" +- "log" +- "sort" +- "strings" +-) +- +-var ( +- // tsclient.go has 3 sections +- cdecls = make(sortedMap[string]) +- ccases = make(sortedMap[string]) +- cfuncs = make(sortedMap[string]) +- // tsserver.go has 3 sections +- sdecls = make(sortedMap[string]) +- scases = make(sortedMap[string]) +- sfuncs = make(sortedMap[string]) +- // tsprotocol.go has 2 sections +- types = make(sortedMap[string]) +- consts = make(sortedMap[string]) +- // tsjson has 1 section +- jsons = make(sortedMap[string]) +-) +- +-func generateOutput(model Model) { +- for _, r := range model.Requests { +- genDecl(r.Method, r.Params, r.Result, r.Direction) +- genCase(r.Method, r.Params, r.Result, r.Direction) +- genFunc(r.Method, r.Params, r.Result, r.Direction, false) +- } +- for _, n := range model.Notifications { +- if n.Method == "$/cancelRequest" { +- continue // handled internally by jsonrpc2 +- } +- genDecl(n.Method, n.Params, nil, n.Direction) +- genCase(n.Method, n.Params, nil, n.Direction) +- genFunc(n.Method, n.Params, nil, n.Direction, true) +- } +- genStructs(model) +- genAliases(model) +- genGenTypes() // generate the unnamed types +- genConsts(model) +- genMarshal() +-} +- +-func genDecl(method string, param, result *Type, dir string) { +- fname := methodName(method) +- p := "" +- if notNil(param) { +- p = ", *" + goplsName(param) +- } +- ret := "error" +- if notNil(result) { +- tp := goplsName(result) +- if !hasNilValue(tp) { +- tp = "*" + tp +- } +- ret = fmt.Sprintf("(%s, error)", tp) +- } +- // special gopls compatibility case (PJW: still needed?) +- switch method { +- case "workspace/configuration": +- // was And_Param_workspace_configuration, but the type substitution doesn't work, +- // as ParamConfiguration is embedded in And_Param_workspace_configuration +- p = ", *ParamConfiguration" +- ret = "([]LSPAny, error)" +- } +- msg := fmt.Sprintf("\t%s(context.Context%s) %s // %s\n", fname, p, ret, method) +- switch dir { +- case "clientToServer": +- sdecls[method] = msg +- case "serverToClient": +- cdecls[method] = msg +- case "both": +- sdecls[method] = msg +- cdecls[method] = msg +- default: +- log.Fatalf("impossible direction %q", dir) +- } +-} +- +-func genCase(method string, param, result *Type, dir string) { +- out := new(bytes.Buffer) +- fmt.Fprintf(out, "\tcase %q:\n", method) +- var p string +- fname := methodName(method) +- if notNil(param) { +- nm := goplsName(param) +- if method == "workspace/configuration" { // gopls compatibility +- // was And_Param_workspace_configuration, which contains ParamConfiguration +- // so renaming the type leads to circular definitions +- nm = "ParamConfiguration" // gopls compatibility +- } +- fmt.Fprintf(out, "\t\tvar params %s\n", nm) +- fmt.Fprintf(out, "\t\tif err := UnmarshalJSON(r.Params(), ¶ms); err != nil {\n") +- fmt.Fprintf(out, "\t\t\treturn true, sendParseError(ctx, reply, err)\n\t\t}\n") +- p = ", ¶ms" +- } +- if notNil(result) { +- fmt.Fprintf(out, "\t\tresp, err := %%s.%s(ctx%s)\n", fname, p) +- out.WriteString("\t\tif err != nil {\n") +- out.WriteString("\t\t\treturn true, reply(ctx, nil, err)\n") +- out.WriteString("\t\t}\n") +- out.WriteString("\t\treturn true, reply(ctx, resp, nil)\n") +- } else { +- fmt.Fprintf(out, "\t\terr := %%s.%s(ctx%s)\n", fname, p) +- out.WriteString("\t\treturn true, reply(ctx, nil, err)\n") +- } +- out.WriteString("\n") +- msg := out.String() +- switch dir { +- case "clientToServer": +- scases[method] = fmt.Sprintf(msg, "server") +- case "serverToClient": +- ccases[method] = fmt.Sprintf(msg, "client") +- case "both": +- scases[method] = fmt.Sprintf(msg, "server") +- ccases[method] = fmt.Sprintf(msg, "client") +- default: +- log.Fatalf("impossible direction %q", dir) +- } +-} +- +-func genFunc(method string, param, result *Type, dir string, isnotify bool) { +- out := new(bytes.Buffer) +- var p, r string +- var goResult string +- if notNil(param) { +- p = ", params *" + goplsName(param) +- } +- if notNil(result) { +- goResult = goplsName(result) +- if !hasNilValue(goResult) { +- goResult = "*" + goResult +- } +- r = fmt.Sprintf("(%s, error)", goResult) +- } else { +- r = "error" +- } +- // special gopls compatibility case +- switch method { +- case "workspace/configuration": +- // was And_Param_workspace_configuration, but the type substitution doesn't work, +- // as ParamConfiguration is embedded in And_Param_workspace_configuration +- p = ", params *ParamConfiguration" +- r = "([]LSPAny, error)" +- goResult = "[]LSPAny" +- } +- fname := methodName(method) +- fmt.Fprintf(out, "func (s *%%sDispatcher) %s(ctx context.Context%s) %s {\n", +- fname, p, r) +- +- if !notNil(result) { +- if isnotify { +- if notNil(param) { +- fmt.Fprintf(out, "\treturn s.sender.Notify(ctx, %q, params)\n", method) +- } else { +- fmt.Fprintf(out, "\treturn s.sender.Notify(ctx, %q, nil)\n", method) +- } +- } else { +- if notNil(param) { +- fmt.Fprintf(out, "\treturn s.sender.Call(ctx, %q, params, nil)\n", method) +- } else { +- fmt.Fprintf(out, "\treturn s.sender.Call(ctx, %q, nil, nil)\n", method) +- } +- } +- } else { +- fmt.Fprintf(out, "\tvar result %s\n", goResult) +- if isnotify { +- if notNil(param) { +- fmt.Fprintf(out, "\ts.sender.Notify(ctx, %q, params)\n", method) +- } else { +- fmt.Fprintf(out, "\t\tif err := s.sender.Notify(ctx, %q, nil); err != nil {\n", method) +- } +- } else { +- if notNil(param) { +- fmt.Fprintf(out, "\t\tif err := s.sender.Call(ctx, %q, params, &result); err != nil {\n", method) +- } else { +- fmt.Fprintf(out, "\t\tif err := s.sender.Call(ctx, %q, nil, &result); err != nil {\n", method) +- } +- } +- fmt.Fprintf(out, "\t\treturn nil, err\n\t}\n\treturn result, nil\n") +- } +- out.WriteString("}\n") +- msg := out.String() +- switch dir { +- case "clientToServer": +- sfuncs[method] = fmt.Sprintf(msg, "server") +- case "serverToClient": +- cfuncs[method] = fmt.Sprintf(msg, "client") +- case "both": +- sfuncs[method] = fmt.Sprintf(msg, "server") +- cfuncs[method] = fmt.Sprintf(msg, "client") +- default: +- log.Fatalf("impossible direction %q", dir) +- } +-} +- +-func genStructs(model Model) { +- structures := make(map[string]*Structure) // for expanding Extends +- for _, s := range model.Structures { +- structures[s.Name] = s +- } +- for _, s := range model.Structures { +- out := new(bytes.Buffer) +- generateDoc(out, s.Documentation) +- nm := goName(s.Name) +- if nm == "string" { // an unacceptable strut name +- // a weird case, and needed only so the generated code contains the old gopls code +- nm = "DocumentDiagnosticParams" +- } +- fmt.Fprintf(out, "type %s struct {%s\n", nm, linex(s.Line)) +- // for gpls compatibilitye, embed most extensions, but expand the rest some day +- props := append([]NameType{}, s.Properties...) +- if s.Name == "SymbolInformation" { // but expand this one +- for _, ex := range s.Extends { +- fmt.Fprintf(out, "\t// extends %s\n", ex.Name) +- props = append(props, structures[ex.Name].Properties...) +- } +- genProps(out, props, nm) +- } else { +- genProps(out, props, nm) +- for _, ex := range s.Extends { +- fmt.Fprintf(out, "\t%s\n", goName(ex.Name)) +- } +- } +- for _, ex := range s.Mixins { +- fmt.Fprintf(out, "\t%s\n", goName(ex.Name)) +- } +- out.WriteString("}\n") +- types[nm] = out.String() +- } +- +- // base types +- // (For URI and DocumentURI, see ../uri.go.) +- types["LSPAny"] = "type LSPAny = interface{}\n" +- // A special case, the only previously existing Or type +- types["DocumentDiagnosticReport"] = "type DocumentDiagnosticReport = Or_DocumentDiagnosticReport // (alias) \n" +- +-} +- +-func genProps(out *bytes.Buffer, props []NameType, name string) { +- for _, p := range props { +- tp := goplsName(p.Type) +- if newNm, ok := renameProp[prop{name, p.Name}]; ok { +- usedRenameProp[prop{name, p.Name}] = true +- if tp == newNm { +- log.Printf("renameProp useless {%q, %q} for %s", name, p.Name, tp) +- } +- tp = newNm +- } +- // it's a pointer if it is optional, or for gopls compatibility +- opt, star := propStar(name, p, tp) +- json := fmt.Sprintf(" `json:\"%s%s\"`", p.Name, opt) +- generateDoc(out, p.Documentation) +- fmt.Fprintf(out, "\t%s %s%s %s\n", goName(p.Name), star, tp, json) +- } +-} +- +-func genAliases(model Model) { +- for _, ta := range model.TypeAliases { +- out := new(bytes.Buffer) +- generateDoc(out, ta.Documentation) +- nm := goName(ta.Name) +- if nm != ta.Name { +- continue // renamed the type, e.g., "DocumentDiagnosticReport", an or-type to "string" +- } +- tp := goplsName(ta.Type) +- fmt.Fprintf(out, "type %s = %s // (alias)\n", nm, tp) +- types[nm] = out.String() +- } +-} +- +-func genGenTypes() { +- for _, nt := range genTypes { +- out := new(bytes.Buffer) +- nm := goplsName(nt.typ) +- switch nt.kind { +- case "literal": +- fmt.Fprintf(out, "// created for Literal (%s)\n", nt.name) +- fmt.Fprintf(out, "type %s struct {%s\n", nm, linex(nt.line+1)) +- genProps(out, nt.properties, nt.name) // systematic name, not gopls name; is this a good choice? +- case "or": +- if !strings.HasPrefix(nm, "Or") { +- // It was replaced by a narrower type defined elsewhere +- continue +- } +- names := []string{} +- for _, t := range nt.items { +- if notNil(t) { +- names = append(names, goplsName(t)) +- } +- } +- sort.Strings(names) +- fmt.Fprintf(out, "// created for Or %v\n", names) +- fmt.Fprintf(out, "type %s struct {%s\n", nm, linex(nt.line+1)) +- fmt.Fprintf(out, "\tValue interface{} `json:\"value\"`\n") +- case "and": +- fmt.Fprintf(out, "// created for And\n") +- fmt.Fprintf(out, "type %s struct {%s\n", nm, linex(nt.line+1)) +- for _, x := range nt.items { +- nm := goplsName(x) +- fmt.Fprintf(out, "\t%s\n", nm) +- } +- case "tuple": // there's only this one +- nt.name = "UIntCommaUInt" +- fmt.Fprintf(out, "//created for Tuple\ntype %s struct {%s\n", nm, linex(nt.line+1)) +- fmt.Fprintf(out, "\tFld0 uint32 `json:\"fld0\"`\n") +- fmt.Fprintf(out, "\tFld1 uint32 `json:\"fld1\"`\n") +- default: +- log.Fatalf("%s not handled", nt.kind) +- } +- out.WriteString("}\n") +- types[nm] = out.String() +- } +-} +-func genConsts(model Model) { +- for _, e := range model.Enumerations { +- out := new(bytes.Buffer) +- generateDoc(out, e.Documentation) +- tp := goplsName(e.Type) +- nm := goName(e.Name) +- fmt.Fprintf(out, "type %s %s%s\n", nm, tp, linex(e.Line)) +- types[nm] = out.String() +- vals := new(bytes.Buffer) +- generateDoc(vals, e.Documentation) +- for _, v := range e.Values { +- generateDoc(vals, v.Documentation) +- nm := goName(v.Name) +- more, ok := disambiguate[e.Name] +- if ok { +- usedDisambiguate[e.Name] = true +- nm = more.prefix + nm + more.suffix +- nm = goName(nm) // stringType +- } +- var val string +- switch v := v.Value.(type) { +- case string: +- val = fmt.Sprintf("%q", v) +- case float64: +- val = fmt.Sprintf("%d", int(v)) +- default: +- log.Fatalf("impossible type %T", v) +- } +- fmt.Fprintf(vals, "\t%s %s = %s%s\n", nm, e.Name, val, linex(v.Line)) +- } +- consts[nm] = vals.String() +- } +-} +-func genMarshal() { +- for _, nt := range genTypes { +- nm := goplsName(nt.typ) +- if !strings.HasPrefix(nm, "Or") { +- continue +- } +- names := []string{} +- for _, t := range nt.items { +- if notNil(t) { +- names = append(names, goplsName(t)) +- } +- } +- sort.Strings(names) +- var buf bytes.Buffer +- fmt.Fprintf(&buf, "func (t %s) MarshalJSON() ([]byte, error) {\n", nm) +- buf.WriteString("\tswitch x := t.Value.(type){\n") +- for _, nmx := range names { +- fmt.Fprintf(&buf, "\tcase %s:\n", nmx) +- fmt.Fprintf(&buf, "\t\treturn json.Marshal(x)\n") +- } +- buf.WriteString("\tcase nil:\n\t\treturn []byte(\"null\"), nil\n\t}\n") +- fmt.Fprintf(&buf, "\treturn nil, fmt.Errorf(\"type %%T not one of %v\", t)\n", names) +- buf.WriteString("}\n\n") +- +- fmt.Fprintf(&buf, "func (t *%s) UnmarshalJSON(x []byte) error {\n", nm) +- buf.WriteString("\tif string(x) == \"null\" {\n\t\tt.Value = nil\n\t\t\treturn nil\n\t}\n") +- for i, nmx := range names { +- fmt.Fprintf(&buf, "\tvar h%d %s\n", i, nmx) +- fmt.Fprintf(&buf, "\tif err := json.Unmarshal(x, &h%d); err == nil {\n\t\tt.Value = h%d\n\t\t\treturn nil\n\t\t}\n", i, i) +- } +- fmt.Fprintf(&buf, "return &UnmarshalError{\"unmarshal failed to match one of %v\"}", names) +- buf.WriteString("}\n\n") +- jsons[nm] = buf.String() +- } +-} +- +-func linex(n int) string { +- if *lineNumbers { +- return fmt.Sprintf(" // line %d", n) +- } +- return "" +-} +- +-func goplsName(t *Type) string { +- nm := typeNames[t] +- // translate systematic name to gopls name +- if newNm, ok := goplsType[nm]; ok { +- usedGoplsType[nm] = true +- nm = newNm +- } +- return nm +-} +- +-func notNil(t *Type) bool { // shutdwon is the special case that needs this +- return t != nil && (t.Kind != "base" || t.Name != "null") +-} +- +-func hasNilValue(t string) bool { +- // this may be unreliable, and need a supplementary table +- if strings.HasPrefix(t, "[]") || strings.HasPrefix(t, "*") { +- return true +- } +- if t == "interface{}" || t == "any" { +- return true +- } +- // that's all the cases that occur currently +- return false +-} +diff -urN a/gopls/internal/protocol/generate/README.md b/gopls/internal/protocol/generate/README.md +--- a/gopls/internal/protocol/generate/README.md 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/generate/README.md 1970-01-01 00:00:00.000000000 +0000 +@@ -1,144 +0,0 @@ +-# LSP Support for gopls +- +-## The protocol +- +-The LSP protocol exchanges json-encoded messages between the client and the server. +-(gopls is the server.) The messages are either Requests, which require Responses, or +-Notifications, which generate no response. Each Request or Notification has a method name +-such as "textDocument/hover" that indicates its meaning and determines which function in the server will handle it. +-The protocol is described in a +-[web page](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.18/specification/), +-in words, and in a json file (metaModel.json) available either linked towards the bottom of the +-web page, or in the vscode-languageserver-node repository. This code uses the latter so the +-exact version can be tied to a githash. By default, the command will download the `github.com/microsoft/vscode-languageserver-node` repository to a temporary directory. +- +-The specification has five sections +- +-1. Requests, which describe the Request and Response types for request methods (e.g., *textDocument/didChange*), +-2. Notifications, which describe the Request types for notification methods, +-3. Structures, which describe named struct-like types, +-4. TypeAliases, which describe type aliases, +-5. Enumerations, which describe named constants. +- +-Requests and Notifications are tagged with a Method (e.g., `"textDocument/hover"`). +-The specification does not specify the names of the functions that handle the messages. These +-names are specified by the `methodNames` map. Enumerations generate Go `const`s, but +-in Typescript they are scoped to namespaces, while in Go they are scoped to a package, so the Go names +-may need to be modified to avoid name collisions. (See the `disambiguate` map, and its use.) +- +-Finally, the specified types are Typescript types, which are quite different from Go types. +- +-### Optionality +- +-The specification can mark fields in structs as Optional. The client distinguishes between missing +-fields and `null` fields in some cases. The Go translation for an optional type +-should be making sure the field's value +-can be `nil`, and adding the json tag `,omitempty`. The former condition would be satisfied by +-adding `*` to the field's type if the type is not a reference type. +- +-### Types +- +-The specification uses a number of different types, only a few of which correspond directly to Go types. +-The specification's types are "base", "reference", "map", "literal", "stringLiteral", "tuple", "and", "or". +-The "base" types correspond directly to Go types, although some Go types needs to be chosen for `URI` and `DocumentUri`. (The "base" types`RegExp`, `BooleanLiteral`, `NumericLiteral` never occur.) +- +-"reference" types are the struct-like types in the Structures section of the specification. The given +-names are suitable for Go to use, except the code needs to change names like `_Initialze` to `XInitialize` so +-they are exported for json marshaling and unmarshaling. +- +-"map" types are just like Go. (The key type in all of them is `DocumentUri`.) +- +-"stringLiteral" types are types whose type name and value are a single string. The chosen Go equivalent +-is to make the type `string` and the value a constant. (The alternative would be to generate a new +-named type, which seemed redundant.) +- +-"literal" types are like Go anonymous structs, so they have to be given a name. (All instances +-of the remaining types have to be given names. One approach is to construct the name from the components +-of the type, but this leads to misleading punning, and is unstable if components are added. The other approach +-is to construct the name from the context of the definition, that is, from the types it is defined within. +-For instance `Lit__InitializeParams_clientInfo` is the "literal" type at the +-`clientInfo` field in the `_InitializeParams` +-struct. Although this choice is sensitive to the ordering of the components, the code uses this approach, +-presuming that reordering components is an unlikely protocol change.) +- +-"tuple" types are generated as Go structs. (There is only one, with two `uint32` fields.) +- +-"and" types are Go structs with embedded type names. (There is only one, `And_Param_workspace_configuration`.) +- +-"or" types are the most complicated. There are a lot of them and there is no simple Go equivalent. +-They are defined as structs with a single `Value interface{}` field and custom json marshaling +-and unmarshaling code. Users can assign anything to `Value` but the type will be checked, and +-correctly marshaled, by the custom marshaling code. The unmarshaling code checks types, so `Value` +-will have one of the permitted types. (`nil` is always allowed.) There are about 40 "or" types that +-have a single non-null component, and these are converted to the component type. +- +-## Processing +- +-The code parses the json specification file, and scans all the types. It assigns names, as described +-above, to the types that are unnamed in the specification, and constructs Go equivalents as required. +-(Most of this code is in typenames.go.) +- +-There are four output files. tsclient.go and tsserver.go contain the definition and implementation +-of the `protocol.Client` and `protocol.Server` types and the code that dispatches on the Method +-of the Request or Notification. tsjson.go contains the custom marshaling and unmarshaling code. +-And tsprotocol.go contains the type and const definitions. +- +-### Accommodating gopls +- +-As the code generates output, mostly in generateoutput.go and main.go, +-it makes adjustments so that no changes are required to the existing Go code. +-(Organizing the computation this way makes the code's structure simpler, but results in +-a lot of unused types.) +-There are three major classes of these adjustments, and leftover special cases. +- +-The first major +-adjustment is to change generated type names to the ones gopls expects. Some of these don't change the +-semantics of the type, just the name. +-But for historical reasons a lot of them replace "or" types by a single +-component of the type. (Until fairly recently Go only saw or used only one of components.) +-The `goplsType` map in tables.go controls this process. +- +-The second major adjustment is to the types of fields of structs, which is done using the +-`renameProp` map in tables.go. +- +-The third major adjustment handles optionality, controlling `*` and `,omitempty` placement when +-the default rules don't match what gopls is expecting. (The map is `goplsStar`, also in tables.go) +-(If the intermediate components in expressions of the form `A.B.C.S` were optional, the code would need +-a lot of useless checking for nils. Typescript has a language construct to avoid most checks.) +- +-Then there are some additional special cases. There are a few places with adjustments to avoid +-recursive types. For instance `LSPArray` is `[]LSPAny`, but `LSPAny` is an "or" type including `LSPArray`. +-The solution is to make `LSPAny` an `interface{}`. Another instance is `_InitializeParams.trace` +-whose type is an "or" of 3 stringLiterals, which just becomes a `string`. +- +-### Checking +- +-`TestAll(t *testing.T)` checks that there are no unexpected fields in the json specification. +- +-While the code is executing, it checks that all the entries in the maps in tables.go are used. +-It also checks that the entries in `renameProp` and `goplsStar` are not redundant. +- +-As a one-time check on the first release of this code, diff-ing the existing and generated tsclient.go +-and tsserver.go code results in only whitespace and comment diffs. The existing and generated +-tsprotocol.go differ in whitespace and comments, and in a substantial number of new type definitions +-that the older, more heuristic, code did not generate. (And the unused type `_InitializeParams` differs +-slightly between the new and the old, and is not worth fixing.) +- +-### Some history +- +-The original stub code was written by hand, but with the protocol under active development, that +-couldn't last. The web page existed before the json specification, but it lagged the implementation +-and was hard to process by machine. So the earlier version of the generating code was written in Typescript, and +-used the Typescript compiler's API to parse the protocol code in the repository. +-It then used a set of heuristics +-to pick out the elements of the protocol, and another set of overlapping heuristics to create the Go code. +-The output was functional, but idiosyncratic, and the code was fragile and barely maintainable. +- +-### The future +- +-Most of the adjustments using the maps in tables.go could be removed by making changes, mostly to names, +-in the gopls code. Using more "or" types in gopls requires more elaborate, but stereotyped, changes. +-But even without all the adjustments, making this its own module would face problems; a number of +-dependencies would have to be factored out. And, it is fragile. The custom unmarshaling code knows what +-types it expects. A design that return an 'any' on unexpected types would match the json +-'ignore unexpected values' philosophy better, but the Go code would need extra checking. +diff -urN a/gopls/internal/protocol/generate/tables.go b/gopls/internal/protocol/generate/tables.go +--- a/gopls/internal/protocol/generate/tables.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/generate/tables.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,275 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package main +- +-import "log" +- +-// prop combines the name of a property with the name of the structure it is in. +-type prop [2]string +- +-const ( +- nothing = iota +- wantStar +- wantOpt +- wantOptStar +-) +- +-// goplsStar records the optionality of each field in the protocol. +-// The comments are vague hints as to why removing the line is not trivial. +-// A.B.C.D means that one of B or C would change to a pointer +-// so a test or initialization would be needed +-var goplsStar = map[prop]int{ +- {"AnnotatedTextEdit", "annotationId"}: wantOptStar, +- {"ClientCapabilities", "textDocument"}: wantOpt, // A.B.C.D at fake/editor.go:255 +- {"ClientCapabilities", "window"}: wantOpt, // test failures +- {"ClientCapabilities", "workspace"}: wantOpt, // test failures +- {"CodeAction", "kind"}: wantOpt, // A.B.C.D +- +- {"CodeActionClientCapabilities", "codeActionLiteralSupport"}: wantOpt, // test failures +- +- {"CompletionClientCapabilities", "completionItem"}: wantOpt, // A.B.C.D +- {"CompletionClientCapabilities", "insertTextMode"}: wantOpt, // A.B.C.D +- {"CompletionItem", "kind"}: wantOpt, // need temporary variables +- {"CompletionParams", "context"}: wantOpt, // needs nil checks +- +- {"Diagnostic", "severity"}: wantOpt, // nil checks or more careful thought +- {"DidSaveTextDocumentParams", "text"}: wantOptStar, // capabilities_test.go:112 logic +- {"DocumentHighlight", "kind"}: wantOpt, // need temporary variables +- {"Hover", "range"}: wantOpt, // complex expressions +- {"InlayHint", "kind"}: wantOpt, // temporary variables +- +- {"TextDocumentClientCapabilities", "codeAction"}: wantOpt, // A.B.C.D +- {"TextDocumentClientCapabilities", "completion"}: wantOpt, // A.B.C.D +- {"TextDocumentClientCapabilities", "documentSymbol"}: wantOpt, // A.B.C.D +- {"TextDocumentClientCapabilities", "publishDiagnostics"}: wantOpt, //A.B.C.D +- {"TextDocumentClientCapabilities", "semanticTokens"}: wantOpt, // A.B.C.D +- {"TextDocumentContentChangePartial", "range"}: wantOptStar, // == nil test +- {"TextDocumentSyncOptions", "change"}: wantOpt, // &constant +- {"WorkDoneProgressParams", "workDoneToken"}: wantOpt, // test failures +- {"WorkspaceClientCapabilities", "didChangeConfiguration"}: wantOpt, // A.B.C.D +- {"WorkspaceClientCapabilities", "didChangeWatchedFiles"}: wantOpt, // A.B.C.D +-} +- +-// keep track of which entries in goplsStar are used +-var usedGoplsStar = make(map[prop]bool) +- +-// For gopls compatibility, use a different, typically more restrictive, type for some fields. +-var renameProp = map[prop]string{ +- {"CancelParams", "id"}: "interface{}", +- {"Command", "arguments"}: "[]json.RawMessage", +- {"CompletionItem", "textEdit"}: "TextEdit", +- {"CodeAction", "data"}: "json.RawMessage", // delay unmarshalling commands +- {"Diagnostic", "code"}: "interface{}", +- {"Diagnostic", "data"}: "json.RawMessage", // delay unmarshalling quickfixes +- +- {"DocumentDiagnosticReportPartialResult", "relatedDocuments"}: "map[DocumentURI]interface{}", +- +- {"ExecuteCommandParams", "arguments"}: "[]json.RawMessage", +- {"FoldingRange", "kind"}: "string", +- {"Hover", "contents"}: "MarkupContent", +- {"InlayHint", "label"}: "[]InlayHintLabelPart", +- +- {"RelatedFullDocumentDiagnosticReport", "relatedDocuments"}: "map[DocumentURI]interface{}", +- {"RelatedUnchangedDocumentDiagnosticReport", "relatedDocuments"}: "map[DocumentURI]interface{}", +- +- // PJW: this one is tricky. +- {"ServerCapabilities", "codeActionProvider"}: "interface{}", +- +- {"ServerCapabilities", "inlayHintProvider"}: "interface{}", +- // slightly tricky +- {"ServerCapabilities", "renameProvider"}: "interface{}", +- // slightly tricky +- {"ServerCapabilities", "semanticTokensProvider"}: "interface{}", +- // slightly tricky +- {"ServerCapabilities", "textDocumentSync"}: "interface{}", +- {"TextDocumentSyncOptions", "save"}: "SaveOptions", +- {"WorkspaceEdit", "documentChanges"}: "[]DocumentChanges", +-} +- +-// which entries of renameProp were used +-var usedRenameProp = make(map[prop]bool) +- +-type adjust struct { +- prefix, suffix string +-} +- +-// disambiguate specifies prefixes or suffixes to add to all values of +-// some enum types to avoid name conflicts +-var disambiguate = map[string]adjust{ +- "CodeActionTriggerKind": {"CodeAction", ""}, +- "CompletionItemKind": {"", "Completion"}, +- "CompletionItemTag": {"Compl", ""}, +- "DiagnosticSeverity": {"Severity", ""}, +- "DocumentDiagnosticReportKind": {"Diagnostic", ""}, +- "FileOperationPatternKind": {"", "Pattern"}, +- "InlineCompletionTriggerKind": {"Inline", ""}, +- "InsertTextFormat": {"", "TextFormat"}, +- "LanguageKind": {"Lang", ""}, +- "SemanticTokenModifiers": {"Mod", ""}, +- "SemanticTokenTypes": {"", "Type"}, +- "SignatureHelpTriggerKind": {"Sig", ""}, +- "SymbolTag": {"", "Symbol"}, +- "WatchKind": {"Watch", ""}, +-} +- +-// which entries of disambiguate got used +-var usedDisambiguate = make(map[string]bool) +- +-// for gopls compatibility, replace generated type names with existing ones +-var goplsType = map[string]string{ +- "And_RegOpt_textDocument_colorPresentation": "WorkDoneProgressOptionsAndTextDocumentRegistrationOptions", +- "ConfigurationParams": "ParamConfiguration", +- "DocumentDiagnosticParams": "string", +- "DocumentDiagnosticReport": "string", +- "DocumentUri": "DocumentURI", +- "InitializeParams": "ParamInitialize", +- "LSPAny": "interface{}", +- +- "Lit_SemanticTokensOptions_range_Item1": "PRangeESemanticTokensOptions", +- +- "Or_Declaration": "[]Location", +- "Or_DidChangeConfigurationRegistrationOptions_section": "OrPSection_workspace_didChangeConfiguration", +- "Or_InlayHintLabelPart_tooltip": "OrPTooltipPLabel", +- "Or_InlayHint_tooltip": "OrPTooltip_textDocument_inlayHint", +- "Or_LSPAny": "interface{}", +- +- "Or_ParameterInformation_documentation": "string", +- "Or_ParameterInformation_label": "string", +- "Or_PrepareRenameResult": "PrepareRenamePlaceholder", +- "Or_ProgressToken": "interface{}", +- "Or_Result_textDocument_completion": "CompletionList", +- "Or_Result_textDocument_declaration": "Or_textDocument_declaration", +- "Or_Result_textDocument_definition": "[]Location", +- "Or_Result_textDocument_documentSymbol": "[]interface{}", +- "Or_Result_textDocument_implementation": "[]Location", +- "Or_Result_textDocument_semanticTokens_full_delta": "interface{}", +- "Or_Result_textDocument_typeDefinition": "[]Location", +- "Or_Result_workspace_symbol": "[]SymbolInformation", +- "Or_TextDocumentContentChangeEvent": "TextDocumentContentChangePartial", +- "Or_RelativePattern_baseUri": "DocumentURI", +- +- "Or_WorkspaceFoldersServerCapabilities_changeNotifications": "string", +- "Or_WorkspaceSymbol_location": "OrPLocation_workspace_symbol", +- +- "Tuple_ParameterInformation_label_Item1": "UIntCommaUInt", +- "WorkspaceFoldersServerCapabilities": "WorkspaceFolders5Gn", +- "[]LSPAny": "[]interface{}", +- +- "[]Or_Result_textDocument_codeAction_Item0_Elem": "[]CodeAction", +- "[]PreviousResultId": "[]PreviousResultID", +- "[]uinteger": "[]uint32", +- "boolean": "bool", +- "decimal": "float64", +- "integer": "int32", +- "map[DocumentUri][]TextEdit": "map[DocumentURI][]TextEdit", +- "uinteger": "uint32", +-} +- +-var usedGoplsType = make(map[string]bool) +- +-// methodNames is a map from the method to the name of the function that handles it +-var methodNames = map[string]string{ +- "$/cancelRequest": "CancelRequest", +- "$/logTrace": "LogTrace", +- "$/progress": "Progress", +- "$/setTrace": "SetTrace", +- "callHierarchy/incomingCalls": "IncomingCalls", +- "callHierarchy/outgoingCalls": "OutgoingCalls", +- "client/registerCapability": "RegisterCapability", +- "client/unregisterCapability": "UnregisterCapability", +- "codeAction/resolve": "ResolveCodeAction", +- "codeLens/resolve": "ResolveCodeLens", +- "completionItem/resolve": "ResolveCompletionItem", +- "documentLink/resolve": "ResolveDocumentLink", +- "exit": "Exit", +- "initialize": "Initialize", +- "initialized": "Initialized", +- "inlayHint/resolve": "Resolve", +- "notebookDocument/didChange": "DidChangeNotebookDocument", +- "notebookDocument/didClose": "DidCloseNotebookDocument", +- "notebookDocument/didOpen": "DidOpenNotebookDocument", +- "notebookDocument/didSave": "DidSaveNotebookDocument", +- "shutdown": "Shutdown", +- "telemetry/event": "Event", +- "textDocument/codeAction": "CodeAction", +- "textDocument/codeLens": "CodeLens", +- "textDocument/colorPresentation": "ColorPresentation", +- "textDocument/completion": "Completion", +- "textDocument/declaration": "Declaration", +- "textDocument/definition": "Definition", +- "textDocument/diagnostic": "Diagnostic", +- "textDocument/didChange": "DidChange", +- "textDocument/didClose": "DidClose", +- "textDocument/didOpen": "DidOpen", +- "textDocument/didSave": "DidSave", +- "textDocument/documentColor": "DocumentColor", +- "textDocument/documentHighlight": "DocumentHighlight", +- "textDocument/documentLink": "DocumentLink", +- "textDocument/documentSymbol": "DocumentSymbol", +- "textDocument/foldingRange": "FoldingRange", +- "textDocument/formatting": "Formatting", +- "textDocument/hover": "Hover", +- "textDocument/implementation": "Implementation", +- "textDocument/inlayHint": "InlayHint", +- "textDocument/inlineCompletion": "InlineCompletion", +- "textDocument/inlineValue": "InlineValue", +- "textDocument/linkedEditingRange": "LinkedEditingRange", +- "textDocument/moniker": "Moniker", +- "textDocument/onTypeFormatting": "OnTypeFormatting", +- "textDocument/prepareCallHierarchy": "PrepareCallHierarchy", +- "textDocument/prepareRename": "PrepareRename", +- "textDocument/prepareTypeHierarchy": "PrepareTypeHierarchy", +- "textDocument/publishDiagnostics": "PublishDiagnostics", +- "textDocument/rangeFormatting": "RangeFormatting", +- "textDocument/rangesFormatting": "RangesFormatting", +- "textDocument/references": "References", +- "textDocument/rename": "Rename", +- "textDocument/selectionRange": "SelectionRange", +- "textDocument/semanticTokens/full": "SemanticTokensFull", +- "textDocument/semanticTokens/full/delta": "SemanticTokensFullDelta", +- "textDocument/semanticTokens/range": "SemanticTokensRange", +- "textDocument/signatureHelp": "SignatureHelp", +- "textDocument/typeDefinition": "TypeDefinition", +- "textDocument/willSave": "WillSave", +- "textDocument/willSaveWaitUntil": "WillSaveWaitUntil", +- "typeHierarchy/subtypes": "Subtypes", +- "typeHierarchy/supertypes": "Supertypes", +- "window/logMessage": "LogMessage", +- "window/showDocument": "ShowDocument", +- "window/showMessage": "ShowMessage", +- "window/showMessageRequest": "ShowMessageRequest", +- "window/workDoneProgress/cancel": "WorkDoneProgressCancel", +- "window/workDoneProgress/create": "WorkDoneProgressCreate", +- "workspace/applyEdit": "ApplyEdit", +- "workspace/codeLens/refresh": "CodeLensRefresh", +- "workspace/configuration": "Configuration", +- "workspace/diagnostic": "DiagnosticWorkspace", +- "workspace/diagnostic/refresh": "DiagnosticRefresh", +- "workspace/didChangeConfiguration": "DidChangeConfiguration", +- "workspace/didChangeWatchedFiles": "DidChangeWatchedFiles", +- "workspace/didChangeWorkspaceFolders": "DidChangeWorkspaceFolders", +- "workspace/didCreateFiles": "DidCreateFiles", +- "workspace/didDeleteFiles": "DidDeleteFiles", +- "workspace/didRenameFiles": "DidRenameFiles", +- "workspace/executeCommand": "ExecuteCommand", +- "workspace/foldingRange/refresh": "FoldingRangeRefresh", +- "workspace/inlayHint/refresh": "InlayHintRefresh", +- "workspace/inlineValue/refresh": "InlineValueRefresh", +- "workspace/semanticTokens/refresh": "SemanticTokensRefresh", +- "workspace/symbol": "Symbol", +- "workspace/willCreateFiles": "WillCreateFiles", +- "workspace/willDeleteFiles": "WillDeleteFiles", +- "workspace/willRenameFiles": "WillRenameFiles", +- "workspace/workspaceFolders": "WorkspaceFolders", +- "workspaceSymbol/resolve": "ResolveWorkspaceSymbol", +-} +- +-func methodName(method string) string { +- ans := methodNames[method] +- if ans == "" { +- log.Fatalf("unknown method %q", method) +- } +- return ans +-} +diff -urN a/gopls/internal/protocol/generate/typenames.go b/gopls/internal/protocol/generate/typenames.go +--- a/gopls/internal/protocol/generate/typenames.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/generate/typenames.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,181 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package main +- +-import ( +- "fmt" +- "log" +- "strings" +-) +- +-var typeNames = make(map[*Type]string) +-var genTypes []*newType +- +-func findTypeNames(model Model) { +- for _, s := range model.Structures { +- for _, e := range s.Extends { +- nameType(e, nil) // all references +- } +- for _, m := range s.Mixins { +- nameType(m, nil) // all references +- } +- for _, p := range s.Properties { +- nameType(p.Type, []string{s.Name, p.Name}) +- } +- } +- for _, t := range model.Enumerations { +- nameType(t.Type, []string{t.Name}) +- } +- for _, t := range model.TypeAliases { +- nameType(t.Type, []string{t.Name}) +- } +- for _, r := range model.Requests { +- nameType(r.Params, []string{"Param", r.Method}) +- nameType(r.Result, []string{"Result", r.Method}) +- nameType(r.RegistrationOptions, []string{"RegOpt", r.Method}) +- } +- for _, n := range model.Notifications { +- nameType(n.Params, []string{"Param", n.Method}) +- nameType(n.RegistrationOptions, []string{"RegOpt", n.Method}) +- } +-} +- +-// nameType populates typeNames[t] with the computed name of the type. +-// path is the list of enclosing constructs in the JSON model. +-func nameType(t *Type, path []string) string { +- if t == nil || typeNames[t] != "" { +- return "" +- } +- switch t.Kind { +- case "base": +- typeNames[t] = t.Name +- return t.Name +- case "reference": +- typeNames[t] = t.Name +- return t.Name +- case "array": +- nm := "[]" + nameType(t.Element, append(path, "Elem")) +- typeNames[t] = nm +- return nm +- case "map": +- key := nameType(t.Key, nil) // never a generated type +- value := nameType(t.Value.(*Type), append(path, "Value")) +- nm := "map[" + key + "]" + value +- typeNames[t] = nm +- return nm +- // generated types +- case "and": +- nm := nameFromPath("And", path) +- typeNames[t] = nm +- for _, it := range t.Items { +- nameType(it, append(path, "Item")) +- } +- genTypes = append(genTypes, &newType{ +- name: nm, +- typ: t, +- kind: "and", +- items: t.Items, +- line: t.Line, +- }) +- return nm +- case "literal": +- nm := nameFromPath("Lit", path) +- typeNames[t] = nm +- for _, p := range t.Value.(ParseLiteral).Properties { +- nameType(p.Type, append(path, p.Name)) +- } +- genTypes = append(genTypes, &newType{ +- name: nm, +- typ: t, +- kind: "literal", +- properties: t.Value.(ParseLiteral).Properties, +- line: t.Line, +- }) +- return nm +- case "tuple": +- nm := nameFromPath("Tuple", path) +- typeNames[t] = nm +- for _, it := range t.Items { +- nameType(it, append(path, "Item")) +- } +- genTypes = append(genTypes, &newType{ +- name: nm, +- typ: t, +- kind: "tuple", +- items: t.Items, +- line: t.Line, +- }) +- return nm +- case "or": +- nm := nameFromPath("Or", path) +- typeNames[t] = nm +- for i, it := range t.Items { +- // these names depend on the ordering within the "or" type +- nameType(it, append(path, fmt.Sprintf("Item%d", i))) +- } +- // this code handles an "or" of stringLiterals (_InitializeParams.trace) +- names := make(map[string]int) +- msg := "" +- for _, it := range t.Items { +- if line, ok := names[typeNames[it]]; ok { +- // duplicate component names are bad +- msg += fmt.Sprintf("lines %d %d dup, %s for %s\n", line, it.Line, typeNames[it], nm) +- } +- names[typeNames[it]] = t.Line +- } +- // this code handles an "or" of stringLiterals (_InitializeParams.trace) +- if len(names) == 1 { +- var solekey string +- for k := range names { +- solekey = k // the sole name +- } +- if solekey == "string" { // _InitializeParams.trace +- typeNames[t] = "string" +- return "string" +- } +- // otherwise unexpected +- log.Printf("unexpected: single-case 'or' type has non-string key %s: %s", nm, solekey) +- log.Fatal(msg) +- } else if len(names) == 2 { +- // if one of the names is null, just use the other, rather than generating an "or". +- // This removes about 40 types from the generated code. An entry in goplsStar +- // could be added to handle the null case, if necessary. +- newNm := "" +- sawNull := false +- for k := range names { +- if k == "null" { +- sawNull = true +- } else { +- newNm = k +- } +- } +- if sawNull { +- typeNames[t] = newNm +- return newNm +- } +- } +- genTypes = append(genTypes, &newType{ +- name: nm, +- typ: t, +- kind: "or", +- items: t.Items, +- line: t.Line, +- }) +- return nm +- case "stringLiteral": // a single type, like 'kind' or 'rename' +- typeNames[t] = "string" +- return "string" +- default: +- log.Fatalf("nameType: %T unexpected, line:%d path:%v", t, t.Line, path) +- panic("unreachable in nameType") +- } +-} +- +-func nameFromPath(prefix string, path []string) string { +- nm := prefix + "_" + strings.Join(path, "_") +- // methods have slashes +- nm = strings.ReplaceAll(nm, "/", "_") +- return nm +-} +diff -urN a/gopls/internal/protocol/generate/types.go b/gopls/internal/protocol/generate/types.go +--- a/gopls/internal/protocol/generate/types.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/generate/types.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,167 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package main +- +-import ( +- "fmt" +- "sort" +-) +- +-// Model contains the parsed version of the spec +-type Model struct { +- Version Metadata `json:"metaData"` +- Requests []*Request `json:"requests"` +- Notifications []*Notification `json:"notifications"` +- Structures []*Structure `json:"structures"` +- Enumerations []*Enumeration `json:"enumerations"` +- TypeAliases []*TypeAlias `json:"typeAliases"` +- Line int `json:"line"` +-} +- +-// Metadata is information about the version of the spec +-type Metadata struct { +- Version string `json:"version"` +- Line int `json:"line"` +-} +- +-// A Request is the parsed version of an LSP request +-type Request struct { +- Documentation string `json:"documentation"` +- ErrorData *Type `json:"errorData"` +- Direction string `json:"messageDirection"` +- Method string `json:"method"` +- Params *Type `json:"params"` +- PartialResult *Type `json:"partialResult"` +- Proposed bool `json:"proposed"` +- RegistrationMethod string `json:"registrationMethod"` +- RegistrationOptions *Type `json:"registrationOptions"` +- Result *Type `json:"result"` +- Since string `json:"since"` +- Line int `json:"line"` +-} +- +-// A Notificatin is the parsed version of an LSP notification +-type Notification struct { +- Documentation string `json:"documentation"` +- Direction string `json:"messageDirection"` +- Method string `json:"method"` +- Params *Type `json:"params"` +- Proposed bool `json:"proposed"` +- RegistrationMethod string `json:"registrationMethod"` +- RegistrationOptions *Type `json:"registrationOptions"` +- Since string `json:"since"` +- Line int `json:"line"` +-} +- +-// A Structure is the parsed version of an LSP structure from the spec +-type Structure struct { +- Documentation string `json:"documentation"` +- Extends []*Type `json:"extends"` +- Mixins []*Type `json:"mixins"` +- Name string `json:"name"` +- Properties []NameType `json:"properties"` +- Proposed bool `json:"proposed"` +- Since string `json:"since"` +- Line int `json:"line"` +-} +- +-// An enumeration is the parsed version of an LSP enumeration from the spec +-type Enumeration struct { +- Documentation string `json:"documentation"` +- Name string `json:"name"` +- Proposed bool `json:"proposed"` +- Since string `json:"since"` +- SupportsCustomValues bool `json:"supportsCustomValues"` +- Type *Type `json:"type"` +- Values []NameValue `json:"values"` +- Line int `json:"line"` +-} +- +-// A TypeAlias is the parsed version of an LSP type alias from the spec +-type TypeAlias struct { +- Documentation string `json:"documentation"` +- Deprecated string `json:"deprecated"` +- Name string `json:"name"` +- Proposed bool `json:"proposed"` +- Since string `json:"since"` +- Type *Type `json:"type"` +- Line int `json:"line"` +-} +- +-// A NameValue describes an enumeration constant +-type NameValue struct { +- Documentation string `json:"documentation"` +- Name string `json:"name"` +- Proposed bool `json:"proposed"` +- Since string `json:"since"` +- Value any `json:"value"` // number or string +- Line int `json:"line"` +-} +- +-// A Type is the parsed version of an LSP type from the spec, +-// or a Type the code constructs +-type Type struct { +- Kind string `json:"kind"` // -- which kind goes with which field -- +- Items []*Type `json:"items"` // "and", "or", "tuple" +- Element *Type `json:"element"` // "array" +- Name string `json:"name"` // "base", "reference" +- Key *Type `json:"key"` // "map" +- Value any `json:"value"` // "map", "stringLiteral", "literal" +- Line int `json:"line"` // JSON source line +-} +- +-// ParsedLiteral is Type.Value when Type.Kind is "literal" +-type ParseLiteral struct { +- Properties `json:"properties"` +-} +- +-// A NameType represents the name and type of a structure element +-type NameType struct { +- Name string `json:"name"` +- Type *Type `json:"type"` +- Optional bool `json:"optional"` +- Documentation string `json:"documentation"` +- Deprecated string `json:"deprecated"` +- Since string `json:"since"` +- Proposed bool `json:"proposed"` +- Line int `json:"line"` +-} +- +-// Properties are the collection of structure fields +-type Properties []NameType +- +-// addLineNumbers adds a "line" field to each object in the JSON. +-func addLineNumbers(buf []byte) []byte { +- var ans []byte +- // In the specification .json file, the delimiter '{' is +- // always followed by a newline. There are other {s embedded in strings. +- // json.Token does not return \n, or :, or , so using it would +- // require parsing the json to reconstruct the missing information. +- for linecnt, i := 1, 0; i < len(buf); i++ { +- ans = append(ans, buf[i]) +- switch buf[i] { +- case '{': +- if buf[i+1] == '\n' { +- ans = append(ans, fmt.Sprintf(`"line": %d, `, linecnt)...) +- // warning: this would fail if the spec file had +- // `"value": {\n}`, but it does not, as comma is a separator. +- } +- case '\n': +- linecnt++ +- } +- } +- return ans +-} +- +-type sortedMap[T any] map[string]T +- +-func (s sortedMap[T]) keys() []string { +- var keys []string +- for k := range s { +- keys = append(keys, k) +- } +- sort.Strings(keys) +- return keys +-} +diff -urN a/gopls/internal/protocol/json_test.go b/gopls/internal/protocol/json_test.go +--- a/gopls/internal/protocol/json_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/json_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,140 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package protocol_test +- +-import ( +- "encoding/json" +- "fmt" +- "regexp" +- "strings" +- "testing" +- +- "github.com/google/go-cmp/cmp" +- "golang.org/x/tools/gopls/internal/protocol" +-) +- +-// verify that type errors in Initialize lsp messages don't cause +-// any other unmarshalling errors. The code looks at single values and the +-// first component of array values. Each occurrence is replaced by something +-// of a different type, the resulting string unmarshalled, and compared to +-// the unmarshalling of the unchanged strings. The test passes if there is no +-// more than a single difference reported. That is, if changing a single value +-// in the message changes no more than a single value in the unmarshalled struct, +-// it is safe to ignore *json.UnmarshalTypeError. +- +-// strings are changed to numbers or bools (true) +-// bools are changed to numbers or strings +-// numbers are changed to strings or bools +- +-// a recent Initialize message taken from a log (at some point +-// some field incompatibly changed from bool to int32) +-const input = `{"processId":46408,"clientInfo":{"name":"Visual Studio Code - Insiders","version":"1.76.0-insider"},"locale":"en-us","rootPath":"/Users/pjw/hakim","rootUri":"file:///Users/pjw/hakim","capabilities":{"workspace":{"applyEdit":true,"workspaceEdit":{"documentChanges":true,"resourceOperations":["create","rename","delete"],"failureHandling":"textOnlyTransactional","normalizesLineEndings":true,"changeAnnotationSupport":{"groupsOnLabel":true}},"configuration":true,"didChangeWatchedFiles":{"dynamicRegistration":true,"relativePatternSupport":true},"symbol":{"dynamicRegistration":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]},"tagSupport":{"valueSet":[1]},"resolveSupport":{"properties":["location.range"]}},"codeLens":{"refreshSupport":true},"executeCommand":{"dynamicRegistration":true},"didChangeConfiguration":{"dynamicRegistration":true},"workspaceFolders":true,"semanticTokens":{"refreshSupport":true},"fileOperations":{"dynamicRegistration":true,"didCreate":true,"didRename":true,"didDelete":true,"willCreate":true,"willRename":true,"willDelete":true},"inlineValue":{"refreshSupport":true},"inlayHint":{"refreshSupport":true},"diagnostics":{"refreshSupport":true}},"textDocument":{"publishDiagnostics":{"relatedInformation":true,"versionSupport":false,"tagSupport":{"valueSet":[1,2]},"codeDescriptionSupport":true,"dataSupport":true},"synchronization":{"dynamicRegistration":true,"willSave":true,"willSaveWaitUntil":true,"didSave":true},"completion":{"dynamicRegistration":true,"contextSupport":true,"completionItem":{"snippetSupport":true,"commitCharactersSupport":true,"documentationFormat":["markdown","plaintext"],"deprecatedSupport":true,"preselectSupport":true,"tagSupport":{"valueSet":[1]},"insertReplaceSupport":true,"resolveSupport":{"properties":["documentation","detail","additionalTextEdits"]},"insertTextModeSupport":{"valueSet":[1,2]},"labelDetailsSupport":true},"insertTextMode":2,"completionItemKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25]},"completionList":{"itemDefaults":["commitCharacters","editRange","insertTextFormat","insertTextMode"]}},"hover":{"dynamicRegistration":true,"contentFormat":["markdown","plaintext"]},"signatureHelp":{"dynamicRegistration":true,"signatureInformation":{"documentationFormat":["markdown","plaintext"],"parameterInformation":{"labelOffsetSupport":true},"activeParameterSupport":true},"contextSupport":true},"definition":{"dynamicRegistration":true,"linkSupport":true},"references":{"dynamicRegistration":true},"documentHighlight":{"dynamicRegistration":true},"documentSymbol":{"dynamicRegistration":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]},"hierarchicalDocumentSymbolSupport":true,"tagSupport":{"valueSet":[1]},"labelSupport":true},"codeAction":{"dynamicRegistration":true,"isPreferredSupport":true,"disabledSupport":true,"dataSupport":true,"resolveSupport":{"properties":["edit"]},"codeActionLiteralSupport":{"codeActionKind":{"valueSet":["","quickfix","refactor","refactor.extract","refactor.inline","refactor.rewrite","source","source.organizeImports"]}},"honorsChangeAnnotations":false},"codeLens":{"dynamicRegistration":true},"formatting":{"dynamicRegistration":true},"rangeFormatting":{"dynamicRegistration":true},"onTypeFormatting":{"dynamicRegistration":true},"rename":{"dynamicRegistration":true,"prepareSupport":true,"prepareSupportDefaultBehavior":1,"honorsChangeAnnotations":true},"documentLink":{"dynamicRegistration":true,"tooltipSupport":true},"typeDefinition":{"dynamicRegistration":true,"linkSupport":true},"implementation":{"dynamicRegistration":true,"linkSupport":true},"colorProvider":{"dynamicRegistration":true},"foldingRange":{"dynamicRegistration":true,"rangeLimit":5000,"lineFoldingOnly":true,"foldingRangeKind":{"valueSet":["comment","imports","region"]},"foldingRange":{"collapsedText":false}},"declaration":{"dynamicRegistration":true,"linkSupport":true},"selectionRange":{"dynamicRegistration":true},"callHierarchy":{"dynamicRegistration":true},"semanticTokens":{"dynamicRegistration":true,"tokenTypes":["namespace","type","class","enum","interface","struct","typeParameter","parameter","variable","property","enumMember","event","function","method","macro","keyword","modifier","comment","string","number","regexp","operator","decorator"],"tokenModifiers":["declaration","definition","readonly","static","deprecated","abstract","async","modification","documentation","defaultLibrary"],"formats":["relative"],"requests":{"range":true,"full":{"delta":true}},"multilineTokenSupport":false,"overlappingTokenSupport":false,"serverCancelSupport":true,"augmentsSyntaxTokens":true},"linkedEditingRange":{"dynamicRegistration":true},"typeHierarchy":{"dynamicRegistration":true},"inlineValue":{"dynamicRegistration":true},"inlayHint":{"dynamicRegistration":true,"resolveSupport":{"properties":["tooltip","textEdits","label.tooltip","label.location","label.command"]}},"diagnostic":{"dynamicRegistration":true,"relatedDocumentSupport":false}},"window":{"showMessage":{"messageActionItem":{"additionalPropertiesSupport":true}},"showDocument":{"support":true},"workDoneProgress":true},"general":{"staleRequestSupport":{"cancel":true,"retryOnContentModified":["textDocument/semanticTokens/full","textDocument/semanticTokens/range","textDocument/semanticTokens/full/delta"]},"regularExpressions":{"engine":"ECMAScript","version":"ES2020"},"markdown":{"parser":"marked","version":"1.1.0"},"positionEncodings":["utf-16"]},"notebookDocument":{"synchronization":{"dynamicRegistration":true,"executionSummarySupport":true}}},"initializationOptions":{"usePlaceholders":true,"completionDocumentation":true,"verboseOutput":false,"build.directoryFilters":["-foof","-internal/protocol/typescript"],"codelenses":{"reference":true,"gc_details":true},"analyses":{"fillstruct":true,"staticcheck":true,"unusedparams":false,"composites":false},"semanticTokens":true,"noSemanticString":true,"noSemanticNumber":true,"templateExtensions":["tmpl","gotmpl"],"ui.completion.matcher":"Fuzzy","ui.inlayhint.hints":{"assignVariableTypes":false,"compositeLiteralFields":false,"compositeLiteralTypes":false,"constantValues":false,"functionTypeParameters":false,"parameterNames":false,"rangeVariableTypes":false},"ui.vulncheck":"Off","allExperiments":true},"trace":"off","workspaceFolders":[{"uri":"file:///Users/pjw/hakim","name":"hakim"}]}` +- +-type DiffReporter struct { +- path cmp.Path +- diffs []string +-} +- +-func (r *DiffReporter) PushStep(ps cmp.PathStep) { +- r.path = append(r.path, ps) +-} +- +-func (r *DiffReporter) Report(rs cmp.Result) { +- if !rs.Equal() { +- vx, vy := r.path.Last().Values() +- r.diffs = append(r.diffs, fmt.Sprintf("%#v:\n\t-: %+v\n\t+: %+v\n", r.path, vx, vy)) +- } +-} +- +-func (r *DiffReporter) PopStep() { +- r.path = r.path[:len(r.path)-1] +-} +- +-func (r *DiffReporter) String() string { +- return strings.Join(r.diffs, "\n") +-} +- +-func TestStringChanges(t *testing.T) { +- // string as value +- stringLeaf := regexp.MustCompile(`:("[^"]*")`) +- leafs := stringLeaf.FindAllStringSubmatchIndex(input, -1) +- allDeltas(t, leafs, "23", "true") +- // string as first element of array +- stringArray := regexp.MustCompile(`[[]("[^"]*")`) +- arrays := stringArray.FindAllStringSubmatchIndex(input, -1) +- allDeltas(t, arrays, "23", "true") +-} +- +-func TestBoolChanges(t *testing.T) { +- boolLeaf := regexp.MustCompile(`:(true|false)(,|})`) +- leafs := boolLeaf.FindAllStringSubmatchIndex(input, -1) +- allDeltas(t, leafs, "23", `"xx"`) +- boolArray := regexp.MustCompile(`:[[](true|false)(,|])`) +- arrays := boolArray.FindAllStringSubmatchIndex(input, -1) +- allDeltas(t, arrays, "23", `"xx"`) +-} +- +-func TestNumberChanges(t *testing.T) { +- numLeaf := regexp.MustCompile(`:(\d+)(,|})`) +- leafs := numLeaf.FindAllStringSubmatchIndex(input, -1) +- allDeltas(t, leafs, "true", `"xx"`) +- numArray := regexp.MustCompile(`:[[](\d+)(,|])`) +- arrays := numArray.FindAllStringSubmatchIndex(input, -1) +- allDeltas(t, arrays, "true", `"xx"`) +-} +- +-// v is a set of matches. check that substituting any repl never +-// creates more than 1 unmarshaling error +-func allDeltas(t *testing.T, v [][]int, repls ...string) { +- t.Helper() +- for _, repl := range repls { +- for i, x := range v { +- err := tryChange(x[2], x[3], repl) +- if err != nil { +- t.Errorf("%d:%q %v", i, input[x[2]:x[3]], err) +- } +- } +- } +-} +- +-func tryChange(start, end int, repl string) error { +- var p, q protocol.ParamInitialize +- mod := input[:start] + repl + input[end:] +- excerpt := func() (string, string) { +- a := start - 5 +- if a < 0 { +- a = 0 +- } +- b := end + 5 +- if b > len(input) { +- // trusting repl to be no longer than what it replaces +- b = len(input) +- } +- ma := input[a:b] +- mb := mod[a:b] +- return ma, mb +- } +- +- if err := json.Unmarshal([]byte(input), &p); err != nil { +- return fmt.Errorf("%s %v", repl, err) +- } +- switch err := json.Unmarshal([]byte(mod), &q).(type) { +- case nil: //ok +- case *json.UnmarshalTypeError: +- break +- case *protocol.UnmarshalError: +- return nil // cmp.Diff produces several diffs for custom unmrshalers +- default: +- return fmt.Errorf("%T unexpected unmarshal error", err) +- } +- +- var r DiffReporter +- cmp.Diff(p, q, cmp.Reporter(&r)) +- if len(r.diffs) > 1 { // 0 is possible, e.g., for interface{} +- ma, mb := excerpt() +- return fmt.Errorf("got %d diffs for %q\n%s\n%s", len(r.diffs), repl, ma, mb) +- } +- return nil +-} +diff -urN a/gopls/internal/protocol/log.go b/gopls/internal/protocol/log.go +--- a/gopls/internal/protocol/log.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/log.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,136 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package protocol +- +-import ( +- "context" +- "fmt" +- "io" +- "strings" +- "sync" +- "time" +- +- "golang.org/x/tools/internal/jsonrpc2" +-) +- +-type loggingStream struct { +- stream jsonrpc2.Stream +- logMu sync.Mutex +- log io.Writer +-} +- +-// LoggingStream returns a stream that does LSP protocol logging too +-func LoggingStream(str jsonrpc2.Stream, w io.Writer) jsonrpc2.Stream { +- return &loggingStream{stream: str, log: w} +-} +- +-func (s *loggingStream) Read(ctx context.Context) (jsonrpc2.Message, int64, error) { +- msg, count, err := s.stream.Read(ctx) +- if err == nil { +- s.logCommon(msg, true) +- } +- return msg, count, err +-} +- +-func (s *loggingStream) Write(ctx context.Context, msg jsonrpc2.Message) (int64, error) { +- s.logCommon(msg, false) +- count, err := s.stream.Write(ctx, msg) +- return count, err +-} +- +-func (s *loggingStream) Close() error { +- return s.stream.Close() +-} +- +-type req struct { +- method string +- start time.Time +-} +- +-type mapped struct { +- mu sync.Mutex +- clientCalls map[string]req +- serverCalls map[string]req +-} +- +-var maps = &mapped{ +- sync.Mutex{}, +- make(map[string]req), +- make(map[string]req), +-} +- +-// these 4 methods are each used exactly once, but it seemed +-// better to have the encapsulation rather than ad hoc mutex +-// code in 4 places +-func (m *mapped) client(id string) req { +- m.mu.Lock() +- defer m.mu.Unlock() +- v := m.clientCalls[id] +- delete(m.clientCalls, id) +- return v +-} +- +-func (m *mapped) server(id string) req { +- m.mu.Lock() +- defer m.mu.Unlock() +- v := m.serverCalls[id] +- delete(m.serverCalls, id) +- return v +-} +- +-func (m *mapped) setClient(id string, r req) { +- m.mu.Lock() +- defer m.mu.Unlock() +- m.clientCalls[id] = r +-} +- +-func (m *mapped) setServer(id string, r req) { +- m.mu.Lock() +- defer m.mu.Unlock() +- m.serverCalls[id] = r +-} +- +-const eor = "\r\n\r\n\r\n" +- +-func (s *loggingStream) logCommon(msg jsonrpc2.Message, isRead bool) { +- s.logMu.Lock() +- defer s.logMu.Unlock() +- direction, pastTense := "Received", "Received" +- get, set := maps.client, maps.setServer +- if isRead { +- direction, pastTense = "Sending", "Sent" +- get, set = maps.server, maps.setClient +- } +- if msg == nil || s.log == nil { +- return +- } +- tm := time.Now() +- tmfmt := tm.Format("15:04:05.000 PM") +- +- buf := strings.Builder{} +- fmt.Fprintf(&buf, "[Trace - %s] ", tmfmt) // common beginning +- switch msg := msg.(type) { +- case *jsonrpc2.Call: +- id := fmt.Sprint(msg.ID()) +- fmt.Fprintf(&buf, "%s request '%s - (%s)'.\n", direction, msg.Method(), id) +- fmt.Fprintf(&buf, "Params: %s%s", msg.Params(), eor) +- set(id, req{method: msg.Method(), start: tm}) +- case *jsonrpc2.Notification: +- fmt.Fprintf(&buf, "%s notification '%s'.\n", direction, msg.Method()) +- fmt.Fprintf(&buf, "Params: %s%s", msg.Params(), eor) +- case *jsonrpc2.Response: +- id := fmt.Sprint(msg.ID()) +- if err := msg.Err(); err != nil { +- fmt.Fprintf(s.log, "[Error - %s] %s #%s %s%s", pastTense, tmfmt, id, err, eor) +- return +- } +- cc := get(id) +- elapsed := tm.Sub(cc.start) +- fmt.Fprintf(&buf, "%s response '%s - (%s)' in %dms.\n", +- direction, cc.method, id, elapsed/time.Millisecond) +- fmt.Fprintf(&buf, "Result: %s%s", msg.Result(), eor) +- } +- s.log.Write([]byte(buf.String())) +-} +diff -urN a/gopls/internal/protocol/mapper.go b/gopls/internal/protocol/mapper.go +--- a/gopls/internal/protocol/mapper.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/mapper.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,434 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package protocol +- +-// This file defines Mapper, which wraps a file content buffer +-// ([]byte) and provides efficient conversion between every kind of +-// position representation. +-// +-// gopls uses four main representations of position: +-// +-// 1. byte offsets, e.g. (start, end int), starting from zero. +-// +-// 2. go/token notation. Use these types when interacting directly +-// with the go/* syntax packages: +-// +-// token.Pos +-// token.FileSet +-// token.File +-// +-// Because File.Offset and File.Pos panic on invalid inputs, +-// we do not call them directly and instead use the safetoken package +-// for these conversions. This is enforced by a static check. +-// +-// Beware also that the methods of token.File have two bugs for which +-// safetoken contains workarounds: +-// - #57490, whereby the parser may create ast.Nodes during error +-// recovery whose computed positions are out of bounds (EOF+1). +-// - #41029, whereby the wrong line number is returned for the EOF position. +-// +-// 3. the cmd package. +-// +-// cmd.point = (line, col8, offset). +-// cmd.Span = (uri URI, start, end cmd.point) +-// +-// Line and column are 1-based. +-// Columns are measured in bytes (UTF-8 codes). +-// All fields are optional. +-// +-// These types are useful as intermediate conversions of validated +-// ranges (though MappedRange is superior as it is self contained +-// and universally convertible). Since their fields are optional +-// they are also useful for parsing user-provided positions (e.g. in +-// the CLI) before we have access to file contents. +-// +-// 4. protocol, the LSP RPC message format. +-// +-// protocol.Position = (Line, Character uint32) +-// protocol.Range = (start, end Position) +-// protocol.Location = (URI, protocol.Range) +-// +-// Line and Character are 0-based. +-// Characters (columns) are measured in UTF-16 codes. +-// +-// protocol.Mapper holds the (URI, Content) of a file, enabling +-// efficient mapping between byte offsets, cmd ranges, and +-// protocol ranges. +-// +-// protocol.MappedRange holds a protocol.Mapper and valid (start, +-// end int) byte offsets, enabling infallible, efficient conversion +-// to any other format. +- +-import ( +- "bytes" +- "fmt" +- "go/ast" +- "go/token" +- "sort" +- "strings" +- "sync" +- "unicode/utf8" +- +- "golang.org/x/tools/gopls/internal/util/safetoken" +-) +- +-// A Mapper wraps the content of a file and provides mapping +-// between byte offsets and notations of position such as: +-// +-// - (line, col8) pairs, where col8 is a 1-based UTF-8 column number +-// (bytes), as used by the go/token and cmd packages. +-// +-// - (line, col16) pairs, where col16 is a 1-based UTF-16 column +-// number, as used by the LSP protocol. +-// +-// All conversion methods are named "FromTo", where From and To are the two types. +-// For example, the PointPosition method converts from a Point to a Position. +-// +-// Mapper does not intrinsically depend on go/token-based +-// representations. Use safetoken to map between token.Pos <=> byte +-// offsets, or the convenience methods such as PosPosition, +-// NodePosition, or NodeRange. +-// +-// See overview comments at top of this file. +-type Mapper struct { +- URI DocumentURI +- Content []byte +- +- // Line-number information is requested only for a tiny +- // fraction of Mappers, so we compute it lazily. +- // Call initLines() before accessing fields below. +- linesOnce sync.Once +- lineStart []int // byte offset of start of ith line (0-based); last=EOF iff \n-terminated +- nonASCII bool +- +- // TODO(adonovan): adding an extra lineStart entry for EOF +- // might simplify every method that accesses it. Try it out. +-} +- +-// NewMapper creates a new mapper for the given URI and content. +-func NewMapper(uri DocumentURI, content []byte) *Mapper { +- return &Mapper{URI: uri, Content: content} +-} +- +-// initLines populates the lineStart table. +-func (m *Mapper) initLines() { +- m.linesOnce.Do(func() { +- nlines := bytes.Count(m.Content, []byte("\n")) +- m.lineStart = make([]int, 1, nlines+1) // initially []int{0} +- for offset, b := range m.Content { +- if b == '\n' { +- m.lineStart = append(m.lineStart, offset+1) +- } +- if b >= utf8.RuneSelf { +- m.nonASCII = true +- } +- } +- }) +-} +- +-// LineCol8Position converts a valid line and UTF-8 column number, +-// both 1-based, to a protocol (UTF-16) position. +-func (m *Mapper) LineCol8Position(line, col8 int) (Position, error) { +- m.initLines() +- line0 := line - 1 // 0-based +- if !(0 <= line0 && line0 < len(m.lineStart)) { +- return Position{}, fmt.Errorf("line number %d out of range (max %d)", line, len(m.lineStart)) +- } +- +- // content[start:end] is the preceding partial line. +- start := m.lineStart[line0] +- end := start + col8 - 1 +- +- // Validate column. +- if end > len(m.Content) { +- return Position{}, fmt.Errorf("column is beyond end of file") +- } else if line0+1 < len(m.lineStart) && end >= m.lineStart[line0+1] { +- return Position{}, fmt.Errorf("column is beyond end of line") +- } +- +- char := UTF16Len(m.Content[start:end]) +- return Position{Line: uint32(line0), Character: uint32(char)}, nil +-} +- +-// -- conversions from byte offsets -- +- +-// OffsetLocation converts a byte-offset interval to a protocol (UTF-16) location. +-func (m *Mapper) OffsetLocation(start, end int) (Location, error) { +- rng, err := m.OffsetRange(start, end) +- if err != nil { +- return Location{}, err +- } +- return m.RangeLocation(rng), nil +-} +- +-// OffsetRange converts a byte-offset interval to a protocol (UTF-16) range. +-func (m *Mapper) OffsetRange(start, end int) (Range, error) { +- if start > end { +- return Range{}, fmt.Errorf("start offset (%d) > end (%d)", start, end) +- } +- startPosition, err := m.OffsetPosition(start) +- if err != nil { +- return Range{}, fmt.Errorf("start: %v", err) +- } +- endPosition, err := m.OffsetPosition(end) +- if err != nil { +- return Range{}, fmt.Errorf("end: %v", err) +- } +- return Range{Start: startPosition, End: endPosition}, nil +-} +- +-// OffsetPosition converts a byte offset to a protocol (UTF-16) position. +-func (m *Mapper) OffsetPosition(offset int) (Position, error) { +- if !(0 <= offset && offset <= len(m.Content)) { +- return Position{}, fmt.Errorf("invalid offset %d (want 0-%d)", offset, len(m.Content)) +- } +- // No error may be returned after this point, +- // even if the offset does not fall at a rune boundary. +- // (See panic in MappedRange.Range reachable.) +- +- line, col16 := m.lineCol16(offset) +- return Position{Line: uint32(line), Character: uint32(col16)}, nil +-} +- +-// lineCol16 converts a valid byte offset to line and UTF-16 column numbers, both 0-based. +-func (m *Mapper) lineCol16(offset int) (int, int) { +- line, start, cr := m.line(offset) +- var col16 int +- if m.nonASCII { +- col16 = UTF16Len(m.Content[start:offset]) +- } else { +- col16 = offset - start +- } +- if cr { +- col16-- // retreat from \r at line end +- } +- return line, col16 +-} +- +-// OffsetLineCol8 converts a valid byte offset to line and UTF-8 column numbers, both 1-based. +-func (m *Mapper) OffsetLineCol8(offset int) (int, int) { +- line, start, cr := m.line(offset) +- col8 := offset - start +- if cr { +- col8-- // retreat from \r at line end +- } +- return line + 1, col8 + 1 +-} +- +-// line returns: +-// - the 0-based index of the line that encloses the (valid) byte offset; +-// - the start offset of that line; and +-// - whether the offset denotes a carriage return (\r) at line end. +-func (m *Mapper) line(offset int) (int, int, bool) { +- m.initLines() +- // In effect, binary search returns a 1-based result. +- line := sort.Search(len(m.lineStart), func(i int) bool { +- return offset < m.lineStart[i] +- }) +- +- // Adjustment for line-endings: \r|\n is the same as |\r\n. +- var eol int +- if line == len(m.lineStart) { +- eol = len(m.Content) // EOF +- } else { +- eol = m.lineStart[line] - 1 +- } +- cr := offset == eol && offset > 0 && m.Content[offset-1] == '\r' +- +- line-- // 0-based +- +- return line, m.lineStart[line], cr +-} +- +-// OffsetMappedRange returns a MappedRange for the given byte offsets. +-// A MappedRange can be converted to any other form. +-func (m *Mapper) OffsetMappedRange(start, end int) (MappedRange, error) { +- if !(0 <= start && start <= end && end <= len(m.Content)) { +- return MappedRange{}, fmt.Errorf("invalid offsets (%d, %d) (file %s has size %d)", start, end, m.URI, len(m.Content)) +- } +- return MappedRange{m, start, end}, nil +-} +- +-// -- conversions from protocol (UTF-16) domain -- +- +-// RangeOffsets converts a protocol (UTF-16) range to start/end byte offsets. +-func (m *Mapper) RangeOffsets(r Range) (int, int, error) { +- start, err := m.PositionOffset(r.Start) +- if err != nil { +- return 0, 0, err +- } +- end, err := m.PositionOffset(r.End) +- if err != nil { +- return 0, 0, err +- } +- return start, end, nil +-} +- +-// PositionOffset converts a protocol (UTF-16) position to a byte offset. +-func (m *Mapper) PositionOffset(p Position) (int, error) { +- m.initLines() +- +- // Validate line number. +- if p.Line > uint32(len(m.lineStart)) { +- return 0, fmt.Errorf("line number %d out of range 0-%d", p.Line, len(m.lineStart)) +- } else if p.Line == uint32(len(m.lineStart)) { +- if p.Character == 0 { +- return len(m.Content), nil // EOF +- } +- return 0, fmt.Errorf("column is beyond end of file") +- } +- +- offset := m.lineStart[p.Line] +- content := m.Content[offset:] // rest of file from start of enclosing line +- +- // Advance bytes up to the required number of UTF-16 codes. +- col8 := 0 +- for col16 := 0; col16 < int(p.Character); col16++ { +- r, sz := utf8.DecodeRune(content) +- if sz == 0 { +- return 0, fmt.Errorf("column is beyond end of file") +- } +- if r == '\n' { +- return 0, fmt.Errorf("column is beyond end of line") +- } +- if sz == 1 && r == utf8.RuneError { +- return 0, fmt.Errorf("buffer contains invalid UTF-8 text") +- } +- content = content[sz:] +- +- if r >= 0x10000 { +- col16++ // rune was encoded by a pair of surrogate UTF-16 codes +- +- if col16 == int(p.Character) { +- break // requested position is in the middle of a rune +- } +- } +- col8 += sz +- } +- return offset + col8, nil +-} +- +-// -- go/token domain convenience methods -- +- +-// PosPosition converts a token pos to a protocol (UTF-16) position. +-func (m *Mapper) PosPosition(tf *token.File, pos token.Pos) (Position, error) { +- offset, err := safetoken.Offset(tf, pos) +- if err != nil { +- return Position{}, err +- } +- return m.OffsetPosition(offset) +-} +- +-// PosLocation converts a token range to a protocol (UTF-16) location. +-func (m *Mapper) PosLocation(tf *token.File, start, end token.Pos) (Location, error) { +- startOffset, endOffset, err := safetoken.Offsets(tf, start, end) +- if err != nil { +- return Location{}, err +- } +- rng, err := m.OffsetRange(startOffset, endOffset) +- if err != nil { +- return Location{}, err +- } +- return m.RangeLocation(rng), nil +-} +- +-// PosRange converts a token range to a protocol (UTF-16) range. +-func (m *Mapper) PosRange(tf *token.File, start, end token.Pos) (Range, error) { +- startOffset, endOffset, err := safetoken.Offsets(tf, start, end) +- if err != nil { +- return Range{}, err +- } +- return m.OffsetRange(startOffset, endOffset) +-} +- +-// NodeRange converts a syntax node range to a protocol (UTF-16) range. +-func (m *Mapper) NodeRange(tf *token.File, node ast.Node) (Range, error) { +- return m.PosRange(tf, node.Pos(), node.End()) +-} +- +-// RangeLocation pairs a protocol Range with its URI, in a Location. +-func (m *Mapper) RangeLocation(rng Range) Location { +- return Location{URI: m.URI, Range: rng} +-} +- +-// PosMappedRange returns a MappedRange for the given token.Pos range. +-func (m *Mapper) PosMappedRange(tf *token.File, start, end token.Pos) (MappedRange, error) { +- startOffset, endOffset, err := safetoken.Offsets(tf, start, end) +- if err != nil { +- return MappedRange{}, nil +- } +- return m.OffsetMappedRange(startOffset, endOffset) +-} +- +-// NodeMappedRange returns a MappedRange for the given node range. +-func (m *Mapper) NodeMappedRange(tf *token.File, node ast.Node) (MappedRange, error) { +- return m.PosMappedRange(tf, node.Pos(), node.End()) +-} +- +-// -- MappedRange -- +- +-// A MappedRange represents a valid byte-offset range of a file. +-// Through its Mapper it can be converted into other forms such +-// as protocol.Range or UTF-8. +-// +-// Construct one by calling Mapper.OffsetMappedRange with start/end offsets. +-// From the go/token domain, call safetoken.Offsets first, +-// or use a helper such as parsego.File.MappedPosRange. +-// +-// Two MappedRanges produced the same Mapper are equal if and only if they +-// denote the same range. Two MappedRanges produced by different Mappers +-// are unequal even when they represent the same range of the same file. +-type MappedRange struct { +- Mapper *Mapper +- start, end int // valid byte offsets: 0 <= start <= end <= len(Mapper.Content) +-} +- +-// Offsets returns the (start, end) byte offsets of this range. +-func (mr MappedRange) Offsets() (start, end int) { return mr.start, mr.end } +- +-// -- convenience functions -- +- +-// URI returns the URI of the range's file. +-func (mr MappedRange) URI() DocumentURI { +- return mr.Mapper.URI +-} +- +-// Range returns the range in protocol (UTF-16) form. +-func (mr MappedRange) Range() Range { +- rng, err := mr.Mapper.OffsetRange(mr.start, mr.end) +- if err != nil { +- panic(err) // can't happen +- } +- return rng +-} +- +-// Location returns the range in protocol location (UTF-16) form. +-func (mr MappedRange) Location() Location { +- return mr.Mapper.RangeLocation(mr.Range()) +-} +- +-// String formats the range in UTF-8 notation. +-func (mr MappedRange) String() string { +- var s strings.Builder +- startLine, startCol8 := mr.Mapper.OffsetLineCol8(mr.start) +- fmt.Fprintf(&s, "%d:%d", startLine, startCol8) +- if mr.end != mr.start { +- endLine, endCol8 := mr.Mapper.OffsetLineCol8(mr.end) +- if endLine == startLine { +- fmt.Fprintf(&s, "-%d", endCol8) +- } else { +- fmt.Fprintf(&s, "-%d:%d", endLine, endCol8) +- } +- } +- return s.String() +-} +- +-// LocationTextDocumentPositionParams converts its argument to its result. +-func LocationTextDocumentPositionParams(loc Location) TextDocumentPositionParams { +- return TextDocumentPositionParams{ +- TextDocument: TextDocumentIdentifier{URI: loc.URI}, +- Position: loc.Range.Start, +- } +-} +diff -urN a/gopls/internal/protocol/mapper_test.go b/gopls/internal/protocol/mapper_test.go +--- a/gopls/internal/protocol/mapper_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/mapper_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,449 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package protocol_test +- +-import ( +- "fmt" +- "strings" +- "testing" +- +- "golang.org/x/tools/gopls/internal/protocol" +-) +- +-// This file tests Mapper's logic for converting between offsets, +-// UTF-8 columns, and UTF-16 columns. (The strange form attests to +-// earlier abstractions.) +- +-// 𐐀 is U+10400 = [F0 90 90 80] in UTF-8, [D801 DC00] in UTF-16. +-var funnyString = []byte("𐐀23\n𐐀45") +- +-var toUTF16Tests = []struct { +- scenario string +- input []byte +- line int // 1-indexed count +- col int // 1-indexed byte position in line +- offset int // 0-indexed byte offset into input +- resUTF16col int // 1-indexed UTF-16 col number +- pre string // everything before the cursor on the line +- post string // everything from the cursor onwards +- err string // expected error string in call to ToUTF16Column +- issue *bool +-}{ +- { +- scenario: "cursor missing content", +- input: nil, +- offset: -1, +- err: "point has neither offset nor line/column", - }, -- Commands: []*CommandJSON{ -- { -- Command: "gopls.add_dependency", -- Title: "Add a dependency", -- Doc: "Adds a dependency to the go.mod file for a module.", -- ArgDoc: "{\n\t// The go.mod file URI.\n\t\"URI\": string,\n\t// Additional args to pass to the go command.\n\t\"GoCmdArgs\": []string,\n\t// Whether to add a require directive.\n\t\"AddRequire\": bool,\n}", -- }, -- { -- Command: "gopls.add_import", -- Title: "Add an import", -- Doc: "Ask the server to add an import path to a given Go file. The method will\ncall applyEdit on the client so that clients don't have to apply the edit\nthemselves.", -- ArgDoc: "{\n\t// ImportPath is the target import path that should\n\t// be added to the URI file\n\t\"ImportPath\": string,\n\t// URI is the file that the ImportPath should be\n\t// added to\n\t\"URI\": string,\n}", -- }, -- { -- Command: "gopls.add_telemetry_counters", -- Title: "update the given telemetry counters.", -- Doc: "Gopls will prepend \"fwd/\" to all the counters updated using this command\nto avoid conflicts with other counters gopls collects.", -- ArgDoc: "{\n\t// Names and Values must have the same length.\n\t\"Names\": []string,\n\t\"Values\": []int64,\n}", -- }, -- { -- Command: "gopls.apply_fix", -- Title: "Apply a fix", -- Doc: "Applies a fix to a region of source code.", -- ArgDoc: "{\n\t// The fix to apply.\n\t\"Fix\": string,\n\t// The file URI for the document to fix.\n\t\"URI\": string,\n\t// The document range to scan for fixes.\n\t\"Range\": {\n\t\t\"start\": {\n\t\t\t\"line\": uint32,\n\t\t\t\"character\": uint32,\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": uint32,\n\t\t\t\"character\": uint32,\n\t\t},\n\t},\n}", -- }, -- { -- Command: "gopls.change_signature", -- Title: "performs a \"change signature\" refactoring.", -- Doc: "This command is experimental, currently only supporting parameter removal.\nIts signature will certainly change in the future (pun intended).", -- ArgDoc: "{\n\t\"RemoveParameter\": {\n\t\t\"uri\": string,\n\t\t\"range\": {\n\t\t\t\"start\": { ... },\n\t\t\t\"end\": { ... },\n\t\t},\n\t},\n}", -- }, -- { -- Command: "gopls.check_upgrades", -- Title: "Check for upgrades", -- Doc: "Checks for module upgrades.", -- ArgDoc: "{\n\t// The go.mod file URI.\n\t\"URI\": string,\n\t// The modules to check.\n\t\"Modules\": []string,\n}", -- }, -- { -- Command: "gopls.edit_go_directive", -- Title: "Run go mod edit -go=version", -- Doc: "Runs `go mod edit -go=version` for a module.", -- ArgDoc: "{\n\t// Any document URI within the relevant module.\n\t\"URI\": string,\n\t// The version to pass to `go mod edit -go`.\n\t\"Version\": string,\n}", -- }, -- { -- Command: "gopls.fetch_vulncheck_result", -- Title: "Get known vulncheck result", -- Doc: "Fetch the result of latest vulnerability check (`govulncheck`).", -- ArgDoc: "{\n\t// The file URI.\n\t\"URI\": string,\n}", -- ResultDoc: "map[golang.org/x/tools/gopls/internal/lsp/protocol.DocumentURI]*golang.org/x/tools/gopls/internal/vulncheck.Result", -- }, -- { -- Command: "gopls.gc_details", -- Title: "Toggle gc_details", -- Doc: "Toggle the calculation of gc annotations.", -- ArgDoc: "string", -- }, -- { -- Command: "gopls.generate", -- Title: "Run go generate", -- Doc: "Runs `go generate` for a given directory.", -- ArgDoc: "{\n\t// URI for the directory to generate.\n\t\"Dir\": string,\n\t// Whether to generate recursively (go generate ./...)\n\t\"Recursive\": bool,\n}", -- }, -- { -- Command: "gopls.go_get_package", -- Title: "go get a package", -- Doc: "Runs `go get` to fetch a package.", -- ArgDoc: "{\n\t// Any document URI within the relevant module.\n\t\"URI\": string,\n\t// The package to go get.\n\t\"Pkg\": string,\n\t\"AddRequire\": bool,\n}", -- }, -- { -- Command: "gopls.list_imports", -- Title: "List imports of a file and its package", -- Doc: "Retrieve a list of imports in the given Go file, and the package it\nbelongs to.", -- ArgDoc: "{\n\t// The file URI.\n\t\"URI\": string,\n}", -- ResultDoc: "{\n\t// Imports is a list of imports in the requested file.\n\t\"Imports\": []{\n\t\t\"Path\": string,\n\t\t\"Name\": string,\n\t},\n\t// PackageImports is a list of all imports in the requested file's package.\n\t\"PackageImports\": []{\n\t\t\"Path\": string,\n\t},\n}", -- }, -- { -- Command: "gopls.list_known_packages", -- Title: "List known packages", -- Doc: "Retrieve a list of packages that are importable from the given URI.", -- ArgDoc: "{\n\t// The file URI.\n\t\"URI\": string,\n}", -- ResultDoc: "{\n\t// Packages is a list of packages relative\n\t// to the URIArg passed by the command request.\n\t// In other words, it omits paths that are already\n\t// imported or cannot be imported due to compiler\n\t// restrictions.\n\t\"Packages\": []string,\n}", -- }, -- { -- Command: "gopls.maybe_prompt_for_telemetry", -- Title: "checks for the right conditions, and then prompts", -- Doc: "the user to ask if they want to enable Go telemetry uploading. If the user\nresponds 'Yes', the telemetry mode is set to \"on\".", -- }, -- { -- Command: "gopls.mem_stats", -- Title: "fetch memory statistics", -- Doc: "Call runtime.GC multiple times and return memory statistics as reported by\nruntime.MemStats.\n\nThis command is used for benchmarking, and may change in the future.", -- ResultDoc: "{\n\t\"HeapAlloc\": uint64,\n\t\"HeapInUse\": uint64,\n\t\"TotalAlloc\": uint64,\n}", -- }, -- { -- Command: "gopls.regenerate_cgo", -- Title: "Regenerate cgo", -- Doc: "Regenerates cgo definitions.", -- ArgDoc: "{\n\t// The file URI.\n\t\"URI\": string,\n}", -- }, -- { -- Command: "gopls.remove_dependency", -- Title: "Remove a dependency", -- Doc: "Removes a dependency from the go.mod file of a module.", -- ArgDoc: "{\n\t// The go.mod file URI.\n\t\"URI\": string,\n\t// The module path to remove.\n\t\"ModulePath\": string,\n\t// If the module is tidied apart from the one unused diagnostic, we can\n\t// run `go get module@none`, and then run `go mod tidy`. Otherwise, we\n\t// must make textual edits.\n\t\"OnlyDiagnostic\": bool,\n}", -- }, -- { -- Command: "gopls.reset_go_mod_diagnostics", -- Title: "Reset go.mod diagnostics", -- Doc: "Reset diagnostics in the go.mod file of a module.", -- ArgDoc: "{\n\t\"URIArg\": {\n\t\t\"URI\": string,\n\t},\n\t// Optional: source of the diagnostics to reset.\n\t// If not set, all resettable go.mod diagnostics will be cleared.\n\t\"DiagnosticSource\": string,\n}", -- }, -- { -- Command: "gopls.run_go_work_command", -- Title: "run `go work [args...]`, and apply the resulting go.work", -- Doc: "edits to the current go.work file.", -- ArgDoc: "{\n\t\"ViewID\": string,\n\t\"InitFirst\": bool,\n\t\"Args\": []string,\n}", -- }, -- { -- Command: "gopls.run_govulncheck", -- Title: "Run vulncheck.", -- Doc: "Run vulnerability check (`govulncheck`).", -- ArgDoc: "{\n\t// Any document in the directory from which govulncheck will run.\n\t\"URI\": string,\n\t// Package pattern. E.g. \"\", \".\", \"./...\".\n\t\"Pattern\": string,\n}", -- ResultDoc: "{\n\t// Token holds the progress token for LSP workDone reporting of the vulncheck\n\t// invocation.\n\t\"Token\": interface{},\n}", -- }, -- { -- Command: "gopls.run_tests", -- Title: "Run test(s)", -- Doc: "Runs `go test` for a specific set of test or benchmark functions.", -- ArgDoc: "{\n\t// The test file containing the tests to run.\n\t\"URI\": string,\n\t// Specific test names to run, e.g. TestFoo.\n\t\"Tests\": []string,\n\t// Specific benchmarks to run, e.g. BenchmarkFoo.\n\t\"Benchmarks\": []string,\n}", -- }, -- { -- Command: "gopls.start_debugging", -- Title: "Start the gopls debug server", -- Doc: "Start the gopls debug server if it isn't running, and return the debug\naddress.", -- ArgDoc: "{\n\t// Optional: the address (including port) for the debug server to listen on.\n\t// If not provided, the debug server will bind to \"localhost:0\", and the\n\t// full debug URL will be contained in the result.\n\t//\n\t// If there is more than one gopls instance along the serving path (i.e. you\n\t// are using a daemon), each gopls instance will attempt to start debugging.\n\t// If Addr specifies a port, only the daemon will be able to bind to that\n\t// port, and each intermediate gopls instance will fail to start debugging.\n\t// For this reason it is recommended not to specify a port (or equivalently,\n\t// to specify \":0\").\n\t//\n\t// If the server was already debugging this field has no effect, and the\n\t// result will contain the previously configured debug URL(s).\n\t\"Addr\": string,\n}", -- ResultDoc: "{\n\t// The URLs to use to access the debug servers, for all gopls instances in\n\t// the serving path. For the common case of a single gopls instance (i.e. no\n\t// daemon), this will be exactly one address.\n\t//\n\t// In the case of one or more gopls instances forwarding the LSP to a daemon,\n\t// URLs will contain debug addresses for each server in the serving path, in\n\t// serving order. The daemon debug address will be the last entry in the\n\t// slice. If any intermediate gopls instance fails to start debugging, no\n\t// error will be returned but the debug URL for that server in the URLs slice\n\t// will be empty.\n\t\"URLs\": []string,\n}", -- }, -- { -- Command: "gopls.start_profile", -- Title: "start capturing a profile of gopls' execution.", -- Doc: "Start a new pprof profile. Before using the resulting file, profiling must\nbe stopped with a corresponding call to StopProfile.\n\nThis command is intended for internal use only, by the gopls benchmark\nrunner.", -- ArgDoc: "struct{}", -- ResultDoc: "struct{}", -- }, -- { -- Command: "gopls.stop_profile", -- Title: "stop an ongoing profile.", -- Doc: "This command is intended for internal use only, by the gopls benchmark\nrunner.", -- ArgDoc: "struct{}", -- ResultDoc: "{\n\t// File is the profile file name.\n\t\"File\": string,\n}", -- }, -- { -- Command: "gopls.test", -- Title: "Run test(s) (legacy)", -- Doc: "Runs `go test` for a specific set of test or benchmark functions.", -- ArgDoc: "string,\n[]string,\n[]string", -- }, -- { -- Command: "gopls.tidy", -- Title: "Run go mod tidy", -- Doc: "Runs `go mod tidy` for a module.", -- ArgDoc: "{\n\t// The file URIs.\n\t\"URIs\": []string,\n}", -- }, -- { -- Command: "gopls.toggle_gc_details", -- Title: "Toggle gc_details", -- Doc: "Toggle the calculation of gc annotations.", -- ArgDoc: "{\n\t// The file URI.\n\t\"URI\": string,\n}", -- }, -- { -- Command: "gopls.update_go_sum", -- Title: "Update go.sum", -- Doc: "Updates the go.sum file for a module.", -- ArgDoc: "{\n\t// The file URIs.\n\t\"URIs\": []string,\n}", -- }, -- { -- Command: "gopls.upgrade_dependency", -- Title: "Upgrade a dependency", -- Doc: "Upgrades a dependency in the go.mod file for a module.", -- ArgDoc: "{\n\t// The go.mod file URI.\n\t\"URI\": string,\n\t// Additional args to pass to the go command.\n\t\"GoCmdArgs\": []string,\n\t// Whether to add a require directive.\n\t\"AddRequire\": bool,\n}", -- }, -- { -- Command: "gopls.vendor", -- Title: "Run go mod vendor", -- Doc: "Runs `go mod vendor` for a module.", -- ArgDoc: "{\n\t// The file URI.\n\t\"URI\": string,\n}", -- }, -- { -- Command: "gopls.workspace_stats", -- Title: "fetch workspace statistics", -- Doc: "Query statistics about workspace builds, modules, packages, and files.\n\nThis command is intended for internal use only, by the gopls stats\ncommand.", -- ResultDoc: "{\n\t\"Files\": {\n\t\t\"Total\": int,\n\t\t\"Largest\": int,\n\t\t\"Errs\": int,\n\t},\n\t\"Views\": []{\n\t\t\"GoCommandVersion\": string,\n\t\t\"AllPackages\": {\n\t\t\t\"Packages\": int,\n\t\t\t\"LargestPackage\": int,\n\t\t\t\"CompiledGoFiles\": int,\n\t\t\t\"Modules\": int,\n\t\t},\n\t\t\"WorkspacePackages\": {\n\t\t\t\"Packages\": int,\n\t\t\t\"LargestPackage\": int,\n\t\t\t\"CompiledGoFiles\": int,\n\t\t\t\"Modules\": int,\n\t\t},\n\t\t\"Diagnostics\": int,\n\t},\n}", -- }, +- { +- scenario: "cursor missing position", +- input: funnyString, +- line: -1, +- col: -1, +- offset: -1, +- err: "point has neither offset nor line/column", +- }, +- { +- scenario: "zero length input; cursor at first col, first line", +- input: []byte(""), +- line: 1, +- col: 1, +- offset: 0, +- resUTF16col: 1, +- }, +- { +- scenario: "cursor before funny character; first line", +- input: funnyString, +- line: 1, +- col: 1, +- offset: 0, +- resUTF16col: 1, +- pre: "", +- post: "𐐀23", +- }, +- { +- scenario: "cursor after funny character; first line", +- input: funnyString, +- line: 1, +- col: 5, // 4 + 1 (1-indexed) +- offset: 4, // (unused since we have line+col) +- resUTF16col: 3, // 2 + 1 (1-indexed) +- pre: "𐐀", +- post: "23", +- }, +- { +- scenario: "cursor after last character on first line", +- input: funnyString, +- line: 1, +- col: 7, // 4 + 1 + 1 + 1 (1-indexed) +- offset: 6, // 4 + 1 + 1 (unused since we have line+col) +- resUTF16col: 5, // 2 + 1 + 1 + 1 (1-indexed) +- pre: "𐐀23", +- post: "", +- }, +- { +- scenario: "cursor before funny character; second line", +- input: funnyString, +- line: 2, +- col: 1, +- offset: 7, // length of first line (unused since we have line+col) +- resUTF16col: 1, +- pre: "", +- post: "𐐀45", - }, -- Lenses: []*LensJSON{ -- { -- Lens: "gc_details", -- Title: "Toggle gc_details", -- Doc: "Toggle the calculation of gc annotations.", -- }, -- { -- Lens: "generate", -- Title: "Run go generate", -- Doc: "Runs `go generate` for a given directory.", -- }, -- { -- Lens: "regenerate_cgo", -- Title: "Regenerate cgo", -- Doc: "Regenerates cgo definitions.", -- }, -- { -- Lens: "run_govulncheck", -- Title: "Run vulncheck.", -- Doc: "Run vulnerability check (`govulncheck`).", -- }, -- { -- Lens: "test", -- Title: "Run test(s) (legacy)", -- Doc: "Runs `go test` for a specific set of test or benchmark functions.", -- }, -- { -- Lens: "tidy", -- Title: "Run go mod tidy", -- Doc: "Runs `go mod tidy` for a module.", -- }, -- { -- Lens: "upgrade_dependency", -- Title: "Upgrade a dependency", -- Doc: "Upgrades a dependency in the go.mod file for a module.", -- }, -- { -- Lens: "vendor", -- Title: "Run go mod vendor", -- Doc: "Runs `go mod vendor` for a module.", -- }, +- { +- scenario: "cursor after funny character; second line", +- input: funnyString, +- line: 1, +- col: 5, // 4 + 1 (1-indexed) +- offset: 11, // 7 (length of first line) + 4 (unused since we have line+col) +- resUTF16col: 3, // 2 + 1 (1-indexed) +- pre: "𐐀", +- post: "45", - }, -- Analyzers: []*AnalyzerJSON{ -- { -- Name: "appends", -- Doc: "check for missing values after append\n\nThis checker reports calls to append that pass\nno values to be appended to the slice.\n\n\ts := []string{\"a\", \"b\", \"c\"}\n\t_ = append(s)\n\nSuch calls are always no-ops and often indicate an\nunderlying mistake.", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/appends", -- Default: true, -- }, -- { -- Name: "asmdecl", -- Doc: "report mismatches between assembly files and Go declarations", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/asmdecl", -- Default: true, -- }, -- { -- Name: "assign", -- Doc: "check for useless assignments\n\nThis checker reports assignments of the form x = x or a[i] = a[i].\nThese are almost always useless, and even when they aren't they are\nusually a mistake.", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/assign", -- Default: true, -- }, -- { -- Name: "atomic", -- Doc: "check for common mistakes using the sync/atomic package\n\nThe atomic checker looks for assignment statements of the form:\n\n\tx = atomic.AddUint64(&x, 1)\n\nwhich are not atomic.", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/atomic", -- Default: true, -- }, -- { -- Name: "atomicalign", -- Doc: "check for non-64-bits-aligned arguments to sync/atomic functions", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/atomicalign", -- Default: true, -- }, -- { -- Name: "bools", -- Doc: "check for common mistakes involving boolean operators", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/bools", -- Default: true, -- }, -- { -- Name: "buildtag", -- Doc: "check //go:build and // +build directives", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/buildtag", -- Default: true, -- }, -- { -- Name: "cgocall", -- Doc: "detect some violations of the cgo pointer passing rules\n\nCheck for invalid cgo pointer passing.\nThis looks for code that uses cgo to call C code passing values\nwhose types are almost always invalid according to the cgo pointer\nsharing rules.\nSpecifically, it warns about attempts to pass a Go chan, map, func,\nor slice to C, either directly, or via a pointer, array, or struct.", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/cgocall", -- Default: true, -- }, -- { -- Name: "composites", -- Doc: "check for unkeyed composite literals\n\nThis analyzer reports a diagnostic for composite literals of struct\ntypes imported from another package that do not use the field-keyed\nsyntax. Such literals are fragile because the addition of a new field\n(even if unexported) to the struct will cause compilation to fail.\n\nAs an example,\n\n\terr = &net.DNSConfigError{err}\n\nshould be replaced by:\n\n\terr = &net.DNSConfigError{Err: err}\n", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/composite", -- Default: true, -- }, -- { -- Name: "copylocks", -- Doc: "check for locks erroneously passed by value\n\nInadvertently copying a value containing a lock, such as sync.Mutex or\nsync.WaitGroup, may cause both copies to malfunction. Generally such\nvalues should be referred to through a pointer.", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/copylocks", -- Default: true, -- }, -- { -- Name: "deepequalerrors", -- Doc: "check for calls of reflect.DeepEqual on error values\n\nThe deepequalerrors checker looks for calls of the form:\n\n reflect.DeepEqual(err1, err2)\n\nwhere err1 and err2 are errors. Using reflect.DeepEqual to compare\nerrors is discouraged.", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/deepequalerrors", -- Default: true, -- }, -- { -- Name: "defers", -- Doc: "report common mistakes in defer statements\n\nThe defers analyzer reports a diagnostic when a defer statement would\nresult in a non-deferred call to time.Since, as experience has shown\nthat this is nearly always a mistake.\n\nFor example:\n\n\tstart := time.Now()\n\t...\n\tdefer recordLatency(time.Since(start)) // error: call to time.Since is not deferred\n\nThe correct code is:\n\n\tdefer func() { recordLatency(time.Since(start)) }()", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/defers", -- Default: true, -- }, -- { -- Name: "deprecated", -- Doc: "check for use of deprecated identifiers\n\nThe deprecated analyzer looks for deprecated symbols and package imports.\n\nSee https://go.dev/wiki/Deprecated to learn about Go's convention\nfor documenting and signaling deprecated identifiers.", -- Default: true, -- }, -- { -- Name: "directive", -- Doc: "check Go toolchain directives such as //go:debug\n\nThis analyzer checks for problems with known Go toolchain directives\nin all Go source files in a package directory, even those excluded by\n//go:build constraints, and all non-Go source files too.\n\nFor //go:debug (see https://go.dev/doc/godebug), the analyzer checks\nthat the directives are placed only in Go source files, only above the\npackage comment, and only in package main or *_test.go files.\n\nSupport for other known directives may be added in the future.\n\nThis analyzer does not check //go:build, which is handled by the\nbuildtag analyzer.\n", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/directive", -- Default: true, -- }, -- { -- Name: "embed", -- Doc: "check //go:embed directive usage\n\nThis analyzer checks that the embed package is imported if //go:embed\ndirectives are present, providing a suggested fix to add the import if\nit is missing.\n\nThis analyzer also checks that //go:embed directives precede the\ndeclaration of a single variable.", -- Default: true, -- }, -- { -- Name: "errorsas", -- Doc: "report passing non-pointer or non-error values to errors.As\n\nThe errorsas analysis reports calls to errors.As where the type\nof the second argument is not a pointer to a type implementing error.", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/errorsas", -- Default: true, -- }, -- { -- Name: "fieldalignment", -- Doc: "find structs that would use less memory if their fields were sorted\n\nThis analyzer find structs that can be rearranged to use less memory, and provides\na suggested edit with the most compact order.\n\nNote that there are two different diagnostics reported. One checks struct size,\nand the other reports \"pointer bytes\" used. Pointer bytes is how many bytes of the\nobject that the garbage collector has to potentially scan for pointers, for example:\n\n\tstruct { uint32; string }\n\nhave 16 pointer bytes because the garbage collector has to scan up through the string's\ninner pointer.\n\n\tstruct { string; *uint32 }\n\nhas 24 pointer bytes because it has to scan further through the *uint32.\n\n\tstruct { string; uint32 }\n\nhas 8 because it can stop immediately after the string pointer.\n\nBe aware that the most compact order is not always the most efficient.\nIn rare cases it may cause two variables each updated by its own goroutine\nto occupy the same CPU cache line, inducing a form of memory contention\nknown as \"false sharing\" that slows down both goroutines.\n", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/fieldalignment", -- }, -- { -- Name: "httpresponse", -- Doc: "check for mistakes using HTTP responses\n\nA common mistake when using the net/http package is to defer a function\ncall to close the http.Response Body before checking the error that\ndetermines whether the response is valid:\n\n\tresp, err := http.Head(url)\n\tdefer resp.Body.Close()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\t// (defer statement belongs here)\n\nThis checker helps uncover latent nil dereference bugs by reporting a\ndiagnostic for such mistakes.", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/httpresponse", -- Default: true, -- }, -- { -- Name: "ifaceassert", -- Doc: "detect impossible interface-to-interface type assertions\n\nThis checker flags type assertions v.(T) and corresponding type-switch cases\nin which the static type V of v is an interface that cannot possibly implement\nthe target interface T. This occurs when V and T contain methods with the same\nname but different signatures. Example:\n\n\tvar v interface {\n\t\tRead()\n\t}\n\t_ = v.(io.Reader)\n\nThe Read method in v has a different signature than the Read method in\nio.Reader, so this assertion cannot succeed.", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/ifaceassert", -- Default: true, -- }, -- { -- Name: "loopclosure", -- Doc: "check references to loop variables from within nested functions\n\nThis analyzer reports places where a function literal references the\niteration variable of an enclosing loop, and the loop calls the function\nin such a way (e.g. with go or defer) that it may outlive the loop\niteration and possibly observe the wrong value of the variable.\n\nIn this example, all the deferred functions run after the loop has\ncompleted, so all observe the final value of v.\n\n\tfor _, v := range list {\n\t defer func() {\n\t use(v) // incorrect\n\t }()\n\t}\n\nOne fix is to create a new variable for each iteration of the loop:\n\n\tfor _, v := range list {\n\t v := v // new var per iteration\n\t defer func() {\n\t use(v) // ok\n\t }()\n\t}\n\nThe next example uses a go statement and has a similar problem.\nIn addition, it has a data race because the loop updates v\nconcurrent with the goroutines accessing it.\n\n\tfor _, v := range elem {\n\t go func() {\n\t use(v) // incorrect, and a data race\n\t }()\n\t}\n\nA fix is the same as before. The checker also reports problems\nin goroutines started by golang.org/x/sync/errgroup.Group.\nA hard-to-spot variant of this form is common in parallel tests:\n\n\tfunc Test(t *testing.T) {\n\t for _, test := range tests {\n\t t.Run(test.name, func(t *testing.T) {\n\t t.Parallel()\n\t use(test) // incorrect, and a data race\n\t })\n\t }\n\t}\n\nThe t.Parallel() call causes the rest of the function to execute\nconcurrent with the loop.\n\nThe analyzer reports references only in the last statement,\nas it is not deep enough to understand the effects of subsequent\nstatements that might render the reference benign.\n(\"Last statement\" is defined recursively in compound\nstatements such as if, switch, and select.)\n\nSee: https://golang.org/doc/go_faq.html#closures_and_goroutines", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/loopclosure", -- Default: true, -- }, -- { -- Name: "lostcancel", -- Doc: "check cancel func returned by context.WithCancel is called\n\nThe cancellation function returned by context.WithCancel, WithTimeout,\nand WithDeadline must be called or the new context will remain live\nuntil its parent context is cancelled.\n(The background context is never cancelled.)", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/lostcancel", -- Default: true, -- }, -- { -- Name: "nilfunc", -- Doc: "check for useless comparisons between functions and nil\n\nA useless comparison is one like f == nil as opposed to f() == nil.", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/nilfunc", -- Default: true, -- }, -- { -- Name: "nilness", -- Doc: "check for redundant or impossible nil comparisons\n\nThe nilness checker inspects the control-flow graph of each function in\na package and reports nil pointer dereferences, degenerate nil\npointers, and panics with nil values. A degenerate comparison is of the form\nx==nil or x!=nil where x is statically known to be nil or non-nil. These are\noften a mistake, especially in control flow related to errors. Panics with nil\nvalues are checked because they are not detectable by\n\n\tif r := recover(); r != nil {\n\nThis check reports conditions such as:\n\n\tif f == nil { // impossible condition (f is a function)\n\t}\n\nand:\n\n\tp := &v\n\t...\n\tif p != nil { // tautological condition\n\t}\n\nand:\n\n\tif p == nil {\n\t\tprint(*p) // nil dereference\n\t}\n\nand:\n\n\tif p == nil {\n\t\tpanic(p)\n\t}", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/nilness", -- Default: true, -- }, -- { -- Name: "printf", -- Doc: "check consistency of Printf format strings and arguments\n\nThe check applies to calls of the formatting functions such as\n[fmt.Printf] and [fmt.Sprintf], as well as any detected wrappers of\nthose functions.\n\nIn this example, the %d format operator requires an integer operand:\n\n\tfmt.Printf(\"%d\", \"hello\") // fmt.Printf format %d has arg \"hello\" of wrong type string\n\nSee the documentation of the fmt package for the complete set of\nformat operators and their operand types.\n\nTo enable printf checking on a function that is not found by this\nanalyzer's heuristics (for example, because control is obscured by\ndynamic method calls), insert a bogus call:\n\n\tfunc MyPrintf(format string, args ...any) {\n\t\tif false {\n\t\t\t_ = fmt.Sprintf(format, args...) // enable printf checker\n\t\t}\n\t\t...\n\t}\n\nThe -funcs flag specifies a comma-separated list of names of additional\nknown formatting functions or methods. If the name contains a period,\nit must denote a specific function using one of the following forms:\n\n\tdir/pkg.Function\n\tdir/pkg.Type.Method\n\t(*dir/pkg.Type).Method\n\nOtherwise the name is interpreted as a case-insensitive unqualified\nidentifier such as \"errorf\". Either way, if a listed name ends in f, the\nfunction is assumed to be Printf-like, taking a format string before the\nargument list. Otherwise it is assumed to be Print-like, taking a list\nof arguments with no format string.", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/printf", -- Default: true, -- }, -- { -- Name: "shadow", -- Doc: "check for possible unintended shadowing of variables\n\nThis analyzer check for shadowed variables.\nA shadowed variable is a variable declared in an inner scope\nwith the same name and type as a variable in an outer scope,\nand where the outer variable is mentioned after the inner one\nis declared.\n\n(This definition can be refined; the module generates too many\nfalse positives and is not yet enabled by default.)\n\nFor example:\n\n\tfunc BadRead(f *os.File, buf []byte) error {\n\t\tvar err error\n\t\tfor {\n\t\t\tn, err := f.Read(buf) // shadows the function variable 'err'\n\t\t\tif err != nil {\n\t\t\t\tbreak // causes return of wrong value\n\t\t\t}\n\t\t\tfoo(buf)\n\t\t}\n\t\treturn err\n\t}", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shadow", -- }, -- { -- Name: "shift", -- Doc: "check for shifts that equal or exceed the width of the integer", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/shift", -- Default: true, -- }, -- { -- Name: "simplifycompositelit", -- Doc: "check for composite literal simplifications\n\nAn array, slice, or map composite literal of the form:\n\t[]T{T{}, T{}}\nwill be simplified to:\n\t[]T{{}, {}}\n\nThis is one of the simplifications that \"gofmt -s\" applies.", -- Default: true, -- }, -- { -- Name: "simplifyrange", -- Doc: "check for range statement simplifications\n\nA range of the form:\n\tfor x, _ = range v {...}\nwill be simplified to:\n\tfor x = range v {...}\n\nA range of the form:\n\tfor _ = range v {...}\nwill be simplified to:\n\tfor range v {...}\n\nThis is one of the simplifications that \"gofmt -s\" applies.", -- Default: true, -- }, -- { -- Name: "simplifyslice", -- Doc: "check for slice simplifications\n\nA slice expression of the form:\n\ts[a:len(s)]\nwill be simplified to:\n\ts[a:]\n\nThis is one of the simplifications that \"gofmt -s\" applies.", -- Default: true, -- }, -- { -- Name: "slog", -- Doc: "check for invalid structured logging calls\n\nThe slog checker looks for calls to functions from the log/slog\npackage that take alternating key-value pairs. It reports calls\nwhere an argument in a key position is neither a string nor a\nslog.Attr, and where a final key is missing its value.\nFor example,it would report\n\n\tslog.Warn(\"message\", 11, \"k\") // slog.Warn arg \"11\" should be a string or a slog.Attr\n\nand\n\n\tslog.Info(\"message\", \"k1\", v1, \"k2\") // call to slog.Info missing a final value", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/slog", -- Default: true, -- }, -- { -- Name: "sortslice", -- Doc: "check the argument type of sort.Slice\n\nsort.Slice requires an argument of a slice type. Check that\nthe interface{} value passed to sort.Slice is actually a slice.", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/sortslice", -- Default: true, -- }, -- { -- Name: "stdmethods", -- Doc: "check signature of methods of well-known interfaces\n\nSometimes a type may be intended to satisfy an interface but may fail to\ndo so because of a mistake in its method signature.\nFor example, the result of this WriteTo method should be (int64, error),\nnot error, to satisfy io.WriterTo:\n\n\ttype myWriterTo struct{...}\n\tfunc (myWriterTo) WriteTo(w io.Writer) error { ... }\n\nThis check ensures that each method whose name matches one of several\nwell-known interface methods from the standard library has the correct\nsignature for that interface.\n\nChecked method names include:\n\n\tFormat GobEncode GobDecode MarshalJSON MarshalXML\n\tPeek ReadByte ReadFrom ReadRune Scan Seek\n\tUnmarshalJSON UnreadByte UnreadRune WriteByte\n\tWriteTo", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/stdmethods", -- Default: true, -- }, -- { -- Name: "stringintconv", -- Doc: "check for string(int) conversions\n\nThis checker flags conversions of the form string(x) where x is an integer\n(but not byte or rune) type. Such conversions are discouraged because they\nreturn the UTF-8 representation of the Unicode code point x, and not a decimal\nstring representation of x as one might expect. Furthermore, if x denotes an\ninvalid code point, the conversion cannot be statically rejected.\n\nFor conversions that intend on using the code point, consider replacing them\nwith string(rune(x)). Otherwise, strconv.Itoa and its equivalents return the\nstring representation of the value in the desired base.", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/stringintconv", -- Default: true, -- }, -- { -- Name: "structtag", -- Doc: "check that struct field tags conform to reflect.StructTag.Get\n\nAlso report certain struct tags (json, xml) used with unexported fields.", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/structtag", -- Default: true, -- }, -- { -- Name: "testinggoroutine", -- Doc: "report calls to (*testing.T).Fatal from goroutines started by a test.\n\nFunctions that abruptly terminate a test, such as the Fatal, Fatalf, FailNow, and\nSkip{,f,Now} methods of *testing.T, must be called from the test goroutine itself.\nThis checker detects calls to these functions that occur within a goroutine\nstarted by the test. For example:\n\n\tfunc TestFoo(t *testing.T) {\n\t go func() {\n\t t.Fatal(\"oops\") // error: (*T).Fatal called from non-test goroutine\n\t }()\n\t}", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/testinggoroutine", -- Default: true, -- }, -- { -- Name: "tests", -- Doc: "check for common mistaken usages of tests and examples\n\nThe tests checker walks Test, Benchmark, Fuzzing and Example functions checking\nmalformed names, wrong signatures and examples documenting non-existent\nidentifiers.\n\nPlease see the documentation for package testing in golang.org/pkg/testing\nfor the conventions that are enforced for Tests, Benchmarks, and Examples.", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/tests", -- Default: true, -- }, -- { -- Name: "timeformat", -- Doc: "check for calls of (time.Time).Format or time.Parse with 2006-02-01\n\nThe timeformat checker looks for time formats with the 2006-02-01 (yyyy-dd-mm)\nformat. Internationally, \"yyyy-dd-mm\" does not occur in common calendar date\nstandards, and so it is more likely that 2006-01-02 (yyyy-mm-dd) was intended.", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/timeformat", -- Default: true, -- }, -- { -- Name: "unmarshal", -- Doc: "report passing non-pointer or non-interface values to unmarshal\n\nThe unmarshal analysis reports calls to functions such as json.Unmarshal\nin which the argument type is not a pointer or an interface.", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unmarshal", -- Default: true, -- }, -- { -- Name: "unreachable", -- Doc: "check for unreachable code\n\nThe unreachable analyzer finds statements that execution can never reach\nbecause they are preceded by an return statement, a call to panic, an\ninfinite loop, or similar constructs.", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unreachable", -- Default: true, -- }, -- { -- Name: "unsafeptr", -- Doc: "check for invalid conversions of uintptr to unsafe.Pointer\n\nThe unsafeptr analyzer reports likely incorrect uses of unsafe.Pointer\nto convert integers to pointers. A conversion from uintptr to\nunsafe.Pointer is invalid if it implies that there is a uintptr-typed\nword in memory that holds a pointer value, because that word will be\ninvisible to stack copying and to the garbage collector.", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unsafeptr", -- Default: true, -- }, -- { -- Name: "unusedparams", -- Doc: "check for unused parameters of functions\n\nThe unusedparams analyzer checks functions to see if there are\nany parameters that are not being used.\n\nTo reduce false positives it ignores:\n- methods\n- parameters that do not have a name or have the name '_' (the blank identifier)\n- functions in test files\n- functions with empty bodies or those with just a return stmt", -- }, -- { -- Name: "unusedresult", -- Doc: "check for unused results of calls to some functions\n\nSome functions like fmt.Errorf return a result and have no side\neffects, so it is always a mistake to discard the result. Other\nfunctions may return an error that must not be ignored, or a cleanup\noperation that must be called. This analyzer reports calls to\nfunctions like these when the result of the call is ignored.\n\nThe set of functions may be controlled using flags.", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unusedresult", -- Default: true, -- }, -- { -- Name: "unusedwrite", -- Doc: "checks for unused writes\n\nThe analyzer reports instances of writes to struct fields and\narrays that are never read. Specifically, when a struct object\nor an array is copied, its elements are copied implicitly by\nthe compiler, and any element write to this copy does nothing\nwith the original object.\n\nFor example:\n\n\ttype T struct { x int }\n\n\tfunc f(input []T) {\n\t\tfor i, v := range input { // v is a copy\n\t\t\tv.x = i // unused write to field x\n\t\t}\n\t}\n\nAnother example is about non-pointer receiver:\n\n\ttype T struct { x int }\n\n\tfunc (t T) f() { // t is a copy\n\t\tt.x = i // unused write to field x\n\t}", -- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unusedwrite", -- }, -- { -- Name: "useany", -- Doc: "check for constraints that could be simplified to \"any\"", -- }, -- { -- Name: "fillreturns", -- Doc: "suggest fixes for errors due to an incorrect number of return values\n\nThis checker provides suggested fixes for type errors of the\ntype \"wrong number of return values (want %d, got %d)\". For example:\n\tfunc m() (int, string, *bool, error) {\n\t\treturn\n\t}\nwill turn into\n\tfunc m() (int, string, *bool, error) {\n\t\treturn 0, \"\", nil, nil\n\t}\n\nThis functionality is similar to https://github.com/sqs/goreturns.\n", -- Default: true, -- }, -- { -- Name: "nonewvars", -- Doc: "suggested fixes for \"no new vars on left side of :=\"\n\nThis checker provides suggested fixes for type errors of the\ntype \"no new vars on left side of :=\". For example:\n\tz := 1\n\tz := 2\nwill turn into\n\tz := 1\n\tz = 2\n", -- Default: true, -- }, -- { -- Name: "noresultvalues", -- Doc: "suggested fixes for unexpected return values\n\nThis checker provides suggested fixes for type errors of the\ntype \"no result values expected\" or \"too many return values\".\nFor example:\n\tfunc z() { return nil }\nwill turn into\n\tfunc z() { return }\n", -- Default: true, -- }, -- { -- Name: "undeclaredname", -- Doc: "suggested fixes for \"undeclared name: <>\"\n\nThis checker provides suggested fixes for type errors of the\ntype \"undeclared name: <>\". It will either insert a new statement,\nsuch as:\n\n\"<> := \"\n\nor a new function declaration, such as:\n\nfunc <>(inferred parameters) {\n\tpanic(\"implement me!\")\n}\n", -- Default: true, -- }, -- { -- Name: "unusedvariable", -- Doc: "check for unused variables\n\nThe unusedvariable analyzer suggests fixes for unused variables errors.\n", -- }, -- { -- Name: "fillstruct", -- Doc: "note incomplete struct initializations\n\nThis analyzer provides diagnostics for any struct literals that do not have\nany fields initialized. Because the suggested fix for this analysis is\nexpensive to compute, callers should compute it separately, using the\nSuggestedFix function below.\n", -- Default: true, -- }, -- { -- Name: "infertypeargs", -- Doc: "check for unnecessary type arguments in call expressions\n\nExplicit type arguments may be omitted from call expressions if they can be\ninferred from function arguments, or from other type arguments:\n\n\tfunc f[T any](T) {}\n\t\n\tfunc _() {\n\t\tf[string](\"foo\") // string could be inferred\n\t}\n", -- Default: true, -- }, -- { -- Name: "stubmethods", -- Doc: "stub methods analyzer\n\nThis analyzer generates method stubs for concrete types\nin order to implement a target interface", -- Default: true, -- }, +- { +- scenario: "cursor after last character on second line", +- input: funnyString, +- line: 2, +- col: 7, // 4 + 1 + 1 + 1 (1-indexed) +- offset: 13, // 7 (length of first line) + 4 + 1 + 1 (unused since we have line+col) +- resUTF16col: 5, // 2 + 1 + 1 + 1 (1-indexed) +- pre: "𐐀45", +- post: "", - }, -- Hints: []*HintJSON{ -- { -- Name: "assignVariableTypes", -- Doc: "Enable/disable inlay hints for variable types in assign statements:\n```go\n\ti/* int*/, j/* int*/ := 0, len(r)-1\n```", -- }, -- { -- Name: "compositeLiteralFields", -- Doc: "Enable/disable inlay hints for composite literal field names:\n```go\n\t{/*in: */\"Hello, world\", /*want: */\"dlrow ,olleH\"}\n```", -- }, -- { -- Name: "compositeLiteralTypes", -- Doc: "Enable/disable inlay hints for composite literal types:\n```go\n\tfor _, c := range []struct {\n\t\tin, want string\n\t}{\n\t\t/*struct{ in string; want string }*/{\"Hello, world\", \"dlrow ,olleH\"},\n\t}\n```", -- }, -- { -- Name: "constantValues", -- Doc: "Enable/disable inlay hints for constant values:\n```go\n\tconst (\n\t\tKindNone Kind = iota/* = 0*/\n\t\tKindPrint/* = 1*/\n\t\tKindPrintf/* = 2*/\n\t\tKindErrorf/* = 3*/\n\t)\n```", -- }, -- { -- Name: "functionTypeParameters", -- Doc: "Enable/disable inlay hints for implicit type parameters on generic functions:\n```go\n\tmyFoo/*[int, string]*/(1, \"hello\")\n```", -- }, -- { -- Name: "parameterNames", -- Doc: "Enable/disable inlay hints for parameter names:\n```go\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)\n```", -- }, -- { -- Name: "rangeVariableTypes", -- Doc: "Enable/disable inlay hints for variable types in range statements:\n```go\n\tfor k/* int*/, v/* string*/ := range []string{} {\n\t\tfmt.Println(k, v)\n\t}\n```", -- }, +- { +- scenario: "cursor beyond end of file", +- input: funnyString, +- line: 2, +- col: 8, // 4 + 1 + 1 + 1 + 1 (1-indexed) +- offset: 14, // 4 + 1 + 1 + 1 (unused since we have line+col) +- err: "column is beyond end of file", +- }, +-} +- +-var fromUTF16Tests = []struct { +- scenario string +- input []byte +- line int // 1-indexed line number (isn't actually used) +- utf16col int // 1-indexed UTF-16 col number +- resCol int // 1-indexed byte position in line +- resOffset int // 0-indexed byte offset into input +- pre string // everything before the cursor on the line +- post string // everything from the cursor onwards +- err string // expected error string in call to ToUTF16Column +-}{ +- { +- scenario: "zero length input; cursor at first col, first line", +- input: []byte(""), +- line: 1, +- utf16col: 1, +- resCol: 1, +- resOffset: 0, +- pre: "", +- post: "", +- }, +- { +- scenario: "cursor before funny character", +- input: funnyString, +- line: 1, +- utf16col: 1, +- resCol: 1, +- resOffset: 0, +- pre: "", +- post: "𐐀23", +- }, +- { +- scenario: "cursor after funny character", +- input: funnyString, +- line: 1, +- utf16col: 3, +- resCol: 5, +- resOffset: 4, +- pre: "𐐀", +- post: "23", +- }, +- { +- scenario: "cursor after last character on line", +- input: funnyString, +- line: 1, +- utf16col: 5, +- resCol: 7, +- resOffset: 6, +- pre: "𐐀23", +- post: "", +- }, +- { +- scenario: "cursor beyond last character on line", +- input: funnyString, +- line: 1, +- utf16col: 6, +- resCol: 7, +- resOffset: 6, +- pre: "𐐀23", +- post: "", +- err: "column is beyond end of line", +- }, +- { +- scenario: "cursor before funny character; second line", +- input: funnyString, +- line: 2, +- utf16col: 1, +- resCol: 1, +- resOffset: 7, +- pre: "", +- post: "𐐀45", +- }, +- { +- scenario: "cursor after funny character; second line", +- input: funnyString, +- line: 2, +- utf16col: 3, // 2 + 1 (1-indexed) +- resCol: 5, // 4 + 1 (1-indexed) +- resOffset: 11, // 7 (length of first line) + 4 +- pre: "𐐀", +- post: "45", +- }, +- { +- scenario: "cursor after last character on second line", +- input: funnyString, +- line: 2, +- utf16col: 5, // 2 + 1 + 1 + 1 (1-indexed) +- resCol: 7, // 4 + 1 + 1 + 1 (1-indexed) +- resOffset: 13, // 7 (length of first line) + 4 + 1 + 1 +- pre: "𐐀45", +- post: "", +- }, +- { +- scenario: "cursor beyond end of file", +- input: funnyString, +- line: 2, +- utf16col: 6, // 2 + 1 + 1 + 1 + 1(1-indexed) +- resCol: 8, // 4 + 1 + 1 + 1 + 1 (1-indexed) +- resOffset: 14, // 7 (length of first line) + 4 + 1 + 1 + 1 +- err: "column is beyond end of file", - }, -} -diff -urN a/gopls/internal/lsp/source/call_hierarchy.go b/gopls/internal/lsp/source/call_hierarchy.go ---- a/gopls/internal/lsp/source/call_hierarchy.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/call_hierarchy.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,311 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +- +-func TestToUTF16(t *testing.T) { +- for _, e := range toUTF16Tests { +- t.Run(e.scenario, func(t *testing.T) { +- if e.issue != nil && !*e.issue { +- t.Skip("expected to fail") +- } +- m := protocol.NewMapper("", e.input) +- var pos protocol.Position +- var err error +- if e.line > 0 { +- pos, err = m.LineCol8Position(e.line, e.col) +- } else if e.offset >= 0 { +- pos, err = m.OffsetPosition(e.offset) +- } else { +- err = fmt.Errorf("point has neither offset nor line/column") +- } +- if err != nil { +- if err.Error() != e.err { +- t.Fatalf("expected error %v; got %v", e.err, err) +- } +- return +- } +- if e.err != "" { +- t.Fatalf("unexpected success; wanted %v", e.err) +- } +- got := int(pos.Character) + 1 +- if got != e.resUTF16col { +- t.Fatalf("expected result %v; got %v", e.resUTF16col, got) +- } +- pre, post := getPrePost(e.input, e.offset) +- if pre != e.pre { +- t.Fatalf("expected #%d pre %q; got %q", e.offset, e.pre, pre) +- } +- if post != e.post { +- t.Fatalf("expected #%d, post %q; got %q", e.offset, e.post, post) +- } +- }) +- } +-} +- +-func TestFromUTF16(t *testing.T) { +- for _, e := range fromUTF16Tests { +- t.Run(e.scenario, func(t *testing.T) { +- m := protocol.NewMapper("", e.input) +- offset, err := m.PositionOffset(protocol.Position{ +- Line: uint32(e.line - 1), +- Character: uint32(e.utf16col - 1), +- }) +- if err != nil { +- if err.Error() != e.err { +- t.Fatalf("expected error %v; got %v", e.err, err) +- } +- return +- } +- if e.err != "" { +- t.Fatalf("unexpected success; wanted %v", e.err) +- } +- if offset != e.resOffset { +- t.Fatalf("expected offset %v; got %v", e.resOffset, offset) +- } +- line, col8 := m.OffsetLineCol8(offset) +- if line != e.line { +- t.Fatalf("expected resulting line %v; got %v", e.line, line) +- } +- if col8 != e.resCol { +- t.Fatalf("expected resulting col %v; got %v", e.resCol, col8) +- } +- pre, post := getPrePost(e.input, offset) +- if pre != e.pre { +- t.Fatalf("expected #%d pre %q; got %q", offset, e.pre, pre) +- } +- if post != e.post { +- t.Fatalf("expected #%d post %q; got %q", offset, e.post, post) +- } +- }) +- } +-} +- +-func getPrePost(content []byte, offset int) (string, string) { +- pre, post := string(content)[:offset], string(content)[offset:] +- if i := strings.LastIndex(pre, "\n"); i >= 0 { +- pre = pre[i+1:] +- } +- if i := strings.IndexRune(post, '\n'); i >= 0 { +- post = post[:i] +- } +- return pre, post +-} +- +-// -- these are the historical lsppos tests -- +- +-type testCase struct { +- content string // input text +- substrOrOffset interface{} // explicit integer offset, or a substring +- wantLine, wantChar int // expected LSP position information +-} +- +-// offset returns the test case byte offset +-func (c testCase) offset() int { +- switch x := c.substrOrOffset.(type) { +- case int: +- return x +- case string: +- i := strings.Index(c.content, x) +- if i < 0 { +- panic(fmt.Sprintf("%q does not contain substring %q", c.content, x)) +- } +- return i +- } +- panic("substrOrIndex must be an integer or string") +-} +- +-var tests = []testCase{ +- {"a𐐀b", "a", 0, 0}, +- {"a𐐀b", "𐐀", 0, 1}, +- {"a𐐀b", "b", 0, 3}, +- {"a𐐀b\n", "\n", 0, 4}, +- {"a𐐀b\r\n", "\n", 0, 4}, // \r|\n is not a valid position, so we move back to the end of the first line. +- {"a𐐀b\r\nx", "x", 1, 0}, +- {"a𐐀b\r\nx\ny", "y", 2, 0}, +- +- // Testing EOL and EOF positions +- {"", 0, 0, 0}, // 0th position of an empty buffer is (0, 0) +- {"abc", "c", 0, 2}, +- {"abc", 3, 0, 3}, +- {"abc\n", "\n", 0, 3}, +- {"abc\n", 4, 1, 0}, // position after a newline is on the next line +-} +- +-func TestLineChar(t *testing.T) { +- for _, test := range tests { +- m := protocol.NewMapper("", []byte(test.content)) +- offset := test.offset() +- posn, _ := m.OffsetPosition(offset) +- gotLine, gotChar := int(posn.Line), int(posn.Character) +- if gotLine != test.wantLine || gotChar != test.wantChar { +- t.Errorf("LineChar(%d) = (%d,%d), want (%d,%d)", offset, gotLine, gotChar, test.wantLine, test.wantChar) +- } +- } +-} +- +-func TestInvalidOffset(t *testing.T) { +- content := []byte("a𐐀b\r\nx\ny") +- m := protocol.NewMapper("", content) +- for _, offset := range []int{-1, 100} { +- posn, err := m.OffsetPosition(offset) +- if err == nil { +- t.Errorf("OffsetPosition(%d) = %s, want error", offset, posn) +- } +- } +-} +- +-func TestPosition(t *testing.T) { +- for _, test := range tests { +- m := protocol.NewMapper("", []byte(test.content)) +- offset := test.offset() +- got, err := m.OffsetPosition(offset) +- if err != nil { +- t.Errorf("OffsetPosition(%d) failed: %v", offset, err) +- continue +- } +- want := protocol.Position{Line: uint32(test.wantLine), Character: uint32(test.wantChar)} +- if got != want { +- t.Errorf("Position(%d) = %v, want %v", offset, got, want) +- } +- } +-} +- +-func TestRange(t *testing.T) { +- for _, test := range tests { +- m := protocol.NewMapper("", []byte(test.content)) +- offset := test.offset() +- got, err := m.OffsetRange(0, offset) +- if err != nil { +- t.Fatal(err) +- } +- want := protocol.Range{ +- End: protocol.Position{Line: uint32(test.wantLine), Character: uint32(test.wantChar)}, +- } +- if got != want { +- t.Errorf("Range(%d) = %v, want %v", offset, got, want) +- } +- } +-} +- +-func TestBytesOffset(t *testing.T) { +- tests := []struct { +- text string +- pos protocol.Position +- want int +- }{ +- // U+10400 encodes as [F0 90 90 80] in UTF-8 and [D801 DC00] in UTF-16. +- {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 0}, want: 0}, +- {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 1}, want: 1}, +- {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 2}, want: 1}, +- {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 3}, want: 5}, +- {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 4}, want: 6}, +- {text: `a𐐀b`, pos: protocol.Position{Line: 0, Character: 5}, want: -1}, +- {text: "aaa\nbbb\n", pos: protocol.Position{Line: 0, Character: 3}, want: 3}, +- {text: "aaa\nbbb\n", pos: protocol.Position{Line: 0, Character: 4}, want: -1}, +- {text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 0}, want: 4}, +- {text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 3}, want: 7}, +- {text: "aaa\nbbb\n", pos: protocol.Position{Line: 1, Character: 4}, want: -1}, +- {text: "aaa\nbbb\n", pos: protocol.Position{Line: 2, Character: 0}, want: 8}, +- {text: "aaa\nbbb\n", pos: protocol.Position{Line: 2, Character: 1}, want: -1}, +- {text: "aaa\nbbb\n\n", pos: protocol.Position{Line: 2, Character: 0}, want: 8}, +- } +- +- for i, test := range tests { +- fname := fmt.Sprintf("test %d", i) +- uri := protocol.URIFromPath(fname) +- mapper := protocol.NewMapper(uri, []byte(test.text)) +- got, err := mapper.PositionOffset(test.pos) +- if err != nil && test.want != -1 { +- t.Errorf("%d: unexpected error: %v", i, err) +- } +- if err == nil && got != test.want { +- t.Errorf("want %d for %q(Line:%d,Character:%d), but got %d", test.want, test.text, int(test.pos.Line), int(test.pos.Character), got) +- } +- } +-} +diff -urN a/gopls/internal/protocol/protocol.go b/gopls/internal/protocol/protocol.go +--- a/gopls/internal/protocol/protocol.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/protocol.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,313 +0,0 @@ +-// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package source +-package protocol - -import ( +- "bytes" - "context" -- "errors" +- "encoding/json" - "fmt" -- "go/ast" -- "go/token" -- "go/types" -- "path/filepath" +- "io" - -- "golang.org/x/tools/go/ast/astutil" -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/gopls/internal/span" +- "golang.org/x/telemetry/crashmonitor" +- "golang.org/x/tools/gopls/internal/util/bug" - "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/tag" +- "golang.org/x/tools/internal/jsonrpc2" +- jsonrpc2_v2 "golang.org/x/tools/internal/jsonrpc2_v2" +- "golang.org/x/tools/internal/xcontext" -) - --// PrepareCallHierarchy returns an array of CallHierarchyItem for a file and the position within the file. --func PrepareCallHierarchy(ctx context.Context, snapshot Snapshot, fh FileHandle, pp protocol.Position) ([]protocol.CallHierarchyItem, error) { -- ctx, done := event.Start(ctx, "source.PrepareCallHierarchy") -- defer done() +-var ( +- // RequestCancelledError should be used when a request is cancelled early. +- RequestCancelledError = jsonrpc2.NewError(-32800, "JSON RPC cancelled") +- RequestCancelledErrorV2 = jsonrpc2_v2.NewError(-32800, "JSON RPC cancelled") +-) - -- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) -- if err != nil { -- return nil, err +-type ClientCloser interface { +- Client +- io.Closer +-} +- +-type connSender interface { +- io.Closer +- +- Notify(ctx context.Context, method string, params interface{}) error +- Call(ctx context.Context, method string, params, result interface{}) error +-} +- +-type clientDispatcher struct { +- sender connSender +-} +- +-func (c *clientDispatcher) Close() error { +- return c.sender.Close() +-} +- +-// ClientDispatcher returns a Client that dispatches LSP requests across the +-// given jsonrpc2 connection. +-func ClientDispatcher(conn jsonrpc2.Conn) ClientCloser { +- return &clientDispatcher{sender: clientConn{conn}} +-} +- +-type clientConn struct { +- conn jsonrpc2.Conn +-} +- +-func (c clientConn) Close() error { +- return c.conn.Close() +-} +- +-func (c clientConn) Notify(ctx context.Context, method string, params interface{}) error { +- return c.conn.Notify(ctx, method, params) +-} +- +-func (c clientConn) Call(ctx context.Context, method string, params interface{}, result interface{}) error { +- id, err := c.conn.Call(ctx, method, params, result) +- if ctx.Err() != nil { +- cancelCall(ctx, c, id) - } -- pos, err := pgf.PositionPos(pp) -- if err != nil { -- return nil, err +- return err +-} +- +-func ClientDispatcherV2(conn *jsonrpc2_v2.Connection) ClientCloser { +- return &clientDispatcher{clientConnV2{conn}} +-} +- +-type clientConnV2 struct { +- conn *jsonrpc2_v2.Connection +-} +- +-func (c clientConnV2) Close() error { +- return c.conn.Close() +-} +- +-func (c clientConnV2) Notify(ctx context.Context, method string, params interface{}) error { +- return c.conn.Notify(ctx, method, params) +-} +- +-func (c clientConnV2) Call(ctx context.Context, method string, params interface{}, result interface{}) error { +- call := c.conn.Call(ctx, method, params) +- err := call.Await(ctx, result) +- if ctx.Err() != nil { +- detached := xcontext.Detach(ctx) +- c.conn.Notify(detached, "$/cancelRequest", &CancelParams{ID: call.ID().Raw()}) - } +- return err +-} - -- _, obj, _ := referencedObject(pkg, pgf, pos) -- if obj == nil { -- return nil, nil +-// ServerDispatcher returns a Server that dispatches LSP requests across the +-// given jsonrpc2 connection. +-func ServerDispatcher(conn jsonrpc2.Conn) Server { +- return &serverDispatcher{sender: clientConn{conn}} +-} +- +-func ServerDispatcherV2(conn *jsonrpc2_v2.Connection) Server { +- return &serverDispatcher{sender: clientConnV2{conn}} +-} +- +-type serverDispatcher struct { +- sender connSender +-} +- +-func ClientHandler(client Client, handler jsonrpc2.Handler) jsonrpc2.Handler { +- return func(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error { +- if ctx.Err() != nil { +- ctx := xcontext.Detach(ctx) +- return reply(ctx, nil, RequestCancelledError) +- } +- handled, err := clientDispatch(ctx, client, reply, req) +- if handled || err != nil { +- return err +- } +- return handler(ctx, reply, req) - } +-} - -- if _, ok := obj.Type().Underlying().(*types.Signature); !ok { -- return nil, nil +-func ClientHandlerV2(client Client) jsonrpc2_v2.Handler { +- return jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) { +- if ctx.Err() != nil { +- return nil, RequestCancelledErrorV2 +- } +- req1 := req2to1(req) +- var ( +- result interface{} +- resErr error +- ) +- replier := func(_ context.Context, res interface{}, err error) error { +- if err != nil { +- resErr = err +- return nil +- } +- result = res +- return nil +- } +- _, err := clientDispatch(ctx, client, replier, req1) +- if err != nil { +- return nil, err +- } +- return result, resErr +- }) +-} +- +-func ServerHandler(server Server, handler jsonrpc2.Handler) jsonrpc2.Handler { +- return func(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error { +- if ctx.Err() != nil { +- ctx := xcontext.Detach(ctx) +- return reply(ctx, nil, RequestCancelledError) +- } +- handled, err := serverDispatch(ctx, server, reply, req) +- if handled || err != nil { +- return err +- } +- return handler(ctx, reply, req) - } +-} - -- declLoc, err := mapPosition(ctx, pkg.FileSet(), snapshot, obj.Pos(), adjustedObjEnd(obj)) +-func ServerHandlerV2(server Server) jsonrpc2_v2.Handler { +- return jsonrpc2_v2.HandlerFunc(func(ctx context.Context, req *jsonrpc2_v2.Request) (interface{}, error) { +- if ctx.Err() != nil { +- return nil, RequestCancelledErrorV2 +- } +- req1 := req2to1(req) +- var ( +- result interface{} +- resErr error +- ) +- replier := func(_ context.Context, res interface{}, err error) error { +- if err != nil { +- resErr = err +- return nil +- } +- result = res +- return nil +- } +- _, err := serverDispatch(ctx, server, replier, req1) +- if err != nil { +- return nil, err +- } +- return result, resErr +- }) +-} +- +-func req2to1(req2 *jsonrpc2_v2.Request) jsonrpc2.Request { +- if req2.ID.IsValid() { +- raw := req2.ID.Raw() +- var idv1 jsonrpc2.ID +- switch v := raw.(type) { +- case int64: +- idv1 = jsonrpc2.NewIntID(v) +- case string: +- idv1 = jsonrpc2.NewStringID(v) +- default: +- panic(fmt.Sprintf("unsupported ID type %T", raw)) +- } +- req1, err := jsonrpc2.NewCall(idv1, req2.Method, req2.Params) +- if err != nil { +- panic(err) +- } +- return req1 +- } +- req1, err := jsonrpc2.NewNotification(req2.Method, req2.Params) - if err != nil { -- return nil, err +- panic(err) - } -- rng := declLoc.Range +- return req1 +-} - -- callHierarchyItem := protocol.CallHierarchyItem{ -- Name: obj.Name(), -- Kind: protocol.Function, -- Tags: []protocol.SymbolTag{}, -- Detail: fmt.Sprintf("%s • %s", obj.Pkg().Path(), filepath.Base(declLoc.URI.SpanURI().Filename())), -- URI: declLoc.URI, -- Range: rng, -- SelectionRange: rng, +-func Handlers(handler jsonrpc2.Handler) jsonrpc2.Handler { +- return CancelHandler( +- jsonrpc2.AsyncHandler( +- jsonrpc2.MustReplyHandler(handler))) +-} +- +-func CancelHandler(handler jsonrpc2.Handler) jsonrpc2.Handler { +- handler, canceller := jsonrpc2.CancelHandler(handler) +- return func(ctx context.Context, reply jsonrpc2.Replier, req jsonrpc2.Request) error { +- if req.Method() != "$/cancelRequest" { +- // TODO(iancottrell): See if we can generate a reply for the request to be cancelled +- // at the point of cancellation rather than waiting for gopls to naturally reply. +- // To do that, we need to keep track of whether a reply has been sent already and +- // be careful about racing between the two paths. +- // TODO(iancottrell): Add a test that watches the stream and verifies the response +- // for the cancelled request flows. +- replyWithDetachedContext := func(ctx context.Context, resp interface{}, err error) error { +- // https://microsoft.github.io/language-server-protocol/specifications/specification-current/#cancelRequest +- if ctx.Err() != nil && err == nil { +- err = RequestCancelledError +- } +- ctx = xcontext.Detach(ctx) +- return reply(ctx, resp, err) +- } +- return handler(ctx, replyWithDetachedContext, req) +- } +- var params CancelParams +- if err := UnmarshalJSON(req.Params(), ¶ms); err != nil { +- return sendParseError(ctx, reply, err) +- } +- if n, ok := params.ID.(float64); ok { +- canceller(jsonrpc2.NewIntID(int64(n))) +- } else if s, ok := params.ID.(string); ok { +- canceller(jsonrpc2.NewStringID(s)) +- } else { +- return sendParseError(ctx, reply, fmt.Errorf("request ID %v malformed", params.ID)) +- } +- return reply(ctx, nil, nil) - } -- return []protocol.CallHierarchyItem{callHierarchyItem}, nil -} - --// IncomingCalls returns an array of CallHierarchyIncomingCall for a file and the position within the file. --func IncomingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, pos protocol.Position) ([]protocol.CallHierarchyIncomingCall, error) { -- ctx, done := event.Start(ctx, "source.IncomingCalls") -- defer done() +-func Call(ctx context.Context, conn jsonrpc2.Conn, method string, params interface{}, result interface{}) error { +- id, err := conn.Call(ctx, method, params, result) +- if ctx.Err() != nil { +- cancelCall(ctx, clientConn{conn}, id) +- } +- return err +-} +- +-func cancelCall(ctx context.Context, sender connSender, id jsonrpc2.ID) { +- ctx = xcontext.Detach(ctx) +- ctx, done := event.Start(ctx, "protocol.canceller") +- defer done() +- // Note that only *jsonrpc2.ID implements json.Marshaler. +- sender.Notify(ctx, "$/cancelRequest", &CancelParams{ID: &id}) +-} +- +-// UnmarshalJSON unmarshals msg into the variable pointed to by +-// params. In JSONRPC, optional messages may be +-// "null", in which case it is a no-op. +-func UnmarshalJSON(msg json.RawMessage, v any) error { +- if len(msg) == 0 || bytes.Equal(msg, []byte("null")) { +- return nil +- } +- return json.Unmarshal(msg, v) +-} +- +-func sendParseError(ctx context.Context, reply jsonrpc2.Replier, err error) error { +- return reply(ctx, nil, fmt.Errorf("%w: %s", jsonrpc2.ErrParse, err)) +-} +- +-// NonNilSlice returns x, or an empty slice if x was nil. +-// +-// (Many slice fields of protocol structs must be non-nil +-// to avoid being encoded as JSON "null".) +-func NonNilSlice[T comparable](x []T) []T { +- if x == nil { +- return []T{} +- } +- return x +-} +- +-func recoverHandlerPanic(method string) { +- // Report panics in the handler goroutine, +- // unless we have enabled the monitor, +- // which reports all crashes. +- if !crashmonitor.Supported() { +- defer func() { +- if x := recover(); x != nil { +- bug.Reportf("panic in %s request", method) +- panic(x) +- } +- }() +- } +-} +diff -urN a/gopls/internal/protocol/semantic.go b/gopls/internal/protocol/semantic.go +--- a/gopls/internal/protocol/semantic.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/semantic.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,56 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package protocol +- +-// The file defines helpers for semantics tokens. +- +-import "fmt" +- +-// SemanticTypes to use in case there is no client, as in the command line, or tests. +-func SemanticTypes() []string { +- return semanticTypes[:] +-} +- +-// SemanticModifiers to use in case there is no client. +-func SemanticModifiers() []string { +- return semanticModifiers[:] +-} +- +-// SemType returns a string equivalent of the type, for gopls semtok +-func SemType(n int) string { +- tokTypes := SemanticTypes() +- tokMods := SemanticModifiers() +- if n >= 0 && n < len(tokTypes) { +- return tokTypes[n] +- } +- // not found for some reason +- return fmt.Sprintf("?%d[%d,%d]?", n, len(tokTypes), len(tokMods)) +-} +- +-// SemMods returns the []string equivalent of the mods, for gopls semtok. +-func SemMods(n int) []string { +- tokMods := SemanticModifiers() +- mods := []string{} +- for i := 0; i < len(tokMods); i++ { +- if (n & (1 << uint(i))) != 0 { +- mods = append(mods, tokMods[i]) +- } +- } +- return mods +-} +- +-// From https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_semanticTokens +-var ( +- semanticTypes = [...]string{ +- "namespace", "type", "class", "enum", "interface", +- "struct", "typeParameter", "parameter", "variable", "property", "enumMember", +- "event", "function", "method", "macro", "keyword", "modifier", "comment", +- "string", "number", "regexp", "operator", +- } +- semanticModifiers = [...]string{ +- "declaration", "definition", "readonly", "static", +- "deprecated", "abstract", "async", "modification", "documentation", "defaultLibrary", +- } +-) +diff -urN a/gopls/internal/protocol/semtok/semtok.go b/gopls/internal/protocol/semtok/semtok.go +--- a/gopls/internal/protocol/semtok/semtok.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/semtok/semtok.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,108 +0,0 @@ +-// Copyright 2024 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-// The semtok package provides an encoder for LSP's semantic tokens. +-package semtok +- +-import "sort" +- +-// A Token provides the extent and semantics of a token. +-type Token struct { +- Line, Start uint32 +- Len uint32 +- Type TokenType +- Modifiers []string +-} +- +-type TokenType string - -- refs, err := references(ctx, snapshot, fh, pos, false) -- if err != nil { -- if errors.Is(err, ErrNoIdentFound) || errors.Is(err, errNoObjectFound) { -- return nil, nil +-const ( +- // These are the tokens defined by LSP 3.17, but a client is +- // free to send its own set; any tokens that the server emits +- // that are not in this set are simply not encoded in the bitfield. +- TokNamespace TokenType = "namespace" +- TokType TokenType = "type" +- TokInterface TokenType = "interface" +- TokTypeParam TokenType = "typeParameter" +- TokParameter TokenType = "parameter" +- TokVariable TokenType = "variable" +- TokMethod TokenType = "method" +- TokFunction TokenType = "function" +- TokKeyword TokenType = "keyword" +- TokComment TokenType = "comment" +- TokString TokenType = "string" +- TokNumber TokenType = "number" +- TokOperator TokenType = "operator" +- TokMacro TokenType = "macro" // for templates +- +- // not part of LSP 3.17 (even though JS has labels) +- // https://github.com/microsoft/vscode-languageserver-node/issues/1422 +- TokLabel TokenType = "label" +-) +- +-// Encode returns the LSP encoding of a sequence of tokens. +-// The noStrings, noNumbers options cause strings, numbers to be skipped. +-// The lists of types and modifiers determines the bitfield encoding. +-func Encode( +- tokens []Token, +- noStrings, noNumbers bool, +- types, modifiers []string) []uint32 { +- +- // binary operators, at least, will be out of order +- sort.Slice(tokens, func(i, j int) bool { +- if tokens[i].Line != tokens[j].Line { +- return tokens[i].Line < tokens[j].Line - } -- return nil, err +- return tokens[i].Start < tokens[j].Start +- }) +- +- typeMap := make(map[TokenType]int) +- for i, t := range types { +- typeMap[TokenType(t)] = i - } - -- // Group references by their enclosing function declaration. -- incomingCalls := make(map[protocol.Location]*protocol.CallHierarchyIncomingCall) -- for _, ref := range refs { -- callItem, err := enclosingNodeCallItem(ctx, snapshot, ref.pkgPath, ref.location) -- if err != nil { -- event.Error(ctx, "error getting enclosing node", err, tag.Method.Of(string(ref.pkgPath))) +- modMap := make(map[string]int) +- for i, m := range modifiers { +- modMap[m] = 1 << uint(i) // go 1.12 compatibility +- } +- +- // each semantic token needs five values +- // (see Integer Encoding for Tokens in the LSP spec) +- x := make([]uint32, 5*len(tokens)) +- var j int +- var last Token +- for i := 0; i < len(tokens); i++ { +- item := tokens[i] +- typ, ok := typeMap[item.Type] +- if !ok { +- continue // client doesn't want typeStr +- } +- if item.Type == TokString && noStrings { - continue - } -- loc := protocol.Location{ -- URI: callItem.URI, -- Range: callItem.Range, +- if item.Type == TokNumber && noNumbers { +- continue - } -- call, ok := incomingCalls[loc] -- if !ok { -- call = &protocol.CallHierarchyIncomingCall{From: callItem} -- incomingCalls[loc] = call +- if j == 0 { +- x[0] = tokens[0].Line +- } else { +- x[j] = item.Line - last.Line - } -- call.FromRanges = append(call.FromRanges, ref.location.Range) -- } -- -- // Flatten the map of pointers into a slice of values. -- incomingCallItems := make([]protocol.CallHierarchyIncomingCall, 0, len(incomingCalls)) -- for _, callItem := range incomingCalls { -- incomingCallItems = append(incomingCallItems, *callItem) +- x[j+1] = item.Start +- if j > 0 && x[j] == 0 { +- x[j+1] = item.Start - last.Start +- } +- x[j+2] = item.Len +- x[j+3] = uint32(typ) +- mask := 0 +- for _, s := range item.Modifiers { +- // modMap[s] is 0 if the client doesn't want this modifier +- mask |= modMap[s] +- } +- x[j+4] = uint32(mask) +- j += 5 +- last = item - } -- return incomingCallItems, nil +- return x[:j] -} +diff -urN a/gopls/internal/protocol/span.go b/gopls/internal/protocol/span.go +--- a/gopls/internal/protocol/span.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/span.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,100 +0,0 @@ +-// Copyright 2018 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// enclosingNodeCallItem creates a CallHierarchyItem representing the function call at loc. --func enclosingNodeCallItem(ctx context.Context, snapshot Snapshot, pkgPath PackagePath, loc protocol.Location) (protocol.CallHierarchyItem, error) { -- // Parse the file containing the reference. -- fh, err := snapshot.ReadFile(ctx, loc.URI.SpanURI()) -- if err != nil { -- return protocol.CallHierarchyItem{}, err -- } -- // TODO(adonovan): opt: before parsing, trim the bodies of functions -- // that don't contain the reference, using either a scanner-based -- // implementation such as https://go.dev/play/p/KUrObH1YkX8 -- // (~31% speedup), or a byte-oriented implementation (2x speedup). -- pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) -- if err != nil { -- return protocol.CallHierarchyItem{}, err -- } -- start, end, err := pgf.RangePos(loc.Range) -- if err != nil { -- return protocol.CallHierarchyItem{}, err -- } +-package protocol - -- // Find the enclosing function, if any, and the number of func literals in between. -- var funcDecl *ast.FuncDecl -- var funcLit *ast.FuncLit // innermost function literal -- var litCount int -- path, _ := astutil.PathEnclosingInterval(pgf.File, start, end) --outer: -- for _, node := range path { -- switch n := node.(type) { -- case *ast.FuncDecl: -- funcDecl = n -- break outer -- case *ast.FuncLit: -- litCount++ -- if litCount > 1 { -- continue -- } -- funcLit = n +-import ( +- "fmt" +- "unicode/utf8" +-) +- +-// CompareLocation defines a three-valued comparison over locations, +-// lexicographically ordered by (URI, Range). +-func CompareLocation(x, y Location) int { +- if x.URI != y.URI { +- if x.URI < y.URI { +- return -1 +- } else { +- return +1 - } - } +- return CompareRange(x.Range, y.Range) +-} - -- nameIdent := path[len(path)-1].(*ast.File).Name -- kind := protocol.Package -- if funcDecl != nil { -- nameIdent = funcDecl.Name -- kind = protocol.Function +-// CompareRange returns -1 if a is before b, 0 if a == b, and 1 if a is after b. +-// +-// A range a is defined to be 'before' b if a.Start is before b.Start, or +-// a.Start == b.Start and a.End is before b.End. +-func CompareRange(a, b Range) int { +- if r := ComparePosition(a.Start, b.Start); r != 0 { +- return r - } +- return ComparePosition(a.End, b.End) +-} - -- nameStart, nameEnd := nameIdent.Pos(), nameIdent.End() -- if funcLit != nil { -- nameStart, nameEnd = funcLit.Type.Func, funcLit.Type.Params.Pos() -- kind = protocol.Function +-// ComparePosition returns -1 if a is before b, 0 if a == b, and 1 if a is after b. +-func ComparePosition(a, b Position) int { +- if a.Line != b.Line { +- if a.Line < b.Line { +- return -1 +- } else { +- return +1 +- } - } -- rng, err := pgf.PosRange(nameStart, nameEnd) -- if err != nil { -- return protocol.CallHierarchyItem{}, err +- if a.Character != b.Character { +- if a.Character < b.Character { +- return -1 +- } else { +- return +1 +- } - } +- return 0 +-} - -- name := nameIdent.Name -- for i := 0; i < litCount; i++ { -- name += ".func()" +-func Intersect(a, b Range) bool { +- if a.Start.Line > b.End.Line || a.End.Line < b.Start.Line { +- return false - } +- return !((a.Start.Line == b.End.Line) && a.Start.Character > b.End.Character || +- (a.End.Line == b.Start.Line) && a.End.Character < b.Start.Character) +-} - -- return protocol.CallHierarchyItem{ -- Name: name, -- Kind: kind, -- Tags: []protocol.SymbolTag{}, -- Detail: fmt.Sprintf("%s • %s", pkgPath, filepath.Base(fh.URI().Filename())), -- URI: loc.URI, -- Range: rng, -- SelectionRange: rng, -- }, nil +-// Format implements fmt.Formatter. +-// +-// Note: Formatter is implemented instead of Stringer (presumably) for +-// performance reasons, though it is not clear that it matters in practice. +-func (r Range) Format(f fmt.State, _ rune) { +- fmt.Fprintf(f, "%v-%v", r.Start, r.End) -} - --// OutgoingCalls returns an array of CallHierarchyOutgoingCall for a file and the position within the file. --func OutgoingCalls(ctx context.Context, snapshot Snapshot, fh FileHandle, pp protocol.Position) ([]protocol.CallHierarchyOutgoingCall, error) { -- ctx, done := event.Start(ctx, "source.OutgoingCalls") -- defer done() +-// Format implements fmt.Formatter. +-// +-// See Range.Format for discussion of why the Formatter interface is +-// implemented rather than Stringer. +-func (p Position) Format(f fmt.State, _ rune) { +- fmt.Fprintf(f, "%v:%v", p.Line, p.Character) +-} - -- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) -- if err != nil { -- return nil, err -- } -- pos, err := pgf.PositionPos(pp) -- if err != nil { -- return nil, err -- } +-// -- implementation helpers -- - -- _, obj, _ := referencedObject(pkg, pgf, pos) -- if obj == nil { -- return nil, nil -- } +-// UTF16Len returns the number of codes in the UTF-16 transcoding of s. +-func UTF16Len(s []byte) int { +- var n int +- for len(s) > 0 { +- n++ - -- if _, ok := obj.Type().Underlying().(*types.Signature); !ok { -- return nil, nil -- } +- // Fast path for ASCII. +- if s[0] < 0x80 { +- s = s[1:] +- continue +- } - -- // Skip builtins. -- if obj.Pkg() == nil { -- return nil, nil +- r, size := utf8.DecodeRune(s) +- if r >= 0x10000 { +- n++ // surrogate pair +- } +- s = s[size:] - } +- return n +-} +diff -urN a/gopls/internal/protocol/tsclient.go b/gopls/internal/protocol/tsclient.go +--- a/gopls/internal/protocol/tsclient.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/tsclient.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,276 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- if !obj.Pos().IsValid() { -- return nil, bug.Errorf("internal error: object %s.%s missing position", obj.Pkg().Path(), obj.Name()) -- } +-// Code generated for LSP. DO NOT EDIT. - -- declFile := pkg.FileSet().File(obj.Pos()) -- if declFile == nil { -- return nil, bug.Errorf("file not found for %d", obj.Pos()) -- } +-package protocol - -- uri := span.URIFromPath(declFile.Name()) -- offset, err := safetoken.Offset(declFile, obj.Pos()) -- if err != nil { -- return nil, err -- } +-// Code generated from protocol/metaModel.json at ref release/protocol/3.17.6-next.2 (hash 654dc9be6673c61476c28fda604406279c3258d7). +-// https://github.com/microsoft/vscode-languageserver-node/blob/release/protocol/3.17.6-next.2/protocol/metaModel.json +-// LSP metaData.version = 3.17.0. - -- // Use TypecheckFull as we want to inspect the body of the function declaration. -- declPkg, declPGF, err := NarrowestPackageForFile(ctx, snapshot, uri) -- if err != nil { -- return nil, err -- } +-import ( +- "context" - -- declPos, err := safetoken.Pos(declPGF.Tok, offset) -- if err != nil { -- return nil, err -- } +- "golang.org/x/tools/internal/jsonrpc2" +-) - -- declNode, _, _ := findDeclInfo([]*ast.File{declPGF.File}, declPos) -- if declNode == nil { -- // TODO(rfindley): why don't we return an error here, or even bug.Errorf? -- return nil, nil -- // return nil, bug.Errorf("failed to find declaration for object %s.%s", obj.Pkg().Path(), obj.Name()) -- } +-type Client interface { +- LogTrace(context.Context, *LogTraceParams) error // $/logTrace +- Progress(context.Context, *ProgressParams) error // $/progress +- RegisterCapability(context.Context, *RegistrationParams) error // client/registerCapability +- UnregisterCapability(context.Context, *UnregistrationParams) error // client/unregisterCapability +- Event(context.Context, *interface{}) error // telemetry/event +- PublishDiagnostics(context.Context, *PublishDiagnosticsParams) error // textDocument/publishDiagnostics +- LogMessage(context.Context, *LogMessageParams) error // window/logMessage +- ShowDocument(context.Context, *ShowDocumentParams) (*ShowDocumentResult, error) // window/showDocument +- ShowMessage(context.Context, *ShowMessageParams) error // window/showMessage +- ShowMessageRequest(context.Context, *ShowMessageRequestParams) (*MessageActionItem, error) // window/showMessageRequest +- WorkDoneProgressCreate(context.Context, *WorkDoneProgressCreateParams) error // window/workDoneProgress/create +- ApplyEdit(context.Context, *ApplyWorkspaceEditParams) (*ApplyWorkspaceEditResult, error) // workspace/applyEdit +- CodeLensRefresh(context.Context) error // workspace/codeLens/refresh +- Configuration(context.Context, *ParamConfiguration) ([]LSPAny, error) // workspace/configuration +- DiagnosticRefresh(context.Context) error // workspace/diagnostic/refresh +- FoldingRangeRefresh(context.Context) error // workspace/foldingRange/refresh +- InlayHintRefresh(context.Context) error // workspace/inlayHint/refresh +- InlineValueRefresh(context.Context) error // workspace/inlineValue/refresh +- SemanticTokensRefresh(context.Context) error // workspace/semanticTokens/refresh +- WorkspaceFolders(context.Context) ([]WorkspaceFolder, error) // workspace/workspaceFolders +-} - -- type callRange struct { -- start, end token.Pos -- } -- callRanges := []callRange{} -- ast.Inspect(declNode, func(n ast.Node) bool { -- if call, ok := n.(*ast.CallExpr); ok { -- var start, end token.Pos -- switch n := call.Fun.(type) { -- case *ast.SelectorExpr: -- start, end = n.Sel.NamePos, call.Lparen -- case *ast.Ident: -- start, end = n.NamePos, call.Lparen -- case *ast.FuncLit: -- // while we don't add the function literal as an 'outgoing' call -- // we still want to traverse into it -- return true -- default: -- // ignore any other kind of call expressions -- // for ex: direct function literal calls since that's not an 'outgoing' call -- return false -- } -- callRanges = append(callRanges, callRange{start: start, end: end}) +-func clientDispatch(ctx context.Context, client Client, reply jsonrpc2.Replier, r jsonrpc2.Request) (bool, error) { +- defer recoverHandlerPanic(r.Method()) +- switch r.Method() { +- case "$/logTrace": +- var params LogTraceParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } -- return true -- }) +- err := client.LogTrace(ctx, ¶ms) +- return true, reply(ctx, nil, err) - -- outgoingCalls := map[token.Pos]*protocol.CallHierarchyOutgoingCall{} -- for _, callRange := range callRanges { -- _, obj, _ := referencedObject(declPkg, declPGF, callRange.start) -- if obj == nil { -- continue +- case "$/progress": +- var params ProgressParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } +- err := client.Progress(ctx, ¶ms) +- return true, reply(ctx, nil, err) - -- // ignore calls to builtin functions -- if obj.Pkg() == nil { -- continue +- case "client/registerCapability": +- var params RegistrationParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } +- err := client.RegisterCapability(ctx, ¶ms) +- return true, reply(ctx, nil, err) - -- outgoingCall, ok := outgoingCalls[obj.Pos()] -- if !ok { -- loc, err := mapPosition(ctx, declPkg.FileSet(), snapshot, obj.Pos(), obj.Pos()+token.Pos(len(obj.Name()))) -- if err != nil { -- return nil, err -- } -- outgoingCall = &protocol.CallHierarchyOutgoingCall{ -- To: protocol.CallHierarchyItem{ -- Name: obj.Name(), -- Kind: protocol.Function, -- Tags: []protocol.SymbolTag{}, -- Detail: fmt.Sprintf("%s • %s", obj.Pkg().Path(), filepath.Base(loc.URI.SpanURI().Filename())), -- URI: loc.URI, -- Range: loc.Range, -- SelectionRange: loc.Range, -- }, -- } -- outgoingCalls[obj.Pos()] = outgoingCall +- case "client/unregisterCapability": +- var params UnregistrationParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- err := client.UnregisterCapability(ctx, ¶ms) +- return true, reply(ctx, nil, err) +- +- case "telemetry/event": +- var params interface{} +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } +- err := client.Event(ctx, ¶ms) +- return true, reply(ctx, nil, err) - -- rng, err := declPGF.PosRange(callRange.start, callRange.end) +- case "textDocument/publishDiagnostics": +- var params PublishDiagnosticsParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- err := client.PublishDiagnostics(ctx, ¶ms) +- return true, reply(ctx, nil, err) +- +- case "window/logMessage": +- var params LogMessageParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- err := client.LogMessage(ctx, ¶ms) +- return true, reply(ctx, nil, err) +- +- case "window/showDocument": +- var params ShowDocumentParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := client.ShowDocument(ctx, ¶ms) - if err != nil { -- return nil, err +- return true, reply(ctx, nil, err) - } -- outgoingCall.FromRanges = append(outgoingCall.FromRanges, rng) +- return true, reply(ctx, resp, nil) +- +- case "window/showMessage": +- var params ShowMessageParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- err := client.ShowMessage(ctx, ¶ms) +- return true, reply(ctx, nil, err) +- +- case "window/showMessageRequest": +- var params ShowMessageRequestParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := client.ShowMessageRequest(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) +- +- case "window/workDoneProgress/create": +- var params WorkDoneProgressCreateParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- err := client.WorkDoneProgressCreate(ctx, ¶ms) +- return true, reply(ctx, nil, err) +- +- case "workspace/applyEdit": +- var params ApplyWorkspaceEditParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := client.ApplyEdit(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) +- +- case "workspace/codeLens/refresh": +- err := client.CodeLensRefresh(ctx) +- return true, reply(ctx, nil, err) +- +- case "workspace/configuration": +- var params ParamConfiguration +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := client.Configuration(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) +- +- case "workspace/diagnostic/refresh": +- err := client.DiagnosticRefresh(ctx) +- return true, reply(ctx, nil, err) +- +- case "workspace/foldingRange/refresh": +- err := client.FoldingRangeRefresh(ctx) +- return true, reply(ctx, nil, err) +- +- case "workspace/inlayHint/refresh": +- err := client.InlayHintRefresh(ctx) +- return true, reply(ctx, nil, err) +- +- case "workspace/inlineValue/refresh": +- err := client.InlineValueRefresh(ctx) +- return true, reply(ctx, nil, err) +- +- case "workspace/semanticTokens/refresh": +- err := client.SemanticTokensRefresh(ctx) +- return true, reply(ctx, nil, err) +- +- case "workspace/workspaceFolders": +- resp, err := client.WorkspaceFolders(ctx) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) +- +- default: +- return false, nil - } +-} - -- outgoingCallItems := make([]protocol.CallHierarchyOutgoingCall, 0, len(outgoingCalls)) -- for _, callItem := range outgoingCalls { -- outgoingCallItems = append(outgoingCallItems, *callItem) +-func (s *clientDispatcher) LogTrace(ctx context.Context, params *LogTraceParams) error { +- return s.sender.Notify(ctx, "$/logTrace", params) +-} +-func (s *clientDispatcher) Progress(ctx context.Context, params *ProgressParams) error { +- return s.sender.Notify(ctx, "$/progress", params) +-} +-func (s *clientDispatcher) RegisterCapability(ctx context.Context, params *RegistrationParams) error { +- return s.sender.Call(ctx, "client/registerCapability", params, nil) +-} +-func (s *clientDispatcher) UnregisterCapability(ctx context.Context, params *UnregistrationParams) error { +- return s.sender.Call(ctx, "client/unregisterCapability", params, nil) +-} +-func (s *clientDispatcher) Event(ctx context.Context, params *interface{}) error { +- return s.sender.Notify(ctx, "telemetry/event", params) +-} +-func (s *clientDispatcher) PublishDiagnostics(ctx context.Context, params *PublishDiagnosticsParams) error { +- return s.sender.Notify(ctx, "textDocument/publishDiagnostics", params) +-} +-func (s *clientDispatcher) LogMessage(ctx context.Context, params *LogMessageParams) error { +- return s.sender.Notify(ctx, "window/logMessage", params) +-} +-func (s *clientDispatcher) ShowDocument(ctx context.Context, params *ShowDocumentParams) (*ShowDocumentResult, error) { +- var result *ShowDocumentResult +- if err := s.sender.Call(ctx, "window/showDocument", params, &result); err != nil { +- return nil, err +- } +- return result, nil +-} +-func (s *clientDispatcher) ShowMessage(ctx context.Context, params *ShowMessageParams) error { +- return s.sender.Notify(ctx, "window/showMessage", params) +-} +-func (s *clientDispatcher) ShowMessageRequest(ctx context.Context, params *ShowMessageRequestParams) (*MessageActionItem, error) { +- var result *MessageActionItem +- if err := s.sender.Call(ctx, "window/showMessageRequest", params, &result); err != nil { +- return nil, err +- } +- return result, nil +-} +-func (s *clientDispatcher) WorkDoneProgressCreate(ctx context.Context, params *WorkDoneProgressCreateParams) error { +- return s.sender.Call(ctx, "window/workDoneProgress/create", params, nil) +-} +-func (s *clientDispatcher) ApplyEdit(ctx context.Context, params *ApplyWorkspaceEditParams) (*ApplyWorkspaceEditResult, error) { +- var result *ApplyWorkspaceEditResult +- if err := s.sender.Call(ctx, "workspace/applyEdit", params, &result); err != nil { +- return nil, err +- } +- return result, nil +-} +-func (s *clientDispatcher) CodeLensRefresh(ctx context.Context) error { +- return s.sender.Call(ctx, "workspace/codeLens/refresh", nil, nil) +-} +-func (s *clientDispatcher) Configuration(ctx context.Context, params *ParamConfiguration) ([]LSPAny, error) { +- var result []LSPAny +- if err := s.sender.Call(ctx, "workspace/configuration", params, &result); err != nil { +- return nil, err +- } +- return result, nil +-} +-func (s *clientDispatcher) DiagnosticRefresh(ctx context.Context) error { +- return s.sender.Call(ctx, "workspace/diagnostic/refresh", nil, nil) +-} +-func (s *clientDispatcher) FoldingRangeRefresh(ctx context.Context) error { +- return s.sender.Call(ctx, "workspace/foldingRange/refresh", nil, nil) +-} +-func (s *clientDispatcher) InlayHintRefresh(ctx context.Context) error { +- return s.sender.Call(ctx, "workspace/inlayHint/refresh", nil, nil) +-} +-func (s *clientDispatcher) InlineValueRefresh(ctx context.Context) error { +- return s.sender.Call(ctx, "workspace/inlineValue/refresh", nil, nil) +-} +-func (s *clientDispatcher) SemanticTokensRefresh(ctx context.Context) error { +- return s.sender.Call(ctx, "workspace/semanticTokens/refresh", nil, nil) +-} +-func (s *clientDispatcher) WorkspaceFolders(ctx context.Context) ([]WorkspaceFolder, error) { +- var result []WorkspaceFolder +- if err := s.sender.Call(ctx, "workspace/workspaceFolders", nil, &result); err != nil { +- return nil, err - } -- return outgoingCallItems, nil +- return result, nil -} -diff -urN a/gopls/internal/lsp/source/change_signature.go b/gopls/internal/lsp/source/change_signature.go ---- a/gopls/internal/lsp/source/change_signature.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/change_signature.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,572 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/protocol/tsdocument_changes.go b/gopls/internal/protocol/tsdocument_changes.go +--- a/gopls/internal/protocol/tsdocument_changes.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/tsdocument_changes.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,42 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package source +-package protocol - -import ( -- "bytes" -- "context" +- "encoding/json" - "fmt" -- "go/ast" -- "go/format" -- "go/parser" -- "go/token" -- "go/types" -- "regexp" -- -- "golang.org/x/tools/go/ast/astutil" -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/imports" -- internalastutil "golang.org/x/tools/internal/astutil" -- "golang.org/x/tools/internal/diff" -- "golang.org/x/tools/internal/refactor/inline" -- "golang.org/x/tools/internal/tokeninternal" -- "golang.org/x/tools/internal/typesinternal" -) - --// RemoveUnusedParameter computes a refactoring to remove the parameter --// indicated by the given range, which must be contained within an unused --// parameter name or field. --// --// This operation is a work in progress. Remaining TODO: --// - Handle function assignment correctly. --// - Improve the extra newlines in output. --// - Stream type checking via ForEachPackage. --// - Avoid unnecessary additional type checking. --func RemoveUnusedParameter(ctx context.Context, fh FileHandle, rng protocol.Range, snapshot Snapshot) ([]protocol.DocumentChanges, error) { -- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) -- if err != nil { -- return nil, err -- } -- if perrors, terrors := pkg.GetParseErrors(), pkg.GetTypeErrors(); len(perrors) > 0 || len(terrors) > 0 { -- var sample string -- if len(perrors) > 0 { -- sample = perrors[0].Error() -- } else { -- sample = terrors[0].Error() -- } -- return nil, fmt.Errorf("can't change signatures for packages with parse or type errors: (e.g. %s)", sample) -- } +-// DocumentChanges is a union of a file edit and directory rename operations +-// for package renaming feature. At most one field of this struct is non-nil. +-type DocumentChanges struct { +- TextDocumentEdit *TextDocumentEdit +- RenameFile *RenameFile +-} - -- info := FindParam(pgf, rng) -- if info.Decl == nil { -- return nil, fmt.Errorf("failed to find declaration") -- } -- if info.Decl.Recv != nil { -- return nil, fmt.Errorf("can't change signature of methods (yet)") -- } -- if info.Field == nil { -- return nil, fmt.Errorf("failed to find field") -- } +-func (d *DocumentChanges) UnmarshalJSON(data []byte) error { +- var m map[string]interface{} - -- // Create the new declaration, which is a copy of the original decl with the -- // unnecessary parameter removed. -- newDecl := internalastutil.CloneNode(info.Decl) -- if info.Name != nil { -- names := remove(newDecl.Type.Params.List[info.FieldIndex].Names, info.NameIndex) -- newDecl.Type.Params.List[info.FieldIndex].Names = names -- } -- if len(newDecl.Type.Params.List[info.FieldIndex].Names) == 0 { -- // Unnamed, or final name was removed: in either case, remove the field. -- newDecl.Type.Params.List = remove(newDecl.Type.Params.List, info.FieldIndex) +- if err := json.Unmarshal(data, &m); err != nil { +- return err - } - -- // Compute inputs into building a wrapper function around the modified -- // signature. -- var ( -- params = internalastutil.CloneNode(info.Decl.Type.Params) // "_" names will be modified -- args []ast.Expr // arguments to delegate -- variadic = false // whether the signature is variadic -- ) -- { -- allNames := make(map[string]bool) // for renaming blanks -- for _, fld := range params.List { -- for _, n := range fld.Names { -- if n.Name != "_" { -- allNames[n.Name] = true -- } -- } -- } -- blanks := 0 -- for i, fld := range params.List { -- for j, n := range fld.Names { -- if i == info.FieldIndex && j == info.NameIndex { -- continue -- } -- if n.Name == "_" { -- // Create names for blank (_) parameters so the delegating wrapper -- // can refer to them. -- for { -- newName := fmt.Sprintf("blank%d", blanks) -- blanks++ -- if !allNames[newName] { -- n.Name = newName -- break -- } -- } -- } -- args = append(args, &ast.Ident{Name: n.Name}) -- if i == len(params.List)-1 { -- _, variadic = fld.Type.(*ast.Ellipsis) -- } -- } -- } +- if _, ok := m["textDocument"]; ok { +- d.TextDocumentEdit = new(TextDocumentEdit) +- return json.Unmarshal(data, d.TextDocumentEdit) - } - -- // Rewrite all referring calls. -- newContent, err := rewriteCalls(ctx, signatureRewrite{ -- snapshot: snapshot, -- pkg: pkg, -- pgf: pgf, -- origDecl: info.Decl, -- newDecl: newDecl, -- params: params, -- callArgs: args, -- variadic: variadic, -- }) -- if err != nil { -- return nil, err -- } -- // Finally, rewrite the original declaration. We do this after inlining all -- // calls, as there may be calls in the same file as the declaration. But none -- // of the inlining should have changed the location of the original -- // declaration. -- { -- idx := findDecl(pgf.File, info.Decl) -- if idx < 0 { -- return nil, bug.Errorf("didn't find original decl") -- } +- d.RenameFile = new(RenameFile) +- return json.Unmarshal(data, d.RenameFile) +-} - -- src, ok := newContent[pgf.URI] -- if !ok { -- src = pgf.Src -- } -- fset := tokeninternal.FileSetFor(pgf.Tok) -- src, err = rewriteSignature(fset, idx, src, newDecl) -- newContent[pgf.URI] = src +-func (d *DocumentChanges) MarshalJSON() ([]byte, error) { +- if d.TextDocumentEdit != nil { +- return json.Marshal(d.TextDocumentEdit) +- } else if d.RenameFile != nil { +- return json.Marshal(d.RenameFile) - } +- return nil, fmt.Errorf("Empty DocumentChanges union value") +-} +diff -urN a/gopls/internal/protocol/tsjson.go b/gopls/internal/protocol/tsjson.go +--- a/gopls/internal/protocol/tsjson.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/tsjson.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,2130 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- // Translate the resulting state into document changes. -- var changes []protocol.DocumentChanges -- for uri, after := range newContent { -- fh, err := snapshot.ReadFile(ctx, uri) -- if err != nil { -- return nil, err -- } -- before, err := fh.Content() -- if err != nil { -- return nil, err -- } -- edits := diff.Bytes(before, after) -- mapper := protocol.NewMapper(uri, before) -- pedits, err := ToProtocolEdits(mapper, edits) -- if err != nil { -- return nil, fmt.Errorf("computing edits for %s: %v", uri, err) -- } -- changes = append(changes, protocol.DocumentChanges{ -- TextDocumentEdit: &protocol.TextDocumentEdit{ -- TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{ -- Version: fh.Version(), -- TextDocumentIdentifier: protocol.TextDocumentIdentifier{URI: protocol.URIFromSpanURI(uri)}, -- }, -- Edits: pedits, -- }, -- }) +-// Code generated for LSP. DO NOT EDIT. +- +-package protocol +- +-// Code generated from protocol/metaModel.json at ref release/protocol/3.17.6-next.2 (hash 654dc9be6673c61476c28fda604406279c3258d7). +-// https://github.com/microsoft/vscode-languageserver-node/blob/release/protocol/3.17.6-next.2/protocol/metaModel.json +-// LSP metaData.version = 3.17.0. +- +-import "encoding/json" +- +-import "fmt" +- +-// UnmarshalError indicates that a JSON value did not conform to +-// one of the expected cases of an LSP union type. +-type UnmarshalError struct { +- msg string +-} +- +-func (e UnmarshalError) Error() string { +- return e.msg +-} +-func (t OrPLocation_workspace_symbol) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case Location: +- return json.Marshal(x) +- case LocationUriOnly: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } -- return changes, nil +- return nil, fmt.Errorf("type %T not one of [Location LocationUriOnly]", t) -} - --// rewriteSignature rewrites the signature of the declIdx'th declaration in src --// to use the signature of newDecl (described by fset). --// --// TODO(rfindley): I think this operation could be generalized, for example by --// using a concept of a 'nodepath' to correlate nodes between two related --// files. --// --// Note that with its current application, rewriteSignature is expected to --// succeed. Separate bug.Errorf calls are used below (rather than one call at --// the callsite) in order to have greater precision. --func rewriteSignature(fset *token.FileSet, declIdx int, src0 []byte, newDecl *ast.FuncDecl) ([]byte, error) { -- // Parse the new file0 content, to locate the original params. -- file0, err := parser.ParseFile(fset, "", src0, parser.ParseComments|parser.SkipObjectResolution) -- if err != nil { -- return nil, bug.Errorf("re-parsing declaring file failed: %v", err) +-func (t *OrPLocation_workspace_symbol) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil - } -- decl0, _ := file0.Decls[declIdx].(*ast.FuncDecl) -- // Inlining shouldn't have changed the location of any declarations, but do -- // a sanity check. -- if decl0 == nil || decl0.Name.Name != newDecl.Name.Name { -- return nil, bug.Errorf("inlining affected declaration order: found %v, not func %s", decl0, newDecl.Name.Name) +- var h0 Location +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil - } -- opening0, closing0, err := safetoken.Offsets(fset.File(decl0.Pos()), decl0.Type.Params.Opening, decl0.Type.Params.Closing) -- if err != nil { -- return nil, bug.Errorf("can't find params: %v", err) +- var h1 LocationUriOnly +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil - } +- return &UnmarshalError{"unmarshal failed to match one of [Location LocationUriOnly]"} +-} - -- // Format the modified signature and apply a textual replacement. This -- // minimizes comment disruption. -- formattedType := FormatNode(fset, newDecl.Type) -- expr, err := parser.ParseExprFrom(fset, "", []byte(formattedType), 0) -- if err != nil { -- return nil, bug.Errorf("parsing modified signature: %v", err) +-func (t OrPSection_workspace_didChangeConfiguration) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case []string: +- return json.Marshal(x) +- case string: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } -- newType := expr.(*ast.FuncType) -- opening1, closing1, err := safetoken.Offsets(fset.File(newType.Pos()), newType.Params.Opening, newType.Params.Closing) -- if err != nil { -- return nil, bug.Errorf("param offsets: %v", err) +- return nil, fmt.Errorf("type %T not one of [[]string string]", t) +-} +- +-func (t *OrPSection_workspace_didChangeConfiguration) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil - } -- newParams := formattedType[opening1 : closing1+1] +- var h0 []string +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 string +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [[]string string]"} +-} - -- // Splice. -- var buf bytes.Buffer -- buf.Write(src0[:opening0]) -- buf.WriteString(newParams) -- buf.Write(src0[closing0+1:]) -- newSrc := buf.Bytes() -- if len(file0.Imports) > 0 { -- formatted, err := imports.Process("output", newSrc, nil) -- if err != nil { -- return nil, bug.Errorf("imports.Process failed: %v", err) -- } -- newSrc = formatted +-func (t OrPTooltipPLabel) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case MarkupContent: +- return json.Marshal(x) +- case string: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } -- return newSrc, nil +- return nil, fmt.Errorf("type %T not one of [MarkupContent string]", t) -} - --// ParamInfo records information about a param identified by a position. --type ParamInfo struct { -- Decl *ast.FuncDecl // enclosing func decl, or nil -- FieldIndex int // index of Field in Decl.Type.Params, or -1 -- Field *ast.Field // enclosing field of Decl, or nil -- NameIndex int // index of Name in Field.Names, or nil -- Name *ast.Ident // indicated name (either enclosing, or Field.Names[0] if len(Field.Names) == 1) +-func (t *OrPTooltipPLabel) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 MarkupContent +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 string +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [MarkupContent string]"} -} - --// FindParam finds the parameter information spanned by the given range. --func FindParam(pgf *ParsedGoFile, rng protocol.Range) ParamInfo { -- info := ParamInfo{FieldIndex: -1, NameIndex: -1} -- start, end, err := pgf.RangePos(rng) -- if err != nil { -- bug.Reportf("(file=%v).RangePos(%v) failed: %v", pgf.URI, rng, err) -- return info +-func (t OrPTooltip_textDocument_inlayHint) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case MarkupContent: +- return json.Marshal(x) +- case string: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } +- return nil, fmt.Errorf("type %T not one of [MarkupContent string]", t) +-} - -- path, _ := astutil.PathEnclosingInterval(pgf.File, start, end) -- var ( -- id *ast.Ident -- field *ast.Field -- decl *ast.FuncDecl -- ) -- // Find the outermost enclosing node of each kind, whether or not they match -- // the semantics described in the docstring. -- for _, n := range path { -- switch n := n.(type) { -- case *ast.Ident: -- id = n -- case *ast.Field: -- field = n -- case *ast.FuncDecl: -- decl = n -- } +-func (t *OrPTooltip_textDocument_inlayHint) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil - } -- // Check the conditions described in the docstring. -- if decl == nil { -- return info +- var h0 MarkupContent +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil - } -- info.Decl = decl -- for fi, f := range decl.Type.Params.List { -- if f == field { -- info.FieldIndex = fi -- info.Field = f -- for ni, n := range f.Names { -- if n == id { -- info.NameIndex = ni -- info.Name = n -- break -- } -- } -- if info.Name == nil && len(info.Field.Names) == 1 { -- info.NameIndex = 0 -- info.Name = info.Field.Names[0] -- } -- break -- } +- var h1 string +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil - } -- return info +- return &UnmarshalError{"unmarshal failed to match one of [MarkupContent string]"} -} - --// signatureRewrite defines a rewritten function signature. --// --// See rewriteCalls for more details. --type signatureRewrite struct { -- snapshot Snapshot -- pkg Package -- pgf *ParsedGoFile -- origDecl, newDecl *ast.FuncDecl -- params *ast.FieldList -- callArgs []ast.Expr -- variadic bool +-func (t Or_CancelParams_id) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case int32: +- return json.Marshal(x) +- case string: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [int32 string]", t) -} - --// rewriteCalls returns the document changes required to rewrite the --// signature of origDecl to that of newDecl. --// --// This is a rather complicated factoring of the rewrite operation, but is able --// to describe arbitrary rewrites. Specifically, rewriteCalls creates a --// synthetic copy of pkg, where the original function declaration is changed to --// be a trivial wrapper around the new declaration. params and callArgs are --// used to perform this delegation: params must have the same type as origDecl, --// but may have renamed parameters (such as is required for delegating blank --// parameters). callArgs are the arguments of the delegated call (i.e. using --// params). --// --// For example, consider removing the unused 'b' parameter below, rewriting --// --// func Foo(a, b, c, _ int) int { --// return a+c --// } --// --// To --// --// func Foo(a, c, _ int) int { --// return a+c --// } --// --// In this case, rewriteCalls is parameterized as follows: --// - origDecl is the original declaration --// - newDecl is the new declaration, which is a copy of origDecl less the 'b' --// parameter. --// - params is a new parameter list (a, b, c, blank0 int) to be used for the --// new wrapper. --// - callArgs is the argument list (a, c, blank0), to be used to call the new --// delegate. --// --// rewriting is expressed this way so that rewriteCalls can own the details --// of *how* this rewriting is performed. For example, as of writing it names --// the synthetic delegate G_o_p_l_s_foo, but the caller need not know this. --// --// By passing an entirely new declaration, rewriteCalls may be used for --// signature refactorings that may affect the function body, such as removing --// or adding return values. --func rewriteCalls(ctx context.Context, rw signatureRewrite) (map[span.URI][]byte, error) { -- // tag is a unique prefix that is added to the delegated declaration. -- // -- // It must have a ~0% probability of causing collisions with existing names. -- const tag = "G_o_p_l_s_" -- -- var ( -- modifiedSrc []byte -- modifiedFile *ast.File -- modifiedDecl *ast.FuncDecl -- ) -- { -- delegate := internalastutil.CloneNode(rw.newDecl) // clone before modifying -- delegate.Name.Name = tag + delegate.Name.Name -- if obj := rw.pkg.GetTypes().Scope().Lookup(delegate.Name.Name); obj != nil { -- return nil, fmt.Errorf("synthetic name %q conflicts with an existing declaration", delegate.Name.Name) -- } -- -- wrapper := internalastutil.CloneNode(rw.origDecl) -- wrapper.Type.Params = rw.params -- call := &ast.CallExpr{ -- Fun: &ast.Ident{Name: delegate.Name.Name}, -- Args: rw.callArgs, -- } -- if rw.variadic { -- call.Ellipsis = 1 // must not be token.NoPos -- } -- -- var stmt ast.Stmt -- if delegate.Type.Results.NumFields() > 0 { -- stmt = &ast.ReturnStmt{ -- Results: []ast.Expr{call}, -- } -- } else { -- stmt = &ast.ExprStmt{ -- X: call, -- } -- } -- wrapper.Body = &ast.BlockStmt{ -- List: []ast.Stmt{stmt}, -- } -- -- fset := tokeninternal.FileSetFor(rw.pgf.Tok) -- var err error -- modifiedSrc, err = replaceFileDecl(rw.pgf, rw.origDecl, delegate) -- if err != nil { -- return nil, err -- } -- // TODO(rfindley): we can probably get away with one fewer parse operations -- // by returning the modified AST from replaceDecl. Investigate if that is -- // accurate. -- modifiedSrc = append(modifiedSrc, []byte("\n\n"+FormatNode(fset, wrapper))...) -- modifiedFile, err = parser.ParseFile(rw.pkg.FileSet(), rw.pgf.URI.Filename(), modifiedSrc, parser.ParseComments|parser.SkipObjectResolution) -- if err != nil { -- return nil, err -- } -- modifiedDecl = modifiedFile.Decls[len(modifiedFile.Decls)-1].(*ast.FuncDecl) +-func (t *Or_CancelParams_id) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil - } -- -- // Type check pkg again with the modified file, to compute the synthetic -- // callee. -- logf := logger(ctx, "change signature", rw.snapshot.Options().VerboseOutput) -- pkg2, info, err := reTypeCheck(logf, rw.pkg, map[span.URI]*ast.File{rw.pgf.URI: modifiedFile}, false) -- if err != nil { -- return nil, err +- var h0 int32 +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil - } -- calleeInfo, err := inline.AnalyzeCallee(logf, rw.pkg.FileSet(), pkg2, info, modifiedDecl, modifiedSrc) -- if err != nil { -- return nil, fmt.Errorf("analyzing callee: %v", err) +- var h1 string +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil - } -- -- post := func(got []byte) []byte { return bytes.ReplaceAll(got, []byte(tag), nil) } -- return inlineAllCalls(ctx, logf, rw.snapshot, rw.pkg, rw.pgf, rw.origDecl, calleeInfo, post) +- return &UnmarshalError{"unmarshal failed to match one of [int32 string]"} -} - --// reTypeCheck re-type checks orig with new file contents defined by fileMask. --// --// It expects that any newly added imports are already present in the --// transitive imports of orig. --// --// If expectErrors is true, reTypeCheck allows errors in the new package. --// TODO(rfindley): perhaps this should be a filter to specify which errors are --// acceptable. --func reTypeCheck(logf func(string, ...any), orig Package, fileMask map[span.URI]*ast.File, expectErrors bool) (*types.Package, *types.Info, error) { -- pkg := types.NewPackage(string(orig.Metadata().PkgPath), string(orig.Metadata().Name)) -- info := &types.Info{ -- Types: make(map[ast.Expr]types.TypeAndValue), -- Defs: make(map[*ast.Ident]types.Object), -- Uses: make(map[*ast.Ident]types.Object), -- Implicits: make(map[ast.Node]types.Object), -- Selections: make(map[*ast.SelectorExpr]*types.Selection), -- Scopes: make(map[ast.Node]*types.Scope), -- Instances: make(map[*ast.Ident]types.Instance), +-func (t Or_ClientSemanticTokensRequestOptions_full) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case ClientSemanticTokensRequestFullDelta: +- return json.Marshal(x) +- case bool: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } -- { -- var files []*ast.File -- for _, pgf := range orig.CompiledGoFiles() { -- if mask, ok := fileMask[pgf.URI]; ok { -- files = append(files, mask) -- } else { -- files = append(files, pgf.File) -- } -- } -- -- // Implement a BFS for imports in the transitive package graph. -- // -- // Note that this only works if any newly added imports are expected to be -- // present among transitive imports. In general we cannot assume this to -- // be the case, but in the special case of removing a parameter it works -- // because any parameter types must be present in export data. -- var importer func(importPath string) (*types.Package, error) -- { -- var ( -- importsByPath = make(map[string]*types.Package) // cached imports -- toSearch = []*types.Package{orig.GetTypes()} // packages to search -- searched = make(map[string]bool) // path -> (false, if present in toSearch; true, if already searched) -- ) -- importer = func(path string) (*types.Package, error) { -- if p, ok := importsByPath[path]; ok { -- return p, nil -- } -- for len(toSearch) > 0 { -- pkg := toSearch[0] -- toSearch = toSearch[1:] -- searched[pkg.Path()] = true -- for _, p := range pkg.Imports() { -- // TODO(rfindley): this is incorrect: p.Path() is a package path, -- // whereas path is an import path. We can fix this by reporting any -- // newly added imports from inlining, or by using the ImporterFrom -- // interface and package metadata. -- // -- // TODO(rfindley): can't the inliner also be wrong here? It's -- // possible that an import path means different things depending on -- // the location. -- importsByPath[p.Path()] = p -- if _, ok := searched[p.Path()]; !ok { -- searched[p.Path()] = false -- toSearch = append(toSearch, p) -- } -- } -- if p, ok := importsByPath[path]; ok { -- return p, nil -- } -- } -- return nil, fmt.Errorf("missing import") -- } -- } -- cfg := &types.Config{ -- Sizes: orig.Metadata().TypesSizes, -- Importer: ImporterFunc(importer), -- } +- return nil, fmt.Errorf("type %T not one of [ClientSemanticTokensRequestFullDelta bool]", t) +-} - -- // Copied from cache/check.go. -- // TODO(rfindley): factor this out and fix goVersionRx. -- // Set Go dialect. -- if module := orig.Metadata().Module; module != nil && module.GoVersion != "" { -- goVersion := "go" + module.GoVersion -- // types.NewChecker panics if GoVersion is invalid. -- // An unparsable mod file should probably stop us -- // before we get here, but double check just in case. -- if goVersionRx.MatchString(goVersion) { -- typesinternal.SetGoVersion(cfg, goVersion) -- } -- } -- if expectErrors { -- cfg.Error = func(err error) { -- logf("re-type checking: expected error: %v", err) -- } -- } -- typesinternal.SetUsesCgo(cfg) -- checker := types.NewChecker(cfg, orig.FileSet(), pkg, info) -- if err := checker.Files(files); err != nil && !expectErrors { -- return nil, nil, fmt.Errorf("type checking rewritten package: %v", err) -- } +-func (t *Or_ClientSemanticTokensRequestOptions_full) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil - } -- return pkg, info, nil +- var h0 ClientSemanticTokensRequestFullDelta +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 bool +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [ClientSemanticTokensRequestFullDelta bool]"} -} - --// TODO(golang/go#63472): this looks wrong with the new Go version syntax. --var goVersionRx = regexp.MustCompile(`^go([1-9][0-9]*)\.(0|[1-9][0-9]*)$`) -- --func remove[T any](s []T, i int) []T { -- return append(s[:i], s[i+1:]...) +-func (t Or_ClientSemanticTokensRequestOptions_range) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case Lit_ClientSemanticTokensRequestOptions_range_Item1: +- return json.Marshal(x) +- case bool: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [Lit_ClientSemanticTokensRequestOptions_range_Item1 bool]", t) -} - --// replaceFileDecl replaces old with new in the file described by pgf. --// --// TODO(rfindley): generalize, and combine with rewriteSignature. --func replaceFileDecl(pgf *ParsedGoFile, old, new ast.Decl) ([]byte, error) { -- i := findDecl(pgf.File, old) -- if i == -1 { -- return nil, bug.Errorf("didn't find old declaration") +-func (t *Or_ClientSemanticTokensRequestOptions_range) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil - } -- start, end, err := safetoken.Offsets(pgf.Tok, old.Pos(), old.End()) -- if err != nil { -- return nil, err +- var h0 Lit_ClientSemanticTokensRequestOptions_range_Item1 +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil - } -- var out bytes.Buffer -- out.Write(pgf.Src[:start]) -- fset := tokeninternal.FileSetFor(pgf.Tok) -- if err := format.Node(&out, fset, new); err != nil { -- return nil, bug.Errorf("formatting new node: %v", err) +- var h1 bool +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil - } -- out.Write(pgf.Src[end:]) -- return out.Bytes(), nil +- return &UnmarshalError{"unmarshal failed to match one of [Lit_ClientSemanticTokensRequestOptions_range_Item1 bool]"} -} - --// findDecl finds the index of decl in file.Decls. --// --// TODO: use slices.Index when it is available. --func findDecl(file *ast.File, decl ast.Decl) int { -- for i, d := range file.Decls { -- if d == decl { -- return i -- } +-func (t Or_CompletionItemDefaults_editRange) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case EditRangeWithInsertReplace: +- return json.Marshal(x) +- case Range: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } -- return -1 +- return nil, fmt.Errorf("type %T not one of [EditRangeWithInsertReplace Range]", t) -} -diff -urN a/gopls/internal/lsp/source/code_lens.go b/gopls/internal/lsp/source/code_lens.go ---- a/gopls/internal/lsp/source/code_lens.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/code_lens.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,248 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package source -- --import ( -- "context" -- "go/ast" -- "go/token" -- "go/types" -- "path/filepath" -- "regexp" -- "strings" - -- "golang.org/x/tools/gopls/internal/lsp/command" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/span" --) -- --type LensFunc func(context.Context, Snapshot, FileHandle) ([]protocol.CodeLens, error) +-func (t *Or_CompletionItemDefaults_editRange) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 EditRangeWithInsertReplace +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 Range +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [EditRangeWithInsertReplace Range]"} +-} - --// LensFuncs returns the supported lensFuncs for Go files. --func LensFuncs() map[command.Command]LensFunc { -- return map[command.Command]LensFunc{ -- command.Generate: goGenerateCodeLens, -- command.Test: runTestCodeLens, -- command.RegenerateCgo: regenerateCgoLens, -- command.GCDetails: toggleDetailsCodeLens, +-func (t Or_CompletionItem_documentation) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case MarkupContent: +- return json.Marshal(x) +- case string: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } +- return nil, fmt.Errorf("type %T not one of [MarkupContent string]", t) -} - --var ( -- testRe = regexp.MustCompile("^Test[^a-z]") -- benchmarkRe = regexp.MustCompile("^Benchmark[^a-z]") --) +-func (t *Or_CompletionItem_documentation) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 MarkupContent +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 string +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [MarkupContent string]"} +-} - --func runTestCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) { -- var codeLens []protocol.CodeLens +-func (t Or_CompletionItem_textEdit) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case InsertReplaceEdit: +- return json.Marshal(x) +- case TextEdit: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [InsertReplaceEdit TextEdit]", t) +-} - -- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) -- if err != nil { -- return nil, err +-func (t *Or_CompletionItem_textEdit) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil - } -- fns, err := TestsAndBenchmarks(pkg, pgf) -- if err != nil { -- return nil, err +- var h0 InsertReplaceEdit +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil - } -- puri := protocol.URIFromSpanURI(fh.URI()) -- for _, fn := range fns.Tests { -- cmd, err := command.NewTestCommand("run test", puri, []string{fn.Name}, nil) -- if err != nil { -- return nil, err -- } -- rng := protocol.Range{Start: fn.Rng.Start, End: fn.Rng.Start} -- codeLens = append(codeLens, protocol.CodeLens{Range: rng, Command: &cmd}) +- var h1 TextEdit +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil - } +- return &UnmarshalError{"unmarshal failed to match one of [InsertReplaceEdit TextEdit]"} +-} - -- for _, fn := range fns.Benchmarks { -- cmd, err := command.NewTestCommand("run benchmark", puri, nil, []string{fn.Name}) -- if err != nil { -- return nil, err -- } -- rng := protocol.Range{Start: fn.Rng.Start, End: fn.Rng.Start} -- codeLens = append(codeLens, protocol.CodeLens{Range: rng, Command: &cmd}) +-func (t Or_Definition) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case Location: +- return json.Marshal(x) +- case []Location: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } +- return nil, fmt.Errorf("type %T not one of [Location []Location]", t) +-} - -- if len(fns.Benchmarks) > 0 { -- pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) -- if err != nil { -- return nil, err -- } -- // add a code lens to the top of the file which runs all benchmarks in the file -- rng, err := pgf.PosRange(pgf.File.Package, pgf.File.Package) -- if err != nil { -- return nil, err -- } -- var benches []string -- for _, fn := range fns.Benchmarks { -- benches = append(benches, fn.Name) -- } -- cmd, err := command.NewTestCommand("run file benchmarks", puri, nil, benches) -- if err != nil { -- return nil, err -- } -- codeLens = append(codeLens, protocol.CodeLens{Range: rng, Command: &cmd}) +-func (t *Or_Definition) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil - } -- return codeLens, nil +- var h0 Location +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 []Location +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [Location []Location]"} -} - --type TestFn struct { -- Name string -- Rng protocol.Range +-func (t Or_Diagnostic_code) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case int32: +- return json.Marshal(x) +- case string: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [int32 string]", t) -} - --type TestFns struct { -- Tests []TestFn -- Benchmarks []TestFn +-func (t *Or_Diagnostic_code) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 int32 +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 string +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [int32 string]"} -} - --func TestsAndBenchmarks(pkg Package, pgf *ParsedGoFile) (TestFns, error) { -- var out TestFns -- -- if !strings.HasSuffix(pgf.URI.Filename(), "_test.go") { -- return out, nil +-func (t Or_DocumentDiagnosticReport) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case RelatedFullDocumentDiagnosticReport: +- return json.Marshal(x) +- case RelatedUnchangedDocumentDiagnosticReport: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } +- return nil, fmt.Errorf("type %T not one of [RelatedFullDocumentDiagnosticReport RelatedUnchangedDocumentDiagnosticReport]", t) +-} - -- for _, d := range pgf.File.Decls { -- fn, ok := d.(*ast.FuncDecl) -- if !ok { -- continue -- } +-func (t *Or_DocumentDiagnosticReport) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 RelatedFullDocumentDiagnosticReport +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 RelatedUnchangedDocumentDiagnosticReport +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [RelatedFullDocumentDiagnosticReport RelatedUnchangedDocumentDiagnosticReport]"} +-} - -- rng, err := pgf.NodeRange(fn) -- if err != nil { -- return out, err -- } +-func (t Or_DocumentDiagnosticReportPartialResult_relatedDocuments_Value) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case FullDocumentDiagnosticReport: +- return json.Marshal(x) +- case UnchangedDocumentDiagnosticReport: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport]", t) +-} - -- if matchTestFunc(fn, pkg, testRe, "T") { -- out.Tests = append(out.Tests, TestFn{fn.Name.Name, rng}) -- } +-func (t *Or_DocumentDiagnosticReportPartialResult_relatedDocuments_Value) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 FullDocumentDiagnosticReport +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 UnchangedDocumentDiagnosticReport +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport]"} +-} - -- if matchTestFunc(fn, pkg, benchmarkRe, "B") { -- out.Benchmarks = append(out.Benchmarks, TestFn{fn.Name.Name, rng}) -- } +-func (t Or_DocumentFilter) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case NotebookCellTextDocumentFilter: +- return json.Marshal(x) +- case TextDocumentFilter: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } +- return nil, fmt.Errorf("type %T not one of [NotebookCellTextDocumentFilter TextDocumentFilter]", t) +-} - -- return out, nil +-func (t *Or_DocumentFilter) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 NotebookCellTextDocumentFilter +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 TextDocumentFilter +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [NotebookCellTextDocumentFilter TextDocumentFilter]"} -} - --func matchTestFunc(fn *ast.FuncDecl, pkg Package, nameRe *regexp.Regexp, paramID string) bool { -- // Make sure that the function name matches a test function. -- if !nameRe.MatchString(fn.Name.Name) { -- return false +-func (t Or_GlobPattern) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case Pattern: +- return json.Marshal(x) +- case RelativePattern: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } -- info := pkg.GetTypesInfo() -- if info == nil { -- return false +- return nil, fmt.Errorf("type %T not one of [Pattern RelativePattern]", t) +-} +- +-func (t *Or_GlobPattern) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil - } -- obj := info.ObjectOf(fn.Name) -- if obj == nil { -- return false +- var h0 Pattern +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil - } -- sig, ok := obj.Type().(*types.Signature) -- if !ok { -- return false +- var h1 RelativePattern +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil - } -- // Test functions should have only one parameter. -- if sig.Params().Len() != 1 { -- return false +- return &UnmarshalError{"unmarshal failed to match one of [Pattern RelativePattern]"} +-} +- +-func (t Or_Hover_contents) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case MarkedString: +- return json.Marshal(x) +- case MarkupContent: +- return json.Marshal(x) +- case []MarkedString: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } +- return nil, fmt.Errorf("type %T not one of [MarkedString MarkupContent []MarkedString]", t) +-} - -- // Check the type of the only parameter -- paramTyp, ok := sig.Params().At(0).Type().(*types.Pointer) -- if !ok { -- return false +-func (t *Or_Hover_contents) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil - } -- named, ok := paramTyp.Elem().(*types.Named) -- if !ok { -- return false +- var h0 MarkedString +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil - } -- namedObj := named.Obj() -- if namedObj.Pkg().Path() != "testing" { -- return false +- var h1 MarkupContent +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil - } -- return namedObj.Id() == paramID --} -- --func goGenerateCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) { -- pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) -- if err != nil { -- return nil, err +- var h2 []MarkedString +- if err := json.Unmarshal(x, &h2); err == nil { +- t.Value = h2 +- return nil - } -- const ggDirective = "//go:generate" -- for _, c := range pgf.File.Comments { -- for _, l := range c.List { -- if !strings.HasPrefix(l.Text, ggDirective) { -- continue -- } -- rng, err := pgf.PosRange(l.Pos(), l.Pos()+token.Pos(len(ggDirective))) -- if err != nil { -- return nil, err -- } -- dir := protocol.URIFromSpanURI(span.URIFromPath(filepath.Dir(fh.URI().Filename()))) -- nonRecursiveCmd, err := command.NewGenerateCommand("run go generate", command.GenerateArgs{Dir: dir, Recursive: false}) -- if err != nil { -- return nil, err -- } -- recursiveCmd, err := command.NewGenerateCommand("run go generate ./...", command.GenerateArgs{Dir: dir, Recursive: true}) -- if err != nil { -- return nil, err -- } -- return []protocol.CodeLens{ -- {Range: rng, Command: &recursiveCmd}, -- {Range: rng, Command: &nonRecursiveCmd}, -- }, nil +- return &UnmarshalError{"unmarshal failed to match one of [MarkedString MarkupContent []MarkedString]"} +-} - -- } +-func (t Or_InlayHint_label) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case []InlayHintLabelPart: +- return json.Marshal(x) +- case string: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } -- return nil, nil +- return nil, fmt.Errorf("type %T not one of [[]InlayHintLabelPart string]", t) -} - --func regenerateCgoLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) { -- pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) -- if err != nil { -- return nil, err -- } -- var c *ast.ImportSpec -- for _, imp := range pgf.File.Imports { -- if imp.Path.Value == `"C"` { -- c = imp -- } -- } -- if c == nil { -- return nil, nil +-func (t *Or_InlayHint_label) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil - } -- rng, err := pgf.NodeRange(c) -- if err != nil { -- return nil, err +- var h0 []InlayHintLabelPart +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil - } -- puri := protocol.URIFromSpanURI(fh.URI()) -- cmd, err := command.NewRegenerateCgoCommand("regenerate cgo definitions", command.URIArg{URI: puri}) -- if err != nil { -- return nil, err +- var h1 string +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil - } -- return []protocol.CodeLens{{Range: rng, Command: &cmd}}, nil +- return &UnmarshalError{"unmarshal failed to match one of [[]InlayHintLabelPart string]"} -} - --func toggleDetailsCodeLens(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.CodeLens, error) { -- pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) -- if err != nil { -- return nil, err +-func (t Or_InlineCompletionItem_insertText) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case StringValue: +- return json.Marshal(x) +- case string: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } -- if !pgf.File.Package.IsValid() { -- // Without a package name we have nowhere to put the codelens, so give up. -- return nil, nil +- return nil, fmt.Errorf("type %T not one of [StringValue string]", t) +-} +- +-func (t *Or_InlineCompletionItem_insertText) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil - } -- rng, err := pgf.PosRange(pgf.File.Package, pgf.File.Package) -- if err != nil { -- return nil, err +- var h0 StringValue +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil - } -- puri := protocol.URIFromSpanURI(fh.URI()) -- cmd, err := command.NewGCDetailsCommand("Toggle gc annotation details", puri) -- if err != nil { -- return nil, err +- var h1 string +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil - } -- return []protocol.CodeLens{{Range: rng, Command: &cmd}}, nil +- return &UnmarshalError{"unmarshal failed to match one of [StringValue string]"} -} -diff -urN a/gopls/internal/lsp/source/comment.go b/gopls/internal/lsp/source/comment.go ---- a/gopls/internal/lsp/source/comment.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/comment.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,384 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --//go:build !go1.19 --// +build !go1.19 -- --package source -- --import ( -- "bytes" -- "io" -- "regexp" -- "strings" -- "unicode" -- "unicode/utf8" --) - --// CommentToMarkdown converts comment text to formatted markdown. --// The comment was prepared by DocReader, --// so it is known not to have leading, trailing blank lines --// nor to have trailing spaces at the end of lines. --// The comment markers have already been removed. --// --// Each line is converted into a markdown line and empty lines are just converted to --// newlines. Heading are prefixed with `### ` to make it a markdown heading. --// --// A span of indented lines retains a 4 space prefix block, with the common indent --// prefix removed unless empty, in which case it will be converted to a newline. --// --// URLs in the comment text are converted into links. --func CommentToMarkdown(text string, _ *Options) string { -- buf := &bytes.Buffer{} -- commentToMarkdown(buf, text) -- return buf.String() +-func (t Or_InlineValue) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case InlineValueEvaluatableExpression: +- return json.Marshal(x) +- case InlineValueText: +- return json.Marshal(x) +- case InlineValueVariableLookup: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [InlineValueEvaluatableExpression InlineValueText InlineValueVariableLookup]", t) -} - --var ( -- mdNewline = []byte("\n") -- mdHeader = []byte("### ") -- mdIndent = []byte(" ") -- mdLinkStart = []byte("[") -- mdLinkDiv = []byte("](") -- mdLinkEnd = []byte(")") --) -- --func commentToMarkdown(w io.Writer, text string) { -- blocks := blocks(text) -- for i, b := range blocks { -- switch b.op { -- case opPara: -- for _, line := range b.lines { -- emphasize(w, line, true) -- } -- case opHead: -- // The header block can consist of only one line. -- // However, check the number of lines, just in case. -- if len(b.lines) == 0 { -- // Skip this block. -- continue -- } -- header := b.lines[0] -- -- w.Write(mdHeader) -- commentEscape(w, header, true) -- // Header doesn't end with \n unlike the lines of other blocks. -- w.Write(mdNewline) -- case opPre: -- for _, line := range b.lines { -- if isBlank(line) { -- w.Write(mdNewline) -- continue -- } -- w.Write(mdIndent) -- w.Write([]byte(line)) -- } -- } -- -- if i < len(blocks)-1 { -- w.Write(mdNewline) -- } +-func (t *Or_InlineValue) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 InlineValueEvaluatableExpression +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 InlineValueText +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- var h2 InlineValueVariableLookup +- if err := json.Unmarshal(x, &h2); err == nil { +- t.Value = h2 +- return nil - } +- return &UnmarshalError{"unmarshal failed to match one of [InlineValueEvaluatableExpression InlineValueText InlineValueVariableLookup]"} -} - --const ( -- ulquo = "“" -- urquo = "”" --) -- --var ( -- markdownEscape = regexp.MustCompile(`([\\\x60*{}[\]()#+\-.!_>~|"$%&'\/:;<=?@^])`) -- -- unicodeQuoteReplacer = strings.NewReplacer("``", ulquo, "''", urquo) --) -- --// commentEscape escapes comment text for markdown. If nice is set, --// also turn double ` and ' into “ and ”. --func commentEscape(w io.Writer, text string, nice bool) { -- if nice { -- text = convertQuotes(text) +-func (t Or_MarkedString) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case MarkedStringWithLanguage: +- return json.Marshal(x) +- case string: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } -- text = escapeRegex(text) -- w.Write([]byte(text)) +- return nil, fmt.Errorf("type %T not one of [MarkedStringWithLanguage string]", t) -} - --func convertQuotes(text string) string { -- return unicodeQuoteReplacer.Replace(text) +-func (t *Or_MarkedString) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 MarkedStringWithLanguage +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 string +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [MarkedStringWithLanguage string]"} -} - --func escapeRegex(text string) string { -- return markdownEscape.ReplaceAllString(text, `\$1`) +-func (t Or_NotebookCellTextDocumentFilter_notebook) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case NotebookDocumentFilter: +- return json.Marshal(x) +- case string: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [NotebookDocumentFilter string]", t) -} - --func emphasize(w io.Writer, line string, nice bool) { -- for { -- m := matchRx.FindStringSubmatchIndex(line) -- if m == nil { -- break -- } -- // m >= 6 (two parenthesized sub-regexps in matchRx, 1st one is urlRx) -- -- // write text before match -- commentEscape(w, line[0:m[0]], nice) -- -- // adjust match for URLs -- match := line[m[0]:m[1]] -- if strings.Contains(match, "://") { -- m0, m1 := m[0], m[1] -- for _, s := range []string{"()", "{}", "[]"} { -- open, close := s[:1], s[1:] // E.g., "(" and ")" -- // require opening parentheses before closing parentheses (#22285) -- if i := strings.Index(match, close); i >= 0 && i < strings.Index(match, open) { -- m1 = m0 + i -- match = line[m0:m1] -- } -- // require balanced pairs of parentheses (#5043) -- for i := 0; strings.Count(match, open) != strings.Count(match, close) && i < 10; i++ { -- m1 = strings.LastIndexAny(line[:m1], s) -- match = line[m0:m1] -- } -- } -- if m1 != m[1] { -- // redo matching with shortened line for correct indices -- m = matchRx.FindStringSubmatchIndex(line[:m[0]+len(match)]) -- } -- } -- -- // Following code has been modified from go/doc since words is always -- // nil. All html formatting has also been transformed into markdown formatting -- -- // analyze match -- url := "" -- if m[2] >= 0 { -- url = match -- } -- -- // write match -- if len(url) > 0 { -- w.Write(mdLinkStart) -- } -- -- commentEscape(w, match, nice) -- -- if len(url) > 0 { -- w.Write(mdLinkDiv) -- w.Write([]byte(urlReplacer.Replace(url))) -- w.Write(mdLinkEnd) -- } -- -- // advance -- line = line[m[1]:] +-func (t *Or_NotebookCellTextDocumentFilter_notebook) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 NotebookDocumentFilter +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 string +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil - } -- commentEscape(w, line, nice) +- return &UnmarshalError{"unmarshal failed to match one of [NotebookDocumentFilter string]"} -} - --// Everything from here on is a copy of go/doc/comment.go -- --const ( -- // Regexp for Go identifiers -- identRx = `[\pL_][\pL_0-9]*` -- -- // Regexp for URLs -- // Match parens, and check later for balance - see #5043, #22285 -- // Match .,:;?! within path, but not at end - see #18139, #16565 -- // This excludes some rare yet valid urls ending in common punctuation -- // in order to allow sentences ending in URLs. -- -- // protocol (required) e.g. http -- protoPart = `(https?|ftp|file|gopher|mailto|nntp)` -- // host (required) e.g. www.example.com or [::1]:8080 -- hostPart = `([a-zA-Z0-9_@\-.\[\]:]+)` -- // path+query+fragment (optional) e.g. /path/index.html?q=foo#bar -- pathPart = `([.,:;?!]*[a-zA-Z0-9$'()*+&#=@~_/\-\[\]%])*` -- -- urlRx = protoPart + `://` + hostPart + pathPart --) -- --var ( -- matchRx = regexp.MustCompile(`(` + urlRx + `)|(` + identRx + `)`) -- urlReplacer = strings.NewReplacer(`(`, `\(`, `)`, `\)`) --) -- --func indentLen(s string) int { -- i := 0 -- for i < len(s) && (s[i] == ' ' || s[i] == '\t') { -- i++ +-func (t Or_NotebookDocumentFilter) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case NotebookDocumentFilterNotebookType: +- return json.Marshal(x) +- case NotebookDocumentFilterPattern: +- return json.Marshal(x) +- case NotebookDocumentFilterScheme: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } -- return i +- return nil, fmt.Errorf("type %T not one of [NotebookDocumentFilterNotebookType NotebookDocumentFilterPattern NotebookDocumentFilterScheme]", t) -} - --func isBlank(s string) bool { -- return len(s) == 0 || (len(s) == 1 && s[0] == '\n') +-func (t *Or_NotebookDocumentFilter) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 NotebookDocumentFilterNotebookType +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 NotebookDocumentFilterPattern +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- var h2 NotebookDocumentFilterScheme +- if err := json.Unmarshal(x, &h2); err == nil { +- t.Value = h2 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [NotebookDocumentFilterNotebookType NotebookDocumentFilterPattern NotebookDocumentFilterScheme]"} -} - --func commonPrefix(a, b string) string { -- i := 0 -- for i < len(a) && i < len(b) && a[i] == b[i] { -- i++ +-func (t Or_NotebookDocumentFilterWithCells_notebook) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case NotebookDocumentFilter: +- return json.Marshal(x) +- case string: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } -- return a[0:i] +- return nil, fmt.Errorf("type %T not one of [NotebookDocumentFilter string]", t) -} - --func unindent(block []string) { -- if len(block) == 0 { -- return +-func (t *Or_NotebookDocumentFilterWithCells_notebook) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil - } -- -- // compute maximum common white prefix -- prefix := block[0][0:indentLen(block[0])] -- for _, line := range block { -- if !isBlank(line) { -- prefix = commonPrefix(prefix, line) -- } +- var h0 NotebookDocumentFilter +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil - } -- n := len(prefix) -- -- // remove -- for i, line := range block { -- if !isBlank(line) { -- block[i] = line[n:] -- } +- var h1 string +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil - } +- return &UnmarshalError{"unmarshal failed to match one of [NotebookDocumentFilter string]"} -} - --// heading returns the trimmed line if it passes as a section heading; --// otherwise it returns the empty string. --func heading(line string) string { -- line = strings.TrimSpace(line) -- if len(line) == 0 { -- return "" +-func (t Or_NotebookDocumentFilterWithNotebook_notebook) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case NotebookDocumentFilter: +- return json.Marshal(x) +- case string: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } +- return nil, fmt.Errorf("type %T not one of [NotebookDocumentFilter string]", t) +-} - -- // a heading must start with an uppercase letter -- r, _ := utf8.DecodeRuneInString(line) -- if !unicode.IsLetter(r) || !unicode.IsUpper(r) { -- return "" +-func (t *Or_NotebookDocumentFilterWithNotebook_notebook) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil - } -- -- // it must end in a letter or digit: -- r, _ = utf8.DecodeLastRuneInString(line) -- if !unicode.IsLetter(r) && !unicode.IsDigit(r) { -- return "" +- var h0 NotebookDocumentFilter +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil - } -- -- // exclude lines with illegal characters. we allow "()," -- if strings.ContainsAny(line, ";:!?+*/=[]{}_^°&§~%#@<\">\\") { -- return "" +- var h1 string +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil - } +- return &UnmarshalError{"unmarshal failed to match one of [NotebookDocumentFilter string]"} +-} - -- // allow "'" for possessive "'s" only -- for b := line; ; { -- i := strings.IndexRune(b, '\'') -- if i < 0 { -- break -- } -- if i+1 >= len(b) || b[i+1] != 's' || (i+2 < len(b) && b[i+2] != ' ') { -- return "" // not followed by "s " -- } -- b = b[i+2:] +-func (t Or_NotebookDocumentSyncOptions_notebookSelector_Elem) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case NotebookDocumentFilterWithCells: +- return json.Marshal(x) +- case NotebookDocumentFilterWithNotebook: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } +- return nil, fmt.Errorf("type %T not one of [NotebookDocumentFilterWithCells NotebookDocumentFilterWithNotebook]", t) +-} - -- // allow "." when followed by non-space -- for b := line; ; { -- i := strings.IndexRune(b, '.') -- if i < 0 { -- break -- } -- if i+1 >= len(b) || b[i+1] == ' ' { -- return "" // not followed by non-space -- } -- b = b[i+1:] +-func (t *Or_NotebookDocumentSyncOptions_notebookSelector_Elem) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil - } -- -- return line +- var h0 NotebookDocumentFilterWithCells +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 NotebookDocumentFilterWithNotebook +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [NotebookDocumentFilterWithCells NotebookDocumentFilterWithNotebook]"} -} - --type op int -- --const ( -- opPara op = iota -- opHead -- opPre --) -- --type block struct { -- op op -- lines []string +-func (t Or_RelatedFullDocumentDiagnosticReport_relatedDocuments_Value) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case FullDocumentDiagnosticReport: +- return json.Marshal(x) +- case UnchangedDocumentDiagnosticReport: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport]", t) -} - --func blocks(text string) []block { -- var ( -- out []block -- para []string -- -- lastWasBlank = false -- lastWasHeading = false -- ) -- -- close := func() { -- if para != nil { -- out = append(out, block{opPara, para}) -- para = nil -- } +-func (t *Or_RelatedFullDocumentDiagnosticReport_relatedDocuments_Value) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil - } -- -- lines := strings.SplitAfter(text, "\n") -- unindent(lines) -- for i := 0; i < len(lines); { -- line := lines[i] -- if isBlank(line) { -- // close paragraph -- close() -- i++ -- lastWasBlank = true -- continue -- } -- if indentLen(line) > 0 { -- // close paragraph -- close() -- -- // count indented or blank lines -- j := i + 1 -- for j < len(lines) && (isBlank(lines[j]) || indentLen(lines[j]) > 0) { -- j++ -- } -- // but not trailing blank lines -- for j > i && isBlank(lines[j-1]) { -- j-- -- } -- pre := lines[i:j] -- i = j -- -- unindent(pre) -- -- // put those lines in a pre block -- out = append(out, block{opPre, pre}) -- lastWasHeading = false -- continue -- } -- -- if lastWasBlank && !lastWasHeading && i+2 < len(lines) && -- isBlank(lines[i+1]) && !isBlank(lines[i+2]) && indentLen(lines[i+2]) == 0 { -- // current line is non-blank, surrounded by blank lines -- // and the next non-blank line is not indented: this -- // might be a heading. -- if head := heading(line); head != "" { -- close() -- out = append(out, block{opHead, []string{head}}) -- i += 2 -- lastWasHeading = true -- continue -- } -- } -- -- // open paragraph -- lastWasBlank = false -- lastWasHeading = false -- para = append(para, lines[i]) -- i++ +- var h0 FullDocumentDiagnosticReport +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil - } -- close() -- -- return out +- var h1 UnchangedDocumentDiagnosticReport +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport]"} -} -diff -urN a/gopls/internal/lsp/source/comment_go118_test.go b/gopls/internal/lsp/source/comment_go118_test.go ---- a/gopls/internal/lsp/source/comment_go118_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/comment_go118_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,371 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --//go:build !go1.19 --// +build !go1.19 - --package source -- --import ( -- "bytes" -- "reflect" -- "strings" -- "testing" --) -- --// This file is a copy of go/doc/comment_test.go with the exception for --// the test cases for TestEmphasize and TestCommentEscape -- --var headingTests = []struct { -- line string -- ok bool --}{ -- {"Section", true}, -- {"A typical usage", true}, -- {"ΔΛΞ is Greek", true}, -- {"Foo 42", true}, -- {"", false}, -- {"section", false}, -- {"A typical usage:", false}, -- {"This code:", false}, -- {"δ is Greek", false}, -- {"Foo §", false}, -- {"Fermat's Last Sentence", true}, -- {"Fermat's", true}, -- {"'sX", false}, -- {"Ted 'Too' Bar", false}, -- {"Use n+m", false}, -- {"Scanning:", false}, -- {"N:M", false}, --} -- --func TestIsHeading(t *testing.T) { -- for _, tt := range headingTests { -- if h := heading(tt.line); (len(h) > 0) != tt.ok { -- t.Errorf("isHeading(%q) = %v, want %v", tt.line, h, tt.ok) -- } -- } --} -- --var blocksTests = []struct { -- in string -- out []block -- text string --}{ -- { -- in: `Para 1. --Para 1 line 2. -- --Para 2. -- --Section -- --Para 3. -- -- pre -- pre1 -- --Para 4. -- -- pre -- pre1 -- -- pre2 -- --Para 5. -- -- -- pre -- -- -- pre1 -- pre2 -- --Para 6. -- pre -- pre2 --`, -- out: []block{ -- {opPara, []string{"Para 1.\n", "Para 1 line 2.\n"}}, -- {opPara, []string{"Para 2.\n"}}, -- {opHead, []string{"Section"}}, -- {opPara, []string{"Para 3.\n"}}, -- {opPre, []string{"pre\n", "pre1\n"}}, -- {opPara, []string{"Para 4.\n"}}, -- {opPre, []string{"pre\n", "pre1\n", "\n", "pre2\n"}}, -- {opPara, []string{"Para 5.\n"}}, -- {opPre, []string{"pre\n", "\n", "\n", "pre1\n", "pre2\n"}}, -- {opPara, []string{"Para 6.\n"}}, -- {opPre, []string{"pre\n", "pre2\n"}}, -- }, -- text: `. Para 1. Para 1 line 2. -- --. Para 2. -- -- --. Section -- --. Para 3. -- --$ pre --$ pre1 -- --. Para 4. -- --$ pre --$ pre1 -- --$ pre2 -- --. Para 5. -- --$ pre -- -- --$ pre1 --$ pre2 -- --. Para 6. -- --$ pre --$ pre2 --`, -- }, -- { -- in: "Para.\n\tshould not be ``escaped''", -- out: []block{ -- {opPara, []string{"Para.\n"}}, -- {opPre, []string{"should not be ``escaped''"}}, -- }, -- text: ". Para.\n\n$ should not be ``escaped''", -- }, -- { -- in: "// A very long line of 46 char for line wrapping.", -- out: []block{ -- {opPara, []string{"// A very long line of 46 char for line wrapping."}}, -- }, -- text: `. // A very long line of 46 char for line --. // wrapping. --`, -- }, -- { -- in: `/* A very long line of 46 char for line wrapping. --A very long line of 46 char for line wrapping. */`, -- out: []block{ -- {opPara, []string{"/* A very long line of 46 char for line wrapping.\n", "A very long line of 46 char for line wrapping. */"}}, -- }, -- text: `. /* A very long line of 46 char for line --. wrapping. A very long line of 46 char --. for line wrapping. */ --`, -- }, +-func (t Or_RelatedUnchangedDocumentDiagnosticReport_relatedDocuments_Value) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case FullDocumentDiagnosticReport: +- return json.Marshal(x) +- case UnchangedDocumentDiagnosticReport: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport]", t) -} - --func TestBlocks(t *testing.T) { -- for i, tt := range blocksTests { -- b := blocks(tt.in) -- if !reflect.DeepEqual(b, tt.out) { -- t.Errorf("#%d: mismatch\nhave: %v\nwant: %v", i, b, tt.out) -- } +-func (t *Or_RelatedUnchangedDocumentDiagnosticReport_relatedDocuments_Value) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 FullDocumentDiagnosticReport +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 UnchangedDocumentDiagnosticReport +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil - } +- return &UnmarshalError{"unmarshal failed to match one of [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport]"} -} - --// This has been modified from go/doc to use markdown links instead of html ones --// and use markdown escaping instead oh html --var emphasizeTests = []struct { -- in, out string --}{ -- {"", ""}, -- {"http://[::1]:8080/foo.txt", `[http\:\/\/\[\:\:1\]\:8080\/foo\.txt](http://[::1]:8080/foo.txt)`}, -- {"before (https://www.google.com) after", `before \([https\:\/\/www\.google\.com](https://www.google.com)\) after`}, -- {"before https://www.google.com:30/x/y/z:b::c. After", `before [https\:\/\/www\.google\.com\:30\/x\/y\/z\:b\:\:c](https://www.google.com:30/x/y/z:b::c)\. After`}, -- {"http://www.google.com/path/:;!-/?query=%34b#093124", `[http\:\/\/www\.google\.com\/path\/\:\;\!\-\/\?query\=\%34b\#093124](http://www.google.com/path/:;!-/?query=%34b#093124)`}, -- {"http://www.google.com/path/:;!-/?query=%34bar#093124", `[http\:\/\/www\.google\.com\/path\/\:\;\!\-\/\?query\=\%34bar\#093124](http://www.google.com/path/:;!-/?query=%34bar#093124)`}, -- {"http://www.google.com/index.html! After", `[http\:\/\/www\.google\.com\/index\.html](http://www.google.com/index.html)\! After`}, -- {"http://www.google.com/", `[http\:\/\/www\.google\.com\/](http://www.google.com/)`}, -- {"https://www.google.com/", `[https\:\/\/www\.google\.com\/](https://www.google.com/)`}, -- {"http://www.google.com/path.", `[http\:\/\/www\.google\.com\/path](http://www.google.com/path)\.`}, -- {"http://en.wikipedia.org/wiki/Camellia_(cipher)", `[http\:\/\/en\.wikipedia\.org\/wiki\/Camellia\_\(cipher\)](http://en.wikipedia.org/wiki/Camellia_\(cipher\))`}, -- {"(http://www.google.com/)", `\([http\:\/\/www\.google\.com\/](http://www.google.com/)\)`}, -- {"http://gmail.com)", `[http\:\/\/gmail\.com](http://gmail.com)\)`}, -- {"((http://gmail.com))", `\(\([http\:\/\/gmail\.com](http://gmail.com)\)\)`}, -- {"http://gmail.com ((http://gmail.com)) ()", `[http\:\/\/gmail\.com](http://gmail.com) \(\([http\:\/\/gmail\.com](http://gmail.com)\)\) \(\)`}, -- {"Foo bar http://example.com/ quux!", `Foo bar [http\:\/\/example\.com\/](http://example.com/) quux\!`}, -- {"Hello http://example.com/%2f/ /world.", `Hello [http\:\/\/example\.com\/\%2f\/](http://example.com/%2f/) \/world\.`}, -- {"Lorem http: ipsum //host/path", `Lorem http\: ipsum \/\/host\/path`}, -- {"javascript://is/not/linked", `javascript\:\/\/is\/not\/linked`}, -- {"http://foo", `[http\:\/\/foo](http://foo)`}, -- {"art by [[https://www.example.com/person/][Person Name]]", `art by \[\[[https\:\/\/www\.example\.com\/person\/](https://www.example.com/person/)\]\[Person Name\]\]`}, -- {"please visit (http://golang.org/)", `please visit \([http\:\/\/golang\.org\/](http://golang.org/)\)`}, -- {"please visit http://golang.org/hello())", `please visit [http\:\/\/golang\.org\/hello\(\)](http://golang.org/hello\(\))\)`}, -- {"http://git.qemu.org/?p=qemu.git;a=blob;f=qapi-schema.json;hb=HEAD", `[http\:\/\/git\.qemu\.org\/\?p\=qemu\.git\;a\=blob\;f\=qapi\-schema\.json\;hb\=HEAD](http://git.qemu.org/?p=qemu.git;a=blob;f=qapi-schema.json;hb=HEAD)`}, -- {"https://foo.bar/bal/x(])", `[https\:\/\/foo\.bar\/bal\/x\(](https://foo.bar/bal/x\()\]\)`}, -- {"foo [ http://bar(])", `foo \[ [http\:\/\/bar\(](http://bar\()\]\)`}, --} -- --func TestEmphasize(t *testing.T) { -- for i, tt := range emphasizeTests { -- var buf bytes.Buffer -- emphasize(&buf, tt.in, true) -- out := buf.String() -- if out != tt.out { -- t.Errorf("#%d: mismatch\nhave: %v\nwant: %v", i, out, tt.out) -- } +-func (t Or_Result_textDocument_codeAction_Item0_Elem) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case CodeAction: +- return json.Marshal(x) +- case Command: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } +- return nil, fmt.Errorf("type %T not one of [CodeAction Command]", t) -} - --func TestCommentEscape(t *testing.T) { -- //ldquo -> ulquo and rdquo -> urquo -- commentTests := []struct { -- in, out string -- }{ -- {"typically invoked as ``go tool asm'',", "typically invoked as " + ulquo + "go tool asm" + urquo + ","}, -- {"For more detail, run ``go help test'' and ``go help testflag''", "For more detail, run " + ulquo + "go help test" + urquo + " and " + ulquo + "go help testflag" + urquo}} -- for i, tt := range commentTests { -- var buf strings.Builder -- commentEscape(&buf, tt.in, true) -- out := buf.String() -- if out != tt.out { -- t.Errorf("#%d: mismatch\nhave: %q\nwant: %q", i, out, tt.out) -- } +-func (t *Or_Result_textDocument_codeAction_Item0_Elem) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 CodeAction +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 Command +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil - } +- return &UnmarshalError{"unmarshal failed to match one of [CodeAction Command]"} -} - --func TestCommentToMarkdown(t *testing.T) { -- tests := []struct { -- in, out string -- }{ -- { -- in: "F declaration.\n", -- out: "F declaration\\.\n", -- }, -- { -- in: ` --F declaration. Lorem ipsum dolor sit amet. --Etiam mattis eros at orci mollis molestie. --`, -- out: ` --F declaration\. Lorem ipsum dolor sit amet\. --Etiam mattis eros at orci mollis molestie\. --`, -- }, -- { -- in: ` --F declaration. -- --Lorem ipsum dolor sit amet. --Sed id dui turpis. -- -- -- -- --Aenean tempus velit non auctor eleifend. --Aenean efficitur a sem id ultricies. -- -- --Phasellus efficitur mauris et viverra bibendum. --`, -- out: ` --F declaration\. -- --Lorem ipsum dolor sit amet\. --Sed id dui turpis\. -- --Aenean tempus velit non auctor eleifend\. --Aenean efficitur a sem id ultricies\. -- --Phasellus efficitur mauris et viverra bibendum\. --`, -- }, -- { -- in: ` --F declaration. -- --Aenean tempus velit non auctor eleifend. -- --Section -- --Lorem ipsum dolor sit amet, consectetur adipiscing elit. -- -- func foo() {} -- -- -- func bar() {} -- --Fusce lorem lacus. -- -- func foo() {} -- -- func bar() {} -- --Maecenas in lobortis lectus. -- -- func foo() {} -- -- func bar() {} -- --Phasellus efficitur mauris et viverra bibendum. --`, -- out: ` --F declaration\. -- --Aenean tempus velit non auctor eleifend\. -- --### Section -- --Lorem ipsum dolor sit amet, consectetur adipiscing elit\. -- -- func foo() {} -- -- -- func bar() {} -- --Fusce lorem lacus\. -- -- func foo() {} -- -- func bar() {} -- --Maecenas in lobortis lectus\. -- -- func foo() {} -- -- func bar() {} -- --Phasellus efficitur mauris et viverra bibendum\. --`, -- }, -- { -- in: ` --F declaration. +-func (t Or_Result_textDocument_inlineCompletion) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case InlineCompletionList: +- return json.Marshal(x) +- case []InlineCompletionItem: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [InlineCompletionList []InlineCompletionItem]", t) +-} - -- func foo() { -- fmt.Println("foo") +-func (t *Or_Result_textDocument_inlineCompletion) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil - } -- func bar() { -- fmt.Println("bar") +- var h0 InlineCompletionList +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil - } --`, -- out: ` --F declaration\. -- -- func foo() { -- fmt.Println("foo") -- } -- func bar() { -- fmt.Println("bar") -- } --`, -- }, +- var h1 []InlineCompletionItem +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil - } -- for i, tt := range tests { -- // Comments start with new lines for better readability. So, we should trim them. -- tt.in = strings.TrimPrefix(tt.in, "\n") -- tt.out = strings.TrimPrefix(tt.out, "\n") +- return &UnmarshalError{"unmarshal failed to match one of [InlineCompletionList []InlineCompletionItem]"} +-} - -- if out := CommentToMarkdown(tt.in, nil); out != tt.out { -- t.Errorf("#%d: mismatch\nhave: %q\nwant: %q", i, out, tt.out) -- } +-func (t Or_SemanticTokensOptions_full) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case SemanticTokensFullDelta: +- return json.Marshal(x) +- case bool: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } +- return nil, fmt.Errorf("type %T not one of [SemanticTokensFullDelta bool]", t) -} -diff -urN a/gopls/internal/lsp/source/comment_go119.go b/gopls/internal/lsp/source/comment_go119.go ---- a/gopls/internal/lsp/source/comment_go119.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/comment_go119.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,56 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --//go:build go1.19 --// +build go1.19 -- --package source -- --// Starting with go1.19, the formatting of comments has changed, and there --// is a new package (go/doc/comment) for processing them. --// As long as gopls has to compile under earlier versions, tests --// have to pass with both the old and new code, which produce --// slightly different results. (cmd/test/definition.go, source/comment_test.go, --// and source/source_test.go) Each of the test files checks the results --// with a function, tests.CheckSameMarkdown, that accepts both the old and the new --// results. (The old code escapes many characters the new code does not, --// and the new code sometimes adds a blank line.) -- --// When gopls no longer needs to compile with go1.18, the old comment.go should --// be replaced by this file, the golden test files should be updated. --// (and checkSameMarkdown() could be replaced by a simple comparison.) -- --import ( -- "fmt" -- "go/doc/comment" --) -- --// CommentToMarkdown converts comment text to formatted markdown. --// The comment was prepared by DocReader, --// so it is known not to have leading, trailing blank lines --// nor to have trailing spaces at the end of lines. --// The comment markers have already been removed. --func CommentToMarkdown(text string, options *Options) string { -- var p comment.Parser -- doc := p.Parse(text) -- var pr comment.Printer -- // The default produces {#Hdr-...} tags for headings. -- // vscode displays thems, which is undesirable. -- // The godoc for comment.Printer says the tags -- // avoid a security problem. -- pr.HeadingID = func(*comment.Heading) string { return "" } -- pr.DocLinkURL = func(link *comment.DocLink) string { -- msg := fmt.Sprintf("https://%s/%s", options.LinkTarget, link.ImportPath) -- if link.Name != "" { -- msg += "#" -- if link.Recv != "" { -- msg += link.Recv + "." -- } -- msg += link.Name -- } -- return msg +-func (t *Or_SemanticTokensOptions_full) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil - } -- easy := pr.Markdown(doc) -- return string(easy) +- var h0 SemanticTokensFullDelta +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 bool +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [SemanticTokensFullDelta bool]"} -} -diff -urN a/gopls/internal/lsp/source/completion/builtin.go b/gopls/internal/lsp/source/completion/builtin.go ---- a/gopls/internal/lsp/source/completion/builtin.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/completion/builtin.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,147 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package completion -- --import ( -- "context" -- "go/ast" -- "go/types" --) - --// builtinArgKind determines the expected object kind for a builtin --// argument. It attempts to use the AST hints from builtin.go where --// possible. --func (c *completer) builtinArgKind(ctx context.Context, obj types.Object, call *ast.CallExpr) objKind { -- builtin, err := c.snapshot.BuiltinFile(ctx) -- if err != nil { -- return 0 +-func (t Or_SemanticTokensOptions_range) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case PRangeESemanticTokensOptions: +- return json.Marshal(x) +- case bool: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } -- exprIdx := exprAtPos(c.pos, call.Args) +- return nil, fmt.Errorf("type %T not one of [PRangeESemanticTokensOptions bool]", t) +-} - -- builtinObj := builtin.File.Scope.Lookup(obj.Name()) -- if builtinObj == nil { -- return 0 +-func (t *Or_SemanticTokensOptions_range) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil - } -- decl, ok := builtinObj.Decl.(*ast.FuncDecl) -- if !ok || exprIdx >= len(decl.Type.Params.List) { -- return 0 +- var h0 PRangeESemanticTokensOptions +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil - } -- -- switch ptyp := decl.Type.Params.List[exprIdx].Type.(type) { -- case *ast.ChanType: -- return kindChan -- case *ast.ArrayType: -- return kindSlice -- case *ast.MapType: -- return kindMap -- case *ast.Ident: -- switch ptyp.Name { -- case "Type": -- switch obj.Name() { -- case "make": -- return kindChan | kindSlice | kindMap -- case "len": -- return kindSlice | kindMap | kindArray | kindString | kindChan -- case "cap": -- return kindSlice | kindArray | kindChan -- } -- } +- var h1 bool +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil - } -- -- return 0 +- return &UnmarshalError{"unmarshal failed to match one of [PRangeESemanticTokensOptions bool]"} -} - --// builtinArgType infers the type of an argument to a builtin --// function. parentInf is the inferred type info for the builtin --// call's parent node. --func (c *completer) builtinArgType(obj types.Object, call *ast.CallExpr, parentInf candidateInference) candidateInference { -- var ( -- exprIdx = exprAtPos(c.pos, call.Args) -- -- // Propagate certain properties from our parent's inference. -- inf = candidateInference{ -- typeName: parentInf.typeName, -- modifiers: parentInf.modifiers, -- } -- ) -- -- switch obj.Name() { -- case "append": -- if exprIdx <= 0 { -- // Infer first append() arg type as apparent return type of -- // append(). -- inf.objType = parentInf.objType -- if parentInf.variadic { -- inf.objType = types.NewSlice(inf.objType) -- } -- break -- } -- -- // For non-initial append() args, infer slice type from the first -- // append() arg, or from parent context. -- if len(call.Args) > 0 { -- inf.objType = c.pkg.GetTypesInfo().TypeOf(call.Args[0]) -- } -- if inf.objType == nil { -- inf.objType = parentInf.objType -- } -- if inf.objType == nil { -- break -- } -- -- inf.objType = deslice(inf.objType) -- -- // Check if we are completing the variadic append() param. -- inf.variadic = exprIdx == 1 && len(call.Args) <= 2 -- -- // Penalize the first append() argument as a candidate. You -- // don't normally append a slice to itself. -- if sliceChain := objChain(c.pkg.GetTypesInfo(), call.Args[0]); len(sliceChain) > 0 { -- inf.penalized = append(inf.penalized, penalizedObj{objChain: sliceChain, penalty: 0.9}) -- } -- case "delete": -- if exprIdx > 0 && len(call.Args) > 0 { -- // Try to fill in expected type of map key. -- firstArgType := c.pkg.GetTypesInfo().TypeOf(call.Args[0]) -- if firstArgType != nil { -- if mt, ok := firstArgType.Underlying().(*types.Map); ok { -- inf.objType = mt.Key() -- } -- } -- } -- case "copy": -- var t1, t2 types.Type -- if len(call.Args) > 0 { -- t1 = c.pkg.GetTypesInfo().TypeOf(call.Args[0]) -- if len(call.Args) > 1 { -- t2 = c.pkg.GetTypesInfo().TypeOf(call.Args[1]) -- } -- } -- -- // Fill in expected type of either arg if the other is already present. -- if exprIdx == 1 && t1 != nil { -- inf.objType = t1 -- } else if exprIdx == 0 && t2 != nil { -- inf.objType = t2 -- } -- case "new": -- inf.typeName.wantTypeName = true -- if parentInf.objType != nil { -- // Expected type for "new" is the de-pointered parent type. -- if ptr, ok := parentInf.objType.Underlying().(*types.Pointer); ok { -- inf.objType = ptr.Elem() -- } -- } -- case "make": -- if exprIdx == 0 { -- inf.typeName.wantTypeName = true -- inf.objType = parentInf.objType -- } else { -- inf.objType = types.Typ[types.UntypedInt] -- } +-func (t Or_ServerCapabilities_callHierarchyProvider) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case CallHierarchyOptions: +- return json.Marshal(x) +- case CallHierarchyRegistrationOptions: +- return json.Marshal(x) +- case bool: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } -- -- return inf +- return nil, fmt.Errorf("type %T not one of [CallHierarchyOptions CallHierarchyRegistrationOptions bool]", t) -} -diff -urN a/gopls/internal/lsp/source/completion/completion.go b/gopls/internal/lsp/source/completion/completion.go ---- a/gopls/internal/lsp/source/completion/completion.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/completion/completion.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,3279 +0,0 @@ --// Copyright 2018 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --// Package completion provides core functionality for code completion in Go --// editors and tools. --package completion -- --import ( -- "context" -- "fmt" -- "go/ast" -- "go/constant" -- "go/parser" -- "go/printer" -- "go/scanner" -- "go/token" -- "go/types" -- "math" -- "sort" -- "strconv" -- "strings" -- "sync" -- "sync/atomic" -- "time" -- "unicode" - -- "golang.org/x/sync/errgroup" -- "golang.org/x/tools/go/ast/astutil" -- goplsastutil "golang.org/x/tools/gopls/internal/astutil" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/gopls/internal/lsp/snippet" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/fuzzy" -- "golang.org/x/tools/internal/imports" -- "golang.org/x/tools/internal/typeparams" --) -- --// A CompletionItem represents a possible completion suggested by the algorithm. --type CompletionItem struct { -- -- // Invariant: CompletionItem does not refer to syntax or types. -- -- // Label is the primary text the user sees for this completion item. -- Label string -- -- // Detail is supplemental information to present to the user. -- // This often contains the type or return type of the completion item. -- Detail string -- -- // InsertText is the text to insert if this item is selected. -- // Any of the prefix that has already been typed is not trimmed. -- // The insert text does not contain snippets. -- InsertText string -- -- Kind protocol.CompletionItemKind -- Tags []protocol.CompletionItemTag -- Deprecated bool // Deprecated, prefer Tags if available -- -- // An optional array of additional TextEdits that are applied when -- // selecting this completion. -- // -- // Additional text edits should be used to change text unrelated to the current cursor position -- // (for example adding an import statement at the top of the file if the completion item will -- // insert an unqualified type). -- AdditionalTextEdits []protocol.TextEdit -- -- // Depth is how many levels were searched to find this completion. -- // For example when completing "foo<>", "fooBar" is depth 0, and -- // "fooBar.Baz" is depth 1. -- Depth int -- -- // Score is the internal relevance score. -- // A higher score indicates that this completion item is more relevant. -- Score float64 +-func (t *Or_ServerCapabilities_callHierarchyProvider) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 CallHierarchyOptions +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 CallHierarchyRegistrationOptions +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- var h2 bool +- if err := json.Unmarshal(x, &h2); err == nil { +- t.Value = h2 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [CallHierarchyOptions CallHierarchyRegistrationOptions bool]"} +-} - -- // snippet is the LSP snippet for the completion item. The LSP -- // specification contains details about LSP snippets. For example, a -- // snippet for a function with the following signature: -- // -- // func foo(a, b, c int) -- // -- // would be: -- // -- // foo(${1:a int}, ${2: b int}, ${3: c int}) -- // -- // If Placeholders is false in the CompletionOptions, the above -- // snippet would instead be: -- // -- // foo(${1:}) -- snippet *snippet.Builder +-func (t Or_ServerCapabilities_codeActionProvider) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case CodeActionOptions: +- return json.Marshal(x) +- case bool: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [CodeActionOptions bool]", t) +-} - -- // Documentation is the documentation for the completion item. -- Documentation string +-func (t *Or_ServerCapabilities_codeActionProvider) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 CodeActionOptions +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 bool +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [CodeActionOptions bool]"} +-} - -- // isSlice reports whether the underlying type of the object -- // from which this candidate was derived is a slice. -- // (Used to complete append() calls.) -- isSlice bool +-func (t Or_ServerCapabilities_colorProvider) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case DocumentColorOptions: +- return json.Marshal(x) +- case DocumentColorRegistrationOptions: +- return json.Marshal(x) +- case bool: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [DocumentColorOptions DocumentColorRegistrationOptions bool]", t) -} - --// completionOptions holds completion specific configuration. --type completionOptions struct { -- unimported bool -- documentation bool -- fullDocumentation bool -- placeholders bool -- literal bool -- snippets bool -- postfix bool -- matcher source.Matcher -- budget time.Duration -- completeFunctionCalls bool +-func (t *Or_ServerCapabilities_colorProvider) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 DocumentColorOptions +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 DocumentColorRegistrationOptions +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- var h2 bool +- if err := json.Unmarshal(x, &h2); err == nil { +- t.Value = h2 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [DocumentColorOptions DocumentColorRegistrationOptions bool]"} -} - --// Snippet is a convenience returns the snippet if available, otherwise --// the InsertText. --// used for an item, depending on if the callee wants placeholders or not. --func (i *CompletionItem) Snippet() string { -- if i.snippet != nil { -- return i.snippet.String() +-func (t Or_ServerCapabilities_declarationProvider) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case DeclarationOptions: +- return json.Marshal(x) +- case DeclarationRegistrationOptions: +- return json.Marshal(x) +- case bool: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } -- return i.InsertText +- return nil, fmt.Errorf("type %T not one of [DeclarationOptions DeclarationRegistrationOptions bool]", t) -} - --// Scoring constants are used for weighting the relevance of different candidates. --const ( -- // stdScore is the base score for all completion items. -- stdScore float64 = 1.0 -- -- // highScore indicates a very relevant completion item. -- highScore float64 = 10.0 -- -- // lowScore indicates an irrelevant or not useful completion item. -- lowScore float64 = 0.01 --) -- --// matcher matches a candidate's label against the user input. The --// returned score reflects the quality of the match. A score of zero --// indicates no match, and a score of one means a perfect match. --type matcher interface { -- Score(candidateLabel string) (score float32) +-func (t *Or_ServerCapabilities_declarationProvider) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 DeclarationOptions +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 DeclarationRegistrationOptions +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- var h2 bool +- if err := json.Unmarshal(x, &h2); err == nil { +- t.Value = h2 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [DeclarationOptions DeclarationRegistrationOptions bool]"} -} - --// prefixMatcher implements case sensitive prefix matching. --type prefixMatcher string -- --func (pm prefixMatcher) Score(candidateLabel string) float32 { -- if strings.HasPrefix(candidateLabel, string(pm)) { -- return 1 +-func (t Or_ServerCapabilities_definitionProvider) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case DefinitionOptions: +- return json.Marshal(x) +- case bool: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } -- return -1 +- return nil, fmt.Errorf("type %T not one of [DefinitionOptions bool]", t) -} - --// insensitivePrefixMatcher implements case insensitive prefix matching. --type insensitivePrefixMatcher string -- --func (ipm insensitivePrefixMatcher) Score(candidateLabel string) float32 { -- if strings.HasPrefix(strings.ToLower(candidateLabel), string(ipm)) { -- return 1 +-func (t *Or_ServerCapabilities_definitionProvider) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil - } -- return -1 +- var h0 DefinitionOptions +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 bool +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [DefinitionOptions bool]"} -} - --// completer contains the necessary information for a single completion request. --type completer struct { -- snapshot source.Snapshot -- pkg source.Package -- qf types.Qualifier // for qualifying typed expressions -- mq source.MetadataQualifier // for syntactic qualifying -- opts *completionOptions -- -- // completionContext contains information about the trigger for this -- // completion request. -- completionContext completionContext -- -- // fh is a handle to the file associated with this completion request. -- fh source.FileHandle -- -- // filename is the name of the file associated with this completion request. -- filename string -- -- // file is the AST of the file associated with this completion request. -- file *ast.File -- -- // (tokFile, pos) is the position at which the request was triggered. -- tokFile *token.File -- pos token.Pos -- -- // path is the path of AST nodes enclosing the position. -- path []ast.Node -- -- // seen is the map that ensures we do not return duplicate results. -- seen map[types.Object]bool -- -- // items is the list of completion items returned. -- items []CompletionItem +-func (t Or_ServerCapabilities_diagnosticProvider) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case DiagnosticOptions: +- return json.Marshal(x) +- case DiagnosticRegistrationOptions: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [DiagnosticOptions DiagnosticRegistrationOptions]", t) +-} - -- // completionCallbacks is a list of callbacks to collect completions that -- // require expensive operations. This includes operations where we search -- // through the entire module cache. -- completionCallbacks []func(context.Context, *imports.Options) error +-func (t *Or_ServerCapabilities_diagnosticProvider) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 DiagnosticOptions +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 DiagnosticRegistrationOptions +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [DiagnosticOptions DiagnosticRegistrationOptions]"} +-} - -- // surrounding describes the identifier surrounding the position. -- surrounding *Selection +-func (t Or_ServerCapabilities_documentFormattingProvider) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case DocumentFormattingOptions: +- return json.Marshal(x) +- case bool: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [DocumentFormattingOptions bool]", t) +-} - -- // inference contains information we've inferred about ideal -- // candidates such as the candidate's type. -- inference candidateInference +-func (t *Or_ServerCapabilities_documentFormattingProvider) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 DocumentFormattingOptions +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 bool +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [DocumentFormattingOptions bool]"} +-} - -- // enclosingFunc contains information about the function enclosing -- // the position. -- enclosingFunc *funcInfo +-func (t Or_ServerCapabilities_documentHighlightProvider) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case DocumentHighlightOptions: +- return json.Marshal(x) +- case bool: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [DocumentHighlightOptions bool]", t) +-} - -- // enclosingCompositeLiteral contains information about the composite literal -- // enclosing the position. -- enclosingCompositeLiteral *compLitInfo +-func (t *Or_ServerCapabilities_documentHighlightProvider) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 DocumentHighlightOptions +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 bool +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [DocumentHighlightOptions bool]"} +-} - -- // deepState contains the current state of our deep completion search. -- deepState deepCompletionState +-func (t Or_ServerCapabilities_documentRangeFormattingProvider) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case DocumentRangeFormattingOptions: +- return json.Marshal(x) +- case bool: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [DocumentRangeFormattingOptions bool]", t) +-} - -- // matcher matches the candidates against the surrounding prefix. -- matcher matcher +-func (t *Or_ServerCapabilities_documentRangeFormattingProvider) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 DocumentRangeFormattingOptions +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 bool +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [DocumentRangeFormattingOptions bool]"} +-} - -- // methodSetCache caches the types.NewMethodSet call, which is relatively -- // expensive and can be called many times for the same type while searching -- // for deep completions. -- methodSetCache map[methodSetKey]*types.MethodSet +-func (t Or_ServerCapabilities_documentSymbolProvider) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case DocumentSymbolOptions: +- return json.Marshal(x) +- case bool: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [DocumentSymbolOptions bool]", t) +-} - -- // mapper converts the positions in the file from which the completion originated. -- mapper *protocol.Mapper +-func (t *Or_ServerCapabilities_documentSymbolProvider) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 DocumentSymbolOptions +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 bool +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [DocumentSymbolOptions bool]"} +-} - -- // startTime is when we started processing this completion request. It does -- // not include any time the request spent in the queue. -- // -- // Note: in CL 503016, startTime move to *after* type checking, but it was -- // subsequently determined that it was better to keep setting it *before* -- // type checking, so that the completion budget best approximates the user -- // experience. See golang/go#62665 for more details. -- startTime time.Time +-func (t Or_ServerCapabilities_foldingRangeProvider) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case FoldingRangeOptions: +- return json.Marshal(x) +- case FoldingRangeRegistrationOptions: +- return json.Marshal(x) +- case bool: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [FoldingRangeOptions FoldingRangeRegistrationOptions bool]", t) +-} - -- // scopes contains all scopes defined by nodes in our path, -- // including nil values for nodes that don't defined a scope. It -- // also includes our package scope and the universal scope at the -- // end. -- scopes []*types.Scope +-func (t *Or_ServerCapabilities_foldingRangeProvider) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 FoldingRangeOptions +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 FoldingRangeRegistrationOptions +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- var h2 bool +- if err := json.Unmarshal(x, &h2); err == nil { +- t.Value = h2 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [FoldingRangeOptions FoldingRangeRegistrationOptions bool]"} -} - --// funcInfo holds info about a function object. --type funcInfo struct { -- // sig is the function declaration enclosing the position. -- sig *types.Signature +-func (t Or_ServerCapabilities_hoverProvider) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case HoverOptions: +- return json.Marshal(x) +- case bool: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [HoverOptions bool]", t) +-} - -- // body is the function's body. -- body *ast.BlockStmt +-func (t *Or_ServerCapabilities_hoverProvider) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 HoverOptions +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 bool +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [HoverOptions bool]"} -} - --type compLitInfo struct { -- // cl is the *ast.CompositeLit enclosing the position. -- cl *ast.CompositeLit +-func (t Or_ServerCapabilities_implementationProvider) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case ImplementationOptions: +- return json.Marshal(x) +- case ImplementationRegistrationOptions: +- return json.Marshal(x) +- case bool: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [ImplementationOptions ImplementationRegistrationOptions bool]", t) +-} - -- // clType is the type of cl. -- clType types.Type +-func (t *Or_ServerCapabilities_implementationProvider) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 ImplementationOptions +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 ImplementationRegistrationOptions +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- var h2 bool +- if err := json.Unmarshal(x, &h2); err == nil { +- t.Value = h2 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [ImplementationOptions ImplementationRegistrationOptions bool]"} +-} - -- // kv is the *ast.KeyValueExpr enclosing the position, if any. -- kv *ast.KeyValueExpr +-func (t Or_ServerCapabilities_inlayHintProvider) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case InlayHintOptions: +- return json.Marshal(x) +- case InlayHintRegistrationOptions: +- return json.Marshal(x) +- case bool: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [InlayHintOptions InlayHintRegistrationOptions bool]", t) +-} - -- // inKey is true if we are certain the position is in the key side -- // of a key-value pair. -- inKey bool +-func (t *Or_ServerCapabilities_inlayHintProvider) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 InlayHintOptions +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 InlayHintRegistrationOptions +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- var h2 bool +- if err := json.Unmarshal(x, &h2); err == nil { +- t.Value = h2 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [InlayHintOptions InlayHintRegistrationOptions bool]"} +-} - -- // maybeInFieldName is true if inKey is false and it is possible -- // we are completing a struct field name. For example, -- // "SomeStruct{<>}" will be inKey=false, but maybeInFieldName=true -- // because we _could_ be completing a field name. -- maybeInFieldName bool +-func (t Or_ServerCapabilities_inlineCompletionProvider) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case InlineCompletionOptions: +- return json.Marshal(x) +- case bool: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [InlineCompletionOptions bool]", t) -} - --type importInfo struct { -- importPath string -- name string +-func (t *Or_ServerCapabilities_inlineCompletionProvider) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 InlineCompletionOptions +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 bool +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [InlineCompletionOptions bool]"} -} - --type methodSetKey struct { -- typ types.Type -- addressable bool +-func (t Or_ServerCapabilities_inlineValueProvider) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case InlineValueOptions: +- return json.Marshal(x) +- case InlineValueRegistrationOptions: +- return json.Marshal(x) +- case bool: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [InlineValueOptions InlineValueRegistrationOptions bool]", t) -} - --type completionContext struct { -- // triggerCharacter is the character used to trigger completion at current -- // position, if any. -- triggerCharacter string -- -- // triggerKind is information about how a completion was triggered. -- triggerKind protocol.CompletionTriggerKind -- -- // commentCompletion is true if we are completing a comment. -- commentCompletion bool +-func (t *Or_ServerCapabilities_inlineValueProvider) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 InlineValueOptions +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 InlineValueRegistrationOptions +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- var h2 bool +- if err := json.Unmarshal(x, &h2); err == nil { +- t.Value = h2 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [InlineValueOptions InlineValueRegistrationOptions bool]"} +-} - -- // packageCompletion is true if we are completing a package name. -- packageCompletion bool +-func (t Or_ServerCapabilities_linkedEditingRangeProvider) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case LinkedEditingRangeOptions: +- return json.Marshal(x) +- case LinkedEditingRangeRegistrationOptions: +- return json.Marshal(x) +- case bool: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [LinkedEditingRangeOptions LinkedEditingRangeRegistrationOptions bool]", t) -} - --// A Selection represents the cursor position and surrounding identifier. --type Selection struct { -- content string -- tokFile *token.File -- start, end, cursor token.Pos // relative to rng.TokFile -- mapper *protocol.Mapper +-func (t *Or_ServerCapabilities_linkedEditingRangeProvider) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 LinkedEditingRangeOptions +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 LinkedEditingRangeRegistrationOptions +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- var h2 bool +- if err := json.Unmarshal(x, &h2); err == nil { +- t.Value = h2 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [LinkedEditingRangeOptions LinkedEditingRangeRegistrationOptions bool]"} -} - --func (p Selection) Range() (protocol.Range, error) { -- return p.mapper.PosRange(p.tokFile, p.start, p.end) +-func (t Or_ServerCapabilities_monikerProvider) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case MonikerOptions: +- return json.Marshal(x) +- case MonikerRegistrationOptions: +- return json.Marshal(x) +- case bool: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [MonikerOptions MonikerRegistrationOptions bool]", t) -} - --func (p Selection) Prefix() string { -- return p.content[:p.cursor-p.start] +-func (t *Or_ServerCapabilities_monikerProvider) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 MonikerOptions +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 MonikerRegistrationOptions +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- var h2 bool +- if err := json.Unmarshal(x, &h2); err == nil { +- t.Value = h2 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [MonikerOptions MonikerRegistrationOptions bool]"} -} - --func (p Selection) Suffix() string { -- return p.content[p.cursor-p.start:] +-func (t Or_ServerCapabilities_notebookDocumentSync) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case NotebookDocumentSyncOptions: +- return json.Marshal(x) +- case NotebookDocumentSyncRegistrationOptions: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [NotebookDocumentSyncOptions NotebookDocumentSyncRegistrationOptions]", t) -} - --func (c *completer) setSurrounding(ident *ast.Ident) { -- if c.surrounding != nil { -- return +-func (t *Or_ServerCapabilities_notebookDocumentSync) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil - } -- if !(ident.Pos() <= c.pos && c.pos <= ident.End()) { -- return +- var h0 NotebookDocumentSyncOptions +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil - } -- -- c.surrounding = &Selection{ -- content: ident.Name, -- cursor: c.pos, -- // Overwrite the prefix only. -- tokFile: c.tokFile, -- start: ident.Pos(), -- end: ident.End(), -- mapper: c.mapper, +- var h1 NotebookDocumentSyncRegistrationOptions +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil - } -- -- c.setMatcherFromPrefix(c.surrounding.Prefix()) +- return &UnmarshalError{"unmarshal failed to match one of [NotebookDocumentSyncOptions NotebookDocumentSyncRegistrationOptions]"} -} - --func (c *completer) setMatcherFromPrefix(prefix string) { -- switch c.opts.matcher { -- case source.Fuzzy: -- c.matcher = fuzzy.NewMatcher(prefix) -- case source.CaseSensitive: -- c.matcher = prefixMatcher(prefix) -- default: -- c.matcher = insensitivePrefixMatcher(strings.ToLower(prefix)) +-func (t Or_ServerCapabilities_referencesProvider) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case ReferenceOptions: +- return json.Marshal(x) +- case bool: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } +- return nil, fmt.Errorf("type %T not one of [ReferenceOptions bool]", t) -} - --func (c *completer) getSurrounding() *Selection { -- if c.surrounding == nil { -- c.surrounding = &Selection{ -- content: "", -- cursor: c.pos, -- tokFile: c.tokFile, -- start: c.pos, -- end: c.pos, -- mapper: c.mapper, -- } +-func (t *Or_ServerCapabilities_referencesProvider) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil - } -- return c.surrounding +- var h0 ReferenceOptions +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 bool +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [ReferenceOptions bool]"} -} - --// candidate represents a completion candidate. --type candidate struct { -- // obj is the types.Object to complete to. -- // TODO(adonovan): eliminate dependence on go/types throughout this struct. -- obj types.Object -- -- // score is used to rank candidates. -- score float64 -- -- // name is the deep object name path, e.g. "foo.bar" -- name string -- -- // detail is additional information about this item. If not specified, -- // defaults to type string for the object. -- detail string -- -- // path holds the path from the search root (excluding the candidate -- // itself) for a deep candidate. -- path []types.Object -- -- // pathInvokeMask is a bit mask tracking whether each entry in path -- // should be formatted with "()" (i.e. whether it is a function -- // invocation). -- pathInvokeMask uint16 -- -- // mods contains modifications that should be applied to the -- // candidate when inserted. For example, "foo" may be inserted as -- // "*foo" or "foo()". -- mods []typeModKind -- -- // addressable is true if a pointer can be taken to the candidate. -- addressable bool -- -- // convertTo is a type that this candidate should be cast to. For -- // example, if convertTo is float64, "foo" should be formatted as -- // "float64(foo)". -- convertTo types.Type -- -- // imp is the import that needs to be added to this package in order -- // for this candidate to be valid. nil if no import needed. -- imp *importInfo +-func (t Or_ServerCapabilities_renameProvider) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case RenameOptions: +- return json.Marshal(x) +- case bool: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [RenameOptions bool]", t) -} - --func (c candidate) hasMod(mod typeModKind) bool { -- for _, m := range c.mods { -- if m == mod { -- return true -- } +-func (t *Or_ServerCapabilities_renameProvider) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil - } -- return false +- var h0 RenameOptions +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 bool +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [RenameOptions bool]"} -} - --// ErrIsDefinition is an error that informs the user they got no --// completions because they tried to complete the name of a new object --// being defined. --type ErrIsDefinition struct { -- objStr string +-func (t Or_ServerCapabilities_selectionRangeProvider) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case SelectionRangeOptions: +- return json.Marshal(x) +- case SelectionRangeRegistrationOptions: +- return json.Marshal(x) +- case bool: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [SelectionRangeOptions SelectionRangeRegistrationOptions bool]", t) -} - --func (e ErrIsDefinition) Error() string { -- msg := "this is a definition" -- if e.objStr != "" { -- msg += " of " + e.objStr +-func (t *Or_ServerCapabilities_selectionRangeProvider) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil - } -- return msg +- var h0 SelectionRangeOptions +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 SelectionRangeRegistrationOptions +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- var h2 bool +- if err := json.Unmarshal(x, &h2); err == nil { +- t.Value = h2 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [SelectionRangeOptions SelectionRangeRegistrationOptions bool]"} -} - --// Completion returns a list of possible candidates for completion, given a --// a file and a position. --// --// The selection is computed based on the preceding identifier and can be used by --// the client to score the quality of the completion. For instance, some clients --// may tolerate imperfect matches as valid completion results, since users may make typos. --func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, protoPos protocol.Position, protoContext protocol.CompletionContext) ([]CompletionItem, *Selection, error) { -- ctx, done := event.Start(ctx, "completion.Completion") -- defer done() -- -- startTime := time.Now() -- -- pkg, pgf, err := source.NarrowestPackageForFile(ctx, snapshot, fh.URI()) -- if err != nil || pgf.File.Package == token.NoPos { -- // If we can't parse this file or find position for the package -- // keyword, it may be missing a package declaration. Try offering -- // suggestions for the package declaration. -- // Note that this would be the case even if the keyword 'package' is -- // present but no package name exists. -- items, surrounding, innerErr := packageClauseCompletions(ctx, snapshot, fh, protoPos) -- if innerErr != nil { -- // return the error for GetParsedFile since it's more relevant in this situation. -- return nil, nil, fmt.Errorf("getting file %s for Completion: %w (package completions: %v)", fh.URI(), err, innerErr) -- } -- return items, surrounding, nil +-func (t Or_ServerCapabilities_semanticTokensProvider) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case SemanticTokensOptions: +- return json.Marshal(x) +- case SemanticTokensRegistrationOptions: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } +- return nil, fmt.Errorf("type %T not one of [SemanticTokensOptions SemanticTokensRegistrationOptions]", t) +-} - -- pos, err := pgf.PositionPos(protoPos) -- if err != nil { -- return nil, nil, err +-func (t *Or_ServerCapabilities_semanticTokensProvider) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil - } -- // Completion is based on what precedes the cursor. -- // Find the path to the position before pos. -- path, _ := astutil.PathEnclosingInterval(pgf.File, pos-1, pos-1) -- if path == nil { -- return nil, nil, fmt.Errorf("cannot find node enclosing position") +- var h0 SemanticTokensOptions +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil - } -- -- // Check if completion at this position is valid. If not, return early. -- switch n := path[0].(type) { -- case *ast.BasicLit: -- // Skip completion inside literals except for ImportSpec -- if len(path) > 1 { -- if _, ok := path[1].(*ast.ImportSpec); ok { -- break -- } -- } -- return nil, nil, nil -- case *ast.CallExpr: -- if n.Ellipsis.IsValid() && pos > n.Ellipsis && pos <= n.Ellipsis+token.Pos(len("...")) { -- // Don't offer completions inside or directly after "...". For -- // example, don't offer completions at "<>" in "foo(bar...<>"). -- return nil, nil, nil -- } -- case *ast.Ident: -- // reject defining identifiers -- if obj, ok := pkg.GetTypesInfo().Defs[n]; ok { -- if v, ok := obj.(*types.Var); ok && v.IsField() && v.Embedded() { -- // An anonymous field is also a reference to a type. -- } else if pgf.File.Name == n { -- // Don't skip completions if Ident is for package name. -- break -- } else { -- objStr := "" -- if obj != nil { -- qual := types.RelativeTo(pkg.GetTypes()) -- objStr = types.ObjectString(obj, qual) -- } -- ans, sel := definition(path, obj, pgf) -- if ans != nil { -- sort.Slice(ans, func(i, j int) bool { -- return ans[i].Score > ans[j].Score -- }) -- return ans, sel, nil -- } -- return nil, nil, ErrIsDefinition{objStr: objStr} -- } -- } +- var h1 SemanticTokensRegistrationOptions +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil - } +- return &UnmarshalError{"unmarshal failed to match one of [SemanticTokensOptions SemanticTokensRegistrationOptions]"} +-} - -- // Collect all surrounding scopes, innermost first. -- scopes := source.CollectScopes(pkg.GetTypesInfo(), path, pos) -- scopes = append(scopes, pkg.GetTypes().Scope(), types.Universe) +-func (t Or_ServerCapabilities_textDocumentSync) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case TextDocumentSyncKind: +- return json.Marshal(x) +- case TextDocumentSyncOptions: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [TextDocumentSyncKind TextDocumentSyncOptions]", t) +-} - -- opts := snapshot.Options() -- c := &completer{ -- pkg: pkg, -- snapshot: snapshot, -- qf: source.Qualifier(pgf.File, pkg.GetTypes(), pkg.GetTypesInfo()), -- mq: source.MetadataQualifierForFile(snapshot, pgf.File, pkg.Metadata()), -- completionContext: completionContext{ -- triggerCharacter: protoContext.TriggerCharacter, -- triggerKind: protoContext.TriggerKind, -- }, -- fh: fh, -- filename: fh.URI().Filename(), -- tokFile: pgf.Tok, -- file: pgf.File, -- path: path, -- pos: pos, -- seen: make(map[types.Object]bool), -- enclosingFunc: enclosingFunction(path, pkg.GetTypesInfo()), -- enclosingCompositeLiteral: enclosingCompositeLiteral(path, pos, pkg.GetTypesInfo()), -- deepState: deepCompletionState{ -- enabled: opts.DeepCompletion, -- }, -- opts: &completionOptions{ -- matcher: opts.Matcher, -- unimported: opts.CompleteUnimported, -- documentation: opts.CompletionDocumentation && opts.HoverKind != source.NoDocumentation, -- fullDocumentation: opts.HoverKind == source.FullDocumentation, -- placeholders: opts.UsePlaceholders, -- literal: opts.LiteralCompletions && opts.InsertTextFormat == protocol.SnippetTextFormat, -- budget: opts.CompletionBudget, -- snippets: opts.InsertTextFormat == protocol.SnippetTextFormat, -- postfix: opts.ExperimentalPostfixCompletions, -- completeFunctionCalls: opts.CompleteFunctionCalls, -- }, -- // default to a matcher that always matches -- matcher: prefixMatcher(""), -- methodSetCache: make(map[methodSetKey]*types.MethodSet), -- mapper: pgf.Mapper, -- startTime: startTime, -- scopes: scopes, +-func (t *Or_ServerCapabilities_textDocumentSync) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 TextDocumentSyncKind +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 TextDocumentSyncOptions +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil - } +- return &UnmarshalError{"unmarshal failed to match one of [TextDocumentSyncKind TextDocumentSyncOptions]"} +-} - -- ctx, cancel := context.WithCancel(ctx) -- defer cancel() +-func (t Or_ServerCapabilities_typeDefinitionProvider) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case TypeDefinitionOptions: +- return json.Marshal(x) +- case TypeDefinitionRegistrationOptions: +- return json.Marshal(x) +- case bool: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [TypeDefinitionOptions TypeDefinitionRegistrationOptions bool]", t) +-} - -- // Compute the deadline for this operation. Deadline is relative to the -- // search operation, not the entire completion RPC, as the work up until this -- // point depends significantly on how long it took to type-check, which in -- // turn depends on the timing of the request relative to other operations on -- // the snapshot. Including that work in the budget leads to inconsistent -- // results (and realistically, if type-checking took 200ms already, the user -- // is unlikely to be significantly more bothered by e.g. another 100ms of -- // search). -- // -- // Don't overload the context with this deadline, as we don't want to -- // conflate user cancellation (=fail the operation) with our time limit -- // (=stop searching and succeed with partial results). -- var deadline *time.Time -- if c.opts.budget > 0 { -- d := startTime.Add(c.opts.budget) -- deadline = &d +-func (t *Or_ServerCapabilities_typeDefinitionProvider) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 TypeDefinitionOptions +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 TypeDefinitionRegistrationOptions +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil - } -- -- if surrounding := c.containingIdent(pgf.Src); surrounding != nil { -- c.setSurrounding(surrounding) +- var h2 bool +- if err := json.Unmarshal(x, &h2); err == nil { +- t.Value = h2 +- return nil - } +- return &UnmarshalError{"unmarshal failed to match one of [TypeDefinitionOptions TypeDefinitionRegistrationOptions bool]"} +-} - -- c.inference = expectedCandidate(ctx, c) -- -- err = c.collectCompletions(ctx) -- if err != nil { -- return nil, nil, err +-func (t Or_ServerCapabilities_typeHierarchyProvider) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case TypeHierarchyOptions: +- return json.Marshal(x) +- case TypeHierarchyRegistrationOptions: +- return json.Marshal(x) +- case bool: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } +- return nil, fmt.Errorf("type %T not one of [TypeHierarchyOptions TypeHierarchyRegistrationOptions bool]", t) +-} - -- // Deep search collected candidates and their members for more candidates. -- c.deepSearch(ctx, 1, deadline) -- -- // At this point we have a sufficiently complete set of results, and want to -- // return as close to the completion budget as possible. Previously, we -- // avoided cancelling the context because it could result in partial results -- // for e.g. struct fields. At this point, we have a minimal valid set of -- // candidates, and so truncating due to context cancellation is acceptable. -- if c.opts.budget > 0 { -- timeoutDuration := time.Until(c.startTime.Add(c.opts.budget)) -- ctx, cancel = context.WithTimeout(ctx, timeoutDuration) -- defer cancel() +-func (t *Or_ServerCapabilities_typeHierarchyProvider) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil - } -- -- for _, callback := range c.completionCallbacks { -- if deadline == nil || time.Now().Before(*deadline) { -- if err := c.snapshot.RunProcessEnvFunc(ctx, callback); err != nil { -- return nil, nil, err -- } -- } +- var h0 TypeHierarchyOptions +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil - } -- -- // Search candidates populated by expensive operations like -- // unimportedMembers etc. for more completion items. -- c.deepSearch(ctx, 0, deadline) -- -- // Statement candidates offer an entire statement in certain contexts, as -- // opposed to a single object. Add statement candidates last because they -- // depend on other candidates having already been collected. -- c.addStatementCandidates() -- -- c.sortItems() -- return c.items, c.getSurrounding(), nil +- var h1 TypeHierarchyRegistrationOptions +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- var h2 bool +- if err := json.Unmarshal(x, &h2); err == nil { +- t.Value = h2 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [TypeHierarchyOptions TypeHierarchyRegistrationOptions bool]"} -} - --// collectCompletions adds possible completion candidates to either the deep --// search queue or completion items directly for different completion contexts. --func (c *completer) collectCompletions(ctx context.Context) error { -- // Inside import blocks, return completions for unimported packages. -- for _, importSpec := range c.file.Imports { -- if !(importSpec.Path.Pos() <= c.pos && c.pos <= importSpec.Path.End()) { -- continue -- } -- return c.populateImportCompletions(importSpec) +-func (t Or_ServerCapabilities_workspaceSymbolProvider) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case WorkspaceSymbolOptions: +- return json.Marshal(x) +- case bool: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } +- return nil, fmt.Errorf("type %T not one of [WorkspaceSymbolOptions bool]", t) +-} - -- // Inside comments, offer completions for the name of the relevant symbol. -- for _, comment := range c.file.Comments { -- if comment.Pos() < c.pos && c.pos <= comment.End() { -- c.populateCommentCompletions(ctx, comment) -- return nil -- } +-func (t *Or_ServerCapabilities_workspaceSymbolProvider) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 WorkspaceSymbolOptions +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil - } +- var h1 bool +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [WorkspaceSymbolOptions bool]"} +-} - -- // Struct literals are handled entirely separately. -- if c.wantStructFieldCompletions() { -- // If we are definitely completing a struct field name, deep completions -- // don't make sense. -- if c.enclosingCompositeLiteral.inKey { -- c.deepState.enabled = false -- } -- return c.structLiteralFieldName(ctx) +-func (t Or_SignatureInformation_documentation) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case MarkupContent: +- return json.Marshal(x) +- case string: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } +- return nil, fmt.Errorf("type %T not one of [MarkupContent string]", t) +-} - -- if lt := c.wantLabelCompletion(); lt != labelNone { -- c.labels(lt) +-func (t *Or_SignatureInformation_documentation) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil - return nil - } -- -- if c.emptySwitchStmt() { -- // Empty switch statements only admit "default" and "case" keywords. -- c.addKeywordItems(map[string]bool{}, highScore, CASE, DEFAULT) +- var h0 MarkupContent +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 - return nil - } -- -- switch n := c.path[0].(type) { -- case *ast.Ident: -- if c.file.Name == n { -- return c.packageNameCompletions(ctx, c.fh.URI(), n) -- } else if sel, ok := c.path[1].(*ast.SelectorExpr); ok && sel.Sel == n { -- // Is this the Sel part of a selector? -- return c.selector(ctx, sel) -- } -- return c.lexical(ctx) -- // The function name hasn't been typed yet, but the parens are there: -- // recv.‸(arg) -- case *ast.TypeAssertExpr: -- // Create a fake selector expression. -- // -- // The name "_" is the convention used by go/parser to represent phantom -- // selectors. -- sel := &ast.Ident{NamePos: n.X.End() + token.Pos(len(".")), Name: "_"} -- return c.selector(ctx, &ast.SelectorExpr{X: n.X, Sel: sel}) -- case *ast.SelectorExpr: -- return c.selector(ctx, n) -- // At the file scope, only keywords are allowed. -- case *ast.BadDecl, *ast.File: -- c.addKeywordCompletions() -- default: -- // fallback to lexical completions -- return c.lexical(ctx) +- var h1 string +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil - } -- -- return nil +- return &UnmarshalError{"unmarshal failed to match one of [MarkupContent string]"} -} - --// containingIdent returns the *ast.Ident containing pos, if any. It --// synthesizes an *ast.Ident to allow completion in the face of --// certain syntax errors. --func (c *completer) containingIdent(src []byte) *ast.Ident { -- // In the normal case, our leaf AST node is the identifier being completed. -- if ident, ok := c.path[0].(*ast.Ident); ok { -- return ident +-func (t Or_TextDocumentEdit_edits_Elem) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case AnnotatedTextEdit: +- return json.Marshal(x) +- case TextEdit: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } +- return nil, fmt.Errorf("type %T not one of [AnnotatedTextEdit TextEdit]", t) +-} - -- pos, tkn, lit := c.scanToken(src) -- if !pos.IsValid() { +-func (t *Or_TextDocumentEdit_edits_Elem) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil - return nil - } -- -- fakeIdent := &ast.Ident{Name: lit, NamePos: pos} -- -- if _, isBadDecl := c.path[0].(*ast.BadDecl); isBadDecl { -- // You don't get *ast.Idents at the file level, so look for bad -- // decls and use the manually extracted token. -- return fakeIdent -- } else if c.emptySwitchStmt() { -- // Only keywords are allowed in empty switch statements. -- // *ast.Idents are not parsed, so we must use the manually -- // extracted token. -- return fakeIdent -- } else if tkn.IsKeyword() { -- // Otherwise, manually extract the prefix if our containing token -- // is a keyword. This improves completion after an "accidental -- // keyword", e.g. completing to "variance" in "someFunc(var<>)". -- return fakeIdent +- var h0 AnnotatedTextEdit +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil - } -- -- return nil +- var h1 TextEdit +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [AnnotatedTextEdit TextEdit]"} -} - --// scanToken scans pgh's contents for the token containing pos. --func (c *completer) scanToken(contents []byte) (token.Pos, token.Token, string) { -- tok := c.pkg.FileSet().File(c.pos) -- -- var s scanner.Scanner -- s.Init(tok, contents, nil, 0) -- for { -- tknPos, tkn, lit := s.Scan() -- if tkn == token.EOF || tknPos >= c.pos { -- return token.NoPos, token.ILLEGAL, "" -- } -- -- if len(lit) > 0 && tknPos <= c.pos && c.pos <= tknPos+token.Pos(len(lit)) { -- return tknPos, tkn, lit -- } +-func (t Or_TextDocumentFilter) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case TextDocumentFilterLanguage: +- return json.Marshal(x) +- case TextDocumentFilterPattern: +- return json.Marshal(x) +- case TextDocumentFilterScheme: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } +- return nil, fmt.Errorf("type %T not one of [TextDocumentFilterLanguage TextDocumentFilterPattern TextDocumentFilterScheme]", t) -} - --func (c *completer) sortItems() { -- sort.SliceStable(c.items, func(i, j int) bool { -- // Sort by score first. -- if c.items[i].Score != c.items[j].Score { -- return c.items[i].Score > c.items[j].Score -- } +-func (t *Or_TextDocumentFilter) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 TextDocumentFilterLanguage +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 TextDocumentFilterPattern +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- var h2 TextDocumentFilterScheme +- if err := json.Unmarshal(x, &h2); err == nil { +- t.Value = h2 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [TextDocumentFilterLanguage TextDocumentFilterPattern TextDocumentFilterScheme]"} +-} - -- // Then sort by label so order stays consistent. This also has the -- // effect of preferring shorter candidates. -- return c.items[i].Label < c.items[j].Label -- }) +-func (t Or_TextDocumentSyncOptions_save) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case SaveOptions: +- return json.Marshal(x) +- case bool: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil +- } +- return nil, fmt.Errorf("type %T not one of [SaveOptions bool]", t) -} - --// emptySwitchStmt reports whether pos is in an empty switch or select --// statement. --func (c *completer) emptySwitchStmt() bool { -- block, ok := c.path[0].(*ast.BlockStmt) -- if !ok || len(block.List) > 0 || len(c.path) == 1 { -- return false +-func (t *Or_TextDocumentSyncOptions_save) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 SaveOptions +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 bool +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil - } +- return &UnmarshalError{"unmarshal failed to match one of [SaveOptions bool]"} +-} - -- switch c.path[1].(type) { -- case *ast.SwitchStmt, *ast.TypeSwitchStmt, *ast.SelectStmt: -- return true -- default: -- return false +-func (t Or_WorkspaceDocumentDiagnosticReport) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case WorkspaceFullDocumentDiagnosticReport: +- return json.Marshal(x) +- case WorkspaceUnchangedDocumentDiagnosticReport: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } +- return nil, fmt.Errorf("type %T not one of [WorkspaceFullDocumentDiagnosticReport WorkspaceUnchangedDocumentDiagnosticReport]", t) -} - --// populateImportCompletions yields completions for an import path around the cursor. --// --// Completions are suggested at the directory depth of the given import path so --// that we don't overwhelm the user with a large list of possibilities. As an --// example, a completion for the prefix "golang" results in "golang.org/". --// Completions for "golang.org/" yield its subdirectories --// (i.e. "golang.org/x/"). The user is meant to accept completion suggestions --// until they reach a complete import path. --func (c *completer) populateImportCompletions(searchImport *ast.ImportSpec) error { -- if !strings.HasPrefix(searchImport.Path.Value, `"`) { +-func (t *Or_WorkspaceDocumentDiagnosticReport) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil - return nil - } +- var h0 WorkspaceFullDocumentDiagnosticReport +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 WorkspaceUnchangedDocumentDiagnosticReport +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- return &UnmarshalError{"unmarshal failed to match one of [WorkspaceFullDocumentDiagnosticReport WorkspaceUnchangedDocumentDiagnosticReport]"} +-} - -- // deepSearch is not valuable for import completions. -- c.deepState.enabled = false -- -- importPath := searchImport.Path.Value -- -- // Extract the text between the quotes (if any) in an import spec. -- // prefix is the part of import path before the cursor. -- prefixEnd := c.pos - searchImport.Path.Pos() -- prefix := strings.Trim(importPath[:prefixEnd], `"`) -- -- // The number of directories in the import path gives us the depth at -- // which to search. -- depth := len(strings.Split(prefix, "/")) - 1 -- -- content := importPath -- start, end := searchImport.Path.Pos(), searchImport.Path.End() -- namePrefix, nameSuffix := `"`, `"` -- // If a starting quote is present, adjust surrounding to either after the -- // cursor or after the first slash (/), except if cursor is at the starting -- // quote. Otherwise we provide a completion including the starting quote. -- if strings.HasPrefix(importPath, `"`) && c.pos > searchImport.Path.Pos() { -- content = content[1:] -- start++ -- if depth > 0 { -- // Adjust textEdit start to replacement range. For ex: if current -- // path was "golang.or/x/to<>ols/internal/", where <> is the cursor -- // position, start of the replacement range would be after -- // "golang.org/x/". -- path := strings.SplitAfter(prefix, "/") -- numChars := len(strings.Join(path[:len(path)-1], "")) -- content = content[numChars:] -- start += token.Pos(numChars) -- } -- namePrefix = "" +-func (t Or_WorkspaceEdit_documentChanges_Elem) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case CreateFile: +- return json.Marshal(x) +- case DeleteFile: +- return json.Marshal(x) +- case RenameFile: +- return json.Marshal(x) +- case TextDocumentEdit: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } +- return nil, fmt.Errorf("type %T not one of [CreateFile DeleteFile RenameFile TextDocumentEdit]", t) +-} - -- // We won't provide an ending quote if one is already present, except if -- // cursor is after the ending quote but still in import spec. This is -- // because cursor has to be in our textEdit range. -- if strings.HasSuffix(importPath, `"`) && c.pos < searchImport.Path.End() { -- end-- -- content = content[:len(content)-1] -- nameSuffix = "" +-func (t *Or_WorkspaceEdit_documentChanges_Elem) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 CreateFile +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 DeleteFile +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil +- } +- var h2 RenameFile +- if err := json.Unmarshal(x, &h2); err == nil { +- t.Value = h2 +- return nil +- } +- var h3 TextDocumentEdit +- if err := json.Unmarshal(x, &h3); err == nil { +- t.Value = h3 +- return nil - } +- return &UnmarshalError{"unmarshal failed to match one of [CreateFile DeleteFile RenameFile TextDocumentEdit]"} +-} - -- c.surrounding = &Selection{ -- content: content, -- cursor: c.pos, -- tokFile: c.tokFile, -- start: start, -- end: end, -- mapper: c.mapper, +-func (t Or_textDocument_declaration) MarshalJSON() ([]byte, error) { +- switch x := t.Value.(type) { +- case Declaration: +- return json.Marshal(x) +- case []DeclarationLink: +- return json.Marshal(x) +- case nil: +- return []byte("null"), nil - } +- return nil, fmt.Errorf("type %T not one of [Declaration []DeclarationLink]", t) +-} - -- seenImports := make(map[string]struct{}) -- for _, importSpec := range c.file.Imports { -- if importSpec.Path.Value == importPath { -- continue -- } -- seenImportPath, err := strconv.Unquote(importSpec.Path.Value) -- if err != nil { -- return err -- } -- seenImports[seenImportPath] = struct{}{} +-func (t *Or_textDocument_declaration) UnmarshalJSON(x []byte) error { +- if string(x) == "null" { +- t.Value = nil +- return nil +- } +- var h0 Declaration +- if err := json.Unmarshal(x, &h0); err == nil { +- t.Value = h0 +- return nil +- } +- var h1 []DeclarationLink +- if err := json.Unmarshal(x, &h1); err == nil { +- t.Value = h1 +- return nil - } +- return &UnmarshalError{"unmarshal failed to match one of [Declaration []DeclarationLink]"} +-} +diff -urN a/gopls/internal/protocol/tsprotocol.go b/gopls/internal/protocol/tsprotocol.go +--- a/gopls/internal/protocol/tsprotocol.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/tsprotocol.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,5931 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- var mu sync.Mutex // guard c.items locally, since searchImports is called in parallel -- seen := make(map[string]struct{}) -- searchImports := func(pkg imports.ImportFix) { -- path := pkg.StmtInfo.ImportPath -- if _, ok := seenImports[path]; ok { -- return -- } +-// Code generated for LSP. DO NOT EDIT. - -- // Any package path containing fewer directories than the search -- // prefix is not a match. -- pkgDirList := strings.Split(path, "/") -- if len(pkgDirList) < depth+1 { -- return -- } -- pkgToConsider := strings.Join(pkgDirList[:depth+1], "/") +-package protocol - -- name := pkgDirList[depth] -- // if we're adding an opening quote to completion too, set name to full -- // package path since we'll need to overwrite that range. -- if namePrefix == `"` { -- name = pkgToConsider -- } +-// Code generated from protocol/metaModel.json at ref release/protocol/3.17.6-next.2 (hash 654dc9be6673c61476c28fda604406279c3258d7). +-// https://github.com/microsoft/vscode-languageserver-node/blob/release/protocol/3.17.6-next.2/protocol/metaModel.json +-// LSP metaData.version = 3.17.0. - -- score := pkg.Relevance -- if len(pkgDirList)-1 == depth { -- score *= highScore -- } else { -- // For incomplete package paths, add a terminal slash to indicate that the -- // user should keep triggering completions. -- name += "/" -- pkgToConsider += "/" -- } +-import "encoding/json" - -- if _, ok := seen[pkgToConsider]; ok { -- return -- } -- seen[pkgToConsider] = struct{}{} +-// A special text edit with an additional change annotation. +-// +-// @since 3.16.0. +-type AnnotatedTextEdit struct { +- // The actual identifier of the change annotation +- AnnotationID *ChangeAnnotationIdentifier `json:"annotationId,omitempty"` +- TextEdit +-} - -- mu.Lock() -- defer mu.Unlock() +-// The parameters passed via an apply workspace edit request. +-type ApplyWorkspaceEditParams struct { +- // An optional label of the workspace edit. This label is +- // presented in the user interface for example on an undo +- // stack to undo the workspace edit. +- Label string `json:"label,omitempty"` +- // The edits to apply. +- Edit WorkspaceEdit `json:"edit"` +-} - -- name = namePrefix + name + nameSuffix -- obj := types.NewPkgName(0, nil, name, types.NewPackage(pkgToConsider, name)) -- c.deepState.enqueue(candidate{ -- obj: obj, -- detail: fmt.Sprintf("%q", pkgToConsider), -- score: score, -- }) -- } +-// The result returned from the apply workspace edit request. +-// +-// @since 3.17 renamed from ApplyWorkspaceEditResponse +-type ApplyWorkspaceEditResult struct { +- // Indicates whether the edit was applied or not. +- Applied bool `json:"applied"` +- // An optional textual description for why the edit was not applied. +- // This may be used by the server for diagnostic logging or to provide +- // a suitable error for a request that triggered the edit. +- FailureReason string `json:"failureReason,omitempty"` +- // Depending on the client's failure handling strategy `failedChange` might +- // contain the index of the change that failed. This property is only available +- // if the client signals a `failureHandlingStrategy` in its client capabilities. +- FailedChange uint32 `json:"failedChange,omitempty"` +-} - -- c.completionCallbacks = append(c.completionCallbacks, func(ctx context.Context, opts *imports.Options) error { -- return imports.GetImportPaths(ctx, searchImports, prefix, c.filename, c.pkg.GetTypes().Name(), opts.Env) -- }) -- return nil +-// A base for all symbol information. +-type BaseSymbolInformation struct { +- // The name of this symbol. +- Name string `json:"name"` +- // The kind of this symbol. +- Kind SymbolKind `json:"kind"` +- // Tags for this symbol. +- // +- // @since 3.16.0 +- Tags []SymbolTag `json:"tags,omitempty"` +- // The name of the symbol containing this symbol. This information is for +- // user interface purposes (e.g. to render a qualifier in the user interface +- // if necessary). It can't be used to re-infer a hierarchy for the document +- // symbols. +- ContainerName string `json:"containerName,omitempty"` -} - --// populateCommentCompletions yields completions for comments preceding or in declarations. --func (c *completer) populateCommentCompletions(ctx context.Context, comment *ast.CommentGroup) { -- // If the completion was triggered by a period, ignore it. These types of -- // completions will not be useful in comments. -- if c.completionContext.triggerCharacter == "." { -- return -- } +-// @since 3.16.0 +-type CallHierarchyClientCapabilities struct { +- // Whether implementation supports dynamic registration. If this is set to `true` +- // the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` +- // return value for the corresponding server capability as well. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +-} - -- // Using the comment position find the line after -- file := c.pkg.FileSet().File(comment.End()) -- if file == nil { -- return -- } +-// Represents an incoming call, e.g. a caller of a method or constructor. +-// +-// @since 3.16.0 +-type CallHierarchyIncomingCall struct { +- // The item that makes the call. +- From CallHierarchyItem `json:"from"` +- // The ranges at which the calls appear. This is relative to the caller +- // denoted by {@link CallHierarchyIncomingCall.from `this.from`}. +- FromRanges []Range `json:"fromRanges"` +-} - -- // Deep completion doesn't work properly in comments since we don't -- // have a type object to complete further. -- c.deepState.enabled = false -- c.completionContext.commentCompletion = true +-// The parameter of a `callHierarchy/incomingCalls` request. +-// +-// @since 3.16.0 +-type CallHierarchyIncomingCallsParams struct { +- Item CallHierarchyItem `json:"item"` +- WorkDoneProgressParams +- PartialResultParams +-} - -- // Documentation isn't useful in comments, since it might end up being the -- // comment itself. -- c.opts.documentation = false +-// Represents programming constructs like functions or constructors in the context +-// of call hierarchy. +-// +-// @since 3.16.0 +-type CallHierarchyItem struct { +- // The name of this item. +- Name string `json:"name"` +- // The kind of this item. +- Kind SymbolKind `json:"kind"` +- // Tags for this item. +- Tags []SymbolTag `json:"tags,omitempty"` +- // More detail for this item, e.g. the signature of a function. +- Detail string `json:"detail,omitempty"` +- // The resource identifier of this item. +- URI DocumentURI `json:"uri"` +- // The range enclosing this symbol not including leading/trailing whitespace but everything else, e.g. comments and code. +- Range Range `json:"range"` +- // The range that should be selected and revealed when this symbol is being picked, e.g. the name of a function. +- // Must be contained by the {@link CallHierarchyItem.range `range`}. +- SelectionRange Range `json:"selectionRange"` +- // A data entry field that is preserved between a call hierarchy prepare and +- // incoming calls or outgoing calls requests. +- Data interface{} `json:"data,omitempty"` +-} - -- commentLine := safetoken.Line(file, comment.End()) +-// Call hierarchy options used during static registration. +-// +-// @since 3.16.0 +-type CallHierarchyOptions struct { +- WorkDoneProgressOptions +-} - -- // comment is valid, set surrounding as word boundaries around cursor -- c.setSurroundingForComment(comment) +-// Represents an outgoing call, e.g. calling a getter from a method or a method from a constructor etc. +-// +-// @since 3.16.0 +-type CallHierarchyOutgoingCall struct { +- // The item that is called. +- To CallHierarchyItem `json:"to"` +- // The range at which this item is called. This is the range relative to the caller, e.g the item +- // passed to {@link CallHierarchyItemProvider.provideCallHierarchyOutgoingCalls `provideCallHierarchyOutgoingCalls`} +- // and not {@link CallHierarchyOutgoingCall.to `this.to`}. +- FromRanges []Range `json:"fromRanges"` +-} - -- // Using the next line pos, grab and parse the exported symbol on that line -- for _, n := range c.file.Decls { -- declLine := safetoken.Line(file, n.Pos()) -- // if the comment is not in, directly above or on the same line as a declaration -- if declLine != commentLine && declLine != commentLine+1 && -- !(n.Pos() <= comment.Pos() && comment.End() <= n.End()) { -- continue -- } -- switch node := n.(type) { -- // handle const, vars, and types -- case *ast.GenDecl: -- for _, spec := range node.Specs { -- switch spec := spec.(type) { -- case *ast.ValueSpec: -- for _, name := range spec.Names { -- if name.String() == "_" { -- continue -- } -- obj := c.pkg.GetTypesInfo().ObjectOf(name) -- c.deepState.enqueue(candidate{obj: obj, score: stdScore}) -- } -- case *ast.TypeSpec: -- // add TypeSpec fields to completion -- switch typeNode := spec.Type.(type) { -- case *ast.StructType: -- c.addFieldItems(ctx, typeNode.Fields) -- case *ast.FuncType: -- c.addFieldItems(ctx, typeNode.Params) -- c.addFieldItems(ctx, typeNode.Results) -- case *ast.InterfaceType: -- c.addFieldItems(ctx, typeNode.Methods) -- } +-// The parameter of a `callHierarchy/outgoingCalls` request. +-// +-// @since 3.16.0 +-type CallHierarchyOutgoingCallsParams struct { +- Item CallHierarchyItem `json:"item"` +- WorkDoneProgressParams +- PartialResultParams +-} - -- if spec.Name.String() == "_" { -- continue -- } +-// The parameter of a `textDocument/prepareCallHierarchy` request. +-// +-// @since 3.16.0 +-type CallHierarchyPrepareParams struct { +- TextDocumentPositionParams +- WorkDoneProgressParams +-} - -- obj := c.pkg.GetTypesInfo().ObjectOf(spec.Name) -- // Type name should get a higher score than fields but not highScore by default -- // since field near a comment cursor gets a highScore -- score := stdScore * 1.1 -- // If type declaration is on the line after comment, give it a highScore. -- if declLine == commentLine+1 { -- score = highScore -- } +-// Call hierarchy options used during static or dynamic registration. +-// +-// @since 3.16.0 +-type CallHierarchyRegistrationOptions struct { +- TextDocumentRegistrationOptions +- CallHierarchyOptions +- StaticRegistrationOptions +-} +-type CancelParams struct { +- // The request id to cancel. +- ID interface{} `json:"id"` +-} - -- c.deepState.enqueue(candidate{obj: obj, score: score}) -- } -- } -- // handle functions -- case *ast.FuncDecl: -- c.addFieldItems(ctx, node.Recv) -- c.addFieldItems(ctx, node.Type.Params) -- c.addFieldItems(ctx, node.Type.Results) +-// Additional information that describes document changes. +-// +-// @since 3.16.0 +-type ChangeAnnotation struct { +- // A human-readable string describing the actual change. The string +- // is rendered prominent in the user interface. +- Label string `json:"label"` +- // A flag which indicates that user confirmation is needed +- // before applying the change. +- NeedsConfirmation bool `json:"needsConfirmation,omitempty"` +- // A human-readable string which is rendered less prominent in +- // the user interface. +- Description string `json:"description,omitempty"` +-} - -- // collect receiver struct fields -- if node.Recv != nil { -- for _, fields := range node.Recv.List { -- for _, name := range fields.Names { -- obj := c.pkg.GetTypesInfo().ObjectOf(name) -- if obj == nil { -- continue -- } +-// An identifier to refer to a change annotation stored with a workspace edit. +-type ChangeAnnotationIdentifier = string // (alias) +-// @since 3.18.0 +-// @proposed +-type ChangeAnnotationsSupportOptions struct { +- // Whether the client groups edits with equal labels into tree nodes, +- // for instance all edits labelled with "Changes in Strings" would +- // be a tree node. +- GroupsOnLabel bool `json:"groupsOnLabel,omitempty"` +-} - -- recvType := obj.Type().Underlying() -- if ptr, ok := recvType.(*types.Pointer); ok { -- recvType = ptr.Elem() -- } -- recvStruct, ok := recvType.Underlying().(*types.Struct) -- if !ok { -- continue -- } -- for i := 0; i < recvStruct.NumFields(); i++ { -- field := recvStruct.Field(i) -- c.deepState.enqueue(candidate{obj: field, score: lowScore}) -- } -- } -- } -- } +-// Defines the capabilities provided by the client. +-type ClientCapabilities struct { +- // Workspace specific client capabilities. +- Workspace WorkspaceClientCapabilities `json:"workspace,omitempty"` +- // Text document specific client capabilities. +- TextDocument TextDocumentClientCapabilities `json:"textDocument,omitempty"` +- // Capabilities specific to the notebook document support. +- // +- // @since 3.17.0 +- NotebookDocument *NotebookDocumentClientCapabilities `json:"notebookDocument,omitempty"` +- // Window specific client capabilities. +- Window WindowClientCapabilities `json:"window,omitempty"` +- // General client capabilities. +- // +- // @since 3.16.0 +- General *GeneralClientCapabilities `json:"general,omitempty"` +- // Experimental client capabilities. +- Experimental interface{} `json:"experimental,omitempty"` +-} - -- if node.Name.String() == "_" { -- continue -- } +-// @since 3.18.0 +-// @proposed +-type ClientCodeActionKindOptions struct { +- // The code action kind values the client supports. When this +- // property exists the client also guarantees that it will +- // handle values outside its set gracefully and falls back +- // to a default value when unknown. +- ValueSet []CodeActionKind `json:"valueSet"` +-} - -- obj := c.pkg.GetTypesInfo().ObjectOf(node.Name) -- if obj == nil || obj.Pkg() != nil && obj.Pkg() != c.pkg.GetTypes() { -- continue -- } +-// @since 3.18.0 +-// @proposed +-type ClientCodeActionLiteralOptions struct { +- // The code action kind is support with the following value +- // set. +- CodeActionKind ClientCodeActionKindOptions `json:"codeActionKind"` +-} - -- c.deepState.enqueue(candidate{obj: obj, score: highScore}) -- } -- } +-// @since 3.18.0 +-// @proposed +-type ClientCodeActionResolveOptions struct { +- // The properties that a client can resolve lazily. +- Properties []string `json:"properties"` -} - --// sets word boundaries surrounding a cursor for a comment --func (c *completer) setSurroundingForComment(comments *ast.CommentGroup) { -- var cursorComment *ast.Comment -- for _, comment := range comments.List { -- if c.pos >= comment.Pos() && c.pos <= comment.End() { -- cursorComment = comment -- break -- } -- } -- // if cursor isn't in the comment -- if cursorComment == nil { -- return -- } +-// @since 3.18.0 +-// @proposed +-type ClientCompletionItemInsertTextModeOptions struct { +- ValueSet []InsertTextMode `json:"valueSet"` +-} - -- // index of cursor in comment text -- cursorOffset := int(c.pos - cursorComment.Pos()) -- start, end := cursorOffset, cursorOffset -- for start > 0 && isValidIdentifierChar(cursorComment.Text[start-1]) { -- start-- -- } -- for end < len(cursorComment.Text) && isValidIdentifierChar(cursorComment.Text[end]) { -- end++ -- } +-// @since 3.18.0 +-// @proposed +-type ClientCompletionItemOptions struct { +- // Client supports snippets as insert text. +- // +- // A snippet can define tab stops and placeholders with `$1`, `$2` +- // and `${3:foo}`. `$0` defines the final tab stop, it defaults to +- // the end of the snippet. Placeholders with equal identifiers are linked, +- // that is typing in one will update others too. +- SnippetSupport bool `json:"snippetSupport,omitempty"` +- // Client supports commit characters on a completion item. +- CommitCharactersSupport bool `json:"commitCharactersSupport,omitempty"` +- // Client supports the following content formats for the documentation +- // property. The order describes the preferred format of the client. +- DocumentationFormat []MarkupKind `json:"documentationFormat,omitempty"` +- // Client supports the deprecated property on a completion item. +- DeprecatedSupport bool `json:"deprecatedSupport,omitempty"` +- // Client supports the preselect property on a completion item. +- PreselectSupport bool `json:"preselectSupport,omitempty"` +- // Client supports the tag property on a completion item. Clients supporting +- // tags have to handle unknown tags gracefully. Clients especially need to +- // preserve unknown tags when sending a completion item back to the server in +- // a resolve call. +- // +- // @since 3.15.0 +- TagSupport *CompletionItemTagOptions `json:"tagSupport,omitempty"` +- // Client support insert replace edit to control different behavior if a +- // completion item is inserted in the text or should replace text. +- // +- // @since 3.16.0 +- InsertReplaceSupport bool `json:"insertReplaceSupport,omitempty"` +- // Indicates which properties a client can resolve lazily on a completion +- // item. Before version 3.16.0 only the predefined properties `documentation` +- // and `details` could be resolved lazily. +- // +- // @since 3.16.0 +- ResolveSupport *ClientCompletionItemResolveOptions `json:"resolveSupport,omitempty"` +- // The client supports the `insertTextMode` property on +- // a completion item to override the whitespace handling mode +- // as defined by the client (see `insertTextMode`). +- // +- // @since 3.16.0 +- InsertTextModeSupport *ClientCompletionItemInsertTextModeOptions `json:"insertTextModeSupport,omitempty"` +- // The client has support for completion item label +- // details (see also `CompletionItemLabelDetails`). +- // +- // @since 3.17.0 +- LabelDetailsSupport bool `json:"labelDetailsSupport,omitempty"` +-} - -- c.surrounding = &Selection{ -- content: cursorComment.Text[start:end], -- cursor: c.pos, -- tokFile: c.tokFile, -- start: token.Pos(int(cursorComment.Slash) + start), -- end: token.Pos(int(cursorComment.Slash) + end), -- mapper: c.mapper, -- } -- c.setMatcherFromPrefix(c.surrounding.Prefix()) +-// @since 3.18.0 +-// @proposed +-type ClientCompletionItemOptionsKind struct { +- // The completion item kind values the client supports. When this +- // property exists the client also guarantees that it will +- // handle values outside its set gracefully and falls back +- // to a default value when unknown. +- // +- // If this property is not present the client only supports +- // the completion items kinds from `Text` to `Reference` as defined in +- // the initial version of the protocol. +- ValueSet []CompletionItemKind `json:"valueSet,omitempty"` -} - --// isValidIdentifierChar returns true if a byte is a valid go identifier --// character, i.e. unicode letter or digit or underscore. --func isValidIdentifierChar(char byte) bool { -- charRune := rune(char) -- return unicode.In(charRune, unicode.Letter, unicode.Digit) || char == '_' +-// @since 3.18.0 +-// @proposed +-type ClientCompletionItemResolveOptions struct { +- // The properties that a client can resolve lazily. +- Properties []string `json:"properties"` -} - --// adds struct fields, interface methods, function declaration fields to completion --func (c *completer) addFieldItems(ctx context.Context, fields *ast.FieldList) { -- if fields == nil { -- return -- } +-// @since 3.18.0 +-// @proposed +-type ClientDiagnosticsTagOptions struct { +- // The tags supported by the client. +- ValueSet []DiagnosticTag `json:"valueSet"` +-} - -- cursor := c.surrounding.cursor -- for _, field := range fields.List { -- for _, name := range field.Names { -- if name.String() == "_" { -- continue -- } -- obj := c.pkg.GetTypesInfo().ObjectOf(name) -- if obj == nil { -- continue -- } +-// @since 3.18.0 +-// @proposed +-type ClientFoldingRangeKindOptions struct { +- // The folding range kind values the client supports. When this +- // property exists the client also guarantees that it will +- // handle values outside its set gracefully and falls back +- // to a default value when unknown. +- ValueSet []FoldingRangeKind `json:"valueSet,omitempty"` +-} - -- // if we're in a field comment/doc, score that field as more relevant -- score := stdScore -- if field.Comment != nil && field.Comment.Pos() <= cursor && cursor <= field.Comment.End() { -- score = highScore -- } else if field.Doc != nil && field.Doc.Pos() <= cursor && cursor <= field.Doc.End() { -- score = highScore -- } +-// @since 3.18.0 +-// @proposed +-type ClientFoldingRangeOptions struct { +- // If set, the client signals that it supports setting collapsedText on +- // folding ranges to display custom labels instead of the default text. +- // +- // @since 3.17.0 +- CollapsedText bool `json:"collapsedText,omitempty"` +-} - -- c.deepState.enqueue(candidate{obj: obj, score: score}) -- } -- } +-// Information about the client +-// +-// @since 3.15.0 +-// @since 3.18.0 ClientInfo type name added. +-// @proposed +-type ClientInfo struct { +- // The name of the client as defined by the client. +- Name string `json:"name"` +- // The client's version as defined by the client. +- Version string `json:"version,omitempty"` -} - --func (c *completer) wantStructFieldCompletions() bool { -- clInfo := c.enclosingCompositeLiteral -- if clInfo == nil { -- return false -- } +-// @since 3.18.0 +-// @proposed +-type ClientInlayHintResolveOptions struct { +- // The properties that a client can resolve lazily. +- Properties []string `json:"properties"` +-} - -- return clInfo.isStruct() && (clInfo.inKey || clInfo.maybeInFieldName) +-// @since 3.18.0 +-// @proposed +-type ClientSemanticTokensRequestFullDelta struct { +- // The client will send the `textDocument/semanticTokens/full/delta` request if +- // the server provides a corresponding handler. +- Delta bool `json:"delta,omitempty"` -} - --func (c *completer) wantTypeName() bool { -- return !c.completionContext.commentCompletion && c.inference.typeName.wantTypeName +-// @since 3.18.0 +-// @proposed +-type ClientSemanticTokensRequestOptions struct { +- // The client will send the `textDocument/semanticTokens/range` request if +- // the server provides a corresponding handler. +- Range *Or_ClientSemanticTokensRequestOptions_range `json:"range,omitempty"` +- // The client will send the `textDocument/semanticTokens/full` request if +- // the server provides a corresponding handler. +- Full *Or_ClientSemanticTokensRequestOptions_full `json:"full,omitempty"` -} - --// See https://golang.org/issue/36001. Unimported completions are expensive. --const ( -- maxUnimportedPackageNames = 5 -- unimportedMemberTarget = 100 --) +-// @since 3.18.0 +-// @proposed +-type ClientShowMessageActionItemOptions struct { +- // Whether the client supports additional attributes which +- // are preserved and send back to the server in the +- // request's response. +- AdditionalPropertiesSupport bool `json:"additionalPropertiesSupport,omitempty"` +-} - --// selector finds completions for the specified selector expression. --func (c *completer) selector(ctx context.Context, sel *ast.SelectorExpr) error { -- c.inference.objChain = objChain(c.pkg.GetTypesInfo(), sel.X) +-// @since 3.18.0 +-// @proposed +-type ClientSignatureInformationOptions struct { +- // Client supports the following content formats for the documentation +- // property. The order describes the preferred format of the client. +- DocumentationFormat []MarkupKind `json:"documentationFormat,omitempty"` +- // Client capabilities specific to parameter information. +- ParameterInformation *ClientSignatureParameterInformationOptions `json:"parameterInformation,omitempty"` +- // The client supports the `activeParameter` property on `SignatureInformation` +- // literal. +- // +- // @since 3.16.0 +- ActiveParameterSupport bool `json:"activeParameterSupport,omitempty"` +- // The client supports the `activeParameter` property on +- // `SignatureHelp`/`SignatureInformation` being set to `null` to +- // indicate that no parameter should be active. +- // +- // @since 3.18.0 +- // @proposed +- NoActiveParameterSupport bool `json:"noActiveParameterSupport,omitempty"` +-} - -- // True selector? -- if tv, ok := c.pkg.GetTypesInfo().Types[sel.X]; ok { -- c.methodsAndFields(tv.Type, tv.Addressable(), nil, c.deepState.enqueue) -- c.addPostfixSnippetCandidates(ctx, sel) -- return nil -- } +-// @since 3.18.0 +-// @proposed +-type ClientSignatureParameterInformationOptions struct { +- // The client supports processing label offsets instead of a +- // simple label string. +- // +- // @since 3.14.0 +- LabelOffsetSupport bool `json:"labelOffsetSupport,omitempty"` +-} - -- id, ok := sel.X.(*ast.Ident) -- if !ok { -- return nil -- } +-// @since 3.18.0 +-// @proposed +-type ClientSymbolKindOptions struct { +- // The symbol kind values the client supports. When this +- // property exists the client also guarantees that it will +- // handle values outside its set gracefully and falls back +- // to a default value when unknown. +- // +- // If this property is not present the client only supports +- // the symbol kinds from `File` to `Array` as defined in +- // the initial version of the protocol. +- ValueSet []SymbolKind `json:"valueSet,omitempty"` +-} - -- // Treat sel as a qualified identifier. -- var filter func(*source.Metadata) bool -- needImport := false -- if pkgName, ok := c.pkg.GetTypesInfo().Uses[id].(*types.PkgName); ok { -- // Qualified identifier with import declaration. -- imp := pkgName.Imported() +-// @since 3.18.0 +-// @proposed +-type ClientSymbolResolveOptions struct { +- // The properties that a client can resolve lazily. Usually +- // `location.range` +- Properties []string `json:"properties"` +-} - -- // Known direct dependency? Expand using type information. -- if _, ok := c.pkg.Metadata().DepsByPkgPath[source.PackagePath(imp.Path())]; ok { -- c.packageMembers(imp, stdScore, nil, c.deepState.enqueue) -- return nil -- } +-// @since 3.18.0 +-// @proposed +-type ClientSymbolTagOptions struct { +- // The tags supported by the client. +- ValueSet []SymbolTag `json:"valueSet"` +-} - -- // Imported declaration with missing type information. -- // Fall through to shallow completion of unimported package members. -- // Match candidate packages by path. -- filter = func(m *source.Metadata) bool { -- return strings.TrimPrefix(string(m.PkgPath), "vendor/") == imp.Path() -- } -- } else { -- // Qualified identifier without import declaration. -- // Match candidate packages by name. -- filter = func(m *source.Metadata) bool { -- return string(m.Name) == id.Name -- } -- needImport = true -- } +-// A code action represents a change that can be performed in code, e.g. to fix a problem or +-// to refactor code. +-// +-// A CodeAction must set either `edit` and/or a `command`. If both are supplied, the `edit` is applied first, then the `command` is executed. +-type CodeAction struct { +- // A short, human-readable, title for this code action. +- Title string `json:"title"` +- // The kind of the code action. +- // +- // Used to filter code actions. +- Kind CodeActionKind `json:"kind,omitempty"` +- // The diagnostics that this code action resolves. +- Diagnostics []Diagnostic `json:"diagnostics,omitempty"` +- // Marks this as a preferred action. Preferred actions are used by the `auto fix` command and can be targeted +- // by keybindings. +- // +- // A quick fix should be marked preferred if it properly addresses the underlying error. +- // A refactoring should be marked preferred if it is the most reasonable choice of actions to take. +- // +- // @since 3.15.0 +- IsPreferred bool `json:"isPreferred,omitempty"` +- // Marks that the code action cannot currently be applied. +- // +- // Clients should follow the following guidelines regarding disabled code actions: +- // +- // - Disabled code actions are not shown in automatic [lightbulbs](https://code.visualstudio.com/docs/editor/editingevolved#_code-action) +- // code action menus. +- // +- // - Disabled actions are shown as faded out in the code action menu when the user requests a more specific type +- // of code action, such as refactorings. +- // +- // - If the user has a [keybinding](https://code.visualstudio.com/docs/editor/refactoring#_keybindings-for-code-actions) +- // that auto applies a code action and only disabled code actions are returned, the client should show the user an +- // error message with `reason` in the editor. +- // +- // @since 3.16.0 +- Disabled *CodeActionDisabled `json:"disabled,omitempty"` +- // The workspace edit this code action performs. +- Edit *WorkspaceEdit `json:"edit,omitempty"` +- // A command this code action executes. If a code action +- // provides an edit and a command, first the edit is +- // executed and then the command. +- Command *Command `json:"command,omitempty"` +- // A data entry field that is preserved on a code action between +- // a `textDocument/codeAction` and a `codeAction/resolve` request. +- // +- // @since 3.16.0 +- Data *json.RawMessage `json:"data,omitempty"` +-} - -- // Search unimported packages. -- if !c.opts.unimported { -- return nil // feature disabled -- } +-// The Client Capabilities of a {@link CodeActionRequest}. +-type CodeActionClientCapabilities struct { +- // Whether code action supports dynamic registration. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +- // The client support code action literals of type `CodeAction` as a valid +- // response of the `textDocument/codeAction` request. If the property is not +- // set the request can only return `Command` literals. +- // +- // @since 3.8.0 +- CodeActionLiteralSupport ClientCodeActionLiteralOptions `json:"codeActionLiteralSupport,omitempty"` +- // Whether code action supports the `isPreferred` property. +- // +- // @since 3.15.0 +- IsPreferredSupport bool `json:"isPreferredSupport,omitempty"` +- // Whether code action supports the `disabled` property. +- // +- // @since 3.16.0 +- DisabledSupport bool `json:"disabledSupport,omitempty"` +- // Whether code action supports the `data` property which is +- // preserved between a `textDocument/codeAction` and a +- // `codeAction/resolve` request. +- // +- // @since 3.16.0 +- DataSupport bool `json:"dataSupport,omitempty"` +- // Whether the client supports resolving additional code action +- // properties via a separate `codeAction/resolve` request. +- // +- // @since 3.16.0 +- ResolveSupport *ClientCodeActionResolveOptions `json:"resolveSupport,omitempty"` +- // Whether the client honors the change annotations in +- // text edits and resource operations returned via the +- // `CodeAction#edit` property by for example presenting +- // the workspace edit in the user interface and asking +- // for confirmation. +- // +- // @since 3.16.0 +- HonorsChangeAnnotations bool `json:"honorsChangeAnnotations,omitempty"` +- // Whether the client supports documentation for a class of +- // code actions. +- // +- // @since 3.18.0 +- // @proposed +- DocumentationSupport bool `json:"documentationSupport,omitempty"` +-} - -- // The deep completion algorithm is exceedingly complex and -- // deeply coupled to the now obsolete notions that all -- // token.Pos values can be interpreted by as a single FileSet -- // belonging to the Snapshot and that all types.Object values -- // are canonicalized by a single types.Importer mapping. -- // These invariants are no longer true now that gopls uses -- // an incremental approach, parsing and type-checking each -- // package separately. +-// Contains additional diagnostic information about the context in which +-// a {@link CodeActionProvider.provideCodeActions code action} is run. +-type CodeActionContext struct { +- // An array of diagnostics known on the client side overlapping the range provided to the +- // `textDocument/codeAction` request. They are provided so that the server knows which +- // errors are currently presented to the user for the given range. There is no guarantee +- // that these accurately reflect the error state of the resource. The primary parameter +- // to compute code actions is the provided range. +- Diagnostics []Diagnostic `json:"diagnostics"` +- // Requested kind of actions to return. - // -- // Consequently, completion of symbols defined in packages that -- // are not currently imported by the query file cannot use the -- // deep completion machinery which is based on type information. -- // Instead it must use only syntax information from a quick -- // parse of top-level declarations (but not function bodies). +- // Actions not of this kind are filtered out by the client before being shown. So servers +- // can omit computing them. +- Only []CodeActionKind `json:"only,omitempty"` +- // The reason why code actions were requested. - // -- // TODO(adonovan): rewrite the deep completion machinery to -- // not assume global Pos/Object realms and then use export -- // data instead of the quick parse approach taken here. +- // @since 3.17.0 +- TriggerKind *CodeActionTriggerKind `json:"triggerKind,omitempty"` +-} - -- // First, we search among packages in the forward transitive -- // closure of the workspace. -- // We'll use a fast parse to extract package members -- // from those that match the name/path criterion. -- all, err := c.snapshot.AllMetadata(ctx) -- if err != nil { -- return err -- } -- known := make(map[source.PackagePath]*source.Metadata) -- for _, m := range all { -- if m.Name == "main" { -- continue // not importable -- } -- if m.IsIntermediateTestVariant() { -- continue -- } -- // The only test variant we admit is "p [p.test]" -- // when we are completing within "p_test [p.test]", -- // as in that case we would like to offer completions -- // of the test variants' additional symbols. -- if m.ForTest != "" && c.pkg.Metadata().PkgPath != m.ForTest+"_test" { -- continue -- } -- if !filter(m) { -- continue -- } -- // Prefer previous entry unless this one is its test variant. -- if m.ForTest != "" || known[m.PkgPath] == nil { -- known[m.PkgPath] = m -- } -- } +-// Captures why the code action is currently disabled. +-// +-// @since 3.18.0 +-// @proposed +-type CodeActionDisabled struct { +- // Human readable description of why the code action is currently disabled. +- // +- // This is displayed in the code actions UI. +- Reason string `json:"reason"` +-} - -- paths := make([]string, 0, len(known)) -- for path := range known { -- paths = append(paths, string(path)) -- } +-// A set of predefined code action kinds +-type CodeActionKind string - -- // Rank import paths as goimports would. -- var relevances map[string]float64 -- if len(paths) > 0 { -- if err := c.snapshot.RunProcessEnvFunc(ctx, func(ctx context.Context, opts *imports.Options) error { -- var err error -- relevances, err = imports.ScoreImportPaths(ctx, opts.Env, paths) -- return err -- }); err != nil { -- return err -- } -- sort.Slice(paths, func(i, j int) bool { -- return relevances[paths[i]] > relevances[paths[j]] -- }) -- } +-// Documentation for a class of code actions. +-// +-// @since 3.18.0 +-// @proposed +-type CodeActionKindDocumentation struct { +- // The kind of the code action being documented. +- // +- // If the kind is generic, such as `CodeActionKind.Refactor`, the documentation will be shown whenever any +- // refactorings are returned. If the kind if more specific, such as `CodeActionKind.RefactorExtract`, the +- // documentation will only be shown when extract refactoring code actions are returned. +- Kind CodeActionKind `json:"kind"` +- // Command that is ued to display the documentation to the user. +- // +- // The title of this documentation code action is taken from {@linkcode Command.title} +- Command Command `json:"command"` +-} - -- // quickParse does a quick parse of a single file of package m, -- // extracts exported package members and adds candidates to c.items. -- // TODO(rfindley): synchronizing access to c here does not feel right. -- // Consider adding a concurrency-safe API for completer. -- var cMu sync.Mutex // guards c.items and c.matcher -- var enough int32 // atomic bool -- quickParse := func(uri span.URI, m *source.Metadata) error { -- if atomic.LoadInt32(&enough) != 0 { -- return nil -- } +-// Provider options for a {@link CodeActionRequest}. +-type CodeActionOptions struct { +- // CodeActionKinds that this server may return. +- // +- // The list of kinds may be generic, such as `CodeActionKind.Refactor`, or the server +- // may list out every specific kind they provide. +- CodeActionKinds []CodeActionKind `json:"codeActionKinds,omitempty"` +- // Static documentation for a class of code actions. +- // +- // Documentation from the provider should be shown in the code actions menu if either: +- // +- // +- // - Code actions of `kind` are requested by the editor. In this case, the editor will show the documentation that +- // most closely matches the requested code action kind. For example, if a provider has documentation for +- // both `Refactor` and `RefactorExtract`, when the user requests code actions for `RefactorExtract`, +- // the editor will use the documentation for `RefactorExtract` instead of the documentation for `Refactor`. +- // +- // +- // - Any code actions of `kind` are returned by the provider. +- // +- // At most one documentation entry should be shown per provider. +- // +- // @since 3.18.0 +- // @proposed +- Documentation []CodeActionKindDocumentation `json:"documentation,omitempty"` +- // The server provides support to resolve additional +- // information for a code action. +- // +- // @since 3.16.0 +- ResolveProvider bool `json:"resolveProvider,omitempty"` +- WorkDoneProgressOptions +-} - -- fh, err := c.snapshot.ReadFile(ctx, uri) -- if err != nil { -- return err -- } -- content, err := fh.Content() -- if err != nil { -- return err -- } -- path := string(m.PkgPath) -- forEachPackageMember(content, func(tok token.Token, id *ast.Ident, fn *ast.FuncDecl) { -- if atomic.LoadInt32(&enough) != 0 { -- return -- } +-// The parameters of a {@link CodeActionRequest}. +-type CodeActionParams struct { +- // The document in which the command was invoked. +- TextDocument TextDocumentIdentifier `json:"textDocument"` +- // The range for which the command was invoked. +- Range Range `json:"range"` +- // Context carrying additional information. +- Context CodeActionContext `json:"context"` +- WorkDoneProgressParams +- PartialResultParams +-} - -- if !id.IsExported() { -- return -- } +-// Registration options for a {@link CodeActionRequest}. +-type CodeActionRegistrationOptions struct { +- TextDocumentRegistrationOptions +- CodeActionOptions +-} - -- cMu.Lock() -- score := c.matcher.Score(id.Name) -- cMu.Unlock() +-// The reason why code actions were requested. +-// +-// @since 3.17.0 +-type CodeActionTriggerKind uint32 - -- if sel.Sel.Name != "_" && score == 0 { -- return // not a match; avoid constructing the completion item below -- } +-// Structure to capture a description for an error code. +-// +-// @since 3.16.0 +-type CodeDescription struct { +- // An URI to open with more information about the diagnostic error. +- Href URI `json:"href"` +-} - -- // The only detail is the kind and package: `var (from "example.com/foo")` -- // TODO(adonovan): pretty-print FuncDecl.FuncType or TypeSpec.Type? -- // TODO(adonovan): should this score consider the actual c.matcher.Score -- // of the item? How does this compare with the deepState.enqueue path? -- item := CompletionItem{ -- Label: id.Name, -- Detail: fmt.Sprintf("%s (from %q)", strings.ToLower(tok.String()), m.PkgPath), -- InsertText: id.Name, -- Score: float64(score) * unimportedScore(relevances[path]), -- } -- switch tok { -- case token.FUNC: -- item.Kind = protocol.FunctionCompletion -- case token.VAR: -- item.Kind = protocol.VariableCompletion -- case token.CONST: -- item.Kind = protocol.ConstantCompletion -- case token.TYPE: -- // Without types, we can't distinguish Class from Interface. -- item.Kind = protocol.ClassCompletion -- } +-// A code lens represents a {@link Command command} that should be shown along with +-// source text, like the number of references, a way to run tests, etc. +-// +-// A code lens is _unresolved_ when no command is associated to it. For performance +-// reasons the creation of a code lens and resolving should be done in two stages. +-type CodeLens struct { +- // The range in which this code lens is valid. Should only span a single line. +- Range Range `json:"range"` +- // The command this code lens represents. +- Command *Command `json:"command,omitempty"` +- // A data entry field that is preserved on a code lens item between +- // a {@link CodeLensRequest} and a {@link CodeLensResolveRequest} +- Data interface{} `json:"data,omitempty"` +-} - -- if needImport { -- imp := &importInfo{importPath: path} -- if imports.ImportPathToAssumedName(path) != string(m.Name) { -- imp.name = string(m.Name) -- } -- item.AdditionalTextEdits, _ = c.importEdits(imp) -- } +-// The client capabilities of a {@link CodeLensRequest}. +-type CodeLensClientCapabilities struct { +- // Whether code lens supports dynamic registration. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +-} - -- // For functions, add a parameter snippet. -- if fn != nil { -- paramList := func(list *ast.FieldList) []string { -- var params []string -- if list != nil { -- var cfg printer.Config // slight overkill -- param := func(name string, typ ast.Expr) { -- var buf strings.Builder -- buf.WriteString(name) -- buf.WriteByte(' ') -- cfg.Fprint(&buf, token.NewFileSet(), typ) -- params = append(params, buf.String()) -- } +-// Code Lens provider options of a {@link CodeLensRequest}. +-type CodeLensOptions struct { +- // Code lens has a resolve provider as well. +- ResolveProvider bool `json:"resolveProvider,omitempty"` +- WorkDoneProgressOptions +-} - -- for _, field := range list.List { -- if field.Names != nil { -- for _, name := range field.Names { -- param(name.Name, field.Type) -- } -- } else { -- param("_", field.Type) -- } -- } -- } -- return params -- } +-// The parameters of a {@link CodeLensRequest}. +-type CodeLensParams struct { +- // The document to request code lens for. +- TextDocument TextDocumentIdentifier `json:"textDocument"` +- WorkDoneProgressParams +- PartialResultParams +-} - -- tparams := paramList(fn.Type.TypeParams) -- params := paramList(fn.Type.Params) -- var sn snippet.Builder -- c.functionCallSnippet(id.Name, tparams, params, &sn) -- item.snippet = &sn -- } +-// Registration options for a {@link CodeLensRequest}. +-type CodeLensRegistrationOptions struct { +- TextDocumentRegistrationOptions +- CodeLensOptions +-} - -- cMu.Lock() -- c.items = append(c.items, item) -- if len(c.items) >= unimportedMemberTarget { -- atomic.StoreInt32(&enough, 1) -- } -- cMu.Unlock() -- }) -- return nil -- } +-// @since 3.16.0 +-type CodeLensWorkspaceClientCapabilities struct { +- // Whether the client implementation supports a refresh request sent from the +- // server to the client. +- // +- // Note that this event is global and will force the client to refresh all +- // code lenses currently shown. It should be used with absolute care and is +- // useful for situation where a server for example detect a project wide +- // change that requires such a calculation. +- RefreshSupport bool `json:"refreshSupport,omitempty"` +-} - -- // Extract the package-level candidates using a quick parse. -- var g errgroup.Group -- for _, path := range paths { -- m := known[source.PackagePath(path)] -- for _, uri := range m.CompiledGoFiles { -- uri := uri -- g.Go(func() error { -- return quickParse(uri, m) -- }) -- } -- } -- if err := g.Wait(); err != nil { -- return err -- } +-// Represents a color in RGBA space. +-type Color struct { +- // The red component of this color in the range [0-1]. +- Red float64 `json:"red"` +- // The green component of this color in the range [0-1]. +- Green float64 `json:"green"` +- // The blue component of this color in the range [0-1]. +- Blue float64 `json:"blue"` +- // The alpha component of this color in the range [0-1]. +- Alpha float64 `json:"alpha"` +-} - -- // In addition, we search in the module cache using goimports. -- ctx, cancel := context.WithCancel(ctx) -- var mu sync.Mutex -- add := func(pkgExport imports.PackageExport) { -- if ignoreUnimportedCompletion(pkgExport.Fix) { -- return -- } +-// Represents a color range from a document. +-type ColorInformation struct { +- // The range in the document where this color appears. +- Range Range `json:"range"` +- // The actual color value for this color range. +- Color Color `json:"color"` +-} +-type ColorPresentation struct { +- // The label of this color presentation. It will be shown on the color +- // picker header. By default this is also the text that is inserted when selecting +- // this color presentation. +- Label string `json:"label"` +- // An {@link TextEdit edit} which is applied to a document when selecting +- // this presentation for the color. When `falsy` the {@link ColorPresentation.label label} +- // is used. +- TextEdit *TextEdit `json:"textEdit,omitempty"` +- // An optional array of additional {@link TextEdit text edits} that are applied when +- // selecting this color presentation. Edits must not overlap with the main {@link ColorPresentation.textEdit edit} nor with themselves. +- AdditionalTextEdits []TextEdit `json:"additionalTextEdits,omitempty"` +-} - -- mu.Lock() -- defer mu.Unlock() -- // TODO(adonovan): what if the actual package has a vendor/ prefix? -- if _, ok := known[source.PackagePath(pkgExport.Fix.StmtInfo.ImportPath)]; ok { -- return // We got this one above. -- } +-// Parameters for a {@link ColorPresentationRequest}. +-type ColorPresentationParams struct { +- // The text document. +- TextDocument TextDocumentIdentifier `json:"textDocument"` +- // The color to request presentations for. +- Color Color `json:"color"` +- // The range where the color would be inserted. Serves as a context. +- Range Range `json:"range"` +- WorkDoneProgressParams +- PartialResultParams +-} - -- // Continue with untyped proposals. -- pkg := types.NewPackage(pkgExport.Fix.StmtInfo.ImportPath, pkgExport.Fix.IdentName) -- for _, export := range pkgExport.Exports { -- score := unimportedScore(pkgExport.Fix.Relevance) -- c.deepState.enqueue(candidate{ -- obj: types.NewVar(0, pkg, export, nil), -- score: score, -- imp: &importInfo{ -- importPath: pkgExport.Fix.StmtInfo.ImportPath, -- name: pkgExport.Fix.StmtInfo.Name, -- }, -- }) -- } -- if len(c.items) >= unimportedMemberTarget { -- cancel() -- } -- } +-// Represents a reference to a command. Provides a title which +-// will be used to represent a command in the UI and, optionally, +-// an array of arguments which will be passed to the command handler +-// function when invoked. +-type Command struct { +- // Title of the command, like `save`. +- Title string `json:"title"` +- // An optional tooltip. +- // +- // @since 3.18.0 +- // @proposed +- Tooltip string `json:"tooltip,omitempty"` +- // The identifier of the actual command handler. +- Command string `json:"command"` +- // Arguments that the command handler should be +- // invoked with. +- Arguments []json.RawMessage `json:"arguments,omitempty"` +-} - -- c.completionCallbacks = append(c.completionCallbacks, func(ctx context.Context, opts *imports.Options) error { -- defer cancel() -- return imports.GetPackageExports(ctx, add, id.Name, c.filename, c.pkg.GetTypes().Name(), opts.Env) -- }) -- return nil +-// Completion client capabilities +-type CompletionClientCapabilities struct { +- // Whether completion supports dynamic registration. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +- // The client supports the following `CompletionItem` specific +- // capabilities. +- CompletionItem ClientCompletionItemOptions `json:"completionItem,omitempty"` +- CompletionItemKind *ClientCompletionItemOptionsKind `json:"completionItemKind,omitempty"` +- // Defines how the client handles whitespace and indentation +- // when accepting a completion item that uses multi line +- // text in either `insertText` or `textEdit`. +- // +- // @since 3.17.0 +- InsertTextMode InsertTextMode `json:"insertTextMode,omitempty"` +- // The client supports to send additional context information for a +- // `textDocument/completion` request. +- ContextSupport bool `json:"contextSupport,omitempty"` +- // The client supports the following `CompletionList` specific +- // capabilities. +- // +- // @since 3.17.0 +- CompletionList *CompletionListCapabilities `json:"completionList,omitempty"` -} - --// unimportedScore returns a score for an unimported package that is generally --// lower than other candidates. --func unimportedScore(relevance float64) float64 { -- return (stdScore + .1*relevance) / 2 +-// Contains additional information about the context in which a completion request is triggered. +-type CompletionContext struct { +- // How the completion was triggered. +- TriggerKind CompletionTriggerKind `json:"triggerKind"` +- // The trigger character (a single character) that has trigger code complete. +- // Is undefined if `triggerKind !== CompletionTriggerKind.TriggerCharacter` +- TriggerCharacter string `json:"triggerCharacter,omitempty"` -} - --func (c *completer) packageMembers(pkg *types.Package, score float64, imp *importInfo, cb func(candidate)) { -- scope := pkg.Scope() -- for _, name := range scope.Names() { -- obj := scope.Lookup(name) -- cb(candidate{ -- obj: obj, -- score: score, -- imp: imp, -- addressable: isVar(obj), -- }) -- } +-// A completion item represents a text snippet that is +-// proposed to complete text that is being typed. +-type CompletionItem struct { +- // The label of this completion item. +- // +- // The label property is also by default the text that +- // is inserted when selecting this completion. +- // +- // If label details are provided the label itself should +- // be an unqualified name of the completion item. +- Label string `json:"label"` +- // Additional details for the label +- // +- // @since 3.17.0 +- LabelDetails *CompletionItemLabelDetails `json:"labelDetails,omitempty"` +- // The kind of this completion item. Based of the kind +- // an icon is chosen by the editor. +- Kind CompletionItemKind `json:"kind,omitempty"` +- // Tags for this completion item. +- // +- // @since 3.15.0 +- Tags []CompletionItemTag `json:"tags,omitempty"` +- // A human-readable string with additional information +- // about this item, like type or symbol information. +- Detail string `json:"detail,omitempty"` +- // A human-readable string that represents a doc-comment. +- Documentation *Or_CompletionItem_documentation `json:"documentation,omitempty"` +- // Indicates if this item is deprecated. +- // @deprecated Use `tags` instead. +- Deprecated bool `json:"deprecated,omitempty"` +- // Select this item when showing. +- // +- // *Note* that only one completion item can be selected and that the +- // tool / client decides which item that is. The rule is that the *first* +- // item of those that match best is selected. +- Preselect bool `json:"preselect,omitempty"` +- // A string that should be used when comparing this item +- // with other items. When `falsy` the {@link CompletionItem.label label} +- // is used. +- SortText string `json:"sortText,omitempty"` +- // A string that should be used when filtering a set of +- // completion items. When `falsy` the {@link CompletionItem.label label} +- // is used. +- FilterText string `json:"filterText,omitempty"` +- // A string that should be inserted into a document when selecting +- // this completion. When `falsy` the {@link CompletionItem.label label} +- // is used. +- // +- // The `insertText` is subject to interpretation by the client side. +- // Some tools might not take the string literally. For example +- // VS Code when code complete is requested in this example +- // `con` and a completion item with an `insertText` of +- // `console` is provided it will only insert `sole`. Therefore it is +- // recommended to use `textEdit` instead since it avoids additional client +- // side interpretation. +- InsertText string `json:"insertText,omitempty"` +- // The format of the insert text. The format applies to both the +- // `insertText` property and the `newText` property of a provided +- // `textEdit`. If omitted defaults to `InsertTextFormat.PlainText`. +- // +- // Please note that the insertTextFormat doesn't apply to +- // `additionalTextEdits`. +- InsertTextFormat *InsertTextFormat `json:"insertTextFormat,omitempty"` +- // How whitespace and indentation is handled during completion +- // item insertion. If not provided the clients default value depends on +- // the `textDocument.completion.insertTextMode` client capability. +- // +- // @since 3.16.0 +- InsertTextMode *InsertTextMode `json:"insertTextMode,omitempty"` +- // An {@link TextEdit edit} which is applied to a document when selecting +- // this completion. When an edit is provided the value of +- // {@link CompletionItem.insertText insertText} is ignored. +- // +- // Most editors support two different operations when accepting a completion +- // item. One is to insert a completion text and the other is to replace an +- // existing text with a completion text. Since this can usually not be +- // predetermined by a server it can report both ranges. Clients need to +- // signal support for `InsertReplaceEdits` via the +- // `textDocument.completion.insertReplaceSupport` client capability +- // property. +- // +- // *Note 1:* The text edit's range as well as both ranges from an insert +- // replace edit must be a [single line] and they must contain the position +- // at which completion has been requested. +- // *Note 2:* If an `InsertReplaceEdit` is returned the edit's insert range +- // must be a prefix of the edit's replace range, that means it must be +- // contained and starting at the same position. +- // +- // @since 3.16.0 additional type `InsertReplaceEdit` +- TextEdit *TextEdit `json:"textEdit,omitempty"` +- // The edit text used if the completion item is part of a CompletionList and +- // CompletionList defines an item default for the text edit range. +- // +- // Clients will only honor this property if they opt into completion list +- // item defaults using the capability `completionList.itemDefaults`. +- // +- // If not provided and a list's default range is provided the label +- // property is used as a text. +- // +- // @since 3.17.0 +- TextEditText string `json:"textEditText,omitempty"` +- // An optional array of additional {@link TextEdit text edits} that are applied when +- // selecting this completion. Edits must not overlap (including the same insert position) +- // with the main {@link CompletionItem.textEdit edit} nor with themselves. +- // +- // Additional text edits should be used to change text unrelated to the current cursor position +- // (for example adding an import statement at the top of the file if the completion item will +- // insert an unqualified type). +- AdditionalTextEdits []TextEdit `json:"additionalTextEdits,omitempty"` +- // An optional set of characters that when pressed while this completion is active will accept it first and +- // then type that character. *Note* that all commit characters should have `length=1` and that superfluous +- // characters will be ignored. +- CommitCharacters []string `json:"commitCharacters,omitempty"` +- // An optional {@link Command command} that is executed *after* inserting this completion. *Note* that +- // additional modifications to the current document should be described with the +- // {@link CompletionItem.additionalTextEdits additionalTextEdits}-property. +- Command *Command `json:"command,omitempty"` +- // A data entry field that is preserved on a completion item between a +- // {@link CompletionRequest} and a {@link CompletionResolveRequest}. +- Data interface{} `json:"data,omitempty"` -} - --// ignoreUnimportedCompletion reports whether an unimported completion --// resulting in the given import should be ignored. --func ignoreUnimportedCompletion(fix *imports.ImportFix) bool { -- // golang/go#60062: don't add unimported completion to golang.org/toolchain. -- return fix != nil && strings.HasPrefix(fix.StmtInfo.ImportPath, "golang.org/toolchain") +-// In many cases the items of an actual completion result share the same +-// value for properties like `commitCharacters` or the range of a text +-// edit. A completion list can therefore define item defaults which will +-// be used if a completion item itself doesn't specify the value. +-// +-// If a completion list specifies a default value and a completion item +-// also specifies a corresponding value the one from the item is used. +-// +-// Servers are only allowed to return default values if the client +-// signals support for this via the `completionList.itemDefaults` +-// capability. +-// +-// @since 3.17.0 +-type CompletionItemDefaults struct { +- // A default commit character set. +- // +- // @since 3.17.0 +- CommitCharacters []string `json:"commitCharacters,omitempty"` +- // A default edit range. +- // +- // @since 3.17.0 +- EditRange *Or_CompletionItemDefaults_editRange `json:"editRange,omitempty"` +- // A default insert text format. +- // +- // @since 3.17.0 +- InsertTextFormat *InsertTextFormat `json:"insertTextFormat,omitempty"` +- // A default insert text mode. +- // +- // @since 3.17.0 +- InsertTextMode *InsertTextMode `json:"insertTextMode,omitempty"` +- // A default data value. +- // +- // @since 3.17.0 +- Data interface{} `json:"data,omitempty"` -} - --func (c *completer) methodsAndFields(typ types.Type, addressable bool, imp *importInfo, cb func(candidate)) { -- mset := c.methodSetCache[methodSetKey{typ, addressable}] -- if mset == nil { -- if addressable && !types.IsInterface(typ) && !isPointer(typ) { -- // Add methods of *T, which includes methods with receiver T. -- mset = types.NewMethodSet(types.NewPointer(typ)) -- } else { -- // Add methods of T. -- mset = types.NewMethodSet(typ) -- } -- c.methodSetCache[methodSetKey{typ, addressable}] = mset -- } +-// The kind of a completion entry. +-type CompletionItemKind uint32 - -- if isStarTestingDotF(typ) && addressable { -- // is that a sufficient test? (or is more care needed?) -- if c.fuzz(typ, mset, imp, cb, c.pkg.FileSet()) { -- return -- } -- } +-// Additional details for a completion item label. +-// +-// @since 3.17.0 +-type CompletionItemLabelDetails struct { +- // An optional string which is rendered less prominently directly after {@link CompletionItem.label label}, +- // without any spacing. Should be used for function signatures and type annotations. +- Detail string `json:"detail,omitempty"` +- // An optional string which is rendered less prominently after {@link CompletionItem.detail}. Should be used +- // for fully qualified names and file paths. +- Description string `json:"description,omitempty"` +-} - -- for i := 0; i < mset.Len(); i++ { -- cb(candidate{ -- obj: mset.At(i).Obj(), -- score: stdScore, -- imp: imp, -- addressable: addressable || isPointer(typ), -- }) -- } +-// Completion item tags are extra annotations that tweak the rendering of a completion +-// item. +-// +-// @since 3.15.0 +-type CompletionItemTag uint32 - -- // Add fields of T. -- eachField(typ, func(v *types.Var) { -- cb(candidate{ -- obj: v, -- score: stdScore - 0.01, -- imp: imp, -- addressable: addressable || isPointer(typ), -- }) -- }) +-// @since 3.18.0 +-// @proposed +-type CompletionItemTagOptions struct { +- // The tags supported by the client. +- ValueSet []CompletionItemTag `json:"valueSet"` -} - --// isStarTestingDotF reports whether typ is *testing.F. --func isStarTestingDotF(typ types.Type) bool { -- ptr, _ := typ.(*types.Pointer) -- if ptr == nil { -- return false -- } -- named, _ := ptr.Elem().(*types.Named) -- if named == nil { -- return false -- } -- obj := named.Obj() -- // obj.Pkg is nil for the error type. -- return obj != nil && obj.Pkg() != nil && obj.Pkg().Path() == "testing" && obj.Name() == "F" +-// Represents a collection of {@link CompletionItem completion items} to be presented +-// in the editor. +-type CompletionList struct { +- // This list it not complete. Further typing results in recomputing this list. +- // +- // Recomputed lists have all their items replaced (not appended) in the +- // incomplete completion sessions. +- IsIncomplete bool `json:"isIncomplete"` +- // In many cases the items of an actual completion result share the same +- // value for properties like `commitCharacters` or the range of a text +- // edit. A completion list can therefore define item defaults which will +- // be used if a completion item itself doesn't specify the value. +- // +- // If a completion list specifies a default value and a completion item +- // also specifies a corresponding value the one from the item is used. +- // +- // Servers are only allowed to return default values if the client +- // signals support for this via the `completionList.itemDefaults` +- // capability. +- // +- // @since 3.17.0 +- ItemDefaults *CompletionItemDefaults `json:"itemDefaults,omitempty"` +- // The completion items. +- Items []CompletionItem `json:"items"` -} - --// lexical finds completions in the lexical environment. --func (c *completer) lexical(ctx context.Context) error { -- var ( -- builtinIota = types.Universe.Lookup("iota") -- builtinNil = types.Universe.Lookup("nil") +-// The client supports the following `CompletionList` specific +-// capabilities. +-// +-// @since 3.17.0 +-type CompletionListCapabilities struct { +- // The client supports the following itemDefaults on +- // a completion list. +- // +- // The value lists the supported property names of the +- // `CompletionList.itemDefaults` object. If omitted +- // no properties are supported. +- // +- // @since 3.17.0 +- ItemDefaults []string `json:"itemDefaults,omitempty"` +-} - -- // TODO(rfindley): only allow "comparable" where it is valid (in constraint -- // position or embedded in interface declarations). -- // builtinComparable = types.Universe.Lookup("comparable") -- ) +-// Completion options. +-type CompletionOptions struct { +- // Most tools trigger completion request automatically without explicitly requesting +- // it using a keyboard shortcut (e.g. Ctrl+Space). Typically they do so when the user +- // starts to type an identifier. For example if the user types `c` in a JavaScript file +- // code complete will automatically pop up present `console` besides others as a +- // completion item. Characters that make up identifiers don't need to be listed here. +- // +- // If code complete should automatically be trigger on characters not being valid inside +- // an identifier (for example `.` in JavaScript) list them in `triggerCharacters`. +- TriggerCharacters []string `json:"triggerCharacters,omitempty"` +- // The list of all possible characters that commit a completion. This field can be used +- // if clients don't support individual commit characters per completion item. See +- // `ClientCapabilities.textDocument.completion.completionItem.commitCharactersSupport` +- // +- // If a server provides both `allCommitCharacters` and commit characters on an individual +- // completion item the ones on the completion item win. +- // +- // @since 3.2.0 +- AllCommitCharacters []string `json:"allCommitCharacters,omitempty"` +- // The server provides support to resolve additional +- // information for a completion item. +- ResolveProvider bool `json:"resolveProvider,omitempty"` +- // The server supports the following `CompletionItem` specific +- // capabilities. +- // +- // @since 3.17.0 +- CompletionItem *ServerCompletionItemOptions `json:"completionItem,omitempty"` +- WorkDoneProgressOptions +-} - -- // Track seen variables to avoid showing completions for shadowed variables. -- // This works since we look at scopes from innermost to outermost. -- seen := make(map[string]struct{}) +-// Completion parameters +-type CompletionParams struct { +- // The completion context. This is only available it the client specifies +- // to send this using the client capability `textDocument.completion.contextSupport === true` +- Context CompletionContext `json:"context,omitempty"` +- TextDocumentPositionParams +- WorkDoneProgressParams +- PartialResultParams +-} - -- // Process scopes innermost first. -- for i, scope := range c.scopes { -- if scope == nil { -- continue -- } +-// Registration options for a {@link CompletionRequest}. +-type CompletionRegistrationOptions struct { +- TextDocumentRegistrationOptions +- CompletionOptions +-} - -- Names: -- for _, name := range scope.Names() { -- declScope, obj := scope.LookupParent(name, c.pos) -- if declScope != scope { -- continue // Name was declared in some enclosing scope, or not at all. -- } +-// How a completion was triggered +-type CompletionTriggerKind uint32 +-type ConfigurationItem struct { +- // The scope to get the configuration section for. +- ScopeURI *URI `json:"scopeUri,omitempty"` +- // The configuration section asked for. +- Section string `json:"section,omitempty"` +-} - -- // If obj's type is invalid, find the AST node that defines the lexical block -- // containing the declaration of obj. Don't resolve types for packages. -- if !isPkgName(obj) && !typeIsValid(obj.Type()) { -- // Match the scope to its ast.Node. If the scope is the package scope, -- // use the *ast.File as the starting node. -- var node ast.Node -- if i < len(c.path) { -- node = c.path[i] -- } else if i == len(c.path) { // use the *ast.File for package scope -- node = c.path[i-1] -- } -- if node != nil { -- if resolved := resolveInvalid(c.pkg.FileSet(), obj, node, c.pkg.GetTypesInfo()); resolved != nil { -- obj = resolved -- } -- } -- } +-// The parameters of a configuration request. +-type ConfigurationParams struct { +- Items []ConfigurationItem `json:"items"` +-} - -- // Don't use LHS of decl in RHS. -- for _, ident := range enclosingDeclLHS(c.path) { -- if obj.Pos() == ident.Pos() { -- continue Names -- } -- } +-// Create file operation. +-type CreateFile struct { +- // A create +- Kind string `json:"kind"` +- // The resource to create. +- URI DocumentURI `json:"uri"` +- // Additional options +- Options *CreateFileOptions `json:"options,omitempty"` +- ResourceOperation +-} - -- // Don't suggest "iota" outside of const decls. -- if obj == builtinIota && !c.inConstDecl() { -- continue -- } +-// Options to create a file. +-type CreateFileOptions struct { +- // Overwrite existing file. Overwrite wins over `ignoreIfExists` +- Overwrite bool `json:"overwrite,omitempty"` +- // Ignore if exists. +- IgnoreIfExists bool `json:"ignoreIfExists,omitempty"` +-} - -- // Rank outer scopes lower than inner. -- score := stdScore * math.Pow(.99, float64(i)) +-// The parameters sent in notifications/requests for user-initiated creation of +-// files. +-// +-// @since 3.16.0 +-type CreateFilesParams struct { +- // An array of all files/folders created in this operation. +- Files []FileCreate `json:"files"` +-} - -- // Dowrank "nil" a bit so it is ranked below more interesting candidates. -- if obj == builtinNil { -- score /= 2 -- } +-// The declaration of a symbol representation as one or many {@link Location locations}. +-type Declaration = []Location // (alias) +-// @since 3.14.0 +-type DeclarationClientCapabilities struct { +- // Whether declaration supports dynamic registration. If this is set to `true` +- // the client supports the new `DeclarationRegistrationOptions` return value +- // for the corresponding server capability as well. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +- // The client supports additional metadata in the form of declaration links. +- LinkSupport bool `json:"linkSupport,omitempty"` +-} - -- // If we haven't already added a candidate for an object with this name. -- if _, ok := seen[obj.Name()]; !ok { -- seen[obj.Name()] = struct{}{} -- c.deepState.enqueue(candidate{ -- obj: obj, -- score: score, -- addressable: isVar(obj), -- }) -- } -- } -- } +-// Information about where a symbol is declared. +-// +-// Provides additional metadata over normal {@link Location location} declarations, including the range of +-// the declaring symbol. +-// +-// Servers should prefer returning `DeclarationLink` over `Declaration` if supported +-// by the client. +-type DeclarationLink = LocationLink // (alias) +-type DeclarationOptions struct { +- WorkDoneProgressOptions +-} +-type DeclarationParams struct { +- TextDocumentPositionParams +- WorkDoneProgressParams +- PartialResultParams +-} +-type DeclarationRegistrationOptions struct { +- DeclarationOptions +- TextDocumentRegistrationOptions +- StaticRegistrationOptions +-} - -- if c.inference.objType != nil { -- if named, _ := source.Deref(c.inference.objType).(*types.Named); named != nil { -- // If we expected a named type, check the type's package for -- // completion items. This is useful when the current file hasn't -- // imported the type's package yet. +-// The definition of a symbol represented as one or many {@link Location locations}. +-// For most programming languages there is only one location at which a symbol is +-// defined. +-// +-// Servers should prefer returning `DefinitionLink` over `Definition` if supported +-// by the client. +-type Definition = Or_Definition // (alias) +-// Client Capabilities for a {@link DefinitionRequest}. +-type DefinitionClientCapabilities struct { +- // Whether definition supports dynamic registration. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +- // The client supports additional metadata in the form of definition links. +- // +- // @since 3.14.0 +- LinkSupport bool `json:"linkSupport,omitempty"` +-} - -- if named.Obj() != nil && named.Obj().Pkg() != nil { -- pkg := named.Obj().Pkg() +-// Information about where a symbol is defined. +-// +-// Provides additional metadata over normal {@link Location location} definitions, including the range of +-// the defining symbol +-type DefinitionLink = LocationLink // (alias) +-// Server Capabilities for a {@link DefinitionRequest}. +-type DefinitionOptions struct { +- WorkDoneProgressOptions +-} +- +-// Parameters for a {@link DefinitionRequest}. +-type DefinitionParams struct { +- TextDocumentPositionParams +- WorkDoneProgressParams +- PartialResultParams +-} - -- // Make sure the package name isn't already in use by another -- // object, and that this file doesn't import the package yet. -- // TODO(adonovan): what if pkg.Path has vendor/ prefix? -- if _, ok := seen[pkg.Name()]; !ok && pkg != c.pkg.GetTypes() && !alreadyImports(c.file, source.ImportPath(pkg.Path())) { -- seen[pkg.Name()] = struct{}{} -- obj := types.NewPkgName(0, nil, pkg.Name(), pkg) -- imp := &importInfo{ -- importPath: pkg.Path(), -- } -- if imports.ImportPathToAssumedName(pkg.Path()) != pkg.Name() { -- imp.name = pkg.Name() -- } -- c.deepState.enqueue(candidate{ -- obj: obj, -- score: stdScore, -- imp: imp, -- }) -- } -- } -- } -- } +-// Registration options for a {@link DefinitionRequest}. +-type DefinitionRegistrationOptions struct { +- TextDocumentRegistrationOptions +- DefinitionOptions +-} - -- if c.opts.unimported { -- if err := c.unimportedPackages(ctx, seen); err != nil { -- return err -- } -- } +-// Delete file operation +-type DeleteFile struct { +- // A delete +- Kind string `json:"kind"` +- // The file to delete. +- URI DocumentURI `json:"uri"` +- // Delete options. +- Options *DeleteFileOptions `json:"options,omitempty"` +- ResourceOperation +-} - -- if c.inference.typeName.isTypeParam { -- // If we are completing a type param, offer each structural type. -- // This ensures we suggest "[]int" and "[]float64" for a constraint -- // with type union "[]int | []float64". -- if t, _ := c.inference.objType.(*types.Interface); t != nil { -- terms, _ := typeparams.InterfaceTermSet(t) -- for _, term := range terms { -- c.injectType(ctx, term.Type()) -- } -- } -- } else { -- c.injectType(ctx, c.inference.objType) -- } +-// Delete file options +-type DeleteFileOptions struct { +- // Delete the content recursively if a folder is denoted. +- Recursive bool `json:"recursive,omitempty"` +- // Ignore the operation if the file doesn't exist. +- IgnoreIfNotExists bool `json:"ignoreIfNotExists,omitempty"` +-} - -- // Add keyword completion items appropriate in the current context. -- c.addKeywordCompletions() +-// The parameters sent in notifications/requests for user-initiated deletes of +-// files. +-// +-// @since 3.16.0 +-type DeleteFilesParams struct { +- // An array of all files/folders deleted in this operation. +- Files []FileDelete `json:"files"` +-} - -- return nil +-// Represents a diagnostic, such as a compiler error or warning. Diagnostic objects +-// are only valid in the scope of a resource. +-type Diagnostic struct { +- // The range at which the message applies +- Range Range `json:"range"` +- // The diagnostic's severity. Can be omitted. If omitted it is up to the +- // client to interpret diagnostics as error, warning, info or hint. +- Severity DiagnosticSeverity `json:"severity,omitempty"` +- // The diagnostic's code, which usually appear in the user interface. +- Code interface{} `json:"code,omitempty"` +- // An optional property to describe the error code. +- // Requires the code field (above) to be present/not null. +- // +- // @since 3.16.0 +- CodeDescription *CodeDescription `json:"codeDescription,omitempty"` +- // A human-readable string describing the source of this +- // diagnostic, e.g. 'typescript' or 'super lint'. It usually +- // appears in the user interface. +- Source string `json:"source,omitempty"` +- // The diagnostic's message. It usually appears in the user interface +- Message string `json:"message"` +- // Additional metadata about the diagnostic. +- // +- // @since 3.15.0 +- Tags []DiagnosticTag `json:"tags,omitempty"` +- // An array of related diagnostic information, e.g. when symbol-names within +- // a scope collide all definitions can be marked via this property. +- RelatedInformation []DiagnosticRelatedInformation `json:"relatedInformation,omitempty"` +- // A data entry field that is preserved between a `textDocument/publishDiagnostics` +- // notification and `textDocument/codeAction` request. +- // +- // @since 3.16.0 +- Data *json.RawMessage `json:"data,omitempty"` -} - --// injectType manufactures candidates based on the given type. This is --// intended for types not discoverable via lexical search, such as --// composite and/or generic types. For example, if the type is "[]int", --// this method makes sure you get candidates "[]int{}" and "[]int" --// (the latter applies when completing a type name). --func (c *completer) injectType(ctx context.Context, t types.Type) { -- if t == nil { -- return -- } +-// Client capabilities specific to diagnostic pull requests. +-// +-// @since 3.17.0 +-type DiagnosticClientCapabilities struct { +- // Whether implementation supports dynamic registration. If this is set to `true` +- // the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` +- // return value for the corresponding server capability as well. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +- // Whether the clients supports related documents for document diagnostic pulls. +- RelatedDocumentSupport bool `json:"relatedDocumentSupport,omitempty"` +-} - -- t = source.Deref(t) +-// Diagnostic options. +-// +-// @since 3.17.0 +-type DiagnosticOptions struct { +- // An optional identifier under which the diagnostics are +- // managed by the client. +- Identifier string `json:"identifier,omitempty"` +- // Whether the language has inter file dependencies meaning that +- // editing code in one file can result in a different diagnostic +- // set in another file. Inter file dependencies are common for +- // most programming languages and typically uncommon for linters. +- InterFileDependencies bool `json:"interFileDependencies"` +- // The server provides support for workspace diagnostics as well. +- WorkspaceDiagnostics bool `json:"workspaceDiagnostics"` +- WorkDoneProgressOptions +-} - -- // If we have an expected type and it is _not_ a named type, handle -- // it specially. Non-named types like "[]int" will never be -- // considered via a lexical search, so we need to directly inject -- // them. Also allow generic types since lexical search does not -- // infer instantiated versions of them. -- if named, _ := t.(*types.Named); named == nil || typeparams.ForNamed(named).Len() > 0 { -- // If our expected type is "[]int", this will add a literal -- // candidate of "[]int{}". -- c.literal(ctx, t, nil) +-// Diagnostic registration options. +-// +-// @since 3.17.0 +-type DiagnosticRegistrationOptions struct { +- TextDocumentRegistrationOptions +- DiagnosticOptions +- StaticRegistrationOptions +-} - -- if _, isBasic := t.(*types.Basic); !isBasic { -- // If we expect a non-basic type name (e.g. "[]int"), hack up -- // a named type whose name is literally "[]int". This allows -- // us to reuse our object based completion machinery. -- fakeNamedType := candidate{ -- obj: types.NewTypeName(token.NoPos, nil, types.TypeString(t, c.qf), t), -- score: stdScore, -- } -- // Make sure the type name matches before considering -- // candidate. This cuts down on useless candidates. -- if c.matchingTypeName(&fakeNamedType) { -- c.deepState.enqueue(fakeNamedType) -- } -- } -- } +-// Represents a related message and source code location for a diagnostic. This should be +-// used to point to code locations that cause or related to a diagnostics, e.g when duplicating +-// a symbol in a scope. +-type DiagnosticRelatedInformation struct { +- // The location of this related diagnostic information. +- Location Location `json:"location"` +- // The message of this related diagnostic information. +- Message string `json:"message"` -} - --func (c *completer) unimportedPackages(ctx context.Context, seen map[string]struct{}) error { -- var prefix string -- if c.surrounding != nil { -- prefix = c.surrounding.Prefix() -- } +-// Cancellation data returned from a diagnostic request. +-// +-// @since 3.17.0 +-type DiagnosticServerCancellationData struct { +- RetriggerRequest bool `json:"retriggerRequest"` +-} - -- // Don't suggest unimported packages if we have absolutely nothing -- // to go on. -- if prefix == "" { -- return nil -- } +-// The diagnostic's severity. +-type DiagnosticSeverity uint32 - -- count := 0 +-// The diagnostic tags. +-// +-// @since 3.15.0 +-type DiagnosticTag uint32 - -- // Search the forward transitive closure of the workspace. -- all, err := c.snapshot.AllMetadata(ctx) -- if err != nil { -- return err -- } -- pkgNameByPath := make(map[source.PackagePath]string) -- var paths []string // actually PackagePaths -- for _, m := range all { -- if m.ForTest != "" { -- continue // skip all test variants -- } -- if m.Name == "main" { -- continue // main is non-importable -- } -- if !strings.HasPrefix(string(m.Name), prefix) { -- continue // not a match -- } -- paths = append(paths, string(m.PkgPath)) -- pkgNameByPath[m.PkgPath] = string(m.Name) -- } +-// Workspace client capabilities specific to diagnostic pull requests. +-// +-// @since 3.17.0 +-type DiagnosticWorkspaceClientCapabilities struct { +- // Whether the client implementation supports a refresh request sent from +- // the server to the client. +- // +- // Note that this event is global and will force the client to refresh all +- // pulled diagnostics currently shown. It should be used with absolute care and +- // is useful for situation where a server for example detects a project wide +- // change that requires such a calculation. +- RefreshSupport bool `json:"refreshSupport,omitempty"` +-} +-type DidChangeConfigurationClientCapabilities struct { +- // Did change configuration notification supports dynamic registration. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +-} - -- // Rank candidates using goimports' algorithm. -- var relevances map[string]float64 -- if len(paths) != 0 { -- if err := c.snapshot.RunProcessEnvFunc(ctx, func(ctx context.Context, opts *imports.Options) error { -- var err error -- relevances, err = imports.ScoreImportPaths(ctx, opts.Env, paths) -- return err -- }); err != nil { -- return err -- } -- } -- sort.Slice(paths, func(i, j int) bool { -- if relevances[paths[i]] != relevances[paths[j]] { -- return relevances[paths[i]] > relevances[paths[j]] -- } +-// The parameters of a change configuration notification. +-type DidChangeConfigurationParams struct { +- // The actual changed settings +- Settings interface{} `json:"settings"` +-} +-type DidChangeConfigurationRegistrationOptions struct { +- Section *OrPSection_workspace_didChangeConfiguration `json:"section,omitempty"` +-} - -- // Fall back to lexical sort to keep truncated set of candidates -- // in a consistent order. -- return paths[i] < paths[j] -- }) +-// The params sent in a change notebook document notification. +-// +-// @since 3.17.0 +-type DidChangeNotebookDocumentParams struct { +- // The notebook document that did change. The version number points +- // to the version after all provided changes have been applied. If +- // only the text document content of a cell changes the notebook version +- // doesn't necessarily have to change. +- NotebookDocument VersionedNotebookDocumentIdentifier `json:"notebookDocument"` +- // The actual changes to the notebook document. +- // +- // The changes describe single state changes to the notebook document. +- // So if there are two changes c1 (at array index 0) and c2 (at array +- // index 1) for a notebook in state S then c1 moves the notebook from +- // S to S' and c2 from S' to S''. So c1 is computed on the state S and +- // c2 is computed on the state S'. +- // +- // To mirror the content of a notebook using change events use the following approach: +- // +- // - start with the same initial content +- // - apply the 'notebookDocument/didChange' notifications in the order you receive them. +- // - apply the `NotebookChangeEvent`s in a single notification in the order +- // you receive them. +- Change NotebookDocumentChangeEvent `json:"change"` +-} - -- for _, path := range paths { -- name := pkgNameByPath[source.PackagePath(path)] -- if _, ok := seen[name]; ok { -- continue -- } -- imp := &importInfo{ -- importPath: path, -- } -- if imports.ImportPathToAssumedName(path) != name { -- imp.name = name -- } -- if count >= maxUnimportedPackageNames { -- return nil -- } -- c.deepState.enqueue(candidate{ -- // Pass an empty *types.Package to disable deep completions. -- obj: types.NewPkgName(0, nil, name, types.NewPackage(path, name)), -- score: unimportedScore(relevances[path]), -- imp: imp, -- }) -- count++ -- } +-// The change text document notification's parameters. +-type DidChangeTextDocumentParams struct { +- // The document that did change. The version number points +- // to the version after all provided content changes have +- // been applied. +- TextDocument VersionedTextDocumentIdentifier `json:"textDocument"` +- // The actual content changes. The content changes describe single state changes +- // to the document. So if there are two content changes c1 (at array index 0) and +- // c2 (at array index 1) for a document in state S then c1 moves the document from +- // S to S' and c2 from S' to S''. So c1 is computed on the state S and c2 is computed +- // on the state S'. +- // +- // To mirror the content of a document using change events use the following approach: +- // +- // - start with the same initial content +- // - apply the 'textDocument/didChange' notifications in the order you receive them. +- // - apply the `TextDocumentContentChangeEvent`s in a single notification in the order +- // you receive them. +- ContentChanges []TextDocumentContentChangeEvent `json:"contentChanges"` +-} +-type DidChangeWatchedFilesClientCapabilities struct { +- // Did change watched files notification supports dynamic registration. Please note +- // that the current protocol doesn't support static configuration for file changes +- // from the server side. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +- // Whether the client has support for {@link RelativePattern relative pattern} +- // or not. +- // +- // @since 3.17.0 +- RelativePatternSupport bool `json:"relativePatternSupport,omitempty"` +-} - -- ctx, cancel := context.WithCancel(ctx) +-// The watched files change notification's parameters. +-type DidChangeWatchedFilesParams struct { +- // The actual file events. +- Changes []FileEvent `json:"changes"` +-} - -- var mu sync.Mutex -- add := func(pkg imports.ImportFix) { -- if ignoreUnimportedCompletion(&pkg) { -- return -- } -- mu.Lock() -- defer mu.Unlock() -- if _, ok := seen[pkg.IdentName]; ok { -- return -- } -- if _, ok := relevances[pkg.StmtInfo.ImportPath]; ok { -- return -- } +-// Describe options to be used when registered for text document change events. +-type DidChangeWatchedFilesRegistrationOptions struct { +- // The watchers to register. +- Watchers []FileSystemWatcher `json:"watchers"` +-} - -- if count >= maxUnimportedPackageNames { -- cancel() -- return -- } +-// The parameters of a `workspace/didChangeWorkspaceFolders` notification. +-type DidChangeWorkspaceFoldersParams struct { +- // The actual workspace folder change event. +- Event WorkspaceFoldersChangeEvent `json:"event"` +-} - -- // Do not add the unimported packages to seen, since we can have -- // multiple packages of the same name as completion suggestions, since -- // only one will be chosen. -- obj := types.NewPkgName(0, nil, pkg.IdentName, types.NewPackage(pkg.StmtInfo.ImportPath, pkg.IdentName)) -- c.deepState.enqueue(candidate{ -- obj: obj, -- score: unimportedScore(pkg.Relevance), -- imp: &importInfo{ -- importPath: pkg.StmtInfo.ImportPath, -- name: pkg.StmtInfo.Name, -- }, -- }) -- count++ -- } -- c.completionCallbacks = append(c.completionCallbacks, func(ctx context.Context, opts *imports.Options) error { -- defer cancel() -- return imports.GetAllCandidates(ctx, add, prefix, c.filename, c.pkg.GetTypes().Name(), opts.Env) -- }) -- return nil +-// The params sent in a close notebook document notification. +-// +-// @since 3.17.0 +-type DidCloseNotebookDocumentParams struct { +- // The notebook document that got closed. +- NotebookDocument NotebookDocumentIdentifier `json:"notebookDocument"` +- // The text documents that represent the content +- // of a notebook cell that got closed. +- CellTextDocuments []TextDocumentIdentifier `json:"cellTextDocuments"` -} - --// alreadyImports reports whether f has an import with the specified path. --func alreadyImports(f *ast.File, path source.ImportPath) bool { -- for _, s := range f.Imports { -- if source.UnquoteImportPath(s) == path { -- return true -- } -- } -- return false +-// The parameters sent in a close text document notification +-type DidCloseTextDocumentParams struct { +- // The document that was closed. +- TextDocument TextDocumentIdentifier `json:"textDocument"` -} - --func (c *completer) inConstDecl() bool { -- for _, n := range c.path { -- if decl, ok := n.(*ast.GenDecl); ok && decl.Tok == token.CONST { -- return true -- } -- } -- return false +-// The params sent in an open notebook document notification. +-// +-// @since 3.17.0 +-type DidOpenNotebookDocumentParams struct { +- // The notebook document that got opened. +- NotebookDocument NotebookDocument `json:"notebookDocument"` +- // The text documents that represent the content +- // of a notebook cell. +- CellTextDocuments []TextDocumentItem `json:"cellTextDocuments"` -} - --// structLiteralFieldName finds completions for struct field names inside a struct literal. --func (c *completer) structLiteralFieldName(ctx context.Context) error { -- clInfo := c.enclosingCompositeLiteral +-// The parameters sent in an open text document notification +-type DidOpenTextDocumentParams struct { +- // The document that was opened. +- TextDocument TextDocumentItem `json:"textDocument"` +-} - -- // Mark fields of the composite literal that have already been set, -- // except for the current field. -- addedFields := make(map[*types.Var]bool) -- for _, el := range clInfo.cl.Elts { -- if kvExpr, ok := el.(*ast.KeyValueExpr); ok { -- if clInfo.kv == kvExpr { -- continue -- } +-// The params sent in a save notebook document notification. +-// +-// @since 3.17.0 +-type DidSaveNotebookDocumentParams struct { +- // The notebook document that got saved. +- NotebookDocument NotebookDocumentIdentifier `json:"notebookDocument"` +-} - -- if key, ok := kvExpr.Key.(*ast.Ident); ok { -- if used, ok := c.pkg.GetTypesInfo().Uses[key]; ok { -- if usedVar, ok := used.(*types.Var); ok { -- addedFields[usedVar] = true -- } -- } -- } -- } -- } +-// The parameters sent in a save text document notification +-type DidSaveTextDocumentParams struct { +- // The document that was saved. +- TextDocument TextDocumentIdentifier `json:"textDocument"` +- // Optional the content when saved. Depends on the includeText value +- // when the save notification was requested. +- Text *string `json:"text,omitempty"` +-} +-type DocumentColorClientCapabilities struct { +- // Whether implementation supports dynamic registration. If this is set to `true` +- // the client supports the new `DocumentColorRegistrationOptions` return value +- // for the corresponding server capability as well. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +-} +-type DocumentColorOptions struct { +- WorkDoneProgressOptions +-} - -- deltaScore := 0.0001 -- switch t := clInfo.clType.(type) { -- case *types.Struct: -- for i := 0; i < t.NumFields(); i++ { -- field := t.Field(i) -- if !addedFields[field] { -- c.deepState.enqueue(candidate{ -- obj: field, -- score: highScore - float64(i)*deltaScore, -- }) -- } -- } +-// Parameters for a {@link DocumentColorRequest}. +-type DocumentColorParams struct { +- // The text document. +- TextDocument TextDocumentIdentifier `json:"textDocument"` +- WorkDoneProgressParams +- PartialResultParams +-} +-type DocumentColorRegistrationOptions struct { +- TextDocumentRegistrationOptions +- DocumentColorOptions +- StaticRegistrationOptions +-} - -- // Add lexical completions if we aren't certain we are in the key part of a -- // key-value pair. -- if clInfo.maybeInFieldName { -- return c.lexical(ctx) -- } -- default: -- return c.lexical(ctx) -- } +-// Parameters of the document diagnostic request. +-// +-// @since 3.17.0 +-type DocumentDiagnosticParams struct { +- // The text document. +- TextDocument TextDocumentIdentifier `json:"textDocument"` +- // The additional identifier provided during registration. +- Identifier string `json:"identifier,omitempty"` +- // The result id of a previous response if provided. +- PreviousResultID string `json:"previousResultId,omitempty"` +- WorkDoneProgressParams +- PartialResultParams +-} +-type DocumentDiagnosticReport = Or_DocumentDiagnosticReport // (alias) +-// The document diagnostic report kinds. +-// +-// @since 3.17.0 +-type DocumentDiagnosticReportKind string - -- return nil +-// A partial result for a document diagnostic report. +-// +-// @since 3.17.0 +-type DocumentDiagnosticReportPartialResult struct { +- RelatedDocuments map[DocumentURI]interface{} `json:"relatedDocuments"` -} - --func (cl *compLitInfo) isStruct() bool { -- _, ok := cl.clType.(*types.Struct) -- return ok +-// A document filter describes a top level text document or +-// a notebook cell document. +-// +-// @since 3.17.0 - proposed support for NotebookCellTextDocumentFilter. +-type DocumentFilter = Or_DocumentFilter // (alias) +-// Client capabilities of a {@link DocumentFormattingRequest}. +-type DocumentFormattingClientCapabilities struct { +- // Whether formatting supports dynamic registration. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -} - --// enclosingCompositeLiteral returns information about the composite literal enclosing the --// position. --func enclosingCompositeLiteral(path []ast.Node, pos token.Pos, info *types.Info) *compLitInfo { -- for _, n := range path { -- switch n := n.(type) { -- case *ast.CompositeLit: -- // The enclosing node will be a composite literal if the user has just -- // opened the curly brace (e.g. &x{<>) or the completion request is triggered -- // from an already completed composite literal expression (e.g. &x{foo: 1, <>}) -- // -- // The position is not part of the composite literal unless it falls within the -- // curly braces (e.g. "foo.Foo<>Struct{}"). -- if !(n.Lbrace < pos && pos <= n.Rbrace) { -- // Keep searching since we may yet be inside a composite literal. -- // For example "Foo{B: Ba<>{}}". -- break -- } -- -- tv, ok := info.Types[n] -- if !ok { -- return nil -- } +-// Provider options for a {@link DocumentFormattingRequest}. +-type DocumentFormattingOptions struct { +- WorkDoneProgressOptions +-} - -- clInfo := compLitInfo{ -- cl: n, -- clType: source.Deref(tv.Type).Underlying(), -- } +-// The parameters of a {@link DocumentFormattingRequest}. +-type DocumentFormattingParams struct { +- // The document to format. +- TextDocument TextDocumentIdentifier `json:"textDocument"` +- // The format options. +- Options FormattingOptions `json:"options"` +- WorkDoneProgressParams +-} - -- var ( -- expr ast.Expr -- hasKeys bool -- ) -- for _, el := range n.Elts { -- // Remember the expression that the position falls in, if any. -- if el.Pos() <= pos && pos <= el.End() { -- expr = el -- } +-// Registration options for a {@link DocumentFormattingRequest}. +-type DocumentFormattingRegistrationOptions struct { +- TextDocumentRegistrationOptions +- DocumentFormattingOptions +-} - -- if kv, ok := el.(*ast.KeyValueExpr); ok { -- hasKeys = true -- // If expr == el then we know the position falls in this expression, -- // so also record kv as the enclosing *ast.KeyValueExpr. -- if expr == el { -- clInfo.kv = kv -- break -- } -- } -- } +-// A document highlight is a range inside a text document which deserves +-// special attention. Usually a document highlight is visualized by changing +-// the background color of its range. +-type DocumentHighlight struct { +- // The range this highlight applies to. +- Range Range `json:"range"` +- // The highlight kind, default is {@link DocumentHighlightKind.Text text}. +- Kind DocumentHighlightKind `json:"kind,omitempty"` +-} - -- if clInfo.kv != nil { -- // If in a *ast.KeyValueExpr, we know we are in the key if the position -- // is to the left of the colon (e.g. "Foo{F<>: V}". -- clInfo.inKey = pos <= clInfo.kv.Colon -- } else if hasKeys { -- // If we aren't in a *ast.KeyValueExpr but the composite literal has -- // other *ast.KeyValueExprs, we must be on the key side of a new -- // *ast.KeyValueExpr (e.g. "Foo{F: V, <>}"). -- clInfo.inKey = true -- } else { -- switch clInfo.clType.(type) { -- case *types.Struct: -- if len(n.Elts) == 0 { -- // If the struct literal is empty, next could be a struct field -- // name or an expression (e.g. "Foo{<>}" could become "Foo{F:}" -- // or "Foo{someVar}"). -- clInfo.maybeInFieldName = true -- } else if len(n.Elts) == 1 { -- // If there is one expression and the position is in that expression -- // and the expression is an identifier, we may be writing a field -- // name or an expression (e.g. "Foo{F<>}"). -- _, clInfo.maybeInFieldName = expr.(*ast.Ident) -- } -- case *types.Map: -- // If we aren't in a *ast.KeyValueExpr we must be adding a new key -- // to the map. -- clInfo.inKey = true -- } -- } +-// Client Capabilities for a {@link DocumentHighlightRequest}. +-type DocumentHighlightClientCapabilities struct { +- // Whether document highlight supports dynamic registration. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +-} - -- return &clInfo -- default: -- if breaksExpectedTypeInference(n, pos) { -- return nil -- } -- } -- } +-// A document highlight kind. +-type DocumentHighlightKind uint32 - -- return nil +-// Provider options for a {@link DocumentHighlightRequest}. +-type DocumentHighlightOptions struct { +- WorkDoneProgressOptions -} - --// enclosingFunction returns the signature and body of the function --// enclosing the given position. --func enclosingFunction(path []ast.Node, info *types.Info) *funcInfo { -- for _, node := range path { -- switch t := node.(type) { -- case *ast.FuncDecl: -- if obj, ok := info.Defs[t.Name]; ok { -- return &funcInfo{ -- sig: obj.Type().(*types.Signature), -- body: t.Body, -- } -- } -- case *ast.FuncLit: -- if typ, ok := info.Types[t]; ok { -- if sig, _ := typ.Type.(*types.Signature); sig == nil { -- // golang/go#49397: it should not be possible, but we somehow arrived -- // here with a non-signature type, most likely due to AST mangling -- // such that node.Type is not a FuncType. -- return nil -- } -- return &funcInfo{ -- sig: typ.Type.(*types.Signature), -- body: t.Body, -- } -- } -- } -- } -- return nil +-// Parameters for a {@link DocumentHighlightRequest}. +-type DocumentHighlightParams struct { +- TextDocumentPositionParams +- WorkDoneProgressParams +- PartialResultParams -} - --func (c *completer) expectedCompositeLiteralType() types.Type { -- clInfo := c.enclosingCompositeLiteral -- switch t := clInfo.clType.(type) { -- case *types.Slice: -- if clInfo.inKey { -- return types.Typ[types.UntypedInt] -- } -- return t.Elem() -- case *types.Array: -- if clInfo.inKey { -- return types.Typ[types.UntypedInt] -- } -- return t.Elem() -- case *types.Map: -- if clInfo.inKey { -- return t.Key() -- } -- return t.Elem() -- case *types.Struct: -- // If we are completing a key (i.e. field name), there is no expected type. -- if clInfo.inKey { -- return nil -- } +-// Registration options for a {@link DocumentHighlightRequest}. +-type DocumentHighlightRegistrationOptions struct { +- TextDocumentRegistrationOptions +- DocumentHighlightOptions +-} - -- // If we are in a key-value pair, but not in the key, then we must be on the -- // value side. The expected type of the value will be determined from the key. -- if clInfo.kv != nil { -- if key, ok := clInfo.kv.Key.(*ast.Ident); ok { -- for i := 0; i < t.NumFields(); i++ { -- if field := t.Field(i); field.Name() == key.Name { -- return field.Type() -- } -- } -- } -- } else { -- // If we aren't in a key-value pair and aren't in the key, we must be using -- // implicit field names. +-// A document link is a range in a text document that links to an internal or external resource, like another +-// text document or a web site. +-type DocumentLink struct { +- // The range this link applies to. +- Range Range `json:"range"` +- // The uri this link points to. If missing a resolve request is sent later. +- Target *URI `json:"target,omitempty"` +- // The tooltip text when you hover over this link. +- // +- // If a tooltip is provided, is will be displayed in a string that includes instructions on how to +- // trigger the link, such as `{0} (ctrl + click)`. The specific instructions vary depending on OS, +- // user settings, and localization. +- // +- // @since 3.15.0 +- Tooltip string `json:"tooltip,omitempty"` +- // A data entry field that is preserved on a document link between a +- // DocumentLinkRequest and a DocumentLinkResolveRequest. +- Data interface{} `json:"data,omitempty"` +-} - -- // The order of the literal fields must match the order in the struct definition. -- // Find the element that the position belongs to and suggest that field's type. -- if i := exprAtPos(c.pos, clInfo.cl.Elts); i < t.NumFields() { -- return t.Field(i).Type() -- } -- } -- } -- return nil +-// The client capabilities of a {@link DocumentLinkRequest}. +-type DocumentLinkClientCapabilities struct { +- // Whether document link supports dynamic registration. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +- // Whether the client supports the `tooltip` property on `DocumentLink`. +- // +- // @since 3.15.0 +- TooltipSupport bool `json:"tooltipSupport,omitempty"` -} - --// typeMod represents an operator that changes the expected type. --type typeMod struct { -- mod typeModKind -- arrayLen int64 +-// Provider options for a {@link DocumentLinkRequest}. +-type DocumentLinkOptions struct { +- // Document links have a resolve provider as well. +- ResolveProvider bool `json:"resolveProvider,omitempty"` +- WorkDoneProgressOptions -} - --type typeModKind int +-// The parameters of a {@link DocumentLinkRequest}. +-type DocumentLinkParams struct { +- // The document to provide document links for. +- TextDocument TextDocumentIdentifier `json:"textDocument"` +- WorkDoneProgressParams +- PartialResultParams +-} - --const ( -- dereference typeModKind = iota // pointer indirection: "*" -- reference // adds level of pointer: "&" for values, "*" for type names -- chanRead // channel read operator: "<-" -- sliceType // make a slice type: "[]" in "[]int" -- arrayType // make an array type: "[2]" in "[2]int" -- invoke // make a function call: "()" in "foo()" -- takeSlice // take slice of array: "[:]" in "foo[:]" -- takeDotDotDot // turn slice into variadic args: "..." in "foo..." -- index // index into slice/array: "[0]" in "foo[0]" --) +-// Registration options for a {@link DocumentLinkRequest}. +-type DocumentLinkRegistrationOptions struct { +- TextDocumentRegistrationOptions +- DocumentLinkOptions +-} - --type objKind int +-// Client capabilities of a {@link DocumentOnTypeFormattingRequest}. +-type DocumentOnTypeFormattingClientCapabilities struct { +- // Whether on type formatting supports dynamic registration. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +-} - --const ( -- kindAny objKind = 0 -- kindArray objKind = 1 << iota -- kindSlice -- kindChan -- kindMap -- kindStruct -- kindString -- kindInt -- kindBool -- kindBytes -- kindPtr -- kindFloat -- kindComplex -- kindError -- kindStringer -- kindFunc --) +-// Provider options for a {@link DocumentOnTypeFormattingRequest}. +-type DocumentOnTypeFormattingOptions struct { +- // A character on which formatting should be triggered, like `{`. +- FirstTriggerCharacter string `json:"firstTriggerCharacter"` +- // More trigger characters. +- MoreTriggerCharacter []string `json:"moreTriggerCharacter,omitempty"` +-} - --// penalizedObj represents an object that should be disfavored as a --// completion candidate. --type penalizedObj struct { -- // objChain is the full "chain", e.g. "foo.bar().baz" becomes -- // []types.Object{foo, bar, baz}. -- objChain []types.Object -- // penalty is score penalty in the range (0, 1). -- penalty float64 +-// The parameters of a {@link DocumentOnTypeFormattingRequest}. +-type DocumentOnTypeFormattingParams struct { +- // The document to format. +- TextDocument TextDocumentIdentifier `json:"textDocument"` +- // The position around which the on type formatting should happen. +- // This is not necessarily the exact position where the character denoted +- // by the property `ch` got typed. +- Position Position `json:"position"` +- // The character that has been typed that triggered the formatting +- // on type request. That is not necessarily the last character that +- // got inserted into the document since the client could auto insert +- // characters as well (e.g. like automatic brace completion). +- Ch string `json:"ch"` +- // The formatting options. +- Options FormattingOptions `json:"options"` -} - --// candidateInference holds information we have inferred about a type that can be --// used at the current position. --type candidateInference struct { -- // objType is the desired type of an object used at the query position. -- objType types.Type +-// Registration options for a {@link DocumentOnTypeFormattingRequest}. +-type DocumentOnTypeFormattingRegistrationOptions struct { +- TextDocumentRegistrationOptions +- DocumentOnTypeFormattingOptions +-} - -- // objKind is a mask of expected kinds of types such as "map", "slice", etc. -- objKind objKind +-// Client capabilities of a {@link DocumentRangeFormattingRequest}. +-type DocumentRangeFormattingClientCapabilities struct { +- // Whether range formatting supports dynamic registration. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +- // Whether the client supports formatting multiple ranges at once. +- // +- // @since 3.18.0 +- // @proposed +- RangesSupport bool `json:"rangesSupport,omitempty"` +-} - -- // variadic is true if we are completing the initial variadic -- // parameter. For example: -- // append([]T{}, <>) // objType=T variadic=true -- // append([]T{}, T{}, <>) // objType=T variadic=false -- variadic bool +-// Provider options for a {@link DocumentRangeFormattingRequest}. +-type DocumentRangeFormattingOptions struct { +- // Whether the server supports formatting multiple ranges at once. +- // +- // @since 3.18.0 +- // @proposed +- RangesSupport bool `json:"rangesSupport,omitempty"` +- WorkDoneProgressOptions +-} - -- // modifiers are prefixes such as "*", "&" or "<-" that influence how -- // a candidate type relates to the expected type. -- modifiers []typeMod +-// The parameters of a {@link DocumentRangeFormattingRequest}. +-type DocumentRangeFormattingParams struct { +- // The document to format. +- TextDocument TextDocumentIdentifier `json:"textDocument"` +- // The range to format +- Range Range `json:"range"` +- // The format options +- Options FormattingOptions `json:"options"` +- WorkDoneProgressParams +-} - -- // convertibleTo is a type our candidate type must be convertible to. -- convertibleTo types.Type +-// Registration options for a {@link DocumentRangeFormattingRequest}. +-type DocumentRangeFormattingRegistrationOptions struct { +- TextDocumentRegistrationOptions +- DocumentRangeFormattingOptions +-} - -- // typeName holds information about the expected type name at -- // position, if any. -- typeName typeNameInference +-// The parameters of a {@link DocumentRangesFormattingRequest}. +-// +-// @since 3.18.0 +-// @proposed +-type DocumentRangesFormattingParams struct { +- // The document to format. +- TextDocument TextDocumentIdentifier `json:"textDocument"` +- // The ranges to format +- Ranges []Range `json:"ranges"` +- // The format options +- Options FormattingOptions `json:"options"` +- WorkDoneProgressParams +-} - -- // assignees are the types that would receive a function call's -- // results at the position. For example: +-// A document selector is the combination of one or many document filters. +-// +-// @sample `let sel:DocumentSelector = [{ language: 'typescript' }, { language: 'json', pattern: '**∕tsconfig.json' }]`; +-// +-// The use of a string as a document filter is deprecated @since 3.16.0. +-type DocumentSelector = []DocumentFilter // (alias) +-// Represents programming constructs like variables, classes, interfaces etc. +-// that appear in a document. Document symbols can be hierarchical and they +-// have two ranges: one that encloses its definition and one that points to +-// its most interesting range, e.g. the range of an identifier. +-type DocumentSymbol struct { +- // The name of this symbol. Will be displayed in the user interface and therefore must not be +- // an empty string or a string only consisting of white spaces. +- Name string `json:"name"` +- // More detail for this symbol, e.g the signature of a function. +- Detail string `json:"detail,omitempty"` +- // The kind of this symbol. +- Kind SymbolKind `json:"kind"` +- // Tags for this document symbol. - // -- // foo := 123 -- // foo, bar := <> +- // @since 3.16.0 +- Tags []SymbolTag `json:"tags,omitempty"` +- // Indicates if this symbol is deprecated. - // -- // at "<>", the assignees are [int, ]. -- assignees []types.Type +- // @deprecated Use tags instead +- Deprecated bool `json:"deprecated,omitempty"` +- // The range enclosing this symbol not including leading/trailing whitespace but everything else +- // like comments. This information is typically used to determine if the clients cursor is +- // inside the symbol to reveal in the symbol in the UI. +- Range Range `json:"range"` +- // The range that should be selected and revealed when this symbol is being picked, e.g the name of a function. +- // Must be contained by the `range`. +- SelectionRange Range `json:"selectionRange"` +- // Children of this symbol, e.g. properties of a class. +- Children []DocumentSymbol `json:"children,omitempty"` +-} - -- // variadicAssignees is true if we could be completing an inner -- // function call that fills out an outer function call's variadic -- // params. For example: +-// Client Capabilities for a {@link DocumentSymbolRequest}. +-type DocumentSymbolClientCapabilities struct { +- // Whether document symbol supports dynamic registration. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +- // Specific capabilities for the `SymbolKind` in the +- // `textDocument/documentSymbol` request. +- SymbolKind *ClientSymbolKindOptions `json:"symbolKind,omitempty"` +- // The client supports hierarchical document symbols. +- HierarchicalDocumentSymbolSupport bool `json:"hierarchicalDocumentSymbolSupport,omitempty"` +- // The client supports tags on `SymbolInformation`. Tags are supported on +- // `DocumentSymbol` if `hierarchicalDocumentSymbolSupport` is set to true. +- // Clients supporting tags have to handle unknown tags gracefully. - // -- // func foo(int, ...string) {} +- // @since 3.16.0 +- TagSupport *ClientSymbolTagOptions `json:"tagSupport,omitempty"` +- // The client supports an additional label presented in the UI when +- // registering a document symbol provider. - // -- // foo(<>) // variadicAssignees=true -- // foo(bar<>) // variadicAssignees=true -- // foo(bar, baz<>) // variadicAssignees=false -- variadicAssignees bool +- // @since 3.16.0 +- LabelSupport bool `json:"labelSupport,omitempty"` +-} - -- // penalized holds expressions that should be disfavored as -- // candidates. For example, it tracks expressions already used in a -- // switch statement's other cases. Each expression is tracked using -- // its entire object "chain" allowing differentiation between -- // "a.foo" and "b.foo" when "a" and "b" are the same type. -- penalized []penalizedObj +-// Provider options for a {@link DocumentSymbolRequest}. +-type DocumentSymbolOptions struct { +- // A human-readable string that is shown when multiple outlines trees +- // are shown for the same document. +- // +- // @since 3.16.0 +- Label string `json:"label,omitempty"` +- WorkDoneProgressOptions +-} - -- // objChain contains the chain of objects representing the -- // surrounding *ast.SelectorExpr. For example, if we are completing -- // "foo.bar.ba<>", objChain will contain []types.Object{foo, bar}. -- objChain []types.Object +-// Parameters for a {@link DocumentSymbolRequest}. +-type DocumentSymbolParams struct { +- // The text document. +- TextDocument TextDocumentIdentifier `json:"textDocument"` +- WorkDoneProgressParams +- PartialResultParams +-} +- +-// Registration options for a {@link DocumentSymbolRequest}. +-type DocumentSymbolRegistrationOptions struct { +- TextDocumentRegistrationOptions +- DocumentSymbolOptions +-} +- +-// Edit range variant that includes ranges for insert and replace operations. +-// +-// @since 3.18.0 +-// @proposed +-type EditRangeWithInsertReplace struct { +- Insert Range `json:"insert"` +- Replace Range `json:"replace"` +-} +- +-// Predefined error codes. +-type ErrorCodes int32 +- +-// The client capabilities of a {@link ExecuteCommandRequest}. +-type ExecuteCommandClientCapabilities struct { +- // Execute command supports dynamic registration. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -} - --// typeNameInference holds information about the expected type name at --// position. --type typeNameInference struct { -- // wantTypeName is true if we expect the name of a type. -- wantTypeName bool +-// The server capabilities of a {@link ExecuteCommandRequest}. +-type ExecuteCommandOptions struct { +- // The commands to be executed on the server +- Commands []string `json:"commands"` +- WorkDoneProgressOptions +-} - -- // modifiers are prefixes such as "*", "&" or "<-" that influence how -- // a candidate type relates to the expected type. -- modifiers []typeMod +-// The parameters of a {@link ExecuteCommandRequest}. +-type ExecuteCommandParams struct { +- // The identifier of the actual command handler. +- Command string `json:"command"` +- // Arguments that the command should be invoked with. +- Arguments []json.RawMessage `json:"arguments,omitempty"` +- WorkDoneProgressParams +-} - -- // assertableFrom is a type that must be assertable to our candidate type. -- assertableFrom types.Type +-// Registration options for a {@link ExecuteCommandRequest}. +-type ExecuteCommandRegistrationOptions struct { +- ExecuteCommandOptions +-} +-type ExecutionSummary struct { +- // A strict monotonically increasing value +- // indicating the execution order of a cell +- // inside a notebook. +- ExecutionOrder uint32 `json:"executionOrder"` +- // Whether the execution was successful or +- // not if known by the client. +- Success bool `json:"success,omitempty"` +-} +-type FailureHandlingKind string - -- // wantComparable is true if we want a comparable type. -- wantComparable bool +-// The file event type +-type FileChangeType uint32 - -- // seenTypeSwitchCases tracks types that have already been used by -- // the containing type switch. -- seenTypeSwitchCases []types.Type +-// Represents information on a file/folder create. +-// +-// @since 3.16.0 +-type FileCreate struct { +- // A file:// URI for the location of the file/folder being created. +- URI string `json:"uri"` +-} - -- // compLitType is true if we are completing a composite literal type -- // name, e.g "foo<>{}". -- compLitType bool +-// Represents information on a file/folder delete. +-// +-// @since 3.16.0 +-type FileDelete struct { +- // A file:// URI for the location of the file/folder being deleted. +- URI string `json:"uri"` +-} - -- // isTypeParam is true if we are completing a type instantiation parameter -- isTypeParam bool +-// An event describing a file change. +-type FileEvent struct { +- // The file's uri. +- URI DocumentURI `json:"uri"` +- // The change type. +- Type FileChangeType `json:"type"` -} - --// expectedCandidate returns information about the expected candidate --// for an expression at the query position. --func expectedCandidate(ctx context.Context, c *completer) (inf candidateInference) { -- inf.typeName = expectTypeName(c) +-// Capabilities relating to events from file operations by the user in the client. +-// +-// These events do not come from the file system, they come from user operations +-// like renaming a file in the UI. +-// +-// @since 3.16.0 +-type FileOperationClientCapabilities struct { +- // Whether the client supports dynamic registration for file requests/notifications. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +- // The client has support for sending didCreateFiles notifications. +- DidCreate bool `json:"didCreate,omitempty"` +- // The client has support for sending willCreateFiles requests. +- WillCreate bool `json:"willCreate,omitempty"` +- // The client has support for sending didRenameFiles notifications. +- DidRename bool `json:"didRename,omitempty"` +- // The client has support for sending willRenameFiles requests. +- WillRename bool `json:"willRename,omitempty"` +- // The client has support for sending didDeleteFiles notifications. +- DidDelete bool `json:"didDelete,omitempty"` +- // The client has support for sending willDeleteFiles requests. +- WillDelete bool `json:"willDelete,omitempty"` +-} - -- if c.enclosingCompositeLiteral != nil { -- inf.objType = c.expectedCompositeLiteralType() -- } +-// A filter to describe in which file operation requests or notifications +-// the server is interested in receiving. +-// +-// @since 3.16.0 +-type FileOperationFilter struct { +- // A Uri scheme like `file` or `untitled`. +- Scheme string `json:"scheme,omitempty"` +- // The actual file operation pattern. +- Pattern FileOperationPattern `json:"pattern"` +-} - --Nodes: -- for i, node := range c.path { -- switch node := node.(type) { -- case *ast.BinaryExpr: -- // Determine if query position comes from left or right of op. -- e := node.X -- if c.pos < node.OpPos { -- e = node.Y -- } -- if tv, ok := c.pkg.GetTypesInfo().Types[e]; ok { -- switch node.Op { -- case token.LAND, token.LOR: -- // Don't infer "bool" type for "&&" or "||". Often you want -- // to compose a boolean expression from non-boolean -- // candidates. -- default: -- inf.objType = tv.Type -- } -- break Nodes -- } -- case *ast.AssignStmt: -- // Only rank completions if you are on the right side of the token. -- if c.pos > node.TokPos { -- i := exprAtPos(c.pos, node.Rhs) -- if i >= len(node.Lhs) { -- i = len(node.Lhs) - 1 -- } -- if tv, ok := c.pkg.GetTypesInfo().Types[node.Lhs[i]]; ok { -- inf.objType = tv.Type -- } +-// Options for notifications/requests for user operations on files. +-// +-// @since 3.16.0 +-type FileOperationOptions struct { +- // The server is interested in receiving didCreateFiles notifications. +- DidCreate *FileOperationRegistrationOptions `json:"didCreate,omitempty"` +- // The server is interested in receiving willCreateFiles requests. +- WillCreate *FileOperationRegistrationOptions `json:"willCreate,omitempty"` +- // The server is interested in receiving didRenameFiles notifications. +- DidRename *FileOperationRegistrationOptions `json:"didRename,omitempty"` +- // The server is interested in receiving willRenameFiles requests. +- WillRename *FileOperationRegistrationOptions `json:"willRename,omitempty"` +- // The server is interested in receiving didDeleteFiles file notifications. +- DidDelete *FileOperationRegistrationOptions `json:"didDelete,omitempty"` +- // The server is interested in receiving willDeleteFiles file requests. +- WillDelete *FileOperationRegistrationOptions `json:"willDelete,omitempty"` +-} - -- // If we have a single expression on the RHS, record the LHS -- // assignees so we can favor multi-return function calls with -- // matching result values. -- if len(node.Rhs) <= 1 { -- for _, lhs := range node.Lhs { -- inf.assignees = append(inf.assignees, c.pkg.GetTypesInfo().TypeOf(lhs)) -- } -- } else { -- // Otherwise, record our single assignee, even if its type is -- // not available. We use this info to downrank functions -- // with the wrong number of result values. -- inf.assignees = append(inf.assignees, c.pkg.GetTypesInfo().TypeOf(node.Lhs[i])) -- } -- } -- return inf -- case *ast.ValueSpec: -- if node.Type != nil && c.pos > node.Type.End() { -- inf.objType = c.pkg.GetTypesInfo().TypeOf(node.Type) -- } -- return inf -- case *ast.CallExpr: -- // Only consider CallExpr args if position falls between parens. -- if node.Lparen < c.pos && c.pos <= node.Rparen { -- // For type conversions like "int64(foo)" we can only infer our -- // desired type is convertible to int64. -- if typ := typeConversion(node, c.pkg.GetTypesInfo()); typ != nil { -- inf.convertibleTo = typ -- break Nodes -- } +-// A pattern to describe in which file operation requests or notifications +-// the server is interested in receiving. +-// +-// @since 3.16.0 +-type FileOperationPattern struct { +- // The glob pattern to match. Glob patterns can have the following syntax: +- // +- // - `*` to match one or more characters in a path segment +- // - `?` to match on one character in a path segment +- // - `**` to match any number of path segments, including none +- // - `{}` to group sub patterns into an OR expression. (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) +- // - `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) +- // - `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) +- Glob string `json:"glob"` +- // Whether to match files or folders with this pattern. +- // +- // Matches both if undefined. +- Matches *FileOperationPatternKind `json:"matches,omitempty"` +- // Additional options used during matching. +- Options *FileOperationPatternOptions `json:"options,omitempty"` +-} - -- sig, _ := c.pkg.GetTypesInfo().Types[node.Fun].Type.(*types.Signature) +-// A pattern kind describing if a glob pattern matches a file a folder or +-// both. +-// +-// @since 3.16.0 +-type FileOperationPatternKind string - -- if sig != nil && typeparams.ForSignature(sig).Len() > 0 { -- // If we are completing a generic func call, re-check the call expression. -- // This allows type param inference to work in cases like: -- // -- // func foo[T any](T) {} -- // foo[int](<>) // <- get "int" completions instead of "T" -- // -- // TODO: remove this after https://go.dev/issue/52503 -- info := &types.Info{Types: make(map[ast.Expr]types.TypeAndValue)} -- types.CheckExpr(c.pkg.FileSet(), c.pkg.GetTypes(), node.Fun.Pos(), node.Fun, info) -- sig, _ = info.Types[node.Fun].Type.(*types.Signature) -- } +-// Matching options for the file operation pattern. +-// +-// @since 3.16.0 +-type FileOperationPatternOptions struct { +- // The pattern should be matched ignoring casing. +- IgnoreCase bool `json:"ignoreCase,omitempty"` +-} - -- if sig != nil { -- inf = c.expectedCallParamType(inf, node, sig) -- } +-// The options to register for file operations. +-// +-// @since 3.16.0 +-type FileOperationRegistrationOptions struct { +- // The actual filters. +- Filters []FileOperationFilter `json:"filters"` +-} - -- if funIdent, ok := node.Fun.(*ast.Ident); ok { -- obj := c.pkg.GetTypesInfo().ObjectOf(funIdent) +-// Represents information on a file/folder rename. +-// +-// @since 3.16.0 +-type FileRename struct { +- // A file:// URI for the original location of the file/folder being renamed. +- OldURI string `json:"oldUri"` +- // A file:// URI for the new location of the file/folder being renamed. +- NewURI string `json:"newUri"` +-} +-type FileSystemWatcher struct { +- // The glob pattern to watch. See {@link GlobPattern glob pattern} for more detail. +- // +- // @since 3.17.0 support for relative patterns. +- GlobPattern GlobPattern `json:"globPattern"` +- // The kind of events of interest. If omitted it defaults +- // to WatchKind.Create | WatchKind.Change | WatchKind.Delete +- // which is 7. +- Kind *WatchKind `json:"kind,omitempty"` +-} - -- if obj != nil && obj.Parent() == types.Universe { -- // Defer call to builtinArgType so we can provide it the -- // inferred type from its parent node. -- defer func() { -- inf = c.builtinArgType(obj, node, inf) -- inf.objKind = c.builtinArgKind(ctx, obj, node) -- }() +-// Represents a folding range. To be valid, start and end line must be bigger than zero and smaller +-// than the number of lines in the document. Clients are free to ignore invalid ranges. +-type FoldingRange struct { +- // The zero-based start line of the range to fold. The folded area starts after the line's last character. +- // To be valid, the end must be zero or larger and smaller than the number of lines in the document. +- StartLine uint32 `json:"startLine"` +- // The zero-based character offset from where the folded range starts. If not defined, defaults to the length of the start line. +- StartCharacter uint32 `json:"startCharacter,omitempty"` +- // The zero-based end line of the range to fold. The folded area ends with the line's last character. +- // To be valid, the end must be zero or larger and smaller than the number of lines in the document. +- EndLine uint32 `json:"endLine"` +- // The zero-based character offset before the folded range ends. If not defined, defaults to the length of the end line. +- EndCharacter uint32 `json:"endCharacter,omitempty"` +- // Describes the kind of the folding range such as 'comment' or 'region'. The kind +- // is used to categorize folding ranges and used by commands like 'Fold all comments'. +- // See {@link FoldingRangeKind} for an enumeration of standardized kinds. +- Kind string `json:"kind,omitempty"` +- // The text that the client should show when the specified range is +- // collapsed. If not defined or not supported by the client, a default +- // will be chosen by the client. +- // +- // @since 3.17.0 +- CollapsedText string `json:"collapsedText,omitempty"` +-} +-type FoldingRangeClientCapabilities struct { +- // Whether implementation supports dynamic registration for folding range +- // providers. If this is set to `true` the client supports the new +- // `FoldingRangeRegistrationOptions` return value for the corresponding +- // server capability as well. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +- // The maximum number of folding ranges that the client prefers to receive +- // per document. The value serves as a hint, servers are free to follow the +- // limit. +- RangeLimit uint32 `json:"rangeLimit,omitempty"` +- // If set, the client signals that it only supports folding complete lines. +- // If set, client will ignore specified `startCharacter` and `endCharacter` +- // properties in a FoldingRange. +- LineFoldingOnly bool `json:"lineFoldingOnly,omitempty"` +- // Specific options for the folding range kind. +- // +- // @since 3.17.0 +- FoldingRangeKind *ClientFoldingRangeKindOptions `json:"foldingRangeKind,omitempty"` +- // Specific options for the folding range. +- // +- // @since 3.17.0 +- FoldingRange *ClientFoldingRangeOptions `json:"foldingRange,omitempty"` +-} - -- // The expected type of builtin arguments like append() is -- // the expected type of the builtin call itself. For -- // example: -- // -- // var foo []int = append(<>) -- // -- // To find the expected type at <> we "skip" the append() -- // node and get the expected type one level up, which is -- // []int. -- continue Nodes -- } -- } +-// A set of predefined range kinds. +-type FoldingRangeKind string +-type FoldingRangeOptions struct { +- WorkDoneProgressOptions +-} - -- return inf -- } -- case *ast.ReturnStmt: -- if c.enclosingFunc != nil { -- sig := c.enclosingFunc.sig -- // Find signature result that corresponds to our return statement. -- if resultIdx := exprAtPos(c.pos, node.Results); resultIdx < len(node.Results) { -- if resultIdx < sig.Results().Len() { -- inf.objType = sig.Results().At(resultIdx).Type() -- } -- } -- } -- return inf -- case *ast.CaseClause: -- if swtch, ok := findSwitchStmt(c.path[i+1:], c.pos, node).(*ast.SwitchStmt); ok { -- if tv, ok := c.pkg.GetTypesInfo().Types[swtch.Tag]; ok { -- inf.objType = tv.Type +-// Parameters for a {@link FoldingRangeRequest}. +-type FoldingRangeParams struct { +- // The text document. +- TextDocument TextDocumentIdentifier `json:"textDocument"` +- WorkDoneProgressParams +- PartialResultParams +-} +-type FoldingRangeRegistrationOptions struct { +- TextDocumentRegistrationOptions +- FoldingRangeOptions +- StaticRegistrationOptions +-} - -- // Record which objects have already been used in the case -- // statements so we don't suggest them again. -- for _, cc := range swtch.Body.List { -- for _, caseExpr := range cc.(*ast.CaseClause).List { -- // Don't record the expression we are currently completing. -- if caseExpr.Pos() < c.pos && c.pos <= caseExpr.End() { -- continue -- } +-// Client workspace capabilities specific to folding ranges +-// +-// @since 3.18.0 +-// @proposed +-type FoldingRangeWorkspaceClientCapabilities struct { +- // Whether the client implementation supports a refresh request sent from the +- // server to the client. +- // +- // Note that this event is global and will force the client to refresh all +- // folding ranges currently shown. It should be used with absolute care and is +- // useful for situation where a server for example detects a project wide +- // change that requires such a calculation. +- // +- // @since 3.18.0 +- // @proposed +- RefreshSupport bool `json:"refreshSupport,omitempty"` +-} - -- if objs := objChain(c.pkg.GetTypesInfo(), caseExpr); len(objs) > 0 { -- inf.penalized = append(inf.penalized, penalizedObj{objChain: objs, penalty: 0.1}) -- } -- } -- } -- } -- } -- return inf -- case *ast.SliceExpr: -- // Make sure position falls within the brackets (e.g. "foo[a:<>]"). -- if node.Lbrack < c.pos && c.pos <= node.Rbrack { -- inf.objType = types.Typ[types.UntypedInt] -- } -- return inf -- case *ast.IndexExpr: -- // Make sure position falls within the brackets (e.g. "foo[<>]"). -- if node.Lbrack < c.pos && c.pos <= node.Rbrack { -- if tv, ok := c.pkg.GetTypesInfo().Types[node.X]; ok { -- switch t := tv.Type.Underlying().(type) { -- case *types.Map: -- inf.objType = t.Key() -- case *types.Slice, *types.Array: -- inf.objType = types.Typ[types.UntypedInt] -- } +-// Value-object describing what options formatting should use. +-type FormattingOptions struct { +- // Size of a tab in spaces. +- TabSize uint32 `json:"tabSize"` +- // Prefer spaces over tabs. +- InsertSpaces bool `json:"insertSpaces"` +- // Trim trailing whitespace on a line. +- // +- // @since 3.15.0 +- TrimTrailingWhitespace bool `json:"trimTrailingWhitespace,omitempty"` +- // Insert a newline character at the end of the file if one does not exist. +- // +- // @since 3.15.0 +- InsertFinalNewline bool `json:"insertFinalNewline,omitempty"` +- // Trim all newlines after the final newline at the end of the file. +- // +- // @since 3.15.0 +- TrimFinalNewlines bool `json:"trimFinalNewlines,omitempty"` +-} - -- if ct := expectedConstraint(tv.Type, 0); ct != nil { -- inf.objType = ct -- inf.typeName.wantTypeName = true -- inf.typeName.isTypeParam = true -- } -- } -- } -- return inf -- case *typeparams.IndexListExpr: -- if node.Lbrack < c.pos && c.pos <= node.Rbrack { -- if tv, ok := c.pkg.GetTypesInfo().Types[node.X]; ok { -- if ct := expectedConstraint(tv.Type, exprAtPos(c.pos, node.Indices)); ct != nil { -- inf.objType = ct -- inf.typeName.wantTypeName = true -- inf.typeName.isTypeParam = true -- } -- } -- } -- return inf -- case *ast.SendStmt: -- // Make sure we are on right side of arrow (e.g. "foo <- <>"). -- if c.pos > node.Arrow+1 { -- if tv, ok := c.pkg.GetTypesInfo().Types[node.Chan]; ok { -- if ch, ok := tv.Type.Underlying().(*types.Chan); ok { -- inf.objType = ch.Elem() -- } -- } -- } -- return inf -- case *ast.RangeStmt: -- if source.NodeContains(node.X, c.pos) { -- inf.objKind |= kindSlice | kindArray | kindMap | kindString -- if node.Value == nil { -- inf.objKind |= kindChan -- } -- } -- return inf -- case *ast.StarExpr: -- inf.modifiers = append(inf.modifiers, typeMod{mod: dereference}) -- case *ast.UnaryExpr: -- switch node.Op { -- case token.AND: -- inf.modifiers = append(inf.modifiers, typeMod{mod: reference}) -- case token.ARROW: -- inf.modifiers = append(inf.modifiers, typeMod{mod: chanRead}) -- } -- case *ast.DeferStmt, *ast.GoStmt: -- inf.objKind |= kindFunc -- return inf -- default: -- if breaksExpectedTypeInference(node, c.pos) { -- return inf -- } -- } -- } +-// A diagnostic report with a full set of problems. +-// +-// @since 3.17.0 +-type FullDocumentDiagnosticReport struct { +- // A full document diagnostic report. +- Kind string `json:"kind"` +- // An optional result id. If provided it will +- // be sent on the next diagnostic request for the +- // same document. +- ResultID string `json:"resultId,omitempty"` +- // The actual items. +- Items []Diagnostic `json:"items"` +-} +- +-// General client capabilities. +-// +-// @since 3.16.0 +-type GeneralClientCapabilities struct { +- // Client capability that signals how the client +- // handles stale requests (e.g. a request +- // for which the client will not process the response +- // anymore since the information is outdated). +- // +- // @since 3.17.0 +- StaleRequestSupport *StaleRequestSupportOptions `json:"staleRequestSupport,omitempty"` +- // Client capabilities specific to regular expressions. +- // +- // @since 3.16.0 +- RegularExpressions *RegularExpressionsClientCapabilities `json:"regularExpressions,omitempty"` +- // Client capabilities specific to the client's markdown parser. +- // +- // @since 3.16.0 +- Markdown *MarkdownClientCapabilities `json:"markdown,omitempty"` +- // The position encodings supported by the client. Client and server +- // have to agree on the same position encoding to ensure that offsets +- // (e.g. character position in a line) are interpreted the same on both +- // sides. +- // +- // To keep the protocol backwards compatible the following applies: if +- // the value 'utf-16' is missing from the array of position encodings +- // servers can assume that the client supports UTF-16. UTF-16 is +- // therefore a mandatory encoding. +- // +- // If omitted it defaults to ['utf-16']. +- // +- // Implementation considerations: since the conversion from one encoding +- // into another requires the content of the file / line the conversion +- // is best done where the file is read which is usually on the server +- // side. +- // +- // @since 3.17.0 +- PositionEncodings []PositionEncodingKind `json:"positionEncodings,omitempty"` +-} - -- return inf +-// The glob pattern. Either a string pattern or a relative pattern. +-// +-// @since 3.17.0 +-type GlobPattern = Or_GlobPattern // (alias) +-// The result of a hover request. +-type Hover struct { +- // The hover's content +- Contents MarkupContent `json:"contents"` +- // An optional range inside the text document that is used to +- // visualize the hover, e.g. by changing the background color. +- Range Range `json:"range,omitempty"` +-} +-type HoverClientCapabilities struct { +- // Whether hover supports dynamic registration. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +- // Client supports the following content formats for the content +- // property. The order describes the preferred format of the client. +- ContentFormat []MarkupKind `json:"contentFormat,omitempty"` -} - --func (c *completer) expectedCallParamType(inf candidateInference, node *ast.CallExpr, sig *types.Signature) candidateInference { -- numParams := sig.Params().Len() -- if numParams == 0 { -- return inf -- } +-// Hover options. +-type HoverOptions struct { +- WorkDoneProgressOptions +-} - -- exprIdx := exprAtPos(c.pos, node.Args) +-// Parameters for a {@link HoverRequest}. +-type HoverParams struct { +- TextDocumentPositionParams +- WorkDoneProgressParams +-} - -- // If we have one or zero arg expressions, we may be -- // completing to a function call that returns multiple -- // values, in turn getting passed in to the surrounding -- // call. Record the assignees so we can favor function -- // calls that return matching values. -- if len(node.Args) <= 1 && exprIdx == 0 { -- for i := 0; i < sig.Params().Len(); i++ { -- inf.assignees = append(inf.assignees, sig.Params().At(i).Type()) -- } +-// Registration options for a {@link HoverRequest}. +-type HoverRegistrationOptions struct { +- TextDocumentRegistrationOptions +- HoverOptions +-} - -- // Record that we may be completing into variadic parameters. -- inf.variadicAssignees = sig.Variadic() -- } +-// @since 3.6.0 +-type ImplementationClientCapabilities struct { +- // Whether implementation supports dynamic registration. If this is set to `true` +- // the client supports the new `ImplementationRegistrationOptions` return value +- // for the corresponding server capability as well. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +- // The client supports additional metadata in the form of definition links. +- // +- // @since 3.14.0 +- LinkSupport bool `json:"linkSupport,omitempty"` +-} +-type ImplementationOptions struct { +- WorkDoneProgressOptions +-} +-type ImplementationParams struct { +- TextDocumentPositionParams +- WorkDoneProgressParams +- PartialResultParams +-} +-type ImplementationRegistrationOptions struct { +- TextDocumentRegistrationOptions +- ImplementationOptions +- StaticRegistrationOptions +-} - -- // Make sure not to run past the end of expected parameters. -- if exprIdx >= numParams { -- inf.objType = sig.Params().At(numParams - 1).Type() -- } else { -- inf.objType = sig.Params().At(exprIdx).Type() -- } +-// The data type of the ResponseError if the +-// initialize request fails. +-type InitializeError struct { +- // Indicates whether the client execute the following retry logic: +- // (1) show the message provided by the ResponseError to the user +- // (2) user selects retry or cancel +- // (3) if user selected retry the initialize method is sent again. +- Retry bool `json:"retry"` +-} +-type InitializeParams struct { +- XInitializeParams +- WorkspaceFoldersInitializeParams +-} - -- if sig.Variadic() && exprIdx >= (numParams-1) { -- // If we are completing a variadic param, deslice the variadic type. -- inf.objType = deslice(inf.objType) -- // Record whether we are completing the initial variadic param. -- inf.variadic = exprIdx == numParams-1 && len(node.Args) <= numParams +-// The result returned from an initialize request. +-type InitializeResult struct { +- // The capabilities the language server provides. +- Capabilities ServerCapabilities `json:"capabilities"` +- // Information about the server. +- // +- // @since 3.15.0 +- ServerInfo *ServerInfo `json:"serverInfo,omitempty"` +-} +-type InitializedParams struct { +-} - -- // Check if we can infer object kind from printf verb. -- inf.objKind |= printfArgKind(c.pkg.GetTypesInfo(), node, exprIdx) -- } +-// Inlay hint information. +-// +-// @since 3.17.0 +-type InlayHint struct { +- // The position of this hint. +- // +- // If multiple hints have the same position, they will be shown in the order +- // they appear in the response. +- Position Position `json:"position"` +- // The label of this hint. A human readable string or an array of +- // InlayHintLabelPart label parts. +- // +- // *Note* that neither the string nor the label part can be empty. +- Label []InlayHintLabelPart `json:"label"` +- // The kind of this hint. Can be omitted in which case the client +- // should fall back to a reasonable default. +- Kind InlayHintKind `json:"kind,omitempty"` +- // Optional text edits that are performed when accepting this inlay hint. +- // +- // *Note* that edits are expected to change the document so that the inlay +- // hint (or its nearest variant) is now part of the document and the inlay +- // hint itself is now obsolete. +- TextEdits []TextEdit `json:"textEdits,omitempty"` +- // The tooltip text when you hover over this item. +- Tooltip *OrPTooltip_textDocument_inlayHint `json:"tooltip,omitempty"` +- // Render padding before the hint. +- // +- // Note: Padding should use the editor's background color, not the +- // background color of the hint itself. That means padding can be used +- // to visually align/separate an inlay hint. +- PaddingLeft bool `json:"paddingLeft,omitempty"` +- // Render padding after the hint. +- // +- // Note: Padding should use the editor's background color, not the +- // background color of the hint itself. That means padding can be used +- // to visually align/separate an inlay hint. +- PaddingRight bool `json:"paddingRight,omitempty"` +- // A data entry field that is preserved on an inlay hint between +- // a `textDocument/inlayHint` and a `inlayHint/resolve` request. +- Data interface{} `json:"data,omitempty"` +-} - -- // If our expected type is an uninstantiated generic type param, -- // swap to the constraint which will do a decent job filtering -- // candidates. -- if tp, _ := inf.objType.(*typeparams.TypeParam); tp != nil { -- inf.objType = tp.Constraint() -- } +-// Inlay hint client capabilities. +-// +-// @since 3.17.0 +-type InlayHintClientCapabilities struct { +- // Whether inlay hints support dynamic registration. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +- // Indicates which properties a client can resolve lazily on an inlay +- // hint. +- ResolveSupport *ClientInlayHintResolveOptions `json:"resolveSupport,omitempty"` +-} - -- return inf +-// Inlay hint kinds. +-// +-// @since 3.17.0 +-type InlayHintKind uint32 +- +-// An inlay hint label part allows for interactive and composite labels +-// of inlay hints. +-// +-// @since 3.17.0 +-type InlayHintLabelPart struct { +- // The value of this label part. +- Value string `json:"value"` +- // The tooltip text when you hover over this label part. Depending on +- // the client capability `inlayHint.resolveSupport` clients might resolve +- // this property late using the resolve request. +- Tooltip *OrPTooltipPLabel `json:"tooltip,omitempty"` +- // An optional source code location that represents this +- // label part. +- // +- // The editor will use this location for the hover and for code navigation +- // features: This part will become a clickable link that resolves to the +- // definition of the symbol at the given location (not necessarily the +- // location itself), it shows the hover that shows at the given location, +- // and it shows a context menu with further code navigation commands. +- // +- // Depending on the client capability `inlayHint.resolveSupport` clients +- // might resolve this property late using the resolve request. +- Location *Location `json:"location,omitempty"` +- // An optional command for this label part. +- // +- // Depending on the client capability `inlayHint.resolveSupport` clients +- // might resolve this property late using the resolve request. +- Command *Command `json:"command,omitempty"` -} - --func expectedConstraint(t types.Type, idx int) types.Type { -- var tp *typeparams.TypeParamList -- if named, _ := t.(*types.Named); named != nil { -- tp = typeparams.ForNamed(named) -- } else if sig, _ := t.Underlying().(*types.Signature); sig != nil { -- tp = typeparams.ForSignature(sig) -- } -- if tp == nil || idx >= tp.Len() { -- return nil -- } -- return tp.At(idx).Constraint() +-// Inlay hint options used during static registration. +-// +-// @since 3.17.0 +-type InlayHintOptions struct { +- // The server provides support to resolve additional +- // information for an inlay hint item. +- ResolveProvider bool `json:"resolveProvider,omitempty"` +- WorkDoneProgressOptions -} - --// objChain decomposes e into a chain of objects if possible. For --// example, "foo.bar().baz" will yield []types.Object{foo, bar, baz}. --// If any part can't be turned into an object, return nil. --func objChain(info *types.Info, e ast.Expr) []types.Object { -- var objs []types.Object +-// A parameter literal used in inlay hint requests. +-// +-// @since 3.17.0 +-type InlayHintParams struct { +- // The text document. +- TextDocument TextDocumentIdentifier `json:"textDocument"` +- // The document range for which inlay hints should be computed. +- Range Range `json:"range"` +- WorkDoneProgressParams +-} - -- for e != nil { -- switch n := e.(type) { -- case *ast.Ident: -- obj := info.ObjectOf(n) -- if obj == nil { -- return nil -- } -- objs = append(objs, obj) -- e = nil -- case *ast.SelectorExpr: -- obj := info.ObjectOf(n.Sel) -- if obj == nil { -- return nil -- } -- objs = append(objs, obj) -- e = n.X -- case *ast.CallExpr: -- if len(n.Args) > 0 { -- return nil -- } -- e = n.Fun -- default: -- return nil -- } -- } +-// Inlay hint options used during static or dynamic registration. +-// +-// @since 3.17.0 +-type InlayHintRegistrationOptions struct { +- InlayHintOptions +- TextDocumentRegistrationOptions +- StaticRegistrationOptions +-} - -- // Reverse order so the layout matches the syntactic order. -- for i := 0; i < len(objs)/2; i++ { -- objs[i], objs[len(objs)-1-i] = objs[len(objs)-1-i], objs[i] -- } +-// Client workspace capabilities specific to inlay hints. +-// +-// @since 3.17.0 +-type InlayHintWorkspaceClientCapabilities struct { +- // Whether the client implementation supports a refresh request sent from +- // the server to the client. +- // +- // Note that this event is global and will force the client to refresh all +- // inlay hints currently shown. It should be used with absolute care and +- // is useful for situation where a server for example detects a project wide +- // change that requires such a calculation. +- RefreshSupport bool `json:"refreshSupport,omitempty"` +-} - -- return objs +-// Client capabilities specific to inline completions. +-// +-// @since 3.18.0 +-// @proposed +-type InlineCompletionClientCapabilities struct { +- // Whether implementation supports dynamic registration for inline completion providers. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` -} - --// applyTypeModifiers applies the list of type modifiers to a type. --// It returns nil if the modifiers could not be applied. --func (ci candidateInference) applyTypeModifiers(typ types.Type, addressable bool) types.Type { -- for _, mod := range ci.modifiers { -- switch mod.mod { -- case dereference: -- // For every "*" indirection operator, remove a pointer layer -- // from candidate type. -- if ptr, ok := typ.Underlying().(*types.Pointer); ok { -- typ = ptr.Elem() -- } else { -- return nil -- } -- case reference: -- // For every "&" address operator, add another pointer layer to -- // candidate type, if the candidate is addressable. -- if addressable { -- typ = types.NewPointer(typ) -- } else { -- return nil -- } -- case chanRead: -- // For every "<-" operator, remove a layer of channelness. -- if ch, ok := typ.(*types.Chan); ok { -- typ = ch.Elem() -- } else { -- return nil -- } -- } -- } +-// Provides information about the context in which an inline completion was requested. +-// +-// @since 3.18.0 +-// @proposed +-type InlineCompletionContext struct { +- // Describes how the inline completion was triggered. +- TriggerKind InlineCompletionTriggerKind `json:"triggerKind"` +- // Provides information about the currently selected item in the autocomplete widget if it is visible. +- SelectedCompletionInfo *SelectedCompletionInfo `json:"selectedCompletionInfo,omitempty"` +-} - -- return typ +-// An inline completion item represents a text snippet that is proposed inline to complete text that is being typed. +-// +-// @since 3.18.0 +-// @proposed +-type InlineCompletionItem struct { +- // The text to replace the range with. Must be set. +- InsertText Or_InlineCompletionItem_insertText `json:"insertText"` +- // A text that is used to decide if this inline completion should be shown. When `falsy` the {@link InlineCompletionItem.insertText} is used. +- FilterText string `json:"filterText,omitempty"` +- // The range to replace. Must begin and end on the same line. +- Range *Range `json:"range,omitempty"` +- // An optional {@link Command} that is executed *after* inserting this completion. +- Command *Command `json:"command,omitempty"` -} - --// applyTypeNameModifiers applies the list of type modifiers to a type name. --func (ci candidateInference) applyTypeNameModifiers(typ types.Type) types.Type { -- for _, mod := range ci.typeName.modifiers { -- switch mod.mod { -- case reference: -- typ = types.NewPointer(typ) -- case arrayType: -- typ = types.NewArray(typ, mod.arrayLen) -- case sliceType: -- typ = types.NewSlice(typ) -- } -- } -- return typ +-// Represents a collection of {@link InlineCompletionItem inline completion items} to be presented in the editor. +-// +-// @since 3.18.0 +-// @proposed +-type InlineCompletionList struct { +- // The inline completion items +- Items []InlineCompletionItem `json:"items"` -} - --// matchesVariadic returns true if we are completing a variadic --// parameter and candType is a compatible slice type. --func (ci candidateInference) matchesVariadic(candType types.Type) bool { -- return ci.variadic && ci.objType != nil && assignableTo(candType, types.NewSlice(ci.objType)) +-// Inline completion options used during static registration. +-// +-// @since 3.18.0 +-// @proposed +-type InlineCompletionOptions struct { +- WorkDoneProgressOptions -} - --// findSwitchStmt returns an *ast.CaseClause's corresponding *ast.SwitchStmt or --// *ast.TypeSwitchStmt. path should start from the case clause's first ancestor. --func findSwitchStmt(path []ast.Node, pos token.Pos, c *ast.CaseClause) ast.Stmt { -- // Make sure position falls within a "case <>:" clause. -- if exprAtPos(pos, c.List) >= len(c.List) { -- return nil -- } -- // A case clause is always nested within a block statement in a switch statement. -- if len(path) < 2 { -- return nil -- } -- if _, ok := path[0].(*ast.BlockStmt); !ok { -- return nil -- } -- switch s := path[1].(type) { -- case *ast.SwitchStmt: -- return s -- case *ast.TypeSwitchStmt: -- return s -- default: -- return nil -- } +-// A parameter literal used in inline completion requests. +-// +-// @since 3.18.0 +-// @proposed +-type InlineCompletionParams struct { +- // Additional information about the context in which inline completions were +- // requested. +- Context InlineCompletionContext `json:"context"` +- TextDocumentPositionParams +- WorkDoneProgressParams -} - --// breaksExpectedTypeInference reports if an expression node's type is unrelated --// to its child expression node types. For example, "Foo{Bar: x.Baz(<>)}" should --// expect a function argument, not a composite literal value. --func breaksExpectedTypeInference(n ast.Node, pos token.Pos) bool { -- switch n := n.(type) { -- case *ast.CompositeLit: -- // Doesn't break inference if pos is in type name. -- // For example: "Foo<>{Bar: 123}" -- return !source.NodeContains(n.Type, pos) -- case *ast.CallExpr: -- // Doesn't break inference if pos is in func name. -- // For example: "Foo<>(123)" -- return !source.NodeContains(n.Fun, pos) -- case *ast.FuncLit, *ast.IndexExpr, *ast.SliceExpr: -- return true -- default: -- return false -- } +-// Inline completion options used during static or dynamic registration. +-// +-// @since 3.18.0 +-// @proposed +-type InlineCompletionRegistrationOptions struct { +- InlineCompletionOptions +- TextDocumentRegistrationOptions +- StaticRegistrationOptions -} - --// expectTypeName returns information about the expected type name at position. --func expectTypeName(c *completer) typeNameInference { -- var inf typeNameInference +-// Describes how an {@link InlineCompletionItemProvider inline completion provider} was triggered. +-// +-// @since 3.18.0 +-// @proposed +-type InlineCompletionTriggerKind uint32 - --Nodes: -- for i, p := range c.path { -- switch n := p.(type) { -- case *ast.FieldList: -- // Expect a type name if pos is in a FieldList. This applies to -- // FuncType params/results, FuncDecl receiver, StructType, and -- // InterfaceType. We don't need to worry about the field name -- // because completion bails out early if pos is in an *ast.Ident -- // that defines an object. -- inf.wantTypeName = true -- break Nodes -- case *ast.CaseClause: -- // Expect type names in type switch case clauses. -- if swtch, ok := findSwitchStmt(c.path[i+1:], c.pos, n).(*ast.TypeSwitchStmt); ok { -- // The case clause types must be assertable from the type switch parameter. -- ast.Inspect(swtch.Assign, func(n ast.Node) bool { -- if ta, ok := n.(*ast.TypeAssertExpr); ok { -- inf.assertableFrom = c.pkg.GetTypesInfo().TypeOf(ta.X) -- return false -- } -- return true -- }) -- inf.wantTypeName = true +-// Inline value information can be provided by different means: +-// +-// - directly as a text value (class InlineValueText). +-// - as a name to use for a variable lookup (class InlineValueVariableLookup) +-// - as an evaluatable expression (class InlineValueEvaluatableExpression) +-// +-// The InlineValue types combines all inline value types into one type. +-// +-// @since 3.17.0 +-type InlineValue = Or_InlineValue // (alias) +-// Client capabilities specific to inline values. +-// +-// @since 3.17.0 +-type InlineValueClientCapabilities struct { +- // Whether implementation supports dynamic registration for inline value providers. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +-} - -- // Track the types that have already been used in this -- // switch's case statements so we don't recommend them. -- for _, e := range swtch.Body.List { -- for _, typeExpr := range e.(*ast.CaseClause).List { -- // Skip if type expression contains pos. We don't want to -- // count it as already used if the user is completing it. -- if typeExpr.Pos() < c.pos && c.pos <= typeExpr.End() { -- continue -- } +-// @since 3.17.0 +-type InlineValueContext struct { +- // The stack frame (as a DAP Id) where the execution has stopped. +- FrameID int32 `json:"frameId"` +- // The document range where execution has stopped. +- // Typically the end position of the range denotes the line where the inline values are shown. +- StoppedLocation Range `json:"stoppedLocation"` +-} - -- if t := c.pkg.GetTypesInfo().TypeOf(typeExpr); t != nil { -- inf.seenTypeSwitchCases = append(inf.seenTypeSwitchCases, t) -- } -- } -- } +-// Provide an inline value through an expression evaluation. +-// If only a range is specified, the expression will be extracted from the underlying document. +-// An optional expression can be used to override the extracted expression. +-// +-// @since 3.17.0 +-type InlineValueEvaluatableExpression struct { +- // The document range for which the inline value applies. +- // The range is used to extract the evaluatable expression from the underlying document. +- Range Range `json:"range"` +- // If specified the expression overrides the extracted expression. +- Expression string `json:"expression,omitempty"` +-} +- +-// Inline value options used during static registration. +-// +-// @since 3.17.0 +-type InlineValueOptions struct { +- WorkDoneProgressOptions +-} - -- break Nodes -- } -- return typeNameInference{} -- case *ast.TypeAssertExpr: -- // Expect type names in type assert expressions. -- if n.Lparen < c.pos && c.pos <= n.Rparen { -- // The type in parens must be assertable from the expression type. -- inf.assertableFrom = c.pkg.GetTypesInfo().TypeOf(n.X) -- inf.wantTypeName = true -- break Nodes -- } -- return typeNameInference{} -- case *ast.StarExpr: -- inf.modifiers = append(inf.modifiers, typeMod{mod: reference}) -- case *ast.CompositeLit: -- // We want a type name if position is in the "Type" part of a -- // composite literal (e.g. "Foo<>{}"). -- if n.Type != nil && n.Type.Pos() <= c.pos && c.pos <= n.Type.End() { -- inf.wantTypeName = true -- inf.compLitType = true +-// A parameter literal used in inline value requests. +-// +-// @since 3.17.0 +-type InlineValueParams struct { +- // The text document. +- TextDocument TextDocumentIdentifier `json:"textDocument"` +- // The document range for which inline values should be computed. +- Range Range `json:"range"` +- // Additional information about the context in which inline values were +- // requested. +- Context InlineValueContext `json:"context"` +- WorkDoneProgressParams +-} - -- if i < len(c.path)-1 { -- // Track preceding "&" operator. Technically it applies to -- // the composite literal and not the type name, but if -- // affects our type completion nonetheless. -- if u, ok := c.path[i+1].(*ast.UnaryExpr); ok && u.Op == token.AND { -- inf.modifiers = append(inf.modifiers, typeMod{mod: reference}) -- } -- } -- } -- break Nodes -- case *ast.ArrayType: -- // If we are inside the "Elt" part of an array type, we want a type name. -- if n.Elt.Pos() <= c.pos && c.pos <= n.Elt.End() { -- inf.wantTypeName = true -- if n.Len == nil { -- // No "Len" expression means a slice type. -- inf.modifiers = append(inf.modifiers, typeMod{mod: sliceType}) -- } else { -- // Try to get the array type using the constant value of "Len". -- tv, ok := c.pkg.GetTypesInfo().Types[n.Len] -- if ok && tv.Value != nil && tv.Value.Kind() == constant.Int { -- if arrayLen, ok := constant.Int64Val(tv.Value); ok { -- inf.modifiers = append(inf.modifiers, typeMod{mod: arrayType, arrayLen: arrayLen}) -- } -- } -- } +-// Inline value options used during static or dynamic registration. +-// +-// @since 3.17.0 +-type InlineValueRegistrationOptions struct { +- InlineValueOptions +- TextDocumentRegistrationOptions +- StaticRegistrationOptions +-} - -- // ArrayTypes can be nested, so keep going if our parent is an -- // ArrayType. -- if i < len(c.path)-1 { -- if _, ok := c.path[i+1].(*ast.ArrayType); ok { -- continue Nodes -- } -- } +-// Provide inline value as text. +-// +-// @since 3.17.0 +-type InlineValueText struct { +- // The document range for which the inline value applies. +- Range Range `json:"range"` +- // The text of the inline value. +- Text string `json:"text"` +-} - -- break Nodes -- } -- case *ast.MapType: -- inf.wantTypeName = true -- if n.Key != nil { -- inf.wantComparable = source.NodeContains(n.Key, c.pos) -- } else { -- // If the key is empty, assume we are completing the key if -- // pos is directly after the "map[". -- inf.wantComparable = c.pos == n.Pos()+token.Pos(len("map[")) -- } -- break Nodes -- case *ast.ValueSpec: -- inf.wantTypeName = source.NodeContains(n.Type, c.pos) -- break Nodes -- case *ast.TypeSpec: -- inf.wantTypeName = source.NodeContains(n.Type, c.pos) -- default: -- if breaksExpectedTypeInference(p, c.pos) { -- return typeNameInference{} -- } -- } -- } +-// Provide inline value through a variable lookup. +-// If only a range is specified, the variable name will be extracted from the underlying document. +-// An optional variable name can be used to override the extracted name. +-// +-// @since 3.17.0 +-type InlineValueVariableLookup struct { +- // The document range for which the inline value applies. +- // The range is used to extract the variable name from the underlying document. +- Range Range `json:"range"` +- // If specified the name of the variable to look up. +- VariableName string `json:"variableName,omitempty"` +- // How to perform the lookup. +- CaseSensitiveLookup bool `json:"caseSensitiveLookup"` +-} - -- return inf +-// Client workspace capabilities specific to inline values. +-// +-// @since 3.17.0 +-type InlineValueWorkspaceClientCapabilities struct { +- // Whether the client implementation supports a refresh request sent from the +- // server to the client. +- // +- // Note that this event is global and will force the client to refresh all +- // inline values currently shown. It should be used with absolute care and is +- // useful for situation where a server for example detects a project wide +- // change that requires such a calculation. +- RefreshSupport bool `json:"refreshSupport,omitempty"` -} - --func (c *completer) fakeObj(T types.Type) *types.Var { -- return types.NewVar(token.NoPos, c.pkg.GetTypes(), "", T) +-// A special text edit to provide an insert and a replace operation. +-// +-// @since 3.16.0 +-type InsertReplaceEdit struct { +- // The string to be inserted. +- NewText string `json:"newText"` +- // The range if the insert is requested +- Insert Range `json:"insert"` +- // The range if the replace is requested. +- Replace Range `json:"replace"` -} - --// derivableTypes iterates types you can derive from t. For example, --// from "foo" we might derive "&foo", and "foo()". --func derivableTypes(t types.Type, addressable bool, f func(t types.Type, addressable bool, mod typeModKind) bool) bool { -- switch t := t.Underlying().(type) { -- case *types.Signature: -- // If t is a func type with a single result, offer the result type. -- if t.Results().Len() == 1 && f(t.Results().At(0).Type(), false, invoke) { -- return true -- } -- case *types.Array: -- if f(t.Elem(), true, index) { -- return true -- } -- // Try converting array to slice. -- if f(types.NewSlice(t.Elem()), false, takeSlice) { -- return true -- } -- case *types.Pointer: -- if f(t.Elem(), false, dereference) { -- return true -- } -- case *types.Slice: -- if f(t.Elem(), true, index) { -- return true -- } -- case *types.Map: -- if f(t.Elem(), false, index) { -- return true -- } -- case *types.Chan: -- if f(t.Elem(), false, chanRead) { -- return true -- } -- } +-// Defines whether the insert text in a completion item should be interpreted as +-// plain text or a snippet. +-type InsertTextFormat uint32 - -- // Check if c is addressable and a pointer to c matches our type inference. -- if addressable && f(types.NewPointer(t), false, reference) { -- return true -- } +-// How whitespace and indentation is handled during completion +-// item insertion. +-// +-// @since 3.16.0 +-type InsertTextMode uint32 +-type LSPAny = interface{} - -- return false +-// LSP arrays. +-// @since 3.17.0 +-type LSPArray = []interface{} // (alias) +-type LSPErrorCodes int32 +- +-// LSP object definition. +-// @since 3.17.0 +-type LSPObject = map[string]LSPAny // (alias) +-// Predefined Language kinds +-// @since 3.18.0 +-// @proposed +-type LanguageKind string +- +-// Client capabilities for the linked editing range request. +-// +-// @since 3.16.0 +-type LinkedEditingRangeClientCapabilities struct { +- // Whether implementation supports dynamic registration. If this is set to `true` +- // the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` +- // return value for the corresponding server capability as well. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +-} +-type LinkedEditingRangeOptions struct { +- WorkDoneProgressOptions +-} +-type LinkedEditingRangeParams struct { +- TextDocumentPositionParams +- WorkDoneProgressParams +-} +-type LinkedEditingRangeRegistrationOptions struct { +- TextDocumentRegistrationOptions +- LinkedEditingRangeOptions +- StaticRegistrationOptions -} - --// anyCandType reports whether f returns true for any candidate type --// derivable from c. It searches up to three levels of type --// modification. For example, given "foo" we could discover "***foo" --// or "*foo()". --func (c *candidate) anyCandType(f func(t types.Type, addressable bool) bool) bool { -- if c.obj == nil || c.obj.Type() == nil { -- return false -- } +-// The result of a linked editing range request. +-// +-// @since 3.16.0 +-type LinkedEditingRanges struct { +- // A list of ranges that can be edited together. The ranges must have +- // identical length and contain identical text content. The ranges cannot overlap. +- Ranges []Range `json:"ranges"` +- // An optional word pattern (regular expression) that describes valid contents for +- // the given ranges. If no pattern is provided, the client configuration's word +- // pattern will be used. +- WordPattern string `json:"wordPattern,omitempty"` +-} - -- const maxDepth = 3 +-// created for Literal (Lit_ClientSemanticTokensRequestOptions_range_Item1) +-type Lit_ClientSemanticTokensRequestOptions_range_Item1 struct { +-} - -- var searchTypes func(t types.Type, addressable bool, mods []typeModKind) bool -- searchTypes = func(t types.Type, addressable bool, mods []typeModKind) bool { -- if f(t, addressable) { -- if len(mods) > 0 { -- newMods := make([]typeModKind, len(mods)+len(c.mods)) -- copy(newMods, mods) -- copy(newMods[len(mods):], c.mods) -- c.mods = newMods -- } -- return true -- } +-// Represents a location inside a resource, such as a line +-// inside a text file. +-type Location struct { +- URI DocumentURI `json:"uri"` +- Range Range `json:"range"` +-} - -- if len(mods) == maxDepth { -- return false -- } +-// Represents the connection of two locations. Provides additional metadata over normal {@link Location locations}, +-// including an origin range. +-type LocationLink struct { +- // Span of the origin of this link. +- // +- // Used as the underlined span for mouse interaction. Defaults to the word range at +- // the definition position. +- OriginSelectionRange *Range `json:"originSelectionRange,omitempty"` +- // The target resource identifier of this link. +- TargetURI DocumentURI `json:"targetUri"` +- // The full target range of this link. If the target for example is a symbol then target range is the +- // range enclosing this symbol not including leading/trailing whitespace but everything else +- // like comments. This information is typically used to highlight the range in the editor. +- TargetRange Range `json:"targetRange"` +- // The range that should be selected and revealed when this link is being followed, e.g the name of a function. +- // Must be contained by the `targetRange`. See also `DocumentSymbol#range` +- TargetSelectionRange Range `json:"targetSelectionRange"` +-} - -- return derivableTypes(t, addressable, func(t types.Type, addressable bool, mod typeModKind) bool { -- return searchTypes(t, addressable, append(mods, mod)) -- }) -- } +-// Location with only uri and does not include range. +-// +-// @since 3.18.0 +-// @proposed +-type LocationUriOnly struct { +- URI DocumentURI `json:"uri"` +-} - -- return searchTypes(c.obj.Type(), c.addressable, make([]typeModKind, 0, maxDepth)) +-// The log message parameters. +-type LogMessageParams struct { +- // The message type. See {@link MessageType} +- Type MessageType `json:"type"` +- // The actual message. +- Message string `json:"message"` +-} +-type LogTraceParams struct { +- Message string `json:"message"` +- Verbose string `json:"verbose,omitempty"` -} - --// matchingCandidate reports whether cand matches our type inferences. --// It mutates cand's score in certain cases. --func (c *completer) matchingCandidate(cand *candidate) bool { -- if c.completionContext.commentCompletion { -- return false -- } +-// Client capabilities specific to the used markdown parser. +-// +-// @since 3.16.0 +-type MarkdownClientCapabilities struct { +- // The name of the parser. +- Parser string `json:"parser"` +- // The version of the parser. +- Version string `json:"version,omitempty"` +- // A list of HTML tags that the client allows / supports in +- // Markdown. +- // +- // @since 3.17.0 +- AllowedTags []string `json:"allowedTags,omitempty"` +-} - -- // Bail out early if we are completing a field name in a composite literal. -- if v, ok := cand.obj.(*types.Var); ok && v.IsField() && c.wantStructFieldCompletions() { -- return true -- } +-// MarkedString can be used to render human readable text. It is either a markdown string +-// or a code-block that provides a language and a code snippet. The language identifier +-// is semantically equal to the optional language identifier in fenced code blocks in GitHub +-// issues. See https://help.github.com/articles/creating-and-highlighting-code-blocks/#syntax-highlighting +-// +-// The pair of a language and a value is an equivalent to markdown: +-// ```${language} +-// ${value} +-// ``` +-// +-// Note that markdown strings will be sanitized - that means html will be escaped. +-// @deprecated use MarkupContent instead. +-type MarkedString = Or_MarkedString // (alias) +-// @since 3.18.0 +-// @proposed +-// @deprecated use MarkupContent instead. +-type MarkedStringWithLanguage struct { +- Language string `json:"language"` +- Value string `json:"value"` +-} - -- if isTypeName(cand.obj) { -- return c.matchingTypeName(cand) -- } else if c.wantTypeName() { -- // If we want a type, a non-type object never matches. -- return false -- } +-// A `MarkupContent` literal represents a string value which content is interpreted base on its +-// kind flag. Currently the protocol supports `plaintext` and `markdown` as markup kinds. +-// +-// If the kind is `markdown` then the value can contain fenced code blocks like in GitHub issues. +-// See https://help.github.com/articles/creating-and-highlighting-code-blocks/#syntax-highlighting +-// +-// Here is an example how such a string can be constructed using JavaScript / TypeScript: +-// ```ts +-// +-// let markdown: MarkdownContent = { +-// kind: MarkupKind.Markdown, +-// value: [ +-// '# Header', +-// 'Some text', +-// '```typescript', +-// 'someCode();', +-// '```' +-// ].join('\n') +-// }; +-// +-// ``` +-// +-// *Please Note* that clients might sanitize the return markdown. A client could decide to +-// remove HTML from the markdown to avoid script execution. +-type MarkupContent struct { +- // The type of the Markup +- Kind MarkupKind `json:"kind"` +- // The content itself +- Value string `json:"value"` +-} - -- if c.inference.candTypeMatches(cand) { -- return true -- } +-// Describes the content type that a client supports in various +-// result literals like `Hover`, `ParameterInfo` or `CompletionItem`. +-// +-// Please note that `MarkupKinds` must not start with a `$`. This kinds +-// are reserved for internal usage. +-type MarkupKind string +-type MessageActionItem struct { +- // A short title like 'Retry', 'Open Log' etc. +- Title string `json:"title"` +-} - -- candType := cand.obj.Type() -- if candType == nil { -- return false -- } +-// The message type +-type MessageType uint32 - -- if sig, ok := candType.Underlying().(*types.Signature); ok { -- if c.inference.assigneesMatch(cand, sig) { -- // Invoke the candidate if its results are multi-assignable. -- cand.mods = append(cand.mods, invoke) -- return true -- } -- } +-// Moniker definition to match LSIF 0.5 moniker definition. +-// +-// @since 3.16.0 +-type Moniker struct { +- // The scheme of the moniker. For example tsc or .Net +- Scheme string `json:"scheme"` +- // The identifier of the moniker. The value is opaque in LSIF however +- // schema owners are allowed to define the structure if they want. +- Identifier string `json:"identifier"` +- // The scope in which the moniker is unique +- Unique UniquenessLevel `json:"unique"` +- // The moniker kind if known. +- Kind *MonikerKind `json:"kind,omitempty"` +-} - -- // Default to invoking *types.Func candidates. This is so function -- // completions in an empty statement (or other cases with no expected type) -- // are invoked by default. -- if isFunc(cand.obj) { -- cand.mods = append(cand.mods, invoke) -- } +-// Client capabilities specific to the moniker request. +-// +-// @since 3.16.0 +-type MonikerClientCapabilities struct { +- // Whether moniker supports dynamic registration. If this is set to `true` +- // the client supports the new `MonikerRegistrationOptions` return value +- // for the corresponding server capability as well. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +-} - -- return false +-// The moniker kind. +-// +-// @since 3.16.0 +-type MonikerKind string +-type MonikerOptions struct { +- WorkDoneProgressOptions +-} +-type MonikerParams struct { +- TextDocumentPositionParams +- WorkDoneProgressParams +- PartialResultParams +-} +-type MonikerRegistrationOptions struct { +- TextDocumentRegistrationOptions +- MonikerOptions -} - --// candTypeMatches reports whether cand makes a good completion --// candidate given the candidate inference. cand's score may be --// mutated to downrank the candidate in certain situations. --func (ci *candidateInference) candTypeMatches(cand *candidate) bool { -- var ( -- expTypes = make([]types.Type, 0, 2) -- variadicType types.Type -- ) -- if ci.objType != nil { -- expTypes = append(expTypes, ci.objType) +-// A notebook cell. +-// +-// A cell's document URI must be unique across ALL notebook +-// cells and can therefore be used to uniquely identify a +-// notebook cell or the cell's text document. +-// +-// @since 3.17.0 +-type NotebookCell struct { +- // The cell's kind +- Kind NotebookCellKind `json:"kind"` +- // The URI of the cell's text document +- // content. +- Document DocumentURI `json:"document"` +- // Additional metadata stored with the cell. +- // +- // Note: should always be an object literal (e.g. LSPObject) +- Metadata *LSPObject `json:"metadata,omitempty"` +- // Additional execution summary information +- // if supported by the client. +- ExecutionSummary *ExecutionSummary `json:"executionSummary,omitempty"` +-} - -- if ci.variadic { -- variadicType = types.NewSlice(ci.objType) -- expTypes = append(expTypes, variadicType) -- } -- } +-// A change describing how to move a `NotebookCell` +-// array from state S to S'. +-// +-// @since 3.17.0 +-type NotebookCellArrayChange struct { +- // The start oftest of the cell that changed. +- Start uint32 `json:"start"` +- // The deleted cells +- DeleteCount uint32 `json:"deleteCount"` +- // The new cells, if any +- Cells []NotebookCell `json:"cells,omitempty"` +-} - -- return cand.anyCandType(func(candType types.Type, addressable bool) bool { -- // Take into account any type modifiers on the expected type. -- candType = ci.applyTypeModifiers(candType, addressable) -- if candType == nil { -- return false -- } +-// A notebook cell kind. +-// +-// @since 3.17.0 +-type NotebookCellKind uint32 - -- if ci.convertibleTo != nil && convertibleTo(candType, ci.convertibleTo) { -- return true -- } +-// @since 3.18.0 +-// @proposed +-type NotebookCellLanguage struct { +- Language string `json:"language"` +-} - -- for _, expType := range expTypes { -- if isEmptyInterface(expType) { -- continue -- } +-// A notebook cell text document filter denotes a cell text +-// document by different properties. +-// +-// @since 3.17.0 +-type NotebookCellTextDocumentFilter struct { +- // A filter that matches against the notebook +- // containing the notebook cell. If a string +- // value is provided it matches against the +- // notebook type. '*' matches every notebook. +- Notebook Or_NotebookCellTextDocumentFilter_notebook `json:"notebook"` +- // A language id like `python`. +- // +- // Will be matched against the language id of the +- // notebook cell document. '*' matches every language. +- Language string `json:"language,omitempty"` +-} - -- matches := ci.typeMatches(expType, candType) -- if !matches { -- // If candType doesn't otherwise match, consider if we can -- // convert candType directly to expType. -- if considerTypeConversion(candType, expType, cand.path) { -- cand.convertTo = expType -- // Give a major score penalty so we always prefer directly -- // assignable candidates, all else equal. -- cand.score *= 0.5 -- return true -- } +-// A notebook document. +-// +-// @since 3.17.0 +-type NotebookDocument struct { +- // The notebook document's uri. +- URI URI `json:"uri"` +- // The type of the notebook. +- NotebookType string `json:"notebookType"` +- // The version number of this document (it will increase after each +- // change, including undo/redo). +- Version int32 `json:"version"` +- // Additional metadata stored with the notebook +- // document. +- // +- // Note: should always be an object literal (e.g. LSPObject) +- Metadata *LSPObject `json:"metadata,omitempty"` +- // The cells of a notebook. +- Cells []NotebookCell `json:"cells"` +-} - -- continue -- } +-// Structural changes to cells in a notebook document. +-// +-// @since 3.18.0 +-// @proposed +-type NotebookDocumentCellChangeStructure struct { +- // The change to the cell array. +- Array NotebookCellArrayChange `json:"array"` +- // Additional opened cell text documents. +- DidOpen []TextDocumentItem `json:"didOpen,omitempty"` +- // Additional closed cell text documents. +- DidClose []TextDocumentIdentifier `json:"didClose,omitempty"` +-} - -- if expType == variadicType { -- cand.mods = append(cand.mods, takeDotDotDot) -- } +-// Cell changes to a notebook document. +-// +-// @since 3.18.0 +-// @proposed +-type NotebookDocumentCellChanges struct { +- // Changes to the cell structure to add or +- // remove cells. +- Structure *NotebookDocumentCellChangeStructure `json:"structure,omitempty"` +- // Changes to notebook cells properties like its +- // kind, execution summary or metadata. +- Data []NotebookCell `json:"data,omitempty"` +- // Changes to the text content of notebook cells. +- TextContent []NotebookDocumentCellContentChanges `json:"textContent,omitempty"` +-} - -- // Lower candidate score for untyped conversions. This avoids -- // ranking untyped constants above candidates with an exact type -- // match. Don't lower score of builtin constants, e.g. "true". -- if isUntyped(candType) && !types.Identical(candType, expType) && cand.obj.Parent() != types.Universe { -- // Bigger penalty for deep completions into other packages to -- // avoid random constants from other packages popping up all -- // the time. -- if len(cand.path) > 0 && isPkgName(cand.path[0]) { -- cand.score *= 0.5 -- } else { -- cand.score *= 0.75 -- } -- } +-// Content changes to a cell in a notebook document. +-// +-// @since 3.18.0 +-// @proposed +-type NotebookDocumentCellContentChanges struct { +- Document VersionedTextDocumentIdentifier `json:"document"` +- Changes []TextDocumentContentChangeEvent `json:"changes"` +-} - -- return true -- } +-// A change event for a notebook document. +-// +-// @since 3.17.0 +-type NotebookDocumentChangeEvent struct { +- // The changed meta data if any. +- // +- // Note: should always be an object literal (e.g. LSPObject) +- Metadata *LSPObject `json:"metadata,omitempty"` +- // Changes to cells +- Cells *NotebookDocumentCellChanges `json:"cells,omitempty"` +-} - -- // If we don't have a specific expected type, fall back to coarser -- // object kind checks. -- if ci.objType == nil || isEmptyInterface(ci.objType) { -- // If we were able to apply type modifiers to our candidate type, -- // count that as a match. For example: -- // -- // var foo chan int -- // <-fo<> -- // -- // We were able to apply the "<-" type modifier to "foo", so "foo" -- // matches. -- if len(ci.modifiers) > 0 { -- return true -- } +-// Capabilities specific to the notebook document support. +-// +-// @since 3.17.0 +-type NotebookDocumentClientCapabilities struct { +- // Capabilities specific to notebook document synchronization +- // +- // @since 3.17.0 +- Synchronization NotebookDocumentSyncClientCapabilities `json:"synchronization"` +-} - -- // If we didn't have an exact type match, check if our object kind -- // matches. -- if ci.kindMatches(candType) { -- if ci.objKind == kindFunc { -- cand.mods = append(cand.mods, invoke) -- } -- return true -- } -- } +-// A notebook document filter denotes a notebook document by +-// different properties. The properties will be match +-// against the notebook's URI (same as with documents) +-// +-// @since 3.17.0 +-type NotebookDocumentFilter = Or_NotebookDocumentFilter // (alias) +-// A notebook document filter where `notebookType` is required field. +-// +-// @since 3.18.0 +-// @proposed +-type NotebookDocumentFilterNotebookType struct { +- // The type of the enclosing notebook. +- NotebookType string `json:"notebookType"` +- // A Uri {@link Uri.scheme scheme}, like `file` or `untitled`. +- Scheme string `json:"scheme,omitempty"` +- // A glob pattern. +- Pattern string `json:"pattern,omitempty"` +-} - -- return false -- }) +-// A notebook document filter where `pattern` is required field. +-// +-// @since 3.18.0 +-// @proposed +-type NotebookDocumentFilterPattern struct { +- // The type of the enclosing notebook. +- NotebookType string `json:"notebookType,omitempty"` +- // A Uri {@link Uri.scheme scheme}, like `file` or `untitled`. +- Scheme string `json:"scheme,omitempty"` +- // A glob pattern. +- Pattern string `json:"pattern"` -} - --// considerTypeConversion returns true if we should offer a completion --// automatically converting "from" to "to". --func considerTypeConversion(from, to types.Type, path []types.Object) bool { -- // Don't offer to convert deep completions from other packages. -- // Otherwise there are many random package level consts/vars that -- // pop up as candidates all the time. -- if len(path) > 0 && isPkgName(path[0]) { -- return false -- } +-// A notebook document filter where `scheme` is required field. +-// +-// @since 3.18.0 +-// @proposed +-type NotebookDocumentFilterScheme struct { +- // The type of the enclosing notebook. +- NotebookType string `json:"notebookType,omitempty"` +- // A Uri {@link Uri.scheme scheme}, like `file` or `untitled`. +- Scheme string `json:"scheme"` +- // A glob pattern. +- Pattern string `json:"pattern,omitempty"` +-} - -- if _, ok := from.(*typeparams.TypeParam); ok { -- return false -- } +-// @since 3.18.0 +-// @proposed +-type NotebookDocumentFilterWithCells struct { +- // The notebook to be synced If a string +- // value is provided it matches against the +- // notebook type. '*' matches every notebook. +- Notebook *Or_NotebookDocumentFilterWithCells_notebook `json:"notebook,omitempty"` +- // The cells of the matching notebook to be synced. +- Cells []NotebookCellLanguage `json:"cells"` +-} - -- if !convertibleTo(from, to) { -- return false -- } +-// @since 3.18.0 +-// @proposed +-type NotebookDocumentFilterWithNotebook struct { +- // The notebook to be synced If a string +- // value is provided it matches against the +- // notebook type. '*' matches every notebook. +- Notebook Or_NotebookDocumentFilterWithNotebook_notebook `json:"notebook"` +- // The cells of the matching notebook to be synced. +- Cells []NotebookCellLanguage `json:"cells,omitempty"` +-} - -- // Don't offer to convert ints to strings since that probably -- // doesn't do what the user wants. -- if isBasicKind(from, types.IsInteger) && isBasicKind(to, types.IsString) { -- return false -- } +-// A literal to identify a notebook document in the client. +-// +-// @since 3.17.0 +-type NotebookDocumentIdentifier struct { +- // The notebook document's uri. +- URI URI `json:"uri"` +-} - -- return true +-// Notebook specific client capabilities. +-// +-// @since 3.17.0 +-type NotebookDocumentSyncClientCapabilities struct { +- // Whether implementation supports dynamic registration. If this is +- // set to `true` the client supports the new +- // `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` +- // return value for the corresponding server capability as well. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +- // The client supports sending execution summary data per cell. +- ExecutionSummarySupport bool `json:"executionSummarySupport,omitempty"` -} - --// typeMatches reports whether an object of candType makes a good --// completion candidate given the expected type expType. --func (ci *candidateInference) typeMatches(expType, candType types.Type) bool { -- // Handle untyped values specially since AssignableTo gives false negatives -- // for them (see https://golang.org/issue/32146). -- if candBasic, ok := candType.Underlying().(*types.Basic); ok { -- if expBasic, ok := expType.Underlying().(*types.Basic); ok { -- // Note that the candidate and/or the expected can be untyped. -- // In "fo<> == 100" the expected type is untyped, and the -- // candidate could also be an untyped constant. +-// Options specific to a notebook plus its cells +-// to be synced to the server. +-// +-// If a selector provides a notebook document +-// filter but no cell selector all cells of a +-// matching notebook document will be synced. +-// +-// If a selector provides no notebook document +-// filter but only a cell selector all notebook +-// document that contain at least one matching +-// cell will be synced. +-// +-// @since 3.17.0 +-type NotebookDocumentSyncOptions struct { +- // The notebooks to be synced +- NotebookSelector []Or_NotebookDocumentSyncOptions_notebookSelector_Elem `json:"notebookSelector"` +- // Whether save notification should be forwarded to +- // the server. Will only be honored if mode === `notebook`. +- Save bool `json:"save,omitempty"` +-} - -- // Sort by is_untyped and then by is_int to simplify below logic. -- a, b := candBasic.Info(), expBasic.Info() -- if a&types.IsUntyped == 0 || (b&types.IsInteger > 0 && b&types.IsUntyped > 0) { -- a, b = b, a -- } +-// Registration options specific to a notebook. +-// +-// @since 3.17.0 +-type NotebookDocumentSyncRegistrationOptions struct { +- NotebookDocumentSyncOptions +- StaticRegistrationOptions +-} - -- // If at least one is untyped... -- if a&types.IsUntyped > 0 { -- switch { -- // Untyped integers are compatible with floats. -- case a&types.IsInteger > 0 && b&types.IsFloat > 0: -- return true +-// A text document identifier to optionally denote a specific version of a text document. +-type OptionalVersionedTextDocumentIdentifier struct { +- // The version number of this document. If a versioned text document identifier +- // is sent from the server to the client and the file is not open in the editor +- // (the server has not received an open notification before) the server can send +- // `null` to indicate that the version is unknown and the content on disk is the +- // truth (as specified with document content ownership). +- Version int32 `json:"version"` +- TextDocumentIdentifier +-} - -- // Check if their constant kind (bool|int|float|complex|string) matches. -- // This doesn't take into account the constant value, so there will be some -- // false positives due to integer sign and overflow. -- case a&types.IsConstType == b&types.IsConstType: -- return true -- } -- } -- } -- } +-// created for Or [Location LocationUriOnly] +-type OrPLocation_workspace_symbol struct { +- Value interface{} `json:"value"` +-} - -- // AssignableTo covers the case where the types are equal, but also handles -- // cases like assigning a concrete type to an interface type. -- return assignableTo(candType, expType) +-// created for Or [[]string string] +-type OrPSection_workspace_didChangeConfiguration struct { +- Value interface{} `json:"value"` -} - --// kindMatches reports whether candType's kind matches our expected --// kind (e.g. slice, map, etc.). --func (ci *candidateInference) kindMatches(candType types.Type) bool { -- return ci.objKind > 0 && ci.objKind&candKind(candType) > 0 +-// created for Or [MarkupContent string] +-type OrPTooltipPLabel struct { +- Value interface{} `json:"value"` -} - --// assigneesMatch reports whether an invocation of sig matches the --// number and type of any assignees. --func (ci *candidateInference) assigneesMatch(cand *candidate, sig *types.Signature) bool { -- if len(ci.assignees) == 0 { -- return false -- } +-// created for Or [MarkupContent string] +-type OrPTooltip_textDocument_inlayHint struct { +- Value interface{} `json:"value"` +-} - -- // Uniresult functions are always usable and are handled by the -- // normal, non-assignees type matching logic. -- if sig.Results().Len() == 1 { -- return false -- } +-// created for Or [int32 string] +-type Or_CancelParams_id struct { +- Value interface{} `json:"value"` +-} - -- // Don't prefer completing into func(...interface{}) calls since all -- // functions would match. -- if ci.variadicAssignees && len(ci.assignees) == 1 && isEmptyInterface(deslice(ci.assignees[0])) { -- return false -- } +-// created for Or [ClientSemanticTokensRequestFullDelta bool] +-type Or_ClientSemanticTokensRequestOptions_full struct { +- Value interface{} `json:"value"` +-} - -- var numberOfResultsCouldMatch bool -- if ci.variadicAssignees { -- numberOfResultsCouldMatch = sig.Results().Len() >= len(ci.assignees)-1 -- } else { -- numberOfResultsCouldMatch = sig.Results().Len() == len(ci.assignees) -- } +-// created for Or [Lit_ClientSemanticTokensRequestOptions_range_Item1 bool] +-type Or_ClientSemanticTokensRequestOptions_range struct { +- Value interface{} `json:"value"` +-} - -- // If our signature doesn't return the right number of values, it's -- // not a match, so downrank it. For example: -- // -- // var foo func() (int, int) -- // a, b, c := <> // downrank "foo()" since it only returns two values -- if !numberOfResultsCouldMatch { -- cand.score /= 2 -- return false -- } +-// created for Or [EditRangeWithInsertReplace Range] +-type Or_CompletionItemDefaults_editRange struct { +- Value interface{} `json:"value"` +-} - -- // If at least one assignee has a valid type, and all valid -- // assignees match the corresponding sig result value, the signature -- // is a match. -- allMatch := false -- for i := 0; i < sig.Results().Len(); i++ { -- var assignee types.Type +-// created for Or [MarkupContent string] +-type Or_CompletionItem_documentation struct { +- Value interface{} `json:"value"` +-} - -- // If we are completing into variadic parameters, deslice the -- // expected variadic type. -- if ci.variadicAssignees && i >= len(ci.assignees)-1 { -- assignee = ci.assignees[len(ci.assignees)-1] -- if elem := deslice(assignee); elem != nil { -- assignee = elem -- } -- } else { -- assignee = ci.assignees[i] -- } +-// created for Or [InsertReplaceEdit TextEdit] +-type Or_CompletionItem_textEdit struct { +- Value interface{} `json:"value"` +-} - -- if assignee == nil || assignee == types.Typ[types.Invalid] { -- continue -- } +-// created for Or [Location []Location] +-type Or_Definition struct { +- Value interface{} `json:"value"` +-} - -- allMatch = ci.typeMatches(assignee, sig.Results().At(i).Type()) -- if !allMatch { -- break -- } -- } -- return allMatch +-// created for Or [int32 string] +-type Or_Diagnostic_code struct { +- Value interface{} `json:"value"` -} - --func (c *completer) matchingTypeName(cand *candidate) bool { -- if !c.wantTypeName() { -- return false -- } +-// created for Or [RelatedFullDocumentDiagnosticReport RelatedUnchangedDocumentDiagnosticReport] +-type Or_DocumentDiagnosticReport struct { +- Value interface{} `json:"value"` +-} - -- typeMatches := func(candType types.Type) bool { -- // Take into account any type name modifier prefixes. -- candType = c.inference.applyTypeNameModifiers(candType) +-// created for Or [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport] +-type Or_DocumentDiagnosticReportPartialResult_relatedDocuments_Value struct { +- Value interface{} `json:"value"` +-} - -- if from := c.inference.typeName.assertableFrom; from != nil { -- // Don't suggest the starting type in type assertions. For example, -- // if "foo" is an io.Writer, don't suggest "foo.(io.Writer)". -- if types.Identical(from, candType) { -- return false -- } +-// created for Or [NotebookCellTextDocumentFilter TextDocumentFilter] +-type Or_DocumentFilter struct { +- Value interface{} `json:"value"` +-} - -- if intf, ok := from.Underlying().(*types.Interface); ok { -- if !types.AssertableTo(intf, candType) { -- return false -- } -- } -- } +-// created for Or [Pattern RelativePattern] +-type Or_GlobPattern struct { +- Value interface{} `json:"value"` +-} - -- if c.inference.typeName.wantComparable && !types.Comparable(candType) { -- return false -- } +-// created for Or [MarkedString MarkupContent []MarkedString] +-type Or_Hover_contents struct { +- Value interface{} `json:"value"` +-} - -- // Skip this type if it has already been used in another type -- // switch case. -- for _, seen := range c.inference.typeName.seenTypeSwitchCases { -- if types.Identical(candType, seen) { -- return false -- } -- } +-// created for Or [[]InlayHintLabelPart string] +-type Or_InlayHint_label struct { +- Value interface{} `json:"value"` +-} - -- // We can expect a type name and have an expected type in cases like: -- // -- // var foo []int -- // foo = []i<> -- // -- // Where our expected type is "[]int", and we expect a type name. -- if c.inference.objType != nil { -- return assignableTo(candType, c.inference.objType) -- } +-// created for Or [StringValue string] +-type Or_InlineCompletionItem_insertText struct { +- Value interface{} `json:"value"` +-} - -- // Default to saying any type name is a match. -- return true -- } +-// created for Or [InlineValueEvaluatableExpression InlineValueText InlineValueVariableLookup] +-type Or_InlineValue struct { +- Value interface{} `json:"value"` +-} - -- t := cand.obj.Type() +-// created for Or [MarkedStringWithLanguage string] +-type Or_MarkedString struct { +- Value interface{} `json:"value"` +-} - -- if typeMatches(t) { -- return true -- } +-// created for Or [NotebookDocumentFilter string] +-type Or_NotebookCellTextDocumentFilter_notebook struct { +- Value interface{} `json:"value"` +-} - -- if !types.IsInterface(t) && typeMatches(types.NewPointer(t)) { -- if c.inference.typeName.compLitType { -- // If we are completing a composite literal type as in -- // "foo<>{}", to make a pointer we must prepend "&". -- cand.mods = append(cand.mods, reference) -- } else { -- // If we are completing a normal type name such as "foo<>", to -- // make a pointer we must prepend "*". -- cand.mods = append(cand.mods, dereference) -- } -- return true -- } +-// created for Or [NotebookDocumentFilterNotebookType NotebookDocumentFilterPattern NotebookDocumentFilterScheme] +-type Or_NotebookDocumentFilter struct { +- Value interface{} `json:"value"` +-} - -- return false +-// created for Or [NotebookDocumentFilter string] +-type Or_NotebookDocumentFilterWithCells_notebook struct { +- Value interface{} `json:"value"` -} - --var ( -- // "interface { Error() string }" (i.e. error) -- errorIntf = types.Universe.Lookup("error").Type().Underlying().(*types.Interface) +-// created for Or [NotebookDocumentFilter string] +-type Or_NotebookDocumentFilterWithNotebook_notebook struct { +- Value interface{} `json:"value"` +-} - -- // "interface { String() string }" (i.e. fmt.Stringer) -- stringerIntf = types.NewInterfaceType([]*types.Func{ -- types.NewFunc(token.NoPos, nil, "String", types.NewSignature( -- nil, -- nil, -- types.NewTuple(types.NewParam(token.NoPos, nil, "", types.Typ[types.String])), -- false, -- )), -- }, nil).Complete() +-// created for Or [NotebookDocumentFilterWithCells NotebookDocumentFilterWithNotebook] +-type Or_NotebookDocumentSyncOptions_notebookSelector_Elem struct { +- Value interface{} `json:"value"` +-} - -- byteType = types.Universe.Lookup("byte").Type() --) +-// created for Or [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport] +-type Or_RelatedFullDocumentDiagnosticReport_relatedDocuments_Value struct { +- Value interface{} `json:"value"` +-} - --// candKind returns the objKind of candType, if any. --func candKind(candType types.Type) objKind { -- var kind objKind +-// created for Or [FullDocumentDiagnosticReport UnchangedDocumentDiagnosticReport] +-type Or_RelatedUnchangedDocumentDiagnosticReport_relatedDocuments_Value struct { +- Value interface{} `json:"value"` +-} - -- switch t := candType.Underlying().(type) { -- case *types.Array: -- kind |= kindArray -- if t.Elem() == byteType { -- kind |= kindBytes -- } -- case *types.Slice: -- kind |= kindSlice -- if t.Elem() == byteType { -- kind |= kindBytes -- } -- case *types.Chan: -- kind |= kindChan -- case *types.Map: -- kind |= kindMap -- case *types.Pointer: -- kind |= kindPtr +-// created for Or [CodeAction Command] +-type Or_Result_textDocument_codeAction_Item0_Elem struct { +- Value interface{} `json:"value"` +-} - -- // Some builtins handle array pointers as arrays, so just report a pointer -- // to an array as an array. -- if _, isArray := t.Elem().Underlying().(*types.Array); isArray { -- kind |= kindArray -- } -- case *types.Basic: -- switch info := t.Info(); { -- case info&types.IsString > 0: -- kind |= kindString -- case info&types.IsInteger > 0: -- kind |= kindInt -- case info&types.IsFloat > 0: -- kind |= kindFloat -- case info&types.IsComplex > 0: -- kind |= kindComplex -- case info&types.IsBoolean > 0: -- kind |= kindBool -- } -- case *types.Signature: -- return kindFunc -- } +-// created for Or [InlineCompletionList []InlineCompletionItem] +-type Or_Result_textDocument_inlineCompletion struct { +- Value interface{} `json:"value"` +-} - -- if types.Implements(candType, errorIntf) { -- kind |= kindError -- } +-// created for Or [SemanticTokensFullDelta bool] +-type Or_SemanticTokensOptions_full struct { +- Value interface{} `json:"value"` +-} - -- if types.Implements(candType, stringerIntf) { -- kind |= kindStringer -- } +-// created for Or [PRangeESemanticTokensOptions bool] +-type Or_SemanticTokensOptions_range struct { +- Value interface{} `json:"value"` +-} - -- return kind +-// created for Or [CallHierarchyOptions CallHierarchyRegistrationOptions bool] +-type Or_ServerCapabilities_callHierarchyProvider struct { +- Value interface{} `json:"value"` -} - --// innermostScope returns the innermost scope for c.pos. --func (c *completer) innermostScope() *types.Scope { -- for _, s := range c.scopes { -- if s != nil { -- return s -- } -- } -- return nil +-// created for Or [CodeActionOptions bool] +-type Or_ServerCapabilities_codeActionProvider struct { +- Value interface{} `json:"value"` -} - --// isSlice reports whether the object's underlying type is a slice. --func isSlice(obj types.Object) bool { -- if obj != nil && obj.Type() != nil { -- if _, ok := obj.Type().Underlying().(*types.Slice); ok { -- return true -- } -- } -- return false +-// created for Or [DocumentColorOptions DocumentColorRegistrationOptions bool] +-type Or_ServerCapabilities_colorProvider struct { +- Value interface{} `json:"value"` -} - --// forEachPackageMember calls f(tok, id, fn) for each package-level --// TYPE/VAR/CONST/FUNC declaration in the Go source file, based on a --// quick partial parse. fn is non-nil only for function declarations. --// The AST position information is garbage. --func forEachPackageMember(content []byte, f func(tok token.Token, id *ast.Ident, fn *ast.FuncDecl)) { -- purged := goplsastutil.PurgeFuncBodies(content) -- file, _ := parser.ParseFile(token.NewFileSet(), "", purged, 0) -- for _, decl := range file.Decls { -- switch decl := decl.(type) { -- case *ast.GenDecl: -- for _, spec := range decl.Specs { -- switch spec := spec.(type) { -- case *ast.ValueSpec: // var/const -- for _, id := range spec.Names { -- f(decl.Tok, id, nil) -- } -- case *ast.TypeSpec: -- f(decl.Tok, spec.Name, nil) -- } -- } -- case *ast.FuncDecl: -- if decl.Recv == nil { -- f(token.FUNC, decl.Name, decl) -- } -- } -- } +-// created for Or [DeclarationOptions DeclarationRegistrationOptions bool] +-type Or_ServerCapabilities_declarationProvider struct { +- Value interface{} `json:"value"` -} -diff -urN a/gopls/internal/lsp/source/completion/deep_completion.go b/gopls/internal/lsp/source/completion/deep_completion.go ---- a/gopls/internal/lsp/source/completion/deep_completion.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/completion/deep_completion.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,378 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package completion +-// created for Or [DefinitionOptions bool] +-type Or_ServerCapabilities_definitionProvider struct { +- Value interface{} `json:"value"` +-} - --import ( -- "context" -- "go/types" -- "strings" -- "time" --) +-// created for Or [DiagnosticOptions DiagnosticRegistrationOptions] +-type Or_ServerCapabilities_diagnosticProvider struct { +- Value interface{} `json:"value"` +-} - --// MaxDeepCompletions limits deep completion results because in most cases --// there are too many to be useful. --const MaxDeepCompletions = 3 +-// created for Or [DocumentFormattingOptions bool] +-type Or_ServerCapabilities_documentFormattingProvider struct { +- Value interface{} `json:"value"` +-} - --// deepCompletionState stores our state as we search for deep completions. --// "deep completion" refers to searching into objects' fields and methods to --// find more completion candidates. --type deepCompletionState struct { -- // enabled indicates whether deep completion is permitted. -- enabled bool +-// created for Or [DocumentHighlightOptions bool] +-type Or_ServerCapabilities_documentHighlightProvider struct { +- Value interface{} `json:"value"` +-} - -- // queueClosed is used to disable adding new sub-fields to search queue -- // once we're running out of our time budget. -- queueClosed bool +-// created for Or [DocumentRangeFormattingOptions bool] +-type Or_ServerCapabilities_documentRangeFormattingProvider struct { +- Value interface{} `json:"value"` +-} - -- // thisQueue holds the current breadth first search queue. -- thisQueue []candidate +-// created for Or [DocumentSymbolOptions bool] +-type Or_ServerCapabilities_documentSymbolProvider struct { +- Value interface{} `json:"value"` +-} - -- // nextQueue holds the next breadth first search iteration's queue. -- nextQueue []candidate +-// created for Or [FoldingRangeOptions FoldingRangeRegistrationOptions bool] +-type Or_ServerCapabilities_foldingRangeProvider struct { +- Value interface{} `json:"value"` +-} - -- // highScores tracks the highest deep candidate scores we have found -- // so far. This is used to avoid work for low scoring deep candidates. -- highScores [MaxDeepCompletions]float64 +-// created for Or [HoverOptions bool] +-type Or_ServerCapabilities_hoverProvider struct { +- Value interface{} `json:"value"` +-} - -- // candidateCount is the count of unique deep candidates encountered -- // so far. -- candidateCount int +-// created for Or [ImplementationOptions ImplementationRegistrationOptions bool] +-type Or_ServerCapabilities_implementationProvider struct { +- Value interface{} `json:"value"` -} - --// enqueue adds a candidate to the search queue. --func (s *deepCompletionState) enqueue(cand candidate) { -- s.nextQueue = append(s.nextQueue, cand) +-// created for Or [InlayHintOptions InlayHintRegistrationOptions bool] +-type Or_ServerCapabilities_inlayHintProvider struct { +- Value interface{} `json:"value"` -} - --// dequeue removes and returns the leftmost element from the search queue. --func (s *deepCompletionState) dequeue() *candidate { -- var cand *candidate -- cand, s.thisQueue = &s.thisQueue[len(s.thisQueue)-1], s.thisQueue[:len(s.thisQueue)-1] -- return cand +-// created for Or [InlineCompletionOptions bool] +-type Or_ServerCapabilities_inlineCompletionProvider struct { +- Value interface{} `json:"value"` -} - --// scorePenalty computes a deep candidate score penalty. A candidate is --// penalized based on depth to favor shallower candidates. We also give a --// slight bonus to unexported objects and a slight additional penalty to --// function objects. --func (s *deepCompletionState) scorePenalty(cand *candidate) float64 { -- var deepPenalty float64 -- for _, dc := range cand.path { -- deepPenalty++ +-// created for Or [InlineValueOptions InlineValueRegistrationOptions bool] +-type Or_ServerCapabilities_inlineValueProvider struct { +- Value interface{} `json:"value"` +-} - -- if !dc.Exported() { -- deepPenalty -= 0.1 -- } +-// created for Or [LinkedEditingRangeOptions LinkedEditingRangeRegistrationOptions bool] +-type Or_ServerCapabilities_linkedEditingRangeProvider struct { +- Value interface{} `json:"value"` +-} - -- if _, isSig := dc.Type().Underlying().(*types.Signature); isSig { -- deepPenalty += 0.1 -- } -- } +-// created for Or [MonikerOptions MonikerRegistrationOptions bool] +-type Or_ServerCapabilities_monikerProvider struct { +- Value interface{} `json:"value"` +-} - -- // Normalize penalty to a max depth of 10. -- return deepPenalty / 10 +-// created for Or [NotebookDocumentSyncOptions NotebookDocumentSyncRegistrationOptions] +-type Or_ServerCapabilities_notebookDocumentSync struct { +- Value interface{} `json:"value"` -} - --// isHighScore returns whether score is among the top MaxDeepCompletions deep --// candidate scores encountered so far. If so, it adds score to highScores, --// possibly displacing an existing high score. --func (s *deepCompletionState) isHighScore(score float64) bool { -- // Invariant: s.highScores is sorted with highest score first. Unclaimed -- // positions are trailing zeros. +-// created for Or [ReferenceOptions bool] +-type Or_ServerCapabilities_referencesProvider struct { +- Value interface{} `json:"value"` +-} - -- // If we beat an existing score then take its spot. -- for i, deepScore := range s.highScores { -- if score <= deepScore { -- continue -- } +-// created for Or [RenameOptions bool] +-type Or_ServerCapabilities_renameProvider struct { +- Value interface{} `json:"value"` +-} - -- if deepScore != 0 && i != len(s.highScores)-1 { -- // If this wasn't an empty slot then we need to scooch everyone -- // down one spot. -- copy(s.highScores[i+1:], s.highScores[i:]) -- } -- s.highScores[i] = score -- return true -- } +-// created for Or [SelectionRangeOptions SelectionRangeRegistrationOptions bool] +-type Or_ServerCapabilities_selectionRangeProvider struct { +- Value interface{} `json:"value"` +-} - -- return false +-// created for Or [SemanticTokensOptions SemanticTokensRegistrationOptions] +-type Or_ServerCapabilities_semanticTokensProvider struct { +- Value interface{} `json:"value"` -} - --// newPath returns path from search root for an object following a given --// candidate. --func (s *deepCompletionState) newPath(cand candidate, obj types.Object) []types.Object { -- path := make([]types.Object, len(cand.path)+1) -- copy(path, cand.path) -- path[len(path)-1] = obj +-// created for Or [TextDocumentSyncKind TextDocumentSyncOptions] +-type Or_ServerCapabilities_textDocumentSync struct { +- Value interface{} `json:"value"` +-} - -- return path +-// created for Or [TypeDefinitionOptions TypeDefinitionRegistrationOptions bool] +-type Or_ServerCapabilities_typeDefinitionProvider struct { +- Value interface{} `json:"value"` -} - --// deepSearch searches a candidate and its subordinate objects for completion --// items if deep completion is enabled and adds the valid candidates to --// completion items. --func (c *completer) deepSearch(ctx context.Context, minDepth int, deadline *time.Time) { -- defer func() { -- // We can return early before completing the search, so be sure to -- // clear out our queues to not impact any further invocations. -- c.deepState.thisQueue = c.deepState.thisQueue[:0] -- c.deepState.nextQueue = c.deepState.nextQueue[:0] -- }() +-// created for Or [TypeHierarchyOptions TypeHierarchyRegistrationOptions bool] +-type Or_ServerCapabilities_typeHierarchyProvider struct { +- Value interface{} `json:"value"` +-} - -- depth := 0 // current depth being processed -- // Stop reports whether we should stop the search immediately. -- stop := func() bool { -- // Context cancellation indicates that the actual completion operation was -- // cancelled, so ignore minDepth and deadline. -- select { -- case <-ctx.Done(): -- return true -- default: -- } -- // Otherwise, only stop if we've searched at least minDepth and reached the deadline. -- return depth > minDepth && deadline != nil && time.Now().After(*deadline) -- } +-// created for Or [WorkspaceSymbolOptions bool] +-type Or_ServerCapabilities_workspaceSymbolProvider struct { +- Value interface{} `json:"value"` +-} - -- for len(c.deepState.nextQueue) > 0 { -- depth++ -- if stop() { -- return -- } -- c.deepState.thisQueue, c.deepState.nextQueue = c.deepState.nextQueue, c.deepState.thisQueue[:0] +-// created for Or [MarkupContent string] +-type Or_SignatureInformation_documentation struct { +- Value interface{} `json:"value"` +-} - -- outer: -- for _, cand := range c.deepState.thisQueue { -- obj := cand.obj +-// created for Or [AnnotatedTextEdit TextEdit] +-type Or_TextDocumentEdit_edits_Elem struct { +- Value interface{} `json:"value"` +-} - -- if obj == nil { -- continue -- } +-// created for Or [TextDocumentFilterLanguage TextDocumentFilterPattern TextDocumentFilterScheme] +-type Or_TextDocumentFilter struct { +- Value interface{} `json:"value"` +-} - -- // At the top level, dedupe by object. -- if len(cand.path) == 0 { -- if c.seen[obj] { -- continue -- } -- c.seen[obj] = true -- } +-// created for Or [SaveOptions bool] +-type Or_TextDocumentSyncOptions_save struct { +- Value interface{} `json:"value"` +-} - -- // If obj is not accessible because it lives in another package and is -- // not exported, don't treat it as a completion candidate unless it's -- // a package completion candidate. -- if !c.completionContext.packageCompletion && -- obj.Pkg() != nil && obj.Pkg() != c.pkg.GetTypes() && !obj.Exported() { -- continue -- } +-// created for Or [WorkspaceFullDocumentDiagnosticReport WorkspaceUnchangedDocumentDiagnosticReport] +-type Or_WorkspaceDocumentDiagnosticReport struct { +- Value interface{} `json:"value"` +-} - -- // If we want a type name, don't offer non-type name candidates. -- // However, do offer package names since they can contain type names, -- // and do offer any candidate without a type since we aren't sure if it -- // is a type name or not (i.e. unimported candidate). -- if c.wantTypeName() && obj.Type() != nil && !isTypeName(obj) && !isPkgName(obj) { -- continue -- } +-// created for Or [CreateFile DeleteFile RenameFile TextDocumentEdit] +-type Or_WorkspaceEdit_documentChanges_Elem struct { +- Value interface{} `json:"value"` +-} - -- // When searching deep, make sure we don't have a cycle in our chain. -- // We don't dedupe by object because we want to allow both "foo.Baz" -- // and "bar.Baz" even though "Baz" is represented the same types.Object -- // in both. -- for _, seenObj := range cand.path { -- if seenObj == obj { -- continue outer -- } -- } +-// created for Or [Declaration []DeclarationLink] +-type Or_textDocument_declaration struct { +- Value interface{} `json:"value"` +-} - -- c.addCandidate(ctx, &cand) +-// created for Literal (Lit_SemanticTokensOptions_range_Item1) +-type PRangeESemanticTokensOptions struct { +-} - -- c.deepState.candidateCount++ -- if c.opts.budget > 0 && c.deepState.candidateCount%100 == 0 { -- if stop() { -- return -- } -- spent := float64(time.Since(c.startTime)) / float64(c.opts.budget) -- // If we are almost out of budgeted time, no further elements -- // should be added to the queue. This ensures remaining time is -- // used for processing current queue. -- if !c.deepState.queueClosed && spent >= 0.85 { -- c.deepState.queueClosed = true -- } -- } +-// The parameters of a configuration request. +-type ParamConfiguration struct { +- Items []ConfigurationItem `json:"items"` +-} +-type ParamInitialize struct { +- XInitializeParams +- WorkspaceFoldersInitializeParams +-} - -- // if deep search is disabled, don't add any more candidates. -- if !c.deepState.enabled || c.deepState.queueClosed { -- continue -- } +-// Represents a parameter of a callable-signature. A parameter can +-// have a label and a doc-comment. +-type ParameterInformation struct { +- // The label of this parameter information. +- // +- // Either a string or an inclusive start and exclusive end offsets within its containing +- // signature label. (see SignatureInformation.label). The offsets are based on a UTF-16 +- // string representation as `Position` and `Range` does. +- // +- // To avoid ambiguities a server should use the [start, end] offset value instead of using +- // a substring. Whether a client support this is controlled via `labelOffsetSupport` client +- // capability. +- // +- // *Note*: a label of type string should be a substring of its containing signature label. +- // Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`. +- Label string `json:"label"` +- // The human-readable doc-comment of this parameter. Will be shown +- // in the UI but can be omitted. +- Documentation string `json:"documentation,omitempty"` +-} +-type PartialResultParams struct { +- // An optional token that a server can use to report partial results (e.g. streaming) to +- // the client. +- PartialResultToken *ProgressToken `json:"partialResultToken,omitempty"` +-} - -- // Searching members for a type name doesn't make sense. -- if isTypeName(obj) { -- continue -- } -- if obj.Type() == nil { -- continue -- } +-// The glob pattern to watch relative to the base path. Glob patterns can have the following syntax: +-// +-// - `*` to match one or more characters in a path segment +-// - `?` to match on one character in a path segment +-// - `**` to match any number of path segments, including none +-// - `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) +-// - `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) +-// - `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) +-// +-// @since 3.17.0 +-type Pattern = string // (alias) +-// Position in a text document expressed as zero-based line and character +-// offset. Prior to 3.17 the offsets were always based on a UTF-16 string +-// representation. So a string of the form `a𐐀b` the character offset of the +-// character `a` is 0, the character offset of `𐐀` is 1 and the character +-// offset of b is 3 since `𐐀` is represented using two code units in UTF-16. +-// Since 3.17 clients and servers can agree on a different string encoding +-// representation (e.g. UTF-8). The client announces it's supported encoding +-// via the client capability [`general.positionEncodings`](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#clientCapabilities). +-// The value is an array of position encodings the client supports, with +-// decreasing preference (e.g. the encoding at index `0` is the most preferred +-// one). To stay backwards compatible the only mandatory encoding is UTF-16 +-// represented via the string `utf-16`. The server can pick one of the +-// encodings offered by the client and signals that encoding back to the +-// client via the initialize result's property +-// [`capabilities.positionEncoding`](https://microsoft.github.io/language-server-protocol/specifications/specification-current/#serverCapabilities). If the string value +-// `utf-16` is missing from the client's capability `general.positionEncodings` +-// servers can safely assume that the client supports UTF-16. If the server +-// omits the position encoding in its initialize result the encoding defaults +-// to the string value `utf-16`. Implementation considerations: since the +-// conversion from one encoding into another requires the content of the +-// file / line the conversion is best done where the file is read which is +-// usually on the server side. +-// +-// Positions are line end character agnostic. So you can not specify a position +-// that denotes `\r|\n` or `\n|` where `|` represents the character offset. +-// +-// @since 3.17.0 - support for negotiated position encoding. +-type Position struct { +- // Line position in a document (zero-based). +- // +- // If a line number is greater than the number of lines in a document, it defaults back to the number of lines in the document. +- // If a line number is negative, it defaults to 0. +- Line uint32 `json:"line"` +- // Character offset on a line in a document (zero-based). +- // +- // The meaning of this offset is determined by the negotiated +- // `PositionEncodingKind`. +- // +- // If the character value is greater than the line length it defaults back to the +- // line length. +- Character uint32 `json:"character"` +-} - -- // Don't search embedded fields because they were already included in their -- // parent's fields. -- if v, ok := obj.(*types.Var); ok && v.Embedded() { -- continue -- } +-// A set of predefined position encoding kinds. +-// +-// @since 3.17.0 +-type PositionEncodingKind string - -- if sig, ok := obj.Type().Underlying().(*types.Signature); ok { -- // If obj is a function that takes no arguments and returns one -- // value, keep searching across the function call. -- if sig.Params().Len() == 0 && sig.Results().Len() == 1 { -- path := c.deepState.newPath(cand, obj) -- // The result of a function call is not addressable. -- c.methodsAndFields(sig.Results().At(0).Type(), false, cand.imp, func(newCand candidate) { -- newCand.pathInvokeMask = cand.pathInvokeMask | (1 << uint64(len(cand.path))) -- newCand.path = path -- c.deepState.enqueue(newCand) -- }) -- } -- } +-// @since 3.18.0 +-// @proposed +-type PrepareRenameDefaultBehavior struct { +- DefaultBehavior bool `json:"defaultBehavior"` +-} +-type PrepareRenameParams struct { +- TextDocumentPositionParams +- WorkDoneProgressParams +-} - -- path := c.deepState.newPath(cand, obj) -- switch obj := obj.(type) { -- case *types.PkgName: -- c.packageMembers(obj.Imported(), stdScore, cand.imp, func(newCand candidate) { -- newCand.pathInvokeMask = cand.pathInvokeMask -- newCand.path = path -- c.deepState.enqueue(newCand) -- }) -- default: -- c.methodsAndFields(obj.Type(), cand.addressable, cand.imp, func(newCand candidate) { -- newCand.pathInvokeMask = cand.pathInvokeMask -- newCand.path = path -- c.deepState.enqueue(newCand) -- }) -- } -- } -- } +-// @since 3.18.0 +-// @proposed +-type PrepareRenamePlaceholder struct { +- Range Range `json:"range"` +- Placeholder string `json:"placeholder"` -} +-type PrepareRenameResult = PrepareRenamePlaceholder // (alias) +-type PrepareSupportDefaultBehavior uint32 - --// addCandidate adds a completion candidate to suggestions, without searching --// its members for more candidates. --func (c *completer) addCandidate(ctx context.Context, cand *candidate) { -- obj := cand.obj -- if c.matchingCandidate(cand) { -- cand.score *= highScore +-// A previous result id in a workspace pull request. +-// +-// @since 3.17.0 +-type PreviousResultID struct { +- // The URI for which the client knowns a +- // result id. +- URI DocumentURI `json:"uri"` +- // The value of the previous result id. +- Value string `json:"value"` +-} - -- if p := c.penalty(cand); p > 0 { -- cand.score *= (1 - p) -- } -- } else if isTypeName(obj) { -- // If obj is a *types.TypeName that didn't otherwise match, check -- // if a literal object of this type makes a good candidate. +-// A previous result id in a workspace pull request. +-// +-// @since 3.17.0 +-type PreviousResultId struct { +- // The URI for which the client knowns a +- // result id. +- URI DocumentURI `json:"uri"` +- // The value of the previous result id. +- Value string `json:"value"` +-} +-type ProgressParams struct { +- // The progress token provided by the client or server. +- Token ProgressToken `json:"token"` +- // The progress data. +- Value interface{} `json:"value"` +-} +-type ProgressToken = interface{} // (alias) +-// The publish diagnostic client capabilities. +-type PublishDiagnosticsClientCapabilities struct { +- // Whether the clients accepts diagnostics with related information. +- RelatedInformation bool `json:"relatedInformation,omitempty"` +- // Client supports the tag property to provide meta data about a diagnostic. +- // Clients supporting tags have to handle unknown tags gracefully. +- // +- // @since 3.15.0 +- TagSupport *ClientDiagnosticsTagOptions `json:"tagSupport,omitempty"` +- // Whether the client interprets the version property of the +- // `textDocument/publishDiagnostics` notification's parameter. +- // +- // @since 3.15.0 +- VersionSupport bool `json:"versionSupport,omitempty"` +- // Client supports a codeDescription property +- // +- // @since 3.16.0 +- CodeDescriptionSupport bool `json:"codeDescriptionSupport,omitempty"` +- // Whether code action supports the `data` property which is +- // preserved between a `textDocument/publishDiagnostics` and +- // `textDocument/codeAction` request. +- // +- // @since 3.16.0 +- DataSupport bool `json:"dataSupport,omitempty"` +-} - -- // We only care about named types (i.e. don't want builtin types). -- if _, isNamed := obj.Type().(*types.Named); isNamed { -- c.literal(ctx, obj.Type(), cand.imp) -- } -- } +-// The publish diagnostic notification's parameters. +-type PublishDiagnosticsParams struct { +- // The URI for which diagnostic information is reported. +- URI DocumentURI `json:"uri"` +- // Optional the version number of the document the diagnostics are published for. +- // +- // @since 3.15.0 +- Version int32 `json:"version,omitempty"` +- // An array of diagnostic information items. +- Diagnostics []Diagnostic `json:"diagnostics"` +-} - -- // Lower score of method calls so we prefer fields and vars over calls. -- if cand.hasMod(invoke) { -- if sig, ok := obj.Type().Underlying().(*types.Signature); ok && sig.Recv() != nil { -- cand.score *= 0.9 -- } -- } +-// A range in a text document expressed as (zero-based) start and end positions. +-// +-// If you want to specify a range that contains a line including the line ending +-// character(s) then use an end position denoting the start of the next line. +-// For example: +-// ```ts +-// +-// { +-// start: { line: 5, character: 23 } +-// end : { line 6, character : 0 } +-// } +-// +-// ``` +-type Range struct { +- // The range's start position. +- Start Position `json:"start"` +- // The range's end position. +- End Position `json:"end"` +-} - -- // Prefer private objects over public ones. -- if !obj.Exported() && obj.Parent() != types.Universe { -- cand.score *= 1.1 -- } +-// Client Capabilities for a {@link ReferencesRequest}. +-type ReferenceClientCapabilities struct { +- // Whether references supports dynamic registration. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +-} - -- // Slight penalty for index modifier (e.g. changing "foo" to -- // "foo[]") to curb false positives. -- if cand.hasMod(index) { -- cand.score *= 0.9 -- } +-// Value-object that contains additional information when +-// requesting references. +-type ReferenceContext struct { +- // Include the declaration of the current symbol. +- IncludeDeclaration bool `json:"includeDeclaration"` +-} - -- // Favor shallow matches by lowering score according to depth. -- cand.score -= cand.score * c.deepState.scorePenalty(cand) +-// Reference options. +-type ReferenceOptions struct { +- WorkDoneProgressOptions +-} - -- if cand.score < 0 { -- cand.score = 0 -- } +-// Parameters for a {@link ReferencesRequest}. +-type ReferenceParams struct { +- Context ReferenceContext `json:"context"` +- TextDocumentPositionParams +- WorkDoneProgressParams +- PartialResultParams +-} - -- cand.name = deepCandName(cand) -- if item, err := c.item(ctx, *cand); err == nil { -- c.items = append(c.items, item) -- } +-// Registration options for a {@link ReferencesRequest}. +-type ReferenceRegistrationOptions struct { +- TextDocumentRegistrationOptions +- ReferenceOptions -} - --// deepCandName produces the full candidate name including any --// ancestor objects. For example, "foo.bar().baz" for candidate "baz". --func deepCandName(cand *candidate) string { -- totalLen := len(cand.obj.Name()) -- for i, obj := range cand.path { -- totalLen += len(obj.Name()) + 1 -- if cand.pathInvokeMask&(1< 0 { -- totalLen += 2 -- } -- } +-// General parameters to register for a notification or to register a provider. +-type Registration struct { +- // The id used to register the request. The id can be used to deregister +- // the request again. +- ID string `json:"id"` +- // The method / capability to register for. +- Method string `json:"method"` +- // Options necessary for the registration. +- RegisterOptions interface{} `json:"registerOptions,omitempty"` +-} +-type RegistrationParams struct { +- Registrations []Registration `json:"registrations"` +-} +-type RegularExpressionEngineKind = string // (alias) +-// Client capabilities specific to regular expressions. +-// +-// @since 3.16.0 +-type RegularExpressionsClientCapabilities struct { +- // The engine's name. +- Engine RegularExpressionEngineKind `json:"engine"` +- // The engine's version. +- Version string `json:"version,omitempty"` +-} - -- var buf strings.Builder -- buf.Grow(totalLen) +-// A full diagnostic report with a set of related documents. +-// +-// @since 3.17.0 +-type RelatedFullDocumentDiagnosticReport struct { +- // Diagnostics of related documents. This information is useful +- // in programming languages where code in a file A can generate +- // diagnostics in a file B which A depends on. An example of +- // such a language is C/C++ where marco definitions in a file +- // a.cpp and result in errors in a header file b.hpp. +- // +- // @since 3.17.0 +- RelatedDocuments map[DocumentURI]interface{} `json:"relatedDocuments,omitempty"` +- FullDocumentDiagnosticReport +-} - -- for i, obj := range cand.path { -- buf.WriteString(obj.Name()) -- if cand.pathInvokeMask&(1< 0 { -- buf.WriteByte('(') -- buf.WriteByte(')') -- } -- buf.WriteByte('.') -- } +-// An unchanged diagnostic report with a set of related documents. +-// +-// @since 3.17.0 +-type RelatedUnchangedDocumentDiagnosticReport struct { +- // Diagnostics of related documents. This information is useful +- // in programming languages where code in a file A can generate +- // diagnostics in a file B which A depends on. An example of +- // such a language is C/C++ where marco definitions in a file +- // a.cpp and result in errors in a header file b.hpp. +- // +- // @since 3.17.0 +- RelatedDocuments map[DocumentURI]interface{} `json:"relatedDocuments,omitempty"` +- UnchangedDocumentDiagnosticReport +-} - -- buf.WriteString(cand.obj.Name()) +-// A relative pattern is a helper to construct glob patterns that are matched +-// relatively to a base URI. The common value for a `baseUri` is a workspace +-// folder root, but it can be another absolute URI as well. +-// +-// @since 3.17.0 +-type RelativePattern struct { +- // A workspace folder or a base URI to which this pattern will be matched +- // against relatively. +- BaseURI DocumentURI `json:"baseUri"` +- // The actual glob pattern; +- Pattern Pattern `json:"pattern"` +-} +-type RenameClientCapabilities struct { +- // Whether rename supports dynamic registration. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +- // Client supports testing for validity of rename operations +- // before execution. +- // +- // @since 3.12.0 +- PrepareSupport bool `json:"prepareSupport,omitempty"` +- // Client supports the default behavior result. +- // +- // The value indicates the default behavior used by the +- // client. +- // +- // @since 3.16.0 +- PrepareSupportDefaultBehavior *PrepareSupportDefaultBehavior `json:"prepareSupportDefaultBehavior,omitempty"` +- // Whether the client honors the change annotations in +- // text edits and resource operations returned via the +- // rename request's workspace edit by for example presenting +- // the workspace edit in the user interface and asking +- // for confirmation. +- // +- // @since 3.16.0 +- HonorsChangeAnnotations bool `json:"honorsChangeAnnotations,omitempty"` +-} - -- return buf.String() +-// Rename file operation +-type RenameFile struct { +- // A rename +- Kind string `json:"kind"` +- // The old (existing) location. +- OldURI DocumentURI `json:"oldUri"` +- // The new location. +- NewURI DocumentURI `json:"newUri"` +- // Rename options. +- Options *RenameFileOptions `json:"options,omitempty"` +- ResourceOperation -} - --// penalty reports a score penalty for cand in the range (0, 1). --// For example, a candidate is penalized if it has already been used --// in another switch case statement. --func (c *completer) penalty(cand *candidate) float64 { -- for _, p := range c.inference.penalized { -- if c.objChainMatches(cand, p.objChain) { -- return p.penalty -- } -- } +-// Rename file options +-type RenameFileOptions struct { +- // Overwrite target if existing. Overwrite wins over `ignoreIfExists` +- Overwrite bool `json:"overwrite,omitempty"` +- // Ignores if target exists. +- IgnoreIfExists bool `json:"ignoreIfExists,omitempty"` +-} - -- return 0 +-// The parameters sent in notifications/requests for user-initiated renames of +-// files. +-// +-// @since 3.16.0 +-type RenameFilesParams struct { +- // An array of all files/folders renamed in this operation. When a folder is renamed, only +- // the folder will be included, and not its children. +- Files []FileRename `json:"files"` -} - --// objChainMatches reports whether cand combined with the surrounding --// object prefix matches chain. --func (c *completer) objChainMatches(cand *candidate, chain []types.Object) bool { -- // For example, when completing: -- // -- // foo.ba<> +-// Provider options for a {@link RenameRequest}. +-type RenameOptions struct { +- // Renames should be checked and tested before being executed. - // -- // If we are considering the deep candidate "bar.baz", cand is baz, -- // objChain is [foo] and deepChain is [bar]. We would match the -- // chain [foo, bar, baz]. -- if len(chain) != len(c.inference.objChain)+len(cand.path)+1 { -- return false -- } +- // @since version 3.12.0 +- PrepareProvider bool `json:"prepareProvider,omitempty"` +- WorkDoneProgressOptions +-} - -- if chain[len(chain)-1] != cand.obj { -- return false -- } +-// The parameters of a {@link RenameRequest}. +-type RenameParams struct { +- // The document to rename. +- TextDocument TextDocumentIdentifier `json:"textDocument"` +- // The position at which this request was sent. +- Position Position `json:"position"` +- // The new name of the symbol. If the given name is not valid the +- // request must return a {@link ResponseError} with an +- // appropriate message set. +- NewName string `json:"newName"` +- WorkDoneProgressParams +-} - -- for i, o := range c.inference.objChain { -- if chain[i] != o { -- return false -- } -- } +-// Registration options for a {@link RenameRequest}. +-type RenameRegistrationOptions struct { +- TextDocumentRegistrationOptions +- RenameOptions +-} - -- for i, o := range cand.path { -- if chain[i+len(c.inference.objChain)] != o { -- return false -- } -- } +-// A generic resource operation. +-type ResourceOperation struct { +- // The resource operation kind. +- Kind string `json:"kind"` +- // An optional annotation identifier describing the operation. +- // +- // @since 3.16.0 +- AnnotationID *ChangeAnnotationIdentifier `json:"annotationId,omitempty"` +-} +-type ResourceOperationKind string - -- return true +-// Save options. +-type SaveOptions struct { +- // The client is supposed to include the content on save. +- IncludeText bool `json:"includeText,omitempty"` -} -diff -urN a/gopls/internal/lsp/source/completion/deep_completion_test.go b/gopls/internal/lsp/source/completion/deep_completion_test.go ---- a/gopls/internal/lsp/source/completion/deep_completion_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/completion/deep_completion_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,33 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package completion +-// Describes the currently selected completion item. +-// +-// @since 3.18.0 +-// @proposed +-type SelectedCompletionInfo struct { +- // The range that will be replaced if this completion item is accepted. +- Range Range `json:"range"` +- // The text the range will be replaced with if this completion is accepted. +- Text string `json:"text"` +-} - --import ( -- "testing" --) +-// A selection range represents a part of a selection hierarchy. A selection range +-// may have a parent selection range that contains it. +-type SelectionRange struct { +- // The {@link Range range} of this selection range. +- Range Range `json:"range"` +- // The parent selection range containing this range. Therefore `parent.range` must contain `this.range`. +- Parent *SelectionRange `json:"parent,omitempty"` +-} +-type SelectionRangeClientCapabilities struct { +- // Whether implementation supports dynamic registration for selection range providers. If this is set to `true` +- // the client supports the new `SelectionRangeRegistrationOptions` return value for the corresponding server +- // capability as well. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +-} +-type SelectionRangeOptions struct { +- WorkDoneProgressOptions +-} - --func TestDeepCompletionIsHighScore(t *testing.T) { -- // Test that deepCompletionState.isHighScore properly tracks the top -- // N=MaxDeepCompletions scores. +-// A parameter literal used in selection range requests. +-type SelectionRangeParams struct { +- // The text document. +- TextDocument TextDocumentIdentifier `json:"textDocument"` +- // The positions inside the text document. +- Positions []Position `json:"positions"` +- WorkDoneProgressParams +- PartialResultParams +-} +-type SelectionRangeRegistrationOptions struct { +- SelectionRangeOptions +- TextDocumentRegistrationOptions +- StaticRegistrationOptions +-} - -- var s deepCompletionState +-// A set of predefined token modifiers. This set is not fixed +-// an clients can specify additional token types via the +-// corresponding client capabilities. +-// +-// @since 3.16.0 +-type SemanticTokenModifiers string - -- if !s.isHighScore(1) { -- // No other scores yet, anything is a winner. -- t.Error("1 should be high score") -- } +-// A set of predefined token types. This set is not fixed +-// an clients can specify additional token types via the +-// corresponding client capabilities. +-// +-// @since 3.16.0 +-type SemanticTokenTypes string - -- // Fill up with higher scores. -- for i := 0; i < MaxDeepCompletions; i++ { -- if !s.isHighScore(10) { -- t.Error("10 should be high score") -- } -- } +-// @since 3.16.0 +-type SemanticTokens struct { +- // An optional result id. If provided and clients support delta updating +- // the client will include the result id in the next semantic token request. +- // A server can then instead of computing all semantic tokens again simply +- // send a delta. +- ResultID string `json:"resultId,omitempty"` +- // The actual tokens. +- Data []uint32 `json:"data"` +-} - -- // High scores should be filled with 10s so 2 is not a high score. -- if s.isHighScore(2) { -- t.Error("2 shouldn't be high score") -- } +-// @since 3.16.0 +-type SemanticTokensClientCapabilities struct { +- // Whether implementation supports dynamic registration. If this is set to `true` +- // the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` +- // return value for the corresponding server capability as well. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +- // Which requests the client supports and might send to the server +- // depending on the server's capability. Please note that clients might not +- // show semantic tokens or degrade some of the user experience if a range +- // or full request is advertised by the client but not provided by the +- // server. If for example the client capability `requests.full` and +- // `request.range` are both set to true but the server only provides a +- // range provider the client might not render a minimap correctly or might +- // even decide to not show any semantic tokens at all. +- Requests ClientSemanticTokensRequestOptions `json:"requests"` +- // The token types that the client supports. +- TokenTypes []string `json:"tokenTypes"` +- // The token modifiers that the client supports. +- TokenModifiers []string `json:"tokenModifiers"` +- // The token formats the clients supports. +- Formats []TokenFormat `json:"formats"` +- // Whether the client supports tokens that can overlap each other. +- OverlappingTokenSupport bool `json:"overlappingTokenSupport,omitempty"` +- // Whether the client supports tokens that can span multiple lines. +- MultilineTokenSupport bool `json:"multilineTokenSupport,omitempty"` +- // Whether the client allows the server to actively cancel a +- // semantic token request, e.g. supports returning +- // LSPErrorCodes.ServerCancelled. If a server does the client +- // needs to retrigger the request. +- // +- // @since 3.17.0 +- ServerCancelSupport bool `json:"serverCancelSupport,omitempty"` +- // Whether the client uses semantic tokens to augment existing +- // syntax tokens. If set to `true` client side created syntax +- // tokens and semantic tokens are both used for colorization. If +- // set to `false` the client only uses the returned semantic tokens +- // for colorization. +- // +- // If the value is `undefined` then the client behavior is not +- // specified. +- // +- // @since 3.17.0 +- AugmentsSyntaxTokens bool `json:"augmentsSyntaxTokens,omitempty"` -} -diff -urN a/gopls/internal/lsp/source/completion/definition.go b/gopls/internal/lsp/source/completion/definition.go ---- a/gopls/internal/lsp/source/completion/definition.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/completion/definition.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,160 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package completion -- --import ( -- "go/ast" -- "go/types" -- "strings" -- "unicode" -- "unicode/utf8" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/snippet" -- "golang.org/x/tools/gopls/internal/lsp/source" --) +-// @since 3.16.0 +-type SemanticTokensDelta struct { +- ResultID string `json:"resultId,omitempty"` +- // The semantic token edits to transform a previous result into a new result. +- Edits []SemanticTokensEdit `json:"edits"` +-} - --// some function definitions in test files can be completed --// So far, TestFoo(t *testing.T), TestMain(m *testing.M) --// BenchmarkFoo(b *testing.B), FuzzFoo(f *testing.F) +-// @since 3.16.0 +-type SemanticTokensDeltaParams struct { +- // The text document. +- TextDocument TextDocumentIdentifier `json:"textDocument"` +- // The result id of a previous response. The result Id can either point to a full response +- // or a delta response depending on what was received last. +- PreviousResultID string `json:"previousResultId"` +- WorkDoneProgressParams +- PartialResultParams +-} - --// path[0] is known to be *ast.Ident --func definition(path []ast.Node, obj types.Object, pgf *source.ParsedGoFile) ([]CompletionItem, *Selection) { -- if _, ok := obj.(*types.Func); !ok { -- return nil, nil // not a function at all -- } -- if !strings.HasSuffix(pgf.URI.Filename(), "_test.go") { -- return nil, nil // not a test file -- } +-// @since 3.16.0 +-type SemanticTokensDeltaPartialResult struct { +- Edits []SemanticTokensEdit `json:"edits"` +-} - -- name := path[0].(*ast.Ident).Name -- if len(name) == 0 { -- // can't happen -- return nil, nil -- } -- start := path[0].Pos() -- end := path[0].End() -- sel := &Selection{ -- content: "", -- cursor: start, -- tokFile: pgf.Tok, -- start: start, -- end: end, -- mapper: pgf.Mapper, -- } -- var ans []CompletionItem -- var hasParens bool -- n, ok := path[1].(*ast.FuncDecl) -- if !ok { -- return nil, nil // can't happen -- } -- if n.Recv != nil { -- return nil, nil // a method, not a function -- } -- t := n.Type.Params -- if t.Closing != t.Opening { -- hasParens = true -- } +-// @since 3.16.0 +-type SemanticTokensEdit struct { +- // The start offset of the edit. +- Start uint32 `json:"start"` +- // The count of elements to remove. +- DeleteCount uint32 `json:"deleteCount"` +- // The elements to insert. +- Data []uint32 `json:"data,omitempty"` +-} - -- // Always suggest TestMain, if possible -- if strings.HasPrefix("TestMain", name) { -- if hasParens { -- ans = append(ans, defItem("TestMain", obj)) -- } else { -- ans = append(ans, defItem("TestMain(m *testing.M)", obj)) -- } -- } +-// Semantic tokens options to support deltas for full documents +-// +-// @since 3.18.0 +-// @proposed +-type SemanticTokensFullDelta struct { +- // The server supports deltas for full documents. +- Delta bool `json:"delta,omitempty"` +-} - -- // If a snippet is possible, suggest it -- if strings.HasPrefix("Test", name) { -- if hasParens { -- ans = append(ans, defItem("Test", obj)) -- } else { -- ans = append(ans, defSnippet("Test", "(t *testing.T)", obj)) -- } -- return ans, sel -- } else if strings.HasPrefix("Benchmark", name) { -- if hasParens { -- ans = append(ans, defItem("Benchmark", obj)) -- } else { -- ans = append(ans, defSnippet("Benchmark", "(b *testing.B)", obj)) -- } -- return ans, sel -- } else if strings.HasPrefix("Fuzz", name) { -- if hasParens { -- ans = append(ans, defItem("Fuzz", obj)) -- } else { -- ans = append(ans, defSnippet("Fuzz", "(f *testing.F)", obj)) -- } -- return ans, sel -- } +-// @since 3.16.0 +-type SemanticTokensLegend struct { +- // The token types a server uses. +- TokenTypes []string `json:"tokenTypes"` +- // The token modifiers a server uses. +- TokenModifiers []string `json:"tokenModifiers"` +-} - -- // Fill in the argument for what the user has already typed -- if got := defMatches(name, "Test", path, "(t *testing.T)"); got != "" { -- ans = append(ans, defItem(got, obj)) -- } else if got := defMatches(name, "Benchmark", path, "(b *testing.B)"); got != "" { -- ans = append(ans, defItem(got, obj)) -- } else if got := defMatches(name, "Fuzz", path, "(f *testing.F)"); got != "" { -- ans = append(ans, defItem(got, obj)) -- } -- return ans, sel +-// @since 3.16.0 +-type SemanticTokensOptions struct { +- // The legend used by the server +- Legend SemanticTokensLegend `json:"legend"` +- // Server supports providing semantic tokens for a specific range +- // of a document. +- Range *Or_SemanticTokensOptions_range `json:"range,omitempty"` +- // Server supports providing semantic tokens for a full document. +- Full *Or_SemanticTokensOptions_full `json:"full,omitempty"` +- WorkDoneProgressOptions -} - --// defMatches returns text for defItem, never for defSnippet --func defMatches(name, pat string, path []ast.Node, arg string) string { -- if !strings.HasPrefix(name, pat) { -- return "" -- } -- c, _ := utf8.DecodeRuneInString(name[len(pat):]) -- if unicode.IsLower(c) { -- return "" -- } -- fd, ok := path[1].(*ast.FuncDecl) -- if !ok { -- // we don't know what's going on -- return "" -- } -- fp := fd.Type.Params -- if len(fp.List) > 0 { -- // signature already there, nothing to suggest -- return "" -- } -- if fp.Opening != fp.Closing { -- // nothing: completion works on words, not easy to insert arg -- return "" -- } -- // suggesting signature too -- return name + arg +-// @since 3.16.0 +-type SemanticTokensParams struct { +- // The text document. +- TextDocument TextDocumentIdentifier `json:"textDocument"` +- WorkDoneProgressParams +- PartialResultParams -} - --func defSnippet(prefix, suffix string, obj types.Object) CompletionItem { -- var sn snippet.Builder -- sn.WriteText(prefix) -- sn.WritePlaceholder(func(b *snippet.Builder) { b.WriteText("Xxx") }) -- sn.WriteText(suffix + " {\n\t") -- sn.WriteFinalTabstop() -- sn.WriteText("\n}") -- return CompletionItem{ -- Label: prefix + "Xxx" + suffix, -- Detail: "tab, type the rest of the name, then tab", -- Kind: protocol.FunctionCompletion, -- Depth: 0, -- Score: 10, -- snippet: &sn, -- Documentation: prefix + " test function", -- isSlice: isSlice(obj), -- } +-// @since 3.16.0 +-type SemanticTokensPartialResult struct { +- Data []uint32 `json:"data"` -} --func defItem(val string, obj types.Object) CompletionItem { -- return CompletionItem{ -- Label: val, -- InsertText: val, -- Kind: protocol.FunctionCompletion, -- Depth: 0, -- Score: 9, // prefer the snippets when available -- Documentation: "complete the function name", -- isSlice: isSlice(obj), -- } +- +-// @since 3.16.0 +-type SemanticTokensRangeParams struct { +- // The text document. +- TextDocument TextDocumentIdentifier `json:"textDocument"` +- // The range the semantic tokens are requested for. +- Range Range `json:"range"` +- WorkDoneProgressParams +- PartialResultParams -} -diff -urN a/gopls/internal/lsp/source/completion/format.go b/gopls/internal/lsp/source/completion/format.go ---- a/gopls/internal/lsp/source/completion/format.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/completion/format.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,345 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package completion +-// @since 3.16.0 +-type SemanticTokensRegistrationOptions struct { +- TextDocumentRegistrationOptions +- SemanticTokensOptions +- StaticRegistrationOptions +-} - --import ( -- "context" -- "errors" -- "fmt" -- "go/ast" -- "go/doc" -- "go/types" -- "strings" +-// @since 3.16.0 +-type SemanticTokensWorkspaceClientCapabilities struct { +- // Whether the client implementation supports a refresh request sent from +- // the server to the client. +- // +- // Note that this event is global and will force the client to refresh all +- // semantic tokens currently shown. It should be used with absolute care +- // and is useful for situation where a server for example detects a project +- // wide change that requires such a calculation. +- RefreshSupport bool `json:"refreshSupport,omitempty"` +-} - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/gopls/internal/lsp/snippet" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/imports" -- "golang.org/x/tools/internal/typeparams" --) +-// Defines the capabilities provided by a language +-// server. +-type ServerCapabilities struct { +- // The position encoding the server picked from the encodings offered +- // by the client via the client capability `general.positionEncodings`. +- // +- // If the client didn't provide any position encodings the only valid +- // value that a server can return is 'utf-16'. +- // +- // If omitted it defaults to 'utf-16'. +- // +- // @since 3.17.0 +- PositionEncoding *PositionEncodingKind `json:"positionEncoding,omitempty"` +- // Defines how text documents are synced. Is either a detailed structure +- // defining each notification or for backwards compatibility the +- // TextDocumentSyncKind number. +- TextDocumentSync interface{} `json:"textDocumentSync,omitempty"` +- // Defines how notebook documents are synced. +- // +- // @since 3.17.0 +- NotebookDocumentSync *Or_ServerCapabilities_notebookDocumentSync `json:"notebookDocumentSync,omitempty"` +- // The server provides completion support. +- CompletionProvider *CompletionOptions `json:"completionProvider,omitempty"` +- // The server provides hover support. +- HoverProvider *Or_ServerCapabilities_hoverProvider `json:"hoverProvider,omitempty"` +- // The server provides signature help support. +- SignatureHelpProvider *SignatureHelpOptions `json:"signatureHelpProvider,omitempty"` +- // The server provides Goto Declaration support. +- DeclarationProvider *Or_ServerCapabilities_declarationProvider `json:"declarationProvider,omitempty"` +- // The server provides goto definition support. +- DefinitionProvider *Or_ServerCapabilities_definitionProvider `json:"definitionProvider,omitempty"` +- // The server provides Goto Type Definition support. +- TypeDefinitionProvider *Or_ServerCapabilities_typeDefinitionProvider `json:"typeDefinitionProvider,omitempty"` +- // The server provides Goto Implementation support. +- ImplementationProvider *Or_ServerCapabilities_implementationProvider `json:"implementationProvider,omitempty"` +- // The server provides find references support. +- ReferencesProvider *Or_ServerCapabilities_referencesProvider `json:"referencesProvider,omitempty"` +- // The server provides document highlight support. +- DocumentHighlightProvider *Or_ServerCapabilities_documentHighlightProvider `json:"documentHighlightProvider,omitempty"` +- // The server provides document symbol support. +- DocumentSymbolProvider *Or_ServerCapabilities_documentSymbolProvider `json:"documentSymbolProvider,omitempty"` +- // The server provides code actions. CodeActionOptions may only be +- // specified if the client states that it supports +- // `codeActionLiteralSupport` in its initial `initialize` request. +- CodeActionProvider interface{} `json:"codeActionProvider,omitempty"` +- // The server provides code lens. +- CodeLensProvider *CodeLensOptions `json:"codeLensProvider,omitempty"` +- // The server provides document link support. +- DocumentLinkProvider *DocumentLinkOptions `json:"documentLinkProvider,omitempty"` +- // The server provides color provider support. +- ColorProvider *Or_ServerCapabilities_colorProvider `json:"colorProvider,omitempty"` +- // The server provides workspace symbol support. +- WorkspaceSymbolProvider *Or_ServerCapabilities_workspaceSymbolProvider `json:"workspaceSymbolProvider,omitempty"` +- // The server provides document formatting. +- DocumentFormattingProvider *Or_ServerCapabilities_documentFormattingProvider `json:"documentFormattingProvider,omitempty"` +- // The server provides document range formatting. +- DocumentRangeFormattingProvider *Or_ServerCapabilities_documentRangeFormattingProvider `json:"documentRangeFormattingProvider,omitempty"` +- // The server provides document formatting on typing. +- DocumentOnTypeFormattingProvider *DocumentOnTypeFormattingOptions `json:"documentOnTypeFormattingProvider,omitempty"` +- // The server provides rename support. RenameOptions may only be +- // specified if the client states that it supports +- // `prepareSupport` in its initial `initialize` request. +- RenameProvider interface{} `json:"renameProvider,omitempty"` +- // The server provides folding provider support. +- FoldingRangeProvider *Or_ServerCapabilities_foldingRangeProvider `json:"foldingRangeProvider,omitempty"` +- // The server provides selection range support. +- SelectionRangeProvider *Or_ServerCapabilities_selectionRangeProvider `json:"selectionRangeProvider,omitempty"` +- // The server provides execute command support. +- ExecuteCommandProvider *ExecuteCommandOptions `json:"executeCommandProvider,omitempty"` +- // The server provides call hierarchy support. +- // +- // @since 3.16.0 +- CallHierarchyProvider *Or_ServerCapabilities_callHierarchyProvider `json:"callHierarchyProvider,omitempty"` +- // The server provides linked editing range support. +- // +- // @since 3.16.0 +- LinkedEditingRangeProvider *Or_ServerCapabilities_linkedEditingRangeProvider `json:"linkedEditingRangeProvider,omitempty"` +- // The server provides semantic tokens support. +- // +- // @since 3.16.0 +- SemanticTokensProvider interface{} `json:"semanticTokensProvider,omitempty"` +- // The server provides moniker support. +- // +- // @since 3.16.0 +- MonikerProvider *Or_ServerCapabilities_monikerProvider `json:"monikerProvider,omitempty"` +- // The server provides type hierarchy support. +- // +- // @since 3.17.0 +- TypeHierarchyProvider *Or_ServerCapabilities_typeHierarchyProvider `json:"typeHierarchyProvider,omitempty"` +- // The server provides inline values. +- // +- // @since 3.17.0 +- InlineValueProvider *Or_ServerCapabilities_inlineValueProvider `json:"inlineValueProvider,omitempty"` +- // The server provides inlay hints. +- // +- // @since 3.17.0 +- InlayHintProvider interface{} `json:"inlayHintProvider,omitempty"` +- // The server has support for pull model diagnostics. +- // +- // @since 3.17.0 +- DiagnosticProvider *Or_ServerCapabilities_diagnosticProvider `json:"diagnosticProvider,omitempty"` +- // Inline completion options used during static registration. +- // +- // @since 3.18.0 +- // @proposed +- InlineCompletionProvider *Or_ServerCapabilities_inlineCompletionProvider `json:"inlineCompletionProvider,omitempty"` +- // Workspace specific server capabilities. +- Workspace *WorkspaceOptions `json:"workspace,omitempty"` +- // Experimental server capabilities. +- Experimental interface{} `json:"experimental,omitempty"` +-} - --var ( -- errNoMatch = errors.New("not a surrounding match") -- errLowScore = errors.New("not a high scoring candidate") --) +-// @since 3.18.0 +-// @proposed +-type ServerCompletionItemOptions struct { +- // The server has support for completion item label +- // details (see also `CompletionItemLabelDetails`) when +- // receiving a completion item in a resolve call. +- // +- // @since 3.17.0 +- LabelDetailsSupport bool `json:"labelDetailsSupport,omitempty"` +-} - --// item formats a candidate to a CompletionItem. --func (c *completer) item(ctx context.Context, cand candidate) (CompletionItem, error) { -- obj := cand.obj +-// Information about the server +-// +-// @since 3.15.0 +-// @since 3.18.0 ServerInfo type name added. +-// @proposed +-type ServerInfo struct { +- // The name of the server as defined by the server. +- Name string `json:"name"` +- // The server's version as defined by the server. +- Version string `json:"version,omitempty"` +-} +-type SetTraceParams struct { +- Value TraceValue `json:"value"` +-} - -- // if the object isn't a valid match against the surrounding, return early. -- matchScore := c.matcher.Score(cand.name) -- if matchScore <= 0 { -- return CompletionItem{}, errNoMatch -- } -- cand.score *= float64(matchScore) +-// Client capabilities for the showDocument request. +-// +-// @since 3.16.0 +-type ShowDocumentClientCapabilities struct { +- // The client has support for the showDocument +- // request. +- Support bool `json:"support"` +-} - -- // Ignore deep candidates that won't be in the MaxDeepCompletions anyway. -- if len(cand.path) != 0 && !c.deepState.isHighScore(cand.score) { -- return CompletionItem{}, errLowScore -- } +-// Params to show a resource in the UI. +-// +-// @since 3.16.0 +-type ShowDocumentParams struct { +- // The uri to show. +- URI URI `json:"uri"` +- // Indicates to show the resource in an external program. +- // To show, for example, `https://code.visualstudio.com/` +- // in the default WEB browser set `external` to `true`. +- External bool `json:"external,omitempty"` +- // An optional property to indicate whether the editor +- // showing the document should take focus or not. +- // Clients might ignore this property if an external +- // program is started. +- TakeFocus bool `json:"takeFocus,omitempty"` +- // An optional selection range if the document is a text +- // document. Clients might ignore the property if an +- // external program is started or the file is not a text +- // file. +- Selection *Range `json:"selection,omitempty"` +-} - -- // Handle builtin types separately. -- if obj.Parent() == types.Universe { -- return c.formatBuiltin(ctx, cand) -- } +-// The result of a showDocument request. +-// +-// @since 3.16.0 +-type ShowDocumentResult struct { +- // A boolean indicating if the show was successful. +- Success bool `json:"success"` +-} - -- var ( -- label = cand.name -- detail = types.TypeString(obj.Type(), c.qf) -- insert = label -- kind = protocol.TextCompletion -- snip snippet.Builder -- protocolEdits []protocol.TextEdit -- ) -- if obj.Type() == nil { -- detail = "" -- } -- if isTypeName(obj) && c.wantTypeParams() { -- x := cand.obj.(*types.TypeName) -- if named, ok := x.Type().(*types.Named); ok { -- tp := typeparams.ForNamed(named) -- label += source.FormatTypeParams(tp) -- insert = label // maintain invariant above (label == insert) -- } -- } +-// The parameters of a notification message. +-type ShowMessageParams struct { +- // The message type. See {@link MessageType} +- Type MessageType `json:"type"` +- // The actual message. +- Message string `json:"message"` +-} - -- snip.WriteText(insert) +-// Show message request client capabilities +-type ShowMessageRequestClientCapabilities struct { +- // Capabilities specific to the `MessageActionItem` type. +- MessageActionItem *ClientShowMessageActionItemOptions `json:"messageActionItem,omitempty"` +-} +-type ShowMessageRequestParams struct { +- // The message type. See {@link MessageType} +- Type MessageType `json:"type"` +- // The actual message. +- Message string `json:"message"` +- // The message action items to present. +- Actions []MessageActionItem `json:"actions,omitempty"` +-} - -- switch obj := obj.(type) { -- case *types.TypeName: -- detail, kind = source.FormatType(obj.Type(), c.qf) -- case *types.Const: -- kind = protocol.ConstantCompletion -- case *types.Var: -- if _, ok := obj.Type().(*types.Struct); ok { -- detail = "struct{...}" // for anonymous structs -- } else if obj.IsField() { -- var err error -- detail, err = source.FormatVarType(ctx, c.snapshot, c.pkg, obj, c.qf, c.mq) -- if err != nil { -- return CompletionItem{}, err -- } -- } -- if obj.IsField() { -- kind = protocol.FieldCompletion -- c.structFieldSnippet(cand, detail, &snip) -- } else { -- kind = protocol.VariableCompletion -- } -- if obj.Type() == nil { -- break -- } -- case *types.Func: -- sig, ok := obj.Type().Underlying().(*types.Signature) -- if !ok { -- break -- } -- kind = protocol.FunctionCompletion -- if sig != nil && sig.Recv() != nil { -- kind = protocol.MethodCompletion -- } -- case *types.PkgName: -- kind = protocol.ModuleCompletion -- detail = fmt.Sprintf("%q", obj.Imported().Path()) -- case *types.Label: -- kind = protocol.ConstantCompletion -- detail = "label" -- } +-// Signature help represents the signature of something +-// callable. There can be multiple signature but only one +-// active and only one active parameter. +-type SignatureHelp struct { +- // One or more signatures. +- Signatures []SignatureInformation `json:"signatures"` +- // The active signature. If omitted or the value lies outside the +- // range of `signatures` the value defaults to zero or is ignored if +- // the `SignatureHelp` has no signatures. +- // +- // Whenever possible implementors should make an active decision about +- // the active signature and shouldn't rely on a default value. +- // +- // In future version of the protocol this property might become +- // mandatory to better express this. +- ActiveSignature uint32 `json:"activeSignature,omitempty"` +- // The active parameter of the active signature. +- // +- // If `null`, no parameter of the signature is active (for example a named +- // argument that does not match any declared parameters). This is only valid +- // if the client specifies the client capability +- // `textDocument.signatureHelp.noActiveParameterSupport === true` +- // +- // If omitted or the value lies outside the range of +- // `signatures[activeSignature].parameters` defaults to 0 if the active +- // signature has parameters. +- // +- // If the active signature has no parameters it is ignored. +- // +- // In future version of the protocol this property might become +- // mandatory (but still nullable) to better express the active parameter if +- // the active signature does have any. +- ActiveParameter uint32 `json:"activeParameter,omitempty"` +-} - -- var prefix string -- for _, mod := range cand.mods { -- switch mod { -- case reference: -- prefix = "&" + prefix -- case dereference: -- prefix = "*" + prefix -- case chanRead: -- prefix = "<-" + prefix -- } -- } +-// Client Capabilities for a {@link SignatureHelpRequest}. +-type SignatureHelpClientCapabilities struct { +- // Whether signature help supports dynamic registration. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +- // The client supports the following `SignatureInformation` +- // specific properties. +- SignatureInformation *ClientSignatureInformationOptions `json:"signatureInformation,omitempty"` +- // The client supports to send additional context information for a +- // `textDocument/signatureHelp` request. A client that opts into +- // contextSupport will also support the `retriggerCharacters` on +- // `SignatureHelpOptions`. +- // +- // @since 3.15.0 +- ContextSupport bool `json:"contextSupport,omitempty"` +-} - -- var ( -- suffix string -- funcType = obj.Type() -- ) --Suffixes: -- for _, mod := range cand.mods { -- switch mod { -- case invoke: -- if sig, ok := funcType.Underlying().(*types.Signature); ok { -- s, err := source.NewSignature(ctx, c.snapshot, c.pkg, sig, nil, c.qf, c.mq) -- if err != nil { -- return CompletionItem{}, err -- } -- c.functionCallSnippet("", s.TypeParams(), s.Params(), &snip) -- if sig.Results().Len() == 1 { -- funcType = sig.Results().At(0).Type() -- } -- detail = "func" + s.Format() -- } +-// Additional information about the context in which a signature help request was triggered. +-// +-// @since 3.15.0 +-type SignatureHelpContext struct { +- // Action that caused signature help to be triggered. +- TriggerKind SignatureHelpTriggerKind `json:"triggerKind"` +- // Character that caused signature help to be triggered. +- // +- // This is undefined when `triggerKind !== SignatureHelpTriggerKind.TriggerCharacter` +- TriggerCharacter string `json:"triggerCharacter,omitempty"` +- // `true` if signature help was already showing when it was triggered. +- // +- // Retriggers occurs when the signature help is already active and can be caused by actions such as +- // typing a trigger character, a cursor move, or document content changes. +- IsRetrigger bool `json:"isRetrigger"` +- // The currently active `SignatureHelp`. +- // +- // The `activeSignatureHelp` has its `SignatureHelp.activeSignature` field updated based on +- // the user navigating through available signatures. +- ActiveSignatureHelp *SignatureHelp `json:"activeSignatureHelp,omitempty"` +-} - -- if !c.opts.snippets { -- // Without snippets the candidate will not include "()". Don't -- // add further suffixes since they will be invalid. For -- // example, with snippets "foo()..." would become "foo..." -- // without snippets if we added the dotDotDot. -- break Suffixes -- } -- case takeSlice: -- suffix += "[:]" -- case takeDotDotDot: -- suffix += "..." -- case index: -- snip.WriteText("[") -- snip.WritePlaceholder(nil) -- snip.WriteText("]") -- } -- } +-// Server Capabilities for a {@link SignatureHelpRequest}. +-type SignatureHelpOptions struct { +- // List of characters that trigger signature help automatically. +- TriggerCharacters []string `json:"triggerCharacters,omitempty"` +- // List of characters that re-trigger signature help. +- // +- // These trigger characters are only active when signature help is already showing. All trigger characters +- // are also counted as re-trigger characters. +- // +- // @since 3.15.0 +- RetriggerCharacters []string `json:"retriggerCharacters,omitempty"` +- WorkDoneProgressOptions +-} - -- // If this candidate needs an additional import statement, -- // add the additional text edits needed. -- if cand.imp != nil { -- addlEdits, err := c.importEdits(cand.imp) +-// Parameters for a {@link SignatureHelpRequest}. +-type SignatureHelpParams struct { +- // The signature help context. This is only available if the client specifies +- // to send this using the client capability `textDocument.signatureHelp.contextSupport === true` +- // +- // @since 3.15.0 +- Context *SignatureHelpContext `json:"context,omitempty"` +- TextDocumentPositionParams +- WorkDoneProgressParams +-} - -- if err != nil { -- return CompletionItem{}, err -- } +-// Registration options for a {@link SignatureHelpRequest}. +-type SignatureHelpRegistrationOptions struct { +- TextDocumentRegistrationOptions +- SignatureHelpOptions +-} - -- protocolEdits = append(protocolEdits, addlEdits...) -- if kind != protocol.ModuleCompletion { -- if detail != "" { -- detail += " " -- } -- detail += fmt.Sprintf("(from %q)", cand.imp.importPath) -- } -- } +-// How a signature help was triggered. +-// +-// @since 3.15.0 +-type SignatureHelpTriggerKind uint32 - -- if cand.convertTo != nil { -- typeName := types.TypeString(cand.convertTo, c.qf) +-// Represents the signature of something callable. A signature +-// can have a label, like a function-name, a doc-comment, and +-// a set of parameters. +-type SignatureInformation struct { +- // The label of this signature. Will be shown in +- // the UI. +- Label string `json:"label"` +- // The human-readable doc-comment of this signature. Will be shown +- // in the UI but can be omitted. +- Documentation *Or_SignatureInformation_documentation `json:"documentation,omitempty"` +- // The parameters of this signature. +- Parameters []ParameterInformation `json:"parameters,omitempty"` +- // The index of the active parameter. +- // +- // If `null`, no parameter of the signature is active (for example a named +- // argument that does not match any declared parameters). This is only valid +- // if the client specifies the client capability +- // `textDocument.signatureHelp.noActiveParameterSupport === true` +- // +- // If provided (or `null`), this is used in place of +- // `SignatureHelp.activeParameter`. +- // +- // @since 3.16.0 +- ActiveParameter uint32 `json:"activeParameter,omitempty"` +-} - -- switch t := cand.convertTo.(type) { -- // We need extra parens when casting to these types. For example, -- // we need "(*int)(foo)", not "*int(foo)". -- case *types.Pointer, *types.Signature: -- typeName = "(" + typeName + ")" -- case *types.Basic: -- // If the types are incompatible (as determined by typeMatches), then we -- // must need a conversion here. However, if the target type is untyped, -- // don't suggest converting to e.g. "untyped float" (golang/go#62141). -- if t.Info()&types.IsUntyped != 0 { -- typeName = types.TypeString(types.Default(cand.convertTo), c.qf) -- } -- } +-// @since 3.18.0 +-// @proposed +-type StaleRequestSupportOptions struct { +- // The client will actively cancel the request. +- Cancel bool `json:"cancel"` +- // The list of requests for which the client +- // will retry the request if it receives a +- // response with error code `ContentModified` +- RetryOnContentModified []string `json:"retryOnContentModified"` +-} - -- prefix = typeName + "(" + prefix -- suffix = ")" -- } +-// Static registration options to be returned in the initialize +-// request. +-type StaticRegistrationOptions struct { +- // The id used to register the request. The id can be used to deregister +- // the request again. See also Registration#id. +- ID string `json:"id,omitempty"` +-} - -- if prefix != "" { -- // If we are in a selector, add an edit to place prefix before selector. -- if sel := enclosingSelector(c.path, c.pos); sel != nil { -- edits, err := c.editText(sel.Pos(), sel.Pos(), prefix) -- if err != nil { -- return CompletionItem{}, err -- } -- protocolEdits = append(protocolEdits, edits...) -- } else { -- // If there is no selector, just stick the prefix at the start. -- insert = prefix + insert -- snip.PrependText(prefix) -- } -- } +-// A string value used as a snippet is a template which allows to insert text +-// and to control the editor cursor when insertion happens. +-// +-// A snippet can define tab stops and placeholders with `$1`, `$2` +-// and `${3:foo}`. `$0` defines the final tab stop, it defaults to +-// the end of the snippet. Variables are defined with `$name` and +-// `${name:default value}`. +-// +-// @since 3.18.0 +-// @proposed +-type StringValue struct { +- // The kind of string value. +- Kind string `json:"kind"` +- // The snippet string. +- Value string `json:"value"` +-} - -- if suffix != "" { -- insert += suffix -- snip.WriteText(suffix) -- } +-// Represents information about programming constructs like variables, classes, +-// interfaces etc. +-type SymbolInformation struct { +- // extends BaseSymbolInformation +- // Indicates if this symbol is deprecated. +- // +- // @deprecated Use tags instead +- Deprecated bool `json:"deprecated,omitempty"` +- // The location of this symbol. The location's range is used by a tool +- // to reveal the location in the editor. If the symbol is selected in the +- // tool the range's start information is used to position the cursor. So +- // the range usually spans more than the actual symbol's name and does +- // normally include things like visibility modifiers. +- // +- // The range doesn't have to denote a node range in the sense of an abstract +- // syntax tree. It can therefore not be used to re-construct a hierarchy of +- // the symbols. +- Location Location `json:"location"` +- // The name of this symbol. +- Name string `json:"name"` +- // The kind of this symbol. +- Kind SymbolKind `json:"kind"` +- // Tags for this symbol. +- // +- // @since 3.16.0 +- Tags []SymbolTag `json:"tags,omitempty"` +- // The name of the symbol containing this symbol. This information is for +- // user interface purposes (e.g. to render a qualifier in the user interface +- // if necessary). It can't be used to re-infer a hierarchy for the document +- // symbols. +- ContainerName string `json:"containerName,omitempty"` +-} - -- detail = strings.TrimPrefix(detail, "untyped ") -- // override computed detail with provided detail, if something is provided. -- if cand.detail != "" { -- detail = cand.detail -- } -- item := CompletionItem{ -- Label: label, -- InsertText: insert, -- AdditionalTextEdits: protocolEdits, -- Detail: detail, -- Kind: kind, -- Score: cand.score, -- Depth: len(cand.path), -- snippet: &snip, -- isSlice: isSlice(obj), -- } -- // If the user doesn't want documentation for completion items. -- if !c.opts.documentation { -- return item, nil -- } -- pos := safetoken.StartPosition(c.pkg.FileSet(), obj.Pos()) +-// A symbol kind. +-type SymbolKind uint32 - -- // We ignore errors here, because some types, like "unsafe" or "error", -- // may not have valid positions that we can use to get documentation. -- if !pos.IsValid() { -- return item, nil -- } +-// Symbol tags are extra annotations that tweak the rendering of a symbol. +-// +-// @since 3.16 +-type SymbolTag uint32 - -- comment, err := source.HoverDocForObject(ctx, c.snapshot, c.pkg.FileSet(), obj) -- if err != nil { -- event.Error(ctx, fmt.Sprintf("failed to find Hover for %q", obj.Name()), err) -- return item, nil -- } -- if c.opts.fullDocumentation { -- item.Documentation = comment.Text() -- } else { -- item.Documentation = doc.Synopsis(comment.Text()) -- } -- // The desired pattern is `^// Deprecated`, but the prefix has been removed -- // TODO(rfindley): It doesn't look like this does the right thing for -- // multi-line comments. -- if strings.HasPrefix(comment.Text(), "Deprecated") { -- if c.snapshot.Options().CompletionTags { -- item.Tags = []protocol.CompletionItemTag{protocol.ComplDeprecated} -- } else if c.snapshot.Options().CompletionDeprecated { -- item.Deprecated = true -- } -- } +-// Describe options to be used when registered for text document change events. +-type TextDocumentChangeRegistrationOptions struct { +- // How documents are synced to the server. +- SyncKind TextDocumentSyncKind `json:"syncKind"` +- TextDocumentRegistrationOptions +-} - -- return item, nil +-// Text document specific client capabilities. +-type TextDocumentClientCapabilities struct { +- // Defines which synchronization capabilities the client supports. +- Synchronization *TextDocumentSyncClientCapabilities `json:"synchronization,omitempty"` +- // Capabilities specific to the `textDocument/completion` request. +- Completion CompletionClientCapabilities `json:"completion,omitempty"` +- // Capabilities specific to the `textDocument/hover` request. +- Hover *HoverClientCapabilities `json:"hover,omitempty"` +- // Capabilities specific to the `textDocument/signatureHelp` request. +- SignatureHelp *SignatureHelpClientCapabilities `json:"signatureHelp,omitempty"` +- // Capabilities specific to the `textDocument/declaration` request. +- // +- // @since 3.14.0 +- Declaration *DeclarationClientCapabilities `json:"declaration,omitempty"` +- // Capabilities specific to the `textDocument/definition` request. +- Definition *DefinitionClientCapabilities `json:"definition,omitempty"` +- // Capabilities specific to the `textDocument/typeDefinition` request. +- // +- // @since 3.6.0 +- TypeDefinition *TypeDefinitionClientCapabilities `json:"typeDefinition,omitempty"` +- // Capabilities specific to the `textDocument/implementation` request. +- // +- // @since 3.6.0 +- Implementation *ImplementationClientCapabilities `json:"implementation,omitempty"` +- // Capabilities specific to the `textDocument/references` request. +- References *ReferenceClientCapabilities `json:"references,omitempty"` +- // Capabilities specific to the `textDocument/documentHighlight` request. +- DocumentHighlight *DocumentHighlightClientCapabilities `json:"documentHighlight,omitempty"` +- // Capabilities specific to the `textDocument/documentSymbol` request. +- DocumentSymbol DocumentSymbolClientCapabilities `json:"documentSymbol,omitempty"` +- // Capabilities specific to the `textDocument/codeAction` request. +- CodeAction CodeActionClientCapabilities `json:"codeAction,omitempty"` +- // Capabilities specific to the `textDocument/codeLens` request. +- CodeLens *CodeLensClientCapabilities `json:"codeLens,omitempty"` +- // Capabilities specific to the `textDocument/documentLink` request. +- DocumentLink *DocumentLinkClientCapabilities `json:"documentLink,omitempty"` +- // Capabilities specific to the `textDocument/documentColor` and the +- // `textDocument/colorPresentation` request. +- // +- // @since 3.6.0 +- ColorProvider *DocumentColorClientCapabilities `json:"colorProvider,omitempty"` +- // Capabilities specific to the `textDocument/formatting` request. +- Formatting *DocumentFormattingClientCapabilities `json:"formatting,omitempty"` +- // Capabilities specific to the `textDocument/rangeFormatting` request. +- RangeFormatting *DocumentRangeFormattingClientCapabilities `json:"rangeFormatting,omitempty"` +- // Capabilities specific to the `textDocument/onTypeFormatting` request. +- OnTypeFormatting *DocumentOnTypeFormattingClientCapabilities `json:"onTypeFormatting,omitempty"` +- // Capabilities specific to the `textDocument/rename` request. +- Rename *RenameClientCapabilities `json:"rename,omitempty"` +- // Capabilities specific to the `textDocument/foldingRange` request. +- // +- // @since 3.10.0 +- FoldingRange *FoldingRangeClientCapabilities `json:"foldingRange,omitempty"` +- // Capabilities specific to the `textDocument/selectionRange` request. +- // +- // @since 3.15.0 +- SelectionRange *SelectionRangeClientCapabilities `json:"selectionRange,omitempty"` +- // Capabilities specific to the `textDocument/publishDiagnostics` notification. +- PublishDiagnostics PublishDiagnosticsClientCapabilities `json:"publishDiagnostics,omitempty"` +- // Capabilities specific to the various call hierarchy requests. +- // +- // @since 3.16.0 +- CallHierarchy *CallHierarchyClientCapabilities `json:"callHierarchy,omitempty"` +- // Capabilities specific to the various semantic token request. +- // +- // @since 3.16.0 +- SemanticTokens SemanticTokensClientCapabilities `json:"semanticTokens,omitempty"` +- // Capabilities specific to the `textDocument/linkedEditingRange` request. +- // +- // @since 3.16.0 +- LinkedEditingRange *LinkedEditingRangeClientCapabilities `json:"linkedEditingRange,omitempty"` +- // Client capabilities specific to the `textDocument/moniker` request. +- // +- // @since 3.16.0 +- Moniker *MonikerClientCapabilities `json:"moniker,omitempty"` +- // Capabilities specific to the various type hierarchy requests. +- // +- // @since 3.17.0 +- TypeHierarchy *TypeHierarchyClientCapabilities `json:"typeHierarchy,omitempty"` +- // Capabilities specific to the `textDocument/inlineValue` request. +- // +- // @since 3.17.0 +- InlineValue *InlineValueClientCapabilities `json:"inlineValue,omitempty"` +- // Capabilities specific to the `textDocument/inlayHint` request. +- // +- // @since 3.17.0 +- InlayHint *InlayHintClientCapabilities `json:"inlayHint,omitempty"` +- // Capabilities specific to the diagnostic pull model. +- // +- // @since 3.17.0 +- Diagnostic *DiagnosticClientCapabilities `json:"diagnostic,omitempty"` +- // Client capabilities specific to inline completions. +- // +- // @since 3.18.0 +- // @proposed +- InlineCompletion *InlineCompletionClientCapabilities `json:"inlineCompletion,omitempty"` -} - --// importEdits produces the text edits necessary to add the given import to the current file. --func (c *completer) importEdits(imp *importInfo) ([]protocol.TextEdit, error) { -- if imp == nil { -- return nil, nil -- } +-// An event describing a change to a text document. If only a text is provided +-// it is considered to be the full content of the document. +-type TextDocumentContentChangeEvent = TextDocumentContentChangePartial // (alias) +-// @since 3.18.0 +-// @proposed +-type TextDocumentContentChangePartial struct { +- // The range of the document that changed. +- Range *Range `json:"range,omitempty"` +- // The optional length of the range that got replaced. +- // +- // @deprecated use range instead. +- RangeLength uint32 `json:"rangeLength,omitempty"` +- // The new text for the provided range. +- Text string `json:"text"` +-} - -- pgf, err := c.pkg.File(span.URIFromPath(c.filename)) -- if err != nil { -- return nil, err -- } +-// @since 3.18.0 +-// @proposed +-type TextDocumentContentChangeWholeDocument struct { +- // The new text of the whole document. +- Text string `json:"text"` +-} - -- return source.ComputeOneImportFixEdits(c.snapshot, pgf, &imports.ImportFix{ -- StmtInfo: imports.ImportInfo{ -- ImportPath: imp.importPath, -- Name: imp.name, -- }, -- // IdentName is unused on this path and is difficult to get. -- FixType: imports.AddImport, -- }) +-// Describes textual changes on a text document. A TextDocumentEdit describes all changes +-// on a document version Si and after they are applied move the document to version Si+1. +-// So the creator of a TextDocumentEdit doesn't need to sort the array of edits or do any +-// kind of ordering. However the edits must be non overlapping. +-type TextDocumentEdit struct { +- // The text document to change. +- TextDocument OptionalVersionedTextDocumentIdentifier `json:"textDocument"` +- // The edits to be applied. +- // +- // @since 3.16.0 - support for AnnotatedTextEdit. This is guarded using a +- // client capability. +- Edits []Or_TextDocumentEdit_edits_Elem `json:"edits"` -} - --func (c *completer) formatBuiltin(ctx context.Context, cand candidate) (CompletionItem, error) { -- obj := cand.obj -- item := CompletionItem{ -- Label: obj.Name(), -- InsertText: obj.Name(), -- Score: cand.score, -- } -- switch obj.(type) { -- case *types.Const: -- item.Kind = protocol.ConstantCompletion -- case *types.Builtin: -- item.Kind = protocol.FunctionCompletion -- sig, err := source.NewBuiltinSignature(ctx, c.snapshot, obj.Name()) -- if err != nil { -- return CompletionItem{}, err -- } -- item.Detail = "func" + sig.Format() -- item.snippet = &snippet.Builder{} -- c.functionCallSnippet(obj.Name(), sig.TypeParams(), sig.Params(), item.snippet) -- case *types.TypeName: -- if types.IsInterface(obj.Type()) { -- item.Kind = protocol.InterfaceCompletion -- } else { -- item.Kind = protocol.ClassCompletion -- } -- case *types.Nil: -- item.Kind = protocol.VariableCompletion -- } -- return item, nil +-// A document filter denotes a document by different properties like +-// the {@link TextDocument.languageId language}, the {@link Uri.scheme scheme} of +-// its resource, or a glob-pattern that is applied to the {@link TextDocument.fileName path}. +-// +-// Glob patterns can have the following syntax: +-// +-// - `*` to match one or more characters in a path segment +-// - `?` to match on one character in a path segment +-// - `**` to match any number of path segments, including none +-// - `{}` to group sub patterns into an OR expression. (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files) +-// - `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) +-// - `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`) +-// +-// @sample A language filter that applies to typescript files on disk: `{ language: 'typescript', scheme: 'file' }` +-// @sample A language filter that applies to all package.json paths: `{ language: 'json', pattern: '**package.json' }` +-// +-// @since 3.17.0 +-type TextDocumentFilter = Or_TextDocumentFilter // (alias) +-// A document filter where `language` is required field. +-// +-// @since 3.18.0 +-// @proposed +-type TextDocumentFilterLanguage struct { +- // A language id, like `typescript`. +- Language string `json:"language"` +- // A Uri {@link Uri.scheme scheme}, like `file` or `untitled`. +- Scheme string `json:"scheme,omitempty"` +- // A glob pattern, like **​/*.{ts,js}. See TextDocumentFilter for examples. +- Pattern string `json:"pattern,omitempty"` +-} +- +-// A document filter where `pattern` is required field. +-// +-// @since 3.18.0 +-// @proposed +-type TextDocumentFilterPattern struct { +- // A language id, like `typescript`. +- Language string `json:"language,omitempty"` +- // A Uri {@link Uri.scheme scheme}, like `file` or `untitled`. +- Scheme string `json:"scheme,omitempty"` +- // A glob pattern, like **​/*.{ts,js}. See TextDocumentFilter for examples. +- Pattern string `json:"pattern"` -} - --// decide if the type params (if any) should be part of the completion --// which only possible for types.Named and types.Signature --// (so far, only in receivers, e.g.; func (s *GENERIC[K, V])..., which is a types.Named) --func (c *completer) wantTypeParams() bool { -- // Need to be lexically in a receiver, and a child of an IndexListExpr -- // (but IndexListExpr only exists with go1.18) -- start := c.path[0].Pos() -- for i, nd := range c.path { -- if fd, ok := nd.(*ast.FuncDecl); ok { -- if i > 0 && fd.Recv != nil && start < fd.Recv.End() { -- return true -- } else { -- return false -- } -- } -- } -- return false +-// A document filter where `scheme` is required field. +-// +-// @since 3.18.0 +-// @proposed +-type TextDocumentFilterScheme struct { +- // A language id, like `typescript`. +- Language string `json:"language,omitempty"` +- // A Uri {@link Uri.scheme scheme}, like `file` or `untitled`. +- Scheme string `json:"scheme"` +- // A glob pattern, like **​/*.{ts,js}. See TextDocumentFilter for examples. +- Pattern string `json:"pattern,omitempty"` -} -diff -urN a/gopls/internal/lsp/source/completion/fuzz.go b/gopls/internal/lsp/source/completion/fuzz.go ---- a/gopls/internal/lsp/source/completion/fuzz.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/completion/fuzz.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,142 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package completion -- --import ( -- "fmt" -- "go/ast" -- "go/token" -- "go/types" -- "strings" +-// A literal to identify a text document in the client. +-type TextDocumentIdentifier struct { +- // The text document's uri. +- URI DocumentURI `json:"uri"` +-} - -- "golang.org/x/tools/gopls/internal/lsp/protocol" --) +-// An item to transfer a text document from the client to the +-// server. +-type TextDocumentItem struct { +- // The text document's uri. +- URI DocumentURI `json:"uri"` +- // The text document's language identifier. +- LanguageID LanguageKind `json:"languageId"` +- // The version number of this document (it will increase after each +- // change, including undo/redo). +- Version int32 `json:"version"` +- // The content of the opened text document. +- Text string `json:"text"` +-} - --// golang/go#51089 --// *testing.F deserves special treatment as member use is constrained: --// The arguments to f.Fuzz are determined by the arguments to a previous f.Add --// Inside f.Fuzz only f.Failed and f.Name are allowed. --// PJW: are there other packages where we can deduce usage constraints? +-// A parameter literal used in requests to pass a text document and a position inside that +-// document. +-type TextDocumentPositionParams struct { +- // The text document. +- TextDocument TextDocumentIdentifier `json:"textDocument"` +- // The position inside the text document. +- Position Position `json:"position"` +-} - --// if we find fuzz completions, then return true, as those are the only completions to offer --func (c *completer) fuzz(typ types.Type, mset *types.MethodSet, imp *importInfo, cb func(candidate), fset *token.FileSet) bool { -- // 1. inside f.Fuzz? (only f.Failed and f.Name) -- // 2. possible completing f.Fuzz? -- // [Ident,SelectorExpr,Callexpr,ExprStmt,BlockiStmt,FuncDecl(Fuzz...)] -- // 3. before f.Fuzz, same (for 2., offer choice when looking at an F) +-// General text document registration options. +-type TextDocumentRegistrationOptions struct { +- // A document selector to identify the scope of the registration. If set to null +- // the document selector provided on the client side will be used. +- DocumentSelector DocumentSelector `json:"documentSelector"` +-} - -- // does the path contain FuncLit as arg to f.Fuzz CallExpr? -- inside := false --Loop: -- for i, n := range c.path { -- switch v := n.(type) { -- case *ast.CallExpr: -- if len(v.Args) != 1 { -- continue Loop -- } -- if _, ok := v.Args[0].(*ast.FuncLit); !ok { -- continue -- } -- if s, ok := v.Fun.(*ast.SelectorExpr); !ok || s.Sel.Name != "Fuzz" { -- continue -- } -- if i > 2 { // avoid t.Fuzz itself in tests -- inside = true -- break Loop -- } -- } -- } -- if inside { -- for i := 0; i < mset.Len(); i++ { -- o := mset.At(i).Obj() -- if o.Name() == "Failed" || o.Name() == "Name" { -- cb(candidate{ -- obj: o, -- score: stdScore, -- imp: imp, -- addressable: true, -- }) -- } -- } -- return true -- } -- // if it could be t.Fuzz, look for the preceding t.Add -- id, ok := c.path[0].(*ast.Ident) -- if ok && strings.HasPrefix("Fuzz", id.Name) { -- var add *ast.CallExpr -- f := func(n ast.Node) bool { -- if n == nil { -- return true -- } -- call, ok := n.(*ast.CallExpr) -- if !ok { -- return true -- } -- s, ok := call.Fun.(*ast.SelectorExpr) -- if !ok { -- return true -- } -- if s.Sel.Name != "Add" { -- return true -- } -- // Sel.X should be of type *testing.F -- got := c.pkg.GetTypesInfo().Types[s.X] -- if got.Type.String() == "*testing.F" { -- add = call -- } -- return false // because we're done... -- } -- // look at the enclosing FuzzFoo functions -- if len(c.path) < 2 { -- return false -- } -- n := c.path[len(c.path)-2] -- if _, ok := n.(*ast.FuncDecl); !ok { -- // the path should start with ast.File, ast.FuncDecl, ... -- // but it didn't, so give up -- return false -- } -- ast.Inspect(n, f) -- if add == nil { -- // looks like f.Fuzz without a preceding f.Add. -- // let the regular completion handle it. -- return false -- } +-// Represents reasons why a text document is saved. +-type TextDocumentSaveReason uint32 - -- lbl := "Fuzz(func(t *testing.T" -- for i, a := range add.Args { -- info := c.pkg.GetTypesInfo().TypeOf(a) -- if info == nil { -- return false // How could this happen, but better safe than panic. -- } -- lbl += fmt.Sprintf(", %c %s", 'a'+i, info) -- } -- lbl += ")" -- xx := CompletionItem{ -- Label: lbl, -- InsertText: lbl, -- Kind: protocol.FunctionCompletion, -- Depth: 0, -- Score: 10, // pretty confident the user should see this -- Documentation: "argument types from f.Add", -- isSlice: false, -- } -- c.items = append(c.items, xx) -- for i := 0; i < mset.Len(); i++ { -- o := mset.At(i).Obj() -- if o.Name() != "Fuzz" { -- cb(candidate{ -- obj: o, -- score: stdScore, -- imp: imp, -- addressable: true, -- }) -- } -- } -- return true // done -- } -- // let the standard processing take care of it instead -- return false +-// Save registration options. +-type TextDocumentSaveRegistrationOptions struct { +- TextDocumentRegistrationOptions +- SaveOptions +-} +-type TextDocumentSyncClientCapabilities struct { +- // Whether text document synchronization supports dynamic registration. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +- // The client supports sending will save notifications. +- WillSave bool `json:"willSave,omitempty"` +- // The client supports sending a will save request and +- // waits for a response providing text edits which will +- // be applied to the document before it is saved. +- WillSaveWaitUntil bool `json:"willSaveWaitUntil,omitempty"` +- // The client supports did save notifications. +- DidSave bool `json:"didSave,omitempty"` -} -diff -urN a/gopls/internal/lsp/source/completion/keywords.go b/gopls/internal/lsp/source/completion/keywords.go ---- a/gopls/internal/lsp/source/completion/keywords.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/completion/keywords.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,154 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package completion +-// Defines how the host (editor) should sync +-// document changes to the language server. +-type TextDocumentSyncKind uint32 +-type TextDocumentSyncOptions struct { +- // Open and close notifications are sent to the server. If omitted open close notification should not +- // be sent. +- OpenClose bool `json:"openClose,omitempty"` +- // Change notifications are sent to the server. See TextDocumentSyncKind.None, TextDocumentSyncKind.Full +- // and TextDocumentSyncKind.Incremental. If omitted it defaults to TextDocumentSyncKind.None. +- Change TextDocumentSyncKind `json:"change,omitempty"` +- // If present will save notifications are sent to the server. If omitted the notification should not be +- // sent. +- WillSave bool `json:"willSave,omitempty"` +- // If present will save wait until requests are sent to the server. If omitted the request should not be +- // sent. +- WillSaveWaitUntil bool `json:"willSaveWaitUntil,omitempty"` +- // If present save notifications are sent to the server. If omitted the notification should not be +- // sent. +- Save *SaveOptions `json:"save,omitempty"` +-} - --import ( -- "go/ast" +-// A text edit applicable to a text document. +-type TextEdit struct { +- // The range of the text document to be manipulated. To insert +- // text into a document create a range where start === end. +- Range Range `json:"range"` +- // The string to be inserted. For delete operations use an +- // empty string. +- NewText string `json:"newText"` +-} +-type TokenFormat string +-type TraceValue string - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" --) +-// Since 3.6.0 +-type TypeDefinitionClientCapabilities struct { +- // Whether implementation supports dynamic registration. If this is set to `true` +- // the client supports the new `TypeDefinitionRegistrationOptions` return value +- // for the corresponding server capability as well. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +- // The client supports additional metadata in the form of definition links. +- // +- // Since 3.14.0 +- LinkSupport bool `json:"linkSupport,omitempty"` +-} +-type TypeDefinitionOptions struct { +- WorkDoneProgressOptions +-} +-type TypeDefinitionParams struct { +- TextDocumentPositionParams +- WorkDoneProgressParams +- PartialResultParams +-} +-type TypeDefinitionRegistrationOptions struct { +- TextDocumentRegistrationOptions +- TypeDefinitionOptions +- StaticRegistrationOptions +-} - --const ( -- BREAK = "break" -- CASE = "case" -- CHAN = "chan" -- CONST = "const" -- CONTINUE = "continue" -- DEFAULT = "default" -- DEFER = "defer" -- ELSE = "else" -- FALLTHROUGH = "fallthrough" -- FOR = "for" -- FUNC = "func" -- GO = "go" -- GOTO = "goto" -- IF = "if" -- IMPORT = "import" -- INTERFACE = "interface" -- MAP = "map" -- PACKAGE = "package" -- RANGE = "range" -- RETURN = "return" -- SELECT = "select" -- STRUCT = "struct" -- SWITCH = "switch" -- TYPE = "type" -- VAR = "var" --) +-// @since 3.17.0 +-type TypeHierarchyClientCapabilities struct { +- // Whether implementation supports dynamic registration. If this is set to `true` +- // the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)` +- // return value for the corresponding server capability as well. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +-} - --// addKeywordCompletions offers keyword candidates appropriate at the position. --func (c *completer) addKeywordCompletions() { -- seen := make(map[string]bool) +-// @since 3.17.0 +-type TypeHierarchyItem struct { +- // The name of this item. +- Name string `json:"name"` +- // The kind of this item. +- Kind SymbolKind `json:"kind"` +- // Tags for this item. +- Tags []SymbolTag `json:"tags,omitempty"` +- // More detail for this item, e.g. the signature of a function. +- Detail string `json:"detail,omitempty"` +- // The resource identifier of this item. +- URI DocumentURI `json:"uri"` +- // The range enclosing this symbol not including leading/trailing whitespace +- // but everything else, e.g. comments and code. +- Range Range `json:"range"` +- // The range that should be selected and revealed when this symbol is being +- // picked, e.g. the name of a function. Must be contained by the +- // {@link TypeHierarchyItem.range `range`}. +- SelectionRange Range `json:"selectionRange"` +- // A data entry field that is preserved between a type hierarchy prepare and +- // supertypes or subtypes requests. It could also be used to identify the +- // type hierarchy in the server, helping improve the performance on +- // resolving supertypes and subtypes. +- Data interface{} `json:"data,omitempty"` +-} - -- if c.wantTypeName() && c.inference.objType == nil { -- // If we want a type name but don't have an expected obj type, -- // include "interface", "struct", "func", "chan", and "map". +-// Type hierarchy options used during static registration. +-// +-// @since 3.17.0 +-type TypeHierarchyOptions struct { +- WorkDoneProgressOptions +-} - -- // "interface" and "struct" are more common declaring named types. -- // Give them a higher score if we are in a type declaration. -- structIntf, funcChanMap := stdScore, highScore -- if len(c.path) > 1 { -- if _, namedDecl := c.path[1].(*ast.TypeSpec); namedDecl { -- structIntf, funcChanMap = highScore, stdScore -- } -- } +-// The parameter of a `textDocument/prepareTypeHierarchy` request. +-// +-// @since 3.17.0 +-type TypeHierarchyPrepareParams struct { +- TextDocumentPositionParams +- WorkDoneProgressParams +-} - -- c.addKeywordItems(seen, structIntf, STRUCT, INTERFACE) -- c.addKeywordItems(seen, funcChanMap, FUNC, CHAN, MAP) -- } +-// Type hierarchy options used during static or dynamic registration. +-// +-// @since 3.17.0 +-type TypeHierarchyRegistrationOptions struct { +- TextDocumentRegistrationOptions +- TypeHierarchyOptions +- StaticRegistrationOptions +-} - -- // If we are at the file scope, only offer decl keywords. We don't -- // get *ast.Idents at the file scope because non-keyword identifiers -- // turn into *ast.BadDecl, not *ast.Ident. -- if len(c.path) == 1 || isASTFile(c.path[1]) { -- c.addKeywordItems(seen, stdScore, TYPE, CONST, VAR, FUNC, IMPORT) -- return -- } else if _, ok := c.path[0].(*ast.Ident); !ok { -- // Otherwise only offer keywords if the client is completing an identifier. -- return -- } +-// The parameter of a `typeHierarchy/subtypes` request. +-// +-// @since 3.17.0 +-type TypeHierarchySubtypesParams struct { +- Item TypeHierarchyItem `json:"item"` +- WorkDoneProgressParams +- PartialResultParams +-} - -- if len(c.path) > 2 { -- // Offer "range" if we are in ast.ForStmt.Init. This is what the -- // AST looks like before "range" is typed, e.g. "for i := r<>". -- if loop, ok := c.path[2].(*ast.ForStmt); ok && source.NodeContains(loop.Init, c.pos) { -- c.addKeywordItems(seen, stdScore, RANGE) -- } -- } +-// The parameter of a `typeHierarchy/supertypes` request. +-// +-// @since 3.17.0 +-type TypeHierarchySupertypesParams struct { +- Item TypeHierarchyItem `json:"item"` +- WorkDoneProgressParams +- PartialResultParams +-} - -- // Only suggest keywords if we are beginning a statement. -- switch n := c.path[1].(type) { -- case *ast.BlockStmt, *ast.ExprStmt: -- // OK - our ident must be at beginning of statement. -- case *ast.CommClause: -- // Make sure we aren't in the Comm statement. -- if !n.Colon.IsValid() || c.pos <= n.Colon { -- return -- } -- case *ast.CaseClause: -- // Make sure we aren't in the case List. -- if !n.Colon.IsValid() || c.pos <= n.Colon { -- return -- } -- default: -- return -- } +-// created for Tuple +-type UIntCommaUInt struct { +- Fld0 uint32 `json:"fld0"` +- Fld1 uint32 `json:"fld1"` +-} - -- // Filter out keywords depending on scope -- // Skip the first one because we want to look at the enclosing scopes -- path := c.path[1:] -- for i, n := range path { -- switch node := n.(type) { -- case *ast.CaseClause: -- // only recommend "fallthrough" and "break" within the bodies of a case clause -- if c.pos > node.Colon { -- c.addKeywordItems(seen, stdScore, BREAK) -- // "fallthrough" is only valid in switch statements. -- // A case clause is always nested within a block statement in a switch statement, -- // that block statement is nested within either a TypeSwitchStmt or a SwitchStmt. -- if i+2 >= len(path) { -- continue -- } -- if _, ok := path[i+2].(*ast.SwitchStmt); ok { -- c.addKeywordItems(seen, stdScore, FALLTHROUGH) -- } -- } -- case *ast.CommClause: -- if c.pos > node.Colon { -- c.addKeywordItems(seen, stdScore, BREAK) -- } -- case *ast.TypeSwitchStmt, *ast.SelectStmt, *ast.SwitchStmt: -- c.addKeywordItems(seen, stdScore, CASE, DEFAULT) -- case *ast.ForStmt, *ast.RangeStmt: -- c.addKeywordItems(seen, stdScore, BREAK, CONTINUE) -- // This is a bit weak, functions allow for many keywords -- case *ast.FuncDecl: -- if node.Body != nil && c.pos > node.Body.Lbrace { -- c.addKeywordItems(seen, stdScore, DEFER, RETURN, FOR, GO, SWITCH, SELECT, IF, ELSE, VAR, CONST, GOTO, TYPE) -- } -- } -- } +-// A diagnostic report indicating that the last returned +-// report is still accurate. +-// +-// @since 3.17.0 +-type UnchangedDocumentDiagnosticReport struct { +- // A document diagnostic report indicating +- // no changes to the last result. A server can +- // only return `unchanged` if result ids are +- // provided. +- Kind string `json:"kind"` +- // A result id which will be sent on the next +- // diagnostic request for the same document. +- ResultID string `json:"resultId"` -} - --// addKeywordItems dedupes and adds completion items for the specified --// keywords with the specified score. --func (c *completer) addKeywordItems(seen map[string]bool, score float64, kws ...string) { -- for _, kw := range kws { -- if seen[kw] { -- continue -- } -- seen[kw] = true +-// Moniker uniqueness level to define scope of the moniker. +-// +-// @since 3.16.0 +-type UniquenessLevel string - -- if matchScore := c.matcher.Score(kw); matchScore > 0 { -- c.items = append(c.items, CompletionItem{ -- Label: kw, -- Kind: protocol.KeywordCompletion, -- InsertText: kw, -- Score: score * float64(matchScore), -- }) -- } -- } +-// General parameters to unregister a request or notification. +-type Unregistration struct { +- // The id used to unregister the request or notification. Usually an id +- // provided during the register request. +- ID string `json:"id"` +- // The method to unregister for. +- Method string `json:"method"` +-} +-type UnregistrationParams struct { +- Unregisterations []Unregistration `json:"unregisterations"` +-} +- +-// A versioned notebook document identifier. +-// +-// @since 3.17.0 +-type VersionedNotebookDocumentIdentifier struct { +- // The version number of this notebook document. +- Version int32 `json:"version"` +- // The notebook document's uri. +- URI URI `json:"uri"` -} -diff -urN a/gopls/internal/lsp/source/completion/labels.go b/gopls/internal/lsp/source/completion/labels.go ---- a/gopls/internal/lsp/source/completion/labels.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/completion/labels.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,112 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package completion - --import ( -- "go/ast" -- "go/token" -- "math" --) +-// A text document identifier to denote a specific version of a text document. +-type VersionedTextDocumentIdentifier struct { +- // The version number of this document. +- Version int32 `json:"version"` +- TextDocumentIdentifier +-} +-type WatchKind = uint32 // The parameters sent in a will save text document notification. +-type WillSaveTextDocumentParams struct { +- // The document that will be saved. +- TextDocument TextDocumentIdentifier `json:"textDocument"` +- // The 'TextDocumentSaveReason'. +- Reason TextDocumentSaveReason `json:"reason"` +-} +-type WindowClientCapabilities struct { +- // It indicates whether the client supports server initiated +- // progress using the `window/workDoneProgress/create` request. +- // +- // The capability also controls Whether client supports handling +- // of progress notifications. If set servers are allowed to report a +- // `workDoneProgress` property in the request specific server +- // capabilities. +- // +- // @since 3.15.0 +- WorkDoneProgress bool `json:"workDoneProgress,omitempty"` +- // Capabilities specific to the showMessage request. +- // +- // @since 3.16.0 +- ShowMessage *ShowMessageRequestClientCapabilities `json:"showMessage,omitempty"` +- // Capabilities specific to the showDocument request. +- // +- // @since 3.16.0 +- ShowDocument *ShowDocumentClientCapabilities `json:"showDocument,omitempty"` +-} +-type WorkDoneProgressBegin struct { +- Kind string `json:"kind"` +- // Mandatory title of the progress operation. Used to briefly inform about +- // the kind of operation being performed. +- // +- // Examples: "Indexing" or "Linking dependencies". +- Title string `json:"title"` +- // Controls if a cancel button should show to allow the user to cancel the +- // long running operation. Clients that don't support cancellation are allowed +- // to ignore the setting. +- Cancellable bool `json:"cancellable,omitempty"` +- // Optional, more detailed associated progress message. Contains +- // complementary information to the `title`. +- // +- // Examples: "3/25 files", "project/src/module2", "node_modules/some_dep". +- // If unset, the previous progress message (if any) is still valid. +- Message string `json:"message,omitempty"` +- // Optional progress percentage to display (value 100 is considered 100%). +- // If not provided infinite progress is assumed and clients are allowed +- // to ignore the `percentage` value in subsequent in report notifications. +- // +- // The value should be steadily rising. Clients are free to ignore values +- // that are not following this rule. The value range is [0, 100]. +- Percentage uint32 `json:"percentage,omitempty"` +-} +-type WorkDoneProgressCancelParams struct { +- // The token to be used to report progress. +- Token ProgressToken `json:"token"` +-} +-type WorkDoneProgressCreateParams struct { +- // The token to be used to report progress. +- Token ProgressToken `json:"token"` +-} +-type WorkDoneProgressEnd struct { +- Kind string `json:"kind"` +- // Optional, a final message indicating to for example indicate the outcome +- // of the operation. +- Message string `json:"message,omitempty"` +-} +-type WorkDoneProgressOptions struct { +- WorkDoneProgress bool `json:"workDoneProgress,omitempty"` +-} - --type labelType int +-// created for And +-type WorkDoneProgressOptionsAndTextDocumentRegistrationOptions struct { +- WorkDoneProgressOptions +- TextDocumentRegistrationOptions +-} +-type WorkDoneProgressParams struct { +- // An optional token that a server can use to report work done progress. +- WorkDoneToken ProgressToken `json:"workDoneToken,omitempty"` +-} +-type WorkDoneProgressReport struct { +- Kind string `json:"kind"` +- // Controls enablement state of a cancel button. +- // +- // Clients that don't support cancellation or don't support controlling the button's +- // enablement state are allowed to ignore the property. +- Cancellable bool `json:"cancellable,omitempty"` +- // Optional, more detailed associated progress message. Contains +- // complementary information to the `title`. +- // +- // Examples: "3/25 files", "project/src/module2", "node_modules/some_dep". +- // If unset, the previous progress message (if any) is still valid. +- Message string `json:"message,omitempty"` +- // Optional progress percentage to display (value 100 is considered 100%). +- // If not provided infinite progress is assumed and clients are allowed +- // to ignore the `percentage` value in subsequent in report notifications. +- // +- // The value should be steadily rising. Clients are free to ignore values +- // that are not following this rule. The value range is [0, 100] +- Percentage uint32 `json:"percentage,omitempty"` +-} - --const ( -- labelNone labelType = iota -- labelBreak -- labelContinue -- labelGoto --) +-// Workspace specific client capabilities. +-type WorkspaceClientCapabilities struct { +- // The client supports applying batch edits +- // to the workspace by supporting the request +- // 'workspace/applyEdit' +- ApplyEdit bool `json:"applyEdit,omitempty"` +- // Capabilities specific to `WorkspaceEdit`s. +- WorkspaceEdit *WorkspaceEditClientCapabilities `json:"workspaceEdit,omitempty"` +- // Capabilities specific to the `workspace/didChangeConfiguration` notification. +- DidChangeConfiguration DidChangeConfigurationClientCapabilities `json:"didChangeConfiguration,omitempty"` +- // Capabilities specific to the `workspace/didChangeWatchedFiles` notification. +- DidChangeWatchedFiles DidChangeWatchedFilesClientCapabilities `json:"didChangeWatchedFiles,omitempty"` +- // Capabilities specific to the `workspace/symbol` request. +- Symbol *WorkspaceSymbolClientCapabilities `json:"symbol,omitempty"` +- // Capabilities specific to the `workspace/executeCommand` request. +- ExecuteCommand *ExecuteCommandClientCapabilities `json:"executeCommand,omitempty"` +- // The client has support for workspace folders. +- // +- // @since 3.6.0 +- WorkspaceFolders bool `json:"workspaceFolders,omitempty"` +- // The client supports `workspace/configuration` requests. +- // +- // @since 3.6.0 +- Configuration bool `json:"configuration,omitempty"` +- // Capabilities specific to the semantic token requests scoped to the +- // workspace. +- // +- // @since 3.16.0. +- SemanticTokens *SemanticTokensWorkspaceClientCapabilities `json:"semanticTokens,omitempty"` +- // Capabilities specific to the code lens requests scoped to the +- // workspace. +- // +- // @since 3.16.0. +- CodeLens *CodeLensWorkspaceClientCapabilities `json:"codeLens,omitempty"` +- // The client has support for file notifications/requests for user operations on files. +- // +- // Since 3.16.0 +- FileOperations *FileOperationClientCapabilities `json:"fileOperations,omitempty"` +- // Capabilities specific to the inline values requests scoped to the +- // workspace. +- // +- // @since 3.17.0. +- InlineValue *InlineValueWorkspaceClientCapabilities `json:"inlineValue,omitempty"` +- // Capabilities specific to the inlay hint requests scoped to the +- // workspace. +- // +- // @since 3.17.0. +- InlayHint *InlayHintWorkspaceClientCapabilities `json:"inlayHint,omitempty"` +- // Capabilities specific to the diagnostic requests scoped to the +- // workspace. +- // +- // @since 3.17.0. +- Diagnostics *DiagnosticWorkspaceClientCapabilities `json:"diagnostics,omitempty"` +- // Capabilities specific to the folding range requests scoped to the workspace. +- // +- // @since 3.18.0 +- // @proposed +- FoldingRange *FoldingRangeWorkspaceClientCapabilities `json:"foldingRange,omitempty"` +-} - --// wantLabelCompletion returns true if we want (only) label --// completions at the position. --func (c *completer) wantLabelCompletion() labelType { -- if _, ok := c.path[0].(*ast.Ident); ok && len(c.path) > 1 { -- // We want a label if we are an *ast.Ident child of a statement -- // that accepts a label, e.g. "break Lo<>". -- return takesLabel(c.path[1]) -- } +-// Parameters of the workspace diagnostic request. +-// +-// @since 3.17.0 +-type WorkspaceDiagnosticParams struct { +- // The additional identifier provided during registration. +- Identifier string `json:"identifier,omitempty"` +- // The currently known diagnostic reports with their +- // previous result ids. +- PreviousResultIds []PreviousResultID `json:"previousResultIds"` +- WorkDoneProgressParams +- PartialResultParams +-} - -- return labelNone +-// A workspace diagnostic report. +-// +-// @since 3.17.0 +-type WorkspaceDiagnosticReport struct { +- Items []WorkspaceDocumentDiagnosticReport `json:"items"` -} - --// takesLabel returns the corresponding labelType if n is a statement --// that accepts a label, otherwise labelNone. --func takesLabel(n ast.Node) labelType { -- if bs, ok := n.(*ast.BranchStmt); ok { -- switch bs.Tok { -- case token.BREAK: -- return labelBreak -- case token.CONTINUE: -- return labelContinue -- case token.GOTO: -- return labelGoto -- } -- } -- return labelNone +-// A partial result for a workspace diagnostic report. +-// +-// @since 3.17.0 +-type WorkspaceDiagnosticReportPartialResult struct { +- Items []WorkspaceDocumentDiagnosticReport `json:"items"` -} - --// labels adds completion items for labels defined in the enclosing --// function. --func (c *completer) labels(lt labelType) { -- if c.enclosingFunc == nil { -- return -- } +-// A workspace diagnostic document report. +-// +-// @since 3.17.0 +-type WorkspaceDocumentDiagnosticReport = Or_WorkspaceDocumentDiagnosticReport // (alias) +-// A workspace edit represents changes to many resources managed in the workspace. The edit +-// should either provide `changes` or `documentChanges`. If documentChanges are present +-// they are preferred over `changes` if the client can handle versioned document edits. +-// +-// Since version 3.13.0 a workspace edit can contain resource operations as well. If resource +-// operations are present clients need to execute the operations in the order in which they +-// are provided. So a workspace edit for example can consist of the following two changes: +-// (1) a create file a.txt and (2) a text document edit which insert text into file a.txt. +-// +-// An invalid sequence (e.g. (1) delete file a.txt and (2) insert text into file a.txt) will +-// cause failure of the operation. How the client recovers from the failure is described by +-// the client capability: `workspace.workspaceEdit.failureHandling` +-type WorkspaceEdit struct { +- // Holds changes to existing resources. +- Changes map[DocumentURI][]TextEdit `json:"changes,omitempty"` +- // Depending on the client capability `workspace.workspaceEdit.resourceOperations` document changes +- // are either an array of `TextDocumentEdit`s to express changes to n different text documents +- // where each text document edit addresses a specific version of a text document. Or it can contain +- // above `TextDocumentEdit`s mixed with create, rename and delete file / folder operations. +- // +- // Whether a client supports versioned document edits is expressed via +- // `workspace.workspaceEdit.documentChanges` client capability. +- // +- // If a client neither supports `documentChanges` nor `workspace.workspaceEdit.resourceOperations` then +- // only plain `TextEdit`s using the `changes` property are supported. +- DocumentChanges []DocumentChanges `json:"documentChanges,omitempty"` +- // A map of change annotations that can be referenced in `AnnotatedTextEdit`s or create, rename and +- // delete file / folder operations. +- // +- // Whether clients honor this property depends on the client capability `workspace.changeAnnotationSupport`. +- // +- // @since 3.16.0 +- ChangeAnnotations map[ChangeAnnotationIdentifier]ChangeAnnotation `json:"changeAnnotations,omitempty"` +-} +-type WorkspaceEditClientCapabilities struct { +- // The client supports versioned document changes in `WorkspaceEdit`s +- DocumentChanges bool `json:"documentChanges,omitempty"` +- // The resource operations the client supports. Clients should at least +- // support 'create', 'rename' and 'delete' files and folders. +- // +- // @since 3.13.0 +- ResourceOperations []ResourceOperationKind `json:"resourceOperations,omitempty"` +- // The failure handling strategy of a client if applying the workspace edit +- // fails. +- // +- // @since 3.13.0 +- FailureHandling *FailureHandlingKind `json:"failureHandling,omitempty"` +- // Whether the client normalizes line endings to the client specific +- // setting. +- // If set to `true` the client will normalize line ending characters +- // in a workspace edit to the client-specified new line +- // character. +- // +- // @since 3.16.0 +- NormalizesLineEndings bool `json:"normalizesLineEndings,omitempty"` +- // Whether the client in general supports change annotations on text edits, +- // create file, rename file and delete file changes. +- // +- // @since 3.16.0 +- ChangeAnnotationSupport *ChangeAnnotationsSupportOptions `json:"changeAnnotationSupport,omitempty"` +-} - -- addLabel := func(score float64, l *ast.LabeledStmt) { -- labelObj := c.pkg.GetTypesInfo().ObjectOf(l.Label) -- if labelObj != nil { -- c.deepState.enqueue(candidate{obj: labelObj, score: score}) -- } -- } +-// A workspace folder inside a client. +-type WorkspaceFolder struct { +- // The associated URI for this workspace folder. +- URI URI `json:"uri"` +- // The name of the workspace folder. Used to refer to this +- // workspace folder in the user interface. +- Name string `json:"name"` +-} +-type WorkspaceFolders5Gn struct { +- // The server has support for workspace folders +- Supported bool `json:"supported,omitempty"` +- // Whether the server wants to receive workspace folder +- // change notifications. +- // +- // If a string is provided the string is treated as an ID +- // under which the notification is registered on the client +- // side. The ID can be used to unregister for these events +- // using the `client/unregisterCapability` request. +- ChangeNotifications string `json:"changeNotifications,omitempty"` +-} - -- switch lt { -- case labelBreak, labelContinue: -- // "break" and "continue" only accept labels from enclosing statements. +-// The workspace folder change event. +-type WorkspaceFoldersChangeEvent struct { +- // The array of added workspace folders +- Added []WorkspaceFolder `json:"added"` +- // The array of the removed workspace folders +- Removed []WorkspaceFolder `json:"removed"` +-} +-type WorkspaceFoldersInitializeParams struct { +- // The workspace folders configured in the client when the server starts. +- // +- // This property is only available if the client supports workspace folders. +- // It can be `null` if the client supports workspace folders but none are +- // configured. +- // +- // @since 3.6.0 +- WorkspaceFolders []WorkspaceFolder `json:"workspaceFolders,omitempty"` +-} +-type WorkspaceFoldersServerCapabilities struct { +- // The server has support for workspace folders +- Supported bool `json:"supported,omitempty"` +- // Whether the server wants to receive workspace folder +- // change notifications. +- // +- // If a string is provided the string is treated as an ID +- // under which the notification is registered on the client +- // side. The ID can be used to unregister for these events +- // using the `client/unregisterCapability` request. +- ChangeNotifications string `json:"changeNotifications,omitempty"` +-} - -- for i, p := range c.path { -- switch p := p.(type) { -- case *ast.FuncLit: -- // Labels are function scoped, so don't continue out of functions. -- return -- case *ast.LabeledStmt: -- switch p.Stmt.(type) { -- case *ast.ForStmt, *ast.RangeStmt: -- // Loop labels can be used for "break" or "continue". -- addLabel(highScore*math.Pow(.99, float64(i)), p) -- case *ast.SwitchStmt, *ast.SelectStmt, *ast.TypeSwitchStmt: -- // Switch and select labels can be used only for "break". -- if lt == labelBreak { -- addLabel(highScore*math.Pow(.99, float64(i)), p) -- } -- } -- } -- } -- case labelGoto: -- // Goto accepts any label in the same function not in a nested -- // block. It also doesn't take labels that would jump across -- // variable definitions, but ignore that case for now. -- ast.Inspect(c.enclosingFunc.body, func(n ast.Node) bool { -- if n == nil { -- return false -- } +-// A full document diagnostic report for a workspace diagnostic result. +-// +-// @since 3.17.0 +-type WorkspaceFullDocumentDiagnosticReport struct { +- // The URI for which diagnostic information is reported. +- URI DocumentURI `json:"uri"` +- // The version number for which the diagnostics are reported. +- // If the document is not marked as open `null` can be provided. +- Version int32 `json:"version"` +- FullDocumentDiagnosticReport +-} - -- switch n := n.(type) { -- // Only search into block-like nodes enclosing our "goto". -- // This prevents us from finding labels in nested blocks. -- case *ast.BlockStmt, *ast.CommClause, *ast.CaseClause: -- for _, p := range c.path { -- if n == p { -- return true -- } -- } -- return false -- case *ast.LabeledStmt: -- addLabel(highScore, n) -- } +-// Defines workspace specific capabilities of the server. +-// +-// @since 3.18.0 +-// @proposed +-type WorkspaceOptions struct { +- // The server supports workspace folder. +- // +- // @since 3.6.0 +- WorkspaceFolders *WorkspaceFolders5Gn `json:"workspaceFolders,omitempty"` +- // The server is interested in notifications/requests for operations on files. +- // +- // @since 3.16.0 +- FileOperations *FileOperationOptions `json:"fileOperations,omitempty"` +-} - -- return true -- }) -- } +-// A special workspace symbol that supports locations without a range. +-// +-// See also SymbolInformation. +-// +-// @since 3.17.0 +-type WorkspaceSymbol struct { +- // The location of the symbol. Whether a server is allowed to +- // return a location without a range depends on the client +- // capability `workspace.symbol.resolveSupport`. +- // +- // See SymbolInformation#location for more details. +- Location OrPLocation_workspace_symbol `json:"location"` +- // A data entry field that is preserved on a workspace symbol between a +- // workspace symbol request and a workspace symbol resolve request. +- Data interface{} `json:"data,omitempty"` +- BaseSymbolInformation -} -diff -urN a/gopls/internal/lsp/source/completion/literal.go b/gopls/internal/lsp/source/completion/literal.go ---- a/gopls/internal/lsp/source/completion/literal.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/completion/literal.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,592 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package completion +-// Client capabilities for a {@link WorkspaceSymbolRequest}. +-type WorkspaceSymbolClientCapabilities struct { +- // Symbol request supports dynamic registration. +- DynamicRegistration bool `json:"dynamicRegistration,omitempty"` +- // Specific capabilities for the `SymbolKind` in the `workspace/symbol` request. +- SymbolKind *ClientSymbolKindOptions `json:"symbolKind,omitempty"` +- // The client supports tags on `SymbolInformation`. +- // Clients supporting tags have to handle unknown tags gracefully. +- // +- // @since 3.16.0 +- TagSupport *ClientSymbolTagOptions `json:"tagSupport,omitempty"` +- // The client support partial workspace symbols. The client will send the +- // request `workspaceSymbol/resolve` to the server to resolve additional +- // properties. +- // +- // @since 3.17.0 +- ResolveSupport *ClientSymbolResolveOptions `json:"resolveSupport,omitempty"` +-} - --import ( -- "context" -- "fmt" -- "go/types" -- "strings" -- "unicode" +-// Server capabilities for a {@link WorkspaceSymbolRequest}. +-type WorkspaceSymbolOptions struct { +- // The server provides support to resolve additional +- // information for a workspace symbol. +- // +- // @since 3.17.0 +- ResolveProvider bool `json:"resolveProvider,omitempty"` +- WorkDoneProgressOptions +-} - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/snippet" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/typeparams" --) +-// The parameters of a {@link WorkspaceSymbolRequest}. +-type WorkspaceSymbolParams struct { +- // A query string to filter symbols by. Clients may send an empty +- // string here to request all symbols. +- Query string `json:"query"` +- WorkDoneProgressParams +- PartialResultParams +-} - --// literal generates composite literal, function literal, and make() --// completion items. --func (c *completer) literal(ctx context.Context, literalType types.Type, imp *importInfo) { -- if !c.opts.literal { -- return -- } +-// Registration options for a {@link WorkspaceSymbolRequest}. +-type WorkspaceSymbolRegistrationOptions struct { +- WorkspaceSymbolOptions +-} - -- expType := c.inference.objType +-// An unchanged document diagnostic report for a workspace diagnostic result. +-// +-// @since 3.17.0 +-type WorkspaceUnchangedDocumentDiagnosticReport struct { +- // The URI for which diagnostic information is reported. +- URI DocumentURI `json:"uri"` +- // The version number for which the diagnostics are reported. +- // If the document is not marked as open `null` can be provided. +- Version int32 `json:"version"` +- UnchangedDocumentDiagnosticReport +-} - -- if c.inference.matchesVariadic(literalType) { -- // Don't offer literal slice candidates for variadic arguments. -- // For example, don't offer "[]interface{}{}" in "fmt.Print(<>)". -- return -- } +-// The initialize parameters +-type XInitializeParams struct { +- // The process Id of the parent process that started +- // the server. +- // +- // Is `null` if the process has not been started by another process. +- // If the parent process is not alive then the server should exit. +- ProcessID int32 `json:"processId"` +- // Information about the client +- // +- // @since 3.15.0 +- ClientInfo *ClientInfo `json:"clientInfo,omitempty"` +- // The locale the client is currently showing the user interface +- // in. This must not necessarily be the locale of the operating +- // system. +- // +- // Uses IETF language tags as the value's syntax +- // (See https://en.wikipedia.org/wiki/IETF_language_tag) +- // +- // @since 3.16.0 +- Locale string `json:"locale,omitempty"` +- // The rootPath of the workspace. Is null +- // if no folder is open. +- // +- // @deprecated in favour of rootUri. +- RootPath string `json:"rootPath,omitempty"` +- // The rootUri of the workspace. Is null if no +- // folder is open. If both `rootPath` and `rootUri` are set +- // `rootUri` wins. +- // +- // @deprecated in favour of workspaceFolders. +- RootURI DocumentURI `json:"rootUri"` +- // The capabilities provided by the client (editor or tool) +- Capabilities ClientCapabilities `json:"capabilities"` +- // User provided initialization options. +- InitializationOptions interface{} `json:"initializationOptions,omitempty"` +- // The initial trace setting. If omitted trace is disabled ('off'). +- Trace *TraceValue `json:"trace,omitempty"` +- WorkDoneProgressParams +-} - -- // Avoid literal candidates if the expected type is an empty -- // interface. It isn't very useful to suggest a literal candidate of -- // every possible type. -- if expType != nil && isEmptyInterface(expType) { -- return -- } +-// The initialize parameters +-type _InitializeParams struct { +- // The process Id of the parent process that started +- // the server. +- // +- // Is `null` if the process has not been started by another process. +- // If the parent process is not alive then the server should exit. +- ProcessID int32 `json:"processId"` +- // Information about the client +- // +- // @since 3.15.0 +- ClientInfo *ClientInfo `json:"clientInfo,omitempty"` +- // The locale the client is currently showing the user interface +- // in. This must not necessarily be the locale of the operating +- // system. +- // +- // Uses IETF language tags as the value's syntax +- // (See https://en.wikipedia.org/wiki/IETF_language_tag) +- // +- // @since 3.16.0 +- Locale string `json:"locale,omitempty"` +- // The rootPath of the workspace. Is null +- // if no folder is open. +- // +- // @deprecated in favour of rootUri. +- RootPath string `json:"rootPath,omitempty"` +- // The rootUri of the workspace. Is null if no +- // folder is open. If both `rootPath` and `rootUri` are set +- // `rootUri` wins. +- // +- // @deprecated in favour of workspaceFolders. +- RootURI DocumentURI `json:"rootUri"` +- // The capabilities provided by the client (editor or tool) +- Capabilities ClientCapabilities `json:"capabilities"` +- // User provided initialization options. +- InitializationOptions interface{} `json:"initializationOptions,omitempty"` +- // The initial trace setting. If omitted trace is disabled ('off'). +- Trace *TraceValue `json:"trace,omitempty"` +- WorkDoneProgressParams +-} - -- // We handle unnamed literal completions explicitly before searching -- // for candidates. Avoid named-type literal completions for -- // unnamed-type expected type since that results in duplicate -- // candidates. For example, in +-const ( +- // A set of predefined code action kinds +- // Empty kind. +- Empty CodeActionKind = "" +- // Base kind for quickfix actions: 'quickfix' +- QuickFix CodeActionKind = "quickfix" +- // Base kind for refactoring actions: 'refactor' +- Refactor CodeActionKind = "refactor" +- // Base kind for refactoring extraction actions: 'refactor.extract' - // -- // type mySlice []int -- // var []int = <> +- // Example extract actions: +- // +- // +- // - Extract method +- // - Extract function +- // - Extract variable +- // - Extract interface from class +- // - ... +- RefactorExtract CodeActionKind = "refactor.extract" +- // Base kind for refactoring inline actions: 'refactor.inline' +- // +- // Example inline actions: +- // +- // +- // - Inline function +- // - Inline variable +- // - Inline constant +- // - ... +- RefactorInline CodeActionKind = "refactor.inline" +- // Base kind for refactoring move actions: `refactor.move` +- // +- // Example move actions: +- // +- // +- // - Move a function to a new file +- // - Move a property between classes +- // - Move method to base class +- // - ... +- // +- // @since 3.18.0 +- // @proposed +- RefactorMove CodeActionKind = "refactor.move" +- // Base kind for refactoring rewrite actions: 'refactor.rewrite' +- // +- // Example rewrite actions: +- // +- // +- // - Convert JavaScript function to class +- // - Add or remove parameter +- // - Encapsulate field +- // - Make method static +- // - Move method to base class +- // - ... +- RefactorRewrite CodeActionKind = "refactor.rewrite" +- // Base kind for source actions: `source` +- // +- // Source code actions apply to the entire file. +- Source CodeActionKind = "source" +- // Base kind for an organize imports source action: `source.organizeImports` +- SourceOrganizeImports CodeActionKind = "source.organizeImports" +- // Base kind for auto-fix source actions: `source.fixAll`. +- // +- // Fix all actions automatically fix errors that have a clear fix that do not require user input. +- // They should not suppress errors or perform unsafe fixes such as generating new types or classes. +- // +- // @since 3.15.0 +- SourceFixAll CodeActionKind = "source.fixAll" +- // Base kind for all code actions applying to the entire notebook's scope. CodeActionKinds using +- // this should always begin with `notebook.` +- // +- // @since 3.18.0 +- Notebook CodeActionKind = "notebook" +- // The reason why code actions were requested. +- // +- // @since 3.17.0 +- // Code actions were explicitly requested by the user or by an extension. +- CodeActionInvoked CodeActionTriggerKind = 1 +- // Code actions were requested automatically. +- // +- // This typically happens when current selection in a file changes, but can +- // also be triggered when file content changes. +- CodeActionAutomatic CodeActionTriggerKind = 2 +- // The kind of a completion entry. +- TextCompletion CompletionItemKind = 1 +- MethodCompletion CompletionItemKind = 2 +- FunctionCompletion CompletionItemKind = 3 +- ConstructorCompletion CompletionItemKind = 4 +- FieldCompletion CompletionItemKind = 5 +- VariableCompletion CompletionItemKind = 6 +- ClassCompletion CompletionItemKind = 7 +- InterfaceCompletion CompletionItemKind = 8 +- ModuleCompletion CompletionItemKind = 9 +- PropertyCompletion CompletionItemKind = 10 +- UnitCompletion CompletionItemKind = 11 +- ValueCompletion CompletionItemKind = 12 +- EnumCompletion CompletionItemKind = 13 +- KeywordCompletion CompletionItemKind = 14 +- SnippetCompletion CompletionItemKind = 15 +- ColorCompletion CompletionItemKind = 16 +- FileCompletion CompletionItemKind = 17 +- ReferenceCompletion CompletionItemKind = 18 +- FolderCompletion CompletionItemKind = 19 +- EnumMemberCompletion CompletionItemKind = 20 +- ConstantCompletion CompletionItemKind = 21 +- StructCompletion CompletionItemKind = 22 +- EventCompletion CompletionItemKind = 23 +- OperatorCompletion CompletionItemKind = 24 +- TypeParameterCompletion CompletionItemKind = 25 +- // Completion item tags are extra annotations that tweak the rendering of a completion +- // item. +- // +- // @since 3.15.0 +- // Render a completion as obsolete, usually using a strike-out. +- ComplDeprecated CompletionItemTag = 1 +- // How a completion was triggered +- // Completion was triggered by typing an identifier (24x7 code +- // complete), manual invocation (e.g Ctrl+Space) or via API. +- Invoked CompletionTriggerKind = 1 +- // Completion was triggered by a trigger character specified by +- // the `triggerCharacters` properties of the `CompletionRegistrationOptions`. +- TriggerCharacter CompletionTriggerKind = 2 +- // Completion was re-triggered as current completion list is incomplete +- TriggerForIncompleteCompletions CompletionTriggerKind = 3 +- // The diagnostic's severity. +- // Reports an error. +- SeverityError DiagnosticSeverity = 1 +- // Reports a warning. +- SeverityWarning DiagnosticSeverity = 2 +- // Reports an information. +- SeverityInformation DiagnosticSeverity = 3 +- // Reports a hint. +- SeverityHint DiagnosticSeverity = 4 +- // The diagnostic tags. +- // +- // @since 3.15.0 +- // Unused or unnecessary code. +- // +- // Clients are allowed to render diagnostics with this tag faded out instead of having +- // an error squiggle. +- Unnecessary DiagnosticTag = 1 +- // Deprecated or obsolete code. +- // +- // Clients are allowed to rendered diagnostics with this tag strike through. +- Deprecated DiagnosticTag = 2 +- // The document diagnostic report kinds. +- // +- // @since 3.17.0 +- // A diagnostic report with a full +- // set of problems. +- DiagnosticFull DocumentDiagnosticReportKind = "full" +- // A report indicating that the last +- // returned report is still accurate. +- DiagnosticUnchanged DocumentDiagnosticReportKind = "unchanged" +- // A document highlight kind. +- // A textual occurrence. +- Text DocumentHighlightKind = 1 +- // Read-access of a symbol, like reading a variable. +- Read DocumentHighlightKind = 2 +- // Write-access of a symbol, like writing to a variable. +- Write DocumentHighlightKind = 3 +- // Predefined error codes. +- ParseError ErrorCodes = -32700 +- InvalidRequest ErrorCodes = -32600 +- MethodNotFound ErrorCodes = -32601 +- InvalidParams ErrorCodes = -32602 +- InternalError ErrorCodes = -32603 +- // Error code indicating that a server received a notification or +- // request before the server has received the `initialize` request. +- ServerNotInitialized ErrorCodes = -32002 +- UnknownErrorCode ErrorCodes = -32001 +- // Applying the workspace change is simply aborted if one of the changes provided +- // fails. All operations executed before the failing operation stay executed. +- Abort FailureHandlingKind = "abort" +- // All operations are executed transactional. That means they either all +- // succeed or no changes at all are applied to the workspace. +- Transactional FailureHandlingKind = "transactional" +- // If the workspace edit contains only textual file changes they are executed transactional. +- // If resource changes (create, rename or delete file) are part of the change the failure +- // handling strategy is abort. +- TextOnlyTransactional FailureHandlingKind = "textOnlyTransactional" +- // The client tries to undo the operations already executed. But there is no +- // guarantee that this is succeeding. +- Undo FailureHandlingKind = "undo" +- // The file event type +- // The file got created. +- Created FileChangeType = 1 +- // The file got changed. +- Changed FileChangeType = 2 +- // The file got deleted. +- Deleted FileChangeType = 3 +- // A pattern kind describing if a glob pattern matches a file a folder or +- // both. +- // +- // @since 3.16.0 +- // The pattern matches a file only. +- FilePattern FileOperationPatternKind = "file" +- // The pattern matches a folder only. +- FolderPattern FileOperationPatternKind = "folder" +- // A set of predefined range kinds. +- // Folding range for a comment +- Comment FoldingRangeKind = "comment" +- // Folding range for an import or include +- Imports FoldingRangeKind = "imports" +- // Folding range for a region (e.g. `#region`) +- Region FoldingRangeKind = "region" +- // Inlay hint kinds. +- // +- // @since 3.17.0 +- // An inlay hint that for a type annotation. +- Type InlayHintKind = 1 +- // An inlay hint that is for a parameter. +- Parameter InlayHintKind = 2 +- // Describes how an {@link InlineCompletionItemProvider inline completion provider} was triggered. +- // +- // @since 3.18.0 +- // @proposed +- // Completion was triggered explicitly by a user gesture. +- InlineInvoked InlineCompletionTriggerKind = 1 +- // Completion was triggered automatically while editing. +- InlineAutomatic InlineCompletionTriggerKind = 2 +- // Defines whether the insert text in a completion item should be interpreted as +- // plain text or a snippet. +- // The primary text to be inserted is treated as a plain string. +- PlainTextTextFormat InsertTextFormat = 1 +- // The primary text to be inserted is treated as a snippet. +- // +- // A snippet can define tab stops and placeholders with `$1`, `$2` +- // and `${3:foo}`. `$0` defines the final tab stop, it defaults to +- // the end of the snippet. Placeholders with equal identifiers are linked, +- // that is typing in one will update others too. +- // +- // See also: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#snippet_syntax +- SnippetTextFormat InsertTextFormat = 2 +- // How whitespace and indentation is handled during completion +- // item insertion. +- // +- // @since 3.16.0 +- // The insertion or replace strings is taken as it is. If the +- // value is multi line the lines below the cursor will be +- // inserted using the indentation defined in the string value. +- // The client will not apply any kind of adjustments to the +- // string. +- AsIs InsertTextMode = 1 +- // The editor adjusts leading whitespace of new lines so that +- // they match the indentation up to the cursor of the line for +- // which the item is accepted. +- // +- // Consider a line like this: <2tabs><3tabs>foo. Accepting a +- // multi line completion item is indented using 2 tabs and all +- // following lines inserted will be indented using 2 tabs as well. +- AdjustIndentation InsertTextMode = 2 +- // A request failed but it was syntactically correct, e.g the +- // method name was known and the parameters were valid. The error +- // message should contain human readable information about why +- // the request failed. +- // +- // @since 3.17.0 +- RequestFailed LSPErrorCodes = -32803 +- // The server cancelled the request. This error code should +- // only be used for requests that explicitly support being +- // server cancellable. +- // +- // @since 3.17.0 +- ServerCancelled LSPErrorCodes = -32802 +- // The server detected that the content of a document got +- // modified outside normal conditions. A server should +- // NOT send this error code if it detects a content change +- // in it unprocessed messages. The result even computed +- // on an older state might still be useful for the client. +- // +- // If a client decides that a result is not of any use anymore +- // the client should cancel the request. +- ContentModified LSPErrorCodes = -32801 +- // The client has canceled a request and a server as detected +- // the cancel. +- RequestCancelled LSPErrorCodes = -32800 +- // Predefined Language kinds +- // @since 3.18.0 +- // @proposed +- LangABAP LanguageKind = "abap" +- LangWindowsBat LanguageKind = "bat" +- LangBibTeX LanguageKind = "bibtex" +- LangClojure LanguageKind = "clojure" +- LangCoffeescript LanguageKind = "coffeescript" +- LangC LanguageKind = "c" +- LangCPP LanguageKind = "cpp" +- LangCSharp LanguageKind = "csharp" +- LangCSS LanguageKind = "css" +- // @since 3.18.0 +- // @proposed +- LangD LanguageKind = "d" +- // @since 3.18.0 +- // @proposed +- LangDelphi LanguageKind = "pascal" +- LangDiff LanguageKind = "diff" +- LangDart LanguageKind = "dart" +- LangDockerfile LanguageKind = "dockerfile" +- LangElixir LanguageKind = "elixir" +- LangErlang LanguageKind = "erlang" +- LangFSharp LanguageKind = "fsharp" +- LangGitCommit LanguageKind = "git-commit" +- LangGitRebase LanguageKind = "rebase" +- LangGo LanguageKind = "go" +- LangGroovy LanguageKind = "groovy" +- LangHandlebars LanguageKind = "handlebars" +- LangHTML LanguageKind = "html" +- LangIni LanguageKind = "ini" +- LangJava LanguageKind = "java" +- LangJavaScript LanguageKind = "javascript" +- LangJavaScriptReact LanguageKind = "javascriptreact" +- LangJSON LanguageKind = "json" +- LangLaTeX LanguageKind = "latex" +- LangLess LanguageKind = "less" +- LangLua LanguageKind = "lua" +- LangMakefile LanguageKind = "makefile" +- LangMarkdown LanguageKind = "markdown" +- LangObjectiveC LanguageKind = "objective-c" +- LangObjectiveCPP LanguageKind = "objective-cpp" +- // @since 3.18.0 +- // @proposed +- LangPascal LanguageKind = "pascal" +- LangPerl LanguageKind = "perl" +- LangPerl6 LanguageKind = "perl6" +- LangPHP LanguageKind = "php" +- LangPowershell LanguageKind = "powershell" +- LangPug LanguageKind = "jade" +- LangPython LanguageKind = "python" +- LangR LanguageKind = "r" +- LangRazor LanguageKind = "razor" +- LangRuby LanguageKind = "ruby" +- LangRust LanguageKind = "rust" +- LangSCSS LanguageKind = "scss" +- LangSASS LanguageKind = "sass" +- LangScala LanguageKind = "scala" +- LangShaderLab LanguageKind = "shaderlab" +- LangShellScript LanguageKind = "shellscript" +- LangSQL LanguageKind = "sql" +- LangSwift LanguageKind = "swift" +- LangTypeScript LanguageKind = "typescript" +- LangTypeScriptReact LanguageKind = "typescriptreact" +- LangTeX LanguageKind = "tex" +- LangVisualBasic LanguageKind = "vb" +- LangXML LanguageKind = "xml" +- LangXSL LanguageKind = "xsl" +- LangYAML LanguageKind = "yaml" +- // Describes the content type that a client supports in various +- // result literals like `Hover`, `ParameterInfo` or `CompletionItem`. +- // +- // Please note that `MarkupKinds` must not start with a `$`. This kinds +- // are reserved for internal usage. +- // Plain text is supported as a content format +- PlainText MarkupKind = "plaintext" +- // Markdown is supported as a content format +- Markdown MarkupKind = "markdown" +- // The message type +- // An error message. +- Error MessageType = 1 +- // A warning message. +- Warning MessageType = 2 +- // An information message. +- Info MessageType = 3 +- // A log message. +- Log MessageType = 4 +- // A debug message. +- // +- // @since 3.18.0 +- // @proposed +- Debug MessageType = 5 +- // The moniker kind. +- // +- // @since 3.16.0 +- // The moniker represent a symbol that is imported into a project +- Import MonikerKind = "import" +- // The moniker represents a symbol that is exported from a project +- Export MonikerKind = "export" +- // The moniker represents a symbol that is local to a project (e.g. a local +- // variable of a function, a class not visible outside the project, ...) +- Local MonikerKind = "local" +- // A notebook cell kind. +- // +- // @since 3.17.0 +- // A markup-cell is formatted source that is used for display. +- Markup NotebookCellKind = 1 +- // A code-cell is source code. +- Code NotebookCellKind = 2 +- // A set of predefined position encoding kinds. +- // +- // @since 3.17.0 +- // Character offsets count UTF-8 code units (e.g. bytes). +- UTF8 PositionEncodingKind = "utf-8" +- // Character offsets count UTF-16 code units. +- // +- // This is the default and must always be supported +- // by servers +- UTF16 PositionEncodingKind = "utf-16" +- // Character offsets count UTF-32 code units. +- // +- // Implementation note: these are the same as Unicode codepoints, +- // so this `PositionEncodingKind` may also be used for an +- // encoding-agnostic representation of character offsets. +- UTF32 PositionEncodingKind = "utf-32" +- // The client's default behavior is to select the identifier +- // according the to language's syntax rule. +- Identifier PrepareSupportDefaultBehavior = 1 +- // Supports creating new files and folders. +- Create ResourceOperationKind = "create" +- // Supports renaming existing files and folders. +- Rename ResourceOperationKind = "rename" +- // Supports deleting existing files and folders. +- Delete ResourceOperationKind = "delete" +- // A set of predefined token modifiers. This set is not fixed +- // an clients can specify additional token types via the +- // corresponding client capabilities. +- // +- // @since 3.16.0 +- ModDeclaration SemanticTokenModifiers = "declaration" +- ModDefinition SemanticTokenModifiers = "definition" +- ModReadonly SemanticTokenModifiers = "readonly" +- ModStatic SemanticTokenModifiers = "static" +- ModDeprecated SemanticTokenModifiers = "deprecated" +- ModAbstract SemanticTokenModifiers = "abstract" +- ModAsync SemanticTokenModifiers = "async" +- ModModification SemanticTokenModifiers = "modification" +- ModDocumentation SemanticTokenModifiers = "documentation" +- ModDefaultLibrary SemanticTokenModifiers = "defaultLibrary" +- // A set of predefined token types. This set is not fixed +- // an clients can specify additional token types via the +- // corresponding client capabilities. +- // +- // @since 3.16.0 +- NamespaceType SemanticTokenTypes = "namespace" +- // Represents a generic type. Acts as a fallback for types which can't be mapped to +- // a specific type like class or enum. +- TypeType SemanticTokenTypes = "type" +- ClassType SemanticTokenTypes = "class" +- EnumType SemanticTokenTypes = "enum" +- InterfaceType SemanticTokenTypes = "interface" +- StructType SemanticTokenTypes = "struct" +- TypeParameterType SemanticTokenTypes = "typeParameter" +- ParameterType SemanticTokenTypes = "parameter" +- VariableType SemanticTokenTypes = "variable" +- PropertyType SemanticTokenTypes = "property" +- EnumMemberType SemanticTokenTypes = "enumMember" +- EventType SemanticTokenTypes = "event" +- FunctionType SemanticTokenTypes = "function" +- MethodType SemanticTokenTypes = "method" +- MacroType SemanticTokenTypes = "macro" +- KeywordType SemanticTokenTypes = "keyword" +- ModifierType SemanticTokenTypes = "modifier" +- CommentType SemanticTokenTypes = "comment" +- StringType SemanticTokenTypes = "string" +- NumberType SemanticTokenTypes = "number" +- RegexpType SemanticTokenTypes = "regexp" +- OperatorType SemanticTokenTypes = "operator" +- // @since 3.17.0 +- DecoratorType SemanticTokenTypes = "decorator" +- // How a signature help was triggered. +- // +- // @since 3.15.0 +- // Signature help was invoked manually by the user or by a command. +- SigInvoked SignatureHelpTriggerKind = 1 +- // Signature help was triggered by a trigger character. +- SigTriggerCharacter SignatureHelpTriggerKind = 2 +- // Signature help was triggered by the cursor moving or by the document content changing. +- SigContentChange SignatureHelpTriggerKind = 3 +- // A symbol kind. +- File SymbolKind = 1 +- Module SymbolKind = 2 +- Namespace SymbolKind = 3 +- Package SymbolKind = 4 +- Class SymbolKind = 5 +- Method SymbolKind = 6 +- Property SymbolKind = 7 +- Field SymbolKind = 8 +- Constructor SymbolKind = 9 +- Enum SymbolKind = 10 +- Interface SymbolKind = 11 +- Function SymbolKind = 12 +- Variable SymbolKind = 13 +- Constant SymbolKind = 14 +- String SymbolKind = 15 +- Number SymbolKind = 16 +- Boolean SymbolKind = 17 +- Array SymbolKind = 18 +- Object SymbolKind = 19 +- Key SymbolKind = 20 +- Null SymbolKind = 21 +- EnumMember SymbolKind = 22 +- Struct SymbolKind = 23 +- Event SymbolKind = 24 +- Operator SymbolKind = 25 +- TypeParameter SymbolKind = 26 +- // Symbol tags are extra annotations that tweak the rendering of a symbol. +- // +- // @since 3.16 +- // Render a symbol as obsolete, usually using a strike-out. +- DeprecatedSymbol SymbolTag = 1 +- // Represents reasons why a text document is saved. +- // Manually triggered, e.g. by the user pressing save, by starting debugging, +- // or by an API call. +- Manual TextDocumentSaveReason = 1 +- // Automatic after a delay. +- AfterDelay TextDocumentSaveReason = 2 +- // When the editor lost focus. +- FocusOut TextDocumentSaveReason = 3 +- // Defines how the host (editor) should sync +- // document changes to the language server. +- // Documents should not be synced at all. +- None TextDocumentSyncKind = 0 +- // Documents are synced by always sending the full content +- // of the document. +- Full TextDocumentSyncKind = 1 +- // Documents are synced by sending the full content on open. +- // After that only incremental updates to the document are +- // send. +- Incremental TextDocumentSyncKind = 2 +- Relative TokenFormat = "relative" +- // Turn tracing off. +- Off TraceValue = "off" +- // Trace messages only. +- Messages TraceValue = "messages" +- // Verbose message tracing. +- Verbose TraceValue = "verbose" +- // Moniker uniqueness level to define scope of the moniker. - // -- // don't offer "mySlice{}" since we have already added a candidate -- // of "[]int{}". -- if _, named := literalType.(*types.Named); named && expType != nil { -- if _, named := source.Deref(expType).(*types.Named); !named { -- return -- } -- } -- -- // Check if an object of type literalType would match our expected type. -- cand := candidate{ -- obj: c.fakeObj(literalType), -- } -- -- switch literalType.Underlying().(type) { -- // These literal types are addressable (e.g. "&[]int{}"), others are -- // not (e.g. can't do "&(func(){})"). -- case *types.Struct, *types.Array, *types.Slice, *types.Map: -- cand.addressable = true -- } -- -- if !c.matchingCandidate(&cand) || cand.convertTo != nil { -- return -- } -- -- var ( -- qf = c.qf -- sel = enclosingSelector(c.path, c.pos) -- ) -- -- // Don't qualify the type name if we are in a selector expression -- // since the package name is already present. -- if sel != nil { -- qf = func(_ *types.Package) string { return "" } -- } +- // @since 3.16.0 +- // The moniker is only unique inside a document +- Document UniquenessLevel = "document" +- // The moniker is unique inside a project for which a dump got created +- Project UniquenessLevel = "project" +- // The moniker is unique inside the group to which a project belongs +- Group UniquenessLevel = "group" +- // The moniker is unique inside the moniker scheme. +- Scheme UniquenessLevel = "scheme" +- // The moniker is globally unique +- Global UniquenessLevel = "global" +- // Interested in create events. +- WatchCreate WatchKind = 1 +- // Interested in change events +- WatchChange WatchKind = 2 +- // Interested in delete events +- WatchDelete WatchKind = 4 +-) +diff -urN a/gopls/internal/protocol/tsserver.go b/gopls/internal/protocol/tsserver.go +--- a/gopls/internal/protocol/tsserver.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/tsserver.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,1262 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- snip, typeName := c.typeNameSnippet(literalType, qf) +-// Code generated for LSP. DO NOT EDIT. - -- // A type name of "[]int" doesn't work very will with the matcher -- // since "[" isn't a valid identifier prefix. Here we strip off the -- // slice (and array) prefix yielding just "int". -- matchName := typeName -- switch t := literalType.(type) { -- case *types.Slice: -- matchName = types.TypeString(t.Elem(), qf) -- case *types.Array: -- matchName = types.TypeString(t.Elem(), qf) -- } +-package protocol - -- addlEdits, err := c.importEdits(imp) -- if err != nil { -- event.Error(ctx, "error adding import for literal candidate", err) -- return -- } +-// Code generated from protocol/metaModel.json at ref release/protocol/3.17.6-next.2 (hash 654dc9be6673c61476c28fda604406279c3258d7). +-// https://github.com/microsoft/vscode-languageserver-node/blob/release/protocol/3.17.6-next.2/protocol/metaModel.json +-// LSP metaData.version = 3.17.0. - -- // If prefix matches the type name, client may want a composite literal. -- if score := c.matcher.Score(matchName); score > 0 { -- if cand.hasMod(reference) { -- if sel != nil { -- // If we are in a selector we must place the "&" before the selector. -- // For example, "foo.B<>" must complete to "&foo.Bar{}", not -- // "foo.&Bar{}". -- edits, err := c.editText(sel.Pos(), sel.Pos(), "&") -- if err != nil { -- event.Error(ctx, "error making edit for literal pointer completion", err) -- return -- } -- addlEdits = append(addlEdits, edits...) -- } else { -- // Otherwise we can stick the "&" directly before the type name. -- typeName = "&" + typeName -- snip.PrependText("&") -- } -- } +-import ( +- "context" - -- switch t := literalType.Underlying().(type) { -- case *types.Struct, *types.Array, *types.Slice, *types.Map: -- c.compositeLiteral(t, snip.Clone(), typeName, float64(score), addlEdits) -- case *types.Signature: -- // Add a literal completion for a signature type that implements -- // an interface. For example, offer "http.HandlerFunc()" when -- // expected type is "http.Handler". -- if expType != nil && types.IsInterface(expType) { -- c.basicLiteral(t, snip.Clone(), typeName, float64(score), addlEdits) -- } -- case *types.Basic: -- // Add a literal completion for basic types that implement our -- // expected interface (e.g. named string type http.Dir -- // implements http.FileSystem), or are identical to our expected -- // type (i.e. yielding a type conversion such as "float64()"). -- if expType != nil && (types.IsInterface(expType) || types.Identical(expType, literalType)) { -- c.basicLiteral(t, snip.Clone(), typeName, float64(score), addlEdits) -- } -- } -- } +- "golang.org/x/tools/internal/jsonrpc2" +-) - -- // If prefix matches "make", client may want a "make()" -- // invocation. We also include the type name to allow for more -- // flexible fuzzy matching. -- if score := c.matcher.Score("make." + matchName); !cand.hasMod(reference) && score > 0 { -- switch literalType.Underlying().(type) { -- case *types.Slice: -- // The second argument to "make()" for slices is required, so default to "0". -- c.makeCall(snip.Clone(), typeName, "0", float64(score), addlEdits) -- case *types.Map, *types.Chan: -- // Maps and channels don't require the second argument, so omit -- // to keep things simple for now. -- c.makeCall(snip.Clone(), typeName, "", float64(score), addlEdits) -- } -- } +-type Server interface { +- Progress(context.Context, *ProgressParams) error // $/progress +- SetTrace(context.Context, *SetTraceParams) error // $/setTrace +- IncomingCalls(context.Context, *CallHierarchyIncomingCallsParams) ([]CallHierarchyIncomingCall, error) // callHierarchy/incomingCalls +- OutgoingCalls(context.Context, *CallHierarchyOutgoingCallsParams) ([]CallHierarchyOutgoingCall, error) // callHierarchy/outgoingCalls +- ResolveCodeAction(context.Context, *CodeAction) (*CodeAction, error) // codeAction/resolve +- ResolveCodeLens(context.Context, *CodeLens) (*CodeLens, error) // codeLens/resolve +- ResolveCompletionItem(context.Context, *CompletionItem) (*CompletionItem, error) // completionItem/resolve +- ResolveDocumentLink(context.Context, *DocumentLink) (*DocumentLink, error) // documentLink/resolve +- Exit(context.Context) error // exit +- Initialize(context.Context, *ParamInitialize) (*InitializeResult, error) // initialize +- Initialized(context.Context, *InitializedParams) error // initialized +- Resolve(context.Context, *InlayHint) (*InlayHint, error) // inlayHint/resolve +- DidChangeNotebookDocument(context.Context, *DidChangeNotebookDocumentParams) error // notebookDocument/didChange +- DidCloseNotebookDocument(context.Context, *DidCloseNotebookDocumentParams) error // notebookDocument/didClose +- DidOpenNotebookDocument(context.Context, *DidOpenNotebookDocumentParams) error // notebookDocument/didOpen +- DidSaveNotebookDocument(context.Context, *DidSaveNotebookDocumentParams) error // notebookDocument/didSave +- Shutdown(context.Context) error // shutdown +- CodeAction(context.Context, *CodeActionParams) ([]CodeAction, error) // textDocument/codeAction +- CodeLens(context.Context, *CodeLensParams) ([]CodeLens, error) // textDocument/codeLens +- ColorPresentation(context.Context, *ColorPresentationParams) ([]ColorPresentation, error) // textDocument/colorPresentation +- Completion(context.Context, *CompletionParams) (*CompletionList, error) // textDocument/completion +- Declaration(context.Context, *DeclarationParams) (*Or_textDocument_declaration, error) // textDocument/declaration +- Definition(context.Context, *DefinitionParams) ([]Location, error) // textDocument/definition +- Diagnostic(context.Context, *string) (*string, error) // textDocument/diagnostic +- DidChange(context.Context, *DidChangeTextDocumentParams) error // textDocument/didChange +- DidClose(context.Context, *DidCloseTextDocumentParams) error // textDocument/didClose +- DidOpen(context.Context, *DidOpenTextDocumentParams) error // textDocument/didOpen +- DidSave(context.Context, *DidSaveTextDocumentParams) error // textDocument/didSave +- DocumentColor(context.Context, *DocumentColorParams) ([]ColorInformation, error) // textDocument/documentColor +- DocumentHighlight(context.Context, *DocumentHighlightParams) ([]DocumentHighlight, error) // textDocument/documentHighlight +- DocumentLink(context.Context, *DocumentLinkParams) ([]DocumentLink, error) // textDocument/documentLink +- DocumentSymbol(context.Context, *DocumentSymbolParams) ([]interface{}, error) // textDocument/documentSymbol +- FoldingRange(context.Context, *FoldingRangeParams) ([]FoldingRange, error) // textDocument/foldingRange +- Formatting(context.Context, *DocumentFormattingParams) ([]TextEdit, error) // textDocument/formatting +- Hover(context.Context, *HoverParams) (*Hover, error) // textDocument/hover +- Implementation(context.Context, *ImplementationParams) ([]Location, error) // textDocument/implementation +- InlayHint(context.Context, *InlayHintParams) ([]InlayHint, error) // textDocument/inlayHint +- InlineCompletion(context.Context, *InlineCompletionParams) (*Or_Result_textDocument_inlineCompletion, error) // textDocument/inlineCompletion +- InlineValue(context.Context, *InlineValueParams) ([]InlineValue, error) // textDocument/inlineValue +- LinkedEditingRange(context.Context, *LinkedEditingRangeParams) (*LinkedEditingRanges, error) // textDocument/linkedEditingRange +- Moniker(context.Context, *MonikerParams) ([]Moniker, error) // textDocument/moniker +- OnTypeFormatting(context.Context, *DocumentOnTypeFormattingParams) ([]TextEdit, error) // textDocument/onTypeFormatting +- PrepareCallHierarchy(context.Context, *CallHierarchyPrepareParams) ([]CallHierarchyItem, error) // textDocument/prepareCallHierarchy +- PrepareRename(context.Context, *PrepareRenameParams) (*PrepareRenameResult, error) // textDocument/prepareRename +- PrepareTypeHierarchy(context.Context, *TypeHierarchyPrepareParams) ([]TypeHierarchyItem, error) // textDocument/prepareTypeHierarchy +- RangeFormatting(context.Context, *DocumentRangeFormattingParams) ([]TextEdit, error) // textDocument/rangeFormatting +- RangesFormatting(context.Context, *DocumentRangesFormattingParams) ([]TextEdit, error) // textDocument/rangesFormatting +- References(context.Context, *ReferenceParams) ([]Location, error) // textDocument/references +- Rename(context.Context, *RenameParams) (*WorkspaceEdit, error) // textDocument/rename +- SelectionRange(context.Context, *SelectionRangeParams) ([]SelectionRange, error) // textDocument/selectionRange +- SemanticTokensFull(context.Context, *SemanticTokensParams) (*SemanticTokens, error) // textDocument/semanticTokens/full +- SemanticTokensFullDelta(context.Context, *SemanticTokensDeltaParams) (interface{}, error) // textDocument/semanticTokens/full/delta +- SemanticTokensRange(context.Context, *SemanticTokensRangeParams) (*SemanticTokens, error) // textDocument/semanticTokens/range +- SignatureHelp(context.Context, *SignatureHelpParams) (*SignatureHelp, error) // textDocument/signatureHelp +- TypeDefinition(context.Context, *TypeDefinitionParams) ([]Location, error) // textDocument/typeDefinition +- WillSave(context.Context, *WillSaveTextDocumentParams) error // textDocument/willSave +- WillSaveWaitUntil(context.Context, *WillSaveTextDocumentParams) ([]TextEdit, error) // textDocument/willSaveWaitUntil +- Subtypes(context.Context, *TypeHierarchySubtypesParams) ([]TypeHierarchyItem, error) // typeHierarchy/subtypes +- Supertypes(context.Context, *TypeHierarchySupertypesParams) ([]TypeHierarchyItem, error) // typeHierarchy/supertypes +- WorkDoneProgressCancel(context.Context, *WorkDoneProgressCancelParams) error // window/workDoneProgress/cancel +- DiagnosticWorkspace(context.Context, *WorkspaceDiagnosticParams) (*WorkspaceDiagnosticReport, error) // workspace/diagnostic +- DidChangeConfiguration(context.Context, *DidChangeConfigurationParams) error // workspace/didChangeConfiguration +- DidChangeWatchedFiles(context.Context, *DidChangeWatchedFilesParams) error // workspace/didChangeWatchedFiles +- DidChangeWorkspaceFolders(context.Context, *DidChangeWorkspaceFoldersParams) error // workspace/didChangeWorkspaceFolders +- DidCreateFiles(context.Context, *CreateFilesParams) error // workspace/didCreateFiles +- DidDeleteFiles(context.Context, *DeleteFilesParams) error // workspace/didDeleteFiles +- DidRenameFiles(context.Context, *RenameFilesParams) error // workspace/didRenameFiles +- ExecuteCommand(context.Context, *ExecuteCommandParams) (interface{}, error) // workspace/executeCommand +- Symbol(context.Context, *WorkspaceSymbolParams) ([]SymbolInformation, error) // workspace/symbol +- WillCreateFiles(context.Context, *CreateFilesParams) (*WorkspaceEdit, error) // workspace/willCreateFiles +- WillDeleteFiles(context.Context, *DeleteFilesParams) (*WorkspaceEdit, error) // workspace/willDeleteFiles +- WillRenameFiles(context.Context, *RenameFilesParams) (*WorkspaceEdit, error) // workspace/willRenameFiles +- ResolveWorkspaceSymbol(context.Context, *WorkspaceSymbol) (*WorkspaceSymbol, error) // workspaceSymbol/resolve - -- // If prefix matches "func", client may want a function literal. -- if score := c.matcher.Score("func"); !cand.hasMod(reference) && score > 0 && (expType == nil || !types.IsInterface(expType)) { -- switch t := literalType.Underlying().(type) { -- case *types.Signature: -- c.functionLiteral(ctx, t, float64(score)) -- } -- } -} - --// literalCandidateScore is the base score for literal candidates. --// Literal candidates match the expected type so they should be high --// scoring, but we want them ranked below lexical objects of the --// correct type, so scale down highScore. --const literalCandidateScore = highScore / 2 -- --// functionLiteral adds a function literal completion item for the --// given signature. --func (c *completer) functionLiteral(ctx context.Context, sig *types.Signature, matchScore float64) { -- snip := &snippet.Builder{} -- snip.WriteText("func(") -- -- // First we generate names for each param and keep a seen count so -- // we know if we need to uniquify param names. For example, -- // "func(int)" will become "func(i int)", but "func(int, int64)" -- // will become "func(i1 int, i2 int64)". -- var ( -- paramNames = make([]string, sig.Params().Len()) -- paramNameCount = make(map[string]int) -- hasTypeParams bool -- ) -- for i := 0; i < sig.Params().Len(); i++ { -- var ( -- p = sig.Params().At(i) -- name = p.Name() -- ) -- -- if tp, _ := p.Type().(*typeparams.TypeParam); tp != nil && !c.typeParamInScope(tp) { -- hasTypeParams = true +-func serverDispatch(ctx context.Context, server Server, reply jsonrpc2.Replier, r jsonrpc2.Request) (bool, error) { +- defer recoverHandlerPanic(r.Method()) +- switch r.Method() { +- case "$/progress": +- var params ProgressParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } +- err := server.Progress(ctx, ¶ms) +- return true, reply(ctx, nil, err) - -- if name == "" { -- // If the param has no name in the signature, guess a name based -- // on the type. Use an empty qualifier to ignore the package. -- // For example, we want to name "http.Request" "r", not "hr". -- typeName, err := source.FormatVarType(ctx, c.snapshot, c.pkg, p, -- func(p *types.Package) string { return "" }, -- func(source.PackageName, source.ImportPath, source.PackagePath) string { return "" }) -- if err != nil { -- // In general, the only error we should encounter while formatting is -- // context cancellation. -- if ctx.Err() == nil { -- event.Error(ctx, "formatting var type", err) -- } -- return -- } -- name = abbreviateTypeName(typeName) -- } -- paramNames[i] = name -- if name != "_" { -- paramNameCount[name]++ +- case "$/setTrace": +- var params SetTraceParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } -- } +- err := server.SetTrace(ctx, ¶ms) +- return true, reply(ctx, nil, err) - -- for n, c := range paramNameCount { -- // Any names we saw more than once will need a unique suffix added -- // on. Reset the count to 1 to act as the suffix for the first -- // name. -- if c >= 2 { -- paramNameCount[n] = 1 -- } else { -- delete(paramNameCount, n) +- case "callHierarchy/incomingCalls": +- var params CallHierarchyIncomingCallsParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } -- } -- -- for i := 0; i < sig.Params().Len(); i++ { -- if hasTypeParams && !c.opts.placeholders { -- // If there are type params in the args then the user must -- // choose the concrete types. If placeholders are disabled just -- // drop them between the parens and let them fill things in. -- snip.WritePlaceholder(nil) -- break +- resp, err := server.IncomingCalls(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) - } +- return true, reply(ctx, resp, nil) - -- if i > 0 { -- snip.WriteText(", ") +- case "callHierarchy/outgoingCalls": +- var params CallHierarchyOutgoingCallsParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } -- -- var ( -- p = sig.Params().At(i) -- name = paramNames[i] -- ) -- -- // Uniquify names by adding on an incrementing numeric suffix. -- if idx, found := paramNameCount[name]; found { -- paramNameCount[name]++ -- name = fmt.Sprintf("%s%d", name, idx) +- resp, err := server.OutgoingCalls(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) - } +- return true, reply(ctx, resp, nil) - -- if name != p.Name() && c.opts.placeholders { -- // If we didn't use the signature's param name verbatim then we -- // may have chosen a poor name. Give the user a placeholder so -- // they can easily fix the name. -- snip.WritePlaceholder(func(b *snippet.Builder) { -- b.WriteText(name) -- }) -- } else { -- snip.WriteText(name) +- case "codeAction/resolve": +- var params CodeAction +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } -- -- // If the following param's type is identical to this one, omit -- // this param's type string. For example, emit "i, j int" instead -- // of "i int, j int". -- if i == sig.Params().Len()-1 || !types.Identical(p.Type(), sig.Params().At(i+1).Type()) { -- snip.WriteText(" ") -- typeStr, err := source.FormatVarType(ctx, c.snapshot, c.pkg, p, c.qf, c.mq) -- if err != nil { -- // In general, the only error we should encounter while formatting is -- // context cancellation. -- if ctx.Err() == nil { -- event.Error(ctx, "formatting var type", err) -- } -- return -- } -- if sig.Variadic() && i == sig.Params().Len()-1 { -- typeStr = strings.Replace(typeStr, "[]", "...", 1) -- } -- -- if tp, _ := p.Type().(*typeparams.TypeParam); tp != nil && !c.typeParamInScope(tp) { -- snip.WritePlaceholder(func(snip *snippet.Builder) { -- snip.WriteText(typeStr) -- }) -- } else { -- snip.WriteText(typeStr) -- } +- resp, err := server.ResolveCodeAction(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) - } -- } -- snip.WriteText(")") -- -- results := sig.Results() -- if results.Len() > 0 { -- snip.WriteText(" ") -- } -- -- resultsNeedParens := results.Len() > 1 || -- results.Len() == 1 && results.At(0).Name() != "" +- return true, reply(ctx, resp, nil) - -- var resultHasTypeParams bool -- for i := 0; i < results.Len(); i++ { -- if tp, _ := results.At(i).Type().(*typeparams.TypeParam); tp != nil && !c.typeParamInScope(tp) { -- resultHasTypeParams = true +- case "codeLens/resolve": +- var params CodeLens +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } -- } -- -- if resultsNeedParens { -- snip.WriteText("(") -- } -- for i := 0; i < results.Len(); i++ { -- if resultHasTypeParams && !c.opts.placeholders { -- // Leave an empty tabstop if placeholders are disabled and there -- // are type args that need specificying. -- snip.WritePlaceholder(nil) -- break +- resp, err := server.ResolveCodeLens(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) - } +- return true, reply(ctx, resp, nil) - -- if i > 0 { -- snip.WriteText(", ") +- case "completionItem/resolve": +- var params CompletionItem +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } -- r := results.At(i) -- if name := r.Name(); name != "" { -- snip.WriteText(name + " ") +- resp, err := server.ResolveCompletionItem(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) - } +- return true, reply(ctx, resp, nil) - -- text, err := source.FormatVarType(ctx, c.snapshot, c.pkg, r, c.qf, c.mq) -- if err != nil { -- // In general, the only error we should encounter while formatting is -- // context cancellation. -- if ctx.Err() == nil { -- event.Error(ctx, "formatting var type", err) -- } -- return +- case "documentLink/resolve": +- var params DocumentLink +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } -- if tp, _ := r.Type().(*typeparams.TypeParam); tp != nil && !c.typeParamInScope(tp) { -- snip.WritePlaceholder(func(snip *snippet.Builder) { -- snip.WriteText(text) -- }) -- } else { -- snip.WriteText(text) +- resp, err := server.ResolveDocumentLink(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) - } -- } -- if resultsNeedParens { -- snip.WriteText(")") -- } -- -- snip.WriteText(" {") -- snip.WriteFinalTabstop() -- snip.WriteText("}") -- -- c.items = append(c.items, CompletionItem{ -- Label: "func(...) {}", -- Score: matchScore * literalCandidateScore, -- Kind: protocol.VariableCompletion, -- snippet: snip, -- }) --} -- --// conventionalAcronyms contains conventional acronyms for type names --// in lower case. For example, "ctx" for "context" and "err" for "error". --var conventionalAcronyms = map[string]string{ -- "context": "ctx", -- "error": "err", -- "tx": "tx", -- "responsewriter": "w", --} +- return true, reply(ctx, resp, nil) - --// abbreviateTypeName abbreviates type names into acronyms. For --// example, "fooBar" is abbreviated "fb". Care is taken to ignore --// non-identifier runes. For example, "[]int" becomes "i", and --// "struct { i int }" becomes "s". --func abbreviateTypeName(s string) string { -- var ( -- b strings.Builder -- useNextUpper bool -- ) +- case "exit": +- err := server.Exit(ctx) +- return true, reply(ctx, nil, err) - -- // Trim off leading non-letters. We trim everything between "[" and -- // "]" to handle array types like "[someConst]int". -- var inBracket bool -- s = strings.TrimFunc(s, func(r rune) bool { -- if inBracket { -- inBracket = r != ']' -- return true +- case "initialize": +- var params ParamInitialize +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } -- -- if r == '[' { -- inBracket = true +- resp, err := server.Initialize(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) - } +- return true, reply(ctx, resp, nil) - -- return !unicode.IsLetter(r) -- }) -- -- if acr, ok := conventionalAcronyms[strings.ToLower(s)]; ok { -- return acr -- } -- -- for i, r := range s { -- // Stop if we encounter a non-identifier rune. -- if !unicode.IsLetter(r) && !unicode.IsNumber(r) { -- break +- case "initialized": +- var params InitializedParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } +- err := server.Initialized(ctx, ¶ms) +- return true, reply(ctx, nil, err) - -- if i == 0 { -- b.WriteRune(unicode.ToLower(r)) +- case "inlayHint/resolve": +- var params InlayHint +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } -- -- if unicode.IsUpper(r) { -- if useNextUpper { -- b.WriteRune(unicode.ToLower(r)) -- useNextUpper = false -- } -- } else { -- useNextUpper = true +- resp, err := server.Resolve(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) - } -- } +- return true, reply(ctx, resp, nil) - -- return b.String() --} +- case "notebookDocument/didChange": +- var params DidChangeNotebookDocumentParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- err := server.DidChangeNotebookDocument(ctx, ¶ms) +- return true, reply(ctx, nil, err) - --// compositeLiteral adds a composite literal completion item for the given typeName. --func (c *completer) compositeLiteral(T types.Type, snip *snippet.Builder, typeName string, matchScore float64, edits []protocol.TextEdit) { -- snip.WriteText("{") -- // Don't put the tab stop inside the composite literal curlies "{}" -- // for structs that have no accessible fields. -- if strct, ok := T.(*types.Struct); !ok || fieldsAccessible(strct, c.pkg.GetTypes()) { -- snip.WriteFinalTabstop() -- } -- snip.WriteText("}") +- case "notebookDocument/didClose": +- var params DidCloseNotebookDocumentParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- err := server.DidCloseNotebookDocument(ctx, ¶ms) +- return true, reply(ctx, nil, err) - -- nonSnippet := typeName + "{}" +- case "notebookDocument/didOpen": +- var params DidOpenNotebookDocumentParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- err := server.DidOpenNotebookDocument(ctx, ¶ms) +- return true, reply(ctx, nil, err) - -- c.items = append(c.items, CompletionItem{ -- Label: nonSnippet, -- InsertText: nonSnippet, -- Score: matchScore * literalCandidateScore, -- Kind: protocol.VariableCompletion, -- AdditionalTextEdits: edits, -- snippet: snip, -- }) --} +- case "notebookDocument/didSave": +- var params DidSaveNotebookDocumentParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- err := server.DidSaveNotebookDocument(ctx, ¶ms) +- return true, reply(ctx, nil, err) - --// basicLiteral adds a literal completion item for the given basic --// type name typeName. --func (c *completer) basicLiteral(T types.Type, snip *snippet.Builder, typeName string, matchScore float64, edits []protocol.TextEdit) { -- // Never give type conversions like "untyped int()". -- if isUntyped(T) { -- return -- } +- case "shutdown": +- err := server.Shutdown(ctx) +- return true, reply(ctx, nil, err) - -- snip.WriteText("(") -- snip.WriteFinalTabstop() -- snip.WriteText(")") +- case "textDocument/codeAction": +- var params CodeActionParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.CodeAction(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- nonSnippet := typeName + "()" +- case "textDocument/codeLens": +- var params CodeLensParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.CodeLens(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- c.items = append(c.items, CompletionItem{ -- Label: nonSnippet, -- InsertText: nonSnippet, -- Detail: T.String(), -- Score: matchScore * literalCandidateScore, -- Kind: protocol.VariableCompletion, -- AdditionalTextEdits: edits, -- snippet: snip, -- }) --} +- case "textDocument/colorPresentation": +- var params ColorPresentationParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.ColorPresentation(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - --// makeCall adds a completion item for a "make()" call given a specific type. --func (c *completer) makeCall(snip *snippet.Builder, typeName string, secondArg string, matchScore float64, edits []protocol.TextEdit) { -- // Keep it simple and don't add any placeholders for optional "make()" arguments. +- case "textDocument/completion": +- var params CompletionParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.Completion(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- snip.PrependText("make(") -- if secondArg != "" { -- snip.WriteText(", ") -- snip.WritePlaceholder(func(b *snippet.Builder) { -- if c.opts.placeholders { -- b.WriteText(secondArg) -- } -- }) -- } -- snip.WriteText(")") +- case "textDocument/declaration": +- var params DeclarationParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.Declaration(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- var nonSnippet strings.Builder -- nonSnippet.WriteString("make(" + typeName) -- if secondArg != "" { -- nonSnippet.WriteString(", ") -- nonSnippet.WriteString(secondArg) -- } -- nonSnippet.WriteByte(')') +- case "textDocument/definition": +- var params DefinitionParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.Definition(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- c.items = append(c.items, CompletionItem{ -- Label: nonSnippet.String(), -- InsertText: nonSnippet.String(), -- Score: matchScore * literalCandidateScore, -- Kind: protocol.FunctionCompletion, -- AdditionalTextEdits: edits, -- snippet: snip, -- }) --} +- case "textDocument/diagnostic": +- var params string +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.Diagnostic(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - --// Create a snippet for a type name where type params become placeholders. --func (c *completer) typeNameSnippet(literalType types.Type, qf types.Qualifier) (*snippet.Builder, string) { -- var ( -- snip snippet.Builder -- typeName string -- named, _ = literalType.(*types.Named) -- ) +- case "textDocument/didChange": +- var params DidChangeTextDocumentParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- err := server.DidChange(ctx, ¶ms) +- return true, reply(ctx, nil, err) - -- if named != nil && named.Obj() != nil && typeparams.ForNamed(named).Len() > 0 && !c.fullyInstantiated(named) { -- // We are not "fully instantiated" meaning we have type params that must be specified. -- if pkg := qf(named.Obj().Pkg()); pkg != "" { -- typeName = pkg + "." +- case "textDocument/didClose": +- var params DidCloseTextDocumentParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } +- err := server.DidClose(ctx, ¶ms) +- return true, reply(ctx, nil, err) - -- // We do this to get "someType" instead of "someType[T]". -- typeName += named.Obj().Name() -- snip.WriteText(typeName + "[") +- case "textDocument/didOpen": +- var params DidOpenTextDocumentParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- err := server.DidOpen(ctx, ¶ms) +- return true, reply(ctx, nil, err) - -- if c.opts.placeholders { -- for i := 0; i < typeparams.ForNamed(named).Len(); i++ { -- if i > 0 { -- snip.WriteText(", ") -- } -- snip.WritePlaceholder(func(snip *snippet.Builder) { -- snip.WriteText(types.TypeString(typeparams.ForNamed(named).At(i), qf)) -- }) -- } -- } else { -- snip.WritePlaceholder(nil) +- case "textDocument/didSave": +- var params DidSaveTextDocumentParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } -- snip.WriteText("]") -- typeName += "[...]" -- } else { -- // We don't have unspecified type params so use default type formatting. -- typeName = types.TypeString(literalType, qf) -- snip.WriteText(typeName) -- } +- err := server.DidSave(ctx, ¶ms) +- return true, reply(ctx, nil, err) - -- return &snip, typeName --} +- case "textDocument/documentColor": +- var params DocumentColorParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.DocumentColor(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - --// fullyInstantiated reports whether all of t's type params have --// specified type args. --func (c *completer) fullyInstantiated(t *types.Named) bool { -- tps := typeparams.ForNamed(t) -- tas := typeparams.NamedTypeArgs(t) +- case "textDocument/documentHighlight": +- var params DocumentHighlightParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.DocumentHighlight(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- if tps.Len() != tas.Len() { -- return false -- } +- case "textDocument/documentLink": +- var params DocumentLinkParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.DocumentLink(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- for i := 0; i < tas.Len(); i++ { -- switch ta := tas.At(i).(type) { -- case *typeparams.TypeParam: -- // A *TypeParam only counts as specified if it is currently in -- // scope (i.e. we are in a generic definition). -- if !c.typeParamInScope(ta) { -- return false -- } -- case *types.Named: -- if !c.fullyInstantiated(ta) { -- return false -- } +- case "textDocument/documentSymbol": +- var params DocumentSymbolParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } -- } -- return true --} +- resp, err := server.DocumentSymbol(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - --// typeParamInScope returns whether tp's object is in scope at c.pos. --// This tells you whether you are in a generic definition and can --// assume tp has been specified. --func (c *completer) typeParamInScope(tp *typeparams.TypeParam) bool { -- obj := tp.Obj() -- if obj == nil { -- return false -- } +- case "textDocument/foldingRange": +- var params FoldingRangeParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.FoldingRange(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- scope := c.innermostScope() -- if scope == nil { -- return false -- } +- case "textDocument/formatting": +- var params DocumentFormattingParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.Formatting(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- _, foundObj := scope.LookupParent(obj.Name(), c.pos) -- return obj == foundObj --} -diff -urN a/gopls/internal/lsp/source/completion/package.go b/gopls/internal/lsp/source/completion/package.go ---- a/gopls/internal/lsp/source/completion/package.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/completion/package.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,351 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +- case "textDocument/hover": +- var params HoverParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.Hover(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - --package completion +- case "textDocument/implementation": +- var params ImplementationParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.Implementation(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - --import ( -- "bytes" -- "context" -- "errors" -- "fmt" -- "go/ast" -- "go/parser" -- "go/scanner" -- "go/token" -- "go/types" -- "path/filepath" -- "strings" -- "unicode" +- case "textDocument/inlayHint": +- var params InlayHintParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.InlayHint(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/fuzzy" --) +- case "textDocument/inlineCompletion": +- var params InlineCompletionParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.InlineCompletion(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - --// packageClauseCompletions offers completions for a package declaration when --// one is not present in the given file. --func packageClauseCompletions(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) ([]CompletionItem, *Selection, error) { -- // We know that the AST for this file will be empty due to the missing -- // package declaration, but parse it anyway to get a mapper. -- // TODO(adonovan): opt: there's no need to parse just to get a mapper. -- pgf, err := snapshot.ParseGo(ctx, fh, source.ParseFull) -- if err != nil { -- return nil, nil, err -- } +- case "textDocument/inlineValue": +- var params InlineValueParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.InlineValue(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- offset, err := pgf.Mapper.PositionOffset(position) -- if err != nil { -- return nil, nil, err -- } -- surrounding, err := packageCompletionSurrounding(pgf, offset) -- if err != nil { -- return nil, nil, fmt.Errorf("invalid position for package completion: %w", err) -- } +- case "textDocument/linkedEditingRange": +- var params LinkedEditingRangeParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.LinkedEditingRange(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- packageSuggestions, err := packageSuggestions(ctx, snapshot, fh.URI(), "") -- if err != nil { -- return nil, nil, err -- } +- case "textDocument/moniker": +- var params MonikerParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.Moniker(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- var items []CompletionItem -- for _, pkg := range packageSuggestions { -- insertText := fmt.Sprintf("package %s", pkg.name) -- items = append(items, CompletionItem{ -- Label: insertText, -- Kind: protocol.ModuleCompletion, -- InsertText: insertText, -- Score: pkg.score, -- }) -- } +- case "textDocument/onTypeFormatting": +- var params DocumentOnTypeFormattingParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.OnTypeFormatting(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- return items, surrounding, nil --} +- case "textDocument/prepareCallHierarchy": +- var params CallHierarchyPrepareParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.PrepareCallHierarchy(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - --// packageCompletionSurrounding returns surrounding for package completion if a --// package completions can be suggested at a given cursor offset. A valid location --// for package completion is above any declarations or import statements. --func packageCompletionSurrounding(pgf *source.ParsedGoFile, offset int) (*Selection, error) { -- m := pgf.Mapper -- // If the file lacks a package declaration, the parser will return an empty -- // AST. As a work-around, try to parse an expression from the file contents. -- fset := token.NewFileSet() -- expr, _ := parser.ParseExprFrom(fset, m.URI.Filename(), pgf.Src, parser.Mode(0)) -- if expr == nil { -- return nil, fmt.Errorf("unparseable file (%s)", m.URI) -- } -- tok := fset.File(expr.Pos()) -- cursor := tok.Pos(offset) +- case "textDocument/prepareRename": +- var params PrepareRenameParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.PrepareRename(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- // If we were able to parse out an identifier as the first expression from -- // the file, it may be the beginning of a package declaration ("pack "). -- // We can offer package completions if the cursor is in the identifier. -- if name, ok := expr.(*ast.Ident); ok { -- if cursor >= name.Pos() && cursor <= name.End() { -- if !strings.HasPrefix(PACKAGE, name.Name) { -- return nil, fmt.Errorf("cursor in non-matching ident") -- } -- return &Selection{ -- content: name.Name, -- cursor: cursor, -- tokFile: tok, -- start: name.Pos(), -- end: name.End(), -- mapper: m, -- }, nil +- case "textDocument/prepareTypeHierarchy": +- var params TypeHierarchyPrepareParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } -- } +- resp, err := server.PrepareTypeHierarchy(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- // The file is invalid, but it contains an expression that we were able to -- // parse. We will use this expression to construct the cursor's -- // "surrounding". +- case "textDocument/rangeFormatting": +- var params DocumentRangeFormattingParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.RangeFormatting(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- // First, consider the possibility that we have a valid "package" keyword -- // with an empty package name ("package "). "package" is parsed as an -- // *ast.BadDecl since it is a keyword. This logic would allow "package" to -- // appear on any line of the file as long as it's the first code expression -- // in the file. -- lines := strings.Split(string(pgf.Src), "\n") -- cursorLine := safetoken.Line(tok, cursor) -- if cursorLine <= 0 || cursorLine > len(lines) { -- return nil, fmt.Errorf("invalid line number") -- } -- if safetoken.StartPosition(fset, expr.Pos()).Line == cursorLine { -- words := strings.Fields(lines[cursorLine-1]) -- if len(words) > 0 && words[0] == PACKAGE { -- content := PACKAGE -- // Account for spaces if there are any. -- if len(words) > 1 { -- content += " " -- } +- case "textDocument/rangesFormatting": +- var params DocumentRangesFormattingParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.RangesFormatting(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- start := expr.Pos() -- end := token.Pos(int(expr.Pos()) + len(content) + 1) -- // We have verified that we have a valid 'package' keyword as our -- // first expression. Ensure that cursor is in this keyword or -- // otherwise fallback to the general case. -- if cursor >= start && cursor <= end { -- return &Selection{ -- content: content, -- cursor: cursor, -- tokFile: tok, -- start: start, -- end: end, -- mapper: m, -- }, nil -- } +- case "textDocument/references": +- var params ReferenceParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } -- } +- resp, err := server.References(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- // If the cursor is after the start of the expression, no package -- // declaration will be valid. -- if cursor > expr.Pos() { -- return nil, fmt.Errorf("cursor after expression") -- } +- case "textDocument/rename": +- var params RenameParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.Rename(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- // If the cursor is in a comment, don't offer any completions. -- if cursorInComment(tok, cursor, m.Content) { -- return nil, fmt.Errorf("cursor in comment") -- } +- case "textDocument/selectionRange": +- var params SelectionRangeParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.SelectionRange(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- // The surrounding range in this case is the cursor. -- return &Selection{ -- content: "", -- tokFile: tok, -- start: cursor, -- end: cursor, -- cursor: cursor, -- mapper: m, -- }, nil --} +- case "textDocument/semanticTokens/full": +- var params SemanticTokensParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.SemanticTokensFull(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - --func cursorInComment(file *token.File, cursor token.Pos, src []byte) bool { -- var s scanner.Scanner -- s.Init(file, src, func(_ token.Position, _ string) {}, scanner.ScanComments) -- for { -- pos, tok, lit := s.Scan() -- if pos <= cursor && cursor <= token.Pos(int(pos)+len(lit)) { -- return tok == token.COMMENT +- case "textDocument/semanticTokens/full/delta": +- var params SemanticTokensDeltaParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } -- if tok == token.EOF { -- break +- resp, err := server.SemanticTokensFullDelta(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) - } -- } -- return false --} +- return true, reply(ctx, resp, nil) - --// packageNameCompletions returns name completions for a package clause using --// the current name as prefix. --func (c *completer) packageNameCompletions(ctx context.Context, fileURI span.URI, name *ast.Ident) error { -- cursor := int(c.pos - name.NamePos) -- if cursor < 0 || cursor > len(name.Name) { -- return errors.New("cursor is not in package name identifier") -- } +- case "textDocument/semanticTokens/range": +- var params SemanticTokensRangeParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.SemanticTokensRange(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- c.completionContext.packageCompletion = true +- case "textDocument/signatureHelp": +- var params SignatureHelpParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.SignatureHelp(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- prefix := name.Name[:cursor] -- packageSuggestions, err := packageSuggestions(ctx, c.snapshot, fileURI, prefix) -- if err != nil { -- return err -- } +- case "textDocument/typeDefinition": +- var params TypeDefinitionParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.TypeDefinition(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- for _, pkg := range packageSuggestions { -- c.deepState.enqueue(pkg) -- } -- return nil --} +- case "textDocument/willSave": +- var params WillSaveTextDocumentParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- err := server.WillSave(ctx, ¶ms) +- return true, reply(ctx, nil, err) - --// packageSuggestions returns a list of packages from workspace packages that --// have the given prefix and are used in the same directory as the given --// file. This also includes test packages for these packages (_test) and --// the directory name itself. --func packageSuggestions(ctx context.Context, snapshot source.Snapshot, fileURI span.URI, prefix string) (packages []candidate, err error) { -- active, err := snapshot.WorkspaceMetadata(ctx) -- if err != nil { -- return nil, err -- } +- case "textDocument/willSaveWaitUntil": +- var params WillSaveTextDocumentParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.WillSaveWaitUntil(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- toCandidate := func(name string, score float64) candidate { -- obj := types.NewPkgName(0, nil, name, types.NewPackage("", name)) -- return candidate{obj: obj, name: name, detail: name, score: score} -- } +- case "typeHierarchy/subtypes": +- var params TypeHierarchySubtypesParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.Subtypes(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- matcher := fuzzy.NewMatcher(prefix) +- case "typeHierarchy/supertypes": +- var params TypeHierarchySupertypesParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.Supertypes(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- // Always try to suggest a main package -- defer func() { -- if score := float64(matcher.Score("main")); score > 0 { -- packages = append(packages, toCandidate("main", score*lowScore)) +- case "window/workDoneProgress/cancel": +- var params WorkDoneProgressCancelParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } -- }() +- err := server.WorkDoneProgressCancel(ctx, ¶ms) +- return true, reply(ctx, nil, err) - -- dirPath := filepath.Dir(fileURI.Filename()) -- dirName := filepath.Base(dirPath) -- if !isValidDirName(dirName) { -- return packages, nil -- } -- pkgName := convertDirNameToPkgName(dirName) +- case "workspace/diagnostic": +- var params WorkspaceDiagnosticParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.DiagnosticWorkspace(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- seenPkgs := make(map[source.PackageName]struct{}) +- case "workspace/didChangeConfiguration": +- var params DidChangeConfigurationParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- err := server.DidChangeConfiguration(ctx, ¶ms) +- return true, reply(ctx, nil, err) - -- // The `go` command by default only allows one package per directory but we -- // support multiple package suggestions since gopls is build system agnostic. -- for _, m := range active { -- if m.Name == "main" || m.Name == "" { -- continue +- case "workspace/didChangeWatchedFiles": +- var params DidChangeWatchedFilesParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } -- if _, ok := seenPkgs[m.Name]; ok { -- continue +- err := server.DidChangeWatchedFiles(ctx, ¶ms) +- return true, reply(ctx, nil, err) +- +- case "workspace/didChangeWorkspaceFolders": +- var params DidChangeWorkspaceFoldersParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } +- err := server.DidChangeWorkspaceFolders(ctx, ¶ms) +- return true, reply(ctx, nil, err) - -- // Only add packages that are previously used in the current directory. -- var relevantPkg bool -- for _, uri := range m.CompiledGoFiles { -- if filepath.Dir(uri.Filename()) == dirPath { -- relevantPkg = true -- break -- } +- case "workspace/didCreateFiles": +- var params CreateFilesParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } -- if !relevantPkg { -- continue +- err := server.DidCreateFiles(ctx, ¶ms) +- return true, reply(ctx, nil, err) +- +- case "workspace/didDeleteFiles": +- var params DeleteFilesParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } +- err := server.DidDeleteFiles(ctx, ¶ms) +- return true, reply(ctx, nil, err) - -- // Add a found package used in current directory as a high relevance -- // suggestion and the test package for it as a medium relevance -- // suggestion. -- if score := float64(matcher.Score(string(m.Name))); score > 0 { -- packages = append(packages, toCandidate(string(m.Name), score*highScore)) +- case "workspace/didRenameFiles": +- var params RenameFilesParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } -- seenPkgs[m.Name] = struct{}{} +- err := server.DidRenameFiles(ctx, ¶ms) +- return true, reply(ctx, nil, err) - -- testPkgName := m.Name + "_test" -- if _, ok := seenPkgs[testPkgName]; ok || strings.HasSuffix(string(m.Name), "_test") { -- continue +- case "workspace/executeCommand": +- var params ExecuteCommandParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } -- if score := float64(matcher.Score(string(testPkgName))); score > 0 { -- packages = append(packages, toCandidate(string(testPkgName), score*stdScore)) +- resp, err := server.ExecuteCommand(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) - } -- seenPkgs[testPkgName] = struct{}{} -- } +- return true, reply(ctx, resp, nil) - -- // Add current directory name as a low relevance suggestion. -- if _, ok := seenPkgs[pkgName]; !ok { -- if score := float64(matcher.Score(string(pkgName))); score > 0 { -- packages = append(packages, toCandidate(string(pkgName), score*lowScore)) +- case "workspace/symbol": +- var params WorkspaceSymbolParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.Symbol(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) - } +- return true, reply(ctx, resp, nil) - -- testPkgName := pkgName + "_test" -- if score := float64(matcher.Score(string(testPkgName))); score > 0 { -- packages = append(packages, toCandidate(string(testPkgName), score*lowScore)) +- case "workspace/willCreateFiles": +- var params CreateFilesParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } -- } +- resp, err := server.WillCreateFiles(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- return packages, nil --} +- case "workspace/willDeleteFiles": +- var params DeleteFilesParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.WillDeleteFiles(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - --// isValidDirName checks whether the passed directory name can be used in --// a package path. Requirements for a package path can be found here: --// https://golang.org/ref/mod#go-mod-file-ident. --func isValidDirName(dirName string) bool { -- if dirName == "" { -- return false -- } +- case "workspace/willRenameFiles": +- var params RenameFilesParams +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) +- } +- resp, err := server.WillRenameFiles(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) +- } +- return true, reply(ctx, resp, nil) - -- for i, ch := range dirName { -- if isLetter(ch) || isDigit(ch) { -- continue +- case "workspaceSymbol/resolve": +- var params WorkspaceSymbol +- if err := UnmarshalJSON(r.Params(), ¶ms); err != nil { +- return true, sendParseError(ctx, reply, err) - } -- if i == 0 { -- // Directory name can start only with '_'. '.' is not allowed in module paths. -- // '-' and '~' are not allowed because elements of package paths must be -- // safe command-line arguments. -- if ch == '_' { -- continue -- } -- } else { -- // Modules path elements can't end with '.' -- if isAllowedPunctuation(ch) && (i != len(dirName)-1 || ch != '.') { -- continue -- } +- resp, err := server.ResolveWorkspaceSymbol(ctx, ¶ms) +- if err != nil { +- return true, reply(ctx, nil, err) - } +- return true, reply(ctx, resp, nil) - -- return false +- default: +- return false, nil - } -- return true -} - --// convertDirNameToPkgName converts a valid directory name to a valid package name. --// It leaves only letters and digits. All letters are mapped to lower case. --func convertDirNameToPkgName(dirName string) source.PackageName { -- var buf bytes.Buffer -- for _, ch := range dirName { -- switch { -- case isLetter(ch): -- buf.WriteRune(unicode.ToLower(ch)) -- -- case buf.Len() != 0 && isDigit(ch): -- buf.WriteRune(ch) -- } +-func (s *serverDispatcher) Progress(ctx context.Context, params *ProgressParams) error { +- return s.sender.Notify(ctx, "$/progress", params) +-} +-func (s *serverDispatcher) SetTrace(ctx context.Context, params *SetTraceParams) error { +- return s.sender.Notify(ctx, "$/setTrace", params) +-} +-func (s *serverDispatcher) IncomingCalls(ctx context.Context, params *CallHierarchyIncomingCallsParams) ([]CallHierarchyIncomingCall, error) { +- var result []CallHierarchyIncomingCall +- if err := s.sender.Call(ctx, "callHierarchy/incomingCalls", params, &result); err != nil { +- return nil, err +- } +- return result, nil +-} +-func (s *serverDispatcher) OutgoingCalls(ctx context.Context, params *CallHierarchyOutgoingCallsParams) ([]CallHierarchyOutgoingCall, error) { +- var result []CallHierarchyOutgoingCall +- if err := s.sender.Call(ctx, "callHierarchy/outgoingCalls", params, &result); err != nil { +- return nil, err +- } +- return result, nil +-} +-func (s *serverDispatcher) ResolveCodeAction(ctx context.Context, params *CodeAction) (*CodeAction, error) { +- var result *CodeAction +- if err := s.sender.Call(ctx, "codeAction/resolve", params, &result); err != nil { +- return nil, err +- } +- return result, nil +-} +-func (s *serverDispatcher) ResolveCodeLens(ctx context.Context, params *CodeLens) (*CodeLens, error) { +- var result *CodeLens +- if err := s.sender.Call(ctx, "codeLens/resolve", params, &result); err != nil { +- return nil, err +- } +- return result, nil +-} +-func (s *serverDispatcher) ResolveCompletionItem(ctx context.Context, params *CompletionItem) (*CompletionItem, error) { +- var result *CompletionItem +- if err := s.sender.Call(ctx, "completionItem/resolve", params, &result); err != nil { +- return nil, err +- } +- return result, nil +-} +-func (s *serverDispatcher) ResolveDocumentLink(ctx context.Context, params *DocumentLink) (*DocumentLink, error) { +- var result *DocumentLink +- if err := s.sender.Call(ctx, "documentLink/resolve", params, &result); err != nil { +- return nil, err +- } +- return result, nil +-} +-func (s *serverDispatcher) Exit(ctx context.Context) error { +- return s.sender.Notify(ctx, "exit", nil) +-} +-func (s *serverDispatcher) Initialize(ctx context.Context, params *ParamInitialize) (*InitializeResult, error) { +- var result *InitializeResult +- if err := s.sender.Call(ctx, "initialize", params, &result); err != nil { +- return nil, err +- } +- return result, nil +-} +-func (s *serverDispatcher) Initialized(ctx context.Context, params *InitializedParams) error { +- return s.sender.Notify(ctx, "initialized", params) +-} +-func (s *serverDispatcher) Resolve(ctx context.Context, params *InlayHint) (*InlayHint, error) { +- var result *InlayHint +- if err := s.sender.Call(ctx, "inlayHint/resolve", params, &result); err != nil { +- return nil, err +- } +- return result, nil +-} +-func (s *serverDispatcher) DidChangeNotebookDocument(ctx context.Context, params *DidChangeNotebookDocumentParams) error { +- return s.sender.Notify(ctx, "notebookDocument/didChange", params) +-} +-func (s *serverDispatcher) DidCloseNotebookDocument(ctx context.Context, params *DidCloseNotebookDocumentParams) error { +- return s.sender.Notify(ctx, "notebookDocument/didClose", params) +-} +-func (s *serverDispatcher) DidOpenNotebookDocument(ctx context.Context, params *DidOpenNotebookDocumentParams) error { +- return s.sender.Notify(ctx, "notebookDocument/didOpen", params) +-} +-func (s *serverDispatcher) DidSaveNotebookDocument(ctx context.Context, params *DidSaveNotebookDocumentParams) error { +- return s.sender.Notify(ctx, "notebookDocument/didSave", params) +-} +-func (s *serverDispatcher) Shutdown(ctx context.Context) error { +- return s.sender.Call(ctx, "shutdown", nil, nil) +-} +-func (s *serverDispatcher) CodeAction(ctx context.Context, params *CodeActionParams) ([]CodeAction, error) { +- var result []CodeAction +- if err := s.sender.Call(ctx, "textDocument/codeAction", params, &result); err != nil { +- return nil, err +- } +- return result, nil +-} +-func (s *serverDispatcher) CodeLens(ctx context.Context, params *CodeLensParams) ([]CodeLens, error) { +- var result []CodeLens +- if err := s.sender.Call(ctx, "textDocument/codeLens", params, &result); err != nil { +- return nil, err +- } +- return result, nil +-} +-func (s *serverDispatcher) ColorPresentation(ctx context.Context, params *ColorPresentationParams) ([]ColorPresentation, error) { +- var result []ColorPresentation +- if err := s.sender.Call(ctx, "textDocument/colorPresentation", params, &result); err != nil { +- return nil, err +- } +- return result, nil +-} +-func (s *serverDispatcher) Completion(ctx context.Context, params *CompletionParams) (*CompletionList, error) { +- var result *CompletionList +- if err := s.sender.Call(ctx, "textDocument/completion", params, &result); err != nil { +- return nil, err +- } +- return result, nil +-} +-func (s *serverDispatcher) Declaration(ctx context.Context, params *DeclarationParams) (*Or_textDocument_declaration, error) { +- var result *Or_textDocument_declaration +- if err := s.sender.Call(ctx, "textDocument/declaration", params, &result); err != nil { +- return nil, err +- } +- return result, nil +-} +-func (s *serverDispatcher) Definition(ctx context.Context, params *DefinitionParams) ([]Location, error) { +- var result []Location +- if err := s.sender.Call(ctx, "textDocument/definition", params, &result); err != nil { +- return nil, err +- } +- return result, nil +-} +-func (s *serverDispatcher) Diagnostic(ctx context.Context, params *string) (*string, error) { +- var result *string +- if err := s.sender.Call(ctx, "textDocument/diagnostic", params, &result); err != nil { +- return nil, err +- } +- return result, nil +-} +-func (s *serverDispatcher) DidChange(ctx context.Context, params *DidChangeTextDocumentParams) error { +- return s.sender.Notify(ctx, "textDocument/didChange", params) +-} +-func (s *serverDispatcher) DidClose(ctx context.Context, params *DidCloseTextDocumentParams) error { +- return s.sender.Notify(ctx, "textDocument/didClose", params) +-} +-func (s *serverDispatcher) DidOpen(ctx context.Context, params *DidOpenTextDocumentParams) error { +- return s.sender.Notify(ctx, "textDocument/didOpen", params) +-} +-func (s *serverDispatcher) DidSave(ctx context.Context, params *DidSaveTextDocumentParams) error { +- return s.sender.Notify(ctx, "textDocument/didSave", params) +-} +-func (s *serverDispatcher) DocumentColor(ctx context.Context, params *DocumentColorParams) ([]ColorInformation, error) { +- var result []ColorInformation +- if err := s.sender.Call(ctx, "textDocument/documentColor", params, &result); err != nil { +- return nil, err +- } +- return result, nil +-} +-func (s *serverDispatcher) DocumentHighlight(ctx context.Context, params *DocumentHighlightParams) ([]DocumentHighlight, error) { +- var result []DocumentHighlight +- if err := s.sender.Call(ctx, "textDocument/documentHighlight", params, &result); err != nil { +- return nil, err - } -- return source.PackageName(buf.String()) --} -- --// isLetter and isDigit allow only ASCII characters because --// "Each path element is a non-empty string made of up ASCII letters, --// ASCII digits, and limited ASCII punctuation" --// (see https://golang.org/ref/mod#go-mod-file-ident). -- --func isLetter(ch rune) bool { -- return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' +- return result, nil -} -- --func isDigit(ch rune) bool { -- return '0' <= ch && ch <= '9' +-func (s *serverDispatcher) DocumentLink(ctx context.Context, params *DocumentLinkParams) ([]DocumentLink, error) { +- var result []DocumentLink +- if err := s.sender.Call(ctx, "textDocument/documentLink", params, &result); err != nil { +- return nil, err +- } +- return result, nil -} -- --func isAllowedPunctuation(ch rune) bool { -- return ch == '_' || ch == '-' || ch == '~' || ch == '.' +-func (s *serverDispatcher) DocumentSymbol(ctx context.Context, params *DocumentSymbolParams) ([]interface{}, error) { +- var result []interface{} +- if err := s.sender.Call(ctx, "textDocument/documentSymbol", params, &result); err != nil { +- return nil, err +- } +- return result, nil -} -diff -urN a/gopls/internal/lsp/source/completion/package_test.go b/gopls/internal/lsp/source/completion/package_test.go ---- a/gopls/internal/lsp/source/completion/package_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/completion/package_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,81 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package completion -- --import ( -- "testing" -- -- "golang.org/x/tools/gopls/internal/lsp/source" --) -- --func TestIsValidDirName(t *testing.T) { -- tests := []struct { -- dirName string -- valid bool -- }{ -- {dirName: "", valid: false}, -- // -- {dirName: "a", valid: true}, -- {dirName: "abcdef", valid: true}, -- {dirName: "AbCdEf", valid: true}, -- // -- {dirName: "1a35", valid: true}, -- {dirName: "a16", valid: true}, -- // -- {dirName: "_a", valid: true}, -- {dirName: "a_", valid: true}, -- // -- {dirName: "~a", valid: false}, -- {dirName: "a~", valid: true}, -- // -- {dirName: "-a", valid: false}, -- {dirName: "a-", valid: true}, -- // -- {dirName: ".a", valid: false}, -- {dirName: "a.", valid: false}, -- // -- {dirName: "a~_b--c.-e", valid: true}, -- {dirName: "~a~_b--c.-e", valid: false}, -- {dirName: "a~_b--c.-e--~", valid: true}, -- {dirName: "a~_b--2134dc42.-e6--~", valid: true}, -- {dirName: "abc`def", valid: false}, -- {dirName: "тест", valid: false}, -- {dirName: "你好", valid: false}, +-func (s *serverDispatcher) FoldingRange(ctx context.Context, params *FoldingRangeParams) ([]FoldingRange, error) { +- var result []FoldingRange +- if err := s.sender.Call(ctx, "textDocument/foldingRange", params, &result); err != nil { +- return nil, err - } -- for _, tt := range tests { -- valid := isValidDirName(tt.dirName) -- if tt.valid != valid { -- t.Errorf("%s: expected %v, got %v", tt.dirName, tt.valid, valid) -- } +- return result, nil +-} +-func (s *serverDispatcher) Formatting(ctx context.Context, params *DocumentFormattingParams) ([]TextEdit, error) { +- var result []TextEdit +- if err := s.sender.Call(ctx, "textDocument/formatting", params, &result); err != nil { +- return nil, err - } +- return result, nil -} -- --func TestConvertDirNameToPkgName(t *testing.T) { -- tests := []struct { -- dirName string -- pkgName source.PackageName -- }{ -- {dirName: "a", pkgName: "a"}, -- {dirName: "abcdef", pkgName: "abcdef"}, -- {dirName: "AbCdEf", pkgName: "abcdef"}, -- {dirName: "1a35", pkgName: "a35"}, -- {dirName: "14a35", pkgName: "a35"}, -- {dirName: "a16", pkgName: "a16"}, -- {dirName: "_a", pkgName: "a"}, -- {dirName: "a_", pkgName: "a"}, -- {dirName: "a~", pkgName: "a"}, -- {dirName: "a-", pkgName: "a"}, -- {dirName: "a~_b--c.-e", pkgName: "abce"}, -- {dirName: "a~_b--c.-e--~", pkgName: "abce"}, -- {dirName: "a~_b--2134dc42.-e6--~", pkgName: "ab2134dc42e6"}, +-func (s *serverDispatcher) Hover(ctx context.Context, params *HoverParams) (*Hover, error) { +- var result *Hover +- if err := s.sender.Call(ctx, "textDocument/hover", params, &result); err != nil { +- return nil, err - } -- for _, tt := range tests { -- pkgName := convertDirNameToPkgName(tt.dirName) -- if tt.pkgName != pkgName { -- t.Errorf("%s: expected %v, got %v", tt.dirName, tt.pkgName, pkgName) -- continue -- } +- return result, nil +-} +-func (s *serverDispatcher) Implementation(ctx context.Context, params *ImplementationParams) ([]Location, error) { +- var result []Location +- if err := s.sender.Call(ctx, "textDocument/implementation", params, &result); err != nil { +- return nil, err - } +- return result, nil -} -diff -urN a/gopls/internal/lsp/source/completion/postfix_snippets.go b/gopls/internal/lsp/source/completion/postfix_snippets.go ---- a/gopls/internal/lsp/source/completion/postfix_snippets.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/completion/postfix_snippets.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,481 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package completion -- --import ( -- "context" -- "fmt" -- "go/ast" -- "go/token" -- "go/types" -- "log" -- "reflect" -- "strings" -- "sync" -- "text/template" -- -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/gopls/internal/lsp/snippet" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/imports" --) -- --// Postfix snippets are artificial methods that allow the user to --// compose common operations in an "argument oriented" fashion. For --// example, instead of "sort.Slice(someSlice, ...)" a user can expand --// "someSlice.sort!". -- --// postfixTmpl represents a postfix snippet completion candidate. --type postfixTmpl struct { -- // label is the completion candidate's label presented to the user. -- label string -- -- // details is passed along to the client as the candidate's details. -- details string -- -- // body is the template text. See postfixTmplArgs for details on the -- // facilities available to the template. -- body string -- -- tmpl *template.Template +-func (s *serverDispatcher) InlayHint(ctx context.Context, params *InlayHintParams) ([]InlayHint, error) { +- var result []InlayHint +- if err := s.sender.Call(ctx, "textDocument/inlayHint", params, &result); err != nil { +- return nil, err +- } +- return result, nil -} -- --// postfixTmplArgs are the template execution arguments available to --// the postfix snippet templates. --type postfixTmplArgs struct { -- // StmtOK is true if it is valid to replace the selector with a -- // statement. For example: -- // -- // func foo() { -- // bar.sort! // statement okay -- // -- // someMethod(bar.sort!) // statement not okay -- // } -- StmtOK bool -- -- // X is the textual SelectorExpr.X. For example, when completing -- // "foo.bar.print!", "X" is "foo.bar". -- X string -- -- // Obj is the types.Object of SelectorExpr.X, if any. -- Obj types.Object -- -- // Type is the type of "foo.bar" in "foo.bar.print!". -- Type types.Type -- -- scope *types.Scope -- snip snippet.Builder -- importIfNeeded func(pkgPath string, scope *types.Scope) (name string, edits []protocol.TextEdit, err error) -- edits []protocol.TextEdit -- qf types.Qualifier -- varNames map[string]bool +-func (s *serverDispatcher) InlineCompletion(ctx context.Context, params *InlineCompletionParams) (*Or_Result_textDocument_inlineCompletion, error) { +- var result *Or_Result_textDocument_inlineCompletion +- if err := s.sender.Call(ctx, "textDocument/inlineCompletion", params, &result); err != nil { +- return nil, err +- } +- return result, nil -} -- --var postfixTmpls = []postfixTmpl{{ -- label: "sort", -- details: "sort.Slice()", -- body: `{{if and (eq .Kind "slice") .StmtOK -}} --{{.Import "sort"}}.Slice({{.X}}, func({{.VarName nil "i"}}, {{.VarName nil "j"}} int) bool { -- {{.Cursor}} --}) --{{- end}}`, --}, { -- label: "last", -- details: "s[len(s)-1]", -- body: `{{if and (eq .Kind "slice") .Obj -}} --{{.X}}[len({{.X}})-1] --{{- end}}`, --}, { -- label: "reverse", -- details: "reverse slice", -- body: `{{if and (eq .Kind "slice") .StmtOK -}} --{{$i := .VarName nil "i"}}{{$j := .VarName nil "j" -}} --for {{$i}}, {{$j}} := 0, len({{.X}})-1; {{$i}} < {{$j}}; {{$i}}, {{$j}} = {{$i}}+1, {{$j}}-1 { -- {{.X}}[{{$i}}], {{.X}}[{{$j}}] = {{.X}}[{{$j}}], {{.X}}[{{$i}}] +-func (s *serverDispatcher) InlineValue(ctx context.Context, params *InlineValueParams) ([]InlineValue, error) { +- var result []InlineValue +- if err := s.sender.Call(ctx, "textDocument/inlineValue", params, &result); err != nil { +- return nil, err +- } +- return result, nil -} --{{end}}`, --}, { -- label: "range", -- details: "range over slice", -- body: `{{if and (eq .Kind "slice") .StmtOK -}} --for {{.VarName nil "i"}}, {{.VarName .ElemType "v"}} := range {{.X}} { -- {{.Cursor}} +-func (s *serverDispatcher) LinkedEditingRange(ctx context.Context, params *LinkedEditingRangeParams) (*LinkedEditingRanges, error) { +- var result *LinkedEditingRanges +- if err := s.sender.Call(ctx, "textDocument/linkedEditingRange", params, &result); err != nil { +- return nil, err +- } +- return result, nil -} --{{- end}}`, --}, { -- label: "append", -- details: "append and re-assign slice", -- body: `{{if and (eq .Kind "slice") .StmtOK .Obj -}} --{{.X}} = append({{.X}}, {{.Cursor}}) --{{- end}}`, --}, { -- label: "append", -- details: "append to slice", -- body: `{{if and (eq .Kind "slice") (not .StmtOK) -}} --append({{.X}}, {{.Cursor}}) --{{- end}}`, --}, { -- label: "copy", -- details: "duplicate slice", -- body: `{{if and (eq .Kind "slice") .StmtOK .Obj -}} --{{$v := (.VarName nil (printf "%sCopy" .X))}}{{$v}} := make([]{{.TypeName .ElemType}}, len({{.X}})) --copy({{$v}}, {{.X}}) --{{end}}`, --}, { -- label: "range", -- details: "range over map", -- body: `{{if and (eq .Kind "map") .StmtOK -}} --for {{.VarName .KeyType "k"}}, {{.VarName .ElemType "v"}} := range {{.X}} { -- {{.Cursor}} +-func (s *serverDispatcher) Moniker(ctx context.Context, params *MonikerParams) ([]Moniker, error) { +- var result []Moniker +- if err := s.sender.Call(ctx, "textDocument/moniker", params, &result); err != nil { +- return nil, err +- } +- return result, nil -} --{{- end}}`, --}, { -- label: "clear", -- details: "clear map contents", -- body: `{{if and (eq .Kind "map") .StmtOK -}} --{{$k := (.VarName .KeyType "k")}}for {{$k}} := range {{.X}} { -- delete({{.X}}, {{$k}}) +-func (s *serverDispatcher) OnTypeFormatting(ctx context.Context, params *DocumentOnTypeFormattingParams) ([]TextEdit, error) { +- var result []TextEdit +- if err := s.sender.Call(ctx, "textDocument/onTypeFormatting", params, &result); err != nil { +- return nil, err +- } +- return result, nil -} --{{end}}`, --}, { -- label: "keys", -- details: "create slice of keys", -- body: `{{if and (eq .Kind "map") .StmtOK -}} --{{$keysVar := (.VarName nil "keys")}}{{$keysVar}} := make([]{{.TypeName .KeyType}}, 0, len({{.X}})) --{{$k := (.VarName .KeyType "k")}}for {{$k}} := range {{.X}} { -- {{$keysVar}} = append({{$keysVar}}, {{$k}}) +-func (s *serverDispatcher) PrepareCallHierarchy(ctx context.Context, params *CallHierarchyPrepareParams) ([]CallHierarchyItem, error) { +- var result []CallHierarchyItem +- if err := s.sender.Call(ctx, "textDocument/prepareCallHierarchy", params, &result); err != nil { +- return nil, err +- } +- return result, nil -} --{{end}}`, --}, { -- label: "range", -- details: "range over channel", -- body: `{{if and (eq .Kind "chan") .StmtOK -}} --for {{.VarName .ElemType "e"}} := range {{.X}} { -- {{.Cursor}} +-func (s *serverDispatcher) PrepareRename(ctx context.Context, params *PrepareRenameParams) (*PrepareRenameResult, error) { +- var result *PrepareRenameResult +- if err := s.sender.Call(ctx, "textDocument/prepareRename", params, &result); err != nil { +- return nil, err +- } +- return result, nil -} --{{- end}}`, --}, { -- label: "var", -- details: "assign to variables", -- body: `{{if and (eq .Kind "tuple") .StmtOK -}} --{{$a := .}}{{range $i, $v := .Tuple}}{{if $i}}, {{end}}{{$a.VarName $v.Type $v.Name}}{{end}} := {{.X}} --{{- end}}`, --}, { -- label: "var", -- details: "assign to variable", -- body: `{{if and (ne .Kind "tuple") .StmtOK -}} --{{.VarName .Type ""}} := {{.X}} --{{- end}}`, --}, { -- label: "print", -- details: "print to stdout", -- body: `{{if and (ne .Kind "tuple") .StmtOK -}} --{{.Import "fmt"}}.Printf("{{.EscapeQuotes .X}}: %v\n", {{.X}}) --{{- end}}`, --}, { -- label: "print", -- details: "print to stdout", -- body: `{{if and (eq .Kind "tuple") .StmtOK -}} --{{.Import "fmt"}}.Println({{.X}}) --{{- end}}`, --}, { -- label: "split", -- details: "split string", -- body: `{{if (eq (.TypeName .Type) "string") -}} --{{.Import "strings"}}.Split({{.X}}, "{{.Cursor}}") --{{- end}}`, --}, { -- label: "join", -- details: "join string slice", -- body: `{{if and (eq .Kind "slice") (eq (.TypeName .ElemType) "string") -}} --{{.Import "strings"}}.Join({{.X}}, "{{.Cursor}}") --{{- end}}`, --}, { -- label: "ifnotnil", -- details: "if expr != nil", -- body: `{{if and (or (eq .Kind "pointer") (eq .Kind "chan") (eq .Kind "signature") (eq .Kind "interface") (eq .Kind "map") (eq .Kind "slice")) .StmtOK -}} --if {{.X}} != nil {{"{"}} -- {{.Cursor}} --{{"}"}} --{{- end}}`, --}} -- --// Cursor indicates where the client's cursor should end up after the --// snippet is done. --func (a *postfixTmplArgs) Cursor() string { -- a.snip.WriteFinalTabstop() -- return "" +-func (s *serverDispatcher) PrepareTypeHierarchy(ctx context.Context, params *TypeHierarchyPrepareParams) ([]TypeHierarchyItem, error) { +- var result []TypeHierarchyItem +- if err := s.sender.Call(ctx, "textDocument/prepareTypeHierarchy", params, &result); err != nil { +- return nil, err +- } +- return result, nil -} -- --// Import makes sure the package corresponding to path is imported, --// returning the identifier to use to refer to the package. --func (a *postfixTmplArgs) Import(path string) (string, error) { -- name, edits, err := a.importIfNeeded(path, a.scope) -- if err != nil { -- return "", fmt.Errorf("couldn't import %q: %w", path, err) +-func (s *serverDispatcher) RangeFormatting(ctx context.Context, params *DocumentRangeFormattingParams) ([]TextEdit, error) { +- var result []TextEdit +- if err := s.sender.Call(ctx, "textDocument/rangeFormatting", params, &result); err != nil { +- return nil, err - } -- a.edits = append(a.edits, edits...) -- -- return name, nil +- return result, nil -} -- --func (a *postfixTmplArgs) EscapeQuotes(v string) string { -- return strings.ReplaceAll(v, `"`, `\\"`) +-func (s *serverDispatcher) RangesFormatting(ctx context.Context, params *DocumentRangesFormattingParams) ([]TextEdit, error) { +- var result []TextEdit +- if err := s.sender.Call(ctx, "textDocument/rangesFormatting", params, &result); err != nil { +- return nil, err +- } +- return result, nil -} -- --// ElemType returns the Elem() type of xType, if applicable. --func (a *postfixTmplArgs) ElemType() types.Type { -- if e, _ := a.Type.(interface{ Elem() types.Type }); e != nil { -- return e.Elem() +-func (s *serverDispatcher) References(ctx context.Context, params *ReferenceParams) ([]Location, error) { +- var result []Location +- if err := s.sender.Call(ctx, "textDocument/references", params, &result); err != nil { +- return nil, err - } -- return nil +- return result, nil -} -- --// Kind returns the underlying kind of type, e.g. "slice", "struct", --// etc. --func (a *postfixTmplArgs) Kind() string { -- t := reflect.TypeOf(a.Type.Underlying()) -- return strings.ToLower(strings.TrimPrefix(t.String(), "*types.")) +-func (s *serverDispatcher) Rename(ctx context.Context, params *RenameParams) (*WorkspaceEdit, error) { +- var result *WorkspaceEdit +- if err := s.sender.Call(ctx, "textDocument/rename", params, &result); err != nil { +- return nil, err +- } +- return result, nil -} -- --// KeyType returns the type of X's key. KeyType panics if X is not a --// map. --func (a *postfixTmplArgs) KeyType() types.Type { -- return a.Type.Underlying().(*types.Map).Key() +-func (s *serverDispatcher) SelectionRange(ctx context.Context, params *SelectionRangeParams) ([]SelectionRange, error) { +- var result []SelectionRange +- if err := s.sender.Call(ctx, "textDocument/selectionRange", params, &result); err != nil { +- return nil, err +- } +- return result, nil -} -- --// Tuple returns the tuple result vars if X is a call expression. --func (a *postfixTmplArgs) Tuple() []*types.Var { -- tuple, _ := a.Type.(*types.Tuple) -- if tuple == nil { -- return nil +-func (s *serverDispatcher) SemanticTokensFull(ctx context.Context, params *SemanticTokensParams) (*SemanticTokens, error) { +- var result *SemanticTokens +- if err := s.sender.Call(ctx, "textDocument/semanticTokens/full", params, &result); err != nil { +- return nil, err - } -- -- typs := make([]*types.Var, 0, tuple.Len()) -- for i := 0; i < tuple.Len(); i++ { -- typs = append(typs, tuple.At(i)) +- return result, nil +-} +-func (s *serverDispatcher) SemanticTokensFullDelta(ctx context.Context, params *SemanticTokensDeltaParams) (interface{}, error) { +- var result interface{} +- if err := s.sender.Call(ctx, "textDocument/semanticTokens/full/delta", params, &result); err != nil { +- return nil, err - } -- return typs +- return result, nil -} -- --// TypeName returns the textual representation of type t. --func (a *postfixTmplArgs) TypeName(t types.Type) (string, error) { -- if t == nil || t == types.Typ[types.Invalid] { -- return "", fmt.Errorf("invalid type: %v", t) +-func (s *serverDispatcher) SemanticTokensRange(ctx context.Context, params *SemanticTokensRangeParams) (*SemanticTokens, error) { +- var result *SemanticTokens +- if err := s.sender.Call(ctx, "textDocument/semanticTokens/range", params, &result); err != nil { +- return nil, err - } -- return types.TypeString(t, a.qf), nil +- return result, nil -} -- --// VarName returns a suitable variable name for the type t. If t --// implements the error interface, "err" is used. If t is not a named --// type then nonNamedDefault is used. Otherwise a name is made by --// abbreviating the type name. If the resultant name is already in --// scope, an integer is appended to make a unique name. --func (a *postfixTmplArgs) VarName(t types.Type, nonNamedDefault string) string { -- if t == nil { -- t = types.Typ[types.Invalid] +-func (s *serverDispatcher) SignatureHelp(ctx context.Context, params *SignatureHelpParams) (*SignatureHelp, error) { +- var result *SignatureHelp +- if err := s.sender.Call(ctx, "textDocument/signatureHelp", params, &result); err != nil { +- return nil, err - } -- -- var name string -- // go/types predicates are undefined on types.Typ[types.Invalid]. -- if !types.Identical(t, types.Typ[types.Invalid]) && types.Implements(t, errorIntf) { -- name = "err" -- } else if _, isNamed := source.Deref(t).(*types.Named); !isNamed { -- name = nonNamedDefault +- return result, nil +-} +-func (s *serverDispatcher) TypeDefinition(ctx context.Context, params *TypeDefinitionParams) ([]Location, error) { +- var result []Location +- if err := s.sender.Call(ctx, "textDocument/typeDefinition", params, &result); err != nil { +- return nil, err - } -- -- if name == "" { -- name = types.TypeString(t, func(p *types.Package) string { -- return "" -- }) -- name = abbreviateTypeName(name) +- return result, nil +-} +-func (s *serverDispatcher) WillSave(ctx context.Context, params *WillSaveTextDocumentParams) error { +- return s.sender.Notify(ctx, "textDocument/willSave", params) +-} +-func (s *serverDispatcher) WillSaveWaitUntil(ctx context.Context, params *WillSaveTextDocumentParams) ([]TextEdit, error) { +- var result []TextEdit +- if err := s.sender.Call(ctx, "textDocument/willSaveWaitUntil", params, &result); err != nil { +- return nil, err - } -- -- if dot := strings.LastIndex(name, "."); dot > -1 { -- name = name[dot+1:] +- return result, nil +-} +-func (s *serverDispatcher) Subtypes(ctx context.Context, params *TypeHierarchySubtypesParams) ([]TypeHierarchyItem, error) { +- var result []TypeHierarchyItem +- if err := s.sender.Call(ctx, "typeHierarchy/subtypes", params, &result); err != nil { +- return nil, err - } -- -- uniqueName := name -- for i := 2; ; i++ { -- if s, _ := a.scope.LookupParent(uniqueName, token.NoPos); s == nil && !a.varNames[uniqueName] { -- break -- } -- uniqueName = fmt.Sprintf("%s%d", name, i) +- return result, nil +-} +-func (s *serverDispatcher) Supertypes(ctx context.Context, params *TypeHierarchySupertypesParams) ([]TypeHierarchyItem, error) { +- var result []TypeHierarchyItem +- if err := s.sender.Call(ctx, "typeHierarchy/supertypes", params, &result); err != nil { +- return nil, err - } -- -- a.varNames[uniqueName] = true -- -- return uniqueName +- return result, nil -} -- --func (c *completer) addPostfixSnippetCandidates(ctx context.Context, sel *ast.SelectorExpr) { -- if !c.opts.postfix { -- return +-func (s *serverDispatcher) WorkDoneProgressCancel(ctx context.Context, params *WorkDoneProgressCancelParams) error { +- return s.sender.Notify(ctx, "window/workDoneProgress/cancel", params) +-} +-func (s *serverDispatcher) DiagnosticWorkspace(ctx context.Context, params *WorkspaceDiagnosticParams) (*WorkspaceDiagnosticReport, error) { +- var result *WorkspaceDiagnosticReport +- if err := s.sender.Call(ctx, "workspace/diagnostic", params, &result); err != nil { +- return nil, err - } -- -- initPostfixRules() -- -- if sel == nil || sel.Sel == nil { -- return +- return result, nil +-} +-func (s *serverDispatcher) DidChangeConfiguration(ctx context.Context, params *DidChangeConfigurationParams) error { +- return s.sender.Notify(ctx, "workspace/didChangeConfiguration", params) +-} +-func (s *serverDispatcher) DidChangeWatchedFiles(ctx context.Context, params *DidChangeWatchedFilesParams) error { +- return s.sender.Notify(ctx, "workspace/didChangeWatchedFiles", params) +-} +-func (s *serverDispatcher) DidChangeWorkspaceFolders(ctx context.Context, params *DidChangeWorkspaceFoldersParams) error { +- return s.sender.Notify(ctx, "workspace/didChangeWorkspaceFolders", params) +-} +-func (s *serverDispatcher) DidCreateFiles(ctx context.Context, params *CreateFilesParams) error { +- return s.sender.Notify(ctx, "workspace/didCreateFiles", params) +-} +-func (s *serverDispatcher) DidDeleteFiles(ctx context.Context, params *DeleteFilesParams) error { +- return s.sender.Notify(ctx, "workspace/didDeleteFiles", params) +-} +-func (s *serverDispatcher) DidRenameFiles(ctx context.Context, params *RenameFilesParams) error { +- return s.sender.Notify(ctx, "workspace/didRenameFiles", params) +-} +-func (s *serverDispatcher) ExecuteCommand(ctx context.Context, params *ExecuteCommandParams) (interface{}, error) { +- var result interface{} +- if err := s.sender.Call(ctx, "workspace/executeCommand", params, &result); err != nil { +- return nil, err - } -- -- selType := c.pkg.GetTypesInfo().TypeOf(sel.X) -- if selType == nil { -- return +- return result, nil +-} +-func (s *serverDispatcher) Symbol(ctx context.Context, params *WorkspaceSymbolParams) ([]SymbolInformation, error) { +- var result []SymbolInformation +- if err := s.sender.Call(ctx, "workspace/symbol", params, &result); err != nil { +- return nil, err - } -- -- // Skip empty tuples since there is no value to operate on. -- if tuple, ok := selType.Underlying().(*types.Tuple); ok && tuple == nil { -- return +- return result, nil +-} +-func (s *serverDispatcher) WillCreateFiles(ctx context.Context, params *CreateFilesParams) (*WorkspaceEdit, error) { +- var result *WorkspaceEdit +- if err := s.sender.Call(ctx, "workspace/willCreateFiles", params, &result); err != nil { +- return nil, err - } -- -- tokFile := c.pkg.FileSet().File(c.pos) -- -- // Only replace sel with a statement if sel is already a statement. -- var stmtOK bool -- for i, n := range c.path { -- if n == sel && i < len(c.path)-1 { -- switch p := c.path[i+1].(type) { -- case *ast.ExprStmt: -- stmtOK = true -- case *ast.AssignStmt: -- // In cases like: -- // -- // foo.<> -- // bar = 123 -- // -- // detect that "foo." makes up the entire statement since the -- // apparent selector spans lines. -- stmtOK = safetoken.Line(tokFile, c.pos) < safetoken.Line(tokFile, p.TokPos) -- } -- break -- } +- return result, nil +-} +-func (s *serverDispatcher) WillDeleteFiles(ctx context.Context, params *DeleteFilesParams) (*WorkspaceEdit, error) { +- var result *WorkspaceEdit +- if err := s.sender.Call(ctx, "workspace/willDeleteFiles", params, &result); err != nil { +- return nil, err - } -- -- scope := c.pkg.GetTypes().Scope().Innermost(c.pos) -- if scope == nil { -- return +- return result, nil +-} +-func (s *serverDispatcher) WillRenameFiles(ctx context.Context, params *RenameFilesParams) (*WorkspaceEdit, error) { +- var result *WorkspaceEdit +- if err := s.sender.Call(ctx, "workspace/willRenameFiles", params, &result); err != nil { +- return nil, err - } +- return result, nil +-} +-func (s *serverDispatcher) ResolveWorkspaceSymbol(ctx context.Context, params *WorkspaceSymbol) (*WorkspaceSymbol, error) { +- var result *WorkspaceSymbol +- if err := s.sender.Call(ctx, "workspaceSymbol/resolve", params, &result); err != nil { +- return nil, err +- } +- return result, nil +-} +diff -urN a/gopls/internal/protocol/uri.go b/gopls/internal/protocol/uri.go +--- a/gopls/internal/protocol/uri.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/uri.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,220 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- // afterDot is the position after selector dot, e.g. "|" in -- // "foo.|print". -- afterDot := sel.Sel.Pos() +-package protocol - -- // We must detect dangling selectors such as: -- // -- // foo.<> -- // bar -- // -- // and adjust afterDot so that we don't mistakenly delete the -- // newline thinking "bar" is part of our selector. -- if startLine := safetoken.Line(tokFile, sel.Pos()); startLine != safetoken.Line(tokFile, afterDot) { -- if safetoken.Line(tokFile, c.pos) != startLine { -- return -- } -- afterDot = c.pos -- } +-// This file declares URI, DocumentURI, and its methods. +-// +-// For the LSP definition of these types, see +-// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#uri - -- for _, rule := range postfixTmpls { -- // When completing foo.print<>, "print" is naturally overwritten, -- // but we need to also remove "foo." so the snippet has a clean -- // slate. -- edits, err := c.editText(sel.Pos(), afterDot, "") -- if err != nil { -- event.Error(ctx, "error calculating postfix edits", err) -- return -- } +-import ( +- "fmt" +- "net/url" +- "path/filepath" +- "strings" +- "unicode" - -- tmplArgs := postfixTmplArgs{ -- X: source.FormatNode(c.pkg.FileSet(), sel.X), -- StmtOK: stmtOK, -- Obj: exprObj(c.pkg.GetTypesInfo(), sel.X), -- Type: selType, -- qf: c.qf, -- importIfNeeded: c.importIfNeeded, -- scope: scope, -- varNames: make(map[string]bool), -- } +- "golang.org/x/tools/gopls/internal/util/pathutil" +-) - -- // Feed the template straight into the snippet builder. This -- // allows templates to build snippets as they are executed. -- err = rule.tmpl.Execute(&tmplArgs.snip, &tmplArgs) -- if err != nil { -- event.Error(ctx, "error executing postfix template", err) -- continue -- } +-// A DocumentURI is the URI of a client editor document. +-// +-// According to the LSP specification: +-// +-// Care should be taken to handle encoding in URIs. For +-// example, some clients (such as VS Code) may encode colons +-// in drive letters while others do not. The URIs below are +-// both valid, but clients and servers should be consistent +-// with the form they use themselves to ensure the other party +-// doesn’t interpret them as distinct URIs. Clients and +-// servers should not assume that each other are encoding the +-// same way (for example a client encoding colons in drive +-// letters cannot assume server responses will have encoded +-// colons). The same applies to casing of drive letters - one +-// party should not assume the other party will return paths +-// with drive letters cased the same as it. +-// +-// file:///c:/project/readme.md +-// file:///C%3A/project/readme.md +-// +-// This is done during JSON unmarshalling; +-// see [DocumentURI.UnmarshalText] for details. +-type DocumentURI string - -- if strings.TrimSpace(tmplArgs.snip.String()) == "" { -- continue -- } +-// A URI is an arbitrary URL (e.g. https), not necessarily a file. +-type URI = string - -- score := c.matcher.Score(rule.label) -- if score <= 0 { -- continue -- } +-// UnmarshalText implements decoding of DocumentURI values. +-// +-// In particular, it implements a systematic correction of various odd +-// features of the definition of DocumentURI in the LSP spec that +-// appear to be workarounds for bugs in VS Code. For example, it may +-// URI-encode the URI itself, so that colon becomes %3A, and it may +-// send file://foo.go URIs that have two slashes (not three) and no +-// hostname. +-// +-// We use UnmarshalText, not UnmarshalJSON, because it is called even +-// for non-addressable values such as keys and values of map[K]V, +-// where there is no pointer of type *K or *V on which to call +-// UnmarshalJSON. (See Go issue #28189 for more detail.) +-// +-// Non-empty DocumentURIs are valid "file"-scheme URIs. +-// The empty DocumentURI is valid. +-func (uri *DocumentURI) UnmarshalText(data []byte) (err error) { +- *uri, err = ParseDocumentURI(string(data)) +- return +-} - -- c.items = append(c.items, CompletionItem{ -- Label: rule.label + "!", -- Detail: rule.details, -- Score: float64(score) * 0.01, -- Kind: protocol.SnippetCompletion, -- snippet: &tmplArgs.snip, -- AdditionalTextEdits: append(edits, tmplArgs.edits...), -- }) +-// Path returns the file path for the given URI. +-// +-// DocumentURI("").Path() returns the empty string. +-// +-// Path panics if called on a URI that is not a valid filename. +-func (uri DocumentURI) Path() string { +- filename, err := filename(uri) +- if err != nil { +- // e.g. ParseRequestURI failed. +- // +- // This can only affect DocumentURIs created by +- // direct string manipulation; all DocumentURIs +- // received from the client pass through +- // ParseRequestURI, which ensures validity. +- panic(err) - } +- return filepath.FromSlash(filename) -} - --var postfixRulesOnce sync.Once +-// Dir returns the URI for the directory containing the receiver. +-func (uri DocumentURI) Dir() DocumentURI { +- // This function could be more efficiently implemented by avoiding any call +- // to Path(), but at least consolidates URI manipulation. +- return URIFromPath(filepath.Dir(uri.Path())) +-} - --func initPostfixRules() { -- postfixRulesOnce.Do(func() { -- var idx int -- for _, rule := range postfixTmpls { -- var err error -- rule.tmpl, err = template.New("postfix_snippet").Parse(rule.body) -- if err != nil { -- log.Panicf("error parsing postfix snippet template: %v", err) -- } -- postfixTmpls[idx] = rule -- idx++ -- } -- postfixTmpls = postfixTmpls[:idx] -- }) +-// Encloses reports whether uri's path, considered as a sequence of segments, +-// is a prefix of file's path. +-func (uri DocumentURI) Encloses(file DocumentURI) bool { +- return pathutil.InDir(uri.Path(), file.Path()) -} - --// importIfNeeded returns the package identifier and any necessary --// edits to import package pkgPath. --func (c *completer) importIfNeeded(pkgPath string, scope *types.Scope) (string, []protocol.TextEdit, error) { -- defaultName := imports.ImportPathToAssumedName(pkgPath) +-func filename(uri DocumentURI) (string, error) { +- if uri == "" { +- return "", nil +- } - -- // Check if file already imports pkgPath. -- for _, s := range c.file.Imports { -- // TODO(adonovan): what if pkgPath has a vendor/ suffix? -- // This may be the cause of go.dev/issue/56291. -- if source.UnquoteImportPath(s) == source.ImportPath(pkgPath) { -- if s.Name == nil { -- return defaultName, nil, nil -- } -- if s.Name.Name != "_" { -- return s.Name.Name, nil, nil +- // This conservative check for the common case +- // of a simple non-empty absolute POSIX filename +- // avoids the allocation of a net.URL. +- if strings.HasPrefix(string(uri), "file:///") { +- rest := string(uri)[len("file://"):] // leave one slash +- for i := 0; i < len(rest); i++ { +- b := rest[i] +- // Reject these cases: +- if b < ' ' || b == 0x7f || // control character +- b == '%' || b == '+' || // URI escape +- b == ':' || // Windows drive letter +- b == '@' || b == '&' || b == '?' { // authority or query +- goto slow - } - } +- return rest, nil - } +-slow: - -- // Give up if the package's name is already in use by another object. -- if _, obj := scope.LookupParent(defaultName, token.NoPos); obj != nil { -- return "", nil, fmt.Errorf("import name %q of %q already in use", defaultName, pkgPath) -- } -- -- edits, err := c.importEdits(&importInfo{ -- importPath: pkgPath, -- }) +- u, err := url.ParseRequestURI(string(uri)) - if err != nil { -- return "", nil, err +- return "", err +- } +- if u.Scheme != fileScheme { +- return "", fmt.Errorf("only file URIs are supported, got %q from %q", u.Scheme, uri) +- } +- // If the URI is a Windows URI, we trim the leading "/" and uppercase +- // the drive letter, which will never be case sensitive. +- if isWindowsDriveURIPath(u.Path) { +- u.Path = strings.ToUpper(string(u.Path[1])) + u.Path[2:] - } - -- return defaultName, edits, nil +- return u.Path, nil -} -diff -urN a/gopls/internal/lsp/source/completion/printf.go b/gopls/internal/lsp/source/completion/printf.go ---- a/gopls/internal/lsp/source/completion/printf.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/completion/printf.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,172 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package completion -- --import ( -- "go/ast" -- "go/constant" -- "go/types" -- "strconv" -- "strings" -- "unicode/utf8" --) - --// printfArgKind returns the expected objKind when completing a --// printf-like operand. call is the printf-like function call, and --// argIdx is the index of call.Args being completed. --func printfArgKind(info *types.Info, call *ast.CallExpr, argIdx int) objKind { -- // Printf-like function name must end in "f". -- fn := exprObj(info, call.Fun) -- if fn == nil || !strings.HasSuffix(fn.Name(), "f") { -- return kindAny +-// ParseDocumentURI interprets a string as a DocumentURI, applying VS +-// Code workarounds; see [DocumentURI.UnmarshalText] for details. +-func ParseDocumentURI(s string) (DocumentURI, error) { +- if s == "" { +- return "", nil - } - -- sig, _ := fn.Type().(*types.Signature) -- if sig == nil { -- return kindAny +- if !strings.HasPrefix(s, "file://") { +- return "", fmt.Errorf("DocumentURI scheme is not 'file': %s", s) - } - -- // Must be variadic and take at least two params. -- numParams := sig.Params().Len() -- if !sig.Variadic() || numParams < 2 || argIdx < numParams-1 { -- return kindAny +- // VS Code sends URLs with only two slashes, +- // which are invalid. golang/go#39789. +- if !strings.HasPrefix(s, "file:///") { +- s = "file:///" + s[len("file://"):] - } - -- // Param preceding variadic args must be a (format) string. -- if !types.Identical(sig.Params().At(numParams-2).Type(), types.Typ[types.String]) { -- return kindAny +- // Even though the input is a URI, it may not be in canonical form. VS Code +- // in particular over-escapes :, @, etc. Unescape and re-encode to canonicalize. +- path, err := url.PathUnescape(s[len("file://"):]) +- if err != nil { +- return "", err - } - -- // Format string must be a constant. -- strArg := info.Types[call.Args[numParams-2]].Value -- if strArg == nil || strArg.Kind() != constant.String { -- return kindAny +- // File URIs from Windows may have lowercase drive letters. +- // Since drive letters are guaranteed to be case insensitive, +- // we change them to uppercase to remain consistent. +- // For example, file:///c:/x/y/z becomes file:///C:/x/y/z. +- if isWindowsDriveURIPath(path) { +- path = path[:1] + strings.ToUpper(string(path[1])) + path[2:] - } -- -- return formatOperandKind(constant.StringVal(strArg), argIdx-(numParams-1)+1) +- u := url.URL{Scheme: fileScheme, Path: path} +- return DocumentURI(u.String()), nil -} - --// formatOperandKind returns the objKind corresponding to format's --// operandIdx'th operand. --func formatOperandKind(format string, operandIdx int) objKind { -- var ( -- prevOperandIdx int -- kind = kindAny -- ) -- for { -- i := strings.Index(format, "%") -- if i == -1 { -- break -- } -- -- var operands []formatOperand -- format, operands = parsePrintfVerb(format[i+1:], prevOperandIdx) -- -- // Check if any this verb's operands correspond to our target -- // operandIdx. -- for _, v := range operands { -- if v.idx == operandIdx { -- if kind == kindAny { -- kind = v.kind -- } else if v.kind != kindAny { -- // If multiple verbs refer to the same operand, take the -- // intersection of their kinds. -- kind &= v.kind -- } -- } -- -- prevOperandIdx = v.idx +-// URIFromPath returns DocumentURI for the supplied file path. +-// Given "", it returns "". +-func URIFromPath(path string) DocumentURI { +- if path == "" { +- return "" +- } +- if !isWindowsDrivePath(path) { +- if abs, err := filepath.Abs(path); err == nil { +- path = abs - } - } -- return kind --} -- --type formatOperand struct { -- // idx is the one-based printf operand index. -- idx int -- // kind is a mask of expected kinds of objects for this operand. -- kind objKind --} -- --// parsePrintfVerb parses the leading printf verb in f. The opening --// "%" must already be trimmed from f. prevIdx is the previous --// operand's index, or zero if this is the first verb. The format --// string is returned with the leading verb removed. Multiple operands --// can be returned in the case of dynamic widths such as "%*.*f". --func parsePrintfVerb(f string, prevIdx int) (string, []formatOperand) { -- var verbs []formatOperand -- -- addVerb := func(k objKind) { -- verbs = append(verbs, formatOperand{ -- idx: prevIdx + 1, -- kind: k, -- }) -- prevIdx++ +- // Check the file path again, in case it became absolute. +- if isWindowsDrivePath(path) { +- path = "/" + strings.ToUpper(string(path[0])) + path[1:] - } -- -- for len(f) > 0 { -- // Trim first rune off of f so we are guaranteed to make progress. -- r, l := utf8.DecodeRuneInString(f) -- f = f[l:] -- -- // We care about three things: -- // 1. The verb, which maps directly to object kind. -- // 2. Explicit operand indices like "%[2]s". -- // 3. Dynamic widths using "*". -- switch r { -- case '%': -- return f, nil -- case '*': -- addVerb(kindInt) -- continue -- case '[': -- // Parse operand index as in "%[2]s". -- i := strings.Index(f, "]") -- if i == -1 { -- return f, nil -- } -- -- idx, err := strconv.Atoi(f[:i]) -- f = f[i+1:] -- if err != nil { -- return f, nil -- } -- -- prevIdx = idx - 1 -- continue -- case 'v', 'T': -- addVerb(kindAny) -- case 't': -- addVerb(kindBool) -- case 'c', 'd', 'o', 'O', 'U': -- addVerb(kindInt) -- case 'e', 'E', 'f', 'F', 'g', 'G': -- addVerb(kindFloat | kindComplex) -- case 'b': -- addVerb(kindInt | kindFloat | kindComplex | kindBytes) -- case 'q', 's': -- addVerb(kindString | kindBytes | kindStringer | kindError) -- case 'x', 'X': -- // Omit kindStringer and kindError though technically allowed. -- addVerb(kindString | kindBytes | kindInt | kindFloat | kindComplex) -- case 'p': -- addVerb(kindPtr | kindSlice) -- case 'w': -- addVerb(kindError) -- case '+', '-', '#', ' ', '.', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': -- // Flag or numeric width/precision value. -- continue -- default: -- // Assume unrecognized rune is a custom fmt.Formatter verb. -- addVerb(kindAny) -- } -- -- if len(verbs) > 0 { -- break -- } +- path = filepath.ToSlash(path) +- u := url.URL{ +- Scheme: fileScheme, +- Path: path, - } +- return DocumentURI(u.String()) +-} - -- return f, verbs --} -diff -urN a/gopls/internal/lsp/source/completion/printf_test.go b/gopls/internal/lsp/source/completion/printf_test.go ---- a/gopls/internal/lsp/source/completion/printf_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/completion/printf_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,72 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package completion -- --import ( -- "fmt" -- "testing" --) -- --func TestFormatOperandKind(t *testing.T) { -- cases := []struct { -- f string -- idx int -- kind objKind -- }{ -- {"", 1, kindAny}, -- {"%", 1, kindAny}, -- {"%%%", 1, kindAny}, -- {"%[1", 1, kindAny}, -- {"%[?%s", 2, kindAny}, -- {"%[abc]v", 1, kindAny}, -- -- {"%v", 1, kindAny}, -- {"%T", 1, kindAny}, -- {"%t", 1, kindBool}, -- {"%d", 1, kindInt}, -- {"%c", 1, kindInt}, -- {"%o", 1, kindInt}, -- {"%O", 1, kindInt}, -- {"%U", 1, kindInt}, -- {"%e", 1, kindFloat | kindComplex}, -- {"%E", 1, kindFloat | kindComplex}, -- {"%f", 1, kindFloat | kindComplex}, -- {"%F", 1, kindFloat | kindComplex}, -- {"%g", 1, kindFloat | kindComplex}, -- {"%G", 1, kindFloat | kindComplex}, -- {"%b", 1, kindInt | kindFloat | kindComplex | kindBytes}, -- {"%q", 1, kindString | kindBytes | kindStringer | kindError}, -- {"%s", 1, kindString | kindBytes | kindStringer | kindError}, -- {"%x", 1, kindString | kindBytes | kindInt | kindFloat | kindComplex}, -- {"%X", 1, kindString | kindBytes | kindInt | kindFloat | kindComplex}, -- {"%p", 1, kindPtr | kindSlice}, -- {"%w", 1, kindError}, -- -- {"%1.2f", 1, kindFloat | kindComplex}, -- {"%*f", 1, kindInt}, -- {"%*f", 2, kindFloat | kindComplex}, -- {"%*.*f", 1, kindInt}, -- {"%*.*f", 2, kindInt}, -- {"%*.*f", 3, kindFloat | kindComplex}, -- {"%[3]*.[2]*[1]f", 1, kindFloat | kindComplex}, -- {"%[3]*.[2]*[1]f", 2, kindInt}, -- {"%[3]*.[2]*[1]f", 3, kindInt}, -- -- {"foo %% %d", 1, kindInt}, -- {"%#-12.34f", 1, kindFloat | kindComplex}, -- {"% d", 1, kindInt}, -- -- {"%s %[1]X %d", 1, kindString | kindBytes}, -- {"%s %[1]X %d", 2, kindInt}, +-const fileScheme = "file" +- +-// isWindowsDrivePath returns true if the file path is of the form used by +-// Windows. We check if the path begins with a drive letter, followed by a ":". +-// For example: C:/x/y/z. +-func isWindowsDrivePath(path string) bool { +- if len(path) < 3 { +- return false - } +- return unicode.IsLetter(rune(path[0])) && path[1] == ':' +-} - -- for _, c := range cases { -- t.Run(fmt.Sprintf("%q#%d", c.f, c.idx), func(t *testing.T) { -- if got := formatOperandKind(c.f, c.idx); got != c.kind { -- t.Errorf("expected %d (%[1]b), got %d (%[2]b)", c.kind, got) -- } -- }) +-// isWindowsDriveURIPath returns true if the file URI is of the format used by +-// Windows URIs. The url.Parse package does not specially handle Windows paths +-// (see golang/go#6027), so we check if the URI path has a drive prefix (e.g. "/C:"). +-func isWindowsDriveURIPath(uri string) bool { +- if len(uri) < 4 { +- return false - } +- return uri[0] == '/' && unicode.IsLetter(rune(uri[1])) && uri[2] == ':' -} -diff -urN a/gopls/internal/lsp/source/completion/snippet.go b/gopls/internal/lsp/source/completion/snippet.go ---- a/gopls/internal/lsp/source/completion/snippet.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/completion/snippet.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,121 +0,0 @@ +diff -urN a/gopls/internal/protocol/uri_test.go b/gopls/internal/protocol/uri_test.go +--- a/gopls/internal/protocol/uri_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/uri_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,134 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package completion +-//go:build !windows +-// +build !windows +- +-package protocol_test - -import ( -- "go/ast" +- "testing" - -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/gopls/internal/lsp/snippet" +- "golang.org/x/tools/gopls/internal/protocol" -) - --// structFieldSnippet calculates the snippet for struct literal field names. --func (c *completer) structFieldSnippet(cand candidate, detail string, snip *snippet.Builder) { -- if !c.wantStructFieldCompletions() { -- return +-// TestURIFromPath tests the conversion between URIs and filenames. The test cases +-// include Windows-style URIs and filepaths, but we avoid having OS-specific +-// tests by using only forward slashes, assuming that the standard library +-// functions filepath.ToSlash and filepath.FromSlash do not need testing. +-func TestURIFromPath(t *testing.T) { +- for _, test := range []struct { +- path, wantFile string +- wantURI protocol.DocumentURI +- }{ +- { +- path: ``, +- wantFile: ``, +- wantURI: protocol.DocumentURI(""), +- }, +- { +- path: `C:/Windows/System32`, +- wantFile: `C:/Windows/System32`, +- wantURI: protocol.DocumentURI("file:///C:/Windows/System32"), +- }, +- { +- path: `C:/Go/src/bob.go`, +- wantFile: `C:/Go/src/bob.go`, +- wantURI: protocol.DocumentURI("file:///C:/Go/src/bob.go"), +- }, +- { +- path: `c:/Go/src/bob.go`, +- wantFile: `C:/Go/src/bob.go`, +- wantURI: protocol.DocumentURI("file:///C:/Go/src/bob.go"), +- }, +- { +- path: `/path/to/dir`, +- wantFile: `/path/to/dir`, +- wantURI: protocol.DocumentURI("file:///path/to/dir"), +- }, +- { +- path: `/a/b/c/src/bob.go`, +- wantFile: `/a/b/c/src/bob.go`, +- wantURI: protocol.DocumentURI("file:///a/b/c/src/bob.go"), +- }, +- { +- path: `c:/Go/src/bob george/george/george.go`, +- wantFile: `C:/Go/src/bob george/george/george.go`, +- wantURI: protocol.DocumentURI("file:///C:/Go/src/bob%20george/george/george.go"), +- }, +- } { +- got := protocol.URIFromPath(test.path) +- if got != test.wantURI { +- t.Errorf("URIFromPath(%q): got %q, expected %q", test.path, got, test.wantURI) +- } +- gotFilename := got.Path() +- if gotFilename != test.wantFile { +- t.Errorf("Filename(%q): got %q, expected %q", got, gotFilename, test.wantFile) +- } - } +-} - -- // If we are in a deep completion then we can't be completing a field -- // name (e.g. "Foo{f<>}" completing to "Foo{f.Bar}" should not generate -- // a snippet). -- if len(cand.path) > 0 { -- return +-func TestParseDocumentURI(t *testing.T) { +- for _, test := range []struct { +- input string +- want string // string(DocumentURI) on success or error.Error() on failure +- wantPath string // expected DocumentURI.Path on success +- }{ +- { +- input: `file:///c:/Go/src/bob%20george/george/george.go`, +- want: "file:///C:/Go/src/bob%20george/george/george.go", +- wantPath: `C:/Go/src/bob george/george/george.go`, +- }, +- { +- input: `file:///C%3A/Go/src/bob%20george/george/george.go`, +- want: "file:///C:/Go/src/bob%20george/george/george.go", +- wantPath: `C:/Go/src/bob george/george/george.go`, +- }, +- { +- input: `file:///path/to/%25p%25ercent%25/per%25cent.go`, +- want: `file:///path/to/%25p%25ercent%25/per%25cent.go`, +- wantPath: `/path/to/%p%ercent%/per%cent.go`, +- }, +- { +- input: `file:///C%3A/`, +- want: `file:///C:/`, +- wantPath: `C:/`, +- }, +- { +- input: `file:///`, +- want: `file:///`, +- wantPath: `/`, +- }, +- { +- input: `file://wsl%24/Ubuntu/home/wdcui/repo/VMEnclaves/cvm-runtime`, +- want: `file:///wsl$/Ubuntu/home/wdcui/repo/VMEnclaves/cvm-runtime`, +- wantPath: `/wsl$/Ubuntu/home/wdcui/repo/VMEnclaves/cvm-runtime`, +- }, +- { +- input: "", +- want: "", +- wantPath: "", +- }, +- // Errors: +- { +- input: "https://go.dev/", +- want: "DocumentURI scheme is not 'file': https://go.dev/", +- }, +- } { +- uri, err := protocol.ParseDocumentURI(test.input) +- var got string +- if err != nil { +- got = err.Error() +- } else { +- got = string(uri) +- } +- if got != test.want { +- t.Errorf("ParseDocumentURI(%q): got %q, want %q", test.input, got, test.want) +- } +- if err == nil && uri.Path() != test.wantPath { +- t.Errorf("DocumentURI(%s).Path = %q, want %q", uri, +- uri.Path(), test.wantPath) +- } - } +-} +diff -urN a/gopls/internal/protocol/uri_windows_test.go b/gopls/internal/protocol/uri_windows_test.go +--- a/gopls/internal/protocol/uri_windows_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/protocol/uri_windows_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,139 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- clInfo := c.enclosingCompositeLiteral +-//go:build windows +-// +build windows - -- // If we are already in a key-value expression, we don't want a snippet. -- if clInfo.kv != nil { -- return -- } +-package protocol_test - -- // A plain snippet turns "Foo{Ba<>" into "Foo{Bar: <>". -- snip.WriteText(": ") -- snip.WritePlaceholder(func(b *snippet.Builder) { -- // A placeholder snippet turns "Foo{Ba<>" into "Foo{Bar: <*int*>". -- if c.opts.placeholders { -- b.WriteText(detail) -- } -- }) +-import ( +- "path/filepath" +- "testing" - -- fset := c.pkg.FileSet() +- "golang.org/x/tools/gopls/internal/protocol" +-) - -- // If the cursor position is on a different line from the literal's opening brace, -- // we are in a multiline literal. Ignore line directives. -- if safetoken.StartPosition(fset, c.pos).Line != safetoken.StartPosition(fset, clInfo.cl.Lbrace).Line { -- snip.WriteText(",") +-// TestURIFromPath tests the conversion between URIs and filenames. The test cases +-// include Windows-style URIs and filepaths, but we avoid having OS-specific +-// tests by using only forward slashes, assuming that the standard library +-// functions filepath.ToSlash and filepath.FromSlash do not need testing. +-func TestURIFromPath(t *testing.T) { +- rootPath, err := filepath.Abs("/") +- if err != nil { +- t.Fatal(err) - } --} -- --// functionCallSnippet calculates the snippet for function calls. --func (c *completer) functionCallSnippet(name string, tparams, params []string, snip *snippet.Builder) { -- if !c.opts.completeFunctionCalls { -- snip.WriteText(name) -- return +- if len(rootPath) < 2 || rootPath[1] != ':' { +- t.Fatalf("malformed root path %q", rootPath) - } +- driveLetter := string(rootPath[0]) - -- // If there is no suffix then we need to reuse existing call parens -- // "()" if present. If there is an identifier suffix then we always -- // need to include "()" since we don't overwrite the suffix. -- if c.surrounding != nil && c.surrounding.Suffix() == "" && len(c.path) > 1 { -- // If we are the left side (i.e. "Fun") part of a call expression, -- // we don't want a snippet since there are already parens present. -- switch n := c.path[1].(type) { -- case *ast.CallExpr: -- // The Lparen != Rparen check detects fudged CallExprs we -- // inserted when fixing the AST. In this case, we do still need -- // to insert the calling "()" parens. -- if n.Fun == c.path[0] && n.Lparen != n.Rparen { -- return -- } -- case *ast.SelectorExpr: -- if len(c.path) > 2 { -- if call, ok := c.path[2].(*ast.CallExpr); ok && call.Fun == c.path[1] && call.Lparen != call.Rparen { -- return -- } -- } +- for _, test := range []struct { +- path, wantFile string +- wantURI protocol.DocumentURI +- }{ +- { +- path: ``, +- wantFile: ``, +- wantURI: protocol.DocumentURI(""), +- }, +- { +- path: `C:\Windows\System32`, +- wantFile: `C:\Windows\System32`, +- wantURI: protocol.DocumentURI("file:///C:/Windows/System32"), +- }, +- { +- path: `C:\Go\src\bob.go`, +- wantFile: `C:\Go\src\bob.go`, +- wantURI: protocol.DocumentURI("file:///C:/Go/src/bob.go"), +- }, +- { +- path: `c:\Go\src\bob.go`, +- wantFile: `C:\Go\src\bob.go`, +- wantURI: protocol.DocumentURI("file:///C:/Go/src/bob.go"), +- }, +- { +- path: `\path\to\dir`, +- wantFile: driveLetter + `:\path\to\dir`, +- wantURI: protocol.DocumentURI("file:///" + driveLetter + ":/path/to/dir"), +- }, +- { +- path: `\a\b\c\src\bob.go`, +- wantFile: driveLetter + `:\a\b\c\src\bob.go`, +- wantURI: protocol.DocumentURI("file:///" + driveLetter + ":/a/b/c/src/bob.go"), +- }, +- { +- path: `c:\Go\src\bob george\george\george.go`, +- wantFile: `C:\Go\src\bob george\george\george.go`, +- wantURI: protocol.DocumentURI("file:///C:/Go/src/bob%20george/george/george.go"), +- }, +- } { +- got := protocol.URIFromPath(test.path) +- if got != test.wantURI { +- t.Errorf("URIFromPath(%q): got %q, expected %q", test.path, got, test.wantURI) +- } +- gotFilename := got.Path() +- if gotFilename != test.wantFile { +- t.Errorf("Filename(%q): got %q, expected %q", got, gotFilename, test.wantFile) - } - } +-} - -- snip.WriteText(name) -- -- if len(tparams) > 0 { -- snip.WriteText("[") -- if c.opts.placeholders { -- for i, tp := range tparams { -- if i > 0 { -- snip.WriteText(", ") -- } -- snip.WritePlaceholder(func(b *snippet.Builder) { -- b.WriteText(tp) -- }) -- } +-func TestParseDocumentURI(t *testing.T) { +- for _, test := range []struct { +- input string +- want string // string(DocumentURI) on success or error.Error() on failure +- wantPath string // expected DocumentURI.Path on success +- }{ +- { +- input: `file:///c:/Go/src/bob%20george/george/george.go`, +- want: "file:///C:/Go/src/bob%20george/george/george.go", +- wantPath: `C:\Go\src\bob george\george\george.go`, +- }, +- { +- input: `file:///C%3A/Go/src/bob%20george/george/george.go`, +- want: "file:///C:/Go/src/bob%20george/george/george.go", +- wantPath: `C:\Go\src\bob george\george\george.go`, +- }, +- { +- input: `file:///c:/path/to/%25p%25ercent%25/per%25cent.go`, +- want: `file:///C:/path/to/%25p%25ercent%25/per%25cent.go`, +- wantPath: `C:\path\to\%p%ercent%\per%cent.go`, +- }, +- { +- input: `file:///C%3A/`, +- want: `file:///C:/`, +- wantPath: `C:\`, +- }, +- { +- input: `file:///`, +- want: `file:///`, +- wantPath: `\`, +- }, +- { +- input: "", +- want: "", +- wantPath: "", +- }, +- // Errors: +- { +- input: "https://go.dev/", +- want: "DocumentURI scheme is not 'file': https://go.dev/", +- }, +- } { +- uri, err := protocol.ParseDocumentURI(test.input) +- var got string +- if err != nil { +- got = err.Error() - } else { -- snip.WritePlaceholder(nil) +- got = string(uri) - } -- snip.WriteText("]") -- } -- -- snip.WriteText("(") -- -- if c.opts.placeholders { -- // A placeholder snippet turns "someFun<>" into "someFunc(<*i int*>, *s string*)". -- for i, p := range params { -- if i > 0 { -- snip.WriteText(", ") -- } -- snip.WritePlaceholder(func(b *snippet.Builder) { -- b.WriteText(p) -- }) +- if got != test.want { +- t.Errorf("ParseDocumentURI(%q): got %q, want %q", test.input, got, test.want) - } -- } else { -- // A plain snippet turns "someFun<>" into "someFunc(<>)". -- if len(params) > 0 { -- snip.WritePlaceholder(nil) +- if err == nil && uri.Path() != test.wantPath { +- t.Errorf("DocumentURI(%s).Path = %q, want %q", uri, +- uri.Path(), test.wantPath) - } - } -- -- snip.WriteText(")") -} -diff -urN a/gopls/internal/lsp/source/completion/statements.go b/gopls/internal/lsp/source/completion/statements.go ---- a/gopls/internal/lsp/source/completion/statements.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/completion/statements.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,360 +0,0 @@ +Binary files a/gopls/internal/server/assets/favicon.ico and b/gopls/internal/server/assets/favicon.ico differ +diff -urN a/gopls/internal/server/assets/go-logo-blue.svg b/gopls/internal/server/assets/go-logo-blue.svg +--- a/gopls/internal/server/assets/go-logo-blue.svg 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/assets/go-logo-blue.svg 1970-01-01 00:00:00.000000000 +0000 +@@ -1 +0,0 @@ +- +\ No newline at end of file +diff -urN a/gopls/internal/server/call_hierarchy.go b/gopls/internal/server/call_hierarchy.go +--- a/gopls/internal/server/call_hierarchy.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/call_hierarchy.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,59 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package completion +-package server - -import ( -- "fmt" -- "go/ast" -- "go/token" -- "go/types" +- "context" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/snippet" -- "golang.org/x/tools/gopls/internal/lsp/source" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/event" -) - --// addStatementCandidates adds full statement completion candidates --// appropriate for the current context. --func (c *completer) addStatementCandidates() { -- c.addErrCheck() -- c.addAssignAppend() --} +-func (s *server) PrepareCallHierarchy(ctx context.Context, params *protocol.CallHierarchyPrepareParams) ([]protocol.CallHierarchyItem, error) { +- ctx, done := event.Start(ctx, "lsp.Server.prepareCallHierarchy") +- defer done() - --// addAssignAppend offers a completion candidate of the form: --// --// someSlice = append(someSlice, ) --// --// It will offer the "append" completion in either of two situations: --// --// 1. Position is in RHS of assign, prefix matches "append", and --// corresponding LHS object is a slice. For example, --// "foo = ap<>" completes to "foo = append(foo, )". --// --// 2. Prefix is an ident or selector in an *ast.ExprStmt (i.e. --// beginning of statement), and our best matching candidate is a --// slice. For example: "foo.ba" completes to "foo.bar = append(foo.bar, )". --func (c *completer) addAssignAppend() { -- if len(c.path) < 3 { -- return +- fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI) +- if err != nil { +- return nil, err - } -- -- ident, _ := c.path[0].(*ast.Ident) -- if ident == nil { -- return +- defer release() +- if snapshot.FileKind(fh) != file.Go { +- return nil, nil // empty result - } +- return golang.PrepareCallHierarchy(ctx, snapshot, fh, params.Position) +-} - -- var ( -- // sliceText is the full name of our slice object, e.g. "s.abc" in -- // "s.abc = app<>". -- sliceText string -- // needsLHS is true if we need to prepend the LHS slice name and -- // "=" to our candidate. -- needsLHS = false -- fset = c.pkg.FileSet() -- ) -- -- switch n := c.path[1].(type) { -- case *ast.AssignStmt: -- // We are already in an assignment. Make sure our prefix matches "append". -- if c.matcher.Score("append") <= 0 { -- return -- } -- -- exprIdx := exprAtPos(c.pos, n.Rhs) -- if exprIdx == len(n.Rhs) || exprIdx > len(n.Lhs)-1 { -- return -- } -- -- lhsType := c.pkg.GetTypesInfo().TypeOf(n.Lhs[exprIdx]) -- if lhsType == nil { -- return -- } +-func (s *server) IncomingCalls(ctx context.Context, params *protocol.CallHierarchyIncomingCallsParams) ([]protocol.CallHierarchyIncomingCall, error) { +- ctx, done := event.Start(ctx, "lsp.Server.incomingCalls") +- defer done() - -- // Make sure our corresponding LHS object is a slice. -- if _, isSlice := lhsType.Underlying().(*types.Slice); !isSlice { -- return -- } +- fh, snapshot, release, err := s.fileOf(ctx, params.Item.URI) +- if err != nil { +- return nil, err +- } +- defer release() +- if snapshot.FileKind(fh) != file.Go { +- return nil, nil // empty result +- } +- return golang.IncomingCalls(ctx, snapshot, fh, params.Item.Range.Start) +-} - -- // The name or our slice is whatever's in the LHS expression. -- sliceText = source.FormatNode(fset, n.Lhs[exprIdx]) -- case *ast.SelectorExpr: -- // Make sure we are a selector at the beginning of a statement. -- if _, parentIsExprtStmt := c.path[2].(*ast.ExprStmt); !parentIsExprtStmt { -- return -- } +-func (s *server) OutgoingCalls(ctx context.Context, params *protocol.CallHierarchyOutgoingCallsParams) ([]protocol.CallHierarchyOutgoingCall, error) { +- ctx, done := event.Start(ctx, "lsp.Server.outgoingCalls") +- defer done() - -- // So far we only know the first part of our slice name. For -- // example in "s.a<>" we only know our slice begins with "s." -- // since the user could still be typing. -- sliceText = source.FormatNode(fset, n.X) + "." -- needsLHS = true -- case *ast.ExprStmt: -- needsLHS = true -- default: -- return +- fh, snapshot, release, err := s.fileOf(ctx, params.Item.URI) +- if err != nil { +- return nil, err - } +- defer release() +- if snapshot.FileKind(fh) != file.Go { +- return nil, nil // empty result +- } +- return golang.OutgoingCalls(ctx, snapshot, fh, params.Item.Range.Start) +-} +diff -urN a/gopls/internal/server/code_action.go b/gopls/internal/server/code_action.go +--- a/gopls/internal/server/code_action.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/code_action.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,275 +0,0 @@ +-// Copyright 2018 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- var ( -- label string -- snip snippet.Builder -- score = highScore -- ) +-package server - -- if needsLHS { -- // Offer the long form assign + append candidate if our best -- // candidate is a slice. -- bestItem := c.topCandidate() -- if bestItem == nil || !bestItem.isSlice { -- return -- } +-import ( +- "context" +- "fmt" +- "sort" +- "strings" - -- // Don't rank the full form assign + append candidate above the -- // slice itself. -- score = bestItem.Score - 0.01 +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/mod" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/command" +- "golang.org/x/tools/internal/event" +-) - -- // Fill in rest of sliceText now that we have the object name. -- sliceText += bestItem.Label +-func (s *server) CodeAction(ctx context.Context, params *protocol.CodeActionParams) ([]protocol.CodeAction, error) { +- ctx, done := event.Start(ctx, "lsp.Server.codeAction") +- defer done() - -- // Fill in the candidate's LHS bits. -- label = fmt.Sprintf("%s = ", bestItem.Label) -- snip.WriteText(label) +- fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI) +- if err != nil { +- return nil, err - } +- defer release() +- uri := fh.URI() - -- snip.WriteText(fmt.Sprintf("append(%s, ", sliceText)) -- snip.WritePlaceholder(nil) -- snip.WriteText(")") +- // Determine the supported actions for this file kind. +- kind := snapshot.FileKind(fh) +- supportedCodeActions, ok := snapshot.Options().SupportedCodeActions[kind] +- if !ok { +- return nil, fmt.Errorf("no supported code actions for %v file kind", kind) +- } +- if len(supportedCodeActions) == 0 { +- return nil, nil // not an error if there are none supported +- } - -- c.items = append(c.items, CompletionItem{ -- Label: label + fmt.Sprintf("append(%s, )", sliceText), -- Kind: protocol.FunctionCompletion, -- Score: score, -- snippet: &snip, -- }) --} +- // The Only field of the context specifies which code actions the client wants. +- // If Only is empty, assume that the client wants all of the non-explicit code actions. +- var want map[protocol.CodeActionKind]bool +- { +- // Explicit Code Actions are opt-in and shouldn't be returned to the client unless +- // requested using Only. +- // TODO: Add other CodeLenses such as GoGenerate, RegenerateCgo, etc.. +- explicit := map[protocol.CodeActionKind]bool{ +- protocol.GoTest: true, +- } - --// topCandidate returns the strictly highest scoring candidate --// collected so far. If the top two candidates have the same score, --// nil is returned. --func (c *completer) topCandidate() *CompletionItem { -- var bestItem, secondBestItem *CompletionItem -- for i := range c.items { -- if bestItem == nil || c.items[i].Score > bestItem.Score { -- bestItem = &c.items[i] -- } else if secondBestItem == nil || c.items[i].Score > secondBestItem.Score { -- secondBestItem = &c.items[i] +- if len(params.Context.Only) == 0 { +- want = supportedCodeActions +- } else { +- want = make(map[protocol.CodeActionKind]bool) +- for _, only := range params.Context.Only { +- for k, v := range supportedCodeActions { +- if only == k || strings.HasPrefix(string(k), string(only)+".") { +- want[k] = want[k] || v +- } +- } +- want[only] = want[only] || explicit[only] +- } - } - } -- -- // If secondBestItem has the same score, bestItem isn't -- // the strict best. -- if secondBestItem != nil && secondBestItem.Score == bestItem.Score { -- return nil +- if len(want) == 0 { +- return nil, fmt.Errorf("no supported code action to execute for %s, wanted %v", uri, params.Context.Only) - } - -- return bestItem --} +- switch kind { +- case file.Mod: +- var actions []protocol.CodeAction - --// addErrCheck offers a completion candidate of the form: --// --// if err != nil { --// return nil, err --// } --// --// In the case of test functions, it offers a completion candidate of the form: --// --// if err != nil { --// t.Fatal(err) --// } --// --// The position must be in a function that returns an error, and the --// statement preceding the position must be an assignment where the --// final LHS object is an error. addErrCheck will synthesize --// zero values as necessary to make the return statement valid. --func (c *completer) addErrCheck() { -- if len(c.path) < 2 || c.enclosingFunc == nil || !c.opts.placeholders { -- return -- } +- fixes, err := s.codeActionsMatchingDiagnostics(ctx, fh.URI(), snapshot, params.Context.Diagnostics, want) +- if err != nil { +- return nil, err +- } - -- var ( -- errorType = types.Universe.Lookup("error").Type() -- result = c.enclosingFunc.sig.Results() -- testVar = getTestVar(c.enclosingFunc, c.pkg) -- isTest = testVar != "" -- doesNotReturnErr = result.Len() == 0 || !types.Identical(result.At(result.Len()-1).Type(), errorType) -- ) -- // Make sure our enclosing function is a Test func or returns an error. -- if !isTest && doesNotReturnErr { -- return -- } +- // Group vulnerability fixes by their range, and select only the most +- // appropriate upgrades. +- // +- // TODO(rfindley): can this instead be accomplished on the diagnosis side, +- // so that code action handling remains uniform? +- vulnFixes := make(map[protocol.Range][]protocol.CodeAction) +- searchFixes: +- for _, fix := range fixes { +- for _, diag := range fix.Diagnostics { +- if diag.Source == string(cache.Govulncheck) || diag.Source == string(cache.Vulncheck) { +- vulnFixes[diag.Range] = append(vulnFixes[diag.Range], fix) +- continue searchFixes +- } +- } +- actions = append(actions, fix) +- } - -- prevLine := prevStmt(c.pos, c.path) -- if prevLine == nil { -- return -- } +- for _, fixes := range vulnFixes { +- fixes = mod.SelectUpgradeCodeActions(fixes) +- actions = append(actions, fixes...) +- } - -- // Make sure our preceding statement was as assignment. -- assign, _ := prevLine.(*ast.AssignStmt) -- if assign == nil || len(assign.Lhs) == 0 { -- return -- } +- return actions, nil - -- lastAssignee := assign.Lhs[len(assign.Lhs)-1] +- case file.Go: +- // Don't suggest fixes for generated files, since they are generally +- // not useful and some editors may apply them automatically on save. +- if golang.IsGenerated(ctx, snapshot, uri) { +- return nil, nil +- } - -- // Make sure the final assignee is an error. -- if !types.Identical(c.pkg.GetTypesInfo().TypeOf(lastAssignee), errorType) { -- return -- } +- actions, err := s.codeActionsMatchingDiagnostics(ctx, uri, snapshot, params.Context.Diagnostics, want) +- if err != nil { +- return nil, err +- } - -- var ( -- // errVar is e.g. "err" in "foo, err := bar()". -- errVar = source.FormatNode(c.pkg.FileSet(), lastAssignee) +- moreActions, err := golang.CodeActions(ctx, snapshot, fh, params.Range, params.Context.Diagnostics, want) +- if err != nil { +- return nil, err +- } +- actions = append(actions, moreActions...) - -- // Whether we need to include the "if" keyword in our candidate. -- needsIf = true -- ) +- return actions, nil - -- // If the returned error from the previous statement is "_", it is not a real object. -- // If we don't have an error, and the function signature takes a testing.TB that is either ignored -- // or an "_", then we also can't call t.Fatal(err). -- if errVar == "_" { -- return +- default: +- // Unsupported file kind for a code action. +- return nil, nil - } +-} - -- // Below we try to detect if the user has already started typing "if -- // err" so we can replace what they've typed with our complete -- // statement. -- switch n := c.path[0].(type) { -- case *ast.Ident: -- switch c.path[1].(type) { -- case *ast.ExprStmt: -- // This handles: -- // -- // f, err := os.Open("foo") -- // i<> -- -- // Make sure they are typing "if". -- if c.matcher.Score("if") <= 0 { -- return -- } -- case *ast.IfStmt: -- // This handles: -- // -- // f, err := os.Open("foo") -- // if er<> +-// ResolveCodeAction resolves missing Edit information (that is, computes the +-// details of the necessary patch) in the given code action using the provided +-// Data field of the CodeAction, which should contain the raw json of a protocol.Command. +-// +-// This should be called by the client before applying code actions, when the +-// client has code action resolve support. +-// +-// This feature allows capable clients to preview and selectively apply the diff +-// instead of applying the whole thing unconditionally through workspace/applyEdit. +-func (s *server) ResolveCodeAction(ctx context.Context, ca *protocol.CodeAction) (*protocol.CodeAction, error) { +- ctx, done := event.Start(ctx, "lsp.Server.resolveCodeAction") +- defer done() - -- // Make sure they are typing the error's name. -- if c.matcher.Score(errVar) <= 0 { -- return -- } +- // Only resolve the code action if there is Data provided. +- var cmd protocol.Command +- if ca.Data != nil { +- if err := protocol.UnmarshalJSON(*ca.Data, &cmd); err != nil { +- return nil, err +- } +- } +- if cmd.Command != "" { +- params := &protocol.ExecuteCommandParams{ +- Command: cmd.Command, +- Arguments: cmd.Arguments, +- } - -- needsIf = false -- default: -- return +- handler := &commandHandler{ +- s: s, +- params: params, - } -- case *ast.IfStmt: -- // This handles: -- // -- // f, err := os.Open("foo") -- // if <> +- edit, err := command.Dispatch(ctx, params, handler) +- if err != nil { - -- // Avoid false positives by ensuring the if's cond is a bad -- // expression. For example, don't offer the completion in cases -- // like "if <> somethingElse". -- if _, bad := n.Cond.(*ast.BadExpr); !bad { -- return +- return nil, err +- } +- var ok bool +- if ca.Edit, ok = edit.(*protocol.WorkspaceEdit); !ok { +- return nil, fmt.Errorf("unable to resolve code action %q", ca.Title) - } +- } +- return ca, nil +-} - -- // If "if" is our direct prefix, we need to include it in our -- // candidate since the existing "if" will be overwritten. -- needsIf = c.pos == n.Pos()+token.Pos(len("if")) +-// codeActionsMatchingDiagnostics fetches code actions for the provided +-// diagnostics, by first attempting to unmarshal code actions directly from the +-// bundled protocol.Diagnostic.Data field, and failing that by falling back on +-// fetching a matching Diagnostic from the set of stored diagnostics for +-// this file. +-func (s *server) codeActionsMatchingDiagnostics(ctx context.Context, uri protocol.DocumentURI, snapshot *cache.Snapshot, pds []protocol.Diagnostic, want map[protocol.CodeActionKind]bool) ([]protocol.CodeAction, error) { +- var actions []protocol.CodeAction +- var unbundled []protocol.Diagnostic // diagnostics without bundled code actions in their Data field +- for _, pd := range pds { +- bundled := cache.BundledQuickFixes(pd) +- if len(bundled) > 0 { +- for _, fix := range bundled { +- if want[fix.Kind] { +- actions = append(actions, fix) +- } +- } +- } else { +- // No bundled actions: keep searching for a match. +- unbundled = append(unbundled, pd) +- } - } - -- // Build up a snippet that looks like: -- // -- // if err != nil { -- // return , ..., ${1:err} -- // } -- // -- // We make the error a placeholder so it is easy to alter the error. -- var snip snippet.Builder -- if needsIf { -- snip.WriteText("if ") +- for _, pd := range unbundled { +- for _, sd := range s.findMatchingDiagnostics(uri, pd) { +- diagActions, err := codeActionsForDiagnostic(ctx, snapshot, sd, &pd, want) +- if err != nil { +- return nil, err +- } +- actions = append(actions, diagActions...) +- } - } -- snip.WriteText(fmt.Sprintf("%s != nil {\n\t", errVar)) +- return actions, nil +-} - -- var label string -- if isTest { -- snip.WriteText(fmt.Sprintf("%s.Fatal(%s)", testVar, errVar)) -- label = fmt.Sprintf("%[1]s != nil { %[2]s.Fatal(%[1]s) }", errVar, testVar) -- } else { -- snip.WriteText("return ") -- for i := 0; i < result.Len()-1; i++ { -- snip.WriteText(formatZeroValue(result.At(i).Type(), c.qf)) -- snip.WriteText(", ") +-func codeActionsForDiagnostic(ctx context.Context, snapshot *cache.Snapshot, sd *cache.Diagnostic, pd *protocol.Diagnostic, want map[protocol.CodeActionKind]bool) ([]protocol.CodeAction, error) { +- var actions []protocol.CodeAction +- for _, fix := range sd.SuggestedFixes { +- if !want[fix.ActionKind] { +- continue - } -- snip.WritePlaceholder(func(b *snippet.Builder) { -- b.WriteText(errVar) +- changes := []protocol.DocumentChanges{} // must be a slice +- for uri, edits := range fix.Edits { +- fh, err := snapshot.ReadFile(ctx, uri) +- if err != nil { +- return nil, err +- } +- changes = append(changes, documentChanges(fh, edits)...) +- } +- actions = append(actions, protocol.CodeAction{ +- Title: fix.Title, +- Kind: fix.ActionKind, +- Edit: &protocol.WorkspaceEdit{ +- DocumentChanges: changes, +- }, +- Command: fix.Command, +- Diagnostics: []protocol.Diagnostic{*pd}, - }) -- label = fmt.Sprintf("%[1]s != nil { return %[1]s }", errVar) - } +- return actions, nil +-} - -- snip.WriteText("\n}") -- -- if needsIf { -- label = "if " + label -- } +-func (s *server) findMatchingDiagnostics(uri protocol.DocumentURI, pd protocol.Diagnostic) []*cache.Diagnostic { +- s.diagnosticsMu.Lock() +- defer s.diagnosticsMu.Unlock() - -- c.items = append(c.items, CompletionItem{ -- Label: label, -- Kind: protocol.SnippetCompletion, -- Score: highScore, -- snippet: &snip, -- }) --} +- var sds []*cache.Diagnostic +- for _, viewDiags := range s.diagnostics[uri].byView { +- for _, sd := range viewDiags.diagnostics { +- sameDiagnostic := (pd.Message == strings.TrimSpace(sd.Message) && // extra space may have been trimmed when converting to protocol.Diagnostic +- protocol.CompareRange(pd.Range, sd.Range) == 0 && +- pd.Source == string(sd.Source)) - --// getTestVar checks the function signature's input parameters and returns --// the name of the first parameter that implements "testing.TB". For example, --// func someFunc(t *testing.T) returns the string "t", func someFunc(b *testing.B) --// returns "b" etc. An empty string indicates that the function signature --// does not take a testing.TB parameter or does so but is ignored such --// as func someFunc(*testing.T). --func getTestVar(enclosingFunc *funcInfo, pkg source.Package) string { -- if enclosingFunc == nil || enclosingFunc.sig == nil { -- return "" +- if sameDiagnostic { +- sds = append(sds, sd) +- } +- } - } +- return sds +-} - -- var testingPkg *types.Package -- for _, p := range pkg.GetTypes().Imports() { -- if p.Path() == "testing" { -- testingPkg = p -- break +-func (s *server) getSupportedCodeActions() []protocol.CodeActionKind { +- allCodeActionKinds := make(map[protocol.CodeActionKind]struct{}) +- for _, kinds := range s.Options().SupportedCodeActions { +- for kind := range kinds { +- allCodeActionKinds[kind] = struct{}{} - } - } -- if testingPkg == nil { -- return "" -- } -- tbObj := testingPkg.Scope().Lookup("TB") -- if tbObj == nil { -- return "" -- } -- iface, ok := tbObj.Type().Underlying().(*types.Interface) -- if !ok { -- return "" +- var result []protocol.CodeActionKind +- for kind := range allCodeActionKinds { +- result = append(result, kind) - } +- sort.Slice(result, func(i, j int) bool { +- return result[i] < result[j] +- }) +- return result +-} - -- sig := enclosingFunc.sig -- for i := 0; i < sig.Params().Len(); i++ { -- param := sig.Params().At(i) -- if param.Name() == "_" { -- continue -- } -- if !types.Implements(param.Type(), iface) { -- continue -- } -- return param.Name() -- } +-type unit = struct{} - -- return "" +-func documentChanges(fh file.Handle, edits []protocol.TextEdit) []protocol.DocumentChanges { +- return protocol.TextEditsToDocumentChanges(fh.URI(), fh.Version(), edits) -} -diff -urN a/gopls/internal/lsp/source/completion/util.go b/gopls/internal/lsp/source/completion/util.go ---- a/gopls/internal/lsp/source/completion/util.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/completion/util.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,344 +0,0 @@ +diff -urN a/gopls/internal/server/code_lens.go b/gopls/internal/server/code_lens.go +--- a/gopls/internal/server/code_lens.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/code_lens.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,63 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package completion +-package server - -import ( -- "go/ast" -- "go/token" -- "go/types" +- "context" +- "fmt" +- "sort" - -- "golang.org/x/tools/go/types/typeutil" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/internal/diff" -- "golang.org/x/tools/internal/typeparams" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/mod" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/command" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/tag" -) - --// exprAtPos returns the index of the expression containing pos. --func exprAtPos(pos token.Pos, args []ast.Expr) int { -- for i, expr := range args { -- if expr.Pos() <= pos && pos <= expr.End() { -- return i -- } -- } -- return len(args) --} -- --// eachField invokes fn for each field that can be selected from a --// value of type T. --func eachField(T types.Type, fn func(*types.Var)) { -- // TODO(adonovan): this algorithm doesn't exclude ambiguous -- // selections that match more than one field/method. -- // types.NewSelectionSet should do that for us. -- -- // for termination on recursive types -- var seen typeutil.Map -- -- var visit func(T types.Type) -- visit = func(T types.Type) { -- if T, ok := source.Deref(T).Underlying().(*types.Struct); ok { -- if seen.At(T) != nil { -- return -- } +-func (s *server) CodeLens(ctx context.Context, params *protocol.CodeLensParams) ([]protocol.CodeLens, error) { +- ctx, done := event.Start(ctx, "lsp.Server.codeLens", tag.URI.Of(params.TextDocument.URI)) +- defer done() - -- for i := 0; i < T.NumFields(); i++ { -- f := T.Field(i) -- fn(f) -- if f.Anonymous() { -- seen.Set(T, true) -- visit(f.Type()) -- } -- } -- } +- fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI) +- if err != nil { +- return nil, err - } -- visit(T) --} +- defer release() - --// typeIsValid reports whether typ doesn't contain any Invalid types. --func typeIsValid(typ types.Type) bool { -- // Check named types separately, because we don't want -- // to call Underlying() on them to avoid problems with recursive types. -- if _, ok := typ.(*types.Named); ok { -- return true +- var lenses map[command.Command]golang.LensFunc +- switch snapshot.FileKind(fh) { +- case file.Mod: +- lenses = mod.LensFuncs() +- case file.Go: +- lenses = golang.LensFuncs() +- default: +- // Unsupported file kind for a code lens. +- return nil, nil - } -- -- switch typ := typ.Underlying().(type) { -- case *types.Basic: -- return typ.Kind() != types.Invalid -- case *types.Array: -- return typeIsValid(typ.Elem()) -- case *types.Slice: -- return typeIsValid(typ.Elem()) -- case *types.Pointer: -- return typeIsValid(typ.Elem()) -- case *types.Map: -- return typeIsValid(typ.Key()) && typeIsValid(typ.Elem()) -- case *types.Chan: -- return typeIsValid(typ.Elem()) -- case *types.Signature: -- return typeIsValid(typ.Params()) && typeIsValid(typ.Results()) -- case *types.Tuple: -- for i := 0; i < typ.Len(); i++ { -- if !typeIsValid(typ.At(i).Type()) { -- return false -- } +- var result []protocol.CodeLens +- for cmd, lf := range lenses { +- if !snapshot.Options().Codelenses[string(cmd)] { +- continue - } -- return true -- case *types.Struct, *types.Interface: -- // Don't bother checking structs, interfaces for validity. -- return true -- default: -- return false +- added, err := lf(ctx, snapshot, fh) +- // Code lens is called on every keystroke, so we should just operate in +- // a best-effort mode, ignoring errors. +- if err != nil { +- event.Error(ctx, fmt.Sprintf("code lens %s failed", cmd), err) +- continue +- } +- result = append(result, added...) - } --} -- --// resolveInvalid traverses the node of the AST that defines the scope --// containing the declaration of obj, and attempts to find a user-friendly --// name for its invalid type. The resulting Object and its Type are fake. --func resolveInvalid(fset *token.FileSet, obj types.Object, node ast.Node, info *types.Info) types.Object { -- var resultExpr ast.Expr -- ast.Inspect(node, func(node ast.Node) bool { -- switch n := node.(type) { -- case *ast.ValueSpec: -- for _, name := range n.Names { -- if info.Defs[name] == obj { -- resultExpr = n.Type -- } -- } -- return false -- case *ast.Field: // This case handles parameters and results of a FuncDecl or FuncLit. -- for _, name := range n.Names { -- if info.Defs[name] == obj { -- resultExpr = n.Type -- } -- } -- return false -- default: -- return true +- sort.Slice(result, func(i, j int) bool { +- a, b := result[i], result[j] +- if cmp := protocol.CompareRange(a.Range, b.Range); cmp != 0 { +- return cmp < 0 - } +- return a.Command.Command < b.Command.Command - }) -- // Construct a fake type for the object and return a fake object with this type. -- typename := source.FormatNode(fset, resultExpr) -- typ := types.NewNamed(types.NewTypeName(token.NoPos, obj.Pkg(), typename, nil), types.Typ[types.Invalid], nil) -- return types.NewVar(obj.Pos(), obj.Pkg(), obj.Name(), typ) --} -- --func isPointer(T types.Type) bool { -- _, ok := T.(*types.Pointer) -- return ok +- return result, nil -} +diff -urN a/gopls/internal/server/command.go b/gopls/internal/server/command.go +--- a/gopls/internal/server/command.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/command.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,1431 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func isVar(obj types.Object) bool { -- _, ok := obj.(*types.Var) -- return ok --} +-package server - --func isTypeName(obj types.Object) bool { -- _, ok := obj.(*types.TypeName) -- return ok --} +-import ( +- "bytes" +- "context" +- "encoding/json" +- "errors" +- "fmt" +- "go/ast" +- "io" +- "os" +- "path/filepath" +- "regexp" +- "runtime" +- "runtime/pprof" +- "sort" +- "strings" +- "sync" - --func isFunc(obj types.Object) bool { -- _, ok := obj.(*types.Func) -- return ok --} +- "golang.org/x/mod/modfile" +- "golang.org/x/telemetry/counter" +- "golang.org/x/tools/go/ast/astutil" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/debug" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/progress" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/command" +- "golang.org/x/tools/gopls/internal/settings" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/gopls/internal/vulncheck" +- "golang.org/x/tools/gopls/internal/vulncheck/scan" +- "golang.org/x/tools/internal/diff" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/gocommand" +- "golang.org/x/tools/internal/tokeninternal" +- "golang.org/x/tools/internal/xcontext" +-) - --func isEmptyInterface(T types.Type) bool { -- intf, _ := T.(*types.Interface) -- return intf != nil && intf.NumMethods() == 0 && typeparams.IsMethodSet(intf) --} +-func (s *server) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) { +- ctx, done := event.Start(ctx, "lsp.Server.executeCommand") +- defer done() - --func isUntyped(T types.Type) bool { -- if basic, ok := T.(*types.Basic); ok { -- return basic.Info()&types.IsUntyped > 0 +- var found bool +- for _, name := range s.Options().SupportedCommands { +- if name == params.Command { +- found = true +- break +- } +- } +- if !found { +- return nil, fmt.Errorf("%s is not a supported command", params.Command) - } -- return false --} - --func isPkgName(obj types.Object) bool { -- _, ok := obj.(*types.PkgName) -- return ok +- handler := &commandHandler{ +- s: s, +- params: params, +- } +- return command.Dispatch(ctx, params, handler) -} - --func isASTFile(n ast.Node) bool { -- _, ok := n.(*ast.File) -- return ok +-type commandHandler struct { +- s *server +- params *protocol.ExecuteCommandParams -} - --func deslice(T types.Type) types.Type { -- if slice, ok := T.Underlying().(*types.Slice); ok { -- return slice.Elem() -- } +-func (h *commandHandler) MaybePromptForTelemetry(ctx context.Context) error { +- go h.s.maybePromptForTelemetry(ctx, true) - return nil -} - --// isSelector returns the enclosing *ast.SelectorExpr when pos is in the --// selector. --func enclosingSelector(path []ast.Node, pos token.Pos) *ast.SelectorExpr { -- if len(path) == 0 { -- return nil -- } -- -- if sel, ok := path[0].(*ast.SelectorExpr); ok { -- return sel +-func (*commandHandler) AddTelemetryCounters(_ context.Context, args command.AddTelemetryCountersArgs) error { +- if len(args.Names) != len(args.Values) { +- return fmt.Errorf("Names and Values must have the same length") - } -- -- if _, ok := path[0].(*ast.Ident); ok && len(path) > 1 { -- if sel, ok := path[1].(*ast.SelectorExpr); ok && pos >= sel.Sel.Pos() { -- return sel +- // invalid counter update requests will be silently dropped. (no audience) +- for i, n := range args.Names { +- v := args.Values[i] +- if n == "" || v < 0 { +- continue - } +- counter.Add("fwd/"+n, v) - } -- - return nil -} - --// enclosingDeclLHS returns LHS idents from containing value spec or --// assign statement. --func enclosingDeclLHS(path []ast.Node) []*ast.Ident { -- for _, n := range path { -- switch n := n.(type) { -- case *ast.ValueSpec: -- return n.Names -- case *ast.AssignStmt: -- ids := make([]*ast.Ident, 0, len(n.Lhs)) -- for _, e := range n.Lhs { -- if id, ok := e.(*ast.Ident); ok { -- ids = append(ids, id) -- } -- } -- return ids -- } -- } +-// commandConfig configures common command set-up and execution. +-type commandConfig struct { +- // TODO(adonovan): whether a command is synchronous or +- // asynchronous is part of the server interface contract, not +- // a mere implementation detail of the handler. +- // Export a (command.Command).IsAsync() property so that +- // clients can tell. (The tricky part is ensuring the handler +- // remains consistent with the command.Command metadata, as at +- // the point were we read the 'async' field below, we no +- // longer know that command.Command.) - -- return nil +- async bool // whether to run the command asynchronously. Async commands can only return errors. +- requireSave bool // whether all files must be saved for the command to work +- progress string // title to use for progress reporting. If empty, no progress will be reported. +- forView string // view to resolve to a snapshot; incompatible with forURI +- forURI protocol.DocumentURI // URI to resolve to a snapshot. If unset, snapshot will be nil. -} - --// exprObj returns the types.Object associated with the *ast.Ident or --// *ast.SelectorExpr e. --func exprObj(info *types.Info, e ast.Expr) types.Object { -- var ident *ast.Ident -- switch expr := e.(type) { -- case *ast.Ident: -- ident = expr -- case *ast.SelectorExpr: -- ident = expr.Sel -- default: -- return nil -- } -- -- return info.ObjectOf(ident) +-// commandDeps is evaluated from a commandConfig. Note that not all fields may +-// be populated, depending on which configuration is set. See comments in-line +-// for details. +-type commandDeps struct { +- snapshot *cache.Snapshot // present if cfg.forURI was set +- fh file.Handle // present if cfg.forURI was set +- work *progress.WorkDone // present cfg.progress was set -} - --// typeConversion returns the type being converted to if call is a type --// conversion expression. --func typeConversion(call *ast.CallExpr, info *types.Info) types.Type { -- // Type conversion (e.g. "float64(foo)"). -- if fun, _ := exprObj(info, call.Fun).(*types.TypeName); fun != nil { -- return fun.Type() -- } +-type commandFunc func(context.Context, commandDeps) error - -- return nil --} +-// These strings are reported as the final WorkDoneProgressEnd message +-// for each workspace/executeCommand request. +-const ( +- CommandCanceled = "canceled" +- CommandFailed = "failed" +- CommandCompleted = "completed" +-) - --// fieldsAccessible returns whether s has at least one field accessible by p. --func fieldsAccessible(s *types.Struct, p *types.Package) bool { -- for i := 0; i < s.NumFields(); i++ { -- f := s.Field(i) -- if f.Exported() || f.Pkg() == p { -- return true +-// run performs command setup for command execution, and invokes the given run +-// function. If cfg.async is set, run executes the given func in a separate +-// goroutine, and returns as soon as setup is complete and the goroutine is +-// scheduled. +-// +-// Invariant: if the resulting error is non-nil, the given run func will +-// (eventually) be executed exactly once. +-func (c *commandHandler) run(ctx context.Context, cfg commandConfig, run commandFunc) (err error) { +- if cfg.requireSave { +- var unsaved []string +- for _, overlay := range c.s.session.Overlays() { +- if !overlay.SameContentsOnDisk() { +- unsaved = append(unsaved, overlay.URI().Path()) +- } +- } +- if len(unsaved) > 0 { +- return fmt.Errorf("All files must be saved first (unsaved: %v).", unsaved) - } - } -- return false --} +- var deps commandDeps +- var release func() +- if cfg.forURI != "" && cfg.forView != "" { +- return bug.Errorf("internal error: forURI=%q, forView=%q", cfg.forURI, cfg.forView) +- } +- if cfg.forURI != "" { +- deps.fh, deps.snapshot, release, err = c.s.fileOf(ctx, cfg.forURI) +- if err != nil { +- return err +- } - --// prevStmt returns the statement that precedes the statement containing pos. --// For example: --// --// foo := 1 --// bar(1 + 2<>) --// --// If "<>" is pos, prevStmt returns "foo := 1" --func prevStmt(pos token.Pos, path []ast.Node) ast.Stmt { -- var blockLines []ast.Stmt -- for i := 0; i < len(path) && blockLines == nil; i++ { -- switch n := path[i].(type) { -- case *ast.BlockStmt: -- blockLines = n.List -- case *ast.CommClause: -- blockLines = n.Body -- case *ast.CaseClause: -- blockLines = n.Body +- } else if cfg.forView != "" { +- view, err := c.s.session.View(cfg.forView) +- if err != nil { +- return err +- } +- deps.snapshot, release, err = view.Snapshot() +- if err != nil { +- return err - } +- +- } else { +- release = func() {} - } +- // Inv: release() must be called exactly once after this point. +- // In the async case, runcmd may outlive run(). - -- for i := len(blockLines) - 1; i >= 0; i-- { -- if blockLines[i].End() < pos { -- return blockLines[i] +- ctx, cancel := context.WithCancel(xcontext.Detach(ctx)) +- if cfg.progress != "" { +- deps.work = c.s.progress.Start(ctx, cfg.progress, "Running...", c.params.WorkDoneToken, cancel) +- } +- runcmd := func() error { +- defer release() +- defer cancel() +- err := run(ctx, deps) +- if deps.work != nil { +- switch { +- case errors.Is(err, context.Canceled): +- deps.work.End(ctx, CommandCanceled) +- case err != nil: +- event.Error(ctx, "command error", err) +- deps.work.End(ctx, CommandFailed) +- default: +- deps.work.End(ctx, CommandCompleted) +- } - } +- return err - } -- -- return nil +- if cfg.async { +- go func() { +- if err := runcmd(); err != nil { +- showMessage(ctx, c.s.client, protocol.Error, err.Error()) +- } +- }() +- return nil +- } +- return runcmd() -} - --// formatZeroValue produces Go code representing the zero value of T. It --// returns the empty string if T is invalid. --func formatZeroValue(T types.Type, qf types.Qualifier) string { -- switch u := T.Underlying().(type) { -- case *types.Basic: -- switch { -- case u.Info()&types.IsNumeric > 0: -- return "0" -- case u.Info()&types.IsString > 0: -- return `""` -- case u.Info()&types.IsBoolean > 0: -- return "false" -- default: -- return "" +-func (c *commandHandler) ApplyFix(ctx context.Context, args command.ApplyFixArgs) (*protocol.WorkspaceEdit, error) { +- var result *protocol.WorkspaceEdit +- err := c.run(ctx, commandConfig{ +- // Note: no progress here. Applying fixes should be quick. +- forURI: args.URI, +- }, func(ctx context.Context, deps commandDeps) error { +- edits, err := golang.ApplyFix(ctx, args.Fix, deps.snapshot, deps.fh, args.Range) +- if err != nil { +- return err - } -- case *types.Pointer, *types.Interface, *types.Chan, *types.Map, *types.Slice, *types.Signature: -- return "nil" -- default: -- return types.TypeString(T, qf) + "{}" -- } +- changes := []protocol.DocumentChanges{} // must be a slice +- for _, edit := range edits { +- edit := edit +- changes = append(changes, protocol.DocumentChanges{ +- TextDocumentEdit: &edit, +- }) +- } +- edit := protocol.WorkspaceEdit{ +- DocumentChanges: changes, +- } +- if args.ResolveEdits { +- result = &edit +- return nil +- } +- r, err := c.s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{ +- Edit: edit, +- }) +- if err != nil { +- return err +- } +- if !r.Applied { +- return errors.New(r.FailureReason) +- } +- return nil +- }) +- return result, err -} - --// isBasicKind returns whether t is a basic type of kind k. --func isBasicKind(t types.Type, k types.BasicInfo) bool { -- b, _ := t.Underlying().(*types.Basic) -- return b != nil && b.Info()&k > 0 +-func (c *commandHandler) RegenerateCgo(ctx context.Context, args command.URIArg) error { +- return c.run(ctx, commandConfig{ +- progress: "Regenerating Cgo", +- }, func(ctx context.Context, _ commandDeps) error { +- return c.modifyState(ctx, FromRegenerateCgo, func() (*cache.Snapshot, func(), error) { +- // Resetting the view causes cgo to be regenerated via `go list`. +- v, err := c.s.session.ResetView(ctx, args.URI) +- if err != nil { +- return nil, nil, err +- } +- return v.Snapshot() +- }) +- }) -} - --func (c *completer) editText(from, to token.Pos, newText string) ([]protocol.TextEdit, error) { -- start, end, err := safetoken.Offsets(c.tokFile, from, to) +-// modifyState performs an operation that modifies the snapshot state. +-// +-// It causes a snapshot diagnosis for the provided ModificationSource. +-func (c *commandHandler) modifyState(ctx context.Context, source ModificationSource, work func() (*cache.Snapshot, func(), error)) error { +- var wg sync.WaitGroup // tracks work done on behalf of this function, incl. diagnostics +- wg.Add(1) +- defer wg.Done() +- +- // Track progress on this operation for testing. +- if c.s.Options().VerboseWorkDoneProgress { +- work := c.s.progress.Start(ctx, DiagnosticWorkTitle(source), "Calculating file diagnostics...", nil, nil) +- go func() { +- wg.Wait() +- work.End(ctx, "Done.") +- }() +- } +- snapshot, release, err := work() - if err != nil { -- return nil, err // can't happen: from/to came from c +- return err - } -- return source.ToProtocolEdits(c.mapper, []diff.Edit{{ -- Start: start, -- End: end, -- New: newText, -- }}) +- wg.Add(1) +- go func() { +- c.s.diagnoseSnapshot(snapshot, nil, 0) +- release() +- wg.Done() +- }() +- return nil -} - --// assignableTo is like types.AssignableTo, but returns false if --// either type is invalid. --func assignableTo(x, to types.Type) bool { -- if x == types.Typ[types.Invalid] || to == types.Typ[types.Invalid] { -- return false -- } -- -- return types.AssignableTo(x, to) +-func (c *commandHandler) CheckUpgrades(ctx context.Context, args command.CheckUpgradesArgs) error { +- return c.run(ctx, commandConfig{ +- forURI: args.URI, +- progress: "Checking for upgrades", +- }, func(ctx context.Context, deps commandDeps) error { +- return c.modifyState(ctx, FromCheckUpgrades, func() (*cache.Snapshot, func(), error) { +- upgrades, err := c.s.getUpgrades(ctx, deps.snapshot, args.URI, args.Modules) +- if err != nil { +- return nil, nil, err +- } +- return c.s.session.InvalidateView(ctx, deps.snapshot.View(), cache.StateChange{ +- ModuleUpgrades: map[protocol.DocumentURI]map[string]string{args.URI: upgrades}, +- }) +- }) +- }) -} - --// convertibleTo is like types.ConvertibleTo, but returns false if --// either type is invalid. --func convertibleTo(x, to types.Type) bool { -- if x == types.Typ[types.Invalid] || to == types.Typ[types.Invalid] { -- return false -- } -- -- return types.ConvertibleTo(x, to) +-func (c *commandHandler) AddDependency(ctx context.Context, args command.DependencyArgs) error { +- return c.GoGetModule(ctx, args) -} -diff -urN a/gopls/internal/lsp/source/completion/util_test.go b/gopls/internal/lsp/source/completion/util_test.go ---- a/gopls/internal/lsp/source/completion/util_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/completion/util_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,28 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package completion +-func (c *commandHandler) UpgradeDependency(ctx context.Context, args command.DependencyArgs) error { +- return c.GoGetModule(ctx, args) +-} - --import ( -- "go/types" -- "testing" --) +-func (c *commandHandler) ResetGoModDiagnostics(ctx context.Context, args command.ResetGoModDiagnosticsArgs) error { +- return c.run(ctx, commandConfig{ +- forURI: args.URI, +- }, func(ctx context.Context, deps commandDeps) error { +- return c.modifyState(ctx, FromResetGoModDiagnostics, func() (*cache.Snapshot, func(), error) { +- return c.s.session.InvalidateView(ctx, deps.snapshot.View(), cache.StateChange{ +- ModuleUpgrades: map[protocol.DocumentURI]map[string]string{ +- deps.fh.URI(): nil, +- }, +- Vulns: map[protocol.DocumentURI]*vulncheck.Result{ +- deps.fh.URI(): nil, +- }, +- }) +- }) +- }) +-} - --func TestFormatZeroValue(t *testing.T) { -- tests := []struct { -- typ types.Type -- want string -- }{ -- {types.Typ[types.String], `""`}, -- {types.Typ[types.Byte], "0"}, -- {types.Typ[types.Invalid], ""}, -- {types.Universe.Lookup("error").Type(), "nil"}, -- } +-func (c *commandHandler) GoGetModule(ctx context.Context, args command.DependencyArgs) error { +- return c.run(ctx, commandConfig{ +- progress: "Running go get", +- forURI: args.URI, +- }, func(ctx context.Context, deps commandDeps) error { +- return c.s.runGoModUpdateCommands(ctx, deps.snapshot, args.URI, func(invoke func(...string) (*bytes.Buffer, error)) error { +- return runGoGetModule(invoke, args.AddRequire, args.GoCmdArgs) +- }) +- }) +-} - -- for _, test := range tests { -- if got := formatZeroValue(test.typ, nil); got != test.want { -- t.Errorf("formatZeroValue(%v) = %q, want %q", test.typ, got, test.want) +-// TODO(rFindley): UpdateGoSum, Tidy, and Vendor could probably all be one command. +-func (c *commandHandler) UpdateGoSum(ctx context.Context, args command.URIArgs) error { +- return c.run(ctx, commandConfig{ +- progress: "Updating go.sum", +- }, func(ctx context.Context, _ commandDeps) error { +- for _, uri := range args.URIs { +- fh, snapshot, release, err := c.s.fileOf(ctx, uri) +- if err != nil { +- return err +- } +- defer release() +- if err := c.s.runGoModUpdateCommands(ctx, snapshot, fh.URI(), func(invoke func(...string) (*bytes.Buffer, error)) error { +- _, err := invoke("list", "all") +- return err +- }); err != nil { +- return err +- } - } -- } +- return nil +- }) -} -diff -urN a/gopls/internal/lsp/source/definition.go b/gopls/internal/lsp/source/definition.go ---- a/gopls/internal/lsp/source/definition.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/definition.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,261 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package source +-func (c *commandHandler) Tidy(ctx context.Context, args command.URIArgs) error { +- return c.run(ctx, commandConfig{ +- requireSave: true, +- progress: "Running go mod tidy", +- }, func(ctx context.Context, _ commandDeps) error { +- for _, uri := range args.URIs { +- fh, snapshot, release, err := c.s.fileOf(ctx, uri) +- if err != nil { +- return err +- } +- defer release() +- if err := c.s.runGoModUpdateCommands(ctx, snapshot, fh.URI(), func(invoke func(...string) (*bytes.Buffer, error)) error { +- _, err := invoke("mod", "tidy") +- return err +- }); err != nil { +- return err +- } +- } +- return nil +- }) +-} - --import ( -- "context" -- "errors" -- "fmt" -- "go/ast" -- "go/token" -- "go/types" +-func (c *commandHandler) Vendor(ctx context.Context, args command.URIArg) error { +- return c.run(ctx, commandConfig{ +- requireSave: true, +- progress: "Running go mod vendor", +- forURI: args.URI, +- }, func(ctx context.Context, deps commandDeps) error { +- // Use RunGoCommandPiped here so that we don't compete with any other go +- // command invocations. go mod vendor deletes modules.txt before recreating +- // it, and therefore can run into file locking issues on Windows if that +- // file is in use by another process, such as go list. +- // +- // If golang/go#44119 is resolved, go mod vendor will instead modify +- // modules.txt in-place. In that case we could theoretically allow this +- // command to run concurrently. +- stderr := new(bytes.Buffer) +- err := deps.snapshot.RunGoCommandPiped(ctx, cache.Normal|cache.AllowNetwork, &gocommand.Invocation{ +- Verb: "mod", +- Args: []string{"vendor"}, +- WorkingDir: filepath.Dir(args.URI.Path()), +- }, &bytes.Buffer{}, stderr) +- if err != nil { +- return fmt.Errorf("running go mod vendor failed: %v\nstderr:\n%s", err, stderr.String()) +- } +- return nil +- }) +-} - -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/event" --) +-func (c *commandHandler) EditGoDirective(ctx context.Context, args command.EditGoDirectiveArgs) error { +- return c.run(ctx, commandConfig{ +- requireSave: true, // if go.mod isn't saved it could cause a problem +- forURI: args.URI, +- }, func(ctx context.Context, _ commandDeps) error { +- fh, snapshot, release, err := c.s.fileOf(ctx, args.URI) +- if err != nil { +- return err +- } +- defer release() +- if err := c.s.runGoModUpdateCommands(ctx, snapshot, fh.URI(), func(invoke func(...string) (*bytes.Buffer, error)) error { +- _, err := invoke("mod", "edit", "-go", args.Version) +- return err +- }); err != nil { +- return err +- } +- return nil +- }) +-} - --// Definition handles the textDocument/definition request for Go files. --func Definition(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) ([]protocol.Location, error) { -- ctx, done := event.Start(ctx, "source.Definition") -- defer done() +-func (c *commandHandler) RemoveDependency(ctx context.Context, args command.RemoveDependencyArgs) error { +- return c.run(ctx, commandConfig{ +- progress: "Removing dependency", +- forURI: args.URI, +- }, func(ctx context.Context, deps commandDeps) error { +- // See the documentation for OnlyDiagnostic. +- // +- // TODO(rfindley): In Go 1.17+, we will be able to use the go command +- // without checking if the module is tidy. +- if args.OnlyDiagnostic { +- return c.s.runGoModUpdateCommands(ctx, deps.snapshot, args.URI, func(invoke func(...string) (*bytes.Buffer, error)) error { +- if err := runGoGetModule(invoke, false, []string{args.ModulePath + "@none"}); err != nil { +- return err +- } +- _, err := invoke("mod", "tidy") +- return err +- }) +- } +- pm, err := deps.snapshot.ParseMod(ctx, deps.fh) +- if err != nil { +- return err +- } +- edits, err := dropDependency(pm, args.ModulePath) +- if err != nil { +- return err +- } +- response, err := c.s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{ +- Edit: protocol.WorkspaceEdit{ +- DocumentChanges: documentChanges(deps.fh, edits), +- }, +- }) +- if err != nil { +- return err +- } +- if !response.Applied { +- return fmt.Errorf("edits not applied because of %s", response.FailureReason) +- } +- return nil +- }) +-} - -- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) +-// dropDependency returns the edits to remove the given require from the go.mod +-// file. +-func dropDependency(pm *cache.ParsedModule, modulePath string) ([]protocol.TextEdit, error) { +- // We need a private copy of the parsed go.mod file, since we're going to +- // modify it. +- copied, err := modfile.Parse("", pm.Mapper.Content, nil) - if err != nil { - return nil, err - } -- pos, err := pgf.PositionPos(position) -- if err != nil { +- if err := copied.DropRequire(modulePath); err != nil { - return nil, err - } -- -- // Handle the case where the cursor is in an import. -- importLocations, err := importDefinition(ctx, snapshot, pkg, pgf, pos) +- copied.Cleanup() +- newContent, err := copied.Format() - if err != nil { - return nil, err - } -- if len(importLocations) > 0 { -- return importLocations, nil -- } +- // Calculate the edits to be made due to the change. +- diff := diff.Bytes(pm.Mapper.Content, newContent) +- return protocol.EditsFromDiffEdits(pm.Mapper, diff) +-} - -- // Handle the case where the cursor is in the package name. -- // We use "<= End" to accept a query immediately after the package name. -- if pgf.File != nil && pgf.File.Name.Pos() <= pos && pos <= pgf.File.Name.End() { -- // If there's no package documentation, just use current file. -- declFile := pgf -- for _, pgf := range pkg.CompiledGoFiles() { -- if pgf.File.Name != nil && pgf.File.Doc != nil { -- declFile = pgf -- break -- } -- } -- loc, err := declFile.NodeLocation(declFile.File.Name) +-func (c *commandHandler) Test(ctx context.Context, uri protocol.DocumentURI, tests, benchmarks []string) error { +- return c.RunTests(ctx, command.RunTestsArgs{ +- URI: uri, +- Tests: tests, +- Benchmarks: benchmarks, +- }) +-} +- +-func (c *commandHandler) Doc(ctx context.Context, loc protocol.Location) error { +- return c.run(ctx, commandConfig{ +- progress: "", // the operation should be fast +- forURI: loc.URI, +- }, func(ctx context.Context, deps commandDeps) error { +- pkg, pgf, err := golang.NarrowestPackageForFile(ctx, deps.snapshot, loc.URI) - if err != nil { -- return nil, err +- return err - } -- return []protocol.Location{loc}, nil -- } -- -- // Handle the case where the cursor is in a linkname directive. -- locations, err := LinknameDefinition(ctx, snapshot, pgf.Mapper, position) -- if !errors.Is(err, ErrNoLinkname) { -- return locations, err -- } - -- // Handle the case where the cursor is in an embed directive. -- locations, err = EmbedDefinition(pgf.Mapper, position) -- if !errors.Is(err, ErrNoEmbed) { -- return locations, err -- } +- // When invoked from a _test.go file, show the +- // documentation of the package under test. +- pkgpath := pkg.Metadata().PkgPath +- if pkg.Metadata().ForTest != "" { +- pkgpath = pkg.Metadata().ForTest +- } - -- // The general case: the cursor is on an identifier. -- _, obj, _ := referencedObject(pkg, pgf, pos) -- if obj == nil { -- return nil, nil -- } +- // Start web server. +- web, err := c.s.getWeb() +- if err != nil { +- return err +- } - -- // Handle objects with no position: builtin, unsafe. -- if !obj.Pos().IsValid() { -- var pgf *ParsedGoFile -- if obj.Parent() == types.Universe { -- // pseudo-package "builtin" -- builtinPGF, err := snapshot.BuiltinFile(ctx) -- if err != nil { -- return nil, err -- } -- pgf = builtinPGF +- // Compute fragment (e.g. "#Buffer.Len") based on +- // enclosing top-level declaration, if exported. +- var fragment string +- pos, err := pgf.PositionPos(loc.Range.Start) +- if err != nil { +- return err +- } +- path, _ := astutil.PathEnclosingInterval(pgf.File, pos, pos) +- if n := len(path); n > 1 { +- switch decl := path[n-2].(type) { +- case *ast.FuncDecl: +- if decl.Name.IsExported() { +- // e.g. "#Println" +- fragment = decl.Name.Name +- +- // method? +- if decl.Recv != nil && len(decl.Recv.List) > 0 { +- recv := decl.Recv.List[0].Type +- if star, ok := recv.(*ast.StarExpr); ok { +- recv = star.X // *N -> N +- } +- if id, ok := recv.(*ast.Ident); ok && id.IsExported() { +- // e.g. "#Buffer.Len" +- fragment = id.Name + "." + fragment +- } else { +- fragment = "" +- } +- } +- } - -- } else if obj.Pkg() == types.Unsafe { -- // package "unsafe" -- unsafe := snapshot.Metadata("unsafe") -- if unsafe == nil { -- return nil, fmt.Errorf("no metadata for package 'unsafe'") -- } -- uri := unsafe.GoFiles[0] -- fh, err := snapshot.ReadFile(ctx, uri) -- if err != nil { -- return nil, err -- } -- pgf, err = snapshot.ParseGo(ctx, fh, ParseFull&^SkipObjectResolution) -- if err != nil { -- return nil, err +- case *ast.GenDecl: +- // path=[... Spec? GenDecl File] +- for _, spec := range decl.Specs { +- if n > 2 && spec == path[n-3] { +- var name *ast.Ident +- switch spec := spec.(type) { +- case *ast.ValueSpec: +- // var, const: use first name +- name = spec.Names[0] +- case *ast.TypeSpec: +- name = spec.Name +- } +- if name != nil && name.IsExported() { +- fragment = name.Name +- } +- break +- } +- } - } -- -- } else { -- return nil, bug.Errorf("internal error: no position for %v", obj.Name()) - } -- // Inv: pgf ∈ {builtin,unsafe}.go - -- // Use legacy (go/ast) object resolution. -- astObj := pgf.File.Scope.Lookup(obj.Name()) -- if astObj == nil { -- // Every built-in should have documentation syntax. -- return nil, bug.Errorf("internal error: no object for %s", obj.Name()) -- } -- decl, ok := astObj.Decl.(ast.Node) -- if !ok { -- return nil, bug.Errorf("internal error: no declaration for %s", obj.Name()) -- } -- loc, err := pgf.PosLocation(decl.Pos(), decl.Pos()+token.Pos(len(obj.Name()))) -- if err != nil { -- return nil, err -- } -- return []protocol.Location{loc}, nil -- } +- // Direct the client to open the /pkg page. +- url := web.pkgURL(deps.snapshot.View(), pkgpath, fragment) +- openClientBrowser(ctx, c.s.client, url) - -- // Finally, map the object position. -- loc, err := mapPosition(ctx, pkg.FileSet(), snapshot, obj.Pos(), adjustedObjEnd(obj)) -- if err != nil { -- return nil, err -- } -- return []protocol.Location{loc}, nil +- return nil +- }) +-} +- +-func (c *commandHandler) RunTests(ctx context.Context, args command.RunTestsArgs) error { +- return c.run(ctx, commandConfig{ +- async: true, +- progress: "Running go test", +- requireSave: true, +- forURI: args.URI, +- }, func(ctx context.Context, deps commandDeps) error { +- return c.runTests(ctx, deps.snapshot, deps.work, args.URI, args.Tests, args.Benchmarks) +- }) -} - --// referencedObject returns the identifier and object referenced at the --// specified position, which must be within the file pgf, for the purposes of --// definition/hover/call hierarchy operations. It returns a nil object if no --// object was found at the given position. --// --// If the returned identifier is a type-switch implicit (i.e. the x in x := --// e.(type)), the third result will be the type of the expression being --// switched on (the type of e in the example). This facilitates workarounds for --// limitations of the go/types API, which does not report an object for the --// identifier x. --// --// For embedded fields, referencedObject returns the type name object rather --// than the var (field) object. --// --// TODO(rfindley): this function exists to preserve the pre-existing behavior --// of source.Identifier. Eliminate this helper in favor of sharing --// functionality with objectsAt, after choosing suitable primitives. --func referencedObject(pkg Package, pgf *ParsedGoFile, pos token.Pos) (*ast.Ident, types.Object, types.Type) { -- path := pathEnclosingObjNode(pgf.File, pos) -- if len(path) == 0 { -- return nil, nil, nil +-func (c *commandHandler) runTests(ctx context.Context, snapshot *cache.Snapshot, work *progress.WorkDone, uri protocol.DocumentURI, tests, benchmarks []string) error { +- // TODO: fix the error reporting when this runs async. +- meta, err := golang.NarrowestMetadataForFile(ctx, snapshot, uri) +- if err != nil { +- return err - } -- var obj types.Object -- info := pkg.GetTypesInfo() -- switch n := path[0].(type) { -- case *ast.Ident: -- obj = info.ObjectOf(n) -- // If n is the var's declaring ident in a type switch -- // [i.e. the x in x := foo.(type)], it will not have an object. In this -- // case, set obj to the first implicit object (if any), and return the type -- // of the expression being switched on. -- // -- // The type switch may have no case clauses and thus no -- // implicit objects; this is a type error ("unused x"), -- if obj == nil { -- if implicits, typ := typeSwitchImplicits(info, path); len(implicits) > 0 { -- return n, implicits[0], typ +- pkgPath := string(meta.ForTest) +- +- // create output +- buf := &bytes.Buffer{} +- ew := progress.NewEventWriter(ctx, "test") +- out := io.MultiWriter(ew, progress.NewWorkDoneWriter(ctx, work), buf) +- +- // Run `go test -run Func` on each test. +- var failedTests int +- for _, funcName := range tests { +- inv := &gocommand.Invocation{ +- Verb: "test", +- Args: []string{pkgPath, "-v", "-count=1", fmt.Sprintf("-run=^%s$", regexp.QuoteMeta(funcName))}, +- WorkingDir: filepath.Dir(uri.Path()), +- } +- if err := snapshot.RunGoCommandPiped(ctx, cache.Normal, inv, out, out); err != nil { +- if errors.Is(err, context.Canceled) { +- return err - } +- failedTests++ - } +- } - -- // If the original position was an embedded field, we want to jump -- // to the field's type definition, not the field's definition. -- if v, ok := obj.(*types.Var); ok && v.Embedded() { -- // types.Info.Uses contains the embedded field's *types.TypeName. -- if typeName := info.Uses[n]; typeName != nil { -- obj = typeName +- // Run `go test -run=^$ -bench Func` on each test. +- var failedBenchmarks int +- for _, funcName := range benchmarks { +- inv := &gocommand.Invocation{ +- Verb: "test", +- Args: []string{pkgPath, "-v", "-run=^$", fmt.Sprintf("-bench=^%s$", regexp.QuoteMeta(funcName))}, +- WorkingDir: filepath.Dir(uri.Path()), +- } +- if err := snapshot.RunGoCommandPiped(ctx, cache.Normal, inv, out, out); err != nil { +- if errors.Is(err, context.Canceled) { +- return err - } +- failedBenchmarks++ - } -- return n, obj, nil - } -- return nil, nil, nil --} - --// importDefinition returns locations defining a package referenced by the --// import spec containing pos. --// --// If pos is not inside an import spec, it returns nil, nil. --func importDefinition(ctx context.Context, s Snapshot, pkg Package, pgf *ParsedGoFile, pos token.Pos) ([]protocol.Location, error) { -- var imp *ast.ImportSpec -- for _, spec := range pgf.File.Imports { -- // We use "<= End" to accept a query immediately after an ImportSpec. -- if spec.Path.Pos() <= pos && pos <= spec.Path.End() { -- imp = spec -- } +- var title string +- if len(tests) > 0 && len(benchmarks) > 0 { +- title = "tests and benchmarks" +- } else if len(tests) > 0 { +- title = "tests" +- } else if len(benchmarks) > 0 { +- title = "benchmarks" +- } else { +- return errors.New("No functions were provided") - } -- if imp == nil { -- return nil, nil +- message := fmt.Sprintf("all %s passed", title) +- if failedTests > 0 && failedBenchmarks > 0 { +- message = fmt.Sprintf("%d / %d tests failed and %d / %d benchmarks failed", failedTests, len(tests), failedBenchmarks, len(benchmarks)) +- } else if failedTests > 0 { +- message = fmt.Sprintf("%d / %d tests failed", failedTests, len(tests)) +- } else if failedBenchmarks > 0 { +- message = fmt.Sprintf("%d / %d benchmarks failed", failedBenchmarks, len(benchmarks)) +- } +- if failedTests > 0 || failedBenchmarks > 0 { +- message += "\n" + buf.String() - } - -- importPath := UnquoteImportPath(imp) -- impID := pkg.Metadata().DepsByImpPath[importPath] -- if impID == "" { -- return nil, fmt.Errorf("failed to resolve import %q", importPath) +- showMessage(ctx, c.s.client, protocol.Info, message) +- +- if failedTests > 0 || failedBenchmarks > 0 { +- return errors.New("gopls.test command failed") - } -- impMetadata := s.Metadata(impID) -- if impMetadata == nil { -- return nil, fmt.Errorf("missing information for package %q", impID) +- return nil +-} +- +-func (c *commandHandler) Generate(ctx context.Context, args command.GenerateArgs) error { +- title := "Running go generate ." +- if args.Recursive { +- title = "Running go generate ./..." - } +- return c.run(ctx, commandConfig{ +- requireSave: true, +- progress: title, +- forURI: args.Dir, +- }, func(ctx context.Context, deps commandDeps) error { +- er := progress.NewEventWriter(ctx, "generate") - -- var locs []protocol.Location -- for _, f := range impMetadata.CompiledGoFiles { -- fh, err := s.ReadFile(ctx, f) -- if err != nil { -- if ctx.Err() != nil { -- return nil, ctx.Err() -- } -- continue +- pattern := "." +- if args.Recursive { +- pattern = "./..." - } -- pgf, err := s.ParseGo(ctx, fh, ParseHeader) -- if err != nil { -- if ctx.Err() != nil { -- return nil, ctx.Err() -- } -- continue +- inv := &gocommand.Invocation{ +- Verb: "generate", +- Args: []string{"-x", pattern}, +- WorkingDir: args.Dir.Path(), - } -- loc, err := pgf.NodeLocation(pgf.File) -- if err != nil { -- return nil, err +- stderr := io.MultiWriter(er, progress.NewWorkDoneWriter(ctx, deps.work)) +- if err := deps.snapshot.RunGoCommandPiped(ctx, cache.AllowNetwork, inv, er, stderr); err != nil { +- return err - } -- locs = append(locs, loc) -- } -- -- if len(locs) == 0 { -- return nil, fmt.Errorf("package %q has no readable files", impID) // incl. unsafe -- } +- return nil +- }) +-} - -- return locs, nil +-func (c *commandHandler) GoGetPackage(ctx context.Context, args command.GoGetPackageArgs) error { +- return c.run(ctx, commandConfig{ +- forURI: args.URI, +- progress: "Running go get", +- }, func(ctx context.Context, deps commandDeps) error { +- // Run on a throwaway go.mod, otherwise it'll write to the real one. +- stdout, err := deps.snapshot.RunGoCommandDirect(ctx, cache.WriteTemporaryModFile|cache.AllowNetwork, &gocommand.Invocation{ +- Verb: "list", +- Args: []string{"-f", "{{.Module.Path}}@{{.Module.Version}}", args.Pkg}, +- WorkingDir: filepath.Dir(args.URI.Path()), +- }) +- if err != nil { +- return err +- } +- ver := strings.TrimSpace(stdout.String()) +- return c.s.runGoModUpdateCommands(ctx, deps.snapshot, args.URI, func(invoke func(...string) (*bytes.Buffer, error)) error { +- if args.AddRequire { +- if err := addModuleRequire(invoke, []string{ver}); err != nil { +- return err +- } +- } +- _, err := invoke(append([]string{"get", "-d"}, args.Pkg)...) +- return err +- }) +- }) -} - --// TODO(rfindley): avoid the duplicate column mapping here, by associating a --// column mapper with each file handle. --func mapPosition(ctx context.Context, fset *token.FileSet, s FileSource, start, end token.Pos) (protocol.Location, error) { -- file := fset.File(start) -- uri := span.URIFromPath(file.Name()) -- fh, err := s.ReadFile(ctx, uri) +-func (s *server) runGoModUpdateCommands(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI, run func(invoke func(...string) (*bytes.Buffer, error)) error) error { +- newModBytes, newSumBytes, err := snapshot.RunGoModUpdateCommands(ctx, filepath.Dir(uri.Path()), run) - if err != nil { -- return protocol.Location{}, err +- return err - } -- content, err := fh.Content() +- modURI := snapshot.GoModForFile(uri) +- sumURI := protocol.URIFromPath(strings.TrimSuffix(modURI.Path(), ".mod") + ".sum") +- modEdits, err := collectFileEdits(ctx, snapshot, modURI, newModBytes) - if err != nil { -- return protocol.Location{}, err +- return err - } -- m := protocol.NewMapper(fh.URI(), content) -- return m.PosLocation(file, start, end) --} -diff -urN a/gopls/internal/lsp/source/diagnostics.go b/gopls/internal/lsp/source/diagnostics.go ---- a/gopls/internal/lsp/source/diagnostics.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/diagnostics.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,187 +0,0 @@ --// Copyright 2018 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package source -- --import ( -- "context" -- "encoding/json" -- -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/progress" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/span" --) -- --type SuggestedFix struct { -- Title string -- Edits map[span.URI][]protocol.TextEdit -- Command *protocol.Command -- ActionKind protocol.CodeActionKind +- sumEdits, err := collectFileEdits(ctx, snapshot, sumURI, newSumBytes) +- if err != nil { +- return err +- } +- return applyFileEdits(ctx, s.client, append(sumEdits, modEdits...)) -} - --// Analyze reports go/analysis-framework diagnostics in the specified package. +-// collectFileEdits collects any file edits required to transform the snapshot +-// file specified by uri to the provided new content. -// --// If the provided tracker is non-nil, it may be used to provide notifications --// of the ongoing analysis pass. --func Analyze(ctx context.Context, snapshot Snapshot, pkgIDs map[PackageID]unit, tracker *progress.Tracker) (map[span.URI][]*Diagnostic, error) { -- // Exit early if the context has been canceled. This also protects us -- // from a race on Options, see golang/go#36699. -- if ctx.Err() != nil { -- return nil, ctx.Err() +-// If the file is not open, collectFileEdits simply writes the new content to +-// disk. +-// +-// TODO(rfindley): fix this API asymmetry. It should be up to the caller to +-// write the file or apply the edits. +-func collectFileEdits(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI, newContent []byte) ([]protocol.TextDocumentEdit, error) { +- fh, err := snapshot.ReadFile(ctx, uri) +- if err != nil { +- return nil, err - } -- -- options := snapshot.Options() -- categories := []map[string]*Analyzer{ -- options.DefaultAnalyzers, -- options.StaticcheckAnalyzers, -- options.TypeErrorAnalyzers, +- oldContent, err := fh.Content() +- if err != nil && !os.IsNotExist(err) { +- return nil, err - } - -- var analyzers []*Analyzer -- for _, cat := range categories { -- for _, a := range cat { -- analyzers = append(analyzers, a) -- } +- if bytes.Equal(oldContent, newContent) { +- return nil, nil - } - -- analysisDiagnostics, err := snapshot.Analyze(ctx, pkgIDs, analyzers, tracker) -- if err != nil { +- // Sending a workspace edit to a closed file causes VS Code to open the +- // file and leave it unsaved. We would rather apply the changes directly, +- // especially to go.sum, which should be mostly invisible to the user. +- if !snapshot.IsOpen(uri) { +- err := os.WriteFile(uri.Path(), newContent, 0666) - return nil, err - } - -- // Report diagnostics and errors from root analyzers. -- reports := make(map[span.URI][]*Diagnostic) -- for _, diag := range analysisDiagnostics { -- reports[diag.URI] = append(reports[diag.URI], diag) +- m := protocol.NewMapper(fh.URI(), oldContent) +- diff := diff.Bytes(oldContent, newContent) +- edits, err := protocol.EditsFromDiffEdits(m, diff) +- if err != nil { +- return nil, err - } -- return reports, nil +- return []protocol.TextDocumentEdit{{ +- TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{ +- Version: fh.Version(), +- TextDocumentIdentifier: protocol.TextDocumentIdentifier{ +- URI: uri, +- }, +- }, +- Edits: protocol.AsAnnotatedTextEdits(edits), +- }}, nil -} - --// CombineDiagnostics combines and filters list/parse/type diagnostics from --// tdiags with adiags, and appends the two lists to *outT and *outA, --// respectively. --// --// Type-error analyzers produce diagnostics that are redundant --// with type checker diagnostics, but more detailed (e.g. fixes). --// Rather than report two diagnostics for the same problem, --// we combine them by augmenting the type-checker diagnostic --// and discarding the analyzer diagnostic. --// --// If an analysis diagnostic has the same range and message as --// a list/parse/type diagnostic, the suggested fix information --// (et al) of the latter is merged into a copy of the former. --// This handles the case where a type-error analyzer suggests --// a fix to a type error, and avoids duplication. --// --// The use of out-slices, though irregular, allows the caller to --// easily choose whether to keep the results separate or combined. --// --// The arguments are not modified. --func CombineDiagnostics(tdiags []*Diagnostic, adiags []*Diagnostic, outT, outA *[]*Diagnostic) { -- -- // Build index of (list+parse+)type errors. -- type key struct { -- Range protocol.Range -- message string +-func applyFileEdits(ctx context.Context, cli protocol.Client, edits []protocol.TextDocumentEdit) error { +- if len(edits) == 0 { +- return nil - } -- index := make(map[key]int) // maps (Range,Message) to index in tdiags slice -- for i, diag := range tdiags { -- index[key{diag.Range, diag.Message}] = i +- response, err := cli.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{ +- Edit: protocol.WorkspaceEdit{ +- DocumentChanges: protocol.TextDocumentEditsToDocumentChanges(edits), +- }, +- }) +- if err != nil { +- return err - } +- if !response.Applied { +- return fmt.Errorf("edits not applied because of %s", response.FailureReason) +- } +- return nil +-} - -- // Filter out analysis diagnostics that match type errors, -- // retaining their suggested fix (etc) fields. -- for _, diag := range adiags { -- if i, ok := index[key{diag.Range, diag.Message}]; ok { -- copy := *tdiags[i] -- copy.SuggestedFixes = diag.SuggestedFixes -- copy.Tags = diag.Tags -- tdiags[i] = © -- continue +-func runGoGetModule(invoke func(...string) (*bytes.Buffer, error), addRequire bool, args []string) error { +- if addRequire { +- if err := addModuleRequire(invoke, args); err != nil { +- return err - } -- -- *outA = append(*outA, diag) - } -- -- *outT = append(*outT, tdiags...) +- _, err := invoke(append([]string{"get", "-d"}, args...)...) +- return err -} - --// quickFixesJSON is a JSON-serializable list of quick fixes --// to be saved in the protocol.Diagnostic.Data field. --type quickFixesJSON struct { -- // TODO(rfindley): pack some sort of identifier here for later -- // lookup/validation? -- Fixes []protocol.CodeAction +-func addModuleRequire(invoke func(...string) (*bytes.Buffer, error), args []string) error { +- // Using go get to create a new dependency results in an +- // `// indirect` comment we may not want. The only way to avoid it +- // is to add the require as direct first. Then we can use go get to +- // update go.sum and tidy up. +- _, err := invoke(append([]string{"mod", "edit", "-require"}, args...)...) +- return err -} - --// BundleQuickFixes attempts to bundle sd.SuggestedFixes into the --// sd.BundledFixes field, so that it can be round-tripped through the client. --// It returns false if the quick-fixes cannot be bundled. --func BundleQuickFixes(sd *Diagnostic) bool { -- if len(sd.SuggestedFixes) == 0 { -- return true -- } -- var actions []protocol.CodeAction -- for _, fix := range sd.SuggestedFixes { -- if fix.Edits != nil { -- // For now, we only support bundled code actions that execute commands. -- // -- // In order to cleanly support bundled edits, we'd have to guarantee that -- // the edits were generated on the current snapshot. But this naively -- // implies that every fix would have to include a snapshot ID, which -- // would require us to republish all diagnostics on each new snapshot. -- // -- // TODO(rfindley): in order to avoid this additional chatter, we'd need -- // to build some sort of registry or other mechanism on the snapshot to -- // check whether a diagnostic is still valid. -- return false -- } -- action := protocol.CodeAction{ -- Title: fix.Title, -- Kind: fix.ActionKind, -- Command: fix.Command, -- } -- actions = append(actions, action) -- } -- fixes := quickFixesJSON{ -- Fixes: actions, -- } -- data, err := json.Marshal(fixes) +-// TODO(rfindley): inline. +-func (s *server) getUpgrades(ctx context.Context, snapshot *cache.Snapshot, uri protocol.DocumentURI, modules []string) (map[string]string, error) { +- stdout, err := snapshot.RunGoCommandDirect(ctx, cache.Normal|cache.AllowNetwork, &gocommand.Invocation{ +- Verb: "list", +- Args: append([]string{"-m", "-u", "-json"}, modules...), +- ModFlag: "readonly", // necessary when vendor is present (golang/go#66055) +- WorkingDir: filepath.Dir(uri.Path()), +- }) - if err != nil { -- bug.Reportf("marshalling quick fixes: %v", err) -- return false -- } -- msg := json.RawMessage(data) -- sd.BundledFixes = &msg -- return true --} -- --// BundledQuickFixes extracts any bundled codeActions from the --// diag.Data field. --func BundledQuickFixes(diag protocol.Diagnostic) []protocol.CodeAction { -- if diag.Data == nil { -- return nil -- } -- var fix quickFixesJSON -- if err := json.Unmarshal(*diag.Data, &fix); err != nil { -- bug.Reportf("unmarshalling quick fix: %v", err) -- return nil +- return nil, err - } - -- var actions []protocol.CodeAction -- for _, action := range fix.Fixes { -- // See BundleQuickFixes: for now we only support bundling commands. -- if action.Edit != nil { -- bug.Reportf("bundled fix %q includes workspace edits", action.Title) +- upgrades := map[string]string{} +- for dec := json.NewDecoder(stdout); dec.More(); { +- mod := &gocommand.ModuleJSON{} +- if err := dec.Decode(mod); err != nil { +- return nil, err +- } +- if mod.Update == nil { - continue - } -- // associate the action with the incoming diagnostic -- // (Note that this does not mutate the fix.Fixes slice). -- action.Diagnostics = []protocol.Diagnostic{diag} -- actions = append(actions, action) +- upgrades[mod.Path] = mod.Update.Version - } -- -- return actions +- return upgrades, nil -} -diff -urN a/gopls/internal/lsp/source/embeddirective.go b/gopls/internal/lsp/source/embeddirective.go ---- a/gopls/internal/lsp/source/embeddirective.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/embeddirective.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,195 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package source -- --import ( -- "errors" -- "fmt" -- "io/fs" -- "path/filepath" -- "strconv" -- "strings" -- "unicode" -- "unicode/utf8" -- -- "golang.org/x/tools/gopls/internal/lsp/protocol" --) - --// ErrNoEmbed is returned by EmbedDefinition when no embed --// directive is found at a particular position. --// As such it indicates that other definitions could be worth checking. --var ErrNoEmbed = errors.New("no embed directive found") +-func (c *commandHandler) GCDetails(ctx context.Context, uri protocol.DocumentURI) error { +- return c.ToggleGCDetails(ctx, command.URIArg{URI: uri}) +-} - --var errStopWalk = errors.New("stop walk") +-func (c *commandHandler) ToggleGCDetails(ctx context.Context, args command.URIArg) error { +- return c.run(ctx, commandConfig{ +- requireSave: true, +- progress: "Toggling GC Details", +- forURI: args.URI, +- }, func(ctx context.Context, deps commandDeps) error { +- return c.modifyState(ctx, FromToggleGCDetails, func() (*cache.Snapshot, func(), error) { +- meta, err := golang.NarrowestMetadataForFile(ctx, deps.snapshot, deps.fh.URI()) +- if err != nil { +- return nil, nil, err +- } +- wantDetails := !deps.snapshot.WantGCDetails(meta.ID) // toggle the gc details state +- return c.s.session.InvalidateView(ctx, deps.snapshot.View(), cache.StateChange{ +- GCDetails: map[metadata.PackageID]bool{ +- meta.ID: wantDetails, +- }, +- }) +- }) +- }) +-} - --// EmbedDefinition finds a file matching the embed directive at pos in the mapped file. --// If there is no embed directive at pos, returns ErrNoEmbed. --// If multiple files match the embed pattern, one is picked at random. --func EmbedDefinition(m *protocol.Mapper, pos protocol.Position) ([]protocol.Location, error) { -- pattern, _ := parseEmbedDirective(m, pos) -- if pattern == "" { -- return nil, ErrNoEmbed -- } +-func (c *commandHandler) ListKnownPackages(ctx context.Context, args command.URIArg) (command.ListKnownPackagesResult, error) { +- var result command.ListKnownPackagesResult +- err := c.run(ctx, commandConfig{ +- progress: "Listing packages", +- forURI: args.URI, +- }, func(ctx context.Context, deps commandDeps) error { +- pkgs, err := golang.KnownPackagePaths(ctx, deps.snapshot, deps.fh) +- for _, pkg := range pkgs { +- result.Packages = append(result.Packages, string(pkg)) +- } +- return err +- }) +- return result, err +-} - -- // Find the first matching file. -- var match string -- dir := filepath.Dir(m.URI.Filename()) -- err := filepath.WalkDir(dir, func(abs string, d fs.DirEntry, e error) error { -- if e != nil { -- return e +-func (c *commandHandler) ListImports(ctx context.Context, args command.URIArg) (command.ListImportsResult, error) { +- var result command.ListImportsResult +- err := c.run(ctx, commandConfig{ +- forURI: args.URI, +- }, func(ctx context.Context, deps commandDeps) error { +- fh, err := deps.snapshot.ReadFile(ctx, args.URI) +- if err != nil { +- return err - } -- rel, err := filepath.Rel(dir, abs) +- pgf, err := deps.snapshot.ParseGo(ctx, fh, parsego.Header) - if err != nil { - return err - } -- ok, err := filepath.Match(pattern, rel) +- fset := tokeninternal.FileSetFor(pgf.Tok) +- for _, group := range astutil.Imports(fset, pgf.File) { +- for _, imp := range group { +- if imp.Path == nil { +- continue +- } +- var name string +- if imp.Name != nil { +- name = imp.Name.Name +- } +- result.Imports = append(result.Imports, command.FileImport{ +- Path: string(metadata.UnquoteImportPath(imp)), +- Name: name, +- }) +- } +- } +- meta, err := golang.NarrowestMetadataForFile(ctx, deps.snapshot, args.URI) +- if err != nil { +- return err // e.g. cancelled +- } +- for pkgPath := range meta.DepsByPkgPath { +- result.PackageImports = append(result.PackageImports, +- command.PackageImport{Path: string(pkgPath)}) +- } +- sort.Slice(result.PackageImports, func(i, j int) bool { +- return result.PackageImports[i].Path < result.PackageImports[j].Path +- }) +- return nil +- }) +- return result, err +-} +- +-func (c *commandHandler) AddImport(ctx context.Context, args command.AddImportArgs) error { +- return c.run(ctx, commandConfig{ +- progress: "Adding import", +- forURI: args.URI, +- }, func(ctx context.Context, deps commandDeps) error { +- edits, err := golang.AddImport(ctx, deps.snapshot, deps.fh, args.ImportPath) - if err != nil { -- return err +- return fmt.Errorf("could not add import: %v", err) - } -- if ok && !d.IsDir() { -- match = abs -- return errStopWalk +- if _, err := c.s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{ +- Edit: protocol.WorkspaceEdit{ +- DocumentChanges: documentChanges(deps.fh, edits), +- }, +- }); err != nil { +- return fmt.Errorf("could not apply import edits: %v", err) - } - return nil - }) -- if err != nil && !errors.Is(err, errStopWalk) { -- return nil, err +-} +- +-func (c *commandHandler) StartDebugging(ctx context.Context, args command.DebuggingArgs) (result command.DebuggingResult, _ error) { +- addr := args.Addr +- if addr == "" { +- addr = "localhost:0" - } -- if match == "" { -- return nil, fmt.Errorf("%q does not match any files in %q", pattern, dir) +- di := debug.GetInstance(ctx) +- if di == nil { +- return result, errors.New("internal error: server has no debugging instance") - } -- -- loc := protocol.Location{ -- URI: protocol.URIFromPath(match), -- Range: protocol.Range{ -- Start: protocol.Position{Line: 0, Character: 0}, -- }, +- listenedAddr, err := di.Serve(ctx, addr) +- if err != nil { +- return result, fmt.Errorf("starting debug server: %w", err) - } -- return []protocol.Location{loc}, nil +- result.URLs = []string{"http://" + listenedAddr} +- openClientBrowser(ctx, c.s.client, result.URLs[0]) +- return result, nil -} - --// parseEmbedDirective attempts to parse a go:embed directive argument at pos. --// If successful it return the directive argument and its range, else zero values are returned. --func parseEmbedDirective(m *protocol.Mapper, pos protocol.Position) (string, protocol.Range) { -- lineStart, err := m.PositionOffset(protocol.Position{Line: pos.Line, Character: 0}) -- if err != nil { -- return "", protocol.Range{} -- } -- lineEnd, err := m.PositionOffset(protocol.Position{Line: pos.Line + 1, Character: 0}) +-func (c *commandHandler) StartProfile(ctx context.Context, args command.StartProfileArgs) (result command.StartProfileResult, _ error) { +- file, err := os.CreateTemp("", "gopls-profile-*") - if err != nil { -- return "", protocol.Range{} +- return result, fmt.Errorf("creating temp profile file: %v", err) - } - -- text := string(m.Content[lineStart:lineEnd]) -- if !strings.HasPrefix(text, "//go:embed") { -- return "", protocol.Range{} -- } -- text = text[len("//go:embed"):] -- offset := lineStart + len("//go:embed") +- c.s.ongoingProfileMu.Lock() +- defer c.s.ongoingProfileMu.Unlock() - -- // Find the first pattern in text that covers the offset of the pos we are looking for. -- findOffset, err := m.PositionOffset(pos) -- if err != nil { -- return "", protocol.Range{} -- } -- patterns, err := parseGoEmbed(text, offset) -- if err != nil { -- return "", protocol.Range{} +- if c.s.ongoingProfile != nil { +- file.Close() // ignore error +- return result, fmt.Errorf("profile already started (for %q)", c.s.ongoingProfile.Name()) - } -- for _, p := range patterns { -- if p.startOffset <= findOffset && findOffset <= p.endOffset { -- // Found our match. -- rng, err := m.OffsetRange(p.startOffset, p.endOffset) -- if err != nil { -- return "", protocol.Range{} -- } -- return p.pattern, rng -- } +- +- if err := pprof.StartCPUProfile(file); err != nil { +- file.Close() // ignore error +- return result, fmt.Errorf("starting profile: %v", err) - } - -- return "", protocol.Range{} +- c.s.ongoingProfile = file +- return result, nil -} - --type fileEmbed struct { -- pattern string -- startOffset int -- endOffset int --} +-func (c *commandHandler) StopProfile(ctx context.Context, args command.StopProfileArgs) (result command.StopProfileResult, _ error) { +- c.s.ongoingProfileMu.Lock() +- defer c.s.ongoingProfileMu.Unlock() - --// parseGoEmbed patterns that come after the directive. --// --// Copied and adapted from go/build/read.go. --// Replaced token.Position with start/end offset (including quotes if present). --func parseGoEmbed(args string, offset int) ([]fileEmbed, error) { -- trimBytes := func(n int) { -- offset += n -- args = args[n:] -- } -- trimSpace := func() { -- trim := strings.TrimLeftFunc(args, unicode.IsSpace) -- trimBytes(len(args) - len(trim)) -- } +- prof := c.s.ongoingProfile +- c.s.ongoingProfile = nil - -- var list []fileEmbed -- for trimSpace(); args != ""; trimSpace() { -- var path string -- pathOffset := offset -- Switch: -- switch args[0] { -- default: -- i := len(args) -- for j, c := range args { -- if unicode.IsSpace(c) { -- i = j -- break -- } -- } -- path = args[:i] -- trimBytes(i) +- if prof == nil { +- return result, fmt.Errorf("no ongoing profile") +- } - -- case '`': -- var ok bool -- path, _, ok = strings.Cut(args[1:], "`") -- if !ok { -- return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args) -- } -- trimBytes(1 + len(path) + 1) +- pprof.StopCPUProfile() +- if err := prof.Close(); err != nil { +- return result, fmt.Errorf("closing profile file: %v", err) +- } +- result.File = prof.Name() +- return result, nil +-} - -- case '"': -- i := 1 -- for ; i < len(args); i++ { -- if args[i] == '\\' { -- i++ -- continue -- } -- if args[i] == '"' { -- q, err := strconv.Unquote(args[:i+1]) -- if err != nil { -- return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args[:i+1]) -- } -- path = q -- trimBytes(i + 1) -- break Switch +-func (c *commandHandler) FetchVulncheckResult(ctx context.Context, arg command.URIArg) (map[protocol.DocumentURI]*vulncheck.Result, error) { +- ret := map[protocol.DocumentURI]*vulncheck.Result{} +- err := c.run(ctx, commandConfig{forURI: arg.URI}, func(ctx context.Context, deps commandDeps) error { +- if deps.snapshot.Options().Vulncheck == settings.ModeVulncheckImports { +- for _, modfile := range deps.snapshot.View().ModFiles() { +- res, err := deps.snapshot.ModVuln(ctx, modfile) +- if err != nil { +- return err - } -- } -- if i >= len(args) { -- return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args) +- ret[modfile] = res - } - } -- -- if args != "" { -- r, _ := utf8.DecodeRuneInString(args) -- if !unicode.IsSpace(r) { -- return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args) -- } +- // Overwrite if there is any govulncheck-based result. +- for modfile, result := range deps.snapshot.Vulnerabilities() { +- ret[modfile] = result - } -- list = append(list, fileEmbed{ -- pattern: path, -- startOffset: pathOffset, -- endOffset: offset, -- }) -- } -- return list, nil +- return nil +- }) +- return ret, err -} -diff -urN a/gopls/internal/lsp/source/extract.go b/gopls/internal/lsp/source/extract.go ---- a/gopls/internal/lsp/source/extract.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/extract.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,1351 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package source +-func (c *commandHandler) RunGovulncheck(ctx context.Context, args command.VulncheckArgs) (command.RunVulncheckResult, error) { +- if args.URI == "" { +- return command.RunVulncheckResult{}, errors.New("VulncheckArgs is missing URI field") +- } - --import ( -- "bytes" -- "fmt" -- "go/ast" -- "go/format" -- "go/parser" -- "go/token" -- "go/types" -- "sort" -- "strings" -- "text/scanner" +- // Return the workdone token so that clients can identify when this +- // vulncheck invocation is complete. +- // +- // Since the run function executes asynchronously, we use a channel to +- // synchronize the start of the run and return the token. +- tokenChan := make(chan protocol.ProgressToken, 1) +- err := c.run(ctx, commandConfig{ +- async: true, // need to be async to be cancellable +- progress: "govulncheck", +- requireSave: true, +- forURI: args.URI, +- }, func(ctx context.Context, deps commandDeps) error { +- tokenChan <- deps.work.Token() - -- "golang.org/x/tools/go/analysis" -- "golang.org/x/tools/go/ast/astutil" -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/internal/analysisinternal" --) +- workDoneWriter := progress.NewWorkDoneWriter(ctx, deps.work) +- dir := filepath.Dir(args.URI.Path()) +- pattern := args.Pattern - --func extractVariable(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { -- tokFile := fset.File(file.Pos()) -- expr, path, ok, err := CanExtractVariable(start, end, file) -- if !ok { -- return nil, fmt.Errorf("extractVariable: cannot extract %s: %v", safetoken.StartPosition(fset, start), err) -- } +- result, err := scan.RunGovulncheck(ctx, pattern, deps.snapshot, dir, workDoneWriter) +- if err != nil { +- return err +- } - -- // Create new AST node for extracted code. -- var lhsNames []string -- switch expr := expr.(type) { -- // TODO: stricter rules for selectorExpr. -- case *ast.BasicLit, *ast.CompositeLit, *ast.IndexExpr, *ast.SliceExpr, -- *ast.UnaryExpr, *ast.BinaryExpr, *ast.SelectorExpr: -- lhsName, _ := generateAvailableIdentifier(expr.Pos(), path, pkg, info, "x", 0) -- lhsNames = append(lhsNames, lhsName) -- case *ast.CallExpr: -- tup, ok := info.TypeOf(expr).(*types.Tuple) -- if !ok { -- // If the call expression only has one return value, we can treat it the -- // same as our standard extract variable case. -- lhsName, _ := generateAvailableIdentifier(expr.Pos(), path, pkg, info, "x", 0) -- lhsNames = append(lhsNames, lhsName) -- break +- snapshot, release, err := c.s.session.InvalidateView(ctx, deps.snapshot.View(), cache.StateChange{ +- Vulns: map[protocol.DocumentURI]*vulncheck.Result{args.URI: result}, +- }) +- if err != nil { +- return err - } -- idx := 0 -- for i := 0; i < tup.Len(); i++ { -- // Generate a unique variable for each return value. -- var lhsName string -- lhsName, idx = generateAvailableIdentifier(expr.Pos(), path, pkg, info, "x", idx) -- lhsNames = append(lhsNames, lhsName) +- defer release() +- c.s.diagnoseSnapshot(snapshot, nil, 0) +- +- affecting := make(map[string]bool, len(result.Entries)) +- for _, finding := range result.Findings { +- if len(finding.Trace) > 1 { // at least 2 frames if callstack exists (vulnerability, entry) +- affecting[finding.OSV] = true +- } - } -- default: -- return nil, fmt.Errorf("cannot extract %T", expr) -- } +- if len(affecting) == 0 { +- showMessage(ctx, c.s.client, protocol.Info, "No vulnerabilities found") +- return nil +- } +- affectingOSVs := make([]string, 0, len(affecting)) +- for id := range affecting { +- affectingOSVs = append(affectingOSVs, id) +- } +- sort.Strings(affectingOSVs) - -- insertBeforeStmt := analysisinternal.StmtToInsertVarBefore(path) -- if insertBeforeStmt == nil { -- return nil, fmt.Errorf("cannot find location to insert extraction") -- } -- indent, err := calculateIndentation(src, tokFile, insertBeforeStmt) -- if err != nil { -- return nil, err -- } -- newLineIndent := "\n" + indent +- showMessage(ctx, c.s.client, protocol.Warning, fmt.Sprintf("Found %v", strings.Join(affectingOSVs, ", "))) - -- lhs := strings.Join(lhsNames, ", ") -- assignStmt := &ast.AssignStmt{ -- Lhs: []ast.Expr{ast.NewIdent(lhs)}, -- Tok: token.DEFINE, -- Rhs: []ast.Expr{expr}, +- return nil +- }) +- if err != nil { +- return command.RunVulncheckResult{}, err - } -- var buf bytes.Buffer -- if err := format.Node(&buf, fset, assignStmt); err != nil { -- return nil, err +- select { +- case <-ctx.Done(): +- return command.RunVulncheckResult{}, ctx.Err() +- case token := <-tokenChan: +- return command.RunVulncheckResult{Token: token}, nil - } -- assignment := strings.ReplaceAll(buf.String(), "\n", newLineIndent) + newLineIndent +-} - -- return &analysis.SuggestedFix{ -- TextEdits: []analysis.TextEdit{ -- { -- Pos: insertBeforeStmt.Pos(), -- End: insertBeforeStmt.Pos(), -- NewText: []byte(assignment), -- }, -- { -- Pos: start, -- End: end, -- NewText: []byte(lhs), -- }, -- }, +-// MemStats implements the MemStats command. It returns an error as a +-// future-proof API, but the resulting error is currently always nil. +-func (c *commandHandler) MemStats(ctx context.Context) (command.MemStatsResult, error) { +- // GC a few times for stable results. +- runtime.GC() +- runtime.GC() +- runtime.GC() +- var m runtime.MemStats +- runtime.ReadMemStats(&m) +- return command.MemStatsResult{ +- HeapAlloc: m.HeapAlloc, +- HeapInUse: m.HeapInuse, +- TotalAlloc: m.TotalAlloc, - }, nil -} - --// CanExtractVariable reports whether the code in the given range can be --// extracted to a variable. --func CanExtractVariable(start, end token.Pos, file *ast.File) (ast.Expr, []ast.Node, bool, error) { -- if start == end { -- return nil, nil, false, fmt.Errorf("start and end are equal") -- } -- path, _ := astutil.PathEnclosingInterval(file, start, end) -- if len(path) == 0 { -- return nil, nil, false, fmt.Errorf("no path enclosing interval") -- } -- for _, n := range path { -- if _, ok := n.(*ast.ImportSpec); ok { -- return nil, nil, false, fmt.Errorf("cannot extract variable in an import block") +-// WorkspaceStats implements the WorkspaceStats command, reporting information +-// about the current state of the loaded workspace for the current session. +-func (c *commandHandler) WorkspaceStats(ctx context.Context) (command.WorkspaceStatsResult, error) { +- var res command.WorkspaceStatsResult +- res.Files = c.s.session.Cache().FileStats() +- +- for _, view := range c.s.session.Views() { +- vs, err := collectViewStats(ctx, view) +- if err != nil { +- return res, err - } +- res.Views = append(res.Views, vs) - } -- node := path[0] -- if start != node.Pos() || end != node.End() { -- return nil, nil, false, fmt.Errorf("range does not map to an AST node") -- } -- expr, ok := node.(ast.Expr) -- if !ok { -- return nil, nil, false, fmt.Errorf("node is not an expression") -- } -- switch expr.(type) { -- case *ast.BasicLit, *ast.CompositeLit, *ast.IndexExpr, *ast.CallExpr, -- *ast.SliceExpr, *ast.UnaryExpr, *ast.BinaryExpr, *ast.SelectorExpr: -- return expr, path, true, nil -- } -- return nil, nil, false, fmt.Errorf("cannot extract an %T to a variable", expr) +- return res, nil -} - --// Calculate indentation for insertion. --// When inserting lines of code, we must ensure that the lines have consistent --// formatting (i.e. the proper indentation). To do so, we observe the indentation on the --// line of code on which the insertion occurs. --func calculateIndentation(content []byte, tok *token.File, insertBeforeStmt ast.Node) (string, error) { -- line := safetoken.Line(tok, insertBeforeStmt.Pos()) -- lineOffset, stmtOffset, err := safetoken.Offsets(tok, tok.LineStart(line), insertBeforeStmt.Pos()) +-func collectViewStats(ctx context.Context, view *cache.View) (command.ViewStats, error) { +- s, release, err := view.Snapshot() - if err != nil { -- return "", err +- return command.ViewStats{}, err - } -- return string(content[lineOffset:stmtOffset]), nil --} +- defer release() - --// generateAvailableIdentifier adjusts the new function name until there are no collisions in scope. --// Possible collisions include other function and variable names. Returns the next index to check for prefix. --func generateAvailableIdentifier(pos token.Pos, path []ast.Node, pkg *types.Package, info *types.Info, prefix string, idx int) (string, int) { -- scopes := CollectScopes(info, path, pos) -- scopes = append(scopes, pkg.Scope()) -- return generateIdentifier(idx, prefix, func(name string) bool { -- for _, scope := range scopes { -- if scope != nil && scope.Lookup(name) != nil { -- return true -- } -- } -- return false -- }) --} +- allMD, err := s.AllMetadata(ctx) +- if err != nil { +- return command.ViewStats{}, err +- } +- allPackages := collectPackageStats(allMD) - --func generateIdentifier(idx int, prefix string, hasCollision func(string) bool) (string, int) { -- name := prefix -- if idx != 0 { -- name += fmt.Sprintf("%d", idx) +- wsMD, err := s.WorkspaceMetadata(ctx) +- if err != nil { +- return command.ViewStats{}, err - } -- for hasCollision(name) { -- idx++ -- name = fmt.Sprintf("%v%d", prefix, idx) +- workspacePackages := collectPackageStats(wsMD) +- +- var ids []golang.PackageID +- for _, mp := range wsMD { +- ids = append(ids, mp.ID) - } -- return name, idx + 1 --} - --// returnVariable keeps track of the information we need to properly introduce a new variable --// that we will return in the extracted function. --type returnVariable struct { -- // name is the identifier that is used on the left-hand side of the call to -- // the extracted function. -- name ast.Expr -- // decl is the declaration of the variable. It is used in the type signature of the -- // extracted function and for variable declarations. -- decl *ast.Field -- // zeroVal is the "zero value" of the type of the variable. It is used in a return -- // statement in the extracted function. -- zeroVal ast.Expr --} +- diags, err := s.PackageDiagnostics(ctx, ids...) +- if err != nil { +- return command.ViewStats{}, err +- } - --// extractMethod refactors the selected block of code into a new method. --func extractMethod(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { -- return extractFunctionMethod(fset, start, end, src, file, pkg, info, true) --} +- ndiags := 0 +- for _, d := range diags { +- ndiags += len(d) +- } - --// extractFunction refactors the selected block of code into a new function. --func extractFunction(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) { -- return extractFunctionMethod(fset, start, end, src, file, pkg, info, false) +- return command.ViewStats{ +- GoCommandVersion: view.GoVersionString(), +- AllPackages: allPackages, +- WorkspacePackages: workspacePackages, +- Diagnostics: ndiags, +- }, nil -} - --// extractFunctionMethod refactors the selected block of code into a new function/method. --// It also replaces the selected block of code with a call to the extracted --// function. First, we manually adjust the selection range. We remove trailing --// and leading whitespace characters to ensure the range is precisely bounded --// by AST nodes. Next, we determine the variables that will be the parameters --// and return values of the extracted function/method. Lastly, we construct the call --// of the function/method and insert this call as well as the extracted function/method into --// their proper locations. --func extractFunctionMethod(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, pkg *types.Package, info *types.Info, isMethod bool) (*analysis.SuggestedFix, error) { -- errorPrefix := "extractFunction" -- if isMethod { -- errorPrefix = "extractMethod" -- } -- -- tok := fset.File(file.Pos()) -- if tok == nil { -- return nil, bug.Errorf("no file for position") -- } -- p, ok, methodOk, err := CanExtractFunction(tok, start, end, src, file) -- if (!ok && !isMethod) || (!methodOk && isMethod) { -- return nil, fmt.Errorf("%s: cannot extract %s: %v", errorPrefix, -- safetoken.StartPosition(fset, start), err) -- } -- tok, path, start, end, outer, node := p.tok, p.path, p.start, p.end, p.outer, p.node -- fileScope := info.Scopes[file] -- if fileScope == nil { -- return nil, fmt.Errorf("%s: file scope is empty", errorPrefix) -- } -- pkgScope := fileScope.Parent() -- if pkgScope == nil { -- return nil, fmt.Errorf("%s: package scope is empty", errorPrefix) -- } +-func collectPackageStats(mps []*metadata.Package) command.PackageStats { +- var stats command.PackageStats +- stats.Packages = len(mps) +- modules := make(map[string]bool) - -- // A return statement is non-nested if its parent node is equal to the parent node -- // of the first node in the selection. These cases must be handled separately because -- // non-nested return statements are guaranteed to execute. -- var retStmts []*ast.ReturnStmt -- var hasNonNestedReturn bool -- startParent := findParent(outer, node) -- ast.Inspect(outer, func(n ast.Node) bool { -- if n == nil { -- return false +- for _, mp := range mps { +- n := len(mp.CompiledGoFiles) +- stats.CompiledGoFiles += n +- if n > stats.LargestPackage { +- stats.LargestPackage = n - } -- if n.Pos() < start || n.End() > end { -- return n.Pos() <= end +- if mp.Module != nil { +- modules[mp.Module.Path] = true - } -- ret, ok := n.(*ast.ReturnStmt) -- if !ok { -- return true +- } +- stats.Modules = len(modules) +- +- return stats +-} +- +-// RunGoWorkCommand invokes `go work ` with the provided arguments. +-// +-// args.InitFirst controls whether to first run `go work init`. This allows a +-// single command to both create and recursively populate a go.work file -- as +-// of writing there is no `go work init -r`. +-// +-// Some thought went into implementing this command. Unlike the go.mod commands +-// above, this command simply invokes the go command and relies on the client +-// to notify gopls of file changes via didChangeWatchedFile notifications. +-// We could instead run these commands with GOWORK set to a temp file, but that +-// poses the following problems: +-// - directory locations in the resulting temp go.work file will be computed +-// relative to the directory containing that go.work. If the go.work is in a +-// tempdir, the directories will need to be translated to/from that dir. +-// - it would be simpler to use a temp go.work file in the workspace +-// directory, or whichever directory contains the real go.work file, but +-// that sets a bad precedent of writing to a user-owned directory. We +-// shouldn't start doing that. +-// - Sending workspace edits to create a go.work file would require using +-// the CreateFile resource operation, which would need to be tested in every +-// client as we haven't used it before. We don't have time for that right +-// now. +-// +-// Therefore, we simply require that the current go.work file is saved (if it +-// exists), and delegate to the go command. +-func (c *commandHandler) RunGoWorkCommand(ctx context.Context, args command.RunGoWorkArgs) error { +- return c.run(ctx, commandConfig{ +- progress: "Running go work command", +- forView: args.ViewID, +- }, func(ctx context.Context, deps commandDeps) (runErr error) { +- snapshot := deps.snapshot +- view := snapshot.View() +- viewDir := snapshot.Folder().Path() +- +- if view.Type() != cache.GoWorkView && view.GoWork() != "" { +- // If we are not using an existing go.work file, GOWORK must be explicitly off. +- // TODO(rfindley): what about GO111MODULE=off? +- return fmt.Errorf("cannot modify go.work files when GOWORK=off") - } -- if findParent(outer, n) == startParent { -- hasNonNestedReturn = true +- +- var gowork string +- // If the user has explicitly set GOWORK=off, we should warn them +- // explicitly and avoid potentially misleading errors below. +- if view.GoWork() != "" { +- gowork = view.GoWork().Path() +- fh, err := snapshot.ReadFile(ctx, view.GoWork()) +- if err != nil { +- return err // e.g. canceled +- } +- if !fh.SameContentsOnDisk() { +- return fmt.Errorf("must save workspace file %s before running go work commands", view.GoWork()) +- } +- } else { +- if !args.InitFirst { +- // If go.work does not exist, we should have detected that and asked +- // for InitFirst. +- return bug.Errorf("internal error: cannot run go work command: required go.work file not found") +- } +- gowork = filepath.Join(viewDir, "go.work") +- if err := c.invokeGoWork(ctx, viewDir, gowork, []string{"init"}); err != nil { +- return fmt.Errorf("running `go work init`: %v", err) +- } - } -- retStmts = append(retStmts, ret) -- return false +- +- return c.invokeGoWork(ctx, viewDir, gowork, args.Args) - }) -- containsReturnStatement := len(retStmts) > 0 +-} - -- // Now that we have determined the correct range for the selection block, -- // we must determine the signature of the extracted function. We will then replace -- // the block with an assignment statement that calls the extracted function with -- // the appropriate parameters and return values. -- variables, err := collectFreeVars(info, file, fileScope, pkgScope, start, end, path[0]) -- if err != nil { -- return nil, err +-func (c *commandHandler) invokeGoWork(ctx context.Context, viewDir, gowork string, args []string) error { +- inv := gocommand.Invocation{ +- Verb: "work", +- Args: args, +- WorkingDir: viewDir, +- Env: append(os.Environ(), fmt.Sprintf("GOWORK=%s", gowork)), +- } +- if _, err := c.s.session.GoCommandRunner().Run(ctx, inv); err != nil { +- return fmt.Errorf("running go work command: %v", err) - } +- return nil +-} - -- var ( -- receiverUsed bool -- receiver *ast.Field -- receiverName string -- receiverObj types.Object -- ) -- if isMethod { -- if outer == nil || outer.Recv == nil || len(outer.Recv.List) == 0 { -- return nil, fmt.Errorf("%s: cannot extract need method receiver", errorPrefix) -- } -- receiver = outer.Recv.List[0] -- if len(receiver.Names) == 0 || receiver.Names[0] == nil { -- return nil, fmt.Errorf("%s: cannot extract need method receiver name", errorPrefix) -- } -- recvName := receiver.Names[0] -- receiverName = recvName.Name -- receiverObj = info.ObjectOf(recvName) +-// showMessage causes the client to show a progress or error message. +-// +-// It reports whether it succeeded. If it fails, it writes an error to +-// the server log, so most callers can safely ignore the result. +-func showMessage(ctx context.Context, cli protocol.Client, typ protocol.MessageType, message string) bool { +- err := cli.ShowMessage(ctx, &protocol.ShowMessageParams{ +- Type: typ, +- Message: message, +- }) +- if err != nil { +- event.Error(ctx, "client.showMessage: %v", err) +- return false - } +- return true +-} - -- var ( -- params, returns []ast.Expr // used when calling the extracted function -- paramTypes, returnTypes []*ast.Field // used in the signature of the extracted function -- uninitialized []types.Object // vars we will need to initialize before the call -- ) +-// openClientBrowser causes the LSP client to open the specified URL +-// in an external browser. +-func openClientBrowser(ctx context.Context, cli protocol.Client, url protocol.URI) { +- showDocumentImpl(ctx, cli, url, nil) +-} - -- // Avoid duplicates while traversing vars and uninitialized. -- seenVars := make(map[types.Object]ast.Expr) -- seenUninitialized := make(map[types.Object]struct{}) +-// openClientEditor causes the LSP client to open the specified document +-// and select the indicated range. +-// +-// Note that VS Code 1.87.2 doesn't currently raise the window; this is +-// https://github.com/microsoft/vscode/issues/207634 +-func openClientEditor(ctx context.Context, cli protocol.Client, loc protocol.Location) { +- showDocumentImpl(ctx, cli, protocol.URI(loc.URI), &loc.Range) +-} - -- // Some variables on the left-hand side of our assignment statement may be free. If our -- // selection begins in the same scope in which the free variable is defined, we can -- // redefine it in our assignment statement. See the following example, where 'b' and -- // 'err' (both free variables) can be redefined in the second funcCall() while maintaining -- // correctness. -- // -- // -- // Not Redefined: -- // -- // a, err := funcCall() -- // var b int -- // b, err = funcCall() -- // -- // Redefined: +-func showDocumentImpl(ctx context.Context, cli protocol.Client, url protocol.URI, rangeOpt *protocol.Range) { +- // In principle we shouldn't send a showDocument request to a +- // client that doesn't support it, as reported by +- // ShowDocumentClientCapabilities. But even clients that do +- // support it may defer the real work of opening the document +- // asynchronously, to avoid deadlocks due to rentrancy. - // -- // a, err := funcCall() -- // b, err := funcCall() +- // For example: client sends request to server; server sends +- // showDocument to client; client opens editor; editor causes +- // new RPC to be sent to server, which is still busy with +- // previous request. (This happens in eglot.) - // -- // We track the number of free variables that can be redefined to maintain our preference -- // of using "x, y, z := fn()" style assignment statements. -- var canRedefineCount int +- // So we can't rely on the success/failure information. +- // That's the reason this function doesn't return an error. - -- // Each identifier in the selected block must become (1) a parameter to the -- // extracted function, (2) a return value of the extracted function, or (3) a local -- // variable in the extracted function. Determine the outcome(s) for each variable -- // based on whether it is free, altered within the selected block, and used outside -- // of the selected block. -- for _, v := range variables { -- if _, ok := seenVars[v.obj]; ok { -- continue -- } -- if v.obj.Name() == "_" { -- // The blank identifier is always a local variable -- continue -- } -- typ := analysisinternal.TypeExpr(file, pkg, v.obj.Type()) -- if typ == nil { -- return nil, fmt.Errorf("nil AST expression for type: %v", v.obj.Name()) +- // "External" means run the system-wide handler (e.g. open(1) +- // on macOS or xdg-open(1) on Linux) for this URL, ignoring +- // TakeFocus and Selection. Note that this may still end up +- // opening the same editor (e.g. VSCode) for a file: URL. +- res, err := cli.ShowDocument(ctx, &protocol.ShowDocumentParams{ +- URI: url, +- External: rangeOpt == nil, +- TakeFocus: true, +- Selection: rangeOpt, // optional +- }) +- if err != nil { +- event.Error(ctx, "client.showDocument: %v", err) +- } else if res != nil && !res.Success { +- event.Log(ctx, fmt.Sprintf("client declined to open document %v", url)) +- } +-} +- +-func (c *commandHandler) ChangeSignature(ctx context.Context, args command.ChangeSignatureArgs) (*protocol.WorkspaceEdit, error) { +- var result *protocol.WorkspaceEdit +- err := c.run(ctx, commandConfig{ +- forURI: args.RemoveParameter.URI, +- }, func(ctx context.Context, deps commandDeps) error { +- // For now, gopls only supports removing unused parameters. +- changes, err := golang.RemoveUnusedParameter(ctx, deps.fh, args.RemoveParameter.Range, deps.snapshot) +- if err != nil { +- return err - } -- seenVars[v.obj] = typ -- identifier := ast.NewIdent(v.obj.Name()) -- // An identifier must meet three conditions to become a return value of the -- // extracted function. (1) its value must be defined or reassigned within -- // the selection (isAssigned), (2) it must be used at least once after the -- // selection (isUsed), and (3) its first use after the selection -- // cannot be its own reassignment or redefinition (objOverriden). -- if v.obj.Parent() == nil { -- return nil, fmt.Errorf("parent nil") +- edit := protocol.WorkspaceEdit{ +- DocumentChanges: changes, - } -- isUsed, firstUseAfter := objUsed(info, end, v.obj.Parent().End(), v.obj) -- if v.assigned && isUsed && !varOverridden(info, firstUseAfter, v.obj, v.free, outer) { -- returnTypes = append(returnTypes, &ast.Field{Type: typ}) -- returns = append(returns, identifier) -- if !v.free { -- uninitialized = append(uninitialized, v.obj) -- } else if v.obj.Parent().Pos() == startParent.Pos() { -- canRedefineCount++ -- } +- if args.ResolveEdits { +- result = &edit +- return nil - } -- // An identifier must meet two conditions to become a parameter of the -- // extracted function. (1) it must be free (isFree), and (2) its first -- // use within the selection cannot be its own definition (isDefined). -- if v.free && !v.defined { -- // Skip the selector for a method. -- if isMethod && v.obj == receiverObj { -- receiverUsed = true -- continue -- } -- params = append(params, identifier) -- paramTypes = append(paramTypes, &ast.Field{ -- Names: []*ast.Ident{identifier}, -- Type: typ, -- }) +- r, err := c.s.client.ApplyEdit(ctx, &protocol.ApplyWorkspaceEditParams{ +- Edit: edit, +- }) +- if !r.Applied { +- return fmt.Errorf("failed to apply edits: %v", r.FailureReason) - } -- } - -- reorderParams(params, paramTypes) +- return nil +- }) +- return result, err +-} - -- // Find the function literal that encloses the selection. The enclosing function literal -- // may not be the enclosing function declaration (i.e. 'outer'). For example, in the -- // following block: -- // -- // func main() { -- // ast.Inspect(node, func(n ast.Node) bool { -- // v := 1 // this line extracted -- // return true -- // }) -- // } -- // -- // 'outer' is main(). However, the extracted selection most directly belongs to -- // the anonymous function literal, the second argument of ast.Inspect(). We use the -- // enclosing function literal to determine the proper return types for return statements -- // within the selection. We still need the enclosing function declaration because this is -- // the top-level declaration. We inspect the top-level declaration to look for variables -- // as well as for code replacement. -- enclosing := outer.Type -- for _, p := range path { -- if p == enclosing { -- break -- } -- if fl, ok := p.(*ast.FuncLit); ok { -- enclosing = fl.Type -- break -- } -- } +-func (c *commandHandler) DiagnoseFiles(ctx context.Context, args command.DiagnoseFilesArgs) error { +- return c.run(ctx, commandConfig{ +- progress: "Diagnose files", +- }, func(ctx context.Context, _ commandDeps) error { - -- // We put the selection in a constructed file. We can then traverse and edit -- // the extracted selection without modifying the original AST. -- startOffset, endOffset, err := safetoken.Offsets(tok, start, end) -- if err != nil { -- return nil, err -- } -- selection := src[startOffset:endOffset] -- extractedBlock, err := parseBlockStmt(fset, selection) -- if err != nil { -- return nil, err -- } +- // TODO(rfindley): even better would be textDocument/diagnostics (golang/go#60122). +- // Though note that implementing pull diagnostics may cause some servers to +- // request diagnostics in an ad-hoc manner, and break our intentional pacing. - -- // We need to account for return statements in the selected block, as they will complicate -- // the logical flow of the extracted function. See the following example, where ** denotes -- // the range to be extracted. -- // -- // Before: -- // -- // func _() int { -- // a := 1 -- // b := 2 -- // **if a == b { -- // return a -- // }** -- // ... -- // } -- // -- // After: -- // -- // func _() int { -- // a := 1 -- // b := 2 -- // cond0, ret0 := x0(a, b) -- // if cond0 { -- // return ret0 -- // } -- // ... -- // } -- // -- // func x0(a int, b int) (bool, int) { -- // if a == b { -- // return true, a -- // } -- // return false, 0 -- // } -- // -- // We handle returns by adding an additional boolean return value to the extracted function. -- // This bool reports whether the original function would have returned. Because the -- // extracted selection contains a return statement, we must also add the types in the -- // return signature of the enclosing function to the return signature of the -- // extracted function. We then add an extra if statement checking this boolean value -- // in the original function. If the condition is met, the original function should -- // return a value, mimicking the functionality of the original return statement(s) -- // in the selection. -- // -- // If there is a return that is guaranteed to execute (hasNonNestedReturns=true), then -- // we don't need to include this additional condition check and can simply return. -- // -- // Before: -- // -- // func _() int { -- // a := 1 -- // b := 2 -- // **if a == b { -- // return a -- // } -- // return b** -- // } -- // -- // After: -- // -- // func _() int { -- // a := 1 -- // b := 2 -- // return x0(a, b) -- // } -- // -- // func x0(a int, b int) int { -- // if a == b { -- // return a -- // } -- // return b -- // } +- ctx, done := event.Start(ctx, "lsp.server.DiagnoseFiles") +- defer done() - -- var retVars []*returnVariable -- var ifReturn *ast.IfStmt -- if containsReturnStatement { -- if !hasNonNestedReturn { -- // The selected block contained return statements, so we have to modify the -- // signature of the extracted function as described above. Adjust all of -- // the return statements in the extracted function to reflect this change in -- // signature. -- if err := adjustReturnStatements(returnTypes, seenVars, file, pkg, extractedBlock); err != nil { -- return nil, err +- snapshots := make(map[*cache.Snapshot]bool) +- for _, uri := range args.Files { +- fh, snapshot, release, err := c.s.fileOf(ctx, uri) +- if err != nil { +- return err +- } +- if snapshots[snapshot] || snapshot.FileKind(fh) != file.Go { +- release() +- continue - } +- defer release() +- snapshots[snapshot] = true - } -- // Collect the additional return values and types needed to accommodate return -- // statements in the selection. Update the type signature of the extracted -- // function and construct the if statement that will be inserted in the enclosing -- // function. -- retVars, ifReturn, err = generateReturnInfo(enclosing, pkg, path, file, info, start, hasNonNestedReturn) -- if err != nil { -- return nil, err +- +- var wg sync.WaitGroup +- for snapshot := range snapshots { +- snapshot := snapshot +- wg.Add(1) +- go func() { +- defer wg.Done() +- c.s.diagnoseSnapshot(snapshot, nil, 0) +- }() - } -- } +- wg.Wait() - -- // Add a return statement to the end of the new function. This return statement must include -- // the values for the types of the original extracted function signature and (if a return -- // statement is present in the selection) enclosing function signature. -- // This only needs to be done if the selections does not have a non-nested return, otherwise -- // it already terminates with a return statement. -- hasReturnValues := len(returns)+len(retVars) > 0 -- if hasReturnValues && !hasNonNestedReturn { -- extractedBlock.List = append(extractedBlock.List, &ast.ReturnStmt{ -- Results: append(returns, getZeroVals(retVars)...), +- return nil +- }) +-} +- +-func (c *commandHandler) Views(ctx context.Context) ([]command.View, error) { +- var summaries []command.View +- for _, view := range c.s.session.Views() { +- summaries = append(summaries, command.View{ +- Type: view.Type().String(), +- Root: view.Root(), +- Folder: view.Folder().Dir, +- EnvOverlay: view.EnvOverlay(), - }) - } +- return summaries, nil +-} +diff -urN a/gopls/internal/server/completion.go b/gopls/internal/server/completion.go +--- a/gopls/internal/server/completion.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/completion.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,171 +0,0 @@ +-// Copyright 2018 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- // Construct the appropriate call to the extracted function. -- // We must meet two conditions to use ":=" instead of '='. (1) there must be at least -- // one variable on the lhs that is uninitialized (non-free) prior to the assignment. -- // (2) all of the initialized (free) variables on the lhs must be able to be redefined. -- sym := token.ASSIGN -- canDefineCount := len(uninitialized) + canRedefineCount -- canDefine := len(uninitialized)+len(retVars) > 0 && canDefineCount == len(returns) -- if canDefine { -- sym = token.DEFINE -- } -- var name, funName string -- if isMethod { -- name = "newMethod" -- // TODO(suzmue): generate a name that does not conflict for "newMethod". -- funName = name -- } else { -- name = "newFunction" -- funName, _ = generateAvailableIdentifier(start, path, pkg, info, name, 0) -- } -- extractedFunCall := generateFuncCall(hasNonNestedReturn, hasReturnValues, params, -- append(returns, getNames(retVars)...), funName, sym, receiverName) +-package server - -- // Build the extracted function. -- newFunc := &ast.FuncDecl{ -- Name: ast.NewIdent(funName), -- Type: &ast.FuncType{ -- Params: &ast.FieldList{List: paramTypes}, -- Results: &ast.FieldList{List: append(returnTypes, getDecls(retVars)...)}, -- }, -- Body: extractedBlock, -- } -- if isMethod { -- var names []*ast.Ident -- if receiverUsed { -- names = append(names, ast.NewIdent(receiverName)) -- } -- newFunc.Recv = &ast.FieldList{ -- List: []*ast.Field{{ -- Names: names, -- Type: receiver.Type, -- }}, -- } -- } +-import ( +- "context" +- "fmt" +- "strings" - -- // Create variable declarations for any identifiers that need to be initialized prior to -- // calling the extracted function. We do not manually initialize variables if every return -- // value is uninitialized. We can use := to initialize the variables in this situation. -- var declarations []ast.Stmt -- if canDefineCount != len(returns) { -- declarations = initializeVars(uninitialized, retVars, seenUninitialized, seenVars) -- } +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/golang/completion" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/settings" +- "golang.org/x/tools/gopls/internal/telemetry" +- "golang.org/x/tools/gopls/internal/template" +- "golang.org/x/tools/gopls/internal/work" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/tag" +-) - -- var declBuf, replaceBuf, newFuncBuf, ifBuf, commentBuf bytes.Buffer -- if err := format.Node(&declBuf, fset, declarations); err != nil { -- return nil, err -- } -- if err := format.Node(&replaceBuf, fset, extractedFunCall); err != nil { +-func (s *server) Completion(ctx context.Context, params *protocol.CompletionParams) (_ *protocol.CompletionList, rerr error) { +- recordLatency := telemetry.StartLatencyTimer("completion") +- defer func() { +- recordLatency(ctx, rerr) +- }() +- +- ctx, done := event.Start(ctx, "lsp.Server.completion", tag.URI.Of(params.TextDocument.URI)) +- defer done() +- +- fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI) +- if err != nil { - return nil, err - } -- if ifReturn != nil { -- if err := format.Node(&ifBuf, fset, ifReturn); err != nil { -- return nil, err +- defer release() +- +- var candidates []completion.CompletionItem +- var surrounding *completion.Selection +- switch snapshot.FileKind(fh) { +- case file.Go: +- candidates, surrounding, err = completion.Completion(ctx, snapshot, fh, params.Position, params.Context) +- case file.Mod: +- candidates, surrounding = nil, nil +- case file.Work: +- cl, err := work.Completion(ctx, snapshot, fh, params.Position) +- if err != nil { +- break - } -- } -- if err := format.Node(&newFuncBuf, fset, newFunc); err != nil { -- return nil, err -- } -- // Find all the comments within the range and print them to be put somewhere. -- // TODO(suzmue): print these in the extracted function at the correct place. -- for _, cg := range file.Comments { -- if cg.Pos().IsValid() && cg.Pos() < end && cg.Pos() >= start { -- for _, c := range cg.List { -- fmt.Fprintln(&commentBuf, c.Text) -- } +- return cl, nil +- case file.Tmpl: +- var cl *protocol.CompletionList +- cl, err = template.Completion(ctx, snapshot, fh, params.Position, params.Context) +- if err != nil { +- break // use common error handling, candidates==nil - } +- return cl, nil - } -- -- // We're going to replace the whole enclosing function, -- // so preserve the text before and after the selected block. -- outerStart, outerEnd, err := safetoken.Offsets(tok, outer.Pos(), outer.End()) - if err != nil { -- return nil, err +- event.Error(ctx, "no completions found", err, tag.Position.Of(params.Position)) - } -- before := src[outerStart:startOffset] -- after := src[endOffset:outerEnd] -- indent, err := calculateIndentation(src, tok, node) -- if err != nil { -- return nil, err +- if candidates == nil { +- complEmpty.Inc() +- return &protocol.CompletionList{ +- IsIncomplete: true, +- Items: []protocol.CompletionItem{}, +- }, nil - } -- newLineIndent := "\n" + indent - -- var fullReplacement strings.Builder -- fullReplacement.Write(before) -- if commentBuf.Len() > 0 { -- comments := strings.ReplaceAll(commentBuf.String(), "\n", newLineIndent) -- fullReplacement.WriteString(comments) -- } -- if declBuf.Len() > 0 { // add any initializations, if needed -- initializations := strings.ReplaceAll(declBuf.String(), "\n", newLineIndent) + -- newLineIndent -- fullReplacement.WriteString(initializations) -- } -- fullReplacement.Write(replaceBuf.Bytes()) // call the extracted function -- if ifBuf.Len() > 0 { // add the if statement below the function call, if needed -- ifstatement := newLineIndent + -- strings.ReplaceAll(ifBuf.String(), "\n", newLineIndent) -- fullReplacement.WriteString(ifstatement) +- rng, err := surrounding.Range() +- if err != nil { +- return nil, err - } -- fullReplacement.Write(after) -- fullReplacement.WriteString("\n\n") // add newlines after the enclosing function -- fullReplacement.Write(newFuncBuf.Bytes()) // insert the extracted function - -- return &analysis.SuggestedFix{ -- TextEdits: []analysis.TextEdit{{ -- Pos: outer.Pos(), -- End: outer.End(), -- NewText: []byte(fullReplacement.String()), -- }}, -- }, nil --} +- // When using deep completions/fuzzy matching, report results as incomplete so +- // client fetches updated completions after every key stroke. +- options := snapshot.Options() +- incompleteResults := options.DeepCompletion || options.Matcher == settings.Fuzzy - --// isSelector reports if e is the selector expr , . --func isSelector(e ast.Expr, x, sel string) bool { -- selectorExpr, ok := e.(*ast.SelectorExpr) -- if !ok { -- return false +- items := toProtocolCompletionItems(candidates, rng, options) +- if snapshot.FileKind(fh) == file.Go { +- s.saveLastCompletion(fh.URI(), fh.Version(), items, params.Position) - } -- ident, ok := selectorExpr.X.(*ast.Ident) -- if !ok { -- return false +- +- if len(items) > 10 { +- // TODO(pjw): long completions are ok for field lists +- complLong.Inc() +- } else { +- complShort.Inc() - } -- return ident.Name == x && selectorExpr.Sel.Name == sel +- return &protocol.CompletionList{ +- IsIncomplete: incompleteResults, +- Items: items, +- }, nil -} - --// reorderParams reorders the given parameters in-place to follow common Go conventions. --func reorderParams(params []ast.Expr, paramTypes []*ast.Field) { -- // Move Context parameter (if any) to front. -- for i, t := range paramTypes { -- if isSelector(t.Type, "context", "Context") { -- p, t := params[i], paramTypes[i] -- copy(params[1:], params[:i]) -- copy(paramTypes[1:], paramTypes[:i]) -- params[0], paramTypes[0] = p, t -- break -- } -- } +-func (s *server) saveLastCompletion(uri protocol.DocumentURI, version int32, items []protocol.CompletionItem, pos protocol.Position) { +- s.efficacyMu.Lock() +- defer s.efficacyMu.Unlock() +- s.efficacyVersion = version +- s.efficacyURI = uri +- s.efficacyPos = pos +- s.efficacyItems = items -} - --// adjustRangeForCommentsAndWhiteSpace adjusts the given range to exclude unnecessary leading or --// trailing whitespace characters from selection as well as leading or trailing comments. --// In the following example, each line of the if statement is indented once. There are also two --// extra spaces after the sclosing bracket before the line break and a comment. --// --// \tif (true) { --// \t _ = 1 --// \t} // hello \n --// --// By default, a valid range begins at 'if' and ends at the first whitespace character --// after the '}'. But, users are likely to highlight full lines rather than adjusting --// their cursors for whitespace. To support this use case, we must manually adjust the --// ranges to match the correct AST node. In this particular example, we would adjust --// rng.Start forward to the start of 'if' and rng.End backward to after '}'. --func adjustRangeForCommentsAndWhiteSpace(tok *token.File, start, end token.Pos, content []byte, file *ast.File) (token.Pos, token.Pos, error) { -- // Adjust the end of the range to after leading whitespace and comments. -- prevStart := token.NoPos -- startComment := sort.Search(len(file.Comments), func(i int) bool { -- // Find the index for the first comment that ends after range start. -- return file.Comments[i].End() > start -- }) -- for prevStart != start { -- prevStart = start -- // If start is within a comment, move start to the end -- // of the comment group. -- if startComment < len(file.Comments) && file.Comments[startComment].Pos() <= start && start < file.Comments[startComment].End() { -- start = file.Comments[startComment].End() -- startComment++ +-func toProtocolCompletionItems(candidates []completion.CompletionItem, rng protocol.Range, options *settings.Options) []protocol.CompletionItem { +- var ( +- items = make([]protocol.CompletionItem, 0, len(candidates)) +- numDeepCompletionsSeen int +- ) +- for i, candidate := range candidates { +- // Limit the number of deep completions to not overwhelm the user in cases +- // with dozens of deep completion matches. +- if candidate.Depth > 0 { +- if !options.DeepCompletion { +- continue +- } +- if numDeepCompletionsSeen >= completion.MaxDeepCompletions { +- continue +- } +- numDeepCompletionsSeen++ - } -- // Move forwards to find a non-whitespace character. -- offset, err := safetoken.Offset(tok, start) -- if err != nil { -- return 0, 0, err +- insertText := candidate.InsertText +- if options.InsertTextFormat == protocol.SnippetTextFormat { +- insertText = candidate.Snippet() - } -- for offset < len(content) && isGoWhiteSpace(content[offset]) { -- offset++ +- +- // This can happen if the client has snippets disabled but the +- // candidate only supports snippet insertion. +- if insertText == "" { +- continue - } -- start = tok.Pos(offset) -- } - -- // Adjust the end of the range to before trailing whitespace and comments. -- prevEnd := token.NoPos -- endComment := sort.Search(len(file.Comments), func(i int) bool { -- // Find the index for the first comment that ends after the range end. -- return file.Comments[i].End() >= end -- }) -- // Search will return n if not found, so we need to adjust if there are no -- // comments that would match. -- if endComment == len(file.Comments) { -- endComment = -1 -- } -- for prevEnd != end { -- prevEnd = end -- // If end is within a comment, move end to the start -- // of the comment group. -- if endComment >= 0 && file.Comments[endComment].Pos() < end && end <= file.Comments[endComment].End() { -- end = file.Comments[endComment].Pos() -- endComment-- +- doc := &protocol.Or_CompletionItem_documentation{ +- Value: protocol.MarkupContent{ +- Kind: protocol.Markdown, +- Value: golang.CommentToMarkdown(candidate.Documentation, options), +- }, - } -- // Move backwards to find a non-whitespace character. -- offset, err := safetoken.Offset(tok, end) -- if err != nil { -- return 0, 0, err +- if options.PreferredContentFormat != protocol.Markdown { +- doc.Value = candidate.Documentation - } -- for offset > 0 && isGoWhiteSpace(content[offset-1]) { -- offset-- +- item := protocol.CompletionItem{ +- Label: candidate.Label, +- Detail: candidate.Detail, +- Kind: candidate.Kind, +- TextEdit: &protocol.TextEdit{ +- NewText: insertText, +- Range: rng, +- }, +- InsertTextFormat: &options.InsertTextFormat, +- AdditionalTextEdits: candidate.AdditionalTextEdits, +- // This is a hack so that the client sorts completion results in the order +- // according to their score. This can be removed upon the resolution of +- // https://github.com/Microsoft/language-server-protocol/issues/348. +- SortText: fmt.Sprintf("%05d", i), +- +- // Trim operators (VSCode doesn't like weird characters in +- // filterText). +- FilterText: strings.TrimLeft(candidate.InsertText, "&*"), +- +- Preselect: i == 0, +- Documentation: doc, +- Tags: protocol.NonNilSlice(candidate.Tags), +- Deprecated: candidate.Deprecated, - } -- end = tok.Pos(offset) +- items = append(items, item) - } -- -- return start, end, nil +- return items -} +diff -urN a/gopls/internal/server/counters.go b/gopls/internal/server/counters.go +--- a/gopls/internal/server/counters.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/counters.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,28 +0,0 @@ +-// Copyright 2024 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// isGoWhiteSpace returns true if b is a considered white space in --// Go as defined by scanner.GoWhitespace. --func isGoWhiteSpace(b byte) bool { -- return uint64(scanner.GoWhitespace)&(1<10") // returning more than 10 items +- +- changeFull = counter.New("gopls/completion/used:unknown") // full file change in didChange +- complUnused = counter.New("gopls/completion/used:no") // did not use a completion +- complUsed = counter.New("gopls/completion/used:yes") // used a completion +- +- // exported so tests can verify that counters are incremented +- CompletionCounters = []*counter.Counter{ +- complEmpty, +- complShort, +- complLong, +- changeFull, +- complUnused, +- complUsed, +- } +-) +diff -urN a/gopls/internal/server/definition.go b/gopls/internal/server/definition.go +--- a/gopls/internal/server/definition.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/definition.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,61 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package server +- +-import ( +- "context" +- "fmt" +- +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/telemetry" +- "golang.org/x/tools/gopls/internal/template" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/tag" +-) +- +-func (s *server) Definition(ctx context.Context, params *protocol.DefinitionParams) (_ []protocol.Location, rerr error) { +- recordLatency := telemetry.StartLatencyTimer("definition") +- defer func() { +- recordLatency(ctx, rerr) +- }() +- +- ctx, done := event.Start(ctx, "lsp.Server.definition", tag.URI.Of(params.TextDocument.URI)) +- defer done() +- +- // TODO(rfindley): definition requests should be multiplexed across all views. +- fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI) +- if err != nil { +- return nil, err +- } +- defer release() +- switch kind := snapshot.FileKind(fh); kind { +- case file.Tmpl: +- return template.Definition(snapshot, fh, params.Position) +- case file.Go: +- return golang.Definition(ctx, snapshot, fh, params.Position) +- default: +- return nil, fmt.Errorf("can't find definitions for file type %s", kind) +- } -} - --// findParent finds the parent AST node of the given target node, if the target is a --// descendant of the starting node. --func findParent(start ast.Node, target ast.Node) ast.Node { -- var parent ast.Node -- analysisinternal.WalkASTWithParent(start, func(n, p ast.Node) bool { -- if n == target { -- parent = p -- return false -- } -- return true -- }) -- return parent +-func (s *server) TypeDefinition(ctx context.Context, params *protocol.TypeDefinitionParams) ([]protocol.Location, error) { +- ctx, done := event.Start(ctx, "lsp.Server.typeDefinition", tag.URI.Of(params.TextDocument.URI)) +- defer done() +- +- // TODO(rfindley): type definition requests should be multiplexed across all views. +- fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI) +- if err != nil { +- return nil, err +- } +- defer release() +- switch kind := snapshot.FileKind(fh); kind { +- case file.Go: +- return golang.TypeDefinition(ctx, snapshot, fh, params.Position) +- default: +- return nil, fmt.Errorf("can't find type definitions for file type %s", kind) +- } -} +diff -urN a/gopls/internal/server/diagnostics.go b/gopls/internal/server/diagnostics.go +--- a/gopls/internal/server/diagnostics.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/diagnostics.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,1000 +0,0 @@ +-// Copyright 2018 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// variable describes the status of a variable within a selection. --type variable struct { -- obj types.Object +-package server - -- // free reports whether the variable is a free variable, meaning it should -- // be a parameter to the extracted function. -- free bool +-import ( +- "context" +- "crypto/sha256" +- "errors" +- "fmt" +- "os" +- "path/filepath" +- "runtime" +- "sort" +- "strings" +- "sync" +- "time" - -- // assigned reports whether the variable is assigned to in the selection. -- assigned bool +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/mod" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/settings" +- "golang.org/x/tools/gopls/internal/template" +- "golang.org/x/tools/gopls/internal/util/maps" +- "golang.org/x/tools/gopls/internal/work" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/keys" +- "golang.org/x/tools/internal/event/tag" +-) - -- // defined reports whether the variable is defined in the selection. -- defined bool +-// fileDiagnostics holds the current state of published diagnostics for a file. +-type fileDiagnostics struct { +- publishedHash file.Hash // hash of the last set of diagnostics published for this URI +- mustPublish bool // if set, publish diagnostics even if they haven't changed +- +- // Orphaned file diagnostics are not necessarily associated with any *View +- // (since they are orphaned). Instead, keep track of the modification ID at +- // which they were orphaned (see server.lastModificationID). +- orphanedAt uint64 // modification ID at which this file was orphaned. +- orphanedFileDiagnostics []*cache.Diagnostic +- +- // Files may have their diagnostics computed by multiple views, and so +- // diagnostics are organized by View. See the documentation for update for more +- // details about how the set of file diagnostics evolves over time. +- byView map[*cache.View]viewDiagnostics -} - --// collectFreeVars maps each identifier in the given range to whether it is "free." --// Given a range, a variable in that range is defined as "free" if it is declared --// outside of the range and neither at the file scope nor package scope. These free --// variables will be used as arguments in the extracted function. It also returns a --// list of identifiers that may need to be returned by the extracted function. --// Some of the code in this function has been adapted from tools/cmd/guru/freevars.go. --func collectFreeVars(info *types.Info, file *ast.File, fileScope, pkgScope *types.Scope, start, end token.Pos, node ast.Node) ([]*variable, error) { -- // id returns non-nil if n denotes an object that is referenced by the span -- // and defined either within the span or in the lexical environment. The bool -- // return value acts as an indicator for where it was defined. -- id := func(n *ast.Ident) (types.Object, bool) { -- obj := info.Uses[n] -- if obj == nil { -- return info.Defs[n], false -- } -- if obj.Name() == "_" { -- return nil, false // exclude objects denoting '_' -- } -- if _, ok := obj.(*types.PkgName); ok { -- return nil, false // imported package -- } -- if !(file.Pos() <= obj.Pos() && obj.Pos() <= file.End()) { -- return nil, false // not defined in this file -- } -- scope := obj.Parent() -- if scope == nil { -- return nil, false // e.g. interface method, struct field -- } -- if scope == fileScope || scope == pkgScope { -- return nil, false // defined at file or package scope -- } -- if start <= obj.Pos() && obj.Pos() <= end { -- return obj, false // defined within selection => not free -- } -- return obj, true +-// viewDiagnostics holds a set of file diagnostics computed from a given View. +-type viewDiagnostics struct { +- snapshot uint64 // snapshot sequence ID +- version int32 // file version +- diagnostics []*cache.Diagnostic +-} +- +-// common types; for brevity +-type ( +- viewSet = map[*cache.View]unit +- diagMap = map[protocol.DocumentURI][]*cache.Diagnostic +-) +- +-// hashDiagnostic computes a hash to identify a diagnostic. +-// The hash is for deduplicating within a file, +-// so it need not incorporate d.URI. +-func hashDiagnostic(d *cache.Diagnostic) file.Hash { +- h := sha256.New() +- for _, t := range d.Tags { +- fmt.Fprintf(h, "tag: %s\n", t) - } -- // sel returns non-nil if n denotes a selection o.x.y that is referenced by the -- // span and defined either within the span or in the lexical environment. The bool -- // return value acts as an indicator for where it was defined. -- var sel func(n *ast.SelectorExpr) (types.Object, bool) -- sel = func(n *ast.SelectorExpr) (types.Object, bool) { -- switch x := astutil.Unparen(n.X).(type) { -- case *ast.SelectorExpr: -- return sel(x) -- case *ast.Ident: -- return id(x) -- } -- return nil, false +- for _, r := range d.Related { +- fmt.Fprintf(h, "related: %s %s %s\n", r.Location.URI, r.Message, r.Location.Range) - } -- seen := make(map[types.Object]*variable) -- firstUseIn := make(map[types.Object]token.Pos) -- var vars []types.Object -- ast.Inspect(node, func(n ast.Node) bool { -- if n == nil { -- return false -- } -- if start <= n.Pos() && n.End() <= end { -- var obj types.Object -- var isFree, prune bool -- switch n := n.(type) { -- case *ast.Ident: -- obj, isFree = id(n) -- case *ast.SelectorExpr: -- obj, isFree = sel(n) -- prune = true -- } -- if obj != nil { -- seen[obj] = &variable{ -- obj: obj, -- free: isFree, -- } -- vars = append(vars, obj) -- // Find the first time that the object is used in the selection. -- first, ok := firstUseIn[obj] -- if !ok || n.Pos() < first { -- firstUseIn[obj] = n.Pos() -- } -- if prune { -- return false -- } -- } -- } -- return n.Pos() <= end -- }) +- fmt.Fprintf(h, "code: %s\n", d.Code) +- fmt.Fprintf(h, "codeHref: %s\n", d.CodeHref) +- fmt.Fprintf(h, "message: %s\n", d.Message) +- fmt.Fprintf(h, "range: %s\n", d.Range) +- fmt.Fprintf(h, "severity: %s\n", d.Severity) +- fmt.Fprintf(h, "source: %s\n", d.Source) +- if d.BundledFixes != nil { +- fmt.Fprintf(h, "fixes: %s\n", *d.BundledFixes) +- } +- var hash [sha256.Size]byte +- h.Sum(hash[:0]) +- return hash +-} - -- // Find identifiers that are initialized or whose values are altered at some -- // point in the selected block. For example, in a selected block from lines 2-4, -- // variables x, y, and z are included in assigned. However, in a selected block -- // from lines 3-4, only variables y and z are included in assigned. -- // -- // 1: var a int -- // 2: var x int -- // 3: y := 3 -- // 4: z := x + a -- // -- ast.Inspect(node, func(n ast.Node) bool { -- if n == nil { -- return false -- } -- if n.Pos() < start || n.End() > end { -- return n.Pos() <= end +-func sortDiagnostics(d []*cache.Diagnostic) { +- sort.Slice(d, func(i int, j int) bool { +- a, b := d[i], d[j] +- if r := protocol.CompareRange(a.Range, b.Range); r != 0 { +- return r < 0 - } -- switch n := n.(type) { -- case *ast.AssignStmt: -- for _, assignment := range n.Lhs { -- lhs, ok := assignment.(*ast.Ident) -- if !ok { -- continue -- } -- obj, _ := id(lhs) -- if obj == nil { -- continue -- } -- if _, ok := seen[obj]; !ok { -- continue -- } -- seen[obj].assigned = true -- if n.Tok != token.DEFINE { -- continue -- } -- // Find identifiers that are defined prior to being used -- // elsewhere in the selection. -- // TODO: Include identifiers that are assigned prior to being -- // used elsewhere in the selection. Then, change the assignment -- // to a definition in the extracted function. -- if firstUseIn[obj] != lhs.Pos() { -- continue -- } -- // Ensure that the object is not used in its own re-definition. -- // For example: -- // var f float64 -- // f, e := math.Frexp(f) -- for _, expr := range n.Rhs { -- if referencesObj(info, expr, obj) { -- continue -- } -- if _, ok := seen[obj]; !ok { -- continue -- } -- seen[obj].defined = true -- break -- } -- } -- return false -- case *ast.DeclStmt: -- gen, ok := n.Decl.(*ast.GenDecl) -- if !ok { -- return false -- } -- for _, spec := range gen.Specs { -- vSpecs, ok := spec.(*ast.ValueSpec) -- if !ok { -- continue -- } -- for _, vSpec := range vSpecs.Names { -- obj, _ := id(vSpec) -- if obj == nil { -- continue -- } -- if _, ok := seen[obj]; !ok { -- continue -- } -- seen[obj].assigned = true -- } -- } -- return false -- case *ast.IncDecStmt: -- if ident, ok := n.X.(*ast.Ident); !ok { -- return false -- } else if obj, _ := id(ident); obj == nil { -- return false -- } else { -- if _, ok := seen[obj]; !ok { -- return false -- } -- seen[obj].assigned = true -- } +- if a.Source != b.Source { +- return a.Source < b.Source - } -- return true +- return a.Message < b.Message - }) -- var variables []*variable -- for _, obj := range vars { -- v, ok := seen[obj] -- if !ok { -- return nil, fmt.Errorf("no seen types.Object for %v", obj) -- } -- variables = append(variables, v) -- } -- return variables, nil -} - --// referencesObj checks whether the given object appears in the given expression. --func referencesObj(info *types.Info, expr ast.Expr, obj types.Object) bool { -- var hasObj bool -- ast.Inspect(expr, func(n ast.Node) bool { -- if n == nil { -- return false -- } -- ident, ok := n.(*ast.Ident) -- if !ok { -- return true +-func (s *server) diagnoseChangedViews(ctx context.Context, modID uint64, lastChange map[*cache.View][]protocol.DocumentURI, cause ModificationSource) { +- // Collect views needing diagnosis. +- s.modificationMu.Lock() +- needsDiagnosis := maps.Keys(s.viewsToDiagnose) +- s.modificationMu.Unlock() +- +- // Diagnose views concurrently. +- var wg sync.WaitGroup +- for _, v := range needsDiagnosis { +- v := v +- snapshot, release, err := v.Snapshot() +- if err != nil { +- s.modificationMu.Lock() +- // The View is shut down. Unlike below, no need to check +- // s.needsDiagnosis[v], since the view can never be diagnosed. +- delete(s.viewsToDiagnose, v) +- s.modificationMu.Unlock() +- continue - } -- objUse := info.Uses[ident] -- if obj == objUse { -- hasObj = true -- return false +- +- // Collect uris for fast diagnosis. We only care about the most recent +- // change here, because this is just an optimization for the case where the +- // user is actively editing a single file. +- uris := lastChange[v] +- if snapshot.Options().DiagnosticsTrigger == settings.DiagnosticsOnSave && cause == FromDidChange { +- // The user requested to update the diagnostics only on save. +- // Do not diagnose yet. +- release() +- continue - } -- return false -- }) -- return hasObj --} - --type fnExtractParams struct { -- tok *token.File -- start, end token.Pos -- path []ast.Node -- outer *ast.FuncDecl -- node ast.Node --} +- wg.Add(1) +- go func(snapshot *cache.Snapshot, uris []protocol.DocumentURI) { +- defer release() +- defer wg.Done() +- s.diagnoseSnapshot(snapshot, uris, snapshot.Options().DiagnosticsDelay) +- s.modificationMu.Lock() - --// CanExtractFunction reports whether the code in the given range can be --// extracted to a function. --func CanExtractFunction(tok *token.File, start, end token.Pos, src []byte, file *ast.File) (*fnExtractParams, bool, bool, error) { -- if start == end { -- return nil, false, false, fmt.Errorf("start and end are equal") -- } -- var err error -- start, end, err = adjustRangeForCommentsAndWhiteSpace(tok, start, end, src, file) -- if err != nil { -- return nil, false, false, err +- // Only remove v from s.viewsToDiagnose if the snapshot is not cancelled. +- // This ensures that the snapshot was not cloned before its state was +- // fully evaluated, and therefore avoids missing a change that was +- // irrelevant to an incomplete snapshot. +- // +- // See the documentation for s.viewsToDiagnose for details. +- if snapshot.BackgroundContext().Err() == nil && s.viewsToDiagnose[v] <= modID { +- delete(s.viewsToDiagnose, v) +- } +- s.modificationMu.Unlock() +- }(snapshot, uris) - } -- path, _ := astutil.PathEnclosingInterval(file, start, end) -- if len(path) == 0 { -- return nil, false, false, fmt.Errorf("no path enclosing interval") +- +- wg.Wait() +- +- // Diagnose orphaned files for the session. +- orphanedFileDiagnostics, err := s.session.OrphanedFileDiagnostics(ctx) +- if err == nil { +- err = s.updateOrphanedFileDiagnostics(ctx, modID, orphanedFileDiagnostics) - } -- // Node that encloses the selection must be a statement. -- // TODO: Support function extraction for an expression. -- _, ok := path[0].(ast.Stmt) -- if !ok { -- return nil, false, false, fmt.Errorf("node is not a statement") +- if err != nil { +- if ctx.Err() == nil { +- event.Error(ctx, "warning: while diagnosing orphaned files", err) +- } - } +-} +- +-// diagnoseSnapshot computes and publishes diagnostics for the given snapshot. +-// +-// If delay is non-zero, computing diagnostics does not start until after this +-// delay has expired, to allow work to be cancelled by subsequent changes. +-// +-// If changedURIs is non-empty, it is a set of recently changed files that +-// should be diagnosed immediately, and onDisk reports whether these file +-// changes came from a change to on-disk files. +-func (s *server) diagnoseSnapshot(snapshot *cache.Snapshot, changedURIs []protocol.DocumentURI, delay time.Duration) { +- ctx := snapshot.BackgroundContext() +- ctx, done := event.Start(ctx, "Server.diagnoseSnapshot", snapshot.Labels()...) +- defer done() - -- // Find the function declaration that encloses the selection. -- var outer *ast.FuncDecl -- for _, p := range path { -- if p, ok := p.(*ast.FuncDecl); ok { -- outer = p -- break +- if delay > 0 { +- // 2-phase diagnostics. +- // +- // The first phase just parses and type-checks (but +- // does not analyze) packages directly affected by +- // file modifications. +- // +- // The second phase runs after the delay, and does everything. +- // +- // We wait a brief delay before the first phase, to allow higher priority +- // work such as autocompletion to acquire the type checking mutex (though +- // typically both diagnosing changed files and performing autocompletion +- // will be doing the same work: recomputing active packages). +- const minDelay = 20 * time.Millisecond +- select { +- case <-time.After(minDelay): +- case <-ctx.Done(): +- return - } -- } -- if outer == nil { -- return nil, false, false, fmt.Errorf("no enclosing function") -- } - -- // Find the nodes at the start and end of the selection. -- var startNode, endNode ast.Node -- ast.Inspect(outer, func(n ast.Node) bool { -- if n == nil { -- return false +- if len(changedURIs) > 0 { +- diagnostics, err := s.diagnoseChangedFiles(ctx, snapshot, changedURIs) +- if err != nil { +- if ctx.Err() == nil { +- event.Error(ctx, "warning: while diagnosing changed files", err, snapshot.Labels()...) +- } +- return +- } +- s.updateDiagnostics(ctx, snapshot, diagnostics, false) - } -- // Do not override 'start' with a node that begins at the same location -- // but is nested further from 'outer'. -- if startNode == nil && n.Pos() == start && n.End() <= end { -- startNode = n +- +- if delay < minDelay { +- delay = 0 +- } else { +- delay -= minDelay - } -- if endNode == nil && n.End() == end && n.Pos() >= start { -- endNode = n +- +- select { +- case <-time.After(delay): +- case <-ctx.Done(): +- return - } -- return n.Pos() <= end -- }) -- if startNode == nil || endNode == nil { -- return nil, false, false, fmt.Errorf("range does not map to AST nodes") - } -- // If the region is a blockStmt, use the first and last nodes in the block -- // statement. -- // { ... } => { ... } -- if blockStmt, ok := startNode.(*ast.BlockStmt); ok { -- if len(blockStmt.List) == 0 { -- return nil, false, false, fmt.Errorf("range maps to empty block statement") +- +- diagnostics, err := s.diagnose(ctx, snapshot) +- if err != nil { +- if ctx.Err() == nil { +- event.Error(ctx, "warning: while diagnosing snapshot", err, snapshot.Labels()...) - } -- startNode, endNode = blockStmt.List[0], blockStmt.List[len(blockStmt.List)-1] -- start, end = startNode.Pos(), endNode.End() +- return - } -- return &fnExtractParams{ -- tok: tok, -- start: start, -- end: end, -- path: path, -- outer: outer, -- node: startNode, -- }, true, outer.Recv != nil, nil +- s.updateDiagnostics(ctx, snapshot, diagnostics, true) -} - --// objUsed checks if the object is used within the range. It returns the first --// occurrence of the object in the range, if it exists. --func objUsed(info *types.Info, start, end token.Pos, obj types.Object) (bool, *ast.Ident) { -- var firstUse *ast.Ident -- for id, objUse := range info.Uses { -- if obj != objUse { +-func (s *server) diagnoseChangedFiles(ctx context.Context, snapshot *cache.Snapshot, uris []protocol.DocumentURI) (diagMap, error) { +- ctx, done := event.Start(ctx, "Server.diagnoseChangedFiles", snapshot.Labels()...) +- defer done() +- +- toDiagnose := make(map[metadata.PackageID]*metadata.Package) +- for _, uri := range uris { +- // If the file is not open, don't diagnose its package. +- // +- // We don't care about fast diagnostics for files that are no longer open, +- // because the user isn't looking at them. Also, explicitly requesting a +- // package can lead to "command-line-arguments" packages if the file isn't +- // covered by the current View. By avoiding requesting packages for e.g. +- // unrelated file movement, we can minimize these unnecessary packages. +- if !snapshot.IsOpen(uri) { - continue - } -- if id.Pos() < start || id.End() > end { +- // If the file is not known to the snapshot (e.g., if it was deleted), +- // don't diagnose it. +- if snapshot.FindFile(uri) == nil { - continue - } -- if firstUse == nil || id.Pos() < firstUse.Pos() { -- firstUse = id +- +- // Don't request type-checking for builtin.go: it's not a real package. +- if snapshot.IsBuiltin(uri) { +- continue - } -- } -- return firstUse != nil, firstUse --} - --// varOverridden traverses the given AST node until we find the given identifier. Then, we --// examine the occurrence of the given identifier and check for (1) whether the identifier --// is being redefined. If the identifier is free, we also check for (2) whether the identifier --// is being reassigned. We will not include an identifier in the return statement of the --// extracted function if it meets one of the above conditions. --func varOverridden(info *types.Info, firstUse *ast.Ident, obj types.Object, isFree bool, node ast.Node) bool { -- var isOverriden bool -- ast.Inspect(node, func(n ast.Node) bool { -- if n == nil { -- return false +- // Don't diagnose files that are ignored by `go list` (e.g. testdata). +- if snapshot.IgnoredFile(uri) { +- continue - } -- assignment, ok := n.(*ast.AssignStmt) -- if !ok { -- return true +- +- // Find all packages that include this file and diagnose them in parallel. +- meta, err := golang.NarrowestMetadataForFile(ctx, snapshot, uri) +- if err != nil { +- if ctx.Err() != nil { +- return nil, ctx.Err() +- } +- // TODO(findleyr): we should probably do something with the error here, +- // but as of now this can fail repeatedly if load fails, so can be too +- // noisy to log (and we'll handle things later in the slow pass). +- continue - } -- // A free variable is initialized prior to the selection. We can always reassign -- // this variable after the selection because it has already been defined. -- // Conversely, a non-free variable is initialized within the selection. Thus, we -- // cannot reassign this variable after the selection unless it is initialized and -- // returned by the extracted function. -- if !isFree && assignment.Tok == token.ASSIGN { -- return false +- // golang/go#65801: only diagnose changes to workspace packages. Otherwise, +- // diagnostics will be unstable, as the slow-path diagnostics will erase +- // them. +- if snapshot.IsWorkspacePackage(ctx, meta.ID) { +- toDiagnose[meta.ID] = meta - } -- for _, assigned := range assignment.Lhs { -- ident, ok := assigned.(*ast.Ident) -- // Check if we found the first use of the identifier. -- if !ok || ident != firstUse { -- continue -- } -- objUse := info.Uses[ident] -- if objUse == nil || objUse != obj { -- continue -- } -- // Ensure that the object is not used in its own definition. -- // For example: -- // var f float64 -- // f, e := math.Frexp(f) -- for _, expr := range assignment.Rhs { -- if referencesObj(info, expr, obj) { -- return false -- } +- } +- diags, err := snapshot.PackageDiagnostics(ctx, maps.Keys(toDiagnose)...) +- if err != nil { +- if ctx.Err() == nil { +- event.Error(ctx, "warning: diagnostics failed", err, snapshot.Labels()...) +- } +- return nil, err +- } +- // golang/go#59587: guarantee that we compute type-checking diagnostics +- // for every compiled package file, otherwise diagnostics won't be quickly +- // cleared following a fix. +- for _, meta := range toDiagnose { +- for _, uri := range meta.CompiledGoFiles { +- if _, ok := diags[uri]; !ok { +- diags[uri] = nil - } -- isOverriden = true -- return false - } -- return false -- }) -- return isOverriden +- } +- return diags, nil -} - --// parseBlockStmt generates an AST file from the given text. We then return the portion of the --// file that represents the text. --func parseBlockStmt(fset *token.FileSet, src []byte) (*ast.BlockStmt, error) { -- text := "package main\nfunc _() { " + string(src) + " }" -- extract, err := parser.ParseFile(fset, "", text, 0) -- if err != nil { -- return nil, err +-func (s *server) diagnose(ctx context.Context, snapshot *cache.Snapshot) (diagMap, error) { +- ctx, done := event.Start(ctx, "Server.diagnose", snapshot.Labels()...) +- defer done() +- +- // Wait for a free diagnostics slot. +- // TODO(adonovan): opt: shouldn't it be the analysis implementation's +- // job to de-dup and limit resource consumption? In any case this +- // function spends most its time waiting for awaitLoaded, at +- // least initially. +- select { +- case <-ctx.Done(): +- return nil, ctx.Err() +- case s.diagnosticsSema <- struct{}{}: - } -- if len(extract.Decls) == 0 { -- return nil, fmt.Errorf("parsed file does not contain any declarations") +- defer func() { +- <-s.diagnosticsSema +- }() +- +- var ( +- diagnosticsMu sync.Mutex +- diagnostics = make(diagMap) +- ) +- // common code for dispatching diagnostics +- store := func(operation string, diagsByFile diagMap, err error) { +- if err != nil { +- if ctx.Err() == nil { +- event.Error(ctx, "warning: while "+operation, err, snapshot.Labels()...) +- } +- return +- } +- diagnosticsMu.Lock() +- defer diagnosticsMu.Unlock() +- for uri, diags := range diagsByFile { +- diagnostics[uri] = append(diagnostics[uri], diags...) +- } - } -- decl, ok := extract.Decls[0].(*ast.FuncDecl) -- if !ok { -- return nil, fmt.Errorf("parsed file does not contain expected function declaration") +- +- // Diagnostics below are organized by increasing specificity: +- // go.work > mod > mod upgrade > mod vuln > package, etc. +- +- // Diagnose go.work file. +- workReports, workErr := work.Diagnostics(ctx, snapshot) +- if ctx.Err() != nil { +- return nil, ctx.Err() - } -- if decl.Body == nil { -- return nil, fmt.Errorf("extracted function has no body") +- store("diagnosing go.work file", workReports, workErr) +- +- // Diagnose go.mod file. +- modReports, modErr := mod.ParseDiagnostics(ctx, snapshot) +- if ctx.Err() != nil { +- return nil, ctx.Err() - } -- return decl.Body, nil --} +- store("diagnosing go.mod file", modReports, modErr) - --// generateReturnInfo generates the information we need to adjust the return statements and --// signature of the extracted function. We prepare names, signatures, and "zero values" that --// represent the new variables. We also use this information to construct the if statement that --// is inserted below the call to the extracted function. --func generateReturnInfo(enclosing *ast.FuncType, pkg *types.Package, path []ast.Node, file *ast.File, info *types.Info, pos token.Pos, hasNonNestedReturns bool) ([]*returnVariable, *ast.IfStmt, error) { -- var retVars []*returnVariable -- var cond *ast.Ident -- if !hasNonNestedReturns { -- // Generate information for the added bool value. -- name, _ := generateAvailableIdentifier(pos, path, pkg, info, "shouldReturn", 0) -- cond = &ast.Ident{Name: name} -- retVars = append(retVars, &returnVariable{ -- name: cond, -- decl: &ast.Field{Type: ast.NewIdent("bool")}, -- zeroVal: ast.NewIdent("false"), -- }) +- // Diagnose go.mod upgrades. +- upgradeReports, upgradeErr := mod.UpgradeDiagnostics(ctx, snapshot) +- if ctx.Err() != nil { +- return nil, ctx.Err() - } -- // Generate information for the values in the return signature of the enclosing function. -- if enclosing.Results != nil { -- idx := 0 -- for _, field := range enclosing.Results.List { -- typ := info.TypeOf(field.Type) -- if typ == nil { -- return nil, nil, fmt.Errorf( -- "failed type conversion, AST expression: %T", field.Type) -- } -- expr := analysisinternal.TypeExpr(file, pkg, typ) -- if expr == nil { -- return nil, nil, fmt.Errorf("nil AST expression") -- } -- var name string -- name, idx = generateAvailableIdentifier(pos, path, pkg, info, "returnValue", idx) -- retVars = append(retVars, &returnVariable{ -- name: ast.NewIdent(name), -- decl: &ast.Field{Type: expr}, -- zeroVal: analysisinternal.ZeroValue(file, pkg, typ), -- }) -- } +- store("diagnosing go.mod upgrades", upgradeReports, upgradeErr) +- +- // Diagnose vulnerabilities. +- vulnReports, vulnErr := mod.VulnerabilityDiagnostics(ctx, snapshot) +- if ctx.Err() != nil { +- return nil, ctx.Err() - } -- var ifReturn *ast.IfStmt -- if !hasNonNestedReturns { -- // Create the return statement for the enclosing function. We must exclude the variable -- // for the condition of the if statement (cond) from the return statement. -- ifReturn = &ast.IfStmt{ -- Cond: cond, -- Body: &ast.BlockStmt{ -- List: []ast.Stmt{&ast.ReturnStmt{Results: getNames(retVars)[1:]}}, -- }, -- } +- store("diagnosing vulnerabilities", vulnReports, vulnErr) +- +- workspacePkgs, err := snapshot.WorkspaceMetadata(ctx) +- if s.shouldIgnoreError(snapshot, err) { +- return diagnostics, ctx.Err() - } -- return retVars, ifReturn, nil --} - --// adjustReturnStatements adds "zero values" of the given types to each return statement --// in the given AST node. --func adjustReturnStatements(returnTypes []*ast.Field, seenVars map[types.Object]ast.Expr, file *ast.File, pkg *types.Package, extractedBlock *ast.BlockStmt) error { -- var zeroVals []ast.Expr -- // Create "zero values" for each type. -- for _, returnType := range returnTypes { -- var val ast.Expr -- for obj, typ := range seenVars { -- if typ != returnType.Type { -- continue +- initialErr := snapshot.InitializationError() +- if ctx.Err() != nil { +- // Don't update initialization status if the context is cancelled. +- return nil, ctx.Err() +- } +- +- if initialErr != nil { +- store("critical error", initialErr.Diagnostics, nil) +- } +- +- // Show the error as a progress error report so that it appears in the +- // status bar. If a client doesn't support progress reports, the error +- // will still be shown as a ShowMessage. If there is no error, any running +- // error progress reports will be closed. +- statusErr := initialErr +- if len(snapshot.Overlays()) == 0 { +- // Don't report a hanging status message if there are no open files at this +- // snapshot. +- statusErr = nil +- } +- s.updateCriticalErrorStatus(ctx, snapshot, statusErr) +- +- // Diagnose template (.tmpl) files. +- tmplReports := template.Diagnostics(snapshot) +- // NOTE(rfindley): typeCheckSource is not accurate here. +- // (but this will be gone soon anyway). +- store("diagnosing templates", tmplReports, nil) +- +- // If there are no workspace packages, there is nothing to diagnose and +- // there are no orphaned files. +- if len(workspacePkgs) == 0 { +- return diagnostics, nil +- } +- +- var wg sync.WaitGroup // for potentially slow operations below +- +- // Maybe run go mod tidy (if it has been invalidated). +- // +- // Since go mod tidy can be slow, we run it concurrently to diagnostics. +- wg.Add(1) +- go func() { +- defer wg.Done() +- modTidyReports, err := mod.TidyDiagnostics(ctx, snapshot) +- store("running go mod tidy", modTidyReports, err) +- }() +- +- // Run type checking and go/analysis diagnosis of packages in parallel. +- // +- // For analysis, we use the *widest* package for each open file, +- // for two reasons: +- // +- // - Correctness: some analyzers (e.g. unusedparam) depend +- // on it. If applied to a non-test package for which a +- // corresponding test package exists, they make assumptions +- // that are falsified in the test package, for example that +- // all references to unexported symbols are visible to the +- // analysis. +- // +- // - Efficiency: it may yield a smaller covering set of +- // PackageIDs for a given set of files. For example, {x.go, +- // x_test.go} is covered by the single package x_test using +- // "widest". (Using "narrowest", it would be covered only by +- // the pair of packages {x, x_test}, Originally we used all +- // covering packages, so {x.go} alone would be analyzed +- // twice.) +- var ( +- toDiagnose = make(map[metadata.PackageID]*metadata.Package) +- toAnalyze = make(map[metadata.PackageID]*metadata.Package) +- +- // secondary index, used to eliminate narrower packages. +- toAnalyzeWidest = make(map[golang.PackagePath]*metadata.Package) +- ) +- for _, mp := range workspacePkgs { +- var hasNonIgnored, hasOpenFile bool +- for _, uri := range mp.CompiledGoFiles { +- if !hasNonIgnored && !snapshot.IgnoredFile(uri) { +- hasNonIgnored = true +- } +- if !hasOpenFile && snapshot.IsOpen(uri) { +- hasOpenFile = true - } -- val = analysisinternal.ZeroValue(file, pkg, obj.Type()) -- break - } -- if val == nil { -- return fmt.Errorf( -- "could not find matching AST expression for %T", returnType.Type) +- if hasNonIgnored { +- toDiagnose[mp.ID] = mp +- if hasOpenFile { +- if prev, ok := toAnalyzeWidest[mp.PkgPath]; ok { +- if len(prev.CompiledGoFiles) >= len(mp.CompiledGoFiles) { +- // Previous entry is not narrower; keep it. +- continue +- } +- // Evict previous (narrower) entry. +- delete(toAnalyze, prev.ID) +- } +- toAnalyze[mp.ID] = mp +- toAnalyzeWidest[mp.PkgPath] = mp +- } - } -- zeroVals = append(zeroVals, val) - } -- // Add "zero values" to each return statement. -- // The bool reports whether the enclosing function should return after calling the -- // extracted function. We set the bool to 'true' because, if these return statements -- // execute, the extracted function terminates early, and the enclosing function must -- // return as well. -- zeroVals = append(zeroVals, ast.NewIdent("true")) -- ast.Inspect(extractedBlock, func(n ast.Node) bool { -- if n == nil { -- return false +- +- wg.Add(1) +- go func() { +- defer wg.Done() +- gcDetailsReports, err := s.gcDetailsDiagnostics(ctx, snapshot, toDiagnose) +- store("collecting gc_details", gcDetailsReports, err) +- }() +- +- // Package diagnostics and analysis diagnostics must both be computed and +- // merged before they can be reported. +- var pkgDiags, analysisDiags diagMap +- // Collect package diagnostics. +- wg.Add(1) +- go func() { +- defer wg.Done() +- var err error +- pkgDiags, err = snapshot.PackageDiagnostics(ctx, maps.Keys(toDiagnose)...) +- if err != nil { +- event.Error(ctx, "warning: diagnostics failed", err, snapshot.Labels()...) - } -- if n, ok := n.(*ast.ReturnStmt); ok { -- n.Results = append(zeroVals, n.Results...) -- return false +- }() +- +- // Get diagnostics from analysis framework. +- // This includes type-error analyzers, which suggest fixes to compiler errors. +- wg.Add(1) +- go func() { +- defer wg.Done() +- var err error +- // TODO(rfindley): here and above, we should avoid using the first result +- // if err is non-nil (though as of today it's OK). +- analysisDiags, err = golang.Analyze(ctx, snapshot, toAnalyze, s.progress) +- if err != nil { +- event.Error(ctx, "warning: analyzing package", err, append(snapshot.Labels(), tag.Package.Of(keys.Join(maps.Keys(toDiagnose))))...) +- return - } -- return true -- }) -- return nil --} +- }() - --// generateFuncCall constructs a call expression for the extracted function, described by the --// given parameters and return variables. --func generateFuncCall(hasNonNestedReturn, hasReturnVals bool, params, returns []ast.Expr, name string, token token.Token, selector string) ast.Node { -- var replace ast.Node -- callExpr := &ast.CallExpr{ -- Fun: ast.NewIdent(name), -- Args: params, +- wg.Wait() +- +- // Merge analysis diagnostics with package diagnostics, and store the +- // resulting analysis diagnostics. +- for uri, adiags := range analysisDiags { +- tdiags := pkgDiags[uri] +- var tdiags2, adiags2 []*cache.Diagnostic +- combineDiagnostics(tdiags, adiags, &tdiags2, &adiags2) +- pkgDiags[uri] = tdiags2 +- analysisDiags[uri] = adiags2 - } -- if selector != "" { -- callExpr = &ast.CallExpr{ -- Fun: &ast.SelectorExpr{ -- X: ast.NewIdent(selector), -- Sel: ast.NewIdent(name), -- }, -- Args: params, +- store("type checking", pkgDiags, nil) // error reported above +- store("analyzing packages", analysisDiags, nil) // error reported above +- +- return diagnostics, nil +-} +- +-func (s *server) gcDetailsDiagnostics(ctx context.Context, snapshot *cache.Snapshot, toDiagnose map[metadata.PackageID]*metadata.Package) (diagMap, error) { +- // Process requested gc_details diagnostics. +- // +- // TODO(rfindley): this could be improved: +- // 1. This should memoize its results if the package has not changed. +- // 2. This should not even run gc_details if the package contains unsaved +- // files. +- // 3. See note below about using ReadFile. +- // Consider that these points, in combination with the note below about +- // races, suggest that gc_details should be tracked on the Snapshot. +- var toGCDetail map[metadata.PackageID]*metadata.Package +- for _, mp := range toDiagnose { +- if snapshot.WantGCDetails(mp.ID) { +- if toGCDetail == nil { +- toGCDetail = make(map[metadata.PackageID]*metadata.Package) +- } +- toGCDetail[mp.ID] = mp - } - } -- if hasReturnVals { -- if hasNonNestedReturn { -- // Create a return statement that returns the result of the function call. -- replace = &ast.ReturnStmt{ -- Return: 0, -- Results: []ast.Expr{callExpr}, +- +- diagnostics := make(diagMap) +- for _, mp := range toGCDetail { +- gcReports, err := golang.GCOptimizationDetails(ctx, snapshot, mp) +- if err != nil { +- event.Error(ctx, "warning: gc details", err, append(snapshot.Labels(), tag.Package.Of(string(mp.ID)))...) +- continue +- } +- for uri, diags := range gcReports { +- // TODO(rfindley): reading here should not be necessary: if a file has +- // been deleted we should be notified, and diagnostics will eventually +- // become consistent. +- fh, err := snapshot.ReadFile(ctx, uri) +- if err != nil { +- return nil, err - } -- } else { -- // Assign the result of the function call. -- replace = &ast.AssignStmt{ -- Lhs: returns, -- Tok: token, -- Rhs: []ast.Expr{callExpr}, +- // Don't publish gc details for unsaved buffers, since the underlying +- // logic operates on the file on disk. +- if fh == nil || !fh.SameContentsOnDisk() { +- continue - } +- diagnostics[uri] = append(diagnostics[uri], diags...) - } -- } else { -- replace = callExpr - } -- return replace +- return diagnostics, nil -} - --// initializeVars creates variable declarations, if needed. --// Our preference is to replace the selected block with an "x, y, z := fn()" style --// assignment statement. We can use this style when all of the variables in the --// extracted function's return statement are either not defined prior to the extracted block --// or can be safely redefined. However, for example, if z is already defined --// in a different scope, we replace the selected block with: +-// combineDiagnostics combines and filters list/parse/type diagnostics from +-// tdiags with adiags, and appends the two lists to *outT and *outA, +-// respectively. -// --// var x int --// var y string --// x, y, z = fn() --func initializeVars(uninitialized []types.Object, retVars []*returnVariable, seenUninitialized map[types.Object]struct{}, seenVars map[types.Object]ast.Expr) []ast.Stmt { -- var declarations []ast.Stmt -- for _, obj := range uninitialized { -- if _, ok := seenUninitialized[obj]; ok { -- continue -- } -- seenUninitialized[obj] = struct{}{} -- valSpec := &ast.ValueSpec{ -- Names: []*ast.Ident{ast.NewIdent(obj.Name())}, -- Type: seenVars[obj], -- } -- genDecl := &ast.GenDecl{ -- Tok: token.VAR, -- Specs: []ast.Spec{valSpec}, -- } -- declarations = append(declarations, &ast.DeclStmt{Decl: genDecl}) +-// Type-error analyzers produce diagnostics that are redundant +-// with type checker diagnostics, but more detailed (e.g. fixes). +-// Rather than report two diagnostics for the same problem, +-// we combine them by augmenting the type-checker diagnostic +-// and discarding the analyzer diagnostic. +-// +-// If an analysis diagnostic has the same range and message as +-// a list/parse/type diagnostic, the suggested fix information +-// (et al) of the latter is merged into a copy of the former. +-// This handles the case where a type-error analyzer suggests +-// a fix to a type error, and avoids duplication. +-// +-// The use of out-slices, though irregular, allows the caller to +-// easily choose whether to keep the results separate or combined. +-// +-// The arguments are not modified. +-func combineDiagnostics(tdiags []*cache.Diagnostic, adiags []*cache.Diagnostic, outT, outA *[]*cache.Diagnostic) { +- +- // Build index of (list+parse+)type errors. +- type key struct { +- Range protocol.Range +- message string - } -- // Each variable added from a return statement in the selection -- // must be initialized. -- for i, retVar := range retVars { -- n := retVar.name.(*ast.Ident) -- valSpec := &ast.ValueSpec{ -- Names: []*ast.Ident{n}, -- Type: retVars[i].decl.Type, -- } -- genDecl := &ast.GenDecl{ -- Tok: token.VAR, -- Specs: []ast.Spec{valSpec}, +- index := make(map[key]int) // maps (Range,Message) to index in tdiags slice +- for i, diag := range tdiags { +- index[key{diag.Range, diag.Message}] = i +- } +- +- // Filter out analysis diagnostics that match type errors, +- // retaining their suggested fix (etc) fields. +- for _, diag := range adiags { +- if i, ok := index[key{diag.Range, diag.Message}]; ok { +- copy := *tdiags[i] +- copy.SuggestedFixes = diag.SuggestedFixes +- copy.Tags = diag.Tags +- tdiags[i] = © +- continue - } -- declarations = append(declarations, &ast.DeclStmt{Decl: genDecl}) +- +- *outA = append(*outA, diag) - } -- return declarations +- +- *outT = append(*outT, tdiags...) -} - --// getNames returns the names from the given list of returnVariable. --func getNames(retVars []*returnVariable) []ast.Expr { -- var names []ast.Expr -- for _, retVar := range retVars { -- names = append(names, retVar.name) +-// mustPublishDiagnostics marks the uri as needing publication, independent of +-// whether the published contents have changed. +-// +-// This can be used for ensuring gopls publishes diagnostics after certain file +-// events. +-func (s *server) mustPublishDiagnostics(uri protocol.DocumentURI) { +- s.diagnosticsMu.Lock() +- defer s.diagnosticsMu.Unlock() +- +- if s.diagnostics[uri] == nil { +- s.diagnostics[uri] = new(fileDiagnostics) - } -- return names +- s.diagnostics[uri].mustPublish = true -} - --// getZeroVals returns the "zero values" from the given list of returnVariable. --func getZeroVals(retVars []*returnVariable) []ast.Expr { -- var zvs []ast.Expr -- for _, retVar := range retVars { -- zvs = append(zvs, retVar.zeroVal) +-const WorkspaceLoadFailure = "Error loading workspace" +- +-// updateCriticalErrorStatus updates the critical error progress notification +-// based on err. +-// +-// If err is nil, or if there are no open files, it clears any existing error +-// progress report. +-func (s *server) updateCriticalErrorStatus(ctx context.Context, snapshot *cache.Snapshot, err *cache.InitializationError) { +- s.criticalErrorStatusMu.Lock() +- defer s.criticalErrorStatusMu.Unlock() +- +- // Remove all newlines so that the error message can be formatted in a +- // status bar. +- var errMsg string +- if err != nil { +- errMsg = strings.ReplaceAll(err.MainError.Error(), "\n", " ") - } -- return zvs --} - --// getDecls returns the declarations from the given list of returnVariable. --func getDecls(retVars []*returnVariable) []*ast.Field { -- var decls []*ast.Field -- for _, retVar := range retVars { -- decls = append(decls, retVar.decl) +- if s.criticalErrorStatus == nil { +- if errMsg != "" { +- event.Error(ctx, "errors loading workspace", err.MainError, snapshot.Labels()...) +- s.criticalErrorStatus = s.progress.Start(ctx, WorkspaceLoadFailure, errMsg, nil, nil) +- } +- return - } -- return decls --} -diff -urN a/gopls/internal/lsp/source/fix.go b/gopls/internal/lsp/source/fix.go ---- a/gopls/internal/lsp/source/fix.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/fix.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,195 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package source +- // If an error is already shown to the user, update it or mark it as +- // resolved. +- if errMsg == "" { +- s.criticalErrorStatus.End(ctx, "Done.") +- s.criticalErrorStatus = nil +- } else { +- s.criticalErrorStatus.Report(ctx, errMsg, 0) +- } +-} - --import ( -- "context" -- "fmt" -- "go/ast" -- "go/token" -- "go/types" +-// updateDiagnostics records the result of diagnosing a snapshot, and publishes +-// any diagnostics that need to be updated on the client. +-func (s *server) updateDiagnostics(ctx context.Context, snapshot *cache.Snapshot, diagnostics diagMap, final bool) { +- ctx, done := event.Start(ctx, "Server.publishDiagnostics") +- defer done() - -- "golang.org/x/tools/go/analysis" -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/analysis/embeddirective" -- "golang.org/x/tools/gopls/internal/lsp/analysis/fillstruct" -- "golang.org/x/tools/gopls/internal/lsp/analysis/undeclaredname" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/imports" --) +- s.diagnosticsMu.Lock() +- defer s.diagnosticsMu.Unlock() - --type ( -- // SuggestedFixFunc is a function used to get the suggested fixes for a given -- // gopls command, some of which are provided by go/analysis.Analyzers. Some of -- // the analyzers in internal/lsp/analysis are not efficient enough to include -- // suggested fixes with their diagnostics, so we have to compute them -- // separately. Such analyzers should provide a function with a signature of -- // SuggestedFixFunc. +- // Before updating any diagnostics, check that the context (i.e. snapshot +- // background context) is not cancelled. - // -- // The returned FileSet must map all token.Pos found in the suggested text -- // edits. -- SuggestedFixFunc func(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range) (*token.FileSet, *analysis.SuggestedFix, error) -- singleFileFixFunc func(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, pkg *types.Package, info *types.Info) (*analysis.SuggestedFix, error) --) +- // If not, then we know that we haven't started diagnosing the next snapshot, +- // because the previous snapshot is cancelled before the next snapshot is +- // returned from Invalidate. +- // +- // Therefore, even if we publish stale diagnostics here, they should +- // eventually be overwritten with accurate diagnostics. +- // +- // TODO(rfindley): refactor the API to force that snapshots are diagnosed +- // after they are created. +- if ctx.Err() != nil { +- return +- } - --// These strings identify kinds of suggested fix, both in Analyzer.Fix --// and in the ApplyFix subcommand (see ExecuteCommand and ApplyFixArgs.Fix). --const ( -- FillStruct = "fill_struct" -- StubMethods = "stub_methods" -- UndeclaredName = "undeclared_name" -- ExtractVariable = "extract_variable" -- ExtractFunction = "extract_function" -- ExtractMethod = "extract_method" -- InlineCall = "inline_call" -- InvertIfCondition = "invert_if_condition" -- AddEmbedImport = "add_embed_import" --) -- --// suggestedFixes maps a suggested fix command id to its handler. --var suggestedFixes = map[string]SuggestedFixFunc{ -- FillStruct: singleFile(fillstruct.SuggestedFix), -- UndeclaredName: singleFile(undeclaredname.SuggestedFix), -- ExtractVariable: singleFile(extractVariable), -- InlineCall: inlineCall, -- ExtractFunction: singleFile(extractFunction), -- ExtractMethod: singleFile(extractMethod), -- InvertIfCondition: singleFile(invertIfCondition), -- StubMethods: stubSuggestedFixFunc, -- AddEmbedImport: addEmbedImport, --} -- --// singleFile calls analyzers that expect inputs for a single file --func singleFile(sf singleFileFixFunc) SuggestedFixFunc { -- return func(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range) (*token.FileSet, *analysis.SuggestedFix, error) { -- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) -- if err != nil { -- return nil, nil, err +- // golang/go#65312: since the set of diagnostics depends on the set of views, +- // we get the views *after* locking diagnosticsMu. This ensures that +- // updateDiagnostics does not incorrectly delete diagnostics that have been +- // set for an existing view that was created between the call to +- // s.session.Views() and updateDiagnostics. +- viewMap := make(viewSet) +- for _, v := range s.session.Views() { +- viewMap[v] = unit{} +- } +- +- // updateAndPublish updates diagnostics for a file, checking both the latest +- // diagnostics for the current snapshot, as well as reconciling the set of +- // views. +- updateAndPublish := func(uri protocol.DocumentURI, f *fileDiagnostics, diags []*cache.Diagnostic) error { +- current, ok := f.byView[snapshot.View()] +- // Update the stored diagnostics if: +- // 1. we've never seen diagnostics for this view, +- // 2. diagnostics are for an older snapshot, or +- // 3. we're overwriting with final diagnostics +- // +- // In other words, we shouldn't overwrite existing diagnostics for a +- // snapshot with non-final diagnostics. This avoids the race described at +- // https://github.com/golang/go/issues/64765#issuecomment-1890144575. +- if !ok || current.snapshot < snapshot.SequenceID() || (current.snapshot == snapshot.SequenceID() && final) { +- fh, err := snapshot.ReadFile(ctx, uri) +- if err != nil { +- return err +- } +- current = viewDiagnostics{ +- snapshot: snapshot.SequenceID(), +- version: fh.Version(), +- diagnostics: diags, +- } +- if f.byView == nil { +- f.byView = make(map[*cache.View]viewDiagnostics) +- } +- f.byView[snapshot.View()] = current - } -- start, end, err := pgf.RangePos(pRng) -- if err != nil { -- return nil, nil, err +- +- return s.publishFileDiagnosticsLocked(ctx, viewMap, uri, current.version, f) +- } +- +- seen := make(map[protocol.DocumentURI]bool) +- for uri, diags := range diagnostics { +- f, ok := s.diagnostics[uri] +- if !ok { +- f = new(fileDiagnostics) +- s.diagnostics[uri] = f +- } +- seen[uri] = true +- if err := updateAndPublish(uri, f, diags); err != nil { +- if ctx.Err() != nil { +- return +- } else { +- event.Error(ctx, "updateDiagnostics: failed to deliver diagnostics", err, tag.URI.Of(uri)) +- } - } -- fix, err := sf(pkg.FileSet(), start, end, pgf.Src, pgf.File, pkg.GetTypes(), pkg.GetTypesInfo()) -- return pkg.FileSet(), fix, err - } --} - --func SuggestedFixFromCommand(cmd protocol.Command, kind protocol.CodeActionKind) SuggestedFix { -- return SuggestedFix{ -- Title: cmd.Title, -- Command: &cmd, -- ActionKind: kind, +- // TODO(rfindley): perhaps we should clean up files that have no diagnostics. +- // One could imagine a large operation generating diagnostics for a great +- // number of files, after which gopls has to do more bookkeeping into the +- // future. +- if final { +- for uri, f := range s.diagnostics { +- if !seen[uri] { +- if err := updateAndPublish(uri, f, nil); err != nil { +- if ctx.Err() != nil { +- return +- } else { +- event.Error(ctx, "updateDiagnostics: failed to deliver diagnostics", err, tag.URI.Of(uri)) +- } +- } +- } +- } - } -} - --// ApplyFix applies the command's suggested fix to the given file and --// range, returning the resulting edits. --func ApplyFix(ctx context.Context, fix string, snapshot Snapshot, fh FileHandle, pRng protocol.Range) ([]protocol.TextDocumentEdit, error) { -- handler, ok := suggestedFixes[fix] -- if !ok { -- return nil, fmt.Errorf("no suggested fix function for %s", fix) -- } -- fset, suggestion, err := handler(ctx, snapshot, fh, pRng) -- if err != nil { -- return nil, err -- } -- if suggestion == nil { -- return nil, nil +-// updateOrphanedFileDiagnostics records and publishes orphaned file +-// diagnostics as a given modification time. +-func (s *server) updateOrphanedFileDiagnostics(ctx context.Context, modID uint64, diagnostics diagMap) error { +- views := s.session.Views() +- viewSet := make(viewSet) +- for _, v := range views { +- viewSet[v] = unit{} - } -- editsPerFile := map[span.URI]*protocol.TextDocumentEdit{} -- for _, edit := range suggestion.TextEdits { -- tokFile := fset.File(edit.Pos) -- if tokFile == nil { -- return nil, bug.Errorf("no file for edit position") +- +- s.diagnosticsMu.Lock() +- defer s.diagnosticsMu.Unlock() +- +- for uri, diags := range diagnostics { +- f, ok := s.diagnostics[uri] +- if !ok { +- f = new(fileDiagnostics) +- s.diagnostics[uri] = f - } -- end := edit.End -- if !end.IsValid() { -- end = edit.Pos +- if f.orphanedAt > modID { +- continue - } -- fh, err := snapshot.ReadFile(ctx, span.URIFromPath(tokFile.Name())) +- f.orphanedAt = modID +- f.orphanedFileDiagnostics = diags +- // TODO(rfindley): the version of this file is potentially inaccurate; +- // nevertheless, it should be eventually consistent, because all +- // modifications are diagnosed. +- fh, err := s.session.ReadFile(ctx, uri) - if err != nil { -- return nil, err +- return err - } -- te, ok := editsPerFile[fh.URI()] -- if !ok { -- te = &protocol.TextDocumentEdit{ -- TextDocument: protocol.OptionalVersionedTextDocumentIdentifier{ -- Version: fh.Version(), -- TextDocumentIdentifier: protocol.TextDocumentIdentifier{ -- URI: protocol.URIFromSpanURI(fh.URI()), -- }, -- }, -- } -- editsPerFile[fh.URI()] = te +- if err := s.publishFileDiagnosticsLocked(ctx, viewSet, uri, fh.Version(), f); err != nil { +- return err - } -- content, err := fh.Content() -- if err != nil { -- return nil, err +- } +- +- // Clear any stale orphaned file diagnostics. +- for uri, f := range s.diagnostics { +- if f.orphanedAt < modID { +- f.orphanedFileDiagnostics = nil - } -- m := protocol.NewMapper(fh.URI(), content) -- rng, err := m.PosRange(tokFile, edit.Pos, end) +- fh, err := s.session.ReadFile(ctx, uri) - if err != nil { -- return nil, err +- return err +- } +- if err := s.publishFileDiagnosticsLocked(ctx, viewSet, uri, fh.Version(), f); err != nil { +- return err - } -- te.Edits = append(te.Edits, protocol.TextEdit{ -- Range: rng, -- NewText: string(edit.NewText), -- }) -- } -- var edits []protocol.TextDocumentEdit -- for _, edit := range editsPerFile { -- edits = append(edits, *edit) - } -- return edits, nil +- return nil -} - --// fixedByImportingEmbed returns true if diag can be fixed by addEmbedImport. --func fixedByImportingEmbed(diag *Diagnostic) bool { -- if diag == nil { -- return false +-// publishFileDiagnosticsLocked publishes a fileDiagnostics value, while holding s.diagnosticsMu. +-// +-// If the publication succeeds, it updates f.publishedHash and f.mustPublish. +-func (s *server) publishFileDiagnosticsLocked(ctx context.Context, views viewSet, uri protocol.DocumentURI, version int32, f *fileDiagnostics) error { +- // We add a disambiguating suffix (e.g. " [darwin,arm64]") to +- // each diagnostic that doesn't occur in the default view; +- // see golang/go#65496. +- type diagSuffix struct { +- diag *cache.Diagnostic +- suffix string // "" for default build (or orphans) - } -- return diag.Message == embeddirective.MissingImportMessage --} - --// addEmbedImport adds a missing embed "embed" import with blank name. --func addEmbedImport(ctx context.Context, snapshot Snapshot, fh FileHandle, _ protocol.Range) (*token.FileSet, *analysis.SuggestedFix, error) { -- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) -- if err != nil { -- return nil, nil, fmt.Errorf("narrow pkg: %w", err) +- // diagSuffixes records the set of view suffixes for a given diagnostic. +- diagSuffixes := make(map[file.Hash][]diagSuffix) +- add := func(diag *cache.Diagnostic, suffix string) { +- h := hashDiagnostic(diag) +- diagSuffixes[h] = append(diagSuffixes[h], diagSuffix{diag, suffix}) - } - -- // Like source.AddImport, but with _ as Name and using our pgf. -- protoEdits, err := ComputeOneImportFixEdits(snapshot, pgf, &imports.ImportFix{ -- StmtInfo: imports.ImportInfo{ -- ImportPath: "embed", -- Name: "_", -- }, -- FixType: imports.AddImport, -- }) -- if err != nil { -- return nil, nil, fmt.Errorf("compute edits: %w", err) +- // Construct the inverse mapping, from diagnostic (hash) to its suffixes (views). +- for _, diag := range f.orphanedFileDiagnostics { +- add(diag, "") - } - -- var edits []analysis.TextEdit -- for _, e := range protoEdits { -- start, end, err := pgf.RangePos(e.Range) -- if err != nil { -- return nil, nil, fmt.Errorf("map range: %w", err) +- var allViews []*cache.View +- for view, viewDiags := range f.byView { +- if _, ok := views[view]; !ok { +- delete(f.byView, view) // view no longer exists +- continue - } -- edits = append(edits, analysis.TextEdit{ -- Pos: start, -- End: end, -- NewText: []byte(e.NewText), -- }) +- if viewDiags.version != version { +- continue // a payload of diagnostics applies to a specific file version +- } +- allViews = append(allViews, view) - } - -- fix := &analysis.SuggestedFix{ -- Message: "Add embed import", -- TextEdits: edits, +- // Only report diagnostics from the best views for a file. This avoids +- // spurious import errors when a view has only a partial set of dependencies +- // for a package (golang/go#66425). +- // +- // It's ok to use the session to derive the eligible views, because we +- // publish diagnostics following any state change, so the set of best views +- // is eventually consistent. +- bestViews, err := cache.BestViews(ctx, s.session, uri, allViews) +- if err != nil { +- return err - } -- return pkg.FileSet(), fix, nil --} -diff -urN a/gopls/internal/lsp/source/folding_range.go b/gopls/internal/lsp/source/folding_range.go ---- a/gopls/internal/lsp/source/folding_range.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/folding_range.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,194 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package source +- if len(bestViews) == 0 { +- // If we have no preferred diagnostics for a given file (i.e., the file is +- // not naturally nested within a view), then all diagnostics should be +- // considered valid. +- // +- // This could arise if the user jumps to definition outside the workspace. +- // There is no view that owns the file, so its diagnostics are valid from +- // any view. +- bestViews = allViews +- } - --import ( -- "context" -- "go/ast" -- "go/token" -- "sort" -- "strings" +- for _, view := range bestViews { +- viewDiags := f.byView[view] +- // Compute the view's suffix (e.g. " [darwin,arm64]"). +- var suffix string +- { +- var words []string +- if view.GOOS() != runtime.GOOS { +- words = append(words, view.GOOS()) +- } +- if view.GOARCH() != runtime.GOARCH { +- words = append(words, view.GOARCH()) +- } +- if len(words) > 0 { +- suffix = fmt.Sprintf(" [%s]", strings.Join(words, ",")) +- } +- } - -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" --) +- for _, diag := range viewDiags.diagnostics { +- add(diag, suffix) +- } +- } - --// FoldingRangeInfo holds range and kind info of folding for an ast.Node --type FoldingRangeInfo struct { -- MappedRange protocol.MappedRange -- Kind protocol.FoldingRangeKind --} +- // De-dup diagnostics across views by hash, and sort. +- var ( +- hash file.Hash +- unique []*cache.Diagnostic +- ) +- for h, items := range diagSuffixes { +- // Sort the items by ascending suffix, so that the +- // default view (if present) is first. +- // (The others are ordered arbitrarily.) +- sort.Slice(items, func(i, j int) bool { +- return items[i].suffix < items[j].suffix +- }) - --// FoldingRange gets all of the folding range for f. --func FoldingRange(ctx context.Context, snapshot Snapshot, fh FileHandle, lineFoldingOnly bool) (ranges []*FoldingRangeInfo, err error) { -- // TODO(suzmue): consider limiting the number of folding ranges returned, and -- // implement a way to prioritize folding ranges in that case. -- pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) -- if err != nil { -- return nil, err -- } +- // If the diagnostic was not present in +- // the default view, add the view suffix. +- first := items[0] +- if first.suffix != "" { +- diag2 := *first.diag // shallow copy +- diag2.Message += first.suffix +- first.diag = &diag2 +- h = hashDiagnostic(&diag2) // update the hash +- } - -- // With parse errors, we wouldn't be able to produce accurate folding info. -- // LSP protocol (3.16) currently does not have a way to handle this case -- // (https://github.com/microsoft/language-server-protocol/issues/1200). -- // We cannot return an error either because we are afraid some editors -- // may not handle errors nicely. As a workaround, we now return an empty -- // result and let the client handle this case by double check the file -- // contents (i.e. if the file is not empty and the folding range result -- // is empty, raise an internal error). -- if pgf.ParseErr != nil { -- return nil, nil +- hash.XORWith(h) +- unique = append(unique, first.diag) - } +- sortDiagnostics(unique) - -- // Get folding ranges for comments separately as they are not walked by ast.Inspect. -- ranges = append(ranges, commentsFoldingRange(pgf)...) -- -- visit := func(n ast.Node) bool { -- rng := foldingRangeFunc(pgf, n, lineFoldingOnly) -- if rng != nil { -- ranges = append(ranges, rng) +- // Publish, if necessary. +- if hash != f.publishedHash || f.mustPublish { +- if err := s.client.PublishDiagnostics(ctx, &protocol.PublishDiagnosticsParams{ +- Diagnostics: toProtocolDiagnostics(unique), +- URI: uri, +- Version: version, +- }); err != nil { +- return err - } -- return true +- f.publishedHash = hash +- f.mustPublish = false - } -- // Walk the ast and collect folding ranges. -- ast.Inspect(pgf.File, visit) -- -- sort.Slice(ranges, func(i, j int) bool { -- irng := ranges[i].MappedRange.Range() -- jrng := ranges[j].MappedRange.Range() -- return protocol.CompareRange(irng, jrng) < 0 -- }) -- -- return ranges, nil +- return nil -} - --// foldingRangeFunc calculates the line folding range for ast.Node n --func foldingRangeFunc(pgf *ParsedGoFile, n ast.Node, lineFoldingOnly bool) *FoldingRangeInfo { -- // TODO(suzmue): include trailing empty lines before the closing -- // parenthesis/brace. -- var kind protocol.FoldingRangeKind -- var start, end token.Pos -- switch n := n.(type) { -- case *ast.BlockStmt: -- // Fold between positions of or lines between "{" and "}". -- var startList, endList token.Pos -- if num := len(n.List); num != 0 { -- startList, endList = n.List[0].Pos(), n.List[num-1].End() -- } -- start, end = validLineFoldingRange(pgf.Tok, n.Lbrace, n.Rbrace, startList, endList, lineFoldingOnly) -- case *ast.CaseClause: -- // Fold from position of ":" to end. -- start, end = n.Colon+1, n.End() -- case *ast.CommClause: -- // Fold from position of ":" to end. -- start, end = n.Colon+1, n.End() -- case *ast.CallExpr: -- // Fold from position of "(" to position of ")". -- start, end = n.Lparen+1, n.Rparen -- case *ast.FieldList: -- // Fold between positions of or lines between opening parenthesis/brace and closing parenthesis/brace. -- var startList, endList token.Pos -- if num := len(n.List); num != 0 { -- startList, endList = n.List[0].Pos(), n.List[num-1].End() +-func toProtocolDiagnostics(diagnostics []*cache.Diagnostic) []protocol.Diagnostic { +- reports := []protocol.Diagnostic{} +- for _, diag := range diagnostics { +- pdiag := protocol.Diagnostic{ +- // diag.Message might start with \n or \t +- Message: strings.TrimSpace(diag.Message), +- Range: diag.Range, +- Severity: diag.Severity, +- Source: string(diag.Source), +- Tags: protocol.NonNilSlice(diag.Tags), +- RelatedInformation: diag.Related, +- Data: diag.BundledFixes, - } -- start, end = validLineFoldingRange(pgf.Tok, n.Opening, n.Closing, startList, endList, lineFoldingOnly) -- case *ast.GenDecl: -- // If this is an import declaration, set the kind to be protocol.Imports. -- if n.Tok == token.IMPORT { -- kind = protocol.Imports +- if diag.Code != "" { +- pdiag.Code = diag.Code - } -- // Fold between positions of or lines between "(" and ")". -- var startSpecs, endSpecs token.Pos -- if num := len(n.Specs); num != 0 { -- startSpecs, endSpecs = n.Specs[0].Pos(), n.Specs[num-1].End() +- if diag.CodeHref != "" { +- pdiag.CodeDescription = &protocol.CodeDescription{Href: diag.CodeHref} - } -- start, end = validLineFoldingRange(pgf.Tok, n.Lparen, n.Rparen, startSpecs, endSpecs, lineFoldingOnly) -- case *ast.BasicLit: -- // Fold raw string literals from position of "`" to position of "`". -- if n.Kind == token.STRING && len(n.Value) >= 2 && n.Value[0] == '`' && n.Value[len(n.Value)-1] == '`' { -- start, end = n.Pos(), n.End() +- reports = append(reports, pdiag) +- } +- return reports +-} +- +-func (s *server) shouldIgnoreError(snapshot *cache.Snapshot, err error) bool { +- if err == nil { // if there is no error at all +- return false +- } +- if errors.Is(err, context.Canceled) { +- return true +- } +- // If the folder has no Go code in it, we shouldn't spam the user with a warning. +- // TODO(rfindley): surely it is not correct to walk the folder here just to +- // suppress diagnostics, every time we compute diagnostics. +- var hasGo bool +- _ = filepath.Walk(snapshot.Folder().Path(), func(_ string, info os.FileInfo, err error) error { +- if err != nil { +- return err - } -- case *ast.CompositeLit: -- // Fold between positions of or lines between "{" and "}". -- var startElts, endElts token.Pos -- if num := len(n.Elts); num != 0 { -- startElts, endElts = n.Elts[0].Pos(), n.Elts[num-1].End() +- if !strings.HasSuffix(info.Name(), ".go") { +- return nil - } -- start, end = validLineFoldingRange(pgf.Tok, n.Lbrace, n.Rbrace, startElts, endElts, lineFoldingOnly) -- } +- hasGo = true +- return errors.New("done") +- }) +- return !hasGo +-} +diff -urN a/gopls/internal/server/folding_range.go b/gopls/internal/server/folding_range.go +--- a/gopls/internal/server/folding_range.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/folding_range.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,49 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- // Check that folding positions are valid. -- if !start.IsValid() || !end.IsValid() { -- return nil +-package server +- +-import ( +- "context" +- +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/tag" +-) +- +-func (s *server) FoldingRange(ctx context.Context, params *protocol.FoldingRangeParams) ([]protocol.FoldingRange, error) { +- ctx, done := event.Start(ctx, "lsp.Server.foldingRange", tag.URI.Of(params.TextDocument.URI)) +- defer done() +- +- fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI) +- if err != nil { +- return nil, err - } -- // in line folding mode, do not fold if the start and end lines are the same. -- if lineFoldingOnly && safetoken.Line(pgf.Tok, start) == safetoken.Line(pgf.Tok, end) { -- return nil +- defer release() +- if snapshot.FileKind(fh) != file.Go { +- return nil, nil // empty result - } -- mrng, err := pgf.PosMappedRange(start, end) +- ranges, err := golang.FoldingRange(ctx, snapshot, fh, snapshot.Options().LineFoldingOnly) - if err != nil { -- bug.Errorf("%w", err) // can't happen +- return nil, err - } -- return &FoldingRangeInfo{ -- MappedRange: mrng, -- Kind: kind, +- return toProtocolFoldingRanges(ranges) +-} +- +-func toProtocolFoldingRanges(ranges []*golang.FoldingRangeInfo) ([]protocol.FoldingRange, error) { +- result := make([]protocol.FoldingRange, 0, len(ranges)) +- for _, info := range ranges { +- rng := info.MappedRange.Range() +- result = append(result, protocol.FoldingRange{ +- StartLine: rng.Start.Line, +- StartCharacter: rng.Start.Character, +- EndLine: rng.End.Line, +- EndCharacter: rng.End.Character, +- Kind: string(info.Kind), +- }) - } +- return result, nil -} +diff -urN a/gopls/internal/server/format.go b/gopls/internal/server/format.go +--- a/gopls/internal/server/format.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/format.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,38 +0,0 @@ +-// Copyright 2018 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// validLineFoldingRange returns start and end token.Pos for folding range if the range is valid. --// returns token.NoPos otherwise, which fails token.IsValid check --func validLineFoldingRange(tokFile *token.File, open, close, start, end token.Pos, lineFoldingOnly bool) (token.Pos, token.Pos) { -- if lineFoldingOnly { -- if !open.IsValid() || !close.IsValid() { -- return token.NoPos, token.NoPos -- } +-package server - -- // Don't want to fold if the start/end is on the same line as the open/close -- // as an example, the example below should *not* fold: -- // var x = [2]string{"d", -- // "e" } -- if safetoken.Line(tokFile, open) == safetoken.Line(tokFile, start) || -- safetoken.Line(tokFile, close) == safetoken.Line(tokFile, end) { -- return token.NoPos, token.NoPos -- } +-import ( +- "context" - -- return open + 1, end -- } -- return open + 1, close --} +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/mod" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/work" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/tag" +-) - --// commentsFoldingRange returns the folding ranges for all comment blocks in file. --// The folding range starts at the end of the first line of the comment block, and ends at the end of the --// comment block and has kind protocol.Comment. --func commentsFoldingRange(pgf *ParsedGoFile) (comments []*FoldingRangeInfo) { -- tokFile := pgf.Tok -- for _, commentGrp := range pgf.File.Comments { -- startGrpLine, endGrpLine := safetoken.Line(tokFile, commentGrp.Pos()), safetoken.Line(tokFile, commentGrp.End()) -- if startGrpLine == endGrpLine { -- // Don't fold single line comments. -- continue -- } +-func (s *server) Formatting(ctx context.Context, params *protocol.DocumentFormattingParams) ([]protocol.TextEdit, error) { +- ctx, done := event.Start(ctx, "lsp.Server.formatting", tag.URI.Of(params.TextDocument.URI)) +- defer done() - -- firstComment := commentGrp.List[0] -- startPos, endLinePos := firstComment.Pos(), firstComment.End() -- startCmmntLine, endCmmntLine := safetoken.Line(tokFile, startPos), safetoken.Line(tokFile, endLinePos) -- if startCmmntLine != endCmmntLine { -- // If the first comment spans multiple lines, then we want to have the -- // folding range start at the end of the first line. -- endLinePos = token.Pos(int(startPos) + len(strings.Split(firstComment.Text, "\n")[0])) -- } -- mrng, err := pgf.PosMappedRange(endLinePos, commentGrp.End()) -- if err != nil { -- bug.Errorf("%w", err) // can't happen -- } -- comments = append(comments, &FoldingRangeInfo{ -- // Fold from the end of the first line comment to the end of the comment block. -- MappedRange: mrng, -- Kind: protocol.Comment, -- }) +- fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI) +- if err != nil { +- return nil, err - } -- return comments +- defer release() +- +- switch snapshot.FileKind(fh) { +- case file.Mod: +- return mod.Format(ctx, snapshot, fh) +- case file.Go: +- return golang.Format(ctx, snapshot, fh) +- case file.Work: +- return work.Format(ctx, snapshot, fh) +- } +- return nil, nil // empty result -} -diff -urN a/gopls/internal/lsp/source/format.go b/gopls/internal/lsp/source/format.go ---- a/gopls/internal/lsp/source/format.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/format.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,388 +0,0 @@ --// Copyright 2018 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/server/general.go b/gopls/internal/server/general.go +--- a/gopls/internal/server/general.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/general.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,685 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --// Package source provides core features for use by Go editors and tools. --package source +-package server +- +-// This file defines server methods related to initialization, +-// options, shutdown, and exit. - -import ( -- "bytes" - "context" +- "encoding/json" - "fmt" -- "go/ast" -- "go/format" -- "go/parser" -- "go/token" +- "go/build" +- "log" +- "os" +- "path" +- "path/filepath" +- "sort" - "strings" -- "text/scanner" +- "sync" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/internal/diff" +- "golang.org/x/telemetry/counter" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/debug" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/settings" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/gopls/internal/util/goversion" +- "golang.org/x/tools/gopls/internal/util/maps" - "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/imports" -- "golang.org/x/tools/internal/tokeninternal" +- "golang.org/x/tools/internal/jsonrpc2" -) - --// Format formats a file with a given range. --func Format(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.TextEdit, error) { -- ctx, done := event.Start(ctx, "source.Format") -- defer done() +-func (s *server) Initialize(ctx context.Context, params *protocol.ParamInitialize) (*protocol.InitializeResult, error) { +- ctx, done := event.Start(ctx, "lsp.Server.initialize") +- defer done() +- +- var clientName string +- if params != nil && params.ClientInfo != nil { +- clientName = params.ClientInfo.Name +- } +- recordClientInfo(clientName) +- +- s.stateMu.Lock() +- if s.state >= serverInitializing { +- defer s.stateMu.Unlock() +- return nil, fmt.Errorf("%w: initialize called while server in %v state", jsonrpc2.ErrInvalidRequest, s.state) +- } +- s.state = serverInitializing +- s.stateMu.Unlock() +- +- // For uniqueness, use the gopls PID rather than params.ProcessID (the client +- // pid). Some clients might start multiple gopls servers, though they +- // probably shouldn't. +- pid := os.Getpid() +- s.tempDir = filepath.Join(os.TempDir(), fmt.Sprintf("gopls-%d.%s", pid, s.session.ID())) +- err := os.Mkdir(s.tempDir, 0700) +- if err != nil { +- // MkdirTemp could fail due to permissions issues. This is a problem with +- // the user's environment, but should not block gopls otherwise behaving. +- // All usage of s.tempDir should be predicated on having a non-empty +- // s.tempDir. +- event.Error(ctx, "creating temp dir", err) +- s.tempDir = "" +- } +- s.progress.SetSupportsWorkDoneProgress(params.Capabilities.Window.WorkDoneProgress) +- +- options := s.Options().Clone() +- // TODO(rfindley): remove the error return from handleOptionResults, and +- // eliminate this defer. +- defer func() { s.SetOptions(options) }() - -- // Generated files shouldn't be edited. So, don't format them -- if IsGenerated(ctx, snapshot, fh.URI()) { -- return nil, fmt.Errorf("can't format %q: file is generated", fh.URI().Filename()) +- if err := s.handleOptionResults(ctx, settings.SetOptions(options, params.InitializationOptions)); err != nil { +- return nil, err - } +- options.ForClientCapabilities(params.ClientInfo, params.Capabilities) - -- pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) -- if err != nil { -- return nil, err +- if options.ShowBugReports { +- // Report the next bug that occurs on the server. +- bug.Handle(func(b bug.Bug) { +- msg := &protocol.ShowMessageParams{ +- Type: protocol.Error, +- Message: fmt.Sprintf("A bug occurred on the server: %s\nLocation:%s", b.Description, b.Key), +- } +- go func() { +- if err := s.eventuallyShowMessage(context.Background(), msg); err != nil { +- log.Printf("error showing bug: %v", err) +- } +- }() +- }) - } -- // Even if this file has parse errors, it might still be possible to format it. -- // Using format.Node on an AST with errors may result in code being modified. -- // Attempt to format the source of this file instead. -- if pgf.ParseErr != nil { -- formatted, err := formatSource(ctx, fh) -- if err != nil { -- return nil, err +- +- folders := params.WorkspaceFolders +- if len(folders) == 0 { +- if params.RootURI != "" { +- folders = []protocol.WorkspaceFolder{{ +- URI: string(params.RootURI), +- Name: path.Base(params.RootURI.Path()), +- }} - } -- return computeTextEdits(ctx, snapshot, pgf, string(formatted)) - } -- -- // format.Node changes slightly from one release to another, so the version -- // of Go used to build the LSP server will determine how it formats code. -- // This should be acceptable for all users, who likely be prompted to rebuild -- // the LSP server on each Go release. -- buf := &bytes.Buffer{} -- fset := tokeninternal.FileSetFor(pgf.Tok) -- if err := format.Node(buf, fset, pgf.File); err != nil { -- return nil, err +- for _, folder := range folders { +- if folder.URI == "" { +- return nil, fmt.Errorf("empty WorkspaceFolder.URI") +- } +- if _, err := protocol.ParseDocumentURI(folder.URI); err != nil { +- return nil, fmt.Errorf("invalid WorkspaceFolder.URI: %v", err) +- } +- s.pendingFolders = append(s.pendingFolders, folder) - } -- formatted := buf.String() - -- // Apply additional formatting, if any is supported. Currently, the only -- // supported additional formatter is gofumpt. -- if format := snapshot.Options().GofumptFormat; snapshot.Options().Gofumpt && format != nil { -- // gofumpt can customize formatting based on language version and module -- // path, if available. -- // -- // Try to derive this information, but fall-back on the default behavior. +- var codeActionProvider interface{} = true +- if ca := params.Capabilities.TextDocument.CodeAction; len(ca.CodeActionLiteralSupport.CodeActionKind.ValueSet) > 0 { +- // If the client has specified CodeActionLiteralSupport, +- // send the code actions we support. - // -- // TODO: under which circumstances can we fail to find module information? -- // Can this, for example, result in inconsistent formatting across saves, -- // due to pending calls to packages.Load? -- var langVersion, modulePath string -- meta, err := NarrowestMetadataForFile(ctx, snapshot, fh.URI()) -- if err == nil { -- if mi := meta.Module; mi != nil { -- langVersion = mi.GoVersion -- modulePath = mi.Path -- } +- // Using CodeActionOptions is only valid if codeActionLiteralSupport is set. +- codeActionProvider = &protocol.CodeActionOptions{ +- CodeActionKinds: s.getSupportedCodeActions(), +- ResolveProvider: true, - } -- b, err := format(ctx, langVersion, modulePath, buf.Bytes()) -- if err != nil { -- return nil, err +- } +- var renameOpts interface{} = true +- if r := params.Capabilities.TextDocument.Rename; r != nil && r.PrepareSupport { +- renameOpts = protocol.RenameOptions{ +- PrepareProvider: r.PrepareSupport, - } -- formatted = string(b) - } -- return computeTextEdits(ctx, snapshot, pgf, formatted) --} - --func formatSource(ctx context.Context, fh FileHandle) ([]byte, error) { -- _, done := event.Start(ctx, "source.formatSource") -- defer done() +- versionInfo := debug.VersionInfo() - -- data, err := fh.Content() +- goplsVersion, err := json.Marshal(versionInfo) - if err != nil { - return nil, err - } -- return format.Source(data) --} - --type ImportFix struct { -- Fix *imports.ImportFix -- Edits []protocol.TextEdit +- return &protocol.InitializeResult{ +- Capabilities: protocol.ServerCapabilities{ +- CallHierarchyProvider: &protocol.Or_ServerCapabilities_callHierarchyProvider{Value: true}, +- CodeActionProvider: codeActionProvider, +- CodeLensProvider: &protocol.CodeLensOptions{}, // must be non-nil to enable the code lens capability +- CompletionProvider: &protocol.CompletionOptions{ +- TriggerCharacters: []string{"."}, +- }, +- DefinitionProvider: &protocol.Or_ServerCapabilities_definitionProvider{Value: true}, +- TypeDefinitionProvider: &protocol.Or_ServerCapabilities_typeDefinitionProvider{Value: true}, +- ImplementationProvider: &protocol.Or_ServerCapabilities_implementationProvider{Value: true}, +- DocumentFormattingProvider: &protocol.Or_ServerCapabilities_documentFormattingProvider{Value: true}, +- DocumentSymbolProvider: &protocol.Or_ServerCapabilities_documentSymbolProvider{Value: true}, +- WorkspaceSymbolProvider: &protocol.Or_ServerCapabilities_workspaceSymbolProvider{Value: true}, +- ExecuteCommandProvider: &protocol.ExecuteCommandOptions{ +- Commands: protocol.NonNilSlice(options.SupportedCommands), +- }, +- FoldingRangeProvider: &protocol.Or_ServerCapabilities_foldingRangeProvider{Value: true}, +- HoverProvider: &protocol.Or_ServerCapabilities_hoverProvider{Value: true}, +- DocumentHighlightProvider: &protocol.Or_ServerCapabilities_documentHighlightProvider{Value: true}, +- DocumentLinkProvider: &protocol.DocumentLinkOptions{}, +- InlayHintProvider: protocol.InlayHintOptions{}, +- ReferencesProvider: &protocol.Or_ServerCapabilities_referencesProvider{Value: true}, +- RenameProvider: renameOpts, +- SelectionRangeProvider: &protocol.Or_ServerCapabilities_selectionRangeProvider{Value: true}, +- SemanticTokensProvider: protocol.SemanticTokensOptions{ +- Range: &protocol.Or_SemanticTokensOptions_range{Value: true}, +- Full: &protocol.Or_SemanticTokensOptions_full{Value: true}, +- Legend: protocol.SemanticTokensLegend{ +- TokenTypes: protocol.NonNilSlice(options.SemanticTypes), +- TokenModifiers: protocol.NonNilSlice(options.SemanticMods), +- }, +- }, +- SignatureHelpProvider: &protocol.SignatureHelpOptions{ +- TriggerCharacters: []string{"(", ","}, +- }, +- TextDocumentSync: &protocol.TextDocumentSyncOptions{ +- Change: protocol.Incremental, +- OpenClose: true, +- Save: &protocol.SaveOptions{ +- IncludeText: false, +- }, +- }, +- Workspace: &protocol.WorkspaceOptions{ +- WorkspaceFolders: &protocol.WorkspaceFolders5Gn{ +- Supported: true, +- ChangeNotifications: "workspace/didChangeWorkspaceFolders", +- }, +- }, +- }, +- ServerInfo: &protocol.ServerInfo{ +- Name: "gopls", +- Version: string(goplsVersion), +- }, +- }, nil -} - --// AllImportsFixes formats f for each possible fix to the imports. --// In addition to returning the result of applying all edits, --// it returns a list of fixes that could be applied to the file, with the --// corresponding TextEdits that would be needed to apply that fix. --func AllImportsFixes(ctx context.Context, snapshot Snapshot, pgf *ParsedGoFile) (allFixEdits []protocol.TextEdit, editsPerFix []*ImportFix, err error) { -- ctx, done := event.Start(ctx, "source.AllImportsFixes") +-func (s *server) Initialized(ctx context.Context, params *protocol.InitializedParams) error { +- ctx, done := event.Start(ctx, "lsp.Server.initialized") - defer done() - -- if err := snapshot.RunProcessEnvFunc(ctx, func(ctx context.Context, opts *imports.Options) error { -- allFixEdits, editsPerFix, err = computeImportEdits(ctx, snapshot, pgf, opts) -- return err -- }); err != nil { -- return nil, nil, fmt.Errorf("AllImportsFixes: %v", err) +- s.stateMu.Lock() +- if s.state >= serverInitialized { +- defer s.stateMu.Unlock() +- return fmt.Errorf("%w: initialized called while server in %v state", jsonrpc2.ErrInvalidRequest, s.state) - } -- return allFixEdits, editsPerFix, nil --} +- s.state = serverInitialized +- s.stateMu.Unlock() - --// computeImportEdits computes a set of edits that perform one or all of the --// necessary import fixes. --func computeImportEdits(ctx context.Context, snapshot Snapshot, pgf *ParsedGoFile, options *imports.Options) (allFixEdits []protocol.TextEdit, editsPerFix []*ImportFix, err error) { -- filename := pgf.URI.Filename() +- for _, not := range s.notifications { +- s.client.ShowMessage(ctx, not) +- } +- s.notifications = nil - -- // Build up basic information about the original file. -- allFixes, err := imports.FixImports(ctx, filename, pgf.Src, options) -- if err != nil { -- return nil, nil, err +- s.addFolders(ctx, s.pendingFolders) +- +- s.pendingFolders = nil +- s.checkViewGoVersions() +- +- var registrations []protocol.Registration +- options := s.Options() +- if options.ConfigurationSupported && options.DynamicConfigurationSupported { +- registrations = append(registrations, protocol.Registration{ +- ID: "workspace/didChangeConfiguration", +- Method: "workspace/didChangeConfiguration", +- }) +- } +- if len(registrations) > 0 { +- if err := s.client.RegisterCapability(ctx, &protocol.RegistrationParams{ +- Registrations: registrations, +- }); err != nil { +- return err +- } - } - -- allFixEdits, err = computeFixEdits(snapshot, pgf, options, allFixes) -- if err != nil { -- return nil, nil, err +- // Ask (maybe) about enabling telemetry. Do this asynchronously, as it's OK +- // for users to ignore or dismiss the question. +- go s.maybePromptForTelemetry(ctx, options.TelemetryPrompt) +- +- return nil +-} +- +-// checkViewGoVersions checks whether any Go version used by a view is too old, +-// raising a showMessage notification if so. +-// +-// It should be called after views change. +-func (s *server) checkViewGoVersions() { +- oldestVersion, fromBuild := go1Point(), true +- for _, view := range s.session.Views() { +- viewVersion := view.GoVersion() +- if oldestVersion == -1 || viewVersion < oldestVersion { +- oldestVersion, fromBuild = viewVersion, false +- } +- if viewVersion >= 0 { +- counter.Inc(fmt.Sprintf("gopls/goversion:1.%d", viewVersion)) +- } - } - -- // Apply all of the import fixes to the file. -- // Add the edits for each fix to the result. -- for _, fix := range allFixes { -- edits, err := computeFixEdits(snapshot, pgf, options, []*imports.ImportFix{fix}) -- if err != nil { -- return nil, nil, err +- if msg, isError := goversion.Message(oldestVersion, fromBuild); msg != "" { +- mType := protocol.Warning +- if isError { +- mType = protocol.Error - } -- editsPerFix = append(editsPerFix, &ImportFix{ -- Fix: fix, -- Edits: edits, +- s.eventuallyShowMessage(context.Background(), &protocol.ShowMessageParams{ +- Type: mType, +- Message: msg, - }) - } -- return allFixEdits, editsPerFix, nil -} - --// ComputeOneImportFixEdits returns text edits for a single import fix. --func ComputeOneImportFixEdits(snapshot Snapshot, pgf *ParsedGoFile, fix *imports.ImportFix) ([]protocol.TextEdit, error) { -- options := &imports.Options{ -- LocalPrefix: snapshot.Options().Local, -- // Defaults. -- AllErrors: true, -- Comments: true, -- Fragment: true, -- FormatOnly: false, -- TabIndent: true, -- TabWidth: 8, +-// go1Point returns the x in Go 1.x. If an error occurs extracting the go +-// version, it returns -1. +-// +-// Copied from the testenv package. +-func go1Point() int { +- for i := len(build.Default.ReleaseTags) - 1; i >= 0; i-- { +- var version int +- if _, err := fmt.Sscanf(build.Default.ReleaseTags[i], "go1.%d", &version); err != nil { +- continue +- } +- return version - } -- return computeFixEdits(snapshot, pgf, options, []*imports.ImportFix{fix}) +- return -1 -} - --func computeFixEdits(snapshot Snapshot, pgf *ParsedGoFile, options *imports.Options, fixes []*imports.ImportFix) ([]protocol.TextEdit, error) { -- // trim the original data to match fixedData -- left, err := importPrefix(pgf.Src) -- if err != nil { -- return nil, err -- } -- extra := !strings.Contains(left, "\n") // one line may have more than imports -- if extra { -- left = string(pgf.Src) -- } -- if len(left) > 0 && left[len(left)-1] != '\n' { -- left += "\n" -- } -- // Apply the fixes and re-parse the file so that we can locate the -- // new imports. -- flags := parser.ImportsOnly -- if extra { -- // used all of origData above, use all of it here too -- flags = 0 -- } -- fixedData, err := imports.ApplyFixes(fixes, "", pgf.Src, options, flags) -- if err != nil { -- return nil, err -- } -- if fixedData == nil || fixedData[len(fixedData)-1] != '\n' { -- fixedData = append(fixedData, '\n') // ApplyFixes may miss the newline, go figure. -- } -- edits := snapshot.Options().ComputeEdits(left, string(fixedData)) -- return protocolEditsFromSource([]byte(left), edits) --} +-// addFolders adds the specified list of "folders" (that's Windows for +-// directories) to the session. It does not return an error, though it +-// may report an error to the client over LSP if one or more folders +-// had problems. +-func (s *server) addFolders(ctx context.Context, folders []protocol.WorkspaceFolder) { +- originalViews := len(s.session.Views()) +- viewErrors := make(map[protocol.URI]error) - --// importPrefix returns the prefix of the given file content through the final --// import statement. If there are no imports, the prefix is the package --// statement and any comment groups below it. --func importPrefix(src []byte) (string, error) { -- fset := token.NewFileSet() -- // do as little parsing as possible -- f, err := parser.ParseFile(fset, "", src, parser.ImportsOnly|parser.ParseComments) -- if err != nil { // This can happen if 'package' is misspelled -- return "", fmt.Errorf("importPrefix: failed to parse: %s", err) +- var ndiagnose sync.WaitGroup // number of unfinished diagnose calls +- if s.Options().VerboseWorkDoneProgress { +- work := s.progress.Start(ctx, DiagnosticWorkTitle(FromInitialWorkspaceLoad), "Calculating diagnostics for initial workspace load...", nil, nil) +- defer func() { +- go func() { +- ndiagnose.Wait() +- work.End(ctx, "Done.") +- }() +- }() - } -- tok := fset.File(f.Pos()) -- var importEnd int -- for _, d := range f.Decls { -- if x, ok := d.(*ast.GenDecl); ok && x.Tok == token.IMPORT { -- if e, err := safetoken.Offset(tok, d.End()); err != nil { -- return "", fmt.Errorf("importPrefix: %s", err) -- } else if e > importEnd { -- importEnd = e +- // Only one view gets to have a workspace. +- var nsnapshots sync.WaitGroup // number of unfinished snapshot initializations +- for _, folder := range folders { +- uri, err := protocol.ParseDocumentURI(folder.URI) +- if err != nil { +- viewErrors[folder.URI] = fmt.Errorf("invalid folder URI: %v", err) +- continue +- } +- work := s.progress.Start(ctx, "Setting up workspace", "Loading packages...", nil, nil) +- snapshot, release, err := s.addView(ctx, folder.Name, uri) +- if err != nil { +- if err == cache.ErrViewExists { +- continue - } +- viewErrors[folder.URI] = err +- work.End(ctx, fmt.Sprintf("Error loading packages: %s", err)) +- continue - } +- // Inv: release() must be called once. +- +- // Initialize snapshot asynchronously. +- initialized := make(chan struct{}) +- nsnapshots.Add(1) +- go func() { +- snapshot.AwaitInitialized(ctx) +- work.End(ctx, "Finished loading packages.") +- nsnapshots.Done() +- close(initialized) // signal +- }() +- +- // Diagnose the newly created view asynchronously. +- ndiagnose.Add(1) +- go func() { +- s.diagnoseSnapshot(snapshot, nil, 0) +- <-initialized +- release() +- ndiagnose.Done() +- }() - } - -- maybeAdjustToLineEnd := func(pos token.Pos, isCommentNode bool) int { -- offset, err := safetoken.Offset(tok, pos) -- if err != nil { -- return -1 -- } +- // Wait for snapshots to be initialized so that all files are known. +- // (We don't need to wait for diagnosis to finish.) +- nsnapshots.Wait() - -- // Don't go past the end of the file. -- if offset > len(src) { -- offset = len(src) -- } -- // The go/ast package does not account for different line endings, and -- // specifically, in the text of a comment, it will strip out \r\n line -- // endings in favor of \n. To account for these differences, we try to -- // return a position on the next line whenever possible. -- switch line := safetoken.Line(tok, tok.Pos(offset)); { -- case line < tok.LineCount(): -- nextLineOffset, err := safetoken.Offset(tok, tok.LineStart(line+1)) -- if err != nil { -- return -1 -- } -- // If we found a position that is at the end of a line, move the -- // offset to the start of the next line. -- if offset+1 == nextLineOffset { -- offset = nextLineOffset -- } -- case isCommentNode, offset+1 == tok.Size(): -- // If the last line of the file is a comment, or we are at the end -- // of the file, the prefix is the entire file. -- offset = len(src) +- // Register for file watching notifications, if they are supported. +- if err := s.updateWatchedDirectories(ctx); err != nil { +- event.Error(ctx, "failed to register for file watching notifications", err) +- } +- +- // Report any errors using the protocol. +- if len(viewErrors) > 0 { +- errMsg := fmt.Sprintf("Error loading workspace folders (expected %v, got %v)\n", len(folders), len(s.session.Views())-originalViews) +- for uri, err := range viewErrors { +- errMsg += fmt.Sprintf("failed to load view for %s: %v\n", uri, err) - } -- return offset +- showMessage(ctx, s.client, protocol.Error, errMsg) - } -- if importEnd == 0 { -- pkgEnd := f.Name.End() -- importEnd = maybeAdjustToLineEnd(pkgEnd, false) +-} +- +-// updateWatchedDirectories compares the current set of directories to watch +-// with the previously registered set of directories. If the set of directories +-// has changed, we unregister and re-register for file watching notifications. +-// updatedSnapshots is the set of snapshots that have been updated. +-func (s *server) updateWatchedDirectories(ctx context.Context) error { +- patterns := s.session.FileWatchingGlobPatterns(ctx) +- +- s.watchedGlobPatternsMu.Lock() +- defer s.watchedGlobPatternsMu.Unlock() +- +- // Nothing to do if the set of workspace directories is unchanged. +- if maps.SameKeys(s.watchedGlobPatterns, patterns) { +- return nil - } -- for _, cgroup := range f.Comments { -- for _, c := range cgroup.List { -- if end, err := safetoken.Offset(tok, c.End()); err != nil { -- return "", err -- } else if end > importEnd { -- startLine := safetoken.Position(tok, c.Pos()).Line -- endLine := safetoken.Position(tok, c.End()).Line - -- // Work around golang/go#41197 by checking if the comment might -- // contain "\r", and if so, find the actual end position of the -- // comment by scanning the content of the file. -- startOffset, err := safetoken.Offset(tok, c.Pos()) -- if err != nil { -- return "", err -- } -- if startLine != endLine && bytes.Contains(src[startOffset:], []byte("\r")) { -- if commentEnd := scanForCommentEnd(src[startOffset:]); commentEnd > 0 { -- end = startOffset + commentEnd -- } -- } -- importEnd = maybeAdjustToLineEnd(tok.Pos(end), true) -- } -- } +- // If the set of directories to watch has changed, register the updates and +- // unregister the previously watched directories. This ordering avoids a +- // period where no files are being watched. Still, if a user makes on-disk +- // changes before these updates are complete, we may miss them for the new +- // directories. +- prevID := s.watchRegistrationCount - 1 +- if err := s.registerWatchedDirectoriesLocked(ctx, patterns); err != nil { +- return err - } -- if importEnd > len(src) { -- importEnd = len(src) +- if prevID >= 0 { +- return s.client.UnregisterCapability(ctx, &protocol.UnregistrationParams{ +- Unregisterations: []protocol.Unregistration{{ +- ID: watchedFilesCapabilityID(prevID), +- Method: "workspace/didChangeWatchedFiles", +- }}, +- }) - } -- return string(src[:importEnd]), nil +- return nil -} - --// scanForCommentEnd returns the offset of the end of the multi-line comment --// at the start of the given byte slice. --func scanForCommentEnd(src []byte) int { -- var s scanner.Scanner -- s.Init(bytes.NewReader(src)) -- s.Mode ^= scanner.SkipComments +-func watchedFilesCapabilityID(id int) string { +- return fmt.Sprintf("workspace/didChangeWatchedFiles-%d", id) +-} - -- t := s.Scan() -- if t == scanner.Comment { -- return s.Pos().Offset +-// registerWatchedDirectoriesLocked sends the workspace/didChangeWatchedFiles +-// registrations to the client and updates s.watchedDirectories. +-// The caller must not subsequently mutate patterns. +-func (s *server) registerWatchedDirectoriesLocked(ctx context.Context, patterns map[protocol.RelativePattern]unit) error { +- if !s.Options().DynamicWatchedFilesSupported { +- return nil - } -- return 0 +- +- supportsRelativePatterns := s.Options().RelativePatternsSupported +- +- s.watchedGlobPatterns = patterns +- watchers := make([]protocol.FileSystemWatcher, 0, len(patterns)) // must be a slice +- val := protocol.WatchChange | protocol.WatchDelete | protocol.WatchCreate +- for pattern := range patterns { +- var value any +- if supportsRelativePatterns && pattern.BaseURI != "" { +- value = pattern +- } else { +- p := pattern.Pattern +- if pattern.BaseURI != "" { +- p = path.Join(filepath.ToSlash(pattern.BaseURI.Path()), p) +- } +- value = p +- } +- watchers = append(watchers, protocol.FileSystemWatcher{ +- GlobPattern: protocol.GlobPattern{Value: value}, +- Kind: &val, +- }) +- } +- +- if err := s.client.RegisterCapability(ctx, &protocol.RegistrationParams{ +- Registrations: []protocol.Registration{{ +- ID: watchedFilesCapabilityID(s.watchRegistrationCount), +- Method: "workspace/didChangeWatchedFiles", +- RegisterOptions: protocol.DidChangeWatchedFilesRegistrationOptions{ +- Watchers: watchers, +- }, +- }}, +- }); err != nil { +- return err +- } +- s.watchRegistrationCount++ +- return nil -} - --func computeTextEdits(ctx context.Context, snapshot Snapshot, pgf *ParsedGoFile, formatted string) ([]protocol.TextEdit, error) { -- _, done := event.Start(ctx, "source.computeTextEdits") -- defer done() +-// Options returns the current server options. +-// +-// The caller must not modify the result. +-func (s *server) Options() *settings.Options { +- s.optionsMu.Lock() +- defer s.optionsMu.Unlock() +- return s.options +-} - -- edits := snapshot.Options().ComputeEdits(string(pgf.Src), formatted) -- return ToProtocolEdits(pgf.Mapper, edits) +-// SetOptions sets the current server options. +-// +-// The caller must not subsequently modify the options. +-func (s *server) SetOptions(opts *settings.Options) { +- s.optionsMu.Lock() +- defer s.optionsMu.Unlock() +- s.options = opts -} - --// protocolEditsFromSource converts text edits to LSP edits using the original --// source. --func protocolEditsFromSource(src []byte, edits []diff.Edit) ([]protocol.TextEdit, error) { -- m := protocol.NewMapper("", src) -- var result []protocol.TextEdit -- for _, edit := range edits { -- rng, err := m.OffsetRange(edit.Start, edit.End) +-func (s *server) newFolder(ctx context.Context, folder protocol.DocumentURI, name string) (*cache.Folder, error) { +- opts := s.Options() +- if opts.ConfigurationSupported { +- scope := string(folder) +- configs, err := s.client.Configuration(ctx, &protocol.ParamConfiguration{ +- Items: []protocol.ConfigurationItem{{ +- ScopeURI: &scope, +- Section: "gopls", +- }}, +- }, +- ) - if err != nil { -- return nil, err +- return nil, fmt.Errorf("failed to get workspace configuration from client (%s): %v", folder, err) - } - -- if rng.Start == rng.End && edit.New == "" { -- // Degenerate case, which may result from a diff tool wanting to delete -- // '\r' in line endings. Filter it out. -- continue +- opts = opts.Clone() +- for _, config := range configs { +- if err := s.handleOptionResults(ctx, settings.SetOptions(opts, config)); err != nil { +- return nil, err +- } - } -- result = append(result, protocol.TextEdit{ -- Range: rng, -- NewText: edit.New, -- }) - } -- return result, nil +- +- env, err := cache.FetchGoEnv(ctx, folder, opts) +- if err != nil { +- return nil, err +- } +- return &cache.Folder{ +- Dir: folder, +- Name: name, +- Options: opts, +- Env: env, +- }, nil -} - --// ToProtocolEdits converts diff.Edits to a non-nil slice of LSP TextEdits. --// See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textEditArray --func ToProtocolEdits(m *protocol.Mapper, edits []diff.Edit) ([]protocol.TextEdit, error) { -- // LSP doesn't require TextEditArray to be sorted: -- // this is the receiver's concern. But govim, and perhaps -- // other clients have historically relied on the order. -- edits = append([]diff.Edit(nil), edits...) -- diff.SortEdits(edits) +-// fetchFolderOptions makes a workspace/configuration request for the given +-// folder, and populates options with the result. +-// +-// If folder is "", fetchFolderOptions makes an unscoped request. +-func (s *server) fetchFolderOptions(ctx context.Context, folder protocol.DocumentURI) (*settings.Options, error) { +- opts := s.Options() +- if !opts.ConfigurationSupported { +- return opts, nil +- } +- var scopeURI *string +- if folder != "" { +- scope := string(folder) +- scopeURI = &scope +- } +- configs, err := s.client.Configuration(ctx, &protocol.ParamConfiguration{ +- Items: []protocol.ConfigurationItem{{ +- ScopeURI: scopeURI, +- Section: "gopls", +- }}, +- }, +- ) +- if err != nil { +- return nil, fmt.Errorf("failed to get workspace configuration from client (%s): %v", folder, err) +- } - -- result := make([]protocol.TextEdit, len(edits)) -- for i, edit := range edits { -- rng, err := m.OffsetRange(edit.Start, edit.End) -- if err != nil { +- opts = opts.Clone() +- for _, config := range configs { +- if err := s.handleOptionResults(ctx, settings.SetOptions(opts, config)); err != nil { - return nil, err - } -- result[i] = protocol.TextEdit{ -- Range: rng, -- NewText: edit.New, -- } - } -- return result, nil +- return opts, nil -} - --// FromProtocolEdits converts LSP TextEdits to diff.Edits. --// See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textEditArray --func FromProtocolEdits(m *protocol.Mapper, edits []protocol.TextEdit) ([]diff.Edit, error) { -- if edits == nil { -- return nil, nil +-func (s *server) eventuallyShowMessage(ctx context.Context, msg *protocol.ShowMessageParams) error { +- s.stateMu.Lock() +- defer s.stateMu.Unlock() +- if s.state == serverInitialized { +- return s.client.ShowMessage(ctx, msg) - } -- result := make([]diff.Edit, len(edits)) -- for i, edit := range edits { -- start, end, err := m.RangeOffsets(edit.Range) -- if err != nil { -- return nil, err +- s.notifications = append(s.notifications, msg) +- return nil +-} +- +-func (s *server) handleOptionResults(ctx context.Context, results settings.OptionResults) error { +- var warnings, errors []string +- for _, result := range results { +- switch result.Error.(type) { +- case nil: +- // nothing to do +- case *settings.SoftError: +- warnings = append(warnings, result.Error.Error()) +- default: +- errors = append(errors, result.Error.Error()) - } -- result[i] = diff.Edit{ -- Start: start, -- End: end, -- New: edit.NewText, +- } +- +- // Sort messages, but put errors first. +- // +- // Having stable content for the message allows clients to de-duplicate. This +- // matters because we may send duplicate warnings for clients that support +- // dynamic configuration: one for the initial settings, and then more for the +- // individual viewsettings. +- var msgs []string +- msgType := protocol.Warning +- if len(errors) > 0 { +- msgType = protocol.Error +- sort.Strings(errors) +- msgs = append(msgs, errors...) +- } +- if len(warnings) > 0 { +- sort.Strings(warnings) +- msgs = append(msgs, warnings...) +- } +- +- if len(msgs) > 0 { +- // Settings +- combined := "Invalid settings: " + strings.Join(msgs, "; ") +- params := &protocol.ShowMessageParams{ +- Type: msgType, +- Message: combined, - } +- return s.eventuallyShowMessage(ctx, params) - } -- return result, nil +- +- return nil -} - --// ApplyProtocolEdits applies the patch (edits) to m.Content and returns the result. --// It also returns the edits converted to diff-package form. --func ApplyProtocolEdits(m *protocol.Mapper, edits []protocol.TextEdit) ([]byte, []diff.Edit, error) { -- diffEdits, err := FromProtocolEdits(m, edits) +-// fileOf returns the file for a given URI and its snapshot. +-// On success, the returned function must be called to release the snapshot. +-func (s *server) fileOf(ctx context.Context, uri protocol.DocumentURI) (file.Handle, *cache.Snapshot, func(), error) { +- snapshot, release, err := s.session.SnapshotOf(ctx, uri) - if err != nil { -- return nil, nil, err +- return nil, nil, nil, err - } -- out, err := diff.ApplyBytes(m.Content, diffEdits) -- return out, diffEdits, err +- fh, err := snapshot.ReadFile(ctx, uri) +- if err != nil { +- release() +- return nil, nil, nil, err +- } +- return fh, snapshot, release, nil -} -diff -urN a/gopls/internal/lsp/source/format_test.go b/gopls/internal/lsp/source/format_test.go ---- a/gopls/internal/lsp/source/format_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/format_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,75 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package source -- --import ( -- "strings" -- "testing" -- -- "golang.org/x/tools/gopls/internal/lsp/tests/compare" --) +-// shutdown implements the 'shutdown' LSP handler. It releases resources +-// associated with the server and waits for all ongoing work to complete. +-func (s *server) Shutdown(ctx context.Context) error { +- ctx, done := event.Start(ctx, "lsp.Server.shutdown") +- defer done() - --func TestImportPrefix(t *testing.T) { -- for i, tt := range []struct { -- input, want string -- }{ -- {"package foo", "package foo"}, -- {"package foo\n", "package foo\n"}, -- {"package foo\n\nfunc f(){}\n", "package foo\n"}, -- {"package foo\n\nimport \"fmt\"\n", "package foo\n\nimport \"fmt\""}, -- {"package foo\nimport (\n\"fmt\"\n)\n", "package foo\nimport (\n\"fmt\"\n)"}, -- {"\n\n\npackage foo\n", "\n\n\npackage foo\n"}, -- {"// hi \n\npackage foo //xx\nfunc _(){}\n", "// hi \n\npackage foo //xx\n"}, -- {"package foo //hi\n", "package foo //hi\n"}, -- {"//hi\npackage foo\n//a\n\n//b\n", "//hi\npackage foo\n//a\n\n//b\n"}, -- { -- "package a\n\nimport (\n \"fmt\"\n)\n//hi\n", -- "package a\n\nimport (\n \"fmt\"\n)\n//hi\n", -- }, -- {`package a /*hi*/`, `package a /*hi*/`}, -- {"package main\r\n\r\nimport \"go/types\"\r\n\r\n/*\r\n\r\n */\r\n", "package main\r\n\r\nimport \"go/types\"\r\n\r\n/*\r\n\r\n */\r\n"}, -- {"package x; import \"os\"; func f() {}\n\n", "package x; import \"os\""}, -- {"package x; func f() {fmt.Println()}\n\n", "package x"}, -- } { -- got, err := importPrefix([]byte(tt.input)) -- if err != nil { -- t.Fatal(err) +- s.stateMu.Lock() +- defer s.stateMu.Unlock() +- if s.state < serverInitialized { +- event.Log(ctx, "server shutdown without initialization") +- } +- if s.state != serverShutDown { +- // Wait for the webserver (if any) to finish. +- if s.web != nil { +- s.web.server.Shutdown(ctx) - } -- if d := compare.Text(tt.want, got); d != "" { -- t.Errorf("%d: failed for %q:\n%s", i, tt.input, d) +- +- // drop all the active views +- s.session.Shutdown(ctx) +- s.state = serverShutDown +- if s.tempDir != "" { +- if err := os.RemoveAll(s.tempDir); err != nil { +- event.Error(ctx, "removing temp dir", err) +- } - } - } +- return nil -} - --func TestCRLFFile(t *testing.T) { -- for i, tt := range []struct { -- input, want string -- }{ -- { -- input: `package main +-func (s *server) Exit(ctx context.Context) error { +- ctx, done := event.Start(ctx, "lsp.Server.exit") +- defer done() - --/* --Hi description --*/ --func Hi() { +- s.stateMu.Lock() +- defer s.stateMu.Unlock() +- +- s.client.Close() +- +- if s.state != serverShutDown { +- // TODO: We should be able to do better than this. +- os.Exit(1) +- } +- // We don't terminate the process on a normal exit, we just allow it to +- // close naturally if needed after the connection is closed. +- return nil -} --`, -- want: `package main - --/* --Hi description --*/`, -- }, -- } { -- got, err := importPrefix([]byte(strings.ReplaceAll(tt.input, "\n", "\r\n"))) -- if err != nil { -- t.Fatal(err) -- } -- want := strings.ReplaceAll(tt.want, "\n", "\r\n") -- if d := compare.Text(want, got); d != "" { -- t.Errorf("%d: failed for %q:\n%s", i, tt.input, d) +-// recordClientInfo records gopls client info. +-func recordClientInfo(clientName string) { +- key := "gopls/client:other" +- switch clientName { +- case "Visual Studio Code": +- key = "gopls/client:vscode" +- case "Visual Studio Code - Insiders": +- key = "gopls/client:vscode-insiders" +- case "VSCodium": +- key = "gopls/client:vscodium" +- case "code-server": +- // https://github.com/coder/code-server/blob/3cb92edc76ecc2cfa5809205897d93d4379b16a6/ci/build/build-vscode.sh#L19 +- key = "gopls/client:code-server" +- case "Eglot": +- // https://lists.gnu.org/archive/html/bug-gnu-emacs/2023-03/msg00954.html +- key = "gopls/client:eglot" +- case "govim": +- // https://github.com/govim/govim/pull/1189 +- key = "gopls/client:govim" +- case "Neovim": +- // https://github.com/neovim/neovim/blob/42333ea98dfcd2994ee128a3467dfe68205154cd/runtime/lua/vim/lsp.lua#L1361 +- key = "gopls/client:neovim" +- case "coc.nvim": +- // https://github.com/neoclide/coc.nvim/blob/3dc6153a85ed0f185abec1deb972a66af3fbbfb4/src/language-client/client.ts#L994 +- key = "gopls/client:coc.nvim" +- case "Sublime Text LSP": +- // https://github.com/sublimelsp/LSP/blob/e608f878e7e9dd34aabe4ff0462540fadcd88fcc/plugin/core/sessions.py#L493 +- key = "gopls/client:sublimetext" +- default: +- // Accumulate at least a local counter for an unknown +- // client name, but also fall through to count it as +- // ":other" for collection. +- if clientName != "" { +- counter.New(fmt.Sprintf("gopls/client-other:%s", clientName)).Inc() - } - } +- counter.Inc(key) -} -diff -urN a/gopls/internal/lsp/source/gc_annotations.go b/gopls/internal/lsp/source/gc_annotations.go ---- a/gopls/internal/lsp/source/gc_annotations.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/gc_annotations.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,221 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/server/highlight.go b/gopls/internal/server/highlight.go +--- a/gopls/internal/server/highlight.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/highlight.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,51 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package source +-package server - -import ( -- "bytes" - "context" -- "encoding/json" -- "fmt" -- "os" -- "path/filepath" -- "strings" -- -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/gocommand" --) -- --type Annotation string -- --const ( -- // Nil controls nil checks. -- Nil Annotation = "nil" -- -- // Escape controls diagnostics about escape choices. -- Escape Annotation = "escape" - -- // Inline controls diagnostics about inlining choices. -- Inline Annotation = "inline" -- -- // Bounds controls bounds checking diagnostics. -- Bounds Annotation = "bounds" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/template" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/tag" -) - --func GCOptimizationDetails(ctx context.Context, snapshot Snapshot, m *Metadata) (map[span.URI][]*Diagnostic, error) { -- if len(m.CompiledGoFiles) == 0 { -- return nil, nil -- } -- pkgDir := filepath.Dir(m.CompiledGoFiles[0].Filename()) -- outDir := filepath.Join(os.TempDir(), fmt.Sprintf("gopls-%d.details", os.Getpid())) +-func (s *server) DocumentHighlight(ctx context.Context, params *protocol.DocumentHighlightParams) ([]protocol.DocumentHighlight, error) { +- ctx, done := event.Start(ctx, "lsp.Server.documentHighlight", tag.URI.Of(params.TextDocument.URI)) +- defer done() - -- if err := os.MkdirAll(outDir, 0700); err != nil { -- return nil, err -- } -- tmpFile, err := os.CreateTemp(os.TempDir(), "gopls-x") +- fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI) - if err != nil { - return nil, err - } -- defer os.Remove(tmpFile.Name()) +- defer release() - -- outDirURI := span.URIFromPath(outDir) -- // GC details doesn't handle Windows URIs in the form of "file:///C:/...", -- // so rewrite them to "file://C:/...". See golang/go#41614. -- if !strings.HasPrefix(outDir, "/") { -- outDirURI = span.URI(strings.Replace(string(outDirURI), "file:///", "file://", 1)) -- } -- inv := &gocommand.Invocation{ -- Verb: "build", -- Args: []string{ -- fmt.Sprintf("-gcflags=-json=0,%s", outDirURI), -- fmt.Sprintf("-o=%s", tmpFile.Name()), -- ".", -- }, -- WorkingDir: pkgDir, -- } -- _, err = snapshot.RunGoCommandDirect(ctx, Normal, inv) -- if err != nil { -- return nil, err -- } -- files, err := findJSONFiles(outDir) -- if err != nil { -- return nil, err -- } -- reports := make(map[span.URI][]*Diagnostic) -- opts := snapshot.Options() -- var parseError error -- for _, fn := range files { -- uri, diagnostics, err := parseDetailsFile(fn, opts) +- switch snapshot.FileKind(fh) { +- case file.Tmpl: +- return template.Highlight(ctx, snapshot, fh, params.Position) +- case file.Go: +- rngs, err := golang.Highlight(ctx, snapshot, fh, params.Position) - if err != nil { -- // expect errors for all the files, save 1 -- parseError = err -- } -- fh := snapshot.FindFile(uri) -- if fh == nil { -- continue -- } -- if pkgDir != filepath.Dir(fh.URI().Filename()) { -- // https://github.com/golang/go/issues/42198 -- // sometimes the detail diagnostics generated for files -- // outside the package can never be taken back. -- continue -- } -- reports[fh.URI()] = diagnostics -- } -- return reports, parseError --} -- --func parseDetailsFile(filename string, options *Options) (span.URI, []*Diagnostic, error) { -- buf, err := os.ReadFile(filename) -- if err != nil { -- return "", nil, err -- } -- var ( -- uri span.URI -- i int -- diagnostics []*Diagnostic -- ) -- type metadata struct { -- File string `json:"file,omitempty"` -- } -- for dec := json.NewDecoder(bytes.NewReader(buf)); dec.More(); { -- // The first element always contains metadata. -- if i == 0 { -- i++ -- m := new(metadata) -- if err := dec.Decode(m); err != nil { -- return "", nil, err -- } -- if !strings.HasSuffix(m.File, ".go") { -- continue // -- } -- uri = span.URIFromPath(m.File) -- continue -- } -- d := new(protocol.Diagnostic) -- if err := dec.Decode(d); err != nil { -- return "", nil, err -- } -- d.Tags = []protocol.DiagnosticTag{} // must be an actual slice -- msg := d.Code.(string) -- if msg != "" { -- msg = fmt.Sprintf("%s(%s)", msg, d.Message) -- } -- if !showDiagnostic(msg, d.Source, options) { -- continue -- } -- var related []protocol.DiagnosticRelatedInformation -- for _, ri := range d.RelatedInformation { -- // TODO(rfindley): The compiler uses LSP-like JSON to encode gc details, -- // however the positions it uses are 1-based UTF-8: -- // https://github.com/golang/go/blob/master/src/cmd/compile/internal/logopt/log_opts.go -- // -- // Here, we adjust for 0-based positions, but do not translate UTF-8 to UTF-16. -- related = append(related, protocol.DiagnosticRelatedInformation{ -- Location: protocol.Location{ -- URI: ri.Location.URI, -- Range: zeroIndexedRange(ri.Location.Range), -- }, -- Message: ri.Message, -- }) -- } -- diagnostic := &Diagnostic{ -- URI: uri, -- Range: zeroIndexedRange(d.Range), -- Message: msg, -- Severity: d.Severity, -- Source: OptimizationDetailsError, // d.Source is always "go compiler" as of 1.16, use our own -- Tags: d.Tags, -- Related: related, +- event.Error(ctx, "no highlight", err) - } -- diagnostics = append(diagnostics, diagnostic) -- i++ -- } -- return uri, diagnostics, nil --} -- --// showDiagnostic reports whether a given diagnostic should be shown to the end --// user, given the current options. --func showDiagnostic(msg, source string, o *Options) bool { -- if source != "go compiler" { -- return false -- } -- if o.Annotations == nil { -- return true -- } -- switch { -- case strings.HasPrefix(msg, "canInline") || -- strings.HasPrefix(msg, "cannotInline") || -- strings.HasPrefix(msg, "inlineCall"): -- return o.Annotations[Inline] -- case strings.HasPrefix(msg, "escape") || msg == "leak": -- return o.Annotations[Escape] -- case strings.HasPrefix(msg, "nilcheck"): -- return o.Annotations[Nil] -- case strings.HasPrefix(msg, "isInBounds") || -- strings.HasPrefix(msg, "isSliceInBounds"): -- return o.Annotations[Bounds] -- } -- return false --} -- --// The range produced by the compiler is 1-indexed, so subtract range by 1. --func zeroIndexedRange(rng protocol.Range) protocol.Range { -- return protocol.Range{ -- Start: protocol.Position{ -- Line: rng.Start.Line - 1, -- Character: rng.Start.Character - 1, -- }, -- End: protocol.Position{ -- Line: rng.End.Line - 1, -- Character: rng.End.Character - 1, -- }, +- return toProtocolHighlight(rngs), nil - } +- return nil, nil // empty result -} - --func findJSONFiles(dir string) ([]string, error) { -- ans := []string{} -- f := func(path string, fi os.FileInfo, _ error) error { -- if fi.IsDir() { -- return nil -- } -- if strings.HasSuffix(path, ".json") { -- ans = append(ans, path) -- } -- return nil +-func toProtocolHighlight(rngs []protocol.Range) []protocol.DocumentHighlight { +- result := make([]protocol.DocumentHighlight, 0, len(rngs)) +- kind := protocol.Text +- for _, rng := range rngs { +- result = append(result, protocol.DocumentHighlight{ +- Kind: kind, +- Range: rng, +- }) - } -- err := filepath.Walk(dir, f) -- return ans, err +- return result -} -diff -urN a/gopls/internal/lsp/source/highlight.go b/gopls/internal/lsp/source/highlight.go ---- a/gopls/internal/lsp/source/highlight.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/highlight.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,480 +0,0 @@ +diff -urN a/gopls/internal/server/hover.go b/gopls/internal/server/hover.go +--- a/gopls/internal/server/hover.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/hover.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,47 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package source +-package server - -import ( - "context" -- "fmt" -- "go/ast" -- "go/token" -- "go/types" - -- "golang.org/x/tools/go/ast/astutil" -- "golang.org/x/tools/gopls/internal/lsp/protocol" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/mod" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/telemetry" +- "golang.org/x/tools/gopls/internal/template" +- "golang.org/x/tools/gopls/internal/work" - "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/tag" -) - --func Highlight(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) ([]protocol.Range, error) { -- ctx, done := event.Start(ctx, "source.Highlight") -- defer done() +-func (s *server) Hover(ctx context.Context, params *protocol.HoverParams) (_ *protocol.Hover, rerr error) { +- recordLatency := telemetry.StartLatencyTimer("hover") +- defer func() { +- recordLatency(ctx, rerr) +- }() - -- // We always want fully parsed files for highlight, regardless -- // of whether the file belongs to a workspace package. -- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) -- if err != nil { -- return nil, fmt.Errorf("getting package for Highlight: %w", err) -- } +- ctx, done := event.Start(ctx, "lsp.Server.hover", tag.URI.Of(params.TextDocument.URI)) +- defer done() - -- pos, err := pgf.PositionPos(position) -- if err != nil { -- return nil, err -- } -- path, _ := astutil.PathEnclosingInterval(pgf.File, pos, pos) -- if len(path) == 0 { -- return nil, fmt.Errorf("no enclosing position found for %v:%v", position.Line, position.Character) -- } -- // If start == end for astutil.PathEnclosingInterval, the 1-char interval -- // following start is used instead. As a result, we might not get an exact -- // match so we should check the 1-char interval to the left of the passed -- // in position to see if that is an exact match. -- if _, ok := path[0].(*ast.Ident); !ok { -- if p, _ := astutil.PathEnclosingInterval(pgf.File, pos-1, pos-1); p != nil { -- switch p[0].(type) { -- case *ast.Ident, *ast.SelectorExpr: -- path = p // use preceding ident/selector -- } -- } -- } -- result, err := highlightPath(path, pgf.File, pkg.GetTypesInfo()) +- fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI) - if err != nil { - return nil, err - } -- var ranges []protocol.Range -- for rng := range result { -- rng, err := pgf.PosRange(rng.start, rng.end) -- if err != nil { -- return nil, err -- } -- ranges = append(ranges, rng) +- defer release() +- +- switch snapshot.FileKind(fh) { +- case file.Mod: +- return mod.Hover(ctx, snapshot, fh, params.Position) +- case file.Go: +- return golang.Hover(ctx, snapshot, fh, params.Position) +- case file.Tmpl: +- return template.Hover(ctx, snapshot, fh, params.Position) +- case file.Work: +- return work.Hover(ctx, snapshot, fh, params.Position) - } -- return ranges, nil +- return nil, nil // empty result -} +diff -urN a/gopls/internal/server/implementation.go b/gopls/internal/server/implementation.go +--- a/gopls/internal/server/implementation.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/implementation.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,36 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func highlightPath(path []ast.Node, file *ast.File, info *types.Info) (map[posRange]struct{}, error) { -- result := make(map[posRange]struct{}) -- switch node := path[0].(type) { -- case *ast.BasicLit: -- // Import path string literal? -- if len(path) > 1 { -- if imp, ok := path[1].(*ast.ImportSpec); ok { -- highlight := func(n ast.Node) { -- result[posRange{start: n.Pos(), end: n.End()}] = struct{}{} -- } +-package server - -- // Highlight the import itself... -- highlight(imp) +-import ( +- "context" - -- // ...and all references to it in the file. -- if pkgname, ok := ImportedPkgName(info, imp); ok { -- ast.Inspect(file, func(n ast.Node) bool { -- if id, ok := n.(*ast.Ident); ok && -- info.Uses[id] == pkgname { -- highlight(id) -- } -- return true -- }) -- } -- return result, nil -- } -- } -- highlightFuncControlFlow(path, result) -- case *ast.ReturnStmt, *ast.FuncDecl, *ast.FuncType: -- highlightFuncControlFlow(path, result) -- case *ast.Ident: -- // Check if ident is inside return or func decl. -- highlightFuncControlFlow(path, result) -- highlightIdentifier(node, file, info, result) -- case *ast.ForStmt, *ast.RangeStmt: -- highlightLoopControlFlow(path, info, result) -- case *ast.SwitchStmt: -- highlightSwitchFlow(path, info, result) -- case *ast.BranchStmt: -- // BREAK can exit a loop, switch or select, while CONTINUE exit a loop so -- // these need to be handled separately. They can also be embedded in any -- // other loop/switch/select if they have a label. TODO: add support for -- // GOTO and FALLTHROUGH as well. -- switch node.Tok { -- case token.BREAK: -- if node.Label != nil { -- highlightLabeledFlow(path, info, node, result) -- } else { -- highlightUnlabeledBreakFlow(path, info, result) -- } -- case token.CONTINUE: -- if node.Label != nil { -- highlightLabeledFlow(path, info, node, result) -- } else { -- highlightLoopControlFlow(path, info, result) -- } -- } -- default: -- // If the cursor is in an unidentified area, return empty results. -- return nil, nil +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/telemetry" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/tag" +-) +- +-func (s *server) Implementation(ctx context.Context, params *protocol.ImplementationParams) (_ []protocol.Location, rerr error) { +- recordLatency := telemetry.StartLatencyTimer("implementation") +- defer func() { +- recordLatency(ctx, rerr) +- }() +- +- ctx, done := event.Start(ctx, "lsp.Server.implementation", tag.URI.Of(params.TextDocument.URI)) +- defer done() +- +- fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI) +- if err != nil { +- return nil, err - } -- return result, nil +- defer release() +- if snapshot.FileKind(fh) != file.Go { +- return nil, nil // empty result +- } +- return golang.Implementation(ctx, snapshot, fh, params.Position) -} +diff -urN a/gopls/internal/server/inlay_hint.go b/gopls/internal/server/inlay_hint.go +--- a/gopls/internal/server/inlay_hint.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/inlay_hint.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,35 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --type posRange struct { -- start, end token.Pos --} +-package server - --func highlightFuncControlFlow(path []ast.Node, result map[posRange]struct{}) { -- var enclosingFunc ast.Node -- var returnStmt *ast.ReturnStmt -- var resultsList *ast.FieldList -- inReturnList := false +-import ( +- "context" - --Outer: -- // Reverse walk the path till we get to the func block. -- for i, n := range path { -- switch node := n.(type) { -- case *ast.KeyValueExpr: -- // If cursor is in a key: value expr, we don't want control flow highlighting -- return -- case *ast.CallExpr: -- // If cursor is an arg in a callExpr, we don't want control flow highlighting. -- if i > 0 { -- for _, arg := range node.Args { -- if arg == path[i-1] { -- return -- } -- } -- } -- case *ast.Field: -- inReturnList = true -- case *ast.FuncLit: -- enclosingFunc = n -- resultsList = node.Type.Results -- break Outer -- case *ast.FuncDecl: -- enclosingFunc = n -- resultsList = node.Type.Results -- break Outer -- case *ast.ReturnStmt: -- returnStmt = node -- // If the cursor is not directly in a *ast.ReturnStmt, then -- // we need to know if it is within one of the values that is being returned. -- inReturnList = inReturnList || path[0] != returnStmt -- } -- } -- // Cursor is not in a function. -- if enclosingFunc == nil { -- return +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/mod" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/tag" +-) +- +-func (s *server) InlayHint(ctx context.Context, params *protocol.InlayHintParams) ([]protocol.InlayHint, error) { +- ctx, done := event.Start(ctx, "lsp.Server.inlayHint", tag.URI.Of(params.TextDocument.URI)) +- defer done() +- +- fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI) +- if err != nil { +- return nil, err - } -- // If the cursor is on a "return" or "func" keyword, we should highlight all of the exit -- // points of the function, including the "return" and "func" keywords. -- highlightAllReturnsAndFunc := path[0] == returnStmt || path[0] == enclosingFunc -- switch path[0].(type) { -- case *ast.Ident, *ast.BasicLit: -- // Cursor is in an identifier and not in a return statement or in the results list. -- if returnStmt == nil && !inReturnList { -- return -- } -- case *ast.FuncType: -- highlightAllReturnsAndFunc = true +- defer release() +- +- switch snapshot.FileKind(fh) { +- case file.Mod: +- return mod.InlayHint(ctx, snapshot, fh, params.Range) +- case file.Go: +- return golang.InlayHint(ctx, snapshot, fh, params.Range) - } -- // The user's cursor may be within the return statement of a function, -- // or within the result section of a function's signature. -- // index := -1 -- var nodes []ast.Node -- if returnStmt != nil { -- for _, n := range returnStmt.Results { -- nodes = append(nodes, n) -- } -- } else if resultsList != nil { -- for _, n := range resultsList.List { -- nodes = append(nodes, n) -- } +- return nil, nil // empty result +-} +diff -urN a/gopls/internal/server/link.go b/gopls/internal/server/link.go +--- a/gopls/internal/server/link.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/link.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,285 +0,0 @@ +-// Copyright 2018 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package server +- +-import ( +- "bytes" +- "context" +- "fmt" +- "go/ast" +- "go/token" +- "net/url" +- "regexp" +- "strings" +- "sync" +- +- "golang.org/x/mod/modfile" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/tag" +-) +- +-func (s *server) DocumentLink(ctx context.Context, params *protocol.DocumentLinkParams) (links []protocol.DocumentLink, err error) { +- ctx, done := event.Start(ctx, "lsp.Server.documentLink") +- defer done() +- +- fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI) +- if err != nil { +- return nil, err - } -- _, index := nodeAtPos(nodes, path[0].Pos()) +- defer release() - -- // Highlight the correct argument in the function declaration return types. -- if resultsList != nil && -1 < index && index < len(resultsList.List) { -- rng := posRange{ -- start: resultsList.List[index].Pos(), -- end: resultsList.List[index].End(), -- } -- result[rng] = struct{}{} +- switch snapshot.FileKind(fh) { +- case file.Mod: +- links, err = modLinks(ctx, snapshot, fh) +- case file.Go: +- links, err = goLinks(ctx, snapshot, fh) - } -- // Add the "func" part of the func declaration. -- if highlightAllReturnsAndFunc { -- r := posRange{ -- start: enclosingFunc.Pos(), -- end: enclosingFunc.Pos() + token.Pos(len("func")), -- } -- result[r] = struct{}{} +- // Don't return errors for document links. +- if err != nil { +- event.Error(ctx, "failed to compute document links", err, tag.URI.Of(fh.URI())) +- return nil, nil // empty result - } -- ast.Inspect(enclosingFunc, func(n ast.Node) bool { -- // Don't traverse any other functions. -- switch n.(type) { -- case *ast.FuncDecl, *ast.FuncLit: -- return enclosingFunc == n -- } -- ret, ok := n.(*ast.ReturnStmt) -- if !ok { -- return true -- } -- var toAdd ast.Node -- // Add the entire return statement, applies when highlight the word "return" or "func". -- if highlightAllReturnsAndFunc { -- toAdd = n +- return links, nil // may be empty (for other file types) +-} +- +-func modLinks(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.DocumentLink, error) { +- pm, err := snapshot.ParseMod(ctx, fh) +- if err != nil { +- return nil, err +- } +- +- var links []protocol.DocumentLink +- for _, req := range pm.File.Require { +- if req.Syntax == nil { +- continue - } -- // Add the relevant field within the entire return statement. -- if -1 < index && index < len(ret.Results) { -- toAdd = ret.Results[index] +- // See golang/go#36998: don't link to modules matching GOPRIVATE. +- if snapshot.IsGoPrivatePath(req.Mod.Path) { +- continue - } -- if toAdd != nil { -- result[posRange{start: toAdd.Pos(), end: toAdd.End()}] = struct{}{} +- dep := []byte(req.Mod.Path) +- start, end := req.Syntax.Start.Byte, req.Syntax.End.Byte +- i := bytes.Index(pm.Mapper.Content[start:end], dep) +- if i == -1 { +- continue - } -- return false -- }) --} -- --// highlightUnlabeledBreakFlow highlights the innermost enclosing for/range/switch or swlect --func highlightUnlabeledBreakFlow(path []ast.Node, info *types.Info, result map[posRange]struct{}) { -- // Reverse walk the path until we find closest loop, select, or switch. -- for _, n := range path { -- switch n.(type) { -- case *ast.ForStmt, *ast.RangeStmt: -- highlightLoopControlFlow(path, info, result) -- return // only highlight the innermost statement -- case *ast.SwitchStmt: -- highlightSwitchFlow(path, info, result) -- return -- case *ast.SelectStmt: -- // TODO: add highlight when breaking a select. -- return +- // Shift the start position to the location of the +- // dependency within the require statement. +- target := cache.BuildLink(snapshot.Options().LinkTarget, "mod/"+req.Mod.String(), "") +- l, err := toProtocolLink(pm.Mapper, target, start+i, start+i+len(dep)) +- if err != nil { +- return nil, err - } +- links = append(links, l) - } --} -- --// highlightLabeledFlow highlights the enclosing labeled for, range, --// or switch statement denoted by a labeled break or continue stmt. --func highlightLabeledFlow(path []ast.Node, info *types.Info, stmt *ast.BranchStmt, result map[posRange]struct{}) { -- use := info.Uses[stmt.Label] -- if use == nil { -- return +- // TODO(ridersofrohan): handle links for replace and exclude directives. +- if syntax := pm.File.Syntax; syntax == nil { +- return links, nil - } -- for _, n := range path { -- if label, ok := n.(*ast.LabeledStmt); ok && info.Defs[label.Label] == use { -- switch label.Stmt.(type) { -- case *ast.ForStmt, *ast.RangeStmt: -- highlightLoopControlFlow([]ast.Node{label.Stmt, label}, info, result) -- case *ast.SwitchStmt: -- highlightSwitchFlow([]ast.Node{label.Stmt, label}, info, result) +- +- // Get all the links that are contained in the comments of the file. +- urlRegexp := snapshot.Options().URLRegexp +- for _, expr := range pm.File.Syntax.Stmt { +- comments := expr.Comment() +- if comments == nil { +- continue +- } +- for _, section := range [][]modfile.Comment{comments.Before, comments.Suffix, comments.After} { +- for _, comment := range section { +- l, err := findLinksInString(urlRegexp, comment.Token, comment.Start.Byte, pm.Mapper) +- if err != nil { +- return nil, err +- } +- links = append(links, l...) - } -- return - } - } +- return links, nil -} - --func labelFor(path []ast.Node) *ast.Ident { -- if len(path) > 1 { -- if n, ok := path[1].(*ast.LabeledStmt); ok { -- return n.Label -- } +-// goLinks returns the set of hyperlink annotations for the specified Go file. +-func goLinks(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.DocumentLink, error) { +- +- pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) +- if err != nil { +- return nil, err - } -- return nil --} - --func highlightLoopControlFlow(path []ast.Node, info *types.Info, result map[posRange]struct{}) { -- var loop ast.Node -- var loopLabel *ast.Ident -- stmtLabel := labelFor(path) --Outer: -- // Reverse walk the path till we get to the for loop. -- for i := range path { -- switch n := path[i].(type) { -- case *ast.ForStmt, *ast.RangeStmt: -- loopLabel = labelFor(path[i:]) +- var links []protocol.DocumentLink - -- if stmtLabel == nil || loopLabel == stmtLabel { -- loop = n -- break Outer +- // Create links for import specs. +- if snapshot.Options().ImportShortcut.ShowLinks() { +- +- // If links are to pkg.go.dev, append module version suffixes. +- // This requires the import map from the package metadata. Ignore errors. +- var depsByImpPath map[golang.ImportPath]golang.PackageID +- if strings.ToLower(snapshot.Options().LinkTarget) == "pkg.go.dev" { +- if meta, err := golang.NarrowestMetadataForFile(ctx, snapshot, fh.URI()); err == nil { +- depsByImpPath = meta.DepsByImpPath - } - } -- } -- if loop == nil { -- return -- } - -- // Add the for statement. -- rng := posRange{ -- start: loop.Pos(), -- end: loop.Pos() + token.Pos(len("for")), -- } -- result[rng] = struct{}{} +- for _, imp := range pgf.File.Imports { +- importPath := metadata.UnquoteImportPath(imp) +- if importPath == "" { +- continue // bad import +- } +- // See golang/go#36998: don't link to modules matching GOPRIVATE. +- if snapshot.IsGoPrivatePath(string(importPath)) { +- continue +- } - -- // Traverse AST to find branch statements within the same for-loop. -- ast.Inspect(loop, func(n ast.Node) bool { -- switch n.(type) { -- case *ast.ForStmt, *ast.RangeStmt: -- return loop == n -- case *ast.SwitchStmt, *ast.SelectStmt: -- return false -- } -- b, ok := n.(*ast.BranchStmt) -- if !ok { -- return true -- } -- if b.Label == nil || info.Uses[b.Label] == info.Defs[loopLabel] { -- result[posRange{start: b.Pos(), end: b.End()}] = struct{}{} -- } -- return true -- }) +- urlPath := string(importPath) - -- // Find continue statements in the same loop or switches/selects. -- ast.Inspect(loop, func(n ast.Node) bool { -- switch n.(type) { -- case *ast.ForStmt, *ast.RangeStmt: -- return loop == n -- } +- // For pkg.go.dev, append module version suffix to package import path. +- if mp := snapshot.Metadata(depsByImpPath[importPath]); mp != nil && mp.Module != nil && mp.Module.Path != "" && mp.Module.Version != "" { +- urlPath = strings.Replace(urlPath, mp.Module.Path, mp.Module.Path+"@"+mp.Module.Version, 1) +- } - -- if n, ok := n.(*ast.BranchStmt); ok && n.Tok == token.CONTINUE { -- result[posRange{start: n.Pos(), end: n.End()}] = struct{}{} +- start, end, err := safetoken.Offsets(pgf.Tok, imp.Path.Pos(), imp.Path.End()) +- if err != nil { +- return nil, err +- } +- targetURL := cache.BuildLink(snapshot.Options().LinkTarget, urlPath, "") +- // Account for the quotation marks in the positions. +- l, err := toProtocolLink(pgf.Mapper, targetURL, start+len(`"`), end-len(`"`)) +- if err != nil { +- return nil, err +- } +- links = append(links, l) - } -- return true -- }) -- -- // We don't need to check other for loops if we aren't looking for labeled statements. -- if loopLabel == nil { -- return - } - -- // Find labeled branch statements in any loop. -- ast.Inspect(loop, func(n ast.Node) bool { -- b, ok := n.(*ast.BranchStmt) -- if !ok { -- return true -- } -- // statement with labels that matches the loop -- if b.Label != nil && info.Uses[b.Label] == info.Defs[loopLabel] { -- result[posRange{start: b.Pos(), end: b.End()}] = struct{}{} +- urlRegexp := snapshot.Options().URLRegexp +- +- // Gather links found in string literals. +- var str []*ast.BasicLit +- ast.Inspect(pgf.File, func(node ast.Node) bool { +- switch n := node.(type) { +- case *ast.ImportSpec: +- return false // don't process import strings again +- case *ast.BasicLit: +- if n.Kind == token.STRING { +- str = append(str, n) +- } - } - return true - }) --} +- for _, s := range str { +- strOffset, err := safetoken.Offset(pgf.Tok, s.Pos()) +- if err != nil { +- return nil, err +- } +- l, err := findLinksInString(urlRegexp, s.Value, strOffset, pgf.Mapper) +- if err != nil { +- return nil, err +- } +- links = append(links, l...) +- } - --func highlightSwitchFlow(path []ast.Node, info *types.Info, result map[posRange]struct{}) { -- var switchNode ast.Node -- var switchNodeLabel *ast.Ident -- stmtLabel := labelFor(path) --Outer: -- // Reverse walk the path till we get to the switch statement. -- for i := range path { -- switch n := path[i].(type) { -- case *ast.SwitchStmt: -- switchNodeLabel = labelFor(path[i:]) -- if stmtLabel == nil || switchNodeLabel == stmtLabel { -- switchNode = n -- break Outer +- // Gather links found in comments. +- for _, commentGroup := range pgf.File.Comments { +- for _, comment := range commentGroup.List { +- commentOffset, err := safetoken.Offset(pgf.Tok, comment.Pos()) +- if err != nil { +- return nil, err +- } +- l, err := findLinksInString(urlRegexp, comment.Text, commentOffset, pgf.Mapper) +- if err != nil { +- return nil, err - } +- links = append(links, l...) - } - } -- // Cursor is not in a switch statement -- if switchNode == nil { -- return -- } - -- // Add the switch statement. -- rng := posRange{ -- start: switchNode.Pos(), -- end: switchNode.Pos() + token.Pos(len("switch")), -- } -- result[rng] = struct{}{} +- return links, nil +-} - -- // Traverse AST to find break statements within the same switch. -- ast.Inspect(switchNode, func(n ast.Node) bool { -- switch n.(type) { -- case *ast.SwitchStmt: -- return switchNode == n -- case *ast.ForStmt, *ast.RangeStmt, *ast.SelectStmt: -- return false -- } +-// acceptedSchemes controls the schemes that URLs must have to be shown to the +-// user. Other schemes can't be opened by LSP clients, so linkifying them is +-// distracting. See golang/go#43990. +-var acceptedSchemes = map[string]bool{ +- "http": true, +- "https": true, +-} - -- b, ok := n.(*ast.BranchStmt) -- if !ok || b.Tok != token.BREAK { -- return true +-// urlRegexp is the user-supplied regular expression to match URL. +-// srcOffset is the start offset of 'src' within m's file. +-func findLinksInString(urlRegexp *regexp.Regexp, src string, srcOffset int, m *protocol.Mapper) ([]protocol.DocumentLink, error) { +- var links []protocol.DocumentLink +- for _, index := range urlRegexp.FindAllIndex([]byte(src), -1) { +- start, end := index[0], index[1] +- link := src[start:end] +- linkURL, err := url.Parse(link) +- // Fallback: Linkify IP addresses as suggested in golang/go#18824. +- if err != nil { +- linkURL, err = url.Parse("//" + link) +- // Not all potential links will be valid, so don't return this error. +- if err != nil { +- continue +- } - } -- -- if b.Label == nil || info.Uses[b.Label] == info.Defs[switchNodeLabel] { -- result[posRange{start: b.Pos(), end: b.End()}] = struct{}{} +- // If the URL has no scheme, use https. +- if linkURL.Scheme == "" { +- linkURL.Scheme = "https" +- } +- if !acceptedSchemes[linkURL.Scheme] { +- continue - } -- return true -- }) - -- // We don't need to check other switches if we aren't looking for labeled statements. -- if switchNodeLabel == nil { -- return +- l, err := toProtocolLink(m, linkURL.String(), srcOffset+start, srcOffset+end) +- if err != nil { +- return nil, err +- } +- links = append(links, l) - } -- -- // Find labeled break statements in any switch -- ast.Inspect(switchNode, func(n ast.Node) bool { -- b, ok := n.(*ast.BranchStmt) -- if !ok || b.Tok != token.BREAK { -- return true +- // Handle golang/go#1234-style links. +- r := getIssueRegexp() +- for _, index := range r.FindAllIndex([]byte(src), -1) { +- start, end := index[0], index[1] +- matches := r.FindStringSubmatch(src) +- if len(matches) < 4 { +- continue - } -- -- if b.Label != nil && info.Uses[b.Label] == info.Defs[switchNodeLabel] { -- result[posRange{start: b.Pos(), end: b.End()}] = struct{}{} +- org, repo, number := matches[1], matches[2], matches[3] +- targetURL := fmt.Sprintf("https://github.com/%s/%s/issues/%s", org, repo, number) +- l, err := toProtocolLink(m, targetURL, srcOffset+start, srcOffset+end) +- if err != nil { +- return nil, err - } -- -- return true -- }) --} -- --func highlightIdentifier(id *ast.Ident, file *ast.File, info *types.Info, result map[posRange]struct{}) { -- highlight := func(n ast.Node) { -- result[posRange{start: n.Pos(), end: n.End()}] = struct{}{} +- links = append(links, l) - } +- return links, nil +-} - -- // obj may be nil if the Ident is undefined. -- // In this case, the behavior expected by tests is -- // to match other undefined Idents of the same name. -- obj := info.ObjectOf(id) -- -- ast.Inspect(file, func(n ast.Node) bool { -- switch n := n.(type) { -- case *ast.Ident: -- if n.Name == id.Name && info.ObjectOf(n) == obj { -- highlight(n) -- } -- -- case *ast.ImportSpec: -- pkgname, ok := ImportedPkgName(info, n) -- if ok && pkgname == obj { -- if n.Name != nil { -- highlight(n.Name) -- } else { -- highlight(n) -- } -- } -- } -- return true +-func getIssueRegexp() *regexp.Regexp { +- once.Do(func() { +- issueRegexp = regexp.MustCompile(`(\w+)/([\w-]+)#([0-9]+)`) - }) +- return issueRegexp -} - --// ImportedPkgName returns the PkgName object declared by an ImportSpec. --// TODO(adonovan): make this a method of types.Info. --func ImportedPkgName(info *types.Info, imp *ast.ImportSpec) (*types.PkgName, bool) { -- var obj types.Object -- if imp.Name != nil { -- obj = info.Defs[imp.Name] -- } else { -- obj = info.Implicits[imp] +-var ( +- once sync.Once +- issueRegexp *regexp.Regexp +-) +- +-func toProtocolLink(m *protocol.Mapper, targetURL string, start, end int) (protocol.DocumentLink, error) { +- rng, err := m.OffsetRange(start, end) +- if err != nil { +- return protocol.DocumentLink{}, err - } -- pkgname, ok := obj.(*types.PkgName) -- return pkgname, ok +- return protocol.DocumentLink{ +- Range: rng, +- Target: &targetURL, +- }, nil -} -diff -urN a/gopls/internal/lsp/source/hover.go b/gopls/internal/lsp/source/hover.go ---- a/gopls/internal/lsp/source/hover.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/hover.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,1069 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/server/prompt.go b/gopls/internal/server/prompt.go +--- a/gopls/internal/server/prompt.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/prompt.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,315 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package source -- --import ( -- "context" -- "encoding/json" -- "fmt" -- "go/ast" -- "go/constant" -- "go/doc" -- "go/format" -- "go/token" -- "go/types" -- "io/fs" -- "path/filepath" -- "strconv" -- "strings" -- "time" -- "unicode/utf8" -- -- "golang.org/x/text/unicode/runenames" -- "golang.org/x/tools/go/ast/astutil" -- "golang.org/x/tools/go/types/typeutil" -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/tokeninternal" -- "golang.org/x/tools/internal/typeparams" --) -- --// HoverJSON contains information used by hover. It is also the JSON returned --// for the "structured" hover format --type HoverJSON struct { -- // Synopsis is a single sentence synopsis of the symbol's documentation. -- Synopsis string `json:"synopsis"` -- -- // FullDocumentation is the symbol's full documentation. -- FullDocumentation string `json:"fullDocumentation"` -- -- // Signature is the symbol's signature. -- Signature string `json:"signature"` -- -- // SingleLine is a single line describing the symbol. -- // This is recommended only for use in clients that show a single line for hover. -- SingleLine string `json:"singleLine"` +-package server - -- // SymbolName is the human-readable name to use for the symbol in links. -- SymbolName string `json:"symbolName"` +-import ( +- "context" +- "fmt" +- "os" +- "path/filepath" +- "time" - -- // LinkPath is the pkg.go.dev link for the given symbol. -- // For example, the "go/ast" part of "pkg.go.dev/go/ast#Node". -- LinkPath string `json:"linkPath"` +- "golang.org/x/telemetry" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/event" +-) - -- // LinkAnchor is the pkg.go.dev link anchor for the given symbol. -- // For example, the "Node" part of "pkg.go.dev/go/ast#Node". -- LinkAnchor string `json:"linkAnchor"` --} +-// promptTimeout is the amount of time we wait for an ongoing prompt before +-// prompting again. This gives the user time to reply. However, at some point +-// we must assume that the client is not displaying the prompt, the user is +-// ignoring it, or the prompt has been disrupted in some way (e.g. by a gopls +-// crash). +-const promptTimeout = 24 * time.Hour - --// Hover implements the "textDocument/hover" RPC for Go files. --func Hover(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (*protocol.Hover, error) { -- ctx, done := event.Start(ctx, "source.Hover") -- defer done() +-// The following constants are used for testing telemetry integration. +-const ( +- TelemetryPromptWorkTitle = "Checking telemetry prompt" // progress notification title, for awaiting in tests +- GoplsConfigDirEnvvar = "GOPLS_CONFIG_DIR" // overridden for testing +- FakeTelemetryModefileEnvvar = "GOPLS_FAKE_TELEMETRY_MODEFILE" // overridden for testing +- TelemetryYes = "Yes, I'd like to help." +- TelemetryNo = "No, thanks." +-) - -- rng, h, err := hover(ctx, snapshot, fh, position) -- if err != nil { -- return nil, err -- } -- if h == nil { -- return nil, nil -- } -- hover, err := formatHover(h, snapshot.Options()) -- if err != nil { -- return nil, err +-// getenv returns the effective environment variable value for the provided +-// key, looking up the key in the session environment before falling back on +-// the process environment. +-func (s *server) getenv(key string) string { +- if v, ok := s.Options().Env[key]; ok { +- return v - } -- return &protocol.Hover{ -- Contents: protocol.MarkupContent{ -- Kind: snapshot.Options().PreferredContentFormat, -- Value: hover, -- }, -- Range: rng, -- }, nil +- return os.Getenv(key) -} - --// hover computes hover information at the given position. If we do not support --// hovering at the position, it returns _, nil, nil: an error is only returned --// if the position is valid but we fail to compute hover information. --func hover(ctx context.Context, snapshot Snapshot, fh FileHandle, pp protocol.Position) (protocol.Range, *HoverJSON, error) { -- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) -- if err != nil { -- return protocol.Range{}, nil, err +-// configDir returns the root of the gopls configuration dir. By default this +-// is os.UserConfigDir/gopls, but it may be overridden for tests. +-func (s *server) configDir() (string, error) { +- if d := s.getenv(GoplsConfigDirEnvvar); d != "" { +- return d, nil - } -- pos, err := pgf.PositionPos(pp) +- userDir, err := os.UserConfigDir() - if err != nil { -- return protocol.Range{}, nil, err +- return "", err - } +- return filepath.Join(userDir, "gopls"), nil +-} - -- // Handle hovering over import paths, which do not have an associated -- // identifier. -- for _, spec := range pgf.File.Imports { -- // We are inclusive of the end point here to allow hovering when the cursor -- // is just after the import path. -- if spec.Path.Pos() <= pos && pos <= spec.Path.End() { -- return hoverImport(ctx, snapshot, pkg, pgf, spec) +-// telemetryMode returns the current effective telemetry mode. +-// By default this is x/telemetry.Mode(), but it may be overridden for tests. +-func (s *server) telemetryMode() string { +- if fake := s.getenv(FakeTelemetryModefileEnvvar); fake != "" { +- if data, err := os.ReadFile(fake); err == nil { +- return string(data) - } +- return "local" - } +- return telemetry.Mode() +-} - -- // Handle hovering over the package name, which does not have an associated -- // object. -- // As with import paths, we allow hovering just after the package name. -- if pgf.File.Name != nil && pgf.File.Name.Pos() <= pos && pos <= pgf.File.Name.Pos() { -- return hoverPackageName(pkg, pgf) +-// setTelemetryMode sets the current telemetry mode. +-// By default this calls x/telemetry.SetMode, but it may be overridden for +-// tests. +-func (s *server) setTelemetryMode(mode string) error { +- if fake := s.getenv(FakeTelemetryModefileEnvvar); fake != "" { +- return os.WriteFile(fake, []byte(mode), 0666) - } +- return telemetry.SetMode(mode) +-} - -- // Handle hovering over (non-import-path) literals. -- if path, _ := astutil.PathEnclosingInterval(pgf.File, pos, pos); len(path) > 0 { -- if lit, _ := path[0].(*ast.BasicLit); lit != nil { -- return hoverLit(pgf, lit, pos) -- } +-// maybePromptForTelemetry checks for the right conditions, and then prompts +-// the user to ask if they want to enable Go telemetry uploading. If the user +-// responds 'Yes', the telemetry mode is set to "on". +-// +-// The actual conditions for prompting are defensive, erring on the side of not +-// prompting. +-// If enabled is false, this will not prompt the user in any condition, +-// but will send work progress reports to help testing. +-func (s *server) maybePromptForTelemetry(ctx context.Context, enabled bool) { +- if s.Options().VerboseWorkDoneProgress { +- work := s.progress.Start(ctx, TelemetryPromptWorkTitle, "Checking if gopls should prompt about telemetry...", nil, nil) +- defer work.End(ctx, "Done.") - } - -- // Handle hovering over embed directive argument. -- pattern, embedRng := parseEmbedDirective(pgf.Mapper, pp) -- if pattern != "" { -- return hoverEmbed(fh, embedRng, pattern) +- if !enabled { // check this after the work progress message for testing. +- return // prompt is disabled - } - -- // Handle linkname directive by overriding what to look for. -- var linkedRange *protocol.Range // range referenced by linkname directive, or nil -- if pkgPath, name, offset := parseLinkname(pgf.Mapper, pp); pkgPath != "" && name != "" { -- // rng covering 2nd linkname argument: pkgPath.name. -- rng, err := pgf.PosRange(pgf.Tok.Pos(offset), pgf.Tok.Pos(offset+len(pkgPath)+len(".")+len(name))) -- if err != nil { -- return protocol.Range{}, nil, fmt.Errorf("range over linkname arg: %w", err) -- } -- linkedRange = &rng -- -- pkg, pgf, pos, err = findLinkname(ctx, snapshot, PackagePath(pkgPath), name) -- if err != nil { -- return protocol.Range{}, nil, fmt.Errorf("find linkname: %w", err) -- } +- if s.telemetryMode() == "on" || s.telemetryMode() == "off" { +- // Telemetry is already on or explicitly off -- nothing to ask about. +- return - } - -- // The general case: compute hover information for the object referenced by -- // the identifier at pos. -- ident, obj, selectedType := referencedObject(pkg, pgf, pos) -- if obj == nil || ident == nil { -- return protocol.Range{}, nil, nil // no object to hover +- errorf := func(format string, args ...any) { +- err := fmt.Errorf(format, args...) +- event.Error(ctx, "telemetry prompt failed", err) - } - -- // Unless otherwise specified, rng covers the ident being hovered. -- var rng protocol.Range -- if linkedRange != nil { -- rng = *linkedRange -- } else { -- rng, err = pgf.NodeRange(ident) -- if err != nil { -- return protocol.Range{}, nil, err -- } +- // Only prompt if we can read/write the prompt config file. +- configDir, err := s.configDir() +- if err != nil { +- errorf("unable to determine config dir: %v", err) +- return - } - -- // By convention, we qualify hover information relative to the package -- // from which the request originated. -- qf := Qualifier(pgf.File, pkg.GetTypes(), pkg.GetTypesInfo()) +- var ( +- promptDir = filepath.Join(configDir, "prompt") // prompt configuration directory +- promptFile = filepath.Join(promptDir, "telemetry") // telemetry prompt file +- ) - -- // Handle type switch identifiers as a special case, since they don't have an -- // object. -- // -- // There's not much useful information to provide. -- if selectedType != nil { -- fakeObj := types.NewVar(obj.Pos(), obj.Pkg(), obj.Name(), selectedType) -- signature := types.ObjectString(fakeObj, qf) -- return rng, &HoverJSON{ -- Signature: signature, -- SingleLine: signature, -- SymbolName: fakeObj.Name(), -- }, nil +- // prompt states, to be written to the prompt file +- const ( +- pYes = "yes" // user said yes +- pNo = "no" // user said no +- pPending = "pending" // current prompt is still pending +- pFailed = "failed" // prompt was asked but failed +- ) +- validStates := map[string]bool{ +- pYes: true, +- pNo: true, +- pPending: true, +- pFailed: true, - } - -- // Handle builtins, which don't have a package or position. -- if obj.Pkg() == nil { -- h, err := hoverBuiltin(ctx, snapshot, obj) -- return rng, h, err +- // parse the current prompt file +- var ( +- state string +- attempts = 0 // number of times we've asked already +- ) +- if content, err := os.ReadFile(promptFile); err == nil { +- if _, err := fmt.Sscanf(string(content), "%s %d", &state, &attempts); err == nil && validStates[state] { +- if state == pYes || state == pNo { +- // Prompt has been answered. Nothing to do. +- return +- } +- } else { +- state, attempts = "", 0 +- errorf("malformed prompt result %q", string(content)) +- } +- } else if !os.IsNotExist(err) { +- errorf("reading prompt file: %v", err) +- // Something went wrong. Since we don't know how many times we've asked the +- // prompt, err on the side of not spamming. +- return - } - -- // For all other objects, consider the full syntax of their declaration in -- // order to correctly compute their documentation, signature, and link. -- declPGF, declPos, err := parseFull(ctx, snapshot, pkg.FileSet(), obj.Pos()) -- if err != nil { -- return protocol.Range{}, nil, fmt.Errorf("re-parsing declaration of %s: %v", obj.Name(), err) +- if attempts >= 5 { +- // We've tried asking enough; give up. +- return - } -- decl, spec, field := findDeclInfo([]*ast.File{declPGF.File}, declPos) -- comment := chooseDocComment(decl, spec, field) -- docText := comment.Text() -- -- // By default, types.ObjectString provides a reasonable signature. -- signature := objectString(obj, qf, declPos, declPGF.Tok, spec) -- singleLineSignature := signature -- -- // TODO(rfindley): we could do much better for inferred signatures. -- if inferred := inferredSignature(pkg.GetTypesInfo(), ident); inferred != nil { -- if s := inferredSignatureString(obj, qf, inferred); s != "" { -- signature = s +- if attempts == 0 { +- // First time asking the prompt; we may need to make the prompt dir. +- if err := os.MkdirAll(promptDir, 0777); err != nil { +- errorf("creating prompt dir: %v", err) +- return - } - } - -- // For "objects defined by a type spec", the signature produced by -- // objectString is insufficient: -- // (1) large structs are formatted poorly, with no newlines -- // (2) we lose inline comments -- // -- // Furthermore, we include a summary of their method set. +- // Acquire the lock and write "pending" to the prompt file before actually +- // prompting. - // -- // TODO(rfindley): this should use FormatVarType to get proper qualification -- // of identifiers, and we should revisit the formatting of method set. -- _, isTypeName := obj.(*types.TypeName) -- _, isTypeParam := obj.Type().(*typeparams.TypeParam) -- if isTypeName && !isTypeParam { -- spec, ok := spec.(*ast.TypeSpec) -- if !ok { -- return protocol.Range{}, nil, bug.Errorf("type name %q without type spec", obj.Name()) -- } -- spec2 := *spec -- // Don't duplicate comments when formatting type specs. -- spec2.Doc = nil -- spec2.Comment = nil -- var b strings.Builder -- b.WriteString("type ") -- fset := tokeninternal.FileSetFor(declPGF.Tok) -- if err := format.Node(&b, fset, &spec2); err != nil { -- return protocol.Range{}, nil, err -- } +- // This ensures that the prompt file is writeable, and that we increment the +- // attempt counter before we prompt, so that we don't end up in a failure +- // mode where we keep prompting and then failing to record the response. - -- // Display the declared methods accessible from the identifier. -- // -- // (The format.Node call above displays any struct fields, public -- // or private, in syntactic form. We choose not to recursively -- // enumerate any fields and methods promoted from them.) -- if !types.IsInterface(obj.Type()) { -- sep := "\n\n" -- for _, m := range typeutil.IntuitiveMethodSet(obj.Type(), nil) { -- // Show direct methods that are either exported, or defined in the -- // current package. -- if (m.Obj().Exported() || m.Obj().Pkg() == pkg.GetTypes()) && len(m.Index()) == 1 { -- b.WriteString(sep) -- sep = "\n" -- b.WriteString(types.ObjectString(m.Obj(), qf)) -- } -- } -- } -- signature = b.String() +- release, ok, err := acquireLockFile(promptFile) +- if err != nil { +- errorf("acquiring prompt: %v", err) +- return - } +- if !ok { +- // Another prompt is currently pending. +- return +- } +- defer release() - -- // Compute link data (on pkg.go.dev or other documentation host). -- // -- // If linkPath is empty, the symbol is not linkable. -- var ( -- linkName string // => link title, always non-empty -- linkPath string // => link path -- anchor string // link anchor -- linkMeta *Metadata // metadata for the linked package -- ) -- { -- linkMeta = findFileInDeps(snapshot, pkg.Metadata(), declPGF.URI) -- if linkMeta == nil { -- return protocol.Range{}, nil, bug.Errorf("no metadata for %s", declPGF.URI) -- } -- -- // For package names, we simply link to their imported package. -- if pkgName, ok := obj.(*types.PkgName); ok { -- linkName = pkgName.Name() -- linkPath = pkgName.Imported().Path() -- impID := linkMeta.DepsByPkgPath[PackagePath(pkgName.Imported().Path())] -- linkMeta = snapshot.Metadata(impID) -- if linkMeta == nil { -- return protocol.Range{}, nil, bug.Errorf("no metadata for %s", declPGF.URI) -- } -- } else { -- // For all others, check whether the object is in the package scope, or -- // an exported field or method of an object in the package scope. -- // -- // We try to match pkgsite's heuristics for what is linkable, and what is -- // not. -- var recv types.Object -- switch obj := obj.(type) { -- case *types.Func: -- sig := obj.Type().(*types.Signature) -- if sig.Recv() != nil { -- tname := typeToObject(sig.Recv().Type()) -- if tname != nil { // beware typed nil -- recv = tname -- } -- } -- case *types.Var: -- if obj.IsField() { -- if spec, ok := spec.(*ast.TypeSpec); ok { -- typeName := spec.Name -- scopeObj, _ := obj.Pkg().Scope().Lookup(typeName.Name).(*types.TypeName) -- if scopeObj != nil { -- if st, _ := scopeObj.Type().Underlying().(*types.Struct); st != nil { -- for i := 0; i < st.NumFields(); i++ { -- if obj == st.Field(i) { -- recv = scopeObj -- } -- } -- } -- } -- } -- } -- } -- -- // Even if the object is not available in package documentation, it may -- // be embedded in a documented receiver. Detect this by searching -- // enclosing selector expressions. -- // -- // TODO(rfindley): pkgsite doesn't document fields from embedding, just -- // methods. -- if recv == nil || !recv.Exported() { -- path := pathEnclosingObjNode(pgf.File, pos) -- if enclosing := searchForEnclosing(pkg.GetTypesInfo(), path); enclosing != nil { -- recv = enclosing -- } else { -- recv = nil // note: just recv = ... could result in a typed nil. -- } -- } +- attempts++ - -- pkg := obj.Pkg() -- if recv != nil { -- linkName = fmt.Sprintf("(%s.%s).%s", pkg.Name(), recv.Name(), obj.Name()) -- if obj.Exported() && recv.Exported() && pkg.Scope().Lookup(recv.Name()) == recv { -- linkPath = pkg.Path() -- anchor = fmt.Sprintf("%s.%s", recv.Name(), obj.Name()) -- } -- } else { -- linkName = fmt.Sprintf("%s.%s", pkg.Name(), obj.Name()) -- if obj.Exported() && pkg.Scope().Lookup(obj.Name()) == obj { -- linkPath = pkg.Path() -- anchor = obj.Name() -- } -- } -- } +- pendingContent := []byte(fmt.Sprintf("%s %d", pPending, attempts)) +- if err := os.WriteFile(promptFile, pendingContent, 0666); err != nil { +- errorf("writing pending state: %v", err) +- return - } - -- if snapshot.View().IsGoPrivatePath(linkPath) || linkMeta.ForTest != "" { -- linkPath = "" -- } else if linkMeta.Module != nil && linkMeta.Module.Version != "" { -- mod := linkMeta.Module -- linkPath = strings.Replace(linkPath, mod.Path, mod.Path+"@"+mod.Version, 1) -- } +- var prompt = `Go telemetry helps us improve Go by periodically sending anonymous metrics and crash reports to the Go team. Learn more at https://go.dev/doc/telemetry. - -- return rng, &HoverJSON{ -- Synopsis: doc.Synopsis(docText), -- FullDocumentation: docText, -- SingleLine: singleLineSignature, -- SymbolName: linkName, -- Signature: signature, -- LinkPath: linkPath, -- LinkAnchor: anchor, -- }, nil --} +-Would you like to enable Go telemetry? +-` +- if s.Options().LinkifyShowMessage { +- prompt = `Go telemetry helps us improve Go by periodically sending anonymous metrics and crash reports to the Go team. Learn more at [go.dev/doc/telemetry](https://go.dev/doc/telemetry). - --// hoverBuiltin computes hover information when hovering over a builtin --// identifier. --func hoverBuiltin(ctx context.Context, snapshot Snapshot, obj types.Object) (*HoverJSON, error) { -- // TODO(rfindley): link to the correct version of Go documentation. -- builtin, err := snapshot.BuiltinFile(ctx) -- if err != nil { -- return nil, err +-Would you like to enable Go telemetry? +-` - } -- -- if obj.Name() == "Error" { -- signature := obj.String() -- return &HoverJSON{ -- Signature: signature, -- SingleLine: signature, -- // TODO(rfindley): these are better than the current behavior. -- // SymbolName: "(error).Error", -- // LinkPath: "builtin", -- // LinkAnchor: "error.Error", -- }, nil +- // TODO(rfindley): investigate a "tell me more" action in combination with ShowDocument. +- params := &protocol.ShowMessageRequestParams{ +- Type: protocol.Info, +- Message: prompt, +- Actions: []protocol.MessageActionItem{ +- {Title: TelemetryYes}, +- {Title: TelemetryNo}, +- }, - } - -- builtinObj := builtin.File.Scope.Lookup(obj.Name()) -- if builtinObj == nil { -- // All builtins should have a declaration in the builtin file. -- return nil, bug.Errorf("no builtin object for %s", obj.Name()) -- } -- node, _ := builtinObj.Decl.(ast.Node) -- if node == nil { -- return nil, bug.Errorf("no declaration for %s", obj.Name()) +- item, err := s.client.ShowMessageRequest(ctx, params) +- if err != nil { +- errorf("ShowMessageRequest failed: %v", err) +- // Defensive: ensure item == nil for the logic below. +- item = nil - } - -- var comment *ast.CommentGroup -- path, _ := astutil.PathEnclosingInterval(builtin.File, node.Pos(), node.End()) -- for _, n := range path { -- switch n := n.(type) { -- case *ast.GenDecl: -- // Separate documentation and signature. -- comment = n.Doc -- node2 := *n -- node2.Doc = nil -- node = &node2 -- case *ast.FuncDecl: -- // Ditto. -- comment = n.Doc -- node2 := *n -- node2.Doc = nil -- node = &node2 +- message := func(typ protocol.MessageType, msg string) { +- if !showMessage(ctx, s.client, typ, msg) { +- // Make sure we record that "telemetry prompt failed". +- errorf("showMessage failed: %v", err) - } - } - -- signature := FormatNodeFile(builtin.Tok, node) -- // Replace fake types with their common equivalent. -- // TODO(rfindley): we should instead use obj.Type(), which would have the -- // *actual* types of the builtin call. -- signature = replacer.Replace(signature) +- result := pFailed +- if item == nil { +- // e.g. dialog was dismissed +- errorf("no response") +- } else { +- // Response matches MessageActionItem.Title. +- switch item.Title { +- case TelemetryYes: +- result = pYes +- if err := s.setTelemetryMode("on"); err == nil { +- message(protocol.Info, telemetryOnMessage(s.Options().LinkifyShowMessage)) +- } else { +- errorf("enabling telemetry failed: %v", err) +- msg := fmt.Sprintf("Failed to enable Go telemetry: %v\nTo enable telemetry manually, please run `go run golang.org/x/telemetry/cmd/gotelemetry@latest on`", err) +- message(protocol.Error, msg) +- } - -- docText := comment.Text() -- return &HoverJSON{ -- Synopsis: doc.Synopsis(docText), -- FullDocumentation: docText, -- Signature: signature, -- SingleLine: obj.String(), -- SymbolName: obj.Name(), -- LinkPath: "builtin", -- LinkAnchor: obj.Name(), -- }, nil +- case TelemetryNo: +- result = pNo +- default: +- errorf("unrecognized response %q", item.Title) +- message(protocol.Error, fmt.Sprintf("Unrecognized response %q", item.Title)) +- } +- } +- resultContent := []byte(fmt.Sprintf("%s %d", result, attempts)) +- if err := os.WriteFile(promptFile, resultContent, 0666); err != nil { +- errorf("error writing result state to prompt file: %v", err) +- } -} - --// hoverImport computes hover information when hovering over the import path of --// imp in the file pgf of pkg. --// --// If we do not have metadata for the hovered import, it returns _ --func hoverImport(ctx context.Context, snapshot Snapshot, pkg Package, pgf *ParsedGoFile, imp *ast.ImportSpec) (protocol.Range, *HoverJSON, error) { -- rng, err := pgf.NodeRange(imp.Path) -- if err != nil { -- return protocol.Range{}, nil, err -- } +-func telemetryOnMessage(linkify bool) string { +- format := `Thank you. Telemetry uploading is now enabled. - -- importPath := UnquoteImportPath(imp) -- if importPath == "" { -- return protocol.Range{}, nil, fmt.Errorf("invalid import path") -- } -- impID := pkg.Metadata().DepsByImpPath[importPath] -- if impID == "" { -- return protocol.Range{}, nil, fmt.Errorf("no package data for import %q", importPath) -- } -- impMetadata := snapshot.Metadata(impID) -- if impMetadata == nil { -- return protocol.Range{}, nil, bug.Errorf("failed to resolve import ID %q", impID) +-To disable telemetry uploading, run %s. +-` +- var runCmd = "`go run golang.org/x/telemetry/cmd/gotelemetry@latest local`" +- if linkify { +- runCmd = "[gotelemetry local](https://golang.org/x/telemetry/cmd/gotelemetry)" - } +- return fmt.Sprintf(format, runCmd) +-} - -- // Find the first file with a package doc comment. -- var comment *ast.CommentGroup -- for _, f := range impMetadata.CompiledGoFiles { -- fh, err := snapshot.ReadFile(ctx, f) -- if err != nil { -- if ctx.Err() != nil { -- return protocol.Range{}, nil, ctx.Err() -- } -- continue -- } -- pgf, err := snapshot.ParseGo(ctx, fh, ParseHeader) -- if err != nil { -- if ctx.Err() != nil { -- return protocol.Range{}, nil, ctx.Err() -- } -- continue -- } -- if pgf.File.Doc != nil { -- comment = pgf.File.Doc -- break +-// acquireLockFile attempts to "acquire a lock" for writing to path. +-// +-// This is achieved by creating an exclusive lock file at .lock. Lock +-// files expire after a period, at which point acquireLockFile will remove and +-// recreate the lock file. +-// +-// acquireLockFile fails if path is in a directory that doesn't exist. +-func acquireLockFile(path string) (func(), bool, error) { +- lockpath := path + ".lock" +- fi, err := os.Stat(lockpath) +- if err == nil { +- if time.Since(fi.ModTime()) > promptTimeout { +- _ = os.Remove(lockpath) // ignore error +- } else { +- return nil, false, nil - } +- } else if !os.IsNotExist(err) { +- return nil, false, fmt.Errorf("statting lockfile: %v", err) - } - -- docText := comment.Text() -- return rng, &HoverJSON{ -- Synopsis: doc.Synopsis(docText), -- FullDocumentation: docText, -- }, nil --} -- --// hoverPackageName computes hover information for the package name of the file --// pgf in pkg. --func hoverPackageName(pkg Package, pgf *ParsedGoFile) (protocol.Range, *HoverJSON, error) { -- var comment *ast.CommentGroup -- for _, pgf := range pkg.CompiledGoFiles() { -- if pgf.File.Doc != nil { -- comment = pgf.File.Doc -- break +- f, err := os.OpenFile(lockpath, os.O_CREATE|os.O_EXCL, 0666) +- if err != nil { +- if os.IsExist(err) { +- return nil, false, nil - } +- return nil, false, fmt.Errorf("creating lockfile: %v", err) - } -- rng, err := pgf.NodeRange(pgf.File.Name) +- fi, err = f.Stat() - if err != nil { -- return protocol.Range{}, nil, err +- return nil, false, err - } -- docText := comment.Text() -- return rng, &HoverJSON{ -- Synopsis: doc.Synopsis(docText), -- FullDocumentation: docText, -- // Note: including a signature is redundant, since the cursor is already on the -- // package name. -- }, nil +- release := func() { +- _ = f.Close() // ignore error +- fi2, err := os.Stat(lockpath) +- if err == nil && os.SameFile(fi, fi2) { +- // Only clean up the lockfile if it's the same file we created. +- // Otherwise, our lock has expired and something else has the lock. +- // +- // There's a race here, in that the file could have changed since the +- // stat above; but given that we've already waited 24h this is extremely +- // unlikely, and acceptable. +- _ = os.Remove(lockpath) +- } +- } +- return release, true, nil -} +diff -urN a/gopls/internal/server/prompt_test.go b/gopls/internal/server/prompt_test.go +--- a/gopls/internal/server/prompt_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/prompt_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,82 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// hoverLit computes hover information when hovering over the basic literal lit --// in the file pgf. The provided pos must be the exact position of the cursor, --// as it is used to extract the hovered rune in strings. --// --// For example, hovering over "\u2211" in "foo \u2211 bar" yields: --// --// '∑', U+2211, N-ARY SUMMATION --func hoverLit(pgf *ParsedGoFile, lit *ast.BasicLit, pos token.Pos) (protocol.Range, *HoverJSON, error) { -- var ( -- value string // if non-empty, a constant value to format in hover -- r rune // if non-zero, format a description of this rune in hover -- start, end token.Pos // hover span -- ) -- // Extract a rune from the current position. -- // 'Ω', "...Ω...", or 0x03A9 => 'Ω', U+03A9, GREEK CAPITAL LETTER OMEGA -- switch lit.Kind { -- case token.CHAR: -- s, err := strconv.Unquote(lit.Value) -- if err != nil { -- // If the conversion fails, it's because of an invalid syntax, therefore -- // there is no rune to be found. -- return protocol.Range{}, nil, nil -- } -- r, _ = utf8.DecodeRuneInString(s) -- if r == utf8.RuneError { -- return protocol.Range{}, nil, fmt.Errorf("rune error") -- } -- start, end = lit.Pos(), lit.End() +-package server - -- case token.INT: -- // Short literals (e.g. 99 decimal, 07 octal) are uninteresting. -- if len(lit.Value) < 3 { -- return protocol.Range{}, nil, nil -- } +-import ( +- "path/filepath" +- "sync" +- "sync/atomic" +- "testing" +-) - -- v := constant.MakeFromLiteral(lit.Value, lit.Kind, 0) -- if v.Kind() != constant.Int { -- return protocol.Range{}, nil, nil -- } +-func TestAcquireFileLock(t *testing.T) { +- name := filepath.Join(t.TempDir(), "config.json") - -- switch lit.Value[:2] { -- case "0x", "0X": -- // As a special case, try to recognize hexadecimal literals as runes if -- // they are within the range of valid unicode values. -- if v, ok := constant.Int64Val(v); ok && v > 0 && v <= utf8.MaxRune && utf8.ValidRune(rune(v)) { -- r = rune(v) +- const concurrency = 100 +- var acquired int32 +- var releasers [concurrency]func() +- defer func() { +- for _, r := range releasers { +- if r != nil { +- r() - } -- fallthrough -- case "0o", "0O", "0b", "0B": -- // Format the decimal value of non-decimal literals. -- value = v.ExactString() -- start, end = lit.Pos(), lit.End() -- default: -- return protocol.Range{}, nil, nil - } +- }() - -- case token.STRING: -- // It's a string, scan only if it contains a unicode escape sequence under or before the -- // current cursor position. -- litOffset, err := safetoken.Offset(pgf.Tok, lit.Pos()) -- if err != nil { -- return protocol.Range{}, nil, err -- } -- offset, err := safetoken.Offset(pgf.Tok, pos) -- if err != nil { -- return protocol.Range{}, nil, err -- } -- for i := offset - litOffset; i > 0; i-- { -- // Start at the cursor position and search backward for the beginning of a rune escape sequence. -- rr, _ := utf8.DecodeRuneInString(lit.Value[i:]) -- if rr == utf8.RuneError { -- return protocol.Range{}, nil, fmt.Errorf("rune error") +- var wg sync.WaitGroup +- for i := range releasers { +- i := i +- wg.Add(1) +- go func() { +- defer wg.Done() +- +- release, ok, err := acquireLockFile(name) +- if err != nil { +- t.Errorf("Acquire failed: %v", err) +- return - } -- if rr == '\\' { -- // Got the beginning, decode it. -- var tail string -- r, _, tail, err = strconv.UnquoteChar(lit.Value[i:], '"') -- if err != nil { -- // If the conversion fails, it's because of an invalid syntax, -- // therefore is no rune to be found. -- return protocol.Range{}, nil, nil -- } -- // Only the rune escape sequence part of the string has to be highlighted, recompute the range. -- runeLen := len(lit.Value) - (int(i) + len(tail)) -- start = token.Pos(int(lit.Pos()) + int(i)) -- end = token.Pos(int(start) + runeLen) -- break +- if ok { +- atomic.AddInt32(&acquired, 1) +- releasers[i] = release - } -- } -- } -- -- if value == "" && r == 0 { // nothing to format -- return protocol.Range{}, nil, nil +- }() - } - -- rng, err := pgf.PosRange(start, end) -- if err != nil { -- return protocol.Range{}, nil, err -- } +- wg.Wait() - -- var b strings.Builder -- if value != "" { -- b.WriteString(value) -- } -- if r != 0 { -- runeName := runenames.Name(r) -- if len(runeName) > 0 && runeName[0] == '<' { -- // Check if the rune looks like an HTML tag. If so, trim the surrounding <> -- // characters to work around https://github.com/microsoft/vscode/issues/124042. -- runeName = strings.TrimRight(runeName[1:], ">") -- } -- if b.Len() > 0 { -- b.WriteString(", ") -- } -- if strconv.IsPrint(r) { -- fmt.Fprintf(&b, "'%c', ", r) -- } -- fmt.Fprintf(&b, "U+%04X, %s", r, runeName) +- if acquired != 1 { +- t.Errorf("Acquire succeeded %d times, expected exactly 1", acquired) - } -- hover := b.String() -- return rng, &HoverJSON{ -- Synopsis: hover, -- FullDocumentation: hover, -- }, nil -} - --// hoverEmbed computes hover information for a filepath.Match pattern. --// Assumes that the pattern is relative to the location of fh. --func hoverEmbed(fh FileHandle, rng protocol.Range, pattern string) (protocol.Range, *HoverJSON, error) { -- s := &strings.Builder{} +-func TestReleaseAndAcquireFileLock(t *testing.T) { +- name := filepath.Join(t.TempDir(), "config.json") - -- dir := filepath.Dir(fh.URI().Filename()) -- var matches []string -- err := filepath.WalkDir(dir, func(abs string, d fs.DirEntry, e error) error { -- if e != nil { -- return e -- } -- rel, err := filepath.Rel(dir, abs) -- if err != nil { -- return err -- } -- ok, err := filepath.Match(pattern, rel) +- acquire := func() (func(), bool) { +- t.Helper() +- release, ok, err := acquireLockFile(name) - if err != nil { -- return err -- } -- if ok && !d.IsDir() { -- matches = append(matches, rel) +- t.Fatal(err) - } -- return nil -- }) -- if err != nil { -- return protocol.Range{}, nil, err +- return release, ok - } - -- for _, m := range matches { -- // TODO: Renders each file as separate markdown paragraphs. -- // If forcing (a single) newline is possible it might be more clear. -- fmt.Fprintf(s, "%s\n\n", m) +- release, ok := acquire() +- if !ok { +- t.Fatal("failed to Acquire") - } -- -- json := &HoverJSON{ -- Signature: fmt.Sprintf("Embedding %q", pattern), -- Synopsis: s.String(), -- FullDocumentation: s.String(), +- if release2, ok := acquire(); ok { +- release() +- release2() +- t.Fatalf("Acquire succeeded unexpectedly") - } -- return rng, json, nil --} - --// inferredSignatureString is a wrapper around the types.ObjectString function --// that adds more information to inferred signatures. It will return an empty string --// if the passed types.Object is not a signature. --func inferredSignatureString(obj types.Object, qf types.Qualifier, inferred *types.Signature) string { -- // If the signature type was inferred, prefer the inferred signature with a -- // comment showing the generic signature. -- if sig, _ := obj.Type().(*types.Signature); sig != nil && typeparams.ForSignature(sig).Len() > 0 && inferred != nil { -- obj2 := types.NewFunc(obj.Pos(), obj.Pkg(), obj.Name(), inferred) -- str := types.ObjectString(obj2, qf) -- // Try to avoid overly long lines. -- if len(str) > 60 { -- str += "\n" -- } else { -- str += " " -- } -- str += "// " + types.TypeString(sig, qf) -- return str +- release() +- release3, ok := acquire() +- release3() +- if !ok { +- t.Fatalf("failed to Acquire") - } -- return "" -} +diff -urN a/gopls/internal/server/references.go b/gopls/internal/server/references.go +--- a/gopls/internal/server/references.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/references.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,40 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// objectString is a wrapper around the types.ObjectString function. --// It handles adding more information to the object string. --// If spec is non-nil, it may be used to format additional declaration --// syntax, and file must be the token.File describing its positions. --func objectString(obj types.Object, qf types.Qualifier, declPos token.Pos, file *token.File, spec ast.Spec) string { -- str := types.ObjectString(obj, qf) +-package server - -- switch obj := obj.(type) { -- case *types.Const: -- var ( -- declaration = obj.Val().String() // default formatted declaration -- comment = "" // if non-empty, a clarifying comment -- ) +-import ( +- "context" - -- // Try to use the original declaration. -- switch obj.Val().Kind() { -- case constant.String: -- // Usually the original declaration of a string doesn't carry much information. -- // Also strings can be very long. So, just use the constant's value. +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/telemetry" +- "golang.org/x/tools/gopls/internal/template" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/tag" +-) - -- default: -- if spec, _ := spec.(*ast.ValueSpec); spec != nil { -- for i, name := range spec.Names { -- if declPos == name.Pos() { -- if i < len(spec.Values) { -- originalDeclaration := FormatNodeFile(file, spec.Values[i]) -- if originalDeclaration != declaration { -- comment = declaration -- declaration = originalDeclaration -- } -- } -- break -- } -- } -- } -- } +-func (s *server) References(ctx context.Context, params *protocol.ReferenceParams) (_ []protocol.Location, rerr error) { +- recordLatency := telemetry.StartLatencyTimer("references") +- defer func() { +- recordLatency(ctx, rerr) +- }() - -- // Special formatting cases. -- switch typ := obj.Type().(type) { -- case *types.Named: -- // Try to add a formatted duration as an inline comment. -- pkg := typ.Obj().Pkg() -- if pkg.Path() == "time" && typ.Obj().Name() == "Duration" { -- if d, ok := constant.Int64Val(obj.Val()); ok { -- comment = time.Duration(d).String() -- } -- } -- } -- if comment == declaration { -- comment = "" -- } +- ctx, done := event.Start(ctx, "lsp.Server.references", tag.URI.Of(params.TextDocument.URI)) +- defer done() - -- str += " = " + declaration -- if comment != "" { -- str += " // " + comment -- } +- fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI) +- if err != nil { +- return nil, err - } -- return str +- defer release() +- switch snapshot.FileKind(fh) { +- case file.Tmpl: +- return template.References(ctx, snapshot, fh, params) +- case file.Go: +- return golang.References(ctx, snapshot, fh, params.Position, params.Context.IncludeDeclaration) +- } +- return nil, nil // empty result -} +diff -urN a/gopls/internal/server/rename.go b/gopls/internal/server/rename.go +--- a/gopls/internal/server/rename.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/rename.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,98 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// HoverDocForObject returns the best doc comment for obj (for which --// fset provides file/line information). --// --// TODO(rfindley): there appears to be zero(!) tests for this functionality. --func HoverDocForObject(ctx context.Context, snapshot Snapshot, fset *token.FileSet, obj types.Object) (*ast.CommentGroup, error) { -- if _, isTypeName := obj.(*types.TypeName); isTypeName { -- if _, isTypeParam := obj.Type().(*typeparams.TypeParam); isTypeParam { -- return nil, nil -- } -- } +-package server - -- pgf, pos, err := parseFull(ctx, snapshot, fset, obj.Pos()) +-import ( +- "context" +- "fmt" +- "path/filepath" +- +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/tag" +-) +- +-func (s *server) Rename(ctx context.Context, params *protocol.RenameParams) (*protocol.WorkspaceEdit, error) { +- ctx, done := event.Start(ctx, "lsp.Server.rename", tag.URI.Of(params.TextDocument.URI)) +- defer done() +- +- fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI) - if err != nil { -- return nil, fmt.Errorf("re-parsing: %v", err) +- return nil, err - } +- defer release() - -- decl, spec, field := findDeclInfo([]*ast.File{pgf.File}, pos) -- return chooseDocComment(decl, spec, field), nil --} +- if kind := snapshot.FileKind(fh); kind != file.Go { +- return nil, fmt.Errorf("cannot rename in file of type %s", kind) +- } - --func chooseDocComment(decl ast.Decl, spec ast.Spec, field *ast.Field) *ast.CommentGroup { -- if field != nil { -- if field.Doc != nil { -- return field.Doc -- } -- if field.Comment != nil { -- return field.Comment -- } -- return nil +- // Because we don't handle directory renaming within golang.Rename, golang.Rename returns +- // boolean value isPkgRenaming to determine whether an DocumentChanges of type RenameFile should +- // be added to the return protocol.WorkspaceEdit value. +- edits, isPkgRenaming, err := golang.Rename(ctx, snapshot, fh, params.Position, params.NewName) +- if err != nil { +- return nil, err - } -- switch decl := decl.(type) { -- case *ast.FuncDecl: -- return decl.Doc -- case *ast.GenDecl: -- switch spec := spec.(type) { -- case *ast.ValueSpec: -- if spec.Doc != nil { -- return spec.Doc -- } -- if decl.Doc != nil { -- return decl.Doc -- } -- return spec.Comment -- case *ast.TypeSpec: -- if spec.Doc != nil { -- return spec.Doc -- } -- if decl.Doc != nil { -- return decl.Doc -- } -- return spec.Comment +- +- docChanges := []protocol.DocumentChanges{} // must be a slice +- for uri, e := range edits { +- fh, err := snapshot.ReadFile(ctx, uri) +- if err != nil { +- return nil, err - } +- docChanges = append(docChanges, documentChanges(fh, e)...) - } -- return nil +- if isPkgRenaming { +- // Update the last component of the file's enclosing directory. +- oldBase := filepath.Dir(fh.URI().Path()) +- newURI := filepath.Join(filepath.Dir(oldBase), params.NewName) +- docChanges = append(docChanges, protocol.DocumentChanges{ +- RenameFile: &protocol.RenameFile{ +- Kind: "rename", +- OldURI: protocol.URIFromPath(oldBase), +- NewURI: protocol.URIFromPath(newURI), +- }, +- }) +- } +- return &protocol.WorkspaceEdit{ +- DocumentChanges: docChanges, +- }, nil -} - --// parseFull fully parses the file corresponding to position pos (for --// which fset provides file/line information). +-// PrepareRename implements the textDocument/prepareRename handler. It may +-// return (nil, nil) if there is no rename at the cursor position, but it is +-// not desirable to display an error to the user. -// --// It returns the resulting ParsedGoFile as well as new pos contained in the --// parsed file. --func parseFull(ctx context.Context, snapshot Snapshot, fset *token.FileSet, pos token.Pos) (*ParsedGoFile, token.Pos, error) { -- f := fset.File(pos) -- if f == nil { -- return nil, 0, bug.Errorf("internal error: no file for position %d", pos) -- } +-// TODO(rfindley): why wouldn't we want to show an error to the user, if the +-// user initiated a rename request at the cursor? +-func (s *server) PrepareRename(ctx context.Context, params *protocol.PrepareRenameParams) (*protocol.PrepareRenamePlaceholder, error) { +- ctx, done := event.Start(ctx, "lsp.Server.prepareRename", tag.URI.Of(params.TextDocument.URI)) +- defer done() - -- uri := span.URIFromPath(f.Name()) -- fh, err := snapshot.ReadFile(ctx, uri) +- fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI) - if err != nil { -- return nil, 0, err +- return nil, err +- } +- defer release() +- +- if kind := snapshot.FileKind(fh); kind != file.Go { +- return nil, fmt.Errorf("cannot rename in file of type %s", kind) - } - -- pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) +- // Do not return errors here, as it adds clutter. +- // Returning a nil result means there is not a valid rename. +- item, usererr, err := golang.PrepareRename(ctx, snapshot, fh, params.Position) - if err != nil { -- return nil, 0, err +- // Return usererr here rather than err, to avoid cluttering the UI with +- // internal error details. +- return nil, usererr - } +- return &protocol.PrepareRenamePlaceholder{ +- Range: item.Range, +- Placeholder: item.Text, +- }, nil +-} +diff -urN a/gopls/internal/server/selection_range.go b/gopls/internal/server/selection_range.go +--- a/gopls/internal/server/selection_range.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/selection_range.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,75 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- offset, err := safetoken.Offset(f, pos) +-package server +- +-import ( +- "context" +- "fmt" +- +- "golang.org/x/tools/go/ast/astutil" +- "golang.org/x/tools/gopls/internal/cache/parsego" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/event" +-) +- +-// selectionRange defines the textDocument/selectionRange feature, +-// which, given a list of positions within a file, +-// reports a linked list of enclosing syntactic blocks, innermost first. +-// +-// See https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_selectionRange. +-// +-// This feature can be used by a client to implement "expand selection" in a +-// language-aware fashion. Multiple input positions are supported to allow +-// for multiple cursors, and the entire path up to the whole document is +-// returned for each cursor to avoid multiple round-trips when the user is +-// likely to issue this command multiple times in quick succession. +-func (s *server) SelectionRange(ctx context.Context, params *protocol.SelectionRangeParams) ([]protocol.SelectionRange, error) { +- ctx, done := event.Start(ctx, "lsp.Server.selectionRange") +- defer done() +- +- fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI) - if err != nil { -- return nil, 0, bug.Errorf("offset out of bounds in %q", uri) +- return nil, err - } +- defer release() - -- fullPos, err := safetoken.Pos(pgf.Tok, offset) +- if kind := snapshot.FileKind(fh); kind != file.Go { +- return nil, fmt.Errorf("SelectionRange not supported for file of type %s", kind) +- } +- +- pgf, err := snapshot.ParseGo(ctx, fh, parsego.Full) - if err != nil { -- return nil, 0, err +- return nil, err +- } +- +- result := make([]protocol.SelectionRange, len(params.Positions)) +- for i, protocolPos := range params.Positions { +- pos, err := pgf.PositionPos(protocolPos) +- if err != nil { +- return nil, err +- } +- +- path, _ := astutil.PathEnclosingInterval(pgf.File, pos, pos) +- +- tail := &result[i] // tail of the Parent linked list, built head first +- +- for j, node := range path { +- rng, err := pgf.NodeRange(node) +- if err != nil { +- return nil, err +- } +- +- // Add node to tail. +- if j > 0 { +- tail.Parent = &protocol.SelectionRange{} +- tail = tail.Parent +- } +- tail.Range = rng +- } - } - -- return pgf, fullPos, nil +- return result, nil -} +diff -urN a/gopls/internal/server/semantic.go b/gopls/internal/server/semantic.go +--- a/gopls/internal/server/semantic.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/semantic.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,53 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func formatHover(h *HoverJSON, options *Options) (string, error) { -- signature := formatSignature(h, options) +-package server - -- switch options.HoverKind { -- case SingleLine: -- return h.SingleLine, nil -- case NoDocumentation: -- return signature, nil -- case Structured: -- b, err := json.Marshal(h) -- if err != nil { -- return "", err -- } -- return string(b), nil -- } +-import ( +- "context" +- "fmt" - -- link := formatLink(h, options) -- doc := formatDoc(h, options) +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/template" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/tag" +-) - -- var b strings.Builder -- parts := []string{signature, doc, link} -- for i, el := range parts { -- if el != "" { -- b.WriteString(el) -- -- // If any elements of the remainder of the list are non-empty, -- // write an extra newline. -- if anyNonEmpty(parts[i+1:]) { -- if options.PreferredContentFormat == protocol.Markdown { -- b.WriteString("\n\n") -- } else { -- b.WriteRune('\n') -- } -- } -- } -- } -- return b.String(), nil +-func (s *server) SemanticTokensFull(ctx context.Context, params *protocol.SemanticTokensParams) (*protocol.SemanticTokens, error) { +- return s.semanticTokens(ctx, params.TextDocument, nil) -} - --func formatSignature(h *HoverJSON, options *Options) string { -- signature := h.Signature -- if signature != "" && options.PreferredContentFormat == protocol.Markdown { -- signature = fmt.Sprintf("```go\n%s\n```", signature) -- } -- return signature +-func (s *server) SemanticTokensRange(ctx context.Context, params *protocol.SemanticTokensRangeParams) (*protocol.SemanticTokens, error) { +- return s.semanticTokens(ctx, params.TextDocument, ¶ms.Range) -} - --func formatLink(h *HoverJSON, options *Options) string { -- if !options.LinksInHover || options.LinkTarget == "" || h.LinkPath == "" { -- return "" +-func (s *server) semanticTokens(ctx context.Context, td protocol.TextDocumentIdentifier, rng *protocol.Range) (*protocol.SemanticTokens, error) { +- ctx, done := event.Start(ctx, "lsp.Server.semanticTokens", tag.URI.Of(td.URI)) +- defer done() +- +- fh, snapshot, release, err := s.fileOf(ctx, td.URI) +- if err != nil { +- return nil, err - } -- plainLink := BuildLink(options.LinkTarget, h.LinkPath, h.LinkAnchor) -- switch options.PreferredContentFormat { -- case protocol.Markdown: -- return fmt.Sprintf("[`%s` on %s](%s)", h.SymbolName, options.LinkTarget, plainLink) -- case protocol.PlainText: -- return "" -- default: -- return plainLink +- defer release() +- if !snapshot.Options().SemanticTokens { +- // return an error, so if the option changes +- // the client won't remember the wrong answer +- return nil, fmt.Errorf("semantictokens are disabled") - } --} - --// BuildLink constructs a URL with the given target, path, and anchor. --func BuildLink(target, path, anchor string) string { -- link := fmt.Sprintf("https://%s/%s", target, path) -- if anchor == "" { -- return link +- switch snapshot.FileKind(fh) { +- case file.Tmpl: +- return template.SemanticTokens(ctx, snapshot, fh.URI()) +- +- case file.Go: +- return golang.SemanticTokens(ctx, snapshot, fh, rng) +- +- default: +- // TODO(adonovan): should return an error! +- return nil, nil // empty result - } -- return link + "#" + anchor -} +diff -urN a/gopls/internal/server/server.go b/gopls/internal/server/server.go +--- a/gopls/internal/server/server.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/server.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,398 +0,0 @@ +-// Copyright 2018 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func formatDoc(h *HoverJSON, options *Options) string { -- var doc string -- switch options.HoverKind { -- case SynopsisDocumentation: -- doc = h.Synopsis -- case FullDocumentation: -- doc = h.FullDocumentation -- } -- if options.PreferredContentFormat == protocol.Markdown { -- return CommentToMarkdown(doc, options) +-// Package server defines gopls' implementation of the LSP server +-// interface, [protocol.Server]. Call [New] to create an instance. +-package server +- +-import ( +- "context" +- "crypto/rand" +- "embed" +- "encoding/base64" +- "fmt" +- "log" +- "net" +- "net/http" +- "net/url" +- "os" +- paths "path" +- "strconv" +- "strings" +- "sync" +- +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cache/metadata" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/progress" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/settings" +- "golang.org/x/tools/internal/event" +-) +- +-// New creates an LSP server and binds it to handle incoming client +-// messages on the supplied stream. +-func New(session *cache.Session, client protocol.ClientCloser, options *settings.Options) protocol.Server { +- const concurrentAnalyses = 1 +- // If this assignment fails to compile after a protocol +- // upgrade, it means that one or more new methods need new +- // stub declarations in unimplemented.go. +- return &server{ +- diagnostics: make(map[protocol.DocumentURI]*fileDiagnostics), +- watchedGlobPatterns: nil, // empty +- changedFiles: make(map[protocol.DocumentURI]unit), +- session: session, +- client: client, +- diagnosticsSema: make(chan unit, concurrentAnalyses), +- progress: progress.NewTracker(client), +- options: options, +- viewsToDiagnose: make(map[*cache.View]uint64), - } -- return doc -} - --func anyNonEmpty(x []string) bool { -- for _, el := range x { -- if el != "" { -- return true -- } +-type serverState int +- +-const ( +- serverCreated = serverState(iota) +- serverInitializing // set once the server has received "initialize" request +- serverInitialized // set once the server has received "initialized" request +- serverShutDown +-) +- +-func (s serverState) String() string { +- switch s { +- case serverCreated: +- return "created" +- case serverInitializing: +- return "initializing" +- case serverInitialized: +- return "initialized" +- case serverShutDown: +- return "shutDown" - } -- return false +- return fmt.Sprintf("(unknown state: %d)", int(s)) -} - --// findDeclInfo returns the syntax nodes involved in the declaration of the --// types.Object with position pos, searching the given list of file syntax --// trees. --// --// Pos may be the position of the name-defining identifier in a FuncDecl, --// ValueSpec, TypeSpec, Field, or as a special case the position of --// Ellipsis.Elt in an ellipsis field. --// --// If found, the resulting decl, spec, and field will be the inner-most --// instance of each node type surrounding pos. --// --// If field is non-nil, pos is the position of a field Var. If field is nil and --// spec is non-nil, pos is the position of a Var, Const, or TypeName object. If --// both field and spec are nil and decl is non-nil, pos is the position of a --// Func object. --// --// It returns a nil decl if no object-defining node is found at pos. --// --// TODO(rfindley): this function has tricky semantics, and may be worth unit --// testing and/or refactoring. --func findDeclInfo(files []*ast.File, pos token.Pos) (decl ast.Decl, spec ast.Spec, field *ast.Field) { -- // panic(found{}) breaks off the traversal and -- // causes the function to return normally. -- type found struct{} -- defer func() { -- switch x := recover().(type) { -- case nil: -- case found: -- default: -- panic(x) -- } -- }() +-// server implements the protocol.server interface. +-type server struct { +- client protocol.ClientCloser - -- // Visit the files in search of the node at pos. -- stack := make([]ast.Node, 0, 20) -- // Allocate the closure once, outside the loop. -- f := func(n ast.Node) bool { -- if n != nil { -- stack = append(stack, n) // push -- } else { -- stack = stack[:len(stack)-1] // pop -- return false -- } +- stateMu sync.Mutex +- state serverState +- // notifications generated before serverInitialized +- notifications []*protocol.ShowMessageParams - -- // Skip subtrees (incl. files) that don't contain the search point. -- if !(n.Pos() <= pos && pos < n.End()) { -- return false -- } +- session *cache.Session - -- switch n := n.(type) { -- case *ast.Field: -- findEnclosingDeclAndSpec := func() { -- for i := len(stack) - 1; i >= 0; i-- { -- switch n := stack[i].(type) { -- case ast.Spec: -- spec = n -- case ast.Decl: -- decl = n -- return -- } -- } -- } +- tempDir string - -- // Check each field name since you can have -- // multiple names for the same type expression. -- for _, id := range n.Names { -- if id.Pos() == pos { -- field = n -- findEnclosingDeclAndSpec() -- panic(found{}) -- } -- } +- // changedFiles tracks files for which there has been a textDocument/didChange. +- changedFilesMu sync.Mutex +- changedFiles map[protocol.DocumentURI]unit - -- // Check *ast.Field itself. This handles embedded -- // fields which have no associated *ast.Ident name. -- if n.Pos() == pos { -- field = n -- findEnclosingDeclAndSpec() -- panic(found{}) -- } +- // folders is only valid between initialize and initialized, and holds the +- // set of folders to build views for when we are ready. +- // Each has a valid, non-empty 'file'-scheme URI. +- pendingFolders []protocol.WorkspaceFolder - -- // Also check "X" in "...X". This makes it easy to format variadic -- // signature params properly. -- // -- // TODO(rfindley): I don't understand this comment. How does finding the -- // field in this case make it easier to format variadic signature params? -- if ell, ok := n.Type.(*ast.Ellipsis); ok && ell.Elt != nil && ell.Elt.Pos() == pos { -- field = n -- findEnclosingDeclAndSpec() -- panic(found{}) -- } +- // watchedGlobPatterns is the set of glob patterns that we have requested +- // the client watch on disk. It will be updated as the set of directories +- // that the server should watch changes. +- // The map field may be reassigned but the map is immutable. +- watchedGlobPatternsMu sync.Mutex +- watchedGlobPatterns map[protocol.RelativePattern]unit +- watchRegistrationCount int - -- case *ast.FuncDecl: -- if n.Name.Pos() == pos { -- decl = n -- panic(found{}) -- } +- diagnosticsMu sync.Mutex +- diagnostics map[protocol.DocumentURI]*fileDiagnostics - -- case *ast.GenDecl: -- for _, s := range n.Specs { -- switch s := s.(type) { -- case *ast.TypeSpec: -- if s.Name.Pos() == pos { -- decl = n -- spec = s -- panic(found{}) -- } -- case *ast.ValueSpec: -- for _, id := range s.Names { -- if id.Pos() == pos { -- decl = n -- spec = s -- panic(found{}) -- } -- } -- } -- } -- } -- return true -- } -- for _, file := range files { -- ast.Inspect(file, f) -- } +- // diagnosticsSema limits the concurrency of diagnostics runs, which can be +- // expensive. +- diagnosticsSema chan unit - -- return nil, nil, nil --} -diff -urN a/gopls/internal/lsp/source/identifier.go b/gopls/internal/lsp/source/identifier.go ---- a/gopls/internal/lsp/source/identifier.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/identifier.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,174 +0,0 @@ --// Copyright 2018 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +- progress *progress.Tracker - --package source +- // When the workspace fails to load, we show its status through a progress +- // report with an error message. +- criticalErrorStatusMu sync.Mutex +- criticalErrorStatus *progress.WorkDone - --import ( -- "errors" -- "go/ast" -- "go/types" +- // Track an ongoing CPU profile created with the StartProfile command and +- // terminated with the StopProfile command. +- ongoingProfileMu sync.Mutex +- ongoingProfile *os.File // if non-nil, an ongoing profile is writing to this file - -- "golang.org/x/tools/internal/typeparams" --) +- // Track most recently requested options. +- optionsMu sync.Mutex +- options *settings.Options +- +- // Track the most recent completion results, for measuring completion efficacy +- efficacyMu sync.Mutex +- efficacyURI protocol.DocumentURI +- efficacyVersion int32 +- efficacyItems []protocol.CompletionItem +- efficacyPos protocol.Position +- +- // Web server (for package documentation, etc) associated with this +- // LSP server. Opened on demand, and closed during LSP Shutdown. +- webOnce sync.Once +- web *web +- webErr error +- +- // # Modification tracking and diagnostics +- // +- // For the purpose of tracking diagnostics, we need a monotonically +- // increasing clock. Each time a change occurs on the server, this clock is +- // incremented and the previous diagnostics pass is cancelled. When the +- // changed is processed, the Session (via DidModifyFiles) determines which +- // Views are affected by the change and these views are added to the +- // viewsToDiagnose set. Then the server calls diagnoseChangedViews +- // in a separate goroutine. Any Views that successfully complete their +- // diagnostics are removed from the viewsToDiagnose set, provided they haven't +- // been subsequently marked for re-diagnosis (as determined by the latest +- // modificationID referenced by viewsToDiagnose). +- // +- // In this way, we enforce eventual completeness of the diagnostic set: any +- // views requiring diagnosis are diagnosed, though possibly at a later point +- // in time. Notably, the logic in Session.DidModifyFiles to determines if a +- // view needs diagnosis considers whether any packages in the view were +- // invalidated. Consider the following sequence of snapshots for a given view +- // V: +- // +- // C1 C2 +- // S1 -> S2 -> S3 +- // +- // In this case, suppose that S1 was fully type checked, and then two changes +- // C1 and C2 occur in rapid succession, to a file in their package graph but +- // perhaps not enclosed by V's root. In this case, the logic of +- // DidModifyFiles will detect that V needs to be reloaded following C1. In +- // order for our eventual consistency to be sound, we need to avoid the race +- // where S2 is being diagnosed, C2 arrives, and S3 is not detected as needing +- // diagnosis because the relevant package has not yet been computed in S2. To +- // achieve this, we only remove V from viewsToDiagnose if the diagnosis of S2 +- // completes before C2 is processed, which we can confirm by checking +- // S2.BackgroundContext(). +- modificationMu sync.Mutex +- cancelPrevDiagnostics func() +- viewsToDiagnose map[*cache.View]uint64 // View -> modification at which it last required diagnosis +- lastModificationID uint64 // incrementing clock +-} +- +-func (s *server) WorkDoneProgressCancel(ctx context.Context, params *protocol.WorkDoneProgressCancelParams) error { +- ctx, done := event.Start(ctx, "lsp.Server.workDoneProgressCancel") +- defer done() - --// ErrNoIdentFound is error returned when no identifier is found at a particular position --var ErrNoIdentFound = errors.New("no identifier found") +- return s.progress.Cancel(params.Token) +-} - --// inferredSignature determines the resolved non-generic signature for an --// identifier in an instantiation expression. +-// web encapsulates the web server associated with an LSP server. +-// It is used for package documentation and other queries +-// where HTML makes more sense than a client editor UI. -// --// If no such signature exists, it returns nil. --func inferredSignature(info *types.Info, id *ast.Ident) *types.Signature { -- inst := typeparams.GetInstances(info)[id] -- sig, _ := inst.Type.(*types.Signature) -- return sig +-// Example URL: +-// +-// http://127.0.0.1:PORT/gopls/SECRET/... +-// +-// where +-// - PORT is the random port number; +-// - "gopls" helps the reader guess which program is the server; +-// - SECRET is the 64-bit token; and +-// - ... is the material part of the endpoint. +-// +-// Valid endpoints: +-// +-// open?file=%s&line=%d&col=%d - open a file +-// pkg/PKGPATH?view=%s - show doc for package in a given view +-type web struct { +- server *http.Server +- addr url.URL // "http://127.0.0.1:PORT/gopls/SECRET" +- mux *http.ServeMux -} - --func searchForEnclosing(info *types.Info, path []ast.Node) *types.TypeName { -- for _, n := range path { -- switch n := n.(type) { -- case *ast.SelectorExpr: -- if sel, ok := info.Selections[n]; ok { -- recv := Deref(sel.Recv()) +-// getWeb returns the web server associated with this +-// LSP server, creating it on first request. +-func (s *server) getWeb() (*web, error) { +- s.webOnce.Do(func() { +- s.web, s.webErr = s.initWeb() +- }) +- return s.web, s.webErr +-} - -- // Keep track of the last exported type seen. -- var exported *types.TypeName -- if named, ok := recv.(*types.Named); ok && named.Obj().Exported() { -- exported = named.Obj() -- } -- // We don't want the last element, as that's the field or -- // method itself. -- for _, index := range sel.Index()[:len(sel.Index())-1] { -- if r, ok := recv.Underlying().(*types.Struct); ok { -- recv = Deref(r.Field(index).Type()) -- if named, ok := recv.(*types.Named); ok && named.Obj().Exported() { -- exported = named.Obj() -- } -- } -- } -- return exported -- } -- } +-// initWeb starts the local web server through which gopls +-// serves package documentation and suchlike. +-// +-// Clients should use [getWeb]. +-func (s *server) initWeb() (*web, error) { +- // Use 64 random bits as the base of the URL namespace. +- // This ensures that URLs are unguessable to any local +- // processes that connect to the server, preventing +- // exfiltration of source code. +- // +- // (Note: depending on the LSP client, URLs that are passed to +- // it via showDocument and that result in the opening of a +- // browser tab may be transiently published through the argv +- // array of the open(1) or xdg-open(1) command.) +- token := make([]byte, 8) +- if _, err := rand.Read(token); err != nil { +- return nil, fmt.Errorf("generating secret token: %v", err) - } -- return nil --} - --// typeToObject returns the relevant type name for the given type, after --// unwrapping pointers, arrays, slices, channels, and function signatures with --// a single non-error result, and ignoring built-in named types. --func typeToObject(typ types.Type) *types.TypeName { -- switch typ := typ.(type) { -- case *types.Named: -- // TODO(rfindley): this should use typeparams.NamedTypeOrigin. -- return typ.Obj() -- case *types.Pointer: -- return typeToObject(typ.Elem()) -- case *types.Array: -- return typeToObject(typ.Elem()) -- case *types.Slice: -- return typeToObject(typ.Elem()) -- case *types.Chan: -- return typeToObject(typ.Elem()) -- case *types.Signature: -- // Try to find a return value of a named type. If there's only one -- // such value, jump to its type definition. -- var res *types.TypeName +- // Pick any free port. +- listener, err := net.Listen("tcp", "127.0.0.1:0") +- if err != nil { +- return nil, err +- } - -- results := typ.Results() -- for i := 0; i < results.Len(); i++ { -- obj := typeToObject(results.At(i).Type()) -- if obj == nil || hasErrorType(obj) { -- // Skip builtins. TODO(rfindley): should comparable be handled here as well? -- continue -- } -- if res != nil { -- // The function/method must have only one return value of a named type. -- return nil -- } +- // -- There should be no early returns after this point. -- - -- res = obj +- // The root mux is not authenticated. +- rootMux := http.NewServeMux() +- rootMux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { +- http.Error(w, "request URI lacks authentication segment", http.StatusUnauthorized) +- }) +- rootMux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, req *http.Request) { +- http.Redirect(w, req, "/assets/favicon.ico", http.StatusMovedPermanently) +- }) +- rootMux.HandleFunc("/hang", func(w http.ResponseWriter, req *http.Request) { +- // This endpoint hangs until cancelled. +- // It is used by JS to detect server disconnect. +- <-req.Context().Done() +- }) +- rootMux.Handle("/assets/", http.FileServer(http.FS(assets))) +- +- secret := "/gopls/" + base64.RawURLEncoding.EncodeToString(token) +- webMux := http.NewServeMux() +- rootMux.Handle(secret+"/", http.StripPrefix(secret, webMux)) +- +- webServer := &http.Server{Addr: listener.Addr().String(), Handler: rootMux} +- go func() { +- // This should run until LSP Shutdown, at which point +- // it will return ErrServerClosed. Any other error +- // means it failed to start. +- if err := webServer.Serve(listener); err != nil { +- if err != http.ErrServerClosed { +- log.Print(err) +- } - } -- return res -- default: -- return nil +- }() +- +- web := &web{ +- server: webServer, +- addr: url.URL{Scheme: "http", Host: webServer.Addr, Path: secret}, +- mux: webMux, - } --} - --func hasErrorType(obj types.Object) bool { -- return types.IsInterface(obj.Type()) && obj.Pkg() == nil && obj.Name() == "error" --} +- // The /open handler allows the browser to request that the +- // LSP client editor open a file; see web.urlToOpen. +- webMux.HandleFunc("/open", func(w http.ResponseWriter, req *http.Request) { +- if err := req.ParseForm(); err != nil { +- http.Error(w, err.Error(), http.StatusBadRequest) +- return +- } +- uri := protocol.URIFromPath(req.Form.Get("file")) +- line, _ := strconv.Atoi(req.Form.Get("line")) // 1-based +- col, _ := strconv.Atoi(req.Form.Get("col")) // 1-based UTF-8 +- posn := protocol.Position{ +- Line: uint32(line - 1), +- Character: uint32(col - 1), // TODO(adonovan): map to UTF-16 +- } +- openClientEditor(req.Context(), s.client, protocol.Location{ +- URI: uri, +- Range: protocol.Range{Start: posn, End: posn}, +- }) +- }) - --// typeSwitchImplicits returns all the implicit type switch objects that --// correspond to the leaf *ast.Ident. It also returns the original type --// associated with the identifier (outside of a case clause). --func typeSwitchImplicits(info *types.Info, path []ast.Node) ([]types.Object, types.Type) { -- ident, _ := path[0].(*ast.Ident) -- if ident == nil { -- return nil, nil -- } +- // The /pkg/PATH&view=... handler shows package documentation for PATH. +- webMux.Handle("/pkg/", http.StripPrefix("/pkg/", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { +- ctx := req.Context() +- if err := req.ParseForm(); err != nil { +- http.Error(w, err.Error(), http.StatusBadRequest) +- return +- } - -- var ( -- ts *ast.TypeSwitchStmt -- assign *ast.AssignStmt -- cc *ast.CaseClause -- obj = info.ObjectOf(ident) -- ) +- // Get snapshot of specified view. +- view, err := s.session.View(req.Form.Get("view")) +- if err != nil { +- http.Error(w, err.Error(), http.StatusBadRequest) +- return +- } +- snapshot, release, err := view.Snapshot() +- if err != nil { +- http.Error(w, err.Error(), http.StatusInternalServerError) +- return +- } +- defer release() - -- // Walk our ancestors to determine if our leaf ident refers to a -- // type switch variable, e.g. the "a" from "switch a := b.(type)". --Outer: -- for i := 1; i < len(path); i++ { -- switch n := path[i].(type) { -- case *ast.AssignStmt: -- // Check if ident is the "a" in "a := foo.(type)". The "a" in -- // this case has no types.Object, so check for ident equality. -- if len(n.Lhs) == 1 && n.Lhs[0] == ident { -- assign = n -- } -- case *ast.CaseClause: -- // Check if ident is a use of "a" within a case clause. Each -- // case clause implicitly maps "a" to a different types.Object, -- // so check if ident's object is the case clause's implicit -- // object. -- if obj != nil && info.Implicits[n] == obj { -- cc = n -- } -- case *ast.TypeSwitchStmt: -- // Look for the type switch that owns our previously found -- // *ast.AssignStmt or *ast.CaseClause. -- if n.Assign == assign { -- ts = n -- break Outer +- // Find package by path. +- var found *metadata.Package +- for _, mp := range snapshot.MetadataGraph().Packages { +- if string(mp.PkgPath) == req.URL.Path && mp.ForTest == "" { +- found = mp +- break - } +- } +- if found == nil { +- // TODO(adonovan): what should we do for external test packages? +- http.Error(w, "package not found", http.StatusNotFound) +- return +- } - -- for _, stmt := range n.Body.List { -- if stmt == cc { -- ts = n -- break Outer -- } -- } +- // Type-check the package and render its documentation. +- pkgs, err := snapshot.TypeCheck(ctx, found.ID) +- if err != nil { +- http.Error(w, err.Error(), http.StatusInternalServerError) +- return - } -- } -- if ts == nil { -- return nil, nil -- } -- // Our leaf ident refers to a type switch variable. Fan out to the -- // type switch's implicit case clause objects. -- var objs []types.Object -- for _, cc := range ts.Body.List { -- if ccObj := info.Implicits[cc]; ccObj != nil { -- objs = append(objs, ccObj) +- pkgURL := func(path golang.PackagePath, fragment string) protocol.URI { +- return web.pkgURL(view, path, fragment) - } -- } -- // The right-hand side of a type switch should only have one -- // element, and we need to track its type in order to generate -- // hover information for implicit type switch variables. -- var typ types.Type -- if assign, ok := ts.Assign.(*ast.AssignStmt); ok && len(assign.Rhs) == 1 { -- if rhs := assign.Rhs[0].(*ast.TypeAssertExpr); ok { -- typ = info.TypeOf(rhs.X) +- content, err := golang.RenderPackageDoc(pkgs[0], web.openURL, pkgURL) +- if err != nil { +- http.Error(w, err.Error(), http.StatusInternalServerError) +- return - } -- } -- return objs, typ --} -diff -urN a/gopls/internal/lsp/source/identifier_test.go b/gopls/internal/lsp/source/identifier_test.go ---- a/gopls/internal/lsp/source/identifier_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/identifier_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,103 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +- w.Write(content) +- }))) - --package source -- --import ( -- "bytes" -- "go/ast" -- "go/parser" -- "go/token" -- "go/types" -- "testing" --) +- return web, nil +-} - --func TestSearchForEnclosing(t *testing.T) { -- tests := []struct { -- desc string -- // For convenience, consider the first occurrence of the identifier "X" in -- // src. -- src string -- // By convention, "" means no type found. -- wantTypeName string -- }{ -- { -- // TODO(rFindley): is this correct, or do we want to resolve I2 here? -- desc: "embedded interface in interface", -- src: `package a; var y = i1.X; type i1 interface {I2}; type I2 interface{X()}`, -- wantTypeName: "", -- }, -- { -- desc: "embedded interface in struct", -- src: `package a; var y = t.X; type t struct {I}; type I interface{X()}`, -- wantTypeName: "I", -- }, -- { -- desc: "double embedding", -- src: `package a; var y = t1.X; type t1 struct {t2}; type t2 struct {I}; type I interface{X()}`, -- wantTypeName: "I", -- }, -- } +-// assets holds our static web server content. +-// +-//go:embed assets/* +-var assets embed.FS - -- for _, test := range tests { -- test := test -- t.Run(test.desc, func(t *testing.T) { -- fset := token.NewFileSet() -- file, err := parser.ParseFile(fset, "a.go", test.src, parser.AllErrors) -- if err != nil { -- t.Fatal(err) -- } -- column := 1 + bytes.IndexRune([]byte(test.src), 'X') -- pos := posAt(1, column, fset, "a.go") -- path := pathEnclosingObjNode(file, pos) -- if path == nil { -- t.Fatalf("no ident found at (1, %d)", column) -- } -- info := newInfo() -- if _, err = (*types.Config)(nil).Check("p", fset, []*ast.File{file}, info); err != nil { -- t.Fatal(err) -- } -- obj := searchForEnclosing(info, path) -- if obj == nil { -- if test.wantTypeName != "" { -- t.Errorf("searchForEnclosing(...) = , want %q", test.wantTypeName) -- } -- return -- } -- if got := obj.Name(); got != test.wantTypeName { -- t.Errorf("searchForEnclosing(...) = %q, want %q", got, test.wantTypeName) -- } -- }) -- } +-// openURL returns an /open URL that, when visited, causes the client +-// editor to open the specified file/line/column (in 1-based UTF-8 +-// coordinates). +-// +-// (Rendering may generate hundreds of positions across files of many +-// packages, so don't convert to LSP coordinates yet: wait until the +-// URL is opened.) +-func (w *web) openURL(filename string, line, col8 int) protocol.URI { +- return w.url( +- "open", +- fmt.Sprintf("file=%s&line=%d&col=%d", url.QueryEscape(filename), line, col8), +- "") -} - --// posAt returns the token.Pos corresponding to the 1-based (line, column) --// coordinates in the file fname of fset. --func posAt(line, column int, fset *token.FileSet, fname string) token.Pos { -- var tok *token.File -- fset.Iterate(func(tf *token.File) bool { -- if tf.Name() == fname { -- tok = tf -- return false -- } -- return true -- }) -- if tok == nil { -- return token.NoPos -- } -- start := tok.LineStart(line) -- return start + token.Pos(column-1) +-// pkgURL returns a /pkg URL for the documentation of the specified package. +-// The optional fragment must be of the form "Println" or "Buffer.WriteString". +-func (w *web) pkgURL(v *cache.View, path golang.PackagePath, fragment string) protocol.URI { +- return w.url( +- "pkg/"+string(path), +- "view="+url.QueryEscape(v.ID()), +- fragment) -} - --// newInfo returns a types.Info with all maps populated. --func newInfo() *types.Info { -- return &types.Info{ -- Types: make(map[ast.Expr]types.TypeAndValue), -- Defs: make(map[*ast.Ident]types.Object), -- Uses: make(map[*ast.Ident]types.Object), -- Implicits: make(map[ast.Node]types.Object), -- Selections: make(map[*ast.SelectorExpr]*types.Selection), -- Scopes: make(map[ast.Node]*types.Scope), -- } +-// url returns a URL by joining a relative path, an (encoded) query, +-// and an (unencoded) fragment onto the authenticated base URL of the +-// web server. +-func (w *web) url(path, query, fragment string) protocol.URI { +- url2 := w.addr +- url2.Path = paths.Join(url2.Path, strings.TrimPrefix(path, "/")) +- url2.RawQuery = query +- url2.Fragment = fragment +- return protocol.URI(url2.String()) -} -diff -urN a/gopls/internal/lsp/source/implementation.go b/gopls/internal/lsp/source/implementation.go ---- a/gopls/internal/lsp/source/implementation.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/implementation.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,495 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/server/signature_help.go b/gopls/internal/server/signature_help.go +--- a/gopls/internal/server/signature_help.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/signature_help.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,40 +0,0 @@ +-// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package source +-package server - -import ( - "context" -- "errors" -- "fmt" -- "go/ast" -- "go/token" -- "go/types" -- "reflect" -- "sort" -- "strings" -- "sync" - -- "golang.org/x/sync/errgroup" -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/gopls/internal/lsp/source/methodsets" -- "golang.org/x/tools/gopls/internal/span" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/tag" -) - --// This file defines the new implementation of the 'implementation' --// operator that does not require type-checker data structures for an --// unbounded number of packages. --// --// TODO(adonovan): --// - Audit to ensure robustness in face of type errors. --// - Eliminate false positives due to 'tricky' cases of the global algorithm. --// - Ensure we have test coverage of: --// type aliases --// nil, PkgName, Builtin (all errors) --// any (empty result) --// method of unnamed interface type (e.g. var x interface { f() }) --// (the global algorithm may find implementations of this type --// but will not include it in the index.) -- --// Implementation returns a new sorted array of locations of --// declarations of types that implement (or are implemented by) the --// type referred to at the given position. --// --// If the position denotes a method, the computation is applied to its --// receiver type and then its corresponding methods are returned. --func Implementation(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position) ([]protocol.Location, error) { -- ctx, done := event.Start(ctx, "source.Implementation") +-func (s *server) SignatureHelp(ctx context.Context, params *protocol.SignatureHelpParams) (*protocol.SignatureHelp, error) { +- ctx, done := event.Start(ctx, "lsp.Server.signatureHelp", tag.URI.Of(params.TextDocument.URI)) - defer done() - -- locs, err := implementations(ctx, snapshot, f, pp) +- fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI) - if err != nil { - return nil, err - } +- defer release() - -- // Sort and de-duplicate locations. -- sort.Slice(locs, func(i, j int) bool { -- return protocol.CompareLocation(locs[i], locs[j]) < 0 -- }) -- out := locs[:0] -- for _, loc := range locs { -- if len(out) == 0 || out[len(out)-1] != loc { -- out = append(out, loc) -- } +- if snapshot.FileKind(fh) != file.Go { +- return nil, nil // empty result - } -- locs = out - -- return locs, nil +- info, activeParameter, err := golang.SignatureHelp(ctx, snapshot, fh, params.Position) +- if err != nil { +- event.Error(ctx, "no signature help", err, tag.Position.Of(params.Position)) +- return nil, nil // sic? There could be many reasons for failure. +- } +- return &protocol.SignatureHelp{ +- Signatures: []protocol.SignatureInformation{*info}, +- ActiveParameter: uint32(activeParameter), +- }, nil -} +diff -urN a/gopls/internal/server/symbols.go b/gopls/internal/server/symbols.go +--- a/gopls/internal/server/symbols.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/symbols.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,62 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func implementations(ctx context.Context, snapshot Snapshot, fh FileHandle, pp protocol.Position) ([]protocol.Location, error) { -- obj, pkg, err := implementsObj(ctx, snapshot, fh.URI(), pp) +-package server +- +-import ( +- "context" +- +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/template" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/tag" +-) +- +-func (s *server) DocumentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) ([]any, error) { +- ctx, done := event.Start(ctx, "lsp.Server.documentSymbol", tag.URI.Of(params.TextDocument.URI)) +- defer done() +- +- fh, snapshot, release, err := s.fileOf(ctx, params.TextDocument.URI) - if err != nil { - return nil, err - } +- defer release() - -- var localPkgs []Package -- if obj.Pos().IsValid() { // no local package for error or error.Error -- declPosn := safetoken.StartPosition(pkg.FileSet(), obj.Pos()) -- // Type-check the declaring package (incl. variants) for use -- // by the "local" search, which uses type information to -- // enumerate all types within the package that satisfy the -- // query type, even those defined local to a function. -- declURI := span.URIFromPath(declPosn.Filename) -- declMetas, err := snapshot.MetadataForFile(ctx, declURI) -- if err != nil { -- return nil, err -- } -- RemoveIntermediateTestVariants(&declMetas) -- if len(declMetas) == 0 { -- return nil, fmt.Errorf("no packages for file %s", declURI) -- } -- ids := make([]PackageID, len(declMetas)) -- for i, m := range declMetas { -- ids[i] = m.ID +- var docSymbols []protocol.DocumentSymbol +- switch snapshot.FileKind(fh) { +- case file.Tmpl: +- docSymbols, err = template.DocumentSymbols(snapshot, fh) +- case file.Go: +- docSymbols, err = golang.DocumentSymbols(ctx, snapshot, fh) +- default: +- return nil, nil // empty result +- } +- if err != nil { +- event.Error(ctx, "DocumentSymbols failed", err) +- return nil, nil // empty result +- } +- // Convert the symbols to an interface array. +- // TODO: Remove this once the lsp deprecates SymbolInformation. +- symbols := make([]any, len(docSymbols)) +- for i, s := range docSymbols { +- if snapshot.Options().HierarchicalDocumentSymbolSupport { +- symbols[i] = s +- continue - } -- localPkgs, err = snapshot.TypeCheck(ctx, ids...) -- if err != nil { -- return nil, err +- // If the client does not support hierarchical document symbols, then +- // we need to be backwards compatible for now and return SymbolInformation. +- symbols[i] = protocol.SymbolInformation{ +- Name: s.Name, +- Kind: s.Kind, +- Deprecated: s.Deprecated, +- Location: protocol.Location{ +- URI: params.TextDocument.URI, +- Range: s.Range, +- }, - } - } +- return symbols, nil +-} +diff -urN a/gopls/internal/server/text_synchronization.go b/gopls/internal/server/text_synchronization.go +--- a/gopls/internal/server/text_synchronization.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/text_synchronization.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,409 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- // Is the selected identifier a type name or method? -- // (For methods, report the corresponding method names.) -- var queryType types.Type -- var queryMethodID string -- switch obj := obj.(type) { -- case *types.TypeName: -- queryType = obj.Type() -- case *types.Func: -- // For methods, use the receiver type, which may be anonymous. -- if recv := obj.Type().(*types.Signature).Recv(); recv != nil { -- queryType = recv.Type() -- queryMethodID = obj.Id() -- } -- } -- if queryType == nil { -- return nil, bug.Errorf("%s is not a type or method", obj.Name()) // should have been handled by implementsObj -- } +-package server - -- // Compute the method-set fingerprint used as a key to the global search. -- key, hasMethods := methodsets.KeyOf(queryType) -- if !hasMethods { -- // A type with no methods yields an empty result. -- // (No point reporting that every type satisfies 'any'.) -- return nil, nil +-import ( +- "bytes" +- "context" +- "errors" +- "fmt" +- "path/filepath" +- "strings" +- "sync" +- +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/event/tag" +- "golang.org/x/tools/internal/jsonrpc2" +- "golang.org/x/tools/internal/xcontext" +-) +- +-// ModificationSource identifies the origin of a change. +-type ModificationSource int +- +-const ( +- // FromDidOpen is from a didOpen notification. +- FromDidOpen = ModificationSource(iota) +- +- // FromDidChange is from a didChange notification. +- FromDidChange +- +- // FromDidChangeWatchedFiles is from didChangeWatchedFiles notification. +- FromDidChangeWatchedFiles +- +- // FromDidSave is from a didSave notification. +- FromDidSave +- +- // FromDidClose is from a didClose notification. +- FromDidClose +- +- // FromDidChangeConfiguration is from a didChangeConfiguration notification. +- FromDidChangeConfiguration +- +- // FromRegenerateCgo refers to file modifications caused by regenerating +- // the cgo sources for the workspace. +- FromRegenerateCgo +- +- // FromInitialWorkspaceLoad refers to the loading of all packages in the +- // workspace when the view is first created. +- FromInitialWorkspaceLoad +- +- // FromCheckUpgrades refers to state changes resulting from the CheckUpgrades +- // command, which queries module upgrades. +- FromCheckUpgrades +- +- // FromResetGoModDiagnostics refers to state changes resulting from the +- // ResetGoModDiagnostics command. +- FromResetGoModDiagnostics +- +- // FromToggleGCDetails refers to state changes resulting from toggling +- // gc_details on or off for a package. +- FromToggleGCDetails +-) +- +-func (m ModificationSource) String() string { +- switch m { +- case FromDidOpen: +- return "opened files" +- case FromDidChange: +- return "changed files" +- case FromDidChangeWatchedFiles: +- return "files changed on disk" +- case FromDidSave: +- return "saved files" +- case FromDidClose: +- return "close files" +- case FromRegenerateCgo: +- return "regenerate cgo" +- case FromInitialWorkspaceLoad: +- return "initial workspace load" +- case FromCheckUpgrades: +- return "from check upgrades" +- case FromResetGoModDiagnostics: +- return "from resetting go.mod diagnostics" +- default: +- return "unknown file modification" - } +-} - -- // The global search needs to look at every package in the -- // forward transitive closure of the workspace; see package -- // ./methodsets. +-func (s *server) DidOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error { +- ctx, done := event.Start(ctx, "lsp.Server.didOpen", tag.URI.Of(params.TextDocument.URI)) +- defer done() +- +- uri := params.TextDocument.URI +- // There may not be any matching view in the current session. If that's +- // the case, try creating a new view based on the opened file path. - // -- // For now we do all the type checking before beginning the search. -- // TODO(adonovan): opt: search in parallel topological order -- // so that we can overlap index lookup with typechecking. -- // I suspect a number of algorithms on the result of TypeCheck could -- // be optimized by being applied as soon as each package is available. -- globalMetas, err := snapshot.AllMetadata(ctx) -- if err != nil { -- return nil, err +- // TODO(golang/go#57979): revisit creating a folder here. We should separate +- // the logic for managing folders from the logic for managing views. But it +- // does make sense to ensure at least one workspace folder the first time a +- // file is opened, and we can't do that inside didModifyFiles because we +- // don't want to request configuration while holding a lock. +- if len(s.session.Views()) == 0 { +- dir := filepath.Dir(uri.Path()) +- s.addFolders(ctx, []protocol.WorkspaceFolder{{ +- URI: string(protocol.URIFromPath(dir)), +- Name: filepath.Base(dir), +- }}) - } -- RemoveIntermediateTestVariants(&globalMetas) -- globalIDs := make([]PackageID, 0, len(globalMetas)) +- return s.didModifyFiles(ctx, []file.Modification{{ +- URI: uri, +- Action: file.Open, +- Version: params.TextDocument.Version, +- Text: []byte(params.TextDocument.Text), +- LanguageID: params.TextDocument.LanguageID, +- }}, FromDidOpen) +-} - -- var pkgPath PackagePath -- if obj.Pkg() != nil { // nil for error -- pkgPath = PackagePath(obj.Pkg().Path()) -- } -- for _, m := range globalMetas { -- if m.PkgPath == pkgPath { -- continue // declaring package is handled by local implementation -- } -- globalIDs = append(globalIDs, m.ID) -- } -- indexes, err := snapshot.MethodSets(ctx, globalIDs...) -- if err != nil { -- return nil, fmt.Errorf("querying method sets: %v", err) -- } +-func (s *server) DidChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error { +- ctx, done := event.Start(ctx, "lsp.Server.didChange", tag.URI.Of(params.TextDocument.URI)) +- defer done() - -- // Search local and global packages in parallel. -- var ( -- group errgroup.Group -- locsMu sync.Mutex -- locs []protocol.Location -- ) -- // local search -- for _, localPkg := range localPkgs { -- localPkg := localPkg -- group.Go(func() error { -- localLocs, err := localImplementations(ctx, snapshot, localPkg, queryType, queryMethodID) -- if err != nil { -- return err -- } -- locsMu.Lock() -- locs = append(locs, localLocs...) -- locsMu.Unlock() -- return nil -- }) +- uri := params.TextDocument.URI +- text, err := s.changedText(ctx, uri, params.ContentChanges) +- if err != nil { +- return err - } -- // global search -- for _, index := range indexes { -- index := index -- group.Go(func() error { -- for _, res := range index.Search(key, queryMethodID) { -- loc := res.Location -- // Map offsets to protocol.Locations in parallel (may involve I/O). -- group.Go(func() error { -- ploc, err := offsetToLocation(ctx, snapshot, loc.Filename, loc.Start, loc.End) -- if err != nil { -- return err -- } -- locsMu.Lock() -- locs = append(locs, ploc) -- locsMu.Unlock() -- return nil -- }) -- } -- return nil -- }) +- c := file.Modification{ +- URI: uri, +- Action: file.Change, +- Version: params.TextDocument.Version, +- Text: text, - } -- if err := group.Wait(); err != nil { -- return nil, err +- if err := s.didModifyFiles(ctx, []file.Modification{c}, FromDidChange); err != nil { +- return err - } -- -- return locs, nil +- return s.warnAboutModifyingGeneratedFiles(ctx, uri) -} - --// offsetToLocation converts an offset-based position to a protocol.Location, --// which requires reading the file. --func offsetToLocation(ctx context.Context, snapshot Snapshot, filename string, start, end int) (protocol.Location, error) { -- uri := span.URIFromPath(filename) -- fh, err := snapshot.ReadFile(ctx, uri) -- if err != nil { -- return protocol.Location{}, err // cancelled, perhaps +-// warnAboutModifyingGeneratedFiles shows a warning if a user tries to edit a +-// generated file for the first time. +-func (s *server) warnAboutModifyingGeneratedFiles(ctx context.Context, uri protocol.DocumentURI) error { +- s.changedFilesMu.Lock() +- _, ok := s.changedFiles[uri] +- if !ok { +- s.changedFiles[uri] = struct{}{} - } -- content, err := fh.Content() -- if err != nil { -- return protocol.Location{}, err // nonexistent or deleted ("can't happen") +- s.changedFilesMu.Unlock() +- +- // This file has already been edited before. +- if ok { +- return nil - } -- m := protocol.NewMapper(uri, content) -- return m.OffsetLocation(start, end) --} - --// implementsObj returns the object to query for implementations, which is a --// type name or method. --// --// The returned Package is the narrowest package containing ppos, which is the --// package using the resulting obj but not necessarily the declaring package. --func implementsObj(ctx context.Context, snapshot Snapshot, uri span.URI, ppos protocol.Position) (types.Object, Package, error) { -- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, uri) +- // Ideally, we should be able to specify that a generated file should +- // be opened as read-only. Tell the user that they should not be +- // editing a generated file. +- snapshot, release, err := s.session.SnapshotOf(ctx, uri) - if err != nil { -- return nil, nil, err +- return err - } -- pos, err := pgf.PositionPos(ppos) -- if err != nil { -- return nil, nil, err +- isGenerated := golang.IsGenerated(ctx, snapshot, uri) +- release() +- +- if isGenerated { +- msg := fmt.Sprintf("Do not edit this file! %s is a generated file.", uri.Path()) +- showMessage(ctx, s.client, protocol.Warning, msg) - } +- return nil +-} - -- // This function inherits the limitation of its predecessor in -- // requiring the selection to be an identifier (of a type or -- // method). But there's no fundamental reason why one could -- // not pose this query about any selected piece of syntax that -- // has a type and thus a method set. -- // (If LSP was more thorough about passing text selections as -- // intervals to queries, you could ask about the method set of a -- // subexpression such as x.f().) +-func (s *server) DidChangeWatchedFiles(ctx context.Context, params *protocol.DidChangeWatchedFilesParams) error { +- ctx, done := event.Start(ctx, "lsp.Server.didChangeWatchedFiles") +- defer done() - -- // TODO(adonovan): simplify: use objectsAt? -- path := pathEnclosingObjNode(pgf.File, pos) -- if path == nil { -- return nil, nil, ErrNoIdentFound -- } -- id, ok := path[0].(*ast.Ident) -- if !ok { -- return nil, nil, ErrNoIdentFound +- var modifications []file.Modification +- for _, change := range params.Changes { +- action := changeTypeToFileAction(change.Type) +- modifications = append(modifications, file.Modification{ +- URI: change.URI, +- Action: action, +- OnDisk: true, +- }) - } +- return s.didModifyFiles(ctx, modifications, FromDidChangeWatchedFiles) +-} - -- // Is the object a type or method? Reject other kinds. -- obj := pkg.GetTypesInfo().Uses[id] -- if obj == nil { -- // Check uses first (unlike ObjectOf) so that T in -- // struct{T} is treated as a reference to a type, -- // not a declaration of a field. -- obj = pkg.GetTypesInfo().Defs[id] +-func (s *server) DidSave(ctx context.Context, params *protocol.DidSaveTextDocumentParams) error { +- ctx, done := event.Start(ctx, "lsp.Server.didSave", tag.URI.Of(params.TextDocument.URI)) +- defer done() +- +- c := file.Modification{ +- URI: params.TextDocument.URI, +- Action: file.Save, - } -- switch obj := obj.(type) { -- case *types.TypeName: -- // ok -- case *types.Func: -- if obj.Type().(*types.Signature).Recv() == nil { -- return nil, nil, fmt.Errorf("%s is a function, not a method", id.Name) -- } -- case nil: -- return nil, nil, fmt.Errorf("%s denotes unknown object", id.Name) -- default: -- // e.g. *types.Var -> "var". -- kind := strings.ToLower(strings.TrimPrefix(reflect.TypeOf(obj).String(), "*types.")) -- return nil, nil, fmt.Errorf("%s is a %s, not a type", id.Name, kind) +- if params.Text != nil { +- c.Text = []byte(*params.Text) - } -- -- return obj, pkg, nil +- return s.didModifyFiles(ctx, []file.Modification{c}, FromDidSave) -} - --// localImplementations searches within pkg for declarations of all --// types that are assignable to/from the query type, and returns a new --// unordered array of their locations. --// --// If methodID is non-empty, the function instead returns the location --// of each type's method (if any) of that ID. --// --// ("Local" refers to the search within the same package, but this --// function's results may include type declarations that are local to --// a function body. The global search index excludes such types --// because reliably naming such types is hard.) --func localImplementations(ctx context.Context, snapshot Snapshot, pkg Package, queryType types.Type, methodID string) ([]protocol.Location, error) { -- queryType = methodsets.EnsurePointer(queryType) -- -- // Scan through all type declarations in the syntax. -- var locs []protocol.Location -- var methodLocs []methodsets.Location -- for _, pgf := range pkg.CompiledGoFiles() { -- ast.Inspect(pgf.File, func(n ast.Node) bool { -- spec, ok := n.(*ast.TypeSpec) -- if !ok { -- return true // not a type declaration -- } -- def := pkg.GetTypesInfo().Defs[spec.Name] -- if def == nil { -- return true // "can't happen" for types -- } -- if def.(*types.TypeName).IsAlias() { -- return true // skip type aliases to avoid duplicate reporting -- } -- candidateType := methodsets.EnsurePointer(def.Type()) -- -- // The historical behavior enshrined by this -- // function rejects cases where both are -- // (nontrivial) interface types? -- // That seems like useful information. -- // TODO(adonovan): UX: report I/I pairs too? -- // The same question appears in the global algorithm (methodsets). -- if !concreteImplementsIntf(candidateType, queryType) { -- return true // not assignable -- } +-func (s *server) DidClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error { +- ctx, done := event.Start(ctx, "lsp.Server.didClose", tag.URI.Of(params.TextDocument.URI)) +- defer done() - -- // Ignore types with empty method sets. -- // (No point reporting that every type satisfies 'any'.) -- mset := types.NewMethodSet(candidateType) -- if mset.Len() == 0 { -- return true -- } +- return s.didModifyFiles(ctx, []file.Modification{ +- { +- URI: params.TextDocument.URI, +- Action: file.Close, +- Version: -1, +- Text: nil, +- }, +- }, FromDidClose) +-} - -- if methodID == "" { -- // Found matching type. -- locs = append(locs, mustLocation(pgf, spec.Name)) -- return true -- } +-func (s *server) didModifyFiles(ctx context.Context, modifications []file.Modification, cause ModificationSource) error { +- // wg guards two conditions: +- // 1. didModifyFiles is complete +- // 2. the goroutine diagnosing changes on behalf of didModifyFiles is +- // complete, if it was started +- // +- // Both conditions must be satisfied for the purpose of testing: we don't +- // want to observe the completion of change processing until we have received +- // all diagnostics as well as all server->client notifications done on behalf +- // of this function. +- var wg sync.WaitGroup +- wg.Add(1) +- defer wg.Done() - -- // Find corresponding method. -- // -- // We can't use LookupFieldOrMethod because it requires -- // the methodID's types.Package, which we don't know. -- // We could recursively search pkg.Imports for it, -- // but it's easier to walk the method set. -- for i := 0; i < mset.Len(); i++ { -- method := mset.At(i).Obj() -- if method.Id() == methodID { -- posn := safetoken.StartPosition(pkg.FileSet(), method.Pos()) -- methodLocs = append(methodLocs, methodsets.Location{ -- Filename: posn.Filename, -- Start: posn.Offset, -- End: posn.Offset + len(method.Name()), -- }) -- break -- } -- } -- return true -- }) +- if s.Options().VerboseWorkDoneProgress { +- work := s.progress.Start(ctx, DiagnosticWorkTitle(cause), "Calculating file diagnostics...", nil, nil) +- go func() { +- wg.Wait() +- work.End(ctx, "Done.") +- }() +- } +- +- s.stateMu.Lock() +- if s.state >= serverShutDown { +- // This state check does not prevent races below, and exists only to +- // produce a better error message. The actual race to the cache should be +- // guarded by Session.viewMu. +- s.stateMu.Unlock() +- return errors.New("server is shut down") - } +- s.stateMu.Unlock() - -- // Finally convert method positions to protocol form by reading the files. -- for _, mloc := range methodLocs { -- loc, err := offsetToLocation(ctx, snapshot, mloc.Filename, mloc.Start, mloc.End) -- if err != nil { -- return nil, err -- } -- locs = append(locs, loc) +- // If the set of changes included directories, expand those directories +- // to their files. +- modifications = s.session.ExpandModificationsToDirectories(ctx, modifications) +- +- viewsToDiagnose, err := s.session.DidModifyFiles(ctx, modifications) +- if err != nil { +- return err - } - -- // Special case: for types that satisfy error, report builtin.go (see #59527). -- if types.Implements(queryType, errorInterfaceType) { -- loc, err := errorLocation(ctx, snapshot) -- if err != nil { -- return nil, err -- } -- locs = append(locs, loc) +- // golang/go#50267: diagnostics should be re-sent after each change. +- for _, mod := range modifications { +- s.mustPublishDiagnostics(mod.URI) - } - -- return locs, nil --} +- modCtx, modID := s.needsDiagnosis(ctx, viewsToDiagnose) - --var errorInterfaceType = types.Universe.Lookup("error").Type().Underlying().(*types.Interface) +- wg.Add(1) +- go func() { +- s.diagnoseChangedViews(modCtx, modID, viewsToDiagnose, cause) +- wg.Done() +- }() - --// errorLocation returns the location of the 'error' type in builtin.go. --func errorLocation(ctx context.Context, snapshot Snapshot) (protocol.Location, error) { -- pgf, err := snapshot.BuiltinFile(ctx) -- if err != nil { -- return protocol.Location{}, err +- // After any file modifications, we need to update our watched files, +- // in case something changed. Compute the new set of directories to watch, +- // and if it differs from the current set, send updated registrations. +- return s.updateWatchedDirectories(ctx) +-} +- +-// needsDiagnosis records the given views as needing diagnosis, returning the +-// context and modification id to use for said diagnosis. +-// +-// Only the keys of viewsToDiagnose are used; the changed files are irrelevant. +-func (s *server) needsDiagnosis(ctx context.Context, viewsToDiagnose map[*cache.View][]protocol.DocumentURI) (context.Context, uint64) { +- s.modificationMu.Lock() +- defer s.modificationMu.Unlock() +- if s.cancelPrevDiagnostics != nil { +- s.cancelPrevDiagnostics() - } -- for _, decl := range pgf.File.Decls { -- if decl, ok := decl.(*ast.GenDecl); ok { -- for _, spec := range decl.Specs { -- if spec, ok := spec.(*ast.TypeSpec); ok && spec.Name.Name == "error" { -- return pgf.NodeLocation(spec.Name) -- } -- } +- modCtx := xcontext.Detach(ctx) +- modCtx, s.cancelPrevDiagnostics = context.WithCancel(modCtx) +- s.lastModificationID++ +- modID := s.lastModificationID +- +- for v := range viewsToDiagnose { +- if needs, ok := s.viewsToDiagnose[v]; !ok || needs < modID { +- s.viewsToDiagnose[v] = modID - } - } -- return protocol.Location{}, fmt.Errorf("built-in error type not found") +- return modCtx, modID -} - --// concreteImplementsIntf returns true if a is an interface type implemented by --// concrete type b, or vice versa. --func concreteImplementsIntf(a, b types.Type) bool { -- aIsIntf, bIsIntf := types.IsInterface(a), types.IsInterface(b) +-// DiagnosticWorkTitle returns the title of the diagnostic work resulting from a +-// file change originating from the given cause. +-func DiagnosticWorkTitle(cause ModificationSource) string { +- return fmt.Sprintf("diagnosing %v", cause) +-} - -- // Make sure exactly one is an interface type. -- if aIsIntf == bIsIntf { -- return false +-func (s *server) changedText(ctx context.Context, uri protocol.DocumentURI, changes []protocol.TextDocumentContentChangeEvent) ([]byte, error) { +- if len(changes) == 0 { +- return nil, fmt.Errorf("%w: no content changes provided", jsonrpc2.ErrInternal) - } - -- // Rearrange if needed so "a" is the concrete type. -- if aIsIntf { -- a, b = b, a +- // Check if the client sent the full content of the file. +- // We accept a full content change even if the server expected incremental changes. +- if len(changes) == 1 && changes[0].Range == nil && changes[0].RangeLength == 0 { +- changeFull.Inc() +- return []byte(changes[0].Text), nil - } -- -- // TODO(adonovan): this should really use GenericAssignableTo -- // to report (e.g.) "ArrayList[T] implements List[T]", but -- // GenericAssignableTo doesn't work correctly on pointers to -- // generic named types. Thus the legacy implementation and the -- // "local" part of implementations fail to report generics. -- // The global algorithm based on subsets does the right thing. -- return types.AssignableTo(a, b) +- return s.applyIncrementalChanges(ctx, uri, changes) -} - --var ( -- // TODO(adonovan): why do various RPC handlers related to -- // IncomingCalls return (nil, nil) on the protocol in response -- // to this error? That seems like a violation of the protocol. -- // Is it perhaps a workaround for VSCode behavior? -- errNoObjectFound = errors.New("no object found") --) -- --// pathEnclosingObjNode returns the AST path to the object-defining --// node associated with pos. "Object-defining" means either an --// *ast.Ident mapped directly to a types.Object or an ast.Node mapped --// implicitly to a types.Object. --func pathEnclosingObjNode(f *ast.File, pos token.Pos) []ast.Node { -- var ( -- path []ast.Node -- found bool -- ) +-func (s *server) applyIncrementalChanges(ctx context.Context, uri protocol.DocumentURI, changes []protocol.TextDocumentContentChangeEvent) ([]byte, error) { +- fh, err := s.session.ReadFile(ctx, uri) +- if err != nil { +- return nil, err +- } +- content, err := fh.Content() +- if err != nil { +- return nil, fmt.Errorf("%w: file not found (%v)", jsonrpc2.ErrInternal, err) +- } +- for i, change := range changes { +- // TODO(adonovan): refactor to use diff.Apply, which is robust w.r.t. +- // out-of-order or overlapping changes---and much more efficient. - -- ast.Inspect(f, func(n ast.Node) bool { -- if found { -- return false +- // Make sure to update mapper along with the content. +- m := protocol.NewMapper(uri, content) +- if change.Range == nil { +- return nil, fmt.Errorf("%w: unexpected nil range for change", jsonrpc2.ErrInternal) - } -- -- if n == nil { -- path = path[:len(path)-1] -- return false +- start, end, err := m.RangeOffsets(*change.Range) +- if err != nil { +- return nil, err - } +- if end < start { +- return nil, fmt.Errorf("%w: invalid range for content change", jsonrpc2.ErrInternal) +- } +- var buf bytes.Buffer +- buf.Write(content[:start]) +- buf.WriteString(change.Text) +- buf.Write(content[end:]) +- content = buf.Bytes() +- if i == 0 { // only look at the first change if there are seversl +- // TODO(pjw): understand multi-change) +- s.checkEfficacy(fh.URI(), fh.Version(), change) +- } +- } +- return content, nil +-} - -- path = append(path, n) -- -- switch n := n.(type) { -- case *ast.Ident: -- // Include the position directly after identifier. This handles -- // the common case where the cursor is right after the -- // identifier the user is currently typing. Previously we -- // handled this by calling astutil.PathEnclosingInterval twice, -- // once for "pos" and once for "pos-1". -- found = n.Pos() <= pos && pos <= n.End() -- case *ast.ImportSpec: -- if n.Path.Pos() <= pos && pos < n.Path.End() { -- found = true -- // If import spec has a name, add name to path even though -- // position isn't in the name. -- if n.Name != nil { -- path = append(path, n.Name) -- } +-// increment counters if any of the completions look like there were used +-func (s *server) checkEfficacy(uri protocol.DocumentURI, version int32, change protocol.TextDocumentContentChangePartial) { +- s.efficacyMu.Lock() +- defer s.efficacyMu.Unlock() +- if s.efficacyURI != uri { +- return +- } +- // gopls increments the version, the test client does not +- if version != s.efficacyVersion && version != s.efficacyVersion+1 { +- return +- } +- // does any change at pos match a proposed completion item? +- for _, item := range s.efficacyItems { +- if item.TextEdit == nil { +- continue +- } +- if item.TextEdit.Range.Start == change.Range.Start { +- // the change and the proposed completion start at the same +- if change.RangeLength == 0 && len(change.Text) == 1 { +- // a single character added it does not count as a completion +- continue - } -- case *ast.StarExpr: -- // Follow star expressions to the inner identifier. -- if pos == n.Star { -- pos = n.X.Pos() +- ix := strings.Index(item.TextEdit.NewText, "$") +- if ix < 0 && strings.HasPrefix(change.Text, item.TextEdit.NewText) { +- // not a snippet, suggested completion is a prefix of the change +- complUsed.Inc() +- return +- } +- if ix > 1 && strings.HasPrefix(change.Text, item.TextEdit.NewText[:ix]) { +- // a snippet, suggested completion up to $ marker is a prefix of the change +- complUsed.Inc() +- return - } - } -- -- return !found -- }) -- -- if len(path) == 0 { -- return nil - } +- complUnused.Inc() +-} - -- // Reverse path so leaf is first element. -- for i := 0; i < len(path)/2; i++ { -- path[i], path[len(path)-1-i] = path[len(path)-1-i], path[i] +-func changeTypeToFileAction(ct protocol.FileChangeType) file.Action { +- switch ct { +- case protocol.Changed: +- return file.Change +- case protocol.Created: +- return file.Create +- case protocol.Deleted: +- return file.Delete - } -- -- return path +- return file.UnknownAction -} -diff -urN a/gopls/internal/lsp/source/inlay_hint.go b/gopls/internal/lsp/source/inlay_hint.go ---- a/gopls/internal/lsp/source/inlay_hint.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/inlay_hint.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,394 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/server/unimplemented.go b/gopls/internal/server/unimplemented.go +--- a/gopls/internal/server/unimplemented.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/unimplemented.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,159 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package source +-package server +- +-// This file defines the LSP server methods that gopls does not currently implement. - -import ( - "context" - "fmt" -- "go/ast" -- "go/constant" -- "go/token" -- "go/types" -- "strings" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/typeparams" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/jsonrpc2" -) - --const ( -- maxLabelLength = 28 --) +-func (s *server) ColorPresentation(context.Context, *protocol.ColorPresentationParams) ([]protocol.ColorPresentation, error) { +- return nil, notImplemented("ColorPresentation") +-} - --type InlayHintFunc func(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint +-func (s *server) Declaration(context.Context, *protocol.DeclarationParams) (*protocol.Or_textDocument_declaration, error) { +- return nil, notImplemented("Declaration") +-} - --type Hint struct { -- Name string -- Doc string -- Run InlayHintFunc +-func (s *server) Diagnostic(context.Context, *string) (*string, error) { +- return nil, notImplemented("Diagnostic") -} - --const ( -- ParameterNames = "parameterNames" -- AssignVariableTypes = "assignVariableTypes" -- ConstantValues = "constantValues" -- RangeVariableTypes = "rangeVariableTypes" -- CompositeLiteralTypes = "compositeLiteralTypes" -- CompositeLiteralFieldNames = "compositeLiteralFields" -- FunctionTypeParameters = "functionTypeParameters" --) +-func (s *server) DiagnosticWorkspace(context.Context, *protocol.WorkspaceDiagnosticParams) (*protocol.WorkspaceDiagnosticReport, error) { +- return nil, notImplemented("DiagnosticWorkspace") +-} - --var AllInlayHints = map[string]*Hint{ -- AssignVariableTypes: { -- Name: AssignVariableTypes, -- Doc: "Enable/disable inlay hints for variable types in assign statements:\n```go\n\ti/* int*/, j/* int*/ := 0, len(r)-1\n```", -- Run: assignVariableTypes, -- }, -- ParameterNames: { -- Name: ParameterNames, -- Doc: "Enable/disable inlay hints for parameter names:\n```go\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)\n```", -- Run: parameterNames, -- }, -- ConstantValues: { -- Name: ConstantValues, -- Doc: "Enable/disable inlay hints for constant values:\n```go\n\tconst (\n\t\tKindNone Kind = iota/* = 0*/\n\t\tKindPrint/* = 1*/\n\t\tKindPrintf/* = 2*/\n\t\tKindErrorf/* = 3*/\n\t)\n```", -- Run: constantValues, -- }, -- RangeVariableTypes: { -- Name: RangeVariableTypes, -- Doc: "Enable/disable inlay hints for variable types in range statements:\n```go\n\tfor k/* int*/, v/* string*/ := range []string{} {\n\t\tfmt.Println(k, v)\n\t}\n```", -- Run: rangeVariableTypes, -- }, -- CompositeLiteralTypes: { -- Name: CompositeLiteralTypes, -- Doc: "Enable/disable inlay hints for composite literal types:\n```go\n\tfor _, c := range []struct {\n\t\tin, want string\n\t}{\n\t\t/*struct{ in string; want string }*/{\"Hello, world\", \"dlrow ,olleH\"},\n\t}\n```", -- Run: compositeLiteralTypes, -- }, -- CompositeLiteralFieldNames: { -- Name: CompositeLiteralFieldNames, -- Doc: "Enable/disable inlay hints for composite literal field names:\n```go\n\t{/*in: */\"Hello, world\", /*want: */\"dlrow ,olleH\"}\n```", -- Run: compositeLiteralFields, -- }, -- FunctionTypeParameters: { -- Name: FunctionTypeParameters, -- Doc: "Enable/disable inlay hints for implicit type parameters on generic functions:\n```go\n\tmyFoo/*[int, string]*/(1, \"hello\")\n```", -- Run: funcTypeParams, -- }, +-func (s *server) DidChangeNotebookDocument(context.Context, *protocol.DidChangeNotebookDocumentParams) error { +- return notImplemented("DidChangeNotebookDocument") -} - --func InlayHint(ctx context.Context, snapshot Snapshot, fh FileHandle, pRng protocol.Range) ([]protocol.InlayHint, error) { -- ctx, done := event.Start(ctx, "source.InlayHint") -- defer done() +-func (s *server) DidCloseNotebookDocument(context.Context, *protocol.DidCloseNotebookDocumentParams) error { +- return notImplemented("DidCloseNotebookDocument") +-} - -- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) -- if err != nil { -- return nil, fmt.Errorf("getting file for InlayHint: %w", err) -- } +-func (s *server) DidCreateFiles(context.Context, *protocol.CreateFilesParams) error { +- return notImplemented("DidCreateFiles") +-} - -- // Collect a list of the inlay hints that are enabled. -- inlayHintOptions := snapshot.Options().InlayHintOptions -- var enabledHints []InlayHintFunc -- for hint, enabled := range inlayHintOptions.Hints { -- if !enabled { -- continue -- } -- if h, ok := AllInlayHints[hint]; ok { -- enabledHints = append(enabledHints, h.Run) -- } -- } -- if len(enabledHints) == 0 { -- return nil, nil -- } +-func (s *server) DidDeleteFiles(context.Context, *protocol.DeleteFilesParams) error { +- return notImplemented("DidDeleteFiles") +-} - -- info := pkg.GetTypesInfo() -- q := Qualifier(pgf.File, pkg.GetTypes(), info) +-func (s *server) DidOpenNotebookDocument(context.Context, *protocol.DidOpenNotebookDocumentParams) error { +- return notImplemented("DidOpenNotebookDocument") +-} - -- // Set the range to the full file if the range is not valid. -- start, end := pgf.File.Pos(), pgf.File.End() -- if pRng.Start.Line < pRng.End.Line || pRng.Start.Character < pRng.End.Character { -- // Adjust start and end for the specified range. -- var err error -- start, end, err = pgf.RangePos(pRng) -- if err != nil { -- return nil, err -- } -- } +-func (s *server) DidRenameFiles(context.Context, *protocol.RenameFilesParams) error { +- return notImplemented("DidRenameFiles") +-} - -- var hints []protocol.InlayHint -- ast.Inspect(pgf.File, func(node ast.Node) bool { -- // If not in range, we can stop looking. -- if node == nil || node.End() < start || node.Pos() > end { -- return false -- } -- for _, fn := range enabledHints { -- hints = append(hints, fn(node, pgf.Mapper, pgf.Tok, info, &q)...) -- } -- return true -- }) -- return hints, nil +-func (s *server) DidSaveNotebookDocument(context.Context, *protocol.DidSaveNotebookDocumentParams) error { +- return notImplemented("DidSaveNotebookDocument") -} - --func parameterNames(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { -- callExpr, ok := node.(*ast.CallExpr) -- if !ok { -- return nil -- } -- signature, ok := info.TypeOf(callExpr.Fun).(*types.Signature) -- if !ok { -- return nil -- } +-func (s *server) DocumentColor(context.Context, *protocol.DocumentColorParams) ([]protocol.ColorInformation, error) { +- return nil, notImplemented("DocumentColor") +-} - -- var hints []protocol.InlayHint -- for i, v := range callExpr.Args { -- start, err := m.PosPosition(tf, v.Pos()) -- if err != nil { -- continue -- } -- params := signature.Params() -- // When a function has variadic params, we skip args after -- // params.Len(). -- if i > params.Len()-1 { -- break -- } -- param := params.At(i) -- // param.Name is empty for built-ins like append -- if param.Name() == "" { -- continue -- } -- // Skip the parameter name hint if the arg matches -- // the parameter name. -- if i, ok := v.(*ast.Ident); ok && i.Name == param.Name() { -- continue -- } +-func (s *server) InlineCompletion(context.Context, *protocol.InlineCompletionParams) (*protocol.Or_Result_textDocument_inlineCompletion, error) { +- return nil, notImplemented("InlineCompletion") +-} - -- label := param.Name() -- if signature.Variadic() && i == params.Len()-1 { -- label = label + "..." -- } -- hints = append(hints, protocol.InlayHint{ -- Position: start, -- Label: buildLabel(label + ":"), -- Kind: protocol.Parameter, -- PaddingRight: true, -- }) -- } -- return hints +-func (s *server) InlineValue(context.Context, *protocol.InlineValueParams) ([]protocol.InlineValue, error) { +- return nil, notImplemented("InlineValue") -} - --func funcTypeParams(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { -- ce, ok := node.(*ast.CallExpr) -- if !ok { -- return nil -- } -- id, ok := ce.Fun.(*ast.Ident) -- if !ok { -- return nil -- } -- inst := typeparams.GetInstances(info)[id] -- if inst.TypeArgs == nil { -- return nil -- } -- start, err := m.PosPosition(tf, id.End()) -- if err != nil { -- return nil -- } -- var args []string -- for i := 0; i < inst.TypeArgs.Len(); i++ { -- args = append(args, inst.TypeArgs.At(i).String()) -- } -- if len(args) == 0 { -- return nil -- } -- return []protocol.InlayHint{{ -- Position: start, -- Label: buildLabel("[" + strings.Join(args, ", ") + "]"), -- Kind: protocol.Type, -- }} +-func (s *server) LinkedEditingRange(context.Context, *protocol.LinkedEditingRangeParams) (*protocol.LinkedEditingRanges, error) { +- return nil, notImplemented("LinkedEditingRange") -} - --func assignVariableTypes(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint { -- stmt, ok := node.(*ast.AssignStmt) -- if !ok || stmt.Tok != token.DEFINE { -- return nil -- } +-func (s *server) Moniker(context.Context, *protocol.MonikerParams) ([]protocol.Moniker, error) { +- return nil, notImplemented("Moniker") +-} - -- var hints []protocol.InlayHint -- for _, v := range stmt.Lhs { -- if h := variableType(v, m, tf, info, q); h != nil { -- hints = append(hints, *h) -- } -- } -- return hints +-func (s *server) OnTypeFormatting(context.Context, *protocol.DocumentOnTypeFormattingParams) ([]protocol.TextEdit, error) { +- return nil, notImplemented("OnTypeFormatting") -} - --func rangeVariableTypes(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint { -- rStmt, ok := node.(*ast.RangeStmt) -- if !ok { -- return nil -- } -- var hints []protocol.InlayHint -- if h := variableType(rStmt.Key, m, tf, info, q); h != nil { -- hints = append(hints, *h) -- } -- if h := variableType(rStmt.Value, m, tf, info, q); h != nil { -- hints = append(hints, *h) -- } -- return hints +-func (s *server) PrepareTypeHierarchy(context.Context, *protocol.TypeHierarchyPrepareParams) ([]protocol.TypeHierarchyItem, error) { +- return nil, notImplemented("PrepareTypeHierarchy") -} - --func variableType(e ast.Expr, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) *protocol.InlayHint { -- typ := info.TypeOf(e) -- if typ == nil { -- return nil -- } -- end, err := m.PosPosition(tf, e.End()) -- if err != nil { -- return nil -- } -- return &protocol.InlayHint{ -- Position: end, -- Label: buildLabel(types.TypeString(typ, *q)), -- Kind: protocol.Type, -- PaddingLeft: true, -- } +-func (s *server) Progress(context.Context, *protocol.ProgressParams) error { +- return notImplemented("Progress") -} - --func constantValues(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { -- genDecl, ok := node.(*ast.GenDecl) -- if !ok || genDecl.Tok != token.CONST { -- return nil -- } +-func (s *server) RangeFormatting(context.Context, *protocol.DocumentRangeFormattingParams) ([]protocol.TextEdit, error) { +- return nil, notImplemented("RangeFormatting") +-} - -- var hints []protocol.InlayHint -- for _, v := range genDecl.Specs { -- spec, ok := v.(*ast.ValueSpec) -- if !ok { -- continue -- } -- end, err := m.PosPosition(tf, v.End()) -- if err != nil { -- continue -- } -- // Show hints when values are missing or at least one value is not -- // a basic literal. -- showHints := len(spec.Values) == 0 -- checkValues := len(spec.Names) == len(spec.Values) -- var values []string -- for i, w := range spec.Names { -- obj, ok := info.ObjectOf(w).(*types.Const) -- if !ok || obj.Val().Kind() == constant.Unknown { -- return nil -- } -- if checkValues { -- switch spec.Values[i].(type) { -- case *ast.BadExpr: -- return nil -- case *ast.BasicLit: -- default: -- if obj.Val().Kind() != constant.Bool { -- showHints = true -- } -- } -- } -- values = append(values, fmt.Sprintf("%v", obj.Val())) -- } -- if !showHints || len(values) == 0 { -- continue -- } -- hints = append(hints, protocol.InlayHint{ -- Position: end, -- Label: buildLabel("= " + strings.Join(values, ", ")), -- PaddingLeft: true, -- }) -- } -- return hints +-func (s *server) RangesFormatting(context.Context, *protocol.DocumentRangesFormattingParams) ([]protocol.TextEdit, error) { +- return nil, notImplemented("RangesFormatting") +-} +- +-func (s *server) Resolve(context.Context, *protocol.InlayHint) (*protocol.InlayHint, error) { +- return nil, notImplemented("Resolve") +-} +- +-func (s *server) ResolveCodeLens(context.Context, *protocol.CodeLens) (*protocol.CodeLens, error) { +- return nil, notImplemented("ResolveCodeLens") +-} +- +-func (s *server) ResolveCompletionItem(context.Context, *protocol.CompletionItem) (*protocol.CompletionItem, error) { +- return nil, notImplemented("ResolveCompletionItem") +-} +- +-func (s *server) ResolveDocumentLink(context.Context, *protocol.DocumentLink) (*protocol.DocumentLink, error) { +- return nil, notImplemented("ResolveDocumentLink") +-} +- +-func (s *server) ResolveWorkspaceSymbol(context.Context, *protocol.WorkspaceSymbol) (*protocol.WorkspaceSymbol, error) { +- return nil, notImplemented("ResolveWorkspaceSymbol") +-} +- +-func (s *server) SemanticTokensFullDelta(context.Context, *protocol.SemanticTokensDeltaParams) (interface{}, error) { +- return nil, notImplemented("SemanticTokensFullDelta") +-} +- +-func (s *server) SetTrace(context.Context, *protocol.SetTraceParams) error { +- return notImplemented("SetTrace") +-} +- +-func (s *server) Subtypes(context.Context, *protocol.TypeHierarchySubtypesParams) ([]protocol.TypeHierarchyItem, error) { +- return nil, notImplemented("Subtypes") +-} +- +-func (s *server) Supertypes(context.Context, *protocol.TypeHierarchySupertypesParams) ([]protocol.TypeHierarchyItem, error) { +- return nil, notImplemented("Supertypes") -} - --func compositeLiteralFields(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, _ *types.Qualifier) []protocol.InlayHint { -- compLit, ok := node.(*ast.CompositeLit) -- if !ok { -- return nil -- } -- typ := info.TypeOf(compLit) -- if typ == nil { -- return nil -- } -- if t, ok := typ.(*types.Pointer); ok { -- typ = t.Elem() -- } -- strct, ok := typ.Underlying().(*types.Struct) -- if !ok { -- return nil -- } +-func (s *server) WillCreateFiles(context.Context, *protocol.CreateFilesParams) (*protocol.WorkspaceEdit, error) { +- return nil, notImplemented("WillCreateFiles") +-} - -- var hints []protocol.InlayHint -- var allEdits []protocol.TextEdit -- for i, v := range compLit.Elts { -- if _, ok := v.(*ast.KeyValueExpr); !ok { -- start, err := m.PosPosition(tf, v.Pos()) -- if err != nil { -- continue -- } -- if i > strct.NumFields()-1 { -- break -- } -- hints = append(hints, protocol.InlayHint{ -- Position: start, -- Label: buildLabel(strct.Field(i).Name() + ":"), -- Kind: protocol.Parameter, -- PaddingRight: true, -- }) -- allEdits = append(allEdits, protocol.TextEdit{ -- Range: protocol.Range{Start: start, End: start}, -- NewText: strct.Field(i).Name() + ": ", -- }) -- } -- } -- // It is not allowed to have a mix of keyed and unkeyed fields, so -- // have the text edits add keys to all fields. -- for i := range hints { -- hints[i].TextEdits = allEdits -- } -- return hints +-func (s *server) WillDeleteFiles(context.Context, *protocol.DeleteFilesParams) (*protocol.WorkspaceEdit, error) { +- return nil, notImplemented("WillDeleteFiles") -} - --func compositeLiteralTypes(node ast.Node, m *protocol.Mapper, tf *token.File, info *types.Info, q *types.Qualifier) []protocol.InlayHint { -- compLit, ok := node.(*ast.CompositeLit) -- if !ok { -- return nil -- } -- typ := info.TypeOf(compLit) -- if typ == nil { -- return nil -- } -- if compLit.Type != nil { -- return nil -- } -- prefix := "" -- if t, ok := typ.(*types.Pointer); ok { -- typ = t.Elem() -- prefix = "&" -- } -- // The type for this composite literal is implicit, add an inlay hint. -- start, err := m.PosPosition(tf, compLit.Lbrace) -- if err != nil { -- return nil -- } -- return []protocol.InlayHint{{ -- Position: start, -- Label: buildLabel(fmt.Sprintf("%s%s", prefix, types.TypeString(typ, *q))), -- Kind: protocol.Type, -- }} +-func (s *server) WillRenameFiles(context.Context, *protocol.RenameFilesParams) (*protocol.WorkspaceEdit, error) { +- return nil, notImplemented("WillRenameFiles") -} - --func buildLabel(s string) []protocol.InlayHintLabelPart { -- label := protocol.InlayHintLabelPart{ -- Value: s, -- } -- if len(s) > maxLabelLength+len("...") { -- label.Value = s[:maxLabelLength] + "..." -- } -- return []protocol.InlayHintLabelPart{label} +-func (s *server) WillSave(context.Context, *protocol.WillSaveTextDocumentParams) error { +- return notImplemented("WillSave") -} -diff -urN a/gopls/internal/lsp/source/inline_all.go b/gopls/internal/lsp/source/inline_all.go ---- a/gopls/internal/lsp/source/inline_all.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/inline_all.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,275 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. +- +-func (s *server) WillSaveWaitUntil(context.Context, *protocol.WillSaveTextDocumentParams) ([]protocol.TextEdit, error) { +- return nil, notImplemented("WillSaveWaitUntil") +-} +- +-func notImplemented(method string) error { +- return fmt.Errorf("%w: %q not yet implemented", jsonrpc2.ErrMethodNotFound, method) +-} +diff -urN a/gopls/internal/server/workspace.go b/gopls/internal/server/workspace.go +--- a/gopls/internal/server/workspace.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/workspace.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,104 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package source +-package server - -import ( - "context" - "fmt" -- "go/ast" -- "go/parser" -- "go/types" +- "sync" - -- "golang.org/x/tools/go/ast/astutil" -- "golang.org/x/tools/go/types/typeutil" -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/refactor/inline" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/event" -) - --// inlineAllCalls inlines all calls to the original function declaration --// described by callee, returning the resulting modified file content. --// --// inlining everything is currently an expensive operation: it involves re-type --// checking every package that contains a potential call, as reported by --// References. In cases where there are multiple calls per file, inlineAllCalls --// must type check repeatedly for each additional call. --// --// The provided post processing function is applied to the resulting source --// after each transformation. This is necessary because we are using this --// function to inline synthetic wrappers for the purpose of signature --// rewriting. The delegated function has a fake name that doesn't exist in the --// snapshot, and so we can't re-type check until we replace this fake name. --// --// TODO(rfindley): this only works because removing a parameter is a very --// narrow operation. A better solution would be to allow for ad-hoc snapshots --// that expose the full machinery of real snapshots: minimal invalidation, --// batched type checking, etc. Then we could actually rewrite the declaring --// package in this snapshot (and so 'post' would not be necessary), and could --// robustly re-type check for the purpose of iterative inlining, even if the --// inlined code pulls in new imports that weren't present in export data. --// --// The code below notes where are assumptions are made that only hold true in --// the case of parameter removal (annotated with 'Assumption:') --func inlineAllCalls(ctx context.Context, logf func(string, ...any), snapshot Snapshot, pkg Package, pgf *ParsedGoFile, origDecl *ast.FuncDecl, callee *inline.Callee, post func([]byte) []byte) (map[span.URI][]byte, error) { -- // Collect references. -- var refs []protocol.Location -- { -- funcPos, err := pgf.Mapper.PosPosition(pgf.Tok, origDecl.Name.NamePos) -- if err != nil { -- return nil, err -- } -- fh, err := snapshot.ReadFile(ctx, pgf.URI) +-func (s *server) DidChangeWorkspaceFolders(ctx context.Context, params *protocol.DidChangeWorkspaceFoldersParams) error { +- for _, folder := range params.Event.Removed { +- dir, err := protocol.ParseDocumentURI(folder.URI) - if err != nil { -- return nil, err +- return fmt.Errorf("invalid folder %q: %v", folder.URI, err) - } -- refs, err = References(ctx, snapshot, fh, funcPos, false) -- if err != nil { -- return nil, fmt.Errorf("finding references to rewrite: %v", err) +- if !s.session.RemoveView(dir) { +- return fmt.Errorf("view %q for %v not found", folder.Name, folder.URI) - } - } +- s.addFolders(ctx, params.Event.Added) +- return nil +-} - -- // Type-check the narrowest package containing each reference. -- // TODO(rfindley): we should expose forEachPackage in order to operate in -- // parallel and to reduce peak memory for this operation. -- var ( -- pkgForRef = make(map[protocol.Location]PackageID) -- pkgs = make(map[PackageID]Package) -- ) -- { -- needPkgs := make(map[PackageID]struct{}) -- for _, ref := range refs { -- md, err := NarrowestMetadataForFile(ctx, snapshot, ref.URI.SpanURI()) -- if err != nil { -- return nil, fmt.Errorf("finding ref metadata: %v", err) -- } -- pkgForRef[ref] = md.ID -- needPkgs[md.ID] = struct{}{} -- } -- var pkgIDs []PackageID -- for id := range needPkgs { // TODO: use maps.Keys once it is available to us -- pkgIDs = append(pkgIDs, id) -- } +-// addView returns a Snapshot and a release function that must be +-// called when it is no longer needed. +-func (s *server) addView(ctx context.Context, name string, dir protocol.DocumentURI) (*cache.Snapshot, func(), error) { +- s.stateMu.Lock() +- state := s.state +- s.stateMu.Unlock() +- if state < serverInitialized { +- return nil, nil, fmt.Errorf("addView called before server initialized") +- } +- folder, err := s.newFolder(ctx, dir, name) +- if err != nil { +- return nil, nil, err +- } +- _, snapshot, release, err := s.session.NewView(ctx, folder) +- return snapshot, release, err +-} - -- refPkgs, err := snapshot.TypeCheck(ctx, pkgIDs...) -- if err != nil { -- return nil, fmt.Errorf("type checking reference packages: %v", err) -- } +-func (s *server) DidChangeConfiguration(ctx context.Context, _ *protocol.DidChangeConfigurationParams) error { +- ctx, done := event.Start(ctx, "lsp.Server.didChangeConfiguration") +- defer done() - -- for _, p := range refPkgs { -- pkgs[p.Metadata().ID] = p -- } +- var wg sync.WaitGroup +- wg.Add(1) +- defer wg.Done() +- if s.Options().VerboseWorkDoneProgress { +- work := s.progress.Start(ctx, DiagnosticWorkTitle(FromDidChangeConfiguration), "Calculating diagnostics...", nil, nil) +- go func() { +- wg.Wait() +- work.End(ctx, "Done.") +- }() - } - -- // Organize calls by top file declaration. Calls within a single file may -- // affect each other, as the inlining edit may affect the surrounding scope -- // or imports Therefore, when inlining subsequent calls in the same -- // declaration, we must re-type check. -- -- type fileCalls struct { -- pkg Package -- pgf *ParsedGoFile -- calls []*ast.CallExpr +- // Apply any changes to the session-level settings. +- options, err := s.fetchFolderOptions(ctx, "") +- if err != nil { +- return err - } +- s.SetOptions(options) - -- refsByFile := make(map[span.URI]*fileCalls) -- for _, ref := range refs { -- refpkg := pkgs[pkgForRef[ref]] -- pgf, err := refpkg.File(ref.URI.SpanURI()) -- if err != nil { -- return nil, bug.Errorf("finding %s in %s: %v", ref.URI, refpkg.Metadata().ID, err) +- // Collect options for all workspace folders. +- seen := make(map[protocol.DocumentURI]bool) +- var newFolders []*cache.Folder +- for _, view := range s.session.Views() { +- folder := view.Folder() +- if seen[folder.Dir] { +- continue - } -- start, end, err := pgf.RangePos(ref.Range) +- seen[folder.Dir] = true +- newFolder, err := s.newFolder(ctx, folder.Dir, folder.Name) - if err != nil { -- return nil, bug.Errorf("RangePos(ref): %v", err) +- return err - } +- newFolders = append(newFolders, newFolder) +- } +- s.session.UpdateFolders(ctx, newFolders) - -- // Look for the surrounding call expression. -- var ( -- name *ast.Ident -- call *ast.CallExpr -- ) -- path, _ := astutil.PathEnclosingInterval(pgf.File, start, end) -- name, _ = path[0].(*ast.Ident) -- if _, ok := path[1].(*ast.SelectorExpr); ok { -- call, _ = path[2].(*ast.CallExpr) -- } else { -- call, _ = path[1].(*ast.CallExpr) -- } -- if name == nil || call == nil { -- // TODO(rfindley): handle this case with eta-abstraction: -- // a reference to the target function f in a non-call position -- // use(f) -- // is replaced by -- // use(func(...) { f(...) }) -- return nil, fmt.Errorf("cannot inline: found non-call function reference %v", ref) -- } -- // Sanity check. -- if obj := refpkg.GetTypesInfo().ObjectOf(name); obj == nil || -- obj.Name() != origDecl.Name.Name || -- obj.Pkg() == nil || -- obj.Pkg().Path() != string(pkg.Metadata().PkgPath) { -- return nil, bug.Errorf("cannot inline: corrupted reference %v", ref) -- } +- // The view set may have been updated above. +- viewsToDiagnose := make(map[*cache.View][]protocol.DocumentURI) +- for _, view := range s.session.Views() { +- viewsToDiagnose[view] = nil +- } - -- callInfo, ok := refsByFile[ref.URI.SpanURI()] -- if !ok { -- callInfo = &fileCalls{ -- pkg: refpkg, -- pgf: pgf, -- } -- refsByFile[ref.URI.SpanURI()] = callInfo +- modCtx, modID := s.needsDiagnosis(ctx, viewsToDiagnose) +- wg.Add(1) +- go func() { +- s.diagnoseChangedViews(modCtx, modID, viewsToDiagnose, FromDidChangeConfiguration) +- wg.Done() +- }() +- +- // An options change may have affected the detected Go version. +- s.checkViewGoVersions() +- +- return nil +-} +diff -urN a/gopls/internal/server/workspace_symbol.go b/gopls/internal/server/workspace_symbol.go +--- a/gopls/internal/server/workspace_symbol.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/server/workspace_symbol.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,41 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package server +- +-import ( +- "context" +- +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/telemetry" +- "golang.org/x/tools/internal/event" +-) +- +-func (s *server) Symbol(ctx context.Context, params *protocol.WorkspaceSymbolParams) (_ []protocol.SymbolInformation, rerr error) { +- recordLatency := telemetry.StartLatencyTimer("symbol") +- defer func() { +- recordLatency(ctx, rerr) +- }() +- +- ctx, done := event.Start(ctx, "lsp.Server.symbol") +- defer done() +- +- views := s.session.Views() +- matcher := s.Options().SymbolMatcher +- style := s.Options().SymbolStyle +- +- var snapshots []*cache.Snapshot +- for _, v := range views { +- snapshot, release, err := v.Snapshot() +- if err != nil { +- continue // snapshot is shutting down - } -- callInfo.calls = append(callInfo.calls, call) +- // If err is non-nil, the snapshot is shutting down. Skip it. +- defer release() +- snapshots = append(snapshots, snapshot) - } +- return golang.WorkspaceSymbols(ctx, matcher, style, snapshots, params.Query) +-} +diff -urN a/gopls/internal/settings/analyzer.go b/gopls/internal/settings/analyzer.go +--- a/gopls/internal/settings/analyzer.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/settings/analyzer.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,53 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- // Inline each call within the same decl in sequence, re-typechecking after -- // each one. If there is only a single call within the decl, we can avoid -- // additional type checking. -- // -- // Assumption: inlining does not affect the package scope, so we can operate -- // on separate files independently. -- result := make(map[span.URI][]byte) -- for uri, callInfo := range refsByFile { -- var ( -- calls = callInfo.calls -- fset = callInfo.pkg.FileSet() -- tpkg = callInfo.pkg.GetTypes() -- tinfo = callInfo.pkg.GetTypesInfo() -- file = callInfo.pgf.File -- content = callInfo.pgf.Src -- ) +-package settings - -- // Check for overlapping calls (such as Foo(Foo())). We can't handle these -- // because inlining may change the source order of the inner call with -- // respect to the inlined outer call, and so the heuristic we use to find -- // the next call (counting from top-to-bottom) does not work. -- for i := range calls { -- if i > 0 && calls[i-1].End() > calls[i].Pos() { -- return nil, fmt.Errorf("%s: can't inline overlapping call %s", uri, types.ExprString(calls[i-1])) -- } -- } +-import ( +- "golang.org/x/tools/go/analysis" +- "golang.org/x/tools/gopls/internal/protocol" +-) - -- currentCall := 0 -- for currentCall < len(calls) { -- caller := &inline.Caller{ -- Fset: fset, -- Types: tpkg, -- Info: tinfo, -- File: file, -- Call: calls[currentCall], -- Content: content, -- } -- var err error -- content, err = inline.Inline(logf, caller, callee) -- if err != nil { -- return nil, fmt.Errorf("inlining failed: %v", err) -- } -- if post != nil { -- content = post(content) -- } -- if len(calls) <= 1 { -- // No need to re-type check, as we've inlined all calls. -- break -- } +-// Analyzer augments a go/analysis analyzer with additional LSP configuration. +-type Analyzer struct { +- Analyzer *analysis.Analyzer - -- // TODO(rfindley): develop a theory of "trivial" inlining, which are -- // inlinings that don't require re-type checking. -- // -- // In principle, if the inlining only involves replacing one call with -- // another, the scope of the caller is unchanged and there is no need to -- // type check again before inlining subsequent calls (edits should not -- // overlap, and should not affect each other semantically). However, it -- // feels sufficiently complicated that, to be safe, this optimization is -- // deferred until later. +- // Enabled reports whether the analyzer is enabled. This value can be +- // configured per-analysis in user settings. For staticcheck analyzers, +- // the value of the Staticcheck setting overrides this field. +- // +- // Most clients should use the IsEnabled method. +- Enabled bool - -- file, err = parser.ParseFile(fset, uri.Filename(), content, parser.ParseComments|parser.SkipObjectResolution) -- if err != nil { -- return nil, bug.Errorf("inlined file failed to parse: %v", err) -- } +- // ActionKinds is the set of kinds of code action this analyzer produces. +- // If empty, the set is just QuickFix. +- ActionKinds []protocol.CodeActionKind - -- // After inlining one call with a removed parameter, the package will -- // fail to type check due to "not enough arguments". Therefore, we must -- // allow type errors here. -- // -- // Assumption: the resulting type errors do not affect the correctness of -- // subsequent inlining, because invalid arguments to a call do not affect -- // anything in the surrounding scope. -- // -- // TODO(rfindley): improve this. -- tpkg, tinfo, err = reTypeCheck(logf, callInfo.pkg, map[span.URI]*ast.File{uri: file}, true) -- if err != nil { -- return nil, bug.Errorf("type checking after inlining failed: %v", err) -- } +- // Severity is the severity set for diagnostics reported by this +- // analyzer. If left unset it defaults to Warning. +- // +- // Note: diagnostics with severity protocol.SeverityHint do not show up in +- // the VS Code "problems" tab. +- Severity protocol.DiagnosticSeverity - -- // Collect calls to the target function in the modified declaration. -- var calls2 []*ast.CallExpr -- ast.Inspect(file, func(n ast.Node) bool { -- if call, ok := n.(*ast.CallExpr); ok { -- fn := typeutil.StaticCallee(tinfo, call) -- if fn != nil && fn.Pkg().Path() == string(pkg.Metadata().PkgPath) && fn.Name() == origDecl.Name.Name { -- calls2 = append(calls2, call) -- } -- } -- return true -- }) +- // Tag is extra tags (unnecessary, deprecated, etc) for diagnostics +- // reported by this analyzer. +- Tag []protocol.DiagnosticTag +-} - -- // If the number of calls has increased, this process will never cease. -- // If the number of calls has decreased, assume that inlining removed a -- // call. -- // If the number of calls didn't change, assume that inlining replaced -- // a call, and move on to the next. -- // -- // Assumption: we're inlining a call that has at most one recursive -- // reference (which holds for signature rewrites). -- // -- // TODO(rfindley): this isn't good enough. We should be able to support -- // inlining all existing calls even if they increase calls. How do we -- // correlate the before and after syntax? -- switch { -- case len(calls2) > len(calls): -- return nil, fmt.Errorf("inlining increased calls %d->%d, possible recursive call? content:\n%s", len(calls), len(calls2), content) -- case len(calls2) < len(calls): -- calls = calls2 -- case len(calls2) == len(calls): -- calls = calls2 -- currentCall++ -- } -- } +-func (a *Analyzer) String() string { return a.Analyzer.String() } - -- result[callInfo.pgf.URI] = content +-// IsEnabled reports whether this analyzer is enabled by the given options. +-func (a Analyzer) IsEnabled(options *Options) bool { +- // Staticcheck analyzers can only be enabled when staticcheck is on. +- if _, ok := options.StaticcheckAnalyzers[a.Analyzer.Name]; ok { +- if !options.Staticcheck { +- return false +- } - } -- return result, nil +- if enabled, ok := options.Analyses[a.Analyzer.Name]; ok { +- return enabled +- } +- return a.Enabled +-} +diff -urN a/gopls/internal/settings/api_json.go b/gopls/internal/settings/api_json.go +--- a/gopls/internal/settings/api_json.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/settings/api_json.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,1318 +0,0 @@ +-// Code generated by "golang.org/x/tools/gopls/doc/generate"; DO NOT EDIT. +- +-package settings +- +-var GeneratedAPIJSON = &APIJSON{ +- Options: map[string][]*OptionJSON{ +- "User": { +- { +- Name: "buildFlags", +- Type: "[]string", +- Doc: "buildFlags is the set of flags passed on to the build system when invoked.\nIt is applied to queries like `go list`, which is used when discovering files.\nThe most common use is to set `-tags`.\n", +- Default: "[]", +- Hierarchy: "build", +- }, +- { +- Name: "env", +- Type: "map[string]string", +- Doc: "env adds environment variables to external commands run by `gopls`, most notably `go list`.\n", +- Default: "{}", +- Hierarchy: "build", +- }, +- { +- Name: "directoryFilters", +- Type: "[]string", +- Doc: "directoryFilters can be used to exclude unwanted directories from the\nworkspace. By default, all directories are included. Filters are an\noperator, `+` to include and `-` to exclude, followed by a path prefix\nrelative to the workspace folder. They are evaluated in order, and\nthe last filter that applies to a path controls whether it is included.\nThe path prefix can be empty, so an initial `-` excludes everything.\n\nDirectoryFilters also supports the `**` operator to match 0 or more directories.\n\nExamples:\n\nExclude node_modules at current depth: `-node_modules`\n\nExclude node_modules at any depth: `-**/node_modules`\n\nInclude only project_a: `-` (exclude everything), `+project_a`\n\nInclude only project_a, but not node_modules inside it: `-`, `+project_a`, `-project_a/node_modules`\n", +- Default: "[\"-**/node_modules\"]", +- Hierarchy: "build", +- }, +- { +- Name: "templateExtensions", +- Type: "[]string", +- Doc: "templateExtensions gives the extensions of file names that are treateed\nas template files. (The extension\nis the part of the file name after the final dot.)\n", +- Default: "[]", +- Hierarchy: "build", +- }, +- { +- Name: "memoryMode", +- Type: "string", +- Doc: "obsolete, no effect\n", +- Default: "\"\"", +- Status: "experimental", +- Hierarchy: "build", +- }, +- { +- Name: "expandWorkspaceToModule", +- Type: "bool", +- Doc: "expandWorkspaceToModule determines which packages are considered\n\"workspace packages\" when the workspace is using modules.\n\nWorkspace packages affect the scope of workspace-wide operations. Notably,\ngopls diagnoses all packages considered to be part of the workspace after\nevery keystroke, so by setting \"ExpandWorkspaceToModule\" to false, and\nopening a nested workspace directory, you can reduce the amount of work\ngopls has to do to keep your workspace up to date.\n", +- Default: "true", +- Status: "experimental", +- Hierarchy: "build", +- }, +- { +- Name: "allowModfileModifications", +- Type: "bool", +- Doc: "allowModfileModifications disables -mod=readonly, allowing imports from\nout-of-scope modules. This option will eventually be removed.\n", +- Default: "false", +- Status: "experimental", +- Hierarchy: "build", +- }, +- { +- Name: "allowImplicitNetworkAccess", +- Type: "bool", +- Doc: "allowImplicitNetworkAccess disables GOPROXY=off, allowing implicit module\ndownloads rather than requiring user action. This option will eventually\nbe removed.\n", +- Default: "false", +- Status: "experimental", +- Hierarchy: "build", +- }, +- { +- Name: "standaloneTags", +- Type: "[]string", +- Doc: "standaloneTags specifies a set of build constraints that identify\nindividual Go source files that make up the entire main package of an\nexecutable.\n\nA common example of standalone main files is the convention of using the\ndirective `//go:build ignore` to denote files that are not intended to be\nincluded in any package, for example because they are invoked directly by\nthe developer using `go run`.\n\nGopls considers a file to be a standalone main file if and only if it has\npackage name \"main\" and has a build directive of the exact form\n\"//go:build tag\" or \"// +build tag\", where tag is among the list of tags\nconfigured by this setting. Notably, if the build constraint is more\ncomplicated than a simple tag (such as the composite constraint\n`//go:build tag && go1.18`), the file is not considered to be a standalone\nmain file.\n\nThis setting is only supported when gopls is built with Go 1.16 or later.\n", +- Default: "[\"ignore\"]", +- Hierarchy: "build", +- }, +- { +- Name: "hoverKind", +- Type: "enum", +- Doc: "hoverKind controls the information that appears in the hover text.\nSingleLine and Structured are intended for use only by authors of editor plugins.\n", +- EnumValues: []EnumValue{ +- {Value: "\"FullDocumentation\""}, +- {Value: "\"NoDocumentation\""}, +- {Value: "\"SingleLine\""}, +- { +- Value: "\"Structured\"", +- Doc: "`\"Structured\"` is an experimental setting that returns a structured hover format.\nThis format separates the signature from the documentation, so that the client\ncan do more manipulation of these fields.\n\nThis should only be used by clients that support this behavior.\n", +- }, +- {Value: "\"SynopsisDocumentation\""}, +- }, +- Default: "\"FullDocumentation\"", +- Hierarchy: "ui.documentation", +- }, +- { +- Name: "linkTarget", +- Type: "string", +- Doc: "linkTarget controls where documentation links go.\nIt might be one of:\n\n* `\"godoc.org\"`\n* `\"pkg.go.dev\"`\n\nIf company chooses to use its own `godoc.org`, its address can be used as well.\n\nModules matching the GOPRIVATE environment variable will not have\ndocumentation links in hover.\n", +- Default: "\"pkg.go.dev\"", +- Hierarchy: "ui.documentation", +- }, +- { +- Name: "linksInHover", +- Type: "bool", +- Doc: "linksInHover toggles the presence of links to documentation in hover.\n", +- Default: "true", +- Hierarchy: "ui.documentation", +- }, +- { +- Name: "usePlaceholders", +- Type: "bool", +- Doc: "placeholders enables placeholders for function parameters or struct\nfields in completion responses.\n", +- Default: "false", +- Hierarchy: "ui.completion", +- }, +- { +- Name: "completionBudget", +- Type: "time.Duration", +- Doc: "completionBudget is the soft latency goal for completion requests. Most\nrequests finish in a couple milliseconds, but in some cases deep\ncompletions can take much longer. As we use up our budget we\ndynamically reduce the search scope to ensure we return timely\nresults. Zero means unlimited.\n", +- Default: "\"100ms\"", +- Status: "debug", +- Hierarchy: "ui.completion", +- }, +- { +- Name: "matcher", +- Type: "enum", +- Doc: "matcher sets the algorithm that is used when calculating completion\ncandidates.\n", +- EnumValues: []EnumValue{ +- {Value: "\"CaseInsensitive\""}, +- {Value: "\"CaseSensitive\""}, +- {Value: "\"Fuzzy\""}, +- }, +- Default: "\"Fuzzy\"", +- Status: "advanced", +- Hierarchy: "ui.completion", +- }, +- { +- Name: "experimentalPostfixCompletions", +- Type: "bool", +- Doc: "experimentalPostfixCompletions enables artificial method snippets\nsuch as \"someSlice.sort!\".\n", +- Default: "true", +- Status: "experimental", +- Hierarchy: "ui.completion", +- }, +- { +- Name: "completeFunctionCalls", +- Type: "bool", +- Doc: "completeFunctionCalls enables function call completion.\n\nWhen completing a statement, or when a function return type matches the\nexpected of the expression being completed, completion may suggest call\nexpressions (i.e. may include parentheses).\n", +- Default: "true", +- Hierarchy: "ui.completion", +- }, +- { +- Name: "importShortcut", +- Type: "enum", +- Doc: "importShortcut specifies whether import statements should link to\ndocumentation or go to definitions.\n", +- EnumValues: []EnumValue{ +- {Value: "\"Both\""}, +- {Value: "\"Definition\""}, +- {Value: "\"Link\""}, +- }, +- Default: "\"Both\"", +- Hierarchy: "ui.navigation", +- }, +- { +- Name: "symbolMatcher", +- Type: "enum", +- Doc: "symbolMatcher sets the algorithm that is used when finding workspace symbols.\n", +- EnumValues: []EnumValue{ +- {Value: "\"CaseInsensitive\""}, +- {Value: "\"CaseSensitive\""}, +- {Value: "\"FastFuzzy\""}, +- {Value: "\"Fuzzy\""}, +- }, +- Default: "\"FastFuzzy\"", +- Status: "advanced", +- Hierarchy: "ui.navigation", +- }, +- { +- Name: "symbolStyle", +- Type: "enum", +- Doc: "symbolStyle controls how symbols are qualified in symbol responses.\n\nExample Usage:\n\n```json5\n\"gopls\": {\n...\n \"symbolStyle\": \"Dynamic\",\n...\n}\n```\n", +- EnumValues: []EnumValue{ +- { +- Value: "\"Dynamic\"", +- Doc: "`\"Dynamic\"` uses whichever qualifier results in the highest scoring\nmatch for the given symbol query. Here a \"qualifier\" is any \"/\" or \".\"\ndelimited suffix of the fully qualified symbol. i.e. \"to/pkg.Foo.Field\" or\njust \"Foo.Field\".\n", +- }, +- { +- Value: "\"Full\"", +- Doc: "`\"Full\"` is fully qualified symbols, i.e.\n\"path/to/pkg.Foo.Field\".\n", +- }, +- { +- Value: "\"Package\"", +- Doc: "`\"Package\"` is package qualified symbols i.e.\n\"pkg.Foo.Field\".\n", +- }, +- }, +- Default: "\"Dynamic\"", +- Status: "advanced", +- Hierarchy: "ui.navigation", +- }, +- { +- Name: "symbolScope", +- Type: "enum", +- Doc: "symbolScope controls which packages are searched for workspace/symbol\nrequests. The default value, \"workspace\", searches only workspace\npackages. The legacy behavior, \"all\", causes all loaded packages to be\nsearched, including dependencies; this is more expensive and may return\nunwanted results.\n", +- EnumValues: []EnumValue{ +- { +- Value: "\"all\"", +- Doc: "`\"all\"` matches symbols in any loaded package, including\ndependencies.\n", +- }, +- { +- Value: "\"workspace\"", +- Doc: "`\"workspace\"` matches symbols in workspace packages only.\n", +- }, +- }, +- Default: "\"all\"", +- Hierarchy: "ui.navigation", +- }, +- { +- Name: "analyses", +- Type: "map[string]bool", +- Doc: "analyses specify analyses that the user would like to enable or disable.\nA map of the names of analysis passes that should be enabled/disabled.\nA full list of analyzers that gopls uses can be found in\n[analyzers.md](https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md).\n\nExample Usage:\n\n```json5\n...\n\"analyses\": {\n \"unreachable\": false, // Disable the unreachable analyzer.\n \"unusedvariable\": true // Enable the unusedvariable analyzer.\n}\n...\n```\n", +- EnumKeys: EnumKeys{ +- ValueType: "bool", +- Keys: []EnumKey{ +- { +- Name: "\"appends\"", +- Doc: "check for missing values after append\n\nThis checker reports calls to append that pass\nno values to be appended to the slice.\n\n\ts := []string{\"a\", \"b\", \"c\"}\n\t_ = append(s)\n\nSuch calls are always no-ops and often indicate an\nunderlying mistake.", +- Default: "true", +- }, +- { +- Name: "\"asmdecl\"", +- Doc: "report mismatches between assembly files and Go declarations", +- Default: "true", +- }, +- { +- Name: "\"assign\"", +- Doc: "check for useless assignments\n\nThis checker reports assignments of the form x = x or a[i] = a[i].\nThese are almost always useless, and even when they aren't they are\nusually a mistake.", +- Default: "true", +- }, +- { +- Name: "\"atomic\"", +- Doc: "check for common mistakes using the sync/atomic package\n\nThe atomic checker looks for assignment statements of the form:\n\n\tx = atomic.AddUint64(&x, 1)\n\nwhich are not atomic.", +- Default: "true", +- }, +- { +- Name: "\"atomicalign\"", +- Doc: "check for non-64-bits-aligned arguments to sync/atomic functions", +- Default: "true", +- }, +- { +- Name: "\"bools\"", +- Doc: "check for common mistakes involving boolean operators", +- Default: "true", +- }, +- { +- Name: "\"buildtag\"", +- Doc: "check //go:build and // +build directives", +- Default: "true", +- }, +- { +- Name: "\"cgocall\"", +- Doc: "detect some violations of the cgo pointer passing rules\n\nCheck for invalid cgo pointer passing.\nThis looks for code that uses cgo to call C code passing values\nwhose types are almost always invalid according to the cgo pointer\nsharing rules.\nSpecifically, it warns about attempts to pass a Go chan, map, func,\nor slice to C, either directly, or via a pointer, array, or struct.", +- Default: "true", +- }, +- { +- Name: "\"composites\"", +- Doc: "check for unkeyed composite literals\n\nThis analyzer reports a diagnostic for composite literals of struct\ntypes imported from another package that do not use the field-keyed\nsyntax. Such literals are fragile because the addition of a new field\n(even if unexported) to the struct will cause compilation to fail.\n\nAs an example,\n\n\terr = &net.DNSConfigError{err}\n\nshould be replaced by:\n\n\terr = &net.DNSConfigError{Err: err}\n", +- Default: "true", +- }, +- { +- Name: "\"copylocks\"", +- Doc: "check for locks erroneously passed by value\n\nInadvertently copying a value containing a lock, such as sync.Mutex or\nsync.WaitGroup, may cause both copies to malfunction. Generally such\nvalues should be referred to through a pointer.", +- Default: "true", +- }, +- { +- Name: "\"deepequalerrors\"", +- Doc: "check for calls of reflect.DeepEqual on error values\n\nThe deepequalerrors checker looks for calls of the form:\n\n reflect.DeepEqual(err1, err2)\n\nwhere err1 and err2 are errors. Using reflect.DeepEqual to compare\nerrors is discouraged.", +- Default: "true", +- }, +- { +- Name: "\"defers\"", +- Doc: "report common mistakes in defer statements\n\nThe defers analyzer reports a diagnostic when a defer statement would\nresult in a non-deferred call to time.Since, as experience has shown\nthat this is nearly always a mistake.\n\nFor example:\n\n\tstart := time.Now()\n\t...\n\tdefer recordLatency(time.Since(start)) // error: call to time.Since is not deferred\n\nThe correct code is:\n\n\tdefer func() { recordLatency(time.Since(start)) }()", +- Default: "true", +- }, +- { +- Name: "\"deprecated\"", +- Doc: "check for use of deprecated identifiers\n\nThe deprecated analyzer looks for deprecated symbols and package\nimports.\n\nSee https://go.dev/wiki/Deprecated to learn about Go's convention\nfor documenting and signaling deprecated identifiers.", +- Default: "true", +- }, +- { +- Name: "\"directive\"", +- Doc: "check Go toolchain directives such as //go:debug\n\nThis analyzer checks for problems with known Go toolchain directives\nin all Go source files in a package directory, even those excluded by\n//go:build constraints, and all non-Go source files too.\n\nFor //go:debug (see https://go.dev/doc/godebug), the analyzer checks\nthat the directives are placed only in Go source files, only above the\npackage comment, and only in package main or *_test.go files.\n\nSupport for other known directives may be added in the future.\n\nThis analyzer does not check //go:build, which is handled by the\nbuildtag analyzer.\n", +- Default: "true", +- }, +- { +- Name: "\"embed\"", +- Doc: "check //go:embed directive usage\n\nThis analyzer checks that the embed package is imported if //go:embed\ndirectives are present, providing a suggested fix to add the import if\nit is missing.\n\nThis analyzer also checks that //go:embed directives precede the\ndeclaration of a single variable.", +- Default: "true", +- }, +- { +- Name: "\"errorsas\"", +- Doc: "report passing non-pointer or non-error values to errors.As\n\nThe errorsas analysis reports calls to errors.As where the type\nof the second argument is not a pointer to a type implementing error.", +- Default: "true", +- }, +- { +- Name: "\"fieldalignment\"", +- Doc: "find structs that would use less memory if their fields were sorted\n\nThis analyzer find structs that can be rearranged to use less memory, and provides\na suggested edit with the most compact order.\n\nNote that there are two different diagnostics reported. One checks struct size,\nand the other reports \"pointer bytes\" used. Pointer bytes is how many bytes of the\nobject that the garbage collector has to potentially scan for pointers, for example:\n\n\tstruct { uint32; string }\n\nhave 16 pointer bytes because the garbage collector has to scan up through the string's\ninner pointer.\n\n\tstruct { string; *uint32 }\n\nhas 24 pointer bytes because it has to scan further through the *uint32.\n\n\tstruct { string; uint32 }\n\nhas 8 because it can stop immediately after the string pointer.\n\nBe aware that the most compact order is not always the most efficient.\nIn rare cases it may cause two variables each updated by its own goroutine\nto occupy the same CPU cache line, inducing a form of memory contention\nknown as \"false sharing\" that slows down both goroutines.\n", +- Default: "false", +- }, +- { +- Name: "\"fillreturns\"", +- Doc: "suggest fixes for errors due to an incorrect number of return values\n\nThis checker provides suggested fixes for type errors of the\ntype \"wrong number of return values (want %d, got %d)\". For example:\n\n\tfunc m() (int, string, *bool, error) {\n\t\treturn\n\t}\n\nwill turn into\n\n\tfunc m() (int, string, *bool, error) {\n\t\treturn 0, \"\", nil, nil\n\t}\n\nThis functionality is similar to https://github.com/sqs/goreturns.", +- Default: "true", +- }, +- { +- Name: "\"httpresponse\"", +- Doc: "check for mistakes using HTTP responses\n\nA common mistake when using the net/http package is to defer a function\ncall to close the http.Response Body before checking the error that\ndetermines whether the response is valid:\n\n\tresp, err := http.Head(url)\n\tdefer resp.Body.Close()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\t// (defer statement belongs here)\n\nThis checker helps uncover latent nil dereference bugs by reporting a\ndiagnostic for such mistakes.", +- Default: "true", +- }, +- { +- Name: "\"ifaceassert\"", +- Doc: "detect impossible interface-to-interface type assertions\n\nThis checker flags type assertions v.(T) and corresponding type-switch cases\nin which the static type V of v is an interface that cannot possibly implement\nthe target interface T. This occurs when V and T contain methods with the same\nname but different signatures. Example:\n\n\tvar v interface {\n\t\tRead()\n\t}\n\t_ = v.(io.Reader)\n\nThe Read method in v has a different signature than the Read method in\nio.Reader, so this assertion cannot succeed.", +- Default: "true", +- }, +- { +- Name: "\"infertypeargs\"", +- Doc: "check for unnecessary type arguments in call expressions\n\nExplicit type arguments may be omitted from call expressions if they can be\ninferred from function arguments, or from other type arguments:\n\n\tfunc f[T any](T) {}\n\t\n\tfunc _() {\n\t\tf[string](\"foo\") // string could be inferred\n\t}\n", +- Default: "true", +- }, +- { +- Name: "\"loopclosure\"", +- Doc: "check references to loop variables from within nested functions\n\nThis analyzer reports places where a function literal references the\niteration variable of an enclosing loop, and the loop calls the function\nin such a way (e.g. with go or defer) that it may outlive the loop\niteration and possibly observe the wrong value of the variable.\n\nNote: An iteration variable can only outlive a loop iteration in Go versions <=1.21.\nIn Go 1.22 and later, the loop variable lifetimes changed to create a new\niteration variable per loop iteration. (See go.dev/issue/60078.)\n\nIn this example, all the deferred functions run after the loop has\ncompleted, so all observe the final value of v [\"\n\nThis checker provides suggested fixes for type errors of the\ntype \"undeclared name: <>\". It will either insert a new statement,\nsuch as:\n\n\t<> :=\n\nor a new function declaration, such as:\n\n\tfunc <>(inferred parameters) {\n\t\tpanic(\"implement me!\")\n\t}", +- Default: "true", +- }, +- { +- Name: "\"unmarshal\"", +- Doc: "report passing non-pointer or non-interface values to unmarshal\n\nThe unmarshal analysis reports calls to functions such as json.Unmarshal\nin which the argument type is not a pointer or an interface.", +- Default: "true", +- }, +- { +- Name: "\"unreachable\"", +- Doc: "check for unreachable code\n\nThe unreachable analyzer finds statements that execution can never reach\nbecause they are preceded by an return statement, a call to panic, an\ninfinite loop, or similar constructs.", +- Default: "true", +- }, +- { +- Name: "\"unsafeptr\"", +- Doc: "check for invalid conversions of uintptr to unsafe.Pointer\n\nThe unsafeptr analyzer reports likely incorrect uses of unsafe.Pointer\nto convert integers to pointers. A conversion from uintptr to\nunsafe.Pointer is invalid if it implies that there is a uintptr-typed\nword in memory that holds a pointer value, because that word will be\ninvisible to stack copying and to the garbage collector.", +- Default: "true", +- }, +- { +- Name: "\"unusedparams\"", +- Doc: "check for unused parameters of functions\n\nThe unusedparams analyzer checks functions to see if there are\nany parameters that are not being used.\n\nTo ensure soundness, it ignores:\n - \"address-taken\" functions, that is, functions that are used as\n a value rather than being called directly; their signatures may\n be required to conform to a func type.\n - exported functions or methods, since they may be address-taken\n in another package.\n - unexported methods whose name matches an interface method\n declared in the same package, since the method's signature\n may be required to conform to the interface type.\n - functions with empty bodies, or containing just a call to panic.\n - parameters that are unnamed, or named \"_\", the blank identifier.\n\nThe analyzer suggests a fix of replacing the parameter name by \"_\",\nbut in such cases a deeper fix can be obtained by invoking the\n\"Refactor: remove unused parameter\" code action, which will\neliminate the parameter entirely, along with all corresponding\narguments at call sites, while taking care to preserve any side\neffects in the argument expressions; see\nhttps://github.com/golang/tools/releases/tag/gopls%2Fv0.14.", +- Default: "true", +- }, +- { +- Name: "\"unusedresult\"", +- Doc: "check for unused results of calls to some functions\n\nSome functions like fmt.Errorf return a result and have no side\neffects, so it is always a mistake to discard the result. Other\nfunctions may return an error that must not be ignored, or a cleanup\noperation that must be called. This analyzer reports calls to\nfunctions like these when the result of the call is ignored.\n\nThe set of functions may be controlled using flags.", +- Default: "true", +- }, +- { +- Name: "\"unusedvariable\"", +- Doc: "check for unused variables and suggest fixes", +- Default: "false", +- }, +- { +- Name: "\"unusedwrite\"", +- Doc: "checks for unused writes\n\nThe analyzer reports instances of writes to struct fields and\narrays that are never read. Specifically, when a struct object\nor an array is copied, its elements are copied implicitly by\nthe compiler, and any element write to this copy does nothing\nwith the original object.\n\nFor example:\n\n\ttype T struct { x int }\n\n\tfunc f(input []T) {\n\t\tfor i, v := range input { // v is a copy\n\t\t\tv.x = i // unused write to field x\n\t\t}\n\t}\n\nAnother example is about non-pointer receiver:\n\n\ttype T struct { x int }\n\n\tfunc (t T) f() { // t is a copy\n\t\tt.x = i // unused write to field x\n\t}", +- Default: "true", +- }, +- { +- Name: "\"useany\"", +- Doc: "check for constraints that could be simplified to \"any\"", +- Default: "false", +- }, +- }, +- }, +- Default: "{}", +- Hierarchy: "ui.diagnostic", +- }, +- { +- Name: "staticcheck", +- Type: "bool", +- Doc: "staticcheck enables additional analyses from staticcheck.io.\nThese analyses are documented on\n[Staticcheck's website](https://staticcheck.io/docs/checks/).\n", +- Default: "false", +- Status: "experimental", +- Hierarchy: "ui.diagnostic", +- }, +- { +- Name: "annotations", +- Type: "map[string]bool", +- Doc: "annotations specifies the various kinds of optimization diagnostics\nthat should be reported by the gc_details command.\n", +- EnumKeys: EnumKeys{ +- ValueType: "bool", +- Keys: []EnumKey{ +- { +- Name: "\"bounds\"", +- Doc: "`\"bounds\"` controls bounds checking diagnostics.\n", +- Default: "true", +- }, +- { +- Name: "\"escape\"", +- Doc: "`\"escape\"` controls diagnostics about escape choices.\n", +- Default: "true", +- }, +- { +- Name: "\"inline\"", +- Doc: "`\"inline\"` controls diagnostics about inlining choices.\n", +- Default: "true", +- }, +- { +- Name: "\"nil\"", +- Doc: "`\"nil\"` controls nil checks.\n", +- Default: "true", +- }, +- }, +- }, +- Default: "{\"bounds\":true,\"escape\":true,\"inline\":true,\"nil\":true}", +- Status: "experimental", +- Hierarchy: "ui.diagnostic", +- }, +- { +- Name: "vulncheck", +- Type: "enum", +- Doc: "vulncheck enables vulnerability scanning.\n", +- EnumValues: []EnumValue{ +- { +- Value: "\"Imports\"", +- Doc: "`\"Imports\"`: In Imports mode, `gopls` will report vulnerabilities that affect packages\ndirectly and indirectly used by the analyzed main module.\n", +- }, +- { +- Value: "\"Off\"", +- Doc: "`\"Off\"`: Disable vulnerability analysis.\n", +- }, +- }, +- Default: "\"Off\"", +- Status: "experimental", +- Hierarchy: "ui.diagnostic", +- }, +- { +- Name: "diagnosticsDelay", +- Type: "time.Duration", +- Doc: "diagnosticsDelay controls the amount of time that gopls waits\nafter the most recent file modification before computing deep diagnostics.\nSimple diagnostics (parsing and type-checking) are always run immediately\non recently modified packages.\n\nThis option must be set to a valid duration string, for example `\"250ms\"`.\n", +- Default: "\"1s\"", +- Status: "advanced", +- Hierarchy: "ui.diagnostic", +- }, +- { +- Name: "diagnosticsTrigger", +- Type: "enum", +- Doc: "diagnosticsTrigger controls when to run diagnostics.\n", +- EnumValues: []EnumValue{ +- { +- Value: "\"Edit\"", +- Doc: "`\"Edit\"`: Trigger diagnostics on file edit and save. (default)\n", +- }, +- { +- Value: "\"Save\"", +- Doc: "`\"Save\"`: Trigger diagnostics only on file save. Events like initial workspace load\nor configuration change will still trigger diagnostics.\n", +- }, +- }, +- Default: "\"Edit\"", +- Status: "experimental", +- Hierarchy: "ui.diagnostic", +- }, +- { +- Name: "analysisProgressReporting", +- Type: "bool", +- Doc: "analysisProgressReporting controls whether gopls sends progress\nnotifications when construction of its index of analysis facts is taking a\nlong time. Cancelling these notifications will cancel the indexing task,\nthough it will restart after the next change in the workspace.\n\nWhen a package is opened for the first time and heavyweight analyses such as\nstaticcheck are enabled, it can take a while to construct the index of\nanalysis facts for all its dependencies. The index is cached in the\nfilesystem, so subsequent analysis should be faster.\n", +- Default: "true", +- Hierarchy: "ui.diagnostic", +- }, +- { +- Name: "hints", +- Type: "map[string]bool", +- Doc: "hints specify inlay hints that users want to see. A full list of hints\nthat gopls uses can be found in\n[inlayHints.md](https://github.com/golang/tools/blob/master/gopls/doc/inlayHints.md).\n", +- EnumKeys: EnumKeys{Keys: []EnumKey{ +- { +- Name: "\"assignVariableTypes\"", +- Doc: "Enable/disable inlay hints for variable types in assign statements:\n```go\n\ti/* int*/, j/* int*/ := 0, len(r)-1\n```", +- Default: "false", +- }, +- { +- Name: "\"compositeLiteralFields\"", +- Doc: "Enable/disable inlay hints for composite literal field names:\n```go\n\t{/*in: */\"Hello, world\", /*want: */\"dlrow ,olleH\"}\n```", +- Default: "false", +- }, +- { +- Name: "\"compositeLiteralTypes\"", +- Doc: "Enable/disable inlay hints for composite literal types:\n```go\n\tfor _, c := range []struct {\n\t\tin, want string\n\t}{\n\t\t/*struct{ in string; want string }*/{\"Hello, world\", \"dlrow ,olleH\"},\n\t}\n```", +- Default: "false", +- }, +- { +- Name: "\"constantValues\"", +- Doc: "Enable/disable inlay hints for constant values:\n```go\n\tconst (\n\t\tKindNone Kind = iota/* = 0*/\n\t\tKindPrint/* = 1*/\n\t\tKindPrintf/* = 2*/\n\t\tKindErrorf/* = 3*/\n\t)\n```", +- Default: "false", +- }, +- { +- Name: "\"functionTypeParameters\"", +- Doc: "Enable/disable inlay hints for implicit type parameters on generic functions:\n```go\n\tmyFoo/*[int, string]*/(1, \"hello\")\n```", +- Default: "false", +- }, +- { +- Name: "\"parameterNames\"", +- Doc: "Enable/disable inlay hints for parameter names:\n```go\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)\n```", +- Default: "false", +- }, +- { +- Name: "\"rangeVariableTypes\"", +- Doc: "Enable/disable inlay hints for variable types in range statements:\n```go\n\tfor k/* int*/, v/* string*/ := range []string{} {\n\t\tfmt.Println(k, v)\n\t}\n```", +- Default: "false", +- }, +- }}, +- Default: "{}", +- Status: "experimental", +- Hierarchy: "ui.inlayhint", +- }, +- { +- Name: "codelenses", +- Type: "map[string]bool", +- Doc: "codelenses overrides the enabled/disabled state of code lenses. See the\n\"Code Lenses\" section of the\n[Settings page](https://github.com/golang/tools/blob/master/gopls/doc/settings.md#code-lenses)\nfor the list of supported lenses.\n\nExample Usage:\n\n```json5\n\"gopls\": {\n...\n \"codelenses\": {\n \"generate\": false, // Don't show the `go generate` lens.\n \"gc_details\": true // Show a code lens toggling the display of gc's choices.\n }\n...\n}\n```\n", +- EnumKeys: EnumKeys{ +- ValueType: "bool", +- Keys: []EnumKey{ +- { +- Name: "\"gc_details\"", +- Doc: "Toggle the calculation of gc annotations.", +- Default: "false", +- }, +- { +- Name: "\"generate\"", +- Doc: "Runs `go generate` for a given directory.", +- Default: "true", +- }, +- { +- Name: "\"regenerate_cgo\"", +- Doc: "Regenerates cgo definitions.", +- Default: "true", +- }, +- { +- Name: "\"run_govulncheck\"", +- Doc: "Run vulnerability check (`govulncheck`).", +- Default: "false", +- }, +- { +- Name: "\"test\"", +- Doc: "Runs `go test` for a specific set of test or benchmark functions.", +- Default: "false", +- }, +- { +- Name: "\"tidy\"", +- Doc: "Runs `go mod tidy` for a module.", +- Default: "true", +- }, +- { +- Name: "\"upgrade_dependency\"", +- Doc: "Upgrades a dependency in the go.mod file for a module.", +- Default: "true", +- }, +- { +- Name: "\"vendor\"", +- Doc: "Runs `go mod vendor` for a module.", +- Default: "true", +- }, +- }, +- }, +- Default: "{\"gc_details\":false,\"generate\":true,\"regenerate_cgo\":true,\"tidy\":true,\"upgrade_dependency\":true,\"vendor\":true}", +- Hierarchy: "ui", +- }, +- { +- Name: "semanticTokens", +- Type: "bool", +- Doc: "semanticTokens controls whether the LSP server will send\nsemantic tokens to the client.\n", +- Default: "false", +- Status: "experimental", +- Hierarchy: "ui", +- }, +- { +- Name: "noSemanticString", +- Type: "bool", +- Doc: "noSemanticString turns off the sending of the semantic token 'string'\n", +- Default: "false", +- Status: "experimental", +- Hierarchy: "ui", +- }, +- { +- Name: "noSemanticNumber", +- Type: "bool", +- Doc: "noSemanticNumber turns off the sending of the semantic token 'number'\n", +- Default: "false", +- Status: "experimental", +- Hierarchy: "ui", +- }, +- { +- Name: "local", +- Type: "string", +- Doc: "local is the equivalent of the `goimports -local` flag, which puts\nimports beginning with this string after third-party packages. It should\nbe the prefix of the import path whose imports should be grouped\nseparately.\n", +- Default: "\"\"", +- Hierarchy: "formatting", +- }, +- { +- Name: "gofumpt", +- Type: "bool", +- Doc: "gofumpt indicates if we should run gofumpt formatting.\n", +- Default: "false", +- Hierarchy: "formatting", +- }, +- { +- Name: "verboseOutput", +- Type: "bool", +- Doc: "verboseOutput enables additional debug logging.\n", +- Default: "false", +- Status: "debug", +- }, +- }, +- }, +- Commands: []*CommandJSON{ +- { +- Command: "gopls.add_dependency", +- Title: "Add a dependency", +- Doc: "Adds a dependency to the go.mod file for a module.", +- ArgDoc: "{\n\t// The go.mod file URI.\n\t\"URI\": string,\n\t// Additional args to pass to the go command.\n\t\"GoCmdArgs\": []string,\n\t// Whether to add a require directive.\n\t\"AddRequire\": bool,\n}", +- }, +- { +- Command: "gopls.add_import", +- Title: "Add an import", +- Doc: "Ask the server to add an import path to a given Go file. The method will\ncall applyEdit on the client so that clients don't have to apply the edit\nthemselves.", +- ArgDoc: "{\n\t// ImportPath is the target import path that should\n\t// be added to the URI file\n\t\"ImportPath\": string,\n\t// URI is the file that the ImportPath should be\n\t// added to\n\t\"URI\": string,\n}", +- }, +- { +- Command: "gopls.add_telemetry_counters", +- Title: "Update the given telemetry counters", +- Doc: "Gopls will prepend \"fwd/\" to all the counters updated using this command\nto avoid conflicts with other counters gopls collects.", +- ArgDoc: "{\n\t// Names and Values must have the same length.\n\t\"Names\": []string,\n\t\"Values\": []int64,\n}", +- }, +- { +- Command: "gopls.apply_fix", +- Title: "Apply a fix", +- Doc: "Applies a fix to a region of source code.", +- ArgDoc: "{\n\t// The name of the fix to apply.\n\t//\n\t// For fixes suggested by analyzers, this is a string constant\n\t// advertised by the analyzer that matches the Category of\n\t// the analysis.Diagnostic with a SuggestedFix containing no edits.\n\t//\n\t// For fixes suggested by code actions, this is a string agreed\n\t// upon by the code action and golang.ApplyFix.\n\t\"Fix\": string,\n\t// The file URI for the document to fix.\n\t\"URI\": string,\n\t// The document range to scan for fixes.\n\t\"Range\": {\n\t\t\"start\": {\n\t\t\t\"line\": uint32,\n\t\t\t\"character\": uint32,\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": uint32,\n\t\t\t\"character\": uint32,\n\t\t},\n\t},\n\t// Whether to resolve and return the edits.\n\t\"ResolveEdits\": bool,\n}", +- ResultDoc: "{\n\t// Holds changes to existing resources.\n\t\"changes\": map[golang.org/x/tools/gopls/internal/protocol.DocumentURI][]golang.org/x/tools/gopls/internal/protocol.TextEdit,\n\t// Depending on the client capability `workspace.workspaceEdit.resourceOperations` document changes\n\t// are either an array of `TextDocumentEdit`s to express changes to n different text documents\n\t// where each text document edit addresses a specific version of a text document. Or it can contain\n\t// above `TextDocumentEdit`s mixed with create, rename and delete file / folder operations.\n\t//\n\t// Whether a client supports versioned document edits is expressed via\n\t// `workspace.workspaceEdit.documentChanges` client capability.\n\t//\n\t// If a client neither supports `documentChanges` nor `workspace.workspaceEdit.resourceOperations` then\n\t// only plain `TextEdit`s using the `changes` property are supported.\n\t\"documentChanges\": []{\n\t\t\"TextDocumentEdit\": {\n\t\t\t\"textDocument\": { ... },\n\t\t\t\"edits\": { ... },\n\t\t},\n\t\t\"RenameFile\": {\n\t\t\t\"kind\": string,\n\t\t\t\"oldUri\": string,\n\t\t\t\"newUri\": string,\n\t\t\t\"options\": { ... },\n\t\t\t\"ResourceOperation\": { ... },\n\t\t},\n\t},\n\t// A map of change annotations that can be referenced in `AnnotatedTextEdit`s or create, rename and\n\t// delete file / folder operations.\n\t//\n\t// Whether clients honor this property depends on the client capability `workspace.changeAnnotationSupport`.\n\t//\n\t// @since 3.16.0\n\t\"changeAnnotations\": map[string]golang.org/x/tools/gopls/internal/protocol.ChangeAnnotation,\n}", +- }, +- { +- Command: "gopls.change_signature", +- Title: "Perform a \"change signature\" refactoring", +- Doc: "This command is experimental, currently only supporting parameter removal.\nIts signature will certainly change in the future (pun intended).", +- ArgDoc: "{\n\t\"RemoveParameter\": {\n\t\t\"uri\": string,\n\t\t\"range\": {\n\t\t\t\"start\": { ... },\n\t\t\t\"end\": { ... },\n\t\t},\n\t},\n\t// Whether to resolve and return the edits.\n\t\"ResolveEdits\": bool,\n}", +- ResultDoc: "{\n\t// Holds changes to existing resources.\n\t\"changes\": map[golang.org/x/tools/gopls/internal/protocol.DocumentURI][]golang.org/x/tools/gopls/internal/protocol.TextEdit,\n\t// Depending on the client capability `workspace.workspaceEdit.resourceOperations` document changes\n\t// are either an array of `TextDocumentEdit`s to express changes to n different text documents\n\t// where each text document edit addresses a specific version of a text document. Or it can contain\n\t// above `TextDocumentEdit`s mixed with create, rename and delete file / folder operations.\n\t//\n\t// Whether a client supports versioned document edits is expressed via\n\t// `workspace.workspaceEdit.documentChanges` client capability.\n\t//\n\t// If a client neither supports `documentChanges` nor `workspace.workspaceEdit.resourceOperations` then\n\t// only plain `TextEdit`s using the `changes` property are supported.\n\t\"documentChanges\": []{\n\t\t\"TextDocumentEdit\": {\n\t\t\t\"textDocument\": { ... },\n\t\t\t\"edits\": { ... },\n\t\t},\n\t\t\"RenameFile\": {\n\t\t\t\"kind\": string,\n\t\t\t\"oldUri\": string,\n\t\t\t\"newUri\": string,\n\t\t\t\"options\": { ... },\n\t\t\t\"ResourceOperation\": { ... },\n\t\t},\n\t},\n\t// A map of change annotations that can be referenced in `AnnotatedTextEdit`s or create, rename and\n\t// delete file / folder operations.\n\t//\n\t// Whether clients honor this property depends on the client capability `workspace.changeAnnotationSupport`.\n\t//\n\t// @since 3.16.0\n\t\"changeAnnotations\": map[string]golang.org/x/tools/gopls/internal/protocol.ChangeAnnotation,\n}", +- }, +- { +- Command: "gopls.check_upgrades", +- Title: "Check for upgrades", +- Doc: "Checks for module upgrades.", +- ArgDoc: "{\n\t// The go.mod file URI.\n\t\"URI\": string,\n\t// The modules to check.\n\t\"Modules\": []string,\n}", +- }, +- { +- Command: "gopls.diagnose_files", +- Title: "Cause server to publish diagnostics for the specified files.", +- Doc: "This command is needed by the 'gopls {check,fix}' CLI subcommands.", +- ArgDoc: "{\n\t\"Files\": []string,\n}", +- }, +- { +- Command: "gopls.doc", +- Title: "View package documentation.", +- Doc: "Opens the Go package documentation page for the current\npackage in a browser.", +- ArgDoc: "{\n\t\"uri\": string,\n\t\"range\": {\n\t\t\"start\": {\n\t\t\t\"line\": uint32,\n\t\t\t\"character\": uint32,\n\t\t},\n\t\t\"end\": {\n\t\t\t\"line\": uint32,\n\t\t\t\"character\": uint32,\n\t\t},\n\t},\n}", +- }, +- { +- Command: "gopls.edit_go_directive", +- Title: "Run go mod edit -go=version", +- Doc: "Runs `go mod edit -go=version` for a module.", +- ArgDoc: "{\n\t// Any document URI within the relevant module.\n\t\"URI\": string,\n\t// The version to pass to `go mod edit -go`.\n\t\"Version\": string,\n}", +- }, +- { +- Command: "gopls.fetch_vulncheck_result", +- Title: "Get known vulncheck result", +- Doc: "Fetch the result of latest vulnerability check (`govulncheck`).", +- ArgDoc: "{\n\t// The file URI.\n\t\"URI\": string,\n}", +- ResultDoc: "map[golang.org/x/tools/gopls/internal/protocol.DocumentURI]*golang.org/x/tools/gopls/internal/vulncheck.Result", +- }, +- { +- Command: "gopls.gc_details", +- Title: "Toggle gc_details", +- Doc: "Toggle the calculation of gc annotations.", +- ArgDoc: "string", +- }, +- { +- Command: "gopls.generate", +- Title: "Run go generate", +- Doc: "Runs `go generate` for a given directory.", +- ArgDoc: "{\n\t// URI for the directory to generate.\n\t\"Dir\": string,\n\t// Whether to generate recursively (go generate ./...)\n\t\"Recursive\": bool,\n}", +- }, +- { +- Command: "gopls.go_get_package", +- Title: "'go get' a package", +- Doc: "Runs `go get` to fetch a package.", +- ArgDoc: "{\n\t// Any document URI within the relevant module.\n\t\"URI\": string,\n\t// The package to go get.\n\t\"Pkg\": string,\n\t\"AddRequire\": bool,\n}", +- }, +- { +- Command: "gopls.list_imports", +- Title: "List imports of a file and its package", +- Doc: "Retrieve a list of imports in the given Go file, and the package it\nbelongs to.", +- ArgDoc: "{\n\t// The file URI.\n\t\"URI\": string,\n}", +- ResultDoc: "{\n\t// Imports is a list of imports in the requested file.\n\t\"Imports\": []{\n\t\t\"Path\": string,\n\t\t\"Name\": string,\n\t},\n\t// PackageImports is a list of all imports in the requested file's package.\n\t\"PackageImports\": []{\n\t\t\"Path\": string,\n\t},\n}", +- }, +- { +- Command: "gopls.list_known_packages", +- Title: "List known packages", +- Doc: "Retrieve a list of packages that are importable from the given URI.", +- ArgDoc: "{\n\t// The file URI.\n\t\"URI\": string,\n}", +- ResultDoc: "{\n\t// Packages is a list of packages relative\n\t// to the URIArg passed by the command request.\n\t// In other words, it omits paths that are already\n\t// imported or cannot be imported due to compiler\n\t// restrictions.\n\t\"Packages\": []string,\n}", +- }, +- { +- Command: "gopls.maybe_prompt_for_telemetry", +- Title: "Prompt user to enable telemetry", +- Doc: "Checks for the right conditions, and then prompts the user\nto ask if they want to enable Go telemetry uploading. If\nthe user responds 'Yes', the telemetry mode is set to \"on\".", +- }, +- { +- Command: "gopls.mem_stats", +- Title: "Fetch memory statistics", +- Doc: "Call runtime.GC multiple times and return memory statistics as reported by\nruntime.MemStats.\n\nThis command is used for benchmarking, and may change in the future.", +- ResultDoc: "{\n\t\"HeapAlloc\": uint64,\n\t\"HeapInUse\": uint64,\n\t\"TotalAlloc\": uint64,\n}", +- }, +- { +- Command: "gopls.regenerate_cgo", +- Title: "Regenerate cgo", +- Doc: "Regenerates cgo definitions.", +- ArgDoc: "{\n\t// The file URI.\n\t\"URI\": string,\n}", +- }, +- { +- Command: "gopls.remove_dependency", +- Title: "Remove a dependency", +- Doc: "Removes a dependency from the go.mod file of a module.", +- ArgDoc: "{\n\t// The go.mod file URI.\n\t\"URI\": string,\n\t// The module path to remove.\n\t\"ModulePath\": string,\n\t// If the module is tidied apart from the one unused diagnostic, we can\n\t// run `go get module@none`, and then run `go mod tidy`. Otherwise, we\n\t// must make textual edits.\n\t\"OnlyDiagnostic\": bool,\n}", +- }, +- { +- Command: "gopls.reset_go_mod_diagnostics", +- Title: "Reset go.mod diagnostics", +- Doc: "Reset diagnostics in the go.mod file of a module.", +- ArgDoc: "{\n\t\"URIArg\": {\n\t\t\"URI\": string,\n\t},\n\t// Optional: source of the diagnostics to reset.\n\t// If not set, all resettable go.mod diagnostics will be cleared.\n\t\"DiagnosticSource\": string,\n}", +- }, +- { +- Command: "gopls.run_go_work_command", +- Title: "Run `go work [args...]`, and apply the resulting go.work", +- Doc: "edits to the current go.work file", +- ArgDoc: "{\n\t\"ViewID\": string,\n\t\"InitFirst\": bool,\n\t\"Args\": []string,\n}", +- }, +- { +- Command: "gopls.run_govulncheck", +- Title: "Run vulncheck", +- Doc: "Run vulnerability check (`govulncheck`).", +- ArgDoc: "{\n\t// Any document in the directory from which govulncheck will run.\n\t\"URI\": string,\n\t// Package pattern. E.g. \"\", \".\", \"./...\".\n\t\"Pattern\": string,\n}", +- ResultDoc: "{\n\t// Token holds the progress token for LSP workDone reporting of the vulncheck\n\t// invocation.\n\t\"Token\": interface{},\n}", +- }, +- { +- Command: "gopls.run_tests", +- Title: "Run test(s)", +- Doc: "Runs `go test` for a specific set of test or benchmark functions.", +- ArgDoc: "{\n\t// The test file containing the tests to run.\n\t\"URI\": string,\n\t// Specific test names to run, e.g. TestFoo.\n\t\"Tests\": []string,\n\t// Specific benchmarks to run, e.g. BenchmarkFoo.\n\t\"Benchmarks\": []string,\n}", +- }, +- { +- Command: "gopls.start_debugging", +- Title: "Start the gopls debug server", +- Doc: "Start the gopls debug server if it isn't running, and return the debug\naddress.", +- ArgDoc: "{\n\t// Optional: the address (including port) for the debug server to listen on.\n\t// If not provided, the debug server will bind to \"localhost:0\", and the\n\t// full debug URL will be contained in the result.\n\t//\n\t// If there is more than one gopls instance along the serving path (i.e. you\n\t// are using a daemon), each gopls instance will attempt to start debugging.\n\t// If Addr specifies a port, only the daemon will be able to bind to that\n\t// port, and each intermediate gopls instance will fail to start debugging.\n\t// For this reason it is recommended not to specify a port (or equivalently,\n\t// to specify \":0\").\n\t//\n\t// If the server was already debugging this field has no effect, and the\n\t// result will contain the previously configured debug URL(s).\n\t\"Addr\": string,\n}", +- ResultDoc: "{\n\t// The URLs to use to access the debug servers, for all gopls instances in\n\t// the serving path. For the common case of a single gopls instance (i.e. no\n\t// daemon), this will be exactly one address.\n\t//\n\t// In the case of one or more gopls instances forwarding the LSP to a daemon,\n\t// URLs will contain debug addresses for each server in the serving path, in\n\t// serving order. The daemon debug address will be the last entry in the\n\t// slice. If any intermediate gopls instance fails to start debugging, no\n\t// error will be returned but the debug URL for that server in the URLs slice\n\t// will be empty.\n\t\"URLs\": []string,\n}", +- }, +- { +- Command: "gopls.start_profile", +- Title: "Start capturing a profile of gopls' execution", +- Doc: "Start a new pprof profile. Before using the resulting file, profiling must\nbe stopped with a corresponding call to StopProfile.\n\nThis command is intended for internal use only, by the gopls benchmark\nrunner.", +- ArgDoc: "struct{}", +- ResultDoc: "struct{}", +- }, +- { +- Command: "gopls.stop_profile", +- Title: "Stop an ongoing profile", +- Doc: "This command is intended for internal use only, by the gopls benchmark\nrunner.", +- ArgDoc: "struct{}", +- ResultDoc: "{\n\t// File is the profile file name.\n\t\"File\": string,\n}", +- }, +- { +- Command: "gopls.test", +- Title: "Run test(s) (legacy)", +- Doc: "Runs `go test` for a specific set of test or benchmark functions.", +- ArgDoc: "string,\n[]string,\n[]string", +- }, +- { +- Command: "gopls.tidy", +- Title: "Run go mod tidy", +- Doc: "Runs `go mod tidy` for a module.", +- ArgDoc: "{\n\t// The file URIs.\n\t\"URIs\": []string,\n}", +- }, +- { +- Command: "gopls.toggle_gc_details", +- Title: "Toggle gc_details", +- Doc: "Toggle the calculation of gc annotations.", +- ArgDoc: "{\n\t// The file URI.\n\t\"URI\": string,\n}", +- }, +- { +- Command: "gopls.update_go_sum", +- Title: "Update go.sum", +- Doc: "Updates the go.sum file for a module.", +- ArgDoc: "{\n\t// The file URIs.\n\t\"URIs\": []string,\n}", +- }, +- { +- Command: "gopls.upgrade_dependency", +- Title: "Upgrade a dependency", +- Doc: "Upgrades a dependency in the go.mod file for a module.", +- ArgDoc: "{\n\t// The go.mod file URI.\n\t\"URI\": string,\n\t// Additional args to pass to the go command.\n\t\"GoCmdArgs\": []string,\n\t// Whether to add a require directive.\n\t\"AddRequire\": bool,\n}", +- }, +- { +- Command: "gopls.vendor", +- Title: "Run go mod vendor", +- Doc: "Runs `go mod vendor` for a module.", +- ArgDoc: "{\n\t// The file URI.\n\t\"URI\": string,\n}", +- }, +- { +- Command: "gopls.views", +- Title: "List current Views on the server.", +- Doc: "This command is intended for use by gopls tests only.", +- ResultDoc: "[]{\n\t\"Type\": string,\n\t\"Root\": string,\n\t\"Folder\": string,\n\t\"EnvOverlay\": []string,\n}", +- }, +- { +- Command: "gopls.workspace_stats", +- Title: "Fetch workspace statistics", +- Doc: "Query statistics about workspace builds, modules, packages, and files.\n\nThis command is intended for internal use only, by the gopls stats\ncommand.", +- ResultDoc: "{\n\t\"Files\": {\n\t\t\"Total\": int,\n\t\t\"Largest\": int,\n\t\t\"Errs\": int,\n\t},\n\t\"Views\": []{\n\t\t\"GoCommandVersion\": string,\n\t\t\"AllPackages\": {\n\t\t\t\"Packages\": int,\n\t\t\t\"LargestPackage\": int,\n\t\t\t\"CompiledGoFiles\": int,\n\t\t\t\"Modules\": int,\n\t\t},\n\t\t\"WorkspacePackages\": {\n\t\t\t\"Packages\": int,\n\t\t\t\"LargestPackage\": int,\n\t\t\t\"CompiledGoFiles\": int,\n\t\t\t\"Modules\": int,\n\t\t},\n\t\t\"Diagnostics\": int,\n\t},\n}", +- }, +- }, +- Lenses: []*LensJSON{ +- { +- Lens: "gc_details", +- Title: "Toggle gc_details", +- Doc: "Toggle the calculation of gc annotations.", +- }, +- { +- Lens: "generate", +- Title: "Run go generate", +- Doc: "Runs `go generate` for a given directory.", +- }, +- { +- Lens: "regenerate_cgo", +- Title: "Regenerate cgo", +- Doc: "Regenerates cgo definitions.", +- }, +- { +- Lens: "run_govulncheck", +- Title: "Run vulncheck", +- Doc: "Run vulnerability check (`govulncheck`).", +- }, +- { +- Lens: "test", +- Title: "Run test(s) (legacy)", +- Doc: "Runs `go test` for a specific set of test or benchmark functions.", +- }, +- { +- Lens: "tidy", +- Title: "Run go mod tidy", +- Doc: "Runs `go mod tidy` for a module.", +- }, +- { +- Lens: "upgrade_dependency", +- Title: "Upgrade a dependency", +- Doc: "Upgrades a dependency in the go.mod file for a module.", +- }, +- { +- Lens: "vendor", +- Title: "Run go mod vendor", +- Doc: "Runs `go mod vendor` for a module.", +- }, +- }, +- Analyzers: []*AnalyzerJSON{ +- { +- Name: "appends", +- Doc: "check for missing values after append\n\nThis checker reports calls to append that pass\nno values to be appended to the slice.\n\n\ts := []string{\"a\", \"b\", \"c\"}\n\t_ = append(s)\n\nSuch calls are always no-ops and often indicate an\nunderlying mistake.", +- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/appends", +- Default: true, +- }, +- { +- Name: "asmdecl", +- Doc: "report mismatches between assembly files and Go declarations", +- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/asmdecl", +- Default: true, +- }, +- { +- Name: "assign", +- Doc: "check for useless assignments\n\nThis checker reports assignments of the form x = x or a[i] = a[i].\nThese are almost always useless, and even when they aren't they are\nusually a mistake.", +- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/assign", +- Default: true, +- }, +- { +- Name: "atomic", +- Doc: "check for common mistakes using the sync/atomic package\n\nThe atomic checker looks for assignment statements of the form:\n\n\tx = atomic.AddUint64(&x, 1)\n\nwhich are not atomic.", +- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/atomic", +- Default: true, +- }, +- { +- Name: "atomicalign", +- Doc: "check for non-64-bits-aligned arguments to sync/atomic functions", +- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/atomicalign", +- Default: true, +- }, +- { +- Name: "bools", +- Doc: "check for common mistakes involving boolean operators", +- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/bools", +- Default: true, +- }, +- { +- Name: "buildtag", +- Doc: "check //go:build and // +build directives", +- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/buildtag", +- Default: true, +- }, +- { +- Name: "cgocall", +- Doc: "detect some violations of the cgo pointer passing rules\n\nCheck for invalid cgo pointer passing.\nThis looks for code that uses cgo to call C code passing values\nwhose types are almost always invalid according to the cgo pointer\nsharing rules.\nSpecifically, it warns about attempts to pass a Go chan, map, func,\nor slice to C, either directly, or via a pointer, array, or struct.", +- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/cgocall", +- Default: true, +- }, +- { +- Name: "composites", +- Doc: "check for unkeyed composite literals\n\nThis analyzer reports a diagnostic for composite literals of struct\ntypes imported from another package that do not use the field-keyed\nsyntax. Such literals are fragile because the addition of a new field\n(even if unexported) to the struct will cause compilation to fail.\n\nAs an example,\n\n\terr = &net.DNSConfigError{err}\n\nshould be replaced by:\n\n\terr = &net.DNSConfigError{Err: err}\n", +- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/composite", +- Default: true, +- }, +- { +- Name: "copylocks", +- Doc: "check for locks erroneously passed by value\n\nInadvertently copying a value containing a lock, such as sync.Mutex or\nsync.WaitGroup, may cause both copies to malfunction. Generally such\nvalues should be referred to through a pointer.", +- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/copylocks", +- Default: true, +- }, +- { +- Name: "deepequalerrors", +- Doc: "check for calls of reflect.DeepEqual on error values\n\nThe deepequalerrors checker looks for calls of the form:\n\n reflect.DeepEqual(err1, err2)\n\nwhere err1 and err2 are errors. Using reflect.DeepEqual to compare\nerrors is discouraged.", +- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/deepequalerrors", +- Default: true, +- }, +- { +- Name: "defers", +- Doc: "report common mistakes in defer statements\n\nThe defers analyzer reports a diagnostic when a defer statement would\nresult in a non-deferred call to time.Since, as experience has shown\nthat this is nearly always a mistake.\n\nFor example:\n\n\tstart := time.Now()\n\t...\n\tdefer recordLatency(time.Since(start)) // error: call to time.Since is not deferred\n\nThe correct code is:\n\n\tdefer func() { recordLatency(time.Since(start)) }()", +- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/defers", +- Default: true, +- }, +- { +- Name: "deprecated", +- Doc: "check for use of deprecated identifiers\n\nThe deprecated analyzer looks for deprecated symbols and package\nimports.\n\nSee https://go.dev/wiki/Deprecated to learn about Go's convention\nfor documenting and signaling deprecated identifiers.", +- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/deprecated", +- Default: true, +- }, +- { +- Name: "directive", +- Doc: "check Go toolchain directives such as //go:debug\n\nThis analyzer checks for problems with known Go toolchain directives\nin all Go source files in a package directory, even those excluded by\n//go:build constraints, and all non-Go source files too.\n\nFor //go:debug (see https://go.dev/doc/godebug), the analyzer checks\nthat the directives are placed only in Go source files, only above the\npackage comment, and only in package main or *_test.go files.\n\nSupport for other known directives may be added in the future.\n\nThis analyzer does not check //go:build, which is handled by the\nbuildtag analyzer.\n", +- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/directive", +- Default: true, +- }, +- { +- Name: "embed", +- Doc: "check //go:embed directive usage\n\nThis analyzer checks that the embed package is imported if //go:embed\ndirectives are present, providing a suggested fix to add the import if\nit is missing.\n\nThis analyzer also checks that //go:embed directives precede the\ndeclaration of a single variable.", +- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/embeddirective", +- Default: true, +- }, +- { +- Name: "errorsas", +- Doc: "report passing non-pointer or non-error values to errors.As\n\nThe errorsas analysis reports calls to errors.As where the type\nof the second argument is not a pointer to a type implementing error.", +- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/errorsas", +- Default: true, +- }, +- { +- Name: "fieldalignment", +- Doc: "find structs that would use less memory if their fields were sorted\n\nThis analyzer find structs that can be rearranged to use less memory, and provides\na suggested edit with the most compact order.\n\nNote that there are two different diagnostics reported. One checks struct size,\nand the other reports \"pointer bytes\" used. Pointer bytes is how many bytes of the\nobject that the garbage collector has to potentially scan for pointers, for example:\n\n\tstruct { uint32; string }\n\nhave 16 pointer bytes because the garbage collector has to scan up through the string's\ninner pointer.\n\n\tstruct { string; *uint32 }\n\nhas 24 pointer bytes because it has to scan further through the *uint32.\n\n\tstruct { string; uint32 }\n\nhas 8 because it can stop immediately after the string pointer.\n\nBe aware that the most compact order is not always the most efficient.\nIn rare cases it may cause two variables each updated by its own goroutine\nto occupy the same CPU cache line, inducing a form of memory contention\nknown as \"false sharing\" that slows down both goroutines.\n", +- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/fieldalignment", +- }, +- { +- Name: "fillreturns", +- Doc: "suggest fixes for errors due to an incorrect number of return values\n\nThis checker provides suggested fixes for type errors of the\ntype \"wrong number of return values (want %d, got %d)\". For example:\n\n\tfunc m() (int, string, *bool, error) {\n\t\treturn\n\t}\n\nwill turn into\n\n\tfunc m() (int, string, *bool, error) {\n\t\treturn 0, \"\", nil, nil\n\t}\n\nThis functionality is similar to https://github.com/sqs/goreturns.", +- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/fillreturns", +- Default: true, +- }, +- { +- Name: "httpresponse", +- Doc: "check for mistakes using HTTP responses\n\nA common mistake when using the net/http package is to defer a function\ncall to close the http.Response Body before checking the error that\ndetermines whether the response is valid:\n\n\tresp, err := http.Head(url)\n\tdefer resp.Body.Close()\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\t// (defer statement belongs here)\n\nThis checker helps uncover latent nil dereference bugs by reporting a\ndiagnostic for such mistakes.", +- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/httpresponse", +- Default: true, +- }, +- { +- Name: "ifaceassert", +- Doc: "detect impossible interface-to-interface type assertions\n\nThis checker flags type assertions v.(T) and corresponding type-switch cases\nin which the static type V of v is an interface that cannot possibly implement\nthe target interface T. This occurs when V and T contain methods with the same\nname but different signatures. Example:\n\n\tvar v interface {\n\t\tRead()\n\t}\n\t_ = v.(io.Reader)\n\nThe Read method in v has a different signature than the Read method in\nio.Reader, so this assertion cannot succeed.", +- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/ifaceassert", +- Default: true, +- }, +- { +- Name: "infertypeargs", +- Doc: "check for unnecessary type arguments in call expressions\n\nExplicit type arguments may be omitted from call expressions if they can be\ninferred from function arguments, or from other type arguments:\n\n\tfunc f[T any](T) {}\n\t\n\tfunc _() {\n\t\tf[string](\"foo\") // string could be inferred\n\t}\n", +- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/infertypeargs", +- Default: true, +- }, +- { +- Name: "loopclosure", +- Doc: "check references to loop variables from within nested functions\n\nThis analyzer reports places where a function literal references the\niteration variable of an enclosing loop, and the loop calls the function\nin such a way (e.g. with go or defer) that it may outlive the loop\niteration and possibly observe the wrong value of the variable.\n\nNote: An iteration variable can only outlive a loop iteration in Go versions <=1.21.\nIn Go 1.22 and later, the loop variable lifetimes changed to create a new\niteration variable per loop iteration. (See go.dev/issue/60078.)\n\nIn this example, all the deferred functions run after the loop has\ncompleted, so all observe the final value of v [\"\n\nThis checker provides suggested fixes for type errors of the\ntype \"undeclared name: <>\". It will either insert a new statement,\nsuch as:\n\n\t<> :=\n\nor a new function declaration, such as:\n\n\tfunc <>(inferred parameters) {\n\t\tpanic(\"implement me!\")\n\t}", +- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/undeclaredname", +- Default: true, +- }, +- { +- Name: "unmarshal", +- Doc: "report passing non-pointer or non-interface values to unmarshal\n\nThe unmarshal analysis reports calls to functions such as json.Unmarshal\nin which the argument type is not a pointer or an interface.", +- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unmarshal", +- Default: true, +- }, +- { +- Name: "unreachable", +- Doc: "check for unreachable code\n\nThe unreachable analyzer finds statements that execution can never reach\nbecause they are preceded by an return statement, a call to panic, an\ninfinite loop, or similar constructs.", +- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unreachable", +- Default: true, +- }, +- { +- Name: "unsafeptr", +- Doc: "check for invalid conversions of uintptr to unsafe.Pointer\n\nThe unsafeptr analyzer reports likely incorrect uses of unsafe.Pointer\nto convert integers to pointers. A conversion from uintptr to\nunsafe.Pointer is invalid if it implies that there is a uintptr-typed\nword in memory that holds a pointer value, because that word will be\ninvisible to stack copying and to the garbage collector.", +- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unsafeptr", +- Default: true, +- }, +- { +- Name: "unusedparams", +- Doc: "check for unused parameters of functions\n\nThe unusedparams analyzer checks functions to see if there are\nany parameters that are not being used.\n\nTo ensure soundness, it ignores:\n - \"address-taken\" functions, that is, functions that are used as\n a value rather than being called directly; their signatures may\n be required to conform to a func type.\n - exported functions or methods, since they may be address-taken\n in another package.\n - unexported methods whose name matches an interface method\n declared in the same package, since the method's signature\n may be required to conform to the interface type.\n - functions with empty bodies, or containing just a call to panic.\n - parameters that are unnamed, or named \"_\", the blank identifier.\n\nThe analyzer suggests a fix of replacing the parameter name by \"_\",\nbut in such cases a deeper fix can be obtained by invoking the\n\"Refactor: remove unused parameter\" code action, which will\neliminate the parameter entirely, along with all corresponding\narguments at call sites, while taking care to preserve any side\neffects in the argument expressions; see\nhttps://github.com/golang/tools/releases/tag/gopls%2Fv0.14.", +- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/unusedparams", +- Default: true, +- }, +- { +- Name: "unusedresult", +- Doc: "check for unused results of calls to some functions\n\nSome functions like fmt.Errorf return a result and have no side\neffects, so it is always a mistake to discard the result. Other\nfunctions may return an error that must not be ignored, or a cleanup\noperation that must be called. This analyzer reports calls to\nfunctions like these when the result of the call is ignored.\n\nThe set of functions may be controlled using flags.", +- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unusedresult", +- Default: true, +- }, +- { +- Name: "unusedvariable", +- Doc: "check for unused variables and suggest fixes", +- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/unusedvariable", +- }, +- { +- Name: "unusedwrite", +- Doc: "checks for unused writes\n\nThe analyzer reports instances of writes to struct fields and\narrays that are never read. Specifically, when a struct object\nor an array is copied, its elements are copied implicitly by\nthe compiler, and any element write to this copy does nothing\nwith the original object.\n\nFor example:\n\n\ttype T struct { x int }\n\n\tfunc f(input []T) {\n\t\tfor i, v := range input { // v is a copy\n\t\t\tv.x = i // unused write to field x\n\t\t}\n\t}\n\nAnother example is about non-pointer receiver:\n\n\ttype T struct { x int }\n\n\tfunc (t T) f() { // t is a copy\n\t\tt.x = i // unused write to field x\n\t}", +- URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/unusedwrite", +- Default: true, +- }, +- { +- Name: "useany", +- Doc: "check for constraints that could be simplified to \"any\"", +- URL: "https://pkg.go.dev/golang.org/x/tools/gopls/internal/analysis/useany", +- }, +- }, +- Hints: []*HintJSON{ +- { +- Name: "assignVariableTypes", +- Doc: "Enable/disable inlay hints for variable types in assign statements:\n```go\n\ti/* int*/, j/* int*/ := 0, len(r)-1\n```", +- }, +- { +- Name: "compositeLiteralFields", +- Doc: "Enable/disable inlay hints for composite literal field names:\n```go\n\t{/*in: */\"Hello, world\", /*want: */\"dlrow ,olleH\"}\n```", +- }, +- { +- Name: "compositeLiteralTypes", +- Doc: "Enable/disable inlay hints for composite literal types:\n```go\n\tfor _, c := range []struct {\n\t\tin, want string\n\t}{\n\t\t/*struct{ in string; want string }*/{\"Hello, world\", \"dlrow ,olleH\"},\n\t}\n```", +- }, +- { +- Name: "constantValues", +- Doc: "Enable/disable inlay hints for constant values:\n```go\n\tconst (\n\t\tKindNone Kind = iota/* = 0*/\n\t\tKindPrint/* = 1*/\n\t\tKindPrintf/* = 2*/\n\t\tKindErrorf/* = 3*/\n\t)\n```", +- }, +- { +- Name: "functionTypeParameters", +- Doc: "Enable/disable inlay hints for implicit type parameters on generic functions:\n```go\n\tmyFoo/*[int, string]*/(1, \"hello\")\n```", +- }, +- { +- Name: "parameterNames", +- Doc: "Enable/disable inlay hints for parameter names:\n```go\n\tparseInt(/* str: */ \"123\", /* radix: */ 8)\n```", +- }, +- { +- Name: "rangeVariableTypes", +- Doc: "Enable/disable inlay hints for variable types in range statements:\n```go\n\tfor k/* int*/, v/* string*/ := range []string{} {\n\t\tfmt.Println(k, v)\n\t}\n```", +- }, +- }, -} -diff -urN a/gopls/internal/lsp/source/inline.go b/gopls/internal/lsp/source/inline.go ---- a/gopls/internal/lsp/source/inline.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/inline.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,147 +0,0 @@ +diff -urN a/gopls/internal/settings/default.go b/gopls/internal/settings/default.go +--- a/gopls/internal/settings/default.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/settings/default.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,136 +0,0 @@ -// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package source -- --// This file defines the refactor.inline code action. +-package settings - -import ( -- "context" -- "fmt" -- "go/ast" -- "go/token" -- "go/types" -- "runtime/debug" +- "sync" +- "time" - -- "golang.org/x/tools/go/analysis" -- "golang.org/x/tools/go/ast/astutil" -- "golang.org/x/tools/go/types/typeutil" -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/diff" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/refactor/inline" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/command" -) - --// EnclosingStaticCall returns the innermost function call enclosing --// the selected range, along with the callee. --func EnclosingStaticCall(pkg Package, pgf *ParsedGoFile, rng protocol.Range) (*ast.CallExpr, *types.Func, error) { -- start, end, err := pgf.RangePos(rng) -- if err != nil { -- return nil, nil, err -- } -- path, _ := astutil.PathEnclosingInterval(pgf.File, start, end) -- -- var call *ast.CallExpr --loop: -- for _, n := range path { -- switch n := n.(type) { -- case *ast.FuncLit: -- break loop -- case *ast.CallExpr: -- call = n -- break loop -- } -- } -- if call == nil { -- return nil, nil, fmt.Errorf("no enclosing call") -- } -- if safetoken.Line(pgf.Tok, call.Lparen) != safetoken.Line(pgf.Tok, start) { -- return nil, nil, fmt.Errorf("enclosing call is not on this line") -- } -- fn := typeutil.StaticCallee(pkg.GetTypesInfo(), call) -- if fn == nil { -- return nil, nil, fmt.Errorf("not a static call to a Go function") -- } -- return call, fn, nil --} -- --func inlineCall(ctx context.Context, snapshot Snapshot, fh FileHandle, rng protocol.Range) (_ *token.FileSet, _ *analysis.SuggestedFix, err error) { -- // Find enclosing static call. -- callerPkg, callerPGF, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) -- if err != nil { -- return nil, nil, err -- } -- call, fn, err := EnclosingStaticCall(callerPkg, callerPGF, rng) -- if err != nil { -- return nil, nil, err -- } -- -- // Locate callee by file/line and analyze it. -- calleePosn := safetoken.StartPosition(callerPkg.FileSet(), fn.Pos()) -- calleePkg, calleePGF, err := NarrowestPackageForFile(ctx, snapshot, span.URIFromPath(calleePosn.Filename)) -- if err != nil { -- return nil, nil, err -- } -- var calleeDecl *ast.FuncDecl -- for _, decl := range calleePGF.File.Decls { -- if decl, ok := decl.(*ast.FuncDecl); ok { -- posn := safetoken.StartPosition(calleePkg.FileSet(), decl.Name.Pos()) -- if posn.Line == calleePosn.Line && posn.Column == calleePosn.Column { -- calleeDecl = decl -- break -- } -- } -- } -- if calleeDecl == nil { -- return nil, nil, fmt.Errorf("can't find callee") -- } -- -- // The inliner assumes that input is well-typed, -- // but that is frequently not the case within gopls. -- // Until we are able to harden the inliner, -- // report panics as errors to avoid crashing the server. -- bad := func(p Package) bool { return len(p.GetParseErrors())+len(p.GetTypeErrors()) > 0 } -- if bad(calleePkg) || bad(callerPkg) { -- defer func() { -- if x := recover(); x != nil { -- err = bug.Errorf("inlining failed unexpectedly: %v\nstack: %v", -- x, debug.Stack()) -- } -- }() -- } -- -- // Users can consult the gopls event log to see -- // why a particular inlining strategy was chosen. -- logf := logger(ctx, "inliner", snapshot.Options().VerboseOutput) -- -- callee, err := inline.AnalyzeCallee(logf, calleePkg.FileSet(), calleePkg.GetTypes(), calleePkg.GetTypesInfo(), calleeDecl, calleePGF.Src) -- if err != nil { -- return nil, nil, err -- } -- -- // Inline the call. -- caller := &inline.Caller{ -- Fset: callerPkg.FileSet(), -- Types: callerPkg.GetTypes(), -- Info: callerPkg.GetTypesInfo(), -- File: callerPGF.File, -- Call: call, -- Content: callerPGF.Src, -- } -- -- got, err := inline.Inline(logf, caller, callee) -- if err != nil { -- return nil, nil, err -- } -- -- // Suggest the fix. -- return callerPkg.FileSet(), &analysis.SuggestedFix{ -- Message: fmt.Sprintf("inline call of %v", callee), -- TextEdits: diffToTextEdits(callerPGF.Tok, diff.Bytes(callerPGF.Src, got)), -- }, nil --} -- --// TODO(adonovan): change the inliner to instead accept an io.Writer. --func logger(ctx context.Context, name string, verbose bool) func(format string, args ...any) { -- if verbose { -- return func(format string, args ...any) { -- event.Log(ctx, name+": "+fmt.Sprintf(format, args...)) -- } -- } else { -- return func(string, ...any) {} -- } --} -diff -urN a/gopls/internal/lsp/source/invertifcondition.go b/gopls/internal/lsp/source/invertifcondition.go ---- a/gopls/internal/lsp/source/invertifcondition.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/invertifcondition.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,268 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package source -- --import ( -- "fmt" -- "go/ast" -- "go/token" -- "go/types" -- "strings" -- -- "golang.org/x/tools/go/analysis" -- "golang.org/x/tools/go/ast/astutil" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/internal/typeparams" +-var ( +- optionsOnce sync.Once +- defaultOptions *Options -) - --// invertIfCondition is a singleFileFixFunc that inverts an if/else statement --func invertIfCondition(fset *token.FileSet, start, end token.Pos, src []byte, file *ast.File, _ *types.Package, _ *types.Info) (*analysis.SuggestedFix, error) { -- ifStatement, _, err := CanInvertIfCondition(file, start, end) -- if err != nil { -- return nil, err -- } -- -- var replaceElse analysis.TextEdit -- -- endsWithReturn, err := endsWithReturn(ifStatement.Else) -- if err != nil { -- return nil, err -- } -- -- if endsWithReturn { -- // Replace the whole else part with an empty line and an unindented -- // version of the original if body -- sourcePos := safetoken.StartPosition(fset, ifStatement.Pos()) -- -- indent := sourcePos.Column - 1 -- if indent < 0 { -- indent = 0 -- } -- -- standaloneBodyText := ifBodyToStandaloneCode(fset, ifStatement.Body, src) -- replaceElse = analysis.TextEdit{ -- Pos: ifStatement.Body.Rbrace + 1, // 1 == len("}") -- End: ifStatement.End(), -- NewText: []byte("\n\n" + strings.Repeat("\t", indent) + standaloneBodyText), -- } -- } else { -- // Replace the else body text with the if body text -- bodyStart := safetoken.StartPosition(fset, ifStatement.Body.Lbrace) -- bodyEnd := safetoken.EndPosition(fset, ifStatement.Body.Rbrace+1) // 1 == len("}") -- bodyText := src[bodyStart.Offset:bodyEnd.Offset] -- replaceElse = analysis.TextEdit{ -- Pos: ifStatement.Else.Pos(), -- End: ifStatement.Else.End(), -- NewText: bodyText, -- } -- } -- -- // Replace the if text with the else text -- elsePosInSource := safetoken.StartPosition(fset, ifStatement.Else.Pos()) -- elseEndInSource := safetoken.EndPosition(fset, ifStatement.Else.End()) -- elseText := src[elsePosInSource.Offset:elseEndInSource.Offset] -- replaceBodyWithElse := analysis.TextEdit{ -- Pos: ifStatement.Body.Pos(), -- End: ifStatement.Body.End(), -- NewText: elseText, -- } -- -- // Replace the if condition with its inverse -- inverseCondition, err := invertCondition(fset, ifStatement.Cond, src) -- if err != nil { -- return nil, err -- } -- replaceConditionWithInverse := analysis.TextEdit{ -- Pos: ifStatement.Cond.Pos(), -- End: ifStatement.Cond.End(), -- NewText: inverseCondition, -- } -- -- // Return a SuggestedFix with just that TextEdit in there -- return &analysis.SuggestedFix{ -- TextEdits: []analysis.TextEdit{ -- replaceConditionWithInverse, -- replaceBodyWithElse, -- replaceElse, -- }, -- }, nil --} -- --func endsWithReturn(elseBranch ast.Stmt) (bool, error) { -- elseBlock, isBlockStatement := elseBranch.(*ast.BlockStmt) -- if !isBlockStatement { -- return false, fmt.Errorf("Unable to figure out whether this ends with return: %T", elseBranch) -- } -- -- if len(elseBlock.List) == 0 { -- // Empty blocks don't end in returns -- return false, nil -- } -- -- lastStatement := elseBlock.List[len(elseBlock.List)-1] -- -- _, lastStatementIsReturn := lastStatement.(*ast.ReturnStmt) -- return lastStatementIsReturn, nil --} -- --// Turn { fmt.Println("Hello") } into just fmt.Println("Hello"), with one less --// level of indentation. --// --// The first line of the result will not be indented, but all of the following --// lines will. --func ifBodyToStandaloneCode(fset *token.FileSet, ifBody *ast.BlockStmt, src []byte) string { -- // Get the whole body (without the surrounding braces) as a string -- bodyStart := safetoken.StartPosition(fset, ifBody.Lbrace+1) // 1 == len("}") -- bodyEnd := safetoken.EndPosition(fset, ifBody.Rbrace) -- bodyWithoutBraces := string(src[bodyStart.Offset:bodyEnd.Offset]) -- bodyWithoutBraces = strings.TrimSpace(bodyWithoutBraces) -- -- // Unindent -- bodyWithoutBraces = strings.ReplaceAll(bodyWithoutBraces, "\n\t", "\n") -- -- return bodyWithoutBraces --} -- --func invertCondition(fset *token.FileSet, cond ast.Expr, src []byte) ([]byte, error) { -- condStart := safetoken.StartPosition(fset, cond.Pos()) -- condEnd := safetoken.EndPosition(fset, cond.End()) -- oldText := string(src[condStart.Offset:condEnd.Offset]) -- -- switch expr := cond.(type) { -- case *ast.Ident, *ast.ParenExpr, *ast.CallExpr, *ast.StarExpr, *ast.IndexExpr, *typeparams.IndexListExpr, *ast.SelectorExpr: -- newText := "!" + oldText -- if oldText == "true" { -- newText = "false" -- } else if oldText == "false" { -- newText = "true" -- } -- -- return []byte(newText), nil -- -- case *ast.UnaryExpr: -- if expr.Op != token.NOT { -- // This should never happen -- return dumbInvert(fset, cond, src), nil -- } -- -- inverse := expr.X -- if p, isParen := inverse.(*ast.ParenExpr); isParen { -- // We got !(x), remove the parentheses with the ! so we get just "x" -- inverse = p.X -- -- start := safetoken.StartPosition(fset, inverse.Pos()) -- end := safetoken.EndPosition(fset, inverse.End()) -- if start.Line != end.Line { -- // The expression is multi-line, so we can't remove the parentheses -- inverse = expr.X -- } +-// DefaultOptions is the options that are used for Gopls execution independent +-// of any externally provided configuration (LSP initialization, command +-// invocation, etc.). +-func DefaultOptions(overrides ...func(*Options)) *Options { +- optionsOnce.Do(func() { +- var commands []string +- for _, c := range command.Commands { +- commands = append(commands, c.ID()) - } -- -- start := safetoken.StartPosition(fset, inverse.Pos()) -- end := safetoken.EndPosition(fset, inverse.End()) -- textWithoutNot := src[start.Offset:end.Offset] -- -- return textWithoutNot, nil -- -- case *ast.BinaryExpr: -- // These inversions are unsound for floating point NaN, but that's ok. -- negations := map[token.Token]string{ -- token.EQL: "!=", -- token.LSS: ">=", -- token.GTR: "<=", -- token.NEQ: "==", -- token.LEQ: ">", -- token.GEQ: "<", +- defaultOptions = &Options{ +- ClientOptions: ClientOptions{ +- InsertTextFormat: protocol.PlainTextTextFormat, +- PreferredContentFormat: protocol.Markdown, +- ConfigurationSupported: true, +- DynamicConfigurationSupported: true, +- DynamicRegistrationSemanticTokensSupported: true, +- DynamicWatchedFilesSupported: true, +- LineFoldingOnly: false, +- HierarchicalDocumentSymbolSupport: true, +- }, +- ServerOptions: ServerOptions{ +- SupportedCodeActions: map[file.Kind]map[protocol.CodeActionKind]bool{ +- file.Go: { +- protocol.SourceFixAll: true, +- protocol.SourceOrganizeImports: true, +- protocol.QuickFix: true, +- protocol.RefactorRewrite: true, +- protocol.RefactorInline: true, +- protocol.RefactorExtract: true, +- protocol.GoDoc: true, +- }, +- file.Mod: { +- protocol.SourceOrganizeImports: true, +- protocol.QuickFix: true, +- }, +- file.Work: {}, +- file.Sum: {}, +- file.Tmpl: {}, +- }, +- SupportedCommands: commands, +- }, +- UserOptions: UserOptions{ +- BuildOptions: BuildOptions{ +- ExpandWorkspaceToModule: true, +- DirectoryFilters: []string{"-**/node_modules"}, +- TemplateExtensions: []string{}, +- StandaloneTags: []string{"ignore"}, +- }, +- UIOptions: UIOptions{ +- DiagnosticOptions: DiagnosticOptions{ +- Annotations: map[Annotation]bool{ +- Bounds: true, +- Escape: true, +- Inline: true, +- Nil: true, +- }, +- Vulncheck: ModeVulncheckOff, +- DiagnosticsDelay: 1 * time.Second, +- DiagnosticsTrigger: DiagnosticsOnEdit, +- AnalysisProgressReporting: true, +- }, +- InlayHintOptions: InlayHintOptions{}, +- DocumentationOptions: DocumentationOptions{ +- HoverKind: FullDocumentation, +- LinkTarget: "pkg.go.dev", +- LinksInHover: true, +- }, +- NavigationOptions: NavigationOptions{ +- ImportShortcut: BothShortcuts, +- SymbolMatcher: SymbolFastFuzzy, +- SymbolStyle: DynamicSymbols, +- SymbolScope: AllSymbolScope, +- }, +- CompletionOptions: CompletionOptions{ +- Matcher: Fuzzy, +- CompletionBudget: 100 * time.Millisecond, +- ExperimentalPostfixCompletions: true, +- CompleteFunctionCalls: true, +- }, +- Codelenses: map[string]bool{ +- string(command.Generate): true, +- string(command.RegenerateCgo): true, +- string(command.Tidy): true, +- string(command.GCDetails): false, +- string(command.UpgradeDependency): true, +- string(command.Vendor): true, +- // TODO(hyangah): enable command.RunGovulncheck. +- }, +- }, +- }, +- InternalOptions: InternalOptions{ +- CompleteUnimported: true, +- CompletionDocumentation: true, +- DeepCompletion: true, +- SubdirWatchPatterns: SubdirWatchPatternsAuto, +- ReportAnalysisProgressAfter: 5 * time.Second, +- TelemetryPrompt: false, +- LinkifyShowMessage: false, +- IncludeReplaceInWorkspace: false, +- ZeroConfig: true, +- }, +- Hooks: Hooks{ +- URLRegexp: urlRegexp(), +- DefaultAnalyzers: analyzers(), +- StaticcheckAnalyzers: map[string]*Analyzer{}, +- }, - } -- -- negation, negationFound := negations[expr.Op] -- if !negationFound { -- return invertAndOr(fset, expr, src) +- }) +- options := defaultOptions.Clone() +- for _, override := range overrides { +- if override != nil { +- override(options) - } +- } +- return options +-} +diff -urN a/gopls/internal/settings/json.go b/gopls/internal/settings/json.go +--- a/gopls/internal/settings/json.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/settings/json.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,168 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- xPosInSource := safetoken.StartPosition(fset, expr.X.Pos()) -- opPosInSource := safetoken.StartPosition(fset, expr.OpPos) -- yPosInSource := safetoken.StartPosition(fset, expr.Y.Pos()) -- -- textBeforeOp := string(src[xPosInSource.Offset:opPosInSource.Offset]) -- -- oldOpWithTrailingWhitespace := string(src[opPosInSource.Offset:yPosInSource.Offset]) -- newOpWithTrailingWhitespace := negation + oldOpWithTrailingWhitespace[len(expr.Op.String()):] +-package settings - -- textAfterOp := string(src[yPosInSource.Offset:condEnd.Offset]) +-import ( +- "fmt" +- "io" +- "regexp" +- "strings" +-) - -- return []byte(textBeforeOp + newOpWithTrailingWhitespace + textAfterOp), nil -- } +-type APIJSON struct { +- Options map[string][]*OptionJSON +- Commands []*CommandJSON +- Lenses []*LensJSON +- Analyzers []*AnalyzerJSON +- Hints []*HintJSON +-} - -- return dumbInvert(fset, cond, src), nil +-type OptionJSON struct { +- Name string +- Type string +- Doc string +- EnumKeys EnumKeys +- EnumValues []EnumValue +- Default string +- Status string +- Hierarchy string -} - --// dumbInvert is a fallback, inverting cond into !(cond). --func dumbInvert(fset *token.FileSet, expr ast.Expr, src []byte) []byte { -- start := safetoken.StartPosition(fset, expr.Pos()) -- end := safetoken.EndPosition(fset, expr.End()) -- text := string(src[start.Offset:end.Offset]) -- return []byte("!(" + text + ")") +-func (o *OptionJSON) String() string { +- return o.Name -} - --func invertAndOr(fset *token.FileSet, expr *ast.BinaryExpr, src []byte) ([]byte, error) { -- if expr.Op != token.LAND && expr.Op != token.LOR { -- // Neither AND nor OR, don't know how to invert this -- return dumbInvert(fset, expr, src), nil -- } +-func (o *OptionJSON) Write(w io.Writer) { +- fmt.Fprintf(w, "**%v** *%v*\n\n", o.Name, o.Type) +- writeStatus(w, o.Status) +- enumValues := collectEnums(o) +- fmt.Fprintf(w, "%v%v\nDefault: `%v`.\n\n", o.Doc, enumValues, o.Default) +-} - -- oppositeOp := "&&" -- if expr.Op == token.LAND { -- oppositeOp = "||" +-func writeStatus(section io.Writer, status string) { +- switch status { +- case "": +- case "advanced": +- fmt.Fprint(section, "**This is an advanced setting and should not be configured by most `gopls` users.**\n\n") +- case "debug": +- fmt.Fprint(section, "**This setting is for debugging purposes only.**\n\n") +- case "experimental": +- fmt.Fprint(section, "**This setting is experimental and may be deleted.**\n\n") +- default: +- fmt.Fprintf(section, "**Status: %s.**\n\n", status) - } +-} - -- xEndInSource := safetoken.EndPosition(fset, expr.X.End()) -- opPosInSource := safetoken.StartPosition(fset, expr.OpPos) -- whitespaceAfterBefore := src[xEndInSource.Offset:opPosInSource.Offset] +-var parBreakRE = regexp.MustCompile("\n{2,}") - -- invertedBefore, err := invertCondition(fset, expr.X, src) -- if err != nil { -- return nil, err +-func collectEnums(opt *OptionJSON) string { +- var b strings.Builder +- write := func(name, doc string) { +- if doc != "" { +- unbroken := parBreakRE.ReplaceAllString(doc, "\\\n") +- fmt.Fprintf(&b, "* %s\n", strings.TrimSpace(unbroken)) +- } else { +- fmt.Fprintf(&b, "* `%s`\n", name) +- } - } -- -- invertedAfter, err := invertCondition(fset, expr.Y, src) -- if err != nil { -- return nil, err +- if len(opt.EnumValues) > 0 && opt.Type == "enum" { +- b.WriteString("\nMust be one of:\n\n") +- for _, val := range opt.EnumValues { +- write(val.Value, val.Doc) +- } +- } else if len(opt.EnumKeys.Keys) > 0 && shouldShowEnumKeysInSettings(opt.Name) { +- b.WriteString("\nCan contain any of:\n\n") +- for _, val := range opt.EnumKeys.Keys { +- write(val.Name, val.Doc) +- } - } +- return b.String() +-} - -- yPosInSource := safetoken.StartPosition(fset, expr.Y.Pos()) +-func shouldShowEnumKeysInSettings(name string) bool { +- // These fields have too many possible options to print. +- return !(name == "analyses" || name == "codelenses" || name == "hints") +-} - -- oldOpWithTrailingWhitespace := string(src[opPosInSource.Offset:yPosInSource.Offset]) -- newOpWithTrailingWhitespace := oppositeOp + oldOpWithTrailingWhitespace[len(expr.Op.String()):] +-type EnumKeys struct { +- ValueType string +- Keys []EnumKey +-} - -- return []byte(string(invertedBefore) + string(whitespaceAfterBefore) + newOpWithTrailingWhitespace + string(invertedAfter)), nil +-type EnumKey struct { +- Name string +- Doc string +- Default string -} - --// CanInvertIfCondition reports whether we can do invert-if-condition on the --// code in the given range --func CanInvertIfCondition(file *ast.File, start, end token.Pos) (*ast.IfStmt, bool, error) { -- path, _ := astutil.PathEnclosingInterval(file, start, end) -- for _, node := range path { -- stmt, isIfStatement := node.(*ast.IfStmt) -- if !isIfStatement { -- continue -- } +-type EnumValue struct { +- Value string +- Doc string +-} - -- if stmt.Else == nil { -- // Can't invert conditions without else clauses -- return nil, false, fmt.Errorf("else clause required") -- } +-type CommandJSON struct { +- Command string +- Title string +- Doc string +- ArgDoc string +- ResultDoc string +-} - -- if _, hasElseIf := stmt.Else.(*ast.IfStmt); hasElseIf { -- // Can't invert conditions with else-if clauses, unclear what that -- // would look like -- return nil, false, fmt.Errorf("else-if not supported") -- } +-func (c *CommandJSON) String() string { +- return c.Command +-} - -- return stmt, true, nil +-func (c *CommandJSON) Write(w io.Writer) { +- fmt.Fprintf(w, "### **%v**\nIdentifier: `%v`\n\n%v\n\n", c.Title, c.Command, c.Doc) +- if c.ArgDoc != "" { +- fmt.Fprintf(w, "Args:\n\n```\n%s\n```\n\n", c.ArgDoc) +- } +- if c.ResultDoc != "" { +- fmt.Fprintf(w, "Result:\n\n```\n%s\n```\n\n", c.ResultDoc) - } -- -- return nil, false, fmt.Errorf("not an if statement") -} -diff -urN a/gopls/internal/lsp/source/known_packages.go b/gopls/internal/lsp/source/known_packages.go ---- a/gopls/internal/lsp/source/known_packages.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/known_packages.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,134 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package source -- --import ( -- "context" -- "go/parser" -- "go/token" -- "sort" -- "strings" -- "sync" -- "time" - -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/imports" --) +-type LensJSON struct { +- Lens string +- Title string +- Doc string +-} - --// KnownPackagePaths returns a new list of package paths of all known --// packages in the package graph that could potentially be imported by --// the given file. The list is ordered lexicographically, except that --// all dot-free paths (standard packages) appear before dotful ones. --// --// It is part of the gopls.list_known_packages command. --func KnownPackagePaths(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]PackagePath, error) { -- // This algorithm is expressed in terms of Metadata, not Packages, -- // so it doesn't cause or wait for type checking. +-func (l *LensJSON) String() string { +- return l.Title +-} - -- current, err := NarrowestMetadataForFile(ctx, snapshot, fh.URI()) -- if err != nil { -- return nil, err // e.g. context cancelled -- } +-func (l *LensJSON) Write(w io.Writer) { +- fmt.Fprintf(w, "%s (%s): %s", l.Title, l.Lens, l.Doc) +-} - -- // Parse the file's imports so we can compute which -- // PackagePaths are imported by this specific file. -- src, err := fh.Content() -- if err != nil { -- return nil, err -- } -- file, err := parser.ParseFile(token.NewFileSet(), fh.URI().Filename(), src, parser.ImportsOnly) -- if err != nil { -- return nil, err -- } -- imported := make(map[PackagePath]bool) -- for _, imp := range file.Imports { -- if id := current.DepsByImpPath[UnquoteImportPath(imp)]; id != "" { -- if m := snapshot.Metadata(id); m != nil { -- imported[m.PkgPath] = true -- } -- } -- } +-type AnalyzerJSON struct { +- Name string +- Doc string +- URL string +- Default bool +-} - -- // Now find candidates among all known packages. -- knownPkgs, err := snapshot.AllMetadata(ctx) -- if err != nil { -- return nil, err -- } -- seen := make(map[PackagePath]bool) -- for _, knownPkg := range knownPkgs { -- // package main cannot be imported -- if knownPkg.Name == "main" { -- continue -- } -- // test packages cannot be imported -- if knownPkg.ForTest != "" { -- continue -- } -- // No need to import what the file already imports. -- // This check is based on PackagePath, not PackageID, -- // so that all test variants are filtered out too. -- if imported[knownPkg.PkgPath] { -- continue -- } -- // make sure internal packages are importable by the file -- if !IsValidImport(current.PkgPath, knownPkg.PkgPath) { -- continue -- } -- // naive check on cyclical imports -- if isDirectlyCyclical(current, knownPkg) { -- continue -- } -- // AllMetadata may have multiple variants of a pkg. -- seen[knownPkg.PkgPath] = true -- } +-func (a *AnalyzerJSON) String() string { +- return a.Name +-} - -- // Augment the set by invoking the goimports algorithm. -- if err := snapshot.RunProcessEnvFunc(ctx, func(ctx context.Context, o *imports.Options) error { -- ctx, cancel := context.WithTimeout(ctx, time.Millisecond*80) -- defer cancel() -- var seenMu sync.Mutex -- wrapped := func(ifix imports.ImportFix) { -- seenMu.Lock() -- defer seenMu.Unlock() -- // TODO(adonovan): what if the actual package path has a vendor/ prefix? -- seen[PackagePath(ifix.StmtInfo.ImportPath)] = true -- } -- return imports.GetAllCandidates(ctx, wrapped, "", fh.URI().Filename(), string(current.Name), o.Env) -- }); err != nil { -- // If goimports failed, proceed with just the candidates from the metadata. -- event.Error(ctx, "imports.GetAllCandidates", err) -- } +-func (a *AnalyzerJSON) Write(w io.Writer) { +- fmt.Fprintf(w, "%s (%s): %v", a.Name, a.Doc, a.Default) +-} - -- // Sort lexicographically, but with std before non-std packages. -- paths := make([]PackagePath, 0, len(seen)) -- for path := range seen { -- paths = append(paths, path) -- } -- sort.Slice(paths, func(i, j int) bool { -- importI, importJ := paths[i], paths[j] -- iHasDot := strings.Contains(string(importI), ".") -- jHasDot := strings.Contains(string(importJ), ".") -- if iHasDot != jHasDot { -- return jHasDot // dot-free paths (standard packages) compare less -- } -- return importI < importJ -- }) +-type HintJSON struct { +- Name string +- Doc string +- Default bool +-} - -- return paths, nil +-func (h *HintJSON) String() string { +- return h.Name -} - --// isDirectlyCyclical checks if imported directly imports pkg. --// It does not (yet) offer a full cyclical check because showing a user --// a list of importable packages already generates a very large list --// and having a few false positives in there could be worth the --// performance snappiness. --// --// TODO(adonovan): ensure that metadata graph is always cyclic! --// Many algorithms will get confused or even stuck in the --// presence of cycles. Then replace this function by 'false'. --func isDirectlyCyclical(pkg, imported *Metadata) bool { -- _, ok := imported.DepsByPkgPath[pkg.PkgPath] -- return ok +-func (h *HintJSON) Write(w io.Writer) { +- fmt.Fprintf(w, "%s (%s): %v", h.Name, h.Doc, h.Default) -} -diff -urN a/gopls/internal/lsp/source/linkname.go b/gopls/internal/lsp/source/linkname.go ---- a/gopls/internal/lsp/source/linkname.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/linkname.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,143 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/settings/settings.go b/gopls/internal/settings/settings.go +--- a/gopls/internal/settings/settings.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/settings/settings.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,1503 +0,0 @@ +-// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package source +-package settings - -import ( - "context" -- "errors" - "fmt" -- "go/token" +- "path/filepath" +- "regexp" +- "runtime" - "strings" +- "time" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/gopls/internal/span" +- "golang.org/x/tools/go/analysis" +- "golang.org/x/tools/go/analysis/passes/appends" +- "golang.org/x/tools/go/analysis/passes/asmdecl" +- "golang.org/x/tools/go/analysis/passes/assign" +- "golang.org/x/tools/go/analysis/passes/atomic" +- "golang.org/x/tools/go/analysis/passes/atomicalign" +- "golang.org/x/tools/go/analysis/passes/bools" +- "golang.org/x/tools/go/analysis/passes/buildtag" +- "golang.org/x/tools/go/analysis/passes/cgocall" +- "golang.org/x/tools/go/analysis/passes/composite" +- "golang.org/x/tools/go/analysis/passes/copylock" +- "golang.org/x/tools/go/analysis/passes/deepequalerrors" +- "golang.org/x/tools/go/analysis/passes/defers" +- "golang.org/x/tools/go/analysis/passes/directive" +- "golang.org/x/tools/go/analysis/passes/errorsas" +- "golang.org/x/tools/go/analysis/passes/fieldalignment" +- "golang.org/x/tools/go/analysis/passes/httpresponse" +- "golang.org/x/tools/go/analysis/passes/ifaceassert" +- "golang.org/x/tools/go/analysis/passes/loopclosure" +- "golang.org/x/tools/go/analysis/passes/lostcancel" +- "golang.org/x/tools/go/analysis/passes/nilfunc" +- "golang.org/x/tools/go/analysis/passes/nilness" +- "golang.org/x/tools/go/analysis/passes/printf" +- "golang.org/x/tools/go/analysis/passes/shadow" +- "golang.org/x/tools/go/analysis/passes/shift" +- "golang.org/x/tools/go/analysis/passes/slog" +- "golang.org/x/tools/go/analysis/passes/sortslice" +- "golang.org/x/tools/go/analysis/passes/stdmethods" +- "golang.org/x/tools/go/analysis/passes/stdversion" +- "golang.org/x/tools/go/analysis/passes/stringintconv" +- "golang.org/x/tools/go/analysis/passes/structtag" +- "golang.org/x/tools/go/analysis/passes/testinggoroutine" +- "golang.org/x/tools/go/analysis/passes/tests" +- "golang.org/x/tools/go/analysis/passes/timeformat" +- "golang.org/x/tools/go/analysis/passes/unmarshal" +- "golang.org/x/tools/go/analysis/passes/unreachable" +- "golang.org/x/tools/go/analysis/passes/unsafeptr" +- "golang.org/x/tools/go/analysis/passes/unusedresult" +- "golang.org/x/tools/go/analysis/passes/unusedwrite" +- "golang.org/x/tools/gopls/internal/analysis/deprecated" +- "golang.org/x/tools/gopls/internal/analysis/embeddirective" +- "golang.org/x/tools/gopls/internal/analysis/fillreturns" +- "golang.org/x/tools/gopls/internal/analysis/infertypeargs" +- "golang.org/x/tools/gopls/internal/analysis/nonewvars" +- "golang.org/x/tools/gopls/internal/analysis/noresultvalues" +- "golang.org/x/tools/gopls/internal/analysis/simplifycompositelit" +- "golang.org/x/tools/gopls/internal/analysis/simplifyrange" +- "golang.org/x/tools/gopls/internal/analysis/simplifyslice" +- "golang.org/x/tools/gopls/internal/analysis/stubmethods" +- "golang.org/x/tools/gopls/internal/analysis/undeclaredname" +- "golang.org/x/tools/gopls/internal/analysis/unusedparams" +- "golang.org/x/tools/gopls/internal/analysis/unusedvariable" +- "golang.org/x/tools/gopls/internal/analysis/useany" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/command" -) - --// ErrNoLinkname is returned by LinknameDefinition when no linkname --// directive is found at a particular position. --// As such it indicates that other definitions could be worth checking. --var ErrNoLinkname = errors.New("no linkname directive found") +-type Annotation string - --// LinknameDefinition finds the definition of the linkname directive in m at pos. --// If there is no linkname directive at pos, returns ErrNoLinkname. --func LinknameDefinition(ctx context.Context, snapshot Snapshot, m *protocol.Mapper, from protocol.Position) ([]protocol.Location, error) { -- pkgPath, name, _ := parseLinkname(m, from) -- if pkgPath == "" { -- return nil, ErrNoLinkname -- } +-const ( +- // Nil controls nil checks. +- Nil Annotation = "nil" - -- _, pgf, pos, err := findLinkname(ctx, snapshot, PackagePath(pkgPath), name) -- if err != nil { -- return nil, fmt.Errorf("find linkname: %w", err) -- } -- loc, err := pgf.PosLocation(pos, pos+token.Pos(len(name))) -- if err != nil { -- return nil, fmt.Errorf("location of linkname: %w", err) -- } -- return []protocol.Location{loc}, nil +- // Escape controls diagnostics about escape choices. +- Escape Annotation = "escape" +- +- // Inline controls diagnostics about inlining choices. +- Inline Annotation = "inline" +- +- // Bounds controls bounds checking diagnostics. +- Bounds Annotation = "bounds" +-) +- +-// Options holds various configuration that affects Gopls execution, organized +-// by the nature or origin of the settings. +-type Options struct { +- ClientOptions +- ServerOptions +- UserOptions +- InternalOptions +- Hooks -} - --// parseLinkname attempts to parse a go:linkname declaration at the given pos. --// If successful, it returns --// - package path referenced --// - object name referenced --// - byte offset in mapped file of the start of the link target --// of the linkname directives 2nd argument. +-// IsAnalyzerEnabled reports whether an analyzer with the given name is +-// enabled. -// --// If the position is not in the second argument of a go:linkname directive, --// or parsing fails, it returns "", "", 0. --func parseLinkname(m *protocol.Mapper, pos protocol.Position) (pkgPath, name string, targetOffset int) { -- lineStart, err := m.PositionOffset(protocol.Position{Line: pos.Line, Character: 0}) -- if err != nil { -- return "", "", 0 -- } -- lineEnd, err := m.PositionOffset(protocol.Position{Line: pos.Line + 1, Character: 0}) -- if err != nil { -- return "", "", 0 +-// TODO(rfindley): refactor to simplify this function. We no longer need the +-// different categories of analyzer. +-func (opts *Options) IsAnalyzerEnabled(name string) bool { +- for _, amap := range []map[string]*Analyzer{opts.DefaultAnalyzers, opts.StaticcheckAnalyzers} { +- for _, analyzer := range amap { +- if analyzer.Analyzer.Name == name && analyzer.IsEnabled(opts) { +- return true +- } +- } - } +- return false +-} - -- directive := string(m.Content[lineStart:lineEnd]) -- // (Assumes no leading spaces.) -- if !strings.HasPrefix(directive, "//go:linkname") { -- return "", "", 0 -- } -- // Sometimes source code (typically tests) has another -- // comment after the directive, trim that away. -- if i := strings.LastIndex(directive, "//"); i != 0 { -- directive = strings.TrimSpace(directive[:i]) -- } +-// ClientOptions holds LSP-specific configuration that is provided by the +-// client. +-type ClientOptions struct { +- ClientInfo *protocol.ClientInfo +- InsertTextFormat protocol.InsertTextFormat +- ConfigurationSupported bool +- DynamicConfigurationSupported bool +- DynamicRegistrationSemanticTokensSupported bool +- DynamicWatchedFilesSupported bool +- RelativePatternsSupported bool +- PreferredContentFormat protocol.MarkupKind +- LineFoldingOnly bool +- HierarchicalDocumentSymbolSupport bool +- SemanticTypes []string +- SemanticMods []string +- RelatedInformationSupported bool +- CompletionTags bool +- CompletionDeprecated bool +- SupportedResourceOperations []protocol.ResourceOperationKind +- CodeActionResolveOptions []string +-} - -- // Looking for pkgpath in '//go:linkname f pkgpath.g'. -- // (We ignore 1-arg linkname directives.) -- parts := strings.Fields(directive) -- if len(parts) != 3 { -- return "", "", 0 -- } +-// ServerOptions holds LSP-specific configuration that is provided by the +-// server. +-type ServerOptions struct { +- SupportedCodeActions map[file.Kind]map[protocol.CodeActionKind]bool +- SupportedCommands []string +-} - -- // Inside 2nd arg [start, end]? -- // (Assumes no trailing spaces.) -- offset, err := m.PositionOffset(pos) -- if err != nil { -- return "", "", 0 -- } -- end := lineStart + len(directive) -- start := end - len(parts[2]) -- if !(start <= offset && offset <= end) { -- return "", "", 0 -- } -- linkname := parts[2] +-type BuildOptions struct { +- // BuildFlags is the set of flags passed on to the build system when invoked. +- // It is applied to queries like `go list`, which is used when discovering files. +- // The most common use is to set `-tags`. +- BuildFlags []string - -- // Split the pkg path from the name. -- dot := strings.LastIndexByte(linkname, '.') -- if dot < 0 { -- return "", "", 0 -- } +- // Env adds environment variables to external commands run by `gopls`, most notably `go list`. +- Env map[string]string +- +- // DirectoryFilters can be used to exclude unwanted directories from the +- // workspace. By default, all directories are included. Filters are an +- // operator, `+` to include and `-` to exclude, followed by a path prefix +- // relative to the workspace folder. They are evaluated in order, and +- // the last filter that applies to a path controls whether it is included. +- // The path prefix can be empty, so an initial `-` excludes everything. +- // +- // DirectoryFilters also supports the `**` operator to match 0 or more directories. +- // +- // Examples: +- // +- // Exclude node_modules at current depth: `-node_modules` +- // +- // Exclude node_modules at any depth: `-**/node_modules` +- // +- // Include only project_a: `-` (exclude everything), `+project_a` +- // +- // Include only project_a, but not node_modules inside it: `-`, `+project_a`, `-project_a/node_modules` +- DirectoryFilters []string - -- return linkname[:dot], linkname[dot+1:], start --} +- // TemplateExtensions gives the extensions of file names that are treateed +- // as template files. (The extension +- // is the part of the file name after the final dot.) +- TemplateExtensions []string - --// findLinkname searches dependencies of packages containing fh for an object --// with linker name matching the given package path and name. --func findLinkname(ctx context.Context, snapshot Snapshot, pkgPath PackagePath, name string) (Package, *ParsedGoFile, token.Pos, error) { -- // Typically the linkname refers to a forward dependency -- // or a reverse dependency, but in general it may refer -- // to any package that is linked with this one. -- var pkgMeta *Metadata -- metas, err := snapshot.AllMetadata(ctx) -- if err != nil { -- return nil, nil, token.NoPos, err -- } -- RemoveIntermediateTestVariants(&metas) -- for _, meta := range metas { -- if meta.PkgPath == pkgPath { -- pkgMeta = meta -- break -- } -- } -- if pkgMeta == nil { -- return nil, nil, token.NoPos, fmt.Errorf("cannot find package %q", pkgPath) -- } +- // obsolete, no effect +- MemoryMode string `status:"experimental"` - -- // When found, type check the desired package (snapshot.TypeCheck in TypecheckFull mode), -- pkgs, err := snapshot.TypeCheck(ctx, pkgMeta.ID) -- if err != nil { -- return nil, nil, token.NoPos, err -- } -- pkg := pkgs[0] +- // ExpandWorkspaceToModule determines which packages are considered +- // "workspace packages" when the workspace is using modules. +- // +- // Workspace packages affect the scope of workspace-wide operations. Notably, +- // gopls diagnoses all packages considered to be part of the workspace after +- // every keystroke, so by setting "ExpandWorkspaceToModule" to false, and +- // opening a nested workspace directory, you can reduce the amount of work +- // gopls has to do to keep your workspace up to date. +- ExpandWorkspaceToModule bool `status:"experimental"` - -- obj := pkg.GetTypes().Scope().Lookup(name) -- if obj == nil { -- return nil, nil, token.NoPos, fmt.Errorf("package %q does not define %s", pkgPath, name) -- } +- // AllowModfileModifications disables -mod=readonly, allowing imports from +- // out-of-scope modules. This option will eventually be removed. +- AllowModfileModifications bool `status:"experimental"` - -- objURI := safetoken.StartPosition(pkg.FileSet(), obj.Pos()) -- pgf, err := pkg.File(span.URIFromPath(objURI.Filename)) -- if err != nil { -- return nil, nil, token.NoPos, err -- } +- // AllowImplicitNetworkAccess disables GOPROXY=off, allowing implicit module +- // downloads rather than requiring user action. This option will eventually +- // be removed. +- AllowImplicitNetworkAccess bool `status:"experimental"` - -- return pkg, pgf, obj.Pos(), nil +- // StandaloneTags specifies a set of build constraints that identify +- // individual Go source files that make up the entire main package of an +- // executable. +- // +- // A common example of standalone main files is the convention of using the +- // directive `//go:build ignore` to denote files that are not intended to be +- // included in any package, for example because they are invoked directly by +- // the developer using `go run`. +- // +- // Gopls considers a file to be a standalone main file if and only if it has +- // package name "main" and has a build directive of the exact form +- // "//go:build tag" or "// +build tag", where tag is among the list of tags +- // configured by this setting. Notably, if the build constraint is more +- // complicated than a simple tag (such as the composite constraint +- // `//go:build tag && go1.18`), the file is not considered to be a standalone +- // main file. +- // +- // This setting is only supported when gopls is built with Go 1.16 or later. +- StandaloneTags []string -} -diff -urN a/gopls/internal/lsp/source/methodsets/methodsets.go b/gopls/internal/lsp/source/methodsets/methodsets.go ---- a/gopls/internal/lsp/source/methodsets/methodsets.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/methodsets/methodsets.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,490 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --// Package methodsets defines an incremental, serializable index of --// method-set information that allows efficient 'implements' queries --// across packages of the workspace without using the type checker. --// --// This package provides only the "global" (all workspace) search; the --// "local" search within a given package uses a different --// implementation based on type-checker data structures for a single --// package plus variants; see ../implementation.go. --// The local algorithm is more precise as it tests function-local types too. --// --// A global index of function-local types is challenging since they --// may reference other local types, for which we would need to invent --// stable names, an unsolved problem described in passing in Go issue --// 57497. The global algorithm also does not index anonymous interface --// types, even outside function bodies. --// --// Consequently, global results are not symmetric: applying the --// operation twice may not get you back where you started. --package methodsets +-type UIOptions struct { +- DocumentationOptions +- CompletionOptions +- NavigationOptions +- DiagnosticOptions +- InlayHintOptions - --// DESIGN --// --// See https://go.dev/cl/452060 for a minimal exposition of the algorithm. --// --// For each method, we compute a fingerprint: a string representing --// the method name and type such that equal fingerprint strings mean --// identical method types. --// --// For efficiency, the fingerprint is reduced to a single bit --// of a uint64, so that the method set can be represented as --// the union of those method bits (a uint64 bitmask). --// Assignability thus reduces to a subset check on bitmasks --// followed by equality checks on fingerprints. --// --// In earlier experiments, using 128-bit masks instead of 64 reduced --// the number of candidates by about 2x. Using (like a Bloom filter) a --// different hash function to compute a second 64-bit mask and --// performing a second mask test reduced it by about 4x. --// Neither had much effect on the running time, presumably because a --// single 64-bit mask is quite effective. See CL 452060 for details. +- // Codelenses overrides the enabled/disabled state of code lenses. See the +- // "Code Lenses" section of the +- // [Settings page](https://github.com/golang/tools/blob/master/gopls/doc/settings.md#code-lenses) +- // for the list of supported lenses. +- // +- // Example Usage: +- // +- // ```json5 +- // "gopls": { +- // ... +- // "codelenses": { +- // "generate": false, // Don't show the `go generate` lens. +- // "gc_details": true // Show a code lens toggling the display of gc's choices. +- // } +- // ... +- // } +- // ``` +- Codelenses map[string]bool - --import ( -- "fmt" -- "go/token" -- "go/types" -- "hash/crc32" -- "strconv" -- "strings" +- // SemanticTokens controls whether the LSP server will send +- // semantic tokens to the client. +- SemanticTokens bool `status:"experimental"` - -- "golang.org/x/tools/go/types/objectpath" -- "golang.org/x/tools/gopls/internal/lsp/frob" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/internal/typeparams" --) +- // NoSemanticString turns off the sending of the semantic token 'string' +- NoSemanticString bool `status:"experimental"` - --// An Index records the non-empty method sets of all package-level --// types in a package in a form that permits assignability queries --// without the type checker. --type Index struct { -- pkg gobPackage +- // NoSemanticNumber turns off the sending of the semantic token 'number' +- NoSemanticNumber bool `status:"experimental"` -} - --// Decode decodes the given gob-encoded data as an Index. --func Decode(data []byte) *Index { -- var pkg gobPackage -- packageCodec.Decode(data, &pkg) -- return &Index{pkg} --} +-type CompletionOptions struct { +- // Placeholders enables placeholders for function parameters or struct +- // fields in completion responses. +- UsePlaceholders bool - --// Encode encodes the receiver as gob-encoded data. --func (index *Index) Encode() []byte { -- return packageCodec.Encode(index.pkg) --} +- // CompletionBudget is the soft latency goal for completion requests. Most +- // requests finish in a couple milliseconds, but in some cases deep +- // completions can take much longer. As we use up our budget we +- // dynamically reduce the search scope to ensure we return timely +- // results. Zero means unlimited. +- CompletionBudget time.Duration `status:"debug"` - --// NewIndex returns a new index of method-set information for all --// package-level types in the specified package. --func NewIndex(fset *token.FileSet, pkg *types.Package) *Index { -- return new(indexBuilder).build(fset, pkg) --} +- // Matcher sets the algorithm that is used when calculating completion +- // candidates. +- Matcher Matcher `status:"advanced"` - --// A Location records the extent of an identifier in byte-offset form. --// --// Conversion to protocol (UTF-16) form is done by the caller after a --// search, not during index construction. --type Location struct { -- Filename string -- Start, End int // byte offsets --} +- // ExperimentalPostfixCompletions enables artificial method snippets +- // such as "someSlice.sort!". +- ExperimentalPostfixCompletions bool `status:"experimental"` - --// A Key represents the method set of a given type in a form suitable --// to pass to the (*Index).Search method of many different Indexes. --type Key struct { -- mset gobMethodSet // note: lacks position information +- // CompleteFunctionCalls enables function call completion. +- // +- // When completing a statement, or when a function return type matches the +- // expected of the expression being completed, completion may suggest call +- // expressions (i.e. may include parentheses). +- CompleteFunctionCalls bool -} - --// KeyOf returns the search key for the method sets of a given type. --// It returns false if the type has no methods. --func KeyOf(t types.Type) (Key, bool) { -- mset := methodSetInfo(t, nil) -- if mset.Mask == 0 { -- return Key{}, false // no methods -- } -- return Key{mset}, true --} +-type DocumentationOptions struct { +- // HoverKind controls the information that appears in the hover text. +- // SingleLine and Structured are intended for use only by authors of editor plugins. +- HoverKind HoverKind - --// A Result reports a matching type or method in a method-set search. --type Result struct { -- Location Location // location of the type or method +- // LinkTarget controls where documentation links go. +- // It might be one of: +- // +- // * `"godoc.org"` +- // * `"pkg.go.dev"` +- // +- // If company chooses to use its own `godoc.org`, its address can be used as well. +- // +- // Modules matching the GOPRIVATE environment variable will not have +- // documentation links in hover. +- LinkTarget string - -- // methods only: -- PkgPath string // path of declaring package (may differ due to embedding) -- ObjectPath objectpath.Path // path of method within declaring package +- // LinksInHover toggles the presence of links to documentation in hover. +- LinksInHover bool -} - --// Search reports each type that implements (or is implemented by) the --// type that produced the search key. If methodID is nonempty, only --// that method of each type is reported. --// --// The result does not include the error.Error method. --// TODO(adonovan): give this special case a more systematic treatment. --func (index *Index) Search(key Key, methodID string) []Result { -- var results []Result -- for _, candidate := range index.pkg.MethodSets { -- // Traditionally this feature doesn't report -- // interface/interface elements of the relation. -- // I think that's a mistake. -- // TODO(adonovan): UX: change it, here and in the local implementation. -- if candidate.IsInterface && key.mset.IsInterface { -- continue -- } -- if !satisfies(candidate, key.mset) && !satisfies(key.mset, candidate) { -- continue -- } +-type FormattingOptions struct { +- // Local is the equivalent of the `goimports -local` flag, which puts +- // imports beginning with this string after third-party packages. It should +- // be the prefix of the import path whose imports should be grouped +- // separately. +- Local string - -- if candidate.Tricky { -- // If any interface method is tricky then extra -- // checking may be needed to eliminate a false positive. -- // TODO(adonovan): implement it. -- } +- // Gofumpt indicates if we should run gofumpt formatting. +- Gofumpt bool +-} - -- if methodID == "" { -- results = append(results, Result{Location: index.location(candidate.Posn)}) -- } else { -- for _, m := range candidate.Methods { -- // Here we exploit knowledge of the shape of the fingerprint string. -- if strings.HasPrefix(m.Fingerprint, methodID) && -- m.Fingerprint[len(methodID)] == '(' { +-type DiagnosticOptions struct { +- // Analyses specify analyses that the user would like to enable or disable. +- // A map of the names of analysis passes that should be enabled/disabled. +- // A full list of analyzers that gopls uses can be found in +- // [analyzers.md](https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md). +- // +- // Example Usage: +- // +- // ```json5 +- // ... +- // "analyses": { +- // "unreachable": false, // Disable the unreachable analyzer. +- // "unusedvariable": true // Enable the unusedvariable analyzer. +- // } +- // ... +- // ``` +- Analyses map[string]bool - -- // Don't report error.Error among the results: -- // it has no true source location, no package, -- // and is excluded from the xrefs index. -- if m.PkgPath == 0 || m.ObjectPath == 0 { -- if methodID != "Error" { -- panic("missing info for" + methodID) -- } -- continue -- } +- // Staticcheck enables additional analyses from staticcheck.io. +- // These analyses are documented on +- // [Staticcheck's website](https://staticcheck.io/docs/checks/). +- Staticcheck bool `status:"experimental"` - -- results = append(results, Result{ -- Location: index.location(m.Posn), -- PkgPath: index.pkg.Strings[m.PkgPath], -- ObjectPath: objectpath.Path(index.pkg.Strings[m.ObjectPath]), -- }) -- break -- } -- } -- } -- } -- return results --} +- // Annotations specifies the various kinds of optimization diagnostics +- // that should be reported by the gc_details command. +- Annotations map[Annotation]bool `status:"experimental"` - --// satisfies does a fast check for whether x satisfies y. --func satisfies(x, y gobMethodSet) bool { -- return y.IsInterface && x.Mask&y.Mask == y.Mask && subset(y, x) --} +- // Vulncheck enables vulnerability scanning. +- Vulncheck VulncheckMode `status:"experimental"` - --// subset reports whether method set x is a subset of y. --func subset(x, y gobMethodSet) bool { --outer: -- for _, mx := range x.Methods { -- for _, my := range y.Methods { -- if mx.Sum == my.Sum && mx.Fingerprint == my.Fingerprint { -- continue outer // found; try next x method -- } -- } -- return false // method of x not found in y -- } -- return true // all methods of x found in y --} +- // DiagnosticsDelay controls the amount of time that gopls waits +- // after the most recent file modification before computing deep diagnostics. +- // Simple diagnostics (parsing and type-checking) are always run immediately +- // on recently modified packages. +- // +- // This option must be set to a valid duration string, for example `"250ms"`. +- DiagnosticsDelay time.Duration `status:"advanced"` - --func (index *Index) location(posn gobPosition) Location { -- return Location{ -- Filename: index.pkg.Strings[posn.File], -- Start: posn.Offset, -- End: posn.Offset + posn.Len, -- } +- // DiagnosticsTrigger controls when to run diagnostics. +- DiagnosticsTrigger DiagnosticsTrigger `status:"experimental"` +- +- // AnalysisProgressReporting controls whether gopls sends progress +- // notifications when construction of its index of analysis facts is taking a +- // long time. Cancelling these notifications will cancel the indexing task, +- // though it will restart after the next change in the workspace. +- // +- // When a package is opened for the first time and heavyweight analyses such as +- // staticcheck are enabled, it can take a while to construct the index of +- // analysis facts for all its dependencies. The index is cached in the +- // filesystem, so subsequent analysis should be faster. +- AnalysisProgressReporting bool -} - --// An indexBuilder builds an index for a single package. --type indexBuilder struct { -- gobPackage -- stringIndex map[string]int +-type InlayHintOptions struct { +- // Hints specify inlay hints that users want to see. A full list of hints +- // that gopls uses can be found in +- // [inlayHints.md](https://github.com/golang/tools/blob/master/gopls/doc/inlayHints.md). +- Hints map[string]bool `status:"experimental"` -} - --// build adds to the index all package-level named types of the specified package. --func (b *indexBuilder) build(fset *token.FileSet, pkg *types.Package) *Index { -- _ = b.string("") // 0 => "" +-type NavigationOptions struct { +- // ImportShortcut specifies whether import statements should link to +- // documentation or go to definitions. +- ImportShortcut ImportShortcut - -- objectPos := func(obj types.Object) gobPosition { -- posn := safetoken.StartPosition(fset, obj.Pos()) -- return gobPosition{b.string(posn.Filename), posn.Offset, len(obj.Name())} -- } +- // SymbolMatcher sets the algorithm that is used when finding workspace symbols. +- SymbolMatcher SymbolMatcher `status:"advanced"` - -- objectpathFor := new(objectpath.Encoder).For +- // SymbolStyle controls how symbols are qualified in symbol responses. +- // +- // Example Usage: +- // +- // ```json5 +- // "gopls": { +- // ... +- // "symbolStyle": "Dynamic", +- // ... +- // } +- // ``` +- SymbolStyle SymbolStyle `status:"advanced"` - -- // setindexInfo sets the (Posn, PkgPath, ObjectPath) fields for each method declaration. -- setIndexInfo := func(m *gobMethod, method *types.Func) { -- // error.Error has empty Position, PkgPath, and ObjectPath. -- if method.Pkg() == nil { -- return -- } +- // SymbolScope controls which packages are searched for workspace/symbol +- // requests. The default value, "workspace", searches only workspace +- // packages. The legacy behavior, "all", causes all loaded packages to be +- // searched, including dependencies; this is more expensive and may return +- // unwanted results. +- SymbolScope SymbolScope +-} - -- m.Posn = objectPos(method) -- m.PkgPath = b.string(method.Pkg().Path()) +-// UserOptions holds custom Gopls configuration (not part of the LSP) that is +-// modified by the client. +-type UserOptions struct { +- BuildOptions +- UIOptions +- FormattingOptions - -- // Instantiations of generic methods don't have an -- // object path, so we use the generic. -- if p, err := objectpathFor(typeparams.OriginMethod(method)); err != nil { -- panic(err) // can't happen for a method of a package-level type -- } else { -- m.ObjectPath = b.string(string(p)) -- } -- } +- // VerboseOutput enables additional debug logging. +- VerboseOutput bool `status:"debug"` +-} - -- // We ignore aliases, though in principle they could define a -- // struct{...} or interface{...} type, or an instantiation of -- // a generic, that has a novel method set. -- scope := pkg.Scope() -- for _, name := range scope.Names() { -- if tname, ok := scope.Lookup(name).(*types.TypeName); ok && !tname.IsAlias() { -- if mset := methodSetInfo(tname.Type(), setIndexInfo); mset.Mask != 0 { -- mset.Posn = objectPos(tname) -- // Only record types with non-trivial method sets. -- b.MethodSets = append(b.MethodSets, mset) -- } -- } +-// EnvSlice returns Env as a slice of k=v strings. +-func (u *UserOptions) EnvSlice() []string { +- var result []string +- for k, v := range u.Env { +- result = append(result, fmt.Sprintf("%v=%v", k, v)) - } -- -- return &Index{pkg: b.gobPackage} +- return result -} - --// string returns a small integer that encodes the string. --func (b *indexBuilder) string(s string) int { -- i, ok := b.stringIndex[s] -- if !ok { -- i = len(b.Strings) -- if b.stringIndex == nil { -- b.stringIndex = make(map[string]int) +-// SetEnvSlice sets Env from a slice of k=v strings. +-func (u *UserOptions) SetEnvSlice(env []string) { +- u.Env = map[string]string{} +- for _, kv := range env { +- split := strings.SplitN(kv, "=", 2) +- if len(split) != 2 { +- continue - } -- b.stringIndex[s] = i -- b.Strings = append(b.Strings, s) +- u.Env[split[0]] = split[1] - } -- return i -} - --// methodSetInfo returns the method-set fingerprint of a type. --// It calls the optional setIndexInfo function for each gobMethod. --// This is used during index construction, but not search (KeyOf), --// to store extra information. --func methodSetInfo(t types.Type, setIndexInfo func(*gobMethod, *types.Func)) gobMethodSet { -- // For non-interface types, use *T -- // (if T is not already a pointer) -- // since it may have more methods. -- mset := types.NewMethodSet(EnsurePointer(t)) +-// Hooks contains configuration that is provided to the Gopls command by the +-// main package. +-type Hooks struct { +- // LicensesText holds third party licenses for software used by gopls. +- LicensesText string - -- // Convert the method set into a compact summary. -- var mask uint64 -- tricky := false -- methods := make([]gobMethod, mset.Len()) -- for i := 0; i < mset.Len(); i++ { -- m := mset.At(i).Obj().(*types.Func) -- fp, isTricky := fingerprint(m) -- if isTricky { -- tricky = true -- } -- sum := crc32.ChecksumIEEE([]byte(fp)) -- methods[i] = gobMethod{Fingerprint: fp, Sum: sum} -- if setIndexInfo != nil { -- setIndexInfo(&methods[i], m) // set Position, PkgPath, ObjectPath -- } -- mask |= 1 << uint64(((sum>>24)^(sum>>16)^(sum>>8)^sum)&0x3f) -- } -- return gobMethodSet{ -- IsInterface: types.IsInterface(t), -- Tricky: tricky, -- Mask: mask, -- Methods: methods, -- } --} +- // Whether staticcheck is supported. +- StaticcheckSupported bool +- +- // URLRegexp is used to find potential URLs in comments/strings. +- // +- // Not all matches are shown to the user: if the matched URL is not detected +- // as valid, it will be skipped. +- URLRegexp *regexp.Regexp - --// EnsurePointer wraps T in a types.Pointer if T is a named, non-interface type. --// This is useful to make sure you consider a named type's full method set. --func EnsurePointer(T types.Type) types.Type { -- if _, ok := T.(*types.Named); ok && !types.IsInterface(T) { -- return types.NewPointer(T) -- } +- // GofumptFormat allows the gopls module to wire-in a call to +- // gofumpt/format.Source. langVersion and modulePath are used for some +- // Gofumpt formatting rules -- see the Gofumpt documentation for details. +- GofumptFormat func(ctx context.Context, langVersion, modulePath string, src []byte) ([]byte, error) - -- return T +- DefaultAnalyzers map[string]*Analyzer +- StaticcheckAnalyzers map[string]*Analyzer -} - --// fingerprint returns an encoding of a method signature such that two --// methods with equal encodings have identical types, except for a few --// tricky types whose encodings may spuriously match and whose exact --// identity computation requires the type checker to eliminate false --// positives (which are rare). The boolean result indicates whether --// the result was one of these tricky types. --// --// In the standard library, 99.8% of package-level types have a --// non-tricky method-set. The most common exceptions are due to type --// parameters. +-// InternalOptions contains settings that are not intended for use by the +-// average user. These may be settings used by tests or outdated settings that +-// will soon be deprecated. Some of these settings may not even be configurable +-// by the user. -// --// The fingerprint string starts with method.Id() + "(". --func fingerprint(method *types.Func) (string, bool) { -- var buf strings.Builder -- tricky := false -- var fprint func(t types.Type) -- fprint = func(t types.Type) { -- switch t := t.(type) { -- case *types.Named: -- tname := t.Obj() -- if tname.Pkg() != nil { -- buf.WriteString(strconv.Quote(tname.Pkg().Path())) -- buf.WriteByte('.') -- } else if tname.Name() != "error" && tname.Name() != "comparable" { -- panic(tname) // error and comparable the only named types with no package -- } -- buf.WriteString(tname.Name()) +-// TODO(rfindley): even though these settings are not intended for +-// modification, some of them should be surfaced in our documentation. +-type InternalOptions struct { +- // VerboseWorkDoneProgress controls whether the LSP server should send +- // progress reports for all work done outside the scope of an RPC. +- // Used by the regression tests. +- VerboseWorkDoneProgress bool - -- case *types.Array: -- fmt.Fprintf(&buf, "[%d]", t.Len()) -- fprint(t.Elem()) +- // The following options were previously available to users, but they +- // really shouldn't be configured by anyone other than "power users". - -- case *types.Slice: -- buf.WriteString("[]") -- fprint(t.Elem()) +- // CompletionDocumentation enables documentation with completion results. +- CompletionDocumentation bool - -- case *types.Pointer: -- buf.WriteByte('*') -- fprint(t.Elem()) +- // CompleteUnimported enables completion for packages that you do not +- // currently import. +- CompleteUnimported bool - -- case *types.Map: -- buf.WriteString("map[") -- fprint(t.Key()) -- buf.WriteByte(']') -- fprint(t.Elem()) +- // DeepCompletion enables the ability to return completions from deep +- // inside relevant entities, rather than just the locally accessible ones. +- // +- // Consider this example: +- // +- // ```go +- // package main +- // +- // import "fmt" +- // +- // type wrapString struct { +- // str string +- // } +- // +- // func main() { +- // x := wrapString{"hello world"} +- // fmt.Printf(<>) +- // } +- // ``` +- // +- // At the location of the `<>` in this program, deep completion would suggest +- // the result `x.str`. +- DeepCompletion bool - -- case *types.Chan: -- switch t.Dir() { -- case types.SendRecv: -- buf.WriteString("chan ") -- case types.SendOnly: -- buf.WriteString("<-chan ") -- case types.RecvOnly: -- buf.WriteString("chan<- ") -- } -- fprint(t.Elem()) +- // ShowBugReports causes a message to be shown when the first bug is reported +- // on the server. +- // This option applies only during initialization. +- ShowBugReports bool - -- case *types.Tuple: -- buf.WriteByte('(') -- for i := 0; i < t.Len(); i++ { -- if i > 0 { -- buf.WriteByte(',') -- } -- fprint(t.At(i).Type()) -- } -- buf.WriteByte(')') +- // SubdirWatchPatterns configures the file watching glob patterns registered +- // by gopls. +- // +- // Some clients (namely VS Code) do not send workspace/didChangeWatchedFile +- // notifications for files contained in a directory when that directory is +- // deleted: +- // https://github.com/microsoft/vscode/issues/109754 +- // +- // In this case, gopls would miss important notifications about deleted +- // packages. To work around this, gopls registers a watch pattern for each +- // directory containing Go files. +- // +- // Unfortunately, other clients experience performance problems with this +- // many watch patterns, so there is no single behavior that works well for +- // all clients. +- // +- // The "subdirWatchPatterns" setting allows configuring this behavior. Its +- // default value of "auto" attempts to guess the correct behavior based on +- // the client name. We'd love to avoid this specialization, but as described +- // above there is no single value that works for all clients. +- // +- // If any LSP client does not behave well with the default value (for +- // example, if like VS Code it drops file notifications), please file an +- // issue. +- SubdirWatchPatterns SubdirWatchPatterns - -- case *types.Basic: -- // Use canonical names for uint8 and int32 aliases. -- switch t.Kind() { -- case types.Byte: -- buf.WriteString("byte") -- case types.Rune: -- buf.WriteString("rune") -- default: -- buf.WriteString(t.String()) -- } +- // ReportAnalysisProgressAfter sets the duration for gopls to wait before starting +- // progress reporting for ongoing go/analysis passes. +- // +- // It is intended to be used for testing only. +- ReportAnalysisProgressAfter time.Duration - -- case *types.Signature: -- buf.WriteString("func") -- fprint(t.Params()) -- if t.Variadic() { -- buf.WriteString("...") // not quite Go syntax -- } -- fprint(t.Results()) +- // TelemetryPrompt controls whether gopls prompts about enabling Go telemetry. +- // +- // Once the prompt is answered, gopls doesn't ask again, but TelemetryPrompt +- // can prevent the question from ever being asked in the first place. +- TelemetryPrompt bool - -- case *types.Struct: -- // Non-empty unnamed struct types in method -- // signatures are vanishingly rare. -- buf.WriteString("struct{") -- for i := 0; i < t.NumFields(); i++ { -- if i > 0 { -- buf.WriteByte(';') -- } -- f := t.Field(i) -- // This isn't quite right for embedded type aliases. -- // (See types.TypeString(StructType) and #44410 for context.) -- // But this is vanishingly rare. -- if !f.Embedded() { -- buf.WriteString(f.Id()) -- buf.WriteByte(' ') -- } -- fprint(f.Type()) -- if tag := t.Tag(i); tag != "" { -- buf.WriteByte(' ') -- buf.WriteString(strconv.Quote(tag)) -- } -- } -- buf.WriteString("}") +- // LinkifyShowMessage controls whether the client wants gopls +- // to linkify links in showMessage. e.g. [go.dev](https://go.dev). +- LinkifyShowMessage bool - -- case *types.Interface: -- if t.NumMethods() == 0 { -- buf.WriteString("any") // common case -- } else { -- // Interface assignability is particularly -- // tricky due to the possibility of recursion. -- tricky = true -- // We could still give more disambiguating precision -- // than "..." if we wanted to. -- buf.WriteString("interface{...}") -- } +- // IncludeReplaceInWorkspace controls whether locally replaced modules in a +- // go.mod file are treated like workspace modules. +- // Or in other words, if a go.mod file with local replaces behaves like a +- // go.work file. +- IncludeReplaceInWorkspace bool - -- case *typeparams.TypeParam: -- tricky = true -- // TODO(adonovan): refine this by adding a numeric suffix -- // indicating the index among the receiver type's parameters. -- buf.WriteByte('?') +- // ZeroConfig enables the zero-config algorithm for workspace layout, +- // dynamically creating build configurations for different modules, +- // directories, and GOOS/GOARCH combinations to cover open files. +- ZeroConfig bool +-} - -- default: // incl. *types.Union -- panic(t) -- } -- } +-type SubdirWatchPatterns string - -- buf.WriteString(method.Id()) // e.g. "pkg.Type" -- sig := method.Type().(*types.Signature) -- fprint(sig.Params()) -- fprint(sig.Results()) -- return buf.String(), tricky --} +-const ( +- SubdirWatchPatternsOn SubdirWatchPatterns = "on" +- SubdirWatchPatternsOff SubdirWatchPatterns = "off" +- SubdirWatchPatternsAuto SubdirWatchPatterns = "auto" +-) - --// -- serial format of index -- +-type ImportShortcut string - --// (The name says gob but in fact we use frob.) --var packageCodec = frob.CodecFor[gobPackage]() +-const ( +- BothShortcuts ImportShortcut = "Both" +- LinkShortcut ImportShortcut = "Link" +- DefinitionShortcut ImportShortcut = "Definition" +-) - --// A gobPackage records the method set of each package-level type for a single package. --type gobPackage struct { -- Strings []string // index of strings used by gobPosition.File, gobMethod.{Pkg,Object}Path -- MethodSets []gobMethodSet +-func (s ImportShortcut) ShowLinks() bool { +- return s == BothShortcuts || s == LinkShortcut -} - --// A gobMethodSet records the method set of a single type. --type gobMethodSet struct { -- Posn gobPosition -- IsInterface bool -- Tricky bool // at least one method is tricky; assignability requires go/types -- Mask uint64 // mask with 1 bit from each of methods[*].sum -- Methods []gobMethod +-func (s ImportShortcut) ShowDefinition() bool { +- return s == BothShortcuts || s == DefinitionShortcut -} - --// A gobMethod records the name, type, and position of a single method. --type gobMethod struct { -- Fingerprint string // string of form "methodID(params...)(results)" -- Sum uint32 // checksum of fingerprint +-type Matcher string - -- // index records only (zero in KeyOf; also for index of error.Error). -- Posn gobPosition // location of method declaration -- PkgPath int // path of package containing method declaration -- ObjectPath int // object path of method relative to PkgPath --} +-const ( +- Fuzzy Matcher = "Fuzzy" +- CaseInsensitive Matcher = "CaseInsensitive" +- CaseSensitive Matcher = "CaseSensitive" +-) - --// A gobPosition records the file, offset, and length of an identifier. --type gobPosition struct { -- File int // index into gobPackage.Strings -- Offset, Len int // in bytes --} -diff -urN a/gopls/internal/lsp/source/options.go b/gopls/internal/lsp/source/options.go ---- a/gopls/internal/lsp/source/options.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/options.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,1830 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-// A SymbolMatcher controls the matching of symbols for workspace/symbol +-// requests. +-type SymbolMatcher string - --package source +-const ( +- SymbolFuzzy SymbolMatcher = "Fuzzy" +- SymbolFastFuzzy SymbolMatcher = "FastFuzzy" +- SymbolCaseInsensitive SymbolMatcher = "CaseInsensitive" +- SymbolCaseSensitive SymbolMatcher = "CaseSensitive" +-) - --import ( -- "context" -- "fmt" -- "io" -- "path/filepath" -- "regexp" -- "runtime" -- "strings" -- "sync" -- "time" +-// A SymbolStyle controls the formatting of symbols in workspace/symbol results. +-type SymbolStyle string - -- "golang.org/x/tools/go/analysis" -- "golang.org/x/tools/go/analysis/passes/appends" -- "golang.org/x/tools/go/analysis/passes/asmdecl" -- "golang.org/x/tools/go/analysis/passes/assign" -- "golang.org/x/tools/go/analysis/passes/atomic" -- "golang.org/x/tools/go/analysis/passes/atomicalign" -- "golang.org/x/tools/go/analysis/passes/bools" -- "golang.org/x/tools/go/analysis/passes/buildtag" -- "golang.org/x/tools/go/analysis/passes/cgocall" -- "golang.org/x/tools/go/analysis/passes/composite" -- "golang.org/x/tools/go/analysis/passes/copylock" -- "golang.org/x/tools/go/analysis/passes/deepequalerrors" -- "golang.org/x/tools/go/analysis/passes/defers" -- "golang.org/x/tools/go/analysis/passes/directive" -- "golang.org/x/tools/go/analysis/passes/errorsas" -- "golang.org/x/tools/go/analysis/passes/fieldalignment" -- "golang.org/x/tools/go/analysis/passes/httpresponse" -- "golang.org/x/tools/go/analysis/passes/ifaceassert" -- "golang.org/x/tools/go/analysis/passes/loopclosure" -- "golang.org/x/tools/go/analysis/passes/lostcancel" -- "golang.org/x/tools/go/analysis/passes/nilfunc" -- "golang.org/x/tools/go/analysis/passes/nilness" -- "golang.org/x/tools/go/analysis/passes/printf" -- "golang.org/x/tools/go/analysis/passes/shadow" -- "golang.org/x/tools/go/analysis/passes/shift" -- "golang.org/x/tools/go/analysis/passes/slog" -- "golang.org/x/tools/go/analysis/passes/sortslice" -- "golang.org/x/tools/go/analysis/passes/stdmethods" -- "golang.org/x/tools/go/analysis/passes/stringintconv" -- "golang.org/x/tools/go/analysis/passes/structtag" -- "golang.org/x/tools/go/analysis/passes/testinggoroutine" -- "golang.org/x/tools/go/analysis/passes/tests" -- "golang.org/x/tools/go/analysis/passes/timeformat" -- "golang.org/x/tools/go/analysis/passes/unmarshal" -- "golang.org/x/tools/go/analysis/passes/unreachable" -- "golang.org/x/tools/go/analysis/passes/unsafeptr" -- "golang.org/x/tools/go/analysis/passes/unusedresult" -- "golang.org/x/tools/go/analysis/passes/unusedwrite" -- "golang.org/x/tools/gopls/internal/lsp/analysis/deprecated" -- "golang.org/x/tools/gopls/internal/lsp/analysis/embeddirective" -- "golang.org/x/tools/gopls/internal/lsp/analysis/fillreturns" -- "golang.org/x/tools/gopls/internal/lsp/analysis/fillstruct" -- "golang.org/x/tools/gopls/internal/lsp/analysis/infertypeargs" -- "golang.org/x/tools/gopls/internal/lsp/analysis/nonewvars" -- "golang.org/x/tools/gopls/internal/lsp/analysis/noresultvalues" -- "golang.org/x/tools/gopls/internal/lsp/analysis/simplifycompositelit" -- "golang.org/x/tools/gopls/internal/lsp/analysis/simplifyrange" -- "golang.org/x/tools/gopls/internal/lsp/analysis/simplifyslice" -- "golang.org/x/tools/gopls/internal/lsp/analysis/stubmethods" -- "golang.org/x/tools/gopls/internal/lsp/analysis/undeclaredname" -- "golang.org/x/tools/gopls/internal/lsp/analysis/unusedparams" -- "golang.org/x/tools/gopls/internal/lsp/analysis/unusedvariable" -- "golang.org/x/tools/gopls/internal/lsp/analysis/useany" -- "golang.org/x/tools/gopls/internal/lsp/command" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/internal/diff" -- "golang.org/x/tools/internal/diff/myers" +-const ( +- // PackageQualifiedSymbols is package qualified symbols i.e. +- // "pkg.Foo.Field". +- PackageQualifiedSymbols SymbolStyle = "Package" +- // FullyQualifiedSymbols is fully qualified symbols, i.e. +- // "path/to/pkg.Foo.Field". +- FullyQualifiedSymbols SymbolStyle = "Full" +- // DynamicSymbols uses whichever qualifier results in the highest scoring +- // match for the given symbol query. Here a "qualifier" is any "/" or "." +- // delimited suffix of the fully qualified symbol. i.e. "to/pkg.Foo.Field" or +- // just "Foo.Field". +- DynamicSymbols SymbolStyle = "Dynamic" -) - --var ( -- optionsOnce sync.Once -- defaultOptions *Options +-// A SymbolScope controls the search scope for workspace/symbol requests. +-type SymbolScope string +- +-const ( +- // WorkspaceSymbolScope matches symbols in workspace packages only. +- WorkspaceSymbolScope SymbolScope = "workspace" +- // AllSymbolScope matches symbols in any loaded package, including +- // dependencies. +- AllSymbolScope SymbolScope = "all" -) - --// DefaultOptions is the options that are used for Gopls execution independent --// of any externally provided configuration (LSP initialization, command --// invocation, etc.). --func DefaultOptions(overrides ...func(*Options)) *Options { -- optionsOnce.Do(func() { -- var commands []string -- for _, c := range command.Commands { -- commands = append(commands, c.ID()) -- } -- defaultOptions = &Options{ -- ClientOptions: ClientOptions{ -- InsertTextFormat: protocol.PlainTextTextFormat, -- PreferredContentFormat: protocol.Markdown, -- ConfigurationSupported: true, -- DynamicConfigurationSupported: true, -- DynamicRegistrationSemanticTokensSupported: true, -- DynamicWatchedFilesSupported: true, -- LineFoldingOnly: false, -- HierarchicalDocumentSymbolSupport: true, -- }, -- ServerOptions: ServerOptions{ -- SupportedCodeActions: map[FileKind]map[protocol.CodeActionKind]bool{ -- Go: { -- protocol.SourceFixAll: true, -- protocol.SourceOrganizeImports: true, -- protocol.QuickFix: true, -- protocol.RefactorRewrite: true, -- protocol.RefactorInline: true, -- protocol.RefactorExtract: true, -- }, -- Mod: { -- protocol.SourceOrganizeImports: true, -- protocol.QuickFix: true, -- }, -- Work: {}, -- Sum: {}, -- Tmpl: {}, -- }, -- SupportedCommands: commands, -- }, -- UserOptions: UserOptions{ -- BuildOptions: BuildOptions{ -- ExpandWorkspaceToModule: true, -- MemoryMode: ModeNormal, -- DirectoryFilters: []string{"-**/node_modules"}, -- TemplateExtensions: []string{}, -- StandaloneTags: []string{"ignore"}, -- }, -- UIOptions: UIOptions{ -- DiagnosticOptions: DiagnosticOptions{ -- Annotations: map[Annotation]bool{ -- Bounds: true, -- Escape: true, -- Inline: true, -- Nil: true, -- }, -- Vulncheck: ModeVulncheckOff, -- DiagnosticsDelay: 1 * time.Second, -- DiagnosticsTrigger: DiagnosticsOnEdit, -- AnalysisProgressReporting: true, -- }, -- InlayHintOptions: InlayHintOptions{}, -- DocumentationOptions: DocumentationOptions{ -- HoverKind: FullDocumentation, -- LinkTarget: "pkg.go.dev", -- LinksInHover: true, -- }, -- NavigationOptions: NavigationOptions{ -- ImportShortcut: BothShortcuts, -- SymbolMatcher: SymbolFastFuzzy, -- SymbolStyle: DynamicSymbols, -- SymbolScope: AllSymbolScope, -- }, -- CompletionOptions: CompletionOptions{ -- Matcher: Fuzzy, -- CompletionBudget: 100 * time.Millisecond, -- ExperimentalPostfixCompletions: true, -- CompleteFunctionCalls: true, -- }, -- Codelenses: map[string]bool{ -- string(command.Generate): true, -- string(command.RegenerateCgo): true, -- string(command.Tidy): true, -- string(command.GCDetails): false, -- string(command.UpgradeDependency): true, -- string(command.Vendor): true, -- // TODO(hyangah): enable command.RunGovulncheck. -- }, -- }, -- }, -- InternalOptions: InternalOptions{ -- LiteralCompletions: true, -- TempModfile: true, -- CompleteUnimported: true, -- CompletionDocumentation: true, -- DeepCompletion: true, -- ChattyDiagnostics: true, -- NewDiff: "new", -- SubdirWatchPatterns: SubdirWatchPatternsAuto, -- ReportAnalysisProgressAfter: 5 * time.Second, -- TelemetryPrompt: false, -- LinkifyShowMessage: false, -- }, -- Hooks: Hooks{ -- // TODO(adonovan): switch to new diff.Strings implementation. -- ComputeEdits: myers.ComputeEdits, -- URLRegexp: urlRegexp(), -- DefaultAnalyzers: defaultAnalyzers(), -- TypeErrorAnalyzers: typeErrorAnalyzers(), -- ConvenienceAnalyzers: convenienceAnalyzers(), -- StaticcheckAnalyzers: map[string]*Analyzer{}, -- GoDiff: true, -- }, -- } -- }) -- options := defaultOptions.Clone() -- for _, override := range overrides { -- if override != nil { -- override(options) -- } -- } -- return options --} +-type HoverKind string - --// Options holds various configuration that affects Gopls execution, organized --// by the nature or origin of the settings. --type Options struct { -- ClientOptions -- ServerOptions -- UserOptions -- InternalOptions -- Hooks +-const ( +- SingleLine HoverKind = "SingleLine" +- NoDocumentation HoverKind = "NoDocumentation" +- SynopsisDocumentation HoverKind = "SynopsisDocumentation" +- FullDocumentation HoverKind = "FullDocumentation" +- +- // Structured is an experimental setting that returns a structured hover format. +- // This format separates the signature from the documentation, so that the client +- // can do more manipulation of these fields. +- // +- // This should only be used by clients that support this behavior. +- Structured HoverKind = "Structured" +-) +- +-type VulncheckMode string +- +-const ( +- // Disable vulnerability analysis. +- ModeVulncheckOff VulncheckMode = "Off" +- // In Imports mode, `gopls` will report vulnerabilities that affect packages +- // directly and indirectly used by the analyzed main module. +- ModeVulncheckImports VulncheckMode = "Imports" +- +- // TODO: VulncheckRequire, VulncheckCallgraph +-) +- +-type DiagnosticsTrigger string +- +-const ( +- // Trigger diagnostics on file edit and save. (default) +- DiagnosticsOnEdit DiagnosticsTrigger = "Edit" +- // Trigger diagnostics only on file save. Events like initial workspace load +- // or configuration change will still trigger diagnostics. +- DiagnosticsOnSave DiagnosticsTrigger = "Save" +- // TODO: support "Manual"? +-) +- +-type OptionResults []OptionResult +- +-type OptionResult struct { +- Name string +- Value any +- Error error -} - --// IsAnalyzerEnabled reports whether an analyzer with the given name is --// enabled. --// --// TODO(rfindley): refactor to simplify this function. We no longer need the --// different categories of analyzer. --func (opts *Options) IsAnalyzerEnabled(name string) bool { -- for _, amap := range []map[string]*Analyzer{opts.DefaultAnalyzers, opts.TypeErrorAnalyzers, opts.ConvenienceAnalyzers, opts.StaticcheckAnalyzers} { -- for _, analyzer := range amap { -- if analyzer.Analyzer.Name == name && analyzer.IsEnabled(opts) { -- return true +-func SetOptions(options *Options, opts any) OptionResults { +- var results OptionResults +- switch opts := opts.(type) { +- case nil: +- case map[string]any: +- // If the user's settings contains "allExperiments", set that first, +- // and then let them override individual settings independently. +- var enableExperiments bool +- for name, value := range opts { +- if b, ok := value.(bool); name == "allExperiments" && ok && b { +- enableExperiments = true +- options.EnableAllExperiments() - } - } +- seen := map[string]struct{}{} +- for name, value := range opts { +- results = append(results, options.set(name, value, seen)) +- } +- // Finally, enable any experimental features that are specified in +- // maps, which allows users to individually toggle them on or off. +- if enableExperiments { +- options.enableAllExperimentMaps() +- } +- default: +- results = append(results, OptionResult{ +- Value: opts, +- Error: fmt.Errorf("Invalid options type %T", opts), +- }) - } -- return false --} -- --// ClientOptions holds LSP-specific configuration that is provided by the --// client. --type ClientOptions struct { -- ClientInfo *protocol.Msg_XInitializeParams_clientInfo -- InsertTextFormat protocol.InsertTextFormat -- ConfigurationSupported bool -- DynamicConfigurationSupported bool -- DynamicRegistrationSemanticTokensSupported bool -- DynamicWatchedFilesSupported bool -- PreferredContentFormat protocol.MarkupKind -- LineFoldingOnly bool -- HierarchicalDocumentSymbolSupport bool -- SemanticTypes []string -- SemanticMods []string -- RelatedInformationSupported bool -- CompletionTags bool -- CompletionDeprecated bool -- SupportedResourceOperations []protocol.ResourceOperationKind --} -- --// ServerOptions holds LSP-specific configuration that is provided by the --// server. --type ServerOptions struct { -- SupportedCodeActions map[FileKind]map[protocol.CodeActionKind]bool -- SupportedCommands []string +- return results -} - --type BuildOptions struct { -- // BuildFlags is the set of flags passed on to the build system when invoked. -- // It is applied to queries like `go list`, which is used when discovering files. -- // The most common use is to set `-tags`. -- BuildFlags []string +-func (o *Options) ForClientCapabilities(clientName *protocol.ClientInfo, caps protocol.ClientCapabilities) { +- o.ClientInfo = clientName +- // Check if the client supports snippets in completion items. +- if caps.Workspace.WorkspaceEdit != nil { +- o.SupportedResourceOperations = caps.Workspace.WorkspaceEdit.ResourceOperations +- } +- if c := caps.TextDocument.Completion; c.CompletionItem.SnippetSupport { +- o.InsertTextFormat = protocol.SnippetTextFormat +- } +- // Check if the client supports configuration messages. +- o.ConfigurationSupported = caps.Workspace.Configuration +- o.DynamicConfigurationSupported = caps.Workspace.DidChangeConfiguration.DynamicRegistration +- o.DynamicRegistrationSemanticTokensSupported = caps.TextDocument.SemanticTokens.DynamicRegistration +- o.DynamicWatchedFilesSupported = caps.Workspace.DidChangeWatchedFiles.DynamicRegistration +- o.RelativePatternsSupported = caps.Workspace.DidChangeWatchedFiles.RelativePatternSupport - -- // Env adds environment variables to external commands run by `gopls`, most notably `go list`. -- Env map[string]string +- // Check which types of content format are supported by this client. +- if hover := caps.TextDocument.Hover; hover != nil && len(hover.ContentFormat) > 0 { +- o.PreferredContentFormat = hover.ContentFormat[0] +- } +- // Check if the client supports only line folding. - -- // DirectoryFilters can be used to exclude unwanted directories from the -- // workspace. By default, all directories are included. Filters are an -- // operator, `+` to include and `-` to exclude, followed by a path prefix -- // relative to the workspace folder. They are evaluated in order, and -- // the last filter that applies to a path controls whether it is included. -- // The path prefix can be empty, so an initial `-` excludes everything. -- // -- // DirectoryFilters also supports the `**` operator to match 0 or more directories. -- // -- // Examples: -- // -- // Exclude node_modules at current depth: `-node_modules` -- // -- // Exclude node_modules at any depth: `-**/node_modules` -- // -- // Include only project_a: `-` (exclude everything), `+project_a` -- // -- // Include only project_a, but not node_modules inside it: `-`, `+project_a`, `-project_a/node_modules` -- DirectoryFilters []string +- if fr := caps.TextDocument.FoldingRange; fr != nil { +- o.LineFoldingOnly = fr.LineFoldingOnly +- } +- // Check if the client supports hierarchical document symbols. +- o.HierarchicalDocumentSymbolSupport = caps.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport - -- // TemplateExtensions gives the extensions of file names that are treateed -- // as template files. (The extension -- // is the part of the file name after the final dot.) -- TemplateExtensions []string +- // Client's semantic tokens +- o.SemanticTypes = caps.TextDocument.SemanticTokens.TokenTypes +- o.SemanticMods = caps.TextDocument.SemanticTokens.TokenModifiers +- // we don't need Requests, as we support full functionality +- // we don't need Formats, as there is only one, for now - -- // MemoryMode controls the tradeoff `gopls` makes between memory usage and -- // correctness. -- // -- // Values other than `Normal` are untested and may break in surprising ways. -- MemoryMode MemoryMode `status:"experimental"` +- // Check if the client supports diagnostic related information. +- o.RelatedInformationSupported = caps.TextDocument.PublishDiagnostics.RelatedInformation +- // Check if the client completion support includes tags (preferred) or deprecation +- if caps.TextDocument.Completion.CompletionItem.TagSupport != nil && +- caps.TextDocument.Completion.CompletionItem.TagSupport.ValueSet != nil { +- o.CompletionTags = true +- } else if caps.TextDocument.Completion.CompletionItem.DeprecatedSupport { +- o.CompletionDeprecated = true +- } - -- // ExpandWorkspaceToModule instructs `gopls` to adjust the scope of the -- // workspace to find the best available module root. `gopls` first looks for -- // a go.mod file in any parent directory of the workspace folder, expanding -- // the scope to that directory if it exists. If no viable parent directory is -- // found, gopls will check if there is exactly one child directory containing -- // a go.mod file, narrowing the scope to that directory if it exists. -- ExpandWorkspaceToModule bool `status:"experimental"` +- // Check if the client supports code actions resolving. +- if caps.TextDocument.CodeAction.DataSupport && caps.TextDocument.CodeAction.ResolveSupport != nil { +- o.CodeActionResolveOptions = caps.TextDocument.CodeAction.ResolveSupport.Properties +- } +-} - -- // AllowModfileModifications disables -mod=readonly, allowing imports from -- // out-of-scope modules. This option will eventually be removed. -- AllowModfileModifications bool `status:"experimental"` +-func (o *Options) Clone() *Options { +- // TODO(rfindley): has this function gone stale? It appears that there are +- // settings that are incorrectly cloned here (such as TemplateExtensions). +- result := &Options{ +- ClientOptions: o.ClientOptions, +- InternalOptions: o.InternalOptions, +- Hooks: Hooks{ +- StaticcheckSupported: o.StaticcheckSupported, +- GofumptFormat: o.GofumptFormat, +- URLRegexp: o.URLRegexp, +- }, +- ServerOptions: o.ServerOptions, +- UserOptions: o.UserOptions, +- } +- // Fully clone any slice or map fields. Only Hooks, ExperimentalOptions, +- // and UserOptions can be modified. +- copyStringMap := func(src map[string]bool) map[string]bool { +- dst := make(map[string]bool) +- for k, v := range src { +- dst[k] = v +- } +- return dst +- } +- result.Analyses = copyStringMap(o.Analyses) +- result.Codelenses = copyStringMap(o.Codelenses) - -- // AllowImplicitNetworkAccess disables GOPROXY=off, allowing implicit module -- // downloads rather than requiring user action. This option will eventually -- // be removed. -- AllowImplicitNetworkAccess bool `status:"experimental"` +- copySlice := func(src []string) []string { +- dst := make([]string, len(src)) +- copy(dst, src) +- return dst +- } +- result.SetEnvSlice(o.EnvSlice()) +- result.BuildFlags = copySlice(o.BuildFlags) +- result.DirectoryFilters = copySlice(o.DirectoryFilters) +- result.StandaloneTags = copySlice(o.StandaloneTags) - -- // StandaloneTags specifies a set of build constraints that identify -- // individual Go source files that make up the entire main package of an -- // executable. -- // -- // A common example of standalone main files is the convention of using the -- // directive `//go:build ignore` to denote files that are not intended to be -- // included in any package, for example because they are invoked directly by -- // the developer using `go run`. -- // -- // Gopls considers a file to be a standalone main file if and only if it has -- // package name "main" and has a build directive of the exact form -- // "//go:build tag" or "// +build tag", where tag is among the list of tags -- // configured by this setting. Notably, if the build constraint is more -- // complicated than a simple tag (such as the composite constraint -- // `//go:build tag && go1.18`), the file is not considered to be a standalone -- // main file. -- // -- // This setting is only supported when gopls is built with Go 1.16 or later. -- StandaloneTags []string +- copyAnalyzerMap := func(src map[string]*Analyzer) map[string]*Analyzer { +- dst := make(map[string]*Analyzer) +- for k, v := range src { +- dst[k] = v +- } +- return dst +- } +- result.DefaultAnalyzers = copyAnalyzerMap(o.DefaultAnalyzers) +- result.StaticcheckAnalyzers = copyAnalyzerMap(o.StaticcheckAnalyzers) +- return result -} - --type UIOptions struct { -- DocumentationOptions -- CompletionOptions -- NavigationOptions -- DiagnosticOptions -- InlayHintOptions +-func (o *Options) AddStaticcheckAnalyzer(a *analysis.Analyzer, enabled bool, severity protocol.DiagnosticSeverity) { +- o.StaticcheckAnalyzers[a.Name] = &Analyzer{ +- Analyzer: a, +- Enabled: enabled, +- Severity: severity, +- } +-} - -- // Codelenses overrides the enabled/disabled state of code lenses. See the -- // "Code Lenses" section of the -- // [Settings page](https://github.com/golang/tools/blob/master/gopls/doc/settings.md#code-lenses) -- // for the list of supported lenses. -- // -- // Example Usage: -- // -- // ```json5 -- // "gopls": { -- // ... -- // "codelenses": { -- // "generate": false, // Don't show the `go generate` lens. -- // "gc_details": true // Show a code lens toggling the display of gc's choices. -- // } -- // ... -- // } -- // ``` -- Codelenses map[string]bool +-// EnableAllExperiments turns on all of the experimental "off-by-default" +-// features offered by gopls. Any experimental features specified in maps +-// should be enabled in enableAllExperimentMaps. +-func (o *Options) EnableAllExperiments() { +- o.SemanticTokens = true +-} - -- // SemanticTokens controls whether the LSP server will send -- // semantic tokens to the client. -- SemanticTokens bool `status:"experimental"` +-func (o *Options) enableAllExperimentMaps() { +- if _, ok := o.Codelenses[string(command.GCDetails)]; !ok { +- o.Codelenses[string(command.GCDetails)] = true +- } +- if _, ok := o.Codelenses[string(command.RunGovulncheck)]; !ok { +- o.Codelenses[string(command.RunGovulncheck)] = true +- } +- if _, ok := o.Analyses[unusedvariable.Analyzer.Name]; !ok { +- o.Analyses[unusedvariable.Analyzer.Name] = true +- } +-} - -- // NoSemanticString turns off the sending of the semantic token 'string' -- NoSemanticString bool `status:"experimental"` +-// validateDirectoryFilter validates if the filter string +-// - is not empty +-// - start with either + or - +-// - doesn't contain currently unsupported glob operators: *, ? +-func validateDirectoryFilter(ifilter string) (string, error) { +- filter := fmt.Sprint(ifilter) +- if filter == "" || (filter[0] != '+' && filter[0] != '-') { +- return "", fmt.Errorf("invalid filter %v, must start with + or -", filter) +- } +- segs := strings.Split(filter[1:], "/") +- unsupportedOps := [...]string{"?", "*"} +- for _, seg := range segs { +- if seg != "**" { +- for _, op := range unsupportedOps { +- if strings.Contains(seg, op) { +- return "", fmt.Errorf("invalid filter %v, operator %v not supported. If you want to have this operator supported, consider filing an issue.", filter, op) +- } +- } +- } +- } - -- // NoSemanticNumber turns off the sending of the semantic token 'number' -- NoSemanticNumber bool `status:"experimental"` +- return strings.TrimRight(filepath.FromSlash(filter), "/"), nil -} - --type CompletionOptions struct { -- // Placeholders enables placeholders for function parameters or struct -- // fields in completion responses. -- UsePlaceholders bool -- -- // CompletionBudget is the soft latency goal for completion requests. Most -- // requests finish in a couple milliseconds, but in some cases deep -- // completions can take much longer. As we use up our budget we -- // dynamically reduce the search scope to ensure we return timely -- // results. Zero means unlimited. -- CompletionBudget time.Duration `status:"debug"` +-func (o *Options) set(name string, value interface{}, seen map[string]struct{}) OptionResult { +- // Flatten the name in case we get options with a hierarchy. +- split := strings.Split(name, ".") +- name = split[len(split)-1] - -- // Matcher sets the algorithm that is used when calculating completion -- // candidates. -- Matcher Matcher `status:"advanced"` +- result := OptionResult{Name: name, Value: value} +- if _, ok := seen[name]; ok { +- result.parseErrorf("duplicate configuration for %s", name) +- } +- seen[name] = struct{}{} - -- // ExperimentalPostfixCompletions enables artificial method snippets -- // such as "someSlice.sort!". -- ExperimentalPostfixCompletions bool `status:"experimental"` +- switch name { +- case "env": +- menv, ok := value.(map[string]interface{}) +- if !ok { +- result.parseErrorf("invalid type %T, expect map", value) +- break +- } +- if o.Env == nil { +- o.Env = make(map[string]string) +- } +- for k, v := range menv { +- o.Env[k] = fmt.Sprint(v) +- } - -- // CompleteFunctionCalls enables function call completion. -- // -- // When completing a statement, or when a function return type matches the -- // expected of the expression being completed, completion may suggest call -- // expressions (i.e. may include parentheses). -- CompleteFunctionCalls bool --} +- case "buildFlags": +- // TODO(rfindley): use asStringSlice. +- iflags, ok := value.([]interface{}) +- if !ok { +- result.parseErrorf("invalid type %T, expect list", value) +- break +- } +- flags := make([]string, 0, len(iflags)) +- for _, flag := range iflags { +- flags = append(flags, fmt.Sprintf("%s", flag)) +- } +- o.BuildFlags = flags - --type DocumentationOptions struct { -- // HoverKind controls the information that appears in the hover text. -- // SingleLine and Structured are intended for use only by authors of editor plugins. -- HoverKind HoverKind +- case "directoryFilters": +- // TODO(rfindley): use asStringSlice. +- ifilters, ok := value.([]interface{}) +- if !ok { +- result.parseErrorf("invalid type %T, expect list", value) +- break +- } +- var filters []string +- for _, ifilter := range ifilters { +- filter, err := validateDirectoryFilter(fmt.Sprintf("%v", ifilter)) +- if err != nil { +- result.parseErrorf("%v", err) +- return result +- } +- filters = append(filters, strings.TrimRight(filepath.FromSlash(filter), "/")) +- } +- o.DirectoryFilters = filters - -- // LinkTarget controls where documentation links go. -- // It might be one of: -- // -- // * `"godoc.org"` -- // * `"pkg.go.dev"` -- // -- // If company chooses to use its own `godoc.org`, its address can be used as well. -- // -- // Modules matching the GOPRIVATE environment variable will not have -- // documentation links in hover. -- LinkTarget string +- case "memoryMode": +- result.deprecated("") +- case "completionDocumentation": +- result.setBool(&o.CompletionDocumentation) +- case "usePlaceholders": +- result.setBool(&o.UsePlaceholders) +- case "deepCompletion": +- result.setBool(&o.DeepCompletion) +- case "completeUnimported": +- result.setBool(&o.CompleteUnimported) +- case "completionBudget": +- result.setDuration(&o.CompletionBudget) +- case "matcher": +- if s, ok := result.asOneOf( +- string(Fuzzy), +- string(CaseSensitive), +- string(CaseInsensitive), +- ); ok { +- o.Matcher = Matcher(s) +- } - -- // LinksInHover toggles the presence of links to documentation in hover. -- LinksInHover bool --} +- case "symbolMatcher": +- if s, ok := result.asOneOf( +- string(SymbolFuzzy), +- string(SymbolFastFuzzy), +- string(SymbolCaseInsensitive), +- string(SymbolCaseSensitive), +- ); ok { +- o.SymbolMatcher = SymbolMatcher(s) +- } - --type FormattingOptions struct { -- // Local is the equivalent of the `goimports -local` flag, which puts -- // imports beginning with this string after third-party packages. It should -- // be the prefix of the import path whose imports should be grouped -- // separately. -- Local string +- case "symbolStyle": +- if s, ok := result.asOneOf( +- string(FullyQualifiedSymbols), +- string(PackageQualifiedSymbols), +- string(DynamicSymbols), +- ); ok { +- o.SymbolStyle = SymbolStyle(s) +- } - -- // Gofumpt indicates if we should run gofumpt formatting. -- Gofumpt bool --} +- case "symbolScope": +- if s, ok := result.asOneOf( +- string(WorkspaceSymbolScope), +- string(AllSymbolScope), +- ); ok { +- o.SymbolScope = SymbolScope(s) +- } - --type DiagnosticOptions struct { -- // Analyses specify analyses that the user would like to enable or disable. -- // A map of the names of analysis passes that should be enabled/disabled. -- // A full list of analyzers that gopls uses can be found in -- // [analyzers.md](https://github.com/golang/tools/blob/master/gopls/doc/analyzers.md). -- // -- // Example Usage: -- // -- // ```json5 -- // ... -- // "analyses": { -- // "unreachable": false, // Disable the unreachable analyzer. -- // "unusedparams": true // Enable the unusedparams analyzer. -- // } -- // ... -- // ``` -- Analyses map[string]bool +- case "hoverKind": +- if s, ok := result.asOneOf( +- string(NoDocumentation), +- string(SingleLine), +- string(SynopsisDocumentation), +- string(FullDocumentation), +- string(Structured), +- ); ok { +- o.HoverKind = HoverKind(s) +- } - -- // Staticcheck enables additional analyses from staticcheck.io. -- // These analyses are documented on -- // [Staticcheck's website](https://staticcheck.io/docs/checks/). -- Staticcheck bool `status:"experimental"` +- case "linkTarget": +- result.setString(&o.LinkTarget) - -- // Annotations specifies the various kinds of optimization diagnostics -- // that should be reported by the gc_details command. -- Annotations map[Annotation]bool `status:"experimental"` +- case "linksInHover": +- result.setBool(&o.LinksInHover) - -- // Vulncheck enables vulnerability scanning. -- Vulncheck VulncheckMode `status:"experimental"` +- case "importShortcut": +- if s, ok := result.asOneOf(string(BothShortcuts), string(LinkShortcut), string(DefinitionShortcut)); ok { +- o.ImportShortcut = ImportShortcut(s) +- } - -- // DiagnosticsDelay controls the amount of time that gopls waits -- // after the most recent file modification before computing deep diagnostics. -- // Simple diagnostics (parsing and type-checking) are always run immediately -- // on recently modified packages. -- // -- // This option must be set to a valid duration string, for example `"250ms"`. -- DiagnosticsDelay time.Duration `status:"advanced"` +- case "analyses": +- result.setBoolMap(&o.Analyses) - -- // DiagnosticsTrigger controls when to run diagnostics. -- DiagnosticsTrigger DiagnosticsTrigger `status:"experimental"` +- case "hints": +- result.setBoolMap(&o.Hints) - -- // AnalysisProgressReporting controls whether gopls sends progress -- // notifications when construction of its index of analysis facts is taking a -- // long time. Cancelling these notifications will cancel the indexing task, -- // though it will restart after the next change in the workspace. -- // -- // When a package is opened for the first time and heavyweight analyses such as -- // staticcheck are enabled, it can take a while to construct the index of -- // analysis facts for all its dependencies. The index is cached in the -- // filesystem, so subsequent analysis should be faster. -- AnalysisProgressReporting bool --} +- case "annotations": +- result.setAnnotationMap(&o.Annotations) - --type InlayHintOptions struct { -- // Hints specify inlay hints that users want to see. A full list of hints -- // that gopls uses can be found in -- // [inlayHints.md](https://github.com/golang/tools/blob/master/gopls/doc/inlayHints.md). -- Hints map[string]bool `status:"experimental"` --} +- case "vulncheck": +- if s, ok := result.asOneOf( +- string(ModeVulncheckOff), +- string(ModeVulncheckImports), +- ); ok { +- o.Vulncheck = VulncheckMode(s) +- } - --type NavigationOptions struct { -- // ImportShortcut specifies whether import statements should link to -- // documentation or go to definitions. -- ImportShortcut ImportShortcut +- case "codelenses", "codelens": +- var lensOverrides map[string]bool +- result.setBoolMap(&lensOverrides) +- if result.Error == nil { +- if o.Codelenses == nil { +- o.Codelenses = make(map[string]bool) +- } +- for lens, enabled := range lensOverrides { +- o.Codelenses[lens] = enabled +- } +- } - -- // SymbolMatcher sets the algorithm that is used when finding workspace symbols. -- SymbolMatcher SymbolMatcher `status:"advanced"` +- // codelens is deprecated, but still works for now. +- // TODO(rstambler): Remove this for the gopls/v0.7.0 release. +- if name == "codelens" { +- result.deprecated("codelenses") +- } - -- // SymbolStyle controls how symbols are qualified in symbol responses. -- // -- // Example Usage: -- // -- // ```json5 -- // "gopls": { -- // ... -- // "symbolStyle": "Dynamic", -- // ... -- // } -- // ``` -- SymbolStyle SymbolStyle `status:"advanced"` +- case "staticcheck": +- if v, ok := result.asBool(); ok { +- o.Staticcheck = v +- if v && !o.StaticcheckSupported { +- result.Error = fmt.Errorf("applying setting %q: staticcheck is not supported at %s;"+ +- " rebuild gopls with a more recent version of Go", result.Name, runtime.Version()) +- } +- } - -- // SymbolScope controls which packages are searched for workspace/symbol -- // requests. The default value, "workspace", searches only workspace -- // packages. The legacy behavior, "all", causes all loaded packages to be -- // searched, including dependencies; this is more expensive and may return -- // unwanted results. -- SymbolScope SymbolScope --} +- case "local": +- result.setString(&o.Local) - --// UserOptions holds custom Gopls configuration (not part of the LSP) that is --// modified by the client. --type UserOptions struct { -- BuildOptions -- UIOptions -- FormattingOptions +- case "verboseOutput": +- result.setBool(&o.VerboseOutput) - -- // VerboseOutput enables additional debug logging. -- VerboseOutput bool `status:"debug"` --} +- case "verboseWorkDoneProgress": +- result.setBool(&o.VerboseWorkDoneProgress) - --// EnvSlice returns Env as a slice of k=v strings. --func (u *UserOptions) EnvSlice() []string { -- var result []string -- for k, v := range u.Env { -- result = append(result, fmt.Sprintf("%v=%v", k, v)) -- } -- return result --} +- case "tempModFile": +- result.deprecated("") - --// SetEnvSlice sets Env from a slice of k=v strings. --func (u *UserOptions) SetEnvSlice(env []string) { -- u.Env = map[string]string{} -- for _, kv := range env { -- split := strings.SplitN(kv, "=", 2) -- if len(split) != 2 { -- continue +- case "showBugReports": +- result.setBool(&o.ShowBugReports) +- +- case "gofumpt": +- if v, ok := result.asBool(); ok { +- o.Gofumpt = v +- if v && o.GofumptFormat == nil { +- result.Error = fmt.Errorf("applying setting %q: gofumpt is not supported at %s;"+ +- " rebuild gopls with a more recent version of Go", result.Name, runtime.Version()) +- } - } -- u.Env[split[0]] = split[1] -- } --} +- case "completeFunctionCalls": +- result.setBool(&o.CompleteFunctionCalls) - --// DiffFunction is the type for a function that produces a set of edits that --// convert from the before content to the after content. --type DiffFunction func(before, after string) []diff.Edit +- case "semanticTokens": +- result.setBool(&o.SemanticTokens) - --// Hooks contains configuration that is provided to the Gopls command by the --// main package. --type Hooks struct { -- // LicensesText holds third party licenses for software used by gopls. -- LicensesText string +- case "noSemanticString": +- result.setBool(&o.NoSemanticString) - -- // GoDiff is used in gopls/hooks to get Myers' diff -- GoDiff bool +- case "noSemanticNumber": +- result.setBool(&o.NoSemanticNumber) - -- // Whether staticcheck is supported. -- StaticcheckSupported bool +- case "expandWorkspaceToModule": +- // See golang/go#63536: we can consider deprecating +- // expandWorkspaceToModule, but probably need to change the default +- // behavior in that case to *not* expand to the module. +- result.setBool(&o.ExpandWorkspaceToModule) - -- // ComputeEdits is used to compute edits between file versions. -- ComputeEdits DiffFunction +- case "experimentalPostfixCompletions": +- result.setBool(&o.ExperimentalPostfixCompletions) - -- // URLRegexp is used to find potential URLs in comments/strings. -- // -- // Not all matches are shown to the user: if the matched URL is not detected -- // as valid, it will be skipped. -- URLRegexp *regexp.Regexp +- case "experimentalWorkspaceModule": +- result.deprecated("") - -- // GofumptFormat allows the gopls module to wire-in a call to -- // gofumpt/format.Source. langVersion and modulePath are used for some -- // Gofumpt formatting rules -- see the Gofumpt documentation for details. -- GofumptFormat func(ctx context.Context, langVersion, modulePath string, src []byte) ([]byte, error) +- case "experimentalTemplateSupport": // TODO(pjw): remove after June 2022 +- result.deprecated("") - -- DefaultAnalyzers map[string]*Analyzer -- TypeErrorAnalyzers map[string]*Analyzer -- ConvenienceAnalyzers map[string]*Analyzer -- StaticcheckAnalyzers map[string]*Analyzer --} +- case "templateExtensions": +- if iexts, ok := value.([]interface{}); ok { +- ans := []string{} +- for _, x := range iexts { +- ans = append(ans, fmt.Sprint(x)) +- } +- o.TemplateExtensions = ans +- break +- } +- if value == nil { +- o.TemplateExtensions = nil +- break +- } +- result.parseErrorf("unexpected type %T not []string", value) - --// InternalOptions contains settings that are not intended for use by the --// average user. These may be settings used by tests or outdated settings that --// will soon be deprecated. Some of these settings may not even be configurable --// by the user. --// --// TODO(rfindley): even though these settings are not intended for --// modification, some of them should be surfaced in our documentation. --type InternalOptions struct { -- // LiteralCompletions controls whether literal candidates such as -- // "&someStruct{}" are offered. Tests disable this flag to simplify -- // their expected values. -- // -- // TODO(rfindley): this is almost unnecessary now. Remove it along with the -- // old marker tests. -- LiteralCompletions bool +- case "experimentalDiagnosticsDelay": +- result.deprecated("diagnosticsDelay") - -- // VerboseWorkDoneProgress controls whether the LSP server should send -- // progress reports for all work done outside the scope of an RPC. -- // Used by the regression tests. -- VerboseWorkDoneProgress bool +- case "diagnosticsDelay": +- result.setDuration(&o.DiagnosticsDelay) - -- // The following options were previously available to users, but they -- // really shouldn't be configured by anyone other than "power users". +- case "diagnosticsTrigger": +- if s, ok := result.asOneOf( +- string(DiagnosticsOnEdit), +- string(DiagnosticsOnSave), +- ); ok { +- o.DiagnosticsTrigger = DiagnosticsTrigger(s) +- } - -- // CompletionDocumentation enables documentation with completion results. -- CompletionDocumentation bool +- case "analysisProgressReporting": +- result.setBool(&o.AnalysisProgressReporting) - -- // CompleteUnimported enables completion for packages that you do not -- // currently import. -- CompleteUnimported bool +- case "experimentalWatchedFileDelay": +- result.deprecated("") - -- // DeepCompletion enables the ability to return completions from deep -- // inside relevant entities, rather than just the locally accessible ones. -- // -- // Consider this example: -- // -- // ```go -- // package main -- // -- // import "fmt" -- // -- // type wrapString struct { -- // str string -- // } -- // -- // func main() { -- // x := wrapString{"hello world"} -- // fmt.Printf(<>) -- // } -- // ``` -- // -- // At the location of the `<>` in this program, deep completion would suggest -- // the result `x.str`. -- DeepCompletion bool +- case "experimentalPackageCacheKey": +- result.deprecated("") - -- // TempModfile controls the use of the -modfile flag in Go 1.14. -- TempModfile bool +- case "allowModfileModifications": +- result.softErrorf("gopls setting \"allowModfileModifications\" is deprecated.\nPlease comment on https://go.dev/issue/65546 if this impacts your workflow.") +- result.setBool(&o.AllowModfileModifications) - -- // ShowBugReports causes a message to be shown when the first bug is reported -- // on the server. -- // This option applies only during initialization. -- ShowBugReports bool +- case "allowImplicitNetworkAccess": +- result.setBool(&o.AllowImplicitNetworkAccess) - -- // NewDiff controls the choice of the new diff implementation. It can be -- // 'new', 'old', or 'both', which is the default. 'both' computes diffs with -- // both algorithms, checks that the new algorithm has worked, and write some -- // summary statistics to a file in os.TmpDir(). -- NewDiff string +- case "experimentalUseInvalidMetadata": +- result.deprecated("") - -- // ChattyDiagnostics controls whether to report file diagnostics for each -- // file change. If unset, gopls only reports diagnostics when they change, or -- // when a file is opened or closed. -- ChattyDiagnostics bool +- case "standaloneTags": +- result.setStringSlice(&o.StandaloneTags) - -- // SubdirWatchPatterns configures the file watching glob patterns registered -- // by gopls. -- // -- // Some clients (namely VS Code) do not send workspace/didChangeWatchedFile -- // notifications for files contained in a directory when that directory is -- // deleted: -- // https://github.com/microsoft/vscode/issues/109754 -- // -- // In this case, gopls would miss important notifications about deleted -- // packages. To work around this, gopls registers a watch pattern for each -- // directory containing Go files. -- // -- // Unfortunately, other clients experience performance problems with this -- // many watch patterns, so there is no single behavior that works well for -- // all clients. -- // -- // The "subdirWatchPatterns" setting allows configuring this behavior. Its -- // default value of "auto" attempts to guess the correct behavior based on -- // the client name. We'd love to avoid this specialization, but as described -- // above there is no single value that works for all clients. -- // -- // If any LSP client does not behave well with the default value (for -- // example, if like VS Code it drops file notifications), please file an -- // issue. -- SubdirWatchPatterns SubdirWatchPatterns +- case "allExperiments": +- // This setting should be handled before all of the other options are +- // processed, so do nothing here. - -- // ReportAnalysisProgressAfter sets the duration for gopls to wait before starting -- // progress reporting for ongoing go/analysis passes. -- // -- // It is intended to be used for testing only. -- ReportAnalysisProgressAfter time.Duration +- case "newDiff": +- result.deprecated("") - -- // TelemetryPrompt controls whether gopls prompts about enabling Go telemetry. -- // -- // Once the prompt is answered, gopls doesn't ask again, but TelemetryPrompt -- // can prevent the question from ever being asked in the first place. -- TelemetryPrompt bool +- case "subdirWatchPatterns": +- if s, ok := result.asOneOf( +- string(SubdirWatchPatternsOn), +- string(SubdirWatchPatternsOff), +- string(SubdirWatchPatternsAuto), +- ); ok { +- o.SubdirWatchPatterns = SubdirWatchPatterns(s) +- } - -- // LinkifyShowMessage controls whether the client wants gopls -- // to linkify links in showMessage. e.g. [go.dev](https://go.dev). -- LinkifyShowMessage bool --} +- case "reportAnalysisProgressAfter": +- result.setDuration(&o.ReportAnalysisProgressAfter) - --type SubdirWatchPatterns string +- case "telemetryPrompt": +- result.setBool(&o.TelemetryPrompt) - --const ( -- SubdirWatchPatternsOn SubdirWatchPatterns = "on" -- SubdirWatchPatternsOff SubdirWatchPatterns = "off" -- SubdirWatchPatternsAuto SubdirWatchPatterns = "auto" --) +- case "linkifyShowMessage": +- result.setBool(&o.LinkifyShowMessage) - --type ImportShortcut string +- case "includeReplaceInWorkspace": +- result.setBool(&o.IncludeReplaceInWorkspace) - --const ( -- BothShortcuts ImportShortcut = "Both" -- LinkShortcut ImportShortcut = "Link" -- DefinitionShortcut ImportShortcut = "Definition" --) +- case "zeroConfig": +- result.setBool(&o.ZeroConfig) - --func (s ImportShortcut) ShowLinks() bool { -- return s == BothShortcuts || s == LinkShortcut --} +- // Replaced settings. +- case "experimentalDisabledAnalyses": +- result.deprecated("analyses") - --func (s ImportShortcut) ShowDefinition() bool { -- return s == BothShortcuts || s == DefinitionShortcut --} +- case "disableDeepCompletion": +- result.deprecated("deepCompletion") - --type Matcher string +- case "disableFuzzyMatching": +- result.deprecated("fuzzyMatching") - --const ( -- Fuzzy Matcher = "Fuzzy" -- CaseInsensitive Matcher = "CaseInsensitive" -- CaseSensitive Matcher = "CaseSensitive" --) +- case "wantCompletionDocumentation": +- result.deprecated("completionDocumentation") - --// A SymbolMatcher controls the matching of symbols for workspace/symbol --// requests. --type SymbolMatcher string +- case "wantUnimportedCompletions": +- result.deprecated("completeUnimported") - --const ( -- SymbolFuzzy SymbolMatcher = "Fuzzy" -- SymbolFastFuzzy SymbolMatcher = "FastFuzzy" -- SymbolCaseInsensitive SymbolMatcher = "CaseInsensitive" -- SymbolCaseSensitive SymbolMatcher = "CaseSensitive" --) +- case "fuzzyMatching": +- result.deprecated("matcher") - --// A SymbolStyle controls the formatting of symbols in workspace/symbol results. --type SymbolStyle string +- case "caseSensitiveCompletion": +- result.deprecated("matcher") - --const ( -- // PackageQualifiedSymbols is package qualified symbols i.e. -- // "pkg.Foo.Field". -- PackageQualifiedSymbols SymbolStyle = "Package" -- // FullyQualifiedSymbols is fully qualified symbols, i.e. -- // "path/to/pkg.Foo.Field". -- FullyQualifiedSymbols SymbolStyle = "Full" -- // DynamicSymbols uses whichever qualifier results in the highest scoring -- // match for the given symbol query. Here a "qualifier" is any "/" or "." -- // delimited suffix of the fully qualified symbol. i.e. "to/pkg.Foo.Field" or -- // just "Foo.Field". -- DynamicSymbols SymbolStyle = "Dynamic" --) +- // Deprecated settings. +- case "wantSuggestedFixes": +- result.deprecated("") - --// A SymbolScope controls the search scope for workspace/symbol requests. --type SymbolScope string +- case "noIncrementalSync": +- result.deprecated("") - --const ( -- // WorkspaceSymbolScope matches symbols in workspace packages only. -- WorkspaceSymbolScope SymbolScope = "workspace" -- // AllSymbolScope matches symbols in any loaded package, including -- // dependencies. -- AllSymbolScope SymbolScope = "all" --) +- case "watchFileChanges": +- result.deprecated("") - --type HoverKind string +- case "go-diff": +- result.deprecated("") - --const ( -- SingleLine HoverKind = "SingleLine" -- NoDocumentation HoverKind = "NoDocumentation" -- SynopsisDocumentation HoverKind = "SynopsisDocumentation" -- FullDocumentation HoverKind = "FullDocumentation" +- default: +- result.unexpected() +- } +- return result +-} - -- // Structured is an experimental setting that returns a structured hover format. -- // This format separates the signature from the documentation, so that the client -- // can do more manipulation of these fields. -- // -- // This should only be used by clients that support this behavior. -- Structured HoverKind = "Structured" --) +-// parseErrorf reports an error parsing the current configuration value. +-func (r *OptionResult) parseErrorf(msg string, values ...interface{}) { +- if false { +- _ = fmt.Sprintf(msg, values...) // this causes vet to check this like printf +- } +- prefix := fmt.Sprintf("parsing setting %q: ", r.Name) +- r.Error = fmt.Errorf(prefix+msg, values...) +-} - --type MemoryMode string +-// A SoftError is an error that does not affect the functionality of gopls. +-type SoftError struct { +- msg string +-} - --const ( -- ModeNormal MemoryMode = "Normal" -- // In DegradeClosed mode, `gopls` will collect less information about -- // packages without open files. As a result, features like Find -- // References and Rename will miss results in such packages. -- ModeDegradeClosed MemoryMode = "DegradeClosed" --) +-func (e *SoftError) Error() string { +- return e.msg +-} - --type VulncheckMode string +-// deprecated reports the current setting as deprecated. If 'replacement' is +-// non-nil, it is suggested to the user. +-func (r *OptionResult) deprecated(replacement string) { +- msg := fmt.Sprintf("gopls setting %q is deprecated", r.Name) +- if replacement != "" { +- msg = fmt.Sprintf("%s, use %q instead", msg, replacement) +- } +- r.Error = &SoftError{msg} +-} - --const ( -- // Disable vulnerability analysis. -- ModeVulncheckOff VulncheckMode = "Off" -- // In Imports mode, `gopls` will report vulnerabilities that affect packages -- // directly and indirectly used by the analyzed main module. -- ModeVulncheckImports VulncheckMode = "Imports" +-// softErrorf reports a soft error related to the current option. +-func (r *OptionResult) softErrorf(format string, args ...any) { +- r.Error = &SoftError{fmt.Sprintf(format, args...)} +-} - -- // TODO: VulncheckRequire, VulncheckCallgraph --) +-// unexpected reports that the current setting is not known to gopls. +-func (r *OptionResult) unexpected() { +- r.Error = fmt.Errorf("unexpected gopls setting %q", r.Name) +-} - --type DiagnosticsTrigger string +-func (r *OptionResult) asBool() (bool, bool) { +- b, ok := r.Value.(bool) +- if !ok { +- r.parseErrorf("invalid type %T, expect bool", r.Value) +- return false, false +- } +- return b, true +-} - --const ( -- // Trigger diagnostics on file edit and save. (default) -- DiagnosticsOnEdit DiagnosticsTrigger = "Edit" -- // Trigger diagnostics only on file save. Events like initial workspace load -- // or configuration change will still trigger diagnostics. -- DiagnosticsOnSave DiagnosticsTrigger = "Save" -- // TODO: support "Manual"? --) +-func (r *OptionResult) setBool(b *bool) { +- if v, ok := r.asBool(); ok { +- *b = v +- } +-} - --type OptionResults []OptionResult +-func (r *OptionResult) setDuration(d *time.Duration) { +- if v, ok := r.asString(); ok { +- parsed, err := time.ParseDuration(v) +- if err != nil { +- r.parseErrorf("failed to parse duration %q: %v", v, err) +- return +- } +- *d = parsed +- } +-} - --type OptionResult struct { -- Name string -- Value interface{} -- Error error +-func (r *OptionResult) setBoolMap(bm *map[string]bool) { +- m := r.asBoolMap() +- *bm = m -} - --func SetOptions(options *Options, opts interface{}) OptionResults { -- var results OptionResults -- switch opts := opts.(type) { -- case nil: -- case map[string]interface{}: -- // If the user's settings contains "allExperiments", set that first, -- // and then let them override individual settings independently. -- var enableExperiments bool -- for name, value := range opts { -- if b, ok := value.(bool); name == "allExperiments" && ok && b { -- enableExperiments = true -- options.EnableAllExperiments() +-func (r *OptionResult) setAnnotationMap(bm *map[Annotation]bool) { +- all := r.asBoolMap() +- if all == nil { +- return +- } +- // Default to everything enabled by default. +- m := make(map[Annotation]bool) +- for k, enabled := range all { +- a, err := asOneOf( +- k, +- string(Nil), +- string(Escape), +- string(Inline), +- string(Bounds), +- ) +- if err != nil { +- // In case of an error, process any legacy values. +- switch k { +- case "noEscape": +- m[Escape] = false +- r.parseErrorf(`"noEscape" is deprecated, set "Escape: false" instead`) +- case "noNilcheck": +- m[Nil] = false +- r.parseErrorf(`"noNilcheck" is deprecated, set "Nil: false" instead`) +- case "noInline": +- m[Inline] = false +- r.parseErrorf(`"noInline" is deprecated, set "Inline: false" instead`) +- case "noBounds": +- m[Bounds] = false +- r.parseErrorf(`"noBounds" is deprecated, set "Bounds: false" instead`) +- default: +- r.parseErrorf("%v", err) - } -- } -- seen := map[string]struct{}{} -- for name, value := range opts { -- results = append(results, options.set(name, value, seen)) -- } -- // Finally, enable any experimental features that are specified in -- // maps, which allows users to individually toggle them on or off. -- if enableExperiments { -- options.enableAllExperimentMaps() -- } -- default: -- results = append(results, OptionResult{ -- Value: opts, -- Error: fmt.Errorf("Invalid options type %T", opts), -- }) +- continue +- } +- m[Annotation(a)] = enabled - } -- return results +- *bm = m -} - --func (o *Options) ForClientCapabilities(clientName *protocol.Msg_XInitializeParams_clientInfo, caps protocol.ClientCapabilities) { -- o.ClientInfo = clientName -- // Check if the client supports snippets in completion items. -- if caps.Workspace.WorkspaceEdit != nil { -- o.SupportedResourceOperations = caps.Workspace.WorkspaceEdit.ResourceOperations -- } -- if c := caps.TextDocument.Completion; c.CompletionItem.SnippetSupport { -- o.InsertTextFormat = protocol.SnippetTextFormat -- } -- // Check if the client supports configuration messages. -- o.ConfigurationSupported = caps.Workspace.Configuration -- o.DynamicConfigurationSupported = caps.Workspace.DidChangeConfiguration.DynamicRegistration -- o.DynamicRegistrationSemanticTokensSupported = caps.TextDocument.SemanticTokens.DynamicRegistration -- o.DynamicWatchedFilesSupported = caps.Workspace.DidChangeWatchedFiles.DynamicRegistration -- -- // Check which types of content format are supported by this client. -- if hover := caps.TextDocument.Hover; hover != nil && len(hover.ContentFormat) > 0 { -- o.PreferredContentFormat = hover.ContentFormat[0] +-func (r *OptionResult) asBoolMap() map[string]bool { +- all, ok := r.Value.(map[string]interface{}) +- if !ok { +- r.parseErrorf("invalid type %T for map[string]bool option", r.Value) +- return nil - } -- // Check if the client supports only line folding. -- -- if fr := caps.TextDocument.FoldingRange; fr != nil { -- o.LineFoldingOnly = fr.LineFoldingOnly +- m := make(map[string]bool) +- for a, enabled := range all { +- if e, ok := enabled.(bool); ok { +- m[a] = e +- } else { +- r.parseErrorf("invalid type %T for map key %q", enabled, a) +- return m +- } - } -- // Check if the client supports hierarchical document symbols. -- o.HierarchicalDocumentSymbolSupport = caps.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport -- -- // Client's semantic tokens -- o.SemanticTypes = caps.TextDocument.SemanticTokens.TokenTypes -- o.SemanticMods = caps.TextDocument.SemanticTokens.TokenModifiers -- // we don't need Requests, as we support full functionality -- // we don't need Formats, as there is only one, for now +- return m +-} - -- // Check if the client supports diagnostic related information. -- o.RelatedInformationSupported = caps.TextDocument.PublishDiagnostics.RelatedInformation -- // Check if the client completion support includes tags (preferred) or deprecation -- if caps.TextDocument.Completion.CompletionItem.TagSupport.ValueSet != nil { -- o.CompletionTags = true -- } else if caps.TextDocument.Completion.CompletionItem.DeprecatedSupport { -- o.CompletionDeprecated = true +-func (r *OptionResult) asString() (string, bool) { +- b, ok := r.Value.(string) +- if !ok { +- r.parseErrorf("invalid type %T, expect string", r.Value) +- return "", false - } +- return b, true -} - --func (o *Options) Clone() *Options { -- // TODO(rfindley): has this function gone stale? It appears that there are -- // settings that are incorrectly cloned here (such as TemplateExtensions). -- result := &Options{ -- ClientOptions: o.ClientOptions, -- InternalOptions: o.InternalOptions, -- Hooks: Hooks{ -- GoDiff: o.GoDiff, -- StaticcheckSupported: o.StaticcheckSupported, -- ComputeEdits: o.ComputeEdits, -- GofumptFormat: o.GofumptFormat, -- URLRegexp: o.URLRegexp, -- }, -- ServerOptions: o.ServerOptions, -- UserOptions: o.UserOptions, +-func (r *OptionResult) asStringSlice() ([]string, bool) { +- iList, ok := r.Value.([]interface{}) +- if !ok { +- r.parseErrorf("invalid type %T, expect list", r.Value) +- return nil, false - } -- // Fully clone any slice or map fields. Only Hooks, ExperimentalOptions, -- // and UserOptions can be modified. -- copyStringMap := func(src map[string]bool) map[string]bool { -- dst := make(map[string]bool) -- for k, v := range src { -- dst[k] = v +- var list []string +- for _, elem := range iList { +- s, ok := elem.(string) +- if !ok { +- r.parseErrorf("invalid element type %T, expect string", elem) +- return nil, false - } -- return dst +- list = append(list, s) - } -- result.Analyses = copyStringMap(o.Analyses) -- result.Codelenses = copyStringMap(o.Codelenses) +- return list, true +-} - -- copySlice := func(src []string) []string { -- dst := make([]string, len(src)) -- copy(dst, src) -- return dst +-func (r *OptionResult) asOneOf(options ...string) (string, bool) { +- s, ok := r.asString() +- if !ok { +- return "", false - } -- result.SetEnvSlice(o.EnvSlice()) -- result.BuildFlags = copySlice(o.BuildFlags) -- result.DirectoryFilters = copySlice(o.DirectoryFilters) -- result.StandaloneTags = copySlice(o.StandaloneTags) -- -- copyAnalyzerMap := func(src map[string]*Analyzer) map[string]*Analyzer { -- dst := make(map[string]*Analyzer) -- for k, v := range src { -- dst[k] = v -- } -- return dst +- s, err := asOneOf(s, options...) +- if err != nil { +- r.parseErrorf("%v", err) - } -- result.DefaultAnalyzers = copyAnalyzerMap(o.DefaultAnalyzers) -- result.TypeErrorAnalyzers = copyAnalyzerMap(o.TypeErrorAnalyzers) -- result.ConvenienceAnalyzers = copyAnalyzerMap(o.ConvenienceAnalyzers) -- result.StaticcheckAnalyzers = copyAnalyzerMap(o.StaticcheckAnalyzers) -- return result +- return s, err == nil -} - --func (o *Options) AddStaticcheckAnalyzer(a *analysis.Analyzer, enabled bool, severity protocol.DiagnosticSeverity) { -- o.StaticcheckAnalyzers[a.Name] = &Analyzer{ -- Analyzer: a, -- Enabled: enabled, -- Severity: severity, +-func asOneOf(str string, options ...string) (string, error) { +- lower := strings.ToLower(str) +- for _, opt := range options { +- if strings.ToLower(opt) == lower { +- return opt, nil +- } - } +- return "", fmt.Errorf("invalid option %q for enum", str) -} - --// EnableAllExperiments turns on all of the experimental "off-by-default" --// features offered by gopls. Any experimental features specified in maps --// should be enabled in enableAllExperimentMaps. --func (o *Options) EnableAllExperiments() { -- o.SemanticTokens = true +-func (r *OptionResult) setString(s *string) { +- if v, ok := r.asString(); ok { +- *s = v +- } -} - --func (o *Options) enableAllExperimentMaps() { -- if _, ok := o.Codelenses[string(command.GCDetails)]; !ok { -- o.Codelenses[string(command.GCDetails)] = true -- } -- if _, ok := o.Codelenses[string(command.RunGovulncheck)]; !ok { -- o.Codelenses[string(command.RunGovulncheck)] = true -- } -- if _, ok := o.Analyses[unusedparams.Analyzer.Name]; !ok { -- o.Analyses[unusedparams.Analyzer.Name] = true -- } -- if _, ok := o.Analyses[unusedvariable.Analyzer.Name]; !ok { -- o.Analyses[unusedvariable.Analyzer.Name] = true +-func (r *OptionResult) setStringSlice(s *[]string) { +- if v, ok := r.asStringSlice(); ok { +- *s = v - } -} - --// validateDirectoryFilter validates if the filter string --// - is not empty --// - start with either + or - --// - doesn't contain currently unsupported glob operators: *, ? --func validateDirectoryFilter(ifilter string) (string, error) { -- filter := fmt.Sprint(ifilter) -- if filter == "" || (filter[0] != '+' && filter[0] != '-') { -- return "", fmt.Errorf("invalid filter %v, must start with + or -", filter) -- } -- segs := strings.Split(filter[1:], "/") -- unsupportedOps := [...]string{"?", "*"} -- for _, seg := range segs { -- if seg != "**" { -- for _, op := range unsupportedOps { -- if strings.Contains(seg, op) { -- return "", fmt.Errorf("invalid filter %v, operator %v not supported. If you want to have this operator supported, consider filing an issue.", filter, op) -- } -- } -- } -- } +-func analyzers() map[string]*Analyzer { +- return map[string]*Analyzer{ +- // The traditional vet suite: +- appends.Analyzer.Name: {Analyzer: appends.Analyzer, Enabled: true}, +- asmdecl.Analyzer.Name: {Analyzer: asmdecl.Analyzer, Enabled: true}, +- assign.Analyzer.Name: {Analyzer: assign.Analyzer, Enabled: true}, +- atomic.Analyzer.Name: {Analyzer: atomic.Analyzer, Enabled: true}, +- bools.Analyzer.Name: {Analyzer: bools.Analyzer, Enabled: true}, +- buildtag.Analyzer.Name: {Analyzer: buildtag.Analyzer, Enabled: true}, +- cgocall.Analyzer.Name: {Analyzer: cgocall.Analyzer, Enabled: true}, +- composite.Analyzer.Name: {Analyzer: composite.Analyzer, Enabled: true}, +- copylock.Analyzer.Name: {Analyzer: copylock.Analyzer, Enabled: true}, +- defers.Analyzer.Name: {Analyzer: defers.Analyzer, Enabled: true}, +- deprecated.Analyzer.Name: { +- Analyzer: deprecated.Analyzer, +- Enabled: true, +- Severity: protocol.SeverityHint, +- Tag: []protocol.DiagnosticTag{protocol.Deprecated}, +- }, +- directive.Analyzer.Name: {Analyzer: directive.Analyzer, Enabled: true}, +- errorsas.Analyzer.Name: {Analyzer: errorsas.Analyzer, Enabled: true}, +- httpresponse.Analyzer.Name: {Analyzer: httpresponse.Analyzer, Enabled: true}, +- ifaceassert.Analyzer.Name: {Analyzer: ifaceassert.Analyzer, Enabled: true}, +- loopclosure.Analyzer.Name: {Analyzer: loopclosure.Analyzer, Enabled: true}, +- lostcancel.Analyzer.Name: {Analyzer: lostcancel.Analyzer, Enabled: true}, +- nilfunc.Analyzer.Name: {Analyzer: nilfunc.Analyzer, Enabled: true}, +- printf.Analyzer.Name: {Analyzer: printf.Analyzer, Enabled: true}, +- shift.Analyzer.Name: {Analyzer: shift.Analyzer, Enabled: true}, +- slog.Analyzer.Name: {Analyzer: slog.Analyzer, Enabled: true}, +- stdmethods.Analyzer.Name: {Analyzer: stdmethods.Analyzer, Enabled: true}, +- stringintconv.Analyzer.Name: {Analyzer: stringintconv.Analyzer, Enabled: true}, +- structtag.Analyzer.Name: {Analyzer: structtag.Analyzer, Enabled: true}, +- tests.Analyzer.Name: {Analyzer: tests.Analyzer, Enabled: true}, +- unmarshal.Analyzer.Name: {Analyzer: unmarshal.Analyzer, Enabled: true}, +- unreachable.Analyzer.Name: {Analyzer: unreachable.Analyzer, Enabled: true}, +- unsafeptr.Analyzer.Name: {Analyzer: unsafeptr.Analyzer, Enabled: true}, +- unusedresult.Analyzer.Name: {Analyzer: unusedresult.Analyzer, Enabled: true}, - -- return strings.TrimRight(filepath.FromSlash(filter), "/"), nil --} +- // Non-vet analyzers: +- // - some (nilness, unusedwrite) use go/ssa; +- // - some (unusedwrite) report bad code but not always a bug, +- // so are not suitable for vet. +- atomicalign.Analyzer.Name: {Analyzer: atomicalign.Analyzer, Enabled: true}, +- deepequalerrors.Analyzer.Name: {Analyzer: deepequalerrors.Analyzer, Enabled: true}, +- fieldalignment.Analyzer.Name: {Analyzer: fieldalignment.Analyzer, Enabled: false}, +- nilness.Analyzer.Name: {Analyzer: nilness.Analyzer, Enabled: true}, +- shadow.Analyzer.Name: {Analyzer: shadow.Analyzer, Enabled: false}, +- sortslice.Analyzer.Name: {Analyzer: sortslice.Analyzer, Enabled: true}, +- testinggoroutine.Analyzer.Name: {Analyzer: testinggoroutine.Analyzer, Enabled: true}, +- unusedparams.Analyzer.Name: {Analyzer: unusedparams.Analyzer, Enabled: true}, +- unusedwrite.Analyzer.Name: {Analyzer: unusedwrite.Analyzer, Enabled: true}, +- useany.Analyzer.Name: {Analyzer: useany.Analyzer, Enabled: false}, +- infertypeargs.Analyzer.Name: { +- Analyzer: infertypeargs.Analyzer, +- Enabled: true, +- Severity: protocol.SeverityHint, +- }, +- timeformat.Analyzer.Name: {Analyzer: timeformat.Analyzer, Enabled: true}, +- embeddirective.Analyzer.Name: {Analyzer: embeddirective.Analyzer, Enabled: true}, - --func (o *Options) set(name string, value interface{}, seen map[string]struct{}) OptionResult { -- // Flatten the name in case we get options with a hierarchy. -- split := strings.Split(name, ".") -- name = split[len(split)-1] +- // gofmt -s suite: +- simplifycompositelit.Analyzer.Name: { +- Analyzer: simplifycompositelit.Analyzer, +- Enabled: true, +- ActionKinds: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix}, +- }, +- simplifyrange.Analyzer.Name: { +- Analyzer: simplifyrange.Analyzer, +- Enabled: true, +- ActionKinds: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix}, +- }, +- simplifyslice.Analyzer.Name: { +- Analyzer: simplifyslice.Analyzer, +- Enabled: true, +- ActionKinds: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix}, +- }, +- stdversion.Analyzer.Name: { +- Analyzer: stdversion.Analyzer, +- Enabled: true, +- }, - -- result := OptionResult{Name: name, Value: value} -- if _, ok := seen[name]; ok { -- result.parseErrorf("duplicate configuration for %s", name) +- // Type error analyzers. +- // These analyzers enrich go/types errors with suggested fixes. +- fillreturns.Analyzer.Name: {Analyzer: fillreturns.Analyzer, Enabled: true}, +- nonewvars.Analyzer.Name: {Analyzer: nonewvars.Analyzer, Enabled: true}, +- noresultvalues.Analyzer.Name: {Analyzer: noresultvalues.Analyzer, Enabled: true}, +- stubmethods.Analyzer.Name: {Analyzer: stubmethods.Analyzer, Enabled: true}, +- undeclaredname.Analyzer.Name: {Analyzer: undeclaredname.Analyzer, Enabled: true}, +- // TODO(rfindley): why isn't the 'unusedvariable' analyzer enabled, if it +- // is only enhancing type errors with suggested fixes? +- // +- // In particular, enabling this analyzer could cause unused variables to be +- // greyed out, (due to the 'deletions only' fix). That seems like a nice UI +- // feature. +- unusedvariable.Analyzer.Name: {Analyzer: unusedvariable.Analyzer, Enabled: false}, - } -- seen[name] = struct{}{} -- -- switch name { -- case "env": -- menv, ok := value.(map[string]interface{}) -- if !ok { -- result.parseErrorf("invalid type %T, expect map", value) -- break -- } -- if o.Env == nil { -- o.Env = make(map[string]string) -- } -- for k, v := range menv { -- o.Env[k] = fmt.Sprint(v) -- } +-} - -- case "buildFlags": -- // TODO(rfindley): use asStringSlice. -- iflags, ok := value.([]interface{}) -- if !ok { -- result.parseErrorf("invalid type %T, expect list", value) -- break -- } -- flags := make([]string, 0, len(iflags)) -- for _, flag := range iflags { -- flags = append(flags, fmt.Sprintf("%s", flag)) -- } -- o.BuildFlags = flags +-func urlRegexp() *regexp.Regexp { +- // Ensure links are matched as full words, not anywhere. +- re := regexp.MustCompile(`\b(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?\b`) +- re.Longest() +- return re +-} +diff -urN a/gopls/internal/settings/settings_test.go b/gopls/internal/settings/settings_test.go +--- a/gopls/internal/settings/settings_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/settings/settings_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,206 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- case "directoryFilters": -- // TODO(rfindley): use asStringSlice. -- ifilters, ok := value.([]interface{}) -- if !ok { -- result.parseErrorf("invalid type %T, expect list", value) -- break -- } -- var filters []string -- for _, ifilter := range ifilters { -- filter, err := validateDirectoryFilter(fmt.Sprintf("%v", ifilter)) -- if err != nil { -- result.parseErrorf("%v", err) -- return result -- } -- filters = append(filters, strings.TrimRight(filepath.FromSlash(filter), "/")) -- } -- o.DirectoryFilters = filters +-package settings - -- case "memoryMode": -- if s, ok := result.asOneOf( -- string(ModeNormal), -- string(ModeDegradeClosed), -- ); ok { -- o.MemoryMode = MemoryMode(s) -- } -- case "completionDocumentation": -- result.setBool(&o.CompletionDocumentation) -- case "usePlaceholders": -- result.setBool(&o.UsePlaceholders) -- case "deepCompletion": -- result.setBool(&o.DeepCompletion) -- case "completeUnimported": -- result.setBool(&o.CompleteUnimported) -- case "completionBudget": -- result.setDuration(&o.CompletionBudget) -- case "matcher": -- if s, ok := result.asOneOf( -- string(Fuzzy), -- string(CaseSensitive), -- string(CaseInsensitive), -- ); ok { -- o.Matcher = Matcher(s) -- } +-import ( +- "testing" +- "time" +-) - -- case "symbolMatcher": -- if s, ok := result.asOneOf( -- string(SymbolFuzzy), -- string(SymbolFastFuzzy), -- string(SymbolCaseInsensitive), -- string(SymbolCaseSensitive), -- ); ok { -- o.SymbolMatcher = SymbolMatcher(s) -- } +-func TestSetOption(t *testing.T) { +- tests := []struct { +- name string +- value interface{} +- wantError bool +- check func(Options) bool +- }{ +- { +- name: "symbolStyle", +- value: "Dynamic", +- check: func(o Options) bool { return o.SymbolStyle == DynamicSymbols }, +- }, +- { +- name: "symbolStyle", +- value: "", +- wantError: true, +- check: func(o Options) bool { return o.SymbolStyle == "" }, +- }, +- { +- name: "symbolStyle", +- value: false, +- wantError: true, +- check: func(o Options) bool { return o.SymbolStyle == "" }, +- }, +- { +- name: "symbolMatcher", +- value: "caseInsensitive", +- check: func(o Options) bool { return o.SymbolMatcher == SymbolCaseInsensitive }, +- }, +- { +- name: "completionBudget", +- value: "2s", +- check: func(o Options) bool { return o.CompletionBudget == 2*time.Second }, +- }, +- { +- name: "staticcheck", +- value: true, +- check: func(o Options) bool { return o.Staticcheck == true }, +- wantError: true, // o.StaticcheckSupported is unset +- }, +- { +- name: "codelenses", +- value: map[string]interface{}{"generate": true}, +- check: func(o Options) bool { return o.Codelenses["generate"] }, +- }, +- { +- name: "allExperiments", +- value: true, +- check: func(o Options) bool { +- return true // just confirm that we handle this setting +- }, +- }, +- { +- name: "hoverKind", +- value: "FullDocumentation", +- check: func(o Options) bool { +- return o.HoverKind == FullDocumentation +- }, +- }, +- { +- name: "hoverKind", +- value: "NoDocumentation", +- check: func(o Options) bool { +- return o.HoverKind == NoDocumentation +- }, +- }, +- { +- name: "hoverKind", +- value: "SingleLine", +- check: func(o Options) bool { +- return o.HoverKind == SingleLine +- }, +- }, +- { +- name: "hoverKind", +- value: "Structured", +- check: func(o Options) bool { +- return o.HoverKind == Structured +- }, +- }, +- { +- name: "ui.documentation.hoverKind", +- value: "Structured", +- check: func(o Options) bool { +- return o.HoverKind == Structured +- }, +- }, +- { +- name: "matcher", +- value: "Fuzzy", +- check: func(o Options) bool { +- return o.Matcher == Fuzzy +- }, +- }, +- { +- name: "matcher", +- value: "CaseSensitive", +- check: func(o Options) bool { +- return o.Matcher == CaseSensitive +- }, +- }, +- { +- name: "matcher", +- value: "CaseInsensitive", +- check: func(o Options) bool { +- return o.Matcher == CaseInsensitive +- }, +- }, +- { +- name: "env", +- value: map[string]interface{}{"testing": "true"}, +- check: func(o Options) bool { +- v, found := o.Env["testing"] +- return found && v == "true" +- }, +- }, +- { +- name: "env", +- value: []string{"invalid", "input"}, +- wantError: true, +- check: func(o Options) bool { +- return o.Env == nil +- }, +- }, +- { +- name: "directoryFilters", +- value: []interface{}{"-node_modules", "+project_a"}, +- check: func(o Options) bool { +- return len(o.DirectoryFilters) == 2 +- }, +- }, +- { +- name: "directoryFilters", +- value: []interface{}{"invalid"}, +- wantError: true, +- check: func(o Options) bool { +- return len(o.DirectoryFilters) == 0 +- }, +- }, +- { +- name: "directoryFilters", +- value: []string{"-invalid", "+type"}, +- wantError: true, +- check: func(o Options) bool { +- return len(o.DirectoryFilters) == 0 +- }, +- }, +- { +- name: "annotations", +- value: map[string]interface{}{ +- "Nil": false, +- "noBounds": true, +- }, +- wantError: true, +- check: func(o Options) bool { +- return !o.Annotations[Nil] && !o.Annotations[Bounds] +- }, +- }, +- { +- name: "vulncheck", +- value: []interface{}{"invalid"}, +- wantError: true, +- check: func(o Options) bool { +- return o.Vulncheck == "" // For invalid value, default to 'off'. +- }, +- }, +- { +- name: "vulncheck", +- value: "Imports", +- check: func(o Options) bool { +- return o.Vulncheck == ModeVulncheckImports // For invalid value, default to 'off'. +- }, +- }, +- { +- name: "vulncheck", +- value: "imports", +- check: func(o Options) bool { +- return o.Vulncheck == ModeVulncheckImports +- }, +- }, +- } - -- case "symbolStyle": -- if s, ok := result.asOneOf( -- string(FullyQualifiedSymbols), -- string(PackageQualifiedSymbols), -- string(DynamicSymbols), -- ); ok { -- o.SymbolStyle = SymbolStyle(s) +- for _, test := range tests { +- var opts Options +- result := opts.set(test.name, test.value, map[string]struct{}{}) +- if (result.Error != nil) != test.wantError { +- t.Fatalf("Options.set(%q, %v): result.Error = %v, want error: %t", test.name, test.value, result.Error, test.wantError) - } -- -- case "symbolScope": -- if s, ok := result.asOneOf( -- string(WorkspaceSymbolScope), -- string(AllSymbolScope), -- ); ok { -- o.SymbolScope = SymbolScope(s) +- // TODO: this could be made much better using cmp.Diff, if that becomes +- // available in this module. +- if !test.check(opts) { +- t.Errorf("Options.set(%q, %v): unexpected result %+v", test.name, test.value, opts) - } +- } +-} +diff -urN a/gopls/internal/telemetry/cmd/stacks/stacks.go b/gopls/internal/telemetry/cmd/stacks/stacks.go +--- a/gopls/internal/telemetry/cmd/stacks/stacks.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/telemetry/cmd/stacks/stacks.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,302 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- case "hoverKind": -- if s, ok := result.asOneOf( -- string(NoDocumentation), -- string(SingleLine), -- string(SynopsisDocumentation), -- string(FullDocumentation), -- string(Structured), -- ); ok { -- o.HoverKind = HoverKind(s) -- } +-// The stacks command finds all gopls stack traces reported by +-// telemetry in the past 7 days, and reports their associated GitHub +-// issue, creating new issues as needed. +-package main - -- case "linkTarget": -- result.setString(&o.LinkTarget) +-import ( +- "bytes" +- "encoding/base64" +- "encoding/json" +- "flag" +- "fmt" +- "hash/fnv" +- "log" +- "net/http" +- "net/url" +- "sort" +- "strings" +- "time" - -- case "linksInHover": -- result.setBool(&o.LinksInHover) +- "io" - -- case "importShortcut": -- if s, ok := result.asOneOf(string(BothShortcuts), string(LinkShortcut), string(DefinitionShortcut)); ok { -- o.ImportShortcut = ImportShortcut(s) -- } +- "golang.org/x/telemetry" +- "golang.org/x/tools/gopls/internal/util/browser" +-) - -- case "analyses": -- result.setBoolMap(&o.Analyses) +-// flags +-var ( +- daysFlag = flag.Int("days", 7, "number of previous days of telemetry data to read") +-) - -- case "hints": -- result.setBoolMap(&o.Hints) +-func main() { +- log.SetFlags(0) +- log.SetPrefix("stacks: ") +- flag.Parse() - -- case "annotations": -- result.setAnnotationMap(&o.Annotations) +- // Maps stack text to Version/GoVersion/GOOS/GOARCH string to counter. +- stacks := make(map[string]map[string]int64) +- var total int - -- case "vulncheck": -- if s, ok := result.asOneOf( -- string(ModeVulncheckOff), -- string(ModeVulncheckImports), -- ); ok { -- o.Vulncheck = VulncheckMode(s) -- } +- // Maps stack to a telemetry URL. +- stackToURL := make(map[string]string) - -- case "codelenses", "codelens": -- var lensOverrides map[string]bool -- result.setBoolMap(&lensOverrides) -- if result.Error == nil { -- if o.Codelenses == nil { -- o.Codelenses = make(map[string]bool) -- } -- for lens, enabled := range lensOverrides { -- o.Codelenses[lens] = enabled -- } -- } +- // Read all recent telemetry reports. +- t := time.Now() +- for i := 0; i < *daysFlag; i++ { +- const DateOnly = "2006-01-02" // TODO(adonovan): use time.DateOnly in go1.20. +- date := t.Add(-time.Duration(i+1) * 24 * time.Hour).Format(DateOnly) - -- // codelens is deprecated, but still works for now. -- // TODO(rstambler): Remove this for the gopls/v0.7.0 release. -- if name == "codelens" { -- result.deprecated("codelenses") +- url := fmt.Sprintf("https://storage.googleapis.com/prod-telemetry-merged/%s.json", date) +- resp, err := http.Get(url) +- if err != nil { +- log.Fatalf("can't GET %s: %v", url, err) - } -- -- case "staticcheck": -- if v, ok := result.asBool(); ok { -- o.Staticcheck = v -- if v && !o.StaticcheckSupported { -- result.Error = fmt.Errorf("applying setting %q: staticcheck is not supported at %s;"+ -- " rebuild gopls with a more recent version of Go", result.Name, runtime.Version()) -- } +- defer resp.Body.Close() +- if resp.StatusCode != 200 { +- log.Fatalf("GET %s returned %d %s", url, resp.StatusCode, resp.Status) - } - -- case "local": -- result.setString(&o.Local) -- -- case "verboseOutput": -- result.setBool(&o.VerboseOutput) -- -- case "verboseWorkDoneProgress": -- result.setBool(&o.VerboseWorkDoneProgress) -- -- case "tempModfile": -- result.softErrorf("gopls setting \"tempModfile\" is deprecated.\nPlease comment on https://go.dev/issue/63537 if this impacts your workflow.") -- result.setBool(&o.TempModfile) -- -- case "showBugReports": -- result.setBool(&o.ShowBugReports) +- dec := json.NewDecoder(resp.Body) +- for { +- var report telemetry.Report +- if err := dec.Decode(&report); err != nil { +- if err == io.EOF { +- break +- } +- log.Fatal(err) +- } +- for _, prog := range report.Programs { +- if prog.Program == "golang.org/x/tools/gopls" && len(prog.Stacks) > 0 { +- total++ +- +- // Include applicable client names (e.g. vscode, eglot). +- var clients []string +- var clientSuffix string +- for key := range prog.Counters { +- client := strings.TrimPrefix(key, "gopls/client:") +- if client != key { +- clients = append(clients, client) +- } +- } +- sort.Strings(clients) +- if len(clients) > 0 { +- clientSuffix = " " + strings.Join(clients, ",") +- } - -- case "gofumpt": -- if v, ok := result.asBool(); ok { -- o.Gofumpt = v -- if v && o.GofumptFormat == nil { -- result.Error = fmt.Errorf("applying setting %q: gofumpt is not supported at %s;"+ -- " rebuild gopls with a more recent version of Go", result.Name, runtime.Version()) +- // Ignore @devel versions as they correspond to +- // ephemeral (and often numerous) variations of +- // the program as we work on a fix to a bug. +- if prog.Version == "devel" { +- continue +- } +- info := fmt.Sprintf("%s@%s %s %s/%s%s", +- prog.Program, prog.Version, +- prog.GoVersion, prog.GOOS, prog.GOARCH, +- clientSuffix) +- for stack, count := range prog.Stacks { +- counts := stacks[stack] +- if counts == nil { +- counts = make(map[string]int64) +- stacks[stack] = counts +- } +- counts[info] += count +- stackToURL[stack] = url +- } +- } - } - } -- case "completeFunctionCalls": -- result.setBool(&o.CompleteFunctionCalls) -- -- case "semanticTokens": -- result.setBool(&o.SemanticTokens) -- -- case "noSemanticString": -- result.setBool(&o.NoSemanticString) -- -- case "noSemanticNumber": -- result.setBool(&o.NoSemanticNumber) -- -- case "expandWorkspaceToModule": -- result.softErrorf("gopls setting \"expandWorkspaceToModule\" is deprecated.\nPlease comment on https://go.dev/issue/63536 if this impacts your workflow.") -- result.setBool(&o.ExpandWorkspaceToModule) -- -- case "experimentalPostfixCompletions": -- result.setBool(&o.ExperimentalPostfixCompletions) +- } - -- case "experimentalWorkspaceModule": -- result.deprecated("") +- // Compute IDs of all stacks. +- var stackIDs []string +- for stack := range stacks { +- stackIDs = append(stackIDs, stackID(stack)) +- } - -- case "experimentalTemplateSupport": // TODO(pjw): remove after June 2022 -- result.deprecated("") +- // Query GitHub for existing GitHub issues. +- issuesByStackID := make(map[string]*Issue) +- for len(stackIDs) > 0 { +- // For some reason GitHub returns 422 UnprocessableEntity +- // if we attempt to read more than 6 at once. +- batch := stackIDs[:min(6, len(stackIDs))] +- stackIDs = stackIDs[len(batch):] - -- case "templateExtensions": -- if iexts, ok := value.([]interface{}); ok { -- ans := []string{} -- for _, x := range iexts { -- ans = append(ans, fmt.Sprint(x)) +- query := "label:gopls/telemetry-wins in:body " + strings.Join(batch, " OR ") +- res, err := searchIssues(query) +- if err != nil { +- log.Fatalf("GitHub issues query failed: %v", err) +- } +- for _, issue := range res.Items { +- for _, id := range batch { +- // Matching is a little fuzzy here +- // but base64 will rarely produce +- // words that appear in the body +- // by chance. +- if strings.Contains(issue.Body, id) { +- issuesByStackID[id] = issue +- } - } -- o.TemplateExtensions = ans -- break -- } -- if value == nil { -- o.TemplateExtensions = nil -- break - } -- result.parseErrorf("unexpected type %T not []string", value) +- } - -- case "experimentalDiagnosticsDelay": -- result.deprecated("diagnosticsDelay") +- fmt.Printf("Found %d stacks in last %v days:\n", total, *daysFlag) - -- case "diagnosticsDelay": -- result.setDuration(&o.DiagnosticsDelay) +- // For each stack, show existing issue or create a new one. +- for stack, counts := range stacks { +- id := stackID(stack) - -- case "diagnosticsTrigger": -- if s, ok := result.asOneOf( -- string(DiagnosticsOnEdit), -- string(DiagnosticsOnSave), -- ); ok { -- o.DiagnosticsTrigger = DiagnosticsTrigger(s) +- // Existing issue? +- issue, ok := issuesByStackID[id] +- if ok { +- if issue != nil { +- fmt.Printf("#%d: %s [%s]\n", +- issue.Number, issue.Title, issue.State) +- } else { +- // We just created a "New issue" browser tab +- // for this stackID. +- issuesByStackID[id] = nil // suppress dups +- } +- continue - } - -- case "analysisProgressReporting": -- result.setBool(&o.AnalysisProgressReporting) -- -- case "experimentalWatchedFileDelay": -- result.deprecated("") +- // Create new issue. +- issuesByStackID[id] = nil // suppress dups +- +- // Use a heuristic to find a suitable symbol to blame +- // in the title: the first public function or method +- // of a public type, in gopls, to appear in the stack +- // trace. We can always refine it later. +- var symbol string +- for _, line := range strings.Split(stack, "\n") { +- // Look for: +- // gopls/.../pkg.Func +- // gopls/.../pkg.Type.method +- // gopls/.../pkg.(*Type).method +- if strings.Contains(line, "internal/util/bug.") { +- continue // not interesting +- } +- if _, rest, ok := strings.Cut(line, "golang.org/x/tools/gopls/"); ok { +- if i := strings.IndexByte(rest, '.'); i >= 0 { +- rest = rest[i+1:] +- rest = strings.TrimPrefix(rest, "(*") +- if rest != "" && 'A' <= rest[0] && rest[0] <= 'Z' { +- rest, _, _ = strings.Cut(rest, ":") +- symbol = " " + rest +- break +- } +- } +- } +- } - -- case "experimentalPackageCacheKey": -- result.deprecated("") +- // Populate the form (title, body, label) +- title := fmt.Sprintf("x/tools/gopls:%s bug reported by telemetry", symbol) +- body := new(bytes.Buffer) +- fmt.Fprintf(body, "This stack `%s` was [reported by telemetry](%s):\n\n", +- id, stackToURL[stack]) +- fmt.Fprintf(body, "```\n%s\n```\n", stack) - -- case "allowModfileModifications": -- result.setBool(&o.AllowModfileModifications) +- // Add counts, gopls version, and platform info. +- // This isn't very precise but should provide clues. +- // +- // TODO(adonovan): link each stack (ideally each frame) to source: +- // https://cs.opensource.google/go/x/tools/+/gopls/VERSION:gopls/FILE;l=LINE +- // (Requires parsing stack, shallow-cloning gopls module at that tag, and +- // computing correct line offsets. Would be labor-saving though.) +- fmt.Fprintf(body, "```\n") +- for info, count := range counts { +- fmt.Fprintf(body, "%s (%d)\n", info, count) +- } +- fmt.Fprintf(body, "```\n\n") - -- case "allowImplicitNetworkAccess": -- result.setBool(&o.AllowImplicitNetworkAccess) +- fmt.Fprintf(body, "Issue created by golang.org/x/tools/gopls/internal/telemetry/cmd/stacks.\n") - -- case "experimentalUseInvalidMetadata": -- result.deprecated("") +- const labels = "gopls,Tools,gopls/telemetry-wins,NeedsInvestigation" - -- case "standaloneTags": -- result.setStringSlice(&o.StandaloneTags) +- // Report it. +- if !browser.Open("https://github.com/golang/go/issues/new?labels=" + labels + "&title=" + url.QueryEscape(title) + "&body=" + url.QueryEscape(body.String())) { +- log.Print("Please file a new issue at golang.org/issue/new using this template:\n\n") +- log.Printf("Title: %s\n", title) +- log.Printf("Labels: %s\n", labels) +- log.Printf("Body: %s\n", body) +- } +- } +-} - -- case "allExperiments": -- // This setting should be handled before all of the other options are -- // processed, so do nothing here. +-// stackID returns a 32-bit identifier for a stack +-// suitable for use in GitHub issue titles. +-func stackID(stack string) string { +- // Encode it using base64 (6 bytes) for brevity, +- // as a single issue's body might contain multiple IDs +- // if separate issues with same cause wre manually de-duped, +- // e.g. "AAAAAA, BBBBBB" +- // +- // https://hbfs.wordpress.com/2012/03/30/finding-collisions: +- // the chance of a collision is 1 - exp(-n(n-1)/2d) where n +- // is the number of items and d is the number of distinct values. +- // So, even with n=10^4 telemetry-reported stacks each identified +- // by a uint32 (d=2^32), we have a 1% chance of a collision, +- // which is plenty good enough. +- h := fnv.New32() +- io.WriteString(h, stack) +- return base64.URLEncoding.EncodeToString(h.Sum(nil))[:6] +-} - -- case "newDiff": -- result.setString(&o.NewDiff) +-// -- GitHub search -- - -- case "chattyDiagnostics": -- result.setBool(&o.ChattyDiagnostics) +-// searchIssues queries the GitHub issue tracker. +-func searchIssues(query string) (*IssuesSearchResult, error) { +- q := url.QueryEscape(query) +- resp, err := http.Get(IssuesURL + "?q=" + q) +- if err != nil { +- return nil, err +- } +- if resp.StatusCode != http.StatusOK { +- resp.Body.Close() +- return nil, fmt.Errorf("search query failed: %s", resp.Status) +- } +- var result IssuesSearchResult +- if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { +- resp.Body.Close() +- return nil, err +- } +- resp.Body.Close() +- return &result, nil +-} - -- case "subdirWatchPatterns": -- if s, ok := result.asOneOf( -- string(SubdirWatchPatternsOn), -- string(SubdirWatchPatternsOff), -- string(SubdirWatchPatternsAuto), -- ); ok { -- o.SubdirWatchPatterns = SubdirWatchPatterns(s) -- } +-// See https://developer.github.com/v3/search/#search-issues. - -- case "reportAnalysisProgressAfter": -- result.setDuration(&o.ReportAnalysisProgressAfter) +-const IssuesURL = "https://api.github.com/search/issues" - -- case "telemetryPrompt": -- result.setBool(&o.TelemetryPrompt) -- case "linkifyShowMessage": -- result.setBool(&o.LinkifyShowMessage) +-type IssuesSearchResult struct { +- TotalCount int `json:"total_count"` +- Items []*Issue +-} - -- // Replaced settings. -- case "experimentalDisabledAnalyses": -- result.deprecated("analyses") +-type Issue struct { +- Number int +- HTMLURL string `json:"html_url"` +- Title string +- State string +- User *User +- CreatedAt time.Time `json:"created_at"` +- Body string // in Markdown format +-} - -- case "disableDeepCompletion": -- result.deprecated("deepCompletion") +-type User struct { +- Login string +- HTMLURL string `json:"html_url"` +-} - -- case "disableFuzzyMatching": -- result.deprecated("fuzzyMatching") +-// -- helpers -- - -- case "wantCompletionDocumentation": -- result.deprecated("completionDocumentation") +-func min(x, y int) int { +- if x < y { +- return x +- } else { +- return y +- } +-} +diff -urN a/gopls/internal/telemetry/latency.go b/gopls/internal/telemetry/latency.go +--- a/gopls/internal/telemetry/latency.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/telemetry/latency.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,102 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- case "wantUnimportedCompletions": -- result.deprecated("completeUnimported") +-package telemetry - -- case "fuzzyMatching": -- result.deprecated("matcher") +-import ( +- "context" +- "errors" +- "fmt" +- "sort" +- "sync" +- "time" - -- case "caseSensitiveCompletion": -- result.deprecated("matcher") +- "golang.org/x/telemetry/counter" +-) - -- // Deprecated settings. -- case "wantSuggestedFixes": -- result.deprecated("") +-// latencyKey is used for looking up latency counters. +-type latencyKey struct { +- operation, bucket string +- isError bool +-} - -- case "noIncrementalSync": -- result.deprecated("") +-var ( +- latencyBuckets = []struct { +- end time.Duration +- name string +- }{ +- {10 * time.Millisecond, "<10ms"}, +- {50 * time.Millisecond, "<50ms"}, +- {100 * time.Millisecond, "<100ms"}, +- {200 * time.Millisecond, "<200ms"}, +- {500 * time.Millisecond, "<500ms"}, +- {1 * time.Second, "<1s"}, +- {5 * time.Second, "<5s"}, +- {24 * time.Hour, "<24h"}, +- } - -- case "watchFileChanges": -- result.deprecated("") +- latencyCounterMu sync.Mutex +- latencyCounters = make(map[latencyKey]*counter.Counter) // lazily populated +-) - -- case "go-diff": -- result.deprecated("") +-// ForEachLatencyCounter runs the provided function for each current latency +-// counter measuring the given operation. +-// +-// Exported for testing. +-func ForEachLatencyCounter(operation string, isError bool, f func(*counter.Counter)) { +- latencyCounterMu.Lock() +- defer latencyCounterMu.Unlock() - -- default: -- result.unexpected() +- for k, v := range latencyCounters { +- if k.operation == operation && k.isError == isError { +- f(v) +- } - } -- return result -} - --// parseErrorf reports an error parsing the current configuration value. --func (r *OptionResult) parseErrorf(msg string, values ...interface{}) { -- if false { -- _ = fmt.Sprintf(msg, values...) // this causes vet to check this like printf +-// getLatencyCounter returns the counter used to record latency of the given +-// operation in the given bucket. +-func getLatencyCounter(operation, bucket string, isError bool) *counter.Counter { +- latencyCounterMu.Lock() +- defer latencyCounterMu.Unlock() +- +- key := latencyKey{operation, bucket, isError} +- c, ok := latencyCounters[key] +- if !ok { +- var name string +- if isError { +- name = fmt.Sprintf("gopls/%s/error-latency:%s", operation, bucket) +- } else { +- name = fmt.Sprintf("gopls/%s/latency:%s", operation, bucket) +- } +- c = counter.New(name) +- latencyCounters[key] = c - } -- prefix := fmt.Sprintf("parsing setting %q: ", r.Name) -- r.Error = fmt.Errorf(prefix+msg, values...) +- return c -} - --// A SoftError is an error that does not affect the functionality of gopls. --type SoftError struct { -- msg string +-// StartLatencyTimer starts a timer for the gopls operation with the given +-// name, and returns a func to stop the timer and record the latency sample. +-// +-// If the context provided to the resulting func is done, no observation is +-// recorded. +-func StartLatencyTimer(operation string) func(context.Context, error) { +- start := time.Now() +- return func(ctx context.Context, err error) { +- if errors.Is(ctx.Err(), context.Canceled) { +- // Ignore timing where the operation is cancelled, it may be influenced +- // by client behavior. +- return +- } +- latency := time.Since(start) +- bucketIdx := sort.Search(len(latencyBuckets), func(i int) bool { +- bucket := latencyBuckets[i] +- return latency < bucket.end +- }) +- if bucketIdx < len(latencyBuckets) { // ignore latency longer than a day :) +- bucketName := latencyBuckets[bucketIdx].name +- getLatencyCounter(operation, bucketName, err != nil).Inc() +- } +- } -} +diff -urN a/gopls/internal/telemetry/telemetry_test.go b/gopls/internal/telemetry/telemetry_test.go +--- a/gopls/internal/telemetry/telemetry_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/telemetry/telemetry_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,227 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func (e *SoftError) Error() string { -- return e.msg --} +-//go:build go1.21 && !openbsd && !js && !wasip1 && !solaris && !android && !386 +-// +build go1.21,!openbsd,!js,!wasip1,!solaris,!android,!386 - --// deprecated reports the current setting as deprecated. If 'replacement' is --// non-nil, it is suggested to the user. --func (r *OptionResult) deprecated(replacement string) { -- msg := fmt.Sprintf("gopls setting %q is deprecated", r.Name) -- if replacement != "" { -- msg = fmt.Sprintf("%s, use %q instead", msg, replacement) -- } -- r.Error = &SoftError{msg} --} +-package telemetry_test - --// softErrorf reports a soft error related to the current option. --func (r *OptionResult) softErrorf(format string, args ...any) { -- r.Error = &SoftError{fmt.Sprintf(format, args...)} --} +-import ( +- "context" +- "errors" +- "os" +- "strconv" +- "strings" +- "testing" +- "time" - --// unexpected reports that the current setting is not known to gopls. --func (r *OptionResult) unexpected() { -- r.Error = fmt.Errorf("unexpected gopls setting %q", r.Name) --} +- "golang.org/x/telemetry/counter" +- "golang.org/x/telemetry/counter/countertest" // requires go1.21+ +- "golang.org/x/tools/gopls/internal/hooks" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/command" +- "golang.org/x/tools/gopls/internal/telemetry" +- . "golang.org/x/tools/gopls/internal/test/integration" +- "golang.org/x/tools/gopls/internal/util/bug" +-) - --func (r *OptionResult) asBool() (bool, bool) { -- b, ok := r.Value.(bool) -- if !ok { -- r.parseErrorf("invalid type %T, expect bool", r.Value) -- return false, false +-func TestMain(m *testing.M) { +- tmp, err := os.MkdirTemp("", "gopls-telemetry-test") +- if err != nil { +- panic(err) - } -- return b, true +- countertest.Open(tmp) +- defer os.RemoveAll(tmp) +- Main(m, hooks.Options) -} - --func (r *OptionResult) setBool(b *bool) { -- if v, ok := r.asBool(); ok { -- *b = v -- } --} +-func TestTelemetry(t *testing.T) { +- var ( +- goversion = "" +- editor = "vscode" // We set ClientName("Visual Studio Code") below. +- ) - --func (r *OptionResult) setDuration(d *time.Duration) { -- if v, ok := r.asString(); ok { -- parsed, err := time.ParseDuration(v) +- // Run gopls once to determine the Go version. +- WithOptions( +- Modes(Default), +- ).Run(t, "", func(_ *testing.T, env *Env) { +- goversion = strconv.Itoa(env.GoVersion()) +- }) +- +- // counters that should be incremented once per session +- sessionCounters := []*counter.Counter{ +- counter.New("gopls/client:" + editor), +- counter.New("gopls/goversion:1." + goversion), +- counter.New("fwd/vscode/linter:a"), +- } +- initialCounts := make([]uint64, len(sessionCounters)) +- for i, c := range sessionCounters { +- count, err := countertest.ReadCounter(c) - if err != nil { -- r.parseErrorf("failed to parse duration %q: %v", v, err) -- return +- continue // counter db not open, or counter not found - } -- *d = parsed +- initialCounts[i] = count - } --} - --func (r *OptionResult) setBoolMap(bm *map[string]bool) { -- m := r.asBoolMap() -- *bm = m --} +- // Verify that a properly configured session gets notified of a bug on the +- // server. +- WithOptions( +- Modes(Default), // must be in-process to receive the bug report below +- Settings{"showBugReports": true}, +- ClientName("Visual Studio Code"), +- ).Run(t, "", func(_ *testing.T, env *Env) { +- goversion = strconv.Itoa(env.GoVersion()) +- addForwardedCounters(env, []string{"vscode/linter:a"}, []int64{1}) +- const desc = "got a bug" - --func (r *OptionResult) setAnnotationMap(bm *map[Annotation]bool) { -- all := r.asBoolMap() -- if all == nil { -- return -- } -- // Default to everything enabled by default. -- m := make(map[Annotation]bool) -- for k, enabled := range all { -- a, err := asOneOf( -- k, -- string(Nil), -- string(Escape), -- string(Inline), -- string(Bounds), -- ) -- if err != nil { -- // In case of an error, process any legacy values. -- switch k { -- case "noEscape": -- m[Escape] = false -- r.parseErrorf(`"noEscape" is deprecated, set "Escape: false" instead`) -- case "noNilcheck": -- m[Nil] = false -- r.parseErrorf(`"noNilcheck" is deprecated, set "Nil: false" instead`) -- case "noInline": -- m[Inline] = false -- r.parseErrorf(`"noInline" is deprecated, set "Inline: false" instead`) -- case "noBounds": -- m[Bounds] = false -- r.parseErrorf(`"noBounds" is deprecated, set "Bounds: false" instead`) -- default: -- r.parseErrorf("%v", err) -- } -- continue +- // This will increment a counter named something like: +- // +- // `gopls/bug +- // golang.org/x/tools/gopls/internal/util/bug.report:+35 +- // golang.org/x/tools/gopls/internal/util/bug.Report:=68 +- // golang.org/x/tools/gopls/internal/telemetry_test.TestTelemetry.func2:+4 +- // golang.org/x/tools/gopls/internal/test/integration.(*Runner).Run.func1:+87 +- // testing.tRunner:+150 +- // runtime.goexit:+0` +- // +- bug.Report(desc) // want a stack counter with the trace starting from here. +- +- env.Await(ShownMessage(desc)) +- }) +- +- // gopls/editor:client +- // gopls/goversion:1.x +- // fwd/vscode/linter:a +- for i, c := range sessionCounters { +- want := initialCounts[i] + 1 +- got, err := countertest.ReadCounter(c) +- if err != nil || got != want { +- t.Errorf("ReadCounter(%q) = (%v, %v), want (%v, nil)", c.Name(), got, err, want) +- t.Logf("Current timestamp = %v", time.Now().UTC()) - } -- m[Annotation(a)] = enabled - } -- *bm = m --} - --func (r *OptionResult) asBoolMap() map[string]bool { -- all, ok := r.Value.(map[string]interface{}) -- if !ok { -- r.parseErrorf("invalid type %T for map[string]bool option", r.Value) -- return nil +- // gopls/bug +- bugcount := bug.BugReportCount +- counts, err := countertest.ReadStackCounter(bugcount) +- if err != nil { +- t.Fatalf("ReadStackCounter(bugreportcount) failed - %v", err) - } -- m := make(map[string]bool) -- for a, enabled := range all { -- if e, ok := enabled.(bool); ok { -- m[a] = e -- } else { -- r.parseErrorf("invalid type %T for map key %q", enabled, a) -- return m -- } +- if len(counts) != 1 || !hasEntry(counts, t.Name(), 1) { +- t.Errorf("read stackcounter(%q) = (%#v, %v), want one entry", "gopls/bug", counts, err) +- t.Logf("Current timestamp = %v", time.Now().UTC()) - } -- return m -} - --func (r *OptionResult) asString() (string, bool) { -- b, ok := r.Value.(string) -- if !ok { -- r.parseErrorf("invalid type %T, expect string", r.Value) -- return "", false +-func addForwardedCounters(env *Env, names []string, values []int64) { +- args, err := command.MarshalArgs(command.AddTelemetryCountersArgs{ +- Names: names, Values: values, +- }) +- if err != nil { +- env.T.Fatal(err) +- } +- var res error +- env.ExecuteCommand(&protocol.ExecuteCommandParams{ +- Command: command.AddTelemetryCounters.ID(), +- Arguments: args, +- }, &res) +- if res != nil { +- env.T.Errorf("%v failed - %v", command.AddTelemetryCounters.ID(), res) - } -- return b, true -} - --func (r *OptionResult) asStringSlice() ([]string, bool) { -- iList, ok := r.Value.([]interface{}) -- if !ok { -- r.parseErrorf("invalid type %T, expect list", r.Value) -- return nil, false -- } -- var list []string -- for _, elem := range iList { -- s, ok := elem.(string) -- if !ok { -- r.parseErrorf("invalid element type %T, expect string", elem) -- return nil, false +-func hasEntry(counts map[string]uint64, pattern string, want uint64) bool { +- for k, v := range counts { +- if strings.Contains(k, pattern) && v == want { +- return true - } -- list = append(list, s) - } -- return list, true +- return false -} - --func (r *OptionResult) asOneOf(options ...string) (string, bool) { -- s, ok := r.asString() -- if !ok { -- return "", false -- } -- s, err := asOneOf(s, options...) -- if err != nil { -- r.parseErrorf("%v", err) -- } -- return s, err == nil --} +-func TestLatencyCounter(t *testing.T) { +- const operation = "TestLatencyCounter" // a unique operation name - --func asOneOf(str string, options ...string) (string, error) { -- lower := strings.ToLower(str) -- for _, opt := range options { -- if strings.ToLower(opt) == lower { -- return opt, nil +- stop := telemetry.StartLatencyTimer(operation) +- stop(context.Background(), nil) +- +- for isError, want := range map[bool]uint64{false: 1, true: 0} { +- if got := totalLatencySamples(t, operation, isError); got != want { +- t.Errorf("totalLatencySamples(operation=%v, isError=%v) = %d, want %d", operation, isError, got, want) - } - } -- return "", fmt.Errorf("invalid option %q for enum", str) -} - --func (r *OptionResult) setString(s *string) { -- if v, ok := r.asString(); ok { -- *s = v -- } --} +-func TestLatencyCounter_Error(t *testing.T) { +- const operation = "TestLatencyCounter_Error" // a unique operation name - --func (r *OptionResult) setStringSlice(s *[]string) { -- if v, ok := r.asStringSlice(); ok { -- *s = v -- } --} +- stop := telemetry.StartLatencyTimer(operation) +- stop(context.Background(), errors.New("bad")) - --func typeErrorAnalyzers() map[string]*Analyzer { -- return map[string]*Analyzer{ -- fillreturns.Analyzer.Name: { -- Analyzer: fillreturns.Analyzer, -- // TODO(rfindley): is SourceFixAll even necessary here? Is that not implied? -- ActionKind: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix}, -- Enabled: true, -- }, -- nonewvars.Analyzer.Name: { -- Analyzer: nonewvars.Analyzer, -- Enabled: true, -- }, -- noresultvalues.Analyzer.Name: { -- Analyzer: noresultvalues.Analyzer, -- Enabled: true, -- }, -- undeclaredname.Analyzer.Name: { -- Analyzer: undeclaredname.Analyzer, -- Fix: UndeclaredName, -- Enabled: true, -- }, -- unusedvariable.Analyzer.Name: { -- Analyzer: unusedvariable.Analyzer, -- Enabled: false, -- }, +- for isError, want := range map[bool]uint64{false: 0, true: 1} { +- if got := totalLatencySamples(t, operation, isError); got != want { +- t.Errorf("totalLatencySamples(operation=%v, isError=%v) = %d, want %d", operation, isError, got, want) +- } - } -} - --// TODO(golang/go#61559): remove convenience analyzers now that they are not --// used from the analysis framework. --func convenienceAnalyzers() map[string]*Analyzer { -- return map[string]*Analyzer{ -- fillstruct.Analyzer.Name: { -- Analyzer: fillstruct.Analyzer, -- Fix: FillStruct, -- Enabled: true, -- ActionKind: []protocol.CodeActionKind{protocol.RefactorRewrite}, -- }, -- stubmethods.Analyzer.Name: { -- Analyzer: stubmethods.Analyzer, -- Fix: StubMethods, -- Enabled: true, -- }, -- infertypeargs.Analyzer.Name: { -- Analyzer: infertypeargs.Analyzer, -- Enabled: true, -- ActionKind: []protocol.CodeActionKind{protocol.RefactorRewrite}, -- }, +-func TestLatencyCounter_Cancellation(t *testing.T) { +- const operation = "TestLatencyCounter_Cancellation" +- +- stop := telemetry.StartLatencyTimer(operation) +- ctx, cancel := context.WithCancel(context.Background()) +- cancel() +- stop(ctx, nil) +- +- for isError, want := range map[bool]uint64{false: 0, true: 0} { +- if got := totalLatencySamples(t, operation, isError); got != want { +- t.Errorf("totalLatencySamples(operation=%v, isError=%v) = %d, want %d", operation, isError, got, want) +- } - } -} - --func defaultAnalyzers() map[string]*Analyzer { -- return map[string]*Analyzer{ -- // The traditional vet suite: -- appends.Analyzer.Name: {Analyzer: appends.Analyzer, Enabled: true}, -- asmdecl.Analyzer.Name: {Analyzer: asmdecl.Analyzer, Enabled: true}, -- assign.Analyzer.Name: {Analyzer: assign.Analyzer, Enabled: true}, -- atomic.Analyzer.Name: {Analyzer: atomic.Analyzer, Enabled: true}, -- bools.Analyzer.Name: {Analyzer: bools.Analyzer, Enabled: true}, -- buildtag.Analyzer.Name: {Analyzer: buildtag.Analyzer, Enabled: true}, -- cgocall.Analyzer.Name: {Analyzer: cgocall.Analyzer, Enabled: true}, -- composite.Analyzer.Name: {Analyzer: composite.Analyzer, Enabled: true}, -- copylock.Analyzer.Name: {Analyzer: copylock.Analyzer, Enabled: true}, -- defers.Analyzer.Name: {Analyzer: defers.Analyzer, Enabled: true}, -- deprecated.Analyzer.Name: {Analyzer: deprecated.Analyzer, Enabled: true, Severity: protocol.SeverityHint, Tag: []protocol.DiagnosticTag{protocol.Deprecated}}, -- directive.Analyzer.Name: {Analyzer: directive.Analyzer, Enabled: true}, -- errorsas.Analyzer.Name: {Analyzer: errorsas.Analyzer, Enabled: true}, -- httpresponse.Analyzer.Name: {Analyzer: httpresponse.Analyzer, Enabled: true}, -- ifaceassert.Analyzer.Name: {Analyzer: ifaceassert.Analyzer, Enabled: true}, -- loopclosure.Analyzer.Name: {Analyzer: loopclosure.Analyzer, Enabled: true}, -- lostcancel.Analyzer.Name: {Analyzer: lostcancel.Analyzer, Enabled: true}, -- nilfunc.Analyzer.Name: {Analyzer: nilfunc.Analyzer, Enabled: true}, -- printf.Analyzer.Name: {Analyzer: printf.Analyzer, Enabled: true}, -- shift.Analyzer.Name: {Analyzer: shift.Analyzer, Enabled: true}, -- slog.Analyzer.Name: {Analyzer: slog.Analyzer, Enabled: true}, -- stdmethods.Analyzer.Name: {Analyzer: stdmethods.Analyzer, Enabled: true}, -- stringintconv.Analyzer.Name: {Analyzer: stringintconv.Analyzer, Enabled: true}, -- structtag.Analyzer.Name: {Analyzer: structtag.Analyzer, Enabled: true}, -- tests.Analyzer.Name: {Analyzer: tests.Analyzer, Enabled: true}, -- unmarshal.Analyzer.Name: {Analyzer: unmarshal.Analyzer, Enabled: true}, -- unreachable.Analyzer.Name: {Analyzer: unreachable.Analyzer, Enabled: true}, -- unsafeptr.Analyzer.Name: {Analyzer: unsafeptr.Analyzer, Enabled: true}, -- unusedresult.Analyzer.Name: {Analyzer: unusedresult.Analyzer, Enabled: true}, +-func totalLatencySamples(t *testing.T, operation string, isError bool) uint64 { +- var total uint64 +- telemetry.ForEachLatencyCounter(operation, isError, func(c *counter.Counter) { +- count, err := countertest.ReadCounter(c) +- if err != nil { +- t.Errorf("ReadCounter(%s) failed: %v", c.Name(), err) +- } else { +- total += count +- } +- }) +- return total +-} - -- // Non-vet analyzers: -- atomicalign.Analyzer.Name: {Analyzer: atomicalign.Analyzer, Enabled: true}, -- deepequalerrors.Analyzer.Name: {Analyzer: deepequalerrors.Analyzer, Enabled: true}, -- fieldalignment.Analyzer.Name: {Analyzer: fieldalignment.Analyzer, Enabled: false}, -- nilness.Analyzer.Name: {Analyzer: nilness.Analyzer, Enabled: true}, -- shadow.Analyzer.Name: {Analyzer: shadow.Analyzer, Enabled: false}, -- sortslice.Analyzer.Name: {Analyzer: sortslice.Analyzer, Enabled: true}, -- testinggoroutine.Analyzer.Name: {Analyzer: testinggoroutine.Analyzer, Enabled: true}, -- unusedparams.Analyzer.Name: {Analyzer: unusedparams.Analyzer, Enabled: false}, -- unusedwrite.Analyzer.Name: {Analyzer: unusedwrite.Analyzer, Enabled: false}, -- useany.Analyzer.Name: {Analyzer: useany.Analyzer, Enabled: false}, -- timeformat.Analyzer.Name: {Analyzer: timeformat.Analyzer, Enabled: true}, -- embeddirective.Analyzer.Name: { -- Analyzer: embeddirective.Analyzer, -- Enabled: true, -- Fix: AddEmbedImport, -- fixesDiagnostic: fixedByImportingEmbed, -- }, +-func TestLatencyInstrumentation(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.test/a +-go 1.18 +--- a.go -- +-package a - -- // gofmt -s suite: -- simplifycompositelit.Analyzer.Name: { -- Analyzer: simplifycompositelit.Analyzer, -- Enabled: true, -- ActionKind: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix}, -- }, -- simplifyrange.Analyzer.Name: { -- Analyzer: simplifyrange.Analyzer, -- Enabled: true, -- ActionKind: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix}, -- }, -- simplifyslice.Analyzer.Name: { -- Analyzer: simplifyslice.Analyzer, -- Enabled: true, -- ActionKind: []protocol.CodeActionKind{protocol.SourceFixAll, protocol.QuickFix}, -- }, -- } +-func _() { +- x := 0 +- _ = x -} +-` - --func urlRegexp() *regexp.Regexp { -- // Ensure links are matched as full words, not anywhere. -- re := regexp.MustCompile(`\b(http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?\b`) -- re.Longest() -- return re +- // Verify that a properly configured session gets notified of a bug on the +- // server. +- WithOptions( +- Modes(Default), // must be in-process to receive the bug report below +- ).Run(t, files, func(_ *testing.T, env *Env) { +- env.OpenFile("a.go") +- before := totalLatencySamples(t, "completion", false) +- loc := env.RegexpSearch("a.go", "x") +- for i := 0; i < 10; i++ { +- env.Completion(loc) +- } +- after := totalLatencySamples(t, "completion", false) +- if after-before < 10 { +- t.Errorf("after 10 completions, completion counter went from %d to %d", before, after) +- } +- }) -} +diff -urN a/gopls/internal/template/completion.go b/gopls/internal/template/completion.go +--- a/gopls/internal/template/completion.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/template/completion.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,253 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --type APIJSON struct { -- Options map[string][]*OptionJSON -- Commands []*CommandJSON -- Lenses []*LensJSON -- Analyzers []*AnalyzerJSON -- Hints []*HintJSON --} +-package template - --type OptionJSON struct { -- Name string -- Type string -- Doc string -- EnumKeys EnumKeys -- EnumValues []EnumValue -- Default string -- Status string -- Hierarchy string +-import ( +- "bytes" +- "context" +- "fmt" +- "go/scanner" +- "go/token" +- "strings" +- +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +-) +- +-// information needed for completion +-type completer struct { +- p *Parsed +- pos protocol.Position +- offset int // offset of the start of the Token +- ctx protocol.CompletionContext +- syms map[string]symbol -} - --func (o *OptionJSON) String() string { -- return o.Name +-func Completion(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, pos protocol.Position, context protocol.CompletionContext) (*protocol.CompletionList, error) { +- all := New(snapshot.Templates()) +- var start int // the beginning of the Token (completed or not) +- syms := make(map[string]symbol) +- var p *Parsed +- for fn, fc := range all.files { +- // collect symbols from all template files +- filterSyms(syms, fc.symbols) +- if fn.Path() != fh.URI().Path() { +- continue +- } +- if start = inTemplate(fc, pos); start == -1 { +- return nil, nil +- } +- p = fc +- } +- if p == nil { +- // this cannot happen unless the search missed a template file +- return nil, fmt.Errorf("%s not found", fh.Identity().URI.Path()) +- } +- c := completer{ +- p: p, +- pos: pos, +- offset: start + len(Left), +- ctx: context, +- syms: syms, +- } +- return c.complete() -} - --func (o *OptionJSON) Write(w io.Writer) { -- fmt.Fprintf(w, "**%v** *%v*\n\n", o.Name, o.Type) -- writeStatus(w, o.Status) -- enumValues := collectEnums(o) -- fmt.Fprintf(w, "%v%v\nDefault: `%v`.\n\n", o.Doc, enumValues, o.Default) +-func filterSyms(syms map[string]symbol, ns []symbol) { +- for _, xsym := range ns { +- switch xsym.kind { +- case protocol.Method, protocol.Package, protocol.Boolean, protocol.Namespace, +- protocol.Function: +- syms[xsym.name] = xsym // we don't care which symbol we get +- case protocol.Variable: +- if xsym.name != "dot" { +- syms[xsym.name] = xsym +- } +- case protocol.Constant: +- if xsym.name == "nil" { +- syms[xsym.name] = xsym +- } +- } +- } -} - --func writeStatus(section io.Writer, status string) { -- switch status { -- case "": -- case "advanced": -- fmt.Fprint(section, "**This is an advanced setting and should not be configured by most `gopls` users.**\n\n") -- case "debug": -- fmt.Fprint(section, "**This setting is for debugging purposes only.**\n\n") -- case "experimental": -- fmt.Fprint(section, "**This setting is experimental and may be deleted.**\n\n") -- default: -- fmt.Fprintf(section, "**Status: %s.**\n\n", status) +-// return the starting position of the enclosing token, or -1 if none +-func inTemplate(fc *Parsed, pos protocol.Position) int { +- // pos is the pos-th character. if the cursor is at the beginning +- // of the file, pos is 0. That is, we've only seen characters before pos +- // 1. pos might be in a Token, return tk.Start +- // 2. pos might be after an elided but before a Token, return elided +- // 3. return -1 for false +- offset := fc.FromPosition(pos) +- // this could be a binary search, as the tokens are ordered +- for _, tk := range fc.tokens { +- if tk.Start < offset && offset <= tk.End { +- return tk.Start +- } +- } +- for _, x := range fc.elided { +- if x > offset { +- // fc.elided is sorted +- break +- } +- // If the interval [x,offset] does not contain Left or Right +- // then provide completions. (do we need the test for Right?) +- if !bytes.Contains(fc.buf[x:offset], Left) && !bytes.Contains(fc.buf[x:offset], Right) { +- return x +- } - } +- return -1 -} - --var parBreakRE = regexp.MustCompile("\n{2,}") +-var ( +- keywords = []string{"if", "with", "else", "block", "range", "template", "end}}", "end"} +- globals = []string{"and", "call", "html", "index", "slice", "js", "len", "not", "or", +- "urlquery", "printf", "println", "print", "eq", "ne", "le", "lt", "ge", "gt"} +-) - --func collectEnums(opt *OptionJSON) string { -- var b strings.Builder -- write := func(name, doc string) { -- if doc != "" { -- unbroken := parBreakRE.ReplaceAllString(doc, "\\\n") -- fmt.Fprintf(&b, "* %s\n", strings.TrimSpace(unbroken)) -- } else { -- fmt.Fprintf(&b, "* `%s`\n", name) +-// find the completions. start is the offset of either the Token enclosing pos, or where +-// the incomplete token starts. +-// The error return is always nil. +-func (c *completer) complete() (*protocol.CompletionList, error) { +- ans := &protocol.CompletionList{IsIncomplete: true, Items: []protocol.CompletionItem{}} +- start := c.p.FromPosition(c.pos) +- sofar := c.p.buf[c.offset:start] +- if len(sofar) == 0 || sofar[len(sofar)-1] == ' ' || sofar[len(sofar)-1] == '\t' { +- return ans, nil +- } +- // sofar could be parsed by either c.analyzer() or scan(). The latter is precise +- // and slower, but fast enough +- words := scan(sofar) +- // 1. if pattern starts $, show variables +- // 2. if pattern starts ., show methods (and . by itself?) +- // 3. if len(words) == 1, show firstWords (but if it were a |, show functions and globals) +- // 4. ...? (parenthetical expressions, arguments, ...) (packages, namespaces, nil?) +- if len(words) == 0 { +- return nil, nil // if this happens, why were we called? +- } +- pattern := words[len(words)-1] +- if pattern[0] == '$' { +- // should we also return a raw "$"? +- for _, s := range c.syms { +- if s.kind == protocol.Variable && weakMatch(s.name, pattern) > 0 { +- ans.Items = append(ans.Items, protocol.CompletionItem{ +- Label: s.name, +- Kind: protocol.VariableCompletion, +- Detail: "Variable", +- }) +- } +- } +- return ans, nil +- } +- if pattern[0] == '.' { +- for _, s := range c.syms { +- if s.kind == protocol.Method && weakMatch("."+s.name, pattern) > 0 { +- ans.Items = append(ans.Items, protocol.CompletionItem{ +- Label: s.name, +- Kind: protocol.MethodCompletion, +- Detail: "Method/member", +- }) +- } - } +- return ans, nil - } -- if len(opt.EnumValues) > 0 && opt.Type == "enum" { -- b.WriteString("\nMust be one of:\n\n") -- for _, val := range opt.EnumValues { -- write(val.Value, val.Doc) +- // could we get completion attempts in strings or numbers, and if so, do we care? +- // globals +- for _, kw := range globals { +- if weakMatch(kw, pattern) != 0 { +- ans.Items = append(ans.Items, protocol.CompletionItem{ +- Label: kw, +- Kind: protocol.KeywordCompletion, +- Detail: "Function", +- }) - } -- } else if len(opt.EnumKeys.Keys) > 0 && shouldShowEnumKeysInSettings(opt.Name) { -- b.WriteString("\nCan contain any of:\n\n") -- for _, val := range opt.EnumKeys.Keys { -- write(val.Name, val.Doc) +- } +- // and functions +- for _, s := range c.syms { +- if s.kind == protocol.Function && weakMatch(s.name, pattern) != 0 { +- ans.Items = append(ans.Items, protocol.CompletionItem{ +- Label: s.name, +- Kind: protocol.FunctionCompletion, +- Detail: "Function", +- }) - } - } -- return b.String() +- // keywords if we're at the beginning +- if len(words) <= 1 || len(words[len(words)-2]) == 1 && words[len(words)-2][0] == '|' { +- for _, kw := range keywords { +- if weakMatch(kw, pattern) != 0 { +- ans.Items = append(ans.Items, protocol.CompletionItem{ +- Label: kw, +- Kind: protocol.KeywordCompletion, +- Detail: "keyword", +- }) +- } +- } +- } +- return ans, nil -} - --func shouldShowEnumKeysInSettings(name string) bool { -- // These fields have too many possible options to print. -- return !(name == "analyses" || name == "codelenses" || name == "hints") +-// version of c.analyze that uses go/scanner. +-func scan(buf []byte) []string { +- fset := token.NewFileSet() +- fp := fset.AddFile("", -1, len(buf)) +- var sc scanner.Scanner +- sc.Init(fp, buf, func(pos token.Position, msg string) {}, scanner.ScanComments) +- ans := make([]string, 0, 10) // preallocating gives a measurable savings +- for { +- _, tok, lit := sc.Scan() // tok is an int +- if tok == token.EOF { +- break // done +- } else if tok == token.SEMICOLON && lit == "\n" { +- continue // don't care, but probably can't happen +- } else if tok == token.PERIOD { +- ans = append(ans, ".") // lit is empty +- } else if tok == token.IDENT && len(ans) > 0 && ans[len(ans)-1] == "." { +- ans[len(ans)-1] = "." + lit +- } else if tok == token.IDENT && len(ans) > 0 && ans[len(ans)-1] == "$" { +- ans[len(ans)-1] = "$" + lit +- } else if lit != "" { +- ans = append(ans, lit) +- } +- } +- return ans -} - --type EnumKeys struct { -- ValueType string -- Keys []EnumKey +-// pattern is what the user has typed +-func weakMatch(choice, pattern string) float64 { +- lower := strings.ToLower(choice) +- // for now, use only lower-case everywhere +- pattern = strings.ToLower(pattern) +- // The first char has to match +- if pattern[0] != lower[0] { +- return 0 +- } +- // If they start with ., then the second char has to match +- from := 1 +- if pattern[0] == '.' { +- if len(pattern) < 2 { +- return 1 // pattern just a ., so it matches +- } +- if pattern[1] != lower[1] { +- return 0 +- } +- from = 2 +- } +- // check that all the characters of pattern occur as a subsequence of choice +- i, j := from, from +- for ; i < len(lower) && j < len(pattern); j++ { +- if pattern[j] == lower[i] { +- i++ +- if i >= len(lower) { +- return 0 +- } +- } +- } +- if j < len(pattern) { +- return 0 +- } +- return 1 -} +diff -urN a/gopls/internal/template/completion_test.go b/gopls/internal/template/completion_test.go +--- a/gopls/internal/template/completion_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/template/completion_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,102 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --type EnumKey struct { -- Name string -- Doc string -- Default string --} +-package template +- +-import ( +- "log" +- "sort" +- "strings" +- "testing" - --type EnumValue struct { -- Value string -- Doc string --} +- "golang.org/x/tools/gopls/internal/protocol" +-) - --type CommandJSON struct { -- Command string -- Title string -- Doc string -- ArgDoc string -- ResultDoc string +-func init() { +- log.SetFlags(log.Lshortfile) -} - --func (c *CommandJSON) String() string { -- return c.Command +-type tparse struct { +- marked string // ^ shows where to ask for completions. (The user just typed the following character.) +- wanted []string // expected completions -} - --func (c *CommandJSON) Write(w io.Writer) { -- fmt.Fprintf(w, "### **%v**\nIdentifier: `%v`\n\n%v\n\n", c.Title, c.Command, c.Doc) -- if c.ArgDoc != "" { -- fmt.Fprintf(w, "Args:\n\n```\n%s\n```\n\n", c.ArgDoc) +-// Test completions in templates that parse enough (if completion needs symbols) +-// Seen characters up to the ^ +-func TestParsed(t *testing.T) { +- var tests = []tparse{ +- {"{{x}}{{12. xx^", nil}, // https://github.com/golang/go/issues/50430 +- {``, nil}, +- {"{{i^f}}", []string{"index", "if"}}, +- {"{{if .}}{{e^ {{end}}", []string{"eq", "end}}", "else", "end"}}, +- {"{{foo}}{{f^", []string{"foo"}}, +- {"{{$^}}", []string{"$"}}, +- {"{{$x:=4}}{{$^", []string{"$x"}}, +- {"{{$x:=4}}{{$ ^ ", []string{}}, +- {"{{len .Modified}}{{.^Mo", []string{"Modified"}}, +- {"{{len .Modified}}{{.mf^", []string{"Modified"}}, +- {"{{$^ }}", []string{"$"}}, +- {"{{$a =3}}{{$^", []string{"$a"}}, +- // .two is not good here: fix someday +- {`{{.Modified}}{{.^{{if $.one.two}}xxx{{end}}`, []string{"Modified", "one", "two"}}, +- {`{{.Modified}}{{.o^{{if $.one.two}}xxx{{end}}`, []string{"one"}}, +- {"{{.Modiifed}}{{.one.t^{{if $.one.two}}xxx{{end}}", []string{"two"}}, +- {`{{block "foo" .}}{{i^`, []string{"index", "if"}}, +- {"{{in^{{Internal}}", []string{"index", "Internal", "if"}}, +- // simple number has no completions +- {"{{4^e", []string{}}, +- // simple string has no completions +- {"{{`e^", []string{}}, +- {"{{`No i^", []string{}}, // example of why go/scanner is used +- {"{{xavier}}{{12. x^", []string{"xavier"}}, - } -- if c.ResultDoc != "" { -- fmt.Fprintf(w, "Result:\n\n```\n%s\n```\n\n", c.ResultDoc) +- for _, tx := range tests { +- c := testCompleter(t, tx) +- var v []string +- if c != nil { +- ans, _ := c.complete() +- for _, a := range ans.Items { +- v = append(v, a.Label) +- } +- } +- if len(v) != len(tx.wanted) { +- t.Errorf("%q: got %q, wanted %q %d,%d", tx.marked, v, tx.wanted, len(v), len(tx.wanted)) +- continue +- } +- sort.Strings(tx.wanted) +- sort.Strings(v) +- for i := 0; i < len(v); i++ { +- if tx.wanted[i] != v[i] { +- t.Errorf("%q at %d: got %v, wanted %v", tx.marked, i, v, tx.wanted) +- break +- } +- } - } -} - --type LensJSON struct { -- Lens string -- Title string -- Doc string +-func testCompleter(t *testing.T, tx tparse) *completer { +- t.Helper() +- // seen chars up to ^ +- col := strings.Index(tx.marked, "^") +- buf := strings.Replace(tx.marked, "^", "", 1) +- p := parseBuffer([]byte(buf)) +- pos := protocol.Position{Line: 0, Character: uint32(col)} +- if p.ParseErr != nil { +- log.Printf("%q: %v", tx.marked, p.ParseErr) +- } +- offset := inTemplate(p, pos) +- if offset == -1 { +- return nil +- } +- syms := make(map[string]symbol) +- filterSyms(syms, p.symbols) +- c := &completer{ +- p: p, +- pos: protocol.Position{Line: 0, Character: uint32(col)}, +- offset: offset + len(Left), +- ctx: protocol.CompletionContext{TriggerKind: protocol.Invoked}, +- syms: syms, +- } +- return c -} +diff -urN a/gopls/internal/template/highlight.go b/gopls/internal/template/highlight.go +--- a/gopls/internal/template/highlight.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/template/highlight.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,97 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func (l *LensJSON) String() string { -- return l.Title --} +-package template - --func (l *LensJSON) Write(w io.Writer) { -- fmt.Fprintf(w, "%s (%s): %s", l.Title, l.Lens, l.Doc) --} +-import ( +- "context" +- "fmt" +- "regexp" - --type AnalyzerJSON struct { -- Name string -- Doc string -- URL string -- Default bool --} +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +-) - --func (a *AnalyzerJSON) String() string { -- return a.Name +-func Highlight(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, loc protocol.Position) ([]protocol.DocumentHighlight, error) { +- buf, err := fh.Content() +- if err != nil { +- return nil, err +- } +- p := parseBuffer(buf) +- pos := p.FromPosition(loc) +- var ans []protocol.DocumentHighlight +- if p.ParseErr == nil { +- for _, s := range p.symbols { +- if s.start <= pos && pos < s.start+s.length { +- return markSymbols(p, s) +- } +- } +- } +- // these tokens exist whether or not there was a parse error +- // (symbols require a successful parse) +- for _, tok := range p.tokens { +- if tok.Start <= pos && pos < tok.End { +- wordAt := findWordAt(p, pos) +- if len(wordAt) > 0 { +- return markWordInToken(p, wordAt) +- } +- } +- } +- // find the 'word' at pos, etc: someday +- // until then we get the default action, which doesn't respect word boundaries +- return ans, nil -} - --func (a *AnalyzerJSON) Write(w io.Writer) { -- fmt.Fprintf(w, "%s (%s): %v", a.Name, a.Doc, a.Default) +-func markSymbols(p *Parsed, sym symbol) ([]protocol.DocumentHighlight, error) { +- var ans []protocol.DocumentHighlight +- for _, s := range p.symbols { +- if s.name == sym.name { +- kind := protocol.Read +- if s.vardef { +- kind = protocol.Write +- } +- ans = append(ans, protocol.DocumentHighlight{ +- Range: p.Range(s.start, s.length), +- Kind: kind, +- }) +- } +- } +- return ans, nil -} - --type HintJSON struct { -- Name string -- Doc string -- Default bool +-// A token is {{...}}, and this marks words in the token that equal the give word +-func markWordInToken(p *Parsed, wordAt string) ([]protocol.DocumentHighlight, error) { +- var ans []protocol.DocumentHighlight +- pat, err := regexp.Compile(fmt.Sprintf(`\b%s\b`, wordAt)) +- if err != nil { +- return nil, fmt.Errorf("%q: unmatchable word (%v)", wordAt, err) +- } +- for _, tok := range p.tokens { +- got := pat.FindAllIndex(p.buf[tok.Start:tok.End], -1) +- for i := 0; i < len(got); i++ { +- ans = append(ans, protocol.DocumentHighlight{ +- Range: p.Range(got[i][0], got[i][1]-got[i][0]), +- Kind: protocol.Text, +- }) +- } +- } +- return ans, nil -} - --func (h *HintJSON) String() string { -- return h.Name --} +-var wordRe = regexp.MustCompile(`[$]?\w+$`) +-var moreRe = regexp.MustCompile(`^[$]?\w+`) - --func (h *HintJSON) Write(w io.Writer) { -- fmt.Fprintf(w, "%s (%s): %v", h.Name, h.Doc, h.Default) +-// findWordAt finds the word the cursor is in (meaning in or just before) +-func findWordAt(p *Parsed, pos int) string { +- if pos >= len(p.buf) { +- return "" // can't happen, as we are called with pos < tok.End +- } +- after := moreRe.Find(p.buf[pos:]) +- if len(after) == 0 { +- return "" // end of the word +- } +- got := wordRe.Find(p.buf[:pos+len(after)]) +- return string(got) -} -diff -urN a/gopls/internal/lsp/source/options_test.go b/gopls/internal/lsp/source/options_test.go ---- a/gopls/internal/lsp/source/options_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/options_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,206 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/template/implementations.go b/gopls/internal/template/implementations.go +--- a/gopls/internal/template/implementations.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/template/implementations.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,218 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package source +-package template - -import ( -- "testing" +- "context" +- "fmt" +- "regexp" +- "strconv" - "time" +- +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/semtok" -) - --func TestSetOption(t *testing.T) { -- tests := []struct { -- name string -- value interface{} -- wantError bool -- check func(Options) bool -- }{ -- { -- name: "symbolStyle", -- value: "Dynamic", -- check: func(o Options) bool { return o.SymbolStyle == DynamicSymbols }, -- }, -- { -- name: "symbolStyle", -- value: "", -- wantError: true, -- check: func(o Options) bool { return o.SymbolStyle == "" }, -- }, -- { -- name: "symbolStyle", -- value: false, -- wantError: true, -- check: func(o Options) bool { return o.SymbolStyle == "" }, -- }, -- { -- name: "symbolMatcher", -- value: "caseInsensitive", -- check: func(o Options) bool { return o.SymbolMatcher == SymbolCaseInsensitive }, -- }, -- { -- name: "completionBudget", -- value: "2s", -- check: func(o Options) bool { return o.CompletionBudget == 2*time.Second }, -- }, -- { -- name: "staticcheck", -- value: true, -- check: func(o Options) bool { return o.Staticcheck == true }, -- wantError: true, // o.StaticcheckSupported is unset -- }, -- { -- name: "codelenses", -- value: map[string]interface{}{"generate": true}, -- check: func(o Options) bool { return o.Codelenses["generate"] }, -- }, -- { -- name: "allExperiments", -- value: true, -- check: func(o Options) bool { -- return true // just confirm that we handle this setting -- }, -- }, -- { -- name: "hoverKind", -- value: "FullDocumentation", -- check: func(o Options) bool { -- return o.HoverKind == FullDocumentation -- }, -- }, -- { -- name: "hoverKind", -- value: "NoDocumentation", -- check: func(o Options) bool { -- return o.HoverKind == NoDocumentation -- }, -- }, -- { -- name: "hoverKind", -- value: "SingleLine", -- check: func(o Options) bool { -- return o.HoverKind == SingleLine -- }, -- }, -- { -- name: "hoverKind", -- value: "Structured", -- check: func(o Options) bool { -- return o.HoverKind == Structured -- }, -- }, -- { -- name: "ui.documentation.hoverKind", -- value: "Structured", -- check: func(o Options) bool { -- return o.HoverKind == Structured -- }, -- }, -- { -- name: "matcher", -- value: "Fuzzy", -- check: func(o Options) bool { -- return o.Matcher == Fuzzy -- }, -- }, -- { -- name: "matcher", -- value: "CaseSensitive", -- check: func(o Options) bool { -- return o.Matcher == CaseSensitive -- }, -- }, -- { -- name: "matcher", -- value: "CaseInsensitive", -- check: func(o Options) bool { -- return o.Matcher == CaseInsensitive -- }, -- }, -- { -- name: "env", -- value: map[string]interface{}{"testing": "true"}, -- check: func(o Options) bool { -- v, found := o.Env["testing"] -- return found && v == "true" -- }, -- }, -- { -- name: "env", -- value: []string{"invalid", "input"}, -- wantError: true, -- check: func(o Options) bool { -- return o.Env == nil -- }, -- }, -- { -- name: "directoryFilters", -- value: []interface{}{"-node_modules", "+project_a"}, -- check: func(o Options) bool { -- return len(o.DirectoryFilters) == 2 -- }, -- }, -- { -- name: "directoryFilters", -- value: []interface{}{"invalid"}, -- wantError: true, -- check: func(o Options) bool { -- return len(o.DirectoryFilters) == 0 -- }, -- }, -- { -- name: "directoryFilters", -- value: []string{"-invalid", "+type"}, -- wantError: true, -- check: func(o Options) bool { -- return len(o.DirectoryFilters) == 0 -- }, -- }, -- { -- name: "annotations", -- value: map[string]interface{}{ -- "Nil": false, -- "noBounds": true, -- }, -- wantError: true, -- check: func(o Options) bool { -- return !o.Annotations[Nil] && !o.Annotations[Bounds] -- }, -- }, -- { -- name: "vulncheck", -- value: []interface{}{"invalid"}, -- wantError: true, -- check: func(o Options) bool { -- return o.Vulncheck == "" // For invalid value, default to 'off'. -- }, -- }, -- { -- name: "vulncheck", -- value: "Imports", -- check: func(o Options) bool { -- return o.Vulncheck == ModeVulncheckImports // For invalid value, default to 'off'. -- }, -- }, -- { -- name: "vulncheck", -- value: "imports", -- check: func(o Options) bool { -- return o.Vulncheck == ModeVulncheckImports -- }, -- }, -- } +-// line number (1-based) and message +-var errRe = regexp.MustCompile(`template.*:(\d+): (.*)`) - -- for _, test := range tests { -- var opts Options -- result := opts.set(test.name, test.value, map[string]struct{}{}) -- if (result.Error != nil) != test.wantError { -- t.Fatalf("Options.set(%q, %v): result.Error = %v, want error: %t", test.name, test.value, result.Error, test.wantError) -- } -- // TODO: this could be made much better using cmp.Diff, if that becomes -- // available in this module. -- if !test.check(opts) { -- t.Errorf("Options.set(%q, %v): unexpected result %+v", test.name, test.value, opts) -- } +-// Diagnostics returns parse errors. There is only one per file. +-// The errors are not always helpful. For instance { {end}} +-// will likely point to the end of the file. +-func Diagnostics(snapshot *cache.Snapshot) map[protocol.DocumentURI][]*cache.Diagnostic { +- diags := make(map[protocol.DocumentURI][]*cache.Diagnostic) +- for uri, fh := range snapshot.Templates() { +- diags[uri] = diagnoseOne(fh) - } +- return diags -} -diff -urN a/gopls/internal/lsp/source/origin_119.go b/gopls/internal/lsp/source/origin_119.go ---- a/gopls/internal/lsp/source/origin_119.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/origin_119.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,33 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --//go:build go1.19 --// +build go1.19 - --package source -- --import "go/types" +-func diagnoseOne(fh file.Handle) []*cache.Diagnostic { +- // no need for skipTemplate check, as Diagnose is called on the +- // snapshot's template files +- buf, err := fh.Content() +- if err != nil { +- // Is a Diagnostic with no Range useful? event.Error also? +- msg := fmt.Sprintf("failed to read %s (%v)", fh.URI().Path(), err) +- d := cache.Diagnostic{Message: msg, Severity: protocol.SeverityError, URI: fh.URI(), +- Source: cache.TemplateError} +- return []*cache.Diagnostic{&d} +- } +- p := parseBuffer(buf) +- if p.ParseErr == nil { +- return nil +- } +- unknownError := func(msg string) []*cache.Diagnostic { +- s := fmt.Sprintf("malformed template error %q: %s", p.ParseErr.Error(), msg) +- d := cache.Diagnostic{ +- Message: s, Severity: protocol.SeverityError, Range: p.Range(p.nls[0], 1), +- URI: fh.URI(), Source: cache.TemplateError} +- return []*cache.Diagnostic{&d} +- } +- // errors look like `template: :40: unexpected "}" in operand` +- // so the string needs to be parsed +- matches := errRe.FindStringSubmatch(p.ParseErr.Error()) +- if len(matches) != 3 { +- msg := fmt.Sprintf("expected 3 matches, got %d (%v)", len(matches), matches) +- return unknownError(msg) +- } +- lineno, err := strconv.Atoi(matches[1]) +- if err != nil { +- msg := fmt.Sprintf("couldn't convert %q to int, %v", matches[1], err) +- return unknownError(msg) +- } +- msg := matches[2] +- d := cache.Diagnostic{Message: msg, Severity: protocol.SeverityError, +- Source: cache.TemplateError} +- start := p.nls[lineno-1] +- if lineno < len(p.nls) { +- size := p.nls[lineno] - start +- d.Range = p.Range(start, size) +- } else { +- d.Range = p.Range(start, 1) +- } +- return []*cache.Diagnostic{&d} +-} - --// containsOrigin reports whether the provided object set contains an object --// with the same origin as the provided obj (which may be a synthetic object --// created during instantiation). --func containsOrigin(objSet map[types.Object]bool, obj types.Object) bool { -- objOrigin := origin(obj) -- for target := range objSet { -- if origin(target) == objOrigin { -- return true +-// Definition finds the definitions of the symbol at loc. It +-// does not understand scoping (if any) in templates. This code is +-// for definitions, type definitions, and implementations. +-// Results only for variables and templates. +-func Definition(snapshot *cache.Snapshot, fh file.Handle, loc protocol.Position) ([]protocol.Location, error) { +- x, _, err := symAtPosition(fh, loc) +- if err != nil { +- return nil, err +- } +- sym := x.name +- ans := []protocol.Location{} +- // PJW: this is probably a pattern to abstract +- a := New(snapshot.Templates()) +- for k, p := range a.files { +- for _, s := range p.symbols { +- if !s.vardef || s.name != sym { +- continue +- } +- ans = append(ans, protocol.Location{URI: k, Range: p.Range(s.start, s.length)}) - } - } -- return false +- return ans, nil -} - --func origin(obj types.Object) types.Object { -- switch obj := obj.(type) { -- case *types.Var: -- return obj.Origin() -- case *types.Func: -- return obj.Origin() +-func Hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) (*protocol.Hover, error) { +- sym, p, err := symAtPosition(fh, position) +- if sym == nil || err != nil { +- return nil, err +- } +- ans := protocol.Hover{Range: p.Range(sym.start, sym.length), Contents: protocol.MarkupContent{Kind: protocol.Markdown}} +- switch sym.kind { +- case protocol.Function: +- ans.Contents.Value = fmt.Sprintf("function: %s", sym.name) +- case protocol.Variable: +- ans.Contents.Value = fmt.Sprintf("variable: %s", sym.name) +- case protocol.Constant: +- ans.Contents.Value = fmt.Sprintf("constant %s", sym.name) +- case protocol.Method: // field or method +- ans.Contents.Value = fmt.Sprintf("%s: field or method", sym.name) +- case protocol.Package: // template use, template def (PJW: do we want two?) +- ans.Contents.Value = fmt.Sprintf("template %s\n(add definition)", sym.name) +- case protocol.Namespace: +- ans.Contents.Value = fmt.Sprintf("template %s defined", sym.name) +- case protocol.Number: +- ans.Contents.Value = "number" +- case protocol.String: +- ans.Contents.Value = "string" +- case protocol.Boolean: +- ans.Contents.Value = "boolean" +- default: +- ans.Contents.Value = fmt.Sprintf("oops, sym=%#v", sym) - } -- return obj +- return &ans, nil -} -diff -urN a/gopls/internal/lsp/source/origin.go b/gopls/internal/lsp/source/origin.go ---- a/gopls/internal/lsp/source/origin.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/origin.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,26 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --//go:build !go1.19 --// +build !go1.19 - --package source -- --import "go/types" -- --// containsOrigin reports whether the provided object set contains an object --// with the same origin as the provided obj (which may be a synthetic object --// created during instantiation). --func containsOrigin(objSet map[types.Object]bool, obj types.Object) bool { -- if obj == nil { -- return objSet[obj] +-func References(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, params *protocol.ReferenceParams) ([]protocol.Location, error) { +- sym, _, err := symAtPosition(fh, params.Position) +- if sym == nil || err != nil || sym.name == "" { +- return nil, err - } -- // In Go 1.18, we can't use the types.Var.Origin and types.Func.Origin methods. -- for target := range objSet { -- if target.Pkg() == obj.Pkg() && target.Pos() == obj.Pos() && target.Name() == obj.Name() { -- return true +- ans := []protocol.Location{} +- +- a := New(snapshot.Templates()) +- for k, p := range a.files { +- for _, s := range p.symbols { +- if s.name != sym.name { +- continue +- } +- if s.vardef && !params.Context.IncludeDeclaration { +- continue +- } +- ans = append(ans, protocol.Location{URI: k, Range: p.Range(s.start, s.length)}) - } - } -- return false +- // do these need to be sorted? (a.files is a map) +- return ans, nil -} -diff -urN a/gopls/internal/lsp/source/parsemode_go116.go b/gopls/internal/lsp/source/parsemode_go116.go ---- a/gopls/internal/lsp/source/parsemode_go116.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/parsemode_go116.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,13 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --//go:build !go1.17 --// +build !go1.17 -- --package source -- --import "go/parser" -- --// The parser.SkipObjectResolution mode flag is not supported before Go 1.17. --const SkipObjectResolution parser.Mode = 0 -diff -urN a/gopls/internal/lsp/source/parsemode_go117.go b/gopls/internal/lsp/source/parsemode_go117.go ---- a/gopls/internal/lsp/source/parsemode_go117.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/parsemode_go117.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,12 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --//go:build go1.17 --// +build go1.17 +-func SemanticTokens(ctx context.Context, snapshot *cache.Snapshot, spn protocol.DocumentURI) (*protocol.SemanticTokens, error) { +- fh, err := snapshot.ReadFile(ctx, spn) +- if err != nil { +- return nil, err +- } +- buf, err := fh.Content() +- if err != nil { +- return nil, err +- } +- p := parseBuffer(buf) - --package source +- var items []semtok.Token +- add := func(line, start, len uint32) { +- if len == 0 { +- return // vscode doesn't like 0-length Tokens +- } +- // TODO(adonovan): don't ignore the rng restriction, if any. +- items = append(items, semtok.Token{ +- Line: line, +- Start: start, +- Len: len, +- Type: semtok.TokMacro, +- }) +- } - --import "go/parser" +- for _, t := range p.Tokens() { +- if t.Multiline { +- la, ca := p.LineCol(t.Start) +- lb, cb := p.LineCol(t.End) +- add(la, ca, p.RuneCount(la, ca, 0)) +- for l := la + 1; l < lb; l++ { +- add(l, 0, p.RuneCount(l, 0, 0)) +- } +- add(lb, 0, p.RuneCount(lb, 0, cb)) +- continue +- } +- sz, err := p.TokenSize(t) +- if err != nil { +- return nil, err +- } +- line, col := p.LineCol(t.Start) +- add(line, col, uint32(sz)) +- } +- const noStrings = false +- const noNumbers = false +- ans := &protocol.SemanticTokens{ +- Data: semtok.Encode( +- items, +- noStrings, +- noNumbers, +- snapshot.Options().SemanticTypes, +- snapshot.Options().SemanticMods), +- // for small cache, some day. for now, the LSP client ignores this +- // (that is, when the LSP client starts returning these, we can cache) +- ResultID: fmt.Sprintf("%v", time.Now()), +- } +- return ans, nil +-} - --const SkipObjectResolution = parser.SkipObjectResolution -diff -urN a/gopls/internal/lsp/source/references.go b/gopls/internal/lsp/source/references.go ---- a/gopls/internal/lsp/source/references.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/references.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,692 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. +-// still need to do rename, etc +diff -urN a/gopls/internal/template/parse.go b/gopls/internal/template/parse.go +--- a/gopls/internal/template/parse.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/template/parse.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,504 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package source +-// Package template contains code for dealing with templates +-package template - --// This file defines the 'references' query based on a serializable --// index constructed during type checking, thus avoiding the need to --// type-check packages at search time. --// --// See the ./xrefs/ subpackage for the index construction and lookup. --// --// This implementation does not intermingle objects from distinct --// calls to TypeCheck. +-// template files are small enough that the code reprocesses them each time +-// this may be a bad choice for projects with lots of template files. - -import ( +- "bytes" - "context" - "fmt" -- "go/ast" -- "go/token" -- "go/types" +- "io" +- "log" +- "regexp" +- "runtime" - "sort" -- "strings" -- "sync" +- "text/template" +- "text/template/parse" +- "unicode/utf8" - -- "golang.org/x/sync/errgroup" -- "golang.org/x/tools/go/types/objectpath" -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/gopls/internal/lsp/source/methodsets" -- "golang.org/x/tools/gopls/internal/span" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/internal/event" -) - --// References returns a list of all references (sorted with --// definitions before uses) to the object denoted by the identifier at --// the given file/position, searching the entire workspace. --func References(ctx context.Context, snapshot Snapshot, fh FileHandle, pp protocol.Position, includeDeclaration bool) ([]protocol.Location, error) { -- references, err := references(ctx, snapshot, fh, pp, includeDeclaration) -- if err != nil { -- return nil, err -- } -- locations := make([]protocol.Location, len(references)) -- for i, ref := range references { -- locations[i] = ref.location -- } -- return locations, nil +-var ( +- Left = []byte("{{") +- Right = []byte("}}") +-) +- +-type Parsed struct { +- buf []byte //contents +- lines [][]byte // needed?, other than for debugging? +- elided []int // offsets where Left was replaced by blanks +- +- // tokens are matched Left-Right pairs, computed before trying to parse +- tokens []Token +- +- // result of parsing +- named []*template.Template // the template and embedded templates +- ParseErr error +- symbols []symbol +- stack []parse.Node // used while computing symbols +- +- // for mapping from offsets in buf to LSP coordinates +- // See FromPosition() and LineCol() +- nls []int // offset of newlines before each line (nls[0]==-1) +- lastnl int // last line seen +- check int // used to decide whether to use lastnl or search through nls +- nonASCII bool // are there any non-ascii runes in buf? -} - --// A reference describes an identifier that refers to the same --// object as the subject of a References query. --type reference struct { -- isDeclaration bool -- location protocol.Location -- pkgPath PackagePath // of declaring package (same for all elements of the slice) +-// Token is a single {{...}}. More precisely, Left...Right +-type Token struct { +- Start, End int // offset from start of template +- Multiline bool -} - --// references returns a list of all references (sorted with --// definitions before uses) to the object denoted by the identifier at --// the given file/position, searching the entire workspace. --func references(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position, includeDeclaration bool) ([]reference, error) { -- ctx, done := event.Start(ctx, "source.references") -- defer done() +-// All contains the Parse of all the template files +-type All struct { +- files map[protocol.DocumentURI]*Parsed +-} - -- // Is the cursor within the package name declaration? -- _, inPackageName, err := parsePackageNameDecl(ctx, snapshot, f, pp) -- if err != nil { -- return nil, err +-// New returns the Parses of the snapshot's tmpl files +-// (maybe cache these, but then avoiding import cycles needs code rearrangements) +-func New(tmpls map[protocol.DocumentURI]file.Handle) *All { +- all := make(map[protocol.DocumentURI]*Parsed) +- for k, v := range tmpls { +- buf, err := v.Content() +- if err != nil { // PJW: decide what to do with these errors +- log.Printf("failed to read %s (%v)", v.URI().Path(), err) +- continue +- } +- all[k] = parseBuffer(buf) - } +- return &All{files: all} +-} - -- var refs []reference -- if inPackageName { -- refs, err = packageReferences(ctx, snapshot, f.URI()) -- } else { -- refs, err = ordinaryReferences(ctx, snapshot, f.URI(), pp) +-func parseBuffer(buf []byte) *Parsed { +- ans := &Parsed{ +- buf: buf, +- check: -1, +- nls: []int{-1}, - } -- if err != nil { -- return nil, err +- if len(buf) == 0 { +- return ans - } -- -- sort.Slice(refs, func(i, j int) bool { -- x, y := refs[i], refs[j] -- if x.isDeclaration != y.isDeclaration { -- return x.isDeclaration // decls < refs -- } -- return protocol.CompareLocation(x.location, y.location) < 0 -- }) -- -- // De-duplicate by location, and optionally remove declarations. -- out := refs[:0] -- for _, ref := range refs { -- if !includeDeclaration && ref.isDeclaration { -- continue +- // how to compute allAscii... +- for _, b := range buf { +- if b >= utf8.RuneSelf { +- ans.nonASCII = true +- break - } -- if len(out) == 0 || out[len(out)-1].location != ref.location { -- out = append(out, ref) +- } +- if buf[len(buf)-1] != '\n' { +- ans.buf = append(buf, '\n') +- } +- for i, p := range ans.buf { +- if p == '\n' { +- ans.nls = append(ans.nls, i) - } - } -- refs = out -- -- return refs, nil --} -- --// packageReferences returns a list of references to the package --// declaration of the specified name and uri by searching among the --// import declarations of all packages that directly import the target --// package. --func packageReferences(ctx context.Context, snapshot Snapshot, uri span.URI) ([]reference, error) { -- metas, err := snapshot.MetadataForFile(ctx, uri) +- ans.setTokens() // ans.buf may be a new []byte +- ans.lines = bytes.Split(ans.buf, []byte{'\n'}) +- t, err := template.New("").Parse(string(ans.buf)) - if err != nil { -- return nil, err +- funcs := make(template.FuncMap) +- for t == nil && ans.ParseErr == nil { +- // in 1.17 it may be possible to avoid getting this error +- // template: :2: function "foo" not defined +- matches := parseErrR.FindStringSubmatch(err.Error()) +- if len(matches) == 2 { +- // suppress the error by giving it a function with the right name +- funcs[matches[1]] = func() interface{} { return nil } +- t, err = template.New("").Funcs(funcs).Parse(string(ans.buf)) +- continue +- } +- ans.ParseErr = err // unfixed error +- return ans +- } - } -- if len(metas) == 0 { -- return nil, fmt.Errorf("found no package containing %s", uri) +- ans.named = t.Templates() +- // set the symbols +- for _, t := range ans.named { +- ans.stack = append(ans.stack, t.Root) +- ans.findSymbols() +- if t.Name() != "" { +- // defining a template. The pos is just after {{define...}} (or {{block...}}?) +- at, sz := ans.FindLiteralBefore(int(t.Root.Pos)) +- s := symbol{start: at, length: sz, name: t.Name(), kind: protocol.Namespace, vardef: true} +- ans.symbols = append(ans.symbols, s) +- } - } - -- var refs []reference -- -- // Find external references to the package declaration -- // from each direct import of the package. -- // -- // The narrowest package is the most broadly imported, -- // so we choose it for the external references. -- // -- // But if the file ends with _test.go then we need to -- // find the package it is testing; there's no direct way -- // to do that, so pick a file from the same package that -- // doesn't end in _test.go and start over. -- narrowest := metas[0] -- if narrowest.ForTest != "" && strings.HasSuffix(string(uri), "_test.go") { -- for _, f := range narrowest.CompiledGoFiles { -- if !strings.HasSuffix(string(f), "_test.go") { -- return packageReferences(ctx, snapshot, f) -- } +- sort.Slice(ans.symbols, func(i, j int) bool { +- left, right := ans.symbols[i], ans.symbols[j] +- if left.start != right.start { +- return left.start < right.start - } -- // This package has no non-test files. -- // Skip the search for external references. -- // (Conceivably one could blank-import an empty package, but why?) -- } else { -- rdeps, err := snapshot.ReverseDependencies(ctx, narrowest.ID, false) // direct -- if err != nil { -- return nil, err +- if left.vardef != right.vardef { +- return left.vardef - } +- return left.kind < right.kind +- }) +- return ans +-} - -- // Restrict search to workspace packages. -- workspace, err := snapshot.WorkspaceMetadata(ctx) -- if err != nil { -- return nil, err +-// FindLiteralBefore locates the first preceding string literal +-// returning its position and length in buf +-// or returns -1 if there is none. +-// Assume double-quoted string rather than backquoted string for now. +-func (p *Parsed) FindLiteralBefore(pos int) (int, int) { +- left, right := -1, -1 +- for i := pos - 1; i >= 0; i-- { +- if p.buf[i] != '"' { +- continue - } -- workspaceMap := make(map[PackageID]*Metadata, len(workspace)) -- for _, m := range workspace { -- workspaceMap[m.ID] = m +- if right == -1 { +- right = i +- continue - } +- left = i +- break +- } +- if left == -1 { +- return -1, 0 +- } +- return left + 1, right - left - 1 +-} - -- for _, rdep := range rdeps { -- if _, ok := workspaceMap[rdep.ID]; !ok { +-var ( +- parseErrR = regexp.MustCompile(`template:.*function "([^"]+)" not defined`) +-) +- +-func (p *Parsed) setTokens() { +- const ( +- // InRaw and InString only occur inside an action (SeenLeft) +- Start = iota +- InRaw +- InString +- SeenLeft +- ) +- state := Start +- var left, oldState int +- for n := 0; n < len(p.buf); n++ { +- c := p.buf[n] +- switch state { +- case InRaw: +- if c == '`' { +- state = oldState +- } +- case InString: +- if c == '"' && !isEscaped(p.buf[:n]) { +- state = oldState +- } +- case SeenLeft: +- if c == '`' { +- oldState = state // it's SeenLeft, but a little clearer this way +- state = InRaw - continue - } -- for _, uri := range rdep.CompiledGoFiles { -- fh, err := snapshot.ReadFile(ctx, uri) -- if err != nil { -- return nil, err -- } -- f, err := snapshot.ParseGo(ctx, fh, ParseHeader) -- if err != nil { -- return nil, err -- } -- for _, imp := range f.File.Imports { -- if rdep.DepsByImpPath[UnquoteImportPath(imp)] == narrowest.ID { -- refs = append(refs, reference{ -- isDeclaration: false, -- location: mustLocation(f, imp), -- pkgPath: narrowest.PkgPath, -- }) -- } +- if c == '"' { +- oldState = state +- state = InString +- continue +- } +- if bytes.HasPrefix(p.buf[n:], Right) { +- right := n + len(Right) +- tok := Token{Start: left, +- End: right, +- Multiline: bytes.Contains(p.buf[left:right], []byte{'\n'}), - } +- p.tokens = append(p.tokens, tok) +- state = Start +- } +- // If we see (unquoted) Left then the original left is probably the user +- // typing. Suppress the original left +- if bytes.HasPrefix(p.buf[n:], Left) { +- p.elideAt(left) +- left = n +- n += len(Left) - 1 // skip the rest +- } +- case Start: +- if bytes.HasPrefix(p.buf[n:], Left) { +- left = n +- state = SeenLeft +- n += len(Left) - 1 // skip the rest (avoids {{{ bug) - } - } - } +- // this error occurs after typing {{ at the end of the file +- if state != Start { +- // Unclosed Left. remove the Left at left +- p.elideAt(left) +- } +-} - -- // Find internal "references" to the package from -- // of each package declaration in the target package itself. -- // -- // The widest package (possibly a test variant) has the -- // greatest number of files and thus we choose it for the -- // "internal" references. -- widest := metas[len(metas)-1] // may include _test.go files -- for _, uri := range widest.CompiledGoFiles { -- fh, err := snapshot.ReadFile(ctx, uri) -- if err != nil { -- return nil, err -- } -- f, err := snapshot.ParseGo(ctx, fh, ParseHeader) -- if err != nil { -- return nil, err -- } -- refs = append(refs, reference{ -- isDeclaration: true, // (one of many) -- location: mustLocation(f, f.File.Name), -- pkgPath: widest.PkgPath, -- }) +-func (p *Parsed) elideAt(left int) { +- if p.elided == nil { +- // p.buf is the same buffer that v.Read() returns, so copy it. +- // (otherwise the next time it's parsed, elided information is lost) +- b := make([]byte, len(p.buf)) +- copy(b, p.buf) +- p.buf = b +- } +- for i := 0; i < len(Left); i++ { +- p.buf[left+i] = ' ' - } +- p.elided = append(p.elided, left) +-} - -- return refs, nil +-// isEscaped reports whether the byte after buf is escaped +-func isEscaped(buf []byte) bool { +- backSlashes := 0 +- for j := len(buf) - 1; j >= 0 && buf[j] == '\\'; j-- { +- backSlashes++ +- } +- return backSlashes%2 == 1 -} - --// ordinaryReferences computes references for all ordinary objects (not package declarations). --func ordinaryReferences(ctx context.Context, snapshot Snapshot, uri span.URI, pp protocol.Position) ([]reference, error) { -- // Strategy: use the reference information computed by the -- // type checker to find the declaration. First type-check this -- // package to find the declaration, then type check the -- // declaring package (which may be different), plus variants, -- // to find local (in-package) references. -- // Global references are satisfied by the index. +-func (p *Parsed) Tokens() []Token { +- return p.tokens +-} - -- // Strictly speaking, a wider package could provide a different -- // declaration (e.g. because the _test.go files can change the -- // meaning of a field or method selection), but the narrower -- // package reports the more broadly referenced object. -- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, uri) -- if err != nil { -- return nil, err +-// TODO(adonovan): the next 100 lines could perhaps replaced by use of protocol.Mapper. +- +-func (p *Parsed) utf16len(buf []byte) int { +- cnt := 0 +- if !p.nonASCII { +- return len(buf) +- } +- // we need a utf16len(rune), but we don't have it +- for _, r := range string(buf) { +- cnt++ +- if r >= 1<<16 { +- cnt++ +- } - } +- return cnt +-} - -- // Find the selected object (declaration or reference). -- // For struct{T}, we choose the field (Def) over the type (Use). -- pos, err := pgf.PositionPos(pp) -- if err != nil { -- return nil, err +-func (p *Parsed) TokenSize(t Token) (int, error) { +- if t.Multiline { +- return -1, fmt.Errorf("TokenSize called with Multiline token %#v", t) +- } +- ans := p.utf16len(p.buf[t.Start:t.End]) +- return ans, nil +-} +- +-// RuneCount counts runes in line l, from col s to e +-// (e==0 for end of line. called only for multiline tokens) +-func (p *Parsed) RuneCount(l, s, e uint32) uint32 { +- start := p.nls[l] + 1 + int(s) +- end := p.nls[l] + 1 + int(e) +- if e == 0 || end > p.nls[l+1] { +- end = p.nls[l+1] +- } +- return uint32(utf8.RuneCount(p.buf[start:end])) +-} +- +-// LineCol converts from a 0-based byte offset to 0-based line, col. col in runes +-func (p *Parsed) LineCol(x int) (uint32, uint32) { +- if x < p.check { +- p.lastnl = 0 +- } +- p.check = x +- for i := p.lastnl; i < len(p.nls); i++ { +- if p.nls[i] <= x { +- continue +- } +- p.lastnl = i +- var count int +- if i > 0 && x == p.nls[i-1] { // \n +- count = 0 +- } else { +- count = p.utf16len(p.buf[p.nls[i-1]+1 : x]) +- } +- return uint32(i - 1), uint32(count) - } -- candidates, _, err := objectsAt(pkg.GetTypesInfo(), pgf.File, pos) -- if err != nil { -- return nil, err +- if x == len(p.buf)-1 { // trailing \n +- return uint32(len(p.nls) - 1), 0 - } -- -- // Pick first object arbitrarily. -- // The case variables of a type switch have different -- // types but that difference is immaterial here. -- var obj types.Object -- for obj = range candidates { -- break +- // shouldn't happen +- for i := 1; i < 4; i++ { +- _, f, l, ok := runtime.Caller(i) +- if !ok { +- break +- } +- log.Printf("%d: %s:%d", i, f, l) - } -- if obj == nil { -- return nil, ErrNoIdentFound // can't happen +- +- msg := fmt.Errorf("LineCol off the end, %d of %d, nls=%v, %q", x, len(p.buf), p.nls, p.buf[x:]) +- event.Error(context.Background(), "internal error", msg) +- return 0, 0 +-} +- +-// Position produces a protocol.Position from an offset in the template +-func (p *Parsed) Position(pos int) protocol.Position { +- line, col := p.LineCol(pos) +- return protocol.Position{Line: line, Character: col} +-} +- +-func (p *Parsed) Range(x, length int) protocol.Range { +- line, col := p.LineCol(x) +- ans := protocol.Range{ +- Start: protocol.Position{Line: line, Character: col}, +- End: protocol.Position{Line: line, Character: col + uint32(length)}, - } +- return ans +-} - -- // nil, error, error.Error, iota, or other built-in? -- if obj.Pkg() == nil { -- return nil, fmt.Errorf("references to builtin %q are not supported", obj.Name()) +-// FromPosition translates a protocol.Position into an offset into the template +-func (p *Parsed) FromPosition(x protocol.Position) int { +- l, c := int(x.Line), int(x.Character) +- if l >= len(p.nls) || p.nls[l]+1 >= len(p.buf) { +- // paranoia to avoid panic. return the largest offset +- return len(p.buf) - } -- if !obj.Pos().IsValid() { -- if obj.Pkg().Path() != "unsafe" { -- bug.Reportf("references: object %v has no position", obj) +- line := p.buf[p.nls[l]+1:] +- cnt := 0 +- for w := range string(line) { +- if cnt >= c { +- return w + p.nls[l] + 1 - } -- return nil, fmt.Errorf("references to unsafe.%s are not supported", obj.Name()) +- cnt++ - } +- // do we get here? NO +- pos := int(x.Character) + p.nls[int(x.Line)] + 1 +- event.Error(context.Background(), "internal error", fmt.Errorf("surprise %#v", x)) +- return pos +-} - -- // Find metadata of all packages containing the object's defining file. -- // This may include the query pkg, and possibly other variants. -- declPosn := safetoken.StartPosition(pkg.FileSet(), obj.Pos()) -- declURI := span.URIFromPath(declPosn.Filename) -- variants, err := snapshot.MetadataForFile(ctx, declURI) +-func symAtPosition(fh file.Handle, loc protocol.Position) (*symbol, *Parsed, error) { +- buf, err := fh.Content() - if err != nil { -- return nil, err +- return nil, nil, err - } -- if len(variants) == 0 { -- return nil, fmt.Errorf("no packages for file %q", declURI) // can't happen +- p := parseBuffer(buf) +- pos := p.FromPosition(loc) +- syms := p.SymsAtPos(pos) +- if len(syms) == 0 { +- return nil, p, fmt.Errorf("no symbol found") - } -- // (variants must include ITVs for reverse dependency computation below.) +- if len(syms) > 1 { +- log.Printf("Hover: %d syms, not 1 %v", len(syms), syms) +- } +- sym := syms[0] +- return &sym, p, nil +-} - -- // Is object exported? -- // If so, compute scope and targets of the global search. -- var ( -- globalScope = make(map[PackageID]*Metadata) // (excludes ITVs) -- globalTargets map[PackagePath]map[objectpath.Path]unit -- expansions = make(map[PackageID]unit) // packages that caused search expansion -- ) -- // TODO(adonovan): what about generic functions? Need to consider both -- // uninstantiated and instantiated. The latter have no objectpath. Use Origin? -- if path, err := objectpath.For(obj); err == nil && obj.Exported() { -- pkgPath := variants[0].PkgPath // (all variants have same package path) -- globalTargets = map[PackagePath]map[objectpath.Path]unit{ -- pkgPath: {path: {}}, // primary target +-func (p *Parsed) SymsAtPos(pos int) []symbol { +- ans := []symbol{} +- for _, s := range p.symbols { +- if s.start <= pos && pos < s.start+s.length { +- ans = append(ans, s) - } +- } +- return ans +-} - -- // Compute set of (non-ITV) workspace packages. -- // We restrict references to this subset. -- workspace, err := snapshot.WorkspaceMetadata(ctx) -- if err != nil { -- return nil, err -- } -- workspaceMap := make(map[PackageID]*Metadata, len(workspace)) -- workspaceIDs := make([]PackageID, 0, len(workspace)) -- for _, m := range workspace { -- workspaceMap[m.ID] = m -- workspaceIDs = append(workspaceIDs, m.ID) -- } +-type wrNode struct { +- p *Parsed +- w io.Writer +-} - -- // addRdeps expands the global scope to include the -- // reverse dependencies of the specified package. -- addRdeps := func(id PackageID, transitive bool) error { -- rdeps, err := snapshot.ReverseDependencies(ctx, id, transitive) -- if err != nil { -- return err -- } -- for rdepID, rdep := range rdeps { -- // Skip non-workspace packages. -- // -- // This means we also skip any expansion of the -- // search that might be caused by a non-workspace -- // package, possibly causing us to miss references -- // to the expanded target set from workspace packages. -- // -- // TODO(adonovan): don't skip those expansions. -- // The challenge is how to so without type-checking -- // a lot of non-workspace packages not covered by -- // the initial workspace load. -- if _, ok := workspaceMap[rdepID]; !ok { -- continue -- } +-// WriteNode is for debugging +-func (p *Parsed) WriteNode(w io.Writer, n parse.Node) { +- wr := wrNode{p: p, w: w} +- wr.writeNode(n, "") +-} - -- globalScope[rdepID] = rdep -- } -- return nil +-func (wr wrNode) writeNode(n parse.Node, indent string) { +- if n == nil { +- return +- } +- at := func(pos parse.Pos) string { +- line, col := wr.p.LineCol(int(pos)) +- return fmt.Sprintf("(%d)%v:%v", pos, line, col) +- } +- switch x := n.(type) { +- case *parse.ActionNode: +- fmt.Fprintf(wr.w, "%sActionNode at %s\n", indent, at(x.Pos)) +- wr.writeNode(x.Pipe, indent+". ") +- case *parse.BoolNode: +- fmt.Fprintf(wr.w, "%sBoolNode at %s, %v\n", indent, at(x.Pos), x.True) +- case *parse.BranchNode: +- fmt.Fprintf(wr.w, "%sBranchNode at %s\n", indent, at(x.Pos)) +- wr.writeNode(x.Pipe, indent+"Pipe. ") +- wr.writeNode(x.List, indent+"List. ") +- wr.writeNode(x.ElseList, indent+"Else. ") +- case *parse.ChainNode: +- fmt.Fprintf(wr.w, "%sChainNode at %s, %v\n", indent, at(x.Pos), x.Field) +- case *parse.CommandNode: +- fmt.Fprintf(wr.w, "%sCommandNode at %s, %d children\n", indent, at(x.Pos), len(x.Args)) +- for _, a := range x.Args { +- wr.writeNode(a, indent+". ") - } -- -- // How far need we search? -- // For package-level objects, we need only search the direct importers. -- // For fields and methods, we must search transitively. -- transitive := obj.Pkg().Scope().Lookup(obj.Name()) != obj -- -- // The scope is the union of rdeps of each variant. -- // (Each set is disjoint so there's no benefit to -- // combining the metadata graph traversals.) -- for _, m := range variants { -- if err := addRdeps(m.ID, transitive); err != nil { -- return nil, err -- } +- //case *parse.CommentNode: // 1.16 +- case *parse.DotNode: +- fmt.Fprintf(wr.w, "%sDotNode at %s\n", indent, at(x.Pos)) +- case *parse.FieldNode: +- fmt.Fprintf(wr.w, "%sFieldNode at %s, %v\n", indent, at(x.Pos), x.Ident) +- case *parse.IdentifierNode: +- fmt.Fprintf(wr.w, "%sIdentifierNode at %s, %v\n", indent, at(x.Pos), x.Ident) +- case *parse.IfNode: +- fmt.Fprintf(wr.w, "%sIfNode at %s\n", indent, at(x.Pos)) +- wr.writeNode(&x.BranchNode, indent+". ") +- case *parse.ListNode: +- if x == nil { +- return // nil BranchNode.ElseList - } -- -- // Is object a method? -- // -- // If so, expand the search so that the targets include -- // all methods that correspond to it through interface -- // satisfaction, and the scope includes the rdeps of -- // the package that declares each corresponding type. -- // -- // 'expansions' records the packages that declared -- // such types. -- if recv := effectiveReceiver(obj); recv != nil { -- if err := expandMethodSearch(ctx, snapshot, workspaceIDs, obj.(*types.Func), recv, addRdeps, globalTargets, expansions); err != nil { -- return nil, err -- } +- fmt.Fprintf(wr.w, "%sListNode at %s, %d children\n", indent, at(x.Pos), len(x.Nodes)) +- for _, n := range x.Nodes { +- wr.writeNode(n, indent+". ") - } -- } -- -- // The search functions will call report(loc) for each hit. -- var ( -- refsMu sync.Mutex -- refs []reference -- ) -- report := func(loc protocol.Location, isDecl bool) { -- ref := reference{ -- isDeclaration: isDecl, -- location: loc, -- pkgPath: pkg.Metadata().PkgPath, +- case *parse.NilNode: +- fmt.Fprintf(wr.w, "%sNilNode at %s\n", indent, at(x.Pos)) +- case *parse.NumberNode: +- fmt.Fprintf(wr.w, "%sNumberNode at %s, %s\n", indent, at(x.Pos), x.Text) +- case *parse.PipeNode: +- if x == nil { +- return // {{template "xxx"}} - } -- refsMu.Lock() -- refs = append(refs, ref) -- refsMu.Unlock() +- fmt.Fprintf(wr.w, "%sPipeNode at %s, %d vars, %d cmds, IsAssign:%v\n", +- indent, at(x.Pos), len(x.Decl), len(x.Cmds), x.IsAssign) +- for _, d := range x.Decl { +- wr.writeNode(d, indent+"Decl. ") +- } +- for _, c := range x.Cmds { +- wr.writeNode(c, indent+"Cmd. ") +- } +- case *parse.RangeNode: +- fmt.Fprintf(wr.w, "%sRangeNode at %s\n", indent, at(x.Pos)) +- wr.writeNode(&x.BranchNode, indent+". ") +- case *parse.StringNode: +- fmt.Fprintf(wr.w, "%sStringNode at %s, %s\n", indent, at(x.Pos), x.Quoted) +- case *parse.TemplateNode: +- fmt.Fprintf(wr.w, "%sTemplateNode at %s, %s\n", indent, at(x.Pos), x.Name) +- wr.writeNode(x.Pipe, indent+". ") +- case *parse.TextNode: +- fmt.Fprintf(wr.w, "%sTextNode at %s, len %d\n", indent, at(x.Pos), len(x.Text)) +- case *parse.VariableNode: +- fmt.Fprintf(wr.w, "%sVariableNode at %s, %v\n", indent, at(x.Pos), x.Ident) +- case *parse.WithNode: +- fmt.Fprintf(wr.w, "%sWithNode at %s\n", indent, at(x.Pos)) +- wr.writeNode(&x.BranchNode, indent+". ") - } +-} - -- // Loop over the variants of the declaring package, -- // and perform both the local (in-package) and global -- // (cross-package) searches, in parallel. -- // -- // TODO(adonovan): opt: support LSP reference streaming. See: -- // - https://github.com/microsoft/vscode-languageserver-node/pull/164 -- // - https://github.com/microsoft/language-server-protocol/pull/182 -- // -- // Careful: this goroutine must not return before group.Wait. -- var group errgroup.Group -- -- // Compute local references for each variant. -- // The target objects are identified by (URI, offset). -- for _, m := range variants { -- // We want the ordinary importable package, -- // plus any test-augmented variants, since -- // declarations in _test.go files may change -- // the reference of a selection, or even a -- // field into a method or vice versa. -- // -- // But we don't need intermediate test variants, -- // as their local references will be covered -- // already by other variants. -- if m.IsIntermediateTestVariant() { -- continue -- } -- m := m -- group.Go(func() error { -- // TODO(adonovan): opt: batch these TypeChecks. -- pkgs, err := snapshot.TypeCheck(ctx, m.ID) -- if err != nil { -- return err -- } -- pkg := pkgs[0] +-var kindNames = []string{"", "File", "Module", "Namespace", "Package", "Class", "Method", "Property", +- "Field", "Constructor", "Enum", "Interface", "Function", "Variable", "Constant", "String", +- "Number", "Boolean", "Array", "Object", "Key", "Null", "EnumMember", "Struct", "Event", +- "Operator", "TypeParameter"} - -- // Find the declaration of the corresponding -- // object in this package based on (URI, offset). -- pgf, err := pkg.File(declURI) -- if err != nil { -- return err -- } -- pos, err := safetoken.Pos(pgf.Tok, declPosn.Offset) -- if err != nil { -- return err -- } -- objects, _, err := objectsAt(pkg.GetTypesInfo(), pgf.File, pos) -- if err != nil { -- return err // unreachable? (probably caught earlier) -- } +-func kindStr(k protocol.SymbolKind) string { +- n := int(k) +- if n < 1 || n >= len(kindNames) { +- return fmt.Sprintf("?SymbolKind %d?", n) +- } +- return kindNames[n] +-} +diff -urN a/gopls/internal/template/parse_test.go b/gopls/internal/template/parse_test.go +--- a/gopls/internal/template/parse_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/template/parse_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,238 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- // Report the locations of the declaration(s). -- // TODO(adonovan): what about for corresponding methods? Add tests. -- for _, node := range objects { -- report(mustLocation(pgf, node), true) -- } +-package template - -- // Convert targets map to set. -- targets := make(map[types.Object]bool) -- for obj := range objects { -- targets[obj] = true -- } +-import ( +- "strings" +- "testing" +-) - -- return localReferences(pkg, targets, true, report) -- }) -- } +-type datum struct { +- buf string +- cnt int +- syms []string // the symbols in the parse of buf +-} - -- // Also compute local references within packages that declare -- // corresponding methods (see above), which expand the global search. -- // The target objects are identified by (PkgPath, objectpath). -- for id := range expansions { -- id := id -- group.Go(func() error { -- // TODO(adonovan): opt: batch these TypeChecks. -- pkgs, err := snapshot.TypeCheck(ctx, id) -- if err != nil { -- return err -- } -- pkg := pkgs[0] +-var tmpl = []datum{{` +-{{if (foo .X.Y)}}{{$A := "hi"}}{{.Z $A}}{{else}} +-{{$A.X 12}} +-{{foo (.X.Y) 23 ($A.Zü)}} +-{{end}}`, 1, []string{"{7,3,foo,Function,false}", "{12,1,X,Method,false}", +- "{14,1,Y,Method,false}", "{21,2,$A,Variable,true}", "{26,2,,String,false}", +- "{35,1,Z,Method,false}", "{38,2,$A,Variable,false}", +- "{53,2,$A,Variable,false}", "{56,1,X,Method,false}", "{57,2,,Number,false}", +- "{64,3,foo,Function,false}", "{70,1,X,Method,false}", +- "{72,1,Y,Method,false}", "{75,2,,Number,false}", "{80,2,$A,Variable,false}", +- "{83,2,Zü,Method,false}", "{94,3,,Constant,false}"}}, - -- targets := make(map[types.Object]bool) -- for objpath := range globalTargets[pkg.Metadata().PkgPath] { -- obj, err := objectpath.Object(pkg.GetTypes(), objpath) -- if err != nil { -- // No such object, because it was -- // declared only in the test variant. -- continue -- } -- targets[obj] = true -- } +- {`{{define "zzz"}}{{.}}{{end}} +-{{template "zzz"}}`, 2, []string{"{10,3,zzz,Namespace,true}", "{18,1,dot,Variable,false}", +- "{41,3,zzz,Package,false}"}}, - -- // Don't include corresponding types or methods -- // since expansions did that already, and we don't -- // want (e.g.) concrete -> interface -> concrete. -- const correspond = false -- return localReferences(pkg, targets, correspond, report) -- }) -- } +- {`{{block "aaa" foo}}b{{end}}`, 2, []string{"{9,3,aaa,Namespace,true}", +- "{9,3,aaa,Package,false}", "{14,3,foo,Function,false}", "{19,1,,Constant,false}"}}, +- {"", 0, nil}, +-} - -- // Compute global references for selected reverse dependencies. -- group.Go(func() error { -- var globalIDs []PackageID -- for id := range globalScope { -- globalIDs = append(globalIDs, id) +-func TestSymbols(t *testing.T) { +- for i, x := range tmpl { +- got := parseBuffer([]byte(x.buf)) +- if got.ParseErr != nil { +- t.Errorf("error:%v", got.ParseErr) +- continue - } -- indexes, err := snapshot.References(ctx, globalIDs...) -- if err != nil { -- return err +- if len(got.named) != x.cnt { +- t.Errorf("%d: got %d, expected %d", i, len(got.named), x.cnt) - } -- for _, index := range indexes { -- for _, loc := range index.Lookup(globalTargets) { -- report(loc, false) +- for n, s := range got.symbols { +- if s.String() != x.syms[n] { +- t.Errorf("%d: got %s, expected %s", i, s.String(), x.syms[n]) - } - } -- return nil -- }) -- -- if err := group.Wait(); err != nil { -- return nil, err - } -- return refs, nil -} - --// expandMethodSearch expands the scope and targets of a global search --// for an exported method to include all methods in the workspace --// that correspond to it through interface satisfaction. --// --// Each package that declares a corresponding type is added to --// expansions so that we can also find local references to the type --// within the package, which of course requires type checking. --// --// The scope is expanded by a sequence of calls (not concurrent) to addRdeps. --// --// recv is the method's effective receiver type, for method-set computations. --func expandMethodSearch(ctx context.Context, snapshot Snapshot, workspaceIDs []PackageID, method *types.Func, recv types.Type, addRdeps func(id PackageID, transitive bool) error, targets map[PackagePath]map[objectpath.Path]unit, expansions map[PackageID]unit) error { -- // Compute the method-set fingerprint used as a key to the global search. -- key, hasMethods := methodsets.KeyOf(recv) -- if !hasMethods { -- return bug.Errorf("KeyOf(%s)={} yet %s is a method", recv, method) -- } -- // Search the methodset index of each package in the workspace. -- indexes, err := snapshot.MethodSets(ctx, workspaceIDs...) -- if err != nil { -- return err -- } -- var mu sync.Mutex // guards addRdeps, targets, expansions -- var group errgroup.Group -- for i, index := range indexes { -- i := i -- index := index -- group.Go(func() error { -- // Consult index for matching methods. -- results := index.Search(key, method.Name()) -- if len(results) == 0 { -- return nil -- } -- -- // We have discovered one or more corresponding types. -- id := workspaceIDs[i] -- -- mu.Lock() -- defer mu.Unlock() -- -- // Expand global search scope to include rdeps of this pkg. -- if err := addRdeps(id, true); err != nil { -- return err -- } -- -- // Mark this package so that we search within it for -- // local references to the additional types/methods. -- expansions[id] = unit{} -- -- // Add each corresponding method the to set of global search targets. -- for _, res := range results { -- methodPkg := PackagePath(res.PkgPath) -- opaths, ok := targets[methodPkg] -- if !ok { -- opaths = make(map[objectpath.Path]unit) -- targets[methodPkg] = opaths -- } -- opaths[res.ObjectPath] = unit{} -- } -- return nil -- }) +-func TestWordAt(t *testing.T) { +- want := []string{"", "", "$A", "$A", "", "", "", "", "", "", +- "", "", "", "if", "if", "", "$A", "$A", "", "", +- "B", "", "", "end", "end", "end", "", "", ""} +- p := parseBuffer([]byte("{{$A := .}}{{if $A}}B{{end}}")) +- for i := 0; i < len(p.buf); i++ { +- got := findWordAt(p, i) +- if got != want[i] { +- t.Errorf("for %d, got %q, wanted %q", i, got, want[i]) +- } - } -- return group.Wait() -} - --// localReferences traverses syntax and reports each reference to one --// of the target objects, or (if correspond is set) an object that --// corresponds to one of them via interface satisfaction. --func localReferences(pkg Package, targets map[types.Object]bool, correspond bool, report func(loc protocol.Location, isDecl bool)) error { -- // If we're searching for references to a method optionally -- // broaden the search to include references to corresponding -- // methods of mutually assignable receiver types. -- // (We use a slice, but objectsAt never returns >1 methods.) -- var methodRecvs []types.Type -- var methodName string // name of an arbitrary target, iff a method -- if correspond { -- for obj := range targets { -- if t := effectiveReceiver(obj); t != nil { -- methodRecvs = append(methodRecvs, t) -- methodName = obj.Name() -- } +-func TestNLS(t *testing.T) { +- buf := `{{if (foÜx .X.Y)}}{{$A := "hi"}}{{.Z $A}}{{else}} +- {{$A.X 12}} +- {{foo (.X.Y) 23 ($A.Z)}} +- {{end}} +- ` +- p := parseBuffer([]byte(buf)) +- if p.ParseErr != nil { +- t.Fatal(p.ParseErr) +- } +- // line 0 doesn't have a \n in front of it +- for i := 1; i < len(p.nls)-1; i++ { +- if buf[p.nls[i]] != '\n' { +- t.Errorf("line %d got %c", i, buf[p.nls[i]]) - } - } +- // fake line at end of file +- if p.nls[len(p.nls)-1] != len(buf) { +- t.Errorf("got %d expected %d", p.nls[len(p.nls)-1], len(buf)) +- } +-} - -- // matches reports whether obj either is or corresponds to a target. -- // (Correspondence is defined as usual for interface methods.) -- matches := func(obj types.Object) bool { -- if containsOrigin(targets, obj) { -- return true +-func TestLineCol(t *testing.T) { +- buf := `{{if (foÜx .X.Y)}}{{$A := "hi"}}{{.Z $A}}{{else}} +- {{$A.X 12}} +- {{foo (.X.Y) 23 ($A.Z)}} +- {{end}}` +- if false { +- t.Error(buf) +- } +- for n, cx := range tmpl { +- buf := cx.buf +- p := parseBuffer([]byte(buf)) +- if p.ParseErr != nil { +- t.Fatal(p.ParseErr) - } -- if methodRecvs != nil && obj.Name() == methodName { -- if orecv := effectiveReceiver(obj); orecv != nil { -- for _, mrecv := range methodRecvs { -- if concreteImplementsIntf(orecv, mrecv) { -- return true -- } +- type loc struct { +- offset int +- l, c uint32 +- } +- saved := []loc{} +- // forwards +- var lastl, lastc uint32 +- for offset := range buf { +- l, c := p.LineCol(offset) +- saved = append(saved, loc{offset, l, c}) +- if l > lastl { +- lastl = l +- if c != 0 { +- t.Errorf("line %d, got %d instead of 0", l, c) - } - } +- if c > lastc { +- lastc = c +- } - } -- return false -- } -- -- // Scan through syntax looking for uses of one of the target objects. -- for _, pgf := range pkg.CompiledGoFiles() { -- ast.Inspect(pgf.File, func(n ast.Node) bool { -- if id, ok := n.(*ast.Ident); ok { -- if obj, ok := pkg.GetTypesInfo().Uses[id]; ok && matches(obj) { -- report(mustLocation(pgf, id), false) -- } +- lines := strings.Split(buf, "\n") +- mxlen := -1 +- for _, l := range lines { +- if len(l) > mxlen { +- mxlen = len(l) - } -- return true -- }) +- } +- if int(lastl) != len(lines)-1 && int(lastc) != mxlen { +- // lastl is 0 if there is only 1 line(?) +- t.Errorf("expected %d, %d, got %d, %d for case %d", len(lines)-1, mxlen, lastl, lastc, n) +- } +- // backwards +- for j := len(saved) - 1; j >= 0; j-- { +- s := saved[j] +- xl, xc := p.LineCol(s.offset) +- if xl != s.l || xc != s.c { +- t.Errorf("at offset %d(%d), got (%d,%d), expected (%d,%d)", s.offset, j, xl, xc, s.l, s.c) +- } +- } - } -- return nil -} - --// effectiveReceiver returns the effective receiver type for method-set --// comparisons for obj, if it is a method, or nil otherwise. --func effectiveReceiver(obj types.Object) types.Type { -- if fn, ok := obj.(*types.Func); ok { -- if recv := fn.Type().(*types.Signature).Recv(); recv != nil { -- return methodsets.EnsurePointer(recv.Type()) +-func TestLineColNL(t *testing.T) { +- buf := "\n\n\n\n\n" +- p := parseBuffer([]byte(buf)) +- if p.ParseErr != nil { +- t.Fatal(p.ParseErr) +- } +- for i := 0; i < len(buf); i++ { +- l, c := p.LineCol(i) +- if c != 0 || int(l) != i+1 { +- t.Errorf("got (%d,%d), expected (%d,0)", l, c, i) - } - } -- return nil -} - --// objectsAt returns the non-empty set of objects denoted (def or use) --// by the specified position within a file syntax tree, or an error if --// none were found. --// --// The result may contain more than one element because all case --// variables of a type switch appear to be declared at the same --// position. --// --// Each object is mapped to the syntax node that was treated as an --// identifier, which is not always an ast.Ident. The second component --// of the result is the innermost node enclosing pos. --// --// TODO(adonovan): factor in common with referencedObject. --func objectsAt(info *types.Info, file *ast.File, pos token.Pos) (map[types.Object]ast.Node, ast.Node, error) { -- path := pathEnclosingObjNode(file, pos) -- if path == nil { -- return nil, nil, ErrNoIdentFound +-func TestPos(t *testing.T) { +- buf := ` +- {{if (foÜx .X.Y)}}{{$A := "hi"}}{{.Z $A}}{{else}} +- {{$A.X 12}} +- {{foo (.X.Y) 23 ($A.Z)}} +- {{end}}` +- p := parseBuffer([]byte(buf)) +- if p.ParseErr != nil { +- t.Fatal(p.ParseErr) - } -- -- targets := make(map[types.Object]ast.Node) -- -- switch leaf := path[0].(type) { -- case *ast.Ident: -- // If leaf represents an implicit type switch object or the type -- // switch "assign" variable, expand to all of the type switch's -- // implicit objects. -- if implicits, _ := typeSwitchImplicits(info, path); len(implicits) > 0 { -- for _, obj := range implicits { -- targets[obj] = leaf -- } -- } else { -- // Note: prior to go1.21, go/types issue #60372 causes the position -- // a field Var T created for struct{*p.T} to be recorded at the -- // start of the field type ("*") not the location of the T. -- // This affects references and other gopls operations (issue #60369). -- // TODO(adonovan): delete this comment when we drop support for go1.20. -- -- // For struct{T}, we prefer the defined field Var over the used TypeName. -- obj := info.ObjectOf(leaf) -- if obj == nil { -- return nil, nil, fmt.Errorf("%w for %q", errNoObjectFound, leaf.Name) -- } -- targets[obj] = leaf +- for pos, r := range buf { +- if r == '\n' { +- continue - } -- case *ast.ImportSpec: -- // Look up the implicit *types.PkgName. -- obj := info.Implicits[leaf] -- if obj == nil { -- return nil, nil, fmt.Errorf("%w for import %s", errNoObjectFound, UnquoteImportPath(leaf)) +- x := p.Position(pos) +- n := p.FromPosition(x) +- if n != pos { +- // once it's wrong, it will be wrong forever +- t.Fatalf("at pos %d (rune %c) got %d {%#v]", pos, r, n, x) +- } +- +- } +-} +-func TestLen(t *testing.T) { +- data := []struct { +- cnt int +- v string +- }{{1, "a"}, {1, "膈"}, {4, "😆🥸"}, {7, "3😀4567"}} +- p := &Parsed{nonASCII: true} +- for _, d := range data { +- got := p.utf16len([]byte(d.v)) +- if got != d.cnt { +- t.Errorf("%v, got %d wanted %d", d, got, d.cnt) - } -- targets[obj] = leaf - } +-} - -- if len(targets) == 0 { -- return nil, nil, fmt.Errorf("objectAt: internal error: no targets") // can't happen +-func TestUtf16(t *testing.T) { +- buf := ` +- {{if (foÜx .X.Y)}}😀{{$A := "hi"}}{{.Z $A}}{{else}} +- {{$A.X 12}} +- {{foo (.X.Y) 23 ($A.Z)}} +- {{end}}` +- p := parseBuffer([]byte(buf)) +- if p.nonASCII == false { +- t.Error("expected nonASCII to be true") - } -- return targets, path[0], nil -} - --// mustLocation reports the location interval a syntax node, --// which must belong to m.File. --// --// Safe for use only by references and implementations. --func mustLocation(pgf *ParsedGoFile, n ast.Node) protocol.Location { -- loc, err := pgf.NodeLocation(n) -- if err != nil { -- panic(err) // can't happen in references or implementations +-type ttest struct { +- tmpl string +- tokCnt int +- elidedCnt int8 +-} +- +-func TestQuotes(t *testing.T) { +- tsts := []ttest{ +- {"{{- /*comment*/ -}}", 1, 0}, +- {"{{/*`\ncomment\n`*/}}", 1, 0}, +- //{"{{foo\nbar}}\n", 1, 0}, // this action spanning lines parses in 1.16 +- {"{{\"{{foo}}{{\"}}", 1, 0}, +- {"{{\n{{- when}}", 1, 1}, // corrected +- {"{{{{if .}}xx{{\n{{end}}", 2, 2}, // corrected +- } +- for _, s := range tsts { +- p := parseBuffer([]byte(s.tmpl)) +- if len(p.tokens) != s.tokCnt { +- t.Errorf("%q: got %d tokens, expected %d", s, len(p.tokens), s.tokCnt) +- } +- if p.ParseErr != nil { +- t.Errorf("%q: %v", string(p.buf), p.ParseErr) +- } +- if len(p.elided) != int(s.elidedCnt) { +- t.Errorf("%q: elided %d, expected %d", s, len(p.elided), s.elidedCnt) +- } - } -- return loc -} -diff -urN a/gopls/internal/lsp/source/rename_check.go b/gopls/internal/lsp/source/rename_check.go ---- a/gopls/internal/lsp/source/rename_check.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/rename_check.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,921 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/template/symbols.go b/gopls/internal/template/symbols.go +--- a/gopls/internal/template/symbols.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/template/symbols.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,231 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. --// --// Taken from golang.org/x/tools/refactor/rename. -- --package source - --// This file defines the conflict-checking portion of the rename operation. --// --// The renamer works on a single package of type-checked syntax, and --// is called in parallel for all necessary packages in the workspace, --// possibly up to the transitive reverse dependencies of the --// declaration. Finally the union of all edits and errors is computed. --// --// Renaming one object may entail renaming of others. For example: --// --// - An embedded field couples a Var (field) and a TypeName. --// So, renaming either one requires renaming the other. --// If the initial object is an embedded field, we must add its --// TypeName (and its enclosing package) to the renaming set; --// this is easily discovered at the outset. --// --// Conversely, if the initial object is a TypeName, we must observe --// whether any of its references (from directly importing packages) --// is coincident with an embedded field Var and, if so, initiate a --// renaming of it. --// --// - A method of an interface type is coupled to all corresponding --// methods of types that are assigned to the interface (as --// discovered by the 'satisfy' pass). As a matter of usability, we --// require that such renamings be initiated from the interface --// method, not the concrete method. +-package template - -import ( +- "bytes" +- "context" - "fmt" -- "go/ast" -- "go/token" -- "go/types" -- "path/filepath" -- "reflect" -- "strings" -- "unicode" +- "text/template/parse" +- "unicode/utf8" - -- "golang.org/x/tools/go/ast/astutil" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/refactor/satisfy" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/event" -) - --// errorf reports an error (e.g. conflict) and prevents file modification. --func (r *renamer) errorf(pos token.Pos, format string, args ...interface{}) { -- // Conflict error messages in the old gorename tool (whence this -- // logic originated) contain rich information associated with -- // multiple source lines, such as: -- // -- // p/a.go:1:2: renaming "x" to "y" here -- // p/b.go:3:4: \t would cause this reference to "y" -- // p/c.go:5:5: \t to become shadowed by this intervening declaration. -- // -- // Unfortunately LSP provides no means to transmit the -- // structure of this error, so we format the positions briefly -- // using dir/file.go where dir is the base name of the parent -- // directory. +-// in local coordinates, to be translated to protocol.DocumentSymbol +-type symbol struct { +- start int // for sorting +- length int // in runes (unicode code points) +- name string +- kind protocol.SymbolKind +- vardef bool // is this a variable definition? +- // do we care about selection range, or children? +- // no children yet, and selection range is the same as range +-} - -- var conflict strings.Builder +-func (s symbol) String() string { +- return fmt.Sprintf("{%d,%d,%s,%s,%v}", s.start, s.length, s.name, s.kind, s.vardef) +-} - -- // Add prefix of (truncated) position. -- if pos != token.NoPos { -- // TODO(adonovan): skip position of first error if it is -- // on the same line as the renaming itself. -- posn := safetoken.StartPosition(r.pkg.FileSet(), pos).String() -- segments := strings.Split(filepath.ToSlash(posn), "/") -- if n := len(segments); n > 2 { -- segments = segments[n-2:] +-// for FieldNode or VariableNode (or ChainNode?) +-func (p *Parsed) fields(flds []string, x parse.Node) []symbol { +- ans := []symbol{} +- // guessing that there are no embedded blanks allowed. The doc is unclear +- lookfor := "" +- switch x.(type) { +- case *parse.FieldNode: +- for _, f := range flds { +- lookfor += "." + f // quadratic, but probably ok - } -- posn = strings.Join(segments, "/") -- fmt.Fprintf(&conflict, "%s:", posn) -- -- if !strings.HasPrefix(format, "\t") { -- conflict.WriteByte(' ') +- case *parse.VariableNode: +- lookfor = flds[0] +- for i := 1; i < len(flds); i++ { +- lookfor += "." + flds[i] +- } +- case *parse.ChainNode: // PJW, what are these? +- for _, f := range flds { +- lookfor += "." + f // quadratic, but probably ok - } +- default: +- // If these happen they will happen even if gopls is restarted +- // and the users does the same thing, so it is better not to panic. +- // context.Background() is used because we don't have access +- // to any other context. [we could, but it would be complicated] +- event.Log(context.Background(), fmt.Sprintf("%T unexpected in fields()", x)) +- return nil - } -- -- fmt.Fprintf(&conflict, format, args...) -- r.conflicts = append(r.conflicts, conflict.String()) +- if len(lookfor) == 0 { +- event.Log(context.Background(), fmt.Sprintf("no strings in fields() %#v", x)) +- return nil +- } +- startsAt := int(x.Position()) +- ix := bytes.Index(p.buf[startsAt:], []byte(lookfor)) // HasPrefix? PJW? +- if ix < 0 || ix > len(lookfor) { // lookfor expected to be at start (or so) +- // probably golang.go/#43388, so back up +- startsAt -= len(flds[0]) + 1 +- ix = bytes.Index(p.buf[startsAt:], []byte(lookfor)) // ix might be 1? PJW +- if ix < 0 { +- return ans +- } +- } +- at := ix + startsAt +- for _, f := range flds { +- at += 1 // . +- kind := protocol.Method +- if f[0] == '$' { +- kind = protocol.Variable +- } +- sym := symbol{name: f, kind: kind, start: at, length: utf8.RuneCount([]byte(f))} +- if kind == protocol.Variable && len(p.stack) > 1 { +- if pipe, ok := p.stack[len(p.stack)-2].(*parse.PipeNode); ok { +- for _, y := range pipe.Decl { +- if x == y { +- sym.vardef = true +- } +- } +- } +- } +- ans = append(ans, sym) +- at += len(f) +- } +- return ans -} - --// check performs safety checks of the renaming of the 'from' object to r.to. --func (r *renamer) check(from types.Object) { -- if r.objsToUpdate[from] { +-func (p *Parsed) findSymbols() { +- if len(p.stack) == 0 { - return - } -- r.objsToUpdate[from] = true +- n := p.stack[len(p.stack)-1] +- pop := func() { +- p.stack = p.stack[:len(p.stack)-1] +- } +- if n == nil { // allowing nil simplifies the code +- pop() +- return +- } +- nxt := func(nd parse.Node) { +- p.stack = append(p.stack, nd) +- p.findSymbols() +- } +- switch x := n.(type) { +- case *parse.ActionNode: +- nxt(x.Pipe) +- case *parse.BoolNode: +- // need to compute the length from the value +- msg := fmt.Sprintf("%v", x.True) +- p.symbols = append(p.symbols, symbol{start: int(x.Pos), length: len(msg), kind: protocol.Boolean}) +- case *parse.BranchNode: +- nxt(x.Pipe) +- nxt(x.List) +- nxt(x.ElseList) +- case *parse.ChainNode: +- p.symbols = append(p.symbols, p.fields(x.Field, x)...) +- nxt(x.Node) +- case *parse.CommandNode: +- for _, a := range x.Args { +- nxt(a) +- } +- //case *parse.CommentNode: // go 1.16 +- // log.Printf("implement %d", x.Type()) +- case *parse.DotNode: +- sym := symbol{name: "dot", kind: protocol.Variable, start: int(x.Pos), length: 1} +- p.symbols = append(p.symbols, sym) +- case *parse.FieldNode: +- p.symbols = append(p.symbols, p.fields(x.Ident, x)...) +- case *parse.IdentifierNode: +- sym := symbol{name: x.Ident, kind: protocol.Function, start: int(x.Pos), +- length: utf8.RuneCount([]byte(x.Ident))} +- p.symbols = append(p.symbols, sym) +- case *parse.IfNode: +- nxt(&x.BranchNode) +- case *parse.ListNode: +- if x != nil { // wretched typed nils. Node should have an IfNil +- for _, nd := range x.Nodes { +- nxt(nd) +- } +- } +- case *parse.NilNode: +- sym := symbol{name: "nil", kind: protocol.Constant, start: int(x.Pos), length: 3} +- p.symbols = append(p.symbols, sym) +- case *parse.NumberNode: +- // no name; ascii +- p.symbols = append(p.symbols, symbol{start: int(x.Pos), length: len(x.Text), kind: protocol.Number}) +- case *parse.PipeNode: +- if x == nil { // {{template "foo"}} +- return +- } +- for _, d := range x.Decl { +- nxt(d) +- } +- for _, c := range x.Cmds { +- nxt(c) +- } +- case *parse.RangeNode: +- nxt(&x.BranchNode) +- case *parse.StringNode: +- // no name +- sz := utf8.RuneCount([]byte(x.Text)) +- p.symbols = append(p.symbols, symbol{start: int(x.Pos), length: sz, kind: protocol.String}) +- case *parse.TemplateNode: // invoking a template +- // x.Pos points to the quote before the name +- p.symbols = append(p.symbols, symbol{name: x.Name, kind: protocol.Package, start: int(x.Pos) + 1, +- length: utf8.RuneCount([]byte(x.Name))}) +- nxt(x.Pipe) +- case *parse.TextNode: +- if len(x.Text) == 1 && x.Text[0] == '\n' { +- break +- } +- // nothing to report, but build one for hover +- sz := utf8.RuneCount(x.Text) +- p.symbols = append(p.symbols, symbol{start: int(x.Pos), length: sz, kind: protocol.Constant}) +- case *parse.VariableNode: +- p.symbols = append(p.symbols, p.fields(x.Ident, x)...) +- case *parse.WithNode: +- nxt(&x.BranchNode) - -- // NB: order of conditions is important. -- if from_, ok := from.(*types.PkgName); ok { -- r.checkInFileBlock(from_) -- } else if from_, ok := from.(*types.Label); ok { -- r.checkLabel(from_) -- } else if isPackageLevel(from) { -- r.checkInPackageBlock(from) -- } else if v, ok := from.(*types.Var); ok && v.IsField() { -- r.checkStructField(v) -- } else if f, ok := from.(*types.Func); ok && recv(f) != nil { -- r.checkMethod(f) -- } else if isLocal(from) { -- r.checkInLexicalScope(from) -- } else { -- r.errorf(from.Pos(), "unexpected %s object %q (please report a bug)\n", -- objectKind(from), from) - } +- pop() +-} +- +-// DocumentSymbols returns a hierarchy of the symbols defined in a template file. +-// (The hierarchy is flat. SymbolInformation might be better.) +-func DocumentSymbols(snapshot *cache.Snapshot, fh file.Handle) ([]protocol.DocumentSymbol, error) { +- buf, err := fh.Content() +- if err != nil { +- return nil, err +- } +- p := parseBuffer(buf) +- if p.ParseErr != nil { +- return nil, p.ParseErr +- } +- var ans []protocol.DocumentSymbol +- for _, s := range p.symbols { +- if s.kind == protocol.Constant { +- continue +- } +- d := kindStr(s.kind) +- if d == "Namespace" { +- d = "Template" +- } +- if s.vardef { +- d += "(def)" +- } else { +- d += "(use)" +- } +- r := p.Range(s.start, s.length) +- y := protocol.DocumentSymbol{ +- Name: s.name, +- Detail: d, +- Kind: s.kind, +- Range: r, +- SelectionRange: r, // or should this be the entire {{...}}? +- } +- ans = append(ans, y) +- } +- return ans, nil -} +diff -urN a/gopls/internal/test/compare/text.go b/gopls/internal/test/compare/text.go +--- a/gopls/internal/test/compare/text.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/compare/text.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,49 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// checkInFileBlock performs safety checks for renames of objects in the file block, --// i.e. imported package names. --func (r *renamer) checkInFileBlock(from *types.PkgName) { -- // Check import name is not "init". -- if r.to == "init" { -- r.errorf(from.Pos(), "%q is not a valid imported package name", r.to) -- } +-package compare - -- // Check for conflicts between file and package block. -- if prev := from.Pkg().Scope().Lookup(r.to); prev != nil { -- r.errorf(from.Pos(), "renaming this %s %q to %q would conflict", -- objectKind(from), from.Name(), r.to) -- r.errorf(prev.Pos(), "\twith this package member %s", -- objectKind(prev)) -- return // since checkInPackageBlock would report redundant errors -- } +-import ( +- "bytes" - -- // Check for conflicts in lexical scope. -- r.checkInLexicalScope(from) +- "golang.org/x/tools/internal/diff" +-) +- +-// Text returns a formatted unified diff of the edits to go from want to +-// got, returning "" if and only if want == got. +-// +-// This function is intended for use in testing, and panics if any error occurs +-// while computing the diff. It is not sufficiently tested for production use. +-func Text(want, got string) string { +- return NamedText("want", "got", want, got) -} - --// checkInPackageBlock performs safety checks for renames of --// func/var/const/type objects in the package block. --func (r *renamer) checkInPackageBlock(from types.Object) { -- // Check that there are no references to the name from another -- // package if the renaming would make it unexported. -- if typ := r.pkg.GetTypes(); typ != from.Pkg() && ast.IsExported(r.from) && !ast.IsExported(r.to) { -- if id := someUse(r.pkg.GetTypesInfo(), from); id != nil { -- r.checkExport(id, typ, from) -- } +-// NamedText is like text, but allows passing custom names of the 'want' and +-// 'got' content. +-func NamedText(wantName, gotName, want, got string) string { +- if want == got { +- return "" - } - -- // Check that in the package block, "init" is a function, and never referenced. -- if r.to == "init" { -- kind := objectKind(from) -- if kind == "func" { -- // Reject if intra-package references to it exist. -- for id, obj := range r.pkg.GetTypesInfo().Uses { -- if obj == from { -- r.errorf(from.Pos(), -- "renaming this func %q to %q would make it a package initializer", -- from.Name(), r.to) -- r.errorf(id.Pos(), "\tbut references to it exist") -- break -- } -- } -- } else { -- r.errorf(from.Pos(), "you cannot have a %s at package level named %q", -- kind, r.to) -- } -- } +- // Add newlines to avoid verbose newline messages ("No newline at end of file"). +- unified := diff.Unified(wantName, gotName, want+"\n", got+"\n") - -- // Check for conflicts between package block and all file blocks. -- for _, f := range r.pkg.GetSyntax() { -- fileScope := r.pkg.GetTypesInfo().Scopes[f] -- b, prev := fileScope.LookupParent(r.to, token.NoPos) -- if b == fileScope { -- r.errorf(from.Pos(), "renaming this %s %q to %q would conflict", objectKind(from), from.Name(), r.to) -- var prevPos token.Pos -- if prev != nil { -- prevPos = prev.Pos() -- } -- r.errorf(prevPos, "\twith this %s", objectKind(prev)) -- return // since checkInPackageBlock would report redundant errors -- } +- // Defensively assert that we get an actual diff, so that we guarantee the +- // invariant that we return "" if and only if want == got. +- // +- // This is probably unnecessary, but convenient. +- if unified == "" { +- panic("empty diff for non-identical input") - } - -- // Check for conflicts in lexical scope. -- r.checkInLexicalScope(from) +- return unified -} - --// checkInLexicalScope performs safety checks that a renaming does not --// change the lexical reference structure of the specified package. --// --// For objects in lexical scope, there are three kinds of conflicts: --// same-, sub-, and super-block conflicts. We will illustrate all three --// using this example: --// --// var x int --// var z int --// --// func f(y int) { --// print(x) --// print(y) --// } --// --// Renaming x to z encounters a "same-block conflict", because an object --// with the new name already exists, defined in the same lexical block --// as the old object. --// --// Renaming x to y encounters a "sub-block conflict", because there exists --// a reference to x from within (what would become) a hole in its scope. --// The definition of y in an (inner) sub-block would cast a shadow in --// the scope of the renamed variable. --// --// Renaming y to x encounters a "super-block conflict". This is the --// converse situation: there is an existing definition of the new name --// (x) in an (enclosing) super-block, and the renaming would create a --// hole in its scope, within which there exist references to it. The --// new name shadows the existing definition of x in the super-block. --// --// Removing the old name (and all references to it) is always safe, and --// requires no checks. --func (r *renamer) checkInLexicalScope(from types.Object) { -- b := from.Parent() // the block defining the 'from' object -- if b != nil { -- toBlock, to := b.LookupParent(r.to, from.Parent().End()) -- if toBlock == b { -- // same-block conflict -- r.errorf(from.Pos(), "renaming this %s %q to %q", -- objectKind(from), from.Name(), r.to) -- r.errorf(to.Pos(), "\tconflicts with %s in same block", -- objectKind(to)) -- return -- } else if toBlock != nil { -- // Check for super-block conflict. -- // The name r.to is defined in a superblock. -- // Is that name referenced from within this block? -- forEachLexicalRef(r.pkg, to, func(id *ast.Ident, block *types.Scope) bool { -- _, obj := block.LookupParent(from.Name(), id.Pos()) -- if obj == from { -- // super-block conflict -- r.errorf(from.Pos(), "renaming this %s %q to %q", -- objectKind(from), from.Name(), r.to) -- r.errorf(id.Pos(), "\twould shadow this reference") -- r.errorf(to.Pos(), "\tto the %s declared here", -- objectKind(to)) -- return false // stop -- } -- return true -- }) -- } +-// Bytes is like Text but using byte slices. +-func Bytes(want, got []byte) string { +- if bytes.Equal(want, got) { +- return "" // common case - } -- // Check for sub-block conflict. -- // Is there an intervening definition of r.to between -- // the block defining 'from' and some reference to it? -- forEachLexicalRef(r.pkg, from, func(id *ast.Ident, block *types.Scope) bool { -- // Find the block that defines the found reference. -- // It may be an ancestor. -- fromBlock, _ := block.LookupParent(from.Name(), id.Pos()) -- // See what r.to would resolve to in the same scope. -- toBlock, to := block.LookupParent(r.to, id.Pos()) -- if to != nil { -- // sub-block conflict -- if deeper(toBlock, fromBlock) { -- r.errorf(from.Pos(), "renaming this %s %q to %q", -- objectKind(from), from.Name(), r.to) -- r.errorf(id.Pos(), "\twould cause this reference to become shadowed") -- r.errorf(to.Pos(), "\tby this intervening %s definition", -- objectKind(to)) -- return false // stop -- } -- } -- return true -- }) +- return Text(string(want), string(got)) +-} +diff -urN a/gopls/internal/test/compare/text_test.go b/gopls/internal/test/compare/text_test.go +--- a/gopls/internal/test/compare/text_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/compare/text_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,28 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- // Renaming a type that is used as an embedded field -- // requires renaming the field too. e.g. -- // type T int // if we rename this to U.. -- // var s struct {T} -- // print(s.T) // ...this must change too -- if _, ok := from.(*types.TypeName); ok { -- for id, obj := range r.pkg.GetTypesInfo().Uses { -- if obj == from { -- if field := r.pkg.GetTypesInfo().Defs[id]; field != nil { -- r.check(field) -- } -- } -- } +-package compare_test +- +-import ( +- "testing" +- +- "golang.org/x/tools/gopls/internal/test/compare" +-) +- +-func TestText(t *testing.T) { +- tests := []struct { +- got, want, wantDiff string +- }{ +- {"", "", ""}, +- {"equal", "equal", ""}, +- {"a", "b", "--- want\n+++ got\n@@ -1 +1 @@\n-b\n+a\n"}, +- {"a\nd\nc\n", "a\nb\nc\n", "--- want\n+++ got\n@@ -1,4 +1,4 @@\n a\n-b\n+d\n c\n \n"}, - } --} - --// deeper reports whether block x is lexically deeper than y. --func deeper(x, y *types.Scope) bool { -- if x == y || x == nil { -- return false -- } else if y == nil { -- return true -- } else { -- return deeper(x.Parent(), y.Parent()) +- for _, test := range tests { +- if gotDiff := compare.Text(test.want, test.got); gotDiff != test.wantDiff { +- t.Errorf("compare.Text(%q, %q) =\n%q, want\n%q", test.want, test.got, gotDiff, test.wantDiff) +- } - } -} +diff -urN a/gopls/internal/test/integration/bench/bench_test.go b/gopls/internal/test/integration/bench/bench_test.go +--- a/gopls/internal/test/integration/bench/bench_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/bench/bench_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,351 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// forEachLexicalRef calls fn(id, block) for each identifier id in package --// pkg that is a reference to obj in lexical scope. block is the --// lexical block enclosing the reference. If fn returns false the --// iteration is terminated and findLexicalRefs returns false. --func forEachLexicalRef(pkg Package, obj types.Object, fn func(id *ast.Ident, block *types.Scope) bool) bool { -- ok := true -- var stack []ast.Node +-package bench - -- var visit func(n ast.Node) bool -- visit = func(n ast.Node) bool { -- if n == nil { -- stack = stack[:len(stack)-1] // pop -- return false -- } -- if !ok { -- return false // bail out -- } +-import ( +- "bytes" +- "compress/gzip" +- "context" +- "flag" +- "fmt" +- "io" +- "log" +- "os" +- "os/exec" +- "path/filepath" +- "strings" +- "sync" +- "testing" +- "time" - -- stack = append(stack, n) // push -- switch n := n.(type) { -- case *ast.Ident: -- if pkg.GetTypesInfo().Uses[n] == obj { -- block := enclosingBlock(pkg.GetTypesInfo(), stack) -- if !fn(n, block) { -- ok = false -- } -- } -- return visit(nil) // pop stack +- "golang.org/x/tools/gopls/internal/cmd" +- "golang.org/x/tools/gopls/internal/hooks" +- "golang.org/x/tools/gopls/internal/protocol/command" +- "golang.org/x/tools/gopls/internal/test/integration" +- "golang.org/x/tools/gopls/internal/test/integration/fake" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/internal/fakenet" +- "golang.org/x/tools/internal/jsonrpc2" +- "golang.org/x/tools/internal/jsonrpc2/servertest" +- "golang.org/x/tools/internal/pprof" +- "golang.org/x/tools/internal/tool" +-) - -- case *ast.SelectorExpr: -- // don't visit n.Sel -- ast.Inspect(n.X, visit) -- return visit(nil) // pop stack, don't descend +-var ( +- goplsPath = flag.String("gopls_path", "", "if set, use this gopls for testing; incompatible with -gopls_commit") - -- case *ast.CompositeLit: -- // Handle recursion ourselves for struct literals -- // so we don't visit field identifiers. -- tv, ok := pkg.GetTypesInfo().Types[n] -- if !ok { -- return visit(nil) // pop stack, don't descend -- } -- if _, ok := Deref(tv.Type).Underlying().(*types.Struct); ok { -- if n.Type != nil { -- ast.Inspect(n.Type, visit) -- } -- for _, elt := range n.Elts { -- if kv, ok := elt.(*ast.KeyValueExpr); ok { -- ast.Inspect(kv.Value, visit) -- } else { -- ast.Inspect(elt, visit) -- } -- } -- return visit(nil) // pop stack, don't descend -- } -- } -- return true -- } +- installGoplsOnce sync.Once // guards installing gopls at -gopls_commit +- goplsCommit = flag.String("gopls_commit", "", "if set, install and use gopls at this commit for testing; incompatible with -gopls_path") - -- for _, f := range pkg.GetSyntax() { -- ast.Inspect(f, visit) -- if len(stack) != 0 { -- panic(stack) -- } -- if !ok { -- break +- cpuProfile = flag.String("gopls_cpuprofile", "", "if set, the cpu profile file suffix; see \"Profiling\" in the package doc") +- memProfile = flag.String("gopls_memprofile", "", "if set, the mem profile file suffix; see \"Profiling\" in the package doc") +- allocProfile = flag.String("gopls_allocprofile", "", "if set, the alloc profile file suffix; see \"Profiling\" in the package doc") +- trace = flag.String("gopls_trace", "", "if set, the trace file suffix; see \"Profiling\" in the package doc") +- +- // If non-empty, tempDir is a temporary working dir that was created by this +- // test suite. +- makeTempDirOnce sync.Once // guards creation of the temp dir +- tempDir string +-) +- +-// if runAsGopls is "true", run the gopls command instead of the testing.M. +-const runAsGopls = "_GOPLS_BENCH_RUN_AS_GOPLS" +- +-func TestMain(m *testing.M) { +- bug.PanicOnBugs = true +- if os.Getenv(runAsGopls) == "true" { +- tool.Main(context.Background(), cmd.New(hooks.Options), os.Args[1:]) +- os.Exit(0) +- } +- event.SetExporter(nil) // don't log to stderr +- code := m.Run() +- if err := cleanup(); err != nil { +- fmt.Fprintf(os.Stderr, "cleaning up after benchmarks: %v\n", err) +- if code == 0 { +- code = 1 - } - } -- return ok +- os.Exit(code) -} - --// enclosingBlock returns the innermost block enclosing the specified --// AST node, specified in the form of a path from the root of the file, --// [file...n]. --func enclosingBlock(info *types.Info, stack []ast.Node) *types.Scope { -- for i := range stack { -- n := stack[len(stack)-1-i] -- // For some reason, go/types always associates a -- // function's scope with its FuncType. -- // TODO(adonovan): feature or a bug? -- switch f := n.(type) { -- case *ast.FuncDecl: -- n = f.Type -- case *ast.FuncLit: -- n = f.Type -- } -- if b := info.Scopes[n]; b != nil { -- return b +-// getTempDir returns the temporary directory to use for benchmark files, +-// creating it if necessary. +-func getTempDir() string { +- makeTempDirOnce.Do(func() { +- var err error +- tempDir, err = os.MkdirTemp("", "gopls-bench") +- if err != nil { +- log.Fatal(err) - } -- } -- panic("no Scope for *ast.File") +- }) +- return tempDir -} - --func (r *renamer) checkLabel(label *types.Label) { -- // Check there are no identical labels in the function's label block. -- // (Label blocks don't nest, so this is easy.) -- if prev := label.Parent().Lookup(r.to); prev != nil { -- r.errorf(label.Pos(), "renaming this label %q to %q", label.Name(), prev.Name()) -- r.errorf(prev.Pos(), "\twould conflict with this one") +-// shallowClone performs a shallow clone of repo into dir at the given +-// 'commitish' ref (any commit reference understood by git). +-// +-// The directory dir must not already exist. +-func shallowClone(dir, repo, commitish string) error { +- if err := os.Mkdir(dir, 0750); err != nil { +- return fmt.Errorf("creating dir for %s: %v", repo, err) +- } +- +- // Set a timeout for git fetch. If this proves flaky, it can be removed. +- ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) +- defer cancel() +- +- // Use a shallow fetch to download just the relevant commit. +- shInit := fmt.Sprintf("git init && git fetch --depth=1 %q %q && git checkout FETCH_HEAD", repo, commitish) +- initCmd := exec.CommandContext(ctx, "/bin/sh", "-c", shInit) +- initCmd.Dir = dir +- if output, err := initCmd.CombinedOutput(); err != nil { +- return fmt.Errorf("checking out %s: %v\n%s", repo, err, output) - } +- return nil -} - --// checkStructField checks that the field renaming will not cause --// conflicts at its declaration, or ambiguity or changes to any selection. --func (r *renamer) checkStructField(from *types.Var) { +-// connectEditor connects a fake editor session in the given dir, using the +-// given editor config. +-func connectEditor(dir string, config fake.EditorConfig, ts servertest.Connector) (*fake.Sandbox, *fake.Editor, *integration.Awaiter, error) { +- s, err := fake.NewSandbox(&fake.SandboxConfig{ +- Workdir: dir, +- GOPROXY: "https://proxy.golang.org", +- }) +- if err != nil { +- return nil, nil, nil, err +- } - -- // If this is the declaring package, check that the struct -- // declaration is free of field conflicts, and field/method -- // conflicts. -- // -- // go/types offers no easy way to get from a field (or interface -- // method) to its declaring struct (or interface), so we must -- // ascend the AST. -- if pgf, ok := enclosingFile(r.pkg, from.Pos()); ok { -- path, _ := astutil.PathEnclosingInterval(pgf.File, from.Pos(), from.Pos()) -- // path matches this pattern: -- // [Ident SelectorExpr? StarExpr? Field FieldList StructType ParenExpr* ... File] +- a := integration.NewAwaiter(s.Workdir) +- const skipApplyEdits = false +- editor, err := fake.NewEditor(s, config).Connect(context.Background(), ts, a.Hooks(), skipApplyEdits) +- if err != nil { +- return nil, nil, nil, err +- } - -- // Ascend to FieldList. -- var i int -- for { -- if _, ok := path[i].(*ast.FieldList); ok { -- break -- } -- i++ -- } -- i++ -- tStruct := path[i].(*ast.StructType) -- i++ -- // Ascend past parens (unlikely). -- for { -- _, ok := path[i].(*ast.ParenExpr) -- if !ok { -- break -- } -- i++ -- } -- if spec, ok := path[i].(*ast.TypeSpec); ok { -- // This struct is also a named type. -- // We must check for direct (non-promoted) field/field -- // and method/field conflicts. -- named := r.pkg.GetTypesInfo().Defs[spec.Name].Type() -- prev, indices, _ := types.LookupFieldOrMethod(named, true, r.pkg.GetTypes(), r.to) -- if len(indices) == 1 { -- r.errorf(from.Pos(), "renaming this field %q to %q", -- from.Name(), r.to) -- r.errorf(prev.Pos(), "\twould conflict with this %s", -- objectKind(prev)) -- return // skip checkSelections to avoid redundant errors -- } -- } else { -- // This struct is not a named type. -- // We need only check for direct (non-promoted) field/field conflicts. -- T := r.pkg.GetTypesInfo().Types[tStruct].Type.Underlying().(*types.Struct) -- for i := 0; i < T.NumFields(); i++ { -- if prev := T.Field(i); prev.Name() == r.to { -- r.errorf(from.Pos(), "renaming this field %q to %q", -- from.Name(), r.to) -- r.errorf(prev.Pos(), "\twould conflict with this field") -- return // skip checkSelections to avoid redundant errors -- } -- } +- return s, editor, a, nil +-} +- +-// newGoplsConnector returns a connector that connects to a new gopls process, +-// executed with the provided arguments. +-func newGoplsConnector(args []string) (servertest.Connector, error) { +- if *goplsPath != "" && *goplsCommit != "" { +- panic("can't set both -gopls_path and -gopls_commit") +- } +- var ( +- goplsPath = *goplsPath +- env []string +- ) +- if *goplsCommit != "" { +- goplsPath = getInstalledGopls() +- } +- if goplsPath == "" { +- var err error +- goplsPath, err = os.Executable() +- if err != nil { +- return nil, err - } +- env = []string{fmt.Sprintf("%s=true", runAsGopls)} - } +- return &SidecarServer{ +- goplsPath: goplsPath, +- env: env, +- args: args, +- }, nil +-} - -- // Renaming an anonymous field requires renaming the type too. e.g. -- // print(s.T) // if we rename T to U, -- // type T int // this and -- // var s struct {T} // this must change too. -- if from.Anonymous() { -- if named, ok := from.Type().(*types.Named); ok { -- r.check(named.Obj()) -- } else if named, ok := Deref(from.Type()).(*types.Named); ok { -- r.check(named.Obj()) -- } +-// profileArgs returns additional command-line arguments to use when invoking +-// gopls, to enable the user-requested profiles. +-// +-// If wantCPU is set, CPU profiling is enabled as well. Some tests may want to +-// instrument profiling around specific critical sections of the benchmark, +-// rather than the entire process. +-// +-// TODO(rfindley): like CPU, all of these would be better served by a custom +-// command. Very rarely do we care about memory usage as the process exits: we +-// care about specific points in time during the benchmark. mem and alloc +-// should be snapshotted, and tracing should be bracketed around critical +-// sections. +-func profileArgs(name string, wantCPU bool) []string { +- var args []string +- if wantCPU && *cpuProfile != "" { +- args = append(args, fmt.Sprintf("-profile.cpu=%s", qualifiedName(name, *cpuProfile))) +- } +- if *memProfile != "" { +- args = append(args, fmt.Sprintf("-profile.mem=%s", qualifiedName(name, *memProfile))) +- } +- if *allocProfile != "" { +- args = append(args, fmt.Sprintf("-profile.alloc=%s", qualifiedName(name, *allocProfile))) +- } +- if *trace != "" { +- args = append(args, fmt.Sprintf("-profile.trace=%s", qualifiedName(name, *trace))) - } +- return args +-} - -- // Check integrity of existing (field and method) selections. -- r.checkSelections(from) +-func qualifiedName(args ...string) string { +- return strings.Join(args, ".") -} - --// checkSelections checks that all uses and selections that resolve to --// the specified object would continue to do so after the renaming. --func (r *renamer) checkSelections(from types.Object) { -- pkg := r.pkg -- typ := pkg.GetTypes() -- { -- if id := someUse(pkg.GetTypesInfo(), from); id != nil { -- if !r.checkExport(id, typ, from) { -- return -- } -- } +-// getInstalledGopls builds gopls at the given -gopls_commit, returning the +-// path to the gopls binary. +-func getInstalledGopls() string { +- if *goplsCommit == "" { +- panic("must provide -gopls_commit") +- } +- toolsDir := filepath.Join(getTempDir(), "gopls_build") +- goplsPath := filepath.Join(toolsDir, "gopls", "gopls") - -- for syntax, sel := range pkg.GetTypesInfo().Selections { -- // There may be extant selections of only the old -- // name or only the new name, so we must check both. -- // (If neither, the renaming is sound.) -- // -- // In both cases, we wish to compare the lengths -- // of the implicit field path (Selection.Index) -- // to see if the renaming would change it. -- // -- // If a selection that resolves to 'from', when renamed, -- // would yield a path of the same or shorter length, -- // this indicates ambiguity or a changed referent, -- // analogous to same- or sub-block lexical conflict. -- // -- // If a selection using the name 'to' would -- // yield a path of the same or shorter length, -- // this indicates ambiguity or shadowing, -- // analogous to same- or super-block lexical conflict. +- installGoplsOnce.Do(func() { +- log.Printf("installing gopls: checking out x/tools@%s into %s\n", *goplsCommit, toolsDir) +- if err := shallowClone(toolsDir, "https://go.googlesource.com/tools", *goplsCommit); err != nil { +- log.Fatal(err) +- } - -- // TODO(adonovan): fix: derive from Types[syntax.X].Mode -- // TODO(adonovan): test with pointer, value, addressable value. -- isAddressable := true +- log.Println("installing gopls: building...") +- bld := exec.Command("go", "build", ".") +- bld.Dir = filepath.Join(toolsDir, "gopls") +- if output, err := bld.CombinedOutput(); err != nil { +- log.Fatalf("building gopls: %v\n%s", err, output) +- } - -- if sel.Obj() == from { -- if obj, indices, _ := types.LookupFieldOrMethod(sel.Recv(), isAddressable, from.Pkg(), r.to); obj != nil { -- // Renaming this existing selection of -- // 'from' may block access to an existing -- // type member named 'to'. -- delta := len(indices) - len(sel.Index()) -- if delta > 0 { -- continue // no ambiguity -- } -- r.selectionConflict(from, delta, syntax, obj) -- return -- } -- } else if sel.Obj().Name() == r.to { -- if obj, indices, _ := types.LookupFieldOrMethod(sel.Recv(), isAddressable, from.Pkg(), from.Name()); obj == from { -- // Renaming 'from' may cause this existing -- // selection of the name 'to' to change -- // its meaning. -- delta := len(indices) - len(sel.Index()) -- if delta > 0 { -- continue // no ambiguity -- } -- r.selectionConflict(from, -delta, syntax, sel.Obj()) -- return -- } -- } +- // Confirm that the resulting path now exists. +- if _, err := os.Stat(goplsPath); err != nil { +- log.Fatalf("os.Stat(%s): %v", goplsPath, err) - } -- } +- }) +- return goplsPath -} - --func (r *renamer) selectionConflict(from types.Object, delta int, syntax *ast.SelectorExpr, obj types.Object) { -- r.errorf(from.Pos(), "renaming this %s %q to %q", -- objectKind(from), from.Name(), r.to) -- -- switch { -- case delta < 0: -- // analogous to sub-block conflict -- r.errorf(syntax.Sel.Pos(), -- "\twould change the referent of this selection") -- r.errorf(obj.Pos(), "\tof this %s", objectKind(obj)) -- case delta == 0: -- // analogous to same-block conflict -- r.errorf(syntax.Sel.Pos(), -- "\twould make this reference ambiguous") -- r.errorf(obj.Pos(), "\twith this %s", objectKind(obj)) -- case delta > 0: -- // analogous to super-block conflict -- r.errorf(syntax.Sel.Pos(), -- "\twould shadow this selection") -- r.errorf(obj.Pos(), "\tof the %s declared here", -- objectKind(obj)) -- } +-// A SidecarServer starts (and connects to) a separate gopls process at the +-// given path. +-type SidecarServer struct { +- goplsPath string +- env []string // additional environment bindings +- args []string // command-line arguments -} - --// checkMethod performs safety checks for renaming a method. --// There are three hazards: --// - declaration conflicts --// - selection ambiguity/changes --// - entailed renamings of assignable concrete/interface types. +-// Connect creates new io.Pipes and binds them to the underlying StreamServer. -// --// We reject renamings initiated at concrete methods if it would --// change the assignability relation. For renamings of abstract --// methods, we rename all methods transitively coupled to it via --// assignability. --func (r *renamer) checkMethod(from *types.Func) { -- // e.g. error.Error -- if from.Pkg() == nil { -- r.errorf(from.Pos(), "you cannot rename built-in method %s", from) -- return -- } -- -- // ASSIGNABILITY: We reject renamings of concrete methods that -- // would break a 'satisfy' constraint; but renamings of abstract -- // methods are allowed to proceed, and we rename affected -- // concrete and abstract methods as necessary. It is the -- // initial method that determines the policy. +-// It implements the servertest.Connector interface. +-func (s *SidecarServer) Connect(ctx context.Context) jsonrpc2.Conn { +- // Note: don't use CommandContext here, as we want gopls to exit gracefully +- // in order to write out profile data. +- // +- // We close the connection on context cancelation below. +- cmd := exec.Command(s.goplsPath, s.args...) - -- // Check for conflict at point of declaration. -- // Check to ensure preservation of assignability requirements. -- R := recv(from).Type() -- if types.IsInterface(R) { -- // Abstract method +- stdin, err := cmd.StdinPipe() +- if err != nil { +- log.Fatal(err) +- } +- stdout, err := cmd.StdoutPipe() +- if err != nil { +- log.Fatal(err) +- } +- cmd.Stderr = os.Stderr +- cmd.Env = append(os.Environ(), s.env...) +- if err := cmd.Start(); err != nil { +- log.Fatalf("starting gopls: %v", err) +- } - -- // declaration -- prev, _, _ := types.LookupFieldOrMethod(R, false, from.Pkg(), r.to) -- if prev != nil { -- r.errorf(from.Pos(), "renaming this interface method %q to %q", -- from.Name(), r.to) -- r.errorf(prev.Pos(), "\twould conflict with this method") -- return +- go func() { +- // If we don't log.Fatal here, benchmarks may hang indefinitely if gopls +- // exits abnormally. +- // +- // TODO(rfindley): ideally we would shut down the connection gracefully, +- // but that doesn't currently work. +- if err := cmd.Wait(); err != nil { +- log.Fatalf("gopls invocation failed with error: %v", err) - } +- }() - -- // Check all interfaces that embed this one for -- // declaration conflicts too. -- { -- // Start with named interface types (better errors) -- for _, obj := range r.pkg.GetTypesInfo().Defs { -- if obj, ok := obj.(*types.TypeName); ok && types.IsInterface(obj.Type()) { -- f, _, _ := types.LookupFieldOrMethod( -- obj.Type(), false, from.Pkg(), from.Name()) -- if f == nil { -- continue -- } -- t, _, _ := types.LookupFieldOrMethod( -- obj.Type(), false, from.Pkg(), r.to) -- if t == nil { -- continue -- } -- r.errorf(from.Pos(), "renaming this interface method %q to %q", -- from.Name(), r.to) -- r.errorf(t.Pos(), "\twould conflict with this method") -- r.errorf(obj.Pos(), "\tin named interface type %q", obj.Name()) -- } -- } +- clientStream := jsonrpc2.NewHeaderStream(fakenet.NewConn("stdio", stdout, stdin)) +- clientConn := jsonrpc2.NewConn(clientStream) - -- // Now look at all literal interface types (includes named ones again). -- for e, tv := range r.pkg.GetTypesInfo().Types { -- if e, ok := e.(*ast.InterfaceType); ok { -- _ = e -- _ = tv.Type.(*types.Interface) -- // TODO(adonovan): implement same check as above. -- } -- } +- go func() { +- select { +- case <-ctx.Done(): +- clientConn.Close() +- clientStream.Close() +- case <-clientConn.Done(): - } +- }() - -- // assignability -- // -- // Find the set of concrete or abstract methods directly -- // coupled to abstract method 'from' by some -- // satisfy.Constraint, and rename them too. -- for key := range r.satisfy() { -- // key = (lhs, rhs) where lhs is always an interface. +- return clientConn +-} - -- lsel := r.msets.MethodSet(key.LHS).Lookup(from.Pkg(), from.Name()) -- if lsel == nil { -- continue +-// startProfileIfSupported checks to see if the remote gopls instance supports +-// the start/stop profiling commands. If so, it starts profiling and returns a +-// function that stops profiling and records the total CPU seconds sampled in the +-// cpu_seconds benchmark metric. +-// +-// If the remote gopls instance does not support profiling commands, this +-// function returns nil. +-// +-// If the supplied userSuffix is non-empty, the profile is written to +-// ., and not deleted when the benchmark exits. Otherwise, +-// the profile is written to a temp file that is deleted after the cpu_seconds +-// metric has been computed. +-func startProfileIfSupported(b *testing.B, env *integration.Env, name string) func() { +- if !env.Editor.HasCommand(command.StartProfile.ID()) { +- return nil +- } +- b.StopTimer() +- stopProfile := env.StartProfile() +- b.StartTimer() +- return func() { +- b.StopTimer() +- profFile := stopProfile() +- totalCPU, err := totalCPUForProfile(profFile) +- if err != nil { +- b.Fatalf("reading profile: %v", err) +- } +- b.ReportMetric(totalCPU.Seconds()/float64(b.N), "cpu_seconds/op") +- if *cpuProfile == "" { +- // The user didn't request profiles, so delete it to clean up. +- if err := os.Remove(profFile); err != nil { +- b.Errorf("removing profile file: %v", err) - } -- rmethods := r.msets.MethodSet(key.RHS) -- rsel := rmethods.Lookup(from.Pkg(), from.Name()) -- if rsel == nil { -- continue +- } else { +- // NOTE: if this proves unreliable (due to e.g. EXDEV), we can fall back +- // on Read+Write+Remove. +- name := qualifiedName(name, *cpuProfile) +- if err := os.Rename(profFile, name); err != nil { +- b.Fatalf("renaming profile file: %v", err) - } +- } +- } +-} - -- // If both sides have a method of this name, -- // and one of them is m, the other must be coupled. -- var coupled *types.Func -- switch from { -- case lsel.Obj(): -- coupled = rsel.Obj().(*types.Func) -- case rsel.Obj(): -- coupled = lsel.Obj().(*types.Func) -- default: -- continue -- } +-// totalCPUForProfile reads the pprof profile with the given file name, parses, +-// and aggregates the total CPU sampled during the profile. +-func totalCPUForProfile(filename string) (time.Duration, error) { +- protoGz, err := os.ReadFile(filename) +- if err != nil { +- return 0, err +- } +- rd, err := gzip.NewReader(bytes.NewReader(protoGz)) +- if err != nil { +- return 0, fmt.Errorf("creating gzip reader for %s: %v", filename, err) +- } +- data, err := io.ReadAll(rd) +- if err != nil { +- return 0, fmt.Errorf("reading %s: %v", filename, err) +- } +- return pprof.TotalTime(data) +-} - -- // We must treat concrete-to-interface -- // constraints like an implicit selection C.f of -- // each interface method I.f, and check that the -- // renaming leaves the selection unchanged and -- // unambiguous. -- // -- // Fun fact: the implicit selection of C.f -- // type I interface{f()} -- // type C struct{I} -- // func (C) g() -- // var _ I = C{} // here -- // yields abstract method I.f. This can make error -- // messages less than obvious. -- // -- if !types.IsInterface(key.RHS) { -- // The logic below was derived from checkSelections. +-// closeBuffer stops the benchmark timer and closes the buffer with the given +-// name. +-// +-// It may be used to clean up files opened in the shared environment during +-// benchmarking. +-func closeBuffer(b *testing.B, env *integration.Env, name string) { +- b.StopTimer() +- env.CloseBuffer(name) +- env.AfterChange() +- b.StartTimer() +-} +diff -urN a/gopls/internal/test/integration/bench/codeaction_test.go b/gopls/internal/test/integration/bench/codeaction_test.go +--- a/gopls/internal/test/integration/bench/codeaction_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/bench/codeaction_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,69 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- rtosel := rmethods.Lookup(from.Pkg(), r.to) -- if rtosel != nil { -- rto := rtosel.Obj().(*types.Func) -- delta := len(rsel.Index()) - len(rtosel.Index()) -- if delta < 0 { -- continue // no ambiguity -- } +-package bench - -- // TODO(adonovan): record the constraint's position. -- keyPos := token.NoPos +-import ( +- "fmt" +- "sync/atomic" +- "testing" - -- r.errorf(from.Pos(), "renaming this method %q to %q", -- from.Name(), r.to) -- if delta == 0 { -- // analogous to same-block conflict -- r.errorf(keyPos, "\twould make the %s method of %s invoked via interface %s ambiguous", -- r.to, key.RHS, key.LHS) -- r.errorf(rto.Pos(), "\twith (%s).%s", -- recv(rto).Type(), r.to) -- } else { -- // analogous to super-block conflict -- r.errorf(keyPos, "\twould change the %s method of %s invoked via interface %s", -- r.to, key.RHS, key.LHS) -- r.errorf(coupled.Pos(), "\tfrom (%s).%s", -- recv(coupled).Type(), r.to) -- r.errorf(rto.Pos(), "\tto (%s).%s", -- recv(rto).Type(), r.to) -- } -- return // one error is enough -- } -- } +- "golang.org/x/tools/gopls/internal/protocol" +-) - -- if !r.changeMethods { -- // This should be unreachable. -- r.errorf(from.Pos(), "internal error: during renaming of abstract method %s", from) -- r.errorf(coupled.Pos(), "\tchangedMethods=false, coupled method=%s", coupled) -- r.errorf(from.Pos(), "\tPlease file a bug report") -- return -- } +-func BenchmarkCodeAction(b *testing.B) { +- for _, test := range didChangeTests { +- b.Run(test.repo, func(b *testing.B) { +- env := getRepo(b, test.repo).sharedEnv(b) +- env.OpenFile(test.file) +- defer closeBuffer(b, env, test.file) +- env.AfterChange() - -- // Rename the coupled method to preserve assignability. -- r.check(coupled) -- } -- } else { -- // Concrete method +- env.CodeAction(test.file, nil) // pre-warm - -- // declaration -- prev, indices, _ := types.LookupFieldOrMethod(R, true, from.Pkg(), r.to) -- if prev != nil && len(indices) == 1 { -- r.errorf(from.Pos(), "renaming this method %q to %q", -- from.Name(), r.to) -- r.errorf(prev.Pos(), "\twould conflict with this %s", -- objectKind(prev)) -- return -- } +- b.ResetTimer() - -- // assignability -- // -- // Find the set of abstract methods coupled to concrete -- // method 'from' by some satisfy.Constraint, and rename -- // them too. -- // -- // Coupling may be indirect, e.g. I.f <-> C.f via type D. -- // -- // type I interface {f()} -- // type C int -- // type (C) f() -- // type D struct{C} -- // var _ I = D{} -- // -- for key := range r.satisfy() { -- // key = (lhs, rhs) where lhs is always an interface. -- if types.IsInterface(key.RHS) { -- continue -- } -- rsel := r.msets.MethodSet(key.RHS).Lookup(from.Pkg(), from.Name()) -- if rsel == nil || rsel.Obj() != from { -- continue // rhs does not have the method -- } -- lsel := r.msets.MethodSet(key.LHS).Lookup(from.Pkg(), from.Name()) -- if lsel == nil { -- continue +- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, "hover")); stopAndRecord != nil { +- defer stopAndRecord() - } -- imeth := lsel.Obj().(*types.Func) -- -- // imeth is the abstract method (e.g. I.f) -- // and key.RHS is the concrete coupling type (e.g. D). -- if !r.changeMethods { -- r.errorf(from.Pos(), "renaming this method %q to %q", -- from.Name(), r.to) -- var pos token.Pos -- var iface string - -- I := recv(imeth).Type() -- if named, ok := I.(*types.Named); ok { -- pos = named.Obj().Pos() -- iface = "interface " + named.Obj().Name() -- } else { -- pos = from.Pos() -- iface = I.String() -- } -- r.errorf(pos, "\twould make %s no longer assignable to %s", -- key.RHS, iface) -- r.errorf(imeth.Pos(), "\t(rename %s.%s if you intend to change both types)", -- I, from.Name()) -- return // one error is enough +- for i := 0; i < b.N; i++ { +- env.CodeAction(test.file, nil) - } -- -- // Rename the coupled interface method to preserve assignability. -- r.check(imeth) -- } +- }) - } -- -- // Check integrity of existing (field and method) selections. -- // We skip this if there were errors above, to avoid redundant errors. -- r.checkSelections(from) -} - --func (r *renamer) checkExport(id *ast.Ident, pkg *types.Package, from types.Object) bool { -- // Reject cross-package references if r.to is unexported. -- // (Such references may be qualified identifiers or field/method -- // selections.) -- if !ast.IsExported(r.to) && pkg != from.Pkg() { -- r.errorf(from.Pos(), -- "renaming %q to %q would make it unexported", -- from.Name(), r.to) -- r.errorf(id.Pos(), "\tbreaking references from packages such as %q", -- pkg.Path()) -- return false -- } -- return true --} +-func BenchmarkCodeActionFollowingEdit(b *testing.B) { +- for _, test := range didChangeTests { +- b.Run(test.repo, func(b *testing.B) { +- env := getRepo(b, test.repo).sharedEnv(b) +- env.OpenFile(test.file) +- defer closeBuffer(b, env, test.file) +- env.EditBuffer(test.file, protocol.TextEdit{NewText: "// __TEST_PLACEHOLDER_0__\n"}) +- env.AfterChange() - --// satisfy returns the set of interface satisfaction constraints. --func (r *renamer) satisfy() map[satisfy.Constraint]bool { -- if r.satisfyConstraints == nil { -- // Compute on demand: it's expensive. -- var f satisfy.Finder -- pkg := r.pkg -- { -- // From satisfy.Finder documentation: -- // -- // The package must be free of type errors, and -- // info.{Defs,Uses,Selections,Types} must have been populated by the -- // type-checker. -- // -- // Only proceed if all packages have no errors. -- if len(pkg.GetParseErrors()) > 0 || len(pkg.GetTypeErrors()) > 0 { -- r.errorf(token.NoPos, // we don't have a position for this error. -- "renaming %q to %q not possible because %q has errors", -- r.from, r.to, pkg.Metadata().PkgPath) -- return nil +- env.CodeAction(test.file, nil) // pre-warm +- +- b.ResetTimer() +- +- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, "hover")); stopAndRecord != nil { +- defer stopAndRecord() +- } +- +- for i := 0; i < b.N; i++ { +- edits := atomic.AddInt64(&editID, 1) +- env.EditBuffer(test.file, protocol.TextEdit{ +- Range: protocol.Range{ +- Start: protocol.Position{Line: 0, Character: 0}, +- End: protocol.Position{Line: 1, Character: 0}, +- }, +- // Increment the placeholder text, to ensure cache misses. +- NewText: fmt.Sprintf("// __TEST_PLACEHOLDER_%d__\n", edits), +- }) +- env.CodeAction(test.file, nil) - } -- f.Find(pkg.GetTypesInfo(), pkg.GetSyntax()) -- } -- r.satisfyConstraints = f.Result +- }) - } -- return r.satisfyConstraints -} +diff -urN a/gopls/internal/test/integration/bench/completion_test.go b/gopls/internal/test/integration/bench/completion_test.go +--- a/gopls/internal/test/integration/bench/completion_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/bench/completion_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,330 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// -- helpers ---------------------------------------------------------- +-package bench - --// recv returns the method's receiver. --func recv(meth *types.Func) *types.Var { -- return meth.Type().(*types.Signature).Recv() --} +-import ( +- "flag" +- "fmt" +- "sync/atomic" +- "testing" - --// someUse returns an arbitrary use of obj within info. --func someUse(info *types.Info, obj types.Object) *ast.Ident { -- for id, o := range info.Uses { -- if o == obj { -- return id -- } -- } -- return nil +- "golang.org/x/tools/gopls/internal/protocol" +- . "golang.org/x/tools/gopls/internal/test/integration" +- "golang.org/x/tools/gopls/internal/test/integration/fake" +-) +- +-var completionGOPATH = flag.String("completion_gopath", "", "if set, use this GOPATH for BenchmarkCompletion") +- +-type completionBenchOptions struct { +- file, locationRegexp string +- +- // Hooks to run edits before initial completion +- setup func(*Env) // run before the benchmark starts +- beforeCompletion func(*Env) // run before each completion -} - --func objectKind(obj types.Object) string { -- if obj == nil { -- return "nil object" +-// Deprecated: new tests should be expressed in BenchmarkCompletion. +-func benchmarkCompletion(options completionBenchOptions, b *testing.B) { +- repo := getRepo(b, "tools") +- _ = repo.sharedEnv(b) // ensure cache is warm +- env := repo.newEnv(b, fake.EditorConfig{}, "completion", false) +- defer env.Close() +- +- // Run edits required for this completion. +- if options.setup != nil { +- options.setup(env) - } -- switch obj := obj.(type) { -- case *types.PkgName: -- return "imported package name" -- case *types.TypeName: -- return "type" -- case *types.Var: -- if obj.IsField() { -- return "field" -- } -- case *types.Func: -- if obj.Type().(*types.Signature).Recv() != nil { -- return "method" +- +- // Run a completion to make sure the system is warm. +- loc := env.RegexpSearch(options.file, options.locationRegexp) +- completions := env.Completion(loc) +- +- if testing.Verbose() { +- fmt.Println("Results:") +- for i := 0; i < len(completions.Items); i++ { +- fmt.Printf("\t%d. %v\n", i, completions.Items[i]) - } - } -- // label, func, var, const -- return strings.ToLower(strings.TrimPrefix(reflect.TypeOf(obj).String(), "*types.")) --} - --// NB: for renamings, blank is not considered valid. --func isValidIdentifier(id string) bool { -- if id == "" || id == "_" { -- return false -- } -- for i, r := range id { -- if !isLetter(r) && (i == 0 || !isDigit(r)) { -- return false +- b.Run("tools", func(b *testing.B) { +- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName("tools", "completion")); stopAndRecord != nil { +- defer stopAndRecord() - } -- } -- return token.Lookup(id) == token.IDENT --} - --// isLocal reports whether obj is local to some function. --// Precondition: not a struct field or interface method. --func isLocal(obj types.Object) bool { -- // [... 5=stmt 4=func 3=file 2=pkg 1=universe] -- var depth int -- for scope := obj.Parent(); scope != nil; scope = scope.Parent() { -- depth++ -- } -- return depth >= 4 +- for i := 0; i < b.N; i++ { +- if options.beforeCompletion != nil { +- options.beforeCompletion(env) +- } +- env.Completion(loc) +- } +- }) -} - --func isPackageLevel(obj types.Object) bool { -- if obj == nil { -- return false +-// endRangeInBuffer returns the position for last character in the buffer for +-// the given file. +-func endRangeInBuffer(env *Env, name string) protocol.Range { +- buffer := env.BufferText(name) +- m := protocol.NewMapper("", []byte(buffer)) +- rng, err := m.OffsetRange(len(buffer), len(buffer)) +- if err != nil { +- env.T.Fatal(err) - } -- return obj.Pkg().Scope().Lookup(obj.Name()) == obj +- return rng -} - --// -- Plundered from go/scanner: --------------------------------------- +-// Benchmark struct completion in tools codebase. +-func BenchmarkStructCompletion(b *testing.B) { +- file := "internal/lsp/cache/session.go" - --func isLetter(ch rune) bool { -- return 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' || ch >= 0x80 && unicode.IsLetter(ch) --} +- setup := func(env *Env) { +- env.OpenFile(file) +- env.EditBuffer(file, protocol.TextEdit{ +- Range: endRangeInBuffer(env, file), +- NewText: "\nvar testVariable map[string]bool = Session{}.\n", +- }) +- } - --func isDigit(ch rune) bool { -- return '0' <= ch && ch <= '9' || ch >= 0x80 && unicode.IsDigit(ch) +- benchmarkCompletion(completionBenchOptions{ +- file: file, +- locationRegexp: `var testVariable map\[string\]bool = Session{}(\.)`, +- setup: setup, +- }, b) -} -diff -urN a/gopls/internal/lsp/source/rename.go b/gopls/internal/lsp/source/rename.go ---- a/gopls/internal/lsp/source/rename.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/rename.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,1277 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package source +-// Benchmark import completion in tools codebase. +-func BenchmarkImportCompletion(b *testing.B) { +- const file = "internal/lsp/source/completion/completion.go" +- benchmarkCompletion(completionBenchOptions{ +- file: file, +- locationRegexp: `go\/()`, +- setup: func(env *Env) { env.OpenFile(file) }, +- }, b) +-} - --// TODO(adonovan): --// --// - method of generic concrete type -> arbitrary instances of same --// --// - make satisfy work across packages. --// --// - tests, tests, tests: --// - play with renamings in the k8s tree. --// - generics --// - error cases (e.g. conflicts) --// - renaming a symbol declared in the module cache --// (currently proceeds with half of the renaming!) --// - make sure all tests have both a local and a cross-package analogue. --// - look at coverage --// - special cases: embedded fields, interfaces, test variants, --// function-local things with uppercase names; --// packages with type errors (currently 'satisfy' rejects them), --// package with missing imports; --// --// - measure performance in k8s. --// --// - The original gorename tool assumed well-typedness, but the gopls feature --// does no such check (which actually makes it much more useful). --// Audit to ensure it is safe on ill-typed code. --// --// - Generics support was no doubt buggy before but incrementalization --// may have exacerbated it. If the problem were just about objects, --// defs and uses it would be fairly simple, but type assignability --// comes into play in the 'satisfy' check for method renamings. --// De-instantiating Vector[int] to Vector[T] changes its type. --// We need to come up with a theory for the satisfy check that --// works with generics, and across packages. We currently have no --// simple way to pass types between packages (think: objectpath for --// types), though presumably exportdata could be pressed into service. --// --// - FileID-based de-duplication of edits to different URIs for the same file. +-// Benchmark slice completion in tools codebase. +-func BenchmarkSliceCompletion(b *testing.B) { +- file := "internal/lsp/cache/session.go" - --import ( -- "context" -- "errors" -- "fmt" -- "go/ast" -- "go/token" -- "go/types" -- "path" -- "path/filepath" -- "regexp" -- "sort" -- "strconv" -- "strings" +- setup := func(env *Env) { +- env.OpenFile(file) +- env.EditBuffer(file, protocol.TextEdit{ +- Range: endRangeInBuffer(env, file), +- NewText: "\nvar testVariable []byte = \n", +- }) +- } - -- "golang.org/x/mod/modfile" -- "golang.org/x/tools/go/ast/astutil" -- "golang.org/x/tools/go/types/objectpath" -- "golang.org/x/tools/go/types/typeutil" -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/diff" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/typeparams" -- "golang.org/x/tools/refactor/satisfy" --) +- benchmarkCompletion(completionBenchOptions{ +- file: file, +- locationRegexp: `var testVariable \[\]byte (=)`, +- setup: setup, +- }, b) +-} - --// A renamer holds state of a single call to renameObj, which renames --// an object (or several coupled objects) within a single type-checked --// syntax package. --type renamer struct { -- pkg Package // the syntax package in which the renaming is applied -- objsToUpdate map[types.Object]bool // records progress of calls to check -- hadConflicts bool -- conflicts []string -- from, to string -- satisfyConstraints map[satisfy.Constraint]bool -- msets typeutil.MethodSetCache -- changeMethods bool +-// Benchmark deep completion in function call in tools codebase. +-func BenchmarkFuncDeepCompletion(b *testing.B) { +- file := "internal/lsp/source/completion/completion.go" +- fileContent := ` +-func (c *completer) _() { +- c.inference.kindMatches(c.) -} +-` +- setup := func(env *Env) { +- env.OpenFile(file) +- originalBuffer := env.BufferText(file) +- env.EditBuffer(file, protocol.TextEdit{ +- Range: endRangeInBuffer(env, file), +- // TODO(rfindley): this is a bug: it should just be fileContent. +- NewText: originalBuffer + fileContent, +- }) +- } - --// A PrepareItem holds the result of a "prepare rename" operation: --// the source range and value of a selected identifier. --type PrepareItem struct { -- Range protocol.Range -- Text string +- benchmarkCompletion(completionBenchOptions{ +- file: file, +- locationRegexp: `func \(c \*completer\) _\(\) {\n\tc\.inference\.kindMatches\((c)`, +- setup: setup, +- }, b) -} - --// PrepareRename searches for a valid renaming at position pp. --// --// The returned usererr is intended to be displayed to the user to explain why --// the prepare fails. Probably we could eliminate the redundancy in returning --// two errors, but for now this is done defensively. --func PrepareRename(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position) (_ *PrepareItem, usererr, err error) { -- ctx, done := event.Start(ctx, "source.PrepareRename") -- defer done() +-type completionTest struct { +- repo string +- name string +- file string // repo-relative file to create +- content string // file content +- locationRegexp string // regexp for completion +-} - -- // Is the cursor within the package name declaration? -- if pgf, inPackageName, err := parsePackageNameDecl(ctx, snapshot, f, pp); err != nil { -- return nil, err, err -- } else if inPackageName { -- item, err := prepareRenamePackageName(ctx, snapshot, pgf) -- return item, err, err -- } +-var completionTests = []completionTest{ +- { +- "tools", +- "selector", +- "internal/lsp/source/completion/completion2.go", +- ` +-package completion - -- // Ordinary (non-package) renaming. -- // -- // Type-check the current package, locate the reference at the position, -- // validate the object, and report its name and range. -- // -- // TODO(adonovan): in all cases below, we return usererr=nil, -- // which means we return (nil, nil) at the protocol -- // layer. This seems like a bug, or at best an exploitation of -- // knowledge of VSCode-specific behavior. Can we avoid that? -- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, f.URI()) -- if err != nil { -- return nil, nil, err -- } -- pos, err := pgf.PositionPos(pp) -- if err != nil { -- return nil, nil, err -- } -- targets, node, err := objectsAt(pkg.GetTypesInfo(), pgf.File, pos) -- if err != nil { -- return nil, nil, err -- } -- var obj types.Object -- for obj = range targets { -- break // pick one arbitrarily -- } -- if err := checkRenamable(obj); err != nil { -- return nil, nil, err -- } -- rng, err := pgf.NodeRange(node) -- if err != nil { -- return nil, nil, err -- } -- if _, isImport := node.(*ast.ImportSpec); isImport { -- // We're not really renaming the import path. -- rng.End = rng.Start -- } -- return &PrepareItem{ -- Range: rng, -- Text: obj.Name(), -- }, nil, nil +-func (c *completer) _() { +- c.inference.kindMatches(c.) -} +-`, +- `func \(c \*completer\) _\(\) {\n\tc\.inference\.kindMatches\((c)`, +- }, +- { +- "tools", +- "unimportedident", +- "internal/lsp/source/completion/completion2.go", +- ` +-package completion - --func prepareRenamePackageName(ctx context.Context, snapshot Snapshot, pgf *ParsedGoFile) (*PrepareItem, error) { -- // Does the client support file renaming? -- fileRenameSupported := false -- for _, op := range snapshot.Options().SupportedResourceOperations { -- if op == protocol.Rename { -- fileRenameSupported = true -- break -- } -- } -- if !fileRenameSupported { -- return nil, errors.New("can't rename package: LSP client does not support file renaming") -- } +-func (c *completer) _() { +- lo +-} +-`, +- `lo()`, +- }, +- { +- "tools", +- "unimportedselector", +- "internal/lsp/source/completion/completion2.go", +- ` +-package completion - -- // Check validity of the metadata for the file's containing package. -- meta, err := NarrowestMetadataForFile(ctx, snapshot, pgf.URI) -- if err != nil { -- return nil, err -- } -- if meta.Name == "main" { -- return nil, fmt.Errorf("can't rename package \"main\"") -- } -- if strings.HasSuffix(string(meta.Name), "_test") { -- return nil, fmt.Errorf("can't rename x_test packages") -- } -- if meta.Module == nil { -- return nil, fmt.Errorf("can't rename package: missing module information for package %q", meta.PkgPath) -- } -- if meta.Module.Path == string(meta.PkgPath) { -- return nil, fmt.Errorf("can't rename package: package path %q is the same as module path %q", meta.PkgPath, meta.Module.Path) -- } +-func (c *completer) _() { +- log. +-} +-`, +- `log\.()`, +- }, +- { +- "kubernetes", +- "selector", +- "pkg/kubelet/kubelet2.go", +- ` +-package kubelet - -- // Return the location of the package declaration. -- rng, err := pgf.NodeRange(pgf.File.Name) -- if err != nil { -- return nil, err -- } -- return &PrepareItem{ -- Range: rng, -- Text: string(meta.Name), -- }, nil +-func (kl *Kubelet) _() { +- kl. -} +-`, +- `kl\.()`, +- }, +- { +- "kubernetes", +- "identifier", +- "pkg/kubelet/kubelet2.go", +- ` +-package kubelet - --func checkRenamable(obj types.Object) error { -- switch obj := obj.(type) { -- case *types.Var: -- if obj.Embedded() { -- return fmt.Errorf("can't rename embedded fields: rename the type directly or name the field") -- } -- case *types.Builtin, *types.Nil: -- return fmt.Errorf("%s is built in and cannot be renamed", obj.Name()) -- } -- if obj.Pkg() == nil || obj.Pkg().Path() == "unsafe" { -- // e.g. error.Error, unsafe.Pointer -- return fmt.Errorf("%s is built in and cannot be renamed", obj.Name()) -- } -- if obj.Name() == "_" { -- return errors.New("can't rename \"_\"") -- } -- return nil +-func (kl *Kubelet) _() { +- k // here -} +-`, +- `k() // here`, +- }, +- { +- "oracle", +- "selector", +- "dataintegration/pivot2.go", +- ` +-package dataintegration - --// Rename returns a map of TextEdits for each file modified when renaming a --// given identifier within a package and a boolean value of true for renaming --// package and false otherwise. --func Rename(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position, newName string) (map[span.URI][]protocol.TextEdit, bool, error) { -- ctx, done := event.Start(ctx, "source.Rename") -- defer done() +-func (p *Pivot) _() { +- p. +-} +-`, +- `p\.()`, +- }, +-} - -- if !isValidIdentifier(newName) { -- return nil, false, fmt.Errorf("invalid identifier to rename: %q", newName) +-// Benchmark completion following an arbitrary edit. +-// +-// Edits force type-checked packages to be invalidated, so we want to measure +-// how long it takes before completion results are available. +-func BenchmarkCompletion(b *testing.B) { +- for _, test := range completionTests { +- b.Run(fmt.Sprintf("%s_%s", test.repo, test.name), func(b *testing.B) { +- for _, followingEdit := range []bool{true, false} { +- b.Run(fmt.Sprintf("edit=%v", followingEdit), func(b *testing.B) { +- for _, completeUnimported := range []bool{true, false} { +- b.Run(fmt.Sprintf("unimported=%v", completeUnimported), func(b *testing.B) { +- for _, budget := range []string{"0s", "100ms"} { +- b.Run(fmt.Sprintf("budget=%s", budget), func(b *testing.B) { +- runCompletion(b, test, followingEdit, completeUnimported, budget) +- }) +- } +- }) +- } +- }) +- } +- }) - } +-} - -- // Cursor within package name declaration? -- _, inPackageName, err := parsePackageNameDecl(ctx, snapshot, f, pp) -- if err != nil { -- return nil, false, err -- } +-// For optimizing unimported completion, it can be useful to benchmark with a +-// huge GOMODCACHE. +-var gomodcache = flag.String("gomodcache", "", "optional GOMODCACHE for unimported completion benchmarks") - -- var editMap map[span.URI][]diff.Edit -- if inPackageName { -- editMap, err = renamePackageName(ctx, snapshot, f, PackageName(newName)) -- } else { -- editMap, err = renameOrdinary(ctx, snapshot, f, pp, newName) +-func runCompletion(b *testing.B, test completionTest, followingEdit, completeUnimported bool, budget string) { +- repo := getRepo(b, test.repo) +- gopath := *completionGOPATH +- if gopath == "" { +- // use a warm GOPATH +- sharedEnv := repo.sharedEnv(b) +- gopath = sharedEnv.Sandbox.GOPATH() - } -- if err != nil { -- return nil, false, err +- envvars := map[string]string{ +- "GOPATH": gopath, - } - -- // Convert edits to protocol form. -- result := make(map[span.URI][]protocol.TextEdit) -- for uri, edits := range editMap { -- // Sort and de-duplicate edits. -- // -- // Overlapping edits may arise in local renamings (due -- // to type switch implicits) and globals ones (due to -- // processing multiple package variants). -- // -- // We assume renaming produces diffs that are all -- // replacements (no adjacent insertions that might -- // become reordered) and that are either identical or -- // non-overlapping. -- diff.SortEdits(edits) -- filtered := edits[:0] -- for i, edit := range edits { -- if i == 0 || edit != filtered[len(filtered)-1] { -- filtered = append(filtered, edit) -- } -- } -- edits = filtered -- -- // TODO(adonovan): the logic above handles repeat edits to the -- // same file URI (e.g. as a member of package p and p_test) but -- // is not sufficient to handle file-system level aliasing arising -- // from symbolic or hard links. For that, we should use a -- // robustio-FileID-keyed map. -- // See https://go.dev/cl/457615 for example. -- // This really occurs in practice, e.g. kubernetes has -- // vendor/k8s.io/kubectl -> ../../staging/src/k8s.io/kubectl. -- fh, err := snapshot.ReadFile(ctx, uri) -- if err != nil { -- return nil, false, err -- } -- data, err := fh.Content() -- if err != nil { -- return nil, false, err -- } -- m := protocol.NewMapper(uri, data) -- protocolEdits, err := ToProtocolEdits(m, edits) -- if err != nil { -- return nil, false, err -- } -- result[uri] = protocolEdits +- if *gomodcache != "" { +- envvars["GOMODCACHE"] = *gomodcache - } - -- return result, inPackageName, nil --} +- env := repo.newEnv(b, fake.EditorConfig{ +- Env: envvars, +- Settings: map[string]interface{}{ +- "completeUnimported": completeUnimported, +- "completionBudget": budget, +- }, +- }, "completion", false) +- defer env.Close() - --// renameOrdinary renames an ordinary (non-package) name throughout the workspace. --func renameOrdinary(ctx context.Context, snapshot Snapshot, f FileHandle, pp protocol.Position, newName string) (map[span.URI][]diff.Edit, error) { -- // Type-check the referring package and locate the object(s). -- // -- // Unlike NarrowestPackageForFile, this operation prefers the -- // widest variant as, for non-exported identifiers, it is the -- // only package we need. (In case you're wondering why -- // 'references' doesn't also want the widest variant: it -- // computes the union across all variants.) -- var targets map[types.Object]ast.Node -- var pkg Package -- { -- metas, err := snapshot.MetadataForFile(ctx, f.URI()) -- if err != nil { -- return nil, err -- } -- RemoveIntermediateTestVariants(&metas) -- if len(metas) == 0 { -- return nil, fmt.Errorf("no package metadata for file %s", f.URI()) -- } -- widest := metas[len(metas)-1] // widest variant may include _test.go files -- pkgs, err := snapshot.TypeCheck(ctx, widest.ID) -- if err != nil { -- return nil, err -- } -- pkg = pkgs[0] -- pgf, err := pkg.File(f.URI()) -- if err != nil { -- return nil, err // "can't happen" -- } -- pos, err := pgf.PositionPos(pp) -- if err != nil { -- return nil, err -- } -- objects, _, err := objectsAt(pkg.GetTypesInfo(), pgf.File, pos) -- if err != nil { -- return nil, err +- env.CreateBuffer(test.file, "// __TEST_PLACEHOLDER_0__\n"+test.content) +- editPlaceholder := func() { +- edits := atomic.AddInt64(&editID, 1) +- env.EditBuffer(test.file, protocol.TextEdit{ +- Range: protocol.Range{ +- Start: protocol.Position{Line: 0, Character: 0}, +- End: protocol.Position{Line: 1, Character: 0}, +- }, +- // Increment the placeholder text, to ensure cache misses. +- NewText: fmt.Sprintf("// __TEST_PLACEHOLDER_%d__\n", edits), +- }) +- } +- env.AfterChange() +- +- // Run a completion to make sure the system is warm. +- loc := env.RegexpSearch(test.file, test.locationRegexp) +- completions := env.Completion(loc) +- +- if testing.Verbose() { +- fmt.Println("Results:") +- for i, item := range completions.Items { +- fmt.Printf("\t%d. %v\n", i, item) - } -- targets = objects - } - -- // Pick a representative object arbitrarily. -- // (All share the same name, pos, and kind.) -- var obj types.Object -- for obj = range targets { -- break +- b.ResetTimer() +- +- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, "completion")); stopAndRecord != nil { +- defer stopAndRecord() - } -- if obj.Name() == newName { -- return nil, fmt.Errorf("old and new names are the same: %s", newName) +- +- for i := 0; i < b.N; i++ { +- if followingEdit { +- editPlaceholder() +- } +- loc := env.RegexpSearch(test.file, test.locationRegexp) +- env.Completion(loc) - } -- if err := checkRenamable(obj); err != nil { -- return nil, err +-} +diff -urN a/gopls/internal/test/integration/bench/definition_test.go b/gopls/internal/test/integration/bench/definition_test.go +--- a/gopls/internal/test/integration/bench/definition_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/bench/definition_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,46 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package bench +- +-import ( +- "testing" +-) +- +-func BenchmarkDefinition(b *testing.B) { +- tests := []struct { +- repo string +- file string +- regexp string +- }{ +- {"istio", "pkg/config/model.go", `gogotypes\.(MarshalAny)`}, +- {"google-cloud-go", "httpreplay/httpreplay.go", `proxy\.(ForRecording)`}, +- {"kubernetes", "pkg/controller/lookup_cache.go", `hashutil\.(DeepHashObject)`}, +- {"kuma", "api/generic/insights.go", `proto\.(Message)`}, +- {"pkgsite", "internal/log/log.go", `derrors\.(Wrap)`}, +- {"starlark", "starlark/eval.go", "prog.compiled.(Encode)"}, +- {"tools", "internal/lsp/cache/check.go", `(snapshot)\) buildKey`}, - } - -- // Find objectpath, if object is exported ("" otherwise). -- var declObjPath objectpath.Path -- if obj.Exported() { -- // objectpath.For requires the origin of a generic function or type, not an -- // instantiation (a bug?). Unfortunately we can't call Func.Origin as this -- // is not available in go/types@go1.18. So we take a scenic route. -- // -- // Note that unlike Funcs, TypeNames are always canonical (they are "left" -- // of the type parameters, unlike methods). -- switch obj.(type) { // avoid "obj :=" since cases reassign the var -- case *types.TypeName: -- if _, ok := obj.Type().(*typeparams.TypeParam); ok { -- // As with capitalized function parameters below, type parameters are -- // local. -- goto skipObjectPath +- for _, test := range tests { +- b.Run(test.repo, func(b *testing.B) { +- env := getRepo(b, test.repo).sharedEnv(b) +- env.OpenFile(test.file) +- defer closeBuffer(b, env, test.file) +- +- loc := env.RegexpSearch(test.file, test.regexp) +- env.Await(env.DoneWithOpen()) +- env.GoToDefinition(loc) // pre-warm the query, and open the target file +- b.ResetTimer() +- +- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, "definition")); stopAndRecord != nil { +- defer stopAndRecord() - } -- case *types.Func: -- obj = funcOrigin(obj.(*types.Func)) -- case *types.Var: -- // TODO(adonovan): do vars need the origin treatment too? (issue #58462) - -- // Function parameter and result vars that are (unusually) -- // capitalized are technically exported, even though they -- // cannot be referenced, because they may affect downstream -- // error messages. But we can safely treat them as local. -- // -- // This is not merely an optimization: the renameExported -- // operation gets confused by such vars. It finds them from -- // objectpath, the classifies them as local vars, but as -- // they came from export data they lack syntax and the -- // correct scope tree (issue #61294). -- if !obj.(*types.Var).IsField() && !isPackageLevel(obj) { -- goto skipObjectPath +- for i := 0; i < b.N; i++ { +- env.GoToDefinition(loc) // pre-warm the query - } -- } -- if path, err := objectpath.For(obj); err == nil { -- declObjPath = path -- } -- skipObjectPath: +- }) - } +-} +diff -urN a/gopls/internal/test/integration/bench/didchange_test.go b/gopls/internal/test/integration/bench/didchange_test.go +--- a/gopls/internal/test/integration/bench/didchange_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/bench/didchange_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,142 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- // Nonexported? Search locally. -- if declObjPath == "" { -- var objects []types.Object -- for obj := range targets { -- objects = append(objects, obj) -- } -- editMap, _, err := renameObjects(newName, pkg, objects...) -- return editMap, err -- } +-package bench - -- // Exported: search globally. -- // -- // For exported package-level var/const/func/type objects, the -- // search scope is just the direct importers. -- // -- // For exported fields and methods, the scope is the -- // transitive rdeps. (The exportedness of the field's struct -- // or method's receiver is irrelevant.) -- transitive := false -- switch obj.(type) { -- case *types.TypeName: -- // Renaming an exported package-level type -- // requires us to inspect all transitive rdeps -- // in the event that the type is embedded. -- // -- // TODO(adonovan): opt: this is conservative -- // but inefficient. Instead, expand the scope -- // of the search only if we actually encounter -- // an embedding of the type, and only then to -- // the rdeps of the embedding package. -- if obj.Parent() == obj.Pkg().Scope() { -- transitive = true -- } +-import ( +- "fmt" +- "sync/atomic" +- "testing" +- "time" - -- case *types.Var: -- if obj.(*types.Var).IsField() { -- transitive = true // field -- } +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/test/integration/fake" +-) - -- // TODO(adonovan): opt: process only packages that -- // contain a reference (xrefs) to the target field. +-// Use a global edit counter as bench function may execute multiple times, and +-// we want to avoid cache hits. Use time.Now to also avoid cache hits from the +-// shared file cache. +-var editID int64 = time.Now().UnixNano() - -- case *types.Func: -- if obj.Type().(*types.Signature).Recv() != nil { -- transitive = true // method -- } +-type changeTest struct { +- repo string +- file string +- canSave bool +-} - -- // It's tempting to optimize by skipping -- // packages that don't contain a reference to -- // the method in the xrefs index, but we still -- // need to apply the satisfy check to those -- // packages to find assignment statements that -- // might expands the scope of the renaming. -- } +-var didChangeTests = []changeTest{ +- {"google-cloud-go", "internal/annotate.go", true}, +- {"istio", "pkg/fuzz/util.go", true}, +- {"kubernetes", "pkg/controller/lookup_cache.go", true}, +- {"kuma", "api/generic/insights.go", true}, +- {"oracle", "dataintegration/data_type.go", false}, // diagnoseSave fails because this package is generated +- {"pkgsite", "internal/frontend/server.go", true}, +- {"starlark", "starlark/eval.go", true}, +- {"tools", "internal/lsp/cache/snapshot.go", true}, +-} - -- // Type-check all the packages to inspect. -- declURI := span.URIFromPath(pkg.FileSet().File(obj.Pos()).Name()) -- pkgs, err := typeCheckReverseDependencies(ctx, snapshot, declURI, transitive) -- if err != nil { -- return nil, err -- } +-// BenchmarkDidChange benchmarks modifications of a single file by making +-// synthetic modifications in a comment. It controls pacing by waiting for the +-// server to actually start processing the didChange notification before +-// proceeding. Notably it does not wait for diagnostics to complete. +-func BenchmarkDidChange(b *testing.B) { +- for _, test := range didChangeTests { +- b.Run(test.repo, func(b *testing.B) { +- env := getRepo(b, test.repo).sharedEnv(b) +- env.OpenFile(test.file) +- defer closeBuffer(b, env, test.file) - -- // Apply the renaming to the (initial) object. -- declPkgPath := PackagePath(obj.Pkg().Path()) -- return renameExported(pkgs, declPkgPath, declObjPath, newName) +- // Insert the text we'll be modifying at the top of the file. +- env.EditBuffer(test.file, protocol.TextEdit{NewText: "// __TEST_PLACEHOLDER_0__\n"}) +- env.AfterChange() +- b.ResetTimer() +- +- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, "didchange")); stopAndRecord != nil { +- defer stopAndRecord() +- } +- +- for i := 0; i < b.N; i++ { +- edits := atomic.AddInt64(&editID, 1) +- env.EditBuffer(test.file, protocol.TextEdit{ +- Range: protocol.Range{ +- Start: protocol.Position{Line: 0, Character: 0}, +- End: protocol.Position{Line: 1, Character: 0}, +- }, +- // Increment the placeholder text, to ensure cache misses. +- NewText: fmt.Sprintf("// __TEST_PLACEHOLDER_%d__\n", edits), +- }) +- env.Await(env.StartedChange()) +- } +- }) +- } -} - --// funcOrigin is a go1.18-portable implementation of (*types.Func).Origin. --func funcOrigin(fn *types.Func) *types.Func { -- // Method? -- if fn.Type().(*types.Signature).Recv() != nil { -- return typeparams.OriginMethod(fn) +-func BenchmarkDiagnoseChange(b *testing.B) { +- for _, test := range didChangeTests { +- runChangeDiagnosticsBenchmark(b, test, false, "diagnoseChange") - } +-} - -- // Package-level function? -- // (Assume the origin has the same position.) -- gen := fn.Pkg().Scope().Lookup(fn.Name()) -- if gen != nil && gen.Pos() == fn.Pos() { -- return gen.(*types.Func) +-// TODO(rfindley): add a benchmark for with a metadata-affecting change, when +-// this matters. +-func BenchmarkDiagnoseSave(b *testing.B) { +- for _, test := range didChangeTests { +- runChangeDiagnosticsBenchmark(b, test, true, "diagnoseSave") - } +-} +- +-// runChangeDiagnosticsBenchmark runs a benchmark to edit the test file and +-// await the resulting diagnostics pass. If save is set, the file is also saved. +-func runChangeDiagnosticsBenchmark(b *testing.B, test changeTest, save bool, operation string) { +- b.Run(test.repo, func(b *testing.B) { +- if !test.canSave { +- b.Skipf("skipping as %s cannot be saved", test.file) +- } +- sharedEnv := getRepo(b, test.repo).sharedEnv(b) +- config := fake.EditorConfig{ +- Env: map[string]string{ +- "GOPATH": sharedEnv.Sandbox.GOPATH(), +- }, +- Settings: map[string]interface{}{ +- "diagnosticsDelay": "0s", +- }, +- } +- // Use a new env to avoid the diagnostic delay: we want to measure how +- // long it takes to produce the diagnostics. +- env := getRepo(b, test.repo).newEnv(b, config, operation, false) +- defer env.Close() +- env.OpenFile(test.file) +- // Insert the text we'll be modifying at the top of the file. +- env.EditBuffer(test.file, protocol.TextEdit{NewText: "// __TEST_PLACEHOLDER_0__\n"}) +- if save { +- env.SaveBuffer(test.file) +- } +- env.AfterChange() +- b.ResetTimer() - -- return fn +- // We must use an extra subtest layer here, so that we only set up the +- // shared env once (otherwise we pay additional overhead and the profiling +- // flags don't work). +- b.Run("diagnose", func(b *testing.B) { +- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, operation)); stopAndRecord != nil { +- defer stopAndRecord() +- } +- for i := 0; i < b.N; i++ { +- edits := atomic.AddInt64(&editID, 1) +- env.EditBuffer(test.file, protocol.TextEdit{ +- Range: protocol.Range{ +- Start: protocol.Position{Line: 0, Character: 0}, +- End: protocol.Position{Line: 1, Character: 0}, +- }, +- // Increment the placeholder text, to ensure cache misses. +- NewText: fmt.Sprintf("// __TEST_PLACEHOLDER_%d__\n", edits), +- }) +- if save { +- env.SaveBuffer(test.file) +- } +- env.AfterChange() +- } +- }) +- }) -} +diff -urN a/gopls/internal/test/integration/bench/doc.go b/gopls/internal/test/integration/bench/doc.go +--- a/gopls/internal/test/integration/bench/doc.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/bench/doc.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,40 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// typeCheckReverseDependencies returns the type-checked packages for --// the reverse dependencies of all packages variants containing --// file declURI. The packages are in some topological order. +-// The bench package implements benchmarks for various LSP operations. -// --// It includes all variants (even intermediate test variants) for the --// purposes of computing reverse dependencies, but discards ITVs for --// the actual renaming work. +-// Benchmarks check out specific commits of popular and/or exemplary +-// repositories, and script an external gopls process via a fake text editor. +-// By default, benchmarks run the test executable as gopls (using a special +-// "gopls mode" environment variable). A different gopls binary may be used by +-// setting the -gopls_path or -gopls_commit flags. -// --// (This neglects obscure edge cases where a _test.go file changes the --// selectors used only in an ITV, but life is short. Also sin must be --// punished.) --func typeCheckReverseDependencies(ctx context.Context, snapshot Snapshot, declURI span.URI, transitive bool) ([]Package, error) { -- variants, err := snapshot.MetadataForFile(ctx, declURI) -- if err != nil { -- return nil, err +-// This package is a work in progress. +-// +-// # Profiling +-// +-// Benchmark functions run gopls in a separate process, which means the normal +-// test flags for profiling aren't useful. Instead the -gopls_cpuprofile, +-// -gopls_memprofile, -gopls_allocprofile, and -gopls_trace flags may be used +-// to pass through profiling to the gopls subproces. +-// +-// Each of these flags sets a suffix for the respective gopls profile, which is +-// named according to the schema ... For example, +-// setting -gopls_cpuprofile=cpu will result in profiles named tools.iwl.cpu, +-// tools.rename.cpu, etc. In some cases, these profiles are for the entire +-// gopls subprocess (as in the initial workspace load), whereas in others they +-// span only the critical section of the benchmark. It is up to each benchmark +-// to implement profiling as appropriate. +-// +-// # Integration with perf.golang.org +-// +-// Benchmarks that run with -short are automatically tracked by +-// perf.golang.org, at +-// https://perf.golang.org/dashboard/?benchmark=all&repository=tools&branch=release-branch.go1.20 +-// +-// # TODO +-// - add more benchmarks, and more repositories +-// - fix the perf dashboard to not require the branch= parameter +-// - improve this documentation +-package bench +diff -urN a/gopls/internal/test/integration/bench/hover_test.go b/gopls/internal/test/integration/bench/hover_test.go +--- a/gopls/internal/test/integration/bench/hover_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/bench/hover_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,47 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package bench +- +-import ( +- "testing" +-) +- +-func BenchmarkHover(b *testing.B) { +- tests := []struct { +- repo string +- file string +- regexp string +- }{ +- {"google-cloud-go", "httpreplay/httpreplay.go", `proxy\.(ForRecording)`}, +- {"istio", "pkg/config/model.go", `gogotypes\.(MarshalAny)`}, +- {"kubernetes", "pkg/apis/core/types.go", "type (Pod)"}, +- {"kuma", "api/generic/insights.go", `proto\.(Message)`}, +- {"pkgsite", "internal/log/log.go", `derrors\.(Wrap)`}, +- {"starlark", "starlark/eval.go", "prog.compiled.(Encode)"}, +- {"tools", "internal/lsp/cache/check.go", `(snapshot)\) buildKey`}, - } -- // variants must include ITVs for the reverse dependency -- // computation, but they are filtered out before we typecheck. -- allRdeps := make(map[PackageID]*Metadata) -- for _, variant := range variants { -- rdeps, err := snapshot.ReverseDependencies(ctx, variant.ID, transitive) -- if err != nil { -- return nil, err -- } -- allRdeps[variant.ID] = variant // include self -- for id, meta := range rdeps { -- allRdeps[id] = meta -- } +- +- for _, test := range tests { +- b.Run(test.repo, func(b *testing.B) { +- env := getRepo(b, test.repo).sharedEnv(b) +- env.OpenFile(test.file) +- defer closeBuffer(b, env, test.file) +- +- loc := env.RegexpSearch(test.file, test.regexp) +- env.AfterChange() +- +- env.Hover(loc) // pre-warm the query +- b.ResetTimer() +- +- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, "hover")); stopAndRecord != nil { +- defer stopAndRecord() +- } +- +- for i := 0; i < b.N; i++ { +- env.Hover(loc) // pre-warm the query +- } +- }) - } -- var ids []PackageID -- for id, meta := range allRdeps { -- if meta.IsIntermediateTestVariant() { -- continue -- } -- ids = append(ids, id) +-} +diff -urN a/gopls/internal/test/integration/bench/implementations_test.go b/gopls/internal/test/integration/bench/implementations_test.go +--- a/gopls/internal/test/integration/bench/implementations_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/bench/implementations_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,44 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package bench +- +-import "testing" +- +-func BenchmarkImplementations(b *testing.B) { +- tests := []struct { +- repo string +- file string +- regexp string +- }{ +- {"google-cloud-go", "httpreplay/httpreplay.go", `type (Recorder)`}, +- {"istio", "pkg/config/mesh/watcher.go", `type (Watcher)`}, +- {"kubernetes", "pkg/controller/lookup_cache.go", `objectWithMeta`}, +- {"kuma", "api/generic/insights.go", `type (Insight)`}, +- {"pkgsite", "internal/datasource.go", `type (DataSource)`}, +- {"starlark", "syntax/syntax.go", `type (Expr)`}, +- {"tools", "internal/lsp/source/view.go", `type (Snapshot)`}, - } - -- // Sort the packages into some topological order of the -- // (unfiltered) metadata graph. -- SortPostOrder(snapshot, ids) +- for _, test := range tests { +- b.Run(test.repo, func(b *testing.B) { +- env := getRepo(b, test.repo).sharedEnv(b) +- env.OpenFile(test.file) +- defer closeBuffer(b, env, test.file) - -- // Dependencies must be visited first since they can expand -- // the search set. Ideally we would process the (filtered) set -- // of packages in the parallel postorder of the snapshot's -- // (unfiltered) metadata graph, but this is quite tricky -- // without a good graph abstraction. -- // -- // For now, we visit packages sequentially in order of -- // ascending height, like an inverted breadth-first search. -- // -- // Type checking is by far the dominant cost, so -- // overlapping it with renaming may not be worthwhile. -- return snapshot.TypeCheck(ctx, ids...) --} +- loc := env.RegexpSearch(test.file, test.regexp) +- env.AfterChange() +- env.Implementations(loc) // pre-warm the query +- b.ResetTimer() - --// SortPostOrder sorts the IDs so that if x depends on y, then y appears before x. --func SortPostOrder(meta MetadataSource, ids []PackageID) { -- postorder := make(map[PackageID]int) -- order := 0 -- var visit func(PackageID) -- visit = func(id PackageID) { -- if _, ok := postorder[id]; !ok { -- postorder[id] = -1 // break recursion -- if m := meta.Metadata(id); m != nil { -- for _, depID := range m.DepsByPkgPath { -- visit(depID) -- } +- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, "implementations")); stopAndRecord != nil { +- defer stopAndRecord() - } -- order++ -- postorder[id] = order -- } -- } -- for _, id := range ids { -- visit(id) +- +- for i := 0; i < b.N; i++ { +- env.Implementations(loc) +- } +- }) - } -- sort.Slice(ids, func(i, j int) bool { -- return postorder[ids[i]] < postorder[ids[j]] -- }) -} +diff -urN a/gopls/internal/test/integration/bench/iwl_test.go b/gopls/internal/test/integration/bench/iwl_test.go +--- a/gopls/internal/test/integration/bench/iwl_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/bench/iwl_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,72 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// renameExported renames the object denoted by (pkgPath, objPath) --// within the specified packages, along with any other objects that --// must be renamed as a consequence. The slice of packages must be --// topologically ordered. --func renameExported(pkgs []Package, declPkgPath PackagePath, declObjPath objectpath.Path, newName string) (map[span.URI][]diff.Edit, error) { +-package bench - -- // A target is a name for an object that is stable across types.Packages. -- type target struct { -- pkg PackagePath -- obj objectpath.Path -- } +-import ( +- "testing" - -- // Populate the initial set of target objects. -- // This set may grow as we discover the consequences of each renaming. -- // -- // TODO(adonovan): strictly, each cone of reverse dependencies -- // of a single variant should have its own target map that -- // monotonically expands as we go up the import graph, because -- // declarations in test files can alter the set of -- // package-level names and change the meaning of field and -- // method selectors. So if we parallelize the graph -- // visitation (see above), we should also compute the targets -- // as a union of dependencies. -- // -- // Or we could decide that the logic below is fast enough not -- // to need parallelism. In small measurements so far the -- // type-checking step is about 95% and the renaming only 5%. -- targets := map[target]bool{{declPkgPath, declObjPath}: true} +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/command" +- . "golang.org/x/tools/gopls/internal/test/integration" +- "golang.org/x/tools/gopls/internal/test/integration/fake" +-) - -- // Apply the renaming operation to each package. -- allEdits := make(map[span.URI][]diff.Edit) -- for _, pkg := range pkgs { +-// BenchmarkInitialWorkspaceLoad benchmarks the initial workspace load time for +-// a new editing session. +-func BenchmarkInitialWorkspaceLoad(b *testing.B) { +- repoNames := []string{ +- "google-cloud-go", +- "istio", +- "kubernetes", +- "kuma", +- "oracle", +- "pkgsite", +- "starlark", +- "tools", +- "hashiform", +- } +- for _, repoName := range repoNames { +- b.Run(repoName, func(b *testing.B) { +- repo := getRepo(b, repoName) +- // get the (initialized) shared env to ensure the cache is warm. +- // Reuse its GOPATH so that we get cache hits for things in the module +- // cache. +- sharedEnv := repo.sharedEnv(b) +- b.ResetTimer() - -- // Resolved target objects within package pkg. -- var objects []types.Object -- for t := range targets { -- p := pkg.DependencyTypes(t.pkg) -- if p == nil { -- continue // indirect dependency of no consequence -- } -- obj, err := objectpath.Object(p, t.obj) -- if err != nil { -- // Possibly a method or an unexported type -- // that is not reachable through export data? -- // See https://github.com/golang/go/issues/60789. -- // -- // TODO(adonovan): it seems unsatisfactory that Object -- // should return an error for a "valid" path. Perhaps -- // we should define such paths as invalid and make -- // objectpath.For compute reachability? -- // Would that be a compatible change? -- continue +- for i := 0; i < b.N; i++ { +- doIWL(b, sharedEnv.Sandbox.GOPATH(), repo) - } -- objects = append(objects, obj) -- } -- if len(objects) == 0 { -- continue // no targets of consequence to this package -- } +- }) +- } +-} - -- // Apply the renaming. -- editMap, moreObjects, err := renameObjects(newName, pkg, objects...) -- if err != nil { -- return nil, err -- } +-func doIWL(b *testing.B, gopath string, repo *repo) { +- // Exclude the time to set up the env from the benchmark time, as this may +- // involve installing gopls and/or checking out the repo dir. +- b.StopTimer() +- config := fake.EditorConfig{Env: map[string]string{"GOPATH": gopath}} +- env := repo.newEnv(b, config, "iwl", true) +- defer env.Close() +- b.StartTimer() - -- // It is safe to concatenate the edits as they are non-overlapping -- // (or identical, in which case they will be de-duped by Rename). -- for uri, edits := range editMap { -- allEdits[uri] = append(allEdits[uri], edits...) -- } +- // Note: in the future, we may need to open a file in order to cause gopls to +- // start loading the workspace. - -- // Expand the search set? -- for obj := range moreObjects { -- objpath, err := objectpath.For(obj) -- if err != nil { -- continue // not exported -- } -- target := target{PackagePath(obj.Pkg().Path()), objpath} -- targets[target] = true +- env.Await(InitialWorkspaceLoad) - -- // TODO(adonovan): methods requires dynamic -- // programming of the product targets x -- // packages as any package might add a new -- // target (from a foward dep) as a -- // consequence, and any target might imply a -- // new set of rdeps. See golang/go#58461. +- if env.Editor.HasCommand(command.MemStats.ID()) { +- b.StopTimer() +- params := &protocol.ExecuteCommandParams{ +- Command: command.MemStats.ID(), - } +- var memstats command.MemStatsResult +- env.ExecuteCommand(params, &memstats) +- b.ReportMetric(float64(memstats.HeapAlloc), "alloc_bytes") +- b.ReportMetric(float64(memstats.HeapInUse), "in_use_bytes") +- b.ReportMetric(float64(memstats.TotalAlloc), "total_alloc_bytes") +- b.StartTimer() - } -- -- return allEdits, nil -} +diff -urN a/gopls/internal/test/integration/bench/references_test.go b/gopls/internal/test/integration/bench/references_test.go +--- a/gopls/internal/test/integration/bench/references_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/bench/references_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,44 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// renamePackageName renames package declarations, imports, and go.mod files. --func renamePackageName(ctx context.Context, s Snapshot, f FileHandle, newName PackageName) (map[span.URI][]diff.Edit, error) { -- // Rename the package decl and all imports. -- renamingEdits, err := renamePackage(ctx, s, f, newName) -- if err != nil { -- return nil, err -- } +-package bench - -- // Update the last component of the file's enclosing directory. -- oldBase := filepath.Dir(f.URI().Filename()) -- newPkgDir := filepath.Join(filepath.Dir(oldBase), string(newName)) +-import "testing" - -- // Update any affected replace directives in go.mod files. -- // TODO(adonovan): extract into its own function. -- // -- // Get all workspace modules. -- // TODO(adonovan): should this operate on all go.mod files, -- // irrespective of whether they are included in the workspace? -- modFiles := s.ModFiles() -- for _, m := range modFiles { -- fh, err := s.ReadFile(ctx, m) -- if err != nil { -- return nil, err -- } -- pm, err := s.ParseMod(ctx, fh) -- if err != nil { -- return nil, err -- } +-func BenchmarkReferences(b *testing.B) { +- tests := []struct { +- repo string +- file string +- regexp string +- }{ +- {"google-cloud-go", "httpreplay/httpreplay.go", `func (NewRecorder)`}, +- {"istio", "pkg/config/model.go", "type (Meta)"}, +- {"kubernetes", "pkg/controller/lookup_cache.go", "type (objectWithMeta)"}, // TODO: choose an exported identifier +- {"kuma", "pkg/events/interfaces.go", "type (Event)"}, +- {"pkgsite", "internal/log/log.go", "func (Infof)"}, +- {"starlark", "syntax/syntax.go", "type (Ident)"}, +- {"tools", "internal/lsp/source/view.go", "type (Snapshot)"}, +- } - -- modFileDir := filepath.Dir(pm.URI.Filename()) -- affectedReplaces := []*modfile.Replace{} +- for _, test := range tests { +- b.Run(test.repo, func(b *testing.B) { +- env := getRepo(b, test.repo).sharedEnv(b) +- env.OpenFile(test.file) +- defer closeBuffer(b, env, test.file) - -- // Check if any replace directives need to be fixed -- for _, r := range pm.File.Replace { -- if !strings.HasPrefix(r.New.Path, "/") && !strings.HasPrefix(r.New.Path, "./") && !strings.HasPrefix(r.New.Path, "../") { -- continue -- } +- loc := env.RegexpSearch(test.file, test.regexp) +- env.AfterChange() +- env.References(loc) // pre-warm the query +- b.ResetTimer() - -- replacedPath := r.New.Path -- if strings.HasPrefix(r.New.Path, "./") || strings.HasPrefix(r.New.Path, "../") { -- replacedPath = filepath.Join(modFileDir, r.New.Path) +- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, "references")); stopAndRecord != nil { +- defer stopAndRecord() - } - -- // TODO: Is there a risk of converting a '\' delimited replacement to a '/' delimited replacement? -- if !strings.HasPrefix(filepath.ToSlash(replacedPath)+"/", filepath.ToSlash(oldBase)+"/") { -- continue // not affected by the package renaming +- for i := 0; i < b.N; i++ { +- env.References(loc) - } +- }) +- } +-} +diff -urN a/gopls/internal/test/integration/bench/reload_test.go b/gopls/internal/test/integration/bench/reload_test.go +--- a/gopls/internal/test/integration/bench/reload_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/bench/reload_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,52 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +-package bench - -- affectedReplaces = append(affectedReplaces, r) -- } +-import ( +- "testing" - -- if len(affectedReplaces) == 0 { -- continue +- . "golang.org/x/tools/gopls/internal/test/integration" +-) +- +-// BenchmarkReload benchmarks reloading a file metadata after a change to an import. +-// +-// This ensures we are able to diagnose a changed file without reloading all +-// invalidated packages. See also golang/go#61344 +-func BenchmarkReload(b *testing.B) { +- // TODO(rfindley): add more tests, make this test table-driven +- const ( +- repo = "kubernetes" +- // pkg/util/hash is transitively imported by a large number of packages. +- // We should not need to reload those packages to get a diagnostic. +- file = "pkg/util/hash/hash.go" +- ) +- b.Run(repo, func(b *testing.B) { +- env := getRepo(b, repo).sharedEnv(b) +- +- env.OpenFile(file) +- defer closeBuffer(b, env, file) +- +- env.AfterChange() +- +- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(repo, "reload")); stopAndRecord != nil { +- defer stopAndRecord() - } -- copied, err := modfile.Parse("", pm.Mapper.Content, nil) -- if err != nil { -- return nil, err +- +- b.ResetTimer() +- for i := 0; i < b.N; i++ { +- // Change the "hash" import. This may result in cache hits, but that's +- // OK: the goal is to ensure that we don't reload more than just the +- // current package. +- env.RegexpReplace(file, `"hash"`, `"hashx"`) +- // Note: don't use env.AfterChange() here: we only want to await the +- // first diagnostic. +- // +- // Awaiting a full diagnosis would await diagnosing everything, which +- // would require reloading everything. +- env.Await(Diagnostics(ForFile(file))) +- env.RegexpReplace(file, `"hashx"`, `"hash"`) +- env.Await(NoDiagnostics(ForFile(file))) - } +- }) +-} +diff -urN a/gopls/internal/test/integration/bench/rename_test.go b/gopls/internal/test/integration/bench/rename_test.go +--- a/gopls/internal/test/integration/bench/rename_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/bench/rename_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,49 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- for _, r := range affectedReplaces { -- replacedPath := r.New.Path -- if strings.HasPrefix(r.New.Path, "./") || strings.HasPrefix(r.New.Path, "../") { -- replacedPath = filepath.Join(modFileDir, r.New.Path) -- } +-package bench - -- suffix := strings.TrimPrefix(replacedPath, string(oldBase)) +-import ( +- "fmt" +- "testing" +-) - -- newReplacedPath, err := filepath.Rel(modFileDir, newPkgDir+suffix) -- if err != nil { -- return nil, err -- } +-func BenchmarkRename(b *testing.B) { +- tests := []struct { +- repo string +- file string +- regexp string +- baseName string +- }{ +- {"google-cloud-go", "httpreplay/httpreplay.go", `func (NewRecorder)`, "NewRecorder"}, +- {"istio", "pkg/config/model.go", `(Namespace) string`, "Namespace"}, +- {"kubernetes", "pkg/controller/lookup_cache.go", `hashutil\.(DeepHashObject)`, "DeepHashObject"}, +- {"kuma", "pkg/events/interfaces.go", `Delete`, "Delete"}, +- {"pkgsite", "internal/log/log.go", `func (Infof)`, "Infof"}, +- {"starlark", "starlark/eval.go", `Program\) (Filename)`, "Filename"}, +- {"tools", "internal/lsp/cache/snapshot.go", `meta \*(metadataGraph)`, "metadataGraph"}, +- } - -- newReplacedPath = filepath.ToSlash(newReplacedPath) +- for _, test := range tests { +- names := 0 // bench function may execute multiple times +- b.Run(test.repo, func(b *testing.B) { +- env := getRepo(b, test.repo).sharedEnv(b) +- env.OpenFile(test.file) +- loc := env.RegexpSearch(test.file, test.regexp) +- env.Await(env.DoneWithOpen()) +- env.Rename(loc, test.baseName+"X") // pre-warm the query +- b.ResetTimer() - -- if !strings.HasPrefix(newReplacedPath, "/") && !strings.HasPrefix(newReplacedPath, "../") { -- newReplacedPath = "./" + newReplacedPath +- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, "rename")); stopAndRecord != nil { +- defer stopAndRecord() - } - -- if err := copied.AddReplace(r.Old.Path, "", newReplacedPath, ""); err != nil { -- return nil, err +- for i := 0; i < b.N; i++ { +- names++ +- newName := fmt.Sprintf("%s%d", test.baseName, names) +- env.Rename(loc, newName) - } -- } +- }) +- } +-} +diff -urN a/gopls/internal/test/integration/bench/repo_test.go b/gopls/internal/test/integration/bench/repo_test.go +--- a/gopls/internal/test/integration/bench/repo_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/bench/repo_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,290 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- copied.Cleanup() -- newContent, err := copied.Format() -- if err != nil { -- return nil, err -- } +-package bench - -- // Calculate the edits to be made due to the change. -- edits := s.Options().ComputeEdits(string(pm.Mapper.Content), string(newContent)) -- renamingEdits[pm.URI] = append(renamingEdits[pm.URI], edits...) -- } +-import ( +- "bytes" +- "context" +- "errors" +- "flag" +- "fmt" +- "log" +- "os" +- "path/filepath" +- "sync" +- "testing" +- "time" - -- return renamingEdits, nil --} +- . "golang.org/x/tools/gopls/internal/test/integration" +- "golang.org/x/tools/gopls/internal/test/integration/fake" +-) - --// renamePackage computes all workspace edits required to rename the package --// described by the given metadata, to newName, by renaming its package --// directory. +-// repos holds shared repositories for use in benchmarks. -// --// It updates package clauses and import paths for the renamed package as well --// as any other packages affected by the directory renaming among all packages --// known to the snapshot. --func renamePackage(ctx context.Context, s Snapshot, f FileHandle, newName PackageName) (map[span.URI][]diff.Edit, error) { -- if strings.HasSuffix(string(newName), "_test") { -- return nil, fmt.Errorf("cannot rename to _test package") -- } +-// These repos were selected to represent a variety of different types of +-// codebases. +-var repos = map[string]*repo{ +- // google-cloud-go has 145 workspace modules (!), and is quite large. +- "google-cloud-go": { +- name: "google-cloud-go", +- url: "https://github.com/googleapis/google-cloud-go.git", +- commit: "07da765765218debf83148cc7ed8a36d6e8921d5", +- inDir: flag.String("cloud_go_dir", "", "if set, reuse this directory as google-cloud-go@07da7657"), +- }, - -- // We need metadata for the relevant package and module paths. -- // These should be the same for all packages containing the file. -- meta, err := NarrowestMetadataForFile(ctx, s, f.URI()) -- if err != nil { -- return nil, err -- } +- // Used by x/benchmarks; large. +- "istio": { +- name: "istio", +- url: "https://github.com/istio/istio", +- commit: "1.17.0", +- inDir: flag.String("istio_dir", "", "if set, reuse this directory as istio@v1.17.0"), +- }, - -- oldPkgPath := meta.PkgPath -- if meta.Module == nil { -- return nil, fmt.Errorf("cannot rename package: missing module information for package %q", meta.PkgPath) -- } -- modulePath := PackagePath(meta.Module.Path) -- if modulePath == oldPkgPath { -- return nil, fmt.Errorf("cannot rename package: module path %q is the same as the package path, so renaming the package directory would have no effect", modulePath) -- } +- // Kubernetes is a large repo with many dependencies, and in the past has +- // been about as large a repo as gopls could handle. +- "kubernetes": { +- name: "kubernetes", +- url: "https://github.com/kubernetes/kubernetes", +- commit: "v1.24.0", +- short: true, +- inDir: flag.String("kubernetes_dir", "", "if set, reuse this directory as kubernetes@v1.24.0"), +- }, - -- newPathPrefix := path.Join(path.Dir(string(oldPkgPath)), string(newName)) +- // A large, industrial application. +- "kuma": { +- name: "kuma", +- url: "https://github.com/kumahq/kuma", +- commit: "2.1.1", +- inDir: flag.String("kuma_dir", "", "if set, reuse this directory as kuma@v2.1.1"), +- }, - -- // We must inspect all packages, not just direct importers, -- // because we also rename subpackages, which may be unrelated. -- // (If the renamed package imports a subpackage it may require -- // edits to both its package and import decls.) -- allMetadata, err := s.AllMetadata(ctx) -- if err != nil { -- return nil, err -- } +- // A repo containing a very large package (./dataintegration). +- "oracle": { +- name: "oracle", +- url: "https://github.com/oracle/oci-go-sdk.git", +- commit: "v65.43.0", +- short: true, +- inDir: flag.String("oracle_dir", "", "if set, reuse this directory as oracle/oci-go-sdk@v65.43.0"), +- }, - -- // Rename package and import declarations in all relevant packages. -- edits := make(map[span.URI][]diff.Edit) -- for _, m := range allMetadata { -- // Special case: x_test packages for the renamed package will not have the -- // package path as a dir prefix, but still need their package clauses -- // renamed. -- if m.PkgPath == oldPkgPath+"_test" { -- if err := renamePackageClause(ctx, m, s, newName+"_test", edits); err != nil { -- return nil, err -- } -- continue -- } +- // x/pkgsite is familiar and represents a common use case (a webserver). It +- // also has a number of static non-go files and template files. +- "pkgsite": { +- name: "pkgsite", +- url: "https://go.googlesource.com/pkgsite", +- commit: "81f6f8d4175ad0bf6feaa03543cc433f8b04b19b", +- short: true, +- inDir: flag.String("pkgsite_dir", "", "if set, reuse this directory as pkgsite@81f6f8d4"), +- }, - -- // Subtle: check this condition before checking for valid module info -- // below, because we should not fail this operation if unrelated packages -- // lack module info. -- if !strings.HasPrefix(string(m.PkgPath)+"/", string(oldPkgPath)+"/") { -- continue // not affected by the package renaming -- } +- // A tiny self-contained project. +- "starlark": { +- name: "starlark", +- url: "https://github.com/google/starlark-go", +- commit: "3f75dec8e4039385901a30981e3703470d77e027", +- short: true, +- inDir: flag.String("starlark_dir", "", "if set, reuse this directory as starlark@3f75dec8"), +- }, - -- if m.Module == nil { -- // This check will always fail under Bazel. -- return nil, fmt.Errorf("cannot rename package: missing module information for package %q", m.PkgPath) -- } +- // The current repository, which is medium-small and has very few dependencies. +- "tools": { +- name: "tools", +- url: "https://go.googlesource.com/tools", +- commit: "gopls/v0.9.0", +- short: true, +- inDir: flag.String("tools_dir", "", "if set, reuse this directory as x/tools@v0.9.0"), +- }, - -- if modulePath != PackagePath(m.Module.Path) { -- continue // don't edit imports if nested package and renaming package have different module paths -- } +- // A repo of similar size to kubernetes, but with substantially more +- // complex types that led to a serious performance regression (issue #60621). +- "hashiform": { +- name: "hashiform", +- url: "https://github.com/hashicorp/terraform-provider-aws", +- commit: "ac55de2b1950972d93feaa250d7505d9ed829c7c", +- inDir: flag.String("hashiform_dir", "", "if set, reuse this directory as hashiform@ac55de2"), +- }, +-} - -- // Renaming a package consists of changing its import path and package name. -- suffix := strings.TrimPrefix(string(m.PkgPath), string(oldPkgPath)) -- newPath := newPathPrefix + suffix +-// getRepo gets the requested repo, and skips the test if -short is set and +-// repo is not configured as a short repo. +-func getRepo(tb testing.TB, name string) *repo { +- tb.Helper() +- repo := repos[name] +- if repo == nil { +- tb.Fatalf("repo %s does not exist", name) +- } +- if !repo.short && testing.Short() { +- tb.Skipf("large repo %s does not run with -short", repo.name) +- } +- return repo +-} +- +-// A repo represents a working directory for a repository checked out at a +-// specific commit. +-// +-// Repos are used for sharing state across benchmarks that operate on the same +-// codebase. +-type repo struct { +- // static configuration +- name string // must be unique, used for subdirectory +- url string // repo url +- commit string // full commit hash or tag +- short bool // whether this repo runs with -short +- inDir *string // if set, use this dir as url@commit, and don't delete - -- pkgName := m.Name -- if m.PkgPath == oldPkgPath { -- pkgName = PackageName(newName) +- dirOnce sync.Once +- dir string // directory contaning source code checked out to url@commit - -- if err := renamePackageClause(ctx, m, s, newName, edits); err != nil { -- return nil, err -- } +- // shared editor state +- editorOnce sync.Once +- editor *fake.Editor +- sandbox *fake.Sandbox +- awaiter *Awaiter +-} +- +-// reusableDir return a reusable directory for benchmarking, or "". +-// +-// If the user specifies a directory, the test will create and populate it +-// on the first run an re-use it on subsequent runs. Otherwise it will +-// create, populate, and delete a temporary directory. +-func (r *repo) reusableDir() string { +- if r.inDir == nil { +- return "" +- } +- return *r.inDir +-} +- +-// getDir returns directory containing repo source code, creating it if +-// necessary. It is safe for concurrent use. +-func (r *repo) getDir() string { +- r.dirOnce.Do(func() { +- if r.dir = r.reusableDir(); r.dir == "" { +- r.dir = filepath.Join(getTempDir(), r.name) - } - -- imp := ImportPath(newPath) // TODO(adonovan): what if newPath has vendor/ prefix? -- if err := renameImports(ctx, s, m, imp, pkgName, edits); err != nil { -- return nil, err +- _, err := os.Stat(r.dir) +- switch { +- case os.IsNotExist(err): +- log.Printf("cloning %s@%s into %s", r.url, r.commit, r.dir) +- if err := shallowClone(r.dir, r.url, r.commit); err != nil { +- log.Fatal(err) +- } +- case err != nil: +- log.Fatal(err) +- default: +- log.Printf("reusing %s as %s@%s", r.dir, r.url, r.commit) - } -- } -- -- return edits, nil +- }) +- return r.dir -} - --// renamePackageClause computes edits renaming the package clause of files in --// the package described by the given metadata, to newName. +-// sharedEnv returns a shared benchmark environment. It is safe for concurrent +-// use. -// --// Edits are written into the edits map. --func renamePackageClause(ctx context.Context, m *Metadata, snapshot Snapshot, newName PackageName, edits map[span.URI][]diff.Edit) error { -- // Rename internal references to the package in the renaming package. -- for _, uri := range m.CompiledGoFiles { -- fh, err := snapshot.ReadFile(ctx, uri) +-// Every call to sharedEnv uses the same editor and sandbox, as a means to +-// avoid reinitializing the editor for large repos. Calling repo.Close cleans +-// up the shared environment. +-// +-// Repos in the package-local Repos var are closed at the end of the test main +-// function. +-func (r *repo) sharedEnv(tb testing.TB) *Env { +- r.editorOnce.Do(func() { +- dir := r.getDir() +- +- start := time.Now() +- log.Printf("starting initial workspace load for %s", r.name) +- ts, err := newGoplsConnector(profileArgs(r.name, false)) - if err != nil { -- return err +- log.Fatal(err) - } -- f, err := snapshot.ParseGo(ctx, fh, ParseHeader) +- r.sandbox, r.editor, r.awaiter, err = connectEditor(dir, fake.EditorConfig{}, ts) - if err != nil { -- return err -- } -- if f.File.Name == nil { -- continue // no package declaration +- log.Fatalf("connecting editor: %v", err) - } - -- edit, err := posEdit(f.Tok, f.File.Name.Pos(), f.File.Name.End(), string(newName)) -- if err != nil { -- return err +- if err := r.awaiter.Await(context.Background(), InitialWorkspaceLoad); err != nil { +- log.Fatal(err) - } -- edits[f.URI] = append(edits[f.URI], edit) -- } +- log.Printf("initial workspace load (cold) for %s took %v", r.name, time.Since(start)) +- }) - -- return nil +- return &Env{ +- T: tb, +- Ctx: context.Background(), +- Editor: r.editor, +- Sandbox: r.sandbox, +- Awaiter: r.awaiter, +- } -} - --// renameImports computes the set of edits to imports resulting from renaming --// the package described by the given metadata, to a package with import path --// newPath and name newName. +-// newEnv returns a new Env connected to a new gopls process communicating +-// over stdin/stdout. It is safe for concurrent use. -// --// Edits are written into the edits map. --func renameImports(ctx context.Context, snapshot Snapshot, m *Metadata, newPath ImportPath, newName PackageName, allEdits map[span.URI][]diff.Edit) error { -- rdeps, err := snapshot.ReverseDependencies(ctx, m.ID, false) // find direct importers +-// It is the caller's responsibility to call Close on the resulting Env when it +-// is no longer needed. +-func (r *repo) newEnv(tb testing.TB, config fake.EditorConfig, forOperation string, cpuProfile bool) *Env { +- dir := r.getDir() +- +- args := profileArgs(qualifiedName(r.name, forOperation), cpuProfile) +- ts, err := newGoplsConnector(args) - if err != nil { -- return err +- tb.Fatal(err) - } -- -- // Pass 1: rename import paths in import declarations. -- needsTypeCheck := make(map[PackageID][]span.URI) -- for _, rdep := range rdeps { -- if rdep.IsIntermediateTestVariant() { -- continue // for renaming, these variants are redundant -- } -- -- for _, uri := range rdep.CompiledGoFiles { -- fh, err := snapshot.ReadFile(ctx, uri) -- if err != nil { -- return err -- } -- f, err := snapshot.ParseGo(ctx, fh, ParseHeader) -- if err != nil { -- return err -- } -- if f.File.Name == nil { -- continue // no package declaration -- } -- for _, imp := range f.File.Imports { -- if rdep.DepsByImpPath[UnquoteImportPath(imp)] != m.ID { -- continue // not the import we're looking for -- } -- -- // If the import does not explicitly specify -- // a local name, then we need to invoke the -- // type checker to locate references to update. -- // -- // TODO(adonovan): is this actually true? -- // Renaming an import with a local name can still -- // cause conflicts: shadowing of built-ins, or of -- // package-level decls in the same or another file. -- if imp.Name == nil { -- needsTypeCheck[rdep.ID] = append(needsTypeCheck[rdep.ID], uri) -- } -- -- // Create text edit for the import path (string literal). -- edit, err := posEdit(f.Tok, imp.Path.Pos(), imp.Path.End(), strconv.Quote(string(newPath))) -- if err != nil { -- return err -- } -- allEdits[uri] = append(allEdits[uri], edit) -- } -- } +- sandbox, editor, awaiter, err := connectEditor(dir, config, ts) +- if err != nil { +- log.Fatalf("connecting editor: %v", err) - } - -- // If the imported package's name hasn't changed, -- // we don't need to rename references within each file. -- if newName == m.Name { -- return nil +- return &Env{ +- T: tb, +- Ctx: context.Background(), +- Editor: editor, +- Sandbox: sandbox, +- Awaiter: awaiter, - } +-} - -- // Pass 2: rename local name (types.PkgName) of imported -- // package throughout one or more files of the package. -- ids := make([]PackageID, 0, len(needsTypeCheck)) -- for id := range needsTypeCheck { -- ids = append(ids, id) +-// Close cleans up shared state referenced by the repo. +-func (r *repo) Close() error { +- var errBuf bytes.Buffer +- if r.editor != nil { +- if err := r.editor.Close(context.Background()); err != nil { +- fmt.Fprintf(&errBuf, "closing editor: %v", err) +- } - } -- pkgs, err := snapshot.TypeCheck(ctx, ids...) -- if err != nil { -- return err +- if r.sandbox != nil { +- if err := r.sandbox.Close(); err != nil { +- fmt.Fprintf(&errBuf, "closing sandbox: %v", err) +- } - } -- for i, id := range ids { -- pkg := pkgs[i] -- for _, uri := range needsTypeCheck[id] { -- f, err := pkg.File(uri) -- if err != nil { -- return err -- } -- for _, imp := range f.File.Imports { -- if imp.Name != nil { -- continue // has explicit local name -- } -- if rdeps[id].DepsByImpPath[UnquoteImportPath(imp)] != m.ID { -- continue // not the import we're looking for -- } -- -- pkgname := pkg.GetTypesInfo().Implicits[imp].(*types.PkgName) -- -- pkgScope := pkg.GetTypes().Scope() -- fileScope := pkg.GetTypesInfo().Scopes[f.File] -- -- localName := string(newName) -- try := 0 -- -- // Keep trying with fresh names until one succeeds. -- // -- // TODO(adonovan): fix: this loop is not sufficient to choose a name -- // that is guaranteed to be conflict-free; renameObj may still fail. -- // So the retry loop should be around renameObj, and we shouldn't -- // bother with scopes here. -- for fileScope.Lookup(localName) != nil || pkgScope.Lookup(localName) != nil { -- try++ -- localName = fmt.Sprintf("%s%d", newName, try) -- } -- -- // renameObj detects various conflicts, including: -- // - new name conflicts with a package-level decl in this file; -- // - new name hides a package-level decl in another file that -- // is actually referenced in this file; -- // - new name hides a built-in that is actually referenced -- // in this file; -- // - a reference in this file to the old package name would -- // become shadowed by an intervening declaration that -- // uses the new name. -- // It returns the edits if no conflict was detected. -- editMap, _, err := renameObjects(localName, pkg, pkgname) -- if err != nil { -- return err -- } -- -- // If the chosen local package name matches the package's -- // new name, delete the change that would have inserted -- // an explicit local name, which is always the lexically -- // first change. -- if localName == string(newName) { -- edits, ok := editMap[uri] -- if !ok { -- return fmt.Errorf("internal error: no changes for %s", uri) -- } -- diff.SortEdits(edits) -- editMap[uri] = edits[1:] -- } -- for uri, edits := range editMap { -- allEdits[uri] = append(allEdits[uri], edits...) -- } -- } +- if r.dir != "" && r.reusableDir() == "" { +- if err := os.RemoveAll(r.dir); err != nil { +- fmt.Fprintf(&errBuf, "cleaning dir: %v", err) - } - } +- if errBuf.Len() > 0 { +- return errors.New(errBuf.String()) +- } - return nil -} - --// renameObjects computes the edits to the type-checked syntax package pkg --// required to rename a set of target objects to newName. --// --// It also returns the set of objects that were found (due to --// corresponding methods and embedded fields) to require renaming as a --// consequence of the requested renamings. --// --// It returns an error if the renaming would cause a conflict. --func renameObjects(newName string, pkg Package, targets ...types.Object) (map[span.URI][]diff.Edit, map[types.Object]bool, error) { -- r := renamer{ -- pkg: pkg, -- objsToUpdate: make(map[types.Object]bool), -- from: targets[0].Name(), -- to: newName, -- } -- -- // A renaming initiated at an interface method indicates the -- // intention to rename abstract and concrete methods as needed -- // to preserve assignability. -- // TODO(adonovan): pull this into the caller. -- for _, obj := range targets { -- if obj, ok := obj.(*types.Func); ok { -- recv := obj.Type().(*types.Signature).Recv() -- if recv != nil && types.IsInterface(recv.Type().Underlying()) { -- r.changeMethods = true -- break -- } +-// cleanup cleans up state that is shared across benchmark functions. +-func cleanup() error { +- var errBuf bytes.Buffer +- for _, repo := range repos { +- if err := repo.Close(); err != nil { +- fmt.Fprintf(&errBuf, "closing %q: %v", repo.name, err) - } - } -- -- // Check that the renaming of the identifier is ok. -- for _, obj := range targets { -- r.check(obj) -- if len(r.conflicts) > 0 { -- // Stop at first error. -- return nil, nil, fmt.Errorf("%s", strings.Join(r.conflicts, "\n")) +- if tempDir != "" { +- if err := os.RemoveAll(tempDir); err != nil { +- fmt.Fprintf(&errBuf, "cleaning tempDir: %v", err) - } - } -- -- editMap, err := r.update() -- if err != nil { -- return nil, nil, err -- } -- -- // Remove initial targets so that only 'consequences' remain. -- for _, obj := range targets { -- delete(r.objsToUpdate, obj) +- if errBuf.Len() > 0 { +- return errors.New(errBuf.String()) - } -- return editMap, r.objsToUpdate, nil +- return nil -} +diff -urN a/gopls/internal/test/integration/bench/stress_test.go b/gopls/internal/test/integration/bench/stress_test.go +--- a/gopls/internal/test/integration/bench/stress_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/bench/stress_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,94 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// Rename all references to the target objects. --func (r *renamer) update() (map[span.URI][]diff.Edit, error) { -- result := make(map[span.URI][]diff.Edit) +-package bench - -- // shouldUpdate reports whether obj is one of (or an -- // instantiation of one of) the target objects. -- shouldUpdate := func(obj types.Object) bool { -- return containsOrigin(r.objsToUpdate, obj) +-import ( +- "context" +- "flag" +- "fmt" +- "testing" +- "time" +- +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/hooks" +- "golang.org/x/tools/gopls/internal/lsprpc" +- "golang.org/x/tools/gopls/internal/test/integration/fake" +- "golang.org/x/tools/internal/jsonrpc2" +- "golang.org/x/tools/internal/jsonrpc2/servertest" +-) +- +-// github.com/pilosa/pilosa is a repository that has historically caused +-// significant memory problems for Gopls. We use it for a simple stress test +-// that types arbitrarily in a file with lots of dependents. +- +-var pilosaPath = flag.String("pilosa_path", "", "Path to a directory containing "+ +- "github.com/pilosa/pilosa, for stress testing. Do not set this unless you "+ +- "know what you're doing!") +- +-func TestPilosaStress(t *testing.T) { +- // TODO(rfindley): revisit this test and make it is hermetic: it should check +- // out pilosa into a directory. +- // +- // Note: This stress test has not been run recently, and may no longer +- // function properly. +- if *pilosaPath == "" { +- t.Skip("-pilosa_path not configured") - } - -- // Find all identifiers in the package that define or use a -- // renamed object. We iterate over info as it is more efficient -- // than calling ast.Inspect for each of r.pkg.CompiledGoFiles(). -- type item struct { -- node ast.Node // Ident, ImportSpec (obj=PkgName), or CaseClause (obj=Var) -- obj types.Object -- isDef bool +- sandbox, err := fake.NewSandbox(&fake.SandboxConfig{ +- Workdir: *pilosaPath, +- GOPROXY: "https://proxy.golang.org", +- }) +- if err != nil { +- t.Fatal(err) - } -- var items []item -- info := r.pkg.GetTypesInfo() -- for id, obj := range info.Uses { -- if shouldUpdate(obj) { -- items = append(items, item{id, obj, false}) -- } +- +- server := lsprpc.NewStreamServer(cache.New(nil), false, hooks.Options) +- ts := servertest.NewPipeServer(server, jsonrpc2.NewRawStream) +- ctx := context.Background() +- +- const skipApplyEdits = false +- editor, err := fake.NewEditor(sandbox, fake.EditorConfig{}).Connect(ctx, ts, fake.ClientHooks{}, skipApplyEdits) +- if err != nil { +- t.Fatal(err) - } -- for id, obj := range info.Defs { -- if shouldUpdate(obj) { -- items = append(items, item{id, obj, true}) -- } +- +- files := []string{ +- "cmd.go", +- "internal/private.pb.go", +- "roaring/roaring.go", +- "roaring/roaring_internal_test.go", +- "server/handler_test.go", - } -- for node, obj := range info.Implicits { -- if shouldUpdate(obj) { -- switch node.(type) { -- case *ast.ImportSpec, *ast.CaseClause: -- items = append(items, item{node, obj, true}) -- } +- for _, file := range files { +- if err := editor.OpenFile(ctx, file); err != nil { +- t.Fatal(err) - } - } -- sort.Slice(items, func(i, j int) bool { -- return items[i].node.Pos() < items[j].node.Pos() -- }) -- -- // Update each identifier. -- for _, item := range items { -- pgf, ok := enclosingFile(r.pkg, item.node.Pos()) -- if !ok { -- bug.Reportf("edit does not belong to syntax of package %q", r.pkg) -- continue -- } -- -- // Renaming a types.PkgName may result in the addition or removal of an identifier, -- // so we deal with this separately. -- if pkgName, ok := item.obj.(*types.PkgName); ok && item.isDef { -- edit, err := r.updatePkgName(pgf, pkgName) -- if err != nil { -- return nil, err -- } -- result[pgf.URI] = append(result[pgf.URI], edit) -- continue -- } +- ctx, cancel := context.WithTimeout(ctx, 10*time.Minute) +- defer cancel() - -- // Workaround the unfortunate lack of a Var object -- // for x in "switch x := expr.(type) {}" by adjusting -- // the case clause to the switch ident. -- // This may result in duplicate edits, but we de-dup later. -- if _, ok := item.node.(*ast.CaseClause); ok { -- path, _ := astutil.PathEnclosingInterval(pgf.File, item.obj.Pos(), item.obj.Pos()) -- item.node = path[0].(*ast.Ident) +- i := 1 +- // MagicNumber is an identifier that occurs in roaring.go. Just change it +- // arbitrarily. +- if err := editor.RegexpReplace(ctx, "roaring/roaring.go", "MagicNumber", fmt.Sprintf("MagicNumber%d", 1)); err != nil { +- t.Fatal(err) +- } +- for { +- select { +- case <-ctx.Done(): +- return +- default: - } -- -- // Replace the identifier with r.to. -- edit, err := posEdit(pgf.Tok, item.node.Pos(), item.node.End(), r.to) -- if err != nil { -- return nil, err +- if err := editor.RegexpReplace(ctx, "roaring/roaring.go", fmt.Sprintf("MagicNumber%d", i), fmt.Sprintf("MagicNumber%d", i+1)); err != nil { +- t.Fatal(err) - } +- // Simulate (very fast) typing. +- // +- // Typing 80 wpm ~150ms per keystroke. +- time.Sleep(150 * time.Millisecond) +- i++ +- } +-} +diff -urN a/gopls/internal/test/integration/bench/typing_test.go b/gopls/internal/test/integration/bench/typing_test.go +--- a/gopls/internal/test/integration/bench/typing_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/bench/typing_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,63 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- result[pgf.URI] = append(result[pgf.URI], edit) -- -- if !item.isDef { // uses do not have doc comments to update. -- continue -- } +-package bench - -- doc := docComment(pgf, item.node.(*ast.Ident)) -- if doc == nil { -- continue -- } +-import ( +- "fmt" +- "sync/atomic" +- "testing" +- "time" - -- // Perform the rename in doc comments declared in the original package. -- // go/parser strips out \r\n returns from the comment text, so go -- // line-by-line through the comment text to get the correct positions. -- docRegexp := regexp.MustCompile(`\b` + r.from + `\b`) // valid identifier => valid regexp -- for _, comment := range doc.List { -- if isDirective(comment.Text) { -- continue -- } -- // TODO(adonovan): why are we looping over lines? -- // Just run the loop body once over the entire multiline comment. -- lines := strings.Split(comment.Text, "\n") -- tokFile := pgf.Tok -- commentLine := safetoken.Line(tokFile, comment.Pos()) -- uri := span.URIFromPath(tokFile.Name()) -- for i, line := range lines { -- lineStart := comment.Pos() -- if i > 0 { -- lineStart = tokFile.LineStart(commentLine + i) -- } -- for _, locs := range docRegexp.FindAllIndex([]byte(line), -1) { -- edit, err := posEdit(tokFile, lineStart+token.Pos(locs[0]), lineStart+token.Pos(locs[1]), r.to) -- if err != nil { -- return nil, err // can't happen -- } -- result[uri] = append(result[uri], edit) -- } -- } -- } -- } +- "golang.org/x/tools/gopls/internal/protocol" +-) - -- return result, nil --} +-// BenchmarkTyping simulates typing steadily in a single file at different +-// paces. +-// +-// The key metric for this benchmark is not latency, but cpu_seconds per +-// operation. +-func BenchmarkTyping(b *testing.B) { +- for _, test := range didChangeTests { +- b.Run(test.repo, func(b *testing.B) { +- env := getRepo(b, test.repo).sharedEnv(b) +- env.OpenFile(test.file) +- defer closeBuffer(b, env, test.file) - --// docComment returns the doc for an identifier within the specified file. --func docComment(pgf *ParsedGoFile, id *ast.Ident) *ast.CommentGroup { -- nodes, _ := astutil.PathEnclosingInterval(pgf.File, id.Pos(), id.End()) -- for _, node := range nodes { -- switch decl := node.(type) { -- case *ast.FuncDecl: -- return decl.Doc -- case *ast.Field: -- return decl.Doc -- case *ast.GenDecl: -- return decl.Doc -- // For {Type,Value}Spec, if the doc on the spec is absent, -- // search for the enclosing GenDecl -- case *ast.TypeSpec: -- if decl.Doc != nil { -- return decl.Doc -- } -- case *ast.ValueSpec: -- if decl.Doc != nil { -- return decl.Doc -- } -- case *ast.Ident: -- case *ast.AssignStmt: -- // *ast.AssignStmt doesn't have an associated comment group. -- // So, we try to find a comment just before the identifier. +- // Insert the text we'll be modifying at the top of the file. +- env.EditBuffer(test.file, protocol.TextEdit{NewText: "// __TEST_PLACEHOLDER_0__\n"}) +- env.AfterChange() - -- // Try to find a comment group only for short variable declarations (:=). -- if decl.Tok != token.DEFINE { -- return nil +- delays := []time.Duration{ +- 10 * time.Millisecond, // automated changes +- 50 * time.Millisecond, // very fast mashing, or fast key sequences +- 150 * time.Millisecond, // avg interval for 80wpm typing. - } - -- identLine := safetoken.Line(pgf.Tok, id.Pos()) -- for _, comment := range nodes[len(nodes)-1].(*ast.File).Comments { -- if comment.Pos() > id.Pos() { -- // Comment is after the identifier. -- continue -- } -- -- lastCommentLine := safetoken.Line(pgf.Tok, comment.End()) -- if lastCommentLine+1 == identLine { -- return comment -- } +- for _, delay := range delays { +- b.Run(delay.String(), func(b *testing.B) { +- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, "typing")); stopAndRecord != nil { +- defer stopAndRecord() +- } +- ticker := time.NewTicker(delay) +- for i := 0; i < b.N; i++ { +- edits := atomic.AddInt64(&editID, 1) +- env.EditBuffer(test.file, protocol.TextEdit{ +- Range: protocol.Range{ +- Start: protocol.Position{Line: 0, Character: 0}, +- End: protocol.Position{Line: 1, Character: 0}, +- }, +- // Increment the placeholder text, to ensure cache misses. +- NewText: fmt.Sprintf("// __TEST_PLACEHOLDER_%d__\n", edits), +- }) +- <-ticker.C +- } +- b.StopTimer() +- ticker.Stop() +- env.AfterChange() // wait for all change processing to complete +- }) - } -- default: -- return nil -- } +- }) - } -- return nil -} +diff -urN a/gopls/internal/test/integration/bench/workspace_symbols_test.go b/gopls/internal/test/integration/bench/workspace_symbols_test.go +--- a/gopls/internal/test/integration/bench/workspace_symbols_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/bench/workspace_symbols_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,41 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// updatePkgName returns the updates to rename a pkgName in the import spec by --// only modifying the package name portion of the import declaration. --func (r *renamer) updatePkgName(pgf *ParsedGoFile, pkgName *types.PkgName) (diff.Edit, error) { -- // Modify ImportSpec syntax to add or remove the Name as needed. -- path, _ := astutil.PathEnclosingInterval(pgf.File, pkgName.Pos(), pkgName.Pos()) -- if len(path) < 2 { -- return diff.Edit{}, fmt.Errorf("no path enclosing interval for %s", pkgName.Name()) -- } -- spec, ok := path[1].(*ast.ImportSpec) -- if !ok { -- return diff.Edit{}, fmt.Errorf("failed to update PkgName for %s", pkgName.Name()) -- } +-package bench - -- newText := "" -- if pkgName.Imported().Name() != r.to { -- newText = r.to + " " -- } +-import ( +- "flag" +- "fmt" +- "testing" +-) - -- // Replace the portion (possibly empty) of the spec before the path: -- // local "path" or "path" -- // -> <- -><- -- return posEdit(pgf.Tok, spec.Pos(), spec.Path.Pos(), newText) --} +-var symbolQuery = flag.String("symbol_query", "test", "symbol query to use in benchmark") - --// parsePackageNameDecl is a convenience function that parses and --// returns the package name declaration of file fh, and reports --// whether the position ppos lies within it. --// --// Note: also used by references. --func parsePackageNameDecl(ctx context.Context, snapshot Snapshot, fh FileHandle, ppos protocol.Position) (*ParsedGoFile, bool, error) { -- pgf, err := snapshot.ParseGo(ctx, fh, ParseHeader) -- if err != nil { -- return nil, false, err -- } -- // Careful: because we used ParseHeader, -- // pgf.Pos(ppos) may be beyond EOF => (0, err). -- pos, _ := pgf.PositionPos(ppos) -- return pgf, pgf.File.Name.Pos() <= pos && pos <= pgf.File.Name.End(), nil --} +-// BenchmarkWorkspaceSymbols benchmarks the time to execute a workspace symbols +-// request (controlled by the -symbol_query flag). +-func BenchmarkWorkspaceSymbols(b *testing.B) { +- for name := range repos { +- b.Run(name, func(b *testing.B) { +- env := getRepo(b, name).sharedEnv(b) +- symbols := env.Symbol(*symbolQuery) // warm the cache - --// enclosingFile returns the CompiledGoFile of pkg that contains the specified position. --func enclosingFile(pkg Package, pos token.Pos) (*ParsedGoFile, bool) { -- for _, pgf := range pkg.CompiledGoFiles() { -- if pgf.File.Pos() <= pos && pos <= pgf.File.End() { -- return pgf, true -- } -- } -- return nil, false --} +- if testing.Verbose() { +- fmt.Println("Results:") +- for i, symbol := range symbols { +- fmt.Printf("\t%d. %s (%s)\n", i, symbol.Name, symbol.ContainerName) +- } +- } - --// posEdit returns an edit to replace the (start, end) range of tf with 'new'. --func posEdit(tf *token.File, start, end token.Pos, new string) (diff.Edit, error) { -- startOffset, endOffset, err := safetoken.Offsets(tf, start, end) -- if err != nil { -- return diff.Edit{}, err +- b.ResetTimer() +- +- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(name, "workspaceSymbols")); stopAndRecord != nil { +- defer stopAndRecord() +- } +- +- for i := 0; i < b.N; i++ { +- env.Symbol(*symbolQuery) +- } +- }) - } -- return diff.Edit{Start: startOffset, End: endOffset, New: new}, nil -} -diff -urN a/gopls/internal/lsp/source/signature_help.go b/gopls/internal/lsp/source/signature_help.go ---- a/gopls/internal/lsp/source/signature_help.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/signature_help.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,198 +0,0 @@ --// Copyright 2018 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/test/integration/codelens/codelens_test.go b/gopls/internal/test/integration/codelens/codelens_test.go +--- a/gopls/internal/test/integration/codelens/codelens_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/codelens/codelens_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,405 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package source +-package codelens - -import ( -- "context" - "fmt" -- "go/ast" -- "go/token" -- "go/types" -- "strings" +- "testing" - -- "golang.org/x/tools/go/ast/astutil" -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/gopls/internal/hooks" +- "golang.org/x/tools/gopls/internal/server" +- "golang.org/x/tools/gopls/internal/test/compare" +- . "golang.org/x/tools/gopls/internal/test/integration" +- "golang.org/x/tools/gopls/internal/util/bug" +- +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/command" +- "golang.org/x/tools/internal/testenv" -) - --func SignatureHelp(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) (*protocol.SignatureInformation, int, error) { -- ctx, done := event.Start(ctx, "source.SignatureHelp") -- defer done() +-func TestMain(m *testing.M) { +- bug.PanicOnBugs = true +- Main(m, hooks.Options) +-} - -- // We need full type-checking here, as we must type-check function bodies in -- // order to provide signature help at the requested position. -- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) -- if err != nil { -- return nil, 0, fmt.Errorf("getting file for SignatureHelp: %w", err) -- } -- pos, err := pgf.PositionPos(position) -- if err != nil { -- return nil, 0, err -- } -- // Find a call expression surrounding the query position. -- var callExpr *ast.CallExpr -- path, _ := astutil.PathEnclosingInterval(pgf.File, pos, pos) -- if path == nil { -- return nil, 0, fmt.Errorf("cannot find node enclosing position") -- } --FindCall: -- for _, node := range path { -- switch node := node.(type) { -- case *ast.CallExpr: -- if pos >= node.Lparen && pos <= node.Rparen { -- callExpr = node -- break FindCall -- } -- case *ast.FuncLit, *ast.FuncType: -- // The user is within an anonymous function, -- // which may be the parameter to the *ast.CallExpr. -- // Don't show signature help in this case. -- return nil, 0, fmt.Errorf("no signature help within a function declaration") -- case *ast.BasicLit: -- if node.Kind == token.STRING { -- return nil, 0, fmt.Errorf("no signature help within a string literal") -- } -- } +-func TestDisablingCodeLens(t *testing.T) { +- const workspace = ` +--- go.mod -- +-module codelens.test +- +-go 1.12 +--- lib.go -- +-package lib +- +-type Number int +- +-const ( +- Zero Number = iota +- One +- Two +-) - +-//` + `go:generate stringer -type=Number +-` +- tests := []struct { +- label string +- enabled map[string]bool +- wantCodeLens bool +- }{ +- { +- label: "default", +- wantCodeLens: true, +- }, +- { +- label: "generate disabled", +- enabled: map[string]bool{string(command.Generate): false}, +- wantCodeLens: false, +- }, - } -- if callExpr == nil || callExpr.Fun == nil { -- return nil, 0, fmt.Errorf("cannot find an enclosing function") +- for _, test := range tests { +- t.Run(test.label, func(t *testing.T) { +- WithOptions( +- Settings{"codelenses": test.enabled}, +- ).Run(t, workspace, func(t *testing.T, env *Env) { +- env.OpenFile("lib.go") +- lens := env.CodeLens("lib.go") +- if gotCodeLens := len(lens) > 0; gotCodeLens != test.wantCodeLens { +- t.Errorf("got codeLens: %t, want %t", gotCodeLens, test.wantCodeLens) +- } +- }) +- }) - } +-} - -- qf := Qualifier(pgf.File, pkg.GetTypes(), pkg.GetTypesInfo()) +-const proxyWithLatest = ` +--- golang.org/x/hello@v1.3.3/go.mod -- +-module golang.org/x/hello - -- // Get the object representing the function, if available. -- // There is no object in certain cases such as calling a function returned by -- // a function (e.g. "foo()()"). -- var obj types.Object -- switch t := callExpr.Fun.(type) { -- case *ast.Ident: -- obj = pkg.GetTypesInfo().ObjectOf(t) -- case *ast.SelectorExpr: -- obj = pkg.GetTypesInfo().ObjectOf(t.Sel) -- } +-go 1.12 +--- golang.org/x/hello@v1.3.3/hi/hi.go -- +-package hi - -- // Built-in? -- if obj != nil && !obj.Pos().IsValid() { -- // built-in function? -- if obj, ok := obj.(*types.Builtin); ok { -- return builtinSignature(ctx, snapshot, callExpr, obj.Name(), pos) -- } +-var Goodbye error +--- golang.org/x/hello@v1.2.3/go.mod -- +-module golang.org/x/hello - -- // error.Error? -- if fn, ok := obj.(*types.Func); ok && fn.Name() == "Error" { -- return &protocol.SignatureInformation{ -- Label: "Error()", -- Documentation: stringToSigInfoDocumentation("Error returns the error message.", snapshot.Options()), -- }, 0, nil -- } +-go 1.12 +--- golang.org/x/hello@v1.2.3/hi/hi.go -- +-package hi - -- return nil, 0, bug.Errorf("call to unexpected built-in %v (%T)", obj, obj) -- } +-var Goodbye error +-` - -- // Get the type information for the function being called. -- sigType := pkg.GetTypesInfo().TypeOf(callExpr.Fun) -- if sigType == nil { -- return nil, 0, fmt.Errorf("cannot get type for Fun %[1]T (%[1]v)", callExpr.Fun) -- } +-// This test confirms the full functionality of the code lenses for updating +-// dependencies in a go.mod file, when using a go.work file. It checks for the +-// code lens that suggests an update and then executes the command associated +-// with that code lens. A regression test for golang/go#39446. It also checks +-// that these code lenses only affect the diagnostics and contents of the +-// containing go.mod file. +-func TestUpgradeCodelens_Workspace(t *testing.T) { +- const shouldUpdateDep = ` +--- go.work -- +-go 1.18 - -- sig, _ := sigType.Underlying().(*types.Signature) -- if sig == nil { -- return nil, 0, fmt.Errorf("cannot find signature for Fun %[1]T (%[1]v)", callExpr.Fun) -- } +-use ( +- ./a +- ./b +-) +--- a/go.mod -- +-module mod.com/a - -- activeParam := activeParameter(callExpr, sig.Params().Len(), sig.Variadic(), pos) +-go 1.14 - -- var ( -- name string -- comment *ast.CommentGroup -- ) -- if obj != nil { -- d, err := HoverDocForObject(ctx, snapshot, pkg.FileSet(), obj) -- if err != nil { -- return nil, 0, err -- } -- name = obj.Name() -- comment = d -- } else { -- name = "func" -- } -- mq := MetadataQualifierForFile(snapshot, pgf.File, pkg.Metadata()) -- s, err := NewSignature(ctx, snapshot, pkg, sig, comment, qf, mq) -- if err != nil { -- return nil, 0, err -- } -- paramInfo := make([]protocol.ParameterInformation, 0, len(s.params)) -- for _, p := range s.params { -- paramInfo = append(paramInfo, protocol.ParameterInformation{Label: p}) -- } -- return &protocol.SignatureInformation{ -- Label: name + s.Format(), -- Documentation: stringToSigInfoDocumentation(s.doc, snapshot.Options()), -- Parameters: paramInfo, -- }, activeParam, nil --} +-require golang.org/x/hello v1.2.3 +--- a/go.sum -- +-golang.org/x/hello v1.2.3 h1:7Wesfkx/uBd+eFgPrq0irYj/1XfmbvLV8jZ/W7C2Dwg= +-golang.org/x/hello v1.2.3/go.mod h1:OgtlzsxVMUUdsdQCIDYgaauCTH47B8T8vofouNJfzgY= +--- a/main.go -- +-package main - --func builtinSignature(ctx context.Context, snapshot Snapshot, callExpr *ast.CallExpr, name string, pos token.Pos) (*protocol.SignatureInformation, int, error) { -- sig, err := NewBuiltinSignature(ctx, snapshot, name) -- if err != nil { -- return nil, 0, err -- } -- paramInfo := make([]protocol.ParameterInformation, 0, len(sig.params)) -- for _, p := range sig.params { -- paramInfo = append(paramInfo, protocol.ParameterInformation{Label: p}) -- } -- activeParam := activeParameter(callExpr, len(sig.params), sig.variadic, pos) -- return &protocol.SignatureInformation{ -- Label: sig.name + sig.Format(), -- Documentation: stringToSigInfoDocumentation(sig.doc, snapshot.Options()), -- Parameters: paramInfo, -- }, activeParam, nil --} +-import "golang.org/x/hello/hi" - --func activeParameter(callExpr *ast.CallExpr, numParams int, variadic bool, pos token.Pos) (activeParam int) { -- if len(callExpr.Args) == 0 { -- return 0 -- } -- // First, check if the position is even in the range of the arguments. -- start, end := callExpr.Lparen, callExpr.Rparen -- if !(start <= pos && pos <= end) { -- return 0 -- } -- for _, expr := range callExpr.Args { -- if start == token.NoPos { -- start = expr.Pos() -- } -- end = expr.End() -- if start <= pos && pos <= end { -- break -- } -- // Don't advance the active parameter for the last parameter of a variadic function. -- if !variadic || activeParam < numParams-1 { -- activeParam++ -- } -- start = expr.Pos() + 1 // to account for commas -- } -- return activeParam +-func main() { +- _ = hi.Goodbye -} +--- b/go.mod -- +-module mod.com/b - --func stringToSigInfoDocumentation(s string, options *Options) *protocol.Or_SignatureInformation_documentation { -- v := s -- k := protocol.PlainText -- if options.PreferredContentFormat == protocol.Markdown { -- v = CommentToMarkdown(s, options) -- // whether or not content is newline terminated may not matter for LSP clients, -- // but our tests expect trailing newlines to be stripped. -- v = strings.TrimSuffix(v, "\n") // TODO(pjw): change the golden files -- k = protocol.Markdown -- } -- return &protocol.Or_SignatureInformation_documentation{ -- Value: protocol.MarkupContent{ -- Kind: k, -- Value: v, -- }, -- } --} -diff -urN a/gopls/internal/lsp/source/stub.go b/gopls/internal/lsp/source/stub.go ---- a/gopls/internal/lsp/source/stub.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/stub.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,250 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-go 1.14 - --package source +-require golang.org/x/hello v1.2.3 +--- b/go.sum -- +-golang.org/x/hello v1.2.3 h1:7Wesfkx/uBd+eFgPrq0irYj/1XfmbvLV8jZ/W7C2Dwg= +-golang.org/x/hello v1.2.3/go.mod h1:OgtlzsxVMUUdsdQCIDYgaauCTH47B8T8vofouNJfzgY= +--- b/main.go -- +-package main - -import ( -- "bytes" -- "context" -- "fmt" -- "go/format" -- "go/parser" -- "go/token" -- "go/types" -- "io" -- "path" -- "strings" -- -- "golang.org/x/tools/go/analysis" -- "golang.org/x/tools/go/ast/astutil" -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/analysis/stubmethods" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/internal/diff" -- "golang.org/x/tools/internal/tokeninternal" -- "golang.org/x/tools/internal/typeparams" +- "golang.org/x/hello/hi" -) - --// stubSuggestedFixFunc returns a suggested fix to declare the missing --// methods of the concrete type that is assigned to an interface type --// at the cursor position. --func stubSuggestedFixFunc(ctx context.Context, snapshot Snapshot, fh FileHandle, rng protocol.Range) (*token.FileSet, *analysis.SuggestedFix, error) { -- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) -- if err != nil { -- return nil, nil, fmt.Errorf("GetTypedFile: %w", err) -- } -- start, end, err := pgf.RangePos(rng) -- if err != nil { -- return nil, nil, err +-func main() { +- _ = hi.Goodbye +-} +-` +- +- const wantGoModA = `module mod.com/a +- +-go 1.14 +- +-require golang.org/x/hello v1.3.3 +-` +- // Applying the diagnostics or running the codelenses for a/go.mod +- // should not change the contents of b/go.mod +- const wantGoModB = `module mod.com/b +- +-go 1.14 +- +-require golang.org/x/hello v1.2.3 +-` +- +- for _, commandTitle := range []string{ +- "Upgrade transitive dependencies", +- "Upgrade direct dependencies", +- } { +- t.Run(commandTitle, func(t *testing.T) { +- WithOptions( +- ProxyFiles(proxyWithLatest), +- ).Run(t, shouldUpdateDep, func(t *testing.T, env *Env) { +- env.OpenFile("a/go.mod") +- env.OpenFile("b/go.mod") +- var lens protocol.CodeLens +- var found bool +- for _, l := range env.CodeLens("a/go.mod") { +- if l.Command.Title == commandTitle { +- lens = l +- found = true +- } +- } +- if !found { +- t.Fatalf("found no command with the title %s", commandTitle) +- } +- if _, err := env.Editor.ExecuteCommand(env.Ctx, &protocol.ExecuteCommandParams{ +- Command: lens.Command.Command, +- Arguments: lens.Command.Arguments, +- }); err != nil { +- t.Fatal(err) +- } +- env.AfterChange() +- if got := env.BufferText("a/go.mod"); got != wantGoModA { +- t.Fatalf("a/go.mod upgrade failed:\n%s", compare.Text(wantGoModA, got)) +- } +- if got := env.BufferText("b/go.mod"); got != wantGoModB { +- t.Fatalf("b/go.mod changed unexpectedly:\n%s", compare.Text(wantGoModB, got)) +- } +- }) +- }) - } -- nodes, _ := astutil.PathEnclosingInterval(pgf.File, start, end) -- si := stubmethods.GetStubInfo(pkg.FileSet(), pkg.GetTypesInfo(), nodes, start) -- if si == nil { -- return nil, nil, fmt.Errorf("nil interface request") +- for _, vendoring := range []bool{false, true} { +- t.Run(fmt.Sprintf("Upgrade individual dependency vendoring=%v", vendoring), func(t *testing.T) { +- WithOptions( +- ProxyFiles(proxyWithLatest), +- ).Run(t, shouldUpdateDep, func(t *testing.T, env *Env) { +- if vendoring { +- env.RunGoCommandInDirWithEnv("a", []string{"GOWORK=off"}, "mod", "vendor") +- } +- env.AfterChange() +- env.OpenFile("a/go.mod") +- env.OpenFile("b/go.mod") +- +- env.ExecuteCodeLensCommand("a/go.mod", command.CheckUpgrades, nil) +- d := &protocol.PublishDiagnosticsParams{} +- env.OnceMet( +- CompletedWork(server.DiagnosticWorkTitle(server.FromCheckUpgrades), 1, true), +- Diagnostics(env.AtRegexp("a/go.mod", `require`), WithMessage("can be upgraded")), +- ReadDiagnostics("a/go.mod", d), +- // We do not want there to be a diagnostic for b/go.mod, +- // but there may be some subtlety in timing here, where this +- // should always succeed, but may not actually test the correct +- // behavior. +- NoDiagnostics(env.AtRegexp("b/go.mod", `require`)), +- ) +- // Check for upgrades in b/go.mod and then clear them. +- env.ExecuteCodeLensCommand("b/go.mod", command.CheckUpgrades, nil) +- env.OnceMet( +- CompletedWork(server.DiagnosticWorkTitle(server.FromCheckUpgrades), 2, true), +- Diagnostics(env.AtRegexp("b/go.mod", `require`), WithMessage("can be upgraded")), +- ) +- env.ExecuteCodeLensCommand("b/go.mod", command.ResetGoModDiagnostics, nil) +- env.OnceMet( +- CompletedWork(server.DiagnosticWorkTitle(server.FromResetGoModDiagnostics), 1, true), +- NoDiagnostics(ForFile("b/go.mod")), +- ) +- +- // Apply the diagnostics to a/go.mod. +- env.ApplyQuickFixes("a/go.mod", d.Diagnostics) +- env.AfterChange() +- if got := env.BufferText("a/go.mod"); got != wantGoModA { +- t.Fatalf("a/go.mod upgrade failed:\n%s", compare.Text(wantGoModA, got)) +- } +- if got := env.BufferText("b/go.mod"); got != wantGoModB { +- t.Fatalf("b/go.mod changed unexpectedly:\n%s", compare.Text(wantGoModB, got)) +- } +- }) +- }) - } -- return stub(ctx, snapshot, si) -} - --// stub returns a suggested fix to declare the missing methods of si.Concrete. --func stub(ctx context.Context, snapshot Snapshot, si *stubmethods.StubInfo) (*token.FileSet, *analysis.SuggestedFix, error) { -- // A function-local type cannot be stubbed -- // since there's nowhere to put the methods. -- conc := si.Concrete.Obj() -- if conc.Parent() != conc.Pkg().Scope() { -- return nil, nil, fmt.Errorf("local type %q cannot be stubbed", conc.Name()) -- } +-func TestUpgradeCodelens_ModVendor(t *testing.T) { +- // This test checks the regression of golang/go#66055. The upgrade codelens +- // should work in a mod vendor context (the test above using a go.work file +- // was not broken). +- testenv.NeedsGo1Point(t, 22) +- const shouldUpdateDep = ` +--- go.mod -- +-module mod.com/a +- +-go 1.22 +- +-require golang.org/x/hello v1.2.3 +--- go.sum -- +-golang.org/x/hello v1.2.3 h1:7Wesfkx/uBd+eFgPrq0irYj/1XfmbvLV8jZ/W7C2Dwg= +-golang.org/x/hello v1.2.3/go.mod h1:OgtlzsxVMUUdsdQCIDYgaauCTH47B8T8vofouNJfzgY= +--- main.go -- +-package main +- +-import "golang.org/x/hello/hi" +- +-func main() { +- _ = hi.Goodbye +-} +-` +- +- const wantGoModA = `module mod.com/a - -- // Parse the file declaring the concrete type. -- declPGF, _, err := parseFull(ctx, snapshot, si.Fset, conc.Pos()) -- if err != nil { -- return nil, nil, fmt.Errorf("failed to parse file %q declaring implementation type: %w", declPGF.URI, err) -- } -- if declPGF.Fixed() { -- return nil, nil, fmt.Errorf("file contains parse errors: %s", declPGF.URI) -- } +-go 1.22 - -- // Build import environment for the declaring file. -- importEnv := make(map[ImportPath]string) // value is local name -- for _, imp := range declPGF.File.Imports { -- importPath := UnquoteImportPath(imp) -- var name string -- if imp.Name != nil { -- name = imp.Name.Name -- if name == "_" { -- continue -- } else if name == "." { -- name = "" // see types.Qualifier -- } -- } else { -- // TODO(adonovan): may omit a vendor/ prefix; consult the Metadata. -- name = path.Base(string(importPath)) -- } -- importEnv[importPath] = name // latest alias wins -- } +-require golang.org/x/hello v1.3.3 +-` - -- // Find subset of interface methods that the concrete type lacks. -- var missing []*types.Func -- ifaceType := si.Interface.Type().Underlying().(*types.Interface) -- for i := 0; i < ifaceType.NumMethods(); i++ { -- imethod := ifaceType.Method(i) -- cmethod, _, _ := types.LookupFieldOrMethod(si.Concrete, si.Pointer, imethod.Pkg(), imethod.Name()) -- if cmethod == nil { -- missing = append(missing, imethod) -- continue -- } +- WithOptions( +- ProxyFiles(proxyWithLatest), +- ).Run(t, shouldUpdateDep, func(t *testing.T, env *Env) { +- env.RunGoCommand("mod", "vendor") +- env.AfterChange() +- env.OpenFile("go.mod") - -- if _, ok := cmethod.(*types.Var); ok { -- // len(LookupFieldOrMethod.index) = 1 => conflict, >1 => shadow. -- return nil, nil, fmt.Errorf("adding method %s.%s would conflict with (or shadow) existing field", -- conc.Name(), imethod.Name()) -- } +- env.ExecuteCodeLensCommand("go.mod", command.CheckUpgrades, nil) +- d := &protocol.PublishDiagnosticsParams{} +- env.OnceMet( +- CompletedWork(server.DiagnosticWorkTitle(server.FromCheckUpgrades), 1, true), +- Diagnostics(env.AtRegexp("go.mod", `require`), WithMessage("can be upgraded")), +- ReadDiagnostics("go.mod", d), +- ) - -- if !types.Identical(cmethod.Type(), imethod.Type()) { -- return nil, nil, fmt.Errorf("method %s.%s already exists but has the wrong type: got %s, want %s", -- conc.Name(), imethod.Name(), cmethod.Type(), imethod.Type()) +- // Apply the diagnostics to a/go.mod. +- env.ApplyQuickFixes("go.mod", d.Diagnostics) +- env.AfterChange() +- if got := env.BufferText("go.mod"); got != wantGoModA { +- t.Fatalf("go.mod upgrade failed:\n%s", compare.Text(wantGoModA, got)) - } -- } -- if len(missing) == 0 { -- return nil, nil, fmt.Errorf("no missing methods found") -- } +- }) +-} - -- // Create a package name qualifier that uses the -- // locally appropriate imported package name. -- // It records any needed new imports. -- // TODO(adonovan): factor with source.FormatVarType, stubmethods.RelativeToFiles? -- // -- // Prior to CL 469155 this logic preserved any renaming -- // imports from the file that declares the interface -- // method--ostensibly the preferred name for imports of -- // frequently renamed packages such as protobufs. -- // Now we use the package's declared name. If this turns out -- // to be a mistake, then use parseHeader(si.iface.Pos()). -- // -- type newImport struct{ name, importPath string } -- var newImports []newImport // for AddNamedImport -- qual := func(pkg *types.Package) string { -- // TODO(adonovan): don't ignore vendor prefix. -- // -- // Ignore the current package import. -- if pkg.Path() == conc.Pkg().Path() { -- return "" -- } +-func TestUnusedDependenciesCodelens(t *testing.T) { +- const proxy = ` +--- golang.org/x/hello@v1.0.0/go.mod -- +-module golang.org/x/hello - -- importPath := ImportPath(pkg.Path()) -- name, ok := importEnv[importPath] -- if !ok { -- // Insert new import using package's declared name. -- // -- // TODO(adonovan): resolve conflict between declared -- // name and existing file-level (declPGF.File.Imports) -- // or package-level (si.Concrete.Pkg.Scope) decls by -- // generating a fresh name. -- name = pkg.Name() -- importEnv[importPath] = name -- new := newImport{importPath: string(importPath)} -- // For clarity, use a renaming import whenever the -- // local name does not match the path's last segment. -- if name != path.Base(new.importPath) { -- new.name = name -- } -- newImports = append(newImports, new) -- } -- return name -- } +-go 1.14 +--- golang.org/x/hello@v1.0.0/hi/hi.go -- +-package hi - -- // Format interface name (used only in a comment). -- iface := si.Interface.Name() -- if ipkg := si.Interface.Pkg(); ipkg != nil && ipkg != conc.Pkg() { -- iface = ipkg.Name() + "." + iface -- } +-var Goodbye error +--- golang.org/x/unused@v1.0.0/go.mod -- +-module golang.org/x/unused - -- // Pointer receiver? -- var star string -- if si.Pointer { -- star = "*" -- } +-go 1.14 +--- golang.org/x/unused@v1.0.0/nouse/nouse.go -- +-package nouse - -- // Format the new methods. -- var newMethods bytes.Buffer -- for _, method := range missing { -- fmt.Fprintf(&newMethods, `// %s implements %s. --func (%s%s%s) %s%s { -- panic("unimplemented") +-var NotUsed error +-` +- +- const shouldRemoveDep = ` +--- go.mod -- +-module mod.com +- +-go 1.14 +- +-require golang.org/x/hello v1.0.0 +-require golang.org/x/unused v1.0.0 +--- go.sum -- +-golang.org/x/hello v1.0.0 h1:qbzE1/qT0/zojAMd/JcPsO2Vb9K4Bkeyq0vB2JGMmsw= +-golang.org/x/hello v1.0.0/go.mod h1:WW7ER2MRNXWA6c8/4bDIek4Hc/+DofTrMaQQitGXcco= +-golang.org/x/unused v1.0.0 h1:LecSbCn5P3vTcxubungSt1Pn4D/WocCaiWOPDC0y0rw= +-golang.org/x/unused v1.0.0/go.mod h1:ihoW8SgWzugwwj0N2SfLfPZCxTB1QOVfhMfB5PWTQ8U= +--- main.go -- +-package main +- +-import "golang.org/x/hello/hi" +- +-func main() { +- _ = hi.Goodbye -} --`, -- method.Name(), -- iface, -- star, -- si.Concrete.Obj().Name(), -- FormatTypeParams(typeparams.ForNamed(si.Concrete)), -- method.Name(), -- strings.TrimPrefix(types.TypeString(method.Type(), qual), "func")) -- } +-` +- WithOptions(ProxyFiles(proxy)).Run(t, shouldRemoveDep, func(t *testing.T, env *Env) { +- env.OpenFile("go.mod") +- env.ExecuteCodeLensCommand("go.mod", command.Tidy, nil) +- env.AfterChange() +- got := env.BufferText("go.mod") +- const wantGoMod = `module mod.com - -- // Compute insertion point for new methods: -- // after the top-level declaration enclosing the (package-level) type. -- insertOffset, err := safetoken.Offset(declPGF.Tok, declPGF.File.End()) -- if err != nil { -- return nil, nil, bug.Errorf("internal error: end position outside file bounds: %v", err) -- } -- concOffset, err := safetoken.Offset(si.Fset.File(conc.Pos()), conc.Pos()) -- if err != nil { -- return nil, nil, bug.Errorf("internal error: finding type decl offset: %v", err) -- } -- for _, decl := range declPGF.File.Decls { -- declEndOffset, err := safetoken.Offset(declPGF.Tok, decl.End()) -- if err != nil { -- return nil, nil, bug.Errorf("internal error: finding decl offset: %v", err) -- } -- if declEndOffset > concOffset { -- insertOffset = declEndOffset -- break -- } -- } +-go 1.14 - -- // Splice the new methods into the file content. -- var buf bytes.Buffer -- input := declPGF.Mapper.Content // unfixed content of file -- buf.Write(input[:insertOffset]) -- buf.WriteByte('\n') -- io.Copy(&buf, &newMethods) -- buf.Write(input[insertOffset:]) +-require golang.org/x/hello v1.0.0 +-` +- if got != wantGoMod { +- t.Fatalf("go.mod tidy failed:\n%s", compare.Text(wantGoMod, got)) +- } +- }) +-} - -- // Re-parse the file. -- fset := token.NewFileSet() -- newF, err := parser.ParseFile(fset, declPGF.File.Name.Name, buf.Bytes(), parser.ParseComments) -- if err != nil { -- return nil, nil, fmt.Errorf("could not reparse file: %w", err) -- } +-func TestRegenerateCgo(t *testing.T) { +- testenv.NeedsTool(t, "cgo") +- const workspace = ` +--- go.mod -- +-module example.com - -- // Splice the new imports into the syntax tree. -- for _, imp := range newImports { -- astutil.AddNamedImport(fset, newF, imp.name, imp.importPath) -- } +-go 1.12 +--- cgo.go -- +-package x - -- // Pretty-print. -- var output strings.Builder -- if err := format.Node(&output, fset, newF); err != nil { -- return nil, nil, fmt.Errorf("format.Node: %w", err) -- } +-/* +-int fortythree() { return 42; } +-*/ +-import "C" - -- // Report the diff. -- diffs := snapshot.Options().ComputeEdits(string(input), output.String()) -- return tokeninternal.FileSetFor(declPGF.Tok), // edits use declPGF.Tok -- &analysis.SuggestedFix{TextEdits: diffToTextEdits(declPGF.Tok, diffs)}, -- nil +-func Foo() { +- print(C.fortytwo()) -} +-` +- Run(t, workspace, func(t *testing.T, env *Env) { +- // Open the file. We have a nonexistant symbol that will break cgo processing. +- env.OpenFile("cgo.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("cgo.go", ``), WithMessage("go list failed to return CompiledGoFiles")), +- ) - --func diffToTextEdits(tok *token.File, diffs []diff.Edit) []analysis.TextEdit { -- edits := make([]analysis.TextEdit, 0, len(diffs)) -- for _, edit := range diffs { -- edits = append(edits, analysis.TextEdit{ -- Pos: tok.Pos(edit.Start), -- End: tok.Pos(edit.End), -- NewText: []byte(edit.New), -- }) -- } -- return edits +- // Fix the C function name. We haven't regenerated cgo, so nothing should be fixed. +- env.RegexpReplace("cgo.go", `int fortythree`, "int fortytwo") +- env.SaveBuffer("cgo.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("cgo.go", ``), WithMessage("go list failed to return CompiledGoFiles")), +- ) +- +- // Regenerate cgo, fixing the diagnostic. +- env.ExecuteCodeLensCommand("cgo.go", command.RegenerateCgo, nil) +- env.OnceMet( +- CompletedWork(server.DiagnosticWorkTitle(server.FromRegenerateCgo), 1, true), +- NoDiagnostics(ForFile("cgo.go")), +- ) +- }) -} -diff -urN a/gopls/internal/lsp/source/symbols.go b/gopls/internal/lsp/source/symbols.go ---- a/gopls/internal/lsp/source/symbols.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/symbols.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,227 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/test/integration/codelens/gcdetails_test.go b/gopls/internal/test/integration/codelens/gcdetails_test.go +--- a/gopls/internal/test/integration/codelens/gcdetails_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/codelens/gcdetails_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,129 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package source +-package codelens - -import ( -- "context" -- "fmt" -- "go/ast" -- "go/token" -- "go/types" +- "runtime" +- "strings" +- "testing" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/command" +- "golang.org/x/tools/gopls/internal/server" +- . "golang.org/x/tools/gopls/internal/test/integration" +- "golang.org/x/tools/gopls/internal/test/integration/fake" +- "golang.org/x/tools/gopls/internal/util/bug" -) - --func DocumentSymbols(ctx context.Context, snapshot Snapshot, fh FileHandle) ([]protocol.DocumentSymbol, error) { -- ctx, done := event.Start(ctx, "source.DocumentSymbols") -- defer done() -- -- pgf, err := snapshot.ParseGo(ctx, fh, ParseFull) -- if err != nil { -- return nil, fmt.Errorf("getting file for DocumentSymbols: %w", err) +-func TestGCDetails_Toggle(t *testing.T) { +- if runtime.GOOS == "android" { +- t.Skipf("the gc details code lens doesn't work on Android") - } - -- // Build symbols for file declarations. When encountering a declaration with -- // errors (typically because positions are invalid), we skip the declaration -- // entirely. VS Code fails to show any symbols if one of the top-level -- // symbols is missing position information. -- var symbols []protocol.DocumentSymbol -- for _, decl := range pgf.File.Decls { -- switch decl := decl.(type) { -- case *ast.FuncDecl: -- if decl.Name.Name == "_" { -- continue -- } -- fs, err := funcSymbol(pgf.Mapper, pgf.Tok, decl) -- if err == nil { -- // If function is a method, prepend the type of the method. -- if decl.Recv != nil && len(decl.Recv.List) > 0 { -- fs.Name = fmt.Sprintf("(%s).%s", types.ExprString(decl.Recv.List[0].Type), fs.Name) -- } -- symbols = append(symbols, fs) +- const mod = ` +--- go.mod -- +-module mod.com +- +-go 1.15 +--- main.go -- +-package main +- +-import "fmt" +- +-func main() { +- fmt.Println(42) +-} +-` +- WithOptions( +- Settings{ +- "codelenses": map[string]bool{ +- "gc_details": true, +- }, +- }, +- ).Run(t, mod, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- env.ExecuteCodeLensCommand("main.go", command.GCDetails, nil) +- d := &protocol.PublishDiagnosticsParams{} +- env.OnceMet( +- CompletedWork(server.DiagnosticWorkTitle(server.FromToggleGCDetails), 1, true), +- ReadDiagnostics("main.go", d), +- ) +- // Confirm that the diagnostics come from the gc details code lens. +- var found bool +- for _, d := range d.Diagnostics { +- if d.Severity != protocol.SeverityInformation { +- t.Fatalf("unexpected diagnostic severity %v, wanted Information", d.Severity) - } -- case *ast.GenDecl: -- for _, spec := range decl.Specs { -- switch spec := spec.(type) { -- case *ast.TypeSpec: -- if spec.Name.Name == "_" { -- continue -- } -- ts, err := typeSymbol(pgf.Mapper, pgf.Tok, spec) -- if err == nil { -- symbols = append(symbols, ts) -- } -- case *ast.ValueSpec: -- for _, name := range spec.Names { -- if name.Name == "_" { -- continue -- } -- vs, err := varSymbol(pgf.Mapper, pgf.Tok, spec, name, decl.Tok == token.CONST) -- if err == nil { -- symbols = append(symbols, vs) -- } -- } -- } +- if strings.Contains(d.Message, "42 escapes") { +- found = true - } - } -- } -- return symbols, nil --} +- if !found { +- t.Fatalf(`expected to find diagnostic with message "escape(42 escapes to heap)", found none`) +- } - --func funcSymbol(m *protocol.Mapper, tf *token.File, decl *ast.FuncDecl) (protocol.DocumentSymbol, error) { -- s := protocol.DocumentSymbol{ -- Name: decl.Name.Name, -- Kind: protocol.Function, -- } -- if decl.Recv != nil { -- s.Kind = protocol.Method -- } -- var err error -- s.Range, err = m.NodeRange(tf, decl) -- if err != nil { -- return protocol.DocumentSymbol{}, err -- } -- s.SelectionRange, err = m.NodeRange(tf, decl.Name) -- if err != nil { -- return protocol.DocumentSymbol{}, err -- } -- s.Detail = types.ExprString(decl.Type) -- return s, nil --} +- // Editing a buffer should cause gc_details diagnostics to disappear, since +- // they only apply to saved buffers. +- env.EditBuffer("main.go", fake.NewEdit(0, 0, 0, 0, "\n\n")) +- env.AfterChange(NoDiagnostics(ForFile("main.go"))) - --func typeSymbol(m *protocol.Mapper, tf *token.File, spec *ast.TypeSpec) (protocol.DocumentSymbol, error) { -- s := protocol.DocumentSymbol{ -- Name: spec.Name.Name, -- } -- var err error -- s.Range, err = m.NodeRange(tf, spec) -- if err != nil { -- return protocol.DocumentSymbol{}, err -- } -- s.SelectionRange, err = m.NodeRange(tf, spec.Name) -- if err != nil { -- return protocol.DocumentSymbol{}, err -- } -- s.Kind, s.Detail, s.Children = typeDetails(m, tf, spec.Type) -- return s, nil --} +- // Saving a buffer should re-format back to the original state, and +- // re-enable the gc_details diagnostics. +- env.SaveBuffer("main.go") +- env.AfterChange(Diagnostics(AtPosition("main.go", 5, 13))) - --func typeDetails(m *protocol.Mapper, tf *token.File, typExpr ast.Expr) (kind protocol.SymbolKind, detail string, children []protocol.DocumentSymbol) { -- switch typExpr := typExpr.(type) { -- case *ast.StructType: -- kind = protocol.Struct -- children = fieldListSymbols(m, tf, typExpr.Fields, protocol.Field) -- if len(children) > 0 { -- detail = "struct{...}" -- } else { -- detail = "struct{}" -- } +- // Toggle the GC details code lens again so now it should be off. +- env.ExecuteCodeLensCommand("main.go", command.GCDetails, nil) +- env.OnceMet( +- CompletedWork(server.DiagnosticWorkTitle(server.FromToggleGCDetails), 2, true), +- NoDiagnostics(ForFile("main.go")), +- ) +- }) +-} - -- // Find interface methods and embedded types. -- case *ast.InterfaceType: -- kind = protocol.Interface -- children = fieldListSymbols(m, tf, typExpr.Methods, protocol.Method) -- if len(children) > 0 { -- detail = "interface{...}" -- } else { -- detail = "interface{}" -- } +-// Test for the crasher in golang/go#54199 +-func TestGCDetails_NewFile(t *testing.T) { +- bug.PanicOnBugs = false +- const src = ` +--- go.mod -- +-module mod.test - -- case *ast.FuncType: -- kind = protocol.Function -- detail = types.ExprString(typExpr) +-go 1.12 +-` - -- default: -- kind = protocol.Class // catch-all, for cases where we don't know the kind syntactically -- detail = types.ExprString(typExpr) -- } -- return --} +- WithOptions( +- Settings{ +- "codelenses": map[string]bool{ +- "gc_details": true, +- }, +- }, +- ).Run(t, src, func(t *testing.T, env *Env) { +- env.CreateBuffer("p_test.go", "") - --func fieldListSymbols(m *protocol.Mapper, tf *token.File, fields *ast.FieldList, fieldKind protocol.SymbolKind) []protocol.DocumentSymbol { -- if fields == nil { -- return nil -- } +- hasGCDetails := func() bool { +- lenses := env.CodeLens("p_test.go") // should not crash +- for _, lens := range lenses { +- if lens.Command.Command == command.GCDetails.ID() { +- return true +- } +- } +- return false +- } - -- var symbols []protocol.DocumentSymbol -- for _, field := range fields.List { -- detail, children := "", []protocol.DocumentSymbol(nil) -- if field.Type != nil { -- _, detail, children = typeDetails(m, tf, field.Type) +- // With an empty file, we shouldn't get the gc_details codelens because +- // there is nowhere to position it (it needs a package name). +- if hasGCDetails() { +- t.Errorf("got the gc_details codelens for an empty file") - } -- if len(field.Names) == 0 { // embedded interface or struct field -- // By default, use the formatted type details as the name of this field. -- // This handles potentially invalid syntax, as well as type embeddings in -- // interfaces. -- child := protocol.DocumentSymbol{ -- Name: detail, -- Kind: protocol.Field, // consider all embeddings to be fields -- Children: children, -- } - -- // If the field is a valid embedding, promote the type name to field -- // name. -- selection := field.Type -- if id := embeddedIdent(field.Type); id != nil { -- child.Name = id.Name -- child.Detail = detail -- selection = id -- } +- // Edit to provide a package name. +- env.EditBuffer("p_test.go", fake.NewEdit(0, 0, 0, 0, "package p")) +- +- // Now we should get the gc_details codelens. +- if !hasGCDetails() { +- t.Errorf("didn't get the gc_details codelens for a valid non-empty Go file") +- } +- }) +-} +diff -urN a/gopls/internal/test/integration/completion/completion18_test.go b/gopls/internal/test/integration/completion/completion18_test.go +--- a/gopls/internal/test/integration/completion/completion18_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/completion/completion18_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,121 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- if rng, err := m.NodeRange(tf, field.Type); err == nil { -- child.Range = rng -- } -- if rng, err := m.NodeRange(tf, selection); err == nil { -- child.SelectionRange = rng -- } +-package completion - -- symbols = append(symbols, child) -- } else { -- for _, name := range field.Names { -- child := protocol.DocumentSymbol{ -- Name: name.Name, -- Kind: fieldKind, -- Detail: detail, -- Children: children, -- } +-import ( +- "testing" - -- if rng, err := m.NodeRange(tf, field); err == nil { -- child.Range = rng -- } -- if rng, err := m.NodeRange(tf, name); err == nil { -- child.SelectionRange = rng -- } +- "golang.org/x/tools/gopls/internal/protocol" +- . "golang.org/x/tools/gopls/internal/test/integration" +-) - -- symbols = append(symbols, child) +-// test generic receivers +-func TestGenericReceiver(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com +- +-go 1.18 +--- main.go -- +-package main +-type SyncMap[K any, V comparable] struct {} +-func (s *SyncMap[K,V]) f() {} +-type XX[T any] struct {} +-type UU[T any] struct {} +-func (s SyncMap[XX,string]) g(v UU) {} +-` +- +- tests := []struct { +- pat string +- want []string +- }{ +- {"s .Syn", []string{"SyncMap[K, V]"}}, +- {"Map.X", []string{}}, // This is probably wrong, Maybe "XX"? +- {"v U", []string{"UU", "uint", "uint16", "uint32", "uint64", "uint8", "uintptr"}}, // not U[T] +- } +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- env.Await(env.DoneWithOpen()) +- for _, tst := range tests { +- loc := env.RegexpSearch("main.go", tst.pat) +- loc.Range.Start.Character += uint32(protocol.UTF16Len([]byte(tst.pat))) +- completions := env.Completion(loc) +- result := compareCompletionLabels(tst.want, completions.Items) +- if result != "" { +- t.Errorf("%s: wanted %v", result, tst.want) +- for i, g := range completions.Items { +- t.Errorf("got %d %s %s", i, g.Label, g.Detail) +- } - } - } +- }) +-} +-func TestFuzzFunc(t *testing.T) { +- // use the example from the package documentation +- modfile := ` +--- go.mod -- +-module mod.com - +-go 1.18 +-` +- part0 := `package foo +-import "testing" +-func FuzzNone(f *testing.F) { +- f.Add(12) // better not find this f.Add +-} +-func FuzzHex(f *testing.F) { +- for _, seed := range [][]byte{{}, {0}, {9}, {0xa}, {0xf}, {1, 2, 3, 4}} { +- f.Ad` +- part1 := `d(seed) - } -- return symbols +- f.F` +- part2 := `uzz(func(t *testing.T, in []byte) { +- enc := hex.EncodeToString(in) +- out, err := hex.DecodeString(enc) +- if err != nil { +- f.Failed() +- } +- if !bytes.Equal(in, out) { +- t.Fatalf("%v: round trip: %v, %s", in, out, f.Name()) +- } +- }) -} +-` +- data := modfile + `-- a_test.go -- +-` + part0 + ` +--- b_test.go -- +-` + part0 + part1 + ` +--- c_test.go -- +-` + part0 + part1 + part2 - --func varSymbol(m *protocol.Mapper, tf *token.File, spec *ast.ValueSpec, name *ast.Ident, isConst bool) (protocol.DocumentSymbol, error) { -- s := protocol.DocumentSymbol{ -- Name: name.Name, -- Kind: protocol.Variable, -- } -- if isConst { -- s.Kind = protocol.Constant -- } -- var err error -- s.Range, err = m.NodeRange(tf, spec) -- if err != nil { -- return protocol.DocumentSymbol{}, err -- } -- s.SelectionRange, err = m.NodeRange(tf, name) -- if err != nil { -- return protocol.DocumentSymbol{}, err -- } -- if spec.Type != nil { // type may be missing from the syntax -- _, s.Detail, s.Children = typeDetails(m, tf, spec.Type) +- tests := []struct { +- file string +- pat string +- offset uint32 // UTF16 length from the beginning of pat to what the user just typed +- want []string +- }{ +- {"a_test.go", "f.Ad", 3, []string{"Add"}}, +- {"c_test.go", " f.F", 4, []string{"Failed"}}, +- {"c_test.go", "f.N", 3, []string{"Name"}}, +- {"b_test.go", "f.F", 3, []string{"Fuzz(func(t *testing.T, a []byte)", "Fail", "FailNow", +- "Failed", "Fatal", "Fatalf"}}, - } -- return s, nil +- Run(t, data, func(t *testing.T, env *Env) { +- for _, test := range tests { +- env.OpenFile(test.file) +- env.Await(env.DoneWithOpen()) +- loc := env.RegexpSearch(test.file, test.pat) +- loc.Range.Start.Character += test.offset // character user just typed? will type? +- completions := env.Completion(loc) +- result := compareCompletionLabels(test.want, completions.Items) +- if result != "" { +- t.Errorf("pat %q %q", test.pat, result) +- for i, it := range completions.Items { +- t.Errorf("%d got %q %q", i, it.Label, it.Detail) +- } +- } +- } +- }) -} -diff -urN a/gopls/internal/lsp/source/type_definition.go b/gopls/internal/lsp/source/type_definition.go ---- a/gopls/internal/lsp/source/type_definition.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/type_definition.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,57 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/test/integration/completion/completion_test.go b/gopls/internal/test/integration/completion/completion_test.go +--- a/gopls/internal/test/integration/completion/completion_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/completion/completion_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,1065 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package source +-package completion - -import ( -- "context" - "fmt" -- "go/token" +- "sort" +- "strings" +- "testing" +- "time" - -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/internal/event" +- "github.com/google/go-cmp/cmp" +- "golang.org/x/telemetry/counter" +- "golang.org/x/telemetry/counter/countertest" +- "golang.org/x/tools/gopls/internal/hooks" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/server" +- . "golang.org/x/tools/gopls/internal/test/integration" +- "golang.org/x/tools/gopls/internal/test/integration/fake" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/internal/testenv" -) - --// TypeDefinition handles the textDocument/typeDefinition request for Go files. --func TypeDefinition(ctx context.Context, snapshot Snapshot, fh FileHandle, position protocol.Position) ([]protocol.Location, error) { -- ctx, done := event.Start(ctx, "source.TypeDefinition") -- defer done() +-func TestMain(m *testing.M) { +- bug.PanicOnBugs = true +- Main(m, hooks.Options) +-} - -- pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI()) -- if err != nil { -- return nil, err -- } -- pos, err := pgf.PositionPos(position) -- if err != nil { -- return nil, err -- } +-const proxy = ` +--- example.com@v1.2.3/go.mod -- +-module example.com - -- // TODO(rfindley): handle type switch implicits correctly here: if the user -- // jumps to the type definition of x in x := y.(type), it makes sense to jump -- // to the type of y. -- _, obj, _ := referencedObject(pkg, pgf, pos) -- if obj == nil { -- return nil, nil -- } +-go 1.12 +--- example.com@v1.2.3/blah/blah.go -- +-package blah - -- tname := typeToObject(obj.Type()) -- if tname == nil { -- return nil, fmt.Errorf("no type definition for %s", obj.Name()) -- } +-const Name = "Blah" +--- random.org@v1.2.3/go.mod -- +-module random.org - -- if !tname.Pos().IsValid() { -- // The only defined types with no position are error and comparable. -- if tname.Name() != "error" && tname.Name() != "comparable" { -- bug.Reportf("unexpected type name with no position: %s", tname) -- } -- return nil, nil -- } +-go 1.12 +--- random.org@v1.2.3/blah/blah.go -- +-package hello - -- loc, err := mapPosition(ctx, pkg.FileSet(), snapshot, tname.Pos(), tname.Pos()+token.Pos(len(tname.Name()))) -- if err != nil { -- return nil, err -- } -- return []protocol.Location{loc}, nil +-const Name = "Hello" +-` +- +-func TestPackageCompletion(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com +- +-go 1.12 +--- fruits/apple.go -- +-package apple +- +-fun apple() int { +- return 0 -} -diff -urN a/gopls/internal/lsp/source/typerefs/doc.go b/gopls/internal/lsp/source/typerefs/doc.go ---- a/gopls/internal/lsp/source/typerefs/doc.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/typerefs/doc.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,151 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --// Package typerefs extracts symbol-level reachability information --// from the syntax of a Go package. --// --// # Background --// --// The goal of this analysis is to determine, for each package P, a nearly --// minimal set of packages that could affect the type checking of P. This set --// may contain false positives, but the smaller this set the better we can --// invalidate and prune packages in gopls. --// --// More precisely, for each package P we define the set of "reachable" packages --// from P as the set of packages that may affect the (deep) export data of the --// direct dependencies of P. By this definition, the complement of this set --// cannot affect any information derived from type checking P, such as --// diagnostics, cross references, or method sets. Therefore we need not --// invalidate any results for P when a package in the complement of this set --// changes. --// --// # Computing references --// --// For a given declaration D, references are computed based on identifiers or --// dotted identifiers referenced in the declaration of D, that may affect --// the type of D. However, these references reflect only local knowledge of the --// package and its dependency metadata, and do not depend on any analysis of --// the dependencies themselves. This allows the reference information for --// a package to be cached independent of all others. --// --// Specifically, if a referring identifier I appears in the declaration, we --// record an edge from D to each object possibly referenced by I. We search for --// references within type syntax, but do not actually type-check, so we can't --// reliably determine whether an expression is a type or a term, or whether a --// function is a builtin or generic. For example, the type of x in var x = --// p.F(W) only depends on W if p.F is a builtin or generic function, which we --// cannot know without type-checking package p. So we may over-approximate in --// this way. --// --// - If I is declared in the current package, record a reference to its --// declaration. --// - Otherwise, if there are any dot imports in the current --// file and I is exported, record a (possibly dangling) edge to --// the corresponding declaration in each dot-imported package. --// --// If a dotted identifier q.I appears in the declaration, we --// perform a similar operation: --// --// - If q is declared in the current package, we record a reference to that --// object. It may be a var or const that has a field or method I. --// - Otherwise, if q is a valid import name based on imports in the current file --// and the provided metadata for dependency package names, record a --// reference to the object I in that package. --// - Additionally, handle the case where Q is exported, and Q.I may refer to --// a field or method in a dot-imported package. --// --// That is essentially the entire algorithm, though there is some subtlety to --// visiting the set of identifiers or dotted identifiers that may affect the --// declaration type. See the visitDeclOrSpec function for the details of this --// analysis. Notably, we also skip identifiers that refer to type parameters in --// generic declarations. --// --// # Graph optimizations --// --// The references extracted from the syntax are used to construct --// edges between nodes representing declarations. Edges are of two --// kinds: internal references, from one package-level declaration to --// another; and external references, from a symbol in this package to --// a symbol imported from a direct dependency. --// --// Once the symbol reference graph is constructed, we find its --// strongly connected components (SCCs) using Tarjan's algorithm. --// As we coalesce the nodes of each SCC we compute the union of --// external references reached by each package-level declaration. --// The final result is the mapping from each exported package-level --// declaration to the set of external (imported) declarations that it --// reaches. --// --// Because it is common for many package members to have the same --// reachability, the result takes the form of a set of equivalence --// classes, each mapping a set of package-level declarations to a set --// of external symbols. We use a hash table to canonicalize sets so that --// repeated occurrences of the same set (which are common) are only --// represented once in memory or in the file system. --// For example, all declarations that ultimately reference only --// {fmt.Println,strings.Join} would be classed as equivalent. --// --// This approach was inspired by the Hash-Value Numbering (HVN) --// optimization described by Hardekopf and Lin. See --// golang.org/x/tools/go/pointer/hvn.go for an implementation. (Like --// pointer analysis, this problem is fundamentally one of graph --// reachability.) The HVN algorithm takes the compression a step --// further by preserving the topology of the SCC DAG, in which edges --// represent "is a superset of" constraints. Redundant edges that --// don't increase the solution can be deleted. We could apply the same --// technique here to further reduce the worst-case size of the result, --// but the current implementation seems adequate. --// --// # API --// --// The main entry point for this analysis is the [Encode] function, --// which implements the analysis described above for one package, and --// encodes the result as a binary message. --// --// The [Decode] function decodes the message into a usable form: a set --// of equivalence classes. The decoder uses a shared [PackageIndex] to --// enable more compact representations of sets of packages --// ([PackageSet]) during the global reacahability computation. --// --// The [BuildPackageGraph] constructor implements a whole-graph analysis similar --// to that which will be implemented by gopls, but for various reasons the --// logic for this analysis will eventually live in the --// [golang.org/x/tools/gopls/internal/lsp/cache] package. Nevertheless, --// BuildPackageGraph and its test serve to verify the syntactic analysis, and --// may serve as a proving ground for new optimizations of the whole-graph analysis. --// --// # Export data is insufficient --// --// At first it may seem that the simplest way to implement this analysis would --// be to consider the types.Packages of the dependencies of P, for example --// during export. After all, it makes sense that the type checked packages --// themselves could describe their dependencies. However, this does not work as --// type information does not describe certain syntactic relationships. --// --// For example, the following scenarios cause type information to miss --// syntactic relationships: --// --// Named type forwarding: --// --// package a; type A b.B --// package b; type B int --// --// Aliases: --// --// package a; func A(f b.B) --// package b; type B = func() --// --// Initializers: --// --// package a; var A = b.B() --// package b; func B() string { return "hi" } --// --// Use of the unsafe package: --// --// package a; type A [unsafe.Sizeof(B{})]int --// package b; type B struct { f1, f2, f3 int } --// --// In all of these examples, types do not contain information about the edge --// between the a.A and b.B declarations. --package typerefs -diff -urN a/gopls/internal/lsp/source/typerefs/packageset.go b/gopls/internal/lsp/source/typerefs/packageset.go ---- a/gopls/internal/lsp/source/typerefs/packageset.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/typerefs/packageset.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,148 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +--- fruits/testfile.go -- +-// this is a comment +- +-/* +- this is a multiline comment +-*/ +- +-import "fmt" +- +-func test() {} +- +--- fruits/testfile2.go -- +-package +- +--- fruits/testfile3.go -- +-pac +--- 123f_r.u~its-123/testfile.go -- +-package +- +--- .invalid-dir@-name/testfile.go -- +-package +-` +- var ( +- testfile4 = "" +- testfile5 = "/*a comment*/ " +- testfile6 = "/*a comment*/\n" +- ) +- for _, tc := range []struct { +- name string +- filename string +- content *string +- triggerRegexp string +- want []string +- editRegexp string +- }{ +- { +- name: "package completion at valid position", +- filename: "fruits/testfile.go", +- triggerRegexp: "\n()", +- want: []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"}, +- editRegexp: "\n()", +- }, +- { +- name: "package completion in a comment", +- filename: "fruits/testfile.go", +- triggerRegexp: "th(i)s", +- want: nil, +- }, +- { +- name: "package completion in a multiline comment", +- filename: "fruits/testfile.go", +- triggerRegexp: `\/\*\n()`, +- want: nil, +- }, +- { +- name: "package completion at invalid position", +- filename: "fruits/testfile.go", +- triggerRegexp: "import \"fmt\"\n()", +- want: nil, +- }, +- { +- name: "package completion after keyword 'package'", +- filename: "fruits/testfile2.go", +- triggerRegexp: "package()", +- want: []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"}, +- editRegexp: "package\n", +- }, +- { +- name: "package completion with 'pac' prefix", +- filename: "fruits/testfile3.go", +- triggerRegexp: "pac()", +- want: []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"}, +- editRegexp: "pac", +- }, +- { +- name: "package completion for empty file", +- filename: "fruits/testfile4.go", +- triggerRegexp: "^$", +- content: &testfile4, +- want: []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"}, +- editRegexp: "^$", +- }, +- { +- name: "package completion without terminal newline", +- filename: "fruits/testfile5.go", +- triggerRegexp: `\*\/ ()`, +- content: &testfile5, +- want: []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"}, +- editRegexp: `\*\/ ()`, +- }, +- { +- name: "package completion on terminal newline", +- filename: "fruits/testfile6.go", +- triggerRegexp: `\*\/\n()`, +- content: &testfile6, +- want: []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"}, +- editRegexp: `\*\/\n()`, +- }, +- // Issue golang/go#44680 +- { +- name: "package completion for dir name with punctuation", +- filename: "123f_r.u~its-123/testfile.go", +- triggerRegexp: "package()", +- want: []string{"package fruits123", "package fruits123_test", "package main"}, +- editRegexp: "package\n", +- }, +- { +- name: "package completion for invalid dir name", +- filename: ".invalid-dir@-name/testfile.go", +- triggerRegexp: "package()", +- want: []string{"package main"}, +- editRegexp: "package\n", +- }, +- } { +- t.Run(tc.name, func(t *testing.T) { +- Run(t, files, func(t *testing.T, env *Env) { +- if tc.content != nil { +- env.WriteWorkspaceFile(tc.filename, *tc.content) +- env.Await(env.DoneWithChangeWatchedFiles()) +- } +- env.OpenFile(tc.filename) +- completions := env.Completion(env.RegexpSearch(tc.filename, tc.triggerRegexp)) +- +- // Check that the completion item suggestions are in the range +- // of the file. {Start,End}.Line are zero-based. +- lineCount := len(strings.Split(env.BufferText(tc.filename), "\n")) +- for _, item := range completions.Items { +- if start := int(item.TextEdit.Range.Start.Line); start > lineCount { +- t.Fatalf("unexpected text edit range start line number: got %d, want <= %d", start, lineCount) +- } +- if end := int(item.TextEdit.Range.End.Line); end > lineCount { +- t.Fatalf("unexpected text edit range end line number: got %d, want <= %d", end, lineCount) +- } +- } - --package typerefs +- if tc.want != nil { +- expectedLoc := env.RegexpSearch(tc.filename, tc.editRegexp) +- for _, item := range completions.Items { +- gotRng := item.TextEdit.Range +- if expectedLoc.Range != gotRng { +- t.Errorf("unexpected completion range for completion item %s: got %v, want %v", +- item.Label, gotRng, expectedLoc.Range) +- } +- } +- } - --import ( -- "fmt" -- "math/bits" -- "sort" -- "strings" -- "sync" +- diff := compareCompletionLabels(tc.want, completions.Items) +- if diff != "" { +- t.Error(diff) +- } +- }) +- }) +- } +-} - -- "golang.org/x/tools/gopls/internal/lsp/source" --) +-func TestPackageNameCompletion(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - --// PackageIndex stores common data to enable efficient representation of --// references and package sets. --type PackageIndex struct { -- // For now, PackageIndex just indexes package ids, to save space and allow for -- // faster unions via sparse int vectors. -- mu sync.Mutex -- ids []source.PackageID -- m map[source.PackageID]IndexID +-go 1.12 +--- math/add.go -- +-package ma +-` +- +- want := []string{"ma", "ma_test", "main", "math", "math_test"} +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("math/add.go") +- completions := env.Completion(env.RegexpSearch("math/add.go", "package ma()")) +- +- diff := compareCompletionLabels(want, completions.Items) +- if diff != "" { +- t.Fatal(diff) +- } +- }) -} - --// NewPackageIndex creates a new PackageIndex instance for use in building --// reference and package sets. --func NewPackageIndex() *PackageIndex { -- return &PackageIndex{ -- m: make(map[source.PackageID]IndexID), +-// TODO(rfindley): audit/clean up call sites for this helper, to ensure +-// consistent test errors. +-func compareCompletionLabels(want []string, gotItems []protocol.CompletionItem) string { +- var got []string +- for _, item := range gotItems { +- got = append(got, item.Label) +- if item.Label != item.InsertText && item.TextEdit == nil { +- // Label should be the same as InsertText, if InsertText is to be used +- return fmt.Sprintf("label not the same as InsertText %#v", item) +- } - } --} - --// IndexID returns the packageIdx referencing id, creating one if id is not yet --// tracked by the receiver. --func (index *PackageIndex) IndexID(id source.PackageID) IndexID { -- index.mu.Lock() -- defer index.mu.Unlock() -- if i, ok := index.m[id]; ok { -- return i +- if len(got) == 0 && len(want) == 0 { +- return "" // treat nil and the empty slice as equivalent - } -- i := IndexID(len(index.ids)) -- index.m[id] = i -- index.ids = append(index.ids, id) -- return i +- +- if diff := cmp.Diff(want, got); diff != "" { +- return fmt.Sprintf("completion item mismatch (-want +got):\n%s", diff) +- } +- return "" -} - --// PackageID returns the PackageID for idx. --// --// idx must have been created by this PackageIndex instance. --func (index *PackageIndex) PackageID(idx IndexID) source.PackageID { -- index.mu.Lock() -- defer index.mu.Unlock() -- return index.ids[idx] +-func TestUnimportedCompletion(t *testing.T) { +- const mod = ` +--- go.mod -- +-module mod.com +- +-go 1.14 +- +-require example.com v1.2.3 +--- go.sum -- +-example.com v1.2.3 h1:ihBTGWGjTU3V4ZJ9OmHITkU9WQ4lGdQkMjgyLFk0FaY= +-example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= +--- main.go -- +-package main +- +-func main() { +- _ = blah -} +--- main2.go -- +-package main - --// A PackageSet is a set of source.PackageIDs, optimized for inuse memory --// footprint and efficient union operations. --type PackageSet struct { -- // PackageSet is a sparse int vector of package indexes from parent. -- parent *PackageIndex -- sparse map[int]blockType // high bits in key, set of low bits in value +-import "example.com/blah" +- +-func _() { +- _ = blah.Hello -} +-` +- WithOptions( +- ProxyFiles(proxy), +- ).Run(t, mod, func(t *testing.T, env *Env) { +- // Make sure the dependency is in the module cache and accessible for +- // unimported completions, and then remove it before proceeding. +- env.RemoveWorkspaceFile("main2.go") +- env.RunGoCommand("mod", "tidy") +- env.Await(env.DoneWithChangeWatchedFiles()) - --type blockType = uint // type of each sparse vector element --const blockSize = bits.UintSize +- // Trigger unimported completions for the example.com/blah package. +- env.OpenFile("main.go") +- env.Await(env.DoneWithOpen()) +- loc := env.RegexpSearch("main.go", "ah") +- completions := env.Completion(loc) +- if len(completions.Items) == 0 { +- t.Fatalf("no completion items") +- } +- env.AcceptCompletion(loc, completions.Items[0]) // adds blah import to main.go +- env.Await(env.DoneWithChange()) - --// NewSet creates a new PackageSet bound to this PackageIndex instance. --// --// PackageSets may only be combined with other PackageSets from the same --// instance. --func (index *PackageIndex) NewSet() *PackageSet { -- return &PackageSet{ -- parent: index, -- sparse: make(map[int]blockType), -- } +- // Trigger completions once again for the blah.<> selector. +- env.RegexpReplace("main.go", "_ = blah", "_ = blah.") +- env.Await(env.DoneWithChange()) +- loc = env.RegexpSearch("main.go", "\n}") +- completions = env.Completion(loc) +- if len(completions.Items) != 1 { +- t.Fatalf("expected 1 completion item, got %v", len(completions.Items)) +- } +- item := completions.Items[0] +- if item.Label != "Name" { +- t.Fatalf("expected completion item blah.Name, got %v", item.Label) +- } +- env.AcceptCompletion(loc, item) +- +- // Await the diagnostics to add example.com/blah to the go.mod file. +- env.AfterChange( +- Diagnostics(env.AtRegexp("main.go", `"example.com/blah"`)), +- ) +- }) -} - --// DeclaringPackage returns the ID of the symbol's declaring package. --// The package index must be the one used during decoding. --func (index *PackageIndex) DeclaringPackage(sym Symbol) source.PackageID { -- return index.PackageID(sym.Package) +-// Test that completions still work with an undownloaded module, golang/go#43333. +-func TestUndownloadedModule(t *testing.T) { +- // mod.com depends on example.com, but only in a file that's hidden by a +- // build tag, so the IWL won't download example.com. That will cause errors +- // in the go list -m call performed by the imports package. +- const files = ` +--- go.mod -- +-module mod.com +- +-go 1.14 +- +-require example.com v1.2.3 +--- go.sum -- +-example.com v1.2.3 h1:ihBTGWGjTU3V4ZJ9OmHITkU9WQ4lGdQkMjgyLFk0FaY= +-example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= +--- useblah.go -- +-// +build hidden +- +-package pkg +-import "example.com/blah" +-var _ = blah.Name +--- mainmod/mainmod.go -- +-package mainmod +- +-const Name = "mainmod" +-` +- WithOptions(ProxyFiles(proxy)).Run(t, files, func(t *testing.T, env *Env) { +- env.CreateBuffer("import.go", "package pkg\nvar _ = mainmod.Name\n") +- env.SaveBuffer("import.go") +- content := env.ReadWorkspaceFile("import.go") +- if !strings.Contains(content, `import "mod.com/mainmod`) { +- t.Errorf("expected import of mod.com/mainmod in %q", content) +- } +- }) -} - --// Add records a new element in the package set, for the provided package ID. --func (s *PackageSet) AddPackage(id source.PackageID) { -- s.Add(s.parent.IndexID(id)) +-// Test that we can doctor the source code enough so the file is +-// parseable and completion works as expected. +-func TestSourceFixup(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com +- +-go 1.12 +--- foo.go -- +-package foo +- +-func _() { +- var s S +- if s. -} - --// Add records a new element in the package set. --// It is the caller's responsibility to ensure that idx was created with the --// same PackageIndex as the PackageSet. --func (s *PackageSet) Add(idx IndexID) { -- i := int(idx) -- s.sparse[i/blockSize] |= 1 << (i % blockSize) +-type S struct { +- i int -} +-` - --// Union records all elements from other into the receiver, mutating the --// receiver set but not the argument set. The receiver must not be nil, but the --// argument set may be nil. --// --// Precondition: both package sets were created with the same PackageIndex. --func (s *PackageSet) Union(other *PackageSet) { -- if other == nil { -- return // e.g. unsafe -- } -- if other.parent != s.parent { -- panic("other set is from a different PackageIndex instance") -- } -- for k, v := range other.sparse { -- if v0 := s.sparse[k]; v0 != v { -- s.sparse[k] = v0 | v +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("foo.go") +- completions := env.Completion(env.RegexpSearch("foo.go", `if s\.()`)) +- diff := compareCompletionLabels([]string{"i"}, completions.Items) +- if diff != "" { +- t.Fatal(diff) - } -- } +- }) -} - --// Contains reports whether id is contained in the receiver set. --func (s *PackageSet) Contains(id source.PackageID) bool { -- i := int(s.parent.IndexID(id)) -- return s.sparse[i/blockSize]&(1<<(i%blockSize)) != 0 +-func TestCompletion_Issue45510(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com +- +-go 1.12 +--- main.go -- +-package main +- +-func _() { +- type a *a +- var aaaa1, aaaa2 a +- var _ a = aaaa +- +- type b a +- var bbbb1, bbbb2 b +- var _ b = bbbb -} - --// Elems calls f for each element of the set in ascending order. --func (s *PackageSet) Elems(f func(IndexID)) { -- blockIndexes := make([]int, 0, len(s.sparse)) -- for k := range s.sparse { -- blockIndexes = append(blockIndexes, k) -- } -- sort.Ints(blockIndexes) -- for _, i := range blockIndexes { -- v := s.sparse[i] -- for b := 0; b < blockSize; b++ { -- if (v & (1 << b)) != 0 { -- f(IndexID(i*blockSize + b)) +-type ( +- c *d +- d *e +- e **c +-) +- +-func _() { +- var ( +- xxxxc c +- xxxxd d +- xxxxe e +- ) +- +- var _ c = xxxx +- var _ d = xxxx +- var _ e = xxxx +-} +-` +- +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- +- tests := []struct { +- re string +- want []string +- }{ +- {`var _ a = aaaa()`, []string{"aaaa1", "aaaa2"}}, +- {`var _ b = bbbb()`, []string{"bbbb1", "bbbb2"}}, +- {`var _ c = xxxx()`, []string{"xxxxc", "xxxxd", "xxxxe"}}, +- {`var _ d = xxxx()`, []string{"xxxxc", "xxxxd", "xxxxe"}}, +- {`var _ e = xxxx()`, []string{"xxxxc", "xxxxd", "xxxxe"}}, +- } +- for _, tt := range tests { +- completions := env.Completion(env.RegexpSearch("main.go", tt.re)) +- diff := compareCompletionLabels(tt.want, completions.Items) +- if diff != "" { +- t.Errorf("%s: %s", tt.re, diff) - } - } -- } +- }) -} - --// String returns a human-readable representation of the set: {A, B, ...}. --func (s *PackageSet) String() string { -- var ids []string -- s.Elems(func(id IndexID) { -- ids = append(ids, string(s.parent.PackageID(id))) +-func TestCompletionDeprecation(t *testing.T) { +- const files = ` +--- go.mod -- +-module test.com +- +-go 1.16 +--- prog.go -- +-package waste +-// Deprecated, use newFoof +-func fooFunc() bool { +- return false +-} +- +-// Deprecated +-const badPi = 3.14 +- +-func doit() { +- if fooF +- panic() +- x := badP +-} +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("prog.go") +- loc := env.RegexpSearch("prog.go", "if fooF") +- loc.Range.Start.Character += uint32(protocol.UTF16Len([]byte("if fooF"))) +- completions := env.Completion(loc) +- diff := compareCompletionLabels([]string{"fooFunc"}, completions.Items) +- if diff != "" { +- t.Error(diff) +- } +- if completions.Items[0].Tags == nil { +- t.Errorf("expected Tags to show deprecation %#v", completions.Items[0].Tags) +- } +- loc = env.RegexpSearch("prog.go", "= badP") +- loc.Range.Start.Character += uint32(protocol.UTF16Len([]byte("= badP"))) +- completions = env.Completion(loc) +- diff = compareCompletionLabels([]string{"badPi"}, completions.Items) +- if diff != "" { +- t.Error(diff) +- } +- if completions.Items[0].Tags == nil { +- t.Errorf("expected Tags to show deprecation %#v", completions.Items[0].Tags) +- } - }) -- return fmt.Sprintf("{%s}", strings.Join(ids, ", ")) -} -diff -urN a/gopls/internal/lsp/source/typerefs/pkggraph_test.go b/gopls/internal/lsp/source/typerefs/pkggraph_test.go ---- a/gopls/internal/lsp/source/typerefs/pkggraph_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/typerefs/pkggraph_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,243 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package typerefs_test +-func TestUnimportedCompletion_VSCodeIssue1489(t *testing.T) { +- const src = ` +--- go.mod -- +-module mod.com +- +-go 1.14 +- +--- main.go -- +-package main +- +-import "fmt" +- +-func main() { +- fmt.Println("a") +- math.Sqr +-} +-` +- WithOptions( +- WindowsLineEndings(), +- Settings{"ui.completion.usePlaceholders": true}, +- ).Run(t, src, func(t *testing.T, env *Env) { +- // Trigger unimported completions for the mod.com package. +- env.OpenFile("main.go") +- env.Await(env.DoneWithOpen()) +- loc := env.RegexpSearch("main.go", "Sqr()") +- completions := env.Completion(loc) +- if len(completions.Items) == 0 { +- t.Fatalf("no completion items") +- } +- env.AcceptCompletion(loc, completions.Items[0]) +- env.Await(env.DoneWithChange()) +- got := env.BufferText("main.go") +- want := "package main\r\n\r\nimport (\r\n\t\"fmt\"\r\n\t\"math\"\r\n)\r\n\r\nfunc main() {\r\n\tfmt.Println(\"a\")\r\n\tmath.Sqrt(${1:x float64})\r\n}\r\n" +- if diff := cmp.Diff(want, got); diff != "" { +- t.Errorf("unimported completion (-want +got):\n%s", diff) +- } +- }) +-} +- +-func TestUnimportedCompletionHasPlaceholders60269(t *testing.T) { +- // We can't express this as a marker test because it doesn't support AcceptCompletion. +- const src = ` +--- go.mod -- +-module example.com +-go 1.12 +- +--- a/a.go -- +-package a +- +-var _ = b.F +- +--- b/b.go -- +-package b +- +-func F0(a, b int, c float64) {} +-func F1(int, chan *string) {} +-func F2[K, V any](map[K]V, chan V) {} // missing type parameters was issue #60959 +-func F3[K comparable, V any](map[K]V, chan V) {} +-` +- WithOptions( +- WindowsLineEndings(), +- Settings{"ui.completion.usePlaceholders": true}, +- ).Run(t, src, func(t *testing.T, env *Env) { +- env.OpenFile("a/a.go") +- env.Await(env.DoneWithOpen()) +- +- // The table lists the expected completions of b.F as they appear in Items. +- const common = "package a\r\n\r\nimport \"example.com/b\"\r\n\r\nvar _ = " +- for i, want := range []string{ +- common + "b.F0(${1:a int}, ${2:b int}, ${3:c float64})\r\n", +- common + "b.F1(${1:_ int}, ${2:_ chan *string})\r\n", +- common + "b.F2[${1:K any}, ${2:V any}](${3:_ map[K]V}, ${4:_ chan V})\r\n", +- common + "b.F3[${1:K comparable}, ${2:V any}](${3:_ map[K]V}, ${4:_ chan V})\r\n", +- } { +- loc := env.RegexpSearch("a/a.go", "b.F()") +- completions := env.Completion(loc) +- if len(completions.Items) == 0 { +- t.Fatalf("no completion items") +- } +- saved := env.BufferText("a/a.go") +- env.AcceptCompletion(loc, completions.Items[i]) +- env.Await(env.DoneWithChange()) +- got := env.BufferText("a/a.go") +- if diff := cmp.Diff(want, got); diff != "" { +- t.Errorf("%d: unimported completion (-want +got):\n%s", i, diff) +- } +- env.SetBufferContent("a/a.go", saved) // restore +- } +- }) +-} +- +-func TestPackageMemberCompletionAfterSyntaxError(t *testing.T) { +- // This test documents the current broken behavior due to golang/go#58833. +- const src = ` +--- go.mod -- +-module mod.com +- +-go 1.14 +- +--- main.go -- +-package main +- +-import "math" +- +-func main() { +- math.Sqrt(,0) +- math.Ldex +-} +-` +- Run(t, src, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- env.Await(env.DoneWithOpen()) +- loc := env.RegexpSearch("main.go", "Ldex()") +- completions := env.Completion(loc) +- if len(completions.Items) == 0 { +- t.Fatalf("no completion items") +- } +- env.AcceptCompletion(loc, completions.Items[0]) +- env.Await(env.DoneWithChange()) +- got := env.BufferText("main.go") +- // The completion of math.Ldex after the syntax error on the +- // previous line is not "math.Ldexp" but "math.Ldexmath.Abs". +- // (In VSCode, "Abs" wrongly appears in the completion menu.) +- // This is a consequence of poor error recovery in the parser +- // causing "math.Ldex" to become a BadExpr. +- want := "package main\n\nimport \"math\"\n\nfunc main() {\n\tmath.Sqrt(,0)\n\tmath.Ldexmath.Abs(${1:})\n}\n" +- if diff := cmp.Diff(want, got); diff != "" { +- t.Errorf("unimported completion (-want +got):\n%s", diff) +- } +- }) +-} +- +-func TestCompleteAllFields(t *testing.T) { +- // This test verifies that completion results always include all struct fields. +- // See golang/go#53992. +- +- const src = ` +--- go.mod -- +-module mod.com - --// This file is logically part of the test in pkgrefs_test.go: that --// file defines the test assertion logic; this file provides a --// reference implementation of a client of the typerefs package. +-go 1.18 +- +--- p/p.go -- +-package p - -import ( -- "bytes" -- "context" - "fmt" -- "os" -- "runtime" -- "sync" - -- "golang.org/x/sync/errgroup" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/lsp/source/typerefs" -- "golang.org/x/tools/gopls/internal/span" --) -- --const ( -- // trace enables additional trace output to stdout, for debugging. -- // -- // Warning: produces a lot of output! Best to run with small package queries. -- trace = false +- . "net/http" +- . "runtime" +- . "go/types" +- . "go/parser" +- . "go/ast" -) - --// A Package holds reference information for a single package. --type Package struct { -- // metadata holds metadata about this package and its dependencies. -- metadata *source.Metadata -- -- // transitiveRefs records, for each exported declaration in the package, the -- // transitive set of packages within the containing graph that are -- // transitively reachable through references, starting with the given decl. -- transitiveRefs map[string]*typerefs.PackageSet -- -- // ReachesViaDeps records the set of packages in the containing graph whose -- // syntax may affect the current package's types. See the package -- // documentation for more details of what this means. -- ReachesByDeps *typerefs.PackageSet +-type S struct { +- a, b, c, d, e, f, g, h, i, j, k, l, m int +- n, o, p, q, r, s, t, u, v, w, x, y, z int -} - --// A PackageGraph represents a fully analyzed graph of packages and their --// dependencies. --type PackageGraph struct { -- pkgIndex *typerefs.PackageIndex -- meta source.MetadataSource -- parse func(context.Context, span.URI) (*source.ParsedGoFile, error) -- -- mu sync.Mutex -- packages map[source.PackageID]*futurePackage +-func _() { +- var s S +- fmt.Println(s.) -} +-` - --// BuildPackageGraph analyzes the package graph for the requested ids, whose --// metadata is described by meta. --// --// The provided parse function is used to parse the CompiledGoFiles of each package. --// --// The resulting PackageGraph is fully evaluated, and may be investigated using --// the Package method. --// --// See the package documentation for more information on the package reference --// algorithm. --func BuildPackageGraph(ctx context.Context, meta source.MetadataSource, ids []source.PackageID, parse func(context.Context, span.URI) (*source.ParsedGoFile, error)) (*PackageGraph, error) { -- g := &PackageGraph{ -- pkgIndex: typerefs.NewPackageIndex(), -- meta: meta, -- parse: parse, -- packages: make(map[source.PackageID]*futurePackage), -- } -- source.SortPostOrder(meta, ids) -- -- workers := runtime.GOMAXPROCS(0) -- if trace { -- workers = 1 -- } -- -- var eg errgroup.Group -- eg.SetLimit(workers) -- for _, id := range ids { -- id := id -- eg.Go(func() error { -- _, err := g.Package(ctx, id) -- return err -- }) -- } -- return g, eg.Wait() --} +- WithOptions(Settings{ +- "completionBudget": "1ns", // must be non-zero as 0 => infinity +- }).Run(t, src, func(t *testing.T, env *Env) { +- wantFields := make(map[string]bool) +- for c := 'a'; c <= 'z'; c++ { +- wantFields[string(c)] = true +- } - --// futurePackage is a future result of analyzing a package, for use from Package only. --type futurePackage struct { -- done chan struct{} -- pkg *Package -- err error --} +- env.OpenFile("p/p.go") +- // Make an arbitrary edit to ensure we're not hitting the cache. +- env.EditBuffer("p/p.go", fake.NewEdit(0, 0, 0, 0, fmt.Sprintf("// current time: %v\n", time.Now()))) +- loc := env.RegexpSearch("p/p.go", `s\.()`) +- completions := env.Completion(loc) +- gotFields := make(map[string]bool) +- for _, item := range completions.Items { +- if item.Kind == protocol.FieldCompletion { +- gotFields[item.Label] = true +- } +- } - --// Package gets the result of analyzing references for a single package. --func (g *PackageGraph) Package(ctx context.Context, id source.PackageID) (*Package, error) { -- g.mu.Lock() -- fut, ok := g.packages[id] -- if ok { -- g.mu.Unlock() -- select { -- case <-fut.done: -- case <-ctx.Done(): -- return nil, ctx.Err() +- if diff := cmp.Diff(wantFields, gotFields); diff != "" { +- t.Errorf("Completion(...) returned mismatching fields (-want +got):\n%s", diff) - } -- } else { -- fut = &futurePackage{done: make(chan struct{})} -- g.packages[id] = fut -- g.mu.Unlock() -- fut.pkg, fut.err = g.buildPackage(ctx, id) -- close(fut.done) -- } -- return fut.pkg, fut.err +- }) -} - --// buildPackage parses a package and extracts its reference graph. It should --// only be called from Package. --func (g *PackageGraph) buildPackage(ctx context.Context, id source.PackageID) (*Package, error) { -- p := &Package{ -- metadata: g.meta.Metadata(id), -- transitiveRefs: make(map[string]*typerefs.PackageSet), -- } -- var files []*source.ParsedGoFile -- for _, filename := range p.metadata.CompiledGoFiles { -- f, err := g.parse(ctx, filename) -- if err != nil { -- return nil, err -- } -- files = append(files, f) +-func TestDefinition(t *testing.T) { +- files := ` +--- go.mod -- +-module mod.com +- +-go 1.18 +--- a_test.go -- +-package foo +-` +- tests := []struct { +- line string // the sole line in the buffer after the package statement +- pat string // the pattern to search for +- want []string // expected completions +- }{ +- {"func T", "T", []string{"TestXxx(t *testing.T)", "TestMain(m *testing.M)"}}, +- {"func T()", "T", []string{"TestMain", "Test"}}, +- {"func TestM", "TestM", []string{"TestMain(m *testing.M)", "TestM(t *testing.T)"}}, +- {"func TestM()", "TestM", []string{"TestMain"}}, +- {"func TestMi", "TestMi", []string{"TestMi(t *testing.T)"}}, +- {"func TestMi()", "TestMi", nil}, +- {"func TestG", "TestG", []string{"TestG(t *testing.T)"}}, +- {"func TestG(", "TestG", nil}, +- {"func Ben", "B", []string{"BenchmarkXxx(b *testing.B)"}}, +- {"func Ben(", "Ben", []string{"Benchmark"}}, +- {"func BenchmarkFoo", "BenchmarkFoo", []string{"BenchmarkFoo(b *testing.B)"}}, +- {"func BenchmarkFoo(", "BenchmarkFoo", nil}, +- {"func Fuz", "F", []string{"FuzzXxx(f *testing.F)"}}, +- {"func Fuz(", "Fuz", []string{"Fuzz"}}, +- {"func Testx", "Testx", nil}, +- {"func TestMe(t *testing.T)", "TestMe", nil}, +- {"func Te(t *testing.T)", "Te", []string{"TestMain", "Test"}}, - } -- imports := make(map[source.ImportPath]*source.Metadata) -- for impPath, depID := range p.metadata.DepsByImpPath { -- if depID != "" { -- imports[impPath] = g.meta.Metadata(depID) +- fname := "a_test.go" +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile(fname) +- env.Await(env.DoneWithOpen()) +- for _, test := range tests { +- env.SetBufferContent(fname, "package foo\n"+test.line) +- loc := env.RegexpSearch(fname, test.pat) +- loc.Range.Start.Character += uint32(protocol.UTF16Len([]byte(test.pat))) +- completions := env.Completion(loc) +- if diff := compareCompletionLabels(test.want, completions.Items); diff != "" { +- t.Error(diff) +- } - } -- } -- -- // Compute the symbol-level dependencies through this package. -- data := typerefs.Encode(files, id, imports) +- }) +-} - -- // data can be persisted in a filecache, keyed -- // by hash(id, CompiledGoFiles, imports). +-// Test that completing a definition replaces source text when applied, golang/go#56852. +-func TestDefinitionReplaceRange(t *testing.T) { +- const mod = ` +--- go.mod -- +-module mod.com - -- // This point separates the local preprocessing -- // -- of a single package (above) from the global -- -- // transitive reachability query (below). +-go 1.17 +-` - -- // classes records syntactic edges between declarations in this -- // package and declarations in this package or another -- // package. See the package documentation for a detailed -- // description of what these edges do (and do not) represent. -- classes := typerefs.Decode(g.pkgIndex, id, data) +- tests := []struct { +- name string +- before, after string +- }{ +- { +- name: "func TestMa", +- before: ` +-package foo_test - -- // Debug -- if trace && len(classes) > 0 { -- var buf bytes.Buffer -- fmt.Fprintf(&buf, "%s\n", id) -- for _, class := range classes { -- for i, name := range class.Decls { -- if i == 0 { -- fmt.Fprintf(&buf, "\t") -- } -- fmt.Fprintf(&buf, " .%s", name) -- } -- // Group symbols by package. -- var prevID PackageID -- for _, sym := range class.Refs { -- id := g.pkgIndex.DeclaringPackage(sym) -- if id != prevID { -- prevID = id -- fmt.Fprintf(&buf, "\n\t\t-> %s:", id) -- } -- fmt.Fprintf(&buf, " .%s", sym.Name) -- } -- fmt.Fprintln(&buf) -- } -- os.Stderr.Write(buf.Bytes()) -- } +-func TestMa +-`, +- after: ` +-package foo_test - -- // Now compute the transitive closure of packages reachable -- // from any exported symbol of this package. -- for _, class := range classes { -- set := g.pkgIndex.NewSet() +-func TestMain(m *testing.M) +-`, +- }, +- { +- name: "func TestSome", +- before: ` +-package foo_test - -- // The Refs slice is sorted by (PackageID, name), -- // so we can economize by calling g.Package only -- // when the package id changes. -- depP := p -- for _, sym := range class.Refs { -- symPkgID := g.pkgIndex.DeclaringPackage(sym) -- if symPkgID == id { -- panic("intra-package edge") -- } -- if depP.metadata.ID != symPkgID { -- // package changed -- var err error -- depP, err = g.Package(ctx, symPkgID) -- if err != nil { -- return nil, err -- } -- } -- set.Add(sym.Package) -- set.Union(depP.transitiveRefs[sym.Name]) -- } -- for _, name := range class.Decls { -- p.transitiveRefs[name] = set -- } -- } +-func TestSome +-`, +- after: ` +-package foo_test - -- // Finally compute the union of transitiveRefs -- // across the direct deps of this package. -- byDeps, err := g.reachesByDeps(ctx, p.metadata) -- if err != nil { -- return nil, err -- } -- p.ReachesByDeps = byDeps +-func TestSome(t *testing.T) +-`, +- }, +- { +- name: "func Bench", +- before: ` +-package foo_test - -- return p, nil --} +-func Bench +-`, +- // Note: Snippet with escaped }. +- after: ` +-package foo_test - --// reachesByDeps computes the set of packages that are reachable through --// dependencies of the package m. --func (g *PackageGraph) reachesByDeps(ctx context.Context, m *source.Metadata) (*typerefs.PackageSet, error) { -- transitive := g.pkgIndex.NewSet() -- for _, depID := range m.DepsByPkgPath { -- dep, err := g.Package(ctx, depID) -- if err != nil { -- return nil, err -- } -- transitive.AddPackage(dep.metadata.ID) -- for _, set := range dep.transitiveRefs { -- transitive.Union(set) -- } +-func Benchmark${1:Xxx}(b *testing.B) { +- $0 +-\} +-`, +- }, - } -- return transitive, nil --} -diff -urN a/gopls/internal/lsp/source/typerefs/pkgrefs_test.go b/gopls/internal/lsp/source/typerefs/pkgrefs_test.go ---- a/gopls/internal/lsp/source/typerefs/pkgrefs_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/typerefs/pkgrefs_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,407 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package typerefs_test +- Run(t, mod, func(t *testing.T, env *Env) { +- env.CreateBuffer("foo_test.go", "") - --import ( -- "bytes" -- "context" -- "flag" -- "fmt" -- "go/token" -- "go/types" -- "os" -- "sort" -- "strings" -- "sync" -- "testing" -- "time" +- for _, tst := range tests { +- tst.before = strings.Trim(tst.before, "\n") +- tst.after = strings.Trim(tst.after, "\n") +- env.SetBufferContent("foo_test.go", tst.before) - -- "golang.org/x/tools/go/gcexportdata" -- "golang.org/x/tools/go/packages" -- "golang.org/x/tools/gopls/internal/astutil" -- "golang.org/x/tools/gopls/internal/lsp/cache" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/lsp/source/typerefs" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/packagesinternal" -- "golang.org/x/tools/internal/testenv" --) +- loc := env.RegexpSearch("foo_test.go", tst.name) +- loc.Range.Start.Character = uint32(protocol.UTF16Len([]byte(tst.name))) +- completions := env.Completion(loc) +- if len(completions.Items) == 0 { +- t.Fatalf("no completion items") +- } - --var ( -- dir = flag.String("dir", "", "dir to run go/packages from") -- query = flag.String("query", "std", "go/packages load query to use for walkdecl tests") -- verify = flag.Bool("verify", true, "whether to verify reachable packages using export data (may be slow on large graphs)") --) +- env.AcceptCompletion(loc, completions.Items[0]) +- env.Await(env.DoneWithChange()) +- if buf := env.BufferText("foo_test.go"); buf != tst.after { +- t.Errorf("%s:incorrect completion: got %q, want %q", tst.name, buf, tst.after) +- } +- } +- }) +-} - --type ( -- packageName = source.PackageName -- PackageID = source.PackageID -- ImportPath = source.ImportPath -- PackagePath = source.PackagePath -- Metadata = source.Metadata -- MetadataSource = source.MetadataSource -- ParsedGoFile = source.ParsedGoFile --) +-func TestGoWorkCompletion(t *testing.T) { +- const files = ` +--- go.work -- +-go 1.18 - --// TestBuildPackageGraph tests the BuildPackageGraph constructor, which uses --// the reference analysis of the Refs function to build a graph of --// relationships between packages. --// --// It simulates the operation of gopls at startup: packages are loaded via --// go/packages, and their syntax+metadata analyzed to determine which packages --// are reachable from others. --// --// The test then verifies that the 'load' graph (the graph of relationships in --// export data) is a subgraph of the 'reach' graph constructed by --// BuildPackageGraph. While doing so, it constructs some statistics about the --// relative sizes of these graphs, along with the 'transitive imports' graph, --// to report the effectiveness of the reachability analysis. --// --// The following flags affect this test: --// - dir sets the dir from which to run go/packages --// - query sets the go/packages query to load --// - verify toggles the verification w.r.t. the load graph (which may be --// prohibitively expensive with large queries). --func TestBuildPackageGraph(t *testing.T) { -- if testing.Short() { -- t.Skip("skipping with -short: loading the packages can take a long time with a cold cache") -- } -- testenv.NeedsGoBuild(t) // for go/packages +-use ./a +-use ./a/ba +-use ./a/b/ +-use ./dir/foo +-use ./dir/foobar/ +-use ./missing/ +--- a/go.mod -- +--- go.mod -- +--- a/bar/go.mod -- +--- a/b/c/d/e/f/go.mod -- +--- dir/bar -- +--- dir/foobar/go.mod -- +-` - -- t0 := time.Now() -- exports, meta, err := load(*query, *verify) -- if err != nil { -- t.Fatalf("loading failed: %v", err) -- } -- t.Logf("loaded %d packages in %v", len(exports), time.Since(t0)) +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("go.work") - -- ctx := context.Background() -- var ids []PackageID -- for id := range exports { -- ids = append(ids, id) -- } -- sort.Slice(ids, func(i, j int) bool { -- return ids[i] < ids[j] +- tests := []struct { +- re string +- want []string +- }{ +- {`use ()\.`, []string{".", "./a", "./a/bar", "./dir/foobar"}}, +- {`use \.()`, []string{"", "/a", "/a/bar", "/dir/foobar"}}, +- {`use \./()`, []string{"a", "a/bar", "dir/foobar"}}, +- {`use ./a()`, []string{"", "/b/c/d/e/f", "/bar"}}, +- {`use ./a/b()`, []string{"/c/d/e/f", "ar"}}, +- {`use ./a/b/()`, []string{`c/d/e/f`}}, +- {`use ./a/ba()`, []string{"r"}}, +- {`use ./dir/foo()`, []string{"bar"}}, +- {`use ./dir/foobar/()`, []string{}}, +- {`use ./missing/()`, []string{}}, +- } +- for _, tt := range tests { +- completions := env.Completion(env.RegexpSearch("go.work", tt.re)) +- diff := compareCompletionLabels(tt.want, completions.Items) +- if diff != "" { +- t.Errorf("%s: %s", tt.re, diff) +- } +- } - }) +-} - -- t0 = time.Now() -- g, err := BuildPackageGraph(ctx, meta, ids, newParser().parse) -- if err != nil { -- t.Fatal(err) -- } -- t.Logf("building package graph took %v", time.Since(t0)) +-func TestBuiltinCompletion(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - -- // Collect information about the edges between packages for later analysis. -- // -- // We compare the following package graphs: -- // - the imports graph: edges are transitive imports -- // - the reaches graph: edges are reachability relationships through syntax -- // of imports (as defined in the package doc) -- // - the loads graph: edges are packages loaded through the export data of -- // imports -- // -- // By definition, loads < reaches < imports. -- type edgeSet map[PackageID]map[PackageID]bool -- var ( -- imports = make(edgeSet) // A imports B transitively -- importedBy = make(edgeSet) // A is imported by B transitively -- reaches = make(edgeSet) // A reaches B through top-level declaration syntax -- reachedBy = make(edgeSet) // A is reached by B through top-level declaration syntax -- loads = make(edgeSet) // A loads B through export data of its direct dependencies -- loadedBy = make(edgeSet) // A is loaded by B through export data of B's direct dependencies -- ) -- recordEdge := func(from, to PackageID, fwd, rev edgeSet) { -- if fwd[from] == nil { -- fwd[from] = make(map[PackageID]bool) -- } -- fwd[from][to] = true -- if rev[to] == nil { -- rev[to] = make(map[PackageID]bool) -- } -- rev[to][from] = true -- } +-go 1.18 +--- a.go -- +-package a - -- exportedPackages := make(map[PackageID]*types.Package) -- importPackage := func(id PackageID) *types.Package { -- exportFile := exports[id] -- if exportFile == "" { -- return nil // no exported symbols +-func _() { +- // here +-} +-` +- +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("a.go") +- result := env.Completion(env.RegexpSearch("a.go", `// here`)) +- builtins := []string{ +- "any", "append", "bool", "byte", "cap", "close", +- "comparable", "complex", "complex128", "complex64", "copy", "delete", +- "error", "false", "float32", "float64", "imag", "int", "int16", "int32", +- "int64", "int8", "len", "make", "new", "panic", "print", "println", "real", +- "recover", "rune", "string", "true", "uint", "uint16", "uint32", "uint64", +- "uint8", "uintptr", "nil", - } -- m := meta.Metadata(id) -- tpkg, ok := exportedPackages[id] -- if !ok { -- pkgPath := string(m.PkgPath) -- tpkg, err = importFromExportData(pkgPath, exportFile) -- if err != nil { -- t.Fatalf("importFromExportData(%s, %s) failed: %v", pkgPath, exportFile, err) -- } -- exportedPackages[id] = tpkg +- if testenv.Go1Point() >= 21 { +- builtins = append(builtins, "clear", "max", "min") - } -- return tpkg -- } +- sort.Strings(builtins) +- var got []string - -- for _, id := range ids { -- pkg, err := g.Package(ctx, id) -- if err != nil { -- t.Fatal(err) +- for _, item := range result.Items { +- // TODO(rfindley): for flexibility, ignore zero while it is being +- // implemented. Remove this if/when zero lands. +- if item.Label != "zero" { +- got = append(got, item.Label) +- } - } -- pkg.ReachesByDeps.Elems(func(id2 typerefs.IndexID) { -- recordEdge(id, g.pkgIndex.PackageID(id2), reaches, reachedBy) -- }) +- sort.Strings(got) - -- importMap := importMap(id, meta) -- for _, id2 := range importMap { -- recordEdge(id, id2, imports, importedBy) +- if diff := cmp.Diff(builtins, got); diff != "" { +- t.Errorf("Completion: unexpected mismatch (-want +got):\n%s", diff) - } +- }) +-} - -- if *verify { -- for _, depID := range meta.Metadata(id).DepsByPkgPath { -- tpkg := importPackage(depID) -- if tpkg == nil { -- continue -- } -- for _, imp := range tpkg.Imports() { -- depID, ok := importMap[PackagePath(imp.Path())] -- if !ok { -- t.Errorf("import map (len: %d) for %s missing imported types.Package %s", len(importMap), id, imp.Path()) -- continue -- } -- recordEdge(id, depID, loads, loadedBy) -- } -- } +-func TestOverlayCompletion(t *testing.T) { +- const files = ` +--- go.mod -- +-module foo.test - -- for depID := range loads[id] { -- if !pkg.ReachesByDeps.Contains(depID) { -- t.Errorf("package %s was imported by %s, but not detected as reachable", depID, id) -- } -- } -- } -- } +-go 1.18 - -- if testing.Verbose() { -- fmt.Printf("%-52s%8s%8s%8s%8s%8s%8s\n", "package ID", "imp", "impBy", "reach", "reachBy", "load", "loadBy") -- for _, id := range ids { -- fmt.Printf("%-52s%8d%8d%8d%8d%8d%8d\n", id, len(imports[id]), len(importedBy[id]), len(reaches[id]), len(reachedBy[id]), len(loads[id]), len(loadedBy[id])) -- } -- fmt.Println(strings.Repeat("-", 100)) -- fmt.Printf("%-52s%8s%8s%8s%8s%8s%8s\n", "package ID", "imp", "impBy", "reach", "reachBy", "load", "loadBy") +--- foo/foo.go -- +-package foo - -- avg := func(m edgeSet) float64 { -- var avg float64 -- for _, id := range ids { -- s := m[id] -- avg += float64(len(s)) / float64(len(ids)) -- } -- return avg -- } -- fmt.Printf("%52s%8.1f%8.1f%8.1f%8.1f%8.1f%8.1f\n", "averages:", avg(imports), avg(importedBy), avg(reaches), avg(reachedBy), avg(loads), avg(loadedBy)) -- } --} +-type Foo struct{} +-` - --func importMap(id PackageID, meta MetadataSource) map[PackagePath]PackageID { -- imports := make(map[PackagePath]PackageID) -- var recordIDs func(PackageID) -- recordIDs = func(id PackageID) { -- m := meta.Metadata(id) -- if _, ok := imports[m.PkgPath]; ok { -- return +- Run(t, files, func(t *testing.T, env *Env) { +- env.CreateBuffer("nodisk/nodisk.go", ` +-package nodisk +- +-import ( +- "foo.test/foo" +-) +- +-func _() { +- foo.Foo() +-} +-`) +- list := env.Completion(env.RegexpSearch("nodisk/nodisk.go", "foo.(Foo)")) +- want := []string{"Foo"} +- var got []string +- for _, item := range list.Items { +- got = append(got, item.Label) - } -- imports[m.PkgPath] = id -- for _, id := range m.DepsByPkgPath { -- recordIDs(id) +- if diff := cmp.Diff(want, got); diff != "" { +- t.Errorf("Completion: unexpected mismatch (-want +got):\n%s", diff) - } -- } -- for _, id := range meta.Metadata(id).DepsByPkgPath { -- recordIDs(id) -- } -- return imports +- }) -} - --func importFromExportData(pkgPath, exportFile string) (*types.Package, error) { -- file, err := os.Open(exportFile) -- if err != nil { -- return nil, err -- } -- r, err := gcexportdata.NewReader(file) -- if err != nil { -- file.Close() -- return nil, err -- } -- fset := token.NewFileSet() -- tpkg, err := gcexportdata.Read(r, fset, make(map[string]*types.Package), pkgPath) -- file.Close() -- if err != nil { -- return nil, err -- } -- // The export file reported by go/packages is produced by the compiler, which -- // has additional package dependencies due to inlining. -- // -- // Export and re-import so that we only observe dependencies from the -- // exported API. -- var out bytes.Buffer -- err = gcexportdata.Write(&out, fset, tpkg) -- if err != nil { -- return nil, err -- } -- return gcexportdata.Read(&out, token.NewFileSet(), make(map[string]*types.Package), pkgPath) --} +-// Fix for golang/go#60062: unimported completion included "golang.org/toolchain" results. +-func TestToolchainCompletions(t *testing.T) { +- const files = ` +--- go.mod -- +-module foo.test/foo - --func BenchmarkBuildPackageGraph(b *testing.B) { -- t0 := time.Now() -- exports, meta, err := load(*query, *verify) -- if err != nil { -- b.Fatalf("loading failed: %v", err) -- } -- b.Logf("loaded %d packages in %v", len(exports), time.Since(t0)) -- ctx := context.Background() -- var ids []PackageID -- for id := range exports { -- ids = append(ids, id) -- } -- b.ResetTimer() +-go 1.21 - -- for i := 0; i < b.N; i++ { -- _, err := BuildPackageGraph(ctx, meta, ids, newParser().parse) -- if err != nil { -- b.Fatal(err) -- } -- } --} +--- foo.go -- +-package foo - --type memoizedParser struct { -- mu sync.Mutex -- files map[span.URI]*futureParse +-func _() { +- os.Open -} - --type futureParse struct { -- done chan struct{} -- pgf *ParsedGoFile -- err error +-func _() { +- strings -} +-` - --func newParser() *memoizedParser { -- return &memoizedParser{ -- files: make(map[span.URI]*futureParse), -- } --} +- const proxy = ` +--- golang.org/toolchain@v0.0.1-go1.21.1.linux-amd64/go.mod -- +-module golang.org/toolchain +--- golang.org/toolchain@v0.0.1-go1.21.1.linux-amd64/src/os/os.go -- +-package os - --func (p *memoizedParser) parse(ctx context.Context, uri span.URI) (*ParsedGoFile, error) { -- doParse := func(ctx context.Context, uri span.URI) (*ParsedGoFile, error) { -- // TODO(adonovan): hoist this operation outside the benchmark critsec. -- content, err := os.ReadFile(uri.Filename()) -- if err != nil { -- return nil, err -- } -- content = astutil.PurgeFuncBodies(content) -- pgf, _ := cache.ParseGoSrc(ctx, token.NewFileSet(), uri, content, source.ParseFull, false) -- return pgf, nil -- } +-func Open() {} +--- golang.org/toolchain@v0.0.1-go1.21.1.linux-amd64/src/strings/strings.go -- +-package strings - -- p.mu.Lock() -- fut, ok := p.files[uri] -- if ok { -- p.mu.Unlock() -- select { -- case <-fut.done: -- case <-ctx.Done(): -- return nil, ctx.Err() -- } -- } else { -- fut = &futureParse{done: make(chan struct{})} -- p.files[uri] = fut -- p.mu.Unlock() -- fut.pgf, fut.err = doParse(ctx, uri) -- close(fut.done) -- } -- return fut.pgf, fut.err --} +-func Join() {} +-` - --type mapMetadataSource struct { -- m map[PackageID]*Metadata --} +- WithOptions( +- ProxyFiles(proxy), +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.RunGoCommand("mod", "download", "golang.org/toolchain@v0.0.1-go1.21.1.linux-amd64") +- env.OpenFile("foo.go") - --func (s mapMetadataSource) Metadata(id PackageID) *Metadata { -- return s.m[id] +- for _, pattern := range []string{"os.Open()", "string()"} { +- loc := env.RegexpSearch("foo.go", pattern) +- res := env.Completion(loc) +- for _, item := range res.Items { +- if strings.Contains(item.Detail, "golang.org/toolchain") { +- t.Errorf("Completion(...) returned toolchain item %#v", item) +- } +- } +- } +- }) -} - --// This function is a compressed version of snapshot.load from the --// internal/lsp/cache package, for use in testing. --// --// TODO(rfindley): it may be valuable to extract this logic from the snapshot, --// since it is otherwise standalone. --func load(query string, needExport bool) (map[PackageID]string, MetadataSource, error) { -- cfg := &packages.Config{ -- Dir: *dir, -- Mode: packages.NeedName | -- packages.NeedFiles | -- packages.NeedCompiledGoFiles | -- packages.NeedImports | -- packages.NeedDeps | -- packages.NeedTypesSizes | -- packages.NeedModule | -- packages.NeedEmbedFiles | -- packages.LoadMode(packagesinternal.DepsErrors) | -- packages.LoadMode(packagesinternal.ForTest), -- Tests: true, -- } -- if needExport { -- cfg.Mode |= packages.NeedExportFile // ExportFile is not requested by gopls: this is used to verify reachability -- } -- pkgs, err := packages.Load(cfg, query) -- if err != nil { -- return nil, nil, err -- } +-// show that the efficacy counters get exercised. Fortuntely a small program +-// exercises them all +-func TestCounters(t *testing.T) { +- const files = ` +--- go.mod -- +-module foo +-go 1.21 +--- x.go -- +-package foo - -- meta := make(map[PackageID]*Metadata) -- var buildMetadata func(pkg *packages.Package) -- buildMetadata = func(pkg *packages.Package) { -- id := PackageID(pkg.ID) -- if meta[id] != nil { -- return -- } -- m := &Metadata{ -- ID: id, -- PkgPath: PackagePath(pkg.PkgPath), -- Name: packageName(pkg.Name), -- ForTest: PackagePath(packagesinternal.GetForTest(pkg)), -- TypesSizes: pkg.TypesSizes, -- LoadDir: cfg.Dir, -- Module: pkg.Module, -- Errors: pkg.Errors, -- DepsErrors: packagesinternal.GetDepsErrors(pkg), -- } -- meta[id] = m +-func main() { +-} - -- for _, filename := range pkg.CompiledGoFiles { -- m.CompiledGoFiles = append(m.CompiledGoFiles, span.URIFromPath(filename)) +-` +- WithOptions( +- Modes(Default), +- ).Run(t, files, func(t *testing.T, env *Env) { +- cts := func() map[*counter.Counter]uint64 { +- ans := make(map[*counter.Counter]uint64) +- for _, c := range server.CompletionCounters { +- ans[c], _ = countertest.ReadCounter(c) +- } +- return ans - } -- for _, filename := range pkg.GoFiles { -- m.GoFiles = append(m.GoFiles, span.URIFromPath(filename)) +- before := cts() +- env.OpenFile("x.go") +- env.Await(env.DoneWithOpen()) +- saved := env.BufferText("x.go") +- lines := strings.Split(saved, "\n") +- // make sure the unused counter is exercised +- loc := env.RegexpSearch("x.go", "main") +- loc.Range.End = loc.Range.Start +- env.Completion(loc) // ignore the proposed completions +- env.RegexpReplace("x.go", "main", "Main") // completions are unused +- env.SetBufferContent("x.go", saved) // restore x.go +- // used:no +- +- // all the action is after 4 characters on line 2 (counting from 0) +- for i := 2; i < len(lines); i++ { +- l := lines[i] +- loc.Range.Start.Line = uint32(i) +- for j := 4; j < len(l); j++ { +- loc.Range.Start.Character = uint32(j) +- loc.Range.End = loc.Range.Start +- res := env.Completion(loc) +- if len(res.Items) > 0 { +- r := res.Items[0] +- env.AcceptCompletion(loc, r) +- env.SetBufferContent("x.go", saved) +- } +- } - } -- -- m.DepsByImpPath = make(map[ImportPath]PackageID) -- m.DepsByPkgPath = make(map[PackagePath]PackageID) -- for importPath, imported := range pkg.Imports { -- importPath := ImportPath(importPath) -- -- // see note in gopls/internal/lsp/cache/load.go for an explanation of this check. -- if importPath != "unsafe" && len(imported.CompiledGoFiles) == 0 { -- m.DepsByImpPath[importPath] = "" // missing -- continue +- after := cts() +- for c := range after { +- if after[c] <= before[c] { +- t.Errorf("%s did not increase", c.Name()) - } -- -- m.DepsByImpPath[importPath] = PackageID(imported.ID) -- m.DepsByPkgPath[PackagePath(imported.PkgPath)] = PackageID(imported.ID) -- buildMetadata(imported) - } -- } -- -- exportFiles := make(map[PackageID]string) -- for _, pkg := range pkgs { -- exportFiles[PackageID(pkg.ID)] = pkg.ExportFile -- buildMetadata(pkg) -- } -- return exportFiles, &mapMetadataSource{meta}, nil +- }) -} -diff -urN a/gopls/internal/lsp/source/typerefs/refs.go b/gopls/internal/lsp/source/typerefs/refs.go ---- a/gopls/internal/lsp/source/typerefs/refs.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/typerefs/refs.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,832 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/test/integration/completion/postfix_snippet_test.go b/gopls/internal/test/integration/completion/postfix_snippet_test.go +--- a/gopls/internal/test/integration/completion/postfix_snippet_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/completion/postfix_snippet_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,762 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package typerefs +-package completion - -import ( -- "fmt" -- "go/ast" -- "go/token" -- "sort" - "strings" +- "testing" - -- "golang.org/x/tools/gopls/internal/astutil" -- "golang.org/x/tools/gopls/internal/lsp/frob" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/internal/typeparams" +- . "golang.org/x/tools/gopls/internal/test/integration" -) - --// Encode analyzes the Go syntax trees of a package, constructs a --// reference graph, and uses it to compute, for each exported --// declaration, the set of exported symbols of directly imported --// packages that it references, perhaps indirectly. --// --// It returns a serializable index of this information. --// Use Decode to expand the result. --func Encode(files []*source.ParsedGoFile, id source.PackageID, imports map[source.ImportPath]*source.Metadata) []byte { -- return index(files, id, imports) +-func TestPostfixSnippetCompletion(t *testing.T) { +- const mod = ` +--- go.mod -- +-module mod.com +- +-go 1.12 +-` +- +- cases := []struct { +- name string +- before, after string +- allowMultipleItem bool +- }{ +- { +- name: "sort", +- before: ` +-package foo +- +-func _() { +- var foo []int +- foo.sort -} +-`, +- after: ` +-package foo - --// Decode decodes a serializable index of symbol --// reachability produced by Encode. --// --// Because many declarations reference the exact same set of symbols, --// the results are grouped into equivalence classes. --// Classes are sorted by Decls[0], ascending. --// The class with empty reachability is omitted. --// --// See the package documentation for more details as to what a --// reference does (and does not) represent. --func Decode(pkgIndex *PackageIndex, id source.PackageID, data []byte) []Class { -- return decode(pkgIndex, id, data) +-import "sort" +- +-func _() { +- var foo []int +- sort.Slice(foo, func(i, j int) bool { +- $0 +-}) -} +-`, +- }, +- { +- name: "sort_renamed_sort_package", +- before: ` +-package foo - --// A Class is a reachability equivalence class. --// --// It attests that each exported package-level declaration in Decls --// references (perhaps indirectly) one of the external (imported) --// symbols in Refs. --// --// Because many Decls reach the same Refs, --// it is more efficient to group them into classes. --type Class struct { -- Decls []string // sorted set of names of exported decls with same reachability -- Refs []Symbol // set of external symbols, in ascending (PackageID, Name) order +-import blahsort "sort" +- +-var j int +- +-func _() { +- var foo []int +- foo.sort -} +-`, +- after: ` +-package foo - --// A Symbol represents an external (imported) symbol --// referenced by the analyzed package. --type Symbol struct { -- Package IndexID // w.r.t. PackageIndex passed to decoder -- Name string +-import blahsort "sort" +- +-var j int +- +-func _() { +- var foo []int +- blahsort.Slice(foo, func(i, j2 int) bool { +- $0 +-}) -} +-`, +- }, +- { +- name: "last", +- before: ` +-package foo - --// An IndexID is a small integer that uniquely identifies a package within a --// given PackageIndex. --type IndexID int +-func _() { +- var s struct { i []int } +- s.i.last +-} +-`, +- after: ` +-package foo +- +-func _() { +- var s struct { i []int } +- s.i[len(s.i)-1] +-} +-`, +- }, +- { +- name: "reverse", +- before: ` +-package foo +- +-func _() { +- var foo []int +- foo.reverse +-} +-`, +- after: ` +-package foo +- +-import "slices" +- +-func _() { +- var foo []int +- slices.Reverse(foo) +-} +-`, +- }, +- { +- name: "slice_range", +- before: ` +-package foo +- +-func _() { +- type myThing struct{} +- var foo []myThing +- foo.range +-} +-`, +- after: ` +-package foo +- +-func _() { +- type myThing struct{} +- var foo []myThing +- for ${1:}, ${2:} := range foo { +- $0 +-} +-} +-`, +- }, +- { +- name: "append_stmt", +- before: ` +-package foo +- +-func _() { +- var foo []int +- foo.append +-} +-`, +- after: ` +-package foo +- +-func _() { +- var foo []int +- foo = append(foo, $0) +-} +-`, +- }, +- { +- name: "append_expr", +- before: ` +-package foo +- +-func _() { +- var foo []int +- var _ []int = foo.append +-} +-`, +- after: ` +-package foo +- +-func _() { +- var foo []int +- var _ []int = append(foo, $0) +-} +-`, +- }, +- { +- name: "slice_copy", +- before: ` +-package foo - --// -- internals -- +-func _() { +- var foo []int +- foo.copy +-} +-`, +- after: ` +-package foo - --// A symbolSet is a set of symbols used internally during index construction. --// --// TODO(adonovan): opt: evaluate unifying Symbol and symbol. --// (Encode would have to create a private PackageIndex.) --type symbolSet map[symbol]bool +-func _() { +- var foo []int +- fooCopy := make([]int, len(foo)) +-copy(fooCopy, foo) - --// A symbol is the internal representation of an external --// (imported) symbol referenced by the analyzed package. --type symbol struct { -- pkg source.PackageID -- name string -} +-`, +- }, +- { +- name: "map_range", +- before: ` +-package foo - --// declNode holds information about a package-level declaration --// (or more than one with the same name, in ill-typed code). --// --// It is a node in the symbol reference graph, whose outgoing edges --// are of two kinds: intRefs and extRefs. --type declNode struct { -- name string -- rep *declNode // canonical representative of this SCC (initially self) -- -- // outgoing graph edges -- intRefs map[*declNode]bool // to symbols in this package -- extRefs symbolSet // to imported symbols -- extRefsClass int // extRefs equivalence class number (-1 until set at end) +-func _() { +- var foo map[string]int +- foo.range +-} +-`, +- after: ` +-package foo - -- // Tarjan's SCC algorithm -- index, lowlink int32 // Tarjan numbering -- scc int32 // -ve => on stack; 0 => unvisited; +ve => node is root of a found SCC +-func _() { +- var foo map[string]int +- for ${1:}, ${2:} := range foo { +- $0 -} +-} +-`, +- }, +- { +- name: "map_clear", +- before: ` +-package foo - --// state holds the working state of the Refs algorithm for a single package. --// --// The number of distinct symbols referenced by a single package --// (measured across all of kubernetes), was found to be: --// - max = 1750. --// - Several packages reference > 100 symbols. --// - p95 = 32, p90 = 22, p50 = 8. --type state struct { -- // numbering of unique symbol sets -- class []symbolSet // unique symbol sets -- classIndex map[string]int // index of above (using SymbolSet.hash as key) +-func _() { +- var foo map[string]int +- foo.clear +-} +-`, +- after: ` +-package foo - -- // Tarjan's SCC algorithm -- index int32 -- stack []*declNode +-func _() { +- var foo map[string]int +- for k := range foo { +- delete(foo, k) -} - --// getClassIndex returns the small integer (an index into --// state.class) that identifies the given set. --func (st *state) getClassIndex(set symbolSet) int { -- key := classKey(set) -- i, ok := st.classIndex[key] -- if !ok { -- i = len(st.class) -- st.classIndex[key] = i -- st.class = append(st.class, set) -- } -- return i -} +-`, +- }, +- { +- name: "map_keys", +- before: ` +-package foo - --// appendSorted appends the symbols to syms, sorts by ascending --// (PackageID, name), and returns the result. --// The argument must be an empty slice, ideally with capacity len(set). --func (set symbolSet) appendSorted(syms []symbol) []symbol { -- for sym := range set { -- syms = append(syms, sym) -- } -- sort.Slice(syms, func(i, j int) bool { -- x, y := syms[i], syms[j] -- if x.pkg != y.pkg { -- return x.pkg < y.pkg -- } -- return x.name < y.name -- }) -- return syms +-func _() { +- var foo map[string]int +- foo.keys -} +-`, +- after: ` +-package foo - --// classKey returns a key such that equal keys imply equal sets. --// (e.g. a sorted string representation, or a cryptographic hash of same). --func classKey(set symbolSet) string { -- // Sort symbols into a stable order. -- // TODO(adonovan): opt: a cheap crypto hash (e.g. BLAKE2b) might -- // make a cheaper map key than a large string. -- // Try using a hasher instead of a builder. -- var s strings.Builder -- for _, sym := range set.appendSorted(make([]symbol, 0, len(set))) { -- fmt.Fprintf(&s, "%s:%s;", sym.pkg, sym.name) -- } -- return s.String() +-func _() { +- var foo map[string]int +- keys := make([]string, 0, len(foo)) +-for k := range foo { +- keys = append(keys, k) -} - --// index builds the reference graph and encodes the index. --func index(pgfs []*source.ParsedGoFile, id source.PackageID, imports map[source.ImportPath]*source.Metadata) []byte { -- // First pass: gather package-level names and create a declNode for each. -- // -- // In ill-typed code, there may be multiple declarations of the -- // same name; a single declInfo node will represent them all. -- decls := make(map[string]*declNode) -- addDecl := func(id *ast.Ident) { -- if name := id.Name; name != "_" && decls[name] == nil { -- node := &declNode{name: name, extRefsClass: -1} -- node.rep = node -- decls[name] = node -- } -- } -- for _, pgf := range pgfs { -- for _, d := range pgf.File.Decls { -- switch d := d.(type) { -- case *ast.GenDecl: -- switch d.Tok { -- case token.TYPE: -- for _, spec := range d.Specs { -- addDecl(spec.(*ast.TypeSpec).Name) -- } +-} +-`, +- }, +- { +- name: "channel_range", +- before: ` +-package foo - -- case token.VAR, token.CONST: -- for _, spec := range d.Specs { -- for _, ident := range spec.(*ast.ValueSpec).Names { -- addDecl(ident) -- } -- } -- } +-func _() { +- foo := make(chan int) +- foo.range +-} +-`, +- after: ` +-package foo - -- case *ast.FuncDecl: -- // non-method functions -- if d.Recv.NumFields() == 0 { -- addDecl(d.Name) -- } -- } -- } -- } +-func _() { +- foo := make(chan int) +- for ${1:} := range foo { +- $0 +-} +-} +-`, +- }, +- { +- name: "var", +- before: ` +-package foo - -- // Second pass: process files to collect referring identifiers. -- st := &state{classIndex: make(map[string]int)} -- for _, pgf := range pgfs { -- visitFile(pgf.File, imports, decls) -- } +-func foo() (int, error) { return 0, nil } - -- // Find the strong components of the declNode graph -- // using Tarjan's algorithm, and coalesce each component. -- st.index = 1 -- for _, decl := range decls { -- if decl.index == 0 { // unvisited -- st.visit(decl) -- } -- } +-func _() { +- foo().var +-} +-`, +- after: ` +-package foo - -- // TODO(adonovan): opt: consider compressing the serialized -- // representation by recording not the classes but the DAG of -- // non-trivial union operations (the "pointer equivalence" -- // optimization of Hardekopf & Lin). Unlike that algorithm, -- // which piggybacks on SCC coalescing, in our case it would -- // be better to make a forward traversal from the exported -- // decls, since it avoids visiting unreachable nodes, and -- // results in a dense (not sparse) numbering of the sets. +-func foo() (int, error) { return 0, nil } - -- // Tabulate the unique reachability sets of -- // each exported package member. -- classNames := make(map[int][]string) // set of decls (names) for a given reachability set -- for name, decl := range decls { -- if !ast.IsExported(name) { -- continue -- } +-func _() { +- ${1:}, ${2:} := foo() +-} +-`, +- allowMultipleItem: true, +- }, +- { +- name: "var_single_value", +- before: ` +-package foo - -- decl = decl.find() +-func foo() error { return nil } - -- // Skip decls with empty reachability. -- if len(decl.extRefs) == 0 { -- continue -- } +-func _() { +- foo().var +-} +-`, +- allowMultipleItem: true, +- after: ` +-package foo - -- // Canonicalize the set (and memoize). -- class := decl.extRefsClass -- if class < 0 { -- class = st.getClassIndex(decl.extRefs) -- decl.extRefsClass = class -- } -- classNames[class] = append(classNames[class], name) -- } +-func foo() error { return nil } - -- return encode(classNames, st.class) +-func _() { +- ${1:} := foo() -} +-`, +- }, +- { +- name: "var_same_type", +- before: ` +-package foo - --// visitFile inspects the file syntax for referring identifiers, and --// populates the internal and external references of decls. --func visitFile(file *ast.File, imports map[source.ImportPath]*source.Metadata, decls map[string]*declNode) { -- // Import information for this file. Multiple packages -- // may be referenced by a given name in the presence -- // of type errors (or multiple dot imports, which are -- // keyed by "."). -- fileImports := make(map[string][]source.PackageID) +-func foo() (int, int) { return 0, 0 } - -- // importEdge records a reference from decl to an imported symbol -- // (pkgname.name). The package name may be ".". -- importEdge := func(decl *declNode, pkgname, name string) { -- if token.IsExported(name) { -- for _, depID := range fileImports[pkgname] { -- if decl.extRefs == nil { -- decl.extRefs = make(symbolSet) -- } -- decl.extRefs[symbol{depID, name}] = true -- } -- } -- } +-func _() { +- foo().var +-} +-`, +- after: ` +-package foo - -- // visit finds refs within node and builds edges from fromId's decl. -- // References to the type parameters are ignored. -- visit := func(fromId *ast.Ident, node ast.Node, tparams map[string]bool) { -- if fromId.Name == "_" { -- return -- } -- from := decls[fromId.Name] -- // When visiting a method, there may not be a valid type declaration for -- // the receiver. In this case there is no way to refer to the method, so -- // we need not record edges. -- if from == nil { -- return -- } +-func foo() (int, int) { return 0, 0 } - -- // Visit each reference to name or name.sel. -- visitDeclOrSpec(node, func(name, sel string) { -- // Ignore references to type parameters. -- if tparams[name] { -- return -- } +-func _() { +- ${1:}, ${2:} := foo() +-} +-`, +- }, +- { +- name: "print_scalar", +- before: ` +-package foo - -- // If name is declared in the package scope, -- // record an edge whether or not sel is empty. -- // A field or method selector may affect the -- // type of the current decl via initializers: -- // -- // package p -- // var x = y.F -- // var y = struct{ F int }{} -- if to, ok := decls[name]; ok { -- if from.intRefs == nil { -- from.intRefs = make(map[*declNode]bool) -- } -- from.intRefs[to] = true +-func _() { +- var foo int +- foo.print +-} +-`, +- after: ` +-package foo - -- } else { -- // Only record an edge to dot-imported packages -- // if there was no edge to a local name. -- // This assumes that there are no duplicate declarations. -- // We conservatively, assume that this name comes from -- // every dot-imported package. -- importEdge(from, ".", name) -- } +-import "fmt" - -- // Record an edge to an import if it matches the name, even if that -- // name collides with a package level name. Unlike the case of dotted -- // imports, we know the package is invalid here, and choose to fail -- // conservatively. -- if sel != "" { -- importEdge(from, name, sel) -- } -- }) -- } +-func _() { +- var foo int +- fmt.Printf("foo: %v\n", foo) +-} +-`, +- }, +- { +- name: "print_multi", +- before: ` +-package foo - -- // Visit the declarations and gather reference edges. -- // Import declarations appear before all others. -- for _, d := range file.Decls { -- switch d := d.(type) { -- case *ast.GenDecl: -- switch d.Tok { -- case token.IMPORT: -- // Record local import names for this file. -- for _, spec := range d.Specs { -- spec := spec.(*ast.ImportSpec) -- path := source.UnquoteImportPath(spec) -- if path == "" { -- continue -- } -- dep := imports[path] -- if dep == nil { -- // Note here that we don't try to "guess" -- // the name of an import based on e.g. -- // its importPath. Doing so would only -- // result in edges that don't go anywhere. -- continue -- } -- name := string(dep.Name) -- if spec.Name != nil { -- if spec.Name.Name == "_" { -- continue -- } -- name = spec.Name.Name // possibly "." -- } -- fileImports[name] = append(fileImports[name], dep.ID) -- } +-func foo() (int, error) { return 0, nil } - -- case token.TYPE: -- for _, spec := range d.Specs { -- spec := spec.(*ast.TypeSpec) -- tparams := tparamsMap(typeparams.ForTypeSpec(spec)) -- visit(spec.Name, spec, tparams) -- } +-func _() { +- foo().print +-} +-`, +- after: ` +-package foo - -- case token.VAR, token.CONST: -- for _, spec := range d.Specs { -- spec := spec.(*ast.ValueSpec) -- for _, name := range spec.Names { -- visit(name, spec, nil) -- } -- } -- } +-import "fmt" - -- case *ast.FuncDecl: -- // This check for NumFields() > 0 is consistent with go/types, -- // which reports an error but treats the declaration like a -- // normal function when Recv is non-nil but empty -- // (as in func () f()). -- if d.Recv.NumFields() > 0 { -- // Method. Associate it with the receiver. -- _, id, typeParams := astutil.UnpackRecv(d.Recv.List[0].Type) -- if id != nil { -- var tparams map[string]bool -- if len(typeParams) > 0 { -- tparams = make(map[string]bool) -- for _, tparam := range typeParams { -- if tparam.Name != "_" { -- tparams[tparam.Name] = true -- } -- } -- } -- visit(id, d, tparams) -- } -- } else { -- // Non-method. -- tparams := tparamsMap(typeparams.ForFuncType(d.Type)) -- visit(d.Name, d, tparams) -- } -- } -- } --} +-func foo() (int, error) { return 0, nil } - --// tparamsMap returns a set recording each name declared by the provided field --// list. It so happens that we only care about names declared by type parameter --// lists. --func tparamsMap(tparams *ast.FieldList) map[string]bool { -- if tparams == nil || len(tparams.List) == 0 { -- return nil -- } -- m := make(map[string]bool) -- for _, f := range tparams.List { -- for _, name := range f.Names { -- if name.Name != "_" { -- m[name.Name] = true -- } -- } -- } -- return m +-func _() { +- fmt.Println(foo()) -} +-`, +- }, +- { +- name: "string split", +- before: ` +-package foo - --// A refVisitor visits referring identifiers and dotted identifiers. --// --// For a referring identifier I, name="I" and sel="". For a dotted identifier --// q.I, name="q" and sel="I". --type refVisitor = func(name, sel string) +-func foo() []string { +- x := "test" +- return x.split +-}`, +- after: ` +-package foo - --// visitDeclOrSpec visits referring idents or dotted idents that may affect --// the type of the declaration at the given node, which must be an ast.Decl or --// ast.Spec. --func visitDeclOrSpec(node ast.Node, f refVisitor) { -- // Declarations -- switch n := node.(type) { -- // ImportSpecs should not appear here, and will panic in the default case. +-import "strings" - -- case *ast.ValueSpec: -- // Skip Doc, Names, Comments, which do not affect the decl type. -- // Initializers only affect the type of a value spec if the type is unset. -- if n.Type != nil { -- visitExpr(n.Type, f) -- } else { // only need to walk expr list if type is nil -- visitExprList(n.Values, f) -- } +-func foo() []string { +- x := "test" +- return strings.Split(x, "$0") +-}`, +- }, +- { +- name: "string slice join", +- before: ` +-package foo - -- case *ast.TypeSpec: -- // Skip Doc, Name, and Comment, which do not affect the decl type. -- if tparams := typeparams.ForTypeSpec(n); tparams != nil { -- visitFieldList(tparams, f) -- } -- visitExpr(n.Type, f) +-func foo() string { +- x := []string{"a", "test"} +- return x.join +-}`, +- after: ` +-package foo - -- case *ast.BadDecl: -- // nothing to do +-import "strings" - -- // We should not reach here with a GenDecl, so panic below in the default case. +-func foo() string { +- x := []string{"a", "test"} +- return strings.Join(x, "$0") +-}`, +- }, +- { +- name: "if not nil interface", +- before: ` +-package foo - -- case *ast.FuncDecl: -- // Skip Doc, Name, and Body, which do not affect the type. -- // Recv is handled by Refs: methods are associated with their type. -- visitExpr(n.Type, f) +-func _() { +- var foo error +- foo.ifnotnil +-} +-`, +- after: ` +-package foo - -- default: -- panic(fmt.Sprintf("unexpected node type %T", node)) -- } +-func _() { +- var foo error +- if foo != nil { +- $0 -} +-} +-`, +- }, +- { +- name: "if not nil pointer", +- before: ` +-package foo - --// visitExpr visits referring idents and dotted idents that may affect the --// type of expr. --// --// visitExpr can't reliably distinguish a dotted ident pkg.X from a --// selection expr.f or T.method. --func visitExpr(expr ast.Expr, f refVisitor) { -- switch n := expr.(type) { -- // These four cases account for about two thirds of all nodes, -- // so we place them first to shorten the common control paths. -- // (See go.dev/cl/480915.) -- case *ast.Ident: -- f(n.Name, "") +-func _() { +- var foo *int +- foo.ifnotnil +-} +-`, +- after: ` +-package foo - -- case *ast.BasicLit: -- // nothing to do +-func _() { +- var foo *int +- if foo != nil { +- $0 +-} +-} +-`, +- }, +- { +- name: "if not nil slice", +- before: ` +-package foo - -- case *ast.SelectorExpr: -- if ident, ok := n.X.(*ast.Ident); ok { -- f(ident.Name, n.Sel.Name) -- } else { -- visitExpr(n.X, f) -- // Skip n.Sel as we don't care about which field or method is selected, -- // as we'll have recorded an edge to all declarations relevant to the -- // receiver type via visiting n.X above. -- } +-func _() { +- var foo []int +- foo.ifnotnil +-} +-`, +- after: ` +-package foo - -- case *ast.CallExpr: -- visitExpr(n.Fun, f) -- visitExprList(n.Args, f) // args affect types for unsafe.Sizeof or builtins or generics +-func _() { +- var foo []int +- if foo != nil { +- $0 +-} +-} +-`, +- }, +- { +- name: "if not nil map", +- before: ` +-package foo - -- // Expressions -- case *ast.Ellipsis: -- if n.Elt != nil { -- visitExpr(n.Elt, f) -- } +-func _() { +- var foo map[string]any +- foo.ifnotnil +-} +-`, +- after: ` +-package foo - -- case *ast.FuncLit: -- visitExpr(n.Type, f) -- // Skip Body, which does not affect the type. +-func _() { +- var foo map[string]any +- if foo != nil { +- $0 +-} +-} +-`, +- }, +- { +- name: "if not nil channel", +- before: ` +-package foo - -- case *ast.CompositeLit: -- if n.Type != nil { -- visitExpr(n.Type, f) -- } -- // Skip Elts, which do not affect the type. +-func _() { +- var foo chan int +- foo.ifnotnil +-} +-`, +- after: ` +-package foo - -- case *ast.ParenExpr: -- visitExpr(n.X, f) +-func _() { +- var foo chan int +- if foo != nil { +- $0 +-} +-} +-`, +- }, +- { +- name: "if not nil function", +- before: ` +-package foo - -- case *ast.IndexExpr: -- visitExpr(n.X, f) -- visitExpr(n.Index, f) // may affect type for instantiations +-func _() { +- var foo func() +- foo.ifnotnil +-} +-`, +- after: ` +-package foo - -- case *typeparams.IndexListExpr: -- visitExpr(n.X, f) -- for _, index := range n.Indices { -- visitExpr(index, f) // may affect the type for instantiations -- } +-func _() { +- var foo func() +- if foo != nil { +- $0 +-} +-} +-`, +- }, +- { +- name: "slice_len", +- before: ` +-package foo - -- case *ast.SliceExpr: -- visitExpr(n.X, f) -- // skip Low, High, and Max, which do not affect type. +-func _() { +- var foo []int +- foo.len +-} +-`, +- after: ` +-package foo - -- case *ast.TypeAssertExpr: -- // Skip X, as it doesn't actually affect the resulting type of the type -- // assertion. -- if n.Type != nil { -- visitExpr(n.Type, f) -- } +-func _() { +- var foo []int +- len(foo) +-} +-`, +- }, +- { +- name: "map_len", +- before: ` +-package foo - -- case *ast.StarExpr: -- visitExpr(n.X, f) +-func _() { +- var foo map[string]int +- foo.len +-} +-`, +- after: ` +-package foo - -- case *ast.UnaryExpr: -- visitExpr(n.X, f) +-func _() { +- var foo map[string]int +- len(foo) +-} +-`, +- }, +- { +- name: "slice_for", +- allowMultipleItem: true, +- before: ` +-package foo - -- case *ast.BinaryExpr: -- visitExpr(n.X, f) -- visitExpr(n.Y, f) +-func _() { +- var foo []int +- foo.for +-} +-`, +- after: ` +-package foo - -- case *ast.KeyValueExpr: -- panic("unreachable") // unreachable, as we don't descend into elts of composite lits. +-func _() { +- var foo []int +- for ${1:} := range foo { +- $0 +-} +-} +-`, +- }, +- { +- name: "map_for", +- allowMultipleItem: true, +- before: ` +-package foo - -- case *ast.ArrayType: -- if n.Len != nil { -- visitExpr(n.Len, f) -- } -- visitExpr(n.Elt, f) +-func _() { +- var foo map[string]int +- foo.for +-} +-`, +- after: ` +-package foo - -- case *ast.StructType: -- visitFieldList(n.Fields, f) +-func _() { +- var foo map[string]int +- for ${1:} := range foo { +- $0 +-} +-} +-`, +- }, +- { +- name: "chan_for", +- allowMultipleItem: true, +- before: ` +-package foo - -- case *ast.FuncType: -- if tparams := typeparams.ForFuncType(n); tparams != nil { -- visitFieldList(tparams, f) -- } -- if n.Params != nil { -- visitFieldList(n.Params, f) -- } -- if n.Results != nil { -- visitFieldList(n.Results, f) -- } +-func _() { +- var foo chan int +- foo.for +-} +-`, +- after: ` +-package foo - -- case *ast.InterfaceType: -- visitFieldList(n.Methods, f) +-func _() { +- var foo chan int +- for ${1:} := range foo { +- $0 +-} +-} +-`, +- }, +- { +- name: "slice_forr", +- before: ` +-package foo - -- case *ast.MapType: -- visitExpr(n.Key, f) -- visitExpr(n.Value, f) +-func _() { +- var foo []int +- foo.forr +-} +-`, +- after: ` +-package foo - -- case *ast.ChanType: -- visitExpr(n.Value, f) +-func _() { +- var foo []int +- for ${1:}, ${2:} := range foo { +- $0 +-} +-} +-`, +- }, +- { +- name: "slice_forr", +- before: ` +-package foo - -- case *ast.BadExpr: -- // nothing to do +-func _() { +- var foo []int +- foo.forr +-} +-`, +- after: ` +-package foo - -- default: -- panic(fmt.Sprintf("ast.Walk: unexpected node type %T", n)) -- } +-func _() { +- var foo []int +- for ${1:}, ${2:} := range foo { +- $0 -} +-} +-`, +- }, +- { +- name: "map_forr", +- before: ` +-package foo - --func visitExprList(list []ast.Expr, f refVisitor) { -- for _, x := range list { -- visitExpr(x, f) -- } +-func _() { +- var foo map[string]int +- foo.forr -} +-`, +- after: ` +-package foo - --func visitFieldList(n *ast.FieldList, f refVisitor) { -- for _, field := range n.List { -- visitExpr(field.Type, f) +-func _() { +- var foo map[string]int +- for ${1:}, ${2:} := range foo { +- $0 +-} +-} +-`, +- }, - } +- +- r := WithOptions( +- Settings{ +- "experimentalPostfixCompletions": true, +- }, +- ) +- r.Run(t, mod, func(t *testing.T, env *Env) { +- env.CreateBuffer("foo.go", "") +- +- for _, c := range cases { +- t.Run(c.name, func(t *testing.T) { +- c.before = strings.Trim(c.before, "\n") +- c.after = strings.Trim(c.after, "\n") +- +- env.SetBufferContent("foo.go", c.before) +- +- loc := env.RegexpSearch("foo.go", "\n}") +- completions := env.Completion(loc) +- if len(completions.Items) < 1 { +- t.Fatalf("expected at least one completion, got %v", completions.Items) +- } +- if !c.allowMultipleItem && len(completions.Items) > 1 { +- t.Fatalf("expected one completion, got %v", completions.Items) +- } +- +- env.AcceptCompletion(loc, completions.Items[0]) +- +- if buf := env.BufferText("foo.go"); buf != c.after { +- t.Errorf("\nGOT:\n%s\nEXPECTED:\n%s", buf, c.after) +- } +- }) +- } +- }) -} +diff -urN a/gopls/internal/test/integration/debug/debug_test.go b/gopls/internal/test/integration/debug/debug_test.go +--- a/gopls/internal/test/integration/debug/debug_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/debug/debug_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,101 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// -- strong component graph construction (plundered from go/pointer) -- +-package debug - --// visit implements the depth-first search of Tarjan's SCC algorithm --// (see https://doi.org/10.1137/0201010). --// Precondition: x is canonical. --func (st *state) visit(x *declNode) { -- checkCanonical(x) -- x.index = st.index -- x.lowlink = st.index -- st.index++ +-import ( +- "context" +- "encoding/json" +- "io" +- "net/http" +- "strings" +- "testing" - -- st.stack = append(st.stack, x) // push -- assert(x.scc == 0, "node revisited") -- x.scc = -1 +- "golang.org/x/tools/gopls/internal/hooks" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/command" +- . "golang.org/x/tools/gopls/internal/test/integration" +- "golang.org/x/tools/gopls/internal/util/bug" +-) - -- for y := range x.intRefs { -- // Loop invariant: x is canonical. +-func TestMain(m *testing.M) { +- Main(m, hooks.Options) +-} - -- y := y.find() +-func TestBugNotification(t *testing.T) { +- // Verify that a properly configured session gets notified of a bug on the +- // server. +- WithOptions( +- Modes(Default), // must be in-process to receive the bug report below +- Settings{"showBugReports": true}, +- ).Run(t, "", func(t *testing.T, env *Env) { +- const desc = "got a bug" +- bug.Report(desc) +- env.Await(ShownMessage(desc)) +- }) +-} - -- if x == y { -- continue // nodes already coalesced +-// TestStartDebugging executes a gopls.start_debugging command to +-// start the internal web server. +-func TestStartDebugging(t *testing.T) { +- WithOptions( +- Modes(Default|Experimental), // doesn't work in Forwarded mode +- ).Run(t, "", func(t *testing.T, env *Env) { +- // Start a debugging server. +- res, err := startDebugging(env.Ctx, env.Editor.Server, &command.DebuggingArgs{ +- Addr: "", // any free port +- }) +- if err != nil { +- t.Fatalf("startDebugging: %v", err) - } - -- switch { -- case y.scc > 0: -- // y is already a collapsed SCC +- // Assert that the server requested that the +- // client show the debug page in a browser. +- debugURL := res.URLs[0] +- env.Await(ShownDocument(debugURL)) - -- case y.scc < 0: -- // y is on the stack, and thus in the current SCC. -- if y.index < x.lowlink { -- x.lowlink = y.index -- } +- // Send a request to the debug server and ensure it responds. +- resp, err := http.Get(debugURL) +- if err != nil { +- t.Fatal(err) +- } +- defer resp.Body.Close() +- data, err := io.ReadAll(resp.Body) +- if err != nil { +- t.Fatalf("reading HTTP response body: %v", err) +- } +- const want = "Gopls" +- if !strings.Contains(string(data), want) { +- t.Errorf("GET %s response does not contain %q: <<%s>>", debugURL, want, data) +- } +- }) +-} - -- default: -- // y is unvisited; visit it now. -- st.visit(y) -- // Note: x and y are now non-canonical. +-// startDebugging starts a debugging server. +-// TODO(adonovan): move into command package? +-func startDebugging(ctx context.Context, server protocol.Server, args *command.DebuggingArgs) (*command.DebuggingResult, error) { +- rawArgs, err := command.MarshalArgs(args) +- if err != nil { +- return nil, err +- } +- res0, err := server.ExecuteCommand(ctx, &protocol.ExecuteCommandParams{ +- Command: command.StartDebugging.ID(), +- Arguments: rawArgs, +- }) +- if err != nil { +- return nil, err +- } +- // res0 is the result of a schemaless (map[string]any) JSON decoding. +- // Re-encode and decode into the correct Go struct type. +- // TODO(adonovan): fix (*serverDispatcher).ExecuteCommand. +- data, err := json.Marshal(res0) +- if err != nil { +- return nil, err +- } +- var res *command.DebuggingResult +- if err := json.Unmarshal(data, &res); err != nil { +- return nil, err +- } +- return res, nil +-} +diff -urN a/gopls/internal/test/integration/diagnostics/analysis_test.go b/gopls/internal/test/integration/diagnostics/analysis_test.go +--- a/gopls/internal/test/integration/diagnostics/analysis_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/diagnostics/analysis_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,127 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- x = x.find() +-package diagnostics - -- if y.lowlink < x.lowlink { -- x.lowlink = y.lowlink -- } -- } -- } -- checkCanonical(x) +-import ( +- "fmt" +- "testing" - -- // Is x the root of an SCC? -- if x.lowlink == x.index { -- // Coalesce all nodes in the SCC. -- for { -- // Pop y from stack. -- i := len(st.stack) - 1 -- y := st.stack[i] -- st.stack = st.stack[:i] +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/protocol" +- . "golang.org/x/tools/gopls/internal/test/integration" +-) - -- checkCanonical(x) -- checkCanonical(y) +-// Test for the timeformat analyzer, following golang/vscode-go#2406. +-// +-// This test checks that applying the suggested fix from the analyzer resolves +-// the diagnostic warning. +-func TestTimeFormatAnalyzer(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - -- if x == y { -- break // SCC is complete. -- } -- coalesce(x, y) -- } +-go 1.18 +--- main.go -- +-package main - -- // Accumulate union of extRefs over -- // internal edges (to other SCCs). -- for y := range x.intRefs { -- y := y.find() -- if y == x { -- continue // already coalesced -- } -- assert(y.scc == 1, "edge to non-scc node") -- for z := range y.extRefs { -- if x.extRefs == nil { -- x.extRefs = make(symbolSet) -- } -- x.extRefs[z] = true // extRefs: x U= y -- } -- } +-import ( +- "fmt" +- "time" +-) - -- x.scc = 1 -- } --} +-func main() { +- now := time.Now() +- fmt.Println(now.Format("2006-02-01")) +-}` - --// coalesce combines two nodes in the strong component graph. --// Precondition: x and y are canonical. --func coalesce(x, y *declNode) { -- // x becomes y's canonical representative. -- y.rep = x +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") - -- // x accumulates y's internal references. -- for z := range y.intRefs { -- x.intRefs[z] = true -- } -- y.intRefs = nil +- var d protocol.PublishDiagnosticsParams +- env.AfterChange( +- Diagnostics(env.AtRegexp("main.go", "2006-02-01")), +- ReadDiagnostics("main.go", &d), +- ) - -- // x accumulates y's external references. -- for z := range y.extRefs { -- if x.extRefs == nil { -- x.extRefs = make(symbolSet) -- } -- x.extRefs[z] = true -- } -- y.extRefs = nil +- env.ApplyQuickFixes("main.go", d.Diagnostics) +- env.AfterChange(NoDiagnostics(ForFile("main.go"))) +- }) -} - --// find returns the canonical node decl. --// (The nodes form a disjoint set forest.) --func (decl *declNode) find() *declNode { -- rep := decl.rep -- if rep != decl { -- rep = rep.find() -- decl.rep = rep // simple path compression (no union-by-rank) -- } -- return rep --} +-func TestAnalysisProgressReporting(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - --const debugSCC = false // enable assertions in strong-component algorithm +-go 1.18 - --func checkCanonical(x *declNode) { -- if debugSCC { -- assert(x == x.find(), "not canonical") +--- main.go -- +-package main +- +-func main() { +-}` +- +- tests := []struct { +- setting bool +- want Expectation +- }{ +- {true, CompletedWork(cache.AnalysisProgressTitle, 1, true)}, +- {false, Not(CompletedWork(cache.AnalysisProgressTitle, 1, true))}, - } --} - --func assert(cond bool, msg string) { -- if debugSCC && !cond { -- panic(msg) +- for _, test := range tests { +- t.Run(fmt.Sprint(test.setting), func(t *testing.T) { +- WithOptions( +- Settings{ +- "reportAnalysisProgressAfter": "0s", +- "analysisProgressReporting": test.setting, +- }, +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- env.AfterChange(test.want) +- }) +- }) - } -} - --// -- serialization -- +-// Test the embed directive analyzer. +-// +-// There is a fix for missing imports, but it should not trigger for other +-// kinds of issues reported by the analayzer, here the variable +-// declaration following the embed directive is wrong. +-func TestNoSuggestedFixesForEmbedDirectiveDeclaration(t *testing.T) { +- const generated = ` +--- go.mod -- +-module mod.com - --// (The name says gob but in fact we use frob.) --var classesCodec = frob.CodecFor[gobClasses]() +-go 1.20 - --type gobClasses struct { -- Strings []string // table of strings (PackageIDs and names) -- Classes []gobClass --} +--- foo.txt -- +-FOO - --type gobClass struct { -- Decls []int32 // indices into gobClasses.Strings -- Refs []int32 // list of (package, name) pairs, each an index into gobClasses.Strings --} +--- main.go -- +-package main - --// encode encodes the equivalence classes, --// (classNames[i], classes[i]), for i in range classes. --// --// With the current encoding, across kubernetes, --// the encoded size distribution has --// p50 = 511B, p95 = 4.4KB, max = 108K. --func encode(classNames map[int][]string, classes []symbolSet) []byte { -- payload := gobClasses{ -- Classes: make([]gobClass, 0, len(classNames)), -- } +-import _ "embed" - -- // index of unique strings -- strings := make(map[string]int32) -- stringIndex := func(s string) int32 { -- i, ok := strings[s] -- if !ok { -- i = int32(len(payload.Strings)) -- strings[s] = i -- payload.Strings = append(payload.Strings, s) +-//go:embed foo.txt +-var foo, bar string +- +-func main() { +- _ = foo +-} +-` +- Run(t, generated, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- var d protocol.PublishDiagnosticsParams +- env.AfterChange( +- Diagnostics(env.AtRegexp("main.go", "//go:embed")), +- ReadDiagnostics("main.go", &d), +- ) +- if fixes := env.GetQuickFixes("main.go", d.Diagnostics); len(fixes) != 0 { +- t.Errorf("got quick fixes %v, wanted none", fixes) - } -- return i -- } +- }) +-} +diff -urN a/gopls/internal/test/integration/diagnostics/builtin_test.go b/gopls/internal/test/integration/diagnostics/builtin_test.go +--- a/gopls/internal/test/integration/diagnostics/builtin_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/diagnostics/builtin_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,35 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- var refs []symbol // recycled temporary -- for class, names := range classNames { -- set := classes[class] +-package diagnostics - -- // names, sorted -- sort.Strings(names) -- gobDecls := make([]int32, len(names)) -- for i, name := range names { -- gobDecls[i] = stringIndex(name) -- } +-import ( +- "strings" +- "testing" - -- // refs, sorted by ascending (PackageID, name) -- gobRefs := make([]int32, 0, 2*len(set)) -- for _, sym := range set.appendSorted(refs[:0]) { -- gobRefs = append(gobRefs, -- stringIndex(string(sym.pkg)), -- stringIndex(sym.name)) -- } -- payload.Classes = append(payload.Classes, gobClass{ -- Decls: gobDecls, -- Refs: gobRefs, -- }) -- } +- . "golang.org/x/tools/gopls/internal/test/integration" +-) - -- return classesCodec.Encode(payload) --} +-func TestIssue44866(t *testing.T) { +- src := ` +--- go.mod -- +-module mod.com - --func decode(pkgIndex *PackageIndex, id source.PackageID, data []byte) []Class { -- var payload gobClasses -- classesCodec.Decode(data, &payload) +-go 1.12 +--- a.go -- +-package a - -- classes := make([]Class, len(payload.Classes)) -- for i, gobClass := range payload.Classes { -- decls := make([]string, len(gobClass.Decls)) -- for i, decl := range gobClass.Decls { -- decls[i] = payload.Strings[decl] -- } -- refs := make([]Symbol, len(gobClass.Refs)/2) -- for i := range refs { -- pkgID := pkgIndex.IndexID(source.PackageID(payload.Strings[gobClass.Refs[2*i]])) -- name := payload.Strings[gobClass.Refs[2*i+1]] -- refs[i] = Symbol{Package: pkgID, Name: name} -- } -- classes[i] = Class{ -- Decls: decls, -- Refs: refs, +-const ( +- c = iota +-) +-` +- Run(t, src, func(t *testing.T, env *Env) { +- env.OpenFile("a.go") +- loc := env.GoToDefinition(env.RegexpSearch("a.go", "iota")) +- if !strings.HasSuffix(string(loc.URI), "builtin.go") { +- t.Fatalf("jumped to %q, want builtin.go", loc.URI) - } -- } -- -- // Sort by ascending Decls[0]. -- // TODO(adonovan): move sort to encoder. Determinism is good. -- sort.Slice(classes, func(i, j int) bool { -- return classes[i].Decls[0] < classes[j].Decls[0] +- env.AfterChange(NoDiagnostics(ForFile("builtin.go"))) - }) -- -- return classes -} -diff -urN a/gopls/internal/lsp/source/typerefs/refs_test.go b/gopls/internal/lsp/source/typerefs/refs_test.go ---- a/gopls/internal/lsp/source/typerefs/refs_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/typerefs/refs_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,558 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/test/integration/diagnostics/diagnostics_test.go b/gopls/internal/test/integration/diagnostics/diagnostics_test.go +--- a/gopls/internal/test/integration/diagnostics/diagnostics_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/diagnostics/diagnostics_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,2226 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package typerefs_test +-package diagnostics - -import ( - "context" - "fmt" -- "go/token" -- "sort" +- "os/exec" - "testing" - -- "github.com/google/go-cmp/cmp" -- "golang.org/x/tools/gopls/internal/lsp/cache" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/lsp/source/typerefs" -- "golang.org/x/tools/gopls/internal/span" +- "golang.org/x/tools/gopls/internal/hooks" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/server" +- . "golang.org/x/tools/gopls/internal/test/integration" +- "golang.org/x/tools/gopls/internal/test/integration/fake" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/gopls/internal/util/goversion" - "golang.org/x/tools/internal/testenv" -) - --// TestRefs checks that the analysis reports, for each exported member --// of the test package ("p"), its correct dependencies on exported --// members of its direct imports (e.g. "ext"). --func TestRefs(t *testing.T) { -- ctx := context.Background() -- -- tests := []struct { -- label string -- srcs []string // source for the local package; package name must be p -- imports map[string]string // for simplicity: importPath -> pkgID/pkgName (we set pkgName == pkgID); 'ext' is always available. -- want map[string][]string // decl name -> id.<decl name> -- go118 bool // test uses generics -- allowErrs bool // whether we expect parsing errors -- }{ -- { -- label: "empty package", -- want: map[string][]string{}, -- }, -- { -- label: "fields", -- srcs: []string{` --package p -- --import "ext" -- --type A struct{ b B } --type B func(c C) (d D) --type C ext.C --type D ext.D -- --// Should not be referenced by field names. --type b ext.B_ --type c int.C_ --type d ext.D_ --`}, -- want: map[string][]string{ -- "A": {"ext.C", "ext.D"}, -- "B": {"ext.C", "ext.D"}, -- "C": {"ext.C"}, -- "D": {"ext.D"}, -- }, -- }, -- { -- label: "embedding", -- srcs: []string{` --package p -- --import "ext" -- --type A struct{ -- B -- _ struct { -- C -- } -- D --} --type B ext.B --type C ext.C --type D interface{ -- B +-func TestMain(m *testing.M) { +- bug.PanicOnBugs = true +- Main(m, hooks.Options) -} --`}, -- want: map[string][]string{ -- "A": {"ext.B", "ext.C"}, -- "B": {"ext.B"}, -- "C": {"ext.C"}, -- "D": {"ext.B"}, -- }, -- }, -- { -- label: "constraint embedding", -- srcs: []string{` --package p - --import "ext" +-// Use mod.com for all go.mod files due to golang/go#35230. +-const exampleProgram = ` +--- go.mod -- +-module mod.com - --type A interface{ -- int | B | ~C -- struct{D} --} +-go 1.12 +--- main.go -- +-package main - --type B ext.B --type C ext.C --type D ext.D --`}, -- want: map[string][]string{ -- "A": {"ext.B", "ext.C", "ext.D"}, -- "B": {"ext.B"}, -- "C": {"ext.C"}, -- "D": {"ext.D"}, -- }, -- go118: true, -- }, -- { -- label: "funcs", -- srcs: []string{` --package p +-import "fmt" - --import "ext" +-func main() { +- fmt.Println("Hello World.") +-}` - --type A ext.A --type B ext.B --const C B = 2 --func F(A) B { -- return C +-func TestDiagnosticErrorInEditedFile(t *testing.T) { +- // This test is very basic: start with a clean Go program, make an error, and +- // get a diagnostic for that error. However, it also demonstrates how to +- // combine Expectations to await more complex state in the editor. +- Run(t, exampleProgram, func(t *testing.T, env *Env) { +- // Deleting the 'n' at the end of Println should generate a single error +- // diagnostic. +- env.OpenFile("main.go") +- env.RegexpReplace("main.go", "Printl(n)", "") +- env.AfterChange( +- Diagnostics(env.AtRegexp("main.go", "Printl")), +- // Assert that this test has sent no error logs to the client. This is not +- // strictly necessary for testing this regression, but is included here +- // as an example of using the NoErrorLogs() expectation. Feel free to +- // delete. +- NoErrorLogs(), +- ) +- }) -} --var V = F(W) --var W A --`}, -- want: map[string][]string{ -- "A": {"ext.A"}, -- "B": {"ext.B"}, -- "C": {"ext.B"}, -- "F": {"ext.A", "ext.B"}, -- "V": { -- "ext.A", // via F -- "ext.B", // via W: can't be eliminated: F could be builtin or generic -- }, -- "W": {"ext.A"}, -- }, -- }, -- { -- label: "methods", -- srcs: []string{`package p -- --import "ext" -- --type A ext.A --type B ext.B --`, `package p -- --func (A) M(B) --func (*B) M(A) --`}, -- want: map[string][]string{ -- "A": {"ext.A", "ext.B"}, -- "B": {"ext.A", "ext.B"}, -- }, -- }, -- { -- label: "initializers", -- srcs: []string{` --package p - --import "ext" +-func TestMissingImportDiagsClearOnFirstFile(t *testing.T) { +- const onlyMod = ` +--- go.mod -- +-module mod.com - --var A b = C // type does not depend on C --type b ext.B --var C = d // type does depend on D --var d b +-go 1.12 +-` +- Run(t, onlyMod, func(t *testing.T, env *Env) { +- env.CreateBuffer("main.go", `package main - --var e = d + a +-func m() { +- log.Println() +-} +-`) +- env.AfterChange(Diagnostics(env.AtRegexp("main.go", "log"))) +- env.SaveBuffer("main.go") +- env.AfterChange(NoDiagnostics(ForFile("main.go"))) +- }) +-} - --var F = func() B { return E } +-func TestDiagnosticErrorInNewFile(t *testing.T) { +- const brokenFile = `package main - --var G = struct{ -- A b -- _ [unsafe.Sizeof(ext.V)]int // array size + Sizeof creates edge to a var -- _ [unsafe.Sizeof(G)]int // creates a self edge; doesn't affect output though --}{} +-const Foo = "abc +-` +- Run(t, brokenFile, func(t *testing.T, env *Env) { +- env.CreateBuffer("broken.go", brokenFile) +- env.AfterChange(Diagnostics(env.AtRegexp("broken.go", "\"abc"))) +- }) +-} - --var H = (D + A + C*C) +-// badPackage contains a duplicate definition of the 'a' const. +-const badPackage = ` +--- go.mod -- +-module mod.com - --var I = (A+C).F --`}, -- want: map[string][]string{ -- "A": {"ext.B"}, -- "C": {"ext.B"}, // via d -- "G": {"ext.B", "ext.V"}, // via b,C -- "H": {"ext.B"}, // via d,A,C -- "I": {"ext.B"}, -- }, -- }, -- { -- label: "builtins", -- srcs: []string{`package p +-go 1.12 +--- a.go -- +-package consts - --import "ext" +-const a = 1 +--- b.go -- +-package consts - --var A = new(b) --type b struct{ ext.B } +-const a = 2 +-` - --type C chan d --type d ext.D +-func TestDiagnosticClearingOnEdit(t *testing.T) { +- Run(t, badPackage, func(t *testing.T, env *Env) { +- env.OpenFile("b.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("a.go", "a = 1")), +- Diagnostics(env.AtRegexp("b.go", "a = 2")), +- ) - --type S []ext.S --type t ext.T --var U = append(([]*S)(nil), new(t)) +- // Fix the error by editing the const name in b.go to `b`. +- env.RegexpReplace("b.go", "(a) = 2", "b") +- env.AfterChange( +- NoDiagnostics(ForFile("a.go")), +- NoDiagnostics(ForFile("b.go")), +- ) +- }) +-} - --type X map[k]v --type k ext.K --type v ext.V +-func TestDiagnosticClearingOnDelete_Issue37049(t *testing.T) { +- Run(t, badPackage, func(t *testing.T, env *Env) { +- env.OpenFile("a.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("a.go", "a = 1")), +- Diagnostics(env.AtRegexp("b.go", "a = 2")), +- ) +- env.RemoveWorkspaceFile("b.go") - --var Z = make(map[k]A) +- env.AfterChange( +- NoDiagnostics(ForFile("a.go")), +- NoDiagnostics(ForFile("b.go")), +- ) +- }) +-} - --// close, delete, and panic cannot occur outside of statements --`}, -- want: map[string][]string{ -- "A": {"ext.B"}, -- "C": {"ext.D"}, -- "S": {"ext.S"}, -- "U": {"ext.S", "ext.T"}, // ext.T edge could be eliminated -- "X": {"ext.K", "ext.V"}, -- "Z": {"ext.B", "ext.K"}, -- }, -- }, -- { -- label: "builtin shadowing", -- srcs: []string{`package p +-func TestDiagnosticClearingOnClose(t *testing.T) { +- Run(t, badPackage, func(t *testing.T, env *Env) { +- env.CreateBuffer("c.go", `package consts - --import "ext" +-const a = 3`) +- env.AfterChange( +- Diagnostics(env.AtRegexp("a.go", "a = 1")), +- Diagnostics(env.AtRegexp("b.go", "a = 2")), +- Diagnostics(env.AtRegexp("c.go", "a = 3")), +- ) +- env.CloseBuffer("c.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("a.go", "a = 1")), +- Diagnostics(env.AtRegexp("b.go", "a = 2")), +- NoDiagnostics(ForFile("c.go")), +- ) +- }) +-} - --var A = new(ext.B) --func new() c --type c ext.C --`}, -- want: map[string][]string{ -- "A": {"ext.B", "ext.C"}, -- }, -- }, -- { -- label: "named forwarding", -- srcs: []string{`package p +-// Tests golang/go#37978. +-func TestIssue37978(t *testing.T) { +- Run(t, exampleProgram, func(t *testing.T, env *Env) { +- // Create a new workspace-level directory and empty file. +- env.CreateBuffer("c/c.go", "") - --import "ext" +- // Write the file contents with a missing import. +- env.EditBuffer("c/c.go", protocol.TextEdit{ +- NewText: `package c - --type A B --type B c --type c ext.C --`}, -- want: map[string][]string{ -- "A": {"ext.C"}, -- "B": {"ext.C"}, -- }, -- }, -- { -- label: "aliases", -- srcs: []string{`package p +-const a = http.MethodGet +-`, +- }) +- env.AfterChange( +- Diagnostics(env.AtRegexp("c/c.go", "http.MethodGet")), +- ) +- // Save file, which will organize imports, adding the expected import. +- // Expect the diagnostics to clear. +- env.SaveBuffer("c/c.go") +- env.AfterChange( +- NoDiagnostics(ForFile("c/c.go")), +- ) +- }) +-} - --import "ext" +-// Tests golang/go#38878: good a.go, bad a_test.go, remove a_test.go but its errors remain +-// If the file is open in the editor, this is working as intended +-// If the file is not open in the editor, the errors go away +-const test38878 = ` +--- go.mod -- +-module foo - --type A = B --type B = C --type C = ext.C --`}, -- want: map[string][]string{ -- "A": {"ext.C"}, -- "B": {"ext.C"}, -- "C": {"ext.C"}, -- }, -- }, -- { -- label: "array length", -- srcs: []string{`package p +-go 1.12 +--- a.go -- +-package x - --import "ext" --import "unsafe" +-// import "fmt" - --type A [unsafe.Sizeof(ext.B{ext.C})]int --type A2 [unsafe.Sizeof(ext.B{f:ext.C})]int // use a KeyValueExpr +-func f() {} - --type D [unsafe.Sizeof(struct{ f E })]int --type E ext.E +--- a_test.go -- +-package x - --type F [3]G --type G [ext.C]int --`}, -- want: map[string][]string{ -- "A": {"ext.B"}, // no ext.C: doesn't enter CompLit -- "A2": {"ext.B"}, // ditto -- "D": {"ext.E"}, -- "E": {"ext.E"}, -- "F": {"ext.C"}, -- "G": {"ext.C"}, -- }, -- }, -- { -- label: "imports", -- srcs: []string{`package p +-import "testing" - --import "ext" +-func TestA(t *testing.T) { +- f(3) +-} +-` - --import ( -- "q" -- r2 "r" -- "s" // note: package name is t -- "z" --) +-// Tests golang/go#38878: deleting a test file should clear its errors, and +-// not break the workspace. +-func TestDeleteTestVariant(t *testing.T) { +- Run(t, test38878, func(t *testing.T, env *Env) { +- env.AfterChange(Diagnostics(env.AtRegexp("a_test.go", `f\((3)\)`))) +- env.RemoveWorkspaceFile("a_test.go") +- env.AfterChange(NoDiagnostics(ForFile("a_test.go"))) - --type A struct { -- q.Q -- r2.R -- s.S // invalid ref -- z.Z // references both external z.Z as well as package-level type z +- // Make sure the test variant has been removed from the workspace by +- // triggering a metadata load. +- env.OpenFile("a.go") +- env.RegexpReplace("a.go", `// import`, "import") +- env.AfterChange(Diagnostics(env.AtRegexp("a.go", `"fmt"`))) +- }) -} - --type B struct { -- r.R // invalid ref -- t.T +-// Tests golang/go#38878: deleting a test file on disk while it's still open +-// should not clear its errors. +-func TestDeleteTestVariant_DiskOnly(t *testing.T) { +- Run(t, test38878, func(t *testing.T, env *Env) { +- env.OpenFile("a_test.go") +- env.AfterChange(Diagnostics(AtPosition("a_test.go", 5, 3))) +- env.Sandbox.Workdir.RemoveFile(context.Background(), "a_test.go") +- env.AfterChange(Diagnostics(AtPosition("a_test.go", 5, 3))) +- }) -} - --var X int = q.V // X={}: no descent into RHS of 'var v T = rhs' --var Y = q.V.W +-// TestNoMod confirms that gopls continues to work when a user adds a go.mod +-// file to their workspace. +-func TestNoMod(t *testing.T) { +- const noMod = ` +--- main.go -- +-package main - --type z ext.Z --`}, -- imports: map[string]string{"q": "q", "r": "r", "s": "t", "z": "z"}, -- want: map[string][]string{ -- "A": {"ext.Z", "q.Q", "r.R", "z.Z"}, -- "B": {"t.T"}, -- "Y": {"q.V"}, -- }, -- }, -- { -- label: "import blank", -- srcs: []string{`package p +-import "mod.com/bob" - --import _ "q" +-func main() { +- bob.Hello() +-} +--- bob/bob.go -- +-package bob - --type A q.Q --`}, -- imports: map[string]string{"q": "q"}, -- want: map[string][]string{}, -- }, -- { -- label: "import dot", -- srcs: []string{`package p +-func Hello() { +- var x int +-} +-` - --import . "q" +- t.Run("manual", func(t *testing.T) { +- Run(t, noMod, func(t *testing.T, env *Env) { +- env.OnceMet( +- InitialWorkspaceLoad, +- Diagnostics(env.AtRegexp("main.go", `"mod.com/bob"`)), +- ) +- env.CreateBuffer("go.mod", `module mod.com - --type A q.Q // not actually an edge, since q is imported . --type B struct { -- C // assumed to be an edge to q -- D // resolved to package decl +- go 1.12 +-`) +- env.SaveBuffer("go.mod") +- var d protocol.PublishDiagnosticsParams +- env.AfterChange( +- NoDiagnostics(ForFile("main.go")), +- Diagnostics(env.AtRegexp("bob/bob.go", "x")), +- ReadDiagnostics("bob/bob.go", &d), +- ) +- if len(d.Diagnostics) != 1 { +- t.Fatalf("expected 1 diagnostic, got %v", len(d.Diagnostics)) +- } +- }) +- }) +- t.Run("initialized", func(t *testing.T) { +- Run(t, noMod, func(t *testing.T, env *Env) { +- env.OnceMet( +- InitialWorkspaceLoad, +- Diagnostics(env.AtRegexp("main.go", `"mod.com/bob"`)), +- ) +- env.RunGoCommand("mod", "init", "mod.com") +- env.AfterChange( +- NoDiagnostics(ForFile("main.go")), +- Diagnostics(env.AtRegexp("bob/bob.go", "x")), +- ) +- }) +- }) +- +- t.Run("without workspace module", func(t *testing.T) { +- WithOptions( +- Modes(Default), +- ).Run(t, noMod, func(t *testing.T, env *Env) { +- env.OnceMet( +- InitialWorkspaceLoad, +- Diagnostics(env.AtRegexp("main.go", `"mod.com/bob"`)), +- ) +- if err := env.Sandbox.RunGoCommand(env.Ctx, "", "mod", []string{"init", "mod.com"}, nil, true); err != nil { +- t.Fatal(err) +- } +- env.AfterChange( +- NoDiagnostics(ForFile("main.go")), +- Diagnostics(env.AtRegexp("bob/bob.go", "x")), +- ) +- }) +- }) -} - +-// Tests golang/go#38267. +-func TestIssue38267(t *testing.T) { +- const testPackage = ` +--- go.mod -- +-module mod.com - --type E error // unexported, therefore must be universe.error --type F Field --var G = Field.X --`, `package p +-go 1.12 +--- lib.go -- +-package lib - --import "ext" --import "q" +-func Hello(x string) { +- _ = x +-} +--- lib_test.go -- +-package lib - --type D ext.D --`}, -- imports: map[string]string{"q": "q"}, -- want: map[string][]string{ -- "B": {"ext.D", "q.C"}, -- "D": {"ext.D"}, -- "F": {"q.Field"}, -- "G": {"q.Field"}, -- }, -- }, -- { -- label: "typeparams", -- srcs: []string{`package p +-import "testing" - --import "ext" +-type testStruct struct{ +- name string +-} - --type A[T any] struct { -- t T -- b B +-func TestHello(t *testing.T) { +- testStructs := []*testStruct{ +- &testStruct{"hello"}, +- &testStruct{"goodbye"}, +- } +- for y := range testStructs { +- _ = y +- } -} +-` - --type B ext.B +- Run(t, testPackage, func(t *testing.T, env *Env) { +- env.OpenFile("lib_test.go") +- env.AfterChange( +- Diagnostics(AtPosition("lib_test.go", 10, 2)), +- Diagnostics(AtPosition("lib_test.go", 11, 2)), +- ) +- env.OpenFile("lib.go") +- env.RegexpReplace("lib.go", "_ = x", "var y int") +- env.AfterChange( +- Diagnostics(env.AtRegexp("lib.go", "y int")), +- NoDiagnostics(ForFile("lib_test.go")), +- ) +- }) +-} - --func F1[T any](T, B) --func F2[T C]()(T, B) +-// Tests golang/go#38328. +-func TestPackageChange_Issue38328(t *testing.T) { +- const packageChange = ` +--- go.mod -- +-module fake - --type T ext.T +-go 1.12 +--- a.go -- +-package foo +-func main() {} +-` +- Run(t, packageChange, func(t *testing.T, env *Env) { +- env.OpenFile("a.go") +- env.RegexpReplace("a.go", "foo", "foox") +- env.AfterChange( +- NoDiagnostics(ForFile("a.go")), +- ) +- }) +-} - --type C ext.C +-const testPackageWithRequire = ` +--- go.mod -- +-module mod.com - --func F3[T1 ~[]T2, T2 ~[]T3](t1 T1, t2 T2) --type T3 ext.T3 --`, `package p +-go 1.12 - --func (A[B]) M(C) {} --`}, -- want: map[string][]string{ -- "A": {"ext.B", "ext.C"}, -- "B": {"ext.B"}, -- "C": {"ext.C"}, -- "F1": {"ext.B"}, -- "F2": {"ext.B", "ext.C"}, -- "F3": {"ext.T3"}, -- "T": {"ext.T"}, -- "T3": {"ext.T3"}, -- }, -- go118: true, -- }, -- { -- label: "instances", -- srcs: []string{`package p +-require foo.test v1.2.3 +--- go.sum -- +-foo.test v1.2.3 h1:TMA+lyd1ck0TqjSFpNe4T6cf/K6TYkoHwOOcMBMjaEw= +-foo.test v1.2.3/go.mod h1:Ij3kyLIe5lzjycjh13NL8I2gX0quZuTdW0MnmlwGBL4= +--- print.go -- +-package lib - --import "ext" +-import ( +- "fmt" - --type A[T any] ext.A --type B[T1, T2 any] ext.B +- "foo.test/bar" +-) - --type C A[int] --type D B[int, A[E]] --type E ext.E --`}, -- want: map[string][]string{ -- "A": {"ext.A"}, -- "B": {"ext.B"}, -- "C": {"ext.A"}, -- "D": {"ext.A", "ext.B", "ext.E"}, -- "E": {"ext.E"}, -- }, -- go118: true, -- }, -- { -- label: "duplicate decls", -- srcs: []string{`package p +-func PrintAnswer() { +- fmt.Printf("answer: %s", bar.Answer) +-} +-` - --import "a" --import "ext" +-const testPackageWithRequireProxy = ` +--- foo.test@v1.2.3/go.mod -- +-module foo.test - --type a a.A --type A a --type b ext.B --type C a.A --func (C) Foo(x) {} // invalid parameter, but that does not matter --type C b --func (C) Bar(y) {} // invalid parameter, but that does not matter +-go 1.12 +--- foo.test@v1.2.3/bar/const.go -- +-package bar - --var x ext.X --var y ext.Y --`}, -- imports: map[string]string{"a": "a", "b": "b"}, // "b" import should not matter, since it isn't in this file -- want: map[string][]string{ -- "A": {"a.A"}, -- "C": {"a.A", "ext.B", "ext.X", "ext.Y"}, -- }, -- }, -- { -- label: "invalid decls", -- srcs: []string{`package p +-const Answer = 42 +-` - --import "ext" +-func TestResolveDiagnosticWithDownload(t *testing.T) { +- WithOptions( +- ProxyFiles(testPackageWithRequireProxy), +- ).Run(t, testPackageWithRequire, func(t *testing.T, env *Env) { +- env.OpenFile("print.go") +- // Check that gopackages correctly loaded this dependency. We should get a +- // diagnostic for the wrong formatting type. +- env.AfterChange( +- Diagnostics( +- env.AtRegexp("print.go", "fmt.Printf"), +- WithMessage("wrong type int"), +- ), +- ) +- }) +-} - --type A B +-func TestMissingDependency(t *testing.T) { +- Run(t, testPackageWithRequire, func(t *testing.T, env *Env) { +- env.OpenFile("print.go") +- env.Await( +- // Log messages are asynchronous to other events on the LSP stream, so we +- // can't use OnceMet or AfterChange here. +- LogMatching(protocol.Error, "initial workspace load failed", 1, false), +- ) +- }) +-} - --func () Foo(B){} +-// Tests golang/go#36951. +-func TestAdHocPackages_Issue36951(t *testing.T) { +- const adHoc = ` +--- b/b.go -- +-package b - --var B struct{ ext.B --`}, -- want: map[string][]string{ -- "A": {"ext.B"}, -- "B": {"ext.B"}, -- "Foo": {"ext.B"}, -- }, -- allowErrs: true, -- }, -- { -- label: "unmapped receiver", -- srcs: []string{`package p +-func Hello() { +- var x int +-} +-` +- Run(t, adHoc, func(t *testing.T, env *Env) { +- env.OpenFile("b/b.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("b/b.go", "x")), +- ) +- }) +-} - --type P struct{} +-// Tests golang/go#37984: GOPATH should be read from the go command. +-func TestNoGOPATH_Issue37984(t *testing.T) { +- const files = ` +--- main.go -- +-package main - --func (a) x(P) --`}, -- want: map[string][]string{}, -- allowErrs: true, +-func _() { +- fmt.Println("Hello World") +-} +-` +- WithOptions( +- EnvVars{ +- "GOPATH": "", +- "GO111MODULE": "off", - }, -- { -- label: "SCC special case", -- srcs: []string{`package p +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- env.AfterChange(Diagnostics(env.AtRegexp("main.go", "fmt"))) +- env.SaveBuffer("main.go") +- env.AfterChange(NoDiagnostics(ForFile("main.go"))) +- }) +-} - --import "ext" +-// Tests golang/go#38669. +-func TestEqualInEnv_Issue38669(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - --type X Y --type Y struct { Z; *X } --type Z map[ext.A]ext.B --`}, -- want: map[string][]string{ -- "X": {"ext.A", "ext.B"}, -- "Y": {"ext.A", "ext.B"}, -- "Z": {"ext.A", "ext.B"}, -- }, -- allowErrs: true, -- }, -- } +-go 1.12 +--- main.go -- +-package main - -- for _, test := range tests { -- t.Run(test.label, func(t *testing.T) { -- if test.go118 { -- testenv.NeedsGo1Point(t, 18) -- } +-var _ = x.X +--- x/x.go -- +-package x - -- var pgfs []*source.ParsedGoFile -- for i, src := range test.srcs { -- uri := span.URI(fmt.Sprintf("file:///%d.go", i)) -- pgf, _ := cache.ParseGoSrc(ctx, token.NewFileSet(), uri, []byte(src), source.ParseFull, false) -- if !test.allowErrs && pgf.ParseErr != nil { -- t.Fatalf("ParseGoSrc(...) returned parse errors: %v", pgf.ParseErr) -- } -- pgfs = append(pgfs, pgf) -- } +-var X = 0 +-` +- WithOptions( +- EnvVars{"GOFLAGS": "-tags=foo"}, +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- env.OrganizeImports("main.go") +- env.AfterChange(NoDiagnostics(ForFile("main.go"))) +- }) +-} - -- imports := map[source.ImportPath]*source.Metadata{ -- "ext": {ID: "ext", Name: "ext"}, // this one comes for free -- } -- for path, m := range test.imports { -- imports[source.ImportPath(path)] = &source.Metadata{ -- ID: source.PackageID(m), -- Name: source.PackageName(m), -- } -- } +-// Tests golang/go#38467. +-func TestNoSuggestedFixesForGeneratedFiles_Issue38467(t *testing.T) { +- const generated = ` +--- go.mod -- +-module mod.com - -- data := typerefs.Encode(pgfs, "p", imports) +-go 1.12 +--- main.go -- +-package main - -- got := make(map[string][]string) -- index := typerefs.NewPackageIndex() -- for _, class := range typerefs.Decode(index, "p", data) { -- // We redundantly expand out the name x refs cross product -- // here since that's what the existing tests expect. -- for _, name := range class.Decls { -- var syms []string -- for _, sym := range class.Refs { -- syms = append(syms, fmt.Sprintf("%s.%s", index.DeclaringPackage(sym), sym.Name)) -- } -- sort.Strings(syms) -- got[name] = syms -- } -- } +-// Code generated by generator.go. DO NOT EDIT. - -- if diff := cmp.Diff(test.want, got); diff != "" { -- t.Errorf("Refs(...) returned unexpected refs (-want +got):\n%s", diff) -- } -- }) +-func _() { +- for i, _ := range []string{} { +- _ = i - } -} -diff -urN a/gopls/internal/lsp/source/types_format.go b/gopls/internal/lsp/source/types_format.go ---- a/gopls/internal/lsp/source/types_format.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/types_format.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,524 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package source +-` +- Run(t, generated, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- var d protocol.PublishDiagnosticsParams +- env.AfterChange( +- Diagnostics(AtPosition("main.go", 5, 8)), +- ReadDiagnostics("main.go", &d), +- ) +- if fixes := env.GetQuickFixes("main.go", d.Diagnostics); len(fixes) != 0 { +- t.Errorf("got quick fixes %v, wanted none", fixes) +- } +- }) +-} - --import ( -- "bytes" -- "context" -- "fmt" -- "go/ast" -- "go/doc" -- "go/printer" -- "go/token" -- "go/types" -- "strings" +-// Expect a module/GOPATH error if there is an error in the file at startup. +-// Tests golang/go#37279. +-func TestBrokenWorkspace_OutsideModule(t *testing.T) { +- const noModule = ` +--- a.go -- +-package foo - -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/tag" -- "golang.org/x/tools/internal/tokeninternal" -- "golang.org/x/tools/internal/typeparams" --) +-import "mod.com/hello" - --// FormatType returns the detail and kind for a types.Type. --func FormatType(typ types.Type, qf types.Qualifier) (detail string, kind protocol.CompletionItemKind) { -- if types.IsInterface(typ) { -- detail = "interface{...}" -- kind = protocol.InterfaceCompletion -- } else if _, ok := typ.(*types.Struct); ok { -- detail = "struct{...}" -- kind = protocol.StructCompletion -- } else if typ != typ.Underlying() { -- detail, kind = FormatType(typ.Underlying(), qf) -- } else { -- detail = types.TypeString(typ, qf) -- kind = protocol.ClassCompletion -- } -- return detail, kind +-func f() { +- hello.Goodbye() -} -- --type signature struct { -- name, doc string -- typeParams, params, results []string -- variadic bool -- needResultParens bool +-` +- Run(t, noModule, func(t *testing.T, env *Env) { +- env.OpenFile("a.go") +- env.AfterChange( +- // AdHoc views are not critical errors, but their missing import +- // diagnostics should specifically mention GOROOT or GOPATH (and not +- // modules). +- NoOutstandingWork(IgnoreTelemetryPromptWork), +- Diagnostics( +- env.AtRegexp("a.go", `"mod.com`), +- WithMessage("GOROOT or GOPATH"), +- ), +- ) +- // Deleting the import dismisses the warning. +- env.RegexpReplace("a.go", `import "mod.com/hello"`, "") +- env.AfterChange( +- NoOutstandingWork(IgnoreTelemetryPromptWork), +- ) +- }) -} - --func (s *signature) Format() string { -- var b strings.Builder -- b.WriteByte('(') -- for i, p := range s.params { -- if i > 0 { -- b.WriteString(", ") -- } -- b.WriteString(p) -- } -- b.WriteByte(')') -- -- // Add space between parameters and results. -- if len(s.results) > 0 { -- b.WriteByte(' ') -- } -- if s.needResultParens { -- b.WriteByte('(') -- } -- for i, r := range s.results { -- if i > 0 { -- b.WriteString(", ") -- } -- b.WriteString(r) -- } -- if s.needResultParens { -- b.WriteByte(')') +-func TestNonGoFolder(t *testing.T) { +- const files = ` +--- hello.txt -- +-hi mom +-` +- for _, go111module := range []string{"on", "off", ""} { +- t.Run(fmt.Sprintf("GO111MODULE_%v", go111module), func(t *testing.T) { +- WithOptions( +- EnvVars{"GO111MODULE": go111module}, +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.OnceMet( +- InitialWorkspaceLoad, +- NoOutstandingWork(IgnoreTelemetryPromptWork), +- ) +- }) +- }) - } -- return b.String() -} - --func (s *signature) TypeParams() []string { -- return s.typeParams +-// Tests the repro case from golang/go#38602. Diagnostics are now handled properly, +-// which blocks type checking. +-func TestConflictingMainPackageErrors(t *testing.T) { +- const collision = ` +--- x/x.go -- +-package x +- +-import "x/hello" +- +-func Hello() { +- hello.HiThere() -} +--- x/main.go -- +-package main - --func (s *signature) Params() []string { -- return s.params +-func main() { +- fmt.Println("") -} +-` +- WithOptions( +- InGOPATH(), +- EnvVars{"GO111MODULE": "off"}, +- ).Run(t, collision, func(t *testing.T, env *Env) { +- env.OpenFile("x/x.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("x/x.go", `^`), WithMessage("found packages main (main.go) and x (x.go)")), +- Diagnostics(env.AtRegexp("x/main.go", `^`), WithMessage("found packages main (main.go) and x (x.go)")), +- ) - --// NewBuiltinSignature returns signature for the builtin object with a given --// name, if a builtin object with the name exists. --func NewBuiltinSignature(ctx context.Context, s Snapshot, name string) (*signature, error) { -- builtin, err := s.BuiltinFile(ctx) -- if err != nil { -- return nil, err -- } -- obj := builtin.File.Scope.Lookup(name) -- if obj == nil { -- return nil, fmt.Errorf("no builtin object for %s", name) -- } -- decl, ok := obj.Decl.(*ast.FuncDecl) -- if !ok { -- return nil, fmt.Errorf("no function declaration for builtin: %s", name) -- } -- if decl.Type == nil { -- return nil, fmt.Errorf("no type for builtin decl %s", decl.Name) -- } -- var variadic bool -- if decl.Type.Params.List != nil { -- numParams := len(decl.Type.Params.List) -- lastParam := decl.Type.Params.List[numParams-1] -- if _, ok := lastParam.Type.(*ast.Ellipsis); ok { -- variadic = true +- // We don't recover cleanly from the errors without good overlay support. +- if testenv.Go1Point() >= 16 { +- env.RegexpReplace("x/x.go", `package x`, `package main`) +- env.AfterChange( +- Diagnostics(env.AtRegexp("x/main.go", `fmt`)), +- ) - } -- } -- fset := tokeninternal.FileSetFor(builtin.Tok) -- params, _ := formatFieldList(ctx, fset, decl.Type.Params, variadic) -- results, needResultParens := formatFieldList(ctx, fset, decl.Type.Results, false) -- d := decl.Doc.Text() -- switch s.Options().HoverKind { -- case SynopsisDocumentation: -- d = doc.Synopsis(d) -- case NoDocumentation: -- d = "" -- } -- return &signature{ -- doc: d, -- name: name, -- needResultParens: needResultParens, -- params: params, -- results: results, -- variadic: variadic, -- }, nil +- }) -} - --// replacer replaces some synthetic "type classes" used in the builtin file --// with their most common constituent type. --var replacer = strings.NewReplacer( -- `ComplexType`, `complex128`, -- `FloatType`, `float64`, -- `IntegerType`, `int`, --) +-const ardanLabsProxy = ` +--- github.com/ardanlabs/conf@v1.2.3/go.mod -- +-module github.com/ardanlabs/conf - --func formatFieldList(ctx context.Context, fset *token.FileSet, list *ast.FieldList, variadic bool) ([]string, bool) { -- if list == nil { -- return nil, false -- } -- var writeResultParens bool -- var result []string -- for i := 0; i < len(list.List); i++ { -- if i >= 1 { -- writeResultParens = true -- } -- p := list.List[i] -- cfg := printer.Config{Mode: printer.UseSpaces | printer.TabIndent, Tabwidth: 4} -- b := &bytes.Buffer{} -- if err := cfg.Fprint(b, fset, p.Type); err != nil { -- event.Error(ctx, "unable to print type", nil, tag.Type.Of(p.Type)) -- continue -- } -- typ := replacer.Replace(b.String()) -- if len(p.Names) == 0 { -- result = append(result, typ) -- } -- for _, name := range p.Names { -- if name.Name != "" { -- if i == 0 { -- writeResultParens = true -- } -- result = append(result, fmt.Sprintf("%s %s", name.Name, typ)) -- } else { -- result = append(result, typ) -- } -- } -- } -- if variadic { -- result[len(result)-1] = strings.Replace(result[len(result)-1], "[]", "...", 1) -- } -- return result, writeResultParens --} +-go 1.12 +--- github.com/ardanlabs/conf@v1.2.3/conf.go -- +-package conf - --// FormatTypeParams turns TypeParamList into its Go representation, such as: --// [T, Y]. Note that it does not print constraints as this is mainly used for --// formatting type params in method receivers. --func FormatTypeParams(tparams *typeparams.TypeParamList) string { -- if tparams == nil || tparams.Len() == 0 { -- return "" -- } -- var buf bytes.Buffer -- buf.WriteByte('[') -- for i := 0; i < tparams.Len(); i++ { -- if i > 0 { -- buf.WriteString(", ") -- } -- buf.WriteString(tparams.At(i).Obj().Name()) -- } -- buf.WriteByte(']') -- return buf.String() --} +-var ErrHelpWanted error +-` - --// NewSignature returns formatted signature for a types.Signature struct. --func NewSignature(ctx context.Context, s Snapshot, pkg Package, sig *types.Signature, comment *ast.CommentGroup, qf types.Qualifier, mq MetadataQualifier) (*signature, error) { -- var tparams []string -- tpList := typeparams.ForSignature(sig) -- for i := 0; i < tpList.Len(); i++ { -- tparam := tpList.At(i) -- // TODO: is it possible to reuse the logic from FormatVarType here? -- s := tparam.Obj().Name() + " " + tparam.Constraint().String() -- tparams = append(tparams, s) -- } +-// Test for golang/go#38211. +-func Test_Issue38211(t *testing.T) { +- const ardanLabs = ` +--- go.mod -- +-module mod.com - -- params := make([]string, 0, sig.Params().Len()) -- for i := 0; i < sig.Params().Len(); i++ { -- el := sig.Params().At(i) -- typ, err := FormatVarType(ctx, s, pkg, el, qf, mq) -- if err != nil { -- return nil, err -- } -- p := typ -- if el.Name() != "" { -- p = el.Name() + " " + typ -- } -- params = append(params, p) -- } +-go 1.14 +--- main.go -- +-package main - -- var needResultParens bool -- results := make([]string, 0, sig.Results().Len()) -- for i := 0; i < sig.Results().Len(); i++ { -- if i >= 1 { -- needResultParens = true -- } -- el := sig.Results().At(i) -- typ, err := FormatVarType(ctx, s, pkg, el, qf, mq) -- if err != nil { -- return nil, err -- } -- if el.Name() == "" { -- results = append(results, typ) -- } else { -- if i == 0 { -- needResultParens = true -- } -- results = append(results, el.Name()+" "+typ) -- } -- } -- var d string -- if comment != nil { -- d = comment.Text() -- } -- switch s.Options().HoverKind { -- case SynopsisDocumentation: -- d = doc.Synopsis(d) -- case NoDocumentation: -- d = "" -- } -- return &signature{ -- doc: d, -- typeParams: tparams, -- params: params, -- results: results, -- variadic: sig.Variadic(), -- needResultParens: needResultParens, -- }, nil +-import "github.com/ardanlabs/conf" +- +-func main() { +- _ = conf.ErrHelpWanted +-} +-` +- WithOptions( +- ProxyFiles(ardanLabsProxy), +- ).Run(t, ardanLabs, func(t *testing.T, env *Env) { +- // Expect a diagnostic with a suggested fix to add +- // "github.com/ardanlabs/conf" to the go.mod file. +- env.OpenFile("go.mod") +- env.OpenFile("main.go") +- var d protocol.PublishDiagnosticsParams +- env.AfterChange( +- Diagnostics(env.AtRegexp("main.go", `"github.com/ardanlabs/conf"`)), +- ReadDiagnostics("main.go", &d), +- ) +- env.ApplyQuickFixes("main.go", d.Diagnostics) +- env.SaveBuffer("go.mod") +- env.AfterChange( +- NoDiagnostics(ForFile("main.go")), +- ) +- // Comment out the line that depends on conf and expect a +- // diagnostic and a fix to remove the import. +- env.RegexpReplace("main.go", "_ = conf.ErrHelpWanted", "//_ = conf.ErrHelpWanted") +- env.AfterChange( +- Diagnostics(env.AtRegexp("main.go", `"github.com/ardanlabs/conf"`)), +- ) +- env.SaveBuffer("main.go") +- // Expect a diagnostic and fix to remove the dependency in the go.mod. +- env.AfterChange( +- NoDiagnostics(ForFile("main.go")), +- Diagnostics(env.AtRegexp("go.mod", "require github.com/ardanlabs/conf"), WithMessage("not used in this module")), +- ReadDiagnostics("go.mod", &d), +- ) +- env.ApplyQuickFixes("go.mod", d.Diagnostics) +- env.SaveBuffer("go.mod") +- env.AfterChange( +- NoDiagnostics(ForFile("go.mod")), +- ) +- // Uncomment the lines and expect a new diagnostic for the import. +- env.RegexpReplace("main.go", "//_ = conf.ErrHelpWanted", "_ = conf.ErrHelpWanted") +- env.SaveBuffer("main.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("main.go", `"github.com/ardanlabs/conf"`)), +- ) +- }) -} - --// FormatVarType formats a *types.Var, accounting for type aliases. --// To do this, it looks in the AST of the file in which the object is declared. --// On any errors, it always falls back to types.TypeString. --// --// TODO(rfindley): this function could return the actual name used in syntax, --// for better parameter names. --func FormatVarType(ctx context.Context, snapshot Snapshot, srcpkg Package, obj *types.Var, qf types.Qualifier, mq MetadataQualifier) (string, error) { -- // TODO(rfindley): This looks wrong. The previous comment said: -- // "If the given expr refers to a type parameter, then use the -- // object's Type instead of the type parameter declaration. This helps -- // format the instantiated type as opposed to the original undeclared -- // generic type". -- // -- // But of course, if obj is a type param, we are formatting a generic type -- // and not an instantiated type. Handling for instantiated types must be done -- // at a higher level. -- // -- // Left this during refactoring in order to preserve pre-existing logic. -- if typeparams.IsTypeParam(obj.Type()) { -- return types.TypeString(obj.Type(), qf), nil -- } +-// Test for golang/go#38207. +-func TestNewModule_Issue38207(t *testing.T) { +- const emptyFile = ` +--- go.mod -- +-module mod.com +- +-go 1.12 +--- main.go -- +-` +- WithOptions( +- ProxyFiles(ardanLabsProxy), +- ).Run(t, emptyFile, func(t *testing.T, env *Env) { +- env.CreateBuffer("main.go", `package main - -- if obj.Pkg() == nil || !obj.Pos().IsValid() { -- // This is defensive, though it is extremely unlikely we'll ever have a -- // builtin var. -- return types.TypeString(obj.Type(), qf), nil -- } +-import "github.com/ardanlabs/conf" - -- // TODO(rfindley): parsing to produce candidates can be costly; consider -- // using faster methods. -- targetpgf, pos, err := parseFull(ctx, snapshot, srcpkg.FileSet(), obj.Pos()) -- if err != nil { -- return "", err // e.g. ctx cancelled -- } +-func main() { +- _ = conf.ErrHelpWanted +-} +-`) +- env.SaveBuffer("main.go") +- var d protocol.PublishDiagnosticsParams +- env.AfterChange( +- Diagnostics(env.AtRegexp("main.go", `"github.com/ardanlabs/conf"`), WithMessage("no required module")), +- ReadDiagnostics("main.go", &d), +- ) +- env.ApplyQuickFixes("main.go", d.Diagnostics) +- env.AfterChange( +- NoDiagnostics(ForFile("main.go")), +- ) +- }) +-} - -- targetMeta := findFileInDeps(snapshot, srcpkg.Metadata(), targetpgf.URI) -- if targetMeta == nil { -- // If we have an object from type-checking, it should exist in a file in -- // the forward transitive closure. -- return "", bug.Errorf("failed to find file %q in deps of %q", targetpgf.URI, srcpkg.Metadata().ID) -- } +-// Test for golang/go#36960. +-func TestNewFileBadImports_Issue36960(t *testing.T) { +- const simplePackage = ` +--- go.mod -- +-module mod.com - -- decl, spec, field := findDeclInfo([]*ast.File{targetpgf.File}, pos) +-go 1.14 +--- a/a1.go -- +-package a - -- // We can't handle type parameters correctly, so we fall back on TypeString -- // for parameterized decls. -- if decl, _ := decl.(*ast.FuncDecl); decl != nil { -- if typeparams.ForFuncType(decl.Type).NumFields() > 0 { -- return types.TypeString(obj.Type(), qf), nil // in generic function -- } -- if decl.Recv != nil && len(decl.Recv.List) > 0 { -- rtype := decl.Recv.List[0].Type -- if e, ok := rtype.(*ast.StarExpr); ok { -- rtype = e.X -- } -- if x, _, _, _ := typeparams.UnpackIndexExpr(rtype); x != nil { -- return types.TypeString(obj.Type(), qf), nil // in method of generic type -- } -- } -- } -- if spec, _ := spec.(*ast.TypeSpec); spec != nil && typeparams.ForTypeSpec(spec).NumFields() > 0 { -- return types.TypeString(obj.Type(), qf), nil // in generic type decl -- } +-import "fmt" - -- if field == nil { -- // TODO(rfindley): we should never reach here from an ordinary var, so -- // should probably return an error here. -- return types.TypeString(obj.Type(), qf), nil -- } -- expr := field.Type +-func _() { +- fmt.Println("hi") +-} +-` +- Run(t, simplePackage, func(t *testing.T, env *Env) { +- env.OpenFile("a/a1.go") +- env.CreateBuffer("a/a2.go", ``) +- env.SaveBufferWithoutActions("a/a2.go") +- env.AfterChange( +- NoDiagnostics(ForFile("a/a1.go")), +- ) +- env.EditBuffer("a/a2.go", fake.NewEdit(0, 0, 0, 0, `package a`)) +- env.AfterChange( +- NoDiagnostics(ForFile("a/a1.go")), +- ) +- }) +-} - -- rq := requalifier(snapshot, targetpgf.File, targetMeta, mq) +-// This test tries to replicate the workflow of a user creating a new x test. +-// It also tests golang/go#39315. +-func TestManuallyCreatingXTest(t *testing.T) { +- // Create a package that already has a test variant (in-package test). +- const testVariant = ` +--- go.mod -- +-module mod.com - -- // The type names in the AST may not be correctly qualified. -- // Determine the package name to use based on the package that originated -- // the query and the package in which the type is declared. -- // We then qualify the value by cloning the AST node and editing it. -- expr = qualifyTypeExpr(expr, rq) +-go 1.15 +--- hello/hello.go -- +-package hello - -- // If the request came from a different package than the one in which the -- // types are defined, we may need to modify the qualifiers. -- return FormatNodeFile(targetpgf.Tok, expr), nil +-func Hello() { +- var x int -} +--- hello/hello_test.go -- +-package hello - --// qualifyTypeExpr clones the type expression expr after re-qualifying type --// names using the given function, which accepts the current syntactic --// qualifier (possibly "" for unqualified idents), and returns a new qualifier --// (again, possibly "" if the identifier should be unqualified). --// --// The resulting expression may be inaccurate: without type-checking we don't --// properly account for "." imported identifiers or builtins. --// --// TODO(rfindley): add many more tests for this function. --func qualifyTypeExpr(expr ast.Expr, qf func(string) string) ast.Expr { -- switch expr := expr.(type) { -- case *ast.ArrayType: -- return &ast.ArrayType{ -- Lbrack: expr.Lbrack, -- Elt: qualifyTypeExpr(expr.Elt, qf), -- Len: expr.Len, -- } -- -- case *ast.BinaryExpr: -- if expr.Op != token.OR { -- return expr -- } -- return &ast.BinaryExpr{ -- X: qualifyTypeExpr(expr.X, qf), -- OpPos: expr.OpPos, -- Op: expr.Op, -- Y: qualifyTypeExpr(expr.Y, qf), -- } +-import "testing" - -- case *ast.ChanType: -- return &ast.ChanType{ -- Arrow: expr.Arrow, -- Begin: expr.Begin, -- Dir: expr.Dir, -- Value: qualifyTypeExpr(expr.Value, qf), -- } +-func TestHello(t *testing.T) { +- var x int +- Hello() +-} +-` +- Run(t, testVariant, func(t *testing.T, env *Env) { +- // Open the file, triggering the workspace load. +- // There are errors in the code to ensure all is working as expected. +- env.OpenFile("hello/hello.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("hello/hello.go", "x")), +- Diagnostics(env.AtRegexp("hello/hello_test.go", "x")), +- ) - -- case *ast.Ellipsis: -- return &ast.Ellipsis{ -- Ellipsis: expr.Ellipsis, -- Elt: qualifyTypeExpr(expr.Elt, qf), -- } +- // Create an empty file with the intention of making it an x test. +- // This resembles a typical flow in an editor like VS Code, in which +- // a user would create an empty file and add content, saving +- // intermittently. +- // TODO(rstambler): There might be more edge cases here, as file +- // content can be added incrementally. +- env.CreateBuffer("hello/hello_x_test.go", ``) - -- case *ast.FuncType: -- return &ast.FuncType{ -- Func: expr.Func, -- Params: qualifyFieldList(expr.Params, qf), -- Results: qualifyFieldList(expr.Results, qf), -- } +- // Save the empty file (no actions since formatting will fail). +- env.SaveBufferWithoutActions("hello/hello_x_test.go") - -- case *ast.Ident: -- // Unqualified type (builtin, package local, or dot-imported). +- // Add the content. The missing import is for the package under test. +- env.EditBuffer("hello/hello_x_test.go", fake.NewEdit(0, 0, 0, 0, `package hello_test - -- // Don't qualify names that look like builtins. -- // -- // Without type-checking this may be inaccurate. It could be made accurate -- // by doing syntactic object resolution for the entire package, but that -- // does not seem worthwhile and we generally want to avoid using -- // ast.Object, which may be inaccurate. -- if obj := types.Universe.Lookup(expr.Name); obj != nil { -- return expr -- } +-import ( +- "testing" +-) - -- newName := qf("") -- if newName != "" { -- return &ast.SelectorExpr{ -- X: &ast.Ident{ -- NamePos: expr.Pos(), -- Name: newName, -- }, -- Sel: expr, -- } -- } -- return expr +-func TestHello(t *testing.T) { +- hello.Hello() +-} +-`)) +- // Expect a diagnostic for the missing import. Save, which should +- // trigger import organization. The diagnostic should clear. +- env.AfterChange( +- Diagnostics(env.AtRegexp("hello/hello_x_test.go", "hello.Hello")), +- ) +- env.SaveBuffer("hello/hello_x_test.go") +- env.AfterChange( +- NoDiagnostics(ForFile("hello/hello_x_test.go")), +- ) +- }) +-} - -- case *ast.IndexExpr: -- return &ast.IndexExpr{ -- X: qualifyTypeExpr(expr.X, qf), -- Lbrack: expr.Lbrack, -- Index: qualifyTypeExpr(expr.Index, qf), -- Rbrack: expr.Rbrack, -- } +-// Reproduce golang/go#40690. +-func TestCreateOnlyXTest(t *testing.T) { +- const mod = ` +--- go.mod -- +-module mod.com - -- case *typeparams.IndexListExpr: -- indices := make([]ast.Expr, len(expr.Indices)) -- for i, idx := range expr.Indices { -- indices[i] = qualifyTypeExpr(idx, qf) -- } -- return &typeparams.IndexListExpr{ -- X: qualifyTypeExpr(expr.X, qf), -- Lbrack: expr.Lbrack, -- Indices: indices, -- Rbrack: expr.Rbrack, -- } +-go 1.12 +--- foo/foo.go -- +-package foo +--- foo/bar_test.go -- +-` +- Run(t, mod, func(t *testing.T, env *Env) { +- env.OpenFile("foo/bar_test.go") +- env.EditBuffer("foo/bar_test.go", fake.NewEdit(0, 0, 0, 0, "package foo")) +- env.Await(env.DoneWithChange()) +- env.RegexpReplace("foo/bar_test.go", "package foo", `package foo_test - -- case *ast.InterfaceType: -- return &ast.InterfaceType{ -- Interface: expr.Interface, -- Methods: qualifyFieldList(expr.Methods, qf), -- Incomplete: expr.Incomplete, -- } +-import "testing" - -- case *ast.MapType: -- return &ast.MapType{ -- Map: expr.Map, -- Key: qualifyTypeExpr(expr.Key, qf), -- Value: qualifyTypeExpr(expr.Value, qf), -- } +-func TestX(t *testing.T) { +- var x int +-} +-`) +- env.AfterChange( +- Diagnostics(env.AtRegexp("foo/bar_test.go", "x")), +- ) +- }) +-} - -- case *ast.ParenExpr: -- return &ast.ParenExpr{ -- Lparen: expr.Lparen, -- Rparen: expr.Rparen, -- X: qualifyTypeExpr(expr.X, qf), -- } +-func TestChangePackageName(t *testing.T) { +- const mod = ` +--- go.mod -- +-module mod.com - -- case *ast.SelectorExpr: -- if id, ok := expr.X.(*ast.Ident); ok { -- // qualified type -- newName := qf(id.Name) -- if newName == "" { -- return expr.Sel -- } -- return &ast.SelectorExpr{ -- X: &ast.Ident{ -- NamePos: id.NamePos, -- Name: newName, -- }, -- Sel: expr.Sel, -- } -- } -- return expr +-go 1.12 +--- foo/foo.go -- +-package foo +--- foo/bar_test.go -- +-package foo_ +-` +- Run(t, mod, func(t *testing.T, env *Env) { +- env.OpenFile("foo/bar_test.go") +- env.AfterChange() +- env.RegexpReplace("foo/bar_test.go", "package foo_", "package foo_test") +- env.AfterChange( +- NoDiagnostics(ForFile("foo/bar_test.go")), +- NoDiagnostics(ForFile("foo/foo.go")), +- ) +- }) +-} - -- case *ast.StarExpr: -- return &ast.StarExpr{ -- Star: expr.Star, -- X: qualifyTypeExpr(expr.X, qf), -- } +-func TestIgnoredFiles(t *testing.T) { +- const ws = ` +--- go.mod -- +-module mod.com - -- case *ast.StructType: -- return &ast.StructType{ -- Struct: expr.Struct, -- Fields: qualifyFieldList(expr.Fields, qf), -- Incomplete: expr.Incomplete, -- } +-go 1.12 +--- _foo/x.go -- +-package x - -- default: -- return expr -- } +-var _ = foo.Bar +-` +- Run(t, ws, func(t *testing.T, env *Env) { +- env.OpenFile("_foo/x.go") +- env.AfterChange( +- NoDiagnostics(ForFile("_foo/x.go")), +- ) +- }) -} - --func qualifyFieldList(fl *ast.FieldList, qf func(string) string) *ast.FieldList { -- if fl == nil { -- return nil -- } -- if fl.List == nil { -- return &ast.FieldList{ -- Closing: fl.Closing, -- Opening: fl.Opening, -- } -- } -- list := make([]*ast.Field, 0, len(fl.List)) -- for _, f := range fl.List { -- list = append(list, &ast.Field{ -- Comment: f.Comment, -- Doc: f.Doc, -- Names: f.Names, -- Tag: f.Tag, -- Type: qualifyTypeExpr(f.Type, qf), -- }) -- } -- return &ast.FieldList{ -- Closing: fl.Closing, -- Opening: fl.Opening, -- List: list, -- } --} -diff -urN a/gopls/internal/lsp/source/util.go b/gopls/internal/lsp/source/util.go ---- a/gopls/internal/lsp/source/util.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/util.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,541 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-// Partially reproduces golang/go#38977, moving a file between packages. +-// It also gets hit by some go command bug fixed in 1.15, but we don't +-// care about that so much here. +-func TestDeletePackage(t *testing.T) { +- const ws = ` +--- go.mod -- +-module mod.com - --package source +-go 1.15 +--- a/a.go -- +-package a - --import ( -- "context" -- "go/ast" -- "go/printer" -- "go/token" -- "go/types" -- "path/filepath" -- "regexp" -- "sort" -- "strconv" -- "strings" +-const A = 1 - -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/tokeninternal" -- "golang.org/x/tools/internal/typeparams" --) +--- b/b.go -- +-package b - --// IsGenerated gets and reads the file denoted by uri and reports --// whether it contains a "generated file" comment as described at --// https://golang.org/s/generatedcode. --// --// TODO(adonovan): opt: this function does too much. --// Move snapshot.ReadFile into the caller (most of which have already done it). --func IsGenerated(ctx context.Context, snapshot Snapshot, uri span.URI) bool { -- fh, err := snapshot.ReadFile(ctx, uri) -- if err != nil { -- return false -- } -- pgf, err := snapshot.ParseGo(ctx, fh, ParseHeader) -- if err != nil { -- return false -- } -- for _, commentGroup := range pgf.File.Comments { -- for _, comment := range commentGroup.List { -- if matched := generatedRx.MatchString(comment.Text); matched { -- // Check if comment is at the beginning of the line in source. -- if safetoken.Position(pgf.Tok, comment.Slash).Column == 1 { -- return true -- } -- } -- } -- } -- return false --} +-import "mod.com/a" - --// adjustedObjEnd returns the end position of obj, possibly modified for --// package names. --// --// TODO(rfindley): eliminate this function, by inlining it at callsites where --// it makes sense. --func adjustedObjEnd(obj types.Object) token.Pos { -- nameLen := len(obj.Name()) -- if pkgName, ok := obj.(*types.PkgName); ok { -- // An imported Go package has a package-local, unqualified name. -- // When the name matches the imported package name, there is no -- // identifier in the import spec with the local package name. -- // -- // For example: -- // import "go/ast" // name "ast" matches package name -- // import a "go/ast" // name "a" does not match package name -- // -- // When the identifier does not appear in the source, have the range -- // of the object be the import path, including quotes. -- if pkgName.Imported().Name() == pkgName.Name() { -- nameLen = len(pkgName.Imported().Path()) + len(`""`) -- } -- } -- return obj.Pos() + token.Pos(nameLen) +-const B = a.A +- +--- c/c.go -- +-package c +- +-import "mod.com/a" +- +-const C = a.A +-` +- Run(t, ws, func(t *testing.T, env *Env) { +- env.OpenFile("b/b.go") +- env.Await(env.DoneWithOpen()) +- // Delete c/c.go, the only file in package c. +- env.RemoveWorkspaceFile("c/c.go") +- +- // We should still get diagnostics for files that exist. +- env.RegexpReplace("b/b.go", `a.A`, "a.Nonexistant") +- env.AfterChange( +- Diagnostics(env.AtRegexp("b/b.go", `Nonexistant`)), +- ) +- }) -} - --// Matches cgo generated comment as well as the proposed standard: --// --// https://golang.org/s/generatedcode --var generatedRx = regexp.MustCompile(`// .*DO NOT EDIT\.?`) +-// This is a copy of the scenario_default/quickfix_empty_files.txt test from +-// govim. Reproduces golang/go#39646. +-func TestQuickFixEmptyFiles(t *testing.T) { +- const mod = ` +--- go.mod -- +-module mod.com +- +-go 1.12 +-` +- // To fully recreate the govim tests, we create files by inserting +- // a newline, adding to the file, and then deleting the newline. +- // Wait for each event to process to avoid cancellations and force +- // package loads. +- writeGoVim := func(env *Env, name, content string) { +- env.WriteWorkspaceFile(name, "") +- env.Await(env.DoneWithChangeWatchedFiles()) +- +- env.CreateBuffer(name, "\n") +- env.Await(env.DoneWithOpen()) - --// FileKindForLang returns the file kind associated with the given language ID, --// or UnknownKind if the language ID is not recognized. --func FileKindForLang(langID string) FileKind { -- switch langID { -- case "go": -- return Go -- case "go.mod": -- return Mod -- case "go.sum": -- return Sum -- case "tmpl", "gotmpl": -- return Tmpl -- case "go.work": -- return Work -- default: -- return UnknownKind -- } --} +- env.EditBuffer(name, fake.NewEdit(1, 0, 1, 0, content)) +- env.Await(env.DoneWithChange()) - --// nodeAtPos returns the index and the node whose position is contained inside --// the node list. --func nodeAtPos(nodes []ast.Node, pos token.Pos) (ast.Node, int) { -- if nodes == nil { -- return nil, -1 -- } -- for i, node := range nodes { -- if node.Pos() <= pos && pos <= node.End() { -- return node, i -- } +- env.EditBuffer(name, fake.NewEdit(0, 0, 1, 0, "")) +- env.Await(env.DoneWithChange()) - } -- return nil, -1 --} - --// FormatNode returns the "pretty-print" output for an ast node. --func FormatNode(fset *token.FileSet, n ast.Node) string { -- var buf strings.Builder -- if err := printer.Fprint(&buf, fset, n); err != nil { -- // TODO(rfindley): we should use bug.Reportf here. -- // We encounter this during completion.resolveInvalid. -- return "" -- } -- return buf.String() +- const p = `package p; func DoIt(s string) {};` +- const main = `package main +- +-import "mod.com/p" +- +-func main() { +- p.DoIt(5) -} +-` +- // A simple version of the test that reproduces most of the problems it +- // exposes. +- t.Run("short", func(t *testing.T) { +- Run(t, mod, func(t *testing.T, env *Env) { +- writeGoVim(env, "p/p.go", p) +- writeGoVim(env, "main.go", main) +- env.AfterChange( +- Diagnostics(env.AtRegexp("main.go", "5")), +- ) +- }) +- }) - --// FormatNodeFile is like FormatNode, but requires only the token.File for the --// syntax containing the given ast node. --func FormatNodeFile(file *token.File, n ast.Node) string { -- fset := tokeninternal.FileSetFor(file) -- return FormatNode(fset, n) +- // A full version that replicates the whole flow of the test. +- t.Run("full", func(t *testing.T) { +- Run(t, mod, func(t *testing.T, env *Env) { +- writeGoVim(env, "p/p.go", p) +- writeGoVim(env, "main.go", main) +- writeGoVim(env, "p/p_test.go", `package p +- +-import "testing" +- +-func TestDoIt(t *testing.T) { +- DoIt(5) -} +-`) +- writeGoVim(env, "p/x_test.go", `package p_test - --// Deref returns a pointer's element type, traversing as many levels as needed. --// Otherwise it returns typ. --// --// It can return a pointer type for cyclic types (see golang/go#45510). --func Deref(typ types.Type) types.Type { -- var seen map[types.Type]struct{} -- for { -- p, ok := typ.Underlying().(*types.Pointer) -- if !ok { -- return typ -- } -- if _, ok := seen[p.Elem()]; ok { -- return typ -- } +-import ( +- "testing" - -- typ = p.Elem() +- "mod.com/p" +-) - -- if seen == nil { -- seen = make(map[types.Type]struct{}) -- } -- seen[typ] = struct{}{} -- } +-func TestDoIt(t *testing.T) { +- p.DoIt(5) +-} +-`) +- env.AfterChange( +- Diagnostics(env.AtRegexp("main.go", "5")), +- Diagnostics(env.AtRegexp("p/p_test.go", "5")), +- Diagnostics(env.AtRegexp("p/x_test.go", "5")), +- ) +- env.RegexpReplace("p/p.go", "s string", "i int") +- env.AfterChange( +- NoDiagnostics(ForFile("main.go")), +- NoDiagnostics(ForFile("p/p_test.go")), +- NoDiagnostics(ForFile("p/x_test.go")), +- ) +- }) +- }) -} - --func SortDiagnostics(d []*Diagnostic) { -- sort.Slice(d, func(i int, j int) bool { -- return CompareDiagnostic(d[i], d[j]) < 0 +-func TestSingleFile(t *testing.T) { +- const mod = ` +--- go.mod -- +-module mod.com +- +-go 1.13 +--- a/a.go -- +-package a +- +-func _() { +- var x int +-} +-` +- WithOptions( +- // Empty workspace folders. +- WorkspaceFolders(), +- ).Run(t, mod, func(t *testing.T, env *Env) { +- env.OpenFile("a/a.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/a.go", "x")), +- ) - }) -} - --func CompareDiagnostic(a, b *Diagnostic) int { -- if r := protocol.CompareRange(a.Range, b.Range); r != 0 { -- return r -- } -- if a.Source < b.Source { -- return -1 -- } -- if a.Source > b.Source { -- return +1 -- } -- if a.Message < b.Message { -- return -1 -- } -- if a.Message > b.Message { -- return +1 -- } -- return 0 +-// Reproduces the case described in +-// https://github.com/golang/go/issues/39296#issuecomment-652058883. +-func TestPkgm(t *testing.T) { +- const basic = ` +--- go.mod -- +-module mod.com +- +-go 1.15 +--- foo/foo.go -- +-package foo +- +-import "fmt" +- +-func Foo() { +- fmt.Println("") -} +-` +- Run(t, basic, func(t *testing.T, env *Env) { +- env.WriteWorkspaceFile("foo/foo_test.go", `package main - --// findFileInDeps finds package metadata containing URI in the transitive --// dependencies of m. When using the Go command, the answer is unique. --// --// TODO(rfindley): refactor to share logic with findPackageInDeps? --func findFileInDeps(s MetadataSource, m *Metadata, uri span.URI) *Metadata { -- seen := make(map[PackageID]bool) -- var search func(*Metadata) *Metadata -- search = func(m *Metadata) *Metadata { -- if seen[m.ID] { -- return nil -- } -- seen[m.ID] = true -- for _, cgf := range m.CompiledGoFiles { -- if cgf == uri { -- return m -- } -- } -- for _, dep := range m.DepsByPkgPath { -- m := s.Metadata(dep) -- if m == nil { -- bug.Reportf("nil metadata for %q", dep) -- continue -- } -- if found := search(m); found != nil { -- return found -- } -- } -- return nil -- } -- return search(m) +-func main() { +- +-}`) +- env.OpenFile("foo/foo_test.go") +- env.RegexpReplace("foo/foo_test.go", `package main`, `package foo`) +- env.AfterChange(NoDiagnostics(ForFile("foo/foo.go"))) +- }) -} - --// UnquoteImportPath returns the unquoted import path of s, --// or "" if the path is not properly quoted. --func UnquoteImportPath(s *ast.ImportSpec) ImportPath { -- path, err := strconv.Unquote(s.Path.Value) -- if err != nil { -- return "" -- } -- return ImportPath(path) +-func TestClosingBuffer(t *testing.T) { +- const basic = ` +--- go.mod -- +-module mod.com +- +-go 1.14 +--- main.go -- +-package main +- +-func main() {} +-` +- Run(t, basic, func(t *testing.T, env *Env) { +- env.Editor.CreateBuffer(env.Ctx, "foo.go", `package main`) +- env.AfterChange() +- env.CloseBuffer("foo.go") +- env.AfterChange(NoLogMatching(protocol.Info, "packages=0")) +- }) -} - --// NodeContains returns true if a node encloses a given position pos. --func NodeContains(n ast.Node, pos token.Pos) bool { -- return n != nil && n.Pos() <= pos && pos <= n.End() +-// Reproduces golang/go#38424. +-func TestCutAndPaste(t *testing.T) { +- const basic = ` +--- go.mod -- +-module mod.com +- +-go 1.14 +--- main2.go -- +-package main +-` +- Run(t, basic, func(t *testing.T, env *Env) { +- env.CreateBuffer("main.go", "") +- env.Await(env.DoneWithOpen()) +- +- env.SaveBufferWithoutActions("main.go") +- env.Await(env.DoneWithSave(), env.DoneWithChangeWatchedFiles()) +- +- env.EditBuffer("main.go", fake.NewEdit(0, 0, 0, 0, `package main +- +-func main() { -} +-`)) +- env.Await(env.DoneWithChange()) - --// CollectScopes returns all scopes in an ast path, ordered as innermost scope --// first. --func CollectScopes(info *types.Info, path []ast.Node, pos token.Pos) []*types.Scope { -- // scopes[i], where i<len(path), is the possibly nil Scope of path[i]. -- var scopes []*types.Scope -- for _, n := range path { -- // Include *FuncType scope if pos is inside the function body. -- switch node := n.(type) { -- case *ast.FuncDecl: -- if node.Body != nil && NodeContains(node.Body, pos) { -- n = node.Type -- } -- case *ast.FuncLit: -- if node.Body != nil && NodeContains(node.Body, pos) { -- n = node.Type -- } -- } -- scopes = append(scopes, info.Scopes[n]) -- } -- return scopes +- env.SaveBuffer("main.go") +- env.Await(env.DoneWithSave(), env.DoneWithChangeWatchedFiles()) +- +- env.EditBuffer("main.go", fake.NewEdit(0, 0, 4, 0, "")) +- env.Await(env.DoneWithChange()) +- +- env.EditBuffer("main.go", fake.NewEdit(0, 0, 0, 0, `package main +- +-func main() { +- var x int +-} +-`)) +- env.AfterChange( +- Diagnostics(env.AtRegexp("main.go", "x")), +- ) +- }) -} - --// Qualifier returns a function that appropriately formats a types.PkgName --// appearing in a *ast.File. --func Qualifier(f *ast.File, pkg *types.Package, info *types.Info) types.Qualifier { -- // Construct mapping of import paths to their defined or implicit names. -- imports := make(map[*types.Package]string) -- for _, imp := range f.Imports { -- if pkgname, ok := ImportedPkgName(info, imp); ok { -- imports[pkgname.Imported()] = pkgname.Name() -- } -- } -- // Define qualifier to replace full package paths with names of the imports. -- return func(p *types.Package) string { -- if p == pkg { -- return "" -- } -- if name, ok := imports[p]; ok { -- if name == "." { -- return "" -- } -- return name -- } -- return p.Name() -- } +-// Reproduces golang/go#39763. +-func TestInvalidPackageName(t *testing.T) { +- const pkgDefault = ` +--- go.mod -- +-module mod.com +- +-go 1.12 +--- main.go -- +-package default +- +-func main() {} +-` +- Run(t, pkgDefault, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- env.AfterChange( +- Diagnostics( +- env.AtRegexp("main.go", "default"), +- WithMessage("expected 'IDENT'"), +- ), +- ) +- }) -} - --// requalifier returns a function that re-qualifies identifiers and qualified --// identifiers contained in targetFile using the given metadata qualifier. --func requalifier(s MetadataSource, targetFile *ast.File, targetMeta *Metadata, mq MetadataQualifier) func(string) string { -- qm := map[string]string{ -- "": mq(targetMeta.Name, "", targetMeta.PkgPath), -- } +-// This test verifies that the workspace scope is effectively limited to the +-// workspace folder, if expandWorkspaceToModule is set. +-func TestExpandWorkspaceToModule(t *testing.T) { +- const mod = ` +--- go.mod -- +-module mod.com - -- // Construct mapping of import paths to their defined or implicit names. -- for _, imp := range targetFile.Imports { -- name, pkgName, impPath, pkgPath := importInfo(s, imp, targetMeta) +-go 1.12 +--- a/main.go -- +-package main - -- // Re-map the target name for the source file. -- qm[name] = mq(pkgName, impPath, pkgPath) -- } +-func main() {} +--- main.go -- +-package main - -- return func(name string) string { -- if newName, ok := qm[name]; ok { -- return newName -- } -- return name -- } +-func main() { +- var x int +-} +-` +- WithOptions( +- WorkspaceFolders("a"), +- ).Run(t, mod, func(t *testing.T, env *Env) { +- env.OpenFile("a/main.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("main.go", "x")), +- ) +- }) +- WithOptions( +- WorkspaceFolders("a"), +- Settings{"expandWorkspaceToModule": false}, +- ).Run(t, mod, func(t *testing.T, env *Env) { +- env.OpenFile("a/main.go") +- env.AfterChange( +- NoDiagnostics(ForFile("main.go")), +- ) +- }) -} - --// A MetadataQualifier is a function that qualifies an identifier declared in a --// package with the given package name, import path, and package path. +-// This test verifies that the workspace scope is effectively limited to the +-// set of active modules. -// --// In scenarios where metadata is missing the provided PackageName and --// PackagePath may be empty, but ImportPath must always be non-empty. --type MetadataQualifier func(PackageName, ImportPath, PackagePath) string +-// We should not get diagnostics or file watching patterns for paths outside of +-// the active workspace. +-func TestWorkspaceModules(t *testing.T) { +- const mod = ` +--- go.work -- +-go 1.18 - --// MetadataQualifierForFile returns a metadata qualifier that chooses the best --// qualification of an imported package relative to the file f in package with --// metadata m. --func MetadataQualifierForFile(s MetadataSource, f *ast.File, m *Metadata) MetadataQualifier { -- // Record local names for import paths. -- localNames := make(map[ImportPath]string) // local names for imports in f -- for _, imp := range f.Imports { -- name, _, impPath, _ := importInfo(s, imp, m) -- localNames[impPath] = name -- } +-use a +--- a/go.mod -- +-module mod.com/a - -- // Record a package path -> import path mapping. -- inverseDeps := make(map[PackageID]PackagePath) -- for path, id := range m.DepsByPkgPath { -- inverseDeps[id] = path -- } -- importsByPkgPath := make(map[PackagePath]ImportPath) // best import paths by pkgPath -- for impPath, id := range m.DepsByImpPath { -- if id == "" { -- continue -- } -- pkgPath := inverseDeps[id] -- _, hasPath := importsByPkgPath[pkgPath] -- _, hasImp := localNames[impPath] -- // In rare cases, there may be multiple import paths with the same package -- // path. In such scenarios, prefer an import path that already exists in -- // the file. -- if !hasPath || hasImp { -- importsByPkgPath[pkgPath] = impPath -- } -- } +-go 1.12 +--- a/a.go -- +-package a - -- return func(pkgName PackageName, impPath ImportPath, pkgPath PackagePath) string { -- // If supplied, translate the package path to an import path in the source -- // package. -- if pkgPath != "" { -- if srcImp := importsByPkgPath[pkgPath]; srcImp != "" { -- impPath = srcImp -- } -- if pkgPath == m.PkgPath { -- return "" -- } -- } -- if localName, ok := localNames[impPath]; ok && impPath != "" { -- return string(localName) -- } -- if pkgName != "" { -- return string(pkgName) -- } -- idx := strings.LastIndexByte(string(impPath), '/') -- return string(impPath[idx+1:]) -- } +-func _() { +- var x int -} +--- b/go.mod -- +-module mod.com/b - --// importInfo collects information about the import specified by imp, --// extracting its file-local name, package name, import path, and package path. --// --// If metadata is missing for the import, the resulting package name and --// package path may be empty, and the file local name may be guessed based on --// the import path. --// --// Note: previous versions of this helper used a PackageID->PackagePath map --// extracted from m, for extracting package path even in the case where --// metadata for a dep was missing. This should not be necessary, as we should --// always have metadata for IDs contained in DepsByPkgPath. --func importInfo(s MetadataSource, imp *ast.ImportSpec, m *Metadata) (string, PackageName, ImportPath, PackagePath) { -- var ( -- name string // local name -- pkgName PackageName -- impPath = UnquoteImportPath(imp) -- pkgPath PackagePath -- ) +-go 1.18 +-` +- WithOptions( +- Settings{ +- "subdirWatchPatterns": "on", +- }, +- ).Run(t, mod, func(t *testing.T, env *Env) { +- env.OpenFile("a/a.go") +- // Writing this file may cause the snapshot to 'know' about the file b, but +- // that shouldn't cause it to watch the 'b' directory. +- env.WriteWorkspaceFile("b/b.go", `package b - -- // If the import has a local name, use it. -- if imp.Name != nil { -- name = imp.Name.Name -- } +-func _() { +- var x int +-} +-`) +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/a.go", "x")), +- NoDiagnostics(ForFile("b/b.go")), +- FileWatchMatching("a$"), +- NoFileWatchMatching("b$"), +- ) +- }) +-} - -- // Try to find metadata for the import. If successful and there is no local -- // name, the package name is the local name. -- if depID := m.DepsByImpPath[impPath]; depID != "" { -- if depm := s.Metadata(depID); depm != nil { -- if name == "" { -- name = string(depm.Name) -- } -- pkgName = depm.Name -- pkgPath = depm.PkgPath -- } -- } +-func TestSimplifyCompositeLitDiagnostic(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - -- // If the local name is still unknown, guess it based on the import path. -- if name == "" { -- idx := strings.LastIndexByte(string(impPath), '/') -- name = string(impPath[idx+1:]) -- } -- return name, pkgName, impPath, pkgPath --} +-go 1.12 +--- main.go -- +-package main - --// isDirective reports whether c is a comment directive. --// --// Copied and adapted from go/src/go/ast/ast.go. --func isDirective(c string) bool { -- if len(c) < 3 { -- return false -- } -- if c[1] != '/' { -- return false -- } -- //-style comment (no newline at the end) -- c = c[2:] -- if len(c) == 0 { -- // empty line -- return false -- } -- // "//line " is a line directive. -- // (The // has been removed.) -- if strings.HasPrefix(c, "line ") { -- return true -- } +-import "fmt" - -- // "//[a-z0-9]+:[a-z0-9]" -- // (The // has been removed.) -- colon := strings.Index(c, ":") -- if colon <= 0 || colon+1 >= len(c) { -- return false -- } -- for i := 0; i <= colon+1; i++ { -- if i == colon { -- continue -- } -- b := c[i] -- if !('a' <= b && b <= 'z' || '0' <= b && b <= '9') { -- return false +-type t struct { +- msg string +-} +- +-func main() { +- x := []t{t{"msg"}} +- fmt.Println(x) +-} +-` +- +- WithOptions( +- Settings{"staticcheck": true}, +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- var d protocol.PublishDiagnosticsParams +- env.AfterChange( +- Diagnostics(env.AtRegexp("main.go", `t{"msg"}`), WithMessage("redundant type")), +- ReadDiagnostics("main.go", &d), +- ) +- if tags := d.Diagnostics[0].Tags; len(tags) == 0 || tags[0] != protocol.Unnecessary { +- t.Errorf("wanted Unnecessary tag on diagnostic, got %v", tags) - } -- } -- return true +- env.ApplyQuickFixes("main.go", d.Diagnostics) +- env.AfterChange(NoDiagnostics(ForFile("main.go"))) +- }) -} - --// InDir checks whether path is in the file tree rooted at dir. --// It checks only the lexical form of the file names. --// It does not consider symbolic links. --// --// Copied from go/src/cmd/go/internal/search/search.go. --func InDir(dir, path string) bool { -- pv := strings.ToUpper(filepath.VolumeName(path)) -- dv := strings.ToUpper(filepath.VolumeName(dir)) -- path = path[len(pv):] -- dir = dir[len(dv):] -- switch { -- default: -- return false -- case pv != dv: -- return false -- case len(path) == len(dir): -- if path == dir { -- return true +-// Test some secondary diagnostics +-func TestSecondaryDiagnostics(t *testing.T) { +- const dir = ` +--- go.mod -- +-module mod.com +- +-go 1.12 +--- main.go -- +-package main +-func main() { +- panic("not here") +-} +--- other.go -- +-package main +-func main() {} +-` +- Run(t, dir, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- env.OpenFile("other.go") +- var mainDiags, otherDiags protocol.PublishDiagnosticsParams +- env.AfterChange( +- ReadDiagnostics("main.go", &mainDiags), +- ReadDiagnostics("other.go", &otherDiags), +- ) +- if len(mainDiags.Diagnostics) != 1 { +- t.Fatalf("main.go, got %d diagnostics, expected 1", len(mainDiags.Diagnostics)) - } -- return false -- case dir == "": -- return path != "" -- case len(path) > len(dir): -- if dir[len(dir)-1] == filepath.Separator { -- if path[:len(dir)] == dir { -- return path[len(dir):] != "" -- } -- return false +- keep := mainDiags.Diagnostics[0] +- if len(otherDiags.Diagnostics) != 1 { +- t.Fatalf("other.go: got %d diagnostics, expected 1", len(otherDiags.Diagnostics)) - } -- if path[len(dir)] == filepath.Separator && path[:len(dir)] == dir { -- if len(path) == len(dir)+1 { -- return true -- } -- return path[len(dir)+1:] != "" +- if len(otherDiags.Diagnostics[0].RelatedInformation) != 1 { +- t.Fatalf("got %d RelatedInformations, expected 1", len(otherDiags.Diagnostics[0].RelatedInformation)) - } -- return false -- } +- // check that the RelatedInformation matches the error from main.go +- c := otherDiags.Diagnostics[0].RelatedInformation[0] +- if c.Location.Range != keep.Range { +- t.Errorf("locations don't match. Got %v expected %v", c.Location.Range, keep.Range) +- } +- }) -} - --// IsValidImport returns whether importPkgPath is importable --// by pkgPath --func IsValidImport(pkgPath, importPkgPath PackagePath) bool { -- i := strings.LastIndex(string(importPkgPath), "/internal/") -- if i == -1 { -- return true -- } -- // TODO(rfindley): this looks wrong: IsCommandLineArguments is meant to -- // operate on package IDs, not package paths. -- if IsCommandLineArguments(PackageID(pkgPath)) { -- return true -- } -- // TODO(rfindley): this is wrong. mod.testx/p should not be able to -- // import mod.test/internal: https://go.dev/play/p/-Ca6P-E4V4q -- return strings.HasPrefix(string(pkgPath), string(importPkgPath[:i])) +-func TestOrphanedFiles(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com +- +-go 1.12 +--- a/a.go -- +-package a +- +-func main() { +- var x int -} +--- a/a_exclude.go -- +-// +build exclude - --// IsCommandLineArguments reports whether a given value denotes --// "command-line-arguments" package, which is a package with an unknown ID --// created by the go command. It can have a test variant, which is why callers --// should not check that a value equals "command-line-arguments" directly. --func IsCommandLineArguments(id PackageID) bool { -- return strings.Contains(string(id), "command-line-arguments") +-package a +- +-func _() { +- var x int -} +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("a/a.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/a.go", "x")), +- ) +- env.OpenFile("a/a_exclude.go") - --// embeddedIdent returns the type name identifier for an embedding x, if x in a --// valid embedding. Otherwise, it returns nil. --// --// Spec: An embedded field must be specified as a type name T or as a pointer --// to a non-interface type name *T --func embeddedIdent(x ast.Expr) *ast.Ident { -- if star, ok := x.(*ast.StarExpr); ok { -- x = star.X -- } -- switch ix := x.(type) { // check for instantiated receivers -- case *ast.IndexExpr: -- x = ix.X -- case *typeparams.IndexListExpr: -- x = ix.X -- } -- switch x := x.(type) { -- case *ast.Ident: -- return x -- case *ast.SelectorExpr: -- if _, ok := x.X.(*ast.Ident); ok { -- return x.Sel -- } -- } -- return nil +- loadOnce := LogMatching(protocol.Info, "query=.*file=.*a_exclude.go", 1, false) +- +- // can't use OnceMet or AfterChange as logs are async +- env.Await(loadOnce) +- // ...but ensure that the change has been fully processed before editing. +- // Otherwise, there may be a race where the snapshot is cloned before all +- // state changes resulting from the load have been processed +- // (golang/go#61521). +- env.AfterChange() +- +- // Check that orphaned files are not reloaded, by making a change in +- // a.go file and confirming that the workspace diagnosis did not reload +- // a_exclude.go. +- // +- // This is racy (but fails open) because logs are asynchronous to other LSP +- // operations. There's a chance gopls _did_ log, and we just haven't seen +- // it yet. +- env.RegexpReplace("a/a.go", "package a", "package a // arbitrary comment") +- env.AfterChange(loadOnce) +- }) -} - --// An importFunc is an implementation of the single-method --// types.Importer interface based on a function value. --type ImporterFunc func(path string) (*types.Package, error) +-func TestEnableAllExperiments(t *testing.T) { +- // Before the oldest supported Go version, gopls sends a warning to upgrade +- // Go, which fails the expectation below. +- testenv.NeedsGo1Point(t, goversion.OldestSupported()) - --func (f ImporterFunc) Import(path string) (*types.Package, error) { return f(path) } -diff -urN a/gopls/internal/lsp/source/view.go b/gopls/internal/lsp/source/view.go ---- a/gopls/internal/lsp/source/view.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/view.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,1060 +0,0 @@ --// Copyright 2018 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +- const mod = ` +--- go.mod -- +-module mod.com - --package source +-go 1.12 +--- main.go -- +-package main - --import ( -- "bytes" -- "context" -- "crypto/sha256" -- "encoding/json" -- "errors" -- "fmt" -- "go/ast" -- "go/parser" -- "go/scanner" -- "go/token" -- "go/types" -- "io" +-import "bytes" - -- "golang.org/x/mod/modfile" -- "golang.org/x/tools/go/analysis" -- "golang.org/x/tools/go/packages" -- "golang.org/x/tools/go/types/objectpath" -- "golang.org/x/tools/gopls/internal/lsp/progress" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/safetoken" -- "golang.org/x/tools/gopls/internal/lsp/source/methodsets" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/gopls/internal/vulncheck" -- "golang.org/x/tools/internal/event/label" -- "golang.org/x/tools/internal/event/tag" -- "golang.org/x/tools/internal/gocommand" -- "golang.org/x/tools/internal/imports" -- "golang.org/x/tools/internal/packagesinternal" --) +-func b(c bytes.Buffer) { +- _ = 1 +-} +-` +- WithOptions( +- Settings{"allExperiments": true}, +- ).Run(t, mod, func(t *testing.T, env *Env) { +- // Confirm that the setting doesn't cause any warnings. +- env.OnceMet( +- InitialWorkspaceLoad, +- NoShownMessage(""), // empty substring to match any message +- ) +- }) +-} - --// A GlobalSnapshotID uniquely identifies a snapshot within this process and --// increases monotonically with snapshot creation time. --// --// We use a distinct integral type for global IDs to help enforce correct --// usage. --type GlobalSnapshotID uint64 +-func TestSwig(t *testing.T) { +- if _, err := exec.LookPath("swig"); err != nil { +- t.Skip("skipping test: swig not available") +- } +- if _, err := exec.LookPath("g++"); err != nil { +- t.Skip("skipping test: g++ not available") +- } - --// Snapshot represents the current state for the given view. --type Snapshot interface { -- // SequenceID is the sequence id of this snapshot within its containing -- // view. -- // -- // Relative to their view sequence ids are monotonically increasing, but this -- // does not hold globally: when new views are created their initial snapshot -- // has sequence ID 0. For operations that span multiple views, use global -- // IDs. -- SequenceID() uint64 +- const mod = ` +--- go.mod -- +-module mod.com - -- // GlobalID is a globally unique identifier for this snapshot. Global IDs are -- // monotonic: subsequent snapshots will have higher global ID, though -- // subsequent snapshots in a view may not have adjacent global IDs. -- GlobalID() GlobalSnapshotID +-go 1.12 +--- pkg/simple/export_swig.go -- +-package simple - -- // FileKind returns the type of a file. -- // -- // We can't reliably deduce the kind from the file name alone, -- // as some editors can be told to interpret a buffer as -- // language different from the file name heuristic, e.g. that -- // an .html file actually contains Go "html/template" syntax, -- // or even that a .go file contains Python. -- FileKind(FileHandle) FileKind +-func ExportSimple(x, y int) int { +- return Gcd(x, y) +-} +--- pkg/simple/simple.swigcxx -- +-%module simple - -- // Options returns the options associated with this snapshot. -- Options() *Options +-%inline %{ +-extern int gcd(int x, int y) +-{ +- int g; +- g = y; +- while (x > 0) { +- g = x; +- x = y % x; +- y = g; +- } +- return g; +-} +-%} +--- main.go -- +-package a - -- // View returns the View associated with this snapshot. -- View() View +-func main() { +- var x int +-} +-` +- Run(t, mod, func(t *testing.T, env *Env) { +- env.OnceMet( +- InitialWorkspaceLoad, +- NoDiagnostics(WithMessage("illegal character U+0023 '#'")), +- ) +- }) +-} - -- // BackgroundContext returns a context used for all background processing -- // on behalf of this snapshot. -- BackgroundContext() context.Context +-// When foo_test.go is opened, gopls will object to the borked package name. +-// This test asserts that when the package name is fixed, gopls will soon after +-// have no more complaints about it. +-// https://github.com/golang/go/issues/41061 +-func TestRenamePackage(t *testing.T) { +- const proxy = ` +--- example.com@v1.2.3/go.mod -- +-module example.com - -- // A Snapshot is a caching implementation of FileSource whose -- // ReadFile method returns consistent information about the existence -- // and content of each file throughout its lifetime. -- FileSource +-go 1.12 +--- example.com@v1.2.3/blah/blah.go -- +-package blah - -- // FindFile returns the FileHandle for the given URI, if it is already -- // in the given snapshot. -- // TODO(adonovan): delete this operation; use ReadFile instead. -- FindFile(uri span.URI) FileHandle +-const Name = "Blah" +--- random.org@v1.2.3/go.mod -- +-module random.org - -- // AwaitInitialized waits until the snapshot's view is initialized. -- AwaitInitialized(ctx context.Context) +-go 1.12 +--- random.org@v1.2.3/blah/blah.go -- +-package hello - -- // IsOpen returns whether the editor currently has a file open. -- IsOpen(uri span.URI) bool +-const Name = "Hello" +-` - -- // IgnoredFile reports if a file would be ignored by a `go list` of the whole -- // workspace. -- IgnoredFile(uri span.URI) bool +- const contents = ` +--- go.mod -- +-module mod.com - -- // Templates returns the .tmpl files -- Templates() map[span.URI]FileHandle +-go 1.12 +--- main.go -- +-package main - -- // ParseGo returns the parsed AST for the file. -- // If the file is not available, returns nil and an error. -- // Position information is added to FileSet(). -- ParseGo(ctx context.Context, fh FileHandle, mode parser.Mode) (*ParsedGoFile, error) +-import "example.com/blah" - -- // Analyze runs the specified analyzers on the given packages at this snapshot. -- // -- // If the provided tracker is non-nil, it may be used to report progress of -- // the analysis pass. -- Analyze(ctx context.Context, pkgIDs map[PackageID]unit, analyzers []*Analyzer, tracker *progress.Tracker) ([]*Diagnostic, error) +-func main() { +- blah.Hello() +-} +--- bob.go -- +-package main +--- foo/foo.go -- +-package foo +--- foo/foo_test.go -- +-package foo_ +-` - -- // RunGoCommandPiped runs the given `go` command, writing its output -- // to stdout and stderr. Verb, Args, and WorkingDir must be specified. -- // -- // RunGoCommandPiped runs the command serially using gocommand.RunPiped, -- // enforcing that this command executes exclusively to other commands on the -- // server. -- RunGoCommandPiped(ctx context.Context, mode InvocationFlags, inv *gocommand.Invocation, stdout, stderr io.Writer) error +- WithOptions( +- ProxyFiles(proxy), +- InGOPATH(), +- EnvVars{"GO111MODULE": "off"}, +- ).Run(t, contents, func(t *testing.T, env *Env) { +- // Simulate typing character by character. +- env.OpenFile("foo/foo_test.go") +- env.Await(env.DoneWithOpen()) +- env.RegexpReplace("foo/foo_test.go", "_", "_t") +- env.Await(env.DoneWithChange()) +- env.RegexpReplace("foo/foo_test.go", "_t", "_test") +- env.AfterChange( +- NoDiagnostics(ForFile("foo/foo_test.go")), +- NoOutstandingWork(IgnoreTelemetryPromptWork), +- ) +- }) +-} +- +-// TestProgressBarErrors confirms that critical workspace load errors are shown +-// and updated via progress reports. +-func TestProgressBarErrors(t *testing.T) { +- const pkg = ` +--- go.mod -- +-modul mod.com +- +-go 1.12 +--- main.go -- +-package main +-` +- Run(t, pkg, func(t *testing.T, env *Env) { +- env.OpenFile("go.mod") +- env.AfterChange( +- OutstandingWork(server.WorkspaceLoadFailure, "unknown directive"), +- ) +- env.EditBuffer("go.mod", fake.NewEdit(0, 0, 3, 0, `module mod.com +- +-go 1.hello +-`)) +- // As of golang/go#42529, go.mod changes do not reload the workspace until +- // they are saved. +- env.SaveBufferWithoutActions("go.mod") +- env.AfterChange( +- OutstandingWork(server.WorkspaceLoadFailure, "invalid go version"), +- ) +- env.RegexpReplace("go.mod", "go 1.hello", "go 1.12") +- env.SaveBufferWithoutActions("go.mod") +- env.AfterChange( +- NoOutstandingWork(IgnoreTelemetryPromptWork), +- ) +- }) +-} +- +-func TestDeleteDirectory(t *testing.T) { +- const mod = ` +--- bob/bob.go -- +-package bob - -- // RunGoCommandDirect runs the given `go` command. Verb, Args, and -- // WorkingDir must be specified. -- RunGoCommandDirect(ctx context.Context, mode InvocationFlags, inv *gocommand.Invocation) (*bytes.Buffer, error) +-func Hello() { +- var x int +-} +--- go.mod -- +-module mod.com +--- cmd/main.go -- +-package main - -- // RunGoCommands runs a series of `go` commands that updates the go.mod -- // and go.sum file for wd, and returns their updated contents. -- RunGoCommands(ctx context.Context, allowNetwork bool, wd string, run func(invoke func(...string) (*bytes.Buffer, error)) error) (bool, []byte, []byte, error) +-import "mod.com/bob" - -- // RunProcessEnvFunc runs fn with the process env for this snapshot's view. -- // Note: the process env contains cached module and filesystem state. -- RunProcessEnvFunc(ctx context.Context, fn func(context.Context, *imports.Options) error) error +-func main() { +- bob.Hello() +-} +-` +- WithOptions( +- Settings{ +- // Now that we don't watch subdirs by default (except for VS Code), +- // we must explicitly ask gopls to requests subdir watch patterns. +- "subdirWatchPatterns": "on", +- }, +- ).Run(t, mod, func(t *testing.T, env *Env) { +- env.OnceMet( +- InitialWorkspaceLoad, +- FileWatchMatching("bob"), +- ) +- env.RemoveWorkspaceFile("bob") +- env.AfterChange( +- Diagnostics(env.AtRegexp("cmd/main.go", `"mod.com/bob"`)), +- NoDiagnostics(ForFile("bob/bob.go")), +- NoFileWatchMatching("bob"), +- ) +- }) +-} - -- // ModFiles are the go.mod files enclosed in the snapshot's view and known -- // to the snapshot. -- ModFiles() []span.URI +-// Confirms that circular imports are tested and reported. +-func TestCircularImports(t *testing.T) { +- const mod = ` +--- go.mod -- +-module mod.com - -- // ParseMod is used to parse go.mod files. -- ParseMod(ctx context.Context, fh FileHandle) (*ParsedModule, error) +-go 1.12 +--- self/self.go -- +-package self - -- // ModWhy returns the results of `go mod why` for the module specified by -- // the given go.mod file. -- ModWhy(ctx context.Context, fh FileHandle) (map[string]string, error) +-import _ "mod.com/self" +-func Hello() {} +--- double/a/a.go -- +-package a - -- // ModTidy returns the results of `go mod tidy` for the module specified by -- // the given go.mod file. -- ModTidy(ctx context.Context, pm *ParsedModule) (*TidiedModule, error) +-import _ "mod.com/double/b" +--- double/b/b.go -- +-package b - -- // ModVuln returns import vulnerability analysis for the given go.mod URI. -- // Concurrent requests are combined into a single command. -- ModVuln(ctx context.Context, modURI span.URI) (*vulncheck.Result, error) +-import _ "mod.com/double/a" +--- triple/a/a.go -- +-package a - -- // GoModForFile returns the URI of the go.mod file for the given URI. -- GoModForFile(uri span.URI) span.URI +-import _ "mod.com/triple/b" +--- triple/b/b.go -- +-package b - -- // WorkFile, if non-empty, is the go.work file for the workspace. -- WorkFile() span.URI +-import _ "mod.com/triple/c" +--- triple/c/c.go -- +-package c - -- // ParseWork is used to parse go.work files. -- ParseWork(ctx context.Context, fh FileHandle) (*ParsedWorkFile, error) +-import _ "mod.com/triple/a" +-` +- Run(t, mod, func(t *testing.T, env *Env) { +- env.OnceMet( +- InitialWorkspaceLoad, +- Diagnostics(env.AtRegexp("self/self.go", `_ "mod.com/self"`), WithMessage("import cycle not allowed")), +- Diagnostics(env.AtRegexp("double/a/a.go", `_ "mod.com/double/b"`), WithMessage("import cycle not allowed")), +- Diagnostics(env.AtRegexp("triple/a/a.go", `_ "mod.com/triple/b"`), WithMessage("import cycle not allowed")), +- ) +- }) +-} - -- // BuiltinFile returns information about the special builtin package. -- BuiltinFile(ctx context.Context) (*ParsedGoFile, error) +-// Tests golang/go#46667: deleting a problematic import path should resolve +-// import cycle errors. +-func TestResolveImportCycle(t *testing.T) { +- const mod = ` +--- go.mod -- +-module mod.test - -- // IsBuiltin reports whether uri is part of the builtin package. -- IsBuiltin(uri span.URI) bool +-go 1.16 +--- a/a.go -- +-package a - -- // CriticalError returns any critical errors in the workspace. -- // -- // A nil result may mean success, or context cancellation. -- CriticalError(ctx context.Context) *CriticalError +-import "mod.test/b" - -- // Symbols returns all symbols in the snapshot. -- // -- // If workspaceOnly is set, this only includes symbols from files in a -- // workspace package. Otherwise, it returns symbols from all loaded packages. -- Symbols(ctx context.Context, workspaceOnly bool) (map[span.URI][]Symbol, error) +-const A = b.A +-const B = 2 +--- b/b.go -- +-package b - -- // -- package metadata -- +-import "mod.test/a" - -- // ReverseDependencies returns a new mapping whose entries are -- // the ID and Metadata of each package in the workspace that -- // directly or transitively depend on the package denoted by id, -- // excluding id itself. -- ReverseDependencies(ctx context.Context, id PackageID, transitive bool) (map[PackageID]*Metadata, error) +-const A = 1 +-const B = a.B +- ` +- Run(t, mod, func(t *testing.T, env *Env) { +- env.OpenFile("a/a.go") +- env.OpenFile("b/b.go") +- env.AfterChange( +- // The Go command sometimes tells us about only one of the import cycle +- // errors below. Also, sometimes we get an error during type checking +- // instead of during list, due to missing metadata. This is likely due to +- // a race. +- // For robustness of this test, succeed if we get any reasonable error. +- // +- // TODO(golang/go#52904): we should get *both* of these errors. +- // TODO(golang/go#64899): we should always get an import cycle error +- // rather than a missing metadata error. +- AnyOf( +- Diagnostics(env.AtRegexp("a/a.go", `"mod.test/b"`)), +- Diagnostics(env.AtRegexp("b/b.go", `"mod.test/a"`)), +- ), +- ) +- env.RegexpReplace("b/b.go", `const B = a\.B`, "") +- env.SaveBuffer("b/b.go") +- env.AfterChange( +- NoDiagnostics(ForFile("a/a.go")), +- NoDiagnostics(ForFile("b/b.go")), +- ) +- }) +-} - -- // WorkspaceMetadata returns a new, unordered slice containing -- // metadata for all ordinary and test packages (but not -- // intermediate test variants) in the workspace. -- // -- // The workspace is the set of modules typically defined by a -- // go.work file. It is not transitively closed: for example, -- // the standard library is not usually part of the workspace -- // even though every module in the workspace depends on it. -- // -- // Operations that must inspect all the dependencies of the -- // workspace packages should instead use AllMetadata. -- WorkspaceMetadata(ctx context.Context) ([]*Metadata, error) +-func TestBadImport(t *testing.T) { +- const mod = ` +--- go.mod -- +-module mod.com - -- // AllMetadata returns a new unordered array of metadata for -- // all packages known to this snapshot, which includes the -- // packages of all workspace modules plus their transitive -- // import dependencies. -- // -- // It may also contain ad-hoc packages for standalone files. -- // It includes all test variants. -- AllMetadata(ctx context.Context) ([]*Metadata, error) +-go 1.12 +--- main.go -- +-package main - -- // Metadata returns the metadata for the specified package, -- // or nil if it was not found. -- Metadata(id PackageID) *Metadata +-import ( +- _ "nosuchpkg" +-) +-` +- t.Run("module", func(t *testing.T) { +- Run(t, mod, func(t *testing.T, env *Env) { +- env.OnceMet( +- InitialWorkspaceLoad, +- Diagnostics(env.AtRegexp("main.go", `"nosuchpkg"`), WithMessage(`could not import nosuchpkg (no required module provides package "nosuchpkg"`)), +- ) +- }) +- }) +- t.Run("GOPATH", func(t *testing.T) { +- WithOptions( +- InGOPATH(), +- EnvVars{"GO111MODULE": "off"}, +- Modes(Default), +- ).Run(t, mod, func(t *testing.T, env *Env) { +- env.OnceMet( +- InitialWorkspaceLoad, +- Diagnostics(env.AtRegexp("main.go", `"nosuchpkg"`), WithMessage(`cannot find package "nosuchpkg"`)), +- ) +- }) +- }) +-} - -- // MetadataForFile returns a new slice containing metadata for each -- // package containing the Go file identified by uri, ordered by the -- // number of CompiledGoFiles (i.e. "narrowest" to "widest" package), -- // and secondarily by IsIntermediateTestVariant (false < true). -- // The result may include tests and intermediate test variants of -- // importable packages. -- // It returns an error if the context was cancelled. -- MetadataForFile(ctx context.Context, uri span.URI) ([]*Metadata, error) +-func TestNestedModules(t *testing.T) { +- const proxy = ` +--- nested.com@v1.0.0/go.mod -- +-module nested.com - -- // OrphanedFileDiagnostics reports diagnostics for files that have no package -- // associations or which only have only command-line-arguments packages. -- // -- // The caller must not mutate the result. -- OrphanedFileDiagnostics(ctx context.Context) (map[span.URI]*Diagnostic, error) +-go 1.12 +--- nested.com@v1.0.0/hello/hello.go -- +-package hello - -- // -- package type-checking -- +-func Hello() {} +-` - -- // TypeCheck parses and type-checks the specified packages, -- // and returns them in the same order as the ids. -- // The resulting packages' types may belong to different importers, -- // so types from different packages are incommensurable. -- // -- // In general, clients should never need to type-checked -- // syntax for an intermediate test variant (ITV) package. -- // Callers should apply RemoveIntermediateTestVariants (or -- // equivalent) before this method, or any of the potentially -- // type-checking methods below. -- TypeCheck(ctx context.Context, ids ...PackageID) ([]Package, error) +- const nested = ` +--- go.mod -- +-module mod.com - -- // PackageDiagnostics returns diagnostics for files contained in specified -- // packages. -- // -- // If these diagnostics cannot be loaded from cache, the requested packages -- // may be type-checked. -- PackageDiagnostics(ctx context.Context, ids ...PackageID) (map[span.URI][]*Diagnostic, error) +-go 1.12 - -- // References returns cross-references indexes for the specified packages. -- // -- // If these indexes cannot be loaded from cache, the requested packages may -- // be type-checked. -- References(ctx context.Context, ids ...PackageID) ([]XrefIndex, error) +-require nested.com v1.0.0 +--- go.sum -- +-nested.com v1.0.0 h1:I6spLE4CgFqMdBPc+wTV2asDO2QJ3tU0YAT+jkLeN1I= +-nested.com v1.0.0/go.mod h1:ly53UzXQgVjSlV7wicdBB4p8BxfytuGT1Xcyv0ReJfI= +--- main.go -- +-package main - -- // MethodSets returns method-set indexes for the specified packages. -- // -- // If these indexes cannot be loaded from cache, the requested packages may -- // be type-checked. -- MethodSets(ctx context.Context, ids ...PackageID) ([]*methodsets.Index, error) --} +-import "nested.com/hello" - --// NarrowestMetadataForFile returns metadata for the narrowest package --// (the one with the fewest files) that encloses the specified file. --// The result may be a test variant, but never an intermediate test variant. --func NarrowestMetadataForFile(ctx context.Context, snapshot Snapshot, uri span.URI) (*Metadata, error) { -- metas, err := snapshot.MetadataForFile(ctx, uri) -- if err != nil { -- return nil, err -- } -- RemoveIntermediateTestVariants(&metas) -- if len(metas) == 0 { -- return nil, fmt.Errorf("no package metadata for file %s", uri) -- } -- return metas[0], nil +-func main() { +- hello.Hello() -} +--- nested/go.mod -- +-module nested.com - --type XrefIndex interface { -- Lookup(targets map[PackagePath]map[objectpath.Path]struct{}) (locs []protocol.Location) --} +--- nested/hello/hello.go -- +-package hello - --// SnapshotLabels returns a new slice of labels that should be used for events --// related to a snapshot. --func SnapshotLabels(snapshot Snapshot) []label.Label { -- return []label.Label{tag.Snapshot.Of(snapshot.SequenceID()), tag.Directory.Of(snapshot.View().Folder())} +-func Hello() { +- helloHelper() -} +--- nested/hello/hello_helper.go -- +-package hello - --// NarrowestPackageForFile is a convenience function that selects the narrowest --// non-ITV package to which this file belongs, type-checks it in the requested --// mode (full or workspace), and returns it, along with the parse tree of that --// file. --// --// The "narrowest" package is the one with the fewest number of files that --// includes the given file. This solves the problem of test variants, as the --// test will have more files than the non-test package. --// --// An intermediate test variant (ITV) package has identical source to a regular --// package but resolves imports differently. gopls should never need to --// type-check them. --// --// Type-checking is expensive. Call snapshot.ParseGo if all you need is a parse --// tree, or snapshot.MetadataForFile if you only need metadata. --func NarrowestPackageForFile(ctx context.Context, snapshot Snapshot, uri span.URI) (Package, *ParsedGoFile, error) { -- return selectPackageForFile(ctx, snapshot, uri, func(metas []*Metadata) *Metadata { return metas[0] }) +-func helloHelper() {} +-` +- WithOptions( +- ProxyFiles(proxy), +- Modes(Default), +- ).Run(t, nested, func(t *testing.T, env *Env) { +- // Expect a diagnostic in a nested module. +- env.OpenFile("nested/hello/hello.go") +- env.AfterChange( +- NoDiagnostics(ForFile("nested/hello/hello.go")), +- ) +- loc := env.GoToDefinition(env.RegexpSearch("nested/hello/hello.go", "helloHelper")) +- want := "nested/hello/hello_helper.go" +- if got := env.Sandbox.Workdir.URIToPath(loc.URI); got != want { +- t.Errorf("Definition() returned %q, want %q", got, want) +- } +- }) -} - --// WidestPackageForFile is a convenience function that selects the widest --// non-ITV package to which this file belongs, type-checks it in the requested --// mode (full or workspace), and returns it, along with the parse tree of that --// file. --// --// The "widest" package is the one with the most number of files that includes --// the given file. Which is the test variant if one exists. --// --// An intermediate test variant (ITV) package has identical source to a regular --// package but resolves imports differently. gopls should never need to --// type-check them. --// --// Type-checking is expensive. Call snapshot.ParseGo if all you need is a parse --// tree, or snapshot.MetadataForFile if you only need metadata. --func WidestPackageForFile(ctx context.Context, snapshot Snapshot, uri span.URI) (Package, *ParsedGoFile, error) { -- return selectPackageForFile(ctx, snapshot, uri, func(metas []*Metadata) *Metadata { return metas[len(metas)-1] }) --} +-func TestAdHocPackagesReloading(t *testing.T) { +- const nomod = ` +--- main.go -- +-package main - --func selectPackageForFile(ctx context.Context, snapshot Snapshot, uri span.URI, selector func([]*Metadata) *Metadata) (Package, *ParsedGoFile, error) { -- metas, err := snapshot.MetadataForFile(ctx, uri) -- if err != nil { -- return nil, nil, err -- } -- RemoveIntermediateTestVariants(&metas) -- if len(metas) == 0 { -- return nil, nil, fmt.Errorf("no package metadata for file %s", uri) -- } -- md := selector(metas) -- pkgs, err := snapshot.TypeCheck(ctx, md.ID) -- if err != nil { -- return nil, nil, err -- } -- pkg := pkgs[0] -- pgf, err := pkg.File(uri) -- if err != nil { -- return nil, nil, err // "can't happen" -- } -- return pkg, pgf, err +-func main() {} +-` +- Run(t, nomod, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- env.RegexpReplace("main.go", "{}", "{ var x int; }") // simulate typing +- env.AfterChange(NoLogMatching(protocol.Info, "packages=1")) +- }) -} - --// InvocationFlags represents the settings of a particular go command invocation. --// It is a mode, plus a set of flag bits. --type InvocationFlags int +-func TestBuildTagChange(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - --const ( -- // Normal is appropriate for commands that might be run by a user and don't -- // deliberately modify go.mod files, e.g. `go test`. -- Normal InvocationFlags = iota -- // WriteTemporaryModFile is for commands that need information from a -- // modified version of the user's go.mod file, e.g. `go mod tidy` used to -- // generate diagnostics. -- WriteTemporaryModFile -- // LoadWorkspace is for packages.Load, and other operations that should -- // consider the whole workspace at once. -- LoadWorkspace +-go 1.12 +--- foo.go -- +-// decoy comment +-// +build hidden +-// decoy comment - -- // AllowNetwork is a flag bit that indicates the invocation should be -- // allowed to access the network. -- AllowNetwork InvocationFlags = 1 << 10 --) +-package foo +-var Foo = 1 +--- bar.go -- +-package foo +-var Bar = Foo +-` - --func (m InvocationFlags) Mode() InvocationFlags { -- return m & (AllowNetwork - 1) --} +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("foo.go") +- env.AfterChange(Diagnostics(env.AtRegexp("bar.go", `Foo`))) +- env.RegexpReplace("foo.go", `\+build`, "") +- env.AfterChange(NoDiagnostics(ForFile("bar.go"))) +- }) - --func (m InvocationFlags) AllowNetwork() bool { -- return m&AllowNetwork != 0 -} - --// View represents a single build context for a workspace. --// --// A unique build is determined by the workspace folder along with a Go --// environment (GOOS, GOARCH, GOWORK, etc). --// --// Additionally, the View holds a pointer to the current state of that build --// (the Snapshot). --// --// TODO(rfindley): move all other state such as module upgrades into the --// Snapshot. --type View interface { -- // ID returns a globally unique identifier for this view. -- ID() string -- -- // Name returns the name this view was constructed with. -- Name() string -- -- // Folder returns the folder with which this view was created. -- Folder() span.URI -- -- // Snapshot returns the current snapshot for the view, and a -- // release function that must be called when the Snapshot is -- // no longer needed. -- // -- // If the view is shut down, the resulting error will be non-nil, and the -- // release function need not be called. -- Snapshot() (Snapshot, func(), error) -- -- // IsGoPrivatePath reports whether target is a private import path, as identified -- // by the GOPRIVATE environment variable. -- IsGoPrivatePath(path string) bool -- -- // ModuleUpgrades returns known module upgrades for the dependencies of -- // modfile. -- ModuleUpgrades(modfile span.URI) map[string]string -- -- // RegisterModuleUpgrades registers that upgrades exist for the given modules -- // required by modfile. -- RegisterModuleUpgrades(modfile span.URI, upgrades map[string]string) -- -- // ClearModuleUpgrades clears all upgrades for the modules in modfile. -- ClearModuleUpgrades(modfile span.URI) -- -- // Vulnerabilities returns known vulnerabilities for the given modfile. -- // TODO(suzmue): replace command.Vuln with a different type, maybe -- // https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck/govulnchecklib#Summary? -- Vulnerabilities(modfile ...span.URI) map[span.URI]*vulncheck.Result +-func TestIssue44736(t *testing.T) { +- const files = ` +- -- go.mod -- +-module blah.com - -- // SetVulnerabilities resets the list of vulnerabilities that exists for the given modules -- // required by modfile. -- SetVulnerabilities(modfile span.URI, vulncheckResult *vulncheck.Result) +-go 1.16 +--- main.go -- +-package main - -- // GoVersion returns the configured Go version for this view. -- GoVersion() int +-import "fmt" - -- // GoVersionString returns the go version string configured for this view. -- // Unlike [GoVersion], this encodes the minor version and commit hash information. -- GoVersionString() string +-func main() { +- asdf +- fmt.Printf("This is a test %v") +- fdas -} +--- other.go -- +-package main - --// A FileSource maps URIs to FileHandles. --type FileSource interface { -- // ReadFile returns the FileHandle for a given URI, either by -- // reading the content of the file or by obtaining it from a cache. -- // -- // Invariant: ReadFile must only return an error in the case of context -- // cancellation. If ctx.Err() is nil, the resulting error must also be nil. -- ReadFile(ctx context.Context, uri span.URI) (FileHandle, error) +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- env.OpenFile("other.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("main.go", "asdf")), +- Diagnostics(env.AtRegexp("main.go", "fdas")), +- ) +- env.SetBufferContent("other.go", "package main\n\nasdf") +- // The new diagnostic in other.go should not suppress diagnostics in main.go. +- env.AfterChange( +- Diagnostics(env.AtRegexp("other.go", "asdf"), WithMessage("expected declaration")), +- Diagnostics(env.AtRegexp("main.go", "asdf")), +- ) +- }) -} - --// A MetadataSource maps package IDs to metadata. --// --// TODO(rfindley): replace this with a concrete metadata graph, once it is --// exposed from the snapshot. --type MetadataSource interface { -- // Metadata returns Metadata for the given package ID, or nil if it does not -- // exist. -- Metadata(PackageID) *Metadata +-func TestInitialization(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com +- +-go 1.16 +--- main.go -- +-package main +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("go.mod") +- env.Await(env.DoneWithOpen()) +- env.RegexpReplace("go.mod", "module", "modul") +- env.SaveBufferWithoutActions("go.mod") +- env.AfterChange( +- NoLogMatching(protocol.Error, "initial workspace load failed"), +- ) +- }) -} - --// A ParsedGoFile contains the results of parsing a Go file. --type ParsedGoFile struct { -- URI span.URI -- Mode parser.Mode -- File *ast.File -- Tok *token.File -- // Source code used to build the AST. It may be different from the -- // actual content of the file if we have fixed the AST. -- Src []byte +-// This test confirms that the view does not reinitialize when a go.mod file is +-// opened. +-func TestNoReinitialize(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - -- // FixedSrc and Fixed AST report on "fixing" that occurred during parsing of -- // this file. -- // -- // If FixedSrc == true, the source contained in the Src field was modified -- // from the original source to improve parsing. -- // -- // If FixedAST == true, the ast was modified after parsing, and therefore -- // positions encoded in the AST may not accurately represent the content of -- // the Src field. -- // -- // TODO(rfindley): there are many places where we haphazardly use the Src or -- // positions without checking these fields. Audit these places and guard -- // accordingly. After doing so, we may find that we don't need to -- // differentiate FixedSrc and FixedAST. -- FixedSrc bool -- FixedAST bool -- Mapper *protocol.Mapper // may map fixed Src, not file content -- ParseErr scanner.ErrorList --} +-go 1.12 +--- main.go -- +-package main - --// Fixed reports whether p was "Fixed", meaning that its source or positions --// may not correlate with the original file. --func (p ParsedGoFile) Fixed() bool { -- return p.FixedSrc || p.FixedAST +-func main() {} +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("go.mod") +- env.Await( +- // Check that we have only loaded "<dir>/..." once. +- // Log messages are asynchronous to other events on the LSP stream, so we +- // can't use OnceMet or AfterChange here. +- LogMatching(protocol.Info, `.*query=.*\.\.\..*`, 1, false), +- ) +- }) -} - --// -- go/token domain convenience helpers -- +-func TestLangVersion(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - --// PositionPos returns the token.Pos of protocol position p within the file. --func (pgf *ParsedGoFile) PositionPos(p protocol.Position) (token.Pos, error) { -- offset, err := pgf.Mapper.PositionOffset(p) -- if err != nil { -- return token.NoPos, err -- } -- return safetoken.Pos(pgf.Tok, offset) --} +-go 1.12 +--- main.go -- +-package main - --// PosRange returns a protocol Range for the token.Pos interval in this file. --func (pgf *ParsedGoFile) PosRange(start, end token.Pos) (protocol.Range, error) { -- return pgf.Mapper.PosRange(pgf.Tok, start, end) +-const C = 0b10 +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OnceMet( +- InitialWorkspaceLoad, +- Diagnostics(env.AtRegexp("main.go", `0b10`), WithMessage("go1.13 or later")), +- ) +- env.WriteWorkspaceFile("go.mod", "module mod.com \n\ngo 1.13\n") +- env.AfterChange( +- NoDiagnostics(ForFile("main.go")), +- ) +- }) -} - --// PosMappedRange returns a MappedRange for the token.Pos interval in this file. --// A MappedRange can be converted to any other form. --func (pgf *ParsedGoFile) PosMappedRange(start, end token.Pos) (protocol.MappedRange, error) { -- return pgf.Mapper.PosMappedRange(pgf.Tok, start, end) --} +-func TestNoQuickFixForUndeclaredConstraint(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - --// PosLocation returns a protocol Location for the token.Pos interval in this file. --func (pgf *ParsedGoFile) PosLocation(start, end token.Pos) (protocol.Location, error) { -- return pgf.Mapper.PosLocation(pgf.Tok, start, end) --} +-go 1.18 +--- main.go -- +-package main - --// NodeRange returns a protocol Range for the ast.Node interval in this file. --func (pgf *ParsedGoFile) NodeRange(node ast.Node) (protocol.Range, error) { -- return pgf.Mapper.NodeRange(pgf.Tok, node) +-func F[T C](_ T) { -} +-` - --// NodeMappedRange returns a MappedRange for the ast.Node interval in this file. --// A MappedRange can be converted to any other form. --func (pgf *ParsedGoFile) NodeMappedRange(node ast.Node) (protocol.MappedRange, error) { -- return pgf.Mapper.NodeMappedRange(pgf.Tok, node) +- Run(t, files, func(t *testing.T, env *Env) { +- var d protocol.PublishDiagnosticsParams +- env.OnceMet( +- InitialWorkspaceLoad, +- Diagnostics(env.AtRegexp("main.go", `C`)), +- ReadDiagnostics("main.go", &d), +- ) +- if fixes := env.GetQuickFixes("main.go", d.Diagnostics); len(fixes) != 0 { +- t.Errorf("got quick fixes %v, wanted none", fixes) +- } +- }) -} - --// NodeLocation returns a protocol Location for the ast.Node interval in this file. --func (pgf *ParsedGoFile) NodeLocation(node ast.Node) (protocol.Location, error) { -- return pgf.Mapper.PosLocation(pgf.Tok, node.Pos(), node.End()) --} +-func TestEditGoDirective(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - --// RangePos parses a protocol Range back into the go/token domain. --func (pgf *ParsedGoFile) RangePos(r protocol.Range) (token.Pos, token.Pos, error) { -- start, end, err := pgf.Mapper.RangeOffsets(r) -- if err != nil { -- return token.NoPos, token.NoPos, err -- } -- return pgf.Tok.Pos(start), pgf.Tok.Pos(end), nil --} +-go 1.16 +--- main.go -- +-package main - --// A ParsedModule contains the results of parsing a go.mod file. --type ParsedModule struct { -- URI span.URI -- File *modfile.File -- Mapper *protocol.Mapper -- ParseErrors []*Diagnostic +-func F[T any](_ T) { -} +-` +- Run(t, files, func(_ *testing.T, env *Env) { // Create a new workspace-level directory and empty file. +- var d protocol.PublishDiagnosticsParams +- env.OnceMet( +- InitialWorkspaceLoad, +- Diagnostics(env.AtRegexp("main.go", `T any`), WithMessage("type parameter")), +- ReadDiagnostics("main.go", &d), +- ) - --// A ParsedWorkFile contains the results of parsing a go.work file. --type ParsedWorkFile struct { -- URI span.URI -- File *modfile.WorkFile -- Mapper *protocol.Mapper -- ParseErrors []*Diagnostic +- env.ApplyQuickFixes("main.go", d.Diagnostics) +- env.AfterChange( +- NoDiagnostics(ForFile("main.go")), +- ) +- }) -} - --// A TidiedModule contains the results of running `go mod tidy` on a module. --type TidiedModule struct { -- // Diagnostics representing changes made by `go mod tidy`. -- Diagnostics []*Diagnostic -- // The bytes of the go.mod file after it was tidied. -- TidiedContent []byte --} +-func TestEditGoDirectiveWorkspace(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - --// Metadata represents package metadata retrieved from go/packages. --// The Deps* maps do not contain self-import edges. --// --// An ad-hoc package (without go.mod or GOPATH) has its ID, PkgPath, --// and LoadDir equal to the absolute path of its directory. --type Metadata struct { -- ID PackageID -- PkgPath PackagePath -- Name PackageName +-go 1.16 +--- go.work -- +-go 1.18 - -- // these three fields are as defined by go/packages.Package -- GoFiles []span.URI -- CompiledGoFiles []span.URI -- IgnoredFiles []span.URI +-use . +--- main.go -- +-package main - -- ForTest PackagePath // q in a "p [q.test]" package, else "" -- TypesSizes types.Sizes -- Errors []packages.Error // must be set for packages in import cycles -- DepsByImpPath map[ImportPath]PackageID // may contain dups; empty ID => missing -- DepsByPkgPath map[PackagePath]PackageID // values are unique and non-empty -- Module *packages.Module -- DepsErrors []*packagesinternal.PackageError -- Diagnostics []*Diagnostic // processed diagnostics from 'go list' -- LoadDir string // directory from which go/packages was run -- Standalone bool // package synthesized for a standalone file (e.g. ignore-tagged) +-func F[T any](_ T) { -} +-` +- Run(t, files, func(_ *testing.T, env *Env) { // Create a new workspace-level directory and empty file. +- var d protocol.PublishDiagnosticsParams - --func (m *Metadata) String() string { return string(m.ID) } -- --// IsIntermediateTestVariant reports whether the given package is an --// intermediate test variant (ITV), e.g. "net/http [net/url.test]". --// --// An ITV has identical syntax to the regular variant, but different --// import metadata (DepsBy{Imp,Pkg}Path). --// --// Such test variants arise when an x_test package (in this case net/url_test) --// imports a package (in this case net/http) that itself imports the --// non-x_test package (in this case net/url). --// --// This is done so that the forward transitive closure of net/url_test has --// only one package for the "net/url" import. --// The ITV exists to hold the test variant import: --// --// net/url_test [net/url.test] --// --// | "net/http" -> net/http [net/url.test] --// | "net/url" -> net/url [net/url.test] --// | ... --// --// net/http [net/url.test] --// --// | "net/url" -> net/url [net/url.test] --// | ... --// --// This restriction propagates throughout the import graph of net/http: for --// every package imported by net/http that imports net/url, there must be an --// intermediate test variant that instead imports "net/url [net/url.test]". --// --// As one can see from the example of net/url and net/http, intermediate test --// variants can result in many additional packages that are essentially (but --// not quite) identical. For this reason, we filter these variants wherever --// possible. --// --// # Why we mostly ignore intermediate test variants --// --// In projects with complicated tests, there may be a very large --// number of ITVs--asymptotically more than the number of ordinary --// variants. Since they have identical syntax, it is fine in most --// cases to ignore them since the results of analyzing the ordinary --// variant suffice. However, this is not entirely sound. --// --// Consider this package: --// --// // p/p.go -- in all variants of p --// package p --// type T struct { io.Closer } --// --// // p/p_test.go -- in test variant of p --// package p --// func (T) Close() error { ... } --// --// The ordinary variant "p" defines T with a Close method promoted --// from io.Closer. But its test variant "p [p.test]" defines a type T --// with a Close method from p_test.go. --// --// Now consider a package q that imports p, perhaps indirectly. Within --// it, T.Close will resolve to the first Close method: --// --// // q/q.go -- in all variants of q --// package q --// import "p" --// var _ = new(p.T).Close --// --// Let's assume p also contains this file defining an external test (xtest): --// --// // p/p_x_test.go -- external test of p --// package p_test --// import ( "q"; "testing" ) --// func Test(t *testing.T) { ... } --// --// Note that q imports p, but p's xtest imports q. Now, in "q --// [p.test]", the intermediate test variant of q built for p's --// external test, T.Close resolves not to the io.Closer.Close --// interface method, but to the concrete method of T.Close --// declared in p_test.go. --// --// If we now request all references to the T.Close declaration in --// p_test.go, the result should include the reference from q's ITV. --// (It's not just methods that can be affected; fields can too, though --// it requires bizarre code to achieve.) --// --// As a matter of policy, gopls mostly ignores this subtlety, --// because to account for it would require that we type-check every --// intermediate test variant of p, of which there could be many. --// Good code doesn't rely on such trickery. --// --// Most callers of MetadataForFile call RemoveIntermediateTestVariants --// to discard them before requesting type checking, or the products of --// type-checking such as the cross-reference index or method set index. --// --// MetadataForFile doesn't do this filtering itself becaused in some --// cases we need to make a reverse dependency query on the metadata --// graph, and it's important to include the rdeps of ITVs in that --// query. But the filtering of ITVs should be applied after that step, --// before type checking. --// --// In general, we should never type check an ITV. --func (m *Metadata) IsIntermediateTestVariant() bool { -- return m.ForTest != "" && m.ForTest != m.PkgPath && m.ForTest+"_test" != m.PkgPath --} +- // We should have a diagnostic because generics are not supported at 1.16. +- env.OnceMet( +- InitialWorkspaceLoad, +- Diagnostics(env.AtRegexp("main.go", `T any`), WithMessage("type parameter")), +- ReadDiagnostics("main.go", &d), +- ) - --// RemoveIntermediateTestVariants removes intermediate test variants, modifying the array. --// We use a pointer to a slice make it impossible to forget to use the result. --func RemoveIntermediateTestVariants(pmetas *[]*Metadata) { -- metas := *pmetas -- res := metas[:0] -- for _, m := range metas { -- if !m.IsIntermediateTestVariant() { -- res = append(res, m) -- } -- } -- *pmetas = res +- // This diagnostic should have a quick fix to edit the go version. +- env.ApplyQuickFixes("main.go", d.Diagnostics) +- +- // Once the edit is applied, the problematic diagnostics should be +- // resolved. +- env.AfterChange( +- NoDiagnostics(ForFile("main.go")), +- ) +- }) -} - --var ErrViewExists = errors.New("view already exists for session") +-// This test demonstrates that analysis facts are correctly propagated +-// across packages. +-func TestInterpackageAnalysis(t *testing.T) { +- const src = ` +--- go.mod -- +-module example.com +--- a/a.go -- +-package a - --// FileModification represents a modification to a file. --type FileModification struct { -- URI span.URI -- Action FileAction +-import "example.com/b" - -- // OnDisk is true if a watched file is changed on disk. -- // If true, Version will be -1 and Text will be nil. -- OnDisk bool +-func _() { +- new(b.B).Printf("%d", "s") // printf error +-} - -- // Version will be -1 and Text will be nil when they are not supplied, -- // specifically on textDocument/didClose and for on-disk changes. -- Version int32 -- Text []byte +--- b/b.go -- +-package b - -- // LanguageID is only sent from the language client on textDocument/didOpen. -- LanguageID string +-import "example.com/c" +- +-type B struct{} +- +-func (B) Printf(format string, args ...interface{}) { +- c.MyPrintf(format, args...) -} - --type FileAction int +--- c/c.go -- +-package c - --const ( -- UnknownFileAction = FileAction(iota) -- Open -- Change -- Close -- Save -- Create -- Delete --) +-import "fmt" - --func (a FileAction) String() string { -- switch a { -- case Open: -- return "Open" -- case Change: -- return "Change" -- case Close: -- return "Close" -- case Save: -- return "Save" -- case Create: -- return "Create" -- case Delete: -- return "Delete" -- default: -- return "Unknown" -- } +-func MyPrintf(format string, args ...interface{}) { +- fmt.Printf(format, args...) +-} +-` +- Run(t, src, func(t *testing.T, env *Env) { +- env.OpenFile("a/a.go") +- env.AfterChange( +- Diagnostics( +- env.AtRegexp("a/a.go", "new.*Printf"), +- WithMessage("format %d has arg \"s\" of wrong type string"), +- ), +- ) +- }) -} - --var ErrTmpModfileUnsupported = errors.New("-modfile is unsupported for this Go version") --var ErrNoModOnDisk = errors.New("go.mod file is not on disk") +-// This test ensures that only Analyzers with RunDespiteErrors=true +-// are invoked on a package that would not compile, even if the errors +-// are distant and localized. +-func TestErrorsThatPreventAnalysis(t *testing.T) { +- const src = ` +--- go.mod -- +-module example.com +--- a/a.go -- +-package a - --func IsNonFatalGoModError(err error) bool { -- return err == ErrTmpModfileUnsupported || err == ErrNoModOnDisk --} +-import "fmt" +-import "sync" +-import _ "example.com/b" - --// Common parse modes; these should be reused wherever possible to increase --// cache hits. --const ( -- // ParseHeader specifies that the main package declaration and imports are needed. -- // This is the mode used when attempting to examine the package graph structure. -- ParseHeader = parser.AllErrors | parser.ParseComments | parser.ImportsOnly | SkipObjectResolution +-func _() { +- // The copylocks analyzer (RunDespiteErrors, FactTypes={}) does run. +- var mu sync.Mutex +- mu2 := mu // copylocks error, reported +- _ = &mu2 - -- // ParseFull specifies the full AST is needed. -- // This is used for files of direct interest where the entire contents must -- // be considered. -- ParseFull = parser.AllErrors | parser.ParseComments | SkipObjectResolution --) +- // The printf analyzer (!RunDespiteErrors, FactTypes!={}) does not run: +- // (c, printf) failed because of type error in c +- // (b, printf) and (a, printf) do not run because of failed prerequisites. +- fmt.Printf("%d", "s") // printf error, unreported - --// A FileHandle represents the URI, content, hash, and optional --// version of a file tracked by the LSP session. --// --// File content may be provided by the file system (for Saved files) --// or from an overlay, for open files with unsaved edits. --// A FileHandle may record an attempt to read a non-existent file, --// in which case Content returns an error. --type FileHandle interface { -- // URI is the URI for this file handle. -- // TODO(rfindley): this is not actually well-defined. In some cases, there -- // may be more than one URI that resolve to the same FileHandle. Which one is -- // this? -- URI() span.URI -- // FileIdentity returns a FileIdentity for the file, even if there was an -- // error reading it. -- FileIdentity() FileIdentity -- // SameContentsOnDisk reports whether the file has the same content on disk: -- // it is false for files open on an editor with unsaved edits. -- SameContentsOnDisk() bool -- // Version returns the file version, as defined by the LSP client. -- // For on-disk file handles, Version returns 0. -- Version() int32 -- // Content returns the contents of a file. -- // If the file is not available, returns a nil slice and an error. -- Content() ([]byte, error) +- // The bools analyzer (!RunDespiteErrors, FactTypes={}) does not run: +- var cond bool +- _ = cond != true && cond != true // bools error, unreported -} - --// A Hash is a cryptographic digest of the contents of a file. --// (Although at 32B it is larger than a 16B string header, it is smaller --// and has better locality than the string header + 64B of hex digits.) --type Hash [sha256.Size]byte +--- b/b.go -- +-package b - --// HashOf returns the hash of some data. --func HashOf(data []byte) Hash { -- return Hash(sha256.Sum256(data)) --} +-import _ "example.com/c" - --// Hashf returns the hash of a printf-formatted string. --func Hashf(format string, args ...interface{}) Hash { -- // Although this looks alloc-heavy, it is faster than using -- // Fprintf on sha256.New() because the allocations don't escape. -- return HashOf([]byte(fmt.Sprintf(format, args...))) --} +--- c/c.go -- +-package c - --// String returns the digest as a string of hex digits. --func (h Hash) String() string { -- return fmt.Sprintf("%64x", [sha256.Size]byte(h)) --} +-var _ = 1 / "" // type error - --// Less returns true if the given hash is less than the other. --func (h Hash) Less(other Hash) bool { -- return bytes.Compare(h[:], other[:]) < 0 --} +-` +- Run(t, src, func(t *testing.T, env *Env) { +- var diags protocol.PublishDiagnosticsParams +- env.OpenFile("a/a.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/a.go", "mu2 := (mu)"), WithMessage("assignment copies lock value")), +- ReadDiagnostics("a/a.go", &diags)) - --// XORWith updates *h to *h XOR h2. --func (h *Hash) XORWith(h2 Hash) { -- // Small enough that we don't need crypto/subtle.XORBytes. -- for i := range h { -- h[i] ^= h2[i] -- } +- // Assert that there were no other diagnostics. +- // In particular: +- // - "fmt.Printf" does not trigger a [printf] finding; +- // - "cond != true" does not trigger a [bools] finding. +- // +- // We use this check in preference to NoDiagnosticAtRegexp +- // as it is robust in case of minor mistakes in the position +- // regexp, and because it reports unexpected diagnostics. +- if got, want := len(diags.Diagnostics), 1; got != want { +- t.Errorf("got %d diagnostics in a/a.go, want %d:", got, want) +- for i, diag := range diags.Diagnostics { +- t.Logf("Diagnostics[%d] = %+v", i, diag) +- } +- } +- }) -} - --// FileIdentity uniquely identifies a file at a version from a FileSystem. --type FileIdentity struct { -- URI span.URI -- Hash Hash // digest of file contents --} +-// This test demonstrates the deprecated symbol analyzer +-// produces deprecation notices with expected severity and tags. +-func TestDeprecatedAnalysis(t *testing.T) { +- const src = ` +--- go.mod -- +-module example.com +--- a/a.go -- +-package a - --func (id FileIdentity) String() string { -- return fmt.Sprintf("%s%s", id.URI, id.Hash) +-import "example.com/b" +- +-func _() { +- new(b.B).Obsolete() // deprecated -} - --// FileKind describes the kind of the file in question. --// It can be one of Go,mod, Sum, or Tmpl. --type FileKind int +--- b/b.go -- +-package b - --const ( -- // UnknownKind is a file type we don't know about. -- UnknownKind = FileKind(iota) +-type B struct{} - -- // Go is a normal go source file. -- Go -- // Mod is a go.mod file. -- Mod -- // Sum is a go.sum file. -- Sum -- // Tmpl is a template file. -- Tmpl -- // Work is a go.work file. -- Work --) +-// Deprecated: use New instead. +-func (B) Obsolete() {} - --func (k FileKind) String() string { -- switch k { -- case Go: -- return "go" -- case Mod: -- return "go.mod" -- case Sum: -- return "go.sum" -- case Tmpl: -- return "tmpl" -- case Work: -- return "go.work" -- default: -- return fmt.Sprintf("internal error: unknown file kind %d", k) -- } +-func (B) New() {} +-` +- Run(t, src, func(t *testing.T, env *Env) { +- env.OpenFile("a/a.go") +- env.AfterChange( +- Diagnostics( +- env.AtRegexp("a/a.go", "new.*Obsolete"), +- WithMessage("use New instead."), +- WithSeverityTags("deprecated", protocol.SeverityHint, []protocol.DiagnosticTag{protocol.Deprecated}), +- ), +- ) +- }) -} - --// Analyzer represents a go/analysis analyzer with some boolean properties --// that let the user know how to use the analyzer. --type Analyzer struct { -- Analyzer *analysis.Analyzer -- -- // Enabled reports whether the analyzer is enabled. This value can be -- // configured per-analysis in user settings. For staticcheck analyzers, -- // the value of the Staticcheck setting overrides this field. -- // -- // Most clients should use the IsEnabled method. -- Enabled bool +-func TestDiagnosticsOnlyOnSaveFile(t *testing.T) { +- // This functionality is broken because the new orphaned file diagnostics +- // logic wants to publish diagnostics for changed files, independent of any +- // snapshot diagnostics pass, and this causes stale diagnostics to be +- // invalidated. +- // +- // We can fix this behavior more correctly by also honoring the +- // diagnosticsTrigger in DiagnoseOrphanedFiles, but that would require +- // resolving configuration that is independent of the snapshot. In other +- // words, we need to figure out which cache.Folder.Options applies to the +- // changed file, even if it does not have a snapshot. +- t.Skip("temporary skip for golang/go#57979: revisit after zero-config logic is in place") - -- // Fix is the name of the suggested fix name used to invoke the suggested -- // fixes for the analyzer. It is non-empty if we expect this analyzer to -- // provide its fix separately from its diagnostics. That is, we should apply -- // the analyzer's suggested fixes through a Command, not a TextEdit. -- Fix string +- const onlyMod = ` +--- go.mod -- +-module mod.com - -- // fixesDiagnostic reports if a diagnostic from the analyzer can be fixed by Fix. -- // If nil then all diagnostics from the analyzer are assumed to be fixable. -- fixesDiagnostic func(*Diagnostic) bool +-go 1.12 +--- main.go -- +-package main - -- // ActionKind is the kind of code action this analyzer produces. If -- // unspecified the type defaults to quickfix. -- ActionKind []protocol.CodeActionKind +-func main() { +- Foo() +-} +--- foo.go -- +-package main - -- // Severity is the severity set for diagnostics reported by this -- // analyzer. If left unset it defaults to Warning. -- Severity protocol.DiagnosticSeverity +-func Foo() {} +-` +- WithOptions( +- Settings{ +- "diagnosticsTrigger": "Save", +- }, +- ).Run(t, onlyMod, func(t *testing.T, env *Env) { +- env.OpenFile("foo.go") +- env.RegexpReplace("foo.go", "(Foo)", "Bar") // Makes reference to Foo undefined/undeclared. +- env.AfterChange(NoDiagnostics()) // No diagnostics update until file save. - -- // Tag is extra tags (unnecessary, deprecated, etc) for diagnostics -- // reported by this analyzer. -- Tag []protocol.DiagnosticTag --} +- env.SaveBuffer("foo.go") +- // Compiler's error message about undeclared names vary depending on the version, +- // but must be explicit about the problematic name. +- env.AfterChange(Diagnostics(env.AtRegexp("main.go", "Foo"), WithMessage("Foo"))) - --func (a *Analyzer) String() string { return a.Analyzer.String() } +- env.OpenFile("main.go") +- env.RegexpReplace("main.go", "(Foo)", "Bar") +- // No diagnostics update until file save. That results in outdated diagnostic. +- env.AfterChange(Diagnostics(env.AtRegexp("main.go", "Bar"), WithMessage("Foo"))) - --// IsEnabled reports whether this analyzer is enabled by the given options. --func (a Analyzer) IsEnabled(options *Options) bool { -- // Staticcheck analyzers can only be enabled when staticcheck is on. -- if _, ok := options.StaticcheckAnalyzers[a.Analyzer.Name]; ok { -- if !options.Staticcheck { -- return false -- } -- } -- if enabled, ok := options.Analyses[a.Analyzer.Name]; ok { -- return enabled -- } -- return a.Enabled +- env.SaveBuffer("main.go") +- env.AfterChange(NoDiagnostics()) +- }) -} +diff -urN a/gopls/internal/test/integration/diagnostics/golist_test.go b/gopls/internal/test/integration/diagnostics/golist_test.go +--- a/gopls/internal/test/integration/diagnostics/golist_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/diagnostics/golist_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,71 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// FixesDiagnostic returns true if Analyzer.Fix can fix the Diagnostic. --func (a Analyzer) FixesDiagnostic(d *Diagnostic) bool { -- if a.fixesDiagnostic == nil { -- return true -- } -- return a.fixesDiagnostic(d) --} +-package diagnostics - --// Declare explicit types for package paths, names, and IDs to ensure that we --// never use an ID where a path belongs, and vice versa. If we confused these, --// it would result in confusing errors because package IDs often look like --// package paths. --type ( -- PackageID string // go list's unique identifier for a package (e.g. "vendor/example.com/foo [vendor/example.com/bar.test]") -- PackagePath string // name used to prefix linker symbols (e.g. "vendor/example.com/foo") -- PackageName string // identifier in 'package' declaration (e.g. "foo") -- ImportPath string // path that appears in an import declaration (e.g. "example.com/foo") +-import ( +- "testing" +- +- "golang.org/x/tools/gopls/internal/cache" +- . "golang.org/x/tools/gopls/internal/test/integration" +- "golang.org/x/tools/internal/testenv" -) - --// Package represents a Go package that has been parsed and type-checked. --// --// By design, there is no way to reach from a Package to the Package --// representing one of its dependencies. --// --// Callers must not assume that two Packages share the same --// token.FileSet or types.Importer and thus have commensurable --// token.Pos values or types.Objects. Instead, use stable naming --// schemes, such as (URI, byte offset) for positions, or (PackagePath, --// objectpath.Path) for exported declarations. --type Package interface { -- Metadata() *Metadata +-func TestGoListErrors(t *testing.T) { +- testenv.NeedsTool(t, "cgo") +- +- const src = ` +--- go.mod -- +-module a.com +- +-go 1.18 +--- a/a.go -- +-package a +- +-import +--- c/c.go -- +-package c - -- // Results of parsing: -- FileSet() *token.FileSet -- CompiledGoFiles() []*ParsedGoFile // (borrowed) -- File(uri span.URI) (*ParsedGoFile, error) -- GetSyntax() []*ast.File // (borrowed) -- GetParseErrors() []scanner.ErrorList +-/* +-int fortythree() { return 42; } +-*/ +-import "C" - -- // Results of type checking: -- GetTypes() *types.Package -- GetTypeErrors() []types.Error -- GetTypesInfo() *types.Info -- DependencyTypes(PackagePath) *types.Package // nil for indirect dependency of no consequence -- DiagnosticsForFile(ctx context.Context, s Snapshot, uri span.URI) ([]*Diagnostic, error) +-func Foo() { +- print(C.fortytwo()) -} +--- p/p.go -- +-package p - --type unit = struct{} +-import "a.com/q" - --// A CriticalError is a workspace-wide error that generally prevents gopls from --// functioning correctly. In the presence of critical errors, other diagnostics --// in the workspace may not make sense. --type CriticalError struct { -- // MainError is the primary error. Must be non-nil. -- MainError error +-const P = q.Q + 1 +--- q/q.go -- +-package q - -- // Diagnostics contains any supplemental (structured) diagnostics. -- Diagnostics []*Diagnostic +-import "a.com/p" +- +-const Q = p.P + 1 +-` +- +- Run(t, src, func(t *testing.T, env *Env) { +- env.OnceMet( +- InitialWorkspaceLoad, +- Diagnostics( +- env.AtRegexp("a/a.go", "import\n()"), +- FromSource(string(cache.ParseError)), +- ), +- Diagnostics( +- AtPosition("c/c.go", 0, 0), +- FromSource(string(cache.ListError)), +- WithMessage("may indicate failure to perform cgo processing"), +- ), +- Diagnostics( +- env.AtRegexp("p/p.go", `"a.com/q"`), +- FromSource(string(cache.ListError)), +- WithMessage("import cycle not allowed"), +- ), +- ) +- }) -} +diff -urN a/gopls/internal/test/integration/diagnostics/invalidation_test.go b/gopls/internal/test/integration/diagnostics/invalidation_test.go +--- a/gopls/internal/test/integration/diagnostics/invalidation_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/diagnostics/invalidation_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,106 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// An Diagnostic corresponds to an LSP Diagnostic. --// https://microsoft.github.io/language-server-protocol/specification#diagnostic --type Diagnostic struct { -- // TODO(adonovan): should be a protocol.URI, for symmetry. -- URI span.URI // of diagnosed file (not diagnostic documentation) -- Range protocol.Range -- Severity protocol.DiagnosticSeverity -- Code string -- CodeHref string +-package diagnostics - -- // Source is a human-readable description of the source of the error. -- // Diagnostics generated by an analysis.Analyzer set it to Analyzer.Name. -- Source DiagnosticSource +-import ( +- "fmt" +- "testing" - -- Message string +- "golang.org/x/tools/gopls/internal/protocol" +- . "golang.org/x/tools/gopls/internal/test/integration" +-) - -- Tags []protocol.DiagnosticTag -- Related []protocol.DiagnosticRelatedInformation +-// Test for golang/go#50267: diagnostics should be re-sent after a file is +-// opened. +-func TestDiagnosticsAreResentAfterCloseOrOpen(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - -- // Fields below are used internally to generate quick fixes. They aren't -- // part of the LSP spec and historically didn't leave the server. -- // -- // Update(2023-05): version 3.16 of the LSP spec included support for the -- // Diagnostic.data field, which holds arbitrary data preserved in the -- // diagnostic for codeAction requests. This field allows bundling additional -- // information for quick-fixes, and gopls can (and should) use this -- // information to avoid re-evaluating diagnostics in code-action handlers. -- // -- // In order to stage this transition incrementally, the 'BundledFixes' field -- // may store a 'bundled' (=json-serialized) form of the associated -- // SuggestedFixes. Not all diagnostics have their fixes bundled. -- BundledFixes *json.RawMessage -- SuggestedFixes []SuggestedFix +-go 1.16 +--- main.go -- +-package main +- +-func _() { +- x := 2 +-} +-` +- Run(t, files, func(_ *testing.T, env *Env) { // Create a new workspace-level directory and empty file. +- env.OpenFile("main.go") +- var afterOpen protocol.PublishDiagnosticsParams +- env.AfterChange( +- ReadDiagnostics("main.go", &afterOpen), +- ) +- env.CloseBuffer("main.go") +- var afterClose protocol.PublishDiagnosticsParams +- env.AfterChange( +- ReadDiagnostics("main.go", &afterClose), +- ) +- if afterOpen.Version == afterClose.Version { +- t.Errorf("publishDiagnostics: got the same version after closing (%d) as after opening", afterOpen.Version) +- } +- env.OpenFile("main.go") +- var afterReopen protocol.PublishDiagnosticsParams +- env.AfterChange( +- ReadDiagnostics("main.go", &afterReopen), +- ) +- if afterReopen.Version == afterClose.Version { +- t.Errorf("pubslishDiagnostics: got the same version after reopening (%d) as after closing", afterClose.Version) +- } +- }) -} - --func (d *Diagnostic) String() string { -- return fmt.Sprintf("%v: %s", d.Range, d.Message) +-// Test for the "chatty" diagnostics: gopls should re-send diagnostics for +-// changed files after every file change, even if diagnostics did not change. +-func TestChattyDiagnostics(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com +- +-go 1.16 +--- main.go -- +-package main +- +-func _() { +- x := 2 -} - --type DiagnosticSource string +-// Irrelevant comment #0 +-` - --const ( -- UnknownError DiagnosticSource = "<Unknown source>" -- ListError DiagnosticSource = "go list" -- ParseError DiagnosticSource = "syntax" -- TypeError DiagnosticSource = "compiler" -- ModTidyError DiagnosticSource = "go mod tidy" -- OptimizationDetailsError DiagnosticSource = "optimizer details" -- UpgradeNotification DiagnosticSource = "upgrade available" -- Vulncheck DiagnosticSource = "vulncheck imports" -- Govulncheck DiagnosticSource = "govulncheck" -- TemplateError DiagnosticSource = "template" -- WorkFileError DiagnosticSource = "go.work file" -- ConsistencyInfo DiagnosticSource = "consistency" --) +- Run(t, files, func(_ *testing.T, env *Env) { // Create a new workspace-level directory and empty file. +- env.OpenFile("main.go") +- var d protocol.PublishDiagnosticsParams +- env.AfterChange( +- ReadDiagnostics("main.go", &d), +- ) +- +- if len(d.Diagnostics) != 1 { +- t.Fatalf("len(Diagnostics) = %d, want 1", len(d.Diagnostics)) +- } +- msg := d.Diagnostics[0].Message +- +- for i := 0; i < 5; i++ { +- before := d.Version +- env.RegexpReplace("main.go", "Irrelevant comment #.", fmt.Sprintf("Irrelevant comment #%d", i)) +- env.AfterChange( +- ReadDiagnostics("main.go", &d), +- ) +- +- if d.Version == before { +- t.Errorf("after change, got version %d, want new version", d.Version) +- } - --func AnalyzerErrorKind(name string) DiagnosticSource { -- return DiagnosticSource(name) +- // As a sanity check, make sure we have the same diagnostic. +- if len(d.Diagnostics) != 1 { +- t.Fatalf("len(Diagnostics) = %d, want 1", len(d.Diagnostics)) +- } +- newMsg := d.Diagnostics[0].Message +- if newMsg != msg { +- t.Errorf("after change, got message %q, want %q", newMsg, msg) +- } +- } +- }) -} -diff -urN a/gopls/internal/lsp/source/workspace_symbol.go b/gopls/internal/lsp/source/workspace_symbol.go ---- a/gopls/internal/lsp/source/workspace_symbol.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/workspace_symbol.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,611 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/test/integration/diagnostics/undeclared_test.go b/gopls/internal/test/integration/diagnostics/undeclared_test.go +--- a/gopls/internal/test/integration/diagnostics/undeclared_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/diagnostics/undeclared_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,73 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package source +-package diagnostics - -import ( -- "context" -- "fmt" -- "path" -- "path/filepath" -- "regexp" -- "runtime" -- "sort" -- "strings" -- "unicode" +- "testing" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/fuzzy" +- "golang.org/x/tools/gopls/internal/protocol" +- . "golang.org/x/tools/gopls/internal/test/integration" -) - --// Symbol holds a precomputed symbol value. Note: we avoid using the --// protocol.SymbolInformation struct here in order to reduce the size of each --// symbol. --type Symbol struct { -- Name string -- Kind protocol.SymbolKind -- Range protocol.Range +-func TestUndeclaredDiagnostics(t *testing.T) { +- src := ` +--- go.mod -- +-module mod.com +- +-go 1.12 +--- a/a.go -- +-package a +- +-func _() int { +- return x -} +--- b/b.go -- +-package b - --// maxSymbols defines the maximum number of symbol results that should ever be --// sent in response to a client. --const maxSymbols = 100 +-func _() int { +- var y int +- y = y +- return y +-} +-` +- Run(t, src, func(t *testing.T, env *Env) { +- isUnnecessary := func(diag protocol.Diagnostic) bool { +- for _, tag := range diag.Tags { +- if tag == protocol.Unnecessary { +- return true +- } +- } +- return false +- } - --// WorkspaceSymbols matches symbols across all views using the given query, --// according to the match semantics parameterized by matcherType and style. +- // 'x' is undeclared, but still necessary. +- env.OpenFile("a/a.go") +- var adiags protocol.PublishDiagnosticsParams +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/a.go", "x")), +- ReadDiagnostics("a/a.go", &adiags), +- ) +- if got := len(adiags.Diagnostics); got != 1 { +- t.Errorf("len(Diagnostics) = %d, want 1", got) +- } +- if diag := adiags.Diagnostics[0]; isUnnecessary(diag) { +- t.Errorf("%v tagged unnecessary, want necessary", diag) +- } +- +- // 'y = y' is pointless, and should be detected as unnecessary. +- env.OpenFile("b/b.go") +- var bdiags protocol.PublishDiagnosticsParams +- env.AfterChange( +- Diagnostics(env.AtRegexp("b/b.go", "y = y")), +- ReadDiagnostics("b/b.go", &bdiags), +- ) +- if got := len(bdiags.Diagnostics); got != 1 { +- t.Errorf("len(Diagnostics) = %d, want 1", got) +- } +- if diag := bdiags.Diagnostics[0]; !isUnnecessary(diag) { +- t.Errorf("%v tagged necessary, want unnecessary", diag) +- } +- }) +-} +diff -urN a/gopls/internal/test/integration/doc.go b/gopls/internal/test/integration/doc.go +--- a/gopls/internal/test/integration/doc.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/doc.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,156 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-// Package integration provides a framework for writing integration tests of gopls. -// --// The workspace symbol method is defined in the spec as follows: +-// The behaviors that matter to users, and the scenarios they +-// typically describe in bug report, are usually expressed in terms of +-// editor interactions. For example: "When I open my editor in this +-// directory, navigate to this file, and change this line, I get a +-// diagnostic that doesn't make sense". The integration package +-// provides an API for gopls maintainers to express these types of +-// user interactions in ordinary Go tests, validate them, and run them +-// in a variety of execution modes. -// --// The workspace symbol request is sent from the client to the server to --// list project-wide symbols matching the query string. +-// # Test package setup -// --// It is unclear what "project-wide" means here, but given the parameters of --// workspace/symbol do not include any workspace identifier, then it has to be --// assumed that "project-wide" means "across all workspaces". Hence why --// WorkspaceSymbols receives the views []View. +-// The integration test package uses a couple of uncommon patterns to reduce +-// boilerplate in test bodies. First, it is intended to be imported as "." so +-// that helpers do not need to be qualified. Second, it requires some setup +-// that is currently implemented in the integration.Main function, which must be +-// invoked by TestMain. Therefore, a minimal integration testing package looks +-// like this: -// --// However, it then becomes unclear what it would mean to call WorkspaceSymbols --// with a different configured SymbolMatcher per View. Therefore we assume that --// Session level configuration will define the SymbolMatcher to be used for the --// WorkspaceSymbols method. --func WorkspaceSymbols(ctx context.Context, matcher SymbolMatcher, style SymbolStyle, views []View, query string) ([]protocol.SymbolInformation, error) { -- ctx, done := event.Start(ctx, "source.WorkspaceSymbols") -- defer done() -- if query == "" { -- return nil, nil -- } -- -- var s symbolizer -- switch style { -- case DynamicSymbols: -- s = dynamicSymbolMatch -- case FullyQualifiedSymbols: -- s = fullyQualifiedSymbolMatch -- case PackageQualifiedSymbols: -- s = packageSymbolMatch -- default: -- panic(fmt.Errorf("unknown symbol style: %v", style)) -- } -- -- return collectSymbols(ctx, views, matcher, s, query) --} -- --// A matcherFunc returns the index and score of a symbol match. +-// package feature -// --// See the comment for symbolCollector for more information. --type matcherFunc func(chunks []string) (int, float64) -- --// A symbolizer returns the best symbol match for a name with pkg, according to --// some heuristic. The symbol name is passed as the slice nameParts of logical --// name pieces. For example, for myType.field the caller can pass either --// []string{"myType.field"} or []string{"myType.", "field"}. +-// import ( +-// "fmt" +-// "testing" -// --// See the comment for symbolCollector for more information. +-// "golang.org/x/tools/gopls/internal/hooks" +-// . "golang.org/x/tools/gopls/internal/test/integration" +-// ) -// --// The space argument is an empty slice with spare capacity that may be used --// to allocate the result. --type symbolizer func(space []string, name string, pkg *Metadata, m matcherFunc) ([]string, float64) -- --func fullyQualifiedSymbolMatch(space []string, name string, pkg *Metadata, matcher matcherFunc) ([]string, float64) { -- if _, score := dynamicSymbolMatch(space, name, pkg, matcher); score > 0 { -- return append(space, string(pkg.PkgPath), ".", name), score -- } -- return nil, 0 --} -- --func dynamicSymbolMatch(space []string, name string, pkg *Metadata, matcher matcherFunc) ([]string, float64) { -- if IsCommandLineArguments(pkg.ID) { -- // command-line-arguments packages have a non-sensical package path, so -- // just use their package name. -- return packageSymbolMatch(space, name, pkg, matcher) -- } -- -- var score float64 -- -- endsInPkgName := strings.HasSuffix(string(pkg.PkgPath), string(pkg.Name)) -- -- // If the package path does not end in the package name, we need to check the -- // package-qualified symbol as an extra pass first. -- if !endsInPkgName { -- pkgQualified := append(space, string(pkg.Name), ".", name) -- idx, score := matcher(pkgQualified) -- nameStart := len(pkg.Name) + 1 -- if score > 0 { -- // If our match is contained entirely within the unqualified portion, -- // just return that. -- if idx >= nameStart { -- return append(space, name), score -- } -- // Lower the score for matches that include the package name. -- return pkgQualified, score * 0.8 -- } -- } +-// func TestMain(m *testing.M) { +-// Main(m, hooks.Options) +-// } +-// +-// # Writing a simple integration test +-// +-// To run an integration test use the integration.Run function, which accepts a +-// txtar-encoded archive defining the initial workspace state. This function +-// sets up the workspace in a temporary directory, creates a fake text editor, +-// starts gopls, and initializes an LSP session. It then invokes the provided +-// test function with an *Env encapsulating the newly created +-// environment. Because gopls may be run in various modes (as a sidecar or +-// daemon process, with different settings), the test runner may perform this +-// process multiple times, re-running the test function each time with a new +-// environment. +-// +-// func TestOpenFile(t *testing.T) { +-// const files = ` +-// -- go.mod -- +-// module mod.com +-// +-// go 1.12 +-// -- foo.go -- +-// package foo +-// ` +-// Run(t, files, func(t *testing.T, env *Env) { +-// env.OpenFile("foo.go") +-// }) +-// } +-// +-// # Configuring integration test execution +-// +-// The integration package exposes several options that affect the setup process +-// described above. To use these options, use the WithOptions function: +-// +-// WithOptions(opts...).Run(...) +-// +-// See options.go for a full list of available options. +-// +-// # Operating on editor state +-// +-// To operate on editor state within the test body, the Env type provides +-// access to the workspace directory (Env.SandBox), text editor (Env.Editor), +-// LSP server (Env.Server), and 'awaiter' (Env.Awaiter). +-// +-// In most cases, operations on these primitive building blocks of the +-// integration test environment expect a Context (which should be a child of +-// env.Ctx), and return an error. To avoid boilerplate, the Env exposes a set +-// of wrappers in wrappers.go for use in scripting: +-// +-// env.CreateBuffer("c/c.go", "") +-// env.EditBuffer("c/c.go", editor.Edit{ +-// Text: `package c`, +-// }) +-// +-// These wrappers thread through Env.Ctx, and call t.Fatal on any errors. +-// +-// # Expressing expectations +-// +-// The general pattern for an integration test is to script interactions with the +-// fake editor and sandbox, and assert that gopls behaves correctly after each +-// state change. Unfortunately, this is complicated by the fact that state +-// changes are communicated to gopls via unidirectional client->server +-// notifications (didOpen, didChange, etc.), and resulting gopls behavior such +-// as diagnostics, logs, or messages is communicated back via server->client +-// notifications. Therefore, within integration tests we must be able to say "do +-// this, and then eventually gopls should do that". To achieve this, the +-// integration package provides a framework for expressing conditions that must +-// eventually be met, in terms of the Expectation type. +-// +-// To express the assertion that "eventually gopls must meet these +-// expectations", use env.Await(...): +-// +-// env.RegexpReplace("x/x.go", `package x`, `package main`) +-// env.Await(env.DiagnosticAtRegexp("x/main.go", `fmt`)) +-// +-// Await evaluates the provided expectations atomically, whenever the client +-// receives a state-changing notification from gopls. See expectation.go for a +-// full list of available expectations. +-// +-// A problem with this model is that if gopls never meets the provided +-// expectations, the test runner will hang until the test timeout +-// (which defaults to 10m). There are two ways to work around this +-// poor behavior: +-// +-// 1. Use a precondition to define precisely when we expect conditions to be +-// met. Gopls provides the OnceMet(precondition, expectations...) pattern +-// to express ("once this precondition is met, the following expectations +-// must all hold"). To instrument preconditions, gopls uses verbose +-// progress notifications to inform the client about ongoing work (see +-// CompletedWork). The most common precondition is to wait for gopls to be +-// done processing all change notifications, for which the integration package +-// provides the AfterChange helper. For example: +-// +-// // We expect diagnostics to be cleared after gopls is done processing the +-// // didSave notification. +-// env.SaveBuffer("a/go.mod") +-// env.AfterChange(EmptyDiagnostics("a/go.mod")) +-// +-// 2. Set a shorter timeout during development, if you expect to be breaking +-// tests. By setting the environment variable GOPLS_INTEGRATION_TEST_TIMEOUT=5s, +-// integration tests will time out after 5 seconds. +-// +-// # Tips & Tricks +-// +-// Here are some tips and tricks for working with integration tests: +-// +-// 1. Set the environment variable GOPLS_INTEGRRATION_TEST_TIMEOUT=5s during development. +-// 2. Run tests with -short. This will only run integration tests in the +-// default gopls execution mode. +-// 3. Use capture groups to narrow regexp positions. All regular-expression +-// based positions (such as DiagnosticAtRegexp) will match the position of +-// the first capture group, if any are provided. This can be used to +-// identify a specific position in the code for a pattern that may occur in +-// multiple places. For example `var (mu) sync.Mutex` matches the position +-// of "mu" within the variable declaration. +-// 4. Read diagnostics into a variable to implement more complicated +-// assertions about diagnostic state in the editor. To do this, use the +-// pattern OnceMet(precondition, ReadDiagnostics("file.go", &d)) to capture +-// the current diagnostics as soon as the precondition is met. This is +-// preferable to accessing the diagnostics directly, as it avoids races. +-package integration +diff -urN a/gopls/internal/test/integration/env.go b/gopls/internal/test/integration/env.go +--- a/gopls/internal/test/integration/env.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/env.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,392 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- // Now try matching the fully qualified symbol. -- fullyQualified := append(space, string(pkg.PkgPath), ".", name) -- idx, score := matcher(fullyQualified) +-package integration - -- // As above, check if we matched just the unqualified symbol name. -- nameStart := len(pkg.PkgPath) + 1 -- if idx >= nameStart { -- return append(space, name), score -- } +-import ( +- "context" +- "fmt" +- "strings" +- "sync" +- "testing" - -- // If our package path ends in the package name, we'll have skipped the -- // initial pass above, so check if we matched just the package-qualified -- // name. -- if endsInPkgName && idx >= 0 { -- pkgStart := len(pkg.PkgPath) - len(pkg.Name) -- if idx >= pkgStart { -- return append(space, string(pkg.Name), ".", name), score -- } -- } +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/test/integration/fake" +- "golang.org/x/tools/internal/jsonrpc2/servertest" +-) - -- // Our match was not contained within the unqualified or package qualified -- // symbol. Return the fully qualified symbol but discount the score. -- return fullyQualified, score * 0.6 --} +-// Env holds the building blocks of an editor testing environment, providing +-// wrapper methods that hide the boilerplate of plumbing contexts and checking +-// errors. +-type Env struct { +- T testing.TB // TODO(rfindley): rename to TB +- Ctx context.Context - --func packageSymbolMatch(space []string, name string, pkg *Metadata, matcher matcherFunc) ([]string, float64) { -- qualified := append(space, string(pkg.Name), ".", name) -- if _, s := matcher(qualified); s > 0 { -- return qualified, s -- } -- return nil, 0 --} +- // Most tests should not need to access the scratch area, editor, server, or +- // connection, but they are available if needed. +- Sandbox *fake.Sandbox +- Server servertest.Connector - --func buildMatcher(matcher SymbolMatcher, query string) matcherFunc { -- switch matcher { -- case SymbolFuzzy: -- return parseQuery(query, newFuzzyMatcher) -- case SymbolFastFuzzy: -- return parseQuery(query, func(query string) matcherFunc { -- return fuzzy.NewSymbolMatcher(query).Match -- }) -- case SymbolCaseSensitive: -- return matchExact(query) -- case SymbolCaseInsensitive: -- q := strings.ToLower(query) -- exact := matchExact(q) -- wrapper := []string{""} -- return func(chunks []string) (int, float64) { -- s := strings.Join(chunks, "") -- wrapper[0] = strings.ToLower(s) -- return exact(wrapper) -- } -- } -- panic(fmt.Errorf("unknown symbol matcher: %v", matcher)) --} +- // Editor is owned by the Env, and shut down +- Editor *fake.Editor - --func newFuzzyMatcher(query string) matcherFunc { -- fm := fuzzy.NewMatcher(query) -- return func(chunks []string) (int, float64) { -- score := float64(fm.ScoreChunks(chunks)) -- ranges := fm.MatchedRanges() -- if len(ranges) > 0 { -- return ranges[0], score -- } -- return -1, score -- } +- Awaiter *Awaiter -} - --// parseQuery parses a field-separated symbol query, extracting the special --// characters listed below, and returns a matcherFunc corresponding to the AND --// of all field queries. --// --// Special characters: +-// An Awaiter keeps track of relevant LSP state, so that it may be asserted +-// upon with Expectations. -// --// ^ match exact prefix --// $ match exact suffix --// ' match exact +-// Wire it into a fake.Editor using Awaiter.Hooks(). -// --// In all three of these special queries, matches are 'smart-cased', meaning --// they are case sensitive if the symbol query contains any upper-case --// characters, and case insensitive otherwise. --func parseQuery(q string, newMatcher func(string) matcherFunc) matcherFunc { -- fields := strings.Fields(q) -- if len(fields) == 0 { -- return func([]string) (int, float64) { return -1, 0 } -- } -- var funcs []matcherFunc -- for _, field := range fields { -- var f matcherFunc -- switch { -- case strings.HasPrefix(field, "^"): -- prefix := field[1:] -- f = smartCase(prefix, func(chunks []string) (int, float64) { -- s := strings.Join(chunks, "") -- if strings.HasPrefix(s, prefix) { -- return 0, 1 -- } -- return -1, 0 -- }) -- case strings.HasPrefix(field, "'"): -- exact := field[1:] -- f = smartCase(exact, matchExact(exact)) -- case strings.HasSuffix(field, "$"): -- suffix := field[0 : len(field)-1] -- f = smartCase(suffix, func(chunks []string) (int, float64) { -- s := strings.Join(chunks, "") -- if strings.HasSuffix(s, suffix) { -- return len(s) - len(suffix), 1 -- } -- return -1, 0 -- }) -- default: -- f = newMatcher(field) -- } -- funcs = append(funcs, f) -- } -- if len(funcs) == 1 { -- return funcs[0] -- } -- return comboMatcher(funcs).match --} +-// TODO(rfindley): consider simply merging Awaiter with the fake.Editor. It +-// probably is not worth its own abstraction. +-type Awaiter struct { +- workdir *fake.Workdir - --func matchExact(exact string) matcherFunc { -- return func(chunks []string) (int, float64) { -- s := strings.Join(chunks, "") -- if idx := strings.LastIndex(s, exact); idx >= 0 { -- return idx, 1 -- } -- return -1, 0 -- } +- mu sync.Mutex +- // For simplicity, each waiter gets a unique ID. +- nextWaiterID int +- state State +- waiters map[int]*condition -} - --// smartCase returns a matcherFunc that is case-sensitive if q contains any --// upper-case characters, and case-insensitive otherwise. --func smartCase(q string, m matcherFunc) matcherFunc { -- insensitive := strings.ToLower(q) == q -- wrapper := []string{""} -- return func(chunks []string) (int, float64) { -- s := strings.Join(chunks, "") -- if insensitive { -- s = strings.ToLower(s) -- } -- wrapper[0] = s -- return m(wrapper) +-func NewAwaiter(workdir *fake.Workdir) *Awaiter { +- return &Awaiter{ +- workdir: workdir, +- state: State{ +- diagnostics: make(map[string]*protocol.PublishDiagnosticsParams), +- work: make(map[protocol.ProgressToken]*workProgress), +- }, +- waiters: make(map[int]*condition), - } -} - --type comboMatcher []matcherFunc -- --func (c comboMatcher) match(chunks []string) (int, float64) { -- score := 1.0 -- first := 0 -- for _, f := range c { -- idx, s := f(chunks) -- if idx < first { -- first = idx -- } -- score *= s +-// Hooks returns LSP client hooks required for awaiting asynchronous expectations. +-func (a *Awaiter) Hooks() fake.ClientHooks { +- return fake.ClientHooks{ +- OnDiagnostics: a.onDiagnostics, +- OnLogMessage: a.onLogMessage, +- OnWorkDoneProgressCreate: a.onWorkDoneProgressCreate, +- OnProgress: a.onProgress, +- OnShowDocument: a.onShowDocument, +- OnShowMessage: a.onShowMessage, +- OnShowMessageRequest: a.onShowMessageRequest, +- OnRegisterCapability: a.onRegisterCapability, +- OnUnregisterCapability: a.onUnregisterCapability, +- OnApplyEdit: a.onApplyEdit, - } -- return first, score -} - --// collectSymbols calls snapshot.Symbols to walk the syntax trees of --// all files in the views' current snapshots, and returns a sorted, --// scored list of symbols that best match the parameters. --// --// How it matches symbols is parameterized by two interfaces: --// - A matcherFunc determines how well a string symbol matches a query. It --// returns a non-negative score indicating the quality of the match. A score --// of zero indicates no match. --// - A symbolizer determines how we extract the symbol for an object. This --// enables the 'symbolStyle' configuration option. --func collectSymbols(ctx context.Context, views []View, matcherType SymbolMatcher, symbolizer symbolizer, query string) ([]protocol.SymbolInformation, error) { -- // Extract symbols from all files. -- var work []symbolFile -- var roots []string -- seen := make(map[span.URI]bool) -- // TODO(adonovan): opt: parallelize this loop? How often is len > 1? -- for _, v := range views { -- snapshot, release, err := v.Snapshot() -- if err != nil { -- continue // view is shut down; continue with others -- } -- defer release() -- -- // Use the root view URIs for determining (lexically) -- // whether a URI is in any open workspace. -- roots = append(roots, strings.TrimRight(string(v.Folder()), "/")) +-// State encapsulates the server state TODO: explain more +-type State struct { +- // diagnostics are a map of relative path->diagnostics params +- diagnostics map[string]*protocol.PublishDiagnosticsParams +- logs []*protocol.LogMessageParams +- showDocument []*protocol.ShowDocumentParams +- showMessage []*protocol.ShowMessageParams +- showMessageRequest []*protocol.ShowMessageRequestParams - -- filters := snapshot.Options().DirectoryFilters -- filterer := NewFilterer(filters) -- folder := filepath.ToSlash(v.Folder().Filename()) +- registrations []*protocol.RegistrationParams +- registeredCapabilities map[string]protocol.Registration +- unregistrations []*protocol.UnregistrationParams +- documentChanges []protocol.DocumentChanges // collected from ApplyEdit downcalls - -- workspaceOnly := true -- if snapshot.Options().SymbolScope == AllSymbolScope { -- workspaceOnly = false -- } -- symbols, err := snapshot.Symbols(ctx, workspaceOnly) -- if err != nil { -- return nil, err -- } +- // outstandingWork is a map of token->work summary. All tokens are assumed to +- // be string, though the spec allows for numeric tokens as well. When work +- // completes, it is deleted from this map. +- work map[protocol.ProgressToken]*workProgress +-} - -- for uri, syms := range symbols { -- norm := filepath.ToSlash(uri.Filename()) -- nm := strings.TrimPrefix(norm, folder) -- if filterer.Disallow(nm) { -- continue -- } -- // Only scan each file once. -- if seen[uri] { -- continue -- } -- meta, err := NarrowestMetadataForFile(ctx, snapshot, uri) -- if err != nil { -- event.Error(ctx, fmt.Sprintf("missing metadata for %q", uri), err) -- continue -- } -- seen[uri] = true -- work = append(work, symbolFile{uri, meta, syms}) +-// completedWork counts complete work items by title. +-func (s State) completedWork() map[string]uint64 { +- completed := make(map[string]uint64) +- for _, work := range s.work { +- if work.complete { +- completed[work.title]++ - } - } +- return completed +-} - -- // Match symbols in parallel. -- // Each worker has its own symbolStore, -- // which we merge at the end. -- nmatchers := runtime.GOMAXPROCS(-1) // matching is CPU bound -- results := make(chan *symbolStore) -- for i := 0; i < nmatchers; i++ { -- go func(i int) { -- matcher := buildMatcher(matcherType, query) -- store := new(symbolStore) -- // Assign files to workers in round-robin fashion. -- for j := i; j < len(work); j += nmatchers { -- matchFile(store, symbolizer, matcher, roots, work[j]) -- } -- results <- store -- }(i) +-// startedWork counts started (and possibly complete) work items. +-func (s State) startedWork() map[string]uint64 { +- started := make(map[string]uint64) +- for _, work := range s.work { +- started[work.title]++ - } +- return started +-} - -- // Gather and merge results as they arrive. -- var unified symbolStore -- for i := 0; i < nmatchers; i++ { -- store := <-results -- for _, syms := range store.res { -- unified.store(syms) +-type workProgress struct { +- title, msg, endMsg string +- percent float64 +- complete bool // seen 'end'. +-} +- +-// This method, provided for debugging, accesses mutable fields without a lock, +-// so it must not be called concurrent with any State mutation. +-func (s State) String() string { +- var b strings.Builder +- b.WriteString("#### log messages (see RPC logs for full text):\n") +- for _, msg := range s.logs { +- summary := fmt.Sprintf("%v: %q", msg.Type, msg.Message) +- if len(summary) > 60 { +- summary = summary[:57] + "..." +- } +- // Some logs are quite long, and since they should be reproduced in the RPC +- // logs on any failure we include here just a short summary. +- fmt.Fprint(&b, "\t"+summary+"\n") +- } +- b.WriteString("\n") +- b.WriteString("#### diagnostics:\n") +- for name, params := range s.diagnostics { +- fmt.Fprintf(&b, "\t%s (version %d):\n", name, params.Version) +- for _, d := range params.Diagnostics { +- fmt.Fprintf(&b, "\t\t%d:%d [%s]: %s\n", d.Range.Start.Line, d.Range.Start.Character, d.Source, d.Message) - } - } -- return unified.results(), nil +- b.WriteString("\n") +- b.WriteString("#### outstanding work:\n") +- for token, state := range s.work { +- if state.complete { +- continue +- } +- name := state.title +- if name == "" { +- name = fmt.Sprintf("!NO NAME(token: %s)", token) +- } +- fmt.Fprintf(&b, "\t%s: %.2f\n", name, state.percent) +- } +- b.WriteString("#### completed work:\n") +- for name, count := range s.completedWork() { +- fmt.Fprintf(&b, "\t%s: %d\n", name, count) +- } +- return b.String() -} - --type Filterer struct { -- // Whether a filter is excluded depends on the operator (first char of the raw filter). -- // Slices filters and excluded then should have the same length. -- filters []*regexp.Regexp -- excluded []bool +-// A condition is satisfied when all expectations are simultaneously +-// met. At that point, the 'met' channel is closed. On any failure, err is set +-// and the failed channel is closed. +-type condition struct { +- expectations []Expectation +- verdict chan Verdict -} - --// NewFilterer computes regular expression form of all raw filters --func NewFilterer(rawFilters []string) *Filterer { -- var f Filterer -- for _, filter := range rawFilters { -- filter = path.Clean(filepath.ToSlash(filter)) -- // TODO(dungtuanle): fix: validate [+-] prefix. -- op, prefix := filter[0], filter[1:] -- // convertFilterToRegexp adds "/" at the end of prefix to handle cases where a filter is a prefix of another filter. -- // For example, it prevents [+foobar, -foo] from excluding "foobar". -- f.filters = append(f.filters, convertFilterToRegexp(filepath.ToSlash(prefix))) -- f.excluded = append(f.excluded, op == '-') -- } +-func (a *Awaiter) onApplyEdit(_ context.Context, params *protocol.ApplyWorkspaceEditParams) error { +- a.mu.Lock() +- defer a.mu.Unlock() - -- return &f +- a.state.documentChanges = append(a.state.documentChanges, params.Edit.DocumentChanges...) +- a.checkConditionsLocked() +- return nil -} - --// Disallow return true if the path is excluded from the filterer's filters. --func (f *Filterer) Disallow(path string) bool { -- // Ensure trailing but not leading slash. -- path = strings.TrimPrefix(path, "/") -- if !strings.HasSuffix(path, "/") { -- path += "/" -- } +-func (a *Awaiter) onDiagnostics(_ context.Context, d *protocol.PublishDiagnosticsParams) error { +- a.mu.Lock() +- defer a.mu.Unlock() - -- // TODO(adonovan): opt: iterate in reverse and break at first match. -- excluded := false -- for i, filter := range f.filters { -- if filter.MatchString(path) { -- excluded = f.excluded[i] // last match wins -- } -- } -- return excluded +- pth := a.workdir.URIToPath(d.URI) +- a.state.diagnostics[pth] = d +- a.checkConditionsLocked() +- return nil -} - --// convertFilterToRegexp replaces glob-like operator substrings in a string file path to their equivalent regex forms. --// Supporting glob-like operators: --// - **: match zero or more complete path segments --func convertFilterToRegexp(filter string) *regexp.Regexp { -- if filter == "" { -- return regexp.MustCompile(".*") -- } -- var ret strings.Builder -- ret.WriteString("^") -- segs := strings.Split(filter, "/") -- for _, seg := range segs { -- // Inv: seg != "" since path is clean. -- if seg == "**" { -- ret.WriteString(".*") -- } else { -- ret.WriteString(regexp.QuoteMeta(seg)) -- } -- ret.WriteString("/") -- } -- pattern := ret.String() -- -- // Remove unnecessary "^.*" prefix, which increased -- // BenchmarkWorkspaceSymbols time by ~20% (even though -- // filter CPU time increased by only by ~2.5%) when the -- // default filter was changed to "**/node_modules". -- pattern = strings.TrimPrefix(pattern, "^.*") +-func (a *Awaiter) onShowDocument(_ context.Context, params *protocol.ShowDocumentParams) error { +- a.mu.Lock() +- defer a.mu.Unlock() - -- return regexp.MustCompile(pattern) +- a.state.showDocument = append(a.state.showDocument, params) +- a.checkConditionsLocked() +- return nil -} - --// symbolFile holds symbol information for a single file. --type symbolFile struct { -- uri span.URI -- md *Metadata -- syms []Symbol +-func (a *Awaiter) onShowMessage(_ context.Context, m *protocol.ShowMessageParams) error { +- a.mu.Lock() +- defer a.mu.Unlock() +- +- a.state.showMessage = append(a.state.showMessage, m) +- a.checkConditionsLocked() +- return nil -} - --// matchFile scans a symbol file and adds matching symbols to the store. --func matchFile(store *symbolStore, symbolizer symbolizer, matcher matcherFunc, roots []string, i symbolFile) { -- space := make([]string, 0, 3) -- for _, sym := range i.syms { -- symbolParts, score := symbolizer(space, sym.Name, i.md, matcher) +-func (a *Awaiter) onShowMessageRequest(_ context.Context, m *protocol.ShowMessageRequestParams) error { +- a.mu.Lock() +- defer a.mu.Unlock() - -- // Check if the score is too low before applying any downranking. -- if store.tooLow(score) { -- continue -- } +- a.state.showMessageRequest = append(a.state.showMessageRequest, m) +- a.checkConditionsLocked() +- return nil +-} - -- // Factors to apply to the match score for the purpose of downranking -- // results. -- // -- // These numbers were crudely calibrated based on trial-and-error using a -- // small number of sample queries. Adjust as necessary. -- // -- // All factors are multiplicative, meaning if more than one applies they are -- // multiplied together. -- const ( -- // nonWorkspaceFactor is applied to symbols outside the workspace. -- // Developers are less likely to want to jump to code that they -- // are not actively working on. -- nonWorkspaceFactor = 0.5 -- // nonWorkspaceUnexportedFactor is applied to unexported symbols outside -- // the workspace. Since one wouldn't usually jump to unexported -- // symbols to understand a package API, they are particularly irrelevant. -- nonWorkspaceUnexportedFactor = 0.5 -- // every field or method nesting level to access the field decreases -- // the score by a factor of 1.0 - depth*depthFactor, up to a depth of -- // 3. -- // -- // Use a small constant here, as this exists mostly to break ties -- // (e.g. given a type Foo and a field x.Foo, prefer Foo). -- depthFactor = 0.01 -- ) +-func (a *Awaiter) onLogMessage(_ context.Context, m *protocol.LogMessageParams) error { +- a.mu.Lock() +- defer a.mu.Unlock() - -- startWord := true -- exported := true -- depth := 0.0 -- for _, r := range sym.Name { -- if startWord && !unicode.IsUpper(r) { -- exported = false -- } -- if r == '.' { -- startWord = true -- depth++ -- } else { -- startWord = false -- } -- } +- a.state.logs = append(a.state.logs, m) +- a.checkConditionsLocked() +- return nil +-} - -- // TODO(rfindley): use metadata to determine if the file is in a workspace -- // package, rather than this heuristic. -- inWorkspace := false -- for _, root := range roots { -- if strings.HasPrefix(string(i.uri), root) { -- inWorkspace = true -- break -- } -- } +-func (a *Awaiter) onWorkDoneProgressCreate(_ context.Context, m *protocol.WorkDoneProgressCreateParams) error { +- a.mu.Lock() +- defer a.mu.Unlock() - -- // Apply downranking based on workspace position. -- if !inWorkspace { -- score *= nonWorkspaceFactor -- if !exported { -- score *= nonWorkspaceUnexportedFactor -- } -- } +- a.state.work[m.Token] = &workProgress{} +- return nil +-} - -- // Apply downranking based on symbol depth. -- if depth > 3 { -- depth = 3 +-func (a *Awaiter) onProgress(_ context.Context, m *protocol.ProgressParams) error { +- a.mu.Lock() +- defer a.mu.Unlock() +- work, ok := a.state.work[m.Token] +- if !ok { +- panic(fmt.Sprintf("got progress report for unknown report %v: %v", m.Token, m)) +- } +- v := m.Value.(map[string]interface{}) +- switch kind := v["kind"]; kind { +- case "begin": +- work.title = v["title"].(string) +- if msg, ok := v["message"]; ok { +- work.msg = msg.(string) - } -- score *= 1.0 - depth*depthFactor -- -- if store.tooLow(score) { -- continue +- case "report": +- if pct, ok := v["percentage"]; ok { +- work.percent = pct.(float64) - } -- -- si := symbolInformation{ -- score: score, -- symbol: strings.Join(symbolParts, ""), -- kind: sym.Kind, -- uri: i.uri, -- rng: sym.Range, -- container: string(i.md.PkgPath), +- if msg, ok := v["message"]; ok { +- work.msg = msg.(string) +- } +- case "end": +- work.complete = true +- if msg, ok := v["message"]; ok { +- work.endMsg = msg.(string) - } -- store.store(si) - } +- a.checkConditionsLocked() +- return nil -} - --type symbolStore struct { -- res [maxSymbols]symbolInformation --} +-func (a *Awaiter) onRegisterCapability(_ context.Context, m *protocol.RegistrationParams) error { +- a.mu.Lock() +- defer a.mu.Unlock() - --// store inserts si into the sorted results, if si has a high enough score. --func (sc *symbolStore) store(si symbolInformation) { -- if sc.tooLow(si.score) { -- return +- a.state.registrations = append(a.state.registrations, m) +- if a.state.registeredCapabilities == nil { +- a.state.registeredCapabilities = make(map[string]protocol.Registration) - } -- insertAt := sort.Search(len(sc.res), func(i int) bool { -- // Sort by score, then symbol length, and finally lexically. -- if sc.res[i].score != si.score { -- return sc.res[i].score < si.score -- } -- if len(sc.res[i].symbol) != len(si.symbol) { -- return len(sc.res[i].symbol) > len(si.symbol) -- } -- return sc.res[i].symbol > si.symbol -- }) -- if insertAt < len(sc.res)-1 { -- copy(sc.res[insertAt+1:], sc.res[insertAt:len(sc.res)-1]) +- for _, reg := range m.Registrations { +- a.state.registeredCapabilities[reg.Method] = reg - } -- sc.res[insertAt] = si +- a.checkConditionsLocked() +- return nil -} - --func (sc *symbolStore) tooLow(score float64) bool { -- return score <= sc.res[len(sc.res)-1].score +-func (a *Awaiter) onUnregisterCapability(_ context.Context, m *protocol.UnregistrationParams) error { +- a.mu.Lock() +- defer a.mu.Unlock() +- +- a.state.unregistrations = append(a.state.unregistrations, m) +- a.checkConditionsLocked() +- return nil -} - --func (sc *symbolStore) results() []protocol.SymbolInformation { -- var res []protocol.SymbolInformation -- for _, si := range sc.res { -- if si.score <= 0 { -- return res +-func (a *Awaiter) checkConditionsLocked() { +- for id, condition := range a.waiters { +- if v, _ := checkExpectations(a.state, condition.expectations); v != Unmet { +- delete(a.waiters, id) +- condition.verdict <- v - } -- res = append(res, si.asProtocolSymbolInformation()) - } +-} +- +-// TakeDocumentChanges returns any accumulated document changes (from +-// server ApplyEdit RPC downcalls) and resets the list. +-func (a *Awaiter) TakeDocumentChanges() []protocol.DocumentChanges { +- a.mu.Lock() +- defer a.mu.Unlock() +- +- res := a.state.documentChanges +- a.state.documentChanges = nil - return res -} - --// symbolInformation is a cut-down version of protocol.SymbolInformation that --// allows struct values of this type to be used as map keys. --type symbolInformation struct { -- score float64 -- symbol string -- container string -- kind protocol.SymbolKind -- uri span.URI -- rng protocol.Range +-// checkExpectations reports whether s meets all expectations. +-func checkExpectations(s State, expectations []Expectation) (Verdict, string) { +- finalVerdict := Met +- var summary strings.Builder +- for _, e := range expectations { +- v := e.Check(s) +- if v > finalVerdict { +- finalVerdict = v +- } +- fmt.Fprintf(&summary, "%v: %s\n", v, e.Description) +- } +- return finalVerdict, summary.String() -} - --// asProtocolSymbolInformation converts s to a protocol.SymbolInformation value. +-// Await blocks until the given expectations are all simultaneously met. -// --// TODO: work out how to handle tags if/when they are needed. --func (s symbolInformation) asProtocolSymbolInformation() protocol.SymbolInformation { -- return protocol.SymbolInformation{ -- Name: s.symbol, -- Kind: s.kind, -- Location: protocol.Location{ -- URI: protocol.URIFromSpanURI(s.uri), -- Range: s.rng, +-// Generally speaking Await should be avoided because it blocks indefinitely if +-// gopls ends up in a state where the expectations are never going to be met. +-// Use AfterChange or OnceMet instead, so that the runner knows when to stop +-// waiting. +-func (e *Env) Await(expectations ...Expectation) { +- e.T.Helper() +- if err := e.Awaiter.Await(e.Ctx, expectations...); err != nil { +- e.T.Fatal(err) +- } +-} +- +-// OnceMet blocks until the precondition is met by the state or becomes +-// unmeetable. If it was met, OnceMet checks that the state meets all +-// expectations in mustMeets. +-func (e *Env) OnceMet(precondition Expectation, mustMeets ...Expectation) { +- e.T.Helper() +- e.Await(OnceMet(precondition, mustMeets...)) +-} +- +-// Await waits for all expectations to simultaneously be met. It should only be +-// called from the main test goroutine. +-func (a *Awaiter) Await(ctx context.Context, expectations ...Expectation) error { +- a.mu.Lock() +- // Before adding the waiter, we check if the condition is currently met or +- // failed to avoid a race where the condition was realized before Await was +- // called. +- switch verdict, summary := checkExpectations(a.state, expectations); verdict { +- case Met: +- a.mu.Unlock() +- return nil +- case Unmeetable: +- err := fmt.Errorf("unmeetable expectations:\n%s\nstate:\n%v", summary, a.state) +- a.mu.Unlock() +- return err +- } +- cond := &condition{ +- expectations: expectations, +- verdict: make(chan Verdict), +- } +- a.waiters[a.nextWaiterID] = cond +- a.nextWaiterID++ +- a.mu.Unlock() +- +- var err error +- select { +- case <-ctx.Done(): +- err = ctx.Err() +- case v := <-cond.verdict: +- if v != Met { +- err = fmt.Errorf("condition has final verdict %v", v) +- } +- } +- a.mu.Lock() +- defer a.mu.Unlock() +- _, summary := checkExpectations(a.state, expectations) +- +- // Debugging an unmet expectation can be tricky, so we put some effort into +- // nicely formatting the failure. +- if err != nil { +- return fmt.Errorf("waiting on:\n%s\nerr:%v\n\nstate:\n%v", summary, err, a.state) +- } +- return nil +-} +diff -urN a/gopls/internal/test/integration/env_test.go b/gopls/internal/test/integration/env_test.go +--- a/gopls/internal/test/integration/env_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/env_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,66 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package integration +- +-import ( +- "context" +- "encoding/json" +- "testing" +- +- "golang.org/x/tools/gopls/internal/protocol" +-) +- +-func TestProgressUpdating(t *testing.T) { +- a := &Awaiter{ +- state: State{ +- work: make(map[protocol.ProgressToken]*workProgress), - }, -- ContainerName: s.container, +- } +- ctx := context.Background() +- if err := a.onWorkDoneProgressCreate(ctx, &protocol.WorkDoneProgressCreateParams{ +- Token: "foo", +- }); err != nil { +- t.Fatal(err) +- } +- if err := a.onWorkDoneProgressCreate(ctx, &protocol.WorkDoneProgressCreateParams{ +- Token: "bar", +- }); err != nil { +- t.Fatal(err) +- } +- updates := []struct { +- token string +- value interface{} +- }{ +- {"foo", protocol.WorkDoneProgressBegin{Kind: "begin", Title: "foo work"}}, +- {"bar", protocol.WorkDoneProgressBegin{Kind: "begin", Title: "bar work"}}, +- {"foo", protocol.WorkDoneProgressEnd{Kind: "end"}}, +- {"bar", protocol.WorkDoneProgressReport{Kind: "report", Percentage: 42}}, +- } +- for _, update := range updates { +- params := &protocol.ProgressParams{ +- Token: update.token, +- Value: update.value, +- } +- data, err := json.Marshal(params) +- if err != nil { +- t.Fatal(err) +- } +- var unmarshaled protocol.ProgressParams +- if err := json.Unmarshal(data, &unmarshaled); err != nil { +- t.Fatal(err) +- } +- if err := a.onProgress(ctx, &unmarshaled); err != nil { +- t.Fatal(err) +- } +- } +- if !a.state.work["foo"].complete { +- t.Error("work entry \"foo\" is incomplete, want complete") +- } +- got := *a.state.work["bar"] +- want := workProgress{title: "bar work", percent: 42} +- if got != want { +- t.Errorf("work progress for \"bar\": %v, want %v", got, want) - } -} -diff -urN a/gopls/internal/lsp/source/workspace_symbol_test.go b/gopls/internal/lsp/source/workspace_symbol_test.go ---- a/gopls/internal/lsp/source/workspace_symbol_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/workspace_symbol_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,136 +0,0 @@ +diff -urN a/gopls/internal/test/integration/expectation.go b/gopls/internal/test/integration/expectation.go +--- a/gopls/internal/test/integration/expectation.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/expectation.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,824 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package source +-package integration - -import ( -- "testing" +- "fmt" +- "regexp" +- "sort" +- "strings" +- +- "github.com/google/go-cmp/cmp" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/server" -) - --func TestParseQuery(t *testing.T) { -- tests := []struct { -- query, s string -- wantMatch bool -- }{ -- {"", "anything", false}, -- {"any", "anything", true}, -- {"any$", "anything", false}, -- {"ing$", "anything", true}, -- {"ing$", "anythinG", true}, -- {"inG$", "anything", false}, -- {"^any", "anything", true}, -- {"^any", "Anything", true}, -- {"^Any", "anything", false}, -- {"at", "anything", true}, -- // TODO: this appears to be a bug in the fuzzy matching algorithm. 'At' -- // should cause a case-sensitive match. -- // {"At", "anything", false}, -- {"At", "Anything", true}, -- {"'yth", "Anything", true}, -- {"'yti", "Anything", false}, -- {"'any 'thing", "Anything", true}, -- {"anythn nythg", "Anything", true}, -- {"ntx", "Anything", false}, -- {"anythn", "anything", true}, -- {"ing", "anything", true}, -- {"anythn nythgx", "anything", false}, +-var ( +- // InitialWorkspaceLoad is an expectation that the workspace initial load has +- // completed. It is verified via workdone reporting. +- InitialWorkspaceLoad = CompletedWork(server.DiagnosticWorkTitle(server.FromInitialWorkspaceLoad), 1, false) +-) +- +-// A Verdict is the result of checking an expectation against the current +-// editor state. +-type Verdict int +- +-// Order matters for the following constants: verdicts are sorted in order of +-// decisiveness. +-const ( +- // Met indicates that an expectation is satisfied by the current state. +- Met Verdict = iota +- // Unmet indicates that an expectation is not currently met, but could be met +- // in the future. +- Unmet +- // Unmeetable indicates that an expectation cannot be satisfied in the +- // future. +- Unmeetable +-) +- +-func (v Verdict) String() string { +- switch v { +- case Met: +- return "Met" +- case Unmet: +- return "Unmet" +- case Unmeetable: +- return "Unmeetable" - } +- return fmt.Sprintf("unrecognized verdict %d", v) +-} - -- for _, test := range tests { -- matcher := parseQuery(test.query, newFuzzyMatcher) -- if _, score := matcher([]string{test.s}); score > 0 != test.wantMatch { -- t.Errorf("parseQuery(%q) match for %q: %.2g, want match: %t", test.query, test.s, score, test.wantMatch) +-// An Expectation is an expected property of the state of the LSP client. +-// The Check function reports whether the property is met. +-// +-// Expectations are combinators. By composing them, tests may express +-// complex expectations in terms of simpler ones. +-// +-// TODO(rfindley): as expectations are combined, it becomes harder to identify +-// why they failed. A better signature for Check would be +-// +-// func(State) (Verdict, string) +-// +-// returning a reason for the verdict that can be composed similarly to +-// descriptions. +-type Expectation struct { +- Check func(State) Verdict +- +- // Description holds a noun-phrase identifying what the expectation checks. +- // +- // TODO(rfindley): revisit existing descriptions to ensure they compose nicely. +- Description string +-} +- +-// OnceMet returns an Expectation that, once the precondition is met, asserts +-// that mustMeet is met. +-func OnceMet(precondition Expectation, mustMeets ...Expectation) Expectation { +- check := func(s State) Verdict { +- switch pre := precondition.Check(s); pre { +- case Unmeetable: +- return Unmeetable +- case Met: +- for _, mustMeet := range mustMeets { +- verdict := mustMeet.Check(s) +- if verdict != Met { +- return Unmeetable +- } +- } +- return Met +- default: +- return Unmet - } - } +- description := describeExpectations(mustMeets...) +- return Expectation{ +- Check: check, +- Description: fmt.Sprintf("once %q is met, must have:\n%s", precondition.Description, description), +- } -} - --func TestFiltererDisallow(t *testing.T) { -- tests := []struct { -- filters []string -- included []string -- excluded []string -- }{ -- { -- []string{"+**/c.go"}, -- []string{"a/c.go", "a/b/c.go"}, -- []string{}, -- }, -- { -- []string{"+a/**/c.go"}, -- []string{"a/b/c.go", "a/b/d/c.go", "a/c.go"}, -- []string{}, -- }, -- { -- []string{"-a/c.go", "+a/**"}, -- []string{"a/c.go"}, -- []string{}, -- }, -- { -- []string{"+a/**/c.go", "-**/c.go"}, -- []string{}, -- []string{"a/b/c.go"}, -- }, -- { -- []string{"+a/**/c.go", "-a/**"}, -- []string{}, -- []string{"a/b/c.go"}, -- }, -- { -- []string{"+**/c.go", "-a/**/c.go"}, -- []string{}, -- []string{"a/b/c.go"}, -- }, -- { -- []string{"+foobar", "-foo"}, -- []string{"foobar", "foobar/a"}, -- []string{"foo", "foo/a"}, -- }, -- { -- []string{"+", "-"}, -- []string{}, -- []string{"foobar", "foobar/a", "foo", "foo/a"}, -- }, -- { -- []string{"-", "+"}, -- []string{"foobar", "foobar/a", "foo", "foo/a"}, -- []string{}, -- }, -- { -- []string{"-a/**/b/**/c.go"}, -- []string{}, -- []string{"a/x/y/z/b/f/g/h/c.go"}, -- }, -- // tests for unsupported glob operators -- { -- []string{"+**/c.go", "-a/*/c.go"}, -- []string{"a/b/c.go"}, -- []string{}, -- }, -- { -- []string{"+**/c.go", "-a/?/c.go"}, -- []string{"a/b/c.go"}, -- []string{}, -- }, -- { -- []string{"-b"}, // should only filter paths prefixed with the "b" directory -- []string{"a/b/c.go", "bb"}, -- []string{"b/c/d.go", "b"}, -- }, +-func describeExpectations(expectations ...Expectation) string { +- var descriptions []string +- for _, e := range expectations { +- descriptions = append(descriptions, e.Description) - } +- return strings.Join(descriptions, "\n") +-} - -- for _, test := range tests { -- filterer := NewFilterer(test.filters) -- for _, inc := range test.included { -- if filterer.Disallow(inc) { -- t.Errorf("Filters %v excluded %v, wanted included", test.filters, inc) -- } +-// Not inverts the sense of an expectation: a met expectation is unmet, and an +-// unmet expectation is met. +-func Not(e Expectation) Expectation { +- check := func(s State) Verdict { +- switch v := e.Check(s); v { +- case Met: +- return Unmet +- case Unmet, Unmeetable: +- return Met +- default: +- panic(fmt.Sprintf("unexpected verdict %v", v)) - } +- } +- description := describeExpectations(e) +- return Expectation{ +- Check: check, +- Description: fmt.Sprintf("not: %s", description), +- } +-} - -- for _, exc := range test.excluded { -- if !filterer.Disallow(exc) { -- t.Errorf("Filters %v included %v, wanted excluded", test.filters, exc) +-// AnyOf returns an expectation that is satisfied when any of the given +-// expectations is met. +-func AnyOf(anyOf ...Expectation) Expectation { +- check := func(s State) Verdict { +- for _, e := range anyOf { +- verdict := e.Check(s) +- if verdict == Met { +- return Met - } - } +- return Unmet +- } +- description := describeExpectations(anyOf...) +- return Expectation{ +- Check: check, +- Description: fmt.Sprintf("Any of:\n%s", description), - } -} -diff -urN a/gopls/internal/lsp/source/xrefs/xrefs.go b/gopls/internal/lsp/source/xrefs/xrefs.go ---- a/gopls/internal/lsp/source/xrefs/xrefs.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/source/xrefs/xrefs.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,193 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --// Package xrefs defines the serializable index of cross-package --// references that is computed during type checking. +-// AllOf expects that all given expectations are met. -// --// See ../references.go for the 'references' query. --package xrefs -- --import ( -- "go/ast" -- "go/types" -- "sort" -- -- "golang.org/x/tools/go/types/objectpath" -- "golang.org/x/tools/gopls/internal/lsp/frob" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/internal/typeparams" --) -- --// Index constructs a serializable index of outbound cross-references --// for the specified type-checked package. --func Index(files []*source.ParsedGoFile, pkg *types.Package, info *types.Info) []byte { -- // pkgObjects maps each referenced package Q to a mapping: -- // from each referenced symbol in Q to the ordered list -- // of references to that symbol from this package. -- // A nil types.Object indicates a reference -- // to the package as a whole: an import. -- pkgObjects := make(map[*types.Package]map[types.Object]*gobObject) +-// TODO(rfindley): the problem with these types of combinators (OnceMet, AnyOf +-// and AllOf) is that we lose the information of *why* they failed: the Awaiter +-// is not smart enough to look inside. +-// +-// Refactor the API such that the Check function is responsible for explaining +-// why an expectation failed. This should allow us to significantly improve +-// test output: we won't need to summarize state at all, as the verdict +-// explanation itself should describe clearly why the expectation not met. +-func AllOf(allOf ...Expectation) Expectation { +- check := func(s State) Verdict { +- verdict := Met +- for _, e := range allOf { +- if v := e.Check(s); v > verdict { +- verdict = v +- } +- } +- return verdict +- } +- description := describeExpectations(allOf...) +- return Expectation{ +- Check: check, +- Description: fmt.Sprintf("All of:\n%s", description), +- } +-} - -- // getObjects returns the object-to-references mapping for a package. -- getObjects := func(pkg *types.Package) map[types.Object]*gobObject { -- objects, ok := pkgObjects[pkg] +-// ReadDiagnostics is an Expectation that stores the current diagnostics for +-// fileName in into, whenever it is evaluated. +-// +-// It can be used in combination with OnceMet or AfterChange to capture the +-// state of diagnostics when other expectations are satisfied. +-func ReadDiagnostics(fileName string, into *protocol.PublishDiagnosticsParams) Expectation { +- check := func(s State) Verdict { +- diags, ok := s.diagnostics[fileName] - if !ok { -- objects = make(map[types.Object]*gobObject) -- pkgObjects[pkg] = objects +- return Unmeetable - } -- return objects +- *into = *diags +- return Met - } +- return Expectation{ +- Check: check, +- Description: fmt.Sprintf("read diagnostics for %q", fileName), +- } +-} - -- objectpathFor := new(objectpath.Encoder).For -- -- for fileIndex, pgf := range files { +-// ReadAllDiagnostics is an expectation that stores all published diagnostics +-// into the provided map, whenever it is evaluated. +-// +-// It can be used in combination with OnceMet or AfterChange to capture the +-// state of diagnostics when other expectations are satisfied. +-func ReadAllDiagnostics(into *map[string]*protocol.PublishDiagnosticsParams) Expectation { +- check := func(s State) Verdict { +- allDiags := make(map[string]*protocol.PublishDiagnosticsParams) +- for name, diags := range s.diagnostics { +- allDiags[name] = diags +- } +- *into = allDiags +- return Met +- } +- return Expectation{ +- Check: check, +- Description: "read all diagnostics", +- } +-} - -- nodeRange := func(n ast.Node) protocol.Range { -- rng, err := pgf.PosRange(n.Pos(), n.End()) -- if err != nil { -- panic(err) // can't fail +-// ShownDocument asserts that the client has received a +-// ShowDocumentRequest for the given URI. +-func ShownDocument(uri protocol.URI) Expectation { +- check := func(s State) Verdict { +- for _, params := range s.showDocument { +- if params.URI == uri { +- return Met - } -- return rng - } +- return Unmet +- } +- return Expectation{ +- Check: check, +- Description: fmt.Sprintf("received window/showDocument for URI %s", uri), +- } +-} - -- ast.Inspect(pgf.File, func(n ast.Node) bool { -- switch n := n.(type) { -- case *ast.Ident: -- // Report a reference for each identifier that -- // uses a symbol exported from another package. -- // (The built-in error.Error method has no package.) -- if n.IsExported() { -- if obj, ok := info.Uses[n]; ok && -- obj.Pkg() != nil && -- obj.Pkg() != pkg { -- -- // For instantiations of generic methods, -- // use the generic object (see issue #60622). -- if fn, ok := obj.(*types.Func); ok { -- obj = typeparams.OriginMethod(fn) -- } -- -- objects := getObjects(obj.Pkg()) -- gobObj, ok := objects[obj] -- if !ok { -- path, err := objectpathFor(obj) -- if err != nil { -- // Capitalized but not exported -- // (e.g. local const/var/type). -- return true -- } -- gobObj = &gobObject{Path: path} -- objects[obj] = gobObj -- } -- -- gobObj.Refs = append(gobObj.Refs, gobRef{ -- FileIndex: fileIndex, -- Range: nodeRange(n), -- }) -- } -- } +-// ShownDocuments is an expectation that appends each showDocument +-// request into the provided slice, whenever it is evaluated. +-// +-// It can be used in combination with OnceMet or AfterChange to +-// capture the set of showDocument requests when other expectations +-// are satisfied. +-func ShownDocuments(into *[]*protocol.ShowDocumentParams) Expectation { +- check := func(s State) Verdict { +- *into = append(*into, s.showDocument...) +- return Met +- } +- return Expectation{ +- Check: check, +- Description: "read shown documents", +- } +-} - -- case *ast.ImportSpec: -- // Report a reference from each import path -- // string to the imported package. -- pkgname, ok := source.ImportedPkgName(info, n) -- if !ok { -- return true // missing import -- } -- objects := getObjects(pkgname.Imported()) -- gobObj, ok := objects[nil] -- if !ok { -- gobObj = &gobObject{Path: ""} -- objects[nil] = gobObj -- } -- gobObj.Refs = append(gobObj.Refs, gobRef{ -- FileIndex: fileIndex, -- Range: nodeRange(n.Path), -- }) +-// NoShownMessage asserts that the editor has not received a ShowMessage. +-func NoShownMessage(subString string) Expectation { +- check := func(s State) Verdict { +- for _, m := range s.showMessage { +- if strings.Contains(m.Message, subString) { +- return Unmeetable - } -- return true -- }) +- } +- return Met +- } +- return Expectation{ +- Check: check, +- Description: fmt.Sprintf("no ShowMessage received containing %q", subString), - } +-} - -- // Flatten the maps into slices, and sort for determinism. -- var packages []*gobPackage -- for p := range pkgObjects { -- objects := pkgObjects[p] -- gp := &gobPackage{ -- PkgPath: source.PackagePath(p.Path()), -- Objects: make([]*gobObject, 0, len(objects)), -- } -- for _, gobObj := range objects { -- gp.Objects = append(gp.Objects, gobObj) +-// ShownMessage asserts that the editor has received a ShowMessageRequest +-// containing the given substring. +-func ShownMessage(containing string) Expectation { +- check := func(s State) Verdict { +- for _, m := range s.showMessage { +- if strings.Contains(m.Message, containing) { +- return Met +- } - } -- sort.Slice(gp.Objects, func(i, j int) bool { -- return gp.Objects[i].Path < gp.Objects[j].Path -- }) -- packages = append(packages, gp) +- return Unmet +- } +- return Expectation{ +- Check: check, +- Description: fmt.Sprintf("received window/showMessage containing %q", containing), - } -- sort.Slice(packages, func(i, j int) bool { -- return packages[i].PkgPath < packages[j].PkgPath -- }) -- -- return packageCodec.Encode(packages) -} - --// Lookup searches a serialized index produced by an indexPackage --// operation on m, and returns the locations of all references from m --// to any object in the target set. Each object is denoted by a pair --// of (package path, object path). --func Lookup(m *source.Metadata, data []byte, targets map[source.PackagePath]map[objectpath.Path]struct{}) (locs []protocol.Location) { -- var packages []*gobPackage -- packageCodec.Decode(data, &packages) -- for _, gp := range packages { -- if objectSet, ok := targets[gp.PkgPath]; ok { -- for _, gobObj := range gp.Objects { -- if _, ok := objectSet[gobObj.Path]; ok { -- for _, ref := range gobObj.Refs { -- uri := m.CompiledGoFiles[ref.FileIndex] -- locs = append(locs, protocol.Location{ -- URI: protocol.URIFromSpanURI(uri), -- Range: ref.Range, -- }) -- } -- } +-// ShownMessageRequest asserts that the editor has received a +-// ShowMessageRequest with message matching the given regular expression. +-func ShownMessageRequest(messageRegexp string) Expectation { +- msgRE := regexp.MustCompile(messageRegexp) +- check := func(s State) Verdict { +- if len(s.showMessageRequest) == 0 { +- return Unmet +- } +- for _, m := range s.showMessageRequest { +- if msgRE.MatchString(m.Message) { +- return Met - } - } +- return Unmet +- } +- return Expectation{ +- Check: check, +- Description: fmt.Sprintf("ShowMessageRequest matching %q", messageRegexp), +- } +-} +- +-// DoneDiagnosingChanges expects that diagnostics are complete from common +-// change notifications: didOpen, didChange, didSave, didChangeWatchedFiles, +-// and didClose. +-// +-// This can be used when multiple notifications may have been sent, such as +-// when a didChange is immediately followed by a didSave. It is insufficient to +-// simply await NoOutstandingWork, because the LSP client has no control over +-// when the server starts processing a notification. Therefore, we must keep +-// track of +-func (e *Env) DoneDiagnosingChanges() Expectation { +- stats := e.Editor.Stats() +- statsBySource := map[server.ModificationSource]uint64{ +- server.FromDidOpen: stats.DidOpen, +- server.FromDidChange: stats.DidChange, +- server.FromDidSave: stats.DidSave, +- server.FromDidChangeWatchedFiles: stats.DidChangeWatchedFiles, +- server.FromDidClose: stats.DidClose, +- server.FromDidChangeConfiguration: stats.DidChangeConfiguration, - } - -- return locs --} +- var expected []server.ModificationSource +- for k, v := range statsBySource { +- if v > 0 { +- expected = append(expected, k) +- } +- } - --// -- serialized representation -- +- // Sort for stability. +- sort.Slice(expected, func(i, j int) bool { +- return expected[i] < expected[j] +- }) - --// The cross-reference index records the location of all references --// from one package to symbols defined in other packages --// (dependencies). It does not record within-package references. --// The index for package P consists of a list of gopPackage records, --// each enumerating references to symbols defined a single dependency, Q. +- var all []Expectation +- for _, source := range expected { +- all = append(all, CompletedWork(server.DiagnosticWorkTitle(source), statsBySource[source], true)) +- } - --// TODO(adonovan): opt: choose a more compact encoding. --// The gobRef.Range field is the obvious place to begin. +- return AllOf(all...) +-} - --// (The name says gob but in fact we use frob.) --var packageCodec = frob.CodecFor[[]*gobPackage]() +-// AfterChange expects that the given expectations will be met after all +-// state-changing notifications have been processed by the server. +-// +-// It awaits the completion of all anticipated work before checking the given +-// expectations. +-func (e *Env) AfterChange(expectations ...Expectation) { +- e.T.Helper() +- e.OnceMet( +- e.DoneDiagnosingChanges(), +- expectations..., +- ) +-} - --// A gobPackage records the set of outgoing references from the index --// package to symbols defined in a dependency package. --type gobPackage struct { -- PkgPath source.PackagePath // defining package (Q) -- Objects []*gobObject // set of Q objects referenced by P +-// DoneWithOpen expects all didOpen notifications currently sent by the editor +-// to be completely processed. +-func (e *Env) DoneWithOpen() Expectation { +- opens := e.Editor.Stats().DidOpen +- return CompletedWork(server.DiagnosticWorkTitle(server.FromDidOpen), opens, true) -} - --// A gobObject records all references to a particular symbol. --type gobObject struct { -- Path objectpath.Path // symbol name within package; "" => import of package itself -- Refs []gobRef // locations of references within P, in lexical order +-// StartedChange expects that the server has at least started processing all +-// didChange notifications sent from the client. +-func (e *Env) StartedChange() Expectation { +- changes := e.Editor.Stats().DidChange +- return StartedWork(server.DiagnosticWorkTitle(server.FromDidChange), changes) -} - --type gobRef struct { -- FileIndex int // index of enclosing file within P's CompiledGoFiles -- Range protocol.Range // source range of reference +-// DoneWithChange expects all didChange notifications currently sent by the +-// editor to be completely processed. +-func (e *Env) DoneWithChange() Expectation { +- changes := e.Editor.Stats().DidChange +- return CompletedWork(server.DiagnosticWorkTitle(server.FromDidChange), changes, true) -} -diff -urN a/gopls/internal/lsp/symbols.go b/gopls/internal/lsp/symbols.go ---- a/gopls/internal/lsp/symbols.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/symbols.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,60 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package lsp +-// DoneWithSave expects all didSave notifications currently sent by the editor +-// to be completely processed. +-func (e *Env) DoneWithSave() Expectation { +- saves := e.Editor.Stats().DidSave +- return CompletedWork(server.DiagnosticWorkTitle(server.FromDidSave), saves, true) +-} - --import ( -- "context" +-// StartedChangeWatchedFiles expects that the server has at least started +-// processing all didChangeWatchedFiles notifications sent from the client. +-func (e *Env) StartedChangeWatchedFiles() Expectation { +- changes := e.Editor.Stats().DidChangeWatchedFiles +- return StartedWork(server.DiagnosticWorkTitle(server.FromDidChangeWatchedFiles), changes) +-} - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/lsp/template" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/tag" --) +-// DoneWithChangeWatchedFiles expects all didChangeWatchedFiles notifications +-// currently sent by the editor to be completely processed. +-func (e *Env) DoneWithChangeWatchedFiles() Expectation { +- changes := e.Editor.Stats().DidChangeWatchedFiles +- return CompletedWork(server.DiagnosticWorkTitle(server.FromDidChangeWatchedFiles), changes, true) +-} - --func (s *Server) documentSymbol(ctx context.Context, params *protocol.DocumentSymbolParams) ([]interface{}, error) { -- ctx, done := event.Start(ctx, "lsp.Server.documentSymbol", tag.URI.Of(params.TextDocument.URI)) -- defer done() +-// DoneWithClose expects all didClose notifications currently sent by the +-// editor to be completely processed. +-func (e *Env) DoneWithClose() Expectation { +- changes := e.Editor.Stats().DidClose +- return CompletedWork(server.DiagnosticWorkTitle(server.FromDidClose), changes, true) +-} - -- snapshot, fh, ok, release, err := s.beginFileRequest(ctx, params.TextDocument.URI, source.UnknownKind) -- defer release() -- if !ok { -- return []interface{}{}, err -- } -- var docSymbols []protocol.DocumentSymbol -- switch snapshot.FileKind(fh) { -- case source.Tmpl: -- docSymbols, err = template.DocumentSymbols(snapshot, fh) -- case source.Go: -- docSymbols, err = source.DocumentSymbols(ctx, snapshot, fh) -- default: -- return []interface{}{}, nil +-// StartedWork expect a work item to have been started >= atLeast times. +-// +-// See CompletedWork. +-func StartedWork(title string, atLeast uint64) Expectation { +- check := func(s State) Verdict { +- if s.startedWork()[title] >= atLeast { +- return Met +- } +- return Unmet - } -- if err != nil { -- event.Error(ctx, "DocumentSymbols failed", err) -- return []interface{}{}, nil +- return Expectation{ +- Check: check, +- Description: fmt.Sprintf("started work %q at least %d time(s)", title, atLeast), - } -- // Convert the symbols to an interface array. -- // TODO: Remove this once the lsp deprecates SymbolInformation. -- symbols := make([]interface{}, len(docSymbols)) -- for i, s := range docSymbols { -- if snapshot.Options().HierarchicalDocumentSymbolSupport { -- symbols[i] = s -- continue -- } -- // If the client does not support hierarchical document symbols, then -- // we need to be backwards compatible for now and return SymbolInformation. -- symbols[i] = protocol.SymbolInformation{ -- Name: s.Name, -- Kind: s.Kind, -- Deprecated: s.Deprecated, -- Location: protocol.Location{ -- URI: params.TextDocument.URI, -- Range: s.Range, -- }, +-} +- +-// CompletedWork expects a work item to have been completed >= atLeast times. +-// +-// Since the Progress API doesn't include any hidden metadata, we must use the +-// progress notification title to identify the work we expect to be completed. +-func CompletedWork(title string, count uint64, atLeast bool) Expectation { +- check := func(s State) Verdict { +- completed := s.completedWork() +- if completed[title] == count || atLeast && completed[title] > count { +- return Met - } +- return Unmet +- } +- desc := fmt.Sprintf("completed work %q %v times", title, count) +- if atLeast { +- desc = fmt.Sprintf("completed work %q at least %d time(s)", title, count) +- } +- return Expectation{ +- Check: check, +- Description: desc, - } -- return symbols, nil -} -diff -urN a/gopls/internal/lsp/template/completion.go b/gopls/internal/lsp/template/completion.go ---- a/gopls/internal/lsp/template/completion.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/template/completion.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,287 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package template -- --import ( -- "bytes" -- "context" -- "fmt" -- "go/scanner" -- "go/token" -- "strings" -- -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" --) - --// information needed for completion --type completer struct { -- p *Parsed -- pos protocol.Position -- offset int // offset of the start of the Token -- ctx protocol.CompletionContext -- syms map[string]symbol +-type WorkStatus struct { +- // Last seen message from either `begin` or `report` progress. +- Msg string +- // Message sent with `end` progress message. +- EndMsg string -} - --func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, pos protocol.Position, context protocol.CompletionContext) (*protocol.CompletionList, error) { -- all := New(snapshot.Templates()) -- var start int // the beginning of the Token (completed or not) -- syms := make(map[string]symbol) -- var p *Parsed -- for fn, fc := range all.files { -- // collect symbols from all template files -- filterSyms(syms, fc.symbols) -- if fn.Filename() != fh.URI().Filename() { -- continue +-// CompletedProgress expects that workDone progress is complete for the given +-// progress token. When non-nil WorkStatus is provided, it will be filled +-// when the expectation is met. +-// +-// If the token is not a progress token that the client has seen, this +-// expectation is Unmeetable. +-func CompletedProgress(token protocol.ProgressToken, into *WorkStatus) Expectation { +- check := func(s State) Verdict { +- work, ok := s.work[token] +- if !ok { +- return Unmeetable // TODO(rfindley): refactor to allow the verdict to explain this result - } -- if start = inTemplate(fc, pos); start == -1 { -- return nil, nil +- if work.complete { +- if into != nil { +- into.Msg = work.msg +- into.EndMsg = work.endMsg +- } +- return Met - } -- p = fc -- } -- if p == nil { -- // this cannot happen unless the search missed a template file -- return nil, fmt.Errorf("%s not found", fh.FileIdentity().URI.Filename()) +- return Unmet - } -- c := completer{ -- p: p, -- pos: pos, -- offset: start + len(Left), -- ctx: context, -- syms: syms, +- desc := fmt.Sprintf("completed work for token %v", token) +- return Expectation{ +- Check: check, +- Description: desc, - } -- return c.complete() -} - --func filterSyms(syms map[string]symbol, ns []symbol) { -- for _, xsym := range ns { -- switch xsym.kind { -- case protocol.Method, protocol.Package, protocol.Boolean, protocol.Namespace, -- protocol.Function: -- syms[xsym.name] = xsym // we don't care which symbol we get -- case protocol.Variable: -- if xsym.name != "dot" { -- syms[xsym.name] = xsym +-// OutstandingWork expects a work item to be outstanding. The given title must +-// be an exact match, whereas the given msg must only be contained in the work +-// item's message. +-func OutstandingWork(title, msg string) Expectation { +- check := func(s State) Verdict { +- for _, work := range s.work { +- if work.complete { +- continue - } -- case protocol.Constant: -- if xsym.name == "nil" { -- syms[xsym.name] = xsym +- if work.title == title && strings.Contains(work.msg, msg) { +- return Met - } - } +- return Unmet +- } +- return Expectation{ +- Check: check, +- Description: fmt.Sprintf("outstanding work: %q containing %q", title, msg), - } -} - --// return the starting position of the enclosing token, or -1 if none --func inTemplate(fc *Parsed, pos protocol.Position) int { -- // pos is the pos-th character. if the cursor is at the beginning -- // of the file, pos is 0. That is, we've only seen characters before pos -- // 1. pos might be in a Token, return tk.Start -- // 2. pos might be after an elided but before a Token, return elided -- // 3. return -1 for false -- offset := fc.FromPosition(pos) -- // this could be a binary search, as the tokens are ordered -- for _, tk := range fc.tokens { -- if tk.Start < offset && offset <= tk.End { -- return tk.Start +-// NoOutstandingWork asserts that there is no work initiated using the LSP +-// $/progress API that has not completed. +-// +-// If non-nil, the ignore func is used to ignore certain work items for the +-// purpose of this check. +-// +-// TODO(rfindley): consider refactoring to treat outstanding work the same way +-// we treat diagnostics: with an algebra of filters. +-func NoOutstandingWork(ignore func(title, msg string) bool) Expectation { +- check := func(s State) Verdict { +- for _, w := range s.work { +- if w.complete { +- continue +- } +- if w.title == "" { +- // A token that has been created but not yet used. +- // +- // TODO(rfindley): this should be separated in the data model: until +- // the "begin" notification, work should not be in progress. +- continue +- } +- if ignore != nil && ignore(w.title, w.msg) { +- continue +- } +- return Unmet - } +- return Met - } -- for _, x := range fc.elided { -- if x > offset { -- // fc.elided is sorted -- break -- } -- // If the interval [x,offset] does not contain Left or Right -- // then provide completions. (do we need the test for Right?) -- if !bytes.Contains(fc.buf[x:offset], []byte(Left)) && !bytes.Contains(fc.buf[x:offset], []byte(Right)) { -- return x -- } +- return Expectation{ +- Check: check, +- Description: "no outstanding work", - } -- return -1 -} - --var ( -- keywords = []string{"if", "with", "else", "block", "range", "template", "end}}", "end"} -- globals = []string{"and", "call", "html", "index", "slice", "js", "len", "not", "or", -- "urlquery", "printf", "println", "print", "eq", "ne", "le", "lt", "ge", "gt"} --) +-// IgnoreTelemetryPromptWork may be used in conjunction with NoOutStandingWork +-// to ignore the telemetry prompt. +-func IgnoreTelemetryPromptWork(title, msg string) bool { +- return title == server.TelemetryPromptWorkTitle +-} - --// find the completions. start is the offset of either the Token enclosing pos, or where --// the incomplete token starts. --// The error return is always nil. --func (c *completer) complete() (*protocol.CompletionList, error) { -- ans := &protocol.CompletionList{IsIncomplete: true, Items: []protocol.CompletionItem{}} -- start := c.p.FromPosition(c.pos) -- sofar := c.p.buf[c.offset:start] -- if len(sofar) == 0 || sofar[len(sofar)-1] == ' ' || sofar[len(sofar)-1] == '\t' { -- return ans, nil -- } -- // sofar could be parsed by either c.analyzer() or scan(). The latter is precise -- // and slower, but fast enough -- words := scan(sofar) -- // 1. if pattern starts $, show variables -- // 2. if pattern starts ., show methods (and . by itself?) -- // 3. if len(words) == 1, show firstWords (but if it were a |, show functions and globals) -- // 4. ...? (parenthetical expressions, arguments, ...) (packages, namespaces, nil?) -- if len(words) == 0 { -- return nil, nil // if this happens, why were we called? +-// NoErrorLogs asserts that the client has not received any log messages of +-// error severity. +-func NoErrorLogs() Expectation { +- return NoLogMatching(protocol.Error, "") +-} +- +-// LogMatching asserts that the client has received a log message +-// of type typ matching the regexp re a certain number of times. +-// +-// The count argument specifies the expected number of matching logs. If +-// atLeast is set, this is a lower bound, otherwise there must be exactly count +-// matching logs. +-// +-// Logs are asynchronous to other LSP messages, so this expectation should not +-// be used with combinators such as OnceMet or AfterChange that assert on +-// ordering with respect to other operations. +-func LogMatching(typ protocol.MessageType, re string, count int, atLeast bool) Expectation { +- rec, err := regexp.Compile(re) +- if err != nil { +- panic(err) - } -- pattern := string(words[len(words)-1]) -- if pattern[0] == '$' { -- // should we also return a raw "$"? -- for _, s := range c.syms { -- if s.kind == protocol.Variable && weakMatch(s.name, pattern) > 0 { -- ans.Items = append(ans.Items, protocol.CompletionItem{ -- Label: s.name, -- Kind: protocol.VariableCompletion, -- Detail: "Variable", -- }) +- check := func(state State) Verdict { +- var found int +- for _, msg := range state.logs { +- if msg.Type == typ && rec.Match([]byte(msg.Message)) { +- found++ - } - } -- return ans, nil +- // Check for an exact or "at least" match. +- if found == count || (found >= count && atLeast) { +- return Met +- } +- // If we require an exact count, and have received more than expected, the +- // expectation can never be met. +- if found > count && !atLeast { +- return Unmeetable +- } +- return Unmet - } -- if pattern[0] == '.' { -- for _, s := range c.syms { -- if s.kind == protocol.Method && weakMatch("."+s.name, pattern) > 0 { -- ans.Items = append(ans.Items, protocol.CompletionItem{ -- Label: s.name, -- Kind: protocol.MethodCompletion, -- Detail: "Method/member", -- }) -- } +- desc := fmt.Sprintf("log message matching %q expected %v times", re, count) +- if atLeast { +- desc = fmt.Sprintf("log message matching %q expected at least %v times", re, count) +- } +- return Expectation{ +- Check: check, +- Description: desc, +- } +-} +- +-// NoLogMatching asserts that the client has not received a log message +-// of type typ matching the regexp re. If re is an empty string, any log +-// message is considered a match. +-func NoLogMatching(typ protocol.MessageType, re string) Expectation { +- var r *regexp.Regexp +- if re != "" { +- var err error +- r, err = regexp.Compile(re) +- if err != nil { +- panic(err) - } -- return ans, nil - } -- // could we get completion attempts in strings or numbers, and if so, do we care? -- // globals -- for _, kw := range globals { -- if weakMatch(kw, string(pattern)) != 0 { -- ans.Items = append(ans.Items, protocol.CompletionItem{ -- Label: kw, -- Kind: protocol.KeywordCompletion, -- Detail: "Function", -- }) +- check := func(state State) Verdict { +- for _, msg := range state.logs { +- if msg.Type != typ { +- continue +- } +- if r == nil || r.Match([]byte(msg.Message)) { +- return Unmeetable +- } - } +- return Met - } -- // and functions -- for _, s := range c.syms { -- if s.kind == protocol.Function && weakMatch(s.name, pattern) != 0 { -- ans.Items = append(ans.Items, protocol.CompletionItem{ -- Label: s.name, -- Kind: protocol.FunctionCompletion, -- Detail: "Function", -- }) +- return Expectation{ +- Check: check, +- Description: fmt.Sprintf("no log message matching %q", re), +- } +-} +- +-// FileWatchMatching expects that a file registration matches re. +-func FileWatchMatching(re string) Expectation { +- return Expectation{ +- Check: checkFileWatch(re, Met, Unmet), +- Description: fmt.Sprintf("file watch matching %q", re), +- } +-} +- +-// NoFileWatchMatching expects that no file registration matches re. +-func NoFileWatchMatching(re string) Expectation { +- return Expectation{ +- Check: checkFileWatch(re, Unmet, Met), +- Description: fmt.Sprintf("no file watch matching %q", re), +- } +-} +- +-func checkFileWatch(re string, onMatch, onNoMatch Verdict) func(State) Verdict { +- rec := regexp.MustCompile(re) +- return func(s State) Verdict { +- r := s.registeredCapabilities["workspace/didChangeWatchedFiles"] +- watchers := jsonProperty(r.RegisterOptions, "watchers").([]interface{}) +- for _, watcher := range watchers { +- pattern := jsonProperty(watcher, "globPattern").(string) +- if rec.MatchString(pattern) { +- return onMatch +- } - } +- return onNoMatch - } -- // keywords if we're at the beginning -- if len(words) <= 1 || len(words[len(words)-2]) == 1 && words[len(words)-2][0] == '|' { -- for _, kw := range keywords { -- if weakMatch(kw, string(pattern)) != 0 { -- ans.Items = append(ans.Items, protocol.CompletionItem{ -- Label: kw, -- Kind: protocol.KeywordCompletion, -- Detail: "keyword", -- }) +-} +- +-// jsonProperty extracts a value from a path of JSON property names, assuming +-// the default encoding/json unmarshaling to the empty interface (i.e.: that +-// JSON objects are unmarshalled as map[string]interface{}) +-// +-// For example, if obj is unmarshalled from the following json: +-// +-// { +-// "foo": { "bar": 3 } +-// } +-// +-// Then jsonProperty(obj, "foo", "bar") will be 3. +-func jsonProperty(obj interface{}, path ...string) interface{} { +- if len(path) == 0 || obj == nil { +- return obj +- } +- m := obj.(map[string]interface{}) +- return jsonProperty(m[path[0]], path[1:]...) +-} +- +-// Diagnostics asserts that there is at least one diagnostic matching the given +-// filters. +-func Diagnostics(filters ...DiagnosticFilter) Expectation { +- check := func(s State) Verdict { +- diags := flattenDiagnostics(s) +- for _, filter := range filters { +- var filtered []flatDiagnostic +- for _, d := range diags { +- if filter.check(d.name, d.diag) { +- filtered = append(filtered, d) +- } +- } +- if len(filtered) == 0 { +- // TODO(rfindley): if/when expectations describe their own failure, we +- // can provide more useful information here as to which filter caused +- // the failure. +- return Unmet - } +- diags = filtered - } +- return Met +- } +- var descs []string +- for _, filter := range filters { +- descs = append(descs, filter.desc) +- } +- return Expectation{ +- Check: check, +- Description: "any diagnostics " + strings.Join(descs, ", "), - } -- return ans, nil -} - --// someday think about comments, strings, backslashes, etc --// this would repeat some of the template parsing, but because the user is typing --// there may be no parse tree here. --// (go/scanner will report 2 tokens for $a, as $ is not a legal go identifier character) --// (go/scanner is about 2.7 times more expensive) --func (c *completer) analyze(buf []byte) [][]byte { -- // we want to split on whitespace and before dots -- var working []byte -- var ans [][]byte -- for _, ch := range buf { -- if ch == '.' && len(working) > 0 { -- ans = append(ans, working) -- working = []byte{'.'} -- continue -- } -- if ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' { -- if len(working) > 0 { -- ans = append(ans, working) -- working = []byte{} -- continue +-// NoDiagnostics asserts that there are no diagnostics matching the given +-// filters. Notably, if no filters are supplied this assertion checks that +-// there are no diagnostics at all, for any file. +-func NoDiagnostics(filters ...DiagnosticFilter) Expectation { +- check := func(s State) Verdict { +- diags := flattenDiagnostics(s) +- for _, filter := range filters { +- var filtered []flatDiagnostic +- for _, d := range diags { +- if filter.check(d.name, d.diag) { +- filtered = append(filtered, d) +- } - } +- diags = filtered +- } +- if len(diags) > 0 { +- return Unmet - } -- working = append(working, ch) +- return Met - } -- if len(working) > 0 { -- ans = append(ans, working) +- var descs []string +- for _, filter := range filters { +- descs = append(descs, filter.desc) - } -- ch := buf[len(buf)-1] -- if ch == ' ' || ch == '\t' { -- // avoid completing on whitespace -- ans = append(ans, []byte{ch}) +- return Expectation{ +- Check: check, +- Description: "no diagnostics " + strings.Join(descs, ", "), - } -- return ans -} - --// version of c.analyze that uses go/scanner. --func scan(buf []byte) []string { -- fset := token.NewFileSet() -- fp := fset.AddFile("", -1, len(buf)) -- var sc scanner.Scanner -- sc.Init(fp, buf, func(pos token.Position, msg string) {}, scanner.ScanComments) -- ans := make([]string, 0, 10) // preallocating gives a measurable savings -- for { -- _, tok, lit := sc.Scan() // tok is an int -- if tok == token.EOF { -- break // done -- } else if tok == token.SEMICOLON && lit == "\n" { -- continue // don't care, but probably can't happen -- } else if tok == token.PERIOD { -- ans = append(ans, ".") // lit is empty -- } else if tok == token.IDENT && len(ans) > 0 && ans[len(ans)-1] == "." { -- ans[len(ans)-1] = "." + lit -- } else if tok == token.IDENT && len(ans) > 0 && ans[len(ans)-1] == "$" { -- ans[len(ans)-1] = "$" + lit -- } else if lit != "" { -- ans = append(ans, lit) -- } -- } -- return ans +-type flatDiagnostic struct { +- name string +- diag protocol.Diagnostic -} - --// pattern is what the user has typed --func weakMatch(choice, pattern string) float64 { -- lower := strings.ToLower(choice) -- // for now, use only lower-case everywhere -- pattern = strings.ToLower(pattern) -- // The first char has to match -- if pattern[0] != lower[0] { -- return 0 -- } -- // If they start with ., then the second char has to match -- from := 1 -- if pattern[0] == '.' { -- if len(pattern) < 2 { -- return 1 // pattern just a ., so it matches -- } -- if pattern[1] != lower[1] { -- return 0 -- } -- from = 2 -- } -- // check that all the characters of pattern occur as a subsequence of choice -- i, j := from, from -- for ; i < len(lower) && j < len(pattern); j++ { -- if pattern[j] == lower[i] { -- i++ -- if i >= len(lower) { -- return 0 -- } +-func flattenDiagnostics(state State) []flatDiagnostic { +- var result []flatDiagnostic +- for name, diags := range state.diagnostics { +- for _, diag := range diags.Diagnostics { +- result = append(result, flatDiagnostic{name, diag}) - } - } -- if j < len(pattern) { -- return 0 -- } -- return 1 +- return result -} -diff -urN a/gopls/internal/lsp/template/completion_test.go b/gopls/internal/lsp/template/completion_test.go ---- a/gopls/internal/lsp/template/completion_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/template/completion_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,102 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package template -- --import ( -- "log" -- "sort" -- "strings" -- "testing" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" --) +-// -- Diagnostic filters -- - --func init() { -- log.SetFlags(log.Lshortfile) +-// A DiagnosticFilter filters the set of diagnostics, for assertion with +-// Diagnostics or NoDiagnostics. +-type DiagnosticFilter struct { +- desc string +- check func(name string, _ protocol.Diagnostic) bool -} - --type tparse struct { -- marked string // ^ shows where to ask for completions. (The user just typed the following character.) -- wanted []string // expected completions +-// ForFile filters to diagnostics matching the sandbox-relative file name. +-func ForFile(name string) DiagnosticFilter { +- return DiagnosticFilter{ +- desc: fmt.Sprintf("for file %q", name), +- check: func(diagName string, _ protocol.Diagnostic) bool { +- return diagName == name +- }, +- } -} - --// Test completions in templates that parse enough (if completion needs symbols) --// Seen characters up to the ^ --func TestParsed(t *testing.T) { -- var tests = []tparse{ -- {"{{x}}{{12. xx^", nil}, // https://github.com/golang/go/issues/50430 -- {`<table class="chroma" data-new-comment-url="{{if $.PageIsPullFiles}}{{$.Issue.HTMLURL}}/files/reviews/new_comment{{else}}{{$.CommitHTML}}/new_comment^{{end}}">`, nil}, -- {"{{i^f}}", []string{"index", "if"}}, -- {"{{if .}}{{e^ {{end}}", []string{"eq", "end}}", "else", "end"}}, -- {"{{foo}}{{f^", []string{"foo"}}, -- {"{{$^}}", []string{"$"}}, -- {"{{$x:=4}}{{$^", []string{"$x"}}, -- {"{{$x:=4}}{{$ ^ ", []string{}}, -- {"{{len .Modified}}{{.^Mo", []string{"Modified"}}, -- {"{{len .Modified}}{{.mf^", []string{"Modified"}}, -- {"{{$^ }}", []string{"$"}}, -- {"{{$a =3}}{{$^", []string{"$a"}}, -- // .two is not good here: fix someday -- {`{{.Modified}}{{.^{{if $.one.two}}xxx{{end}}`, []string{"Modified", "one", "two"}}, -- {`{{.Modified}}{{.o^{{if $.one.two}}xxx{{end}}`, []string{"one"}}, -- {"{{.Modiifed}}{{.one.t^{{if $.one.two}}xxx{{end}}", []string{"two"}}, -- {`{{block "foo" .}}{{i^`, []string{"index", "if"}}, -- {"{{in^{{Internal}}", []string{"index", "Internal", "if"}}, -- // simple number has no completions -- {"{{4^e", []string{}}, -- // simple string has no completions -- {"{{`e^", []string{}}, -- {"{{`No i^", []string{}}, // example of why go/scanner is used -- {"{{xavier}}{{12. x^", []string{"xavier"}}, +-// FromSource filters to diagnostics matching the given diagnostics source. +-func FromSource(source string) DiagnosticFilter { +- return DiagnosticFilter{ +- desc: fmt.Sprintf("with source %q", source), +- check: func(_ string, d protocol.Diagnostic) bool { +- return d.Source == source +- }, - } -- for _, tx := range tests { -- c := testCompleter(t, tx) -- var v []string -- if c != nil { -- ans, _ := c.complete() -- for _, a := range ans.Items { -- v = append(v, a.Label) -- } -- } -- if len(v) != len(tx.wanted) { -- t.Errorf("%q: got %q, wanted %q %d,%d", tx.marked, v, tx.wanted, len(v), len(tx.wanted)) -- continue -- } -- sort.Strings(tx.wanted) -- sort.Strings(v) -- for i := 0; i < len(v); i++ { -- if tx.wanted[i] != v[i] { -- t.Errorf("%q at %d: got %v, wanted %v", tx.marked, i, v, tx.wanted) -- break -- } -- } +-} +- +-// AtRegexp filters to diagnostics in the file with sandbox-relative path name, +-// at the first position matching the given regexp pattern. +-// +-// TODO(rfindley): pass in the editor to expectations, so that they may depend +-// on editor state and AtRegexp can be a function rather than a method. +-func (e *Env) AtRegexp(name, pattern string) DiagnosticFilter { +- loc := e.RegexpSearch(name, pattern) +- return DiagnosticFilter{ +- desc: fmt.Sprintf("at the first position (%v) matching %#q in %q", loc.Range.Start, pattern, name), +- check: func(diagName string, d protocol.Diagnostic) bool { +- return diagName == name && d.Range.Start == loc.Range.Start +- }, - } -} - --func testCompleter(t *testing.T, tx tparse) *completer { -- t.Helper() -- // seen chars up to ^ -- col := strings.Index(tx.marked, "^") -- buf := strings.Replace(tx.marked, "^", "", 1) -- p := parseBuffer([]byte(buf)) -- pos := protocol.Position{Line: 0, Character: uint32(col)} -- if p.ParseErr != nil { -- log.Printf("%q: %v", tx.marked, p.ParseErr) +-// AtPosition filters to diagnostics at location name:line:character, for a +-// sandbox-relative path name. +-// +-// Line and character are 0-based, and character measures UTF-16 codes. +-// +-// Note: prefer the more readable AtRegexp. +-func AtPosition(name string, line, character uint32) DiagnosticFilter { +- pos := protocol.Position{Line: line, Character: character} +- return DiagnosticFilter{ +- desc: fmt.Sprintf("at %s:%d:%d", name, line, character), +- check: func(diagName string, d protocol.Diagnostic) bool { +- return diagName == name && d.Range.Start == pos +- }, - } -- offset := inTemplate(p, pos) -- if offset == -1 { -- return nil +-} +- +-// WithMessage filters to diagnostics whose message contains the given +-// substring. +-func WithMessage(substring string) DiagnosticFilter { +- return DiagnosticFilter{ +- desc: fmt.Sprintf("with message containing %q", substring), +- check: func(_ string, d protocol.Diagnostic) bool { +- return strings.Contains(d.Message, substring) +- }, - } -- syms := make(map[string]symbol) -- filterSyms(syms, p.symbols) -- c := &completer{ -- p: p, -- pos: protocol.Position{Line: 0, Character: uint32(col)}, -- offset: offset + len(Left), -- ctx: protocol.CompletionContext{TriggerKind: protocol.Invoked}, -- syms: syms, +-} +- +-// WithSeverityTags filters to diagnostics whose severity and tags match +-// the given expectation. +-func WithSeverityTags(diagName string, severity protocol.DiagnosticSeverity, tags []protocol.DiagnosticTag) DiagnosticFilter { +- return DiagnosticFilter{ +- desc: fmt.Sprintf("with diagnostic %q with severity %q and tag %#q", diagName, severity, tags), +- check: func(_ string, d protocol.Diagnostic) bool { +- return d.Source == diagName && d.Severity == severity && cmp.Equal(d.Tags, tags) +- }, - } -- return c -} -diff -urN a/gopls/internal/lsp/template/highlight.go b/gopls/internal/lsp/template/highlight.go ---- a/gopls/internal/lsp/template/highlight.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/template/highlight.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,96 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/test/integration/fake/client.go b/gopls/internal/test/integration/fake/client.go +--- a/gopls/internal/test/integration/fake/client.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/fake/client.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,208 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package template +-package fake - -import ( - "context" +- "encoding/json" - "fmt" -- "regexp" +- "path" +- "path/filepath" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/test/integration/fake/glob" -) - --func Highlight(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, loc protocol.Position) ([]protocol.DocumentHighlight, error) { -- buf, err := fh.Content() -- if err != nil { -- return nil, err -- } -- p := parseBuffer(buf) -- pos := p.FromPosition(loc) -- var ans []protocol.DocumentHighlight -- if p.ParseErr == nil { -- for _, s := range p.symbols { -- if s.start <= pos && pos < s.start+s.length { -- return markSymbols(p, s) -- } -- } -- } -- // these tokens exist whether or not there was a parse error -- // (symbols require a successful parse) -- for _, tok := range p.tokens { -- if tok.Start <= pos && pos < tok.End { -- wordAt := findWordAt(p, pos) -- if len(wordAt) > 0 { -- return markWordInToken(p, wordAt) -- } -- } -- } -- // find the 'word' at pos, etc: someday -- // until then we get the default action, which doesn't respect word boundaries -- return ans, nil +-// ClientHooks are a set of optional hooks called during handling of +-// the corresponding client method (see protocol.Client for the +-// LSP server-to-client RPCs) in order to make test expectations +-// awaitable. +-type ClientHooks struct { +- OnLogMessage func(context.Context, *protocol.LogMessageParams) error +- OnDiagnostics func(context.Context, *protocol.PublishDiagnosticsParams) error +- OnWorkDoneProgressCreate func(context.Context, *protocol.WorkDoneProgressCreateParams) error +- OnProgress func(context.Context, *protocol.ProgressParams) error +- OnShowDocument func(context.Context, *protocol.ShowDocumentParams) error +- OnShowMessage func(context.Context, *protocol.ShowMessageParams) error +- OnShowMessageRequest func(context.Context, *protocol.ShowMessageRequestParams) error +- OnRegisterCapability func(context.Context, *protocol.RegistrationParams) error +- OnUnregisterCapability func(context.Context, *protocol.UnregistrationParams) error +- OnApplyEdit func(context.Context, *protocol.ApplyWorkspaceEditParams) error -} - --func markSymbols(p *Parsed, sym symbol) ([]protocol.DocumentHighlight, error) { -- var ans []protocol.DocumentHighlight -- for _, s := range p.symbols { -- if s.name == sym.name { -- kind := protocol.Read -- if s.vardef { -- kind = protocol.Write -- } -- ans = append(ans, protocol.DocumentHighlight{ -- Range: p.Range(s.start, s.length), -- Kind: kind, -- }) -- } -- } -- return ans, nil +-// Client is an implementation of the [protocol.Client] interface +-// based on the test's fake [Editor]. It mostly delegates +-// functionality to hooks that can be configured by tests. +-type Client struct { +- editor *Editor +- hooks ClientHooks +- skipApplyEdits bool // don't apply edits from ApplyEdit downcalls to Editor -} - --// A token is {{...}}, and this marks words in the token that equal the give word --func markWordInToken(p *Parsed, wordAt string) ([]protocol.DocumentHighlight, error) { -- var ans []protocol.DocumentHighlight -- pat, err := regexp.Compile(fmt.Sprintf(`\b%s\b`, wordAt)) -- if err != nil { -- return nil, fmt.Errorf("%q: unmatchable word (%v)", wordAt, err) -- } -- for _, tok := range p.tokens { -- got := pat.FindAllIndex(p.buf[tok.Start:tok.End], -1) -- for i := 0; i < len(got); i++ { -- ans = append(ans, protocol.DocumentHighlight{ -- Range: p.Range(got[i][0], got[i][1]-got[i][0]), -- Kind: protocol.Text, -- }) -- } +-func (c *Client) CodeLensRefresh(context.Context) error { return nil } +- +-func (c *Client) InlayHintRefresh(context.Context) error { return nil } +- +-func (c *Client) DiagnosticRefresh(context.Context) error { return nil } +- +-func (c *Client) FoldingRangeRefresh(context.Context) error { return nil } +- +-func (c *Client) InlineValueRefresh(context.Context) error { return nil } +- +-func (c *Client) SemanticTokensRefresh(context.Context) error { return nil } +- +-func (c *Client) LogTrace(context.Context, *protocol.LogTraceParams) error { return nil } +- +-func (c *Client) ShowMessage(ctx context.Context, params *protocol.ShowMessageParams) error { +- if c.hooks.OnShowMessage != nil { +- return c.hooks.OnShowMessage(ctx, params) - } -- return ans, nil +- return nil -} - --var wordRe = regexp.MustCompile(`[$]?\w+$`) --var moreRe = regexp.MustCompile(`^[$]?\w+`) -- --// findWordAt finds the word the cursor is in (meaning in or just before) --func findWordAt(p *Parsed, pos int) string { -- if pos >= len(p.buf) { -- return "" // can't happen, as we are called with pos < tok.End +-func (c *Client) ShowMessageRequest(ctx context.Context, params *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error) { +- if c.hooks.OnShowMessageRequest != nil { +- if err := c.hooks.OnShowMessageRequest(ctx, params); err != nil { +- return nil, err +- } - } -- after := moreRe.Find(p.buf[pos:]) -- if len(after) == 0 { -- return "" // end of the word +- if c.editor.config.MessageResponder != nil { +- return c.editor.config.MessageResponder(params) - } -- got := wordRe.Find(p.buf[:pos+len(after)]) -- return string(got) +- return nil, nil // don't choose, which is effectively dismissing the message -} -diff -urN a/gopls/internal/lsp/template/implementations.go b/gopls/internal/lsp/template/implementations.go ---- a/gopls/internal/lsp/template/implementations.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/template/implementations.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,189 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package template +-func (c *Client) LogMessage(ctx context.Context, params *protocol.LogMessageParams) error { +- if c.hooks.OnLogMessage != nil { +- return c.hooks.OnLogMessage(ctx, params) +- } +- return nil +-} - --import ( -- "context" -- "fmt" -- "regexp" -- "strconv" -- "time" +-func (c *Client) Event(ctx context.Context, event *interface{}) error { +- return nil +-} - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" --) +-func (c *Client) PublishDiagnostics(ctx context.Context, params *protocol.PublishDiagnosticsParams) error { +- if c.hooks.OnDiagnostics != nil { +- return c.hooks.OnDiagnostics(ctx, params) +- } +- return nil +-} - --// line number (1-based) and message --var errRe = regexp.MustCompile(`template.*:(\d+): (.*)`) +-func (c *Client) WorkspaceFolders(context.Context) ([]protocol.WorkspaceFolder, error) { +- return []protocol.WorkspaceFolder{}, nil +-} - --// Diagnose returns parse errors. There is only one. --// The errors are not always helpful. For instance { {end}} --// will likely point to the end of the file. --func Diagnose(f source.FileHandle) []*source.Diagnostic { -- // no need for skipTemplate check, as Diagnose is called on the -- // snapshot's template files -- buf, err := f.Content() -- if err != nil { -- // Is a Diagnostic with no Range useful? event.Error also? -- msg := fmt.Sprintf("failed to read %s (%v)", f.URI().Filename(), err) -- d := source.Diagnostic{Message: msg, Severity: protocol.SeverityError, URI: f.URI(), -- Source: source.TemplateError} -- return []*source.Diagnostic{&d} -- } -- p := parseBuffer(buf) -- if p.ParseErr == nil { -- return nil +-func (c *Client) Configuration(_ context.Context, p *protocol.ParamConfiguration) ([]interface{}, error) { +- results := make([]interface{}, len(p.Items)) +- for i, item := range p.Items { +- if item.ScopeURI != nil && *item.ScopeURI == "" { +- return nil, fmt.Errorf(`malformed ScopeURI ""`) +- } +- if item.Section == "gopls" { +- config := c.editor.Config() +- results[i] = makeSettings(c.editor.sandbox, config, item.ScopeURI) +- } - } -- unknownError := func(msg string) []*source.Diagnostic { -- s := fmt.Sprintf("malformed template error %q: %s", p.ParseErr.Error(), msg) -- d := source.Diagnostic{ -- Message: s, Severity: protocol.SeverityError, Range: p.Range(p.nls[0], 1), -- URI: f.URI(), Source: source.TemplateError} -- return []*source.Diagnostic{&d} +- return results, nil +-} +- +-func (c *Client) RegisterCapability(ctx context.Context, params *protocol.RegistrationParams) error { +- if c.hooks.OnRegisterCapability != nil { +- if err := c.hooks.OnRegisterCapability(ctx, params); err != nil { +- return err +- } - } -- // errors look like `template: :40: unexpected "}" in operand` -- // so the string needs to be parsed -- matches := errRe.FindStringSubmatch(p.ParseErr.Error()) -- if len(matches) != 3 { -- msg := fmt.Sprintf("expected 3 matches, got %d (%v)", len(matches), matches) -- return unknownError(msg) +- // Update file watching patterns. +- // +- // TODO(rfindley): We could verify more here, like verify that the +- // registration ID is distinct, and that the capability is not currently +- // registered. +- for _, registration := range params.Registrations { +- if registration.Method == "workspace/didChangeWatchedFiles" { +- // Marshal and unmarshal to interpret RegisterOptions as +- // DidChangeWatchedFilesRegistrationOptions. +- raw, err := json.Marshal(registration.RegisterOptions) +- if err != nil { +- return fmt.Errorf("marshaling registration options: %v", err) +- } +- var opts protocol.DidChangeWatchedFilesRegistrationOptions +- if err := json.Unmarshal(raw, &opts); err != nil { +- return fmt.Errorf("unmarshaling registration options: %v", err) +- } +- var globs []*glob.Glob +- for _, watcher := range opts.Watchers { +- var globPattern string +- switch pattern := watcher.GlobPattern.Value.(type) { +- case protocol.Pattern: +- globPattern = pattern +- case protocol.RelativePattern: +- globPattern = path.Join(filepath.ToSlash(pattern.BaseURI.Path()), pattern.Pattern) +- } +- // TODO(rfindley): honor the watch kind. +- g, err := glob.Parse(globPattern) +- if err != nil { +- return fmt.Errorf("error parsing glob pattern %q: %v", watcher.GlobPattern, err) +- } +- globs = append(globs, g) +- } +- c.editor.mu.Lock() +- c.editor.watchPatterns = globs +- c.editor.mu.Unlock() +- } - } -- lineno, err := strconv.Atoi(matches[1]) -- if err != nil { -- msg := fmt.Sprintf("couldn't convert %q to int, %v", matches[1], err) -- return unknownError(msg) +- return nil +-} +- +-func (c *Client) UnregisterCapability(ctx context.Context, params *protocol.UnregistrationParams) error { +- if c.hooks.OnUnregisterCapability != nil { +- return c.hooks.OnUnregisterCapability(ctx, params) - } -- msg := matches[2] -- d := source.Diagnostic{Message: msg, Severity: protocol.SeverityError, -- Source: source.TemplateError} -- start := p.nls[lineno-1] -- if lineno < len(p.nls) { -- size := p.nls[lineno] - start -- d.Range = p.Range(start, size) -- } else { -- d.Range = p.Range(start, 1) +- return nil +-} +- +-func (c *Client) Progress(ctx context.Context, params *protocol.ProgressParams) error { +- if c.hooks.OnProgress != nil { +- return c.hooks.OnProgress(ctx, params) - } -- return []*source.Diagnostic{&d} +- return nil -} - --// Definition finds the definitions of the symbol at loc. It --// does not understand scoping (if any) in templates. This code is --// for definitions, type definitions, and implementations. --// Results only for variables and templates. --func Definition(snapshot source.Snapshot, fh source.FileHandle, loc protocol.Position) ([]protocol.Location, error) { -- x, _, err := symAtPosition(fh, loc) -- if err != nil { -- return nil, err +-func (c *Client) WorkDoneProgressCreate(ctx context.Context, params *protocol.WorkDoneProgressCreateParams) error { +- if c.hooks.OnWorkDoneProgressCreate != nil { +- return c.hooks.OnWorkDoneProgressCreate(ctx, params) - } -- sym := x.name -- ans := []protocol.Location{} -- // PJW: this is probably a pattern to abstract -- a := New(snapshot.Templates()) -- for k, p := range a.files { -- for _, s := range p.symbols { -- if !s.vardef || s.name != sym { -- continue -- } -- ans = append(ans, protocol.Location{URI: protocol.DocumentURI(k), Range: p.Range(s.start, s.length)}) +- return nil +-} +- +-func (c *Client) ShowDocument(ctx context.Context, params *protocol.ShowDocumentParams) (*protocol.ShowDocumentResult, error) { +- if c.hooks.OnShowDocument != nil { +- if err := c.hooks.OnShowDocument(ctx, params); err != nil { +- return nil, err - } +- return &protocol.ShowDocumentResult{Success: true}, nil - } -- return ans, nil +- return nil, nil -} - --func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.Hover, error) { -- sym, p, err := symAtPosition(fh, position) -- if sym == nil || err != nil { -- return nil, err +-func (c *Client) ApplyEdit(ctx context.Context, params *protocol.ApplyWorkspaceEditParams) (*protocol.ApplyWorkspaceEditResult, error) { +- if len(params.Edit.Changes) != 0 { +- return &protocol.ApplyWorkspaceEditResult{FailureReason: "Edit.Changes is unsupported"}, nil - } -- ans := protocol.Hover{Range: p.Range(sym.start, sym.length), Contents: protocol.MarkupContent{Kind: protocol.Markdown}} -- switch sym.kind { -- case protocol.Function: -- ans.Contents.Value = fmt.Sprintf("function: %s", sym.name) -- case protocol.Variable: -- ans.Contents.Value = fmt.Sprintf("variable: %s", sym.name) -- case protocol.Constant: -- ans.Contents.Value = fmt.Sprintf("constant %s", sym.name) -- case protocol.Method: // field or method -- ans.Contents.Value = fmt.Sprintf("%s: field or method", sym.name) -- case protocol.Package: // template use, template def (PJW: do we want two?) -- ans.Contents.Value = fmt.Sprintf("template %s\n(add definition)", sym.name) -- case protocol.Namespace: -- ans.Contents.Value = fmt.Sprintf("template %s defined", sym.name) -- case protocol.Number: -- ans.Contents.Value = "number" -- case protocol.String: -- ans.Contents.Value = "string" -- case protocol.Boolean: -- ans.Contents.Value = "boolean" -- default: -- ans.Contents.Value = fmt.Sprintf("oops, sym=%#v", sym) +- if c.hooks.OnApplyEdit != nil { +- if err := c.hooks.OnApplyEdit(ctx, params); err != nil { +- return nil, err +- } - } -- return &ans, nil +- if !c.skipApplyEdits { +- for _, change := range params.Edit.DocumentChanges { +- if err := c.editor.applyDocumentChange(ctx, change); err != nil { +- return nil, err +- } +- } +- } +- return &protocol.ApplyWorkspaceEditResult{Applied: true}, nil -} +diff -urN a/gopls/internal/test/integration/fake/doc.go b/gopls/internal/test/integration/fake/doc.go +--- a/gopls/internal/test/integration/fake/doc.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/fake/doc.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,20 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func References(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, params *protocol.ReferenceParams) ([]protocol.Location, error) { -- sym, _, err := symAtPosition(fh, params.Position) -- if sym == nil || err != nil || sym.name == "" { -- return nil, err -- } -- ans := []protocol.Location{} +-// Package fake provides a fake implementation of an LSP-enabled +-// text editor, its LSP client plugin, and a Sandbox environment for +-// use in integration tests. +-// +-// The Editor type provides a high level API for text editor operations +-// (open/modify/save/close a buffer, jump to definition, etc.), and the Client +-// type exposes an LSP client for the editor that can be connected to a +-// language server. By default, the Editor and Client should be compliant with +-// the LSP spec: their intended use is to verify server compliance with the +-// spec in a variety of environment. Possible future enhancements of these +-// types may allow them to misbehave in configurable ways, but that is not +-// their primary use. +-// +-// The Sandbox type provides a facility for executing tests with a temporary +-// directory, module proxy, and GOPATH. +-package fake +diff -urN a/gopls/internal/test/integration/fake/edit.go b/gopls/internal/test/integration/fake/edit.go +--- a/gopls/internal/test/integration/fake/edit.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/fake/edit.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,42 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- a := New(snapshot.Templates()) -- for k, p := range a.files { -- for _, s := range p.symbols { -- if s.name != sym.name { -- continue -- } -- if s.vardef && !params.Context.IncludeDeclaration { -- continue -- } -- ans = append(ans, protocol.Location{URI: protocol.DocumentURI(k), Range: p.Range(s.start, s.length)}) -- } +-package fake +- +-import ( +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/diff" +-) +- +-// NewEdit creates an edit replacing all content between the 0-based +-// (startLine, startColumn) and (endLine, endColumn) with text. +-// +-// Columns measure UTF-16 codes. +-func NewEdit(startLine, startColumn, endLine, endColumn uint32, text string) protocol.TextEdit { +- return protocol.TextEdit{ +- Range: protocol.Range{ +- Start: protocol.Position{Line: startLine, Character: startColumn}, +- End: protocol.Position{Line: endLine, Character: endColumn}, +- }, +- NewText: text, - } -- // do these need to be sorted? (a.files is a map) -- return ans, nil -} - --func SemanticTokens(ctx context.Context, snapshot source.Snapshot, spn span.URI, add func(line, start, len uint32), d func() []uint32) (*protocol.SemanticTokens, error) { -- fh, err := snapshot.ReadFile(ctx, spn) +-// applyEdits applies the edits to a file with the specified lines, +-// and returns a new slice containing the lines of the patched file. +-// It is a wrapper around diff.Apply; see that function for preconditions. +-func applyEdits(mapper *protocol.Mapper, edits []protocol.TextEdit, windowsLineEndings bool) ([]byte, error) { +- diffEdits, err := protocol.EditsToDiffEdits(mapper, edits) - if err != nil { - return nil, err - } -- buf, err := fh.Content() +- patched, err := diff.ApplyBytes(mapper.Content, diffEdits) - if err != nil { - return nil, err - } -- p := parseBuffer(buf) -- -- for _, t := range p.Tokens() { -- if t.Multiline { -- la, ca := p.LineCol(t.Start) -- lb, cb := p.LineCol(t.End) -- add(la, ca, p.RuneCount(la, ca, 0)) -- for l := la + 1; l < lb; l++ { -- add(l, 0, p.RuneCount(l, 0, 0)) -- } -- add(lb, 0, p.RuneCount(lb, 0, cb)) -- continue -- } -- sz, err := p.TokenSize(t) -- if err != nil { -- return nil, err -- } -- line, col := p.LineCol(t.Start) -- add(line, col, uint32(sz)) -- } -- data := d() -- ans := &protocol.SemanticTokens{ -- Data: data, -- // for small cache, some day. for now, the LSP client ignores this -- // (that is, when the LSP client starts returning these, we can cache) -- ResultID: fmt.Sprintf("%v", time.Now()), +- if windowsLineEndings { +- patched = toWindowsLineEndings(patched) - } -- return ans, nil +- return patched, nil -} -- --// still need to do rename, etc -diff -urN a/gopls/internal/lsp/template/parse.go b/gopls/internal/lsp/template/parse.go ---- a/gopls/internal/lsp/template/parse.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/template/parse.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,508 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/test/integration/fake/editor.go b/gopls/internal/test/integration/fake/editor.go +--- a/gopls/internal/test/integration/fake/editor.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/fake/editor.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,1662 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --// Package template contains code for dealing with templates --package template -- --// template files are small enough that the code reprocesses them each time --// this may be a bad choice for projects with lots of template files. -- --// This file contains the parsing code, some debugging printing, and --// implementations for Diagnose, Definition, Hover, References +-package fake - -import ( - "bytes" - "context" +- "encoding/json" +- "errors" - "fmt" -- "io" -- "log" +- "os" +- "path" +- "path/filepath" - "regexp" -- "runtime" -- "sort" -- "text/template" -- "text/template/parse" -- "unicode/utf8" +- "strings" +- "sync" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/event" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/command" +- "golang.org/x/tools/gopls/internal/test/integration/fake/glob" +- "golang.org/x/tools/gopls/internal/util/pathutil" +- "golang.org/x/tools/gopls/internal/util/slices" +- "golang.org/x/tools/internal/jsonrpc2" +- "golang.org/x/tools/internal/jsonrpc2/servertest" +- "golang.org/x/tools/internal/xcontext" -) - --var ( -- Left = []byte("{{") -- Right = []byte("}}") --) +-// Editor is a fake client editor. It keeps track of client state and can be +-// used for writing LSP tests. +-type Editor struct { - --type Parsed struct { -- buf []byte //contents -- lines [][]byte // needed?, other than for debugging? -- elided []int // offsets where Left was replaced by blanks +- // Server, client, and sandbox are concurrency safe and written only +- // at construction time, so do not require synchronization. +- Server protocol.Server +- cancelConn func() +- serverConn jsonrpc2.Conn +- client *Client +- sandbox *Sandbox - -- // tokens are matched Left-Right pairs, computed before trying to parse -- tokens []Token +- // TODO(rfindley): buffers should be keyed by protocol.DocumentURI. +- mu sync.Mutex +- config EditorConfig // editor configuration +- buffers map[string]buffer // open buffers (relative path -> buffer content) +- serverCapabilities protocol.ServerCapabilities // capabilities / options +- semTokOpts protocol.SemanticTokensOptions +- watchPatterns []*glob.Glob // glob patterns to watch - -- // result of parsing -- named []*template.Template // the template and embedded templates -- ParseErr error -- symbols []symbol -- stack []parse.Node // used while computing symbols +- // Call metrics for the purpose of expectations. This is done in an ad-hoc +- // manner for now. Perhaps in the future we should do something more +- // systematic. Guarded with a separate mutex as calls may need to be accessed +- // asynchronously via callbacks into the Editor. +- callsMu sync.Mutex +- calls CallCounts +-} - -- // for mapping from offsets in buf to LSP coordinates -- // See FromPosition() and LineCol() -- nls []int // offset of newlines before each line (nls[0]==-1) -- lastnl int // last line seen -- check int // used to decide whether to use lastnl or search through nls -- nonASCII bool // are there any non-ascii runes in buf? +-// CallCounts tracks the number of protocol notifications of different types. +-type CallCounts struct { +- DidOpen, DidChange, DidSave, DidChangeWatchedFiles, DidClose, DidChangeConfiguration uint64 -} - --// Token is a single {{...}}. More precisely, Left...Right --type Token struct { -- Start, End int // offset from start of template -- Multiline bool +-// buffer holds information about an open buffer in the editor. +-type buffer struct { +- version int // monotonic version; incremented on edits +- path string // relative path in the workspace +- mapper *protocol.Mapper // buffer content +- dirty bool // if true, content is unsaved (TODO(rfindley): rename this field) -} - --// All contains the Parse of all the template files --type All struct { -- files map[span.URI]*Parsed +-func (b buffer) text() string { +- return string(b.mapper.Content) -} - --// New returns the Parses of the snapshot's tmpl files --// (maybe cache these, but then avoiding import cycles needs code rearrangements) --func New(tmpls map[span.URI]source.FileHandle) *All { -- all := make(map[span.URI]*Parsed) -- for k, v := range tmpls { -- buf, err := v.Content() -- if err != nil { // PJW: decide what to do with these errors -- log.Printf("failed to read %s (%v)", v.URI().Filename(), err) -- continue -- } -- all[k] = parseBuffer(buf) -- } -- return &All{files: all} +-// EditorConfig configures the editor's LSP session. This is similar to +-// golang.UserOptions, but we use a separate type here so that we expose only +-// that configuration which we support. +-// +-// The zero value for EditorConfig is the default configuration. +-type EditorConfig struct { +- // ClientName sets the clientInfo.name for the LSP session (in the initialize request). +- // +- // Since this can only be set during initialization, changing this field via +- // Editor.ChangeConfiguration has no effect. +- // +- // If empty, "fake.Editor" is used. +- ClientName string +- +- // Env holds environment variables to apply on top of the default editor +- // environment. When applying these variables, the special string +- // $SANDBOX_WORKDIR is replaced by the absolute path to the sandbox working +- // directory. +- Env map[string]string +- +- // WorkspaceFolders is the workspace folders to configure on the LSP server, +- // relative to the sandbox workdir. +- // +- // As a special case, if WorkspaceFolders is nil the editor defaults to +- // configuring a single workspace folder corresponding to the workdir root. +- // To explicitly send no workspace folders, use an empty (non-nil) slice. +- WorkspaceFolders []string +- +- // Whether to edit files with windows line endings. +- WindowsLineEndings bool +- +- // Map of language ID -> regexp to match, used to set the file type of new +- // buffers. Applied as an overlay on top of the following defaults: +- // "go" -> ".*\.go" +- // "go.mod" -> "go\.mod" +- // "go.sum" -> "go\.sum" +- // "gotmpl" -> ".*tmpl" +- FileAssociations map[string]string +- +- // Settings holds user-provided configuration for the LSP server. +- Settings map[string]any +- +- // FolderSettings holds user-provided per-folder configuration, if any. +- // +- // It maps each folder (as a relative path to the sandbox workdir) to its +- // configuration mapping (like Settings). +- FolderSettings map[string]map[string]any +- +- // CapabilitiesJSON holds JSON client capabilities to overlay over the +- // editor's default client capabilities. +- // +- // Specifically, this JSON string will be unmarshalled into the editor's +- // client capabilities struct, before sending to the server. +- CapabilitiesJSON []byte +- +- // If non-nil, MessageResponder is used to respond to ShowMessageRequest +- // messages. +- MessageResponder func(params *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error) -} - --func parseBuffer(buf []byte) *Parsed { -- ans := &Parsed{ -- buf: buf, -- check: -1, -- nls: []int{-1}, -- } -- if len(buf) == 0 { -- return ans -- } -- // how to compute allAscii... -- for _, b := range buf { -- if b >= utf8.RuneSelf { -- ans.nonASCII = true -- break -- } -- } -- if buf[len(buf)-1] != '\n' { -- ans.buf = append(buf, '\n') -- } -- for i, p := range ans.buf { -- if p == '\n' { -- ans.nls = append(ans.nls, i) -- } -- } -- ans.setTokens() // ans.buf may be a new []byte -- ans.lines = bytes.Split(ans.buf, []byte{'\n'}) -- t, err := template.New("").Parse(string(ans.buf)) -- if err != nil { -- funcs := make(template.FuncMap) -- for t == nil && ans.ParseErr == nil { -- // in 1.17 it may be possible to avoid getting this error -- // template: :2: function "foo" not defined -- matches := parseErrR.FindStringSubmatch(err.Error()) -- if len(matches) == 2 { -- // suppress the error by giving it a function with the right name -- funcs[matches[1]] = func() interface{} { return nil } -- t, err = template.New("").Funcs(funcs).Parse(string(ans.buf)) -- continue -- } -- ans.ParseErr = err // unfixed error -- return ans -- } +-// NewEditor creates a new Editor. +-func NewEditor(sandbox *Sandbox, config EditorConfig) *Editor { +- return &Editor{ +- buffers: make(map[string]buffer), +- sandbox: sandbox, +- config: config, - } -- ans.named = t.Templates() -- // set the symbols -- for _, t := range ans.named { -- ans.stack = append(ans.stack, t.Root) -- ans.findSymbols() -- if t.Name() != "" { -- // defining a template. The pos is just after {{define...}} (or {{block...}}?) -- at, sz := ans.FindLiteralBefore(int(t.Root.Pos)) -- s := symbol{start: at, length: sz, name: t.Name(), kind: protocol.Namespace, vardef: true} -- ans.symbols = append(ans.symbols, s) -- } +-} +- +-// Connect configures the editor to communicate with an LSP server on conn. It +-// is not concurrency safe, and should be called at most once, before using the +-// editor. +-// +-// It returns the editor, so that it may be called as follows: +-// +-// editor, err := NewEditor(s).Connect(ctx, conn, hooks) +-func (e *Editor) Connect(ctx context.Context, connector servertest.Connector, hooks ClientHooks, skipApplyEdits bool) (*Editor, error) { +- bgCtx, cancelConn := context.WithCancel(xcontext.Detach(ctx)) +- conn := connector.Connect(bgCtx) +- e.cancelConn = cancelConn +- +- e.serverConn = conn +- e.Server = protocol.ServerDispatcher(conn) +- e.client = &Client{editor: e, hooks: hooks, skipApplyEdits: skipApplyEdits} +- conn.Go(bgCtx, +- protocol.Handlers( +- protocol.ClientHandler(e.client, +- jsonrpc2.MethodNotFound))) +- +- if err := e.initialize(ctx); err != nil { +- return nil, err - } +- e.sandbox.Workdir.AddWatcher(e.onFileChanges) +- return e, nil +-} - -- sort.Slice(ans.symbols, func(i, j int) bool { -- left, right := ans.symbols[i], ans.symbols[j] -- if left.start != right.start { -- return left.start < right.start -- } -- if left.vardef != right.vardef { -- return left.vardef -- } -- return left.kind < right.kind -- }) -- return ans +-func (e *Editor) Stats() CallCounts { +- e.callsMu.Lock() +- defer e.callsMu.Unlock() +- return e.calls -} - --// FindLiteralBefore locates the first preceding string literal --// returning its position and length in buf --// or returns -1 if there is none. --// Assume double-quoted string rather than backquoted string for now. --func (p *Parsed) FindLiteralBefore(pos int) (int, int) { -- left, right := -1, -1 -- for i := pos - 1; i >= 0; i-- { -- if p.buf[i] != '"' { -- continue -- } -- if right == -1 { -- right = i -- continue +-// Shutdown issues the 'shutdown' LSP notification. +-func (e *Editor) Shutdown(ctx context.Context) error { +- if e.Server != nil { +- if err := e.Server.Shutdown(ctx); err != nil { +- return fmt.Errorf("Shutdown: %w", err) - } -- left = i -- break -- } -- if left == -1 { -- return -1, 0 - } -- return left + 1, right - left - 1 +- return nil -} - --var ( -- parseErrR = regexp.MustCompile(`template:.*function "([^"]+)" not defined`) --) -- --func (p *Parsed) setTokens() { -- const ( -- // InRaw and InString only occur inside an action (SeenLeft) -- Start = iota -- InRaw -- InString -- SeenLeft -- ) -- state := Start -- var left, oldState int -- for n := 0; n < len(p.buf); n++ { -- c := p.buf[n] -- switch state { -- case InRaw: -- if c == '`' { -- state = oldState -- } -- case InString: -- if c == '"' && !isEscaped(p.buf[:n]) { -- state = oldState -- } -- case SeenLeft: -- if c == '`' { -- oldState = state // it's SeenLeft, but a little clearer this way -- state = InRaw -- continue -- } -- if c == '"' { -- oldState = state -- state = InString -- continue -- } -- if bytes.HasPrefix(p.buf[n:], Right) { -- right := n + len(Right) -- tok := Token{Start: left, -- End: right, -- Multiline: bytes.Contains(p.buf[left:right], []byte{'\n'}), -- } -- p.tokens = append(p.tokens, tok) -- state = Start -- } -- // If we see (unquoted) Left then the original left is probably the user -- // typing. Suppress the original left -- if bytes.HasPrefix(p.buf[n:], Left) { -- p.elideAt(left) -- left = n -- n += len(Left) - 1 // skip the rest -- } -- case Start: -- if bytes.HasPrefix(p.buf[n:], Left) { -- left = n -- state = SeenLeft -- n += len(Left) - 1 // skip the rest (avoids {{{ bug) -- } +-// Exit issues the 'exit' LSP notification. +-func (e *Editor) Exit(ctx context.Context) error { +- if e.Server != nil { +- // Not all LSP clients issue the exit RPC, but we do so here to ensure that +- // we gracefully handle it on multi-session servers. +- if err := e.Server.Exit(ctx); err != nil { +- return fmt.Errorf("Exit: %w", err) - } - } -- // this error occurs after typing {{ at the end of the file -- if state != Start { -- // Unclosed Left. remove the Left at left -- p.elideAt(left) -- } +- return nil -} - --func (p *Parsed) elideAt(left int) { -- if p.elided == nil { -- // p.buf is the same buffer that v.Read() returns, so copy it. -- // (otherwise the next time it's parsed, elided information is lost) -- b := make([]byte, len(p.buf)) -- copy(b, p.buf) -- p.buf = b +-// Close issues the shutdown and exit sequence an editor should. +-func (e *Editor) Close(ctx context.Context) error { +- if err := e.Shutdown(ctx); err != nil { +- return err - } -- for i := 0; i < len(Left); i++ { -- p.buf[left+i] = ' ' +- if err := e.Exit(ctx); err != nil { +- return err - } -- p.elided = append(p.elided, left) --} +- defer func() { +- e.cancelConn() +- }() - --// isEscaped reports whether the byte after buf is escaped --func isEscaped(buf []byte) bool { -- backSlashes := 0 -- for j := len(buf) - 1; j >= 0 && buf[j] == '\\'; j-- { -- backSlashes++ +- // called close on the editor should result in the connection closing +- select { +- case <-e.serverConn.Done(): +- // connection closed itself +- return nil +- case <-ctx.Done(): +- return fmt.Errorf("connection not closed: %w", ctx.Err()) - } -- return backSlashes%2 == 1 -} - --func (p *Parsed) Tokens() []Token { -- return p.tokens +-// Client returns the LSP client for this editor. +-func (e *Editor) Client() *Client { +- return e.client -} - --// TODO(adonovan): the next 100 lines could perhaps replaced by use of protocol.Mapper. -- --func (p *Parsed) utf16len(buf []byte) int { -- cnt := 0 -- if !p.nonASCII { -- return len(buf) +-// makeSettings builds the settings map for use in LSP settings RPCs. +-func makeSettings(sandbox *Sandbox, config EditorConfig, scopeURI *protocol.URI) map[string]any { +- env := make(map[string]string) +- for k, v := range sandbox.GoEnv() { +- env[k] = v - } -- // we need a utf16len(rune), but we don't have it -- for _, r := range string(buf) { -- cnt++ -- if r >= 1<<16 { -- cnt++ -- } +- for k, v := range config.Env { +- env[k] = v - } -- return cnt --} -- --func (p *Parsed) TokenSize(t Token) (int, error) { -- if t.Multiline { -- return -1, fmt.Errorf("TokenSize called with Multiline token %#v", t) +- for k, v := range env { +- v = strings.ReplaceAll(v, "$SANDBOX_WORKDIR", sandbox.Workdir.RootURI().Path()) +- env[k] = v - } -- ans := p.utf16len(p.buf[t.Start:t.End]) -- return ans, nil --} - --// RuneCount counts runes in line l, from col s to e --// (e==0 for end of line. called only for multiline tokens) --func (p *Parsed) RuneCount(l, s, e uint32) uint32 { -- start := p.nls[l] + 1 + int(s) -- end := p.nls[l] + 1 + int(e) -- if e == 0 || end > p.nls[l+1] { -- end = p.nls[l+1] -- } -- return uint32(utf8.RuneCount(p.buf[start:end])) --} +- settings := map[string]any{ +- "env": env, - --// LineCol converts from a 0-based byte offset to 0-based line, col. col in runes --func (p *Parsed) LineCol(x int) (uint32, uint32) { -- if x < p.check { -- p.lastnl = 0 +- // Use verbose progress reporting so that integration tests can assert on +- // asynchronous operations being completed (such as diagnosing a snapshot). +- "verboseWorkDoneProgress": true, +- +- // Set an unlimited completion budget, so that tests don't flake because +- // completions are too slow. +- "completionBudget": "0s", - } -- p.check = x -- for i := p.lastnl; i < len(p.nls); i++ { -- if p.nls[i] <= x { -- continue -- } -- p.lastnl = i -- var count int -- if i > 0 && x == p.nls[i-1] { // \n -- count = 0 -- } else { -- count = p.utf16len(p.buf[p.nls[i-1]+1 : x]) +- +- for k, v := range config.Settings { +- if k == "env" { +- panic("must not provide env via the EditorConfig.Settings field: use the EditorConfig.Env field instead") - } -- return uint32(i - 1), uint32(count) -- } -- if x == len(p.buf)-1 { // trailing \n -- return uint32(len(p.nls) - 1), 0 +- settings[k] = v - } -- // shouldn't happen -- for i := 1; i < 4; i++ { -- _, f, l, ok := runtime.Caller(i) -- if !ok { -- break +- +- // If the server is requesting configuration for a specific scope, apply +- // settings for the nearest folder that has customized settings, if any. +- if scopeURI != nil { +- var ( +- scopePath = protocol.DocumentURI(*scopeURI).Path() +- closestDir string // longest dir with settings containing the scope, if any +- closestSettings map[string]any // settings for that dir, if any +- ) +- for relPath, settings := range config.FolderSettings { +- dir := sandbox.Workdir.AbsPath(relPath) +- if strings.HasPrefix(scopePath+string(filepath.Separator), dir+string(filepath.Separator)) && len(dir) > len(closestDir) { +- closestDir = dir +- closestSettings = settings +- } +- } +- if closestSettings != nil { +- for k, v := range closestSettings { +- settings[k] = v +- } - } -- log.Printf("%d: %s:%d", i, f, l) - } - -- msg := fmt.Errorf("LineCol off the end, %d of %d, nls=%v, %q", x, len(p.buf), p.nls, p.buf[x:]) -- event.Error(context.Background(), "internal error", msg) -- return 0, 0 +- return settings -} - --// Position produces a protocol.Position from an offset in the template --func (p *Parsed) Position(pos int) protocol.Position { -- line, col := p.LineCol(pos) -- return protocol.Position{Line: line, Character: col} --} +-func (e *Editor) initialize(ctx context.Context) error { +- config := e.Config() - --func (p *Parsed) Range(x, length int) protocol.Range { -- line, col := p.LineCol(x) -- ans := protocol.Range{ -- Start: protocol.Position{Line: line, Character: col}, -- End: protocol.Position{Line: line, Character: col + uint32(length)}, +- clientName := config.ClientName +- if clientName == "" { +- clientName = "fake.Editor" - } -- return ans --} - --// FromPosition translates a protocol.Position into an offset into the template --func (p *Parsed) FromPosition(x protocol.Position) int { -- l, c := int(x.Line), int(x.Character) -- if l >= len(p.nls) || p.nls[l]+1 >= len(p.buf) { -- // paranoia to avoid panic. return the largest offset -- return len(p.buf) +- params := &protocol.ParamInitialize{} +- params.ClientInfo = &protocol.ClientInfo{ +- Name: clientName, +- Version: "v1.0.0", - } -- line := p.buf[p.nls[l]+1:] -- cnt := 0 -- for w := range string(line) { -- if cnt >= c { -- return w + p.nls[l] + 1 +- params.InitializationOptions = makeSettings(e.sandbox, config, nil) +- params.WorkspaceFolders = makeWorkspaceFolders(e.sandbox, config.WorkspaceFolders) +- +- capabilities, err := clientCapabilities(config) +- if err != nil { +- return fmt.Errorf("unmarshalling EditorConfig.CapabilitiesJSON: %v", err) +- } +- params.Capabilities = capabilities +- +- trace := protocol.TraceValue("messages") +- params.Trace = &trace +- // TODO: support workspace folders. +- if e.Server != nil { +- resp, err := e.Server.Initialize(ctx, params) +- if err != nil { +- return fmt.Errorf("initialize: %w", err) +- } +- semTokOpts, err := marshalUnmarshal[protocol.SemanticTokensOptions](resp.Capabilities.SemanticTokensProvider) +- if err != nil { +- return fmt.Errorf("unmarshalling semantic tokens options: %v", err) +- } +- e.mu.Lock() +- e.serverCapabilities = resp.Capabilities +- e.semTokOpts = semTokOpts +- e.mu.Unlock() +- +- if err := e.Server.Initialized(ctx, &protocol.InitializedParams{}); err != nil { +- return fmt.Errorf("initialized: %w", err) - } -- cnt++ - } -- // do we get here? NO -- pos := int(x.Character) + p.nls[int(x.Line)] + 1 -- event.Error(context.Background(), "internal error", fmt.Errorf("surprise %#v", x)) -- return pos +- // TODO: await initial configuration here, or expect gopls to manage that? +- return nil -} - --func symAtPosition(fh source.FileHandle, loc protocol.Position) (*symbol, *Parsed, error) { -- buf, err := fh.Content() -- if err != nil { -- return nil, nil, err +-func clientCapabilities(cfg EditorConfig) (protocol.ClientCapabilities, error) { +- var capabilities protocol.ClientCapabilities +- // Set various client capabilities that are sought by gopls. +- capabilities.Workspace.Configuration = true // support workspace/configuration +- capabilities.TextDocument.Completion.CompletionItem.TagSupport = &protocol.CompletionItemTagOptions{} +- capabilities.TextDocument.Completion.CompletionItem.TagSupport.ValueSet = []protocol.CompletionItemTag{protocol.ComplDeprecated} +- capabilities.TextDocument.Completion.CompletionItem.SnippetSupport = true +- capabilities.TextDocument.SemanticTokens.Requests.Full = &protocol.Or_ClientSemanticTokensRequestOptions_full{Value: true} +- capabilities.Window.WorkDoneProgress = true // support window/workDoneProgress +- capabilities.TextDocument.SemanticTokens.TokenTypes = []string{ +- "namespace", "type", "class", "enum", "interface", +- "struct", "typeParameter", "parameter", "variable", "property", "enumMember", +- "event", "function", "method", "macro", "keyword", "modifier", "comment", +- "string", "number", "regexp", "operator", +- // Additional types supported by this client: +- "label", - } -- p := parseBuffer(buf) -- pos := p.FromPosition(loc) -- syms := p.SymsAtPos(pos) -- if len(syms) == 0 { -- return nil, p, fmt.Errorf("no symbol found") +- capabilities.TextDocument.SemanticTokens.TokenModifiers = []string{ +- "declaration", "definition", "readonly", "static", +- "deprecated", "abstract", "async", "modification", "documentation", "defaultLibrary", - } -- if len(syms) > 1 { -- log.Printf("Hover: %d syms, not 1 %v", len(syms), syms) +- // The LSP tests have historically enabled this flag, +- // but really we should test both ways for older editors. +- capabilities.TextDocument.DocumentSymbol.HierarchicalDocumentSymbolSupport = true +- // Glob pattern watching is enabled. +- capabilities.Workspace.DidChangeWatchedFiles.DynamicRegistration = true +- // "rename" operations are used for package renaming. +- // +- // TODO(rfindley): add support for other resource operations (create, delete, ...) +- capabilities.Workspace.WorkspaceEdit = &protocol.WorkspaceEditClientCapabilities{ +- ResourceOperations: []protocol.ResourceOperationKind{ +- "rename", +- }, - } -- sym := syms[0] -- return &sym, p, nil --} - --func (p *Parsed) SymsAtPos(pos int) []symbol { -- ans := []symbol{} -- for _, s := range p.symbols { -- if s.start <= pos && pos < s.start+s.length { -- ans = append(ans, s) +- // Apply capabilities overlay. +- if cfg.CapabilitiesJSON != nil { +- if err := json.Unmarshal(cfg.CapabilitiesJSON, &capabilities); err != nil { +- return protocol.ClientCapabilities{}, fmt.Errorf("unmarshalling EditorConfig.CapabilitiesJSON: %v", err) - } - } -- return ans --} -- --type wrNode struct { -- p *Parsed -- w io.Writer +- return capabilities, nil -} - --// WriteNode is for debugging --func (p *Parsed) WriteNode(w io.Writer, n parse.Node) { -- wr := wrNode{p: p, w: w} -- wr.writeNode(n, "") +-// marshalUnmarshal is a helper to json Marshal and then Unmarshal as a +-// different type. Used to work around cases where our protocol types are not +-// specific. +-func marshalUnmarshal[T any](v any) (T, error) { +- var t T +- data, err := json.Marshal(v) +- if err != nil { +- return t, err +- } +- err = json.Unmarshal(data, &t) +- return t, err -} - --func (wr wrNode) writeNode(n parse.Node, indent string) { -- if n == nil { -- return -- } -- at := func(pos parse.Pos) string { -- line, col := wr.p.LineCol(int(pos)) -- return fmt.Sprintf("(%d)%v:%v", pos, line, col) -- } -- switch x := n.(type) { -- case *parse.ActionNode: -- fmt.Fprintf(wr.w, "%sActionNode at %s\n", indent, at(x.Pos)) -- wr.writeNode(x.Pipe, indent+". ") -- case *parse.BoolNode: -- fmt.Fprintf(wr.w, "%sBoolNode at %s, %v\n", indent, at(x.Pos), x.True) -- case *parse.BranchNode: -- fmt.Fprintf(wr.w, "%sBranchNode at %s\n", indent, at(x.Pos)) -- wr.writeNode(x.Pipe, indent+"Pipe. ") -- wr.writeNode(x.List, indent+"List. ") -- wr.writeNode(x.ElseList, indent+"Else. ") -- case *parse.ChainNode: -- fmt.Fprintf(wr.w, "%sChainNode at %s, %v\n", indent, at(x.Pos), x.Field) -- case *parse.CommandNode: -- fmt.Fprintf(wr.w, "%sCommandNode at %s, %d children\n", indent, at(x.Pos), len(x.Args)) -- for _, a := range x.Args { -- wr.writeNode(a, indent+". ") -- } -- //case *parse.CommentNode: // 1.16 -- case *parse.DotNode: -- fmt.Fprintf(wr.w, "%sDotNode at %s\n", indent, at(x.Pos)) -- case *parse.FieldNode: -- fmt.Fprintf(wr.w, "%sFieldNode at %s, %v\n", indent, at(x.Pos), x.Ident) -- case *parse.IdentifierNode: -- fmt.Fprintf(wr.w, "%sIdentifierNode at %s, %v\n", indent, at(x.Pos), x.Ident) -- case *parse.IfNode: -- fmt.Fprintf(wr.w, "%sIfNode at %s\n", indent, at(x.Pos)) -- wr.writeNode(&x.BranchNode, indent+". ") -- case *parse.ListNode: -- if x == nil { -- return // nil BranchNode.ElseList -- } -- fmt.Fprintf(wr.w, "%sListNode at %s, %d children\n", indent, at(x.Pos), len(x.Nodes)) -- for _, n := range x.Nodes { -- wr.writeNode(n, indent+". ") -- } -- case *parse.NilNode: -- fmt.Fprintf(wr.w, "%sNilNode at %s\n", indent, at(x.Pos)) -- case *parse.NumberNode: -- fmt.Fprintf(wr.w, "%sNumberNode at %s, %s\n", indent, at(x.Pos), x.Text) -- case *parse.PipeNode: -- if x == nil { -- return // {{template "xxx"}} -- } -- fmt.Fprintf(wr.w, "%sPipeNode at %s, %d vars, %d cmds, IsAssign:%v\n", -- indent, at(x.Pos), len(x.Decl), len(x.Cmds), x.IsAssign) -- for _, d := range x.Decl { -- wr.writeNode(d, indent+"Decl. ") -- } -- for _, c := range x.Cmds { -- wr.writeNode(c, indent+"Cmd. ") +-// HasCommand reports whether the connected server supports the command with the given ID. +-func (e *Editor) HasCommand(id string) bool { +- for _, command := range e.serverCapabilities.ExecuteCommandProvider.Commands { +- if command == id { +- return true - } -- case *parse.RangeNode: -- fmt.Fprintf(wr.w, "%sRangeNode at %s\n", indent, at(x.Pos)) -- wr.writeNode(&x.BranchNode, indent+". ") -- case *parse.StringNode: -- fmt.Fprintf(wr.w, "%sStringNode at %s, %s\n", indent, at(x.Pos), x.Quoted) -- case *parse.TemplateNode: -- fmt.Fprintf(wr.w, "%sTemplateNode at %s, %s\n", indent, at(x.Pos), x.Name) -- wr.writeNode(x.Pipe, indent+". ") -- case *parse.TextNode: -- fmt.Fprintf(wr.w, "%sTextNode at %s, len %d\n", indent, at(x.Pos), len(x.Text)) -- case *parse.VariableNode: -- fmt.Fprintf(wr.w, "%sVariableNode at %s, %v\n", indent, at(x.Pos), x.Ident) -- case *parse.WithNode: -- fmt.Fprintf(wr.w, "%sWithNode at %s\n", indent, at(x.Pos)) -- wr.writeNode(&x.BranchNode, indent+". ") - } +- return false -} - --var kindNames = []string{"", "File", "Module", "Namespace", "Package", "Class", "Method", "Property", -- "Field", "Constructor", "Enum", "Interface", "Function", "Variable", "Constant", "String", -- "Number", "Boolean", "Array", "Object", "Key", "Null", "EnumMember", "Struct", "Event", -- "Operator", "TypeParameter"} -- --func kindStr(k protocol.SymbolKind) string { -- n := int(k) -- if n < 1 || n >= len(kindNames) { -- return fmt.Sprintf("?SymbolKind %d?", n) +-// makeWorkspaceFolders creates a slice of workspace folders to use for +-// this editing session, based on the editor configuration. +-func makeWorkspaceFolders(sandbox *Sandbox, paths []string) (folders []protocol.WorkspaceFolder) { +- if len(paths) == 0 { +- paths = []string{string(sandbox.Workdir.RelativeTo)} - } -- return kindNames[n] --} -diff -urN a/gopls/internal/lsp/template/parse_test.go b/gopls/internal/lsp/template/parse_test.go ---- a/gopls/internal/lsp/template/parse_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/template/parse_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,238 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package template - --import ( -- "strings" -- "testing" --) +- for _, path := range paths { +- uri := string(sandbox.Workdir.URI(path)) +- folders = append(folders, protocol.WorkspaceFolder{ +- URI: uri, +- Name: filepath.Base(uri), +- }) +- } - --type datum struct { -- buf string -- cnt int -- syms []string // the symbols in the parse of buf +- return folders -} - --var tmpl = []datum{{` --{{if (foo .X.Y)}}{{$A := "hi"}}{{.Z $A}}{{else}} --{{$A.X 12}} --{{foo (.X.Y) 23 ($A.Zü)}} --{{end}}`, 1, []string{"{7,3,foo,Function,false}", "{12,1,X,Method,false}", -- "{14,1,Y,Method,false}", "{21,2,$A,Variable,true}", "{26,2,,String,false}", -- "{35,1,Z,Method,false}", "{38,2,$A,Variable,false}", -- "{53,2,$A,Variable,false}", "{56,1,X,Method,false}", "{57,2,,Number,false}", -- "{64,3,foo,Function,false}", "{70,1,X,Method,false}", -- "{72,1,Y,Method,false}", "{75,2,,Number,false}", "{80,2,$A,Variable,false}", -- "{83,2,Zü,Method,false}", "{94,3,,Constant,false}"}}, +-// onFileChanges is registered to be called by the Workdir on any writes that +-// go through the Workdir API. It is called synchronously by the Workdir. +-func (e *Editor) onFileChanges(ctx context.Context, evts []protocol.FileEvent) { +- if e.Server == nil { +- return +- } - -- {`{{define "zzz"}}{{.}}{{end}} --{{template "zzz"}}`, 2, []string{"{10,3,zzz,Namespace,true}", "{18,1,dot,Variable,false}", -- "{41,3,zzz,Package,false}"}}, +- // e may be locked when onFileChanges is called, but it is important that we +- // synchronously increment this counter so that we can subsequently assert on +- // the number of expected DidChangeWatchedFiles calls. +- e.callsMu.Lock() +- e.calls.DidChangeWatchedFiles++ +- e.callsMu.Unlock() - -- {`{{block "aaa" foo}}b{{end}}`, 2, []string{"{9,3,aaa,Namespace,true}", -- "{9,3,aaa,Package,false}", "{14,3,foo,Function,false}", "{19,1,,Constant,false}"}}, -- {"", 0, nil}, --} +- // Since e may be locked, we must run this mutation asynchronously. +- go func() { +- e.mu.Lock() +- defer e.mu.Unlock() +- for _, evt := range evts { +- // Always send an on-disk change, even for events that seem useless +- // because they're shadowed by an open buffer. +- path := e.sandbox.Workdir.URIToPath(evt.URI) +- if buf, ok := e.buffers[path]; ok { +- // Following VS Code, don't honor deletions or changes to dirty buffers. +- if buf.dirty || evt.Type == protocol.Deleted { +- continue +- } - --func TestSymbols(t *testing.T) { -- for i, x := range tmpl { -- got := parseBuffer([]byte(x.buf)) -- if got.ParseErr != nil { -- t.Errorf("error:%v", got.ParseErr) -- continue -- } -- if len(got.named) != x.cnt { -- t.Errorf("%d: got %d, expected %d", i, len(got.named), x.cnt) +- content, err := e.sandbox.Workdir.ReadFile(path) +- if err != nil { +- continue // A race with some other operation. +- } +- // No need to update if the buffer content hasn't changed. +- if string(content) == buf.text() { +- continue +- } +- // During shutdown, this call will fail. Ignore the error. +- _ = e.setBufferContentLocked(ctx, path, false, content, nil) +- } - } -- for n, s := range got.symbols { -- if s.String() != x.syms[n] { -- t.Errorf("%d: got %s, expected %s", i, s.String(), x.syms[n]) +- var matchedEvts []protocol.FileEvent +- for _, evt := range evts { +- filename := filepath.ToSlash(evt.URI.Path()) +- for _, g := range e.watchPatterns { +- if g.Match(filename) { +- matchedEvts = append(matchedEvts, evt) +- break +- } - } - } -- } --} - --func TestWordAt(t *testing.T) { -- want := []string{"", "", "$A", "$A", "", "", "", "", "", "", -- "", "", "", "if", "if", "", "$A", "$A", "", "", -- "B", "", "", "end", "end", "end", "", "", ""} -- p := parseBuffer([]byte("{{$A := .}}{{if $A}}B{{end}}")) -- for i := 0; i < len(p.buf); i++ { -- got := findWordAt(p, i) -- if got != want[i] { -- t.Errorf("for %d, got %q, wanted %q", i, got, want[i]) -- } -- } +- // TODO(rfindley): don't send notifications while locked. +- e.Server.DidChangeWatchedFiles(ctx, &protocol.DidChangeWatchedFilesParams{ +- Changes: matchedEvts, +- }) +- }() -} - --func TestNLS(t *testing.T) { -- buf := `{{if (foÜx .X.Y)}}{{$A := "hi"}}{{.Z $A}}{{else}} -- {{$A.X 12}} -- {{foo (.X.Y) 23 ($A.Z)}} -- {{end}} -- ` -- p := parseBuffer([]byte(buf)) -- if p.ParseErr != nil { -- t.Fatal(p.ParseErr) +-// OpenFile creates a buffer for the given workdir-relative file. +-// +-// If the file is already open, it is a no-op. +-func (e *Editor) OpenFile(ctx context.Context, path string) error { +- if e.HasBuffer(path) { +- return nil - } -- // line 0 doesn't have a \n in front of it -- for i := 1; i < len(p.nls)-1; i++ { -- if buf[p.nls[i]] != '\n' { -- t.Errorf("line %d got %c", i, buf[p.nls[i]]) -- } +- content, err := e.sandbox.Workdir.ReadFile(path) +- if err != nil { +- return err - } -- // fake line at end of file -- if p.nls[len(p.nls)-1] != len(buf) { -- t.Errorf("got %d expected %d", p.nls[len(p.nls)-1], len(buf)) +- if e.Config().WindowsLineEndings { +- content = toWindowsLineEndings(content) - } +- return e.createBuffer(ctx, path, false, content) -} - --func TestLineCol(t *testing.T) { -- buf := `{{if (foÜx .X.Y)}}{{$A := "hi"}}{{.Z $A}}{{else}} -- {{$A.X 12}} -- {{foo (.X.Y) 23 ($A.Z)}} -- {{end}}` -- if false { -- t.Error(buf) -- } -- for n, cx := range tmpl { -- buf := cx.buf -- p := parseBuffer([]byte(buf)) -- if p.ParseErr != nil { -- t.Fatal(p.ParseErr) -- } -- type loc struct { -- offset int -- l, c uint32 -- } -- saved := []loc{} -- // forwards -- var lastl, lastc uint32 -- for offset := range buf { -- l, c := p.LineCol(offset) -- saved = append(saved, loc{offset, l, c}) -- if l > lastl { -- lastl = l -- if c != 0 { -- t.Errorf("line %d, got %d instead of 0", l, c) -- } -- } -- if c > lastc { -- lastc = c -- } -- } -- lines := strings.Split(buf, "\n") -- mxlen := -1 -- for _, l := range lines { -- if len(l) > mxlen { -- mxlen = len(l) -- } -- } -- if int(lastl) != len(lines)-1 && int(lastc) != mxlen { -- // lastl is 0 if there is only 1 line(?) -- t.Errorf("expected %d, %d, got %d, %d for case %d", len(lines)-1, mxlen, lastl, lastc, n) +-// toWindowsLineEndings checks whether content has windows line endings. +-// +-// If so, it returns content unmodified. If not, it returns a new byte slice modified to use CRLF line endings. +-func toWindowsLineEndings(content []byte) []byte { +- abnormal := false +- for i, b := range content { +- if b == '\n' && (i == 0 || content[i-1] != '\r') { +- abnormal = true +- break - } -- // backwards -- for j := len(saved) - 1; j >= 0; j-- { -- s := saved[j] -- xl, xc := p.LineCol(s.offset) -- if xl != s.l || xc != s.c { -- t.Errorf("at offset %d(%d), got (%d,%d), expected (%d,%d)", s.offset, j, xl, xc, s.l, s.c) -- } +- } +- if !abnormal { +- return content +- } +- var buf bytes.Buffer +- for i, b := range content { +- if b == '\n' && (i == 0 || content[i-1] != '\r') { +- buf.WriteByte('\r') - } +- buf.WriteByte(b) - } +- return buf.Bytes() -} - --func TestLineColNL(t *testing.T) { -- buf := "\n\n\n\n\n" -- p := parseBuffer([]byte(buf)) -- if p.ParseErr != nil { -- t.Fatal(p.ParseErr) +-// CreateBuffer creates a new unsaved buffer corresponding to the workdir path, +-// containing the given textual content. +-func (e *Editor) CreateBuffer(ctx context.Context, path, content string) error { +- return e.createBuffer(ctx, path, true, []byte(content)) +-} +- +-func (e *Editor) createBuffer(ctx context.Context, path string, dirty bool, content []byte) error { +- e.mu.Lock() +- +- if _, ok := e.buffers[path]; ok { +- e.mu.Unlock() +- return fmt.Errorf("buffer %q already exists", path) - } -- for i := 0; i < len(buf); i++ { -- l, c := p.LineCol(i) -- if c != 0 || int(l) != i+1 { -- t.Errorf("got (%d,%d), expected (%d,0)", l, c, i) -- } +- +- uri := e.sandbox.Workdir.URI(path) +- buf := buffer{ +- version: 1, +- path: path, +- mapper: protocol.NewMapper(uri, content), +- dirty: dirty, - } +- e.buffers[path] = buf +- +- item := e.textDocumentItem(buf) +- e.mu.Unlock() +- +- return e.sendDidOpen(ctx, item) -} - --func TestPos(t *testing.T) { -- buf := ` -- {{if (foÜx .X.Y)}}{{$A := "hi"}}{{.Z $A}}{{else}} -- {{$A.X 12}} -- {{foo (.X.Y) 23 ($A.Z)}} -- {{end}}` -- p := parseBuffer([]byte(buf)) -- if p.ParseErr != nil { -- t.Fatal(p.ParseErr) +-// textDocumentItem builds a protocol.TextDocumentItem for the given buffer. +-// +-// Precondition: e.mu must be held. +-func (e *Editor) textDocumentItem(buf buffer) protocol.TextDocumentItem { +- return protocol.TextDocumentItem{ +- URI: e.sandbox.Workdir.URI(buf.path), +- LanguageID: languageID(buf.path, e.config.FileAssociations), +- Version: int32(buf.version), +- Text: buf.text(), - } -- for pos, r := range buf { -- if r == '\n' { -- continue +-} +- +-func (e *Editor) sendDidOpen(ctx context.Context, item protocol.TextDocumentItem) error { +- if e.Server != nil { +- if err := e.Server.DidOpen(ctx, &protocol.DidOpenTextDocumentParams{ +- TextDocument: item, +- }); err != nil { +- return fmt.Errorf("DidOpen: %w", err) - } -- x := p.Position(pos) -- n := p.FromPosition(x) -- if n != pos { -- // once it's wrong, it will be wrong forever -- t.Fatalf("at pos %d (rune %c) got %d {%#v]", pos, r, n, x) +- e.callsMu.Lock() +- e.calls.DidOpen++ +- e.callsMu.Unlock() +- } +- return nil +-} +- +-var defaultFileAssociations = map[string]*regexp.Regexp{ +- "go": regexp.MustCompile(`^.*\.go$`), // '$' is important: don't match .gotmpl! +- "go.mod": regexp.MustCompile(`^go\.mod$`), +- "go.sum": regexp.MustCompile(`^go(\.work)?\.sum$`), +- "go.work": regexp.MustCompile(`^go\.work$`), +- "gotmpl": regexp.MustCompile(`^.*tmpl$`), +-} +- +-// languageID returns the language identifier for the path p given the user +-// configured fileAssociations. +-func languageID(p string, fileAssociations map[string]string) protocol.LanguageKind { +- base := path.Base(p) +- for lang, re := range fileAssociations { +- re := regexp.MustCompile(re) +- if re.MatchString(base) { +- return protocol.LanguageKind(lang) +- } +- } +- for lang, re := range defaultFileAssociations { +- if re.MatchString(base) { +- return protocol.LanguageKind(lang) - } +- } +- return "" +-} - +-// CloseBuffer removes the current buffer (regardless of whether it is saved). +-func (e *Editor) CloseBuffer(ctx context.Context, path string) error { +- e.mu.Lock() +- _, ok := e.buffers[path] +- if !ok { +- e.mu.Unlock() +- return ErrUnknownBuffer - } +- delete(e.buffers, path) +- e.mu.Unlock() +- +- return e.sendDidClose(ctx, e.TextDocumentIdentifier(path)) -} --func TestLen(t *testing.T) { -- data := []struct { -- cnt int -- v string -- }{{1, "a"}, {1, "膈"}, {4, "😆🥸"}, {7, "3😀4567"}} -- p := &Parsed{nonASCII: true} -- for _, d := range data { -- got := p.utf16len([]byte(d.v)) -- if got != d.cnt { -- t.Errorf("%v, got %d wanted %d", d, got, d.cnt) +- +-func (e *Editor) sendDidClose(ctx context.Context, doc protocol.TextDocumentIdentifier) error { +- if e.Server != nil { +- if err := e.Server.DidClose(ctx, &protocol.DidCloseTextDocumentParams{ +- TextDocument: doc, +- }); err != nil { +- return fmt.Errorf("DidClose: %w", err) - } +- e.callsMu.Lock() +- e.calls.DidClose++ +- e.callsMu.Unlock() - } +- return nil -} - --func TestUtf16(t *testing.T) { -- buf := ` -- {{if (foÜx .X.Y)}}😀{{$A := "hi"}}{{.Z $A}}{{else}} -- {{$A.X 12}} -- {{foo (.X.Y) 23 ($A.Z)}} -- {{end}}` -- p := parseBuffer([]byte(buf)) -- if p.nonASCII == false { -- t.Error("expected nonASCII to be true") +-func (e *Editor) TextDocumentIdentifier(path string) protocol.TextDocumentIdentifier { +- return protocol.TextDocumentIdentifier{ +- URI: e.sandbox.Workdir.URI(path), - } -} - --type ttest struct { -- tmpl string -- tokCnt int -- elidedCnt int8 +-// SaveBuffer writes the content of the buffer specified by the given path to +-// the filesystem. +-func (e *Editor) SaveBuffer(ctx context.Context, path string) error { +- if err := e.OrganizeImports(ctx, path); err != nil { +- return fmt.Errorf("organizing imports before save: %w", err) +- } +- if err := e.FormatBuffer(ctx, path); err != nil { +- return fmt.Errorf("formatting before save: %w", err) +- } +- return e.SaveBufferWithoutActions(ctx, path) -} - --func TestQuotes(t *testing.T) { -- tsts := []ttest{ -- {"{{- /*comment*/ -}}", 1, 0}, -- {"{{/*`\ncomment\n`*/}}", 1, 0}, -- //{"{{foo\nbar}}\n", 1, 0}, // this action spanning lines parses in 1.16 -- {"{{\"{{foo}}{{\"}}", 1, 0}, -- {"{{\n{{- when}}", 1, 1}, // corrected -- {"{{{{if .}}xx{{\n{{end}}", 2, 2}, // corrected +-func (e *Editor) SaveBufferWithoutActions(ctx context.Context, path string) error { +- e.mu.Lock() +- defer e.mu.Unlock() +- buf, ok := e.buffers[path] +- if !ok { +- return fmt.Errorf(fmt.Sprintf("unknown buffer: %q", path)) - } -- for _, s := range tsts { -- p := parseBuffer([]byte(s.tmpl)) -- if len(p.tokens) != s.tokCnt { -- t.Errorf("%q: got %d tokens, expected %d", s, len(p.tokens), s.tokCnt) +- content := buf.text() +- includeText := false +- syncOptions, ok := e.serverCapabilities.TextDocumentSync.(protocol.TextDocumentSyncOptions) +- if ok { +- includeText = syncOptions.Save.IncludeText +- } +- +- docID := e.TextDocumentIdentifier(buf.path) +- if e.Server != nil { +- if err := e.Server.WillSave(ctx, &protocol.WillSaveTextDocumentParams{ +- TextDocument: docID, +- Reason: protocol.Manual, +- }); err != nil { +- return fmt.Errorf("WillSave: %w", err) - } -- if p.ParseErr != nil { -- t.Errorf("%q: %v", string(p.buf), p.ParseErr) +- } +- if err := e.sandbox.Workdir.WriteFile(ctx, path, content); err != nil { +- return fmt.Errorf("writing %q: %w", path, err) +- } +- +- buf.dirty = false +- e.buffers[path] = buf +- +- if e.Server != nil { +- params := &protocol.DidSaveTextDocumentParams{ +- TextDocument: docID, - } -- if len(p.elided) != int(s.elidedCnt) { -- t.Errorf("%q: elided %d, expected %d", s, len(p.elided), s.elidedCnt) +- if includeText { +- params.Text = &content - } +- if err := e.Server.DidSave(ctx, params); err != nil { +- return fmt.Errorf("DidSave: %w", err) +- } +- e.callsMu.Lock() +- e.calls.DidSave++ +- e.callsMu.Unlock() - } +- return nil -} -diff -urN a/gopls/internal/lsp/template/symbols.go b/gopls/internal/lsp/template/symbols.go ---- a/gopls/internal/lsp/template/symbols.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/template/symbols.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,230 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package template -- --import ( -- "bytes" -- "context" -- "fmt" -- "text/template/parse" -- "unicode/utf8" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/internal/event" +-// ErrNoMatch is returned if a regexp search fails. +-var ( +- ErrNoMatch = errors.New("no match") +- ErrUnknownBuffer = errors.New("unknown buffer") -) - --// in local coordinates, to be translated to protocol.DocumentSymbol --type symbol struct { -- start int // for sorting -- length int // in runes (unicode code points) -- name string -- kind protocol.SymbolKind -- vardef bool // is this a variable definition? -- // do we care about selection range, or children? -- // no children yet, and selection range is the same as range +-// regexpLocation returns the location of the first occurrence of either re +-// or its singular subgroup. It returns ErrNoMatch if the regexp doesn't match. +-func regexpLocation(mapper *protocol.Mapper, re string) (protocol.Location, error) { +- var start, end int +- rec, err := regexp.Compile(re) +- if err != nil { +- return protocol.Location{}, err +- } +- indexes := rec.FindSubmatchIndex(mapper.Content) +- if indexes == nil { +- return protocol.Location{}, ErrNoMatch +- } +- switch len(indexes) { +- case 2: +- // no subgroups: return the range of the regexp expression +- start, end = indexes[0], indexes[1] +- case 4: +- // one subgroup: return its range +- start, end = indexes[2], indexes[3] +- default: +- return protocol.Location{}, fmt.Errorf("invalid search regexp %q: expect either 0 or 1 subgroups, got %d", re, len(indexes)/2-1) +- } +- return mapper.OffsetLocation(start, end) -} - --func (s symbol) String() string { -- return fmt.Sprintf("{%d,%d,%s,%s,%v}", s.start, s.length, s.name, s.kind, s.vardef) +-// RegexpSearch returns the Location of the first match for re in the buffer +-// bufName. For convenience, RegexpSearch supports the following two modes: +-// 1. If re has no subgroups, return the position of the match for re itself. +-// 2. If re has one subgroup, return the position of the first subgroup. +-// +-// It returns an error re is invalid, has more than one subgroup, or doesn't +-// match the buffer. +-func (e *Editor) RegexpSearch(bufName, re string) (protocol.Location, error) { +- e.mu.Lock() +- buf, ok := e.buffers[bufName] +- e.mu.Unlock() +- if !ok { +- return protocol.Location{}, ErrUnknownBuffer +- } +- return regexpLocation(buf.mapper, re) -} - --// for FieldNode or VariableNode (or ChainNode?) --func (p *Parsed) fields(flds []string, x parse.Node) []symbol { -- ans := []symbol{} -- // guessing that there are no embedded blanks allowed. The doc is unclear -- lookfor := "" -- switch x.(type) { -- case *parse.FieldNode: -- for _, f := range flds { -- lookfor += "." + f // quadratic, but probably ok -- } -- case *parse.VariableNode: -- lookfor = flds[0] -- for i := 1; i < len(flds); i++ { -- lookfor += "." + flds[i] -- } -- case *parse.ChainNode: // PJW, what are these? -- for _, f := range flds { -- lookfor += "." + f // quadratic, but probably ok -- } -- default: -- // If these happen they will happen even if gopls is restarted -- // and the users does the same thing, so it is better not to panic. -- // context.Background() is used because we don't have access -- // to any other context. [we could, but it would be complicated] -- event.Log(context.Background(), fmt.Sprintf("%T unexpected in fields()", x)) -- return nil -- } -- if len(lookfor) == 0 { -- event.Log(context.Background(), fmt.Sprintf("no strings in fields() %#v", x)) -- return nil +-// RegexpReplace edits the buffer corresponding to path by replacing the first +-// instance of re, or its first subgroup, with the replace text. See +-// RegexpSearch for more explanation of these two modes. +-// It returns an error if re is invalid, has more than one subgroup, or doesn't +-// match the buffer. +-func (e *Editor) RegexpReplace(ctx context.Context, path, re, replace string) error { +- e.mu.Lock() +- defer e.mu.Unlock() +- buf, ok := e.buffers[path] +- if !ok { +- return ErrUnknownBuffer - } -- startsAt := int(x.Position()) -- ix := bytes.Index(p.buf[startsAt:], []byte(lookfor)) // HasPrefix? PJW? -- if ix < 0 || ix > len(lookfor) { // lookfor expected to be at start (or so) -- // probably golang.go/#43388, so back up -- startsAt -= len(flds[0]) + 1 -- ix = bytes.Index(p.buf[startsAt:], []byte(lookfor)) // ix might be 1? PJW -- if ix < 0 { -- return ans -- } +- loc, err := regexpLocation(buf.mapper, re) +- if err != nil { +- return err - } -- at := ix + startsAt -- for _, f := range flds { -- at += 1 // . -- kind := protocol.Method -- if f[0] == '$' { -- kind = protocol.Variable -- } -- sym := symbol{name: f, kind: kind, start: at, length: utf8.RuneCount([]byte(f))} -- if kind == protocol.Variable && len(p.stack) > 1 { -- if pipe, ok := p.stack[len(p.stack)-2].(*parse.PipeNode); ok { -- for _, y := range pipe.Decl { -- if x == y { -- sym.vardef = true -- } -- } -- } -- } -- ans = append(ans, sym) -- at += len(f) +- edits := []protocol.TextEdit{{ +- Range: loc.Range, +- NewText: replace, +- }} +- patched, err := applyEdits(buf.mapper, edits, e.config.WindowsLineEndings) +- if err != nil { +- return fmt.Errorf("editing %q: %v", path, err) - } -- return ans +- return e.setBufferContentLocked(ctx, path, true, patched, edits) -} - --func (p *Parsed) findSymbols() { -- if len(p.stack) == 0 { -- return +-// EditBuffer applies the given test edits to the buffer identified by path. +-func (e *Editor) EditBuffer(ctx context.Context, path string, edits []protocol.TextEdit) error { +- e.mu.Lock() +- defer e.mu.Unlock() +- return e.editBufferLocked(ctx, path, edits) +-} +- +-func (e *Editor) SetBufferContent(ctx context.Context, path, content string) error { +- e.mu.Lock() +- defer e.mu.Unlock() +- return e.setBufferContentLocked(ctx, path, true, []byte(content), nil) +-} +- +-// HasBuffer reports whether the file name is open in the editor. +-func (e *Editor) HasBuffer(name string) bool { +- e.mu.Lock() +- defer e.mu.Unlock() +- _, ok := e.buffers[name] +- return ok +-} +- +-// BufferText returns the content of the buffer with the given name, or "" if +-// the file at that path is not open. The second return value reports whether +-// the file is open. +-func (e *Editor) BufferText(name string) (string, bool) { +- e.mu.Lock() +- defer e.mu.Unlock() +- buf, ok := e.buffers[name] +- if !ok { +- return "", false - } -- n := p.stack[len(p.stack)-1] -- pop := func() { -- p.stack = p.stack[:len(p.stack)-1] +- return buf.text(), true +-} +- +-// Mapper returns the protocol.Mapper for the given buffer name, if it is open. +-func (e *Editor) Mapper(name string) (*protocol.Mapper, error) { +- e.mu.Lock() +- defer e.mu.Unlock() +- buf, ok := e.buffers[name] +- if !ok { +- return nil, fmt.Errorf("no mapper for %q", name) - } -- if n == nil { // allowing nil simplifies the code -- pop() -- return +- return buf.mapper, nil +-} +- +-// BufferVersion returns the current version of the buffer corresponding to +-// name (or 0 if it is not being edited). +-func (e *Editor) BufferVersion(name string) int { +- e.mu.Lock() +- defer e.mu.Unlock() +- return e.buffers[name].version +-} +- +-func (e *Editor) editBufferLocked(ctx context.Context, path string, edits []protocol.TextEdit) error { +- buf, ok := e.buffers[path] +- if !ok { +- return fmt.Errorf("unknown buffer %q", path) - } -- nxt := func(nd parse.Node) { -- p.stack = append(p.stack, nd) -- p.findSymbols() +- content, err := applyEdits(buf.mapper, edits, e.config.WindowsLineEndings) +- if err != nil { +- return fmt.Errorf("editing %q: %v; edits:\n%v", path, err, edits) - } -- switch x := n.(type) { -- case *parse.ActionNode: -- nxt(x.Pipe) -- case *parse.BoolNode: -- // need to compute the length from the value -- msg := fmt.Sprintf("%v", x.True) -- p.symbols = append(p.symbols, symbol{start: int(x.Pos), length: len(msg), kind: protocol.Boolean}) -- case *parse.BranchNode: -- nxt(x.Pipe) -- nxt(x.List) -- nxt(x.ElseList) -- case *parse.ChainNode: -- p.symbols = append(p.symbols, p.fields(x.Field, x)...) -- nxt(x.Node) -- case *parse.CommandNode: -- for _, a := range x.Args { -- nxt(a) -- } -- //case *parse.CommentNode: // go 1.16 -- // log.Printf("implement %d", x.Type()) -- case *parse.DotNode: -- sym := symbol{name: "dot", kind: protocol.Variable, start: int(x.Pos), length: 1} -- p.symbols = append(p.symbols, sym) -- case *parse.FieldNode: -- p.symbols = append(p.symbols, p.fields(x.Ident, x)...) -- case *parse.IdentifierNode: -- sym := symbol{name: x.Ident, kind: protocol.Function, start: int(x.Pos), -- length: utf8.RuneCount([]byte(x.Ident))} -- p.symbols = append(p.symbols, sym) -- case *parse.IfNode: -- nxt(&x.BranchNode) -- case *parse.ListNode: -- if x != nil { // wretched typed nils. Node should have an IfNil -- for _, nd := range x.Nodes { -- nxt(nd) -- } -- } -- case *parse.NilNode: -- sym := symbol{name: "nil", kind: protocol.Constant, start: int(x.Pos), length: 3} -- p.symbols = append(p.symbols, sym) -- case *parse.NumberNode: -- // no name; ascii -- p.symbols = append(p.symbols, symbol{start: int(x.Pos), length: len(x.Text), kind: protocol.Number}) -- case *parse.PipeNode: -- if x == nil { // {{template "foo"}} -- return -- } -- for _, d := range x.Decl { -- nxt(d) -- } -- for _, c := range x.Cmds { -- nxt(c) -- } -- case *parse.RangeNode: -- nxt(&x.BranchNode) -- case *parse.StringNode: -- // no name -- sz := utf8.RuneCount([]byte(x.Text)) -- p.symbols = append(p.symbols, symbol{start: int(x.Pos), length: sz, kind: protocol.String}) -- case *parse.TemplateNode: // invoking a template -- // x.Pos points to the quote before the name -- p.symbols = append(p.symbols, symbol{name: x.Name, kind: protocol.Package, start: int(x.Pos) + 1, -- length: utf8.RuneCount([]byte(x.Name))}) -- nxt(x.Pipe) -- case *parse.TextNode: -- if len(x.Text) == 1 && x.Text[0] == '\n' { -- break -- } -- // nothing to report, but build one for hover -- sz := utf8.RuneCount([]byte(x.Text)) -- p.symbols = append(p.symbols, symbol{start: int(x.Pos), length: sz, kind: protocol.Constant}) -- case *parse.VariableNode: -- p.symbols = append(p.symbols, p.fields(x.Ident, x)...) -- case *parse.WithNode: -- nxt(&x.BranchNode) +- return e.setBufferContentLocked(ctx, path, true, content, edits) +-} - +-func (e *Editor) setBufferContentLocked(ctx context.Context, path string, dirty bool, content []byte, fromEdits []protocol.TextEdit) error { +- buf, ok := e.buffers[path] +- if !ok { +- return fmt.Errorf("unknown buffer %q", path) - } -- pop() --} +- buf.mapper = protocol.NewMapper(buf.mapper.URI, content) +- buf.version++ +- buf.dirty = dirty +- e.buffers[path] = buf - --// DocumentSymbols returns a hierarchy of the symbols defined in a template file. --// (The hierarchy is flat. SymbolInformation might be better.) --func DocumentSymbols(snapshot source.Snapshot, fh source.FileHandle) ([]protocol.DocumentSymbol, error) { -- buf, err := fh.Content() -- if err != nil { -- return nil, err +- // A simple heuristic: if there is only one edit, send it incrementally. +- // Otherwise, send the entire content. +- var evt protocol.TextDocumentContentChangeEvent +- if len(fromEdits) == 1 { +- evt.Range = &fromEdits[0].Range +- evt.Text = fromEdits[0].NewText +- } else { +- evt.Text = buf.text() - } -- p := parseBuffer(buf) -- if p.ParseErr != nil { -- return nil, p.ParseErr +- params := &protocol.DidChangeTextDocumentParams{ +- TextDocument: protocol.VersionedTextDocumentIdentifier{ +- Version: int32(buf.version), +- TextDocumentIdentifier: e.TextDocumentIdentifier(buf.path), +- }, +- ContentChanges: []protocol.TextDocumentContentChangeEvent{evt}, - } -- var ans []protocol.DocumentSymbol -- for _, s := range p.symbols { -- if s.kind == protocol.Constant { -- continue -- } -- d := kindStr(s.kind) -- if d == "Namespace" { -- d = "Template" -- } -- if s.vardef { -- d += "(def)" -- } else { -- d += "(use)" -- } -- r := p.Range(s.start, s.length) -- y := protocol.DocumentSymbol{ -- Name: s.name, -- Detail: d, -- Kind: s.kind, -- Range: r, -- SelectionRange: r, // or should this be the entire {{...}}? +- if e.Server != nil { +- if err := e.Server.DidChange(ctx, params); err != nil { +- return fmt.Errorf("DidChange: %w", err) - } -- ans = append(ans, y) +- e.callsMu.Lock() +- e.calls.DidChange++ +- e.callsMu.Unlock() - } -- return ans, nil +- return nil -} -diff -urN a/gopls/internal/lsp/testdata/addimport/addimport.go.golden b/gopls/internal/lsp/testdata/addimport/addimport.go.golden ---- a/gopls/internal/lsp/testdata/addimport/addimport.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/addimport/addimport.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,7 +0,0 @@ ---- addimport -- --package addimport //@addimport("", "bytes") - --import "bytes" -- --func main() {} +-// GoToDefinition jumps to the definition of the symbol at the given position +-// in an open buffer. It returns the location of the resulting jump. +-func (e *Editor) Definition(ctx context.Context, loc protocol.Location) (protocol.Location, error) { +- if err := e.checkBufferLocation(loc); err != nil { +- return protocol.Location{}, err +- } +- params := &protocol.DefinitionParams{} +- params.TextDocument.URI = loc.URI +- params.Position = loc.Range.Start - -diff -urN a/gopls/internal/lsp/testdata/addimport/addimport.go.in b/gopls/internal/lsp/testdata/addimport/addimport.go.in ---- a/gopls/internal/lsp/testdata/addimport/addimport.go.in 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/addimport/addimport.go.in 1970-01-01 00:00:00.000000000 +0000 -@@ -1,3 +0,0 @@ --package addimport //@addimport("", "bytes") +- resp, err := e.Server.Definition(ctx, params) +- if err != nil { +- return protocol.Location{}, fmt.Errorf("definition: %w", err) +- } +- return e.extractFirstLocation(ctx, resp) +-} - --func main() {} -diff -urN a/gopls/internal/lsp/testdata/callhierarchy/callhierarchy.go b/gopls/internal/lsp/testdata/callhierarchy/callhierarchy.go ---- a/gopls/internal/lsp/testdata/callhierarchy/callhierarchy.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/callhierarchy/callhierarchy.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,70 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-// TypeDefinition jumps to the type definition of the symbol at the given +-// location in an open buffer. +-func (e *Editor) TypeDefinition(ctx context.Context, loc protocol.Location) (protocol.Location, error) { +- if err := e.checkBufferLocation(loc); err != nil { +- return protocol.Location{}, err +- } +- params := &protocol.TypeDefinitionParams{} +- params.TextDocument.URI = loc.URI +- params.Position = loc.Range.Start - --package callhierarchy +- resp, err := e.Server.TypeDefinition(ctx, params) +- if err != nil { +- return protocol.Location{}, fmt.Errorf("type definition: %w", err) +- } +- return e.extractFirstLocation(ctx, resp) +-} - --import "golang.org/lsptests/callhierarchy/outgoing" +-// extractFirstLocation returns the first location. +-// It opens the file if needed. +-func (e *Editor) extractFirstLocation(ctx context.Context, locs []protocol.Location) (protocol.Location, error) { +- if len(locs) == 0 { +- return protocol.Location{}, nil +- } - --func a() { //@mark(hierarchyA, "a") -- D() +- newPath := e.sandbox.Workdir.URIToPath(locs[0].URI) +- if !e.HasBuffer(newPath) { +- if err := e.OpenFile(ctx, newPath); err != nil { +- return protocol.Location{}, fmt.Errorf("OpenFile: %w", err) +- } +- } +- return locs[0], nil -} - --func b() { //@mark(hierarchyB, "b") -- D() +-// Symbol performs a workspace symbol search using query +-func (e *Editor) Symbol(ctx context.Context, query string) ([]protocol.SymbolInformation, error) { +- params := &protocol.WorkspaceSymbolParams{Query: query} +- return e.Server.Symbol(ctx, params) -} - --// C is an exported function --func C() { //@mark(hierarchyC, "C") -- D() -- D() +-// OrganizeImports requests and performs the source.organizeImports codeAction. +-func (e *Editor) OrganizeImports(ctx context.Context, path string) error { +- loc := protocol.Location{URI: e.sandbox.Workdir.URI(path)} // zero Range => whole file +- _, err := e.applyCodeActions(ctx, loc, nil, protocol.SourceOrganizeImports) +- return err -} - --// To test hierarchy across function literals --var x = func() { //@mark(hierarchyLiteral, "func"),mark(hierarchyLiteralOut, "x") -- D() +-// RefactorRewrite requests and performs the source.refactorRewrite codeAction. +-func (e *Editor) RefactorRewrite(ctx context.Context, loc protocol.Location) error { +- applied, err := e.applyCodeActions(ctx, loc, nil, protocol.RefactorRewrite) +- if err != nil { +- return err +- } +- if applied == 0 { +- return fmt.Errorf("no refactorings were applied") +- } +- return nil -} - --// D is exported to test incoming/outgoing calls across packages --func D() { //@mark(hierarchyD, "D"),incomingcalls(hierarchyD, hierarchyA, hierarchyB, hierarchyC, hierarchyLiteral, incomingA),outgoingcalls(hierarchyD, hierarchyE, hierarchyF, hierarchyG, hierarchyLiteralOut, outgoingB, hierarchyFoo, hierarchyH, hierarchyI, hierarchyJ, hierarchyK) -- e() -- x() -- F() -- outgoing.B() -- foo := func() {} //@mark(hierarchyFoo, "foo"),incomingcalls(hierarchyFoo, hierarchyD),outgoingcalls(hierarchyFoo) -- foo() -- -- func() { -- g() -- }() -- -- var i Interface = impl{} -- i.H() -- i.I() -- -- s := Struct{} -- s.J() -- s.K() +-// ApplyQuickFixes requests and performs the quickfix codeAction. +-func (e *Editor) ApplyQuickFixes(ctx context.Context, loc protocol.Location, diagnostics []protocol.Diagnostic) error { +- applied, err := e.applyCodeActions(ctx, loc, diagnostics, protocol.SourceFixAll, protocol.QuickFix) +- if applied == 0 { +- return fmt.Errorf("no quick fixes were applied") +- } +- return err -} - --func e() {} //@mark(hierarchyE, "e") -- --// F is an exported function --func F() {} //@mark(hierarchyF, "F") -- --func g() {} //@mark(hierarchyG, "g") +-// ApplyCodeAction applies the given code action. +-func (e *Editor) ApplyCodeAction(ctx context.Context, action protocol.CodeAction) error { +- // Resolve the code actions if necessary and supported. +- if action.Edit == nil { +- editSupport, err := e.EditResolveSupport() +- if err != nil { +- return err +- } +- if editSupport { +- ca, err := e.Server.ResolveCodeAction(ctx, &action) +- if err != nil { +- return err +- } +- action.Edit = ca.Edit +- } +- } - --type Interface interface { -- H() //@mark(hierarchyH, "H") -- I() //@mark(hierarchyI, "I") +- if action.Edit != nil { +- for _, change := range action.Edit.DocumentChanges { +- if change.TextDocumentEdit != nil { +- path := e.sandbox.Workdir.URIToPath(change.TextDocumentEdit.TextDocument.URI) +- if int32(e.buffers[path].version) != change.TextDocumentEdit.TextDocument.Version { +- // Skip edits for old versions. +- continue +- } +- if err := e.EditBuffer(ctx, path, protocol.AsTextEdits(change.TextDocumentEdit.Edits)); err != nil { +- return fmt.Errorf("editing buffer %q: %w", path, err) +- } +- } +- } +- } +- // Execute any commands. The specification says that commands are +- // executed after edits are applied. +- if action.Command != nil { +- if _, err := e.ExecuteCommand(ctx, &protocol.ExecuteCommandParams{ +- Command: action.Command.Command, +- Arguments: action.Command.Arguments, +- }); err != nil { +- return err +- } +- } +- // Some commands may edit files on disk. +- return e.sandbox.Workdir.CheckForFileChanges(ctx) -} - --type impl struct{} -- --func (i impl) H() {} --func (i impl) I() {} -- --type Struct struct { -- J func() //@mark(hierarchyJ, "J") -- K func() //@mark(hierarchyK, "K") +-// GetQuickFixes returns the available quick fix code actions. +-func (e *Editor) GetQuickFixes(ctx context.Context, loc protocol.Location, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) { +- return e.CodeActions(ctx, loc, diagnostics, protocol.QuickFix, protocol.SourceFixAll) -} -diff -urN a/gopls/internal/lsp/testdata/callhierarchy/incoming/incoming.go b/gopls/internal/lsp/testdata/callhierarchy/incoming/incoming.go ---- a/gopls/internal/lsp/testdata/callhierarchy/incoming/incoming.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/callhierarchy/incoming/incoming.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,12 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package incoming - --import "golang.org/lsptests/callhierarchy" +-func (e *Editor) applyCodeActions(ctx context.Context, loc protocol.Location, diagnostics []protocol.Diagnostic, only ...protocol.CodeActionKind) (int, error) { +- actions, err := e.CodeActions(ctx, loc, diagnostics, only...) +- if err != nil { +- return 0, err +- } +- applied := 0 +- for _, action := range actions { +- if action.Title == "" { +- return 0, fmt.Errorf("empty title for code action") +- } +- var match bool +- for _, o := range only { +- if action.Kind == o { +- match = true +- break +- } +- } +- if !match { +- continue +- } +- applied++ +- if err := e.ApplyCodeAction(ctx, action); err != nil { +- return 0, err +- } +- } +- return applied, nil +-} - --// A is exported to test incoming calls across packages --func A() { //@mark(incomingA, "A") -- callhierarchy.D() +-func (e *Editor) CodeActions(ctx context.Context, loc protocol.Location, diagnostics []protocol.Diagnostic, only ...protocol.CodeActionKind) ([]protocol.CodeAction, error) { +- if e.Server == nil { +- return nil, nil +- } +- params := &protocol.CodeActionParams{} +- params.TextDocument.URI = loc.URI +- params.Context.Only = only +- params.Range = loc.Range // may be zero => whole file +- if diagnostics != nil { +- params.Context.Diagnostics = diagnostics +- } +- return e.Server.CodeAction(ctx, params) -} -diff -urN a/gopls/internal/lsp/testdata/callhierarchy/outgoing/outgoing.go b/gopls/internal/lsp/testdata/callhierarchy/outgoing/outgoing.go ---- a/gopls/internal/lsp/testdata/callhierarchy/outgoing/outgoing.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/callhierarchy/outgoing/outgoing.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,9 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package outgoing +-func (e *Editor) ExecuteCommand(ctx context.Context, params *protocol.ExecuteCommandParams) (interface{}, error) { +- if e.Server == nil { +- return nil, nil +- } +- var match bool +- if e.serverCapabilities.ExecuteCommandProvider != nil { +- // Ensure that this command was actually listed as a supported command. +- for _, command := range e.serverCapabilities.ExecuteCommandProvider.Commands { +- if command == params.Command { +- match = true +- break +- } +- } +- } +- if !match { +- return nil, fmt.Errorf("unsupported command %q", params.Command) +- } +- result, err := e.Server.ExecuteCommand(ctx, params) +- if err != nil { +- return nil, err +- } +- // Some commands use the go command, which writes directly to disk. +- // For convenience, check for those changes. +- if err := e.sandbox.Workdir.CheckForFileChanges(ctx); err != nil { +- return nil, fmt.Errorf("checking for file changes: %v", err) +- } +- return result, nil +-} - --// B is exported to test outgoing calls across packages --func B() { //@mark(outgoingB, "B") +-// FormatBuffer gofmts a Go file. +-func (e *Editor) FormatBuffer(ctx context.Context, path string) error { +- if e.Server == nil { +- return nil +- } +- e.mu.Lock() +- version := e.buffers[path].version +- e.mu.Unlock() +- params := &protocol.DocumentFormattingParams{} +- params.TextDocument.URI = e.sandbox.Workdir.URI(path) +- edits, err := e.Server.Formatting(ctx, params) +- if err != nil { +- return fmt.Errorf("textDocument/formatting: %w", err) +- } +- e.mu.Lock() +- defer e.mu.Unlock() +- if versionAfter := e.buffers[path].version; versionAfter != version { +- return fmt.Errorf("before receipt of formatting edits, buffer version changed from %d to %d", version, versionAfter) +- } +- if len(edits) == 0 { +- return nil +- } +- return e.editBufferLocked(ctx, path, edits) -} -diff -urN a/gopls/internal/lsp/testdata/embeddirective/embed.txt b/gopls/internal/lsp/testdata/embeddirective/embed.txt ---- a/gopls/internal/lsp/testdata/embeddirective/embed.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/embeddirective/embed.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1 +0,0 @@ --text -diff -urN a/gopls/internal/lsp/testdata/embeddirective/fix_import.go b/gopls/internal/lsp/testdata/embeddirective/fix_import.go ---- a/gopls/internal/lsp/testdata/embeddirective/fix_import.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/embeddirective/fix_import.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,18 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package embeddirective +-func (e *Editor) checkBufferLocation(loc protocol.Location) error { +- e.mu.Lock() +- defer e.mu.Unlock() +- path := e.sandbox.Workdir.URIToPath(loc.URI) +- buf, ok := e.buffers[path] +- if !ok { +- return fmt.Errorf("buffer %q is not open", path) +- } - --import ( -- "io" -- "os" --) +- _, _, err := buf.mapper.RangeOffsets(loc.Range) +- return err +-} - --//go:embed embed.txt //@suggestedfix("//go:embed", "quickfix", "") --var t string +-// RunGenerate runs `go generate` non-recursively in the workdir-relative dir +-// path. It does not report any resulting file changes as a watched file +-// change, so must be followed by a call to Workdir.CheckForFileChanges once +-// the generate command has completed. +-// TODO(rFindley): this shouldn't be necessary anymore. Delete it. +-func (e *Editor) RunGenerate(ctx context.Context, dir string) error { +- if e.Server == nil { +- return nil +- } +- absDir := e.sandbox.Workdir.AbsPath(dir) +- cmd, err := command.NewGenerateCommand("", command.GenerateArgs{ +- Dir: protocol.URIFromPath(absDir), +- Recursive: false, +- }) +- if err != nil { +- return err +- } +- params := &protocol.ExecuteCommandParams{ +- Command: cmd.Command, +- Arguments: cmd.Arguments, +- } +- if _, err := e.ExecuteCommand(ctx, params); err != nil { +- return fmt.Errorf("running generate: %v", err) +- } +- // Unfortunately we can't simply poll the workdir for file changes here, +- // because server-side command may not have completed. In integration tests, we can +- // Await this state change, but here we must delegate that responsibility to +- // the caller. +- return nil +-} - --func unused() { -- _ = os.Stdin -- _ = io.EOF +-// CodeLens executes a codelens request on the server. +-func (e *Editor) CodeLens(ctx context.Context, path string) ([]protocol.CodeLens, error) { +- if e.Server == nil { +- return nil, nil +- } +- e.mu.Lock() +- _, ok := e.buffers[path] +- e.mu.Unlock() +- if !ok { +- return nil, fmt.Errorf("buffer %q is not open", path) +- } +- params := &protocol.CodeLensParams{ +- TextDocument: e.TextDocumentIdentifier(path), +- } +- lens, err := e.Server.CodeLens(ctx, params) +- if err != nil { +- return nil, err +- } +- return lens, nil -} -diff -urN a/gopls/internal/lsp/testdata/embeddirective/fix_import.go.golden b/gopls/internal/lsp/testdata/embeddirective/fix_import.go.golden ---- a/gopls/internal/lsp/testdata/embeddirective/fix_import.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/embeddirective/fix_import.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,21 +0,0 @@ ---- suggestedfix_fix_import_12_1 -- --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package embeddirective +-// Completion executes a completion request on the server. +-func (e *Editor) Completion(ctx context.Context, loc protocol.Location) (*protocol.CompletionList, error) { +- if e.Server == nil { +- return nil, nil +- } +- path := e.sandbox.Workdir.URIToPath(loc.URI) +- e.mu.Lock() +- _, ok := e.buffers[path] +- e.mu.Unlock() +- if !ok { +- return nil, fmt.Errorf("buffer %q is not open", path) +- } +- params := &protocol.CompletionParams{ +- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), +- } +- completions, err := e.Server.Completion(ctx, params) +- if err != nil { +- return nil, err +- } +- return completions, nil +-} - --import ( -- _ "embed" -- "io" -- "os" --) +-// AcceptCompletion accepts a completion for the given item at the given +-// position. +-func (e *Editor) AcceptCompletion(ctx context.Context, loc protocol.Location, item protocol.CompletionItem) error { +- if e.Server == nil { +- return nil +- } +- e.mu.Lock() +- defer e.mu.Unlock() +- path := e.sandbox.Workdir.URIToPath(loc.URI) +- _, ok := e.buffers[path] +- if !ok { +- return fmt.Errorf("buffer %q is not open", path) +- } +- return e.editBufferLocked(ctx, path, append([]protocol.TextEdit{ +- *item.TextEdit, +- }, item.AdditionalTextEdits...)) +-} - --//go:embed embed.txt //@suggestedfix("//go:embed", "quickfix", "") --var t string +-// Symbols executes a workspace/symbols request on the server. +-func (e *Editor) Symbols(ctx context.Context, sym string) ([]protocol.SymbolInformation, error) { +- if e.Server == nil { +- return nil, nil +- } +- params := &protocol.WorkspaceSymbolParams{Query: sym} +- ans, err := e.Server.Symbol(ctx, params) +- return ans, err +-} - --func unused() { -- _ = os.Stdin -- _ = io.EOF +-// CodeLens executes a codelens request on the server. +-func (e *Editor) InlayHint(ctx context.Context, path string) ([]protocol.InlayHint, error) { +- if e.Server == nil { +- return nil, nil +- } +- e.mu.Lock() +- _, ok := e.buffers[path] +- e.mu.Unlock() +- if !ok { +- return nil, fmt.Errorf("buffer %q is not open", path) +- } +- params := &protocol.InlayHintParams{ +- TextDocument: e.TextDocumentIdentifier(path), +- } +- hints, err := e.Server.InlayHint(ctx, params) +- if err != nil { +- return nil, err +- } +- return hints, nil -} - -diff -urN a/gopls/internal/lsp/testdata/inlay_hint/composite_literals.go b/gopls/internal/lsp/testdata/inlay_hint/composite_literals.go ---- a/gopls/internal/lsp/testdata/inlay_hint/composite_literals.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/inlay_hint/composite_literals.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,27 +0,0 @@ --package inlayHint //@inlayHint("package") +-// References returns references to the object at loc, as returned by +-// the connected LSP server. If no server is connected, it returns (nil, nil). +-func (e *Editor) References(ctx context.Context, loc protocol.Location) ([]protocol.Location, error) { +- if e.Server == nil { +- return nil, nil +- } +- path := e.sandbox.Workdir.URIToPath(loc.URI) +- e.mu.Lock() +- _, ok := e.buffers[path] +- e.mu.Unlock() +- if !ok { +- return nil, fmt.Errorf("buffer %q is not open", path) +- } +- params := &protocol.ReferenceParams{ +- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), +- Context: protocol.ReferenceContext{ +- IncludeDeclaration: true, +- }, +- } +- locations, err := e.Server.References(ctx, params) +- if err != nil { +- return nil, err +- } +- return locations, nil +-} - --import "fmt" +-// Rename performs a rename of the object at loc to newName, using the +-// connected LSP server. If no server is connected, it returns nil. +-func (e *Editor) Rename(ctx context.Context, loc protocol.Location, newName string) error { +- if e.Server == nil { +- return nil +- } +- path := e.sandbox.Workdir.URIToPath(loc.URI) - --func fieldNames() { -- for _, c := range []struct { -- in, want string -- }{ -- struct{ in, want string }{"Hello, world", "dlrow ,olleH"}, -- {"Hello, 世界", "界世 ,olleH"}, -- {"", ""}, -- } { -- fmt.Println(c.in == c.want) +- // Verify that PrepareRename succeeds. +- prepareParams := &protocol.PrepareRenameParams{} +- prepareParams.TextDocument = e.TextDocumentIdentifier(path) +- prepareParams.Position = loc.Range.Start +- if _, err := e.Server.PrepareRename(ctx, prepareParams); err != nil { +- return fmt.Errorf("preparing rename: %v", err) - } --} - --func fieldNamesPointers() { -- for _, c := range []*struct { -- in, want string -- }{ -- &struct{ in, want string }{"Hello, world", "dlrow ,olleH"}, -- {"Hello, 世界", "界世 ,olleH"}, -- {"", ""}, -- } { -- fmt.Println(c.in == c.want) +- params := &protocol.RenameParams{ +- TextDocument: e.TextDocumentIdentifier(path), +- Position: loc.Range.Start, +- NewName: newName, +- } +- wsEdits, err := e.Server.Rename(ctx, params) +- if err != nil { +- return err +- } +- for _, change := range wsEdits.DocumentChanges { +- if err := e.applyDocumentChange(ctx, change); err != nil { +- return err +- } - } +- return nil -} -diff -urN a/gopls/internal/lsp/testdata/inlay_hint/composite_literals.go.golden b/gopls/internal/lsp/testdata/inlay_hint/composite_literals.go.golden ---- a/gopls/internal/lsp/testdata/inlay_hint/composite_literals.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/inlay_hint/composite_literals.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,29 +0,0 @@ ---- inlayHint -- --package inlayHint //@inlayHint("package") -- --import "fmt" - --func fieldNames() { -- for _< int>, c< struct{in string; want string}> := range []struct { -- in, want string -- }{ -- struct{ in, want string }{<in: >"Hello, world", <want: >"dlrow ,olleH"}, -- <struct{in string; want string}>{<in: >"Hello, 世界", <want: >"界世 ,olleH"}, -- <struct{in string; want string}>{<in: >"", <want: >""}, -- } { -- fmt.Println(<a...: >c.in == c.want) +-// Implementations returns implementations for the object at loc, as +-// returned by the connected LSP server. If no server is connected, it returns +-// (nil, nil). +-func (e *Editor) Implementations(ctx context.Context, loc protocol.Location) ([]protocol.Location, error) { +- if e.Server == nil { +- return nil, nil +- } +- path := e.sandbox.Workdir.URIToPath(loc.URI) +- e.mu.Lock() +- _, ok := e.buffers[path] +- e.mu.Unlock() +- if !ok { +- return nil, fmt.Errorf("buffer %q is not open", path) - } +- params := &protocol.ImplementationParams{ +- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), +- } +- return e.Server.Implementation(ctx, params) -} - --func fieldNamesPointers() { -- for _< int>, c< *struct{in string; want string}> := range []*struct { -- in, want string -- }{ -- &struct{ in, want string }{<in: >"Hello, world", <want: >"dlrow ,olleH"}, -- <&struct{in string; want string}>{<in: >"Hello, 世界", <want: >"界世 ,olleH"}, -- <&struct{in string; want string}>{<in: >"", <want: >""}, -- } { -- fmt.Println(<a...: >c.in == c.want) +-func (e *Editor) SignatureHelp(ctx context.Context, loc protocol.Location) (*protocol.SignatureHelp, error) { +- if e.Server == nil { +- return nil, nil +- } +- path := e.sandbox.Workdir.URIToPath(loc.URI) +- e.mu.Lock() +- _, ok := e.buffers[path] +- e.mu.Unlock() +- if !ok { +- return nil, fmt.Errorf("buffer %q is not open", path) +- } +- params := &protocol.SignatureHelpParams{ +- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), - } +- return e.Server.SignatureHelp(ctx, params) -} - -diff -urN a/gopls/internal/lsp/testdata/inlay_hint/constant_values.go b/gopls/internal/lsp/testdata/inlay_hint/constant_values.go ---- a/gopls/internal/lsp/testdata/inlay_hint/constant_values.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/inlay_hint/constant_values.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,45 +0,0 @@ --package inlayHint //@inlayHint("package") -- --const True = true -- --type Kind int -- --const ( -- KindNone Kind = iota -- KindPrint -- KindPrintf -- KindErrorf --) -- --const ( -- u = iota * 4 -- v float64 = iota * 42 -- w = iota * 42 --) -- --const ( -- a, b = 1, 2 -- c, d -- e, f = 5 * 5, "hello" + "world" -- g, h -- i, j = true, f --) -- --// No hint --const ( -- Int = 3 -- Float = 3.14 -- Bool = true -- Rune = '3' -- Complex = 2.7i -- String = "Hello, world!" --) -- --var ( -- varInt = 3 -- varFloat = 3.14 -- varBool = true -- varRune = '3' + '4' -- varComplex = 2.7i -- varString = "Hello, world!" --) -diff -urN a/gopls/internal/lsp/testdata/inlay_hint/constant_values.go.golden b/gopls/internal/lsp/testdata/inlay_hint/constant_values.go.golden ---- a/gopls/internal/lsp/testdata/inlay_hint/constant_values.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/inlay_hint/constant_values.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,47 +0,0 @@ ---- inlayHint -- --package inlayHint //@inlayHint("package") -- --const True = true -- --type Kind int +-func (e *Editor) RenameFile(ctx context.Context, oldPath, newPath string) error { +- closed, opened, err := e.renameBuffers(oldPath, newPath) +- if err != nil { +- return err +- } - --const ( -- KindNone Kind = iota< = 0> -- KindPrint< = 1> -- KindPrintf< = 2> -- KindErrorf< = 3> --) +- for _, c := range closed { +- if err := e.sendDidClose(ctx, c); err != nil { +- return err +- } +- } +- for _, o := range opened { +- if err := e.sendDidOpen(ctx, o); err != nil { +- return err +- } +- } - --const ( -- u = iota * 4< = 0> -- v float64 = iota * 42< = 42> -- w = iota * 42< = 84> --) +- // Finally, perform the renaming on disk. +- if err := e.sandbox.Workdir.RenameFile(ctx, oldPath, newPath); err != nil { +- return fmt.Errorf("renaming sandbox file: %w", err) +- } +- return nil +-} - --const ( -- a, b = 1, 2 -- c, d< = 1, 2> -- e, f = 5 * 5, "hello" + "world"< = 25, "helloworld"> -- g, h< = 25, "helloworld"> -- i, j = true, f< = true, "helloworld"> --) +-// renameBuffers renames in-memory buffers affected by the renaming of +-// oldPath->newPath, returning the resulting text documents that must be closed +-// and opened over the LSP. +-func (e *Editor) renameBuffers(oldPath, newPath string) (closed []protocol.TextDocumentIdentifier, opened []protocol.TextDocumentItem, _ error) { +- e.mu.Lock() +- defer e.mu.Unlock() - --// No hint --const ( -- Int = 3 -- Float = 3.14 -- Bool = true -- Rune = '3' -- Complex = 2.7i -- String = "Hello, world!" --) +- // In case either oldPath or newPath is absolute, convert to absolute paths +- // before checking for containment. +- oldAbs := e.sandbox.Workdir.AbsPath(oldPath) +- newAbs := e.sandbox.Workdir.AbsPath(newPath) - --var ( -- varInt = 3 -- varFloat = 3.14 -- varBool = true -- varRune = '3' + '4' -- varComplex = 2.7i -- varString = "Hello, world!" --) +- // Collect buffers that are affected by the given file or directory renaming. +- buffersToRename := make(map[string]string) // old path -> new path - -diff -urN a/gopls/internal/lsp/testdata/inlay_hint/parameter_names.go b/gopls/internal/lsp/testdata/inlay_hint/parameter_names.go ---- a/gopls/internal/lsp/testdata/inlay_hint/parameter_names.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/inlay_hint/parameter_names.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,50 +0,0 @@ --package inlayHint //@inlayHint("package") +- for path := range e.buffers { +- abs := e.sandbox.Workdir.AbsPath(path) +- if oldAbs == abs || pathutil.InDir(oldAbs, abs) { +- rel, err := filepath.Rel(oldAbs, abs) +- if err != nil { +- return nil, nil, fmt.Errorf("filepath.Rel(%q, %q): %v", oldAbs, abs, err) +- } +- nabs := filepath.Join(newAbs, rel) +- newPath := e.sandbox.Workdir.RelPath(nabs) +- buffersToRename[path] = newPath +- } +- } - --import "fmt" +- // Update buffers, and build protocol changes. +- for old, new := range buffersToRename { +- buf := e.buffers[old] +- delete(e.buffers, old) +- buf.version = 1 +- buf.path = new +- e.buffers[new] = buf - --func hello(name string) string { -- return "Hello " + name --} +- closed = append(closed, e.TextDocumentIdentifier(old)) +- opened = append(opened, e.textDocumentItem(buf)) +- } - --func helloWorld() string { -- return hello("World") +- return closed, opened, nil -} - --type foo struct{} +-func (e *Editor) applyDocumentChange(ctx context.Context, change protocol.DocumentChanges) error { +- if change.RenameFile != nil { +- oldPath := e.sandbox.Workdir.URIToPath(change.RenameFile.OldURI) +- newPath := e.sandbox.Workdir.URIToPath(change.RenameFile.NewURI) - --func (*foo) bar(baz string, qux int) int { -- if baz != "" { -- return qux + 1 +- return e.RenameFile(ctx, oldPath, newPath) - } -- return qux +- if change.TextDocumentEdit != nil { +- return e.applyTextDocumentEdit(ctx, *change.TextDocumentEdit) +- } +- panic("Internal error: one of RenameFile or TextDocumentEdit must be set") -} - --func kase(foo int, bar bool, baz ...string) { -- fmt.Println(foo, bar, baz) +-func (e *Editor) applyTextDocumentEdit(ctx context.Context, change protocol.TextDocumentEdit) error { +- path := e.sandbox.Workdir.URIToPath(change.TextDocument.URI) +- if ver := int32(e.BufferVersion(path)); ver != change.TextDocument.Version { +- return fmt.Errorf("buffer versions for %q do not match: have %d, editing %d", path, ver, change.TextDocument.Version) +- } +- if !e.HasBuffer(path) { +- err := e.OpenFile(ctx, path) +- if os.IsNotExist(err) { +- // TODO: it's unclear if this is correct. Here we create the buffer (with +- // version 1), then apply edits. Perhaps we should apply the edits before +- // sending the didOpen notification. +- e.CreateBuffer(ctx, path, "") +- err = nil +- } +- if err != nil { +- return err +- } +- } +- return e.EditBuffer(ctx, path, protocol.AsTextEdits(change.Edits)) -} - --func kipp(foo string, bar, baz string) { -- fmt.Println(foo, bar, baz) +-// Config returns the current editor configuration. +-func (e *Editor) Config() EditorConfig { +- e.mu.Lock() +- defer e.mu.Unlock() +- return e.config -} - --func plex(foo, bar string, baz string) { -- fmt.Println(foo, bar, baz) +-func (e *Editor) SetConfig(cfg EditorConfig) { +- e.mu.Lock() +- e.config = cfg +- e.mu.Unlock() -} - --func tars(foo string, bar, baz string) { -- fmt.Println(foo, bar, baz) +-// ChangeConfiguration sets the new editor configuration, and if applicable +-// sends a didChangeConfiguration notification. +-// +-// An error is returned if the change notification failed to send. +-func (e *Editor) ChangeConfiguration(ctx context.Context, newConfig EditorConfig) error { +- e.SetConfig(newConfig) +- if e.Server != nil { +- var params protocol.DidChangeConfigurationParams // empty: gopls ignores the Settings field +- if err := e.Server.DidChangeConfiguration(ctx, ¶ms); err != nil { +- return err +- } +- e.callsMu.Lock() +- e.calls.DidChangeConfiguration++ +- e.callsMu.Unlock() +- } +- return nil -} - --func foobar() { -- var x foo -- x.bar("", 1) -- kase(0, true, "c", "d", "e") -- kipp("a", "b", "c") -- plex("a", "b", "c") -- tars("a", "b", "c") -- foo, bar, baz := "a", "b", "c" -- kipp(foo, bar, baz) -- plex("a", bar, baz) -- tars(foo+foo, (bar), "c") +-// ChangeWorkspaceFolders sets the new workspace folders, and sends a +-// didChangeWorkspaceFolders notification to the server. +-// +-// The given folders must all be unique. +-func (e *Editor) ChangeWorkspaceFolders(ctx context.Context, folders []string) error { +- config := e.Config() - --} -diff -urN a/gopls/internal/lsp/testdata/inlay_hint/parameter_names.go.golden b/gopls/internal/lsp/testdata/inlay_hint/parameter_names.go.golden ---- a/gopls/internal/lsp/testdata/inlay_hint/parameter_names.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/inlay_hint/parameter_names.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,52 +0,0 @@ ---- inlayHint -- --package inlayHint //@inlayHint("package") +- // capture existing folders so that we can compute the change. +- oldFolders := makeWorkspaceFolders(e.sandbox, config.WorkspaceFolders) +- newFolders := makeWorkspaceFolders(e.sandbox, folders) +- config.WorkspaceFolders = folders +- e.SetConfig(config) - --import "fmt" +- if e.Server == nil { +- return nil +- } - --func hello(name string) string { -- return "Hello " + name --} +- var params protocol.DidChangeWorkspaceFoldersParams - --func helloWorld() string { -- return hello(<name: >"World") --} +- // Keep track of old workspace folders that must be removed. +- toRemove := make(map[protocol.URI]protocol.WorkspaceFolder) +- for _, folder := range oldFolders { +- toRemove[folder.URI] = folder +- } - --type foo struct{} +- // Sanity check: if we see a folder twice the algorithm below doesn't work, +- // so track seen folders to ensure that we panic in that case. +- seen := make(map[protocol.URI]protocol.WorkspaceFolder) +- for _, folder := range newFolders { +- if _, ok := seen[folder.URI]; ok { +- panic(fmt.Sprintf("folder %s seen twice", folder.URI)) +- } - --func (*foo) bar(baz string, qux int) int { -- if baz != "" { -- return qux + 1 +- // If this folder already exists, we don't want to remove it. +- // Otherwise, we need to add it. +- if _, ok := toRemove[folder.URI]; ok { +- delete(toRemove, folder.URI) +- } else { +- params.Event.Added = append(params.Event.Added, folder) +- } - } -- return qux --} - --func kase(foo int, bar bool, baz ...string) { -- fmt.Println(<a...: >foo, bar, baz) --} +- for _, v := range toRemove { +- params.Event.Removed = append(params.Event.Removed, v) +- } - --func kipp(foo string, bar, baz string) { -- fmt.Println(<a...: >foo, bar, baz) +- return e.Server.DidChangeWorkspaceFolders(ctx, ¶ms) -} - --func plex(foo, bar string, baz string) { -- fmt.Println(<a...: >foo, bar, baz) +-// CodeAction executes a codeAction request on the server. +-// If loc.Range is zero, the whole file is implied. +-func (e *Editor) CodeAction(ctx context.Context, loc protocol.Location, diagnostics []protocol.Diagnostic) ([]protocol.CodeAction, error) { +- if e.Server == nil { +- return nil, nil +- } +- path := e.sandbox.Workdir.URIToPath(loc.URI) +- e.mu.Lock() +- _, ok := e.buffers[path] +- e.mu.Unlock() +- if !ok { +- return nil, fmt.Errorf("buffer %q is not open", path) +- } +- params := &protocol.CodeActionParams{ +- TextDocument: e.TextDocumentIdentifier(path), +- Context: protocol.CodeActionContext{ +- Diagnostics: diagnostics, +- }, +- Range: loc.Range, // may be zero +- } +- lens, err := e.Server.CodeAction(ctx, params) +- if err != nil { +- return nil, err +- } +- return lens, nil -} - --func tars(foo string, bar, baz string) { -- fmt.Println(<a...: >foo, bar, baz) +-func (e *Editor) EditResolveSupport() (bool, error) { +- capabilities, err := clientCapabilities(e.Config()) +- if err != nil { +- return false, err +- } +- return capabilities.TextDocument.CodeAction.ResolveSupport != nil && slices.Contains(capabilities.TextDocument.CodeAction.ResolveSupport.Properties, "edit"), nil -} - --func foobar() { -- var x foo -- x.bar(<baz: >"", <qux: >1) -- kase(<foo: >0, <bar: >true, <baz...: >"c", "d", "e") -- kipp(<foo: >"a", <bar: >"b", <baz: >"c") -- plex(<foo: >"a", <bar: >"b", <baz: >"c") -- tars(<foo: >"a", <bar: >"b", <baz: >"c") -- foo< string>, bar< string>, baz< string> := "a", "b", "c" -- kipp(foo, bar, baz) -- plex(<foo: >"a", bar, baz) -- tars(<foo: >foo+foo, <bar: >(bar), <baz: >"c") +-// Hover triggers a hover at the given position in an open buffer. +-func (e *Editor) Hover(ctx context.Context, loc protocol.Location) (*protocol.MarkupContent, protocol.Location, error) { +- if err := e.checkBufferLocation(loc); err != nil { +- return nil, protocol.Location{}, err +- } +- params := &protocol.HoverParams{} +- params.TextDocument.URI = loc.URI +- params.Position = loc.Range.Start - +- resp, err := e.Server.Hover(ctx, params) +- if err != nil { +- return nil, protocol.Location{}, fmt.Errorf("hover: %w", err) +- } +- if resp == nil { +- return nil, protocol.Location{}, nil +- } +- return &resp.Contents, protocol.Location{URI: loc.URI, Range: resp.Range}, nil -} - -diff -urN a/gopls/internal/lsp/testdata/inlay_hint/type_params.go b/gopls/internal/lsp/testdata/inlay_hint/type_params.go ---- a/gopls/internal/lsp/testdata/inlay_hint/type_params.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/inlay_hint/type_params.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,45 +0,0 @@ --//go:build go1.18 --// +build go1.18 -- --package inlayHint //@inlayHint("package") -- --func main() { -- ints := map[string]int64{ -- "first": 34, -- "second": 12, +-func (e *Editor) DocumentLink(ctx context.Context, path string) ([]protocol.DocumentLink, error) { +- if e.Server == nil { +- return nil, nil - } +- params := &protocol.DocumentLinkParams{} +- params.TextDocument.URI = e.sandbox.Workdir.URI(path) +- return e.Server.DocumentLink(ctx, params) +-} - -- floats := map[string]float64{ -- "first": 35.98, -- "second": 26.99, +-func (e *Editor) DocumentHighlight(ctx context.Context, loc protocol.Location) ([]protocol.DocumentHighlight, error) { +- if e.Server == nil { +- return nil, nil - } +- if err := e.checkBufferLocation(loc); err != nil { +- return nil, err +- } +- params := &protocol.DocumentHighlightParams{} +- params.TextDocument.URI = loc.URI +- params.Position = loc.Range.Start - -- SumIntsOrFloats[string, int64](ints) -- SumIntsOrFloats[string, float64](floats) -- -- SumIntsOrFloats(ints) -- SumIntsOrFloats(floats) -- -- SumNumbers(ints) -- SumNumbers(floats) +- return e.Server.DocumentHighlight(ctx, params) -} - --type Number interface { -- int64 | float64 +-// SemanticTokensFull invokes textDocument/semanticTokens/full, and interprets +-// its result. +-func (e *Editor) SemanticTokensFull(ctx context.Context, path string) ([]SemanticToken, error) { +- p := &protocol.SemanticTokensParams{ +- TextDocument: protocol.TextDocumentIdentifier{ +- URI: e.sandbox.Workdir.URI(path), +- }, +- } +- resp, err := e.Server.SemanticTokensFull(ctx, p) +- if err != nil { +- return nil, err +- } +- content, ok := e.BufferText(path) +- if !ok { +- return nil, fmt.Errorf("buffer %s is not open", path) +- } +- return e.interpretTokens(resp.Data, content), nil -} - --func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V { -- var s V -- for _, v := range m { -- s += v +-// SemanticTokensRange invokes textDocument/semanticTokens/range, and +-// interprets its result. +-func (e *Editor) SemanticTokensRange(ctx context.Context, loc protocol.Location) ([]SemanticToken, error) { +- p := &protocol.SemanticTokensRangeParams{ +- TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, +- Range: loc.Range, - } -- return s +- resp, err := e.Server.SemanticTokensRange(ctx, p) +- if err != nil { +- return nil, err +- } +- path := e.sandbox.Workdir.URIToPath(loc.URI) +- // As noted above: buffers should be keyed by protocol.DocumentURI. +- content, ok := e.BufferText(path) +- if !ok { +- return nil, fmt.Errorf("buffer %s is not open", path) +- } +- return e.interpretTokens(resp.Data, content), nil -} - --func SumNumbers[K comparable, V Number](m map[K]V) V { -- var s V -- for _, v := range m { -- s += v +-// A SemanticToken is an interpreted semantic token value. +-type SemanticToken struct { +- Token string +- TokenType string +- Mod string +-} +- +-// Note: previously this function elided comment, string, and number tokens. +-// Instead, filtering of token types should be done by the caller. +-func (e *Editor) interpretTokens(x []uint32, contents string) []SemanticToken { +- e.mu.Lock() +- legend := e.semTokOpts.Legend +- e.mu.Unlock() +- lines := strings.Split(contents, "\n") +- ans := []SemanticToken{} +- line, col := 1, 1 +- for i := 0; i < len(x); i += 5 { +- line += int(x[i]) +- col += int(x[i+1]) +- if x[i] != 0 { // new line +- col = int(x[i+1]) + 1 // 1-based column numbers +- } +- sz := x[i+2] +- t := legend.TokenTypes[x[i+3]] +- l := x[i+4] +- var mods []string +- for i, mod := range legend.TokenModifiers { +- if l&(1<<i) != 0 { +- mods = append(mods, mod) +- } +- } +- // Preexisting note: "col is a utf-8 offset" +- // TODO(rfindley): is that true? Or is it UTF-16, like other columns in the LSP? +- tok := lines[line-1][col-1 : col-1+int(sz)] +- ans = append(ans, SemanticToken{tok, t, strings.Join(mods, " ")}) - } -- return s +- return ans -} -diff -urN a/gopls/internal/lsp/testdata/inlay_hint/type_params.go.golden b/gopls/internal/lsp/testdata/inlay_hint/type_params.go.golden ---- a/gopls/internal/lsp/testdata/inlay_hint/type_params.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/inlay_hint/type_params.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,47 +0,0 @@ ---- inlayHint -- --//go:build go1.18 --// +build go1.18 -- --package inlayHint //@inlayHint("package") +diff -urN a/gopls/internal/test/integration/fake/editor_test.go b/gopls/internal/test/integration/fake/editor_test.go +--- a/gopls/internal/test/integration/fake/editor_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/fake/editor_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,61 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func main() { -- ints< map[string]int64> := map[string]int64{ -- "first": 34, -- "second": 12, -- } +-package fake - -- floats< map[string]float64> := map[string]float64{ -- "first": 35.98, -- "second": 26.99, -- } +-import ( +- "context" +- "testing" - -- SumIntsOrFloats[string, int64](<m: >ints) -- SumIntsOrFloats[string, float64](<m: >floats) +- "golang.org/x/tools/gopls/internal/protocol" +-) - -- SumIntsOrFloats<[string, int64]>(<m: >ints) -- SumIntsOrFloats<[string, float64]>(<m: >floats) +-const exampleProgram = ` +--- go.mod -- +-go 1.12 +--- main.go -- +-package main - -- SumNumbers<[string, int64]>(<m: >ints) -- SumNumbers<[string, float64]>(<m: >floats) --} +-import "fmt" - --type Number interface { -- int64 | float64 +-func main() { +- fmt.Println("Hello World.") -} +-` - --func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V { -- var s V -- for _< K>, v< V> := range m { -- s += v +-func TestClientEditing(t *testing.T) { +- ws, err := NewSandbox(&SandboxConfig{Files: UnpackTxt(exampleProgram)}) +- if err != nil { +- t.Fatal(err) - } -- return s --} -- --func SumNumbers[K comparable, V Number](m map[K]V) V { -- var s V -- for _< K>, v< V> := range m { -- s += v +- defer ws.Close() +- ctx := context.Background() +- editor := NewEditor(ws, EditorConfig{}) +- if err := editor.OpenFile(ctx, "main.go"); err != nil { +- t.Fatal(err) - } -- return s --} +- if err := editor.EditBuffer(ctx, "main.go", []protocol.TextEdit{ +- { +- Range: protocol.Range{ +- Start: protocol.Position{Line: 5, Character: 14}, +- End: protocol.Position{Line: 5, Character: 26}, +- }, +- NewText: "Hola, mundo.", +- }, +- }); err != nil { +- t.Fatal(err) +- } +- got := editor.buffers["main.go"].text() +- want := `package main - -diff -urN a/gopls/internal/lsp/testdata/inlay_hint/variable_types.go b/gopls/internal/lsp/testdata/inlay_hint/variable_types.go ---- a/gopls/internal/lsp/testdata/inlay_hint/variable_types.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/inlay_hint/variable_types.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,20 +0,0 @@ --package inlayHint //@inlayHint("package") +-import "fmt" - --func assignTypes() { -- i, j := 0, len([]string{})-1 -- println(i, j) +-func main() { +- fmt.Println("Hola, mundo.") -} -- --func rangeTypes() { -- for k, v := range []string{} { -- println(k, v) +-` +- if got != want { +- t.Errorf("got text %q, want %q", got, want) - } -} +diff -urN a/gopls/internal/test/integration/fake/edit_test.go b/gopls/internal/test/integration/fake/edit_test.go +--- a/gopls/internal/test/integration/fake/edit_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/fake/edit_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,96 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func funcLitType() { -- myFunc := func(a string) string { return "" } --} +-package fake - --func compositeLitType() { -- foo := map[string]interface{}{"": ""} --} -diff -urN a/gopls/internal/lsp/testdata/inlay_hint/variable_types.go.golden b/gopls/internal/lsp/testdata/inlay_hint/variable_types.go.golden ---- a/gopls/internal/lsp/testdata/inlay_hint/variable_types.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/inlay_hint/variable_types.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,22 +0,0 @@ ---- inlayHint -- --package inlayHint //@inlayHint("package") +-import ( +- "testing" - --func assignTypes() { -- i< int>, j< int> := 0, len([]string{})-1 -- println(i, j) --} +- "golang.org/x/tools/gopls/internal/protocol" +-) - --func rangeTypes() { -- for k< int>, v< string> := range []string{} { -- println(k, v) +-func TestApplyEdits(t *testing.T) { +- tests := []struct { +- label string +- content string +- edits []protocol.TextEdit +- want string +- wantErr bool +- }{ +- { +- label: "empty content", +- }, +- { +- label: "empty edit", +- content: "hello", +- edits: []protocol.TextEdit{}, +- want: "hello", +- }, +- { +- label: "unicode edit", +- content: "hello, 日本語", +- edits: []protocol.TextEdit{ +- NewEdit(0, 7, 0, 10, "world"), +- }, +- want: "hello, world", +- }, +- { +- label: "range edit", +- content: "ABC\nDEF\nGHI\nJKL", +- edits: []protocol.TextEdit{ +- NewEdit(1, 1, 2, 3, "12\n345"), +- }, +- want: "ABC\nD12\n345\nJKL", +- }, +- { +- label: "regression test for issue #57627", +- content: "go 1.18\nuse moda/a", +- edits: []protocol.TextEdit{ +- NewEdit(1, 0, 1, 0, "\n"), +- NewEdit(2, 0, 2, 0, "\n"), +- }, +- want: "go 1.18\n\nuse moda/a\n", +- }, +- { +- label: "end before start", +- content: "ABC\nDEF\nGHI\nJKL", +- edits: []protocol.TextEdit{ +- NewEdit(2, 3, 1, 1, "12\n345"), +- }, +- wantErr: true, +- }, +- { +- label: "out of bounds line", +- content: "ABC\nDEF\nGHI\nJKL", +- edits: []protocol.TextEdit{ +- NewEdit(1, 1, 4, 3, "12\n345"), +- }, +- wantErr: true, +- }, +- { +- label: "out of bounds column", +- content: "ABC\nDEF\nGHI\nJKL", +- edits: []protocol.TextEdit{ +- NewEdit(1, 4, 2, 3, "12\n345"), +- }, +- wantErr: true, +- }, - } --} -- --func funcLitType() { -- myFunc< func(a string) string> := func(a string) string { return "" } --} - --func compositeLitType() { -- foo< map[string]interface{}> := map[string]interface{}{"": ""} +- for _, test := range tests { +- test := test +- t.Run(test.label, func(t *testing.T) { +- got, err := applyEdits(protocol.NewMapper("", []byte(test.content)), test.edits, false) +- if (err != nil) != test.wantErr { +- t.Errorf("got err %v, want error: %t", err, test.wantErr) +- } +- if err != nil { +- return +- } +- if got := string(got); got != test.want { +- t.Errorf("got %q, want %q", got, test.want) +- } +- }) +- } -} +diff -urN a/gopls/internal/test/integration/fake/glob/glob.go b/gopls/internal/test/integration/fake/glob/glob.go +--- a/gopls/internal/test/integration/fake/glob/glob.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/fake/glob/glob.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,349 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -diff -urN a/gopls/internal/lsp/testdata/invertifcondition/boolean_fn.go b/gopls/internal/lsp/testdata/invertifcondition/boolean_fn.go ---- a/gopls/internal/lsp/testdata/invertifcondition/boolean_fn.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/invertifcondition/boolean_fn.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,14 +0,0 @@ --package invertifcondition +-// Package glob implements an LSP-compliant glob pattern matcher for testing. +-package glob - -import ( +- "errors" - "fmt" -- "os" +- "strings" +- "unicode/utf8" -) - --func BooleanFn() { -- if os.IsPathSeparator('X') { //@suggestedfix("if os.IsPathSeparator('X')", "refactor.rewrite", "") -- fmt.Println("A") -- } else { -- fmt.Println("B") -- } +-// A Glob is an LSP-compliant glob pattern, as defined by the spec: +-// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#documentFilter +-// +-// NOTE: this implementation is currently only intended for testing. In order +-// to make it production ready, we'd need to: +-// - verify it against the VS Code implementation +-// - add more tests +-// - microbenchmark, likely avoiding the element interface +-// - resolve the question of what is meant by "character". If it's a UTF-16 +-// code (as we suspect) it'll be a bit more work. +-// +-// Quoting from the spec: +-// Glob patterns can have the following syntax: +-// - `*` to match one or more characters in a path segment +-// - `?` to match on one character in a path segment +-// - `**` to match any number of path segments, including none +-// - `{}` to group sub patterns into an OR expression. (e.g. `**/*.{ts,js}` +-// matches all TypeScript and JavaScript files) +-// - `[]` to declare a range of characters to match in a path segment +-// (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …) +-// - `[!...]` to negate a range of characters to match in a path segment +-// (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but +-// not `example.0`) +-// +-// Expanding on this: +-// - '/' matches one or more literal slashes. +-// - any other character matches itself literally. +-type Glob struct { +- elems []element // pattern elements -} -diff -urN a/gopls/internal/lsp/testdata/invertifcondition/boolean_fn.go.golden b/gopls/internal/lsp/testdata/invertifcondition/boolean_fn.go.golden ---- a/gopls/internal/lsp/testdata/invertifcondition/boolean_fn.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/invertifcondition/boolean_fn.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,16 +0,0 @@ ---- suggestedfix_boolean_fn_9_2 -- --package invertifcondition - --import ( -- "fmt" -- "os" --) -- --func BooleanFn() { -- if !os.IsPathSeparator('X') { -- fmt.Println("B") -- } else { //@suggestedfix("if os.IsPathSeparator('X')", "refactor.rewrite", "") -- fmt.Println("A") -- } +-// Parse builds a Glob for the given pattern, returning an error if the pattern +-// is invalid. +-func Parse(pattern string) (*Glob, error) { +- g, _, err := parse(pattern, false) +- return g, err -} - -diff -urN a/gopls/internal/lsp/testdata/invertifcondition/boolean.go b/gopls/internal/lsp/testdata/invertifcondition/boolean.go ---- a/gopls/internal/lsp/testdata/invertifcondition/boolean.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/invertifcondition/boolean.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,14 +0,0 @@ --package invertifcondition +-func parse(pattern string, nested bool) (*Glob, string, error) { +- g := new(Glob) +- for len(pattern) > 0 { +- switch pattern[0] { +- case '/': +- pattern = pattern[1:] +- g.elems = append(g.elems, slash{}) - --import ( -- "fmt" --) +- case '*': +- if len(pattern) > 1 && pattern[1] == '*' { +- if (len(g.elems) > 0 && g.elems[len(g.elems)-1] != slash{}) || (len(pattern) > 2 && pattern[2] != '/') { +- return nil, "", errors.New("** may only be adjacent to '/'") +- } +- pattern = pattern[2:] +- g.elems = append(g.elems, starStar{}) +- break +- } +- pattern = pattern[1:] +- g.elems = append(g.elems, star{}) - --func Boolean() { -- b := true -- if b { //@suggestedfix("if b", "refactor.rewrite", "") -- fmt.Println("A") -- } else { -- fmt.Println("B") -- } --} -diff -urN a/gopls/internal/lsp/testdata/invertifcondition/boolean.go.golden b/gopls/internal/lsp/testdata/invertifcondition/boolean.go.golden ---- a/gopls/internal/lsp/testdata/invertifcondition/boolean.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/invertifcondition/boolean.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,16 +0,0 @@ ---- suggestedfix_boolean_9_2 -- --package invertifcondition +- case '?': +- pattern = pattern[1:] +- g.elems = append(g.elems, anyChar{}) - --import ( -- "fmt" --) +- case '{': +- var gs group +- for pattern[0] != '}' { +- pattern = pattern[1:] +- g, pat, err := parse(pattern, true) +- if err != nil { +- return nil, "", err +- } +- if len(pat) == 0 { +- return nil, "", errors.New("unmatched '{'") +- } +- pattern = pat +- gs = append(gs, g) +- } +- pattern = pattern[1:] +- g.elems = append(g.elems, gs) - --func Boolean() { -- b := true -- if !b { -- fmt.Println("B") -- } else { //@suggestedfix("if b", "refactor.rewrite", "") -- fmt.Println("A") +- case '}', ',': +- if nested { +- return g, pattern, nil +- } +- pattern = g.parseLiteral(pattern, false) +- +- case '[': +- pattern = pattern[1:] +- if len(pattern) == 0 { +- return nil, "", errBadRange +- } +- negate := false +- if pattern[0] == '!' { +- pattern = pattern[1:] +- negate = true +- } +- low, sz, err := readRangeRune(pattern) +- if err != nil { +- return nil, "", err +- } +- pattern = pattern[sz:] +- if len(pattern) == 0 || pattern[0] != '-' { +- return nil, "", errBadRange +- } +- pattern = pattern[1:] +- high, sz, err := readRangeRune(pattern) +- if err != nil { +- return nil, "", err +- } +- pattern = pattern[sz:] +- if len(pattern) == 0 || pattern[0] != ']' { +- return nil, "", errBadRange +- } +- pattern = pattern[1:] +- g.elems = append(g.elems, charRange{negate, low, high}) +- +- default: +- pattern = g.parseLiteral(pattern, nested) +- } - } +- return g, "", nil -} - -diff -urN a/gopls/internal/lsp/testdata/invertifcondition/dont_remove_parens.go b/gopls/internal/lsp/testdata/invertifcondition/dont_remove_parens.go ---- a/gopls/internal/lsp/testdata/invertifcondition/dont_remove_parens.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/invertifcondition/dont_remove_parens.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,16 +0,0 @@ --package invertifcondition +-// helper for decoding a rune in range elements, e.g. [a-z] +-func readRangeRune(input string) (rune, int, error) { +- r, sz := utf8.DecodeRuneInString(input) +- var err error +- if r == utf8.RuneError { +- // See the documentation for DecodeRuneInString. +- switch sz { +- case 0: +- err = errBadRange +- case 1: +- err = errInvalidUTF8 +- } +- } +- return r, sz, err +-} - --import ( -- "fmt" +-var ( +- errBadRange = errors.New("'[' patterns must be of the form [x-y]") +- errInvalidUTF8 = errors.New("invalid UTF-8 encoding") -) - --func DontRemoveParens() { -- a := false -- b := true -- if !(a || -- b) { //@suggestedfix("b", "refactor.rewrite", "") -- fmt.Println("A") +-func (g *Glob) parseLiteral(pattern string, nested bool) string { +- var specialChars string +- if nested { +- specialChars = "*?{[/}," - } else { -- fmt.Println("B") +- specialChars = "*?{[/" +- } +- end := strings.IndexAny(pattern, specialChars) +- if end == -1 { +- end = len(pattern) - } +- g.elems = append(g.elems, literal(pattern[:end])) +- return pattern[end:] -} -diff -urN a/gopls/internal/lsp/testdata/invertifcondition/dont_remove_parens.go.golden b/gopls/internal/lsp/testdata/invertifcondition/dont_remove_parens.go.golden ---- a/gopls/internal/lsp/testdata/invertifcondition/dont_remove_parens.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/invertifcondition/dont_remove_parens.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,18 +0,0 @@ ---- suggestedfix_dont_remove_parens_11_3 -- --package invertifcondition -- --import ( -- "fmt" --) - --func DontRemoveParens() { -- a := false -- b := true -- if (a || -- b) { -- fmt.Println("B") -- } else { //@suggestedfix("b", "refactor.rewrite", "") -- fmt.Println("A") +-func (g *Glob) String() string { +- var b strings.Builder +- for _, e := range g.elems { +- fmt.Fprint(&b, e) - } +- return b.String() -} - -diff -urN a/gopls/internal/lsp/testdata/invertifcondition/else_if.go b/gopls/internal/lsp/testdata/invertifcondition/else_if.go ---- a/gopls/internal/lsp/testdata/invertifcondition/else_if.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/invertifcondition/else_if.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,22 +0,0 @@ --package invertifcondition +-// element holds a glob pattern element, as defined below. +-type element fmt.Stringer - --import ( -- "fmt" -- "os" +-// element types. +-type ( +- slash struct{} // One or more '/' separators +- literal string // string literal, not containing /, *, ?, {}, or [] +- star struct{} // * +- anyChar struct{} // ? +- starStar struct{} // ** +- group []*Glob // {foo, bar, ...} grouping +- charRange struct { // [a-z] character range +- negate bool +- low, high rune +- } -) - --func ElseIf() { -- // No inversion expected when there's not else clause -- if len(os.Args) > 2 { -- fmt.Println("A") +-func (s slash) String() string { return "/" } +-func (l literal) String() string { return string(l) } +-func (s star) String() string { return "*" } +-func (a anyChar) String() string { return "?" } +-func (s starStar) String() string { return "**" } +-func (g group) String() string { +- var parts []string +- for _, g := range g { +- parts = append(parts, g.String()) - } +- return "{" + strings.Join(parts, ",") + "}" +-} +-func (r charRange) String() string { +- return "[" + string(r.low) + "-" + string(r.high) + "]" +-} - -- // No inversion expected for else-if, that would become unreadable -- if len(os.Args) > 2 { -- fmt.Println("A") -- } else if os.Args[0] == "X" { //@suggestedfix(re"if os.Args.0. == .X.", "refactor.rewrite", "") -- fmt.Println("B") -- } else { -- fmt.Println("C") -- } +-// Match reports whether the input string matches the glob pattern. +-func (g *Glob) Match(input string) bool { +- return match(g.elems, input) -} -diff -urN a/gopls/internal/lsp/testdata/invertifcondition/else_if.go.golden b/gopls/internal/lsp/testdata/invertifcondition/else_if.go.golden ---- a/gopls/internal/lsp/testdata/invertifcondition/else_if.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/invertifcondition/else_if.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,24 +0,0 @@ ---- suggestedfix_else_if_17_9 -- --package invertifcondition - --import ( -- "fmt" -- "os" --) +-func match(elems []element, input string) (ok bool) { +- var elem interface{} +- for len(elems) > 0 { +- elem, elems = elems[0], elems[1:] +- switch elem := elem.(type) { +- case slash: +- if len(input) == 0 || input[0] != '/' { +- return false +- } +- for input[0] == '/' { +- input = input[1:] +- } - --func ElseIf() { -- // No inversion expected when there's not else clause -- if len(os.Args) > 2 { -- fmt.Println("A") -- } +- case starStar: +- // Special cases: +- // - **/a matches "a" +- // - **/ matches everything +- // +- // Note that if ** is followed by anything, it must be '/' (this is +- // enforced by Parse). +- if len(elems) > 0 { +- elems = elems[1:] +- } - -- // No inversion expected for else-if, that would become unreadable -- if len(os.Args) > 2 { -- fmt.Println("A") -- } else if os.Args[0] != "X" { -- fmt.Println("C") -- } else { //@suggestedfix(re"if os.Args.0. == .X.", "refactor.rewrite", "") -- fmt.Println("B") -- } --} +- // A trailing ** matches anything. +- if len(elems) == 0 { +- return true +- } +- +- // Backtracking: advance pattern segments until the remaining pattern +- // elements match. +- for len(input) != 0 { +- if match(elems, input) { +- return true +- } +- _, input = split(input) +- } +- return false +- +- case literal: +- if !strings.HasPrefix(input, string(elem)) { +- return false +- } +- input = input[len(elem):] - -diff -urN a/gopls/internal/lsp/testdata/invertifcondition/greater_than.go b/gopls/internal/lsp/testdata/invertifcondition/greater_than.go ---- a/gopls/internal/lsp/testdata/invertifcondition/greater_than.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/invertifcondition/greater_than.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,14 +0,0 @@ --package invertifcondition +- case star: +- var segInput string +- segInput, input = split(input) - --import ( -- "fmt" -- "os" --) +- elemEnd := len(elems) +- for i, e := range elems { +- if e == (slash{}) { +- elemEnd = i +- break +- } +- } +- segElems := elems[:elemEnd] +- elems = elems[elemEnd:] - --func GreaterThan() { -- if len(os.Args) > 2 { //@suggestedfix("i", "refactor.rewrite", "") -- fmt.Println("A") -- } else { -- fmt.Println("B") -- } --} -diff -urN a/gopls/internal/lsp/testdata/invertifcondition/greater_than.go.golden b/gopls/internal/lsp/testdata/invertifcondition/greater_than.go.golden ---- a/gopls/internal/lsp/testdata/invertifcondition/greater_than.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/invertifcondition/greater_than.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,16 +0,0 @@ ---- suggestedfix_greater_than_9_2 -- --package invertifcondition +- // A trailing * matches the entire segment. +- if len(segElems) == 0 { +- break +- } - --import ( -- "fmt" -- "os" --) +- // Backtracking: advance characters until remaining subpattern elements +- // match. +- matched := false +- for i := range segInput { +- if match(segElems, segInput[i:]) { +- matched = true +- break +- } +- } +- if !matched { +- return false +- } - --func GreaterThan() { -- if len(os.Args) <= 2 { -- fmt.Println("B") -- } else { //@suggestedfix("i", "refactor.rewrite", "") -- fmt.Println("A") -- } --} +- case anyChar: +- if len(input) == 0 || input[0] == '/' { +- return false +- } +- input = input[1:] - -diff -urN a/gopls/internal/lsp/testdata/invertifcondition/not_boolean.go b/gopls/internal/lsp/testdata/invertifcondition/not_boolean.go ---- a/gopls/internal/lsp/testdata/invertifcondition/not_boolean.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/invertifcondition/not_boolean.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,14 +0,0 @@ --package invertifcondition +- case group: +- // Append remaining pattern elements to each group member looking for a +- // match. +- var branch []element +- for _, m := range elem { +- branch = branch[:0] +- branch = append(branch, m.elems...) +- branch = append(branch, elems...) +- if match(branch, input) { +- return true +- } +- } +- return false - --import ( -- "fmt" --) +- case charRange: +- if len(input) == 0 || input[0] == '/' { +- return false +- } +- c, sz := utf8.DecodeRuneInString(input) +- if c < elem.low || c > elem.high { +- return false +- } +- input = input[sz:] - --func NotBoolean() { -- b := true -- if !b { //@suggestedfix("if !b", "refactor.rewrite", "") -- fmt.Println("A") -- } else { -- fmt.Println("B") +- default: +- panic(fmt.Sprintf("segment type %T not implemented", elem)) +- } - } --} -diff -urN a/gopls/internal/lsp/testdata/invertifcondition/not_boolean.go.golden b/gopls/internal/lsp/testdata/invertifcondition/not_boolean.go.golden ---- a/gopls/internal/lsp/testdata/invertifcondition/not_boolean.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/invertifcondition/not_boolean.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,16 +0,0 @@ ---- suggestedfix_not_boolean_9_2 -- --package invertifcondition - --import ( -- "fmt" --) +- return len(input) == 0 +-} - --func NotBoolean() { -- b := true -- if b { -- fmt.Println("B") -- } else { //@suggestedfix("if !b", "refactor.rewrite", "") -- fmt.Println("A") +-// split returns the portion before and after the first slash +-// (or sequence of consecutive slashes). If there is no slash +-// it returns (input, nil). +-func split(input string) (first, rest string) { +- i := strings.IndexByte(input, '/') +- if i < 0 { +- return input, "" +- } +- first = input[:i] +- for j := i; j < len(input); j++ { +- if input[j] != '/' { +- return first, input[j:] +- } - } +- return first, "" -} +diff -urN a/gopls/internal/test/integration/fake/glob/glob_test.go b/gopls/internal/test/integration/fake/glob/glob_test.go +--- a/gopls/internal/test/integration/fake/glob/glob_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/fake/glob/glob_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,118 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -diff -urN a/gopls/internal/lsp/testdata/invertifcondition/remove_else.go b/gopls/internal/lsp/testdata/invertifcondition/remove_else.go ---- a/gopls/internal/lsp/testdata/invertifcondition/remove_else.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/invertifcondition/remove_else.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,16 +0,0 @@ --package invertifcondition +-package glob_test - -import ( -- "fmt" +- "testing" +- +- "golang.org/x/tools/gopls/internal/test/integration/fake/glob" -) - --func RemoveElse() { -- if true { //@suggestedfix("if true", "refactor.rewrite", "") -- fmt.Println("A") -- } else { -- fmt.Println("B") -- return +-func TestParseErrors(t *testing.T) { +- tests := []string{ +- "***", +- "ab{c", +- "[]", +- "[a-]", +- "ab{c{d}", - } - -- fmt.Println("C") +- for _, test := range tests { +- _, err := glob.Parse(test) +- if err == nil { +- t.Errorf("Parse(%q) succeeded unexpectedly", test) +- } +- } -} -diff -urN a/gopls/internal/lsp/testdata/invertifcondition/remove_else.go.golden b/gopls/internal/lsp/testdata/invertifcondition/remove_else.go.golden ---- a/gopls/internal/lsp/testdata/invertifcondition/remove_else.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/invertifcondition/remove_else.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,19 +0,0 @@ ---- suggestedfix_remove_else_8_2 -- --package invertifcondition -- --import ( -- "fmt" --) - --func RemoveElse() { -- if false { -- fmt.Println("B") -- return -- } +-func TestMatch(t *testing.T) { +- tests := []struct { +- pattern, input string +- want bool +- }{ +- // Basic cases. +- {"", "", true}, +- {"", "a", false}, +- {"", "/", false}, +- {"abc", "abc", true}, - -- //@suggestedfix("if true", "refactor.rewrite", "") -- fmt.Println("A") +- // ** behavior +- {"**", "abc", true}, +- {"**/abc", "abc", true}, +- {"**", "abc/def", true}, +- {"{a/**/c,a/**/d}", "a/b/c", true}, +- {"{a/**/c,a/**/d}", "a/b/c/d", true}, +- {"{a/**/c,a/**/e}", "a/b/c/d", false}, +- {"{a/**/c,a/**/e,a/**/d}", "a/b/c/d", true}, +- {"{/a/**/c,a/**/e,a/**/d}", "a/b/c/d", true}, +- {"{/a/**/c,a/**/e,a/**/d}", "/a/b/c/d", false}, +- {"{/a/**/c,a/**/e,a/**/d}", "/a/b/c", true}, +- {"{/a/**/e,a/**/e,a/**/d}", "/a/b/c", false}, - -- fmt.Println("C") --} +- // * and ? behavior +- {"/*", "/a", true}, +- {"*", "foo", true}, +- {"*o", "foo", true}, +- {"*o", "foox", false}, +- {"f*o", "foo", true}, +- {"f*o", "fo", true}, +- {"fo?", "foo", true}, +- {"fo?", "fox", true}, +- {"fo?", "fooo", false}, +- {"fo?", "fo", false}, +- {"?", "a", true}, +- {"?", "ab", false}, +- {"?", "", false}, +- {"*?", "", false}, +- {"?b", "ab", true}, +- {"?c", "ab", false}, - -diff -urN a/gopls/internal/lsp/testdata/invertifcondition/remove_parens.go b/gopls/internal/lsp/testdata/invertifcondition/remove_parens.go ---- a/gopls/internal/lsp/testdata/invertifcondition/remove_parens.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/invertifcondition/remove_parens.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,14 +0,0 @@ --package invertifcondition +- // {} behavior +- {"ab{c,d}e", "abce", true}, +- {"ab{c,d}e", "abde", true}, +- {"ab{c,d}e", "abxe", false}, +- {"ab{c,d}e", "abe", false}, +- {"{a,b}c", "ac", true}, +- {"{a,b}c", "bc", true}, +- {"{a,b}c", "ab", false}, +- {"a{b,c}", "ab", true}, +- {"a{b,c}", "ac", true}, +- {"a{b,c}", "bc", false}, +- {"ab{c{1,2},d}e", "abc1e", true}, +- {"ab{c{1,2},d}e", "abde", true}, +- {"ab{c{1,2},d}e", "abc1f", false}, +- {"ab{c{1,2},d}e", "abce", false}, +- {"ab{c[}-~]}d", "abc}d", true}, +- {"ab{c[}-~]}d", "abc~d", true}, +- {"ab{c[}-~],y}d", "abcxd", false}, +- {"ab{c[}-~],y}d", "abyd", true}, +- {"ab{c[}-~],y}d", "abd", false}, +- {"{a/b/c,d/e/f}", "a/b/c", true}, +- {"/ab{/c,d}e", "/ab/ce", true}, +- {"/ab{/c,d}e", "/ab/cf", false}, - --import ( -- "fmt" --) +- // [-] behavior +- {"[a-c]", "a", true}, +- {"[a-c]", "b", true}, +- {"[a-c]", "c", true}, +- {"[a-c]", "d", false}, +- {"[a-c]", " ", false}, - --func RemoveParens() { -- b := true -- if !(b) { //@suggestedfix("if", "refactor.rewrite", "") -- fmt.Println("A") -- } else { -- fmt.Println("B") +- // Realistic examples. +- {"**/*.{ts,js}", "path/to/foo.ts", true}, +- {"**/*.{ts,js}", "path/to/foo.js", true}, +- {"**/*.{ts,js}", "path/to/foo.go", false}, - } --} -diff -urN a/gopls/internal/lsp/testdata/invertifcondition/remove_parens.go.golden b/gopls/internal/lsp/testdata/invertifcondition/remove_parens.go.golden ---- a/gopls/internal/lsp/testdata/invertifcondition/remove_parens.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/invertifcondition/remove_parens.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,16 +0,0 @@ ---- suggestedfix_remove_parens_9_2 -- --package invertifcondition - --import ( -- "fmt" --) -- --func RemoveParens() { -- b := true -- if b { -- fmt.Println("B") -- } else { //@suggestedfix("if", "refactor.rewrite", "") -- fmt.Println("A") +- for _, test := range tests { +- g, err := glob.Parse(test.pattern) +- if err != nil { +- t.Fatalf("New(%q) failed unexpectedly: %v", test.pattern, err) +- } +- if got := g.Match(test.input); got != test.want { +- t.Errorf("New(%q).Match(%q) = %t, want %t", test.pattern, test.input, got, test.want) +- } - } -} +diff -urN a/gopls/internal/test/integration/fake/proxy.go b/gopls/internal/test/integration/fake/proxy.go +--- a/gopls/internal/test/integration/fake/proxy.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/fake/proxy.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,35 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -diff -urN a/gopls/internal/lsp/testdata/invertifcondition/semicolon_and.go b/gopls/internal/lsp/testdata/invertifcondition/semicolon_and.go ---- a/gopls/internal/lsp/testdata/invertifcondition/semicolon_and.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/invertifcondition/semicolon_and.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,13 +0,0 @@ --package invertifcondition +-package fake - -import ( - "fmt" --) -- --func SemicolonAnd() { -- if n, err := fmt.Println("x"); err != nil && n > 0 { //@suggestedfix("f", "refactor.rewrite", "") -- fmt.Println("A") -- } else { -- fmt.Println("B") -- } --} -diff -urN a/gopls/internal/lsp/testdata/invertifcondition/semicolon_and.go.golden b/gopls/internal/lsp/testdata/invertifcondition/semicolon_and.go.golden ---- a/gopls/internal/lsp/testdata/invertifcondition/semicolon_and.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/invertifcondition/semicolon_and.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,15 +0,0 @@ ---- suggestedfix_semicolon_and_8_3 -- --package invertifcondition - --import ( -- "fmt" +- "golang.org/x/tools/internal/proxydir" -) - --func SemicolonAnd() { -- if n, err := fmt.Println("x"); err == nil || n <= 0 { -- fmt.Println("B") -- } else { //@suggestedfix("f", "refactor.rewrite", "") -- fmt.Println("A") +-// WriteProxy creates a new proxy file tree using the txtar-encoded content, +-// and returns its URL. +-func WriteProxy(tmpdir string, files map[string][]byte) (string, error) { +- type moduleVersion struct { +- modulePath, version string - } --} -- -diff -urN a/gopls/internal/lsp/testdata/invertifcondition/semicolon.go b/gopls/internal/lsp/testdata/invertifcondition/semicolon.go ---- a/gopls/internal/lsp/testdata/invertifcondition/semicolon.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/invertifcondition/semicolon.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,13 +0,0 @@ --package invertifcondition -- --import ( -- "fmt" --) -- --func Semicolon() { -- if _, err := fmt.Println("x"); err != nil { //@suggestedfix("if", "refactor.rewrite", "") -- fmt.Println("A") -- } else { -- fmt.Println("B") +- // Transform into the format expected by the proxydir package. +- filesByModule := make(map[moduleVersion]map[string][]byte) +- for name, data := range files { +- modulePath, version, suffix := splitModuleVersionPath(name) +- mv := moduleVersion{modulePath, version} +- if _, ok := filesByModule[mv]; !ok { +- filesByModule[mv] = make(map[string][]byte) +- } +- filesByModule[mv][suffix] = data - } --} -diff -urN a/gopls/internal/lsp/testdata/invertifcondition/semicolon.go.golden b/gopls/internal/lsp/testdata/invertifcondition/semicolon.go.golden ---- a/gopls/internal/lsp/testdata/invertifcondition/semicolon.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/invertifcondition/semicolon.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,15 +0,0 @@ ---- suggestedfix_semicolon_8_2 -- --package invertifcondition -- --import ( -- "fmt" --) -- --func Semicolon() { -- if _, err := fmt.Println("x"); err == nil { -- fmt.Println("B") -- } else { //@suggestedfix("if", "refactor.rewrite", "") -- fmt.Println("A") +- for mv, files := range filesByModule { +- if err := proxydir.WriteModuleVersion(tmpdir, mv.modulePath, mv.version, files); err != nil { +- return "", fmt.Errorf("error writing %s@%s: %v", mv.modulePath, mv.version, err) +- } - } +- return proxydir.ToURL(tmpdir), nil -} +diff -urN a/gopls/internal/test/integration/fake/sandbox.go b/gopls/internal/test/integration/fake/sandbox.go +--- a/gopls/internal/test/integration/fake/sandbox.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/fake/sandbox.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,299 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -diff -urN a/gopls/internal/lsp/testdata/invertifcondition/semicolon_or.go b/gopls/internal/lsp/testdata/invertifcondition/semicolon_or.go ---- a/gopls/internal/lsp/testdata/invertifcondition/semicolon_or.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/invertifcondition/semicolon_or.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,13 +0,0 @@ --package invertifcondition +-package fake - -import ( +- "context" +- "errors" - "fmt" --) -- --func SemicolonOr() { -- if n, err := fmt.Println("x"); err != nil || n < 5 { //@suggestedfix(re"if n, err := fmt.Println..x..; err != nil .. n < 5", "refactor.rewrite", "") -- fmt.Println("A") -- } else { -- fmt.Println("B") -- } --} -diff -urN a/gopls/internal/lsp/testdata/invertifcondition/semicolon_or.go.golden b/gopls/internal/lsp/testdata/invertifcondition/semicolon_or.go.golden ---- a/gopls/internal/lsp/testdata/invertifcondition/semicolon_or.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/invertifcondition/semicolon_or.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,15 +0,0 @@ ---- suggestedfix_semicolon_or_8_2 -- --package invertifcondition +- "os" +- "path/filepath" +- "strings" - --import ( -- "fmt" +- "golang.org/x/tools/internal/gocommand" +- "golang.org/x/tools/internal/robustio" +- "golang.org/x/tools/internal/testenv" +- "golang.org/x/tools/txtar" -) - --func SemicolonOr() { -- if n, err := fmt.Println("x"); err == nil && n >= 5 { -- fmt.Println("B") -- } else { //@suggestedfix(re"if n, err := fmt.Println..x..; err != nil .. n < 5", "refactor.rewrite", "") -- fmt.Println("A") -- } --} -- -diff -urN a/gopls/internal/lsp/testdata/missingfunction/channels.go b/gopls/internal/lsp/testdata/missingfunction/channels.go ---- a/gopls/internal/lsp/testdata/missingfunction/channels.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/missingfunction/channels.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,9 +0,0 @@ --package missingfunction -- --func channels(s string) { -- undefinedChannels(c()) //@suggestedfix("undefinedChannels", "quickfix", "") --} -- --func c() (<-chan string, chan string) { -- return make(<-chan string), make(chan string) --} -diff -urN a/gopls/internal/lsp/testdata/missingfunction/channels.go.golden b/gopls/internal/lsp/testdata/missingfunction/channels.go.golden ---- a/gopls/internal/lsp/testdata/missingfunction/channels.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/missingfunction/channels.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,15 +0,0 @@ ---- suggestedfix_channels_4_2 -- --package missingfunction -- --func channels(s string) { -- undefinedChannels(c()) //@suggestedfix("undefinedChannels", "quickfix", "") --} -- --func undefinedChannels(ch1 <-chan string, ch2 chan string) { -- panic("unimplemented") --} -- --func c() (<-chan string, chan string) { -- return make(<-chan string), make(chan string) --} -- -diff -urN a/gopls/internal/lsp/testdata/missingfunction/consecutive_params.go b/gopls/internal/lsp/testdata/missingfunction/consecutive_params.go ---- a/gopls/internal/lsp/testdata/missingfunction/consecutive_params.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/missingfunction/consecutive_params.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,6 +0,0 @@ --package missingfunction -- --func consecutiveParams() { -- var s string -- undefinedConsecutiveParams(s, s) //@suggestedfix("undefinedConsecutiveParams", "quickfix", "") --} -diff -urN a/gopls/internal/lsp/testdata/missingfunction/consecutive_params.go.golden b/gopls/internal/lsp/testdata/missingfunction/consecutive_params.go.golden ---- a/gopls/internal/lsp/testdata/missingfunction/consecutive_params.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/missingfunction/consecutive_params.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,12 +0,0 @@ ---- suggestedfix_consecutive_params_5_2 -- --package missingfunction -- --func consecutiveParams() { -- var s string -- undefinedConsecutiveParams(s, s) //@suggestedfix("undefinedConsecutiveParams", "quickfix", "") --} -- --func undefinedConsecutiveParams(s1, s2 string) { -- panic("unimplemented") --} -- -diff -urN a/gopls/internal/lsp/testdata/missingfunction/error_param.go b/gopls/internal/lsp/testdata/missingfunction/error_param.go ---- a/gopls/internal/lsp/testdata/missingfunction/error_param.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/missingfunction/error_param.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,6 +0,0 @@ --package missingfunction -- --func errorParam() { -- var err error -- undefinedErrorParam(err) //@suggestedfix("undefinedErrorParam", "quickfix", "") --} -diff -urN a/gopls/internal/lsp/testdata/missingfunction/error_param.go.golden b/gopls/internal/lsp/testdata/missingfunction/error_param.go.golden ---- a/gopls/internal/lsp/testdata/missingfunction/error_param.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/missingfunction/error_param.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,12 +0,0 @@ ---- suggestedfix_error_param_5_2 -- --package missingfunction -- --func errorParam() { -- var err error -- undefinedErrorParam(err) //@suggestedfix("undefinedErrorParam", "quickfix", "") +-// Sandbox holds a collection of temporary resources to use for working with Go +-// code in tests. +-type Sandbox struct { +- gopath string +- rootdir string +- goproxy string +- Workdir *Workdir +- goCommandRunner gocommand.Runner -} - --func undefinedErrorParam(err error) { -- panic("unimplemented") +-// SandboxConfig controls the behavior of a test sandbox. The zero value +-// defines a reasonable default. +-type SandboxConfig struct { +- // RootDir sets the base directory to use when creating temporary +- // directories. If not specified, defaults to a new temporary directory. +- RootDir string +- // Files holds a txtar-encoded archive of files to populate the initial state +- // of the working directory. +- // +- // For convenience, the special substring "$SANDBOX_WORKDIR" is replaced with +- // the sandbox's resolved working directory before writing files. +- Files map[string][]byte +- // InGoPath specifies that the working directory should be within the +- // temporary GOPATH. +- InGoPath bool +- // Workdir configures the working directory of the Sandbox. It behaves as +- // follows: +- // - if set to an absolute path, use that path as the working directory. +- // - if set to a relative path, create and use that path relative to the +- // sandbox. +- // - if unset, default to a the 'work' subdirectory of the sandbox. +- // +- // This option is incompatible with InGoPath or Files. +- Workdir string +- // ProxyFiles holds a txtar-encoded archive of files to populate a file-based +- // Go proxy. +- ProxyFiles map[string][]byte +- // GOPROXY is the explicit GOPROXY value that should be used for the sandbox. +- // +- // This option is incompatible with ProxyFiles. +- GOPROXY string -} - -diff -urN a/gopls/internal/lsp/testdata/missingfunction/literals.go b/gopls/internal/lsp/testdata/missingfunction/literals.go ---- a/gopls/internal/lsp/testdata/missingfunction/literals.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/missingfunction/literals.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,7 +0,0 @@ --package missingfunction +-// NewSandbox creates a collection of named temporary resources, with a +-// working directory populated by the txtar-encoded content in srctxt, and a +-// file-based module proxy populated with the txtar-encoded content in +-// proxytxt. +-// +-// If rootDir is non-empty, it will be used as the root of temporary +-// directories created for the sandbox. Otherwise, a new temporary directory +-// will be used as root. +-// +-// TODO(rfindley): the sandbox abstraction doesn't seem to carry its weight. +-// Sandboxes should be composed out of their building-blocks, rather than via a +-// monolithic configuration. +-func NewSandbox(config *SandboxConfig) (_ *Sandbox, err error) { +- if config == nil { +- config = new(SandboxConfig) +- } +- if err := validateConfig(*config); err != nil { +- return nil, fmt.Errorf("invalid SandboxConfig: %v", err) +- } - --type T struct{} +- sb := &Sandbox{} +- defer func() { +- // Clean up if we fail at any point in this constructor. +- if err != nil { +- sb.Close() +- } +- }() - --func literals() { -- undefinedLiterals("hey compiler", T{}, &T{}) //@suggestedfix("undefinedLiterals", "quickfix", "") +- rootDir := config.RootDir +- if rootDir == "" { +- rootDir, err = os.MkdirTemp(config.RootDir, "gopls-sandbox-") +- if err != nil { +- return nil, fmt.Errorf("creating temporary workdir: %v", err) +- } +- } +- sb.rootdir = rootDir +- sb.gopath = filepath.Join(sb.rootdir, "gopath") +- if err := os.Mkdir(sb.gopath, 0755); err != nil { +- return nil, err +- } +- if config.GOPROXY != "" { +- sb.goproxy = config.GOPROXY +- } else { +- proxydir := filepath.Join(sb.rootdir, "proxy") +- if err := os.Mkdir(proxydir, 0755); err != nil { +- return nil, err +- } +- sb.goproxy, err = WriteProxy(proxydir, config.ProxyFiles) +- if err != nil { +- return nil, err +- } +- } +- // Short-circuit writing the workdir if we're given an absolute path, since +- // this is used for running in an existing directory. +- // TODO(findleyr): refactor this to be less of a workaround. +- if filepath.IsAbs(config.Workdir) { +- sb.Workdir, err = NewWorkdir(config.Workdir, nil) +- if err != nil { +- return nil, err +- } +- return sb, nil +- } +- var workdir string +- if config.Workdir == "" { +- if config.InGoPath { +- // Set the working directory as $GOPATH/src. +- workdir = filepath.Join(sb.gopath, "src") +- } else if workdir == "" { +- workdir = filepath.Join(sb.rootdir, "work") +- } +- } else { +- // relative path +- workdir = filepath.Join(sb.rootdir, config.Workdir) +- } +- if err := os.MkdirAll(workdir, 0755); err != nil { +- return nil, err +- } +- sb.Workdir, err = NewWorkdir(workdir, config.Files) +- if err != nil { +- return nil, err +- } +- return sb, nil -} -diff -urN a/gopls/internal/lsp/testdata/missingfunction/literals.go.golden b/gopls/internal/lsp/testdata/missingfunction/literals.go.golden ---- a/gopls/internal/lsp/testdata/missingfunction/literals.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/missingfunction/literals.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,13 +0,0 @@ ---- suggestedfix_literals_6_2 -- --package missingfunction -- --type T struct{} - --func literals() { -- undefinedLiterals("hey compiler", T{}, &T{}) //@suggestedfix("undefinedLiterals", "quickfix", "") +-// Tempdir creates a new temp directory with the given txtar-encoded files. It +-// is the responsibility of the caller to call os.RemoveAll on the returned +-// file path when it is no longer needed. +-func Tempdir(files map[string][]byte) (string, error) { +- dir, err := os.MkdirTemp("", "gopls-tempdir-") +- if err != nil { +- return "", err +- } +- for name, data := range files { +- if err := writeFileData(name, data, RelativeTo(dir)); err != nil { +- return "", fmt.Errorf("writing to tempdir: %w", err) +- } +- } +- return dir, nil -} - --func undefinedLiterals(s string, t1 T, t2 *T) { -- panic("unimplemented") +-func UnpackTxt(txt string) map[string][]byte { +- dataMap := make(map[string][]byte) +- archive := txtar.Parse([]byte(txt)) +- for _, f := range archive.Files { +- if _, ok := dataMap[f.Name]; ok { +- panic(fmt.Sprintf("found file %q twice", f.Name)) +- } +- dataMap[f.Name] = f.Data +- } +- return dataMap -} - -diff -urN a/gopls/internal/lsp/testdata/missingfunction/operation.go b/gopls/internal/lsp/testdata/missingfunction/operation.go ---- a/gopls/internal/lsp/testdata/missingfunction/operation.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/missingfunction/operation.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,7 +0,0 @@ --package missingfunction -- --import "time" -- --func operation() { -- undefinedOperation(10 * time.Second) //@suggestedfix("undefinedOperation", "quickfix", "") +-func validateConfig(config SandboxConfig) error { +- if filepath.IsAbs(config.Workdir) && (len(config.Files) > 0 || config.InGoPath) { +- return errors.New("absolute Workdir cannot be set in conjunction with Files or InGoPath") +- } +- if config.Workdir != "" && config.InGoPath { +- return errors.New("Workdir cannot be set in conjunction with InGoPath") +- } +- if config.GOPROXY != "" && config.ProxyFiles != nil { +- return errors.New("GOPROXY cannot be set in conjunction with ProxyFiles") +- } +- return nil -} -diff -urN a/gopls/internal/lsp/testdata/missingfunction/operation.go.golden b/gopls/internal/lsp/testdata/missingfunction/operation.go.golden ---- a/gopls/internal/lsp/testdata/missingfunction/operation.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/missingfunction/operation.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,13 +0,0 @@ ---- suggestedfix_operation_6_2 -- --package missingfunction - --import "time" -- --func operation() { -- undefinedOperation(10 * time.Second) //@suggestedfix("undefinedOperation", "quickfix", "") +-// splitModuleVersionPath extracts module information from files stored in the +-// directory structure modulePath@version/suffix. +-// For example: +-// +-// splitModuleVersionPath("mod.com@v1.2.3/package") = ("mod.com", "v1.2.3", "package") +-func splitModuleVersionPath(path string) (modulePath, version, suffix string) { +- parts := strings.Split(path, "/") +- var modulePathParts []string +- for i, p := range parts { +- if strings.Contains(p, "@") { +- mv := strings.SplitN(p, "@", 2) +- modulePathParts = append(modulePathParts, mv[0]) +- return strings.Join(modulePathParts, "/"), mv[1], strings.Join(parts[i+1:], "/") +- } +- modulePathParts = append(modulePathParts, p) +- } +- // Default behavior: this is just a module path. +- return path, "", "" -} - --func undefinedOperation(duration time.Duration) { -- panic("unimplemented") +-func (sb *Sandbox) RootDir() string { +- return sb.rootdir -} - -diff -urN a/gopls/internal/lsp/testdata/missingfunction/selector.go b/gopls/internal/lsp/testdata/missingfunction/selector.go ---- a/gopls/internal/lsp/testdata/missingfunction/selector.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/missingfunction/selector.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,6 +0,0 @@ --package missingfunction -- --func selector() { -- m := map[int]bool{} -- undefinedSelector(m[1]) //@suggestedfix("undefinedSelector", "quickfix", "") +-// GOPATH returns the value of the Sandbox GOPATH. +-func (sb *Sandbox) GOPATH() string { +- return sb.gopath -} -diff -urN a/gopls/internal/lsp/testdata/missingfunction/selector.go.golden b/gopls/internal/lsp/testdata/missingfunction/selector.go.golden ---- a/gopls/internal/lsp/testdata/missingfunction/selector.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/missingfunction/selector.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,12 +0,0 @@ ---- suggestedfix_selector_5_2 -- --package missingfunction - --func selector() { -- m := map[int]bool{} -- undefinedSelector(m[1]) //@suggestedfix("undefinedSelector", "quickfix", "") +-// GoEnv returns the default environment variables that can be used for +-// invoking Go commands in the sandbox. +-func (sb *Sandbox) GoEnv() map[string]string { +- vars := map[string]string{ +- "GOPATH": sb.GOPATH(), +- "GOPROXY": sb.goproxy, +- "GO111MODULE": "", +- "GOSUMDB": "off", +- "GOPACKAGESDRIVER": "off", +- } +- if testenv.Go1Point() >= 5 { +- vars["GOMODCACHE"] = "" +- } +- return vars -} - --func undefinedSelector(b bool) { -- panic("unimplemented") +-// goCommandInvocation returns a new gocommand.Invocation initialized with the +-// sandbox environment variables and working directory. +-func (sb *Sandbox) goCommandInvocation() gocommand.Invocation { +- var vars []string +- for k, v := range sb.GoEnv() { +- vars = append(vars, fmt.Sprintf("%s=%s", k, v)) +- } +- inv := gocommand.Invocation{ +- Env: vars, +- } +- // sb.Workdir may be nil if we exited the constructor with errors (we call +- // Close to clean up any partial state from the constructor, which calls +- // RunGoCommand). +- if sb.Workdir != nil { +- inv.WorkingDir = string(sb.Workdir.RelativeTo) +- } +- return inv -} - -diff -urN a/gopls/internal/lsp/testdata/missingfunction/slice.go b/gopls/internal/lsp/testdata/missingfunction/slice.go ---- a/gopls/internal/lsp/testdata/missingfunction/slice.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/missingfunction/slice.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,5 +0,0 @@ --package missingfunction -- --func slice() { -- undefinedSlice([]int{1, 2}) //@suggestedfix("undefinedSlice", "quickfix", "") +-// RunGoCommand executes a go command in the sandbox. If checkForFileChanges is +-// true, the sandbox scans the working directory and emits file change events +-// for any file changes it finds. +-func (sb *Sandbox) RunGoCommand(ctx context.Context, dir, verb string, args, env []string, checkForFileChanges bool) error { +- inv := sb.goCommandInvocation() +- inv.Verb = verb +- inv.Args = args +- inv.Env = append(inv.Env, env...) +- if dir != "" { +- inv.WorkingDir = sb.Workdir.AbsPath(dir) +- } +- stdout, stderr, _, err := sb.goCommandRunner.RunRaw(ctx, inv) +- if err != nil { +- return fmt.Errorf("go command failed (stdout: %s) (stderr: %s): %v", stdout.String(), stderr.String(), err) +- } +- // Since running a go command may result in changes to workspace files, +- // check if we need to send any "watched" file events. +- // +- // TODO(rFindley): this side-effect can impact the usability of the sandbox +- // for benchmarks. Consider refactoring. +- if sb.Workdir != nil && checkForFileChanges { +- if err := sb.Workdir.CheckForFileChanges(ctx); err != nil { +- return fmt.Errorf("checking for file changes: %w", err) +- } +- } +- return nil -} -diff -urN a/gopls/internal/lsp/testdata/missingfunction/slice.go.golden b/gopls/internal/lsp/testdata/missingfunction/slice.go.golden ---- a/gopls/internal/lsp/testdata/missingfunction/slice.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/missingfunction/slice.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,11 +0,0 @@ ---- suggestedfix_slice_4_2 -- --package missingfunction - --func slice() { -- undefinedSlice([]int{1, 2}) //@suggestedfix("undefinedSlice", "quickfix", "") +-// GoVersion checks the version of the go command. +-// It returns the X in Go 1.X. +-func (sb *Sandbox) GoVersion(ctx context.Context) (int, error) { +- inv := sb.goCommandInvocation() +- return gocommand.GoVersion(ctx, inv, &sb.goCommandRunner) -} - --func undefinedSlice(i []int) { -- panic("unimplemented") +-// Close removes all state associated with the sandbox. +-func (sb *Sandbox) Close() error { +- var goCleanErr error +- if sb.gopath != "" { +- goCleanErr = sb.RunGoCommand(context.Background(), "", "clean", []string{"-modcache"}, nil, false) +- } +- err := robustio.RemoveAll(sb.rootdir) +- if err != nil || goCleanErr != nil { +- return fmt.Errorf("error(s) cleaning sandbox: cleaning modcache: %v; removing files: %v", goCleanErr, err) +- } +- return nil -} +diff -urN a/gopls/internal/test/integration/fake/workdir.go b/gopls/internal/test/integration/fake/workdir.go +--- a/gopls/internal/test/integration/fake/workdir.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/fake/workdir.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,424 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -diff -urN a/gopls/internal/lsp/testdata/missingfunction/tuple.go b/gopls/internal/lsp/testdata/missingfunction/tuple.go ---- a/gopls/internal/lsp/testdata/missingfunction/tuple.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/missingfunction/tuple.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,9 +0,0 @@ --package missingfunction -- --func tuple() { -- undefinedTuple(b()) //@suggestedfix("undefinedTuple", "quickfix", "") --} +-package fake - --func b() (string, error) { -- return "", nil --} -diff -urN a/gopls/internal/lsp/testdata/missingfunction/tuple.go.golden b/gopls/internal/lsp/testdata/missingfunction/tuple.go.golden ---- a/gopls/internal/lsp/testdata/missingfunction/tuple.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/missingfunction/tuple.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,15 +0,0 @@ ---- suggestedfix_tuple_4_2 -- --package missingfunction +-import ( +- "bytes" +- "context" +- "crypto/sha256" +- "fmt" +- "io/fs" +- "os" +- "path/filepath" +- "runtime" +- "sort" +- "strings" +- "sync" +- "time" - --func tuple() { -- undefinedTuple(b()) //@suggestedfix("undefinedTuple", "quickfix", "") --} +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/robustio" +-) - --func undefinedTuple(s string, err error) { -- panic("unimplemented") --} +-// RelativeTo is a helper for operations relative to a given directory. +-type RelativeTo string - --func b() (string, error) { -- return "", nil +-// AbsPath returns an absolute filesystem path for the workdir-relative path. +-func (r RelativeTo) AbsPath(path string) string { +- fp := filepath.FromSlash(path) +- if filepath.IsAbs(fp) { +- return fp +- } +- return filepath.Join(string(r), filepath.FromSlash(path)) -} - -diff -urN a/gopls/internal/lsp/testdata/missingfunction/unique_params.go b/gopls/internal/lsp/testdata/missingfunction/unique_params.go ---- a/gopls/internal/lsp/testdata/missingfunction/unique_params.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/missingfunction/unique_params.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,7 +0,0 @@ --package missingfunction -- --func uniqueArguments() { -- var s string -- var i int -- undefinedUniqueArguments(s, i, s) //@suggestedfix("undefinedUniqueArguments", "quickfix", "") +-// RelPath returns a '/'-encoded path relative to the working directory (or an +-// absolute path if the file is outside of workdir) +-func (r RelativeTo) RelPath(fp string) string { +- root := string(r) +- if rel, err := filepath.Rel(root, fp); err == nil && !strings.HasPrefix(rel, "..") { +- return filepath.ToSlash(rel) +- } +- return filepath.ToSlash(fp) -} -diff -urN a/gopls/internal/lsp/testdata/missingfunction/unique_params.go.golden b/gopls/internal/lsp/testdata/missingfunction/unique_params.go.golden ---- a/gopls/internal/lsp/testdata/missingfunction/unique_params.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/missingfunction/unique_params.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,13 +0,0 @@ ---- suggestedfix_unique_params_6_2 -- --package missingfunction - --func uniqueArguments() { -- var s string -- var i int -- undefinedUniqueArguments(s, i, s) //@suggestedfix("undefinedUniqueArguments", "quickfix", "") +-// writeFileData writes content to the relative path, replacing the special +-// token $SANDBOX_WORKDIR with the relative root given by rel. It does not +-// trigger any file events. +-func writeFileData(path string, content []byte, rel RelativeTo) error { +- content = bytes.ReplaceAll(content, []byte("$SANDBOX_WORKDIR"), []byte(rel)) +- fp := rel.AbsPath(path) +- if err := os.MkdirAll(filepath.Dir(fp), 0755); err != nil { +- return fmt.Errorf("creating nested directory: %w", err) +- } +- backoff := 1 * time.Millisecond +- for { +- err := os.WriteFile(fp, content, 0644) +- if err != nil { +- // This lock file violation is not handled by the robustio package, as it +- // indicates a real race condition that could be avoided. +- if isWindowsErrLockViolation(err) { +- time.Sleep(backoff) +- backoff *= 2 +- continue +- } +- return fmt.Errorf("writing %q: %w", path, err) +- } +- return nil +- } -} - --func undefinedUniqueArguments(s1 string, i int, s2 string) { -- panic("unimplemented") --} +-// isWindowsErrLockViolation reports whether err is ERROR_LOCK_VIOLATION +-// on Windows. +-var isWindowsErrLockViolation = func(err error) bool { return false } - -diff -urN a/gopls/internal/lsp/testdata/nested_complit/nested_complit.go.in b/gopls/internal/lsp/testdata/nested_complit/nested_complit.go.in ---- a/gopls/internal/lsp/testdata/nested_complit/nested_complit.go.in 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/nested_complit/nested_complit.go.in 1970-01-01 00:00:00.000000000 +0000 -@@ -1,15 +0,0 @@ --package nested_complit +-// Workdir is a temporary working directory for tests. It exposes file +-// operations in terms of relative paths, and fakes file watching by triggering +-// events on file operations. +-type Workdir struct { +- RelativeTo - --type ncFoo struct {} //@item(structNCFoo, "ncFoo", "struct{...}", "struct") +- watcherMu sync.Mutex +- watchers []func(context.Context, []protocol.FileEvent) - --type ncBar struct { //@item(structNCBar, "ncBar", "struct{...}", "struct") -- baz []ncFoo +- fileMu sync.Mutex +- // File identities we know about, for the purpose of detecting changes. +- // +- // Since files is only used for detecting _changes_, we are tolerant of +- // fileIDs that may have hash and mtime coming from different states of the +- // file: if either are out of sync, then the next poll should detect a +- // discrepancy. It is OK if we detect too many changes, but not OK if we miss +- // changes. +- // +- // For that matter, this mechanism for detecting changes can still be flaky +- // on platforms where mtime is very coarse (such as older versions of WSL). +- // It would be much better to use a proper fs event library, but we can't +- // currently import those into x/tools. +- // +- // TODO(golang/go#52284): replace this polling mechanism with a +- // cross-platform library for filesystem notifications. +- files map[string]fileID -} - --func _() { -- []ncFoo{} //@item(litNCFoo, "[]ncFoo{}", "", "var") -- _ := ncBar{ -- // disabled - see issue #54822 -- baz: [] // complete(" //", structNCFoo, structNCBar) +-// NewWorkdir writes the txtar-encoded file data in txt to dir, and returns a +-// Workir for operating on these files using +-func NewWorkdir(dir string, files map[string][]byte) (*Workdir, error) { +- w := &Workdir{RelativeTo: RelativeTo(dir)} +- for name, data := range files { +- if err := writeFileData(name, data, w.RelativeTo); err != nil { +- return nil, fmt.Errorf("writing to workdir: %w", err) +- } - } +- _, err := w.pollFiles() // poll files to populate the files map. +- return w, err -} -diff -urN a/gopls/internal/lsp/testdata/%percent/perc%ent.go b/gopls/internal/lsp/testdata/%percent/perc%ent.go ---- a/gopls/internal/lsp/testdata/%percent/perc%ent.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/%percent/perc%ent.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1 +0,0 @@ --package percent -diff -urN a/gopls/internal/lsp/testdata/rename/a/random.go.golden b/gopls/internal/lsp/testdata/rename/a/random.go.golden ---- a/gopls/internal/lsp/testdata/rename/a/random.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/a/random.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,616 +0,0 @@ ---- GetSum-rename -- --package a -- --import ( -- lg "log" -- "fmt" //@rename("fmt", "fmty") -- f2 "fmt" //@rename("f2", "f2name"),rename("fmt","f2y") --) - --func Random() int { -- y := 6 + 7 -- return y +-// fileID identifies a file version on disk. +-type fileID struct { +- mtime time.Time +- hash string // empty if mtime is old enough to be reliable; otherwise a file digest -} - --func Random2(y int) int { //@rename("y", "z") -- return y +-func hashFile(data []byte) string { +- return fmt.Sprintf("%x", sha256.Sum256(data)) -} - --type Pos struct { -- x, y int +-// RootURI returns the root URI for this working directory of this scratch +-// environment. +-func (w *Workdir) RootURI() protocol.DocumentURI { +- return protocol.URIFromPath(string(w.RelativeTo)) -} - --func (p *Pos) GetSum() int { -- return p.x + p.y //@rename("x", "myX") +-// AddWatcher registers the given func to be called on any file change. +-func (w *Workdir) AddWatcher(watcher func(context.Context, []protocol.FileEvent)) { +- w.watcherMu.Lock() +- w.watchers = append(w.watchers, watcher) +- w.watcherMu.Unlock() -} - --func _() { -- var p Pos //@rename("p", "pos") -- _ = p.GetSum() //@rename("Sum", "GetSum") +-// URI returns the URI to a the workdir-relative path. +-func (w *Workdir) URI(path string) protocol.DocumentURI { +- return protocol.URIFromPath(w.AbsPath(path)) -} - --func sw() { -- var x interface{} +-// URIToPath converts a uri to a workdir-relative path (or an absolute path, +-// if the uri is outside of the workdir). +-func (w *Workdir) URIToPath(uri protocol.DocumentURI) string { +- return w.RelPath(uri.Path()) +-} - -- switch y := x.(type) { //@rename("y", "y0") -- case int: -- fmt.Printf("%d", y) //@rename("y", "y1"),rename("fmt", "format") -- case string: -- lg.Printf("%s", y) //@rename("y", "y2"),rename("lg","log") -- default: -- f2.Printf("%v", y) //@rename("y", "y3"),rename("f2","fmt2") +-// ReadFile reads a text file specified by a workdir-relative path. +-func (w *Workdir) ReadFile(path string) ([]byte, error) { +- backoff := 1 * time.Millisecond +- for { +- b, err := os.ReadFile(w.AbsPath(path)) +- if err != nil { +- if runtime.GOOS == "plan9" && strings.HasSuffix(err.Error(), " exclusive use file already open") { +- // Plan 9 enforces exclusive access to locked files. +- // Give the owner time to unlock it and retry. +- time.Sleep(backoff) +- backoff *= 2 +- continue +- } +- return nil, err +- } +- return b, nil - } -} - ---- f2name-rename -- --package a -- --import ( -- lg "log" -- "fmt" //@rename("fmt", "fmty") -- f2name "fmt" //@rename("f2", "f2name"),rename("fmt","f2y") --) -- --func Random() int { -- y := 6 + 7 -- return y +-// RegexpSearch searches the file corresponding to path for the first position +-// matching re. +-func (w *Workdir) RegexpSearch(path string, re string) (protocol.Location, error) { +- content, err := w.ReadFile(path) +- if err != nil { +- return protocol.Location{}, err +- } +- mapper := protocol.NewMapper(w.URI(path), content) +- return regexpLocation(mapper, re) -} - --func Random2(y int) int { //@rename("y", "z") -- return y --} +-// RemoveFile removes a workdir-relative file path and notifies watchers of the +-// change. +-func (w *Workdir) RemoveFile(ctx context.Context, path string) error { +- fp := w.AbsPath(path) +- if err := robustio.RemoveAll(fp); err != nil { +- return fmt.Errorf("removing %q: %w", path, err) +- } - --type Pos struct { -- x, y int +- return w.CheckForFileChanges(ctx) -} - --func (p *Pos) Sum() int { -- return p.x + p.y //@rename("x", "myX") +-// WriteFiles writes the text file content to workdir-relative paths and +-// notifies watchers of the changes. +-func (w *Workdir) WriteFiles(ctx context.Context, files map[string]string) error { +- for path, content := range files { +- fp := w.AbsPath(path) +- _, err := os.Stat(fp) +- if err != nil && !os.IsNotExist(err) { +- return fmt.Errorf("checking if %q exists: %w", path, err) +- } +- if err := writeFileData(path, []byte(content), w.RelativeTo); err != nil { +- return err +- } +- } +- return w.CheckForFileChanges(ctx) -} - --func _() { -- var p Pos //@rename("p", "pos") -- _ = p.Sum() //@rename("Sum", "GetSum") +-// WriteFile writes text file content to a workdir-relative path and notifies +-// watchers of the change. +-func (w *Workdir) WriteFile(ctx context.Context, path, content string) error { +- return w.WriteFiles(ctx, map[string]string{path: content}) -} - --func sw() { -- var x interface{} +-// RenameFile performs an on disk-renaming of the workdir-relative oldPath to +-// workdir-relative newPath, and notifies watchers of the changes. +-// +-// oldPath must either be a regular file or in the same directory as newPath. +-func (w *Workdir) RenameFile(ctx context.Context, oldPath, newPath string) error { +- oldAbs := w.AbsPath(oldPath) +- newAbs := w.AbsPath(newPath) - -- switch y := x.(type) { //@rename("y", "y0") -- case int: -- fmt.Printf("%d", y) //@rename("y", "y1"),rename("fmt", "format") -- case string: -- lg.Printf("%s", y) //@rename("y", "y2"),rename("lg","log") -- default: -- f2name.Printf("%v", y) //@rename("y", "y3"),rename("f2","fmt2") +- // For os.Rename, “OS-specific restrictions may apply when oldpath and newpath +- // are in different directories.” If that applies here, we may fall back to +- // ReadFile, WriteFile, and RemoveFile to perform the rename non-atomically. +- // +- // However, the fallback path only works for regular files: renaming a +- // directory would be much more complex and isn't needed for our tests. +- fallbackOk := false +- if filepath.Dir(oldAbs) != filepath.Dir(newAbs) { +- fi, err := os.Stat(oldAbs) +- if err == nil && !fi.Mode().IsRegular() { +- return &os.PathError{ +- Op: "RenameFile", +- Path: oldPath, +- Err: fmt.Errorf("%w: file is not regular and not in the same directory as %s", os.ErrInvalid, newPath), +- } +- } +- fallbackOk = true - } --} -- ---- f2y-rename -- --package a -- --import ( -- lg "log" -- "fmt" //@rename("fmt", "fmty") -- f2y "fmt" //@rename("f2", "f2name"),rename("fmt","f2y") --) -- --func Random() int { -- y := 6 + 7 -- return y --} - --func Random2(y int) int { //@rename("y", "z") -- return y --} +- var renameErr error +- const debugFallback = false +- if fallbackOk && debugFallback { +- renameErr = fmt.Errorf("%w: debugging fallback path", os.ErrInvalid) +- } else { +- renameErr = robustio.Rename(oldAbs, newAbs) +- } +- if renameErr != nil { +- if !fallbackOk { +- return renameErr // The OS-specific Rename restrictions do not apply. +- } - --type Pos struct { -- x, y int --} +- content, err := w.ReadFile(oldPath) +- if err != nil { +- // If we can't even read the file, the error from Rename may be accurate. +- return renameErr +- } +- fi, err := os.Stat(newAbs) +- if err == nil { +- if fi.IsDir() { +- // “If newpath already exists and is not a directory, Rename replaces it.” +- // But if it is a directory, maybe not? +- return renameErr +- } +- // On most platforms, Rename replaces the named file with a new file, +- // rather than overwriting the existing file it in place. Mimic that +- // behavior here. +- if err := robustio.RemoveAll(newAbs); err != nil { +- // Maybe we don't have permission to replace newPath? +- return renameErr +- } +- } else if !os.IsNotExist(err) { +- // If the destination path already exists or there is some problem with it, +- // the error from Rename may be accurate. +- return renameErr +- } +- if writeErr := writeFileData(newPath, content, w.RelativeTo); writeErr != nil { +- // At this point we have tried to actually write the file. +- // If it still doesn't exist, assume that the error from Rename was accurate: +- // for example, maybe we don't have permission to create the new path. +- // Otherwise, return the error from the write, which may indicate some +- // other problem (such as a full disk). +- if _, statErr := os.Stat(newAbs); !os.IsNotExist(statErr) { +- return writeErr +- } +- return renameErr +- } +- if err := robustio.RemoveAll(oldAbs); err != nil { +- // If we failed to remove the old file, that may explain the Rename error too. +- // Make a best effort to back out the write to the new path. +- robustio.RemoveAll(newAbs) +- return renameErr +- } +- } - --func (p *Pos) Sum() int { -- return p.x + p.y //@rename("x", "myX") +- return w.CheckForFileChanges(ctx) -} - --func _() { -- var p Pos //@rename("p", "pos") -- _ = p.Sum() //@rename("Sum", "GetSum") +-// ListFiles returns a new sorted list of the relative paths of files in dir, +-// recursively. +-func (w *Workdir) ListFiles(dir string) ([]string, error) { +- absDir := w.AbsPath(dir) +- var paths []string +- if err := filepath.Walk(absDir, func(fp string, info os.FileInfo, err error) error { +- if err != nil { +- return err +- } +- if info.Mode()&(fs.ModeDir|fs.ModeSymlink) == 0 { +- paths = append(paths, w.RelPath(fp)) +- } +- return nil +- }); err != nil { +- return nil, err +- } +- sort.Strings(paths) +- return paths, nil -} - --func sw() { -- var x interface{} -- -- switch y := x.(type) { //@rename("y", "y0") -- case int: -- fmt.Printf("%d", y) //@rename("y", "y1"),rename("fmt", "format") -- case string: -- lg.Printf("%s", y) //@rename("y", "y2"),rename("lg","log") -- default: -- f2y.Printf("%v", y) //@rename("y", "y3"),rename("f2","fmt2") +-// CheckForFileChanges walks the working directory and checks for any files +-// that have changed since the last poll. +-func (w *Workdir) CheckForFileChanges(ctx context.Context) error { +- evts, err := w.pollFiles() +- if err != nil { +- return err +- } +- if len(evts) == 0 { +- return nil +- } +- w.watcherMu.Lock() +- watchers := make([]func(context.Context, []protocol.FileEvent), len(w.watchers)) +- copy(watchers, w.watchers) +- w.watcherMu.Unlock() +- for _, w := range watchers { +- w(ctx, evts) - } +- return nil -} - ---- fmt2-rename -- --package a -- --import ( -- lg "log" -- "fmt" //@rename("fmt", "fmty") -- fmt2 "fmt" //@rename("f2", "f2name"),rename("fmt","f2y") --) -- --func Random() int { -- y := 6 + 7 -- return y --} +-// pollFiles updates w.files and calculates FileEvents corresponding to file +-// state changes since the last poll. It does not call sendEvents. +-func (w *Workdir) pollFiles() ([]protocol.FileEvent, error) { +- w.fileMu.Lock() +- defer w.fileMu.Unlock() - --func Random2(y int) int { //@rename("y", "z") -- return y --} +- newFiles := make(map[string]fileID) +- var evts []protocol.FileEvent +- if err := filepath.Walk(string(w.RelativeTo), func(fp string, info os.FileInfo, err error) error { +- if err != nil { +- return err +- } +- // Skip directories and symbolic links (which may be links to directories). +- // +- // The latter matters for repos like Kubernetes, which use symlinks. +- if info.Mode()&(fs.ModeDir|fs.ModeSymlink) != 0 { +- return nil +- } - --type Pos struct { -- x, y int --} +- // Opt: avoid reading the file if mtime is sufficiently old to be reliable. +- // +- // If mtime is recent, it may not sufficiently identify the file contents: +- // a subsequent write could result in the same mtime. For these cases, we +- // must read the file contents. +- id := fileID{mtime: info.ModTime()} +- if time.Since(info.ModTime()) < 2*time.Second { +- data, err := os.ReadFile(fp) +- if err != nil { +- return err +- } +- id.hash = hashFile(data) +- } +- path := w.RelPath(fp) +- newFiles[path] = id - --func (p *Pos) Sum() int { -- return p.x + p.y //@rename("x", "myX") --} +- if w.files != nil { +- oldID, ok := w.files[path] +- delete(w.files, path) +- switch { +- case !ok: +- evts = append(evts, protocol.FileEvent{ +- URI: w.URI(path), +- Type: protocol.Created, +- }) +- case oldID != id: +- changed := true - --func _() { -- var p Pos //@rename("p", "pos") -- _ = p.Sum() //@rename("Sum", "GetSum") --} +- // Check whether oldID and id do not match because oldID was polled at +- // a recent enough to time such as to require hashing. +- // +- // In this case, read the content to check whether the file actually +- // changed. +- if oldID.mtime.Equal(id.mtime) && oldID.hash != "" && id.hash == "" { +- data, err := os.ReadFile(fp) +- if err != nil { +- return err +- } +- if hashFile(data) == oldID.hash { +- changed = false +- } +- } +- if changed { +- evts = append(evts, protocol.FileEvent{ +- URI: w.URI(path), +- Type: protocol.Changed, +- }) +- } +- } +- } - --func sw() { -- var x interface{} +- return nil +- }); err != nil { +- return nil, err +- } - -- switch y := x.(type) { //@rename("y", "y0") -- case int: -- fmt.Printf("%d", y) //@rename("y", "y1"),rename("fmt", "format") -- case string: -- lg.Printf("%s", y) //@rename("y", "y2"),rename("lg","log") -- default: -- fmt2.Printf("%v", y) //@rename("y", "y3"),rename("f2","fmt2") +- // Any remaining files must have been deleted. +- for path := range w.files { +- evts = append(evts, protocol.FileEvent{ +- URI: w.URI(path), +- Type: protocol.Deleted, +- }) - } +- w.files = newFiles +- return evts, nil -} +diff -urN a/gopls/internal/test/integration/fake/workdir_test.go b/gopls/internal/test/integration/fake/workdir_test.go +--- a/gopls/internal/test/integration/fake/workdir_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/fake/workdir_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,219 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - ---- fmty-rename -- --package a +-package fake - -import ( -- lg "log" -- fmty "fmt" //@rename("fmt", "fmty") -- f2 "fmt" //@rename("f2", "f2name"),rename("fmt","f2y") --) -- --func Random() int { -- y := 6 + 7 -- return y --} -- --func Random2(y int) int { //@rename("y", "z") -- return y --} -- --type Pos struct { -- x, y int --} +- "context" +- "os" +- "sync" +- "testing" - --func (p *Pos) Sum() int { -- return p.x + p.y //@rename("x", "myX") --} +- "github.com/google/go-cmp/cmp" +- "golang.org/x/tools/gopls/internal/protocol" +-) - --func _() { -- var p Pos //@rename("p", "pos") -- _ = p.Sum() //@rename("Sum", "GetSum") --} +-const sharedData = ` +--- go.mod -- +-go 1.12 +--- nested/README.md -- +-Hello World! +-` - --func sw() { -- var x interface{} +-// newWorkdir sets up a temporary Workdir with the given txtar-encoded content. +-// It also configures an eventBuffer to receive file event notifications. These +-// notifications are sent synchronously for each operation, such that once a +-// workdir file operation has returned the caller can expect that any relevant +-// file notifications are present in the buffer. +-// +-// It is the caller's responsibility to call the returned cleanup function. +-func newWorkdir(t *testing.T, txt string) (*Workdir, *eventBuffer, func()) { +- t.Helper() - -- switch y := x.(type) { //@rename("y", "y0") -- case int: -- fmty.Printf("%d", y) //@rename("y", "y1"),rename("fmt", "format") -- case string: -- lg.Printf("%s", y) //@rename("y", "y2"),rename("lg","log") -- default: -- f2.Printf("%v", y) //@rename("y", "y3"),rename("f2","fmt2") +- tmpdir, err := os.MkdirTemp("", "goplstest-workdir-") +- if err != nil { +- t.Fatal(err) +- } +- wd, err := NewWorkdir(tmpdir, UnpackTxt(txt)) +- if err != nil { +- t.Fatal(err) +- } +- cleanup := func() { +- if err := os.RemoveAll(tmpdir); err != nil { +- t.Error(err) +- } - } --} -- ---- format-rename -- --package a -- --import ( -- lg "log" -- format "fmt" //@rename("fmt", "fmty") -- f2 "fmt" //@rename("f2", "f2name"),rename("fmt","f2y") --) -- --func Random() int { -- y := 6 + 7 -- return y --} - --func Random2(y int) int { //@rename("y", "z") -- return y +- buf := new(eventBuffer) +- wd.AddWatcher(buf.onEvents) +- return wd, buf, cleanup -} - --type Pos struct { -- x, y int +-// eventBuffer collects events from a file watcher. +-type eventBuffer struct { +- mu sync.Mutex +- events []protocol.FileEvent -} - --func (p *Pos) Sum() int { -- return p.x + p.y //@rename("x", "myX") --} +-// onEvents collects adds events to the buffer; to be used with Workdir.AddWatcher. +-func (c *eventBuffer) onEvents(_ context.Context, events []protocol.FileEvent) { +- c.mu.Lock() +- defer c.mu.Unlock() - --func _() { -- var p Pos //@rename("p", "pos") -- _ = p.Sum() //@rename("Sum", "GetSum") +- c.events = append(c.events, events...) -} - --func sw() { -- var x interface{} +-// take empties the buffer, returning its previous contents. +-func (c *eventBuffer) take() []protocol.FileEvent { +- c.mu.Lock() +- defer c.mu.Unlock() - -- switch y := x.(type) { //@rename("y", "y0") -- case int: -- format.Printf("%d", y) //@rename("y", "y1"),rename("fmt", "format") -- case string: -- lg.Printf("%s", y) //@rename("y", "y2"),rename("lg","log") -- default: -- f2.Printf("%v", y) //@rename("y", "y3"),rename("f2","fmt2") -- } +- evts := c.events +- c.events = nil +- return evts -} - ---- log-rename -- --package a -- --import ( -- "log" -- "fmt" //@rename("fmt", "fmty") -- f2 "fmt" //@rename("f2", "f2name"),rename("fmt","f2y") --) -- --func Random() int { -- y := 6 + 7 -- return y --} +-func TestWorkdir_ReadFile(t *testing.T) { +- wd, _, cleanup := newWorkdir(t, sharedData) +- defer cleanup() - --func Random2(y int) int { //@rename("y", "z") -- return y +- got, err := wd.ReadFile("nested/README.md") +- if err != nil { +- t.Fatal(err) +- } +- want := "Hello World!\n" +- if got := string(got); got != want { +- t.Errorf("reading workdir file, got %q, want %q", got, want) +- } -} - --type Pos struct { -- x, y int --} +-func TestWorkdir_WriteFile(t *testing.T) { +- wd, events, cleanup := newWorkdir(t, sharedData) +- defer cleanup() +- ctx := context.Background() - --func (p *Pos) Sum() int { -- return p.x + p.y //@rename("x", "myX") --} +- tests := []struct { +- path string +- wantType protocol.FileChangeType +- }{ +- {"data.txt", protocol.Created}, +- {"nested/README.md", protocol.Changed}, +- } - --func _() { -- var p Pos //@rename("p", "pos") -- _ = p.Sum() //@rename("Sum", "GetSum") +- for _, test := range tests { +- if err := wd.WriteFile(ctx, test.path, "42"); err != nil { +- t.Fatal(err) +- } +- es := events.take() +- if got := len(es); got != 1 { +- t.Fatalf("len(events) = %d, want 1", got) +- } +- path := wd.URIToPath(es[0].URI) +- if path != test.path { +- t.Errorf("event path = %q, want %q", path, test.path) +- } +- if es[0].Type != test.wantType { +- t.Errorf("event type = %v, want %v", es[0].Type, test.wantType) +- } +- got, err := wd.ReadFile(test.path) +- if err != nil { +- t.Fatal(err) +- } +- want := "42" +- if got := string(got); got != want { +- t.Errorf("ws.ReadFile(%q) = %q, want %q", test.path, got, want) +- } +- } -} - --func sw() { -- var x interface{} +-// Test for file notifications following file operations. +-func TestWorkdir_FileWatching(t *testing.T) { +- wd, events, cleanup := newWorkdir(t, "") +- defer cleanup() +- ctx := context.Background() - -- switch y := x.(type) { //@rename("y", "y0") -- case int: -- fmt.Printf("%d", y) //@rename("y", "y1"),rename("fmt", "format") -- case string: -- log.Printf("%s", y) //@rename("y", "y2"),rename("lg","log") -- default: -- f2.Printf("%v", y) //@rename("y", "y3"),rename("f2","fmt2") +- must := func(err error) { +- if err != nil { +- t.Fatal(err) +- } - } --} - ---- myX-rename -- --package a -- --import ( -- lg "log" -- "fmt" //@rename("fmt", "fmty") -- f2 "fmt" //@rename("f2", "f2name"),rename("fmt","f2y") --) +- type changeMap map[string]protocol.FileChangeType +- checkEvent := func(wantChanges changeMap) { +- gotChanges := make(changeMap) +- for _, e := range events.take() { +- gotChanges[wd.URIToPath(e.URI)] = e.Type +- } +- if diff := cmp.Diff(wantChanges, gotChanges); diff != "" { +- t.Errorf("mismatching file events (-want +got):\n%s", diff) +- } +- } - --func Random() int { -- y := 6 + 7 -- return y --} +- must(wd.WriteFile(ctx, "foo.go", "package foo")) +- checkEvent(changeMap{"foo.go": protocol.Created}) - --func Random2(y int) int { //@rename("y", "z") -- return y --} +- must(wd.RenameFile(ctx, "foo.go", "bar.go")) +- checkEvent(changeMap{"foo.go": protocol.Deleted, "bar.go": protocol.Created}) - --type Pos struct { -- myX, y int +- must(wd.RemoveFile(ctx, "bar.go")) +- checkEvent(changeMap{"bar.go": protocol.Deleted}) -} - --func (p *Pos) Sum() int { -- return p.myX + p.y //@rename("x", "myX") --} +-func TestWorkdir_CheckForFileChanges(t *testing.T) { +- t.Skip("broken on darwin-amd64-10_12") +- wd, events, cleanup := newWorkdir(t, sharedData) +- defer cleanup() +- ctx := context.Background() - --func _() { -- var p Pos //@rename("p", "pos") -- _ = p.Sum() //@rename("Sum", "GetSum") +- checkChange := func(wantPath string, wantType protocol.FileChangeType) { +- if err := wd.CheckForFileChanges(ctx); err != nil { +- t.Fatal(err) +- } +- ev := events.take() +- if len(ev) == 0 { +- t.Fatal("no file events received") +- } +- gotEvt := ev[0] +- gotPath := wd.URIToPath(gotEvt.URI) +- // Only check relative path and Type +- if gotPath != wantPath || gotEvt.Type != wantType { +- t.Errorf("file events: got %v, want {Path: %s, Type: %v}", gotEvt, wantPath, wantType) +- } +- } +- // Sleep some positive amount of time to ensure a distinct mtime. +- if err := writeFileData("go.mod", []byte("module foo.test\n"), wd.RelativeTo); err != nil { +- t.Fatal(err) +- } +- checkChange("go.mod", protocol.Changed) +- if err := writeFileData("newFile", []byte("something"), wd.RelativeTo); err != nil { +- t.Fatal(err) +- } +- checkChange("newFile", protocol.Created) +- fp := wd.AbsPath("newFile") +- if err := os.Remove(fp); err != nil { +- t.Fatal(err) +- } +- checkChange("newFile", protocol.Deleted) -} - --func sw() { -- var x interface{} +-func TestSplitModuleVersionPath(t *testing.T) { +- tests := []struct { +- path string +- wantModule, wantVersion, wantSuffix string +- }{ +- {"foo.com@v1.2.3/bar", "foo.com", "v1.2.3", "bar"}, +- {"foo.com/module@v1.2.3/bar", "foo.com/module", "v1.2.3", "bar"}, +- {"foo.com@v1.2.3", "foo.com", "v1.2.3", ""}, +- {"std@v1.14.0", "std", "v1.14.0", ""}, +- {"another/module/path", "another/module/path", "", ""}, +- } - -- switch y := x.(type) { //@rename("y", "y0") -- case int: -- fmt.Printf("%d", y) //@rename("y", "y1"),rename("fmt", "format") -- case string: -- lg.Printf("%s", y) //@rename("y", "y2"),rename("lg","log") -- default: -- f2.Printf("%v", y) //@rename("y", "y3"),rename("f2","fmt2") +- for _, test := range tests { +- module, version, suffix := splitModuleVersionPath(test.path) +- if module != test.wantModule || version != test.wantVersion || suffix != test.wantSuffix { +- t.Errorf("splitModuleVersionPath(%q) =\n\t(%q, %q, %q)\nwant\n\t(%q, %q, %q)", +- test.path, module, version, suffix, test.wantModule, test.wantVersion, test.wantSuffix) +- } - } -} +diff -urN a/gopls/internal/test/integration/fake/workdir_windows.go b/gopls/internal/test/integration/fake/workdir_windows.go +--- a/gopls/internal/test/integration/fake/workdir_windows.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/fake/workdir_windows.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,21 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - ---- pos-rename -- --package a +-package fake - -import ( -- lg "log" -- "fmt" //@rename("fmt", "fmty") -- f2 "fmt" //@rename("f2", "f2name"),rename("fmt","f2y") +- "errors" +- "syscall" -) - --func Random() int { -- y := 6 + 7 -- return y --} +-func init() { +- // constants copied from GOROOT/src/internal/syscall/windows/syscall_windows.go +- const ( +- ERROR_LOCK_VIOLATION syscall.Errno = 33 +- ) - --func Random2(y int) int { //@rename("y", "z") -- return y +- isWindowsErrLockViolation = func(err error) bool { +- return errors.Is(err, ERROR_LOCK_VIOLATION) +- } -} +diff -urN a/gopls/internal/test/integration/inlayhints/inlayhints_test.go b/gopls/internal/test/integration/inlayhints/inlayhints_test.go +--- a/gopls/internal/test/integration/inlayhints/inlayhints_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/inlayhints/inlayhints_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,69 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +-package inlayhint - --type Pos struct { -- x, y int --} +-import ( +- "testing" - --func (p *Pos) Sum() int { -- return p.x + p.y //@rename("x", "myX") --} +- "golang.org/x/tools/gopls/internal/golang" +- "golang.org/x/tools/gopls/internal/hooks" +- . "golang.org/x/tools/gopls/internal/test/integration" +- "golang.org/x/tools/gopls/internal/util/bug" +-) - --func _() { -- var pos Pos //@rename("p", "pos") -- _ = pos.Sum() //@rename("Sum", "GetSum") +-func TestMain(m *testing.M) { +- bug.PanicOnBugs = true +- Main(m, hooks.Options) -} - --func sw() { -- var x interface{} -- -- switch y := x.(type) { //@rename("y", "y0") -- case int: -- fmt.Printf("%d", y) //@rename("y", "y1"),rename("fmt", "format") -- case string: -- lg.Printf("%s", y) //@rename("y", "y2"),rename("lg","log") -- default: -- f2.Printf("%v", y) //@rename("y", "y3"),rename("f2","fmt2") +-func TestEnablingInlayHints(t *testing.T) { +- const workspace = ` +--- go.mod -- +-module inlayHint.test +-go 1.12 +--- lib.go -- +-package lib +-type Number int +-const ( +- Zero Number = iota +- One +- Two +-) +-` +- tests := []struct { +- label string +- enabled map[string]bool +- wantInlayHint bool +- }{ +- { +- label: "default", +- wantInlayHint: false, +- }, +- { +- label: "enable const", +- enabled: map[string]bool{golang.ConstantValues: true}, +- wantInlayHint: true, +- }, +- { +- label: "enable parameter names", +- enabled: map[string]bool{golang.ParameterNames: true}, +- wantInlayHint: false, +- }, +- } +- for _, test := range tests { +- t.Run(test.label, func(t *testing.T) { +- WithOptions( +- Settings{ +- "hints": test.enabled, +- }, +- ).Run(t, workspace, func(t *testing.T, env *Env) { +- env.OpenFile("lib.go") +- lens := env.InlayHints("lib.go") +- if gotInlayHint := len(lens) > 0; gotInlayHint != test.wantInlayHint { +- t.Errorf("got inlayHint: %t, want %t", gotInlayHint, test.wantInlayHint) +- } +- }) +- }) - } -} +diff -urN a/gopls/internal/test/integration/misc/call_hierarchy_test.go b/gopls/internal/test/integration/misc/call_hierarchy_test.go +--- a/gopls/internal/test/integration/misc/call_hierarchy_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/misc/call_hierarchy_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,36 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - ---- y0-rename -- --package a +-package misc - -import ( -- lg "log" -- "fmt" //@rename("fmt", "fmty") -- f2 "fmt" //@rename("f2", "f2name"),rename("fmt","f2y") --) +- "testing" - --func Random() int { -- y := 6 + 7 -- return y --} +- "golang.org/x/tools/gopls/internal/protocol" +- . "golang.org/x/tools/gopls/internal/test/integration" +-) - --func Random2(y int) int { //@rename("y", "z") -- return y --} +-// Test for golang/go#49125 +-func TestCallHierarchy_Issue49125(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - --type Pos struct { -- x, y int --} +-go 1.12 +--- p.go -- +-package pkg +-` +- // TODO(rfindley): this could probably just be a marker test. +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("p.go") +- loc := env.RegexpSearch("p.go", "pkg") - --func (p *Pos) Sum() int { -- return p.x + p.y //@rename("x", "myX") --} +- var params protocol.CallHierarchyPrepareParams +- params.TextDocument.URI = loc.URI +- params.Position = loc.Range.Start - --func _() { -- var p Pos //@rename("p", "pos") -- _ = p.Sum() //@rename("Sum", "GetSum") +- // Check that this doesn't panic. +- env.Editor.Server.PrepareCallHierarchy(env.Ctx, ¶ms) +- }) -} +diff -urN a/gopls/internal/test/integration/misc/configuration_test.go b/gopls/internal/test/integration/misc/configuration_test.go +--- a/gopls/internal/test/integration/misc/configuration_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/misc/configuration_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,186 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func sw() { -- var x interface{} +-package misc - -- switch y0 := x.(type) { //@rename("y", "y0") -- case int: -- fmt.Printf("%d", y0) //@rename("y", "y1"),rename("fmt", "format") -- case string: -- lg.Printf("%s", y0) //@rename("y", "y2"),rename("lg","log") -- default: -- f2.Printf("%v", y0) //@rename("y", "y3"),rename("f2","fmt2") -- } --} +-import ( +- "testing" - ---- y1-rename -- --package a +- . "golang.org/x/tools/gopls/internal/test/integration" - --import ( -- lg "log" -- "fmt" //@rename("fmt", "fmty") -- f2 "fmt" //@rename("f2", "f2name"),rename("fmt","f2y") +- "golang.org/x/tools/internal/testenv" -) - --func Random() int { -- y := 6 + 7 -- return y --} +-// Test that enabling and disabling produces the expected results of showing +-// and hiding staticcheck analysis results. +-func TestChangeConfiguration(t *testing.T) { +- // Staticcheck only supports Go versions >= 1.20. +- // Note: keep this in sync with TestStaticcheckWarning. Below this version we +- // should get an error when setting staticcheck configuration. +- testenv.NeedsGo1Point(t, 20) - --func Random2(y int) int { //@rename("y", "z") -- return y --} +- const files = ` +--- go.mod -- +-module mod.com - --type Pos struct { -- x, y int --} +-go 1.12 +--- a/a.go -- +-package a - --func (p *Pos) Sum() int { -- return p.x + p.y //@rename("x", "myX") --} +-import "errors" - --func _() { -- var p Pos //@rename("p", "pos") -- _ = p.Sum() //@rename("Sum", "GetSum") +-// FooErr should be called ErrFoo (ST1012) +-var FooErr = errors.New("foo") +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("a/a.go") +- env.AfterChange( +- NoDiagnostics(ForFile("a/a.go")), +- ) +- cfg := env.Editor.Config() +- cfg.Settings = map[string]interface{}{ +- "staticcheck": true, +- } +- env.ChangeConfiguration(cfg) +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/a.go", "var (FooErr)")), +- ) +- }) -} - --func sw() { -- var x interface{} +-// Test that clients can configure per-workspace configuration, which is +-// queried via the scopeURI of a workspace/configuration request. +-// (this was broken in golang/go#65519). +-func TestWorkspaceConfiguration(t *testing.T) { +- const files = ` +--- go.mod -- +-module example.com/config - -- switch y1 := x.(type) { //@rename("y", "y0") -- case int: -- fmt.Printf("%d", y1) //@rename("y", "y1"),rename("fmt", "format") -- case string: -- lg.Printf("%s", y1) //@rename("y", "y2"),rename("lg","log") -- default: -- f2.Printf("%v", y1) //@rename("y", "y3"),rename("f2","fmt2") -- } --} +-go 1.18 - ---- y2-rename -- +--- a/a.go -- -package a - --import ( -- lg "log" -- "fmt" //@rename("fmt", "fmty") -- f2 "fmt" //@rename("f2", "f2name"),rename("fmt","f2y") --) -- --func Random() int { -- y := 6 + 7 -- return y --} +-import "example.com/config/b" - --func Random2(y int) int { //@rename("y", "z") -- return y +-func _() { +- _ = b.B{2} -} - --type Pos struct { -- x, y int --} +--- b/b.go -- +-package b - --func (p *Pos) Sum() int { -- return p.x + p.y //@rename("x", "myX") +-type B struct { +- F int -} +-` - --func _() { -- var p Pos //@rename("p", "pos") -- _ = p.Sum() //@rename("Sum", "GetSum") +- WithOptions( +- WorkspaceFolders("a"), +- FolderSettings{ +- "a": { +- "analyses": map[string]bool{ +- "composites": false, +- }, +- }, +- }, +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("a/a.go") +- env.AfterChange(NoDiagnostics()) +- }) -} - --func sw() { -- var x interface{} +-// TestMajorOptionsChange is like TestChangeConfiguration, but modifies an +-// an open buffer before making a major (but inconsequential) change that +-// causes gopls to recreate the view. +-// +-// Gopls should not get confused about buffer content when recreating the view. +-func TestMajorOptionsChange(t *testing.T) { +- testenv.NeedsGo1Point(t, 20) // needs staticcheck - -- switch y2 := x.(type) { //@rename("y", "y0") -- case int: -- fmt.Printf("%d", y2) //@rename("y", "y1"),rename("fmt", "format") -- case string: -- lg.Printf("%s", y2) //@rename("y", "y2"),rename("lg","log") -- default: -- f2.Printf("%v", y2) //@rename("y", "y3"),rename("f2","fmt2") -- } --} +- const files = ` +--- go.mod -- +-module mod.com - ---- y3-rename -- +-go 1.12 +--- a/a.go -- -package a - --import ( -- lg "log" -- "fmt" //@rename("fmt", "fmty") -- f2 "fmt" //@rename("f2", "f2name"),rename("fmt","f2y") --) -- --func Random() int { -- y := 6 + 7 -- return y --} -- --func Random2(y int) int { //@rename("y", "z") -- return y --} -- --type Pos struct { -- x, y int --} -- --func (p *Pos) Sum() int { -- return p.x + p.y //@rename("x", "myX") --} +-import "errors" - --func _() { -- var p Pos //@rename("p", "pos") -- _ = p.Sum() //@rename("Sum", "GetSum") +-var ErrFoo = errors.New("foo") +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("a/a.go") +- // Introduce a staticcheck diagnostic. It should be detected when we enable +- // staticcheck later. +- env.RegexpReplace("a/a.go", "ErrFoo", "FooErr") +- env.AfterChange( +- NoDiagnostics(ForFile("a/a.go")), +- ) +- cfg := env.Editor.Config() +- // Any change to environment recreates the view, but this should not cause +- // gopls to get confused about the content of a/a.go: we should get the +- // staticcheck diagnostic below. +- cfg.Env = map[string]string{ +- "AN_ARBITRARY_VAR": "FOO", +- } +- cfg.Settings = map[string]interface{}{ +- "staticcheck": true, +- } +- env.ChangeConfiguration(cfg) +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/a.go", "var (FooErr)")), +- ) +- }) -} - --func sw() { -- var x interface{} +-func TestStaticcheckWarning(t *testing.T) { +- // Note: keep this in sync with TestChangeConfiguration. +- testenv.SkipAfterGo1Point(t, 19) - -- switch y3 := x.(type) { //@rename("y", "y0") -- case int: -- fmt.Printf("%d", y3) //@rename("y", "y1"),rename("fmt", "format") -- case string: -- lg.Printf("%s", y3) //@rename("y", "y2"),rename("lg","log") -- default: -- f2.Printf("%v", y3) //@rename("y", "y3"),rename("f2","fmt2") -- } --} +- const files = ` +--- go.mod -- +-module mod.com - ---- z-rename -- +-go 1.12 +--- a/a.go -- -package a - --import ( -- lg "log" -- "fmt" //@rename("fmt", "fmty") -- f2 "fmt" //@rename("f2", "f2name"),rename("fmt","f2y") --) -- --func Random() int { -- y := 6 + 7 -- return y --} -- --func Random2(z int) int { //@rename("y", "z") -- return z --} +-import "errors" - --type Pos struct { -- x, y int --} +-// FooErr should be called ErrFoo (ST1012) +-var FooErr = errors.New("foo") +-` - --func (p *Pos) Sum() int { -- return p.x + p.y //@rename("x", "myX") +- WithOptions( +- Settings{"staticcheck": true}, +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.OnceMet( +- InitialWorkspaceLoad, +- ShownMessage("staticcheck is not supported"), +- ) +- }) -} - --func _() { -- var p Pos //@rename("p", "pos") -- _ = p.Sum() //@rename("Sum", "GetSum") +-func TestDeprecatedSettings(t *testing.T) { +- WithOptions( +- Settings{ +- "experimentalUseInvalidMetadata": true, +- "experimentalWatchedFileDelay": "1s", +- "experimentalWorkspaceModule": true, +- "tempModfile": true, +- "allowModfileModifications": true, +- }, +- ).Run(t, "", func(t *testing.T, env *Env) { +- env.OnceMet( +- InitialWorkspaceLoad, +- ShownMessage("experimentalWorkspaceModule"), +- ShownMessage("experimentalUseInvalidMetadata"), +- ShownMessage("experimentalWatchedFileDelay"), +- ShownMessage("tempModfile"), +- ShownMessage("allowModfileModifications"), +- ) +- }) -} +diff -urN a/gopls/internal/test/integration/misc/debugserver_test.go b/gopls/internal/test/integration/misc/debugserver_test.go +--- a/gopls/internal/test/integration/misc/debugserver_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/misc/debugserver_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,46 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func sw() { -- var x interface{} +-package misc - -- switch y := x.(type) { //@rename("y", "y0") -- case int: -- fmt.Printf("%d", y) //@rename("y", "y1"),rename("fmt", "format") -- case string: -- lg.Printf("%s", y) //@rename("y", "y2"),rename("lg","log") -- default: -- f2.Printf("%v", y) //@rename("y", "y3"),rename("f2","fmt2") -- } --} +-import ( +- "net/http" +- "testing" - -diff -urN a/gopls/internal/lsp/testdata/rename/a/random.go.in b/gopls/internal/lsp/testdata/rename/a/random.go.in ---- a/gopls/internal/lsp/testdata/rename/a/random.go.in 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/a/random.go.in 1970-01-01 00:00:00.000000000 +0000 -@@ -1,42 +0,0 @@ --package a +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/command" - --import ( -- lg "log" -- "fmt" //@rename("fmt", "fmty") -- f2 "fmt" //@rename("f2", "f2name"),rename("fmt","f2y") +- . "golang.org/x/tools/gopls/internal/test/integration" -) - --func Random() int { -- y := 6 + 7 -- return y +-func TestStartDebugging(t *testing.T) { +- WithOptions( +- Modes(Forwarded), +- ).Run(t, "", func(t *testing.T, env *Env) { +- args, err := command.MarshalArgs(command.DebuggingArgs{}) +- if err != nil { +- t.Fatal(err) +- } +- params := &protocol.ExecuteCommandParams{ +- Command: command.StartDebugging.ID(), +- Arguments: args, +- } +- var result command.DebuggingResult +- env.ExecuteCommand(params, &result) +- if got, want := len(result.URLs), 2; got != want { +- t.Fatalf("got %d urls, want %d; urls: %#v", got, want, result.URLs) +- } +- for i, u := range result.URLs { +- resp, err := http.Get(u) +- if err != nil { +- t.Errorf("getting url #%d (%q): %v", i, u, err) +- continue +- } +- defer resp.Body.Close() +- if got, want := resp.StatusCode, http.StatusOK; got != want { +- t.Errorf("debug server #%d returned HTTP %d, want %d", i, got, want) +- } +- } +- }) -} +diff -urN a/gopls/internal/test/integration/misc/definition_test.go b/gopls/internal/test/integration/misc/definition_test.go +--- a/gopls/internal/test/integration/misc/definition_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/misc/definition_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,597 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func Random2(y int) int { //@rename("y", "z") -- return y --} +-package misc - --type Pos struct { -- x, y int --} +-import ( +- "os" +- "path" +- "path/filepath" +- "strings" +- "testing" - --func (p *Pos) Sum() int { -- return p.x + p.y //@rename("x", "myX") --} +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/test/compare" +- . "golang.org/x/tools/gopls/internal/test/integration" +-) - --func _() { -- var p Pos //@rename("p", "pos") -- _ = p.Sum() //@rename("Sum", "GetSum") --} +-const internalDefinition = ` +--- go.mod -- +-module mod.com - --func sw() { -- var x interface{} +-go 1.12 +--- main.go -- +-package main - -- switch y := x.(type) { //@rename("y", "y0") -- case int: -- fmt.Printf("%d", y) //@rename("y", "y1"),rename("fmt", "format") -- case string: -- lg.Printf("%s", y) //@rename("y", "y2"),rename("lg","log") -- default: -- f2.Printf("%v", y) //@rename("y", "y3"),rename("f2","fmt2") -- } +-import "fmt" +- +-func main() { +- fmt.Println(message) -} -diff -urN a/gopls/internal/lsp/testdata/rename/b/b.go b/gopls/internal/lsp/testdata/rename/b/b.go ---- a/gopls/internal/lsp/testdata/rename/b/b.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/b/b.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,20 +0,0 @@ --package b +--- const.go -- +-package main - --var c int //@rename("int", "uint") +-const message = "Hello World." +-` - --func _() { -- a := 1 //@rename("a", "error") -- a = 2 -- _ = a +-func TestGoToInternalDefinition(t *testing.T) { +- Run(t, internalDefinition, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- loc := env.GoToDefinition(env.RegexpSearch("main.go", "message")) +- name := env.Sandbox.Workdir.URIToPath(loc.URI) +- if want := "const.go"; name != want { +- t.Errorf("GoToDefinition: got file %q, want %q", name, want) +- } +- if want := env.RegexpSearch("const.go", "message"); loc != want { +- t.Errorf("GoToDefinition: got location %v, want %v", loc, want) +- } +- }) -} - --var ( -- // Hello there. -- // Foo does the thing. -- Foo int //@rename("Foo", "Bob") --) -- --/* --Hello description --*/ --func Hello() {} //@rename("Hello", "Goodbye") -diff -urN a/gopls/internal/lsp/testdata/rename/b/b.go.golden b/gopls/internal/lsp/testdata/rename/b/b.go.golden ---- a/gopls/internal/lsp/testdata/rename/b/b.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/b/b.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,78 +0,0 @@ ---- Bob-rename -- --package b +-const linknameDefinition = ` +--- go.mod -- +-module mod.com - --var c int //@rename("int", "uint") +--- upper/upper.go -- +-package upper - --func _() { -- a := 1 //@rename("a", "error") -- a = 2 -- _ = a --} +-import ( +- _ "unsafe" - --var ( -- // Hello there. -- // Bob does the thing. -- Bob int //@rename("Foo", "Bob") +- _ "mod.com/middle" -) - --/* --Hello description --*/ --func Hello() {} //@rename("Hello", "Goodbye") -- ---- Goodbye-rename -- --b.go: --package b -- --var c int //@rename("int", "uint") +-//go:linkname foo mod.com/lower.bar +-func foo() string - --func _() { -- a := 1 //@rename("a", "error") -- a = 2 -- _ = a --} +--- middle/middle.go -- +-package middle - --var ( -- // Hello there. -- // Foo does the thing. -- Foo int //@rename("Foo", "Bob") +-import ( +- _ "mod.com/lower" -) - --/* --Goodbye description --*/ --func Goodbye() {} //@rename("Hello", "Goodbye") +--- lower/lower.s -- - --c.go: --package c +--- lower/lower.go -- +-package lower - --import "golang.org/lsptests/rename/b" +-func bar() string { +- return "bar as foo" +-}` - --func _() { -- b.Goodbye() //@rename("Hello", "Goodbye") +-func TestGoToLinknameDefinition(t *testing.T) { +- Run(t, linknameDefinition, func(t *testing.T, env *Env) { +- env.OpenFile("upper/upper.go") +- +- // Jump from directives 2nd arg. +- start := env.RegexpSearch("upper/upper.go", `lower.bar`) +- loc := env.GoToDefinition(start) +- name := env.Sandbox.Workdir.URIToPath(loc.URI) +- if want := "lower/lower.go"; name != want { +- t.Errorf("GoToDefinition: got file %q, want %q", name, want) +- } +- if want := env.RegexpSearch("lower/lower.go", `bar`); loc != want { +- t.Errorf("GoToDefinition: got position %v, want %v", loc, want) +- } +- }) -} - ---- error-rename -- --package b +-const linknameDefinitionReverse = ` +--- go.mod -- +-module mod.com - --var c int //@rename("int", "uint") +--- upper/upper.s -- - --func _() { -- error := 1 //@rename("a", "error") -- error = 2 -- _ = error --} +--- upper/upper.go -- +-package upper - --var ( -- // Hello there. -- // Foo does the thing. -- Foo int //@rename("Foo", "Bob") +-import ( +- _ "mod.com/middle" -) - --/* --Hello description --*/ --func Hello() {} //@rename("Hello", "Goodbye") -- ---- uint-rename -- --int is built in and cannot be renamed -diff -urN a/gopls/internal/lsp/testdata/rename/bad/bad.go.golden b/gopls/internal/lsp/testdata/rename/bad/bad.go.golden ---- a/gopls/internal/lsp/testdata/rename/bad/bad.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/bad/bad.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,2 +0,0 @@ ---- rFunc-rename -- --renaming "sFunc" to "rFunc" not possible because "golang.org/lsptests/rename/bad" has errors -diff -urN a/gopls/internal/lsp/testdata/rename/bad/bad.go.in b/gopls/internal/lsp/testdata/rename/bad/bad.go.in ---- a/gopls/internal/lsp/testdata/rename/bad/bad.go.in 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/bad/bad.go.in 1970-01-01 00:00:00.000000000 +0000 -@@ -1,8 +0,0 @@ --package bad +-func foo() string - --type myStruct struct { --} +--- middle/middle.go -- +-package middle - --func (s *myStruct) sFunc() bool { //@rename("sFunc", "rFunc") -- return s.Bad --} -diff -urN a/gopls/internal/lsp/testdata/rename/bad/bad_test.go.in b/gopls/internal/lsp/testdata/rename/bad/bad_test.go.in ---- a/gopls/internal/lsp/testdata/rename/bad/bad_test.go.in 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/bad/bad_test.go.in 1970-01-01 00:00:00.000000000 +0000 -@@ -1 +0,0 @@ --package bad -\ No newline at end of file -diff -urN a/gopls/internal/lsp/testdata/rename/c/c2.go b/gopls/internal/lsp/testdata/rename/c/c2.go ---- a/gopls/internal/lsp/testdata/rename/c/c2.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/c/c2.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,4 +0,0 @@ --package c +-import ( +- _ "mod.com/lower" +-) - --//go:embed Static/* --var Static embed.FS //@rename("Static", "static") -\ No newline at end of file -diff -urN a/gopls/internal/lsp/testdata/rename/c/c2.go.golden b/gopls/internal/lsp/testdata/rename/c/c2.go.golden ---- a/gopls/internal/lsp/testdata/rename/c/c2.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/c/c2.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,5 +0,0 @@ ---- static-rename -- --package c +--- lower/lower.go -- +-package lower - --//go:embed Static/* --var static embed.FS //@rename("Static", "static") -diff -urN a/gopls/internal/lsp/testdata/rename/c/c.go b/gopls/internal/lsp/testdata/rename/c/c.go ---- a/gopls/internal/lsp/testdata/rename/c/c.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/c/c.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,7 +0,0 @@ --package c +-import _ "unsafe" - --import "golang.org/lsptests/rename/b" +-//go:linkname bar mod.com/upper.foo +-func bar() string { +- return "bar as foo" +-}` - --func _() { -- b.Hello() //@rename("Hello", "Goodbye") +-func TestGoToLinknameDefinitionInReverseDep(t *testing.T) { +- Run(t, linknameDefinitionReverse, func(t *testing.T, env *Env) { +- env.OpenFile("lower/lower.go") +- +- // Jump from directives 2nd arg. +- start := env.RegexpSearch("lower/lower.go", `upper.foo`) +- loc := env.GoToDefinition(start) +- name := env.Sandbox.Workdir.URIToPath(loc.URI) +- if want := "upper/upper.go"; name != want { +- t.Errorf("GoToDefinition: got file %q, want %q", name, want) +- } +- if want := env.RegexpSearch("upper/upper.go", `foo`); loc != want { +- t.Errorf("GoToDefinition: got position %v, want %v", loc, want) +- } +- }) -} -diff -urN a/gopls/internal/lsp/testdata/rename/c/c.go.golden b/gopls/internal/lsp/testdata/rename/c/c.go.golden ---- a/gopls/internal/lsp/testdata/rename/c/c.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/c/c.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,32 +0,0 @@ ---- Goodbye-rename -- --b.go: --package b - --var c int //@rename("int", "uint") +-// The linkname directive connects two packages not related in the import graph. +-const linknameDefinitionDisconnected = ` +--- go.mod -- +-module mod.com - --func _() { -- a := 1 //@rename("a", "error") -- a = 2 -- _ = a --} +--- a/a.go -- +-package a - --var ( -- // Hello there. -- // Foo does the thing. -- Foo int //@rename("Foo", "Bob") +-import ( +- _ "unsafe" -) - --/* --Goodbye description --*/ --func Goodbye() {} //@rename("Hello", "Goodbye") +-//go:linkname foo mod.com/b.bar +-func foo() string - --c.go: --package c +--- b/b.go -- +-package b - --import "golang.org/lsptests/rename/b" +-func bar() string { +- return "bar as foo" +-}` - --func _() { -- b.Goodbye() //@rename("Hello", "Goodbye") --} +-func TestGoToLinknameDefinitionDisconnected(t *testing.T) { +- Run(t, linknameDefinitionDisconnected, func(t *testing.T, env *Env) { +- env.OpenFile("a/a.go") - -diff -urN a/gopls/internal/lsp/testdata/rename/crosspkg/another/another.go b/gopls/internal/lsp/testdata/rename/crosspkg/another/another.go ---- a/gopls/internal/lsp/testdata/rename/crosspkg/another/another.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/crosspkg/another/another.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,13 +0,0 @@ --package another +- // Jump from directives 2nd arg. +- start := env.RegexpSearch("a/a.go", `b.bar`) +- loc := env.GoToDefinition(start) +- name := env.Sandbox.Workdir.URIToPath(loc.URI) +- if want := "b/b.go"; name != want { +- t.Errorf("GoToDefinition: got file %q, want %q", name, want) +- } +- if want := env.RegexpSearch("b/b.go", `bar`); loc != want { +- t.Errorf("GoToDefinition: got position %v, want %v", loc, want) +- } +- }) +-} - --type ( -- I interface{ F() } -- C struct{ I } --) +-const stdlibDefinition = ` +--- go.mod -- +-module mod.com - --func (C) g() +-go 1.12 +--- main.go -- +-package main - --func _() { -- var x I = C{} -- x.F() //@rename("F", "G") --} -diff -urN a/gopls/internal/lsp/testdata/rename/crosspkg/another/another.go.golden b/gopls/internal/lsp/testdata/rename/crosspkg/another/another.go.golden ---- a/gopls/internal/lsp/testdata/rename/crosspkg/another/another.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/crosspkg/another/another.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,15 +0,0 @@ ---- G-rename -- --package another +-import "fmt" - --type ( -- I interface{ G() } -- C struct{ I } --) +-func main() { +- fmt.Printf() +-}` - --func (C) g() +-func TestGoToStdlibDefinition_Issue37045(t *testing.T) { +- Run(t, stdlibDefinition, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- loc := env.GoToDefinition(env.RegexpSearch("main.go", `fmt.(Printf)`)) +- name := env.Sandbox.Workdir.URIToPath(loc.URI) +- if got, want := path.Base(name), "print.go"; got != want { +- t.Errorf("GoToDefinition: got file %q, want %q", name, want) +- } - --func _() { -- var x I = C{} -- x.G() //@rename("F", "G") +- // Test that we can jump to definition from outside our workspace. +- // See golang.org/issues/37045. +- newLoc := env.GoToDefinition(loc) +- newName := env.Sandbox.Workdir.URIToPath(newLoc.URI) +- if newName != name { +- t.Errorf("GoToDefinition is not idempotent: got %q, want %q", newName, name) +- } +- if newLoc != loc { +- t.Errorf("GoToDefinition is not idempotent: got %v, want %v", newLoc, loc) +- } +- }) -} - -diff -urN a/gopls/internal/lsp/testdata/rename/crosspkg/crosspkg.go b/gopls/internal/lsp/testdata/rename/crosspkg/crosspkg.go ---- a/gopls/internal/lsp/testdata/rename/crosspkg/crosspkg.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/crosspkg/crosspkg.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,7 +0,0 @@ --package crosspkg +-func TestUnexportedStdlib_Issue40809(t *testing.T) { +- Run(t, stdlibDefinition, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- loc := env.GoToDefinition(env.RegexpSearch("main.go", `fmt.(Printf)`)) +- name := env.Sandbox.Workdir.URIToPath(loc.URI) - --func Foo() { //@rename("Foo", "Dolphin") +- loc = env.RegexpSearch(name, `:=\s*(newPrinter)\(\)`) +- +- // Check that we can find references on a reference +- refs := env.References(loc) +- if len(refs) < 5 { +- t.Errorf("expected 5+ references to newPrinter, found: %#v", refs) +- } - +- loc = env.GoToDefinition(loc) +- content, _ := env.Hover(loc) +- if !strings.Contains(content.Value, "newPrinter") { +- t.Fatal("definition of newPrinter went to the incorrect place") +- } +- // And on the definition too. +- refs = env.References(loc) +- if len(refs) < 5 { +- t.Errorf("expected 5+ references to newPrinter, found: %#v", refs) +- } +- }) -} - --var Bar int //@rename("Bar", "Tomato") -diff -urN a/gopls/internal/lsp/testdata/rename/crosspkg/crosspkg.go.golden b/gopls/internal/lsp/testdata/rename/crosspkg/crosspkg.go.golden ---- a/gopls/internal/lsp/testdata/rename/crosspkg/crosspkg.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/crosspkg/crosspkg.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,40 +0,0 @@ ---- Dolphin-rename -- --crosspkg.go: --package crosspkg +-// Test the hover on an error's Error function. +-// This can't be done via the marker tests because Error is a builtin. +-func TestHoverOnError(t *testing.T) { +- const mod = ` +--- go.mod -- +-module mod.com - --func Dolphin() { //@rename("Foo", "Dolphin") +-go 1.12 +--- main.go -- +-package main - +-func main() { +- var err error +- err.Error() +-}` +- Run(t, mod, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- content, _ := env.Hover(env.RegexpSearch("main.go", "Error")) +- if content == nil { +- t.Fatalf("nil hover content for Error") +- } +- want := "```go\nfunc (error).Error() string\n```" +- if content.Value != want { +- t.Fatalf("hover failed:\n%s", compare.Text(want, content.Value)) +- } +- }) -} - --var Bar int //@rename("Bar", "Tomato") +-func TestImportShortcut(t *testing.T) { +- const mod = ` +--- go.mod -- +-module mod.com - --other.go: --package other +-go 1.12 +--- main.go -- +-package main - --import "golang.org/lsptests/rename/crosspkg" +-import "fmt" - --func Other() { -- crosspkg.Bar -- crosspkg.Dolphin() //@rename("Foo", "Flamingo") +-func main() {} +-` +- for _, tt := range []struct { +- wantLinks int +- importShortcut string +- }{ +- {1, "Link"}, +- {0, "Definition"}, +- {1, "Both"}, +- } { +- t.Run(tt.importShortcut, func(t *testing.T) { +- WithOptions( +- Settings{"importShortcut": tt.importShortcut}, +- ).Run(t, mod, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- loc := env.GoToDefinition(env.RegexpSearch("main.go", `"fmt"`)) +- if loc == (protocol.Location{}) { +- t.Fatalf("expected definition, got none") +- } +- links := env.DocumentLink("main.go") +- if len(links) != tt.wantLinks { +- t.Fatalf("expected %v links, got %v", tt.wantLinks, len(links)) +- } +- }) +- }) +- } -} - ---- Tomato-rename -- --crosspkg.go: --package crosspkg +-func TestGoToTypeDefinition_Issue38589(t *testing.T) { +- const mod = ` +--- go.mod -- +-module mod.com - --func Foo() { //@rename("Foo", "Dolphin") +-go 1.12 +--- main.go -- +-package main - --} +-type Int int - --var Tomato int //@rename("Bar", "Tomato") +-type Struct struct{} - --other.go: --package other +-func F1() {} +-func F2() (int, error) { return 0, nil } +-func F3() (**Struct, bool, *Int, error) { return nil, false, nil, nil } +-func F4() (**Struct, bool, *float64, error) { return nil, false, nil, nil } - --import "golang.org/lsptests/rename/crosspkg" +-func main() {} +-` - --func Other() { -- crosspkg.Tomato -- crosspkg.Foo() //@rename("Foo", "Flamingo") +- for _, tt := range []struct { +- re string +- wantError bool +- wantTypeRe string +- }{ +- {re: `F1`, wantError: true}, +- {re: `F2`, wantError: true}, +- {re: `F3`, wantError: true}, +- {re: `F4`, wantError: false, wantTypeRe: `type (Struct)`}, +- } { +- t.Run(tt.re, func(t *testing.T) { +- Run(t, mod, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- +- loc, err := env.Editor.TypeDefinition(env.Ctx, env.RegexpSearch("main.go", tt.re)) +- if tt.wantError { +- if err == nil { +- t.Fatal("expected error, got nil") +- } +- return +- } +- if err != nil { +- t.Fatalf("expected nil error, got %s", err) +- } +- +- typeLoc := env.RegexpSearch("main.go", tt.wantTypeRe) +- if loc != typeLoc { +- t.Errorf("invalid pos: want %+v, got %+v", typeLoc, loc) +- } +- }) +- }) +- } -} - -diff -urN a/gopls/internal/lsp/testdata/rename/crosspkg/other/other.go b/gopls/internal/lsp/testdata/rename/crosspkg/other/other.go ---- a/gopls/internal/lsp/testdata/rename/crosspkg/other/other.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/crosspkg/other/other.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,8 +0,0 @@ --package other +-func TestGoToTypeDefinition_Issue60544(t *testing.T) { +- const mod = ` +--- go.mod -- +-module mod.com - --import "golang.org/lsptests/rename/crosspkg" +-go 1.19 +--- main.go -- +-package main - --func Other() { -- crosspkg.Bar -- crosspkg.Foo() //@rename("Foo", "Flamingo") --} -diff -urN a/gopls/internal/lsp/testdata/rename/crosspkg/other/other.go.golden b/gopls/internal/lsp/testdata/rename/crosspkg/other/other.go.golden ---- a/gopls/internal/lsp/testdata/rename/crosspkg/other/other.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/crosspkg/other/other.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,20 +0,0 @@ ---- Flamingo-rename -- --crosspkg.go: --package crosspkg +-func F[T comparable]() {} +-` - --func Flamingo() { //@rename("Foo", "Dolphin") +- Run(t, mod, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") - +- _ = env.TypeDefinition(env.RegexpSearch("main.go", "comparable")) // must not panic +- }) -} - --var Bar int //@rename("Bar", "Tomato") +-// Test for golang/go#47825. +-func TestImportTestVariant(t *testing.T) { +- const mod = ` +--- go.mod -- +-module mod.com - --other.go: --package other +-go 1.12 +--- client/test/role.go -- +-package test - --import "golang.org/lsptests/rename/crosspkg" +-import _ "mod.com/client" - --func Other() { -- crosspkg.Bar -- crosspkg.Flamingo() //@rename("Foo", "Flamingo") --} +-type RoleSetup struct{} +--- client/client_role_test.go -- +-package client_test - -diff -urN a/gopls/internal/lsp/testdata/rename/generics/embedded.go b/gopls/internal/lsp/testdata/rename/generics/embedded.go ---- a/gopls/internal/lsp/testdata/rename/generics/embedded.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/generics/embedded.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,10 +0,0 @@ --//go:build go1.18 --// +build go1.18 +-import ( +- "testing" +- _ "mod.com/client" +- ctest "mod.com/client/test" +-) - --package generics +-func TestClient(t *testing.T) { +- _ = ctest.RoleSetup{} +-} +--- client/client_test.go -- +-package client - --type foo[P any] int //@rename("foo","bar") +-import "testing" - --var x struct{ foo[int] } +-func TestClient(t *testing.T) {} +--- client.go -- +-package client +-` +- Run(t, mod, func(t *testing.T, env *Env) { +- env.OpenFile("client/client_role_test.go") +- env.GoToDefinition(env.RegexpSearch("client/client_role_test.go", "RoleSetup")) +- }) +-} - --var _ = x.foo -diff -urN a/gopls/internal/lsp/testdata/rename/generics/embedded.go.golden b/gopls/internal/lsp/testdata/rename/generics/embedded.go.golden ---- a/gopls/internal/lsp/testdata/rename/generics/embedded.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/generics/embedded.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,12 +0,0 @@ ---- bar-rename -- --//go:build go1.18 --// +build go1.18 +-// This test exercises a crashing pattern from golang/go#49223. +-func TestGoToCrashingDefinition_Issue49223(t *testing.T) { +- Run(t, "", func(t *testing.T, env *Env) { +- params := &protocol.DefinitionParams{} +- params.TextDocument.URI = protocol.DocumentURI("fugitive%3A///Users/user/src/mm/ems/.git//0/pkg/domain/treasury/provider.go") +- params.Position.Character = 18 +- params.Position.Line = 0 +- env.Editor.Server.Definition(env.Ctx, params) +- }) +-} - --package generics +-// TestVendoringInvalidatesMetadata ensures that gopls uses the +-// correct metadata even after an external 'go mod vendor' command +-// causes packages to move; see issue #55995. +-// See also TestImplementationsInVendor, which tests the same fix. +-func TestVendoringInvalidatesMetadata(t *testing.T) { +- t.Skip("golang/go#56169: file watching does not capture vendor dirs") - --type bar[P any] int //@rename("foo","bar") +- const proxy = ` +--- other.com/b@v1.0.0/go.mod -- +-module other.com/b +-go 1.14 - --var x struct{ bar[int] } +--- other.com/b@v1.0.0/b.go -- +-package b +-const K = 0 +-` +- const src = ` +--- go.mod -- +-module example.com/a +-go 1.14 +-require other.com/b v1.0.0 - --var _ = x.bar +--- go.sum -- +-other.com/b v1.0.0 h1:1wb3PMGdet5ojzrKl+0iNksRLnOM9Jw+7amBNqmYwqk= +-other.com/b v1.0.0/go.mod h1:TgHQFucl04oGT+vrUm/liAzukYHNxCwKNkQZEyn3m9g= - -diff -urN a/gopls/internal/lsp/testdata/rename/generics/generics.go b/gopls/internal/lsp/testdata/rename/generics/generics.go ---- a/gopls/internal/lsp/testdata/rename/generics/generics.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/generics/generics.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,25 +0,0 @@ --//go:build go1.18 --// +build go1.18 +--- a.go -- +-package a +-import "other.com/b" +-const _ = b.K - --package generics +-` +- WithOptions( +- ProxyFiles(proxy), +- Modes(Default), // fails in 'experimental' mode +- ).Run(t, src, func(t *testing.T, env *Env) { +- // Enable to debug go.sum mismatch, which may appear as +- // "module lookup disabled by GOPROXY=off", confusingly. +- if false { +- env.DumpGoSum(".") +- } - --type G[P any] struct { -- F int --} +- env.OpenFile("a.go") +- refLoc := env.RegexpSearch("a.go", "K") // find "b.K" reference - --func (G[_]) M() {} +- // Initially, b.K is defined in the module cache. +- gotLoc := env.GoToDefinition(refLoc) +- gotFile := env.Sandbox.Workdir.URIToPath(gotLoc.URI) +- wantCache := filepath.ToSlash(env.Sandbox.GOPATH()) + "/pkg/mod/other.com/b@v1.0.0/b.go" +- if gotFile != wantCache { +- t.Errorf("GoToDefinition, before: got file %q, want %q", gotFile, wantCache) +- } - --func F[P any](P) { -- var p P //@rename("P", "Q") -- _ = p --} +- // Run 'go mod vendor' outside the editor. +- env.RunGoCommand("mod", "vendor") - --func _() { -- var x G[int] //@rename("G", "H") -- _ = x.F //@rename("F", "K") -- x.M() //@rename("M", "N") +- // Synchronize changes to watched files. +- env.Await(env.DoneWithChangeWatchedFiles()) - -- var y G[string] -- _ = y.F -- y.M() --} -diff -urN a/gopls/internal/lsp/testdata/rename/generics/generics.go.golden b/gopls/internal/lsp/testdata/rename/generics/generics.go.golden ---- a/gopls/internal/lsp/testdata/rename/generics/generics.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/generics/generics.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,108 +0,0 @@ ---- H-rename -- --//go:build go1.18 --// +build go1.18 +- // Now, b.K is defined in the vendor tree. +- gotLoc = env.GoToDefinition(refLoc) +- wantVendor := "vendor/other.com/b/b.go" +- if gotFile != wantVendor { +- t.Errorf("GoToDefinition, after go mod vendor: got file %q, want %q", gotFile, wantVendor) +- } - --package generics +- // Delete the vendor tree. +- if err := os.RemoveAll(env.Sandbox.Workdir.AbsPath("vendor")); err != nil { +- t.Fatal(err) +- } +- // Notify the server of the deletion. +- if err := env.Sandbox.Workdir.CheckForFileChanges(env.Ctx); err != nil { +- t.Fatal(err) +- } - --type H[P any] struct { -- F int +- // Synchronize again. +- env.Await(env.DoneWithChangeWatchedFiles()) +- +- // b.K is once again defined in the module cache. +- gotLoc = env.GoToDefinition(gotLoc) +- gotFile = env.Sandbox.Workdir.URIToPath(gotLoc.URI) +- if gotFile != wantCache { +- t.Errorf("GoToDefinition, after rm -rf vendor: got file %q, want %q", gotFile, wantCache) +- } +- }) -} - --func (H[_]) M() {} +-const embedDefinition = ` +--- go.mod -- +-module mod.com - --func F[P any](P) { -- var p P //@rename("P", "Q") -- _ = p --} +--- main.go -- +-package main - --func _() { -- var x H[int] //@rename("G", "H") -- _ = x.F //@rename("F", "K") -- x.M() //@rename("M", "N") +-import ( +- "embed" +-) - -- var y H[string] -- _ = y.F -- y.M() --} +-//go:embed *.txt +-var foo embed.FS - ---- K-rename -- --//go:build go1.18 --// +build go1.18 +-func main() {} - --package generics +--- skip.sql -- +-SKIP - --type G[P any] struct { -- K int --} +--- foo.txt -- +-FOO - --func (G[_]) M() {} +--- skip.bat -- +-SKIP +-` - --func F[P any](P) { -- var p P //@rename("P", "Q") -- _ = p --} +-func TestGoToEmbedDefinition(t *testing.T) { +- Run(t, embedDefinition, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") - --func _() { -- var x G[int] //@rename("G", "H") -- _ = x.K //@rename("F", "K") -- x.M() //@rename("M", "N") +- start := env.RegexpSearch("main.go", `\*.txt`) +- loc := env.GoToDefinition(start) - -- var y G[string] -- _ = y.K -- y.M() +- name := env.Sandbox.Workdir.URIToPath(loc.URI) +- if want := "foo.txt"; name != want { +- t.Errorf("GoToDefinition: got file %q, want %q", name, want) +- } +- }) -} - ---- N-rename -- --//go:build go1.18 --// +build go1.18 +-func TestDefinitionOfErrorErrorMethod(t *testing.T) { +- const src = `Regression test for a panic in definition of error.Error (of course). +-golang/go#64086 - --package generics -- --type G[P any] struct { -- F int --} +--- go.mod -- +-module mod.com +-go 1.18 - --func (G[_]) N() {} +--- a.go -- +-package a - --func F[P any](P) { -- var p P //@rename("P", "Q") -- _ = p +-func _(err error) { +- _ = err.Error() -} - --func _() { -- var x G[int] //@rename("G", "H") -- _ = x.F //@rename("F", "K") -- x.N() //@rename("M", "N") +-` +- Run(t, src, func(t *testing.T, env *Env) { +- env.OpenFile("a.go") - -- var y G[string] -- _ = y.F -- y.N() +- start := env.RegexpSearch("a.go", `Error`) +- loc := env.GoToDefinition(start) +- +- if !strings.HasSuffix(string(loc.URI), "builtin.go") { +- t.Errorf("GoToDefinition(err.Error) = %#v, want builtin.go", loc) +- } +- }) -} +diff -urN a/gopls/internal/test/integration/misc/embed_test.go b/gopls/internal/test/integration/misc/embed_test.go +--- a/gopls/internal/test/integration/misc/embed_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/misc/embed_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,41 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - ---- Q-rename -- --//go:build go1.18 --// +build go1.18 +-package misc - --package generics +-import ( +- "testing" - --type G[P any] struct { -- F int --} +- . "golang.org/x/tools/gopls/internal/test/integration" +-) - --func (G[_]) M() {} +-func TestMissingPatternDiagnostic(t *testing.T) { +- const files = ` +--- go.mod -- +-module example.com +--- x.go -- +-package x - --func F[Q any](Q) { -- var p Q //@rename("P", "Q") -- _ = p --} +-import ( +- _ "embed" +-) - --func _() { -- var x G[int] //@rename("G", "H") -- _ = x.F //@rename("F", "K") -- x.M() //@rename("M", "N") +-// Issue 47436 +-func F() {} - -- var y G[string] -- _ = y.F -- y.M() +-//go:embed NONEXISTENT +-var foo string +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("x.go") +- env.AfterChange( +- Diagnostics( +- env.AtRegexp("x.go", `NONEXISTENT`), +- WithMessage("no matching files found"), +- ), +- ) +- env.RegexpReplace("x.go", `NONEXISTENT`, "x.go") +- env.AfterChange(NoDiagnostics(ForFile("x.go"))) +- }) -} +diff -urN a/gopls/internal/test/integration/misc/extract_test.go b/gopls/internal/test/integration/misc/extract_test.go +--- a/gopls/internal/test/integration/misc/extract_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/misc/extract_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,66 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -diff -urN a/gopls/internal/lsp/testdata/rename/generics/unions.go b/gopls/internal/lsp/testdata/rename/generics/unions.go ---- a/gopls/internal/lsp/testdata/rename/generics/unions.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/generics/unions.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,10 +0,0 @@ --//go:build go1.18 --// +build go1.18 +-package misc - --package generics +-import ( +- "testing" - --type T string //@rename("T", "R") +- "golang.org/x/tools/gopls/internal/test/compare" +- . "golang.org/x/tools/gopls/internal/test/integration" - --type C interface { -- T | ~int //@rename("T", "S") --} -diff -urN a/gopls/internal/lsp/testdata/rename/generics/unions.go.golden b/gopls/internal/lsp/testdata/rename/generics/unions.go.golden ---- a/gopls/internal/lsp/testdata/rename/generics/unions.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/generics/unions.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,24 +0,0 @@ ---- R-rename -- --//go:build go1.18 --// +build go1.18 +- "golang.org/x/tools/gopls/internal/protocol" +-) - --package generics +-func TestExtractFunction(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - --type R string //@rename("T", "R") +-go 1.12 +--- main.go -- +-package main - --type C interface { -- R | ~int //@rename("T", "S") +-func Foo() int { +- a := 5 +- return a -} +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- loc := env.RegexpSearch("main.go", `a := 5\n.*return a`) +- actions, err := env.Editor.CodeAction(env.Ctx, loc, nil) +- if err != nil { +- t.Fatal(err) +- } - ---- S-rename -- --//go:build go1.18 --// +build go1.18 -- --package generics +- // Find the extract function code action. +- var extractFunc *protocol.CodeAction +- for _, action := range actions { +- if action.Kind == protocol.RefactorExtract && action.Title == "Extract function" { +- extractFunc = &action +- break +- } +- } +- if extractFunc == nil { +- t.Fatal("could not find extract function action") +- } - --type S string //@rename("T", "R") +- env.ApplyCodeAction(*extractFunc) +- want := `package main - --type C interface { -- S | ~int //@rename("T", "S") +-func Foo() int { +- return newFunction() -} - -diff -urN a/gopls/internal/lsp/testdata/rename/issue39614/issue39614.go.golden b/gopls/internal/lsp/testdata/rename/issue39614/issue39614.go.golden ---- a/gopls/internal/lsp/testdata/rename/issue39614/issue39614.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/issue39614/issue39614.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,10 +0,0 @@ ---- bar-rename -- --package issue39614 -- --func fn() { -- var bar bool //@rename("foo","bar") -- make(map[string]bool -- if true { -- } +-func newFunction() int { +- a := 5 +- return a -} -- -diff -urN a/gopls/internal/lsp/testdata/rename/issue39614/issue39614.go.in b/gopls/internal/lsp/testdata/rename/issue39614/issue39614.go.in ---- a/gopls/internal/lsp/testdata/rename/issue39614/issue39614.go.in 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/issue39614/issue39614.go.in 1970-01-01 00:00:00.000000000 +0000 -@@ -1,8 +0,0 @@ --package issue39614 -- --func fn() { -- var foo bool //@rename("foo","bar") -- make(map[string]bool -- if true { -- } +-` +- if got := env.BufferText("main.go"); got != want { +- t.Fatalf("TestFillStruct failed:\n%s", compare.Text(want, got)) +- } +- }) -} -diff -urN a/gopls/internal/lsp/testdata/rename/issue42134/1.go b/gopls/internal/lsp/testdata/rename/issue42134/1.go ---- a/gopls/internal/lsp/testdata/rename/issue42134/1.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/issue42134/1.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,8 +0,0 @@ --package issue42134 +diff -urN a/gopls/internal/test/integration/misc/failures_test.go b/gopls/internal/test/integration/misc/failures_test.go +--- a/gopls/internal/test/integration/misc/failures_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/misc/failures_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,82 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func _() { -- // foo computes things. -- foo := func() {} +-package misc - -- foo() //@rename("foo", "bar") --} -diff -urN a/gopls/internal/lsp/testdata/rename/issue42134/1.go.golden b/gopls/internal/lsp/testdata/rename/issue42134/1.go.golden ---- a/gopls/internal/lsp/testdata/rename/issue42134/1.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/issue42134/1.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,10 +0,0 @@ ---- bar-rename -- --package issue42134 +-import ( +- "testing" - --func _() { -- // bar computes things. -- bar := func() {} +- . "golang.org/x/tools/gopls/internal/test/integration" +- "golang.org/x/tools/gopls/internal/test/compare" +-) - -- bar() //@rename("foo", "bar") --} +-// This is a slight variant of TestHoverOnError in definition_test.go +-// that includes a line directive, which makes no difference since +-// gopls ignores line directives. +-func TestHoverFailure(t *testing.T) { +- const mod = ` +--- go.mod -- +-module mod.com - -diff -urN a/gopls/internal/lsp/testdata/rename/issue42134/2.go b/gopls/internal/lsp/testdata/rename/issue42134/2.go ---- a/gopls/internal/lsp/testdata/rename/issue42134/2.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/issue42134/2.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,12 +0,0 @@ --package issue42134 +-go 1.12 +--- a.y -- +-DWIM(main) - --import "fmt" +--- main.go -- +-//line a.y:1 +-package main - --func _() { -- // minNumber is a min number. -- // Second line. -- minNumber := min(1, 2) -- fmt.Println(minNumber) //@rename("minNumber", "res") +-func main() { +- var err error +- err.Error() +-}` +- Run(t, mod, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- content, _ := env.Hover(env.RegexpSearch("main.go", "Error")) +- if content == nil { +- t.Fatalf("Hover('Error') returned nil") +- } +- want := "```go\nfunc (error).Error() string\n```" +- if content.Value != want { +- t.Fatalf("wrong Hover('Error') content:\n%s", compare.Text(want, content.Value)) +- } +- }) -} - --func min(a, b int) int { return a } -diff -urN a/gopls/internal/lsp/testdata/rename/issue42134/2.go.golden b/gopls/internal/lsp/testdata/rename/issue42134/2.go.golden ---- a/gopls/internal/lsp/testdata/rename/issue42134/2.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/issue42134/2.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,14 +0,0 @@ ---- res-rename -- --package issue42134 -- --import "fmt" +-// This test demonstrates a case where gopls is not at all confused by +-// line directives, because it completely ignores them. +-func TestFailingDiagnosticClearingOnEdit(t *testing.T) { +- // badPackageDup contains a duplicate definition of the 'a' const. +- // This is a minor variant of TestDiagnosticClearingOnEdit from +- // diagnostics_test.go, with a line directive, which makes no difference. +- const badPackageDup = ` +--- go.mod -- +-module mod.com - --func _() { -- // res is a min number. -- // Second line. -- res := min(1, 2) -- fmt.Println(res) //@rename("minNumber", "res") --} +-go 1.12 +--- a.go -- +-package consts - --func min(a, b int) int { return a } +-const a = 1 +--- b.go -- +-package consts +-//line gen.go:5 +-const a = 2 +-` - -diff -urN a/gopls/internal/lsp/testdata/rename/issue42134/3.go b/gopls/internal/lsp/testdata/rename/issue42134/3.go ---- a/gopls/internal/lsp/testdata/rename/issue42134/3.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/issue42134/3.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,11 +0,0 @@ --package issue42134 +- Run(t, badPackageDup, func(t *testing.T, env *Env) { +- env.OpenFile("b.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("b.go", `a = 2`), WithMessage("a redeclared")), +- Diagnostics(env.AtRegexp("a.go", `a = 1`), WithMessage("other declaration")), +- ) - --func _() { -- /* -- tests contains test cases -- */ -- tests := []struct { //@rename("tests", "testCases") -- in, out string -- }{} -- _ = tests +- // Fix the error by editing the const name in b.go to `b`. +- env.RegexpReplace("b.go", "(a) = 2", "b") +- env.AfterChange( +- NoDiagnostics(ForFile("a.go")), +- NoDiagnostics(ForFile("b.go")), +- ) +- }) -} -diff -urN a/gopls/internal/lsp/testdata/rename/issue42134/3.go.golden b/gopls/internal/lsp/testdata/rename/issue42134/3.go.golden ---- a/gopls/internal/lsp/testdata/rename/issue42134/3.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/issue42134/3.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,13 +0,0 @@ ---- testCases-rename -- --package issue42134 +diff -urN a/gopls/internal/test/integration/misc/fix_test.go b/gopls/internal/test/integration/misc/fix_test.go +--- a/gopls/internal/test/integration/misc/fix_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/misc/fix_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,161 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func _() { -- /* -- testCases contains test cases -- */ -- testCases := []struct { //@rename("tests", "testCases") -- in, out string -- }{} -- _ = testCases --} +-package misc - -diff -urN a/gopls/internal/lsp/testdata/rename/issue42134/4.go b/gopls/internal/lsp/testdata/rename/issue42134/4.go ---- a/gopls/internal/lsp/testdata/rename/issue42134/4.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/issue42134/4.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,8 +0,0 @@ --package issue42134 +-import ( +- "testing" - --func _() { -- // a is equal to 5. Comment must stay the same +- "golang.org/x/tools/gopls/internal/test/compare" +- . "golang.org/x/tools/gopls/internal/test/integration" - -- a := 5 -- _ = a //@rename("a", "b") --} -diff -urN a/gopls/internal/lsp/testdata/rename/issue42134/4.go.golden b/gopls/internal/lsp/testdata/rename/issue42134/4.go.golden ---- a/gopls/internal/lsp/testdata/rename/issue42134/4.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/issue42134/4.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,10 +0,0 @@ ---- b-rename -- --package issue42134 +- "golang.org/x/tools/gopls/internal/protocol" +-) - --func _() { -- // a is equal to 5. Comment must stay the same +-// A basic test for fillstruct, now that it uses a command and supports resolve edits. +-func TestFillStruct(t *testing.T) { +- tc := []struct { +- name string +- capabilities string +- wantCommand bool +- }{ +- {"default", "{}", true}, +- {"no data", `{ "textDocument": {"codeAction": { "resolveSupport": { "properties": ["edit"] } } } }`, true}, +- {"resolve support", `{ "textDocument": {"codeAction": { "dataSupport": true, "resolveSupport": { "properties": ["edit"] } } } }`, false}, +- } - -- b := 5 -- _ = b //@rename("a", "b") --} +- const basic = ` +--- go.mod -- +-module mod.com - -diff -urN a/gopls/internal/lsp/testdata/rename/shadow/shadow.go b/gopls/internal/lsp/testdata/rename/shadow/shadow.go ---- a/gopls/internal/lsp/testdata/rename/shadow/shadow.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/shadow/shadow.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,20 +0,0 @@ --package shadow +-go 1.14 +--- main.go -- +-package main - --func _() { -- a := true -- b, c, _ := A(), B(), D() //@rename("A", "a"),rename("B", "b"),rename("b", "c"),rename("D", "d") -- d := false -- _, _, _, _ = a, b, c, d +-type Info struct { +- WordCounts map[string]int +- Words []string -} - --func A() int { -- return 0 +-func Foo() { +- _ = Info{} -} +-` - --func B() int { -- return 0 --} +- for _, tt := range tc { +- t.Run(tt.name, func(t *testing.T) { +- runner := WithOptions(CapabilitiesJSON([]byte(tt.capabilities))) - --func D() int { -- return 0 --} -diff -urN a/gopls/internal/lsp/testdata/rename/shadow/shadow.go.golden b/gopls/internal/lsp/testdata/rename/shadow/shadow.go.golden ---- a/gopls/internal/lsp/testdata/rename/shadow/shadow.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/shadow/shadow.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,51 +0,0 @@ ---- a-rename -- --shadow/shadow.go:10:6: renaming this func "A" to "a" --shadow/shadow.go:5:13: would cause this reference to become shadowed --shadow/shadow.go:4:2: by this intervening var definition ---- b-rename -- --package shadow +- runner.Run(t, basic, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- fixes, err := env.Editor.CodeActions(env.Ctx, env.RegexpSearch("main.go", "Info{}"), nil, protocol.RefactorRewrite) +- if err != nil { +- t.Fatal(err) +- } - --func _() { -- a := true -- b, c, _ := A(), b(), D() //@rename("A", "a"),rename("B", "b"),rename("b", "c"),rename("D", "d") -- d := false -- _, _, _, _ = a, b, c, d --} +- if len(fixes) != 1 { +- t.Fatalf("expected 1 code action, got %v", len(fixes)) +- } +- if tt.wantCommand { +- if fixes[0].Command == nil || fixes[0].Data != nil { +- t.Errorf("expected code action to have command not data, got %v", fixes[0]) +- } +- } else { +- if fixes[0].Command != nil || fixes[0].Data == nil { +- t.Errorf("expected code action to have command not data, got %v", fixes[0]) +- } +- } - --func A() int { -- return 0 --} +- // Apply the code action (handles resolving the code action), and check that the result is correct. +- if err := env.Editor.RefactorRewrite(env.Ctx, env.RegexpSearch("main.go", "Info{}")); err != nil { +- t.Fatal(err) +- } +- want := `package main - --func b() int { -- return 0 +-type Info struct { +- WordCounts map[string]int +- Words []string -} - --func D() int { -- return 0 +-func Foo() { +- _ = Info{ +- WordCounts: map[string]int{}, +- Words: []string{}, +- } -} -- ---- c-rename -- --shadow/shadow.go:5:2: renaming this var "b" to "c" --shadow/shadow.go:5:5: conflicts with var in same block ---- d-rename -- --package shadow -- --func _() { -- a := true -- b, c, _ := A(), B(), d() //@rename("A", "a"),rename("B", "b"),rename("b", "c"),rename("D", "d") -- d := false -- _, _, _, _ = a, b, c, d +-` +- if got := env.BufferText("main.go"); got != want { +- t.Fatalf("TestFillStruct failed:\n%s", compare.Text(want, got)) +- } +- }) +- }) +- } -} - --func A() int { -- return 0 --} +-func TestFillReturns(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - --func B() int { -- return 0 --} +-go 1.12 +--- main.go -- +-package main - --func d() int { -- return 0 +-func Foo() error { +- return -} -- -diff -urN a/gopls/internal/lsp/testdata/rename/testy/testy.go b/gopls/internal/lsp/testdata/rename/testy/testy.go ---- a/gopls/internal/lsp/testdata/rename/testy/testy.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/testy/testy.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,7 +0,0 @@ --package testy -- --type tt int //@rename("tt", "testyType") -- --func a() { -- foo := 42 //@rename("foo", "bar") +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- var d protocol.PublishDiagnosticsParams +- env.AfterChange( +- // The error message here changed in 1.18; "return values" covers both forms. +- Diagnostics(env.AtRegexp("main.go", `return`), WithMessage("return values")), +- ReadDiagnostics("main.go", &d), +- ) +- var quickFixes []*protocol.CodeAction +- for _, act := range env.CodeAction("main.go", d.Diagnostics) { +- if act.Kind == protocol.QuickFix { +- act := act // remove in go1.22 +- quickFixes = append(quickFixes, &act) +- } +- } +- if len(quickFixes) != 1 { +- t.Fatalf("expected 1 quick fix, got %d:\n%v", len(quickFixes), quickFixes) +- } +- env.ApplyQuickFixes("main.go", d.Diagnostics) +- env.AfterChange(NoDiagnostics(ForFile("main.go"))) +- }) -} -diff -urN a/gopls/internal/lsp/testdata/rename/testy/testy.go.golden b/gopls/internal/lsp/testdata/rename/testy/testy.go.golden ---- a/gopls/internal/lsp/testdata/rename/testy/testy.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/testy/testy.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,18 +0,0 @@ ---- bar-rename -- --package testy -- --type tt int //@rename("tt", "testyType") - --func a() { -- bar := 42 //@rename("foo", "bar") --} +-func TestUnusedParameter_Issue63755(t *testing.T) { +- // This test verifies the fix for #63755, where codeActions panicked on parameters +- // of functions with no function body. - ---- testyType-rename -- --package testy +- // We should not detect parameters as unused for external functions. - --type testyType int //@rename("tt", "testyType") +- const files = ` +--- go.mod -- +-module unused.mod - --func a() { -- foo := 42 //@rename("foo", "bar") --} +-go 1.18 - -diff -urN a/gopls/internal/lsp/testdata/rename/testy/testy_test.go b/gopls/internal/lsp/testdata/rename/testy/testy_test.go ---- a/gopls/internal/lsp/testdata/rename/testy/testy_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/testy/testy_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,8 +0,0 @@ --package testy +--- external.go -- +-package external - --import "testing" +-func External(z int) - --func TestSomething(t *testing.T) { -- var x int //@rename("x", "testyX") -- a() //@rename("a", "b") +-func _() { +- External(1) -} -diff -urN a/gopls/internal/lsp/testdata/rename/testy/testy_test.go.golden b/gopls/internal/lsp/testdata/rename/testy/testy_test.go.golden ---- a/gopls/internal/lsp/testdata/rename/testy/testy_test.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/rename/testy/testy_test.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,30 +0,0 @@ ---- b-rename -- --testy.go: --package testy -- --type tt int //@rename("tt", "testyType") -- --func b() { -- foo := 42 //@rename("foo", "bar") +- ` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("external.go") +- _, err := env.Editor.CodeAction(env.Ctx, env.RegexpSearch("external.go", "z"), nil) +- if err != nil { +- t.Fatal(err) +- } +- // yay, no panic +- }) -} +diff -urN a/gopls/internal/test/integration/misc/formatting_test.go b/gopls/internal/test/integration/misc/formatting_test.go +--- a/gopls/internal/test/integration/misc/formatting_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/misc/formatting_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,394 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --testy_test.go: --package testy +-package misc - --import "testing" +-import ( +- "strings" +- "testing" - --func TestSomething(t *testing.T) { -- var x int //@rename("x", "testyX") -- b() //@rename("a", "b") --} +- "golang.org/x/tools/gopls/internal/test/compare" +- . "golang.org/x/tools/gopls/internal/test/integration" +- "golang.org/x/tools/internal/testenv" +-) - ---- testyX-rename -- --package testy +-const unformattedProgram = ` +--- main.go -- +-package main +-import "fmt" +-func main( ) { +- fmt.Println("Hello World.") +-} +--- main.go.golden -- +-package main - --import "testing" +-import "fmt" - --func TestSomething(t *testing.T) { -- var testyX int //@rename("x", "testyX") -- a() //@rename("a", "b") +-func main() { +- fmt.Println("Hello World.") -} +-` - -diff -urN a/gopls/internal/lsp/testdata/selectionrange/foo.go b/gopls/internal/lsp/testdata/selectionrange/foo.go ---- a/gopls/internal/lsp/testdata/selectionrange/foo.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/selectionrange/foo.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,13 +0,0 @@ --package foo -- --import "time" +-func TestFormatting(t *testing.T) { +- Run(t, unformattedProgram, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- env.FormatBuffer("main.go") +- got := env.BufferText("main.go") +- want := env.ReadWorkspaceFile("main.go.golden") +- if got != want { +- t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) +- } +- }) +-} - --func Bar(x, y int, t time.Time) int { -- zs := []int{1, 2, 3} //@selectionrange("1") +-// Tests golang/go#36824. +-func TestFormattingOneLine36824(t *testing.T) { +- const onelineProgram = ` +--- a.go -- +-package main; func f() {} - -- for _, z := range zs { -- x = x + z + y + zs[1] //@selectionrange("1") -- } +--- a.go.formatted -- +-package main - -- return x + y //@selectionrange("+") +-func f() {} +-` +- Run(t, onelineProgram, func(t *testing.T, env *Env) { +- env.OpenFile("a.go") +- env.FormatBuffer("a.go") +- got := env.BufferText("a.go") +- want := env.ReadWorkspaceFile("a.go.formatted") +- if got != want { +- t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) +- } +- }) -} -diff -urN a/gopls/internal/lsp/testdata/selectionrange/foo.go.golden b/gopls/internal/lsp/testdata/selectionrange/foo.go.golden ---- a/gopls/internal/lsp/testdata/selectionrange/foo.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/selectionrange/foo.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,29 +0,0 @@ ---- selectionrange_foo_12_11 -- --Ranges 0: -- 11:8-11:13 "x + y" -- 11:1-11:13 "return x + y" -- 4:36-12:1 "{\\n\tzs := []int{...ionrange(\"+\")\\n}" -- 4:0-12:1 "func Bar(x, y i...ionrange(\"+\")\\n}" -- 0:0-12:1 "package foo\\n\\nim...ionrange(\"+\")\\n}" -- ---- selectionrange_foo_6_14 -- --Ranges 0: -- 5:13-5:14 "1" -- 5:7-5:21 "[]int{1, 2, 3}" -- 5:1-5:21 "zs := []int{1, 2, 3}" -- 4:36-12:1 "{\\n\tzs := []int{...ionrange(\"+\")\\n}" -- 4:0-12:1 "func Bar(x, y i...ionrange(\"+\")\\n}" -- 0:0-12:1 "package foo\\n\\nim...ionrange(\"+\")\\n}" - ---- selectionrange_foo_9_22 -- --Ranges 0: -- 8:21-8:22 "1" -- 8:18-8:23 "zs[1]" -- 8:6-8:23 "x + z + y + zs[1]" -- 8:2-8:23 "x = x + z + y + zs[1]" -- 7:22-9:2 "{\\n\t\tx = x + z +...onrange(\"1\")\\n\t}" -- 7:1-9:2 "for _, z := ran...onrange(\"1\")\\n\t}" -- 4:36-12:1 "{\\n\tzs := []int{...ionrange(\"+\")\\n}" -- 4:0-12:1 "func Bar(x, y i...ionrange(\"+\")\\n}" -- 0:0-12:1 "package foo\\n\\nim...ionrange(\"+\")\\n}" -- -diff -urN a/gopls/internal/lsp/testdata/semantic/a.go b/gopls/internal/lsp/testdata/semantic/a.go ---- a/gopls/internal/lsp/testdata/semantic/a.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/semantic/a.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,81 +0,0 @@ --package semantictokens //@ semantic("") -- --import ( -- _ "encoding/utf8" -- utf "encoding/utf8" -- "fmt" //@ semantic("fmt") -- . "fmt" -- "unicode/utf8" --) +-// Tests golang/go#36824. +-func TestFormattingOneLineImports36824(t *testing.T) { +- const onelineProgramA = ` +--- a.go -- +-package x; func f() {fmt.Println()} - --var ( -- a = fmt.Print -- b []string = []string{"foo"} -- c1 chan int -- c2 <-chan int -- c3 = make([]chan<- int) -- b = A{X: 23} -- m map[bool][3]*float64 --) +--- a.go.imported -- +-package x - --const ( -- xx F = iota -- yy = xx + 3 -- zz = "" -- ww = "not " + zz --) +-import "fmt" - --type A struct { -- X int `foof` --} --type B interface { -- A -- sad(int) bool +-func f() { fmt.Println() } +-` +- Run(t, onelineProgramA, func(t *testing.T, env *Env) { +- env.OpenFile("a.go") +- env.OrganizeImports("a.go") +- got := env.BufferText("a.go") +- want := env.ReadWorkspaceFile("a.go.imported") +- if got != want { +- t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) +- } +- }) -} - --type F int +-func TestFormattingOneLineRmImports36824(t *testing.T) { +- const onelineProgramB = ` +--- a.go -- +-package x; import "os"; func f() {} - --func (a *A) f() bool { -- var z string -- x := "foo" -- a(x) -- y := "bar" + x -- switch z { -- case "xx": -- default: -- } -- select { -- case z := <-c3[0]: -- default: -- } -- for k, v := range m { -- return (!k) && v[0] == nil -- } -- c2 <- A.X -- w := b[4:] -- j := len(x) -- j-- -- q := []interface{}{j, 23i, &y} -- g(q...) -- return true --} +--- a.go.imported -- +-package x - --func g(vv ...interface{}) { -- ff := func() {} -- defer ff() -- go utf.RuneCount("") -- go utf8.RuneCount(vv.(string)) -- if true { -- } else { -- } --Never: -- for i := 0; i < 10; { -- break Never -- } -- _, ok := vv[0].(A) -- if !ok { -- switch x := vv[0].(type) { +-func f() {} +-` +- Run(t, onelineProgramB, func(t *testing.T, env *Env) { +- env.OpenFile("a.go") +- env.OrganizeImports("a.go") +- got := env.BufferText("a.go") +- want := env.ReadWorkspaceFile("a.go.imported") +- if got != want { +- t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) - } -- goto Never -- } +- }) -} -diff -urN a/gopls/internal/lsp/testdata/semantic/a.go.golden b/gopls/internal/lsp/testdata/semantic/a.go.golden ---- a/gopls/internal/lsp/testdata/semantic/a.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/semantic/a.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,83 +0,0 @@ ---- semantic -- --/*⇒7,keyword,[]*/package /*⇒14,namespace,[]*/semantictokens /*⇒16,comment,[]*///@ semantic("") - --/*⇒6,keyword,[]*/import ( -- _ "encoding/utf8" -- /*⇒3,namespace,[]*/utf "encoding/utf8" -- "fmt"/*⇐3,namespace,[]*/ /*⇒19,comment,[]*///@ semantic("fmt") -- . "fmt" -- "unicode/utf8"/*⇐4,namespace,[]*/ +-const disorganizedProgram = ` +--- main.go -- +-package main +- +-import ( +- "fmt" +- "errors" -) +-func main( ) { +- fmt.Println(errors.New("bad")) +-} +--- main.go.organized -- +-package main - --/*⇒3,keyword,[]*/var ( -- /*⇒1,variable,[definition]*/a = /*⇒3,namespace,[]*/fmt./*⇒5,function,[]*/Print -- /*⇒1,variable,[definition]*/b []/*⇒6,type,[defaultLibrary]*/string = []/*⇒6,type,[defaultLibrary]*/string{/*⇒5,string,[]*/"foo"} -- /*⇒2,variable,[definition]*/c1 /*⇒4,keyword,[]*/chan /*⇒3,type,[defaultLibrary]*/int -- /*⇒2,variable,[definition]*/c2 /*⇒2,operator,[]*/<-/*⇒4,keyword,[]*/chan /*⇒3,type,[defaultLibrary]*/int -- /*⇒2,variable,[definition]*/c3 = /*⇒4,function,[defaultLibrary]*/make([]/*⇒4,keyword,[]*/chan/*⇒2,operator,[]*/<- /*⇒3,type,[defaultLibrary]*/int) -- /*⇒1,variable,[definition]*/b = /*⇒1,type,[]*/A{/*⇒1,variable,[]*/X: /*⇒2,number,[]*/23} -- /*⇒1,variable,[definition]*/m /*⇒3,keyword,[]*/map[/*⇒4,type,[defaultLibrary]*/bool][/*⇒1,number,[]*/3]/*⇒1,operator,[]*/*/*⇒7,type,[defaultLibrary]*/float64 +-import ( +- "errors" +- "fmt" -) +-func main( ) { +- fmt.Println(errors.New("bad")) +-} +--- main.go.formatted -- +-package main - --/*⇒5,keyword,[]*/const ( -- /*⇒2,variable,[definition readonly]*/xx /*⇒1,type,[]*/F = /*⇒4,variable,[readonly]*/iota -- /*⇒2,variable,[definition readonly]*/yy = /*⇒2,variable,[readonly]*/xx /*⇒1,operator,[]*/+ /*⇒1,number,[]*/3 -- /*⇒2,variable,[definition readonly]*/zz = /*⇒2,string,[]*/"" -- /*⇒2,variable,[definition readonly]*/ww = /*⇒6,string,[]*/"not " /*⇒1,operator,[]*/+ /*⇒2,variable,[readonly]*/zz +-import ( +- "errors" +- "fmt" -) - --/*⇒4,keyword,[]*/type /*⇒1,type,[definition]*/A /*⇒6,keyword,[]*/struct { -- /*⇒1,variable,[definition]*/X /*⇒3,type,[defaultLibrary]*/int /*⇒6,string,[]*/`foof` --} --/*⇒4,keyword,[]*/type /*⇒1,type,[definition]*/B /*⇒9,keyword,[]*/interface { -- /*⇒1,type,[]*/A -- /*⇒3,method,[definition]*/sad(/*⇒3,type,[defaultLibrary]*/int) /*⇒4,type,[defaultLibrary]*/bool +-func main() { +- fmt.Println(errors.New("bad")) -} +-` - --/*⇒4,keyword,[]*/type /*⇒1,type,[definition]*/F /*⇒3,type,[defaultLibrary]*/int -- --/*⇒4,keyword,[]*/func (/*⇒1,variable,[]*/a /*⇒1,operator,[]*/*/*⇒1,type,[]*/A) /*⇒1,method,[definition]*/f() /*⇒4,type,[defaultLibrary]*/bool { -- /*⇒3,keyword,[]*/var /*⇒1,variable,[definition]*/z /*⇒6,type,[defaultLibrary]*/string -- /*⇒1,variable,[definition]*/x /*⇒2,operator,[]*/:= /*⇒5,string,[]*/"foo" -- /*⇒1,variable,[]*/a(/*⇒1,variable,[]*/x) -- /*⇒1,variable,[definition]*/y /*⇒2,operator,[]*/:= /*⇒5,string,[]*/"bar" /*⇒1,operator,[]*/+ /*⇒1,variable,[]*/x -- /*⇒6,keyword,[]*/switch /*⇒1,variable,[]*/z { -- /*⇒4,keyword,[]*/case /*⇒4,string,[]*/"xx": -- /*⇒7,keyword,[]*/default: -- } -- /*⇒6,keyword,[]*/select { -- /*⇒4,keyword,[]*/case /*⇒1,variable,[definition]*/z /*⇒2,operator,[]*/:= /*⇒2,operator,[]*/<-/*⇒2,variable,[]*/c3[/*⇒1,number,[]*/0]: -- /*⇒7,keyword,[]*/default: -- } -- /*⇒3,keyword,[]*/for /*⇒1,variable,[definition]*/k, /*⇒1,variable,[definition]*/v := /*⇒5,keyword,[]*/range /*⇒1,variable,[]*/m { -- /*⇒6,keyword,[]*/return (/*⇒1,operator,[]*/!/*⇒1,variable,[]*/k) /*⇒2,operator,[]*/&& /*⇒1,variable,[]*/v[/*⇒1,number,[]*/0] /*⇒2,operator,[]*/== /*⇒3,variable,[readonly defaultLibrary]*/nil -- } -- /*⇒2,variable,[]*/c2 /*⇒2,operator,[]*/<- /*⇒1,type,[]*/A./*⇒1,variable,[]*/X -- /*⇒1,variable,[definition]*/w /*⇒2,operator,[]*/:= /*⇒1,variable,[]*/b[/*⇒1,number,[]*/4:] -- /*⇒1,variable,[definition]*/j /*⇒2,operator,[]*/:= /*⇒3,function,[defaultLibrary]*/len(/*⇒1,variable,[]*/x) -- /*⇒1,variable,[]*/j/*⇒2,operator,[]*/-- -- /*⇒1,variable,[definition]*/q /*⇒2,operator,[]*/:= []/*⇒9,keyword,[]*/interface{}{/*⇒1,variable,[]*/j, /*⇒3,number,[]*/23i, /*⇒1,operator,[]*/&/*⇒1,variable,[]*/y} -- /*⇒1,function,[]*/g(/*⇒1,variable,[]*/q/*⇒3,operator,[]*/...) -- /*⇒6,keyword,[]*/return /*⇒4,variable,[readonly]*/true +-func TestOrganizeImports(t *testing.T) { +- Run(t, disorganizedProgram, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- env.OrganizeImports("main.go") +- got := env.BufferText("main.go") +- want := env.ReadWorkspaceFile("main.go.organized") +- if got != want { +- t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) +- } +- }) -} - --/*⇒4,keyword,[]*/func /*⇒1,function,[definition]*/g(/*⇒2,parameter,[definition]*/vv /*⇒3,operator,[]*/.../*⇒9,keyword,[]*/interface{}) { -- /*⇒2,variable,[definition]*/ff /*⇒2,operator,[]*/:= /*⇒4,keyword,[]*/func() {} -- /*⇒5,keyword,[]*/defer /*⇒2,function,[]*/ff() -- /*⇒2,keyword,[]*/go /*⇒3,namespace,[]*/utf./*⇒9,function,[]*/RuneCount(/*⇒2,string,[]*/"") -- /*⇒2,keyword,[]*/go /*⇒4,namespace,[]*/utf8./*⇒9,function,[]*/RuneCount(/*⇒2,parameter,[]*/vv.(/*⇒6,type,[]*/string)) -- /*⇒2,keyword,[]*/if /*⇒4,variable,[readonly]*/true { -- } /*⇒4,keyword,[]*/else { -- } --/*⇒5,parameter,[definition]*/Never: -- /*⇒3,keyword,[]*/for /*⇒1,variable,[definition]*/i /*⇒2,operator,[]*/:= /*⇒1,number,[]*/0; /*⇒1,variable,[]*/i /*⇒1,operator,[]*/< /*⇒2,number,[]*/10; { -- /*⇒5,keyword,[]*/break Never -- } -- _, /*⇒2,variable,[definition]*/ok /*⇒2,operator,[]*/:= /*⇒2,parameter,[]*/vv[/*⇒1,number,[]*/0].(/*⇒1,type,[]*/A) -- /*⇒2,keyword,[]*/if /*⇒1,operator,[]*/!/*⇒2,variable,[]*/ok { -- /*⇒6,keyword,[]*/switch /*⇒1,variable,[definition]*/x /*⇒2,operator,[]*/:= /*⇒2,parameter,[]*/vv[/*⇒1,number,[]*/0].(/*⇒4,keyword,[]*/type) { +-func TestFormattingOnSave(t *testing.T) { +- Run(t, disorganizedProgram, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- env.SaveBuffer("main.go") +- got := env.BufferText("main.go") +- want := env.ReadWorkspaceFile("main.go.formatted") +- if got != want { +- t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) - } -- /*⇒4,keyword,[]*/goto Never -- } +- }) -} - -diff -urN a/gopls/internal/lsp/testdata/semantic/b.go b/gopls/internal/lsp/testdata/semantic/b.go ---- a/gopls/internal/lsp/testdata/semantic/b.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/semantic/b.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,38 +0,0 @@ --package semantictokens //@ semantic("") +-// Tests various possibilities for comments in files with CRLF line endings. +-// Import organization in these files has historically been a source of bugs. +-func TestCRLFLineEndings(t *testing.T) { +- for _, tt := range []struct { +- issue, input, want string +- }{ +- { +- issue: "41057", +- want: `package main - --func f(x ...interface{}) { +-/* +-Hi description +-*/ +-func Hi() { -} +-`, +- }, +- { +- issue: "42646", +- want: `package main - --func weirⰀd() { /*😀*/ // comment -- const ( -- snil = nil -- nil = true -- true = false -- false = snil -- cmd = `foof` -- double = iota -- iota = copy -- four = (len(cmd)/2 < 5) -- five = four -- ) -- f(cmd, nil, double, iota) --} +-import ( +- "fmt" +-) - -/* +-func upload(c echo.Context) error { +- if err := r.ParseForm(); err != nil { +- fmt.Fprintf(w, "ParseForm() err: %v", err) +- return +- } +- fmt.Fprintf(w, "POST request successful") +- path_ver := r.FormValue("path_ver") +- ukclin_ver := r.FormValue("ukclin_ver") - --multiline */ /* --multiline +- fmt.Fprintf(w, "Name = %s\n", path_ver) +- fmt.Fprintf(w, "Address = %s\n", ukclin_ver) +-} -*/ --type AA int --type BB struct { -- AA --} --type CC struct { -- AA int --} --type D func(aa AA) (BB error) --type E func(AA) BB -- --var a chan<- chan int --var b chan<- <-chan int --var c <-chan <-chan int -diff -urN a/gopls/internal/lsp/testdata/semantic/b.go.golden b/gopls/internal/lsp/testdata/semantic/b.go.golden ---- a/gopls/internal/lsp/testdata/semantic/b.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/semantic/b.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,40 +0,0 @@ ---- semantic -- --/*⇒7,keyword,[]*/package /*⇒14,namespace,[]*/semantictokens /*⇒16,comment,[]*///@ semantic("") -- --/*⇒4,keyword,[]*/func /*⇒1,function,[definition]*/f(/*⇒1,parameter,[definition]*/x /*⇒3,operator,[]*/.../*⇒9,keyword,[]*/interface{}) { --} -- --/*⇒4,keyword,[]*/func /*⇒6,function,[definition]*/weirⰀd() { /*⇒5,comment,[]*//*😀*/ /*⇒10,comment,[]*/// comment -- /*⇒5,keyword,[]*/const ( -- /*⇒4,variable,[definition readonly]*/snil = /*⇒3,variable,[readonly defaultLibrary]*/nil -- /*⇒3,variable,[definition readonly]*/nil = /*⇒4,variable,[readonly]*/true -- /*⇒4,variable,[definition readonly]*/true = /*⇒5,variable,[readonly]*/false -- /*⇒5,variable,[definition readonly]*/false = /*⇒4,variable,[readonly]*/snil -- /*⇒3,variable,[definition readonly]*/cmd = /*⇒6,string,[]*/`foof` -- /*⇒6,variable,[definition readonly]*/double = /*⇒4,variable,[readonly]*/iota -- /*⇒4,variable,[definition readonly]*/iota = /*⇒4,function,[defaultLibrary]*/copy -- /*⇒4,variable,[definition readonly]*/four = (/*⇒3,function,[defaultLibrary]*/len(/*⇒3,variable,[readonly]*/cmd)/*⇒1,operator,[]*// /*⇒1,number,[]*/2 /*⇒1,operator,[]*/< /*⇒1,number,[]*/5) -- /*⇒4,variable,[definition readonly]*/five = /*⇒4,variable,[readonly]*/four -- ) -- /*⇒1,function,[]*/f(/*⇒3,variable,[readonly]*/cmd, /*⇒3,variable,[readonly]*/nil, /*⇒6,variable,[readonly]*/double, /*⇒4,variable,[readonly]*/iota) --} -- --/*⇒2,comment,[]*//* --/*⇒0,comment,[]*/ --/*⇒12,comment,[]*/multiline */ /*⇒2,comment,[]*//* --/*⇒9,comment,[]*/multiline --/*⇒2,comment,[]*/*/ --/*⇒4,keyword,[]*/type /*⇒2,type,[definition]*/AA /*⇒3,type,[defaultLibrary]*/int --/*⇒4,keyword,[]*/type /*⇒2,type,[definition]*/BB /*⇒6,keyword,[]*/struct { -- /*⇒2,type,[]*/AA --} --/*⇒4,keyword,[]*/type /*⇒2,type,[definition]*/CC /*⇒6,keyword,[]*/struct { -- /*⇒2,variable,[definition]*/AA /*⇒3,type,[defaultLibrary]*/int --} --/*⇒4,keyword,[]*/type /*⇒1,type,[definition]*/D /*⇒4,keyword,[]*/func(/*⇒2,parameter,[definition]*/aa /*⇒2,type,[]*/AA) (/*⇒2,parameter,[definition]*/BB /*⇒5,type,[]*/error) --/*⇒4,keyword,[]*/type /*⇒1,type,[definition]*/E /*⇒4,keyword,[]*/func(/*⇒2,type,[]*/AA) /*⇒2,type,[]*/BB -- --/*⇒3,keyword,[]*/var /*⇒1,variable,[definition]*/a /*⇒4,keyword,[]*/chan/*⇒2,operator,[]*/<- /*⇒4,keyword,[]*/chan /*⇒3,type,[defaultLibrary]*/int --/*⇒3,keyword,[]*/var /*⇒1,variable,[definition]*/b /*⇒4,keyword,[]*/chan/*⇒2,operator,[]*/<- /*⇒2,operator,[]*/<-/*⇒4,keyword,[]*/chan /*⇒3,type,[defaultLibrary]*/int --/*⇒3,keyword,[]*/var /*⇒1,variable,[definition]*/c /*⇒2,operator,[]*/<-/*⇒4,keyword,[]*/chan /*⇒2,operator,[]*/<-/*⇒4,keyword,[]*/chan /*⇒3,type,[defaultLibrary]*/int -- -diff -urN a/gopls/internal/lsp/testdata/semantic/README.md b/gopls/internal/lsp/testdata/semantic/README.md ---- a/gopls/internal/lsp/testdata/semantic/README.md 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/semantic/README.md 1970-01-01 00:00:00.000000000 +0000 -@@ -1,2 +0,0 @@ --The golden files are the output of `gopls semtok <src-file>`, with `-- semantic --` --inserted as the first line (the spaces are mandatory) and an extra newline at the end. -diff -urN a/gopls/internal/lsp/testdata/semantic/semantic_test.go b/gopls/internal/lsp/testdata/semantic/semantic_test.go ---- a/gopls/internal/lsp/testdata/semantic/semantic_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/semantic/semantic_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,13 +0,0 @@ --package semantictokens - --import ( -- "os" -- "testing" --) +-func main() { +- const server_port = 8080 +- fmt.Printf("port: %d\n", server_port) +-} +-`, +- }, +- { +- issue: "42923", +- want: `package main +- +-// Line 1. +-// aa +-type Tree struct { +- arr []string +-} +-`, +- }, +- { +- issue: "47200", +- input: `package main - --func TestSemanticTokens(t *testing.T) { -- a, _ := os.Getwd() -- // climb up to find internal/lsp -- // find all the .go files +-import "fmt" - +-func main() { +- math.Sqrt(9) +- fmt.Println("hello") -} -diff -urN a/gopls/internal/lsp/testdata/stub/other/other.go b/gopls/internal/lsp/testdata/stub/other/other.go ---- a/gopls/internal/lsp/testdata/stub/other/other.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/other/other.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,10 +0,0 @@ --package other +-`, +- want: `package main - -import ( -- "bytes" -- renamed_context "context" +- "fmt" +- "math" -) - --type Interface interface { -- Get(renamed_context.Context) *bytes.Buffer +-func main() { +- math.Sqrt(9) +- fmt.Println("hello") +-} +-`, +- }, +- } { +- t.Run(tt.issue, func(t *testing.T) { +- Run(t, "-- main.go --", func(t *testing.T, env *Env) { +- input := tt.input +- if input == "" { +- input = tt.want +- } +- crlf := strings.ReplaceAll(input, "\n", "\r\n") +- env.CreateBuffer("main.go", crlf) +- env.Await(env.DoneWithOpen()) +- env.OrganizeImports("main.go") +- got := env.BufferText("main.go") +- got = strings.ReplaceAll(got, "\r\n", "\n") // convert everything to LF for simplicity +- if tt.want != got { +- t.Errorf("unexpected content after save:\n%s", compare.Text(tt.want, got)) +- } +- }) +- }) +- } -} -diff -urN a/gopls/internal/lsp/testdata/stub/stub_add_selector.go b/gopls/internal/lsp/testdata/stub/stub_add_selector.go ---- a/gopls/internal/lsp/testdata/stub/stub_add_selector.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_add_selector.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,12 +0,0 @@ --package stub -- --import "io" -- --// This file tests that if an interface --// method references a type from its own package --// then our implementation must add the import/package selector --// in the concrete method if the concrete type is outside of the interface --// package --var _ io.ReaderFrom = &readerFrom{} //@suggestedfix("&readerFrom", "quickfix", "") - --type readerFrom struct{} -diff -urN a/gopls/internal/lsp/testdata/stub/stub_add_selector.go.golden b/gopls/internal/lsp/testdata/stub/stub_add_selector.go.golden ---- a/gopls/internal/lsp/testdata/stub/stub_add_selector.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_add_selector.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,19 +0,0 @@ ---- suggestedfix_stub_add_selector_10_23 -- --package stub +-func TestFormattingOfGeneratedFile_Issue49555(t *testing.T) { +- const input = ` +--- main.go -- +-// Code generated by generator.go. DO NOT EDIT. - --import "io" +-package main - --// This file tests that if an interface --// method references a type from its own package --// then our implementation must add the import/package selector --// in the concrete method if the concrete type is outside of the interface --// package --var _ io.ReaderFrom = &readerFrom{} //@suggestedfix("&readerFrom", "quickfix", "") +-import "fmt" - --type readerFrom struct{} +-func main() { - --// ReadFrom implements io.ReaderFrom. --func (*readerFrom) ReadFrom(r io.Reader) (n int64, err error) { -- panic("unimplemented") --} - -diff -urN a/gopls/internal/lsp/testdata/stub/stub_assign.go b/gopls/internal/lsp/testdata/stub/stub_assign.go ---- a/gopls/internal/lsp/testdata/stub/stub_assign.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_assign.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,10 +0,0 @@ --package stub - --import "io" - --func main() { -- var br io.ByteWriter -- br = &byteWriter{} //@suggestedfix("&", "quickfix", "") +- fmt.Print("hello") -} +-` - --type byteWriter struct{} -diff -urN a/gopls/internal/lsp/testdata/stub/stub_assign.go.golden b/gopls/internal/lsp/testdata/stub/stub_assign.go.golden ---- a/gopls/internal/lsp/testdata/stub/stub_assign.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_assign.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,17 +0,0 @@ ---- suggestedfix_stub_assign_7_7 -- --package stub -- --import "io" +- Run(t, input, func(t *testing.T, env *Env) { +- wantErrSuffix := "file is generated" - --func main() { -- var br io.ByteWriter -- br = &byteWriter{} //@suggestedfix("&", "quickfix", "") +- env.OpenFile("main.go") +- err := env.Editor.FormatBuffer(env.Ctx, "main.go") +- if err == nil { +- t.Fatal("expected error, got nil") +- } +- // Check only the suffix because an error contains a dynamic path to main.go +- if !strings.HasSuffix(err.Error(), wantErrSuffix) { +- t.Fatalf("unexpected error %q, want suffix %q", err.Error(), wantErrSuffix) +- } +- }) -} - --type byteWriter struct{} +-func TestGofumptFormatting(t *testing.T) { +- testenv.NeedsGo1Point(t, 20) // gofumpt requires go 1.20+ +- // Exercise some gofumpt formatting rules: +- // - No empty lines following an assignment operator +- // - Octal integer literals should use the 0o prefix on modules using Go +- // 1.13 and later. Requires LangVersion to be correctly resolved. +- // - std imports must be in a separate group at the top. Requires ModulePath +- // to be correctly resolved. +- const input = ` +--- go.mod -- +-module foo - --// WriteByte implements io.ByteWriter. --func (*byteWriter) WriteByte(c byte) error { -- panic("unimplemented") --} +-go 1.17 +--- foo.go -- +-package foo - -diff -urN a/gopls/internal/lsp/testdata/stub/stub_assign_multivars.go b/gopls/internal/lsp/testdata/stub/stub_assign_multivars.go ---- a/gopls/internal/lsp/testdata/stub/stub_assign_multivars.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_assign_multivars.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,11 +0,0 @@ --package stub +-import ( +- "foo/bar" +- "fmt" +-) - --import "io" +-const perm = 0755 - --func main() { -- var br io.ByteWriter -- var i int -- i, br = 1, &multiByteWriter{} //@suggestedfix("&", "quickfix", "") +-func foo() { +- foo := +- "bar" +- fmt.Println(foo, bar.Bar) -} +--- foo.go.formatted -- +-package foo - --type multiByteWriter struct{} -diff -urN a/gopls/internal/lsp/testdata/stub/stub_assign_multivars.go.golden b/gopls/internal/lsp/testdata/stub/stub_assign_multivars.go.golden ---- a/gopls/internal/lsp/testdata/stub/stub_assign_multivars.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_assign_multivars.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,18 +0,0 @@ ---- suggestedfix_stub_assign_multivars_8_13 -- --package stub -- --import "io" +-import ( +- "fmt" - --func main() { -- var br io.ByteWriter -- var i int -- i, br = 1, &multiByteWriter{} //@suggestedfix("&", "quickfix", "") --} +- "foo/bar" +-) - --type multiByteWriter struct{} +-const perm = 0o755 - --// WriteByte implements io.ByteWriter. --func (*multiByteWriter) WriteByte(c byte) error { -- panic("unimplemented") +-func foo() { +- foo := "bar" +- fmt.Println(foo, bar.Bar) -} +--- bar/bar.go -- +-package bar - -diff -urN a/gopls/internal/lsp/testdata/stub/stub_call_expr.go b/gopls/internal/lsp/testdata/stub/stub_call_expr.go ---- a/gopls/internal/lsp/testdata/stub/stub_call_expr.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_call_expr.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,13 +0,0 @@ --package stub +-const Bar = 42 +-` - --func main() { -- check(&callExpr{}) //@suggestedfix("&", "quickfix", "") +- WithOptions( +- Settings{ +- "gofumpt": true, +- }, +- ).Run(t, input, func(t *testing.T, env *Env) { +- env.OpenFile("foo.go") +- env.FormatBuffer("foo.go") +- got := env.BufferText("foo.go") +- want := env.ReadWorkspaceFile("foo.go.formatted") +- if got != want { +- t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) +- } +- }) -} - --func check(err error) { -- if err != nil { -- panic(err) -- } --} +-func TestGofumpt_Issue61692(t *testing.T) { +- testenv.NeedsGo1Point(t, 21) - --type callExpr struct{} -diff -urN a/gopls/internal/lsp/testdata/stub/stub_call_expr.go.golden b/gopls/internal/lsp/testdata/stub/stub_call_expr.go.golden ---- a/gopls/internal/lsp/testdata/stub/stub_call_expr.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_call_expr.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,20 +0,0 @@ ---- suggestedfix_stub_call_expr_4_8 -- --package stub +- const input = ` +--- go.mod -- +-module foo - --func main() { -- check(&callExpr{}) //@suggestedfix("&", "quickfix", "") +-go 1.21rc3 +--- foo.go -- +-package foo +- +-func _() { +- foo := +- "bar" -} +-` - --func check(err error) { -- if err != nil { -- panic(err) -- } +- WithOptions( +- Settings{ +- "gofumpt": true, +- }, +- ).Run(t, input, func(t *testing.T, env *Env) { +- env.OpenFile("foo.go") +- env.FormatBuffer("foo.go") // golang/go#61692: must not panic +- }) -} +diff -urN a/gopls/internal/test/integration/misc/generate_test.go b/gopls/internal/test/integration/misc/generate_test.go +--- a/gopls/internal/test/integration/misc/generate_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/misc/generate_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,105 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --type callExpr struct{} +-// TODO(rfindley): figure out why go generate fails on android builders. - --// Error implements error. --func (*callExpr) Error() string { -- panic("unimplemented") --} +-//go:build !android +-// +build !android - -diff -urN a/gopls/internal/lsp/testdata/stub/stub_embedded.go b/gopls/internal/lsp/testdata/stub/stub_embedded.go ---- a/gopls/internal/lsp/testdata/stub/stub_embedded.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_embedded.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,15 +0,0 @@ --package stub +-package misc - -import ( -- "io" -- "sort" +- "testing" +- +- . "golang.org/x/tools/gopls/internal/test/integration" -) - --var _ embeddedInterface = (*embeddedConcrete)(nil) //@suggestedfix("(", "quickfix", "") +-func TestGenerateProgress(t *testing.T) { +- const generatedWorkspace = ` +--- go.mod -- +-module fake.test - --type embeddedConcrete struct{} +-go 1.14 +--- generate.go -- +-// +build ignore - --type embeddedInterface interface { -- sort.Interface -- io.Reader --} -diff -urN a/gopls/internal/lsp/testdata/stub/stub_embedded.go.golden b/gopls/internal/lsp/testdata/stub/stub_embedded.go.golden ---- a/gopls/internal/lsp/testdata/stub/stub_embedded.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_embedded.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,37 +0,0 @@ ---- suggestedfix_stub_embedded_8_27 -- --package stub +-package main - -import ( -- "io" -- "sort" +- "os" -) - --var _ embeddedInterface = (*embeddedConcrete)(nil) //@suggestedfix("(", "quickfix", "") +-func main() { +- os.WriteFile("generated.go", []byte("package " + os.Args[1] + "\n\nconst Answer = 21"), 0644) +-} - --type embeddedConcrete struct{} +--- lib1/lib.go -- +-package lib1 - --// Len implements embeddedInterface. --func (*embeddedConcrete) Len() int { -- panic("unimplemented") --} +-//` + `go:generate go run ../generate.go lib1 - --// Less implements embeddedInterface. --func (*embeddedConcrete) Less(i int, j int) bool { -- panic("unimplemented") --} +--- lib2/lib.go -- +-package lib2 - --// Read implements embeddedInterface. --func (*embeddedConcrete) Read(p []byte) (n int, err error) { -- panic("unimplemented") --} +-//` + `go:generate go run ../generate.go lib2 - --// Swap implements embeddedInterface. --func (*embeddedConcrete) Swap(i int, j int) { -- panic("unimplemented") +--- main.go -- +-package main +- +-import ( +- "fake.test/lib1" +- "fake.test/lib2" +-) +- +-func main() { +- println(lib1.Answer + lib2.Answer) -} +-` - --type embeddedInterface interface { -- sort.Interface -- io.Reader +- Run(t, generatedWorkspace, func(t *testing.T, env *Env) { +- env.OnceMet( +- InitialWorkspaceLoad, +- Diagnostics(env.AtRegexp("main.go", "lib1.(Answer)")), +- ) +- env.RunGenerate("./lib1") +- env.RunGenerate("./lib2") +- env.AfterChange( +- NoDiagnostics(ForFile("main.go")), +- ) +- }) -} - -diff -urN a/gopls/internal/lsp/testdata/stub/stub_err.go b/gopls/internal/lsp/testdata/stub/stub_err.go ---- a/gopls/internal/lsp/testdata/stub/stub_err.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_err.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,7 +0,0 @@ --package stub +-func TestGenerateUseNetwork(t *testing.T) { +- const proxy = ` +--- example.com@v1.2.3/go.mod -- +-module example.com +- +-go 1.21 +--- example.com@v1.2.3/main.go -- +-package main - -func main() { -- var br error = &customErr{} //@suggestedfix("&", "quickfix", "") +- println("hello world") -} +-` +- const generatedWorkspace = ` +--- go.mod -- +-module fake.test - --type customErr struct{} -diff -urN a/gopls/internal/lsp/testdata/stub/stub_err.go.golden b/gopls/internal/lsp/testdata/stub/stub_err.go.golden ---- a/gopls/internal/lsp/testdata/stub/stub_err.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_err.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,14 +0,0 @@ ---- suggestedfix_stub_err_4_17 -- --package stub +-go 1.21 +--- main.go -- - --func main() { -- var br error = &customErr{} //@suggestedfix("&", "quickfix", "") --} +-package main - --type customErr struct{} +-//go:` + /* hide this string from the go command */ `generate go run example.com@latest - --// Error implements error. --func (*customErr) Error() string { -- panic("unimplemented") +-` +- WithOptions(ProxyFiles(proxy)). +- Run(t, generatedWorkspace, func(t *testing.T, env *Env) { +- env.OnceMet( +- InitialWorkspaceLoad, +- ) +- env.RunGenerate("./") +- }) -} +diff -urN a/gopls/internal/test/integration/misc/highlight_test.go b/gopls/internal/test/integration/misc/highlight_test.go +--- a/gopls/internal/test/integration/misc/highlight_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/misc/highlight_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,153 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -diff -urN a/gopls/internal/lsp/testdata/stub/stub_function_return.go b/gopls/internal/lsp/testdata/stub/stub_function_return.go ---- a/gopls/internal/lsp/testdata/stub/stub_function_return.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_function_return.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,11 +0,0 @@ --package stub +-package misc - -import ( -- "io" --) +- "sort" +- "testing" - --func newCloser() io.Closer { -- return closer{} //@suggestedfix("c", "quickfix", "") --} +- "golang.org/x/tools/gopls/internal/protocol" +- . "golang.org/x/tools/gopls/internal/test/integration" +-) - --type closer struct{} -diff -urN a/gopls/internal/lsp/testdata/stub/stub_function_return.go.golden b/gopls/internal/lsp/testdata/stub/stub_function_return.go.golden ---- a/gopls/internal/lsp/testdata/stub/stub_function_return.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_function_return.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,18 +0,0 @@ ---- suggestedfix_stub_function_return_8_9 -- --package stub +-func TestWorkspacePackageHighlight(t *testing.T) { +- const mod = ` +--- go.mod -- +-module mod.com - --import ( -- "io" --) +-go 1.12 +--- main.go -- +-package main - --func newCloser() io.Closer { -- return closer{} //@suggestedfix("c", "quickfix", "") --} +-func main() { +- var A string = "A" +- x := "x-" + A +- println(A, x) +-}` - --type closer struct{} +- Run(t, mod, func(t *testing.T, env *Env) { +- const file = "main.go" +- env.OpenFile(file) +- loc := env.GoToDefinition(env.RegexpSearch(file, `var (A) string`)) - --// Close implements io.Closer. --func (closer) Close() error { -- panic("unimplemented") +- checkHighlights(env, loc, 3) +- }) -} - -diff -urN a/gopls/internal/lsp/testdata/stub/stub_generic_receiver.go b/gopls/internal/lsp/testdata/stub/stub_generic_receiver.go ---- a/gopls/internal/lsp/testdata/stub/stub_generic_receiver.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_generic_receiver.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,15 +0,0 @@ --//go:build go1.18 --// +build go1.18 +-func TestStdPackageHighlight_Issue43511(t *testing.T) { +- const mod = ` +--- go.mod -- +-module mod.com - --package stub +-go 1.12 +--- main.go -- +-package main - --import "io" +-import "fmt" - --// This file tests that that the stub method generator accounts for concrete --// types that have type parameters defined. --var _ io.ReaderFrom = &genReader[string, int]{} //@suggestedfix("&genReader", "quickfix", "Implement io.ReaderFrom") +-func main() { +- fmt.Printf() +-}` - --type genReader[T, Y any] struct { -- T T -- Y Y +- Run(t, mod, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- defLoc := env.GoToDefinition(env.RegexpSearch("main.go", `fmt\.(Printf)`)) +- file := env.Sandbox.Workdir.URIToPath(defLoc.URI) +- loc := env.RegexpSearch(file, `func Printf\((format) string`) +- +- checkHighlights(env, loc, 2) +- }) -} -diff -urN a/gopls/internal/lsp/testdata/stub/stub_generic_receiver.go.golden b/gopls/internal/lsp/testdata/stub/stub_generic_receiver.go.golden ---- a/gopls/internal/lsp/testdata/stub/stub_generic_receiver.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_generic_receiver.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,22 +0,0 @@ ---- suggestedfix_stub_generic_receiver_10_23 -- --//go:build go1.18 --// +build go1.18 - --package stub +-func TestThirdPartyPackageHighlight_Issue43511(t *testing.T) { +- const proxy = ` +--- example.com@v1.2.3/go.mod -- +-module example.com - --import "io" +-go 1.12 +--- example.com@v1.2.3/global/global.go -- +-package global - --// This file tests that that the stub method generator accounts for concrete --// types that have type parameters defined. --var _ io.ReaderFrom = &genReader[string, int]{} //@suggestedfix("&genReader", "quickfix", "Implement io.ReaderFrom") +-const A = 1 - --type genReader[T, Y any] struct { -- T T -- Y Y +-func foo() { +- _ = A -} - --// ReadFrom implements io.ReaderFrom. --func (*genReader[T, Y]) ReadFrom(r io.Reader) (n int64, err error) { -- panic("unimplemented") +-func bar() int { +- return A + A -} +--- example.com@v1.2.3/local/local.go -- +-package local - -diff -urN a/gopls/internal/lsp/testdata/stub/stub_ignored_imports.go b/gopls/internal/lsp/testdata/stub/stub_ignored_imports.go ---- a/gopls/internal/lsp/testdata/stub/stub_ignored_imports.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_ignored_imports.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,18 +0,0 @@ --package stub -- --import ( -- "compress/zlib" -- . "io" -- _ "io" --) -- --// This file tests that dot-imports and underscore imports --// are properly ignored and that a new import is added to --// reference method types -- --var ( -- _ Reader -- _ zlib.Resetter = (*ignoredResetter)(nil) //@suggestedfix("(", "quickfix", "") --) -- --type ignoredResetter struct{} -diff -urN a/gopls/internal/lsp/testdata/stub/stub_ignored_imports.go.golden b/gopls/internal/lsp/testdata/stub/stub_ignored_imports.go.golden ---- a/gopls/internal/lsp/testdata/stub/stub_ignored_imports.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_ignored_imports.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,25 +0,0 @@ ---- suggestedfix_stub_ignored_imports_15_20 -- --package stub -- --import ( -- "compress/zlib" -- . "io" -- _ "io" --) -- --// This file tests that dot-imports and underscore imports --// are properly ignored and that a new import is added to --// reference method types -- --var ( -- _ Reader -- _ zlib.Resetter = (*ignoredResetter)(nil) //@suggestedfix("(", "quickfix", "") --) +-func foo() int { +- const b = 2 - --type ignoredResetter struct{} +- return b * b * (b+1) + b +-}` - --// Reset implements zlib.Resetter. --func (*ignoredResetter) Reset(r Reader, dict []byte) error { -- panic("unimplemented") --} +- const mod = ` +--- go.mod -- +-module mod.com - -diff -urN a/gopls/internal/lsp/testdata/stub/stub_issue2606.go b/gopls/internal/lsp/testdata/stub/stub_issue2606.go ---- a/gopls/internal/lsp/testdata/stub/stub_issue2606.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_issue2606.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,7 +0,0 @@ --package stub +-go 1.12 - --type I interface{ error } +-require example.com v1.2.3 +--- go.sum -- +-example.com v1.2.3 h1:WFzrgiQJwEDJNLDUOV1f9qlasQkvzXf2UNLaNIqbWsI= +-example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= +--- main.go -- +-package main - --type C int +-import ( +- _ "example.com/global" +- _ "example.com/local" +-) - --var _ I = C(0) //@suggestedfix("C", "quickfix", "") -diff -urN a/gopls/internal/lsp/testdata/stub/stub_issue2606.go.golden b/gopls/internal/lsp/testdata/stub/stub_issue2606.go.golden ---- a/gopls/internal/lsp/testdata/stub/stub_issue2606.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_issue2606.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,14 +0,0 @@ ---- suggestedfix_stub_issue2606_7_11 -- --package stub +-func main() {}` - --type I interface{ error } +- WithOptions( +- ProxyFiles(proxy), +- ).Run(t, mod, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") - --type C int +- defLoc := env.GoToDefinition(env.RegexpSearch("main.go", `"example.com/global"`)) +- file := env.Sandbox.Workdir.URIToPath(defLoc.URI) +- loc := env.RegexpSearch(file, `const (A)`) +- checkHighlights(env, loc, 4) - --// Error implements I. --func (C) Error() string { -- panic("unimplemented") +- defLoc = env.GoToDefinition(env.RegexpSearch("main.go", `"example.com/local"`)) +- file = env.Sandbox.Workdir.URIToPath(defLoc.URI) +- loc = env.RegexpSearch(file, `const (b)`) +- checkHighlights(env, loc, 5) +- }) -} - --var _ I = C(0) //@suggestedfix("C", "quickfix", "") +-func checkHighlights(env *Env, loc protocol.Location, highlightCount int) { +- t := env.T +- t.Helper() - -diff -urN a/gopls/internal/lsp/testdata/stub/stub_multi_var.go b/gopls/internal/lsp/testdata/stub/stub_multi_var.go ---- a/gopls/internal/lsp/testdata/stub/stub_multi_var.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_multi_var.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,11 +0,0 @@ --package stub +- highlights := env.DocumentHighlight(loc) +- if len(highlights) != highlightCount { +- t.Fatalf("expected %v highlight(s), got %v", highlightCount, len(highlights)) +- } - --import "io" +- references := env.References(loc) +- if len(highlights) != len(references) { +- t.Fatalf("number of highlights and references is expected to be equal: %v != %v", len(highlights), len(references)) +- } - --// This test ensures that a variable declaration that --// has multiple values on the same line can still be --// analyzed correctly to target the interface implementation --// diagnostic. --var one, two, three io.Reader = nil, &multiVar{}, nil //@suggestedfix("&", "quickfix", "") +- sort.Slice(highlights, func(i, j int) bool { +- return protocol.CompareRange(highlights[i].Range, highlights[j].Range) < 0 +- }) +- sort.Slice(references, func(i, j int) bool { +- return protocol.CompareRange(references[i].Range, references[j].Range) < 0 +- }) +- for i := range highlights { +- if highlights[i].Range != references[i].Range { +- t.Errorf("highlight and reference ranges are expected to be equal: %v != %v", highlights[i].Range, references[i].Range) +- } +- } +-} +diff -urN a/gopls/internal/test/integration/misc/hover_test.go b/gopls/internal/test/integration/misc/hover_test.go +--- a/gopls/internal/test/integration/misc/hover_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/misc/hover_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,516 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --type multiVar struct{} -diff -urN a/gopls/internal/lsp/testdata/stub/stub_multi_var.go.golden b/gopls/internal/lsp/testdata/stub/stub_multi_var.go.golden ---- a/gopls/internal/lsp/testdata/stub/stub_multi_var.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_multi_var.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,18 +0,0 @@ ---- suggestedfix_stub_multi_var_9_38 -- --package stub +-package misc - --import "io" +-import ( +- "fmt" +- "strings" +- "testing" - --// This test ensures that a variable declaration that --// has multiple values on the same line can still be --// analyzed correctly to target the interface implementation --// diagnostic. --var one, two, three io.Reader = nil, &multiVar{}, nil //@suggestedfix("&", "quickfix", "") +- "golang.org/x/tools/gopls/internal/protocol" +- . "golang.org/x/tools/gopls/internal/test/integration" +- "golang.org/x/tools/gopls/internal/test/integration/fake" +- "golang.org/x/tools/internal/testenv" +-) - --type multiVar struct{} +-func TestHoverUnexported(t *testing.T) { +- const proxy = ` +--- golang.org/x/structs@v1.0.0/go.mod -- +-module golang.org/x/structs - --// Read implements io.Reader. --func (*multiVar) Read(p []byte) (n int, err error) { -- panic("unimplemented") --} +-go 1.12 - -diff -urN a/gopls/internal/lsp/testdata/stub/stub_pointer.go b/gopls/internal/lsp/testdata/stub/stub_pointer.go ---- a/gopls/internal/lsp/testdata/stub/stub_pointer.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_pointer.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,9 +0,0 @@ --package stub +--- golang.org/x/structs@v1.0.0/types.go -- +-package structs - --import "io" +-type Mixed struct { +- // Exported comment +- Exported int +- unexported string +-} - --func getReaderFrom() io.ReaderFrom { -- return &pointerImpl{} //@suggestedfix("&", "quickfix", "") +-func printMixed(m Mixed) { +- println(m) -} +-` +- const mod = ` +--- go.mod -- +-module mod.com - --type pointerImpl struct{} -diff -urN a/gopls/internal/lsp/testdata/stub/stub_pointer.go.golden b/gopls/internal/lsp/testdata/stub/stub_pointer.go.golden ---- a/gopls/internal/lsp/testdata/stub/stub_pointer.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_pointer.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,16 +0,0 @@ ---- suggestedfix_stub_pointer_6_9 -- --package stub +-go 1.12 - --import "io" +-require golang.org/x/structs v1.0.0 +--- go.sum -- +-golang.org/x/structs v1.0.0 h1:Ito/a7hBYZaNKShFrZKjfBA/SIPvmBrcPCBWPx5QeKk= +-golang.org/x/structs v1.0.0/go.mod h1:47gkSIdo5AaQaWJS0upVORsxfEr1LL1MWv9dmYF3iq4= +--- main.go -- +-package main - --func getReaderFrom() io.ReaderFrom { -- return &pointerImpl{} //@suggestedfix("&", "quickfix", "") +-import "golang.org/x/structs" +- +-func main() { +- var m structs.Mixed +- _ = m.Exported -} +-` - --type pointerImpl struct{} +- // TODO: use a nested workspace folder here. +- WithOptions( +- ProxyFiles(proxy), +- ).Run(t, mod, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- mixedLoc := env.RegexpSearch("main.go", "Mixed") +- got, _ := env.Hover(mixedLoc) +- if !strings.Contains(got.Value, "unexported") { +- t.Errorf("Workspace hover: missing expected field 'unexported'. Got:\n%q", got.Value) +- } - --// ReadFrom implements io.ReaderFrom. --func (*pointerImpl) ReadFrom(r io.Reader) (n int64, err error) { -- panic("unimplemented") +- cacheLoc := env.GoToDefinition(mixedLoc) +- cacheFile := env.Sandbox.Workdir.URIToPath(cacheLoc.URI) +- argLoc := env.RegexpSearch(cacheFile, "printMixed.*(Mixed)") +- got, _ = env.Hover(argLoc) +- if !strings.Contains(got.Value, "unexported") { +- t.Errorf("Non-workspace hover: missing expected field 'unexported'. Got:\n%q", got.Value) +- } +- +- exportedFieldLoc := env.RegexpSearch("main.go", "Exported") +- got, _ = env.Hover(exportedFieldLoc) +- if !strings.Contains(got.Value, "comment") { +- t.Errorf("Workspace hover: missing comment for field 'Exported'. Got:\n%q", got.Value) +- } +- }) -} - -diff -urN a/gopls/internal/lsp/testdata/stub/stub_renamed_import.go b/gopls/internal/lsp/testdata/stub/stub_renamed_import.go ---- a/gopls/internal/lsp/testdata/stub/stub_renamed_import.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_renamed_import.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,11 +0,0 @@ --package stub +-func TestHoverIntLiteral(t *testing.T) { +- const source = ` +--- main.go -- +-package main - --import ( -- "compress/zlib" -- myio "io" +-var ( +- bigBin = 0b1001001 -) - --var _ zlib.Resetter = &myIO{} //@suggestedfix("&", "quickfix", "") --var _ myio.Reader -- --type myIO struct{} -diff -urN a/gopls/internal/lsp/testdata/stub/stub_renamed_import.go.golden b/gopls/internal/lsp/testdata/stub/stub_renamed_import.go.golden ---- a/gopls/internal/lsp/testdata/stub/stub_renamed_import.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_renamed_import.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,18 +0,0 @@ ---- suggestedfix_stub_renamed_import_8_23 -- --package stub +-var hex = 0xe34e - --import ( -- "compress/zlib" -- myio "io" --) +-func main() { +-} +-` +- Run(t, source, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- hexExpected := "58190" +- got, _ := env.Hover(env.RegexpSearch("main.go", "0xe")) +- if got != nil && !strings.Contains(got.Value, hexExpected) { +- t.Errorf("Hover: missing expected field '%s'. Got:\n%q", hexExpected, got.Value) +- } - --var _ zlib.Resetter = &myIO{} //@suggestedfix("&", "quickfix", "") --var _ myio.Reader +- binExpected := "73" +- got, _ = env.Hover(env.RegexpSearch("main.go", "0b1")) +- if got != nil && !strings.Contains(got.Value, binExpected) { +- t.Errorf("Hover: missing expected field '%s'. Got:\n%q", binExpected, got.Value) +- } +- }) +-} - --type myIO struct{} +-// Tests that hovering does not trigger the panic in golang/go#48249. +-func TestPanicInHoverBrokenCode(t *testing.T) { +- // Note: this test can not be expressed as a marker test, as it must use +- // content without a trailing newline. +- const source = ` +--- main.go -- +-package main - --// Reset implements zlib.Resetter. --func (*myIO) Reset(r myio.Reader, dict []byte) error { -- panic("unimplemented") +-type Example struct` +- Run(t, source, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- env.Editor.Hover(env.Ctx, env.RegexpSearch("main.go", "Example")) +- }) -} - -diff -urN a/gopls/internal/lsp/testdata/stub/stub_renamed_import_iface.go b/gopls/internal/lsp/testdata/stub/stub_renamed_import_iface.go ---- a/gopls/internal/lsp/testdata/stub/stub_renamed_import_iface.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_renamed_import_iface.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,13 +0,0 @@ --package stub +-func TestHoverRune_48492(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - --import ( -- "golang.org/lsptests/stub/other" --) +-go 1.18 +--- main.go -- +-package main +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- env.EditBuffer("main.go", fake.NewEdit(0, 0, 1, 0, "package main\nfunc main() {\nconst x = `\nfoo\n`\n}")) +- env.Editor.Hover(env.Ctx, env.RegexpSearch("main.go", "foo")) +- }) +-} - --// This file tests that if an interface --// method references an import from its own package --// that the concrete type does not yet import, and that import happens --// to be renamed, then we prefer the renaming of the interface. --var _ other.Interface = &otherInterfaceImpl{} //@suggestedfix("&otherInterfaceImpl", "quickfix", "") +-func TestHoverImport(t *testing.T) { +- const packageDoc1 = "Package lib1 hover documentation" +- const packageDoc2 = "Package lib2 hover documentation" +- tests := []struct { +- hoverPackage string +- want string +- wantError bool +- }{ +- { +- "mod.com/lib1", +- packageDoc1, +- false, +- }, +- { +- "mod.com/lib2", +- packageDoc2, +- false, +- }, +- { +- "mod.com/lib3", +- "", +- false, +- }, +- { +- "mod.com/lib4", +- "", +- true, +- }, +- } +- source := fmt.Sprintf(` +--- go.mod -- +-module mod.com - --type otherInterfaceImpl struct{} -diff -urN a/gopls/internal/lsp/testdata/stub/stub_renamed_import_iface.go.golden b/gopls/internal/lsp/testdata/stub/stub_renamed_import_iface.go.golden ---- a/gopls/internal/lsp/testdata/stub/stub_renamed_import_iface.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_renamed_import_iface.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,22 +0,0 @@ ---- suggestedfix_stub_renamed_import_iface_11_25 -- --package stub +-go 1.12 +--- lib1/a.go -- +-// %s +-package lib1 - --import ( -- "bytes" -- "context" -- "golang.org/lsptests/stub/other" --) +-const C = 1 - --// This file tests that if an interface --// method references an import from its own package --// that the concrete type does not yet import, and that import happens --// to be renamed, then we prefer the renaming of the interface. --var _ other.Interface = &otherInterfaceImpl{} //@suggestedfix("&otherInterfaceImpl", "quickfix", "") +--- lib1/b.go -- +-package lib1 - --type otherInterfaceImpl struct{} +-const D = 1 - --// Get implements other.Interface. --func (*otherInterfaceImpl) Get(context.Context) *bytes.Buffer { -- panic("unimplemented") --} +--- lib2/a.go -- +-// %s +-package lib2 - -diff -urN a/gopls/internal/lsp/testdata/stub/stub_stdlib.go b/gopls/internal/lsp/testdata/stub/stub_stdlib.go ---- a/gopls/internal/lsp/testdata/stub/stub_stdlib.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_stdlib.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,9 +0,0 @@ --package stub +-const E = 1 - --import ( -- "io" --) +--- lib3/a.go -- +-package lib3 - --var _ io.Writer = writer{} //@suggestedfix("w", "quickfix", "") +-const F = 1 - --type writer struct{} -diff -urN a/gopls/internal/lsp/testdata/stub/stub_stdlib.go.golden b/gopls/internal/lsp/testdata/stub/stub_stdlib.go.golden ---- a/gopls/internal/lsp/testdata/stub/stub_stdlib.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_stdlib.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,16 +0,0 @@ ---- suggestedfix_stub_stdlib_7_19 -- --package stub +--- main.go -- +-package main - -import ( -- "io" +- "mod.com/lib1" +- "mod.com/lib2" +- "mod.com/lib3" +- "mod.com/lib4" -) - --var _ io.Writer = writer{} //@suggestedfix("w", "quickfix", "") +-func main() { +- println("Hello") +-} +- `, packageDoc1, packageDoc2) +- Run(t, source, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- for _, test := range tests { +- got, _, err := env.Editor.Hover(env.Ctx, env.RegexpSearch("main.go", test.hoverPackage)) +- if test.wantError { +- if err == nil { +- t.Errorf("Hover(%q) succeeded unexpectedly", test.hoverPackage) +- } +- } else if !strings.Contains(got.Value, test.want) { +- t.Errorf("Hover(%q): got:\n%q\nwant:\n%q", test.hoverPackage, got.Value, test.want) +- } +- } +- }) +-} +- +-// for x/tools/gopls: unhandled named anchor on the hover #57048 +-func TestHoverTags(t *testing.T) { +- const source = ` +--- go.mod -- +-module mod.com - --type writer struct{} +-go 1.19 - --// Write implements io.Writer. --func (writer) Write(p []byte) (n int, err error) { -- panic("unimplemented") --} +--- lib/a.go -- - -diff -urN a/gopls/internal/lsp/testdata/stub/stub_typedecl_group.go b/gopls/internal/lsp/testdata/stub/stub_typedecl_group.go ---- a/gopls/internal/lsp/testdata/stub/stub_typedecl_group.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_typedecl_group.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,27 +0,0 @@ --package stub +-// variety of execution modes. +-// +-// # Test package setup +-// +-// The regression test package uses a couple of uncommon patterns to reduce +-package lib - --// Regression test for Issue #56825: file corrupted by insertion of --// methods after TypeSpec in a parenthesized TypeDecl. +--- a.go -- +- package main +- import "mod.com/lib" - --import "io" +- const A = 1 - --func newReadCloser() io.ReadCloser { -- return rdcloser{} //@suggestedfix("rd", "quickfix", "") +-} +-` +- Run(t, source, func(t *testing.T, env *Env) { +- t.Run("tags", func(t *testing.T) { +- env.OpenFile("a.go") +- z := env.RegexpSearch("a.go", "lib") +- t.Logf("%#v", z) +- got, _ := env.Hover(env.RegexpSearch("a.go", "lib")) +- if strings.Contains(got.Value, "{#hdr-") { +- t.Errorf("Hover: got {#hdr- tag:\n%q", got) +- } +- }) +- }) -} - --type ( -- A int -- rdcloser struct{} -- B int --) -- --func _() { -- // Local types can't be stubbed as there's nowhere to put the methods. -- // The suggestedfix assertion can't express this yet. TODO(adonovan): support it. -- type local struct{} -- var _ io.ReadCloser = local{} // want error: `local type "local" cannot be stubbed` +-// This is a regression test for Go issue #57625. +-func TestHoverModMissingModuleStmt(t *testing.T) { +- const source = ` +--- go.mod -- +-go 1.16 +-` +- Run(t, source, func(t *testing.T, env *Env) { +- env.OpenFile("go.mod") +- env.Hover(env.RegexpSearch("go.mod", "go")) // no panic +- }) -} - --type ( -- C int --) -diff -urN a/gopls/internal/lsp/testdata/stub/stub_typedecl_group.go.golden b/gopls/internal/lsp/testdata/stub/stub_typedecl_group.go.golden ---- a/gopls/internal/lsp/testdata/stub/stub_typedecl_group.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/stub/stub_typedecl_group.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,39 +0,0 @@ ---- suggestedfix_stub_typedecl_group_9_9 -- --package stub +-func TestHoverCompletionMarkdown(t *testing.T) { +- testenv.NeedsGo1Point(t, 19) +- const source = ` +--- go.mod -- +-module mod.com +-go 1.19 +--- main.go -- +-package main +-// Just says [hello]. +-// +-// [hello]: https://en.wikipedia.org/wiki/Hello +-func Hello() string { +- Hello() //Here +- return "hello" +-} +-` +- Run(t, source, func(t *testing.T, env *Env) { +- // Hover, Completion, and SignatureHelp should all produce markdown +- // check that the markdown for SignatureHelp and Completion are +- // the same, and contained in that for Hover (up to trailing \n) +- env.OpenFile("main.go") +- loc := env.RegexpSearch("main.go", "func (Hello)") +- hover, _ := env.Hover(loc) +- hoverContent := hover.Value - --// Regression test for Issue #56825: file corrupted by insertion of --// methods after TypeSpec in a parenthesized TypeDecl. +- loc = env.RegexpSearch("main.go", "//Here") +- loc.Range.Start.Character -= 3 // Hello(_) //Here +- completions := env.Completion(loc) +- signatures := env.SignatureHelp(loc) - --import "io" +- if len(completions.Items) != 1 { +- t.Errorf("got %d completions, expected 1", len(completions.Items)) +- } +- if len(signatures.Signatures) != 1 { +- t.Errorf("got %d signatures, expected 1", len(signatures.Signatures)) +- } +- item := completions.Items[0].Documentation.Value +- var itemContent string +- if x, ok := item.(protocol.MarkupContent); !ok || x.Kind != protocol.Markdown { +- t.Fatalf("%#v is not markdown", item) +- } else { +- itemContent = strings.Trim(x.Value, "\n") +- } +- sig := signatures.Signatures[0].Documentation.Value +- var sigContent string +- if x, ok := sig.(protocol.MarkupContent); !ok || x.Kind != protocol.Markdown { +- t.Fatalf("%#v is not markdown", item) +- } else { +- sigContent = x.Value +- } +- if itemContent != sigContent { +- t.Errorf("item:%q not sig:%q", itemContent, sigContent) +- } +- if !strings.Contains(hoverContent, itemContent) { +- t.Errorf("hover:%q does not containt sig;%q", hoverContent, sigContent) +- } +- }) +-} - --func newReadCloser() io.ReadCloser { -- return rdcloser{} //@suggestedfix("rd", "quickfix", "") +-// Test that the generated markdown contains links for Go references. +-// https://github.com/golang/go/issues/58352 +-func TestHoverLinks(t *testing.T) { +- testenv.NeedsGo1Point(t, 19) +- const input = ` +--- go.mod -- +-go 1.19 +-module mod.com +--- main.go -- +-package main +-// [fmt] +-var A int +-// [fmt.Println] +-var B int +-// [golang.org/x/tools/go/packages.Package.String] +-var C int +-` +- var tests = []struct { +- pat string +- ans string +- }{ +- {"A", "fmt"}, +- {"B", "fmt#Println"}, +- {"C", "golang.org/x/tools/go/packages#Package.String"}, +- } +- for _, test := range tests { +- Run(t, input, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- loc := env.RegexpSearch("main.go", test.pat) +- hover, _ := env.Hover(loc) +- hoverContent := hover.Value +- want := fmt.Sprintf("%s/%s", "https://pkg.go.dev", test.ans) +- if !strings.Contains(hoverContent, want) { +- t.Errorf("hover:%q does not contain link %q", hoverContent, want) +- } +- }) +- } -} - --type ( -- A int -- rdcloser struct{} -- B int +-const linknameHover = ` +--- go.mod -- +-module mod.com +- +--- upper/upper.go -- +-package upper +- +-import ( +- _ "unsafe" +- _ "mod.com/lower" -) - --// Close implements io.ReadCloser. --func (rdcloser) Close() error { -- panic("unimplemented") --} +-//go:linkname foo mod.com/lower.bar +-func foo() string - --// Read implements io.ReadCloser. --func (rdcloser) Read(p []byte) (n int, err error) { -- panic("unimplemented") --} +--- lower/lower.go -- +-package lower - --func _() { -- // Local types can't be stubbed as there's nowhere to put the methods. -- // The suggestedfix assertion can't express this yet. TODO(adonovan): support it. -- type local struct{} -- var _ io.ReadCloser = local{} // want error: `local type "local" cannot be stubbed` --} +-// bar does foo. +-func bar() string { +- return "foo by bar" +-}` - --type ( -- C int --) +-func TestHoverLinknameDirective(t *testing.T) { +- Run(t, linknameHover, func(t *testing.T, env *Env) { +- // Jump from directives 2nd arg. +- env.OpenFile("upper/upper.go") +- from := env.RegexpSearch("upper/upper.go", `lower.bar`) - -diff -urN a/gopls/internal/lsp/testdata/summary.txt.golden b/gopls/internal/lsp/testdata/summary.txt.golden ---- a/gopls/internal/lsp/testdata/summary.txt.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/summary.txt.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,8 +0,0 @@ ---- summary -- --CallHierarchyCount = 2 --SemanticTokenCount = 3 --SuggestedFixCount = 39 --InlayHintsCount = 5 --RenamesCount = 45 --SelectionRangesCount = 3 -- -diff -urN a/gopls/internal/lsp/testdata/typeerrors/noresultvalues.go b/gopls/internal/lsp/testdata/typeerrors/noresultvalues.go ---- a/gopls/internal/lsp/testdata/typeerrors/noresultvalues.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/typeerrors/noresultvalues.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,5 +0,0 @@ --package typeerrors +- hover, _ := env.Hover(from) +- content := hover.Value - --func x() { return nil } //@suggestedfix("nil", "quickfix", "") +- expect := "bar does foo" +- if !strings.Contains(content, expect) { +- t.Errorf("hover: %q does not contain: %q", content, expect) +- } +- }) +-} - --func y() { return nil, "hello" } //@suggestedfix("nil", "quickfix", "") -diff -urN a/gopls/internal/lsp/testdata/typeerrors/noresultvalues.go.golden b/gopls/internal/lsp/testdata/typeerrors/noresultvalues.go.golden ---- a/gopls/internal/lsp/testdata/typeerrors/noresultvalues.go.golden 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/testdata/typeerrors/noresultvalues.go.golden 1970-01-01 00:00:00.000000000 +0000 -@@ -1,14 +0,0 @@ ---- suggestedfix_noresultvalues_3_19 -- --package typeerrors +-func TestHoverGoWork_Issue60821(t *testing.T) { +- const files = ` +--- go.work -- +-go 1.19 - --func x() { return } //@suggestedfix("nil", "quickfix", "") +-use ( +- moda +- modb +-) +--- moda/go.mod -- - --func y() { return nil, "hello" } //@suggestedfix("nil", "quickfix", "") +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("go.work") +- // Neither of the requests below should crash gopls. +- _, _, _ = env.Editor.Hover(env.Ctx, env.RegexpSearch("go.work", "moda")) +- _, _, _ = env.Editor.Hover(env.Ctx, env.RegexpSearch("go.work", "modb")) +- }) +-} - ---- suggestedfix_noresultvalues_5_19 -- --package typeerrors +-const embedHover = ` +--- go.mod -- +-module mod.com +-go 1.19 +--- main.go -- +-package main - --func x() { return nil } //@suggestedfix("nil", "quickfix", "") +-import "embed" - --func y() { return } //@suggestedfix("nil", "quickfix", "") +-//go:embed *.txt +-var foo embed.FS - -diff -urN a/gopls/internal/lsp/tests/compare/text.go b/gopls/internal/lsp/tests/compare/text.go ---- a/gopls/internal/lsp/tests/compare/text.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/tests/compare/text.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,49 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-func main() { +-} +--- foo.txt -- +-FOO +--- bar.txt -- +-BAR +--- baz.txt -- +-BAZ +--- other.sql -- +-SKIPPED +--- dir.txt/skip.txt -- +-SKIPPED +-` - --package compare +-func TestHoverEmbedDirective(t *testing.T) { +- testenv.NeedsGo1Point(t, 19) +- Run(t, embedHover, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- from := env.RegexpSearch("main.go", `\*.txt`) - --import ( -- "bytes" +- got, _ := env.Hover(from) +- if got == nil { +- t.Fatalf("hover over //go:embed arg not found") +- } +- content := got.Value - -- "golang.org/x/tools/internal/diff" --) +- wants := []string{"foo.txt", "bar.txt", "baz.txt"} +- for _, want := range wants { +- if !strings.Contains(content, want) { +- t.Errorf("hover: %q does not contain: %q", content, want) +- } +- } - --// Text returns a formatted unified diff of the edits to go from want to --// got, returning "" if and only if want == got. --// --// This function is intended for use in testing, and panics if any error occurs --// while computing the diff. It is not sufficiently tested for production use. --func Text(want, got string) string { -- return NamedText("want", "got", want, got) +- // A directory should never be matched, even if it happens to have a matching name. +- // Content in subdirectories should not match on only one asterisk. +- skips := []string{"other.sql", "dir.txt", "skip.txt"} +- for _, skip := range skips { +- if strings.Contains(content, skip) { +- t.Errorf("hover: %q should not contain: %q", content, skip) +- } +- } +- }) -} - --// NamedText is like text, but allows passing custom names of the 'want' and --// 'got' content. --func NamedText(wantName, gotName, want, got string) string { -- if want == got { -- return "" -- } +-func TestHoverBrokenImport_Issue60592(t *testing.T) { +- const files = ` +--- go.mod -- +-module testdata +-go 1.18 - -- // Add newlines to avoid verbose newline messages ("No newline at end of file"). -- unified := diff.Unified(wantName, gotName, want+"\n", got+"\n") +--- p.go -- +-package main - -- // Defensively assert that we get an actual diff, so that we guarantee the -- // invariant that we return "" if and only if want == got. -- // -- // This is probably unnecessary, but convenient. -- if unified == "" { -- panic("empty diff for non-identical input") -- } +-import foo "a" - -- return unified +-func _() { +- foo.Print() -} - --// Bytes is like Text but using byte slices. --func Bytes(want, got []byte) string { -- if bytes.Equal(want, got) { -- return "" // common case -- } -- return Text(string(want), string(got)) +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("p.go") +- // This request should not crash gopls. +- _, _, _ = env.Editor.Hover(env.Ctx, env.RegexpSearch("p.go", "foo[.]")) +- }) -} -diff -urN a/gopls/internal/lsp/tests/compare/text_test.go b/gopls/internal/lsp/tests/compare/text_test.go ---- a/gopls/internal/lsp/tests/compare/text_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/tests/compare/text_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,28 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/test/integration/misc/imports_test.go b/gopls/internal/test/integration/misc/imports_test.go +--- a/gopls/internal/test/integration/misc/imports_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/misc/imports_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,293 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package compare_test +-package misc - -import ( +- "os" +- "path/filepath" +- "strings" - "testing" - -- "golang.org/x/tools/gopls/internal/lsp/tests/compare" +- "golang.org/x/tools/gopls/internal/test/compare" +- . "golang.org/x/tools/gopls/internal/test/integration" +- +- "golang.org/x/tools/gopls/internal/protocol" -) - --func TestText(t *testing.T) { -- tests := []struct { -- got, want, wantDiff string -- }{ -- {"", "", ""}, -- {"equal", "equal", ""}, -- {"a", "b", "--- want\n+++ got\n@@ -1 +1 @@\n-b\n+a\n"}, -- {"a\nd\nc\n", "a\nb\nc\n", "--- want\n+++ got\n@@ -1,4 +1,4 @@\n a\n-b\n+d\n c\n \n"}, -- } +-// Tests golang/go#38815. +-func TestIssue38815(t *testing.T) { +- const needs = ` +--- go.mod -- +-module foo - -- for _, test := range tests { -- if gotDiff := compare.Text(test.want, test.got); gotDiff != test.wantDiff { -- t.Errorf("compare.Text(%q, %q) =\n%q, want\n%q", test.want, test.got, gotDiff, test.wantDiff) -- } -- } +-go 1.12 +--- a.go -- +-package main +-func f() {} +-` +- const ntest = `package main +-func TestZ(t *testing.T) { +- f() -} -diff -urN a/gopls/internal/lsp/tests/markdown_go118.go b/gopls/internal/lsp/tests/markdown_go118.go ---- a/gopls/internal/lsp/tests/markdown_go118.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/tests/markdown_go118.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,69 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-` +- const want = `package main - --//go:build !go1.19 --// +build !go1.19 +-import "testing" - --package tests +-func TestZ(t *testing.T) { +- f() +-} +-` - --import ( -- "regexp" -- "strings" +- // it was returning +- // "package main\nimport \"testing\"\npackage main..." +- Run(t, needs, func(t *testing.T, env *Env) { +- env.CreateBuffer("a_test.go", ntest) +- env.SaveBuffer("a_test.go") +- got := env.BufferText("a_test.go") +- if want != got { +- t.Errorf("got\n%q, wanted\n%q", got, want) +- } +- }) +-} - -- "golang.org/x/tools/gopls/internal/lsp/tests/compare" --) -- --// DiffMarkdown compares two markdown strings produced by parsing go doc --// comments. --// --// For go1.19 and later, markdown conversion is done using go/doc/comment. --// Compared to the newer version, the older version has extra escapes, and --// treats code blocks slightly differently. --func DiffMarkdown(want, got string) string { -- want = normalizeMarkdown(want) -- got = normalizeMarkdown(got) -- return compare.Text(want, got) --} -- --// normalizeMarkdown normalizes whitespace and escaping of the input string, to --// eliminate differences between the Go 1.18 and Go 1.19 generated markdown for --// doc comments. Note that it does not normalize to either the 1.18 or 1.19 --// formatting: it simplifies both so that they may be compared. --// --// This function may need to be adjusted as we encounter more differences in --// the generated text. --// --// TODO(rfindley): this function doesn't correctly handle the case of --// multi-line docstrings. --func normalizeMarkdown(input string) string { -- input = strings.TrimSpace(input) -- -- // For simplicity, eliminate blank lines. -- input = regexp.MustCompile("\n+").ReplaceAllString(input, "\n") -- -- // Replace common escaped characters with their unescaped version. -- // -- // This list may not be exhaustive: it was just sufficient to make tests -- // pass. -- input = strings.NewReplacer( -- `\\`, ``, -- `\@`, `@`, -- `\(`, `(`, -- `\)`, `)`, -- `\{`, `{`, -- `\}`, `}`, -- `\"`, `"`, -- `\.`, `.`, -- `\-`, `-`, -- `\'`, `'`, -- `\+`, `+`, -- `\~`, `~`, -- `\=`, `=`, -- `\:`, `:`, -- `\?`, `?`, -- `\n\n\n`, `\n\n`, // Note that these are *escaped* newlines. -- ).Replace(input) -- -- return input --} -diff -urN a/gopls/internal/lsp/tests/markdown_go119.go b/gopls/internal/lsp/tests/markdown_go119.go ---- a/gopls/internal/lsp/tests/markdown_go119.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/tests/markdown_go119.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,22 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-func TestIssue59124(t *testing.T) { +- const stuff = ` +--- go.mod -- +-module foo +-go 1.19 +--- a.go -- +-//line foo.y:102 +-package main - --//go:build go1.19 --// +build go1.19 +-import "fmt" - --package tests +-//this comment is necessary for failure +-func a() { +- fmt.Println("hello") +-} +-` +- Run(t, stuff, func(t *testing.T, env *Env) { +- env.OpenFile("a.go") +- was := env.BufferText("a.go") +- env.Await(NoDiagnostics()) +- env.OrganizeImports("a.go") +- is := env.BufferText("a.go") +- if diff := compare.Text(was, is); diff != "" { +- t.Errorf("unexpected diff after organizeImports:\n%s", diff) +- } +- }) +-} - --import ( -- "golang.org/x/tools/gopls/internal/lsp/tests/compare" --) +-func TestVim1(t *testing.T) { +- const vim1 = `package main - --// DiffMarkdown compares two markdown strings produced by parsing go doc --// comments. --// --// For go1.19 and later, markdown conversion is done using go/doc/comment. --// Compared to the newer version, the older version has extra escapes, and --// treats code blocks slightly differently. --func DiffMarkdown(want, got string) string { -- return compare.Text(want, got) +-import "fmt" +- +-var foo = 1 +-var bar = 2 +- +-func main() { +- fmt.Printf("This is a test %v\n", foo) +- fmt.Printf("This is another test %v\n", foo) +- fmt.Printf("This is also a test %v\n", foo) -} -diff -urN a/gopls/internal/lsp/tests/README.md b/gopls/internal/lsp/tests/README.md ---- a/gopls/internal/lsp/tests/README.md 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/tests/README.md 1970-01-01 00:00:00.000000000 +0000 -@@ -1,66 +0,0 @@ --# Testing +-` +- +- // The file remains unchanged, but if there any quick fixes +- // are returned, they confuse vim (according to CL 233117). +- // Therefore check for no QuickFix CodeActions. +- Run(t, "", func(t *testing.T, env *Env) { +- env.CreateBuffer("main.go", vim1) +- env.OrganizeImports("main.go") - --LSP has "marker tests" defined in `internal/lsp/testdata`, as well as --traditional tests. +- // Assert no quick fixes. +- for _, act := range env.CodeAction("main.go", nil) { +- if act.Kind == protocol.QuickFix { +- t.Errorf("unexpected quick fix action: %#v", act) +- } +- } +- if t.Failed() { +- got := env.BufferText("main.go") +- if got == vim1 { +- t.Errorf("no changes") +- } else { +- t.Errorf("got\n%q", got) +- t.Errorf("was\n%q", vim1) +- } +- } +- }) +-} - --## Marker tests +-func TestVim2(t *testing.T) { +- const vim2 = `package main - --Marker tests have a standard input file, like --`internal/lsp/testdata/foo/bar.go`, and some may have a corresponding golden --file, like `internal/lsp/testdata/foo/bar.go.golden`. The former is the "input" --and the latter is the expected output. +-import ( +- "fmt" - --Each input file contains annotations like --`//@suggestedfix("}", "refactor.rewrite", "Fill anonymous struct")`. These annotations are interpreted by --test runners to perform certain actions. The expected output after those actions --is encoded in the golden file. +- "example.com/blah" - --When tests are run, each annotation results in a new subtest, which is encoded --in the golden file with a heading like, +- "rubbish.com/useless" +-) - --```bash ---- suggestedfix_bar_11_21 -- --// expected contents go here ---- suggestedfix_bar_13_20 -- --// expected contents go here --``` +-func main() { +- fmt.Println(blah.Name, useless.Name) +-} +-` - --The format of these headings vary: they are defined by the --[`Golden`](https://pkg.go.dev/golang.org/x/tools/gopls/internal/lsp/tests#Data.Golden) --function for each annotation. In the case above, the format is: annotation --name, file name, annotation line location, annotation character location. +- Run(t, "", func(t *testing.T, env *Env) { +- env.CreateBuffer("main.go", vim2) +- env.OrganizeImports("main.go") - --So, if `internal/lsp/testdata/foo/bar.go` has three `suggestedfix` annotations, --the golden file should have three headers with `suggestedfix_bar_xx_yy` --headings. +- // Assert no quick fixes. +- for _, act := range env.CodeAction("main.go", nil) { +- if act.Kind == protocol.QuickFix { +- t.Errorf("unexpected quick-fix action: %#v", act) +- } +- } +- }) +-} - --To see a list of all available annotations, see the exported "expectations" in --[tests.go](https://github.com/golang/tools/blob/299f270db45902e93469b1152fafed034bb3f033/internal/lsp/tests/tests.go#L418-L447). +-func TestGOMODCACHE(t *testing.T) { +- const proxy = ` +--- example.com@v1.2.3/go.mod -- +-module example.com - --To run marker tests, +-go 1.12 +--- example.com@v1.2.3/x/x.go -- +-package x - --```bash --cd /path/to/tools +-const X = 1 +--- example.com@v1.2.3/y/y.go -- +-package y - --# The marker tests are located in "internal/lsp", "internal/lsp/cmd, and --# "internal/lsp/source". --go test ./internal/lsp/... --``` +-const Y = 2 +-` +- const files = ` +--- go.mod -- +-module mod.com - --There are quite a lot of marker tests, so to run one individually, pass the test --path and heading into a -run argument: +-go 1.12 - --```bash --cd /path/to/tools --go test ./internal/lsp/... -v -run TestLSP/Modules/SuggestedFix/bar_11_21 --``` +-require example.com v1.2.3 +--- go.sum -- +-example.com v1.2.3 h1:6vTQqzX+pnwngZF1+5gcO3ZEWmix1jJ/h+pWS8wUxK0= +-example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= +--- main.go -- +-package main - --## Resetting marker tests +-import "example.com/x" - --Sometimes, a change is made to lsp that requires a change to multiple golden --files. When this happens, you can run, +-var _, _ = x.X, y.Y +-` +- modcache, err := os.MkdirTemp("", "TestGOMODCACHE-modcache") +- if err != nil { +- t.Fatal(err) +- } +- defer os.RemoveAll(modcache) +- WithOptions( +- EnvVars{"GOMODCACHE": modcache}, +- ProxyFiles(proxy), +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- env.AfterChange(Diagnostics(env.AtRegexp("main.go", `y.Y`))) +- env.SaveBuffer("main.go") +- env.AfterChange(NoDiagnostics(ForFile("main.go"))) +- loc := env.GoToDefinition(env.RegexpSearch("main.go", `y.(Y)`)) +- path := env.Sandbox.Workdir.URIToPath(loc.URI) +- if !strings.HasPrefix(path, filepath.ToSlash(modcache)) { +- t.Errorf("found module dependency outside of GOMODCACHE: got %v, wanted subdir of %v", path, filepath.ToSlash(modcache)) +- } +- }) +-} - --```bash --cd /path/to/tools --./internal/lsp/reset_golden.sh --``` -diff -urN a/gopls/internal/lsp/tests/tests.go b/gopls/internal/lsp/tests/tests.go ---- a/gopls/internal/lsp/tests/tests.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/tests/tests.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,645 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-// Tests golang/go#40685. +-func TestAcceptImportsQuickFixTestVariant(t *testing.T) { +- const pkg = ` +--- go.mod -- +-module mod.com - --// Package tests exports functionality to be used across a variety of gopls tests. --package tests +-go 1.12 +--- a/a.go -- +-package a - -import ( -- "bytes" -- "context" -- "flag" - "fmt" -- "go/ast" -- "go/token" -- "io" +-) +- +-func _() { +- fmt.Println("") +- os.Stat("") +-} +--- a/a_test.go -- +-package a +- +-import ( - "os" -- "path/filepath" -- "sort" -- "strings" -- "sync" - "testing" -- "time" -- -- "golang.org/x/tools/go/packages" -- "golang.org/x/tools/go/packages/packagestest" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/lsp/tests/compare" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/typeparams" -- "golang.org/x/tools/txtar" -) - --const ( -- overlayFileSuffix = ".overlay" -- goldenFileSuffix = ".golden" -- inFileSuffix = ".in" -- summaryFile = "summary.txt" +-func TestA(t *testing.T) { +- os.Stat("") +-} +-` +- Run(t, pkg, func(t *testing.T, env *Env) { +- env.OpenFile("a/a.go") +- var d protocol.PublishDiagnosticsParams +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/a.go", "os.Stat")), +- ReadDiagnostics("a/a.go", &d), +- ) +- env.ApplyQuickFixes("a/a.go", d.Diagnostics) +- env.AfterChange( +- NoDiagnostics(ForFile("a/a.go")), +- ) +- }) +-} - -- // The module path containing the testdata packages. -- // -- // Warning: the length of this module path matters, as we have bumped up -- // against command-line limitations on windows (golang/go#54800). -- testModule = "golang.org/lsptests" +-// Test for golang/go#52784 +-func TestGoWorkImports(t *testing.T) { +- const pkg = ` +--- go.work -- +-go 1.19 +- +-use ( +- ./caller +- ./mod -) +--- caller/go.mod -- +-module caller.com - --var UpdateGolden = flag.Bool("golden", false, "Update golden files") +-go 1.18 - --// These type names apparently avoid the need to repeat the --// type in the field name and the make() expression. --type CallHierarchy = map[span.Span]*CallHierarchyResult --type SemanticTokens = []span.Span --type SuggestedFixes = map[span.Span][]SuggestedFix --type Renames = map[span.Span]string --type InlayHints = []span.Span --type AddImport = map[span.URI]string --type SelectionRanges = []span.Span +-require mod.com v0.0.0 - --type Data struct { -- Config packages.Config -- Exported *packagestest.Exported -- CallHierarchy CallHierarchy -- SemanticTokens SemanticTokens -- SuggestedFixes SuggestedFixes -- Renames Renames -- InlayHints InlayHints -- AddImport AddImport -- SelectionRanges SelectionRanges +-replace mod.com => ../mod +--- caller/caller.go -- +-package main - -- fragments map[string]string -- dir string -- golden map[string]*Golden -- mode string +-func main() { +- a.Test() +-} +--- mod/go.mod -- +-module mod.com - -- ModfileFlagAvailable bool +-go 1.18 +--- mod/a/a.go -- +-package a - -- mappersMu sync.Mutex -- mappers map[span.URI]*protocol.Mapper +-func Test() { -} +-` +- Run(t, pkg, func(t *testing.T, env *Env) { +- env.OpenFile("caller/caller.go") +- env.AfterChange(Diagnostics(env.AtRegexp("caller/caller.go", "a.Test"))) - --// The Tests interface abstracts the LSP-based implementation of the marker --// test operators appearing in files beneath ../testdata/. --// --// TODO(adonovan): reduce duplication; see https://github.com/golang/go/issues/54845. --// There is only one implementation (*runner in ../lsp_test.go), so --// we can abolish the interface now. --type Tests interface { -- CallHierarchy(*testing.T, span.Span, *CallHierarchyResult) -- SemanticTokens(*testing.T, span.Span) -- SuggestedFix(*testing.T, span.Span, []SuggestedFix, int) -- InlayHints(*testing.T, span.Span) -- Rename(*testing.T, span.Span, string) -- AddImport(*testing.T, span.URI, string) -- SelectionRanges(*testing.T, span.Span) +- // Saving caller.go should trigger goimports, which should find a.Test in +- // the mod.com module, thanks to the go.work file. +- env.SaveBuffer("caller/caller.go") +- env.AfterChange(NoDiagnostics(ForFile("caller/caller.go"))) +- }) -} +diff -urN a/gopls/internal/test/integration/misc/import_test.go b/gopls/internal/test/integration/misc/import_test.go +--- a/gopls/internal/test/integration/misc/import_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/misc/import_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,133 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --type Completion struct { -- CompletionItems []token.Pos --} +-package misc - --type CompletionSnippet struct { -- CompletionItem token.Pos -- PlainSnippet string -- PlaceholderSnippet string --} +-import ( +- "testing" - --type CallHierarchyResult struct { -- IncomingCalls, OutgoingCalls []protocol.CallHierarchyItem --} +- "github.com/google/go-cmp/cmp" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/command" +- "golang.org/x/tools/gopls/internal/test/compare" +- . "golang.org/x/tools/gopls/internal/test/integration" +-) - --type Link struct { -- Src span.Span -- Target string -- NotePosition token.Position --} +-func TestAddImport(t *testing.T) { +- const before = `package main - --type SuggestedFix struct { -- ActionKind, Title string --} +-import "fmt" - --type Golden struct { -- Filename string -- Archive *txtar.Archive -- Modified bool +-func main() { +- fmt.Println("hello world") -} +-` - --func Context(t testing.TB) context.Context { -- return context.Background() --} +- const want = `package main - --func DefaultOptions(o *source.Options) { -- o.SupportedCodeActions = map[source.FileKind]map[protocol.CodeActionKind]bool{ -- source.Go: { -- protocol.SourceOrganizeImports: true, -- protocol.QuickFix: true, -- protocol.RefactorRewrite: true, -- protocol.RefactorInline: true, -- protocol.RefactorExtract: true, -- protocol.SourceFixAll: true, -- }, -- source.Mod: { -- protocol.SourceOrganizeImports: true, -- }, -- source.Sum: {}, -- source.Work: {}, -- source.Tmpl: {}, -- } -- o.InsertTextFormat = protocol.SnippetTextFormat -- o.CompletionBudget = time.Minute -- o.HierarchicalDocumentSymbolSupport = true -- o.SemanticTokens = true -- o.InternalOptions.NewDiff = "new" +-import ( +- "bytes" +- "fmt" +-) - -- // Enable all inlay hints. -- if o.Hints == nil { -- o.Hints = make(map[string]bool) -- } -- for name := range source.AllInlayHints { -- o.Hints[name] = true -- } +-func main() { +- fmt.Println("hello world") -} +-` - --func RunTests(t *testing.T, dataDir string, includeMultiModule bool, f func(*testing.T, *Data)) { -- t.Helper() -- modes := []string{"Modules", "GOPATH"} -- if includeMultiModule { -- modes = append(modes, "MultiModule") -- } -- for _, mode := range modes { -- t.Run(mode, func(t *testing.T) { -- datum := load(t, mode, dataDir) -- t.Helper() -- f(t, datum) +- Run(t, "", func(t *testing.T, env *Env) { +- env.CreateBuffer("main.go", before) +- cmd, err := command.NewAddImportCommand("Add Import", command.AddImportArgs{ +- URI: env.Sandbox.Workdir.URI("main.go"), +- ImportPath: "bytes", - }) -- } +- if err != nil { +- t.Fatal(err) +- } +- env.ExecuteCommand(&protocol.ExecuteCommandParams{ +- Command: "gopls.add_import", +- Arguments: cmd.Arguments, +- }, nil) +- got := env.BufferText("main.go") +- if got != want { +- t.Fatalf("gopls.add_import failed\n%s", compare.Text(want, got)) +- } +- }) -} - --func load(t testing.TB, mode string, dir string) *Data { -- datum := &Data{ -- CallHierarchy: make(CallHierarchy), -- Renames: make(Renames), -- SuggestedFixes: make(SuggestedFixes), -- AddImport: make(AddImport), +-func TestListImports(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - -- dir: dir, -- fragments: map[string]string{}, -- golden: map[string]*Golden{}, -- mode: mode, -- mappers: map[span.URI]*protocol.Mapper{}, -- } +-go 1.12 +--- foo.go -- +-package foo +-const C = 1 +--- import_strings_test.go -- +-package foo +-import ( +- x "strings" +- "testing" +-) - -- if !*UpdateGolden { -- summary := filepath.Join(filepath.FromSlash(dir), summaryFile+goldenFileSuffix) -- if _, err := os.Stat(summary); os.IsNotExist(err) { -- t.Fatalf("could not find golden file summary.txt in %#v", dir) -- } -- archive, err := txtar.ParseFile(summary) -- if err != nil { -- t.Fatalf("could not read golden file %v/%v: %v", dir, summary, err) -- } -- datum.golden[summaryFile] = &Golden{ -- Filename: summary, -- Archive: archive, -- } +-func TestFoo(t *testing.T) {} +--- import_testing_test.go -- +-package foo +- +-import "testing" +- +-func TestFoo2(t *testing.T) {} +-` +- tests := []struct { +- filename string +- want command.ListImportsResult +- }{ +- { +- filename: "import_strings_test.go", +- want: command.ListImportsResult{ +- Imports: []command.FileImport{ +- {Name: "x", Path: "strings"}, +- {Path: "testing"}, +- }, +- PackageImports: []command.PackageImport{ +- {Path: "strings"}, +- {Path: "testing"}, +- }, +- }, +- }, +- { +- filename: "import_testing_test.go", +- want: command.ListImportsResult{ +- Imports: []command.FileImport{ +- {Path: "testing"}, +- }, +- PackageImports: []command.PackageImport{ +- {Path: "strings"}, +- {Path: "testing"}, +- }, +- }, +- }, - } - -- files := packagestest.MustCopyFileTree(dir) -- // Prune test cases that exercise generics. -- if !typeparams.Enabled { -- for name := range files { -- if strings.Contains(name, "_generics") { -- delete(files, name) -- } -- } -- } -- overlays := map[string][]byte{} -- for fragment, operation := range files { -- if trimmed := strings.TrimSuffix(fragment, goldenFileSuffix); trimmed != fragment { -- delete(files, fragment) -- goldFile := filepath.Join(dir, fragment) -- archive, err := txtar.ParseFile(goldFile) -- if err != nil { -- t.Fatalf("could not read golden file %v: %v", fragment, err) -- } -- datum.golden[trimmed] = &Golden{ -- Filename: goldFile, -- Archive: archive, -- } -- } else if trimmed := strings.TrimSuffix(fragment, inFileSuffix); trimmed != fragment { -- delete(files, fragment) -- files[trimmed] = operation -- } else if index := strings.Index(fragment, overlayFileSuffix); index >= 0 { -- delete(files, fragment) -- partial := fragment[:index] + fragment[index+len(overlayFileSuffix):] -- contents, err := os.ReadFile(filepath.Join(dir, fragment)) +- Run(t, files, func(t *testing.T, env *Env) { +- for _, tt := range tests { +- cmd, err := command.NewListImportsCommand("List Imports", command.URIArg{ +- URI: env.Sandbox.Workdir.URI(tt.filename), +- }) - if err != nil { - t.Fatal(err) - } -- overlays[partial] = contents -- } -- } -- -- modules := []packagestest.Module{ -- { -- Name: testModule, -- Files: files, -- Overlay: overlays, -- }, -- } -- switch mode { -- case "Modules": -- datum.Exported = packagestest.Export(t, packagestest.Modules, modules) -- case "GOPATH": -- datum.Exported = packagestest.Export(t, packagestest.GOPATH, modules) -- case "MultiModule": -- files := map[string]interface{}{} -- for k, v := range modules[0].Files { -- files[filepath.Join("testmodule", k)] = v -- } -- modules[0].Files = files -- -- overlays := map[string][]byte{} -- for k, v := range modules[0].Overlay { -- overlays[filepath.Join("testmodule", k)] = v -- } -- modules[0].Overlay = overlays -- -- golden := map[string]*Golden{} -- for k, v := range datum.golden { -- if k == summaryFile { -- golden[k] = v -- } else { -- golden[filepath.Join("testmodule", k)] = v +- var result command.ListImportsResult +- env.ExecuteCommand(&protocol.ExecuteCommandParams{ +- Command: command.ListImports.ID(), +- Arguments: cmd.Arguments, +- }, &result) +- if diff := cmp.Diff(tt.want, result); diff != "" { +- t.Errorf("unexpected list imports result for %q (-want +got):\n%s", tt.filename, diff) - } - } -- datum.golden = golden -- -- datum.Exported = packagestest.Export(t, packagestest.Modules, modules) -- default: -- panic("unknown mode " + mode) -- } - -- for _, m := range modules { -- for fragment := range m.Files { -- filename := datum.Exported.File(m.Name, fragment) -- datum.fragments[filename] = fragment -- } -- } +- }) +-} +diff -urN a/gopls/internal/test/integration/misc/link_test.go b/gopls/internal/test/integration/misc/link_test.go +--- a/gopls/internal/test/integration/misc/link_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/misc/link_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,94 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- // Turn off go/packages debug logging. -- datum.Exported.Config.Logf = nil -- datum.Config.Logf = nil +-package misc - -- // Merge the exported.Config with the view.Config. -- datum.Config = *datum.Exported.Config -- datum.Config.Fset = token.NewFileSet() -- datum.Config.Context = Context(nil) -- datum.Config.ParseFile = func(fset *token.FileSet, filename string, src []byte) (*ast.File, error) { -- panic("ParseFile should not be called") -- } +-import ( +- "strings" +- "testing" - -- // Do a first pass to collect special markers for completion and workspace symbols. -- if err := datum.Exported.Expect(map[string]interface{}{ -- "item": func(name string, r packagestest.Range, _ []string) { -- datum.Exported.Mark(name, r) -- }, -- "symbol": func(name string, r packagestest.Range, _ []string) { -- datum.Exported.Mark(name, r) -- }, -- }); err != nil { -- t.Fatal(err) -- } +- . "golang.org/x/tools/gopls/internal/test/integration" +-) - -- // Collect any data that needs to be used by subsequent tests. -- if err := datum.Exported.Expect(map[string]interface{}{ -- "semantic": datum.collectSemanticTokens, -- "inlayHint": datum.collectInlayHints, -- "rename": datum.collectRenames, -- "suggestedfix": datum.collectSuggestedFixes, -- "incomingcalls": datum.collectIncomingCalls, -- "outgoingcalls": datum.collectOutgoingCalls, -- "addimport": datum.collectAddImports, -- "selectionrange": datum.collectSelectionRanges, -- }); err != nil { -- t.Fatal(err) -- } +-func TestHoverAndDocumentLink(t *testing.T) { +- const program = ` +--- go.mod -- +-module mod.test - -- if mode == "MultiModule" { -- if err := moveFile(filepath.Join(datum.Config.Dir, "go.mod"), filepath.Join(datum.Config.Dir, "testmodule/go.mod")); err != nil { -- t.Fatal(err) -- } -- } +-go 1.12 - -- return datum --} +-require import.test v1.2.3 +--- main.go -- +-package main - --// moveFile moves the file at oldpath to newpath, by renaming if possible --// or copying otherwise. --func moveFile(oldpath, newpath string) (err error) { -- renameErr := os.Rename(oldpath, newpath) -- if renameErr == nil { -- return nil -- } +-import "import.test/pkg" - -- src, err := os.Open(oldpath) -- if err != nil { -- return err -- } -- defer func() { -- src.Close() -- if err == nil { -- err = os.Remove(oldpath) -- } -- }() +-func main() { +- // Issue 43990: this is not a link that most users can open from an LSP +- // client: mongodb://not.a.link.com +- println(pkg.Hello) +-}` - -- perm := os.ModePerm -- fi, err := src.Stat() -- if err == nil { -- perm = fi.Mode().Perm() -- } +- const proxy = ` +--- import.test@v1.2.3/go.mod -- +-module import.test - -- dst, err := os.OpenFile(newpath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, perm) -- if err != nil { -- return err -- } +-go 1.12 +--- import.test@v1.2.3/pkg/const.go -- +-package pkg - -- _, err = io.Copy(dst, src) -- if closeErr := dst.Close(); err == nil { -- err = closeErr -- } -- return err --} +-const Hello = "Hello" +-` +- WithOptions( +- ProxyFiles(proxy), +- WriteGoSum("."), +- ).Run(t, program, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- env.OpenFile("go.mod") - --func Run(t *testing.T, tests Tests, data *Data) { -- t.Helper() -- checkData(t, data) +- modLink := "https://pkg.go.dev/mod/import.test@v1.2.3" +- pkgLink := "https://pkg.go.dev/import.test@v1.2.3/pkg" - -- t.Run("CallHierarchy", func(t *testing.T) { -- t.Helper() -- for spn, callHierarchyResult := range data.CallHierarchy { -- t.Run(SpanName(spn), func(t *testing.T) { -- t.Helper() -- tests.CallHierarchy(t, spn, callHierarchyResult) -- }) +- // First, check that we get the expected links via hover and documentLink. +- content, _ := env.Hover(env.RegexpSearch("main.go", "pkg.Hello")) +- if content == nil || !strings.Contains(content.Value, pkgLink) { +- t.Errorf("hover: got %v in main.go, want contains %q", content, pkgLink) - } -- }) -- -- t.Run("SemanticTokens", func(t *testing.T) { -- t.Helper() -- for _, spn := range data.SemanticTokens { -- t.Run(uriName(spn.URI()), func(t *testing.T) { -- t.Helper() -- tests.SemanticTokens(t, spn) -- }) +- content, _ = env.Hover(env.RegexpSearch("go.mod", "import.test")) +- if content == nil || !strings.Contains(content.Value, pkgLink) { +- t.Errorf("hover: got %v in go.mod, want contains %q", content, pkgLink) - } -- }) -- -- t.Run("SuggestedFix", func(t *testing.T) { -- t.Helper() -- for spn, actionKinds := range data.SuggestedFixes { -- // Check if we should skip this spn if the -modfile flag is not available. -- if shouldSkip(data, spn.URI()) { -- continue -- } -- t.Run(SpanName(spn), func(t *testing.T) { -- t.Helper() -- tests.SuggestedFix(t, spn, actionKinds, 1) -- }) +- links := env.DocumentLink("main.go") +- if len(links) != 1 || *links[0].Target != pkgLink { +- t.Errorf("documentLink: got links %+v for main.go, want one link with target %q", links, pkgLink) - } -- }) -- -- t.Run("InlayHints", func(t *testing.T) { -- t.Helper() -- for _, src := range data.InlayHints { -- t.Run(SpanName(src), func(t *testing.T) { -- t.Helper() -- tests.InlayHints(t, src) -- }) +- links = env.DocumentLink("go.mod") +- if len(links) != 1 || *links[0].Target != modLink { +- t.Errorf("documentLink: got links %+v for go.mod, want one link with target %q", links, modLink) - } -- }) - -- t.Run("Renames", func(t *testing.T) { -- t.Helper() -- for spn, newText := range data.Renames { -- t.Run(uriName(spn.URI())+"_"+newText, func(t *testing.T) { -- t.Helper() -- tests.Rename(t, spn, newText) -- }) -- } -- }) +- // Then change the environment to make these links private. +- cfg := env.Editor.Config() +- cfg.Env = map[string]string{"GOPRIVATE": "import.test"} +- env.ChangeConfiguration(cfg) - -- t.Run("AddImport", func(t *testing.T) { -- t.Helper() -- for uri, exp := range data.AddImport { -- t.Run(uriName(uri), func(t *testing.T) { -- tests.AddImport(t, uri, exp) -- }) +- // Finally, verify that the links are gone. +- content, _ = env.Hover(env.RegexpSearch("main.go", "pkg.Hello")) +- if content == nil || strings.Contains(content.Value, pkgLink) { +- t.Errorf("hover: got %v in main.go, want non-empty hover without %q", content, pkgLink) - } -- }) -- -- t.Run("SelectionRanges", func(t *testing.T) { -- t.Helper() -- for _, span := range data.SelectionRanges { -- t.Run(SpanName(span), func(t *testing.T) { -- tests.SelectionRanges(t, span) -- }) +- content, _ = env.Hover(env.RegexpSearch("go.mod", "import.test")) +- if content == nil || strings.Contains(content.Value, modLink) { +- t.Errorf("hover: got %v in go.mod, want contains %q", content, modLink) - } -- }) -- -- if *UpdateGolden { -- for _, golden := range data.golden { -- if !golden.Modified { -- continue -- } -- sort.Slice(golden.Archive.Files, func(i, j int) bool { -- return golden.Archive.Files[i].Name < golden.Archive.Files[j].Name -- }) -- if err := os.WriteFile(golden.Filename, txtar.Format(golden.Archive), 0666); err != nil { -- t.Fatal(err) -- } +- links = env.DocumentLink("main.go") +- if len(links) != 0 { +- t.Errorf("documentLink: got %d document links for main.go, want 0\nlinks: %v", len(links), links) - } -- } --} -- --func checkData(t *testing.T, data *Data) { -- buf := &bytes.Buffer{} -- -- fmt.Fprintf(buf, "CallHierarchyCount = %v\n", len(data.CallHierarchy)) -- fmt.Fprintf(buf, "SemanticTokenCount = %v\n", len(data.SemanticTokens)) -- fmt.Fprintf(buf, "SuggestedFixCount = %v\n", len(data.SuggestedFixes)) -- fmt.Fprintf(buf, "InlayHintsCount = %v\n", len(data.InlayHints)) -- fmt.Fprintf(buf, "RenamesCount = %v\n", len(data.Renames)) -- fmt.Fprintf(buf, "SelectionRangesCount = %v\n", len(data.SelectionRanges)) -- -- want := string(data.Golden(t, "summary", summaryFile, func() ([]byte, error) { -- return buf.Bytes(), nil -- })) -- got := buf.String() -- if want != got { -- // These counters change when assertions are added or removed. -- // They act as an independent safety net to ensure that the -- // tests didn't spuriously pass because they did no work. -- t.Errorf("test summary does not match:\n%s\n(Run with -golden to update golden file; also, there may be one per Go version.)", compare.Text(want, got)) -- } --} -- --func (data *Data) Mapper(uri span.URI) (*protocol.Mapper, error) { -- data.mappersMu.Lock() -- defer data.mappersMu.Unlock() -- -- if _, ok := data.mappers[uri]; !ok { -- content, err := data.Exported.FileContents(uri.Filename()) -- if err != nil { -- return nil, err +- links = env.DocumentLink("go.mod") +- if len(links) != 0 { +- t.Errorf("documentLink: got %d document links for go.mod, want 0\nlinks: %v", len(links), links) - } -- data.mappers[uri] = protocol.NewMapper(uri, content) -- } -- return data.mappers[uri], nil +- }) -} +diff -urN a/gopls/internal/test/integration/misc/misc_test.go b/gopls/internal/test/integration/misc/misc_test.go +--- a/gopls/internal/test/integration/misc/misc_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/misc/misc_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,65 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func (data *Data) Golden(t *testing.T, tag, target string, update func() ([]byte, error)) []byte { -- t.Helper() -- fragment, found := data.fragments[target] -- if !found { -- if filepath.IsAbs(target) { -- t.Fatalf("invalid golden file fragment %v", target) -- } -- fragment = target -- } -- golden := data.golden[fragment] -- if golden == nil { -- if !*UpdateGolden { -- t.Fatalf("could not find golden file %v: %v", fragment, tag) -- } -- golden = &Golden{ -- Filename: filepath.Join(data.dir, fragment+goldenFileSuffix), -- Archive: &txtar.Archive{}, -- Modified: true, -- } -- data.golden[fragment] = golden -- } -- var file *txtar.File -- for i := range golden.Archive.Files { -- f := &golden.Archive.Files[i] -- if f.Name == tag { -- file = f -- break -- } -- } -- if *UpdateGolden { -- if file == nil { -- golden.Archive.Files = append(golden.Archive.Files, txtar.File{ -- Name: tag, -- }) -- file = &golden.Archive.Files[len(golden.Archive.Files)-1] -- } -- contents, err := update() -- if err != nil { -- t.Fatalf("could not update golden file %v: %v", fragment, err) -- } -- file.Data = append(contents, '\n') // add trailing \n for txtar -- golden.Modified = true +-package misc - -- } -- if file == nil { -- t.Fatalf("could not find golden contents %v: %v", fragment, tag) -- } -- if len(file.Data) == 0 { -- return file.Data -- } -- return file.Data[:len(file.Data)-1] // drop the trailing \n --} +-import ( +- "strings" +- "testing" - --func (data *Data) collectAddImports(spn span.Span, imp string) { -- data.AddImport[spn.URI()] = imp --} +- "golang.org/x/tools/gopls/internal/hooks" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/test/integration" +- . "golang.org/x/tools/gopls/internal/test/integration" +- "golang.org/x/tools/gopls/internal/util/bug" +-) - --func (data *Data) collectSemanticTokens(spn span.Span) { -- data.SemanticTokens = append(data.SemanticTokens, spn) +-func TestMain(m *testing.M) { +- bug.PanicOnBugs = true +- integration.Main(m, hooks.Options) -} - --func (data *Data) collectSuggestedFixes(spn span.Span, actionKind, fix string) { -- data.SuggestedFixes[spn] = append(data.SuggestedFixes[spn], SuggestedFix{actionKind, fix}) --} +-// TestDocumentURIFix ensures that a DocumentURI supplied by the +-// client is subject to the "fixing" operation documented at +-// [protocol.DocumentURI.UnmarshalText]. The details of the fixing are +-// tested in the protocol package; here we aim to test only that it +-// occurs at all. +-func TestDocumentURIFix(t *testing.T) { +- const mod = ` +--- go.mod -- +-module testdata +-go 1.18 - --func (data *Data) collectSelectionRanges(spn span.Span) { -- data.SelectionRanges = append(data.SelectionRanges, spn) --} +--- a.go -- +-package a - --func (data *Data) collectIncomingCalls(src span.Span, calls []span.Span) { -- for _, call := range calls { -- rng := data.mustRange(call) -- // we're only comparing protocol.range -- if data.CallHierarchy[src] != nil { -- data.CallHierarchy[src].IncomingCalls = append(data.CallHierarchy[src].IncomingCalls, -- protocol.CallHierarchyItem{ -- URI: protocol.DocumentURI(call.URI()), -- Range: rng, -- }) -- } else { -- data.CallHierarchy[src] = &CallHierarchyResult{ -- IncomingCalls: []protocol.CallHierarchyItem{ -- {URI: protocol.DocumentURI(call.URI()), Range: rng}, -- }, +-const K = 1 +-` +- Run(t, mod, func(t *testing.T, env *Env) { +- env.OpenFile("a.go") +- loc := env.RegexpSearch("a.go", "K") +- path := strings.TrimPrefix(string(loc.URI), "file://") // (absolute) +- +- check := func() { +- t.Helper() +- t.Logf("URI = %s", loc.URI) +- content, _ := env.Hover(loc) // must succeed +- if content == nil || !strings.Contains(content.Value, "const K") { +- t.Errorf("wrong content: %#v", content) - } - } -- } --} -- --func (data *Data) collectOutgoingCalls(src span.Span, calls []span.Span) { -- if data.CallHierarchy[src] == nil { -- data.CallHierarchy[src] = &CallHierarchyResult{} -- } -- for _, call := range calls { -- // we're only comparing protocol.range -- data.CallHierarchy[src].OutgoingCalls = append(data.CallHierarchy[src].OutgoingCalls, -- protocol.CallHierarchyItem{ -- URI: protocol.DocumentURI(call.URI()), -- Range: data.mustRange(call), -- }) -- } --} - --func (data *Data) collectInlayHints(src span.Span) { -- data.InlayHints = append(data.InlayHints, src) --} -- --func (data *Data) collectRenames(src span.Span, newText string) { -- data.Renames[src] = newText --} -- --// mustRange converts spn into a protocol.Range, panicking on any error. --func (data *Data) mustRange(spn span.Span) protocol.Range { -- m, err := data.Mapper(spn.URI()) -- rng, err := m.SpanRange(spn) -- if err != nil { -- panic(fmt.Sprintf("converting span %s to range: %v", spn, err)) -- } -- return rng --} +- // Regular URI (e.g. file://$TMPDIR/TestDocumentURIFix/default/work/a.go) +- check() - --func uriName(uri span.URI) string { -- return filepath.Base(strings.TrimSuffix(uri.Filename(), ".go")) --} -- --// TODO(golang/go#54845): improve the formatting here to match standard --// line:column position formatting. --func SpanName(spn span.Span) string { -- return fmt.Sprintf("%v_%v_%v", uriName(spn.URI()), spn.Start().Line(), spn.Start().Column()) --} +- // URL-encoded path (e.g. contains %2F instead of last /) +- loc.URI = protocol.DocumentURI("file://" + strings.Replace(path, "/a.go", "%2Fa.go", 1)) +- check() - --func shouldSkip(data *Data, uri span.URI) bool { -- if data.ModfileFlagAvailable { -- return false -- } -- // If the -modfile flag is not available, then we do not want to run -- // any tests on the go.mod file. -- if strings.HasSuffix(uri.Filename(), ".mod") { -- return true -- } -- // If the -modfile flag is not available, then we do not want to test any -- // uri that contains "go mod tidy". -- m, err := data.Mapper(uri) -- return err == nil && strings.Contains(string(m.Content), ", \"go mod tidy\",") +- // We intentionally do not test further cases (e.g. +- // file:// without a third slash) as it would quickly +- // get bogged down in irrelevant details of the +- // fake editor's own handling of URIs. +- }) -} -diff -urN a/gopls/internal/lsp/tests/util.go b/gopls/internal/lsp/tests/util.go ---- a/gopls/internal/lsp/tests/util.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/tests/util.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,33 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/test/integration/misc/multiple_adhoc_test.go b/gopls/internal/test/integration/misc/multiple_adhoc_test.go +--- a/gopls/internal/test/integration/misc/multiple_adhoc_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/misc/multiple_adhoc_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,44 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package tests +-package misc - -import ( -- "fmt" +- "testing" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" +- . "golang.org/x/tools/gopls/internal/test/integration" -) - --// DiffCallHierarchyItems returns the diff between expected and actual call locations for incoming/outgoing call hierarchies --func DiffCallHierarchyItems(gotCalls []protocol.CallHierarchyItem, expectedCalls []protocol.CallHierarchyItem) string { -- expected := make(map[protocol.Location]bool) -- for _, call := range expectedCalls { -- expected[protocol.Location{URI: call.URI, Range: call.Range}] = true -- } +-func TestMultipleAdHocPackages(t *testing.T) { +- Run(t, ` +--- a/a.go -- +-package main - -- got := make(map[protocol.Location]bool) -- for _, call := range gotCalls { -- got[protocol.Location{URI: call.URI, Range: call.Range}] = true -- } -- if len(got) != len(expected) { -- return fmt.Sprintf("expected %d calls but got %d", len(expected), len(got)) -- } -- for spn := range got { -- if !expected[spn] { -- return fmt.Sprintf("incorrect calls, expected locations %v but got locations %v", expected, got) +-import "fmt" +- +-func main() { +- fmt.Println("") +-} +--- a/b.go -- +-package main +- +-import "fmt" +- +-func main() () { +- fmt.Println("") +-} +-`, func(t *testing.T, env *Env) { +- env.OpenFile("a/a.go") +- if list := env.Completion(env.RegexpSearch("a/a.go", "Println")); list == nil || len(list.Items) == 0 { +- t.Fatal("expected completions, got none") - } -- } -- return "" +- env.OpenFile("a/b.go") +- if list := env.Completion(env.RegexpSearch("a/b.go", "Println")); list == nil || len(list.Items) == 0 { +- t.Fatal("expected completions, got none") +- } +- if list := env.Completion(env.RegexpSearch("a/a.go", "Println")); list == nil || len(list.Items) == 0 { +- t.Fatal("expected completions, got none") +- } +- }) -} -diff -urN a/gopls/internal/lsp/text_synchronization.go b/gopls/internal/lsp/text_synchronization.go ---- a/gopls/internal/lsp/text_synchronization.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/text_synchronization.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,367 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/test/integration/misc/prompt_test.go b/gopls/internal/test/integration/misc/prompt_test.go +--- a/gopls/internal/test/integration/misc/prompt_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/misc/prompt_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,232 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package lsp +-package misc - -import ( -- "bytes" -- "context" -- "errors" - "fmt" +- "os" - "path/filepath" -- "sync" +- "regexp" +- "testing" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/event/tag" -- "golang.org/x/tools/internal/jsonrpc2" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/command" +- "golang.org/x/tools/gopls/internal/server" +- . "golang.org/x/tools/gopls/internal/test/integration" -) - --// ModificationSource identifies the origin of a change. --type ModificationSource int -- --const ( -- // FromDidOpen is from a didOpen notification. -- FromDidOpen = ModificationSource(iota) -- -- // FromDidChange is from a didChange notification. -- FromDidChange -- -- // FromDidChangeWatchedFiles is from didChangeWatchedFiles notification. -- FromDidChangeWatchedFiles -- -- // FromDidSave is from a didSave notification. -- FromDidSave -- -- // FromDidClose is from a didClose notification. -- FromDidClose -- -- // FromDidChangeConfiguration is from a didChangeConfiguration notification. -- FromDidChangeConfiguration -- -- // FromRegenerateCgo refers to file modifications caused by regenerating -- // the cgo sources for the workspace. -- FromRegenerateCgo +-// Test that gopls prompts for telemetry only when it is supposed to. +-func TestTelemetryPrompt_Conditions(t *testing.T) { +- const src = ` +--- go.mod -- +-module mod.com - -- // FromInitialWorkspaceLoad refers to the loading of all packages in the -- // workspace when the view is first created. -- FromInitialWorkspaceLoad --) +-go 1.12 +--- main.go -- +-package main - --func (m ModificationSource) String() string { -- switch m { -- case FromDidOpen: -- return "opened files" -- case FromDidChange: -- return "changed files" -- case FromDidChangeWatchedFiles: -- return "files changed on disk" -- case FromDidSave: -- return "saved files" -- case FromDidClose: -- return "close files" -- case FromRegenerateCgo: -- return "regenerate cgo" -- case FromInitialWorkspaceLoad: -- return "initial workspace load" -- default: -- return "unknown file modification" -- } +-func main() { -} +-` - --func (s *Server) didOpen(ctx context.Context, params *protocol.DidOpenTextDocumentParams) error { -- ctx, done := event.Start(ctx, "lsp.Server.didOpen", tag.URI.Of(params.TextDocument.URI)) -- defer done() -- -- uri := params.TextDocument.URI.SpanURI() -- if !uri.IsFile() { -- return nil -- } -- // There may not be any matching view in the current session. If that's -- // the case, try creating a new view based on the opened file path. -- // -- // TODO(rstambler): This seems like it would continuously add new -- // views, but it won't because ViewOf only returns an error when there -- // are no views in the session. I don't know if that logic should go -- // here, or if we can continue to rely on that implementation detail. -- // -- // TODO(golang/go#57979): this will be generalized to a different view calculation. -- if _, err := s.session.ViewOf(uri); err != nil { -- dir := filepath.Dir(uri.Filename()) -- if err := s.addFolders(ctx, []protocol.WorkspaceFolder{{ -- URI: string(protocol.URIFromPath(dir)), -- Name: filepath.Base(dir), -- }}); err != nil { -- return err -- } +- for _, enabled := range []bool{true, false} { +- t.Run(fmt.Sprintf("telemetryPrompt=%v", enabled), func(t *testing.T) { +- for _, initialMode := range []string{"", "local", "off", "on"} { +- t.Run(fmt.Sprintf("initial_mode=%s", initialMode), func(t *testing.T) { +- modeFile := filepath.Join(t.TempDir(), "mode") +- if initialMode != "" { +- if err := os.WriteFile(modeFile, []byte(initialMode), 0666); err != nil { +- t.Fatal(err) +- } +- } +- WithOptions( +- Modes(Default), // no need to run this in all modes +- EnvVars{ +- server.GoplsConfigDirEnvvar: t.TempDir(), +- server.FakeTelemetryModefileEnvvar: modeFile, +- }, +- Settings{ +- "telemetryPrompt": enabled, +- }, +- ).Run(t, src, func(t *testing.T, env *Env) { +- wantPrompt := enabled && (initialMode == "" || initialMode == "local") +- expectation := ShownMessageRequest(".*Would you like to enable Go telemetry?") +- if !wantPrompt { +- expectation = Not(expectation) +- } +- env.OnceMet( +- CompletedWork(server.TelemetryPromptWorkTitle, 1, true), +- expectation, +- ) +- }) +- }) +- } +- }) - } -- return s.didModifyFiles(ctx, []source.FileModification{{ -- URI: uri, -- Action: source.Open, -- Version: params.TextDocument.Version, -- Text: []byte(params.TextDocument.Text), -- LanguageID: params.TextDocument.LanguageID, -- }}, FromDidOpen) -} - --func (s *Server) didChange(ctx context.Context, params *protocol.DidChangeTextDocumentParams) error { -- ctx, done := event.Start(ctx, "lsp.Server.didChange", tag.URI.Of(params.TextDocument.URI)) -- defer done() +-// Test that responding to the telemetry prompt results in the expected state. +-func TestTelemetryPrompt_Response(t *testing.T) { +- const src = ` +--- go.mod -- +-module mod.com - -- uri := params.TextDocument.URI.SpanURI() -- if !uri.IsFile() { -- return nil -- } +-go 1.12 +--- main.go -- +-package main - -- text, err := s.changedText(ctx, uri, params.ContentChanges) -- if err != nil { -- return err -- } -- c := source.FileModification{ -- URI: uri, -- Action: source.Change, -- Version: params.TextDocument.Version, -- Text: text, -- } -- if err := s.didModifyFiles(ctx, []source.FileModification{c}, FromDidChange); err != nil { -- return err -- } -- return s.warnAboutModifyingGeneratedFiles(ctx, uri) +-func main() { -} +-` - --// warnAboutModifyingGeneratedFiles shows a warning if a user tries to edit a --// generated file for the first time. --func (s *Server) warnAboutModifyingGeneratedFiles(ctx context.Context, uri span.URI) error { -- s.changedFilesMu.Lock() -- _, ok := s.changedFiles[uri] -- if !ok { -- s.changedFiles[uri] = struct{}{} +- tests := []struct { +- name string // subtest name +- response string // response to choose for the telemetry dialog +- wantMode string // resulting telemetry mode +- wantMsg string // substring contained in the follow-up popup (if empty, no popup is expected) +- }{ +- {"yes", server.TelemetryYes, "on", "uploading is now enabled"}, +- {"no", server.TelemetryNo, "", ""}, +- {"empty", "", "", ""}, - } -- s.changedFilesMu.Unlock() -- -- // This file has already been edited before. -- if ok { -- return nil +- for _, test := range tests { +- t.Run(test.name, func(t *testing.T) { +- modeFile := filepath.Join(t.TempDir(), "mode") +- msgRE := regexp.MustCompile(".*Would you like to enable Go telemetry?") +- respond := func(m *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error) { +- if msgRE.MatchString(m.Message) { +- for _, item := range m.Actions { +- if item.Title == test.response { +- return &item, nil +- } +- } +- if test.response != "" { +- t.Errorf("action item %q not found", test.response) +- } +- } +- return nil, nil +- } +- WithOptions( +- Modes(Default), // no need to run this in all modes +- EnvVars{ +- server.GoplsConfigDirEnvvar: t.TempDir(), +- server.FakeTelemetryModefileEnvvar: modeFile, +- }, +- Settings{ +- "telemetryPrompt": true, +- }, +- MessageResponder(respond), +- ).Run(t, src, func(t *testing.T, env *Env) { +- var postConditions []Expectation +- if test.wantMsg != "" { +- postConditions = append(postConditions, ShownMessage(test.wantMsg)) +- } +- env.OnceMet( +- CompletedWork(server.TelemetryPromptWorkTitle, 1, true), +- postConditions..., +- ) +- gotMode := "" +- if contents, err := os.ReadFile(modeFile); err == nil { +- gotMode = string(contents) +- } else if !os.IsNotExist(err) { +- t.Fatal(err) +- } +- if gotMode != test.wantMode { +- t.Errorf("after prompt, mode=%s, want %s", gotMode, test.wantMode) +- } +- }) +- }) - } +-} - -- // Ideally, we should be able to specify that a generated file should -- // be opened as read-only. Tell the user that they should not be -- // editing a generated file. -- view, err := s.session.ViewOf(uri) -- if err != nil { -- return err -- } -- snapshot, release, err := view.Snapshot() -- if err != nil { -- return err -- } -- isGenerated := source.IsGenerated(ctx, snapshot, uri) -- release() +-// Test that we stop asking about telemetry after the user ignores the question +-// 5 times. +-func TestTelemetryPrompt_GivingUp(t *testing.T) { +- const src = ` +--- go.mod -- +-module mod.com - -- if !isGenerated { -- return nil -- } -- return s.client.ShowMessage(ctx, &protocol.ShowMessageParams{ -- Message: fmt.Sprintf("Do not edit this file! %s is a generated file.", uri.Filename()), -- Type: protocol.Warning, -- }) +-go 1.12 +--- main.go -- +-package main +- +-func main() { -} +-` - --func (s *Server) didChangeWatchedFiles(ctx context.Context, params *protocol.DidChangeWatchedFilesParams) error { -- ctx, done := event.Start(ctx, "lsp.Server.didChangeWatchedFiles") -- defer done() +- // For this test, we want to share state across gopls sessions. +- modeFile := filepath.Join(t.TempDir(), "mode") +- configDir := t.TempDir() - -- var modifications []source.FileModification -- for _, change := range params.Changes { -- uri := change.URI.SpanURI() -- if !uri.IsFile() { -- continue -- } -- action := changeTypeToFileAction(change.Type) -- modifications = append(modifications, source.FileModification{ -- URI: uri, -- Action: action, -- OnDisk: true, +- const maxPrompts = 5 // internal prompt limit defined by gopls +- +- for i := 0; i < maxPrompts+1; i++ { +- WithOptions( +- Modes(Default), // no need to run this in all modes +- EnvVars{ +- server.GoplsConfigDirEnvvar: configDir, +- server.FakeTelemetryModefileEnvvar: modeFile, +- }, +- Settings{ +- "telemetryPrompt": true, +- }, +- ).Run(t, src, func(t *testing.T, env *Env) { +- wantPrompt := i < maxPrompts +- expectation := ShownMessageRequest(".*Would you like to enable Go telemetry?") +- if !wantPrompt { +- expectation = Not(expectation) +- } +- env.OnceMet( +- CompletedWork(server.TelemetryPromptWorkTitle, 1, true), +- expectation, +- ) - }) - } -- return s.didModifyFiles(ctx, modifications, FromDidChangeWatchedFiles) -} - --func (s *Server) didSave(ctx context.Context, params *protocol.DidSaveTextDocumentParams) error { -- ctx, done := event.Start(ctx, "lsp.Server.didSave", tag.URI.Of(params.TextDocument.URI)) -- defer done() -- -- uri := params.TextDocument.URI.SpanURI() -- if !uri.IsFile() { -- return nil -- } -- c := source.FileModification{ -- URI: uri, -- Action: source.Save, -- } -- if params.Text != nil { -- c.Text = []byte(*params.Text) -- } -- return s.didModifyFiles(ctx, []source.FileModification{c}, FromDidSave) --} +-// Test that gopls prompts for telemetry only when it is supposed to. +-func TestTelemetryPrompt_Conditions2(t *testing.T) { +- const src = ` +--- go.mod -- +-module mod.com - --func (s *Server) didClose(ctx context.Context, params *protocol.DidCloseTextDocumentParams) error { -- ctx, done := event.Start(ctx, "lsp.Server.didClose", tag.URI.Of(params.TextDocument.URI)) -- defer done() +-go 1.12 +--- main.go -- +-package main - -- uri := params.TextDocument.URI.SpanURI() -- if !uri.IsFile() { -- return nil -- } -- return s.didModifyFiles(ctx, []source.FileModification{ -- { -- URI: uri, -- Action: source.Close, -- Version: -1, -- Text: nil, +-func main() { +-} +-` +- modeFile := filepath.Join(t.TempDir(), "mode") +- WithOptions( +- Modes(Default), // no need to run this in all modes +- EnvVars{ +- server.GoplsConfigDirEnvvar: t.TempDir(), +- server.FakeTelemetryModefileEnvvar: modeFile, - }, -- }, FromDidClose) +- Settings{ +- // off because we are testing +- // if we can trigger the prompt with command. +- "telemetryPrompt": false, +- }, +- ).Run(t, src, func(t *testing.T, env *Env) { +- cmd, err := command.NewMaybePromptForTelemetryCommand("prompt") +- if err != nil { +- t.Fatal(err) +- } +- var result error +- env.ExecuteCommand(&protocol.ExecuteCommandParams{ +- Command: cmd.Command, +- }, &result) +- if result != nil { +- t.Fatal(err) +- } +- expectation := ShownMessageRequest(".*Would you like to enable Go telemetry?") +- env.OnceMet( +- CompletedWork(server.TelemetryPromptWorkTitle, 2, true), +- expectation, +- ) +- }) -} +diff -urN a/gopls/internal/test/integration/misc/references_test.go b/gopls/internal/test/integration/misc/references_test.go +--- a/gopls/internal/test/integration/misc/references_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/misc/references_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,574 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func (s *Server) didModifyFiles(ctx context.Context, modifications []source.FileModification, cause ModificationSource) error { -- // wg guards two conditions: -- // 1. didModifyFiles is complete -- // 2. the goroutine diagnosing changes on behalf of didModifyFiles is -- // complete, if it was started -- // -- // Both conditions must be satisfied for the purpose of testing: we don't -- // want to observe the completion of change processing until we have received -- // all diagnostics as well as all server->client notifications done on behalf -- // of this function. -- var wg sync.WaitGroup -- wg.Add(1) -- defer wg.Done() +-package misc - -- if s.Options().VerboseWorkDoneProgress { -- work := s.progress.Start(ctx, DiagnosticWorkTitle(cause), "Calculating file diagnostics...", nil, nil) -- go func() { -- wg.Wait() -- work.End(ctx, "Done.") -- }() -- } +-import ( +- "fmt" +- "os" +- "path/filepath" +- "reflect" +- "sort" +- "strings" +- "testing" - -- onDisk := cause == FromDidChangeWatchedFiles +- "github.com/google/go-cmp/cmp" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/test/integration" +- . "golang.org/x/tools/gopls/internal/test/integration" +-) - -- s.stateMu.Lock() -- if s.state >= serverShutDown { -- // This state check does not prevent races below, and exists only to -- // produce a better error message. The actual race to the cache should be -- // guarded by Session.viewMu. -- s.stateMu.Unlock() -- return errors.New("server is shut down") -- } -- s.stateMu.Unlock() +-func TestStdlibReferences(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - -- // If the set of changes included directories, expand those directories -- // to their files. -- modifications = s.session.ExpandModificationsToDirectories(ctx, modifications) +-go 1.12 +--- main.go -- +-package main - -- // Build a lookup map for file modifications, so that we can later join -- // with the snapshot file associations. -- modMap := make(map[span.URI]source.FileModification) -- for _, mod := range modifications { -- modMap[mod.URI] = mod -- } +-import "fmt" - -- snapshots, release, err := s.session.DidModifyFiles(ctx, modifications) -- if err != nil { -- return err -- } +-func main() { +- fmt.Print() +-} +-` - -- // golang/go#50267: diagnostics should be re-sent after an open or close. For -- // some clients, it may be helpful to re-send after each change. -- for snapshot, uris := range snapshots { -- for _, uri := range uris { -- mod := modMap[uri] -- if snapshot.Options().ChattyDiagnostics || mod.Action == source.Open || mod.Action == source.Close { -- s.mustPublishDiagnostics(uri) -- } +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- loc := env.GoToDefinition(env.RegexpSearch("main.go", `fmt.(Print)`)) +- refs, err := env.Editor.References(env.Ctx, loc) +- if err != nil { +- t.Fatal(err) - } -- } +- if len(refs) != 2 { +- // TODO(adonovan): make this assertion less maintainer-hostile. +- t.Fatalf("got %v reference(s), want 2", len(refs)) +- } +- // The first reference is guaranteed to be the definition. +- if got, want := refs[1].URI, env.Sandbox.Workdir.URI("main.go"); got != want { +- t.Errorf("found reference in %v, wanted %v", got, want) +- } +- }) +-} - -- wg.Add(1) -- go func() { -- s.diagnoseSnapshots(snapshots, onDisk, cause) -- release() -- wg.Done() -- }() +-// This is a regression test for golang/go#48400 (a panic). +-func TestReferencesOnErrorMethod(t *testing.T) { +- // Ideally this would actually return the correct answer, +- // instead of merely failing gracefully. +- const files = ` +--- go.mod -- +-module mod.com - -- // After any file modifications, we need to update our watched files, -- // in case something changed. Compute the new set of directories to watch, -- // and if it differs from the current set, send updated registrations. -- return s.updateWatchedDirectories(ctx) --} +-go 1.12 +--- main.go -- +-package main - --// DiagnosticWorkTitle returns the title of the diagnostic work resulting from a --// file change originating from the given cause. --func DiagnosticWorkTitle(cause ModificationSource) string { -- return fmt.Sprintf("diagnosing %v", cause) +-type t interface { +- error -} - --func (s *Server) changedText(ctx context.Context, uri span.URI, changes []protocol.TextDocumentContentChangeEvent) ([]byte, error) { -- if len(changes) == 0 { -- return nil, fmt.Errorf("%w: no content changes provided", jsonrpc2.ErrInternal) -- } +-type s struct{} - -- // Check if the client sent the full content of the file. -- // We accept a full content change even if the server expected incremental changes. -- if len(changes) == 1 && changes[0].Range == nil && changes[0].RangeLength == 0 { -- return []byte(changes[0].Text), nil -- } -- return s.applyIncrementalChanges(ctx, uri, changes) +-func (*s) Error() string { +- return "" -} - --func (s *Server) applyIncrementalChanges(ctx context.Context, uri span.URI, changes []protocol.TextDocumentContentChangeEvent) ([]byte, error) { -- fh, err := s.session.ReadFile(ctx, uri) -- if err != nil { -- return nil, err -- } -- content, err := fh.Content() -- if err != nil { -- return nil, fmt.Errorf("%w: file not found (%v)", jsonrpc2.ErrInternal, err) -- } -- for _, change := range changes { -- // TODO(adonovan): refactor to use diff.Apply, which is robust w.r.t. -- // out-of-order or overlapping changes---and much more efficient. -- -- // Make sure to update mapper along with the content. -- m := protocol.NewMapper(uri, content) -- if change.Range == nil { -- return nil, fmt.Errorf("%w: unexpected nil range for change", jsonrpc2.ErrInternal) -- } -- spn, err := m.RangeSpan(*change.Range) +-func _() { +- var s s +- _ = s.Error() +-} +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- loc := env.GoToDefinition(env.RegexpSearch("main.go", `Error`)) +- refs, err := env.Editor.References(env.Ctx, loc) - if err != nil { -- return nil, err +- t.Fatalf("references on (*s).Error failed: %v", err) - } -- start, end := spn.Start().Offset(), spn.End().Offset() -- if end < start { -- return nil, fmt.Errorf("%w: invalid range for content change", jsonrpc2.ErrInternal) +- // TODO(adonovan): this test is crying out for marker support in integration tests. +- var buf strings.Builder +- for _, ref := range refs { +- fmt.Fprintf(&buf, "%s %s\n", env.Sandbox.Workdir.URIToPath(ref.URI), ref.Range) - } -- var buf bytes.Buffer -- buf.Write(content[:start]) -- buf.WriteString(change.Text) -- buf.Write(content[end:]) -- content = buf.Bytes() -- } -- return content, nil +- got := buf.String() +- want := "main.go 8:10-8:15\n" + // (*s).Error decl +- "main.go 14:7-14:12\n" // s.Error() call +- if diff := cmp.Diff(want, got); diff != "" { +- t.Errorf("unexpected references on (*s).Error (-want +got):\n%s", diff) +- } +- }) -} - --func changeTypeToFileAction(ct protocol.FileChangeType) source.FileAction { -- switch ct { -- case protocol.Changed: -- return source.Change -- case protocol.Created: -- return source.Create -- case protocol.Deleted: -- return source.Delete -- } -- return source.UnknownFileAction --} -diff -urN a/gopls/internal/lsp/work/completion.go b/gopls/internal/lsp/work/completion.go ---- a/gopls/internal/lsp/work/completion.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/work/completion.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,154 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-func TestDefsRefsBuiltins(t *testing.T) { +- // TODO(adonovan): add unsafe.{SliceData,String,StringData} in later go versions. +- const files = ` +--- go.mod -- +-module example.com +-go 1.16 - --package work +--- a.go -- +-package a - --import ( -- "context" -- "errors" -- "fmt" -- "os" -- "path/filepath" -- "sort" -- "strings" +-import "unsafe" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/internal/event" --) +-const _ = iota +-var _ error +-var _ int +-var _ = append() +-var _ = unsafe.Pointer(nil) +-var _ = unsafe.Add(nil, nil) +-var _ = unsafe.Sizeof(0) +-var _ = unsafe.Alignof(0) +-var _ = unsafe.Slice(nil, 0) +-` - --func Completion(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.CompletionList, error) { -- ctx, done := event.Start(ctx, "work.Completion") -- defer done() +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("a.go") +- for _, name := range strings.Fields( +- "iota error int nil append iota Pointer Sizeof Alignof Add Slice") { +- loc := env.RegexpSearch("a.go", `\b`+name+`\b`) - -- // Get the position of the cursor. -- pw, err := snapshot.ParseWork(ctx, fh) -- if err != nil { -- return nil, fmt.Errorf("getting go.work file handle: %w", err) -- } -- cursor, err := pw.Mapper.PositionOffset(position) -- if err != nil { -- return nil, fmt.Errorf("computing cursor offset: %w", err) -- } +- // definition -> {builtin,unsafe}.go +- def := env.GoToDefinition(loc) +- if (!strings.HasSuffix(string(def.URI), "builtin.go") && +- !strings.HasSuffix(string(def.URI), "unsafe.go")) || +- def.Range.Start.Line == 0 { +- t.Errorf("definition(%q) = %v, want {builtin,unsafe}.go", +- name, def) +- } - -- // Find the use statement the user is in. -- use, pathStart, _ := usePath(pw, cursor) -- if use == nil { -- return &protocol.CompletionList{}, nil +- // "references to (builtin "Foo"|unsafe.Foo) are not supported" +- _, err := env.Editor.References(env.Ctx, loc) +- gotErr := fmt.Sprint(err) +- if !strings.Contains(gotErr, "references to") || +- !strings.Contains(gotErr, "not supported") || +- !strings.Contains(gotErr, name) { +- t.Errorf("references(%q) error: got %q, want %q", +- name, gotErr, "references to ... are not supported") +- } +- } +- }) +-} +- +-func TestPackageReferences(t *testing.T) { +- tests := []struct { +- packageName string +- wantRefCount int +- wantFiles []string +- }{ +- { +- "lib1", +- 3, +- []string{ +- "main.go", +- "lib1/a.go", +- "lib1/b.go", +- }, +- }, +- { +- "lib2", +- 2, +- []string{ +- "main.go", +- "lib2/a.go", +- }, +- }, - } -- completingFrom := use.Path[:cursor-pathStart] - -- // We're going to find the completions of the user input -- // (completingFrom) by doing a walk on the innermost directory -- // of the given path, and comparing the found paths to make sure -- // that they match the component of the path after the -- // innermost directory. -- // -- // We'll maintain two paths when doing this: pathPrefixSlash -- // is essentially the path the user typed in, and pathPrefixAbs -- // is the path made absolute from the go.work directory. +- const files = ` +--- go.mod -- +-module mod.com - -- pathPrefixSlash := completingFrom -- pathPrefixAbs := filepath.FromSlash(pathPrefixSlash) -- if !filepath.IsAbs(pathPrefixAbs) { -- pathPrefixAbs = filepath.Join(filepath.Dir(pw.URI.Filename()), pathPrefixAbs) -- } +-go 1.18 +--- lib1/a.go -- +-package lib1 - -- // pathPrefixDir is the directory that will be walked to find matches. -- // If pathPrefixSlash is not explicitly a directory boundary (is either equivalent to "." or -- // ends in a separator) we need to examine its parent directory to find sibling files that -- // match. -- depthBound := 5 -- pathPrefixDir, pathPrefixBase := pathPrefixAbs, "" -- pathPrefixSlashDir := pathPrefixSlash -- if filepath.Clean(pathPrefixSlash) != "." && !strings.HasSuffix(pathPrefixSlash, "/") { -- depthBound++ -- pathPrefixDir, pathPrefixBase = filepath.Split(pathPrefixAbs) -- pathPrefixSlashDir = dirNonClean(pathPrefixSlash) -- } +-const A = 1 - -- var completions []string -- // Stop traversing deeper once we've hit 10k files to try to stay generally under 100ms. -- const numSeenBound = 10000 -- var numSeen int -- stopWalking := errors.New("hit numSeenBound") -- err = filepath.Walk(pathPrefixDir, func(wpath string, info os.FileInfo, err error) error { -- if numSeen > numSeenBound { -- // Stop traversing if we hit bound. -- return stopWalking -- } -- numSeen++ +--- lib1/b.go -- +-package lib1 - -- // rel is the path relative to pathPrefixDir. -- // Make sure that it has pathPrefixBase as a prefix -- // otherwise it won't match the beginning of the -- // base component of the path the user typed in. -- rel := strings.TrimPrefix(wpath[len(pathPrefixDir):], string(filepath.Separator)) -- if info.IsDir() && wpath != pathPrefixDir && !strings.HasPrefix(rel, pathPrefixBase) { -- return filepath.SkipDir -- } +-const B = 1 - -- // Check for a match (a module directory). -- if filepath.Base(rel) == "go.mod" { -- relDir := strings.TrimSuffix(dirNonClean(rel), string(os.PathSeparator)) -- completionPath := join(pathPrefixSlashDir, filepath.ToSlash(relDir)) +--- lib2/a.go -- +-package lib2 - -- if !strings.HasPrefix(completionPath, completingFrom) { -- return nil +-const C = 1 +- +--- main.go -- +-package main +- +-import ( +- "mod.com/lib1" +- "mod.com/lib2" +-) +- +-func main() { +- println("Hello") +-} +-` +- Run(t, files, func(t *testing.T, env *Env) { +- for _, test := range tests { +- file := fmt.Sprintf("%s/a.go", test.packageName) +- env.OpenFile(file) +- loc := env.RegexpSearch(file, test.packageName) +- refs := env.References(loc) +- if len(refs) != test.wantRefCount { +- // TODO(adonovan): make this assertion less maintainer-hostile. +- t.Fatalf("got %v reference(s), want %d", len(refs), test.wantRefCount) - } -- if strings.HasSuffix(completionPath, "/") { -- // Don't suggest paths that end in "/". This happens -- // when the input is a path that ends in "/" and -- // the completion is empty. -- return nil +- var refURIs []string +- for _, ref := range refs { +- refURIs = append(refURIs, string(ref.URI)) - } -- completion := completionPath[len(completingFrom):] -- if completingFrom == "" && !strings.HasPrefix(completion, "./") { -- // Bias towards "./" prefixes. -- completion = join(".", completion) +- for _, base := range test.wantFiles { +- hasBase := false +- for _, ref := range refURIs { +- if strings.HasSuffix(ref, base) { +- hasBase = true +- break +- } +- } +- if !hasBase { +- t.Fatalf("got [%v], want reference ends with \"%v\"", strings.Join(refURIs, ","), base) +- } - } -- -- completions = append(completions, completion) -- } -- -- if depth := strings.Count(rel, string(filepath.Separator)); depth >= depthBound { -- return filepath.SkipDir - } -- return nil - }) -- if err != nil && !errors.Is(err, stopWalking) { -- return nil, fmt.Errorf("walking to find completions: %w", err) -- } +-} +- +-// Test for golang/go#43144. +-// +-// Verify that we search for references and implementations in intermediate +-// test variants. +-func TestReferencesInTestVariants(t *testing.T) { +- const files = ` +--- go.mod -- +-module foo.mod +- +-go 1.12 +--- foo/foo.go -- +-package foo - -- sort.Strings(completions) +-import "foo.mod/bar" - -- items := []protocol.CompletionItem{} // must be a slice -- for _, c := range completions { -- items = append(items, protocol.CompletionItem{ -- Label: c, -- InsertText: c, -- }) -- } -- return &protocol.CompletionList{Items: items}, nil --} +-const Foo = 42 - --// dirNonClean is filepath.Dir, without the Clean at the end. --func dirNonClean(path string) string { -- vol := filepath.VolumeName(path) -- i := len(path) - 1 -- for i >= len(vol) && !os.IsPathSeparator(path[i]) { -- i-- -- } -- return path[len(vol) : i+1] --} +-type T int +-type InterfaceM interface{ M() } +-type InterfaceF interface{ F() } - --func join(a, b string) string { -- if a == "" { -- return b -- } -- if b == "" { -- return a -- } -- return strings.TrimSuffix(a, "/") + "/" + b +-func _() { +- _ = bar.Blah -} -diff -urN a/gopls/internal/lsp/work/diagnostics.go b/gopls/internal/lsp/work/diagnostics.go ---- a/gopls/internal/lsp/work/diagnostics.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/work/diagnostics.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,92 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package work +--- foo/foo_test.go -- +-package foo - --import ( -- "context" -- "fmt" -- "os" -- "path/filepath" +-type Fer struct{} +-func (Fer) F() {} - -- "golang.org/x/mod/modfile" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/event" --) +--- bar/bar.go -- +-package bar - --func Diagnostics(ctx context.Context, snapshot source.Snapshot) (map[span.URI][]*source.Diagnostic, error) { -- ctx, done := event.Start(ctx, "work.Diagnostics", source.SnapshotLabels(snapshot)...) -- defer done() +-var Blah = 123 - -- reports := map[span.URI][]*source.Diagnostic{} -- uri := snapshot.WorkFile() -- if uri == "" { -- return nil, nil -- } -- fh, err := snapshot.ReadFile(ctx, uri) -- if err != nil { -- return nil, err -- } -- reports[fh.URI()] = []*source.Diagnostic{} -- diagnostics, err := DiagnosticsForWork(ctx, snapshot, fh) -- if err != nil { -- return nil, err -- } -- for _, d := range diagnostics { -- fh, err := snapshot.ReadFile(ctx, d.URI) -- if err != nil { -- return nil, err -- } -- reports[fh.URI()] = append(reports[fh.URI()], d) -- } +--- bar/bar_test.go -- +-package bar - -- return reports, nil +-type Mer struct{} +-func (Mer) M() {} +- +-func TestBar() { +- _ = Blah -} +--- bar/bar_x_test.go -- +-package bar_test - --func DiagnosticsForWork(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]*source.Diagnostic, error) { -- pw, err := snapshot.ParseWork(ctx, fh) -- if err != nil { -- if pw == nil || len(pw.ParseErrors) == 0 { -- return nil, err -- } -- return pw.ParseErrors, nil -- } +-import ( +- "foo.mod/bar" +- "foo.mod/foo" +-) - -- // Add diagnostic if a directory does not contain a module. -- var diagnostics []*source.Diagnostic -- for _, use := range pw.File.Use { -- rng, err := pw.Mapper.OffsetRange(use.Syntax.Start.Byte, use.Syntax.End.Byte) -- if err != nil { -- return nil, err -- } +-type Mer struct{} +-func (Mer) M() {} - -- modfh, err := snapshot.ReadFile(ctx, modFileURI(pw, use)) -- if err != nil { -- return nil, err -- } -- if _, err := modfh.Content(); err != nil && os.IsNotExist(err) { -- diagnostics = append(diagnostics, &source.Diagnostic{ -- URI: fh.URI(), -- Range: rng, -- Severity: protocol.SeverityError, -- Source: source.WorkFileError, -- Message: fmt.Sprintf("directory %v does not contain a module", use.Path), -- }) -- } -- } -- return diagnostics, nil +-func _() { +- _ = bar.Blah +- _ = foo.Foo -} +-` - --func modFileURI(pw *source.ParsedWorkFile, use *modfile.Use) span.URI { -- workdir := filepath.Dir(pw.URI.Filename()) +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("foo/foo.go") - -- modroot := filepath.FromSlash(use.Path) -- if !filepath.IsAbs(modroot) { -- modroot = filepath.Join(workdir, modroot) -- } +- refTests := []struct { +- re string +- wantRefs []string +- }{ +- // Blah is referenced: +- // - inside the foo.mod/bar (ordinary) package +- // - inside the foo.mod/bar [foo.mod/bar.test] test variant package +- // - from the foo.mod/bar_test [foo.mod/bar.test] x_test package +- // - from the foo.mod/foo package +- {"Blah", []string{"bar/bar.go:3", "bar/bar_test.go:7", "bar/bar_x_test.go:12", "foo/foo.go:12"}}, - -- return span.URIFromPath(filepath.Join(modroot, "go.mod")) --} -diff -urN a/gopls/internal/lsp/work/format.go b/gopls/internal/lsp/work/format.go ---- a/gopls/internal/lsp/work/format.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/work/format.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,28 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +- // Foo is referenced in bar_x_test.go via the intermediate test variant +- // foo.mod/foo [foo.mod/bar.test]. +- {"Foo", []string{"bar/bar_x_test.go:13", "foo/foo.go:5"}}, +- } - --package work +- for _, test := range refTests { +- loc := env.RegexpSearch("foo/foo.go", test.re) +- refs := env.References(loc) - --import ( -- "context" +- got := fileLocations(env, refs) +- if diff := cmp.Diff(test.wantRefs, got); diff != "" { +- t.Errorf("References(%q) returned unexpected diff (-want +got):\n%s", test.re, diff) +- } +- } - -- "golang.org/x/mod/modfile" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/internal/event" --) +- implTests := []struct { +- re string +- wantImpls []string +- }{ +- // InterfaceM is implemented both in foo.mod/bar [foo.mod/bar.test] (which +- // doesn't import foo), and in foo.mod/bar_test [foo.mod/bar.test], which +- // imports the test variant of foo. +- {"InterfaceM", []string{"bar/bar_test.go:3", "bar/bar_x_test.go:8"}}, - --func Format(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle) ([]protocol.TextEdit, error) { -- ctx, done := event.Start(ctx, "work.Format") -- defer done() +- // A search within the ordinary package to should find implementations +- // (Fer) within the augmented test package. +- {"InterfaceF", []string{"foo/foo_test.go:3"}}, +- } - -- pw, err := snapshot.ParseWork(ctx, fh) -- if err != nil { -- return nil, err -- } -- formatted := modfile.Format(pw.File.Syntax) -- // Calculate the edits to be made due to the change. -- diffs := snapshot.Options().ComputeEdits(string(pw.Mapper.Content), string(formatted)) -- return source.ToProtocolEdits(pw.Mapper, diffs) +- for _, test := range implTests { +- loc := env.RegexpSearch("foo/foo.go", test.re) +- impls := env.Implementations(loc) +- +- got := fileLocations(env, impls) +- if diff := cmp.Diff(test.wantImpls, got); diff != "" { +- t.Errorf("Implementations(%q) returned unexpected diff (-want +got):\n%s", test.re, diff) +- } +- } +- }) -} -diff -urN a/gopls/internal/lsp/work/hover.go b/gopls/internal/lsp/work/hover.go ---- a/gopls/internal/lsp/work/hover.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/work/hover.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,92 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package work +-// This is a regression test for Issue #56169, in which interface +-// implementations in vendored modules were not found. The actual fix +-// was the same as for #55995; see TestVendoringInvalidatesMetadata. +-func TestImplementationsInVendor(t *testing.T) { +- const proxy = ` +--- other.com/b@v1.0.0/go.mod -- +-module other.com/b +-go 1.14 - --import ( -- "bytes" -- "context" -- "fmt" +--- other.com/b@v1.0.0/b.go -- +-package b +-type B int +-func (B) F() {} +-` +- const src = ` +--- go.mod -- +-module example.com/a +-go 1.14 +-require other.com/b v1.0.0 - -- "golang.org/x/mod/modfile" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/internal/event" --) +--- go.sum -- +-other.com/b v1.0.0 h1:9WyCKS+BLAMRQM0CegP6zqP2beP+ShTbPaARpNY31II= +-other.com/b v1.0.0/go.mod h1:TgHQFucl04oGT+vrUm/liAzukYHNxCwKNkQZEyn3m9g= - --func Hover(ctx context.Context, snapshot source.Snapshot, fh source.FileHandle, position protocol.Position) (*protocol.Hover, error) { -- // We only provide hover information for the view's go.work file. -- if fh.URI() != snapshot.WorkFile() { -- return nil, nil -- } +--- a.go -- +-package a +-import "other.com/b" +-type I interface { F() } +-var _ b.B - -- ctx, done := event.Start(ctx, "work.Hover") -- defer done() +-` +- WithOptions( +- ProxyFiles(proxy), +- Modes(Default), // fails in 'experimental' mode +- ).Run(t, src, func(t *testing.T, env *Env) { +- // Enable to debug go.sum mismatch, which may appear as +- // "module lookup disabled by GOPROXY=off", confusingly. +- if false { +- env.DumpGoSum(".") +- } - -- // Get the position of the cursor. -- pw, err := snapshot.ParseWork(ctx, fh) -- if err != nil { -- return nil, fmt.Errorf("getting go.work file handle: %w", err) -- } -- offset, err := pw.Mapper.PositionOffset(position) -- if err != nil { -- return nil, fmt.Errorf("computing cursor offset: %w", err) -- } +- checkVendor := func(locs []protocol.Location, wantVendor bool) { +- if len(locs) != 1 { +- t.Errorf("got %d locations, want 1", len(locs)) +- } else if strings.Contains(string(locs[0].URI), "/vendor/") != wantVendor { +- t.Errorf("got location %s, wantVendor=%t", locs[0], wantVendor) +- } +- } - -- // Confirm that the cursor is inside a use statement, and then find -- // the position of the use statement's directory path. -- use, pathStart, pathEnd := usePath(pw, offset) +- env.OpenFile("a.go") +- refLoc := env.RegexpSearch("a.go", "I") // find "I" reference - -- // The cursor position is not on a use statement. -- if use == nil { -- return nil, nil -- } +- // Initially, a.I has one implementation b.B in +- // the module cache, not the vendor tree. +- checkVendor(env.Implementations(refLoc), false) - -- // Get the mod file denoted by the use. -- modfh, err := snapshot.ReadFile(ctx, modFileURI(pw, use)) -- if err != nil { -- return nil, fmt.Errorf("getting modfile handle: %w", err) -- } -- pm, err := snapshot.ParseMod(ctx, modfh) -- if err != nil { -- return nil, fmt.Errorf("getting modfile handle: %w", err) -- } -- if pm.File.Module == nil { -- return nil, fmt.Errorf("modfile has no module declaration") -- } -- mod := pm.File.Module.Mod +- // Run 'go mod vendor' outside the editor. +- env.RunGoCommand("mod", "vendor") - -- // Get the range to highlight for the hover. -- rng, err := pw.Mapper.OffsetRange(pathStart, pathEnd) -- if err != nil { -- return nil, err -- } -- options := snapshot.Options() -- return &protocol.Hover{ -- Contents: protocol.MarkupContent{ -- Kind: options.PreferredContentFormat, -- Value: mod.Path, -- }, -- Range: rng, -- }, nil --} +- // Synchronize changes to watched files. +- env.Await(env.DoneWithChangeWatchedFiles()) - --func usePath(pw *source.ParsedWorkFile, offset int) (use *modfile.Use, pathStart, pathEnd int) { -- for _, u := range pw.File.Use { -- path := []byte(u.Path) -- s, e := u.Syntax.Start.Byte, u.Syntax.End.Byte -- i := bytes.Index(pw.Mapper.Content[s:e], path) -- if i == -1 { -- // This should not happen. -- continue +- // Now, b.B is found in the vendor tree. +- checkVendor(env.Implementations(refLoc), true) +- +- // Delete the vendor tree. +- if err := os.RemoveAll(env.Sandbox.Workdir.AbsPath("vendor")); err != nil { +- t.Fatal(err) - } -- // Shift the start position to the location of the -- // module directory within the use statement. -- pathStart, pathEnd = s+i, s+i+len(path) -- if pathStart <= offset && offset <= pathEnd { -- return u, pathStart, pathEnd +- // Notify the server of the deletion. +- if err := env.Sandbox.Workdir.CheckForFileChanges(env.Ctx); err != nil { +- t.Fatal(err) - } -- } -- return nil, 0, 0 +- +- // Synchronize again. +- env.Await(env.DoneWithChangeWatchedFiles()) +- +- // b.B is once again defined in the module cache. +- checkVendor(env.Implementations(refLoc), false) +- }) -} -diff -urN a/gopls/internal/lsp/workspace.go b/gopls/internal/lsp/workspace.go ---- a/gopls/internal/lsp/workspace.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/workspace.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,106 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package lsp +-// This test can't be expressed as a marker test because the marker +-// test framework opens all files (which is a bit of a hack), creating +-// a <command-line-arguments> package for packages that otherwise +-// wouldn't be found from the go.work file. +-func TestReferencesFromWorkspacePackages59674(t *testing.T) { +- const src = ` +--- a/go.mod -- +-module example.com/a +-go 1.12 - --import ( -- "context" -- "fmt" -- "sync" +--- b/go.mod -- +-module example.com/b +-go 1.12 - -- "golang.org/x/tools/gopls/internal/lsp/cache" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/span" -- "golang.org/x/tools/internal/event" --) +--- c/go.mod -- +-module example.com/c +-go 1.12 - --func (s *Server) didChangeWorkspaceFolders(ctx context.Context, params *protocol.DidChangeWorkspaceFoldersParams) error { -- event := params.Event -- for _, folder := range event.Removed { -- view := s.session.ViewByName(folder.Name) -- if view != nil { -- s.session.RemoveView(view) -- } else { -- return fmt.Errorf("view %s for %v not found", folder.Name, folder.URI) -- } -- } -- return s.addFolders(ctx, event.Added) --} +--- lib/go.mod -- +-module example.com/lib +-go 1.12 - --// addView returns a Snapshot and a release function that must be --// called when it is no longer needed. --func (s *Server) addView(ctx context.Context, name string, uri span.URI) (source.Snapshot, func(), error) { -- s.stateMu.Lock() -- state := s.state -- s.stateMu.Unlock() -- if state < serverInitialized { -- return nil, nil, fmt.Errorf("addView called before server initialized") -- } -- options, err := s.fetchFolderOptions(ctx, uri) -- if err != nil { -- return nil, nil, err -- } -- folder := &cache.Folder{ -- Dir: uri, -- Name: name, -- Options: options, -- } -- _, snapshot, release, err := s.session.NewView(ctx, folder) -- return snapshot, release, err --} +--- go.work -- +-use ./a +-use ./b +-// don't use ./c +-use ./lib - --func (s *Server) didChangeConfiguration(ctx context.Context, _ *protocol.DidChangeConfigurationParams) error { -- ctx, done := event.Start(ctx, "lsp.Server.didChangeConfiguration") -- defer done() +--- a/a.go -- +-package a - -- // Apply any changes to the session-level settings. -- options, err := s.fetchFolderOptions(ctx, "") -- if err != nil { -- return err -- } -- s.SetOptions(options) +-import "example.com/lib" - -- // Collect options for all workspace folders. -- seen := make(map[span.URI]bool) -- for _, view := range s.session.Views() { -- if seen[view.Folder()] { -- continue -- } -- seen[view.Folder()] = true -- options, err := s.fetchFolderOptions(ctx, view.Folder()) -- if err != nil { -- return err -- } -- s.session.SetFolderOptions(ctx, view.Folder(), options) -- } +-var _ = lib.F // query here - -- var wg sync.WaitGroup -- for _, view := range s.session.Views() { -- view := view -- wg.Add(1) -- go func() { -- defer wg.Done() -- snapshot, release, err := view.Snapshot() -- if err != nil { -- return // view is shut down; no need to diagnose -- } -- defer release() -- s.diagnoseSnapshot(snapshot, nil, false, 0) -- }() -- } +--- b/b.go -- +-package b - -- if s.Options().VerboseWorkDoneProgress { -- work := s.progress.Start(ctx, DiagnosticWorkTitle(FromDidChangeConfiguration), "Calculating diagnostics...", nil, nil) -- go func() { -- wg.Wait() -- work.End(ctx, "Done.") -- }() -- } +-import "example.com/lib" - -- // An options change may have affected the detected Go version. -- s.checkViewGoVersions() +-var _ = lib.F // also found by references - -- return nil +--- c/c.go -- +-package c +- +-import "example.com/lib" +- +-var _ = lib.F // this reference should not be reported +- +--- lib/lib.go -- +-package lib +- +-func F() {} // declaration +-` +- Run(t, src, func(t *testing.T, env *Env) { +- env.OpenFile("a/a.go") +- refLoc := env.RegexpSearch("a/a.go", "F") +- got := fileLocations(env, env.References(refLoc)) +- want := []string{"a/a.go:5", "b/b.go:5", "lib/lib.go:3"} +- if diff := cmp.Diff(want, got); diff != "" { +- t.Errorf("incorrect References (-want +got):\n%s", diff) +- } +- }) -} -diff -urN a/gopls/internal/lsp/workspace_symbol.go b/gopls/internal/lsp/workspace_symbol.go ---- a/gopls/internal/lsp/workspace_symbol.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/lsp/workspace_symbol.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,38 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package lsp +-// Test an 'implementation' query on a type that implements 'error'. +-// (Unfortunately builtin locations cannot be expressed using @loc +-// in the marker test framework.) +-func TestImplementationsOfError(t *testing.T) { +- const src = ` +--- go.mod -- +-module example.com +-go 1.12 - --import ( -- "context" +--- a.go -- +-package a - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/telemetry" -- "golang.org/x/tools/internal/event" --) +-type Error2 interface { +- Error() string +-} - --func (s *Server) symbol(ctx context.Context, params *protocol.WorkspaceSymbolParams) (_ []protocol.SymbolInformation, rerr error) { -- recordLatency := telemetry.StartLatencyTimer("symbol") -- defer func() { -- recordLatency(ctx, rerr) -- }() +-type MyError int +-func (MyError) Error() string { return "" } - -- ctx, done := event.Start(ctx, "lsp.Server.symbol") -- defer done() +-type MyErrorPtr int +-func (*MyErrorPtr) Error() string { return "" } +-` +- Run(t, src, func(t *testing.T, env *Env) { +- env.OpenFile("a.go") - -- views := s.session.Views() -- matcher := s.Options().SymbolMatcher -- style := s.Options().SymbolStyle -- // TODO(rfindley): it looks wrong that we need to pass views here. -- // -- // Evidence: -- // - this is the only place we convert views to []source.View -- // - workspace symbols is the only place where we call source.View.Snapshot -- var sourceViews []source.View -- for _, v := range views { -- sourceViews = append(sourceViews, v) +- for _, test := range []struct { +- re string +- want []string +- }{ +- // error type +- {"Error2", []string{"a.go:10", "a.go:7", "std:builtin/builtin.go"}}, +- {"MyError", []string{"a.go:3", "std:builtin/builtin.go"}}, +- {"MyErrorPtr", []string{"a.go:3", "std:builtin/builtin.go"}}, +- // error.Error method +- {"(Error).. string", []string{"a.go:11", "a.go:8", "std:builtin/builtin.go"}}, +- {"MyError. (Error)", []string{"a.go:4", "std:builtin/builtin.go"}}, +- {"MyErrorPtr. (Error)", []string{"a.go:4", "std:builtin/builtin.go"}}, +- } { +- matchLoc := env.RegexpSearch("a.go", test.re) +- impls := env.Implementations(matchLoc) +- got := fileLocations(env, impls) +- if !reflect.DeepEqual(got, test.want) { +- t.Errorf("Implementations(%q) = %q, want %q", +- test.re, got, test.want) +- } +- } +- }) +-} +- +-// fileLocations returns a new sorted array of the +-// relative file name and line number of each location. +-// Duplicates are not removed. +-// Standard library filenames are abstracted for robustness. +-func fileLocations(env *integration.Env, locs []protocol.Location) []string { +- got := make([]string, 0, len(locs)) +- for _, loc := range locs { +- path := env.Sandbox.Workdir.URIToPath(loc.URI) // (slashified) +- if i := strings.LastIndex(path, "/src/"); i >= 0 && filepath.IsAbs(path) { +- // Absolute path with "src" segment: assume it's in GOROOT. +- // Strip directory and don't add line/column since they are fragile. +- path = "std:" + path[i+len("/src/"):] +- } else { +- path = fmt.Sprintf("%s:%d", path, loc.Range.Start.Line+1) +- } +- got = append(got, path) - } -- return source.WorkspaceSymbols(ctx, matcher, style, sourceViews, params.Query) +- sort.Strings(got) +- return got -} -diff -urN a/gopls/internal/regtest/bench/bench_test.go b/gopls/internal/regtest/bench/bench_test.go ---- a/gopls/internal/regtest/bench/bench_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/bench/bench_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,351 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/test/integration/misc/rename_test.go b/gopls/internal/test/integration/misc/rename_test.go +--- a/gopls/internal/test/integration/misc/rename_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/misc/rename_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,921 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package bench +-package misc - -import ( -- "bytes" -- "compress/gzip" -- "context" -- "flag" - "fmt" -- "io" -- "log" -- "os" -- "os/exec" -- "path/filepath" - "strings" -- "sync" - "testing" -- "time" - -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/hooks" -- "golang.org/x/tools/gopls/internal/lsp/cmd" -- "golang.org/x/tools/gopls/internal/lsp/command" -- "golang.org/x/tools/gopls/internal/lsp/fake" -- "golang.org/x/tools/gopls/internal/lsp/regtest" -- "golang.org/x/tools/internal/event" -- "golang.org/x/tools/internal/fakenet" -- "golang.org/x/tools/internal/jsonrpc2" -- "golang.org/x/tools/internal/jsonrpc2/servertest" -- "golang.org/x/tools/internal/pprof" -- "golang.org/x/tools/internal/tool" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/test/compare" +- . "golang.org/x/tools/gopls/internal/test/integration" -) - --var ( -- goplsPath = flag.String("gopls_path", "", "if set, use this gopls for testing; incompatible with -gopls_commit") -- -- installGoplsOnce sync.Once // guards installing gopls at -gopls_commit -- goplsCommit = flag.String("gopls_commit", "", "if set, install and use gopls at this commit for testing; incompatible with -gopls_path") +-func TestPrepareRenameMainPackage(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - -- cpuProfile = flag.String("gopls_cpuprofile", "", "if set, the cpu profile file suffix; see \"Profiling\" in the package doc") -- memProfile = flag.String("gopls_memprofile", "", "if set, the mem profile file suffix; see \"Profiling\" in the package doc") -- allocProfile = flag.String("gopls_allocprofile", "", "if set, the alloc profile file suffix; see \"Profiling\" in the package doc") -- trace = flag.String("gopls_trace", "", "if set, the trace file suffix; see \"Profiling\" in the package doc") +-go 1.18 +--- main.go -- +-package main - -- // If non-empty, tempDir is a temporary working dir that was created by this -- // test suite. -- makeTempDirOnce sync.Once // guards creation of the temp dir -- tempDir string +-import ( +- "fmt" -) - --// if runAsGopls is "true", run the gopls command instead of the testing.M. --const runAsGopls = "_GOPLS_BENCH_RUN_AS_GOPLS" -- --func TestMain(m *testing.M) { -- bug.PanicOnBugs = true -- if os.Getenv(runAsGopls) == "true" { -- tool.Main(context.Background(), cmd.New("gopls", "", nil, hooks.Options), os.Args[1:]) -- os.Exit(0) -- } -- event.SetExporter(nil) // don't log to stderr -- code := m.Run() -- if err := cleanup(); err != nil { -- fmt.Fprintf(os.Stderr, "cleaning up after benchmarks: %v\n", err) -- if code == 0 { -- code = 1 -- } -- } -- os.Exit(code) +-func main() { +- fmt.Println(1) -} +-` +- const wantErr = "can't rename package \"main\"" +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- loc := env.RegexpSearch("main.go", `main`) +- params := &protocol.PrepareRenameParams{ +- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), +- } +- _, err := env.Editor.Server.PrepareRename(env.Ctx, params) +- if err == nil { +- t.Errorf("missing can't rename package main error from PrepareRename") +- } - --// getTempDir returns the temporary directory to use for benchmark files, --// creating it if necessary. --func getTempDir() string { -- makeTempDirOnce.Do(func() { -- var err error -- tempDir, err = os.MkdirTemp("", "gopls-bench") -- if err != nil { -- log.Fatal(err) +- if err.Error() != wantErr { +- t.Errorf("got %v, want %v", err.Error(), wantErr) - } - }) -- return tempDir -} - --// shallowClone performs a shallow clone of repo into dir at the given --// 'commitish' ref (any commit reference understood by git). --// --// The directory dir must not already exist. --func shallowClone(dir, repo, commitish string) error { -- if err := os.Mkdir(dir, 0750); err != nil { -- return fmt.Errorf("creating dir for %s: %v", repo, err) -- } +-// Test case for golang/go#56227 +-func TestRenameWithUnsafeSlice(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - -- // Set a timeout for git fetch. If this proves flaky, it can be removed. -- ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) -- defer cancel() +-go 1.18 +--- p.go -- +-package p - -- // Use a shallow fetch to download just the relevant commit. -- shInit := fmt.Sprintf("git init && git fetch --depth=1 %q %q && git checkout FETCH_HEAD", repo, commitish) -- initCmd := exec.CommandContext(ctx, "/bin/sh", "-c", shInit) -- initCmd.Dir = dir -- if output, err := initCmd.CombinedOutput(); err != nil { -- return fmt.Errorf("checking out %s: %v\n%s", repo, err, output) -- } -- return nil +-import "unsafe" +- +-type T struct{} +- +-func (T) M() {} +- +-func _() { +- x := [3]int{1, 2, 3} +- ptr := unsafe.Pointer(&x) +- _ = unsafe.Slice((*int)(ptr), 3) -} +-` - --// connectEditor connects a fake editor session in the given dir, using the --// given editor config. --func connectEditor(dir string, config fake.EditorConfig, ts servertest.Connector) (*fake.Sandbox, *fake.Editor, *regtest.Awaiter, error) { -- s, err := fake.NewSandbox(&fake.SandboxConfig{ -- Workdir: dir, -- GOPROXY: "https://proxy.golang.org", +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("p.go") +- env.Rename(env.RegexpSearch("p.go", "M"), "N") // must not panic - }) -- if err != nil { -- return nil, nil, nil, err -- } +-} - -- a := regtest.NewAwaiter(s.Workdir) -- const skipApplyEdits = false -- editor, err := fake.NewEditor(s, config).Connect(context.Background(), ts, a.Hooks(), skipApplyEdits) -- if err != nil { -- return nil, nil, nil, err -- } +-func TestPrepareRenameWithNoPackageDeclaration(t *testing.T) { +- const files = ` +-go 1.14 +--- lib/a.go -- +-import "fmt" - -- return s, editor, a, nil +-const A = 1 +- +-func bar() { +- fmt.Println("Bar") -} - --// newGoplsConnector returns a connector that connects to a new gopls process, --// executed with the provided arguments. --func newGoplsConnector(args []string) (servertest.Connector, error) { -- if *goplsPath != "" && *goplsCommit != "" { -- panic("can't set both -gopls_path and -gopls_commit") -- } -- var ( -- goplsPath = *goplsPath -- env []string -- ) -- if *goplsCommit != "" { -- goplsPath = getInstalledGopls() -- } -- if goplsPath == "" { -- var err error -- goplsPath, err = os.Executable() -- if err != nil { -- return nil, err +--- main.go -- +-package main +- +-import "fmt" +- +-func main() { +- fmt.Println("Hello") +-} +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("lib/a.go") +- err := env.Editor.Rename(env.Ctx, env.RegexpSearch("lib/a.go", "fmt"), "fmt1") +- if got, want := fmt.Sprint(err), "no identifier found"; got != want { +- t.Errorf("Rename: got error %v, want %v", got, want) - } -- env = []string{fmt.Sprintf("%s=true", runAsGopls)} -- } -- return &SidecarServer{ -- goplsPath: goplsPath, -- env: env, -- args: args, -- }, nil +- }) -} - --// profileArgs returns additional command-line arguments to use when invoking --// gopls, to enable the user-requested profiles. --// --// If wantCPU is set, CPU profiling is enabled as well. Some tests may want to --// instrument profiling around specific critical sections of the benchmark, --// rather than the entire process. --// --// TODO(rfindley): like CPU, all of these would be better served by a custom --// command. Very rarely do we care about memory usage as the process exits: we --// care about specific points in time during the benchmark. mem and alloc --// should be snapshotted, and tracing should be bracketed around critical --// sections. --func profileArgs(name string, wantCPU bool) []string { -- var args []string -- if wantCPU && *cpuProfile != "" { -- args = append(args, fmt.Sprintf("-profile.cpu=%s", qualifiedName(name, *cpuProfile))) -- } -- if *memProfile != "" { -- args = append(args, fmt.Sprintf("-profile.mem=%s", qualifiedName(name, *memProfile))) -- } -- if *allocProfile != "" { -- args = append(args, fmt.Sprintf("-profile.alloc=%s", qualifiedName(name, *allocProfile))) -- } -- if *trace != "" { -- args = append(args, fmt.Sprintf("-profile.trace=%s", qualifiedName(name, *trace))) -- } -- return args --} +-func TestPrepareRenameFailWithUnknownModule(t *testing.T) { +- const files = ` +-go 1.14 +--- lib/a.go -- +-package lib - --func qualifiedName(args ...string) string { -- return strings.Join(args, ".") --} +-const A = 1 - --// getInstalledGopls builds gopls at the given -gopls_commit, returning the --// path to the gopls binary. --func getInstalledGopls() string { -- if *goplsCommit == "" { -- panic("must provide -gopls_commit") -- } -- toolsDir := filepath.Join(getTempDir(), "gopls_build") -- goplsPath := filepath.Join(toolsDir, "gopls", "gopls") +--- main.go -- +-package main - -- installGoplsOnce.Do(func() { -- log.Printf("installing gopls: checking out x/tools@%s into %s\n", *goplsCommit, toolsDir) -- if err := shallowClone(toolsDir, "https://go.googlesource.com/tools", *goplsCommit); err != nil { -- log.Fatal(err) -- } +-import ( +- "mod.com/lib" +-) - -- log.Println("installing gopls: building...") -- bld := exec.Command("go", "build", ".") -- bld.Dir = filepath.Join(toolsDir, "gopls") -- if output, err := bld.CombinedOutput(); err != nil { -- log.Fatalf("building gopls: %v\n%s", err, output) +-func main() { +- println("Hello") +-} +-` +- const wantErr = "can't rename package: missing module information for package" +- Run(t, files, func(t *testing.T, env *Env) { +- loc := env.RegexpSearch("lib/a.go", "lib") +- params := &protocol.PrepareRenameParams{ +- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), - } -- -- // Confirm that the resulting path now exists. -- if _, err := os.Stat(goplsPath); err != nil { -- log.Fatalf("os.Stat(%s): %v", goplsPath, err) +- _, err := env.Editor.Server.PrepareRename(env.Ctx, params) +- if err == nil || !strings.Contains(err.Error(), wantErr) { +- t.Errorf("missing cannot rename packages with unknown module from PrepareRename") - } - }) -- return goplsPath -} - --// A SidecarServer starts (and connects to) a separate gopls process at the --// given path. --type SidecarServer struct { -- goplsPath string -- env []string // additional environment bindings -- args []string // command-line arguments +-// This test ensures that each import of a renamed package +-// is also renamed if it would otherwise create a conflict. +-func TestRenamePackageWithConflicts(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com +- +-go 1.18 +--- lib/a.go -- +-package lib +- +-const A = 1 +- +--- lib/nested/a.go -- +-package nested +- +-const B = 1 +- +--- lib/x/a.go -- +-package nested1 +- +-const C = 1 +- +--- main.go -- +-package main +- +-import ( +- "mod.com/lib" +- "mod.com/lib/nested" +- nested1 "mod.com/lib/x" +-) +- +-func main() { +- println("Hello") -} +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("lib/a.go") +- env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested") - --// Connect creates new io.Pipes and binds them to the underlying StreamServer. --// --// It implements the servertest.Connector interface. --func (s *SidecarServer) Connect(ctx context.Context) jsonrpc2.Conn { -- // Note: don't use CommandContext here, as we want gopls to exit gracefully -- // in order to write out profile data. -- // -- // We close the connection on context cancelation below. -- cmd := exec.Command(s.goplsPath, s.args...) +- // Check if the new package name exists. +- env.RegexpSearch("nested/a.go", "package nested") +- env.RegexpSearch("main.go", `nested2 "mod.com/nested"`) +- env.RegexpSearch("main.go", "mod.com/nested/nested") +- env.RegexpSearch("main.go", `nested1 "mod.com/nested/x"`) +- }) +-} - -- stdin, err := cmd.StdinPipe() -- if err != nil { -- log.Fatal(err) -- } -- stdout, err := cmd.StdoutPipe() -- if err != nil { -- log.Fatal(err) -- } -- cmd.Stderr = os.Stderr -- cmd.Env = append(os.Environ(), s.env...) -- if err := cmd.Start(); err != nil { -- log.Fatalf("starting gopls: %v", err) -- } +-func TestRenamePackageWithAlias(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - -- go func() { -- // If we don't log.Fatal here, benchmarks may hang indefinitely if gopls -- // exits abnormally. -- // -- // TODO(rfindley): ideally we would shut down the connection gracefully, -- // but that doesn't currently work. -- if err := cmd.Wait(); err != nil { -- log.Fatalf("gopls invocation failed with error: %v", err) -- } -- }() +-go 1.18 +--- lib/a.go -- +-package lib - -- clientStream := jsonrpc2.NewHeaderStream(fakenet.NewConn("stdio", stdout, stdin)) -- clientConn := jsonrpc2.NewConn(clientStream) +-const A = 1 - -- go func() { -- select { -- case <-ctx.Done(): -- clientConn.Close() -- clientStream.Close() -- case <-clientConn.Done(): -- } -- }() +--- lib/nested/a.go -- +-package nested - -- return clientConn +-const B = 1 +- +--- main.go -- +-package main +- +-import ( +- "mod.com/lib" +- lib1 "mod.com/lib/nested" +-) +- +-func main() { +- println("Hello") -} +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("lib/a.go") +- env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested") - --// startProfileIfSupported checks to see if the remote gopls instance supports --// the start/stop profiling commands. If so, it starts profiling and returns a --// function that stops profiling and records the total CPU seconds sampled in the --// cpu_seconds benchmark metric. --// --// If the remote gopls instance does not support profiling commands, this --// function returns nil. --// --// If the supplied userSuffix is non-empty, the profile is written to --// <repo>.<userSuffix>, and not deleted when the benchmark exits. Otherwise, --// the profile is written to a temp file that is deleted after the cpu_seconds --// metric has been computed. --func startProfileIfSupported(b *testing.B, env *regtest.Env, name string) func() { -- if !env.Editor.HasCommand(command.StartProfile.ID()) { -- return nil -- } -- b.StopTimer() -- stopProfile := env.StartProfile() -- b.StartTimer() -- return func() { -- b.StopTimer() -- profFile := stopProfile() -- totalCPU, err := totalCPUForProfile(profFile) -- if err != nil { -- b.Fatalf("reading profile: %v", err) -- } -- b.ReportMetric(totalCPU.Seconds()/float64(b.N), "cpu_seconds/op") -- if *cpuProfile == "" { -- // The user didn't request profiles, so delete it to clean up. -- if err := os.Remove(profFile); err != nil { -- b.Errorf("removing profile file: %v", err) -- } -- } else { -- // NOTE: if this proves unreliable (due to e.g. EXDEV), we can fall back -- // on Read+Write+Remove. -- name := qualifiedName(name, *cpuProfile) -- if err := os.Rename(profFile, name); err != nil { -- b.Fatalf("renaming profile file: %v", err) -- } -- } -- } +- // Check if the new package name exists. +- env.RegexpSearch("nested/a.go", "package nested") +- env.RegexpSearch("main.go", "mod.com/nested") +- env.RegexpSearch("main.go", `lib1 "mod.com/nested/nested"`) +- }) -} - --// totalCPUForProfile reads the pprof profile with the given file name, parses, --// and aggregates the total CPU sampled during the profile. --func totalCPUForProfile(filename string) (time.Duration, error) { -- protoGz, err := os.ReadFile(filename) -- if err != nil { -- return 0, err -- } -- rd, err := gzip.NewReader(bytes.NewReader(protoGz)) -- if err != nil { -- return 0, fmt.Errorf("creating gzip reader for %s: %v", filename, err) -- } -- data, err := io.ReadAll(rd) -- if err != nil { -- return 0, fmt.Errorf("reading %s: %v", filename, err) -- } -- return pprof.TotalTime(data) +-func TestRenamePackageWithDifferentDirectoryPath(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com +- +-go 1.18 +--- lib/a.go -- +-package lib +- +-const A = 1 +- +--- lib/nested/a.go -- +-package foo +- +-const B = 1 +- +--- main.go -- +-package main +- +-import ( +- "mod.com/lib" +- foo "mod.com/lib/nested" +-) +- +-func main() { +- println("Hello") -} +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("lib/a.go") +- env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested") - --// closeBuffer stops the benchmark timer and closes the buffer with the given --// name. --// --// It may be used to clean up files opened in the shared environment during --// benchmarking. --func closeBuffer(b *testing.B, env *regtest.Env, name string) { -- b.StopTimer() -- env.CloseBuffer(name) -- env.AfterChange() -- b.StartTimer() +- // Check if the new package name exists. +- env.RegexpSearch("nested/a.go", "package nested") +- env.RegexpSearch("main.go", "mod.com/nested") +- env.RegexpSearch("main.go", `foo "mod.com/nested/nested"`) +- }) -} -diff -urN a/gopls/internal/regtest/bench/codeaction_test.go b/gopls/internal/regtest/bench/codeaction_test.go ---- a/gopls/internal/regtest/bench/codeaction_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/bench/codeaction_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,69 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package bench +-func TestRenamePackage(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - --import ( -- "fmt" -- "sync/atomic" -- "testing" +-go 1.18 +--- lib/a.go -- +-package lib - -- "golang.org/x/tools/gopls/internal/lsp/protocol" --) +-const A = 1 - --func BenchmarkCodeAction(b *testing.B) { -- for _, test := range didChangeTests { -- b.Run(test.repo, func(b *testing.B) { -- env := getRepo(b, test.repo).sharedEnv(b) -- env.OpenFile(test.file) -- defer closeBuffer(b, env, test.file) -- env.AfterChange() +--- lib/b.go -- +-package lib - -- env.CodeAction(test.file, nil) // pre-warm +-const B = 1 - -- b.ResetTimer() +--- lib/nested/a.go -- +-package nested - -- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, "hover")); stopAndRecord != nil { -- defer stopAndRecord() -- } +-const C = 1 - -- for i := 0; i < b.N; i++ { -- env.CodeAction(test.file, nil) -- } -- }) -- } +--- main.go -- +-package main +- +-import ( +- "mod.com/lib" +- "mod.com/lib/nested" +-) +- +-func main() { +- println("Hello") -} +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("lib/a.go") +- env.Rename(env.RegexpSearch("lib/a.go", "lib"), "lib1") - --func BenchmarkCodeActionFollowingEdit(b *testing.B) { -- for _, test := range didChangeTests { -- b.Run(test.repo, func(b *testing.B) { -- env := getRepo(b, test.repo).sharedEnv(b) -- env.OpenFile(test.file) -- defer closeBuffer(b, env, test.file) -- env.EditBuffer(test.file, protocol.TextEdit{NewText: "// __REGTEST_PLACEHOLDER_0__\n"}) -- env.AfterChange() +- // Check if the new package name exists. +- env.RegexpSearch("lib1/a.go", "package lib1") +- env.RegexpSearch("lib1/b.go", "package lib1") +- env.RegexpSearch("main.go", "mod.com/lib1") +- env.RegexpSearch("main.go", "mod.com/lib1/nested") +- }) +-} - -- env.CodeAction(test.file, nil) // pre-warm +-// Test for golang/go#47564. +-func TestRenameInTestVariant(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - -- b.ResetTimer() +-go 1.12 +--- stringutil/stringutil.go -- +-package stringutil - -- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, "hover")); stopAndRecord != nil { -- defer stopAndRecord() -- } +-func Identity(s string) string { +- return s +-} +--- stringutil/stringutil_test.go -- +-package stringutil - -- for i := 0; i < b.N; i++ { -- edits := atomic.AddInt64(&editID, 1) -- env.EditBuffer(test.file, protocol.TextEdit{ -- Range: protocol.Range{ -- Start: protocol.Position{Line: 0, Character: 0}, -- End: protocol.Position{Line: 1, Character: 0}, -- }, -- // Increment the placeholder text, to ensure cache misses. -- NewText: fmt.Sprintf("// __REGTEST_PLACEHOLDER_%d__\n", edits), -- }) -- env.CodeAction(test.file, nil) -- } -- }) +-func TestIdentity(t *testing.T) { +- if got := Identity("foo"); got != "foo" { +- t.Errorf("bad") - } -} -diff -urN a/gopls/internal/regtest/bench/completion_test.go b/gopls/internal/regtest/bench/completion_test.go ---- a/gopls/internal/regtest/bench/completion_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/bench/completion_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,290 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package bench +--- main.go -- +-package main - -import ( -- "flag" - "fmt" -- "sync/atomic" -- "testing" - -- "golang.org/x/tools/gopls/internal/lsp/fake" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" +- "mod.com/stringutil" -) - --// TODO(rfindley): update these completion tests to run on multiple repos. -- --type completionBenchOptions struct { -- file, locationRegexp string +-func main() { +- fmt.Println(stringutil.Identity("hello world")) +-} +-` - -- // Hooks to run edits before initial completion -- setup func(*Env) // run before the benchmark starts -- beforeCompletion func(*Env) // run before each completion +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- env.Rename(env.RegexpSearch("main.go", `stringutil\.(Identity)`), "Identityx") +- env.OpenFile("stringutil/stringutil_test.go") +- text := env.BufferText("stringutil/stringutil_test.go") +- if !strings.Contains(text, "Identityx") { +- t.Errorf("stringutil/stringutil_test.go: missing expected token `Identityx` after rename:\n%s", text) +- } +- }) -} - --func benchmarkCompletion(options completionBenchOptions, b *testing.B) { -- repo := getRepo(b, "tools") -- _ = repo.sharedEnv(b) // ensure cache is warm -- env := repo.newEnv(b, fake.EditorConfig{}, "completion", false) -- defer env.Close() +-// This is a test that rename operation initiated by the editor function as expected. +-func TestRenameFileFromEditor(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - -- // Run edits required for this completion. -- if options.setup != nil { -- options.setup(env) -- } +-go 1.16 +--- a/a.go -- +-package a - -- // Run a completion to make sure the system is warm. -- loc := env.RegexpSearch(options.file, options.locationRegexp) -- completions := env.Completion(loc) +-const X = 1 +--- a/x.go -- +-package a - -- if testing.Verbose() { -- fmt.Println("Results:") -- for i := 0; i < len(completions.Items); i++ { -- fmt.Printf("\t%d. %v\n", i, completions.Items[i]) -- } -- } +-const X = 2 +--- b/b.go -- +-package b +-` - -- b.Run("tools", func(b *testing.B) { -- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName("tools", "completion")); stopAndRecord != nil { -- defer stopAndRecord() -- } +- Run(t, files, func(t *testing.T, env *Env) { +- // Rename files and verify that diagnostics are affected accordingly. - -- for i := 0; i < b.N; i++ { -- if options.beforeCompletion != nil { -- options.beforeCompletion(env) -- } -- env.Completion(loc) +- // Initially, we should have diagnostics on both X's, for their duplicate declaration. +- env.OnceMet( +- InitialWorkspaceLoad, +- Diagnostics(env.AtRegexp("a/a.go", "X")), +- Diagnostics(env.AtRegexp("a/x.go", "X")), +- ) +- +- // Moving x.go should make the diagnostic go away. +- env.RenameFile("a/x.go", "b/x.go") +- env.AfterChange( +- NoDiagnostics(ForFile("a/a.go")), // no more duplicate declarations +- Diagnostics(env.AtRegexp("b/b.go", "package")), // as package names mismatch +- ) +- +- // Renaming should also work on open buffers. +- env.OpenFile("b/x.go") +- +- // Moving x.go back to a/ should cause the diagnostics to reappear. +- env.RenameFile("b/x.go", "a/x.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/a.go", "X")), +- Diagnostics(env.AtRegexp("a/x.go", "X")), +- ) +- +- // Renaming the entire directory should move both the open and closed file. +- env.RenameFile("a", "x") +- env.AfterChange( +- Diagnostics(env.AtRegexp("x/a.go", "X")), +- Diagnostics(env.AtRegexp("x/x.go", "X")), +- ) +- +- // As a sanity check, verify that x/x.go is open. +- if text := env.BufferText("x/x.go"); text == "" { +- t.Fatal("got empty buffer for x/x.go") - } - }) -} - --// endRangeInBuffer returns the position for last character in the buffer for --// the given file. --func endRangeInBuffer(env *Env, name string) protocol.Range { -- buffer := env.BufferText(name) -- m := protocol.NewMapper("", []byte(buffer)) -- rng, err := m.OffsetRange(len(buffer), len(buffer)) -- if err != nil { -- env.T.Fatal(err) -- } -- return rng --} +-func TestRenamePackage_Tests(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - --// Benchmark struct completion in tools codebase. --func BenchmarkStructCompletion(b *testing.B) { -- file := "internal/lsp/cache/session.go" +-go 1.18 +--- lib/a.go -- +-package lib - -- setup := func(env *Env) { -- env.OpenFile(file) -- env.EditBuffer(file, protocol.TextEdit{ -- Range: endRangeInBuffer(env, file), -- NewText: "\nvar testVariable map[string]bool = Session{}.\n", -- }) -- } +-const A = 1 - -- benchmarkCompletion(completionBenchOptions{ -- file: file, -- locationRegexp: `var testVariable map\[string\]bool = Session{}(\.)`, -- setup: setup, -- }, b) --} +--- lib/b.go -- +-package lib - --// Benchmark import completion in tools codebase. --func BenchmarkImportCompletion(b *testing.B) { -- const file = "internal/lsp/source/completion/completion.go" -- benchmarkCompletion(completionBenchOptions{ -- file: file, -- locationRegexp: `go\/()`, -- setup: func(env *Env) { env.OpenFile(file) }, -- }, b) --} +-const B = 1 - --// Benchmark slice completion in tools codebase. --func BenchmarkSliceCompletion(b *testing.B) { -- file := "internal/lsp/cache/session.go" +--- lib/a_test.go -- +-package lib_test - -- setup := func(env *Env) { -- env.OpenFile(file) -- env.EditBuffer(file, protocol.TextEdit{ -- Range: endRangeInBuffer(env, file), -- NewText: "\nvar testVariable []byte = \n", -- }) -- } +-import ( +- "mod.com/lib" +- "fmt +-) - -- benchmarkCompletion(completionBenchOptions{ -- file: file, -- locationRegexp: `var testVariable \[\]byte (=)`, -- setup: setup, -- }, b) --} +-const C = 1 - --// Benchmark deep completion in function call in tools codebase. --func BenchmarkFuncDeepCompletion(b *testing.B) { -- file := "internal/lsp/source/completion/completion.go" -- fileContent := ` --func (c *completer) _() { -- c.inference.kindMatches(c.) --} --` -- setup := func(env *Env) { -- env.OpenFile(file) -- originalBuffer := env.BufferText(file) -- env.EditBuffer(file, protocol.TextEdit{ -- Range: endRangeInBuffer(env, file), -- // TODO(rfindley): this is a bug: it should just be fileContent. -- NewText: originalBuffer + fileContent, -- }) -- } +--- lib/b_test.go -- +-package lib - -- benchmarkCompletion(completionBenchOptions{ -- file: file, -- locationRegexp: `func \(c \*completer\) _\(\) {\n\tc\.inference\.kindMatches\((c)`, -- setup: setup, -- }, b) --} +-import ( +- "fmt +-) - --type completionFollowingEditTest struct { -- repo string -- name string -- file string // repo-relative file to create -- content string // file content -- locationRegexp string // regexp for completion --} +-const D = 1 - --var completionFollowingEditTests = []completionFollowingEditTest{ -- { -- "tools", -- "selector", -- "internal/lsp/source/completion/completion2.go", -- ` --package completion +--- lib/nested/a.go -- +-package nested - --func (c *completer) _() { -- c.inference.kindMatches(c.) --} --`, -- `func \(c \*completer\) _\(\) {\n\tc\.inference\.kindMatches\((c)`, -- }, -- { -- "kubernetes", -- "selector", -- "pkg/kubelet/kubelet2.go", -- ` --package kubelet +-const D = 1 - --func (kl *Kubelet) _() { -- kl. --} --`, -- `kl\.()`, -- }, -- { -- "kubernetes", -- "identifier", -- "pkg/kubelet/kubelet2.go", -- ` --package kubelet +--- main.go -- +-package main - --func (kl *Kubelet) _() { -- k // here --} --`, -- `k() // here`, -- }, -- { -- "oracle", -- "selector", -- "dataintegration/pivot2.go", -- ` --package dataintegration +-import ( +- "mod.com/lib" +- "mod.com/lib/nested" +-) - --func (p *Pivot) _() { -- p. --} --`, -- `p\.()`, -- }, +-func main() { +- println("Hello") -} +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("lib/a.go") +- env.Rename(env.RegexpSearch("lib/a.go", "lib"), "lib1") - --// Benchmark completion following an arbitrary edit. --// --// Edits force type-checked packages to be invalidated, so we want to measure --// how long it takes before completion results are available. --func BenchmarkCompletionFollowingEdit(b *testing.B) { -- for _, test := range completionFollowingEditTests { -- b.Run(fmt.Sprintf("%s_%s", test.repo, test.name), func(b *testing.B) { -- for _, completeUnimported := range []bool{true, false} { -- b.Run(fmt.Sprintf("completeUnimported=%v", completeUnimported), func(b *testing.B) { -- for _, budget := range []string{"0s", "100ms"} { -- b.Run(fmt.Sprintf("budget=%s", budget), func(b *testing.B) { -- runCompletionFollowingEdit(b, test, completeUnimported, budget) -- }) -- } -- }) -- } -- }) -- } +- // Check if the new package name exists. +- env.RegexpSearch("lib1/a.go", "package lib1") +- env.RegexpSearch("lib1/b.go", "package lib1") +- env.RegexpSearch("main.go", "mod.com/lib1") +- env.RegexpSearch("main.go", "mod.com/lib1/nested") +- +- // Check if the test package is renamed +- env.RegexpSearch("lib1/a_test.go", "package lib1_test") +- env.RegexpSearch("lib1/b_test.go", "package lib1") +- }) -} - --var gomodcache = flag.String("gomodcache", "", "optional GOMODCACHE for unimported completion benchmarks") +-func TestRenamePackage_NestedModule(t *testing.T) { +- const files = ` +--- go.work -- +-go 1.18 +-use ( +- . +- ./foo/bar +- ./foo/baz +-) - --func runCompletionFollowingEdit(b *testing.B, test completionFollowingEditTest, completeUnimported bool, budget string) { -- repo := getRepo(b, test.repo) -- sharedEnv := repo.sharedEnv(b) // ensure cache is warm -- envvars := map[string]string{ -- "GOPATH": sharedEnv.Sandbox.GOPATH(), // use the warm cache -- } +--- go.mod -- +-module mod.com - -- if *gomodcache != "" { -- envvars["GOMODCACHE"] = *gomodcache -- } +-go 1.18 - -- env := repo.newEnv(b, fake.EditorConfig{ -- Env: envvars, -- Settings: map[string]interface{}{ -- "completeUnimported": completeUnimported, -- "completionBudget": budget, -- }, -- }, "completionFollowingEdit", false) -- defer env.Close() +-require ( +- mod.com/foo/bar v0.0.0 +-) - -- env.CreateBuffer(test.file, "// __REGTEST_PLACEHOLDER_0__\n"+test.content) -- editPlaceholder := func() { -- edits := atomic.AddInt64(&editID, 1) -- env.EditBuffer(test.file, protocol.TextEdit{ -- Range: protocol.Range{ -- Start: protocol.Position{Line: 0, Character: 0}, -- End: protocol.Position{Line: 1, Character: 0}, -- }, -- // Increment the placeholder text, to ensure cache misses. -- NewText: fmt.Sprintf("// __REGTEST_PLACEHOLDER_%d__\n", edits), -- }) -- } -- env.AfterChange() +-replace ( +- mod.com/foo/bar => ./foo/bar +- mod.com/foo/baz => ./foo/baz +-) +--- foo/foo.go -- +-package foo - -- // Run a completion to make sure the system is warm. -- loc := env.RegexpSearch(test.file, test.locationRegexp) -- completions := env.Completion(loc) +-import "fmt" - -- if testing.Verbose() { -- fmt.Println("Results:") -- for i, item := range completions.Items { -- fmt.Printf("\t%d. %v\n", i, item) -- } -- } +-func Bar() { +- fmt.Println("In foo before renamed to foox.") +-} - -- b.ResetTimer() +--- foo/bar/go.mod -- +-module mod.com/foo/bar - -- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, "completionFollowingEdit")); stopAndRecord != nil { -- defer stopAndRecord() -- } +--- foo/bar/bar.go -- +-package bar - -- for i := 0; i < b.N; i++ { -- editPlaceholder() -- loc := env.RegexpSearch(test.file, test.locationRegexp) -- env.Completion(loc) -- } --} -diff -urN a/gopls/internal/regtest/bench/definition_test.go b/gopls/internal/regtest/bench/definition_test.go ---- a/gopls/internal/regtest/bench/definition_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/bench/definition_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,46 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-const Msg = "Hi from package bar" - --package bench +--- foo/baz/go.mod -- +-module mod.com/foo/baz +- +--- foo/baz/baz.go -- +-package baz +- +-const Msg = "Hi from package baz" +- +--- main.go -- +-package main - -import ( -- "testing" +- "fmt" +- "mod.com/foo/bar" +- "mod.com/foo/baz" +- "mod.com/foo" -) - --func BenchmarkDefinition(b *testing.B) { -- tests := []struct { -- repo string -- file string -- regexp string -- }{ -- {"istio", "pkg/config/model.go", `gogotypes\.(MarshalAny)`}, -- {"google-cloud-go", "httpreplay/httpreplay.go", `proxy\.(ForRecording)`}, -- {"kubernetes", "pkg/controller/lookup_cache.go", `hashutil\.(DeepHashObject)`}, -- {"kuma", "api/generic/insights.go", `proto\.(Message)`}, -- {"pkgsite", "internal/log/log.go", `derrors\.(Wrap)`}, -- {"starlark", "starlark/eval.go", "prog.compiled.(Encode)"}, -- {"tools", "internal/lsp/cache/check.go", `(snapshot)\) buildKey`}, -- } +-func main() { +- foo.Bar() +- fmt.Println(bar.Msg) +- fmt.Println(baz.Msg) +-} +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("foo/foo.go") +- env.Rename(env.RegexpSearch("foo/foo.go", "foo"), "foox") +- +- env.RegexpSearch("foox/foo.go", "package foox") +- env.OpenFile("foox/bar/bar.go") +- env.OpenFile("foox/bar/go.mod") +- +- env.RegexpSearch("main.go", "mod.com/foo/bar") +- env.RegexpSearch("main.go", "mod.com/foox") +- env.RegexpSearch("main.go", "foox.Bar()") +- +- env.RegexpSearch("go.mod", "./foox/bar") +- env.RegexpSearch("go.mod", "./foox/baz") +- }) +-} - -- for _, test := range tests { -- b.Run(test.repo, func(b *testing.B) { -- env := getRepo(b, test.repo).sharedEnv(b) -- env.OpenFile(test.file) -- defer closeBuffer(b, env, test.file) +-func TestRenamePackage_DuplicateImport(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - -- loc := env.RegexpSearch(test.file, test.regexp) -- env.Await(env.DoneWithOpen()) -- env.GoToDefinition(loc) // pre-warm the query, and open the target file -- b.ResetTimer() +-go 1.18 +--- lib/a.go -- +-package lib - -- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, "definition")); stopAndRecord != nil { -- defer stopAndRecord() -- } +-const A = 1 - -- for i := 0; i < b.N; i++ { -- env.GoToDefinition(loc) // pre-warm the query -- } -- }) -- } --} -diff -urN a/gopls/internal/regtest/bench/didchange_test.go b/gopls/internal/regtest/bench/didchange_test.go ---- a/gopls/internal/regtest/bench/didchange_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/bench/didchange_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,142 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +--- lib/nested/a.go -- +-package nested - --package bench +-const B = 1 - --import ( -- "fmt" -- "sync/atomic" -- "testing" -- "time" +--- main.go -- +-package main - -- "golang.org/x/tools/gopls/internal/lsp/fake" -- "golang.org/x/tools/gopls/internal/lsp/protocol" +-import ( +- "mod.com/lib" +- lib1 "mod.com/lib" +- lib2 "mod.com/lib/nested" -) - --// Use a global edit counter as bench function may execute multiple times, and --// we want to avoid cache hits. Use time.Now to also avoid cache hits from the --// shared file cache. --var editID int64 = time.Now().UnixNano() -- --type changeTest struct { -- repo string -- file string -- canSave bool +-func main() { +- println("Hello") -} +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("lib/a.go") +- env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested") - --var didChangeTests = []changeTest{ -- {"google-cloud-go", "internal/annotate.go", true}, -- {"istio", "pkg/fuzz/util.go", true}, -- {"kubernetes", "pkg/controller/lookup_cache.go", true}, -- {"kuma", "api/generic/insights.go", true}, -- {"oracle", "dataintegration/data_type.go", false}, // diagnoseSave fails because this package is generated -- {"pkgsite", "internal/frontend/server.go", true}, -- {"starlark", "starlark/eval.go", true}, -- {"tools", "internal/lsp/cache/snapshot.go", true}, +- // Check if the new package name exists. +- env.RegexpSearch("nested/a.go", "package nested") +- env.RegexpSearch("main.go", "mod.com/nested") +- env.RegexpSearch("main.go", `lib1 "mod.com/nested"`) +- env.RegexpSearch("main.go", `lib2 "mod.com/nested/nested"`) +- }) -} - --// BenchmarkDidChange benchmarks modifications of a single file by making --// synthetic modifications in a comment. It controls pacing by waiting for the --// server to actually start processing the didChange notification before --// proceeding. Notably it does not wait for diagnostics to complete. --func BenchmarkDidChange(b *testing.B) { -- for _, test := range didChangeTests { -- b.Run(test.repo, func(b *testing.B) { -- env := getRepo(b, test.repo).sharedEnv(b) -- env.OpenFile(test.file) -- defer closeBuffer(b, env, test.file) +-func TestRenamePackage_DuplicateBlankImport(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - -- // Insert the text we'll be modifying at the top of the file. -- env.EditBuffer(test.file, protocol.TextEdit{NewText: "// __REGTEST_PLACEHOLDER_0__\n"}) -- env.AfterChange() -- b.ResetTimer() +-go 1.18 +--- lib/a.go -- +-package lib - -- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, "didchange")); stopAndRecord != nil { -- defer stopAndRecord() -- } +-const A = 1 - -- for i := 0; i < b.N; i++ { -- edits := atomic.AddInt64(&editID, 1) -- env.EditBuffer(test.file, protocol.TextEdit{ -- Range: protocol.Range{ -- Start: protocol.Position{Line: 0, Character: 0}, -- End: protocol.Position{Line: 1, Character: 0}, -- }, -- // Increment the placeholder text, to ensure cache misses. -- NewText: fmt.Sprintf("// __REGTEST_PLACEHOLDER_%d__\n", edits), -- }) -- env.Await(env.StartedChange()) -- } -- }) -- } --} +--- lib/nested/a.go -- +-package nested - --func BenchmarkDiagnoseChange(b *testing.B) { -- for _, test := range didChangeTests { -- runChangeDiagnosticsBenchmark(b, test, false, "diagnoseChange") -- } --} +-const B = 1 - --// TODO(rfindley): add a benchmark for with a metadata-affecting change, when --// this matters. --func BenchmarkDiagnoseSave(b *testing.B) { -- for _, test := range didChangeTests { -- runChangeDiagnosticsBenchmark(b, test, true, "diagnoseSave") -- } --} +--- main.go -- +-package main - --// runChangeDiagnosticsBenchmark runs a benchmark to edit the test file and --// await the resulting diagnostics pass. If save is set, the file is also saved. --func runChangeDiagnosticsBenchmark(b *testing.B, test changeTest, save bool, operation string) { -- b.Run(test.repo, func(b *testing.B) { -- if !test.canSave { -- b.Skipf("skipping as %s cannot be saved", test.file) -- } -- sharedEnv := getRepo(b, test.repo).sharedEnv(b) -- config := fake.EditorConfig{ -- Env: map[string]string{ -- "GOPATH": sharedEnv.Sandbox.GOPATH(), -- }, -- Settings: map[string]interface{}{ -- "diagnosticsDelay": "0s", -- }, -- } -- // Use a new env to avoid the diagnostic delay: we want to measure how -- // long it takes to produce the diagnostics. -- env := getRepo(b, test.repo).newEnv(b, config, operation, false) -- defer env.Close() -- env.OpenFile(test.file) -- // Insert the text we'll be modifying at the top of the file. -- env.EditBuffer(test.file, protocol.TextEdit{NewText: "// __REGTEST_PLACEHOLDER_0__\n"}) -- if save { -- env.SaveBuffer(test.file) -- } -- env.AfterChange() -- b.ResetTimer() +-import ( +- "mod.com/lib" +- _ "mod.com/lib" +- lib1 "mod.com/lib/nested" +-) - -- // We must use an extra subtest layer here, so that we only set up the -- // shared env once (otherwise we pay additional overhead and the profiling -- // flags don't work). -- b.Run("diagnose", func(b *testing.B) { -- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, operation)); stopAndRecord != nil { -- defer stopAndRecord() -- } -- for i := 0; i < b.N; i++ { -- edits := atomic.AddInt64(&editID, 1) -- env.EditBuffer(test.file, protocol.TextEdit{ -- Range: protocol.Range{ -- Start: protocol.Position{Line: 0, Character: 0}, -- End: protocol.Position{Line: 1, Character: 0}, -- }, -- // Increment the placeholder text, to ensure cache misses. -- NewText: fmt.Sprintf("// __REGTEST_PLACEHOLDER_%d__\n", edits), -- }) -- if save { -- env.SaveBuffer(test.file) -- } -- env.AfterChange() -- } -- }) +-func main() { +- println("Hello") +-} +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("lib/a.go") +- env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested") +- +- // Check if the new package name exists. +- env.RegexpSearch("nested/a.go", "package nested") +- env.RegexpSearch("main.go", "mod.com/nested") +- env.RegexpSearch("main.go", `_ "mod.com/nested"`) +- env.RegexpSearch("main.go", `lib1 "mod.com/nested/nested"`) - }) -} -diff -urN a/gopls/internal/regtest/bench/doc.go b/gopls/internal/regtest/bench/doc.go ---- a/gopls/internal/regtest/bench/doc.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/bench/doc.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,40 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --// The bench package implements benchmarks for various LSP operations. --// --// Benchmarks check out specific commits of popular and/or exemplary --// repositories, and script an external gopls process via a fake text editor. --// By default, benchmarks run the test executable as gopls (using a special --// "gopls mode" environment variable). A different gopls binary may be used by --// setting the -gopls_path or -gopls_commit flags. --// --// This package is a work in progress. --// --// # Profiling --// --// Benchmark functions run gopls in a separate process, which means the normal --// test flags for profiling aren't useful. Instead the -gopls_cpuprofile, --// -gopls_memprofile, -gopls_allocprofile, and -gopls_trace flags may be used --// to pass through profiling to the gopls subproces. --// --// Each of these flags sets a suffix for the respective gopls profile, which is --// named according to the schema <repo>.<operation>.<suffix>. For example, --// setting -gopls_cpuprofile=cpu will result in profiles named tools.iwl.cpu, --// tools.rename.cpu, etc. In some cases, these profiles are for the entire --// gopls subprocess (as in the initial workspace load), whereas in others they --// span only the critical section of the benchmark. It is up to each benchmark --// to implement profiling as appropriate. --// --// # Integration with perf.golang.org --// --// Benchmarks that run with -short are automatically tracked by --// perf.golang.org, at --// https://perf.golang.org/dashboard/?benchmark=all&repository=tools&branch=release-branch.go1.20 --// --// # TODO --// - add more benchmarks, and more repositories --// - fix the perf dashboard to not require the branch= parameter --// - improve this documentation --package bench -diff -urN a/gopls/internal/regtest/bench/hover_test.go b/gopls/internal/regtest/bench/hover_test.go ---- a/gopls/internal/regtest/bench/hover_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/bench/hover_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,47 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-func TestRenamePackage_TestVariant(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - --package bench +-go 1.12 +--- foo/foo.go -- +-package foo - --import ( -- "testing" --) +-const Foo = 42 +--- bar/bar.go -- +-package bar - --func BenchmarkHover(b *testing.B) { -- tests := []struct { -- repo string -- file string -- regexp string -- }{ -- {"google-cloud-go", "httpreplay/httpreplay.go", `proxy\.(ForRecording)`}, -- {"istio", "pkg/config/model.go", `gogotypes\.(MarshalAny)`}, -- {"kubernetes", "pkg/apis/core/types.go", "type (Pod)"}, -- {"kuma", "api/generic/insights.go", `proto\.(Message)`}, -- {"pkgsite", "internal/log/log.go", `derrors\.(Wrap)`}, -- {"starlark", "starlark/eval.go", "prog.compiled.(Encode)"}, -- {"tools", "internal/lsp/cache/check.go", `(snapshot)\) buildKey`}, -- } +-import "mod.com/foo" - -- for _, test := range tests { -- b.Run(test.repo, func(b *testing.B) { -- env := getRepo(b, test.repo).sharedEnv(b) -- env.OpenFile(test.file) -- defer closeBuffer(b, env, test.file) +-const Bar = foo.Foo +--- bar/bar_test.go -- +-package bar - -- loc := env.RegexpSearch(test.file, test.regexp) -- env.AfterChange() +-import "mod.com/foo" - -- env.Hover(loc) // pre-warm the query -- b.ResetTimer() +-const Baz = foo.Foo +--- testdata/bar/bar.go -- +-package bar - -- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, "hover")); stopAndRecord != nil { -- defer stopAndRecord() -- } +-import "mod.com/foox" - -- for i := 0; i < b.N; i++ { -- env.Hover(loc) // pre-warm the query -- } -- }) -- } +-const Bar = foox.Foo +--- testdata/bar/bar_test.go -- +-package bar +- +-import "mod.com/foox" +- +-const Baz = foox.Foo +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("foo/foo.go") +- env.Rename(env.RegexpSearch("foo/foo.go", "package (foo)"), "foox") +- +- checkTestdata(t, env) +- }) -} -diff -urN a/gopls/internal/regtest/bench/implementations_test.go b/gopls/internal/regtest/bench/implementations_test.go ---- a/gopls/internal/regtest/bench/implementations_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/bench/implementations_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,44 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package bench +-func TestRenamePackage_IntermediateTestVariant(t *testing.T) { +- // In this test set up, we have the following import edges: +- // bar_test -> baz -> foo -> bar +- // bar_test -> foo -> bar +- // bar_test -> bar +- // +- // As a consequence, bar_x_test.go is in the reverse closure of both +- // `foo [bar.test]` and `baz [bar.test]`. This test confirms that we don't +- // produce duplicate edits in this case. +- const files = ` +--- go.mod -- +-module foo.mod - --import "testing" +-go 1.12 +--- foo/foo.go -- +-package foo - --func BenchmarkImplementations(b *testing.B) { -- tests := []struct { -- repo string -- file string -- regexp string -- }{ -- {"google-cloud-go", "httpreplay/httpreplay.go", `type (Recorder)`}, -- {"istio", "pkg/config/mesh/watcher.go", `type (Watcher)`}, -- {"kubernetes", "pkg/controller/lookup_cache.go", `objectWithMeta`}, -- {"kuma", "api/generic/insights.go", `type (Insight)`}, -- {"pkgsite", "internal/datasource.go", `type (DataSource)`}, -- {"starlark", "syntax/syntax.go", `type (Expr)`}, -- {"tools", "internal/lsp/source/view.go", `type (Snapshot)`}, -- } +-import "foo.mod/bar" - -- for _, test := range tests { -- b.Run(test.repo, func(b *testing.B) { -- env := getRepo(b, test.repo).sharedEnv(b) -- env.OpenFile(test.file) -- defer closeBuffer(b, env, test.file) +-const Foo = 42 - -- loc := env.RegexpSearch(test.file, test.regexp) -- env.AfterChange() -- env.Implementations(loc) // pre-warm the query -- b.ResetTimer() +-const _ = bar.Bar +--- baz/baz.go -- +-package baz - -- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, "implementations")); stopAndRecord != nil { -- defer stopAndRecord() -- } +-import "foo.mod/foo" - -- for i := 0; i < b.N; i++ { -- env.Implementations(loc) -- } -- }) -- } --} -diff -urN a/gopls/internal/regtest/bench/iwl_test.go b/gopls/internal/regtest/bench/iwl_test.go ---- a/gopls/internal/regtest/bench/iwl_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/bench/iwl_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,76 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-const Baz = foo.Foo +--- bar/bar.go -- +-package bar - --package bench +-var Bar = 123 +--- bar/bar_test.go -- +-package bar - --import ( -- "testing" +-const _ = Bar +--- bar/bar_x_test.go -- +-package bar_test - -- "golang.org/x/tools/gopls/internal/lsp/command" -- "golang.org/x/tools/gopls/internal/lsp/fake" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" +-import ( +- "foo.mod/bar" +- "foo.mod/baz" +- "foo.mod/foo" -) - --// BenchmarkInitialWorkspaceLoad benchmarks the initial workspace load time for --// a new editing session. --func BenchmarkInitialWorkspaceLoad(b *testing.B) { -- tests := []struct { -- repo string -- file string -- }{ -- {"google-cloud-go", "httpreplay/httpreplay.go"}, -- {"istio", "pkg/fuzz/util.go"}, -- {"kubernetes", "pkg/controller/lookup_cache.go"}, -- {"kuma", "api/generic/insights.go"}, -- {"oracle", "dataintegration/data_type.go"}, -- {"pkgsite", "internal/frontend/server.go"}, -- {"starlark", "starlark/eval.go"}, -- {"tools", "internal/lsp/cache/snapshot.go"}, -- {"hashiform", "internal/provider/provider.go"}, -- } +-const _ = bar.Bar + baz.Baz + foo.Foo +--- testdata/foox/foo.go -- +-package foox - -- for _, test := range tests { -- b.Run(test.repo, func(b *testing.B) { -- repo := getRepo(b, test.repo) -- // get the (initialized) shared env to ensure the cache is warm. -- // Reuse its GOPATH so that we get cache hits for things in the module -- // cache. -- sharedEnv := repo.sharedEnv(b) -- b.ResetTimer() +-import "foo.mod/bar" - -- for i := 0; i < b.N; i++ { -- doIWL(b, sharedEnv.Sandbox.GOPATH(), repo, test.file) -- } -- }) -- } --} +-const Foo = 42 - --func doIWL(b *testing.B, gopath string, repo *repo, file string) { -- // Exclude the time to set up the env from the benchmark time, as this may -- // involve installing gopls and/or checking out the repo dir. -- b.StopTimer() -- config := fake.EditorConfig{Env: map[string]string{"GOPATH": gopath}} -- env := repo.newEnv(b, config, "iwl", true) -- defer env.Close() -- b.StartTimer() +-const _ = bar.Bar +--- testdata/baz/baz.go -- +-package baz - -- // Note: in the future, we may need to open a file in order to cause gopls to -- // start loading the workspace. +-import "foo.mod/foox" - -- env.Await(InitialWorkspaceLoad) +-const Baz = foox.Foo +--- testdata/bar/bar_x_test.go -- +-package bar_test - -- if env.Editor.HasCommand(command.MemStats.ID()) { -- b.StopTimer() -- params := &protocol.ExecuteCommandParams{ -- Command: command.MemStats.ID(), -- } -- var memstats command.MemStatsResult -- env.ExecuteCommand(params, &memstats) -- b.ReportMetric(float64(memstats.HeapAlloc), "alloc_bytes") -- b.ReportMetric(float64(memstats.HeapInUse), "in_use_bytes") -- b.ReportMetric(float64(memstats.TotalAlloc), "total_alloc_bytes") -- b.StartTimer() -- } +-import ( +- "foo.mod/bar" +- "foo.mod/baz" +- "foo.mod/foox" +-) +- +-const _ = bar.Bar + baz.Baz + foox.Foo +-` +- +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("foo/foo.go") +- env.Rename(env.RegexpSearch("foo/foo.go", "package (foo)"), "foox") +- +- checkTestdata(t, env) +- }) -} -diff -urN a/gopls/internal/regtest/bench/references_test.go b/gopls/internal/regtest/bench/references_test.go ---- a/gopls/internal/regtest/bench/references_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/bench/references_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,44 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package bench +-func TestRenamePackage_Nesting(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - --import "testing" +-go 1.18 +--- lib/a.go -- +-package lib - --func BenchmarkReferences(b *testing.B) { -- tests := []struct { -- repo string -- file string -- regexp string -- }{ -- {"google-cloud-go", "httpreplay/httpreplay.go", `func (NewRecorder)`}, -- {"istio", "pkg/config/model.go", "type (Meta)"}, -- {"kubernetes", "pkg/controller/lookup_cache.go", "type (objectWithMeta)"}, // TODO: choose an exported identifier -- {"kuma", "pkg/events/interfaces.go", "type (Event)"}, -- {"pkgsite", "internal/log/log.go", "func (Infof)"}, -- {"starlark", "syntax/syntax.go", "type (Ident)"}, -- {"tools", "internal/lsp/source/view.go", "type (Snapshot)"}, -- } +-import "mod.com/lib/nested" - -- for _, test := range tests { -- b.Run(test.repo, func(b *testing.B) { -- env := getRepo(b, test.repo).sharedEnv(b) -- env.OpenFile(test.file) -- defer closeBuffer(b, env, test.file) +-const A = 1 + nested.B +--- lib/nested/a.go -- +-package nested +- +-const B = 1 +--- other/other.go -- +-package other - -- loc := env.RegexpSearch(test.file, test.regexp) -- env.AfterChange() -- env.References(loc) // pre-warm the query -- b.ResetTimer() +-import ( +- "mod.com/lib" +- "mod.com/lib/nested" +-) - -- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, "references")); stopAndRecord != nil { -- defer stopAndRecord() -- } +-const C = lib.A + nested.B +--- testdata/libx/a.go -- +-package libx - -- for i := 0; i < b.N; i++ { -- env.References(loc) -- } -- }) -- } --} -diff -urN a/gopls/internal/regtest/bench/reload_test.go b/gopls/internal/regtest/bench/reload_test.go ---- a/gopls/internal/regtest/bench/reload_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/bench/reload_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,52 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. --package bench +-import "mod.com/libx/nested" - --import ( -- "testing" +-const A = 1 + nested.B +--- testdata/other/other.go -- +-package other - -- . "golang.org/x/tools/gopls/internal/lsp/regtest" +-import ( +- "mod.com/libx" +- "mod.com/libx/nested" -) - --// BenchmarkReload benchmarks reloading a file metadata after a change to an import. --// --// This ensures we are able to diagnose a changed file without reloading all --// invalidated packages. See also golang/go#61344 --func BenchmarkReload(b *testing.B) { -- // TODO(rfindley): add more tests, make this test table-driven -- const ( -- repo = "kubernetes" -- // pkg/util/hash is transitively imported by a large number of packages. -- // We should not need to reload those packages to get a diagnostic. -- file = "pkg/util/hash/hash.go" -- ) -- b.Run(repo, func(b *testing.B) { -- env := getRepo(b, repo).sharedEnv(b) +-const C = libx.A + nested.B +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("lib/a.go") +- env.Rename(env.RegexpSearch("lib/a.go", "package (lib)"), "libx") - -- env.OpenFile(file) -- defer closeBuffer(b, env, file) +- checkTestdata(t, env) +- }) +-} - -- env.AfterChange() +-func TestRenamePackage_InvalidName(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - -- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(repo, "reload")); stopAndRecord != nil { -- defer stopAndRecord() -- } +-go 1.18 +--- lib/a.go -- +-package lib - -- b.ResetTimer() -- for i := 0; i < b.N; i++ { -- // Change the "hash" import. This may result in cache hits, but that's -- // OK: the goal is to ensure that we don't reload more than just the -- // current package. -- env.RegexpReplace(file, `"hash"`, `"hashx"`) -- // Note: don't use env.AfterChange() here: we only want to await the -- // first diagnostic. -- // -- // Awaiting a full diagnosis would await diagnosing everything, which -- // would require reloading everything. -- env.Await(Diagnostics(ForFile(file))) -- env.RegexpReplace(file, `"hashx"`, `"hash"`) -- env.Await(NoDiagnostics(ForFile(file))) +-import "mod.com/lib/nested" +- +-const A = 1 + nested.B +-` +- +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("lib/a.go") +- loc := env.RegexpSearch("lib/a.go", "package (lib)") +- +- for _, badName := range []string{"$$$", "lib_test"} { +- if err := env.Editor.Rename(env.Ctx, loc, badName); err == nil { +- t.Errorf("Rename(lib, libx) succeeded, want non-nil error") +- } - } - }) -} -diff -urN a/gopls/internal/regtest/bench/rename_test.go b/gopls/internal/regtest/bench/rename_test.go ---- a/gopls/internal/regtest/bench/rename_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/bench/rename_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,49 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package bench +-func TestRenamePackage_InternalPackage(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com +- +-go 1.18 +--- lib/a.go -- +-package lib - -import ( - "fmt" -- "testing" +- "mod.com/lib/internal/x" -) - --func BenchmarkRename(b *testing.B) { -- tests := []struct { -- repo string -- file string -- regexp string -- baseName string -- }{ -- {"google-cloud-go", "httpreplay/httpreplay.go", `func (NewRecorder)`, "NewRecorder"}, -- {"istio", "pkg/config/model.go", `(Namespace) string`, "Namespace"}, -- {"kubernetes", "pkg/controller/lookup_cache.go", `hashutil\.(DeepHashObject)`, "DeepHashObject"}, -- {"kuma", "pkg/events/interfaces.go", `Delete`, "Delete"}, -- {"pkgsite", "internal/log/log.go", `func (Infof)`, "Infof"}, -- {"starlark", "starlark/eval.go", `Program\) (Filename)`, "Filename"}, -- {"tools", "internal/lsp/cache/snapshot.go", `meta \*(metadataGraph)`, "metadataGraph"}, -- } +-const A = 1 - -- for _, test := range tests { -- names := 0 // bench function may execute multiple times -- b.Run(test.repo, func(b *testing.B) { -- env := getRepo(b, test.repo).sharedEnv(b) -- env.OpenFile(test.file) -- loc := env.RegexpSearch(test.file, test.regexp) -- env.Await(env.DoneWithOpen()) -- env.Rename(loc, test.baseName+"X") // pre-warm the query -- b.ResetTimer() +-func print() { +- fmt.Println(x.B) +-} - -- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, "rename")); stopAndRecord != nil { -- defer stopAndRecord() -- } +--- lib/internal/x/a.go -- +-package x - -- for i := 0; i < b.N; i++ { -- names++ -- newName := fmt.Sprintf("%s%d", test.baseName, names) -- env.Rename(loc, newName) -- } -- }) +-const B = 1 +- +--- main.go -- +-package main +- +-import "mod.com/lib" +- +-func main() { +- lib.print() +-} +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("lib/internal/x/a.go") +- env.Rename(env.RegexpSearch("lib/internal/x/a.go", "x"), "utils") +- +- // Check if the new package name exists. +- env.RegexpSearch("lib/a.go", "mod.com/lib/internal/utils") +- env.RegexpSearch("lib/a.go", "utils.B") +- +- // Check if the test package is renamed +- env.RegexpSearch("lib/internal/utils/a.go", "package utils") +- +- env.OpenFile("lib/a.go") +- env.Rename(env.RegexpSearch("lib/a.go", "lib"), "lib1") +- +- // Check if the new package name exists. +- env.RegexpSearch("lib1/a.go", "package lib1") +- env.RegexpSearch("lib1/a.go", "mod.com/lib1/internal/utils") +- env.RegexpSearch("main.go", `import "mod.com/lib1"`) +- env.RegexpSearch("main.go", "lib1.print()") +- }) +-} +- +-// checkTestdata checks that current buffer contents match their corresponding +-// expected content in the testdata directory. +-func checkTestdata(t *testing.T, env *Env) { +- t.Helper() +- files := env.ListFiles("testdata") +- if len(files) == 0 { +- t.Fatal("no files in testdata directory") +- } +- for _, file := range files { +- suffix := strings.TrimPrefix(file, "testdata/") +- got := env.BufferText(suffix) +- want := env.ReadWorkspaceFile(file) +- if diff := compare.Text(want, got); diff != "" { +- t.Errorf("Rename: unexpected buffer content for %s (-want +got):\n%s", suffix, diff) +- } - } -} -diff -urN a/gopls/internal/regtest/bench/repo_test.go b/gopls/internal/regtest/bench/repo_test.go ---- a/gopls/internal/regtest/bench/repo_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/bench/repo_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,290 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/test/integration/misc/semantictokens_test.go b/gopls/internal/test/integration/misc/semantictokens_test.go +--- a/gopls/internal/test/integration/misc/semantictokens_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/misc/semantictokens_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,239 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package bench +-package misc - -import ( -- "bytes" -- "context" -- "errors" -- "flag" - "fmt" -- "log" -- "os" -- "path/filepath" -- "sync" +- "strings" - "testing" -- "time" - -- "golang.org/x/tools/gopls/internal/lsp/fake" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" +- "github.com/google/go-cmp/cmp" +- "golang.org/x/tools/gopls/internal/protocol" +- . "golang.org/x/tools/gopls/internal/test/integration" +- "golang.org/x/tools/gopls/internal/test/integration/fake" -) - --// repos holds shared repositories for use in benchmarks. --// --// These repos were selected to represent a variety of different types of --// codebases. --var repos = map[string]*repo{ -- // google-cloud-go has 145 workspace modules (!), and is quite large. -- "google-cloud-go": { -- name: "google-cloud-go", -- url: "https://github.com/googleapis/google-cloud-go.git", -- commit: "07da765765218debf83148cc7ed8a36d6e8921d5", -- inDir: flag.String("cloud_go_dir", "", "if set, reuse this directory as google-cloud-go@07da7657"), -- }, -- -- // Used by x/benchmarks; large. -- "istio": { -- name: "istio", -- url: "https://github.com/istio/istio", -- commit: "1.17.0", -- inDir: flag.String("istio_dir", "", "if set, reuse this directory as istio@v1.17.0"), -- }, -- -- // Kubernetes is a large repo with many dependencies, and in the past has -- // been about as large a repo as gopls could handle. -- "kubernetes": { -- name: "kubernetes", -- url: "https://github.com/kubernetes/kubernetes", -- commit: "v1.24.0", -- short: true, -- inDir: flag.String("kubernetes_dir", "", "if set, reuse this directory as kubernetes@v1.24.0"), -- }, -- -- // A large, industrial application. -- "kuma": { -- name: "kuma", -- url: "https://github.com/kumahq/kuma", -- commit: "2.1.1", -- inDir: flag.String("kuma_dir", "", "if set, reuse this directory as kuma@v2.1.1"), -- }, +-func TestBadURICrash_VSCodeIssue1498(t *testing.T) { +- const src = ` +--- go.mod -- +-module example.com - -- // A repo containing a very large package (./dataintegration). -- "oracle": { -- name: "oracle", -- url: "https://github.com/oracle/oci-go-sdk.git", -- commit: "v65.43.0", -- short: true, -- inDir: flag.String("oracle_dir", "", "if set, reuse this directory as oracle/oci-go-sdk@v65.43.0"), -- }, +-go 1.12 - -- // x/pkgsite is familiar and represents a common use case (a webserver). It -- // also has a number of static non-go files and template files. -- "pkgsite": { -- name: "pkgsite", -- url: "https://go.googlesource.com/pkgsite", -- commit: "81f6f8d4175ad0bf6feaa03543cc433f8b04b19b", -- short: true, -- inDir: flag.String("pkgsite_dir", "", "if set, reuse this directory as pkgsite@81f6f8d4"), -- }, +--- main.go -- +-package main - -- // A tiny self-contained project. -- "starlark": { -- name: "starlark", -- url: "https://github.com/google/starlark-go", -- commit: "3f75dec8e4039385901a30981e3703470d77e027", -- short: true, -- inDir: flag.String("starlark_dir", "", "if set, reuse this directory as starlark@3f75dec8"), -- }, +-func main() {} - -- // The current repository, which is medium-small and has very few dependencies. -- "tools": { -- name: "tools", -- url: "https://go.googlesource.com/tools", -- commit: "gopls/v0.9.0", -- short: true, -- inDir: flag.String("tools_dir", "", "if set, reuse this directory as x/tools@v0.9.0"), -- }, +-` +- WithOptions( +- Modes(Default), +- Settings{"allExperiments": true}, +- ).Run(t, src, func(t *testing.T, env *Env) { +- params := &protocol.SemanticTokensParams{} +- const badURI = "http://foo" +- params.TextDocument.URI = badURI +- // This call panicked in the past: golang/vscode-go#1498. +- _, err := env.Editor.Server.SemanticTokensFull(env.Ctx, params) - -- // A repo of similar size to kubernetes, but with substantially more -- // complex types that led to a serious performance regression (issue #60621). -- "hashiform": { -- name: "hashiform", -- url: "https://github.com/hashicorp/terraform-provider-aws", -- commit: "ac55de2b1950972d93feaa250d7505d9ed829c7c", -- inDir: flag.String("hashiform_dir", "", "if set, reuse this directory as hashiform@ac55de2"), -- }, +- // Requests to an invalid URI scheme now result in an LSP error. +- got := fmt.Sprint(err) +- want := `DocumentURI scheme is not 'file': http://foo` +- if !strings.Contains(got, want) { +- t.Errorf("SemanticTokensFull error is %v, want substring %q", got, want) +- } +- }) -} - --// getRepo gets the requested repo, and skips the test if -short is set and --// repo is not configured as a short repo. --func getRepo(tb testing.TB, name string) *repo { -- tb.Helper() -- repo := repos[name] -- if repo == nil { -- tb.Fatalf("repo %s does not exist", name) -- } -- if !repo.short && testing.Short() { -- tb.Skipf("large repo %s does not run with -short", repo.name) +-// fix bug involving type parameters and regular parameters +-// (golang/vscode-go#2527) +-func TestSemantic_2527(t *testing.T) { +- // these are the expected types of identifiers in text order +- want := []fake.SemanticToken{ +- {Token: "package", TokenType: "keyword"}, +- {Token: "foo", TokenType: "namespace"}, +- {Token: "// Deprecated (for testing)", TokenType: "comment"}, +- {Token: "func", TokenType: "keyword"}, +- {Token: "Add", TokenType: "function", Mod: "definition deprecated"}, +- {Token: "T", TokenType: "typeParameter", Mod: "definition"}, +- {Token: "int", TokenType: "type", Mod: "defaultLibrary"}, +- {Token: "target", TokenType: "parameter", Mod: "definition"}, +- {Token: "T", TokenType: "typeParameter"}, +- {Token: "l", TokenType: "parameter", Mod: "definition"}, +- {Token: "T", TokenType: "typeParameter"}, +- {Token: "T", TokenType: "typeParameter"}, +- {Token: "return", TokenType: "keyword"}, +- {Token: "append", TokenType: "function", Mod: "defaultLibrary"}, +- {Token: "l", TokenType: "parameter"}, +- {Token: "target", TokenType: "parameter"}, +- {Token: "for", TokenType: "keyword"}, +- {Token: "range", TokenType: "keyword"}, +- {Token: "l", TokenType: "parameter"}, +- {Token: "// test coverage", TokenType: "comment"}, +- {Token: "return", TokenType: "keyword"}, +- {Token: "nil", TokenType: "variable", Mod: "readonly defaultLibrary"}, - } -- return repo --} -- --// A repo represents a working directory for a repository checked out at a --// specific commit. --// --// Repos are used for sharing state across benchmarks that operate on the same --// codebase. --type repo struct { -- // static configuration -- name string // must be unique, used for subdirectory -- url string // repo url -- commit string // full commit hash or tag -- short bool // whether this repo runs with -short -- inDir *string // if set, use this dir as url@commit, and don't delete -- -- dirOnce sync.Once -- dir string // directory contaning source code checked out to url@commit +- src := ` +--- go.mod -- +-module example.com - -- // shared editor state -- editorOnce sync.Once -- editor *fake.Editor -- sandbox *fake.Sandbox -- awaiter *Awaiter +-go 1.19 +--- main.go -- +-package foo +-// Deprecated (for testing) +-func Add[T int](target T, l []T) []T { +- return append(l, target) +- for range l {} // test coverage +- return nil -} +-` +- WithOptions( +- Modes(Default), +- Settings{"semanticTokens": true}, +- ).Run(t, src, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("main.go", "for range")), +- ) +- seen := env.SemanticTokensFull("main.go") +- if x := cmp.Diff(want, seen); x != "" { +- t.Errorf("Semantic tokens do not match (-want +got):\n%s", x) +- } +- }) - --// reusableDir return a reusable directory for benchmarking, or "". --// --// If the user specifies a directory, the test will create and populate it --// on the first run an re-use it on subsequent runs. Otherwise it will --// create, populate, and delete a temporary directory. --func (r *repo) reusableDir() string { -- if r.inDir == nil { -- return "" -- } -- return *r.inDir -} - --// getDir returns directory containing repo source code, creating it if --// necessary. It is safe for concurrent use. --func (r *repo) getDir() string { -- r.dirOnce.Do(func() { -- if r.dir = r.reusableDir(); r.dir == "" { -- r.dir = filepath.Join(getTempDir(), r.name) -- } +-// fix inconsistency in TypeParameters +-// https://github.com/golang/go/issues/57619 +-func TestSemantic_57619(t *testing.T) { +- src := ` +--- go.mod -- +-module example.com - -- _, err := os.Stat(r.dir) -- switch { -- case os.IsNotExist(err): -- log.Printf("cloning %s@%s into %s", r.url, r.commit, r.dir) -- if err := shallowClone(r.dir, r.url, r.commit); err != nil { -- log.Fatal(err) +-go 1.19 +--- main.go -- +-package foo +-type Smap[K int, V any] struct { +- Store map[K]V +-} +-func (s *Smap[K, V]) Get(k K) (V, bool) { +- v, ok := s.Store[k] +- return v, ok +-} +-func New[K int, V any]() Smap[K, V] { +- return Smap[K, V]{Store: make(map[K]V)} +-} +-` +- WithOptions( +- Modes(Default), +- Settings{"semanticTokens": true}, +- ).Run(t, src, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- seen := env.SemanticTokensFull("main.go") +- for i, s := range seen { +- if (s.Token == "K" || s.Token == "V") && s.TokenType != "typeParameter" { +- t.Errorf("%d: expected K and V to be type parameters, but got %v", i, s) - } -- case err != nil: -- log.Fatal(err) -- default: -- log.Printf("reusing %s as %s@%s", r.dir, r.url, r.commit) - } - }) -- return r.dir -} - --// sharedEnv returns a shared benchmark environment. It is safe for concurrent --// use. --// --// Every call to sharedEnv uses the same editor and sandbox, as a means to --// avoid reinitializing the editor for large repos. Calling repo.Close cleans --// up the shared environment. --// --// Repos in the package-local Repos var are closed at the end of the test main --// function. --func (r *repo) sharedEnv(tb testing.TB) *Env { -- r.editorOnce.Do(func() { -- dir := r.getDir() +-func TestSemanticGoDirectives(t *testing.T) { +- src := ` +--- go.mod -- +-module example.com - -- start := time.Now() -- log.Printf("starting initial workspace load for %s", r.name) -- ts, err := newGoplsConnector(profileArgs(r.name, false)) -- if err != nil { -- log.Fatal(err) -- } -- r.sandbox, r.editor, r.awaiter, err = connectEditor(dir, fake.EditorConfig{}, ts) -- if err != nil { -- log.Fatalf("connecting editor: %v", err) -- } +-go 1.19 +--- main.go -- +-package foo +- +-//go:linkname now time.Now +-func now() +- +-//go:noinline +-func foo() {} +- +-// Mentioning go:noinline should not tokenize. +- +-//go:notadirective +-func bar() {} +-` +- want := []fake.SemanticToken{ +- {Token: "package", TokenType: "keyword"}, +- {Token: "foo", TokenType: "namespace"}, +- +- {Token: "//", TokenType: "comment"}, +- {Token: "go:linkname", TokenType: "namespace"}, +- {Token: "now time.Now", TokenType: "comment"}, +- {Token: "func", TokenType: "keyword"}, +- {Token: "now", TokenType: "function", Mod: "definition"}, +- +- {Token: "//", TokenType: "comment"}, +- {Token: "go:noinline", TokenType: "namespace"}, +- {Token: "func", TokenType: "keyword"}, +- {Token: "foo", TokenType: "function", Mod: "definition"}, +- +- {Token: "// Mentioning go:noinline should not tokenize.", TokenType: "comment"}, +- +- {Token: "//go:notadirective", TokenType: "comment"}, +- {Token: "func", TokenType: "keyword"}, +- {Token: "bar", TokenType: "function", Mod: "definition"}, +- } - -- if err := r.awaiter.Await(context.Background(), InitialWorkspaceLoad); err != nil { -- log.Fatal(err) +- WithOptions( +- Modes(Default), +- Settings{"semanticTokens": true}, +- ).Run(t, src, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- seen := env.SemanticTokensFull("main.go") +- if x := cmp.Diff(want, seen); x != "" { +- t.Errorf("Semantic tokens do not match (-want +got):\n%s", x) - } -- log.Printf("initial workspace load (cold) for %s took %v", r.name, time.Since(start)) - }) -- -- return &Env{ -- T: tb, -- Ctx: context.Background(), -- Editor: r.editor, -- Sandbox: r.sandbox, -- Awaiter: r.awaiter, -- } -} - --// newEnv returns a new Env connected to a new gopls process communicating --// over stdin/stdout. It is safe for concurrent use. --// --// It is the caller's responsibility to call Close on the resulting Env when it --// is no longer needed. --func (r *repo) newEnv(tb testing.TB, config fake.EditorConfig, forOperation string, cpuProfile bool) *Env { -- dir := r.getDir() +-// Make sure no zero-length tokens occur +-func TestSemantic_65254(t *testing.T) { +- src := ` +--- go.mod -- +-module example.com +- +-go 1.21 +--- main.go -- +-package main - -- args := profileArgs(qualifiedName(r.name, forOperation), cpuProfile) -- ts, err := newGoplsConnector(args) -- if err != nil { -- tb.Fatal(err) -- } -- sandbox, editor, awaiter, err := connectEditor(dir, config, ts) -- if err != nil { -- log.Fatalf("connecting editor: %v", err) -- } +-/* a comment with an - -- return &Env{ -- T: tb, -- Ctx: context.Background(), -- Editor: editor, -- Sandbox: sandbox, -- Awaiter: awaiter, -- } --} +-empty line +-*/ - --// Close cleans up shared state referenced by the repo. --func (r *repo) Close() error { -- var errBuf bytes.Buffer -- if r.editor != nil { -- if err := r.editor.Close(context.Background()); err != nil { -- fmt.Fprintf(&errBuf, "closing editor: %v", err) -- } -- } -- if r.sandbox != nil { -- if err := r.sandbox.Close(); err != nil { -- fmt.Fprintf(&errBuf, "closing sandbox: %v", err) -- } -- } -- if r.dir != "" && r.reusableDir() == "" { -- if err := os.RemoveAll(r.dir); err != nil { -- fmt.Fprintf(&errBuf, "cleaning dir: %v", err) -- } -- } -- if errBuf.Len() > 0 { -- return errors.New(errBuf.String()) -- } -- return nil --} +-const bad = ` - --// cleanup cleans up state that is shared across benchmark functions. --func cleanup() error { -- var errBuf bytes.Buffer -- for _, repo := range repos { -- if err := repo.Close(); err != nil { -- fmt.Fprintf(&errBuf, "closing %q: %v", repo.name, err) -- } +- src += "`foo" + ` +- ` + "bar`" +- want := []fake.SemanticToken{ +- {Token: "package", TokenType: "keyword"}, +- {Token: "main", TokenType: "namespace"}, +- {Token: "/* a comment with an", TokenType: "comment"}, +- // --- Note that the zero length line does not show up +- {Token: "empty line", TokenType: "comment"}, +- {Token: "*/", TokenType: "comment"}, +- {Token: "const", TokenType: "keyword"}, +- {Token: "bad", TokenType: "variable", Mod: "definition readonly"}, +- {Token: "`foo", TokenType: "string"}, +- // --- Note the zero length line does not show up +- {Token: "\tbar`", TokenType: "string"}, - } -- if tempDir != "" { -- if err := os.RemoveAll(tempDir); err != nil { -- fmt.Fprintf(&errBuf, "cleaning tempDir: %v", err) +- WithOptions( +- Modes(Default), +- Settings{"semanticTokens": true}, +- ).Run(t, src, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- seen := env.SemanticTokensFull("main.go") +- if x := cmp.Diff(want, seen); x != "" { +- t.Errorf("Semantic tokens do not match (-want +got):\n%s", x) - } -- } -- if errBuf.Len() > 0 { -- return errors.New(errBuf.String()) -- } -- return nil +- }) -} -diff -urN a/gopls/internal/regtest/bench/stress_test.go b/gopls/internal/regtest/bench/stress_test.go ---- a/gopls/internal/regtest/bench/stress_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/bench/stress_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,94 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/test/integration/misc/settings_test.go b/gopls/internal/test/integration/misc/settings_test.go +--- a/gopls/internal/test/integration/misc/settings_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/misc/settings_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,32 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package bench +-package misc - -import ( -- "context" -- "flag" -- "fmt" - "testing" -- "time" - -- "golang.org/x/tools/gopls/internal/hooks" -- "golang.org/x/tools/gopls/internal/lsp/cache" -- "golang.org/x/tools/gopls/internal/lsp/fake" -- "golang.org/x/tools/gopls/internal/lsp/lsprpc" -- "golang.org/x/tools/internal/jsonrpc2" -- "golang.org/x/tools/internal/jsonrpc2/servertest" +- . "golang.org/x/tools/gopls/internal/test/integration" -) - --// github.com/pilosa/pilosa is a repository that has historically caused --// significant memory problems for Gopls. We use it for a simple stress test --// that types arbitrarily in a file with lots of dependents. +-func TestEmptyDirectoryFilters_Issue51843(t *testing.T) { +- const src = ` +--- go.mod -- +-module mod.com - --var pilosaPath = flag.String("pilosa_path", "", "Path to a directory containing "+ -- "github.com/pilosa/pilosa, for stress testing. Do not set this unless you "+ -- "know what you're doing!") +-go 1.12 +--- main.go -- +-package main - --func TestPilosaStress(t *testing.T) { -- // TODO(rfindley): revisit this test and make it is hermetic: it should check -- // out pilosa into a directory. -- // -- // Note: This stress test has not been run recently, and may no longer -- // function properly. -- if *pilosaPath == "" { -- t.Skip("-pilosa_path not configured") -- } +-func main() { +-} +-` - -- sandbox, err := fake.NewSandbox(&fake.SandboxConfig{ -- Workdir: *pilosaPath, -- GOPROXY: "https://proxy.golang.org", +- WithOptions( +- Settings{"directoryFilters": []string{""}}, +- ).Run(t, src, func(t *testing.T, env *Env) { +- // No need to do anything. Issue golang/go#51843 is triggered by the empty +- // directory filter above. - }) -- if err != nil { -- t.Fatal(err) -- } +-} +diff -urN a/gopls/internal/test/integration/misc/shared_test.go b/gopls/internal/test/integration/misc/shared_test.go +--- a/gopls/internal/test/integration/misc/shared_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/misc/shared_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,72 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- server := lsprpc.NewStreamServer(cache.New(nil), false, hooks.Options) -- ts := servertest.NewPipeServer(server, jsonrpc2.NewRawStream) -- ctx := context.Background() +-package misc - -- const skipApplyEdits = false -- editor, err := fake.NewEditor(sandbox, fake.EditorConfig{}).Connect(ctx, ts, fake.ClientHooks{}, skipApplyEdits) -- if err != nil { -- t.Fatal(err) -- } +-import ( +- "testing" - -- files := []string{ -- "cmd.go", -- "internal/private.pb.go", -- "roaring/roaring.go", -- "roaring/roaring_internal_test.go", -- "server/handler_test.go", -- } -- for _, file := range files { -- if err := editor.OpenFile(ctx, file); err != nil { +- . "golang.org/x/tools/gopls/internal/test/integration" +- "golang.org/x/tools/gopls/internal/test/integration/fake" +-) +- +-// Smoke test that simultaneous editing sessions in the same workspace works. +-func TestSimultaneousEdits(t *testing.T) { +- const sharedProgram = ` +--- go.mod -- +-module mod +- +-go 1.12 +--- main.go -- +-package main +- +-import "fmt" +- +-func main() { +- fmt.Println("Hello World.") +-}` +- +- WithOptions( +- Modes(DefaultModes()&(Forwarded|SeparateProcess)), +- ).Run(t, sharedProgram, func(t *testing.T, env1 *Env) { +- // Create a second test session connected to the same workspace and server +- // as the first. +- awaiter := NewAwaiter(env1.Sandbox.Workdir) +- const skipApplyEdits = false +- editor, err := fake.NewEditor(env1.Sandbox, env1.Editor.Config()).Connect(env1.Ctx, env1.Server, awaiter.Hooks(), skipApplyEdits) +- if err != nil { - t.Fatal(err) - } -- } -- ctx, cancel := context.WithTimeout(ctx, 10*time.Minute) -- defer cancel() -- -- i := 1 -- // MagicNumber is an identifier that occurs in roaring.go. Just change it -- // arbitrarily. -- if err := editor.RegexpReplace(ctx, "roaring/roaring.go", "MagicNumber", fmt.Sprintf("MagicNumber%d", 1)); err != nil { -- t.Fatal(err) -- } -- for { -- select { -- case <-ctx.Done(): -- return -- default: +- env2 := &Env{ +- T: t, +- Ctx: env1.Ctx, +- Sandbox: env1.Sandbox, +- Server: env1.Server, +- Editor: editor, +- Awaiter: awaiter, - } -- if err := editor.RegexpReplace(ctx, "roaring/roaring.go", fmt.Sprintf("MagicNumber%d", i), fmt.Sprintf("MagicNumber%d", i+1)); err != nil { -- t.Fatal(err) +- env2.Await(InitialWorkspaceLoad) +- // In editor #1, break fmt.Println as before. +- env1.OpenFile("main.go") +- env1.RegexpReplace("main.go", "Printl(n)", "") +- // In editor #2 remove the closing brace. +- env2.OpenFile("main.go") +- env2.RegexpReplace("main.go", "\\)\n(})", "") +- +- // Now check that we got different diagnostics in each environment. +- env1.AfterChange(Diagnostics(env1.AtRegexp("main.go", "Printl"))) +- env2.AfterChange(Diagnostics(env2.AtRegexp("main.go", "$"))) +- +- // Now close editor #2, and verify that operation in editor #1 is +- // unaffected. +- if err := env2.Editor.Close(env2.Ctx); err != nil { +- t.Errorf("closing second editor: %v", err) - } -- // Simulate (very fast) typing. -- // -- // Typing 80 wpm ~150ms per keystroke. -- time.Sleep(150 * time.Millisecond) -- i++ -- } +- +- env1.RegexpReplace("main.go", "Printl", "Println") +- env1.AfterChange( +- NoDiagnostics(ForFile("main.go")), +- ) +- }) -} -diff -urN a/gopls/internal/regtest/bench/typing_test.go b/gopls/internal/regtest/bench/typing_test.go ---- a/gopls/internal/regtest/bench/typing_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/bench/typing_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,63 +0,0 @@ +diff -urN a/gopls/internal/test/integration/misc/signature_help_test.go b/gopls/internal/test/integration/misc/signature_help_test.go +--- a/gopls/internal/test/integration/misc/signature_help_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/misc/signature_help_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,69 +0,0 @@ -// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package bench +-package misc - -import ( -- "fmt" -- "sync/atomic" - "testing" -- "time" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" +- "github.com/google/go-cmp/cmp" +- "golang.org/x/tools/gopls/internal/protocol" +- . "golang.org/x/tools/gopls/internal/test/integration" -) - --// BenchmarkTyping simulates typing steadily in a single file at different --// paces. --// --// The key metric for this benchmark is not latency, but cpu_seconds per --// operation. --func BenchmarkTyping(b *testing.B) { -- for _, test := range didChangeTests { -- b.Run(test.repo, func(b *testing.B) { -- env := getRepo(b, test.repo).sharedEnv(b) -- env.OpenFile(test.file) -- defer closeBuffer(b, env, test.file) +-func TestSignatureHelpInNonWorkspacePackage(t *testing.T) { +- const files = ` +--- a/go.mod -- +-module a.com - -- // Insert the text we'll be modifying at the top of the file. -- env.EditBuffer(test.file, protocol.TextEdit{NewText: "// __REGTEST_PLACEHOLDER_0__\n"}) -- env.AfterChange() +-go 1.18 +--- a/a/a.go -- +-package a - -- delays := []time.Duration{ -- 10 * time.Millisecond, // automated changes -- 50 * time.Millisecond, // very fast mashing, or fast key sequences -- 150 * time.Millisecond, // avg interval for 80wpm typing. -- } +-func DoSomething(int) {} - -- for _, delay := range delays { -- b.Run(delay.String(), func(b *testing.B) { -- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(test.repo, "typing")); stopAndRecord != nil { -- defer stopAndRecord() -- } -- ticker := time.NewTicker(delay) -- for i := 0; i < b.N; i++ { -- edits := atomic.AddInt64(&editID, 1) -- env.EditBuffer(test.file, protocol.TextEdit{ -- Range: protocol.Range{ -- Start: protocol.Position{Line: 0, Character: 0}, -- End: protocol.Position{Line: 1, Character: 0}, -- }, -- // Increment the placeholder text, to ensure cache misses. -- NewText: fmt.Sprintf("// __REGTEST_PLACEHOLDER_%d__\n", edits), -- }) -- <-ticker.C -- } -- b.StopTimer() -- ticker.Stop() -- env.AfterChange() // wait for all change processing to complete -- }) -- } -- }) -- } +-func _() { +- DoSomething() -} -diff -urN a/gopls/internal/regtest/bench/workspace_symbols_test.go b/gopls/internal/regtest/bench/workspace_symbols_test.go ---- a/gopls/internal/regtest/bench/workspace_symbols_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/bench/workspace_symbols_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,41 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package bench -- --import ( -- "flag" -- "fmt" -- "testing" --) +--- b/go.mod -- +-module b.com +-go 1.18 - --var symbolQuery = flag.String("symbol_query", "test", "symbol query to use in benchmark") +-require a.com v1.0.0 - --// BenchmarkWorkspaceSymbols benchmarks the time to execute a workspace symbols --// request (controlled by the -symbol_query flag). --func BenchmarkWorkspaceSymbols(b *testing.B) { -- for name := range repos { -- b.Run(name, func(b *testing.B) { -- env := getRepo(b, name).sharedEnv(b) -- symbols := env.Symbol(*symbolQuery) // warm the cache +-replace a.com => ../a +--- b/b/b.go -- +-package b - -- if testing.Verbose() { -- fmt.Println("Results:") -- for i, symbol := range symbols { -- fmt.Printf("\t%d. %s (%s)\n", i, symbol.Name, symbol.ContainerName) -- } -- } +-import "a.com/a" - -- b.ResetTimer() +-func _() { +- a.DoSomething() +-} +-` - -- if stopAndRecord := startProfileIfSupported(b, env, qualifiedName(name, "workspaceSymbols")); stopAndRecord != nil { -- defer stopAndRecord() +- WithOptions( +- WorkspaceFolders("a"), +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("a/a/a.go") +- env.OpenFile("b/b/b.go") +- signatureHelp := func(filename string) *protocol.SignatureHelp { +- loc := env.RegexpSearch(filename, `DoSomething\(()\)`) +- var params protocol.SignatureHelpParams +- params.TextDocument.URI = loc.URI +- params.Position = loc.Range.Start +- help, err := env.Editor.Server.SignatureHelp(env.Ctx, ¶ms) +- if err != nil { +- t.Fatal(err) - } +- return help +- } +- ahelp := signatureHelp("a/a/a.go") +- bhelp := signatureHelp("b/b/b.go") - -- for i := 0; i < b.N; i++ { -- env.Symbol(*symbolQuery) -- } -- }) -- } +- if diff := cmp.Diff(ahelp, bhelp); diff != "" { +- t.Fatal(diff) +- } +- }) -} -diff -urN a/gopls/internal/regtest/codelens/codelens_test.go b/gopls/internal/regtest/codelens/codelens_test.go ---- a/gopls/internal/regtest/codelens/codelens_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/codelens/codelens_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,352 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/test/integration/misc/staticcheck_test.go b/gopls/internal/test/integration/misc/staticcheck_test.go +--- a/gopls/internal/test/integration/misc/staticcheck_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/misc/staticcheck_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,123 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package codelens +-package misc - -import ( -- "fmt" +- "os" +- "strings" - "testing" - -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/hooks" -- "golang.org/x/tools/gopls/internal/lsp" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" -- "golang.org/x/tools/gopls/internal/lsp/tests/compare" -- -- "golang.org/x/tools/gopls/internal/lsp/command" -- "golang.org/x/tools/gopls/internal/lsp/protocol" - "golang.org/x/tools/internal/testenv" +- +- . "golang.org/x/tools/gopls/internal/test/integration" -) - --func TestMain(m *testing.M) { -- bug.PanicOnBugs = true -- Main(m, hooks.Options) --} +-func TestStaticcheckGenerics(t *testing.T) { +- testenv.NeedsGo1Point(t, 20) // staticcheck requires go1.20+ - --func TestDisablingCodeLens(t *testing.T) { -- const workspace = ` ---- go.mod -- --module codelens.test +- // TODO(golang/go#65249): re-enable and fix this test with gotypesalias=1. +- if strings.Contains(os.Getenv("GODEBUG"), "gotypesalias=1") { +- t.Skipf("staticcheck needs updates for materialized aliases") +- } - --go 1.12 ---- lib.go -- --package lib +- const files = ` +--- go.mod -- +-module mod.com - --type Number int +-go 1.18 +--- a/a.go -- +-package a - --const ( -- Zero Number = iota -- One -- Two +-import ( +- "errors" +- "sort" +- "strings" -) - --//` + `go:generate stringer -type=Number --` -- tests := []struct { -- label string -- enabled map[string]bool -- wantCodeLens bool -- }{ -- { -- label: "default", -- wantCodeLens: true, -- }, -- { -- label: "generate disabled", -- enabled: map[string]bool{string(command.Generate): false}, -- wantCodeLens: false, -- }, -- } -- for _, test := range tests { -- t.Run(test.label, func(t *testing.T) { -- WithOptions( -- Settings{"codelenses": test.enabled}, -- ).Run(t, workspace, func(t *testing.T, env *Env) { -- env.OpenFile("lib.go") -- lens := env.CodeLens("lib.go") -- if gotCodeLens := len(lens) > 0; gotCodeLens != test.wantCodeLens { -- t.Errorf("got codeLens: %t, want %t", gotCodeLens, test.wantCodeLens) -- } -- }) -- }) -- } +-func Zero[P any]() P { +- var p P +- return p -} - --// This test confirms the full functionality of the code lenses for updating --// dependencies in a go.mod file. It checks for the code lens that suggests --// an update and then executes the command associated with that code lens. A --// regression test for golang/go#39446. It also checks that these code lenses --// only affect the diagnostics and contents of the containing go.mod file. --func TestUpgradeCodelens(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) // uses go.work -- -- const proxyWithLatest = ` ---- golang.org/x/hello@v1.3.3/go.mod -- --module golang.org/x/hello -- --go 1.12 ---- golang.org/x/hello@v1.3.3/hi/hi.go -- --package hi -- --var Goodbye error ---- golang.org/x/hello@v1.2.3/go.mod -- --module golang.org/x/hello +-type Inst[P any] struct { +- Field P +-} - --go 1.12 ---- golang.org/x/hello@v1.2.3/hi/hi.go -- --package hi +-func testGenerics[P *T, T any](p P) { +- // Calls to instantiated functions should not break checks. +- slice := Zero[string]() +- sort.Slice(slice, func(i, j int) bool { +- return slice[i] < slice[j] +- }) - --var Goodbye error --` +- // Usage of instantiated fields should not break checks. +- g := Inst[string]{"hello"} +- g.Field = strings.TrimLeft(g.Field, "12234") - -- const shouldUpdateDep = ` ---- go.work -- --go 1.18 +- // Use of type parameters should not break checks. +- var q P +- p = q // SA4009: p is overwritten before its first use +- q = &*p // SA4001: &* will be simplified +-} - --use ( -- ./a -- ./b --) ---- a/go.mod -- --module mod.com/a - --go 1.14 +-// FooErr should be called ErrFoo (ST1012) +-var FooErr error = errors.New("foo") +-` - --require golang.org/x/hello v1.2.3 ---- a/go.sum -- --golang.org/x/hello v1.2.3 h1:7Wesfkx/uBd+eFgPrq0irYj/1XfmbvLV8jZ/W7C2Dwg= --golang.org/x/hello v1.2.3/go.mod h1:OgtlzsxVMUUdsdQCIDYgaauCTH47B8T8vofouNJfzgY= ---- a/main.go -- --package main +- WithOptions( +- Settings{"staticcheck": true}, +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("a/a.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/a.go", "sort.Slice"), FromSource("sortslice")), +- Diagnostics(env.AtRegexp("a/a.go", "sort.Slice.(slice)"), FromSource("SA1028")), +- Diagnostics(env.AtRegexp("a/a.go", "var (FooErr)"), FromSource("ST1012")), +- Diagnostics(env.AtRegexp("a/a.go", `"12234"`), FromSource("SA1024")), +- Diagnostics(env.AtRegexp("a/a.go", "testGenerics.*(p P)"), FromSource("SA4009")), +- Diagnostics(env.AtRegexp("a/a.go", "q = (&\\*p)"), FromSource("SA4001")), +- ) +- }) +-} - --import "golang.org/x/hello/hi" +-// Test for golang/go#56270: an analysis with related info should not panic if +-// analysis.RelatedInformation.End is not set. +-func TestStaticcheckRelatedInfo(t *testing.T) { +- testenv.NeedsGo1Point(t, 20) // staticcheck is only supported at Go 1.20+ - --func main() { -- _ = hi.Goodbye --} ---- b/go.mod -- --module mod.com/b +- // TODO(golang/go#65249): re-enable and fix this test with gotypesalias=1. +- if strings.Contains(os.Getenv("GODEBUG"), "gotypesalias=1") { +- t.Skipf("staticcheck needs updates for materialized aliases") +- } - --go 1.14 +- const files = ` +--- go.mod -- +-module mod.test - --require golang.org/x/hello v1.2.3 ---- b/go.sum -- --golang.org/x/hello v1.2.3 h1:7Wesfkx/uBd+eFgPrq0irYj/1XfmbvLV8jZ/W7C2Dwg= --golang.org/x/hello v1.2.3/go.mod h1:OgtlzsxVMUUdsdQCIDYgaauCTH47B8T8vofouNJfzgY= ---- b/main.go -- --package main +-go 1.18 +--- p.go -- +-package p - -import ( -- "golang.org/x/hello/hi" +- "fmt" -) - --func main() { -- _ = hi.Goodbye +-func Foo(enabled interface{}) { +- if enabled, ok := enabled.(bool); ok { +- } else { +- _ = fmt.Sprintf("invalid type %T", enabled) // enabled is always bool here +- } -} -` - -- const wantGoModA = `module mod.com/a -- --go 1.14 -- --require golang.org/x/hello v1.3.3 --` -- // Applying the diagnostics or running the codelenses for a/go.mod -- // should not change the contents of b/go.mod -- const wantGoModB = `module mod.com/b -- --go 1.14 -- --require golang.org/x/hello v1.2.3 --` +- WithOptions( +- Settings{"staticcheck": true}, +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("p.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("p.go", ", (enabled)"), FromSource("SA9008")), +- ) +- }) +-} +diff -urN a/gopls/internal/test/integration/misc/vendor_test.go b/gopls/internal/test/integration/misc/vendor_test.go +--- a/gopls/internal/test/integration/misc/vendor_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/misc/vendor_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,102 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- for _, commandTitle := range []string{ -- "Upgrade transitive dependencies", -- "Upgrade direct dependencies", -- } { -- t.Run(commandTitle, func(t *testing.T) { -- WithOptions( -- ProxyFiles(proxyWithLatest), -- ).Run(t, shouldUpdateDep, func(t *testing.T, env *Env) { -- env.OpenFile("a/go.mod") -- env.OpenFile("b/go.mod") -- var lens protocol.CodeLens -- var found bool -- for _, l := range env.CodeLens("a/go.mod") { -- if l.Command.Title == commandTitle { -- lens = l -- found = true -- } -- } -- if !found { -- t.Fatalf("found no command with the title %s", commandTitle) -- } -- if _, err := env.Editor.ExecuteCommand(env.Ctx, &protocol.ExecuteCommandParams{ -- Command: lens.Command.Command, -- Arguments: lens.Command.Arguments, -- }); err != nil { -- t.Fatal(err) -- } -- env.AfterChange() -- if got := env.BufferText("a/go.mod"); got != wantGoModA { -- t.Fatalf("a/go.mod upgrade failed:\n%s", compare.Text(wantGoModA, got)) -- } -- if got := env.BufferText("b/go.mod"); got != wantGoModB { -- t.Fatalf("b/go.mod changed unexpectedly:\n%s", compare.Text(wantGoModB, got)) -- } -- }) -- }) -- } -- for _, vendoring := range []bool{false, true} { -- t.Run(fmt.Sprintf("Upgrade individual dependency vendoring=%v", vendoring), func(t *testing.T) { -- WithOptions( -- ProxyFiles(proxyWithLatest), -- ).Run(t, shouldUpdateDep, func(t *testing.T, env *Env) { -- if vendoring { -- env.RunGoCommandInDirWithEnv("a", []string{"GOWORK=off"}, "mod", "vendor") -- } -- env.AfterChange() -- env.OpenFile("a/go.mod") -- env.OpenFile("b/go.mod") +-package misc - -- // Await the diagnostics resulting from opening the modfiles, because -- // otherwise they may cause races when running asynchronously to the -- // explicit re-diagnosing below. -- // -- // TODO(golang/go#58750): there is still a race here, inherent to -- // accessing state on the View; we should create a new snapshot when -- // the view diagnostics change. -- env.AfterChange() +-import ( +- "testing" - -- env.ExecuteCodeLensCommand("a/go.mod", command.CheckUpgrades, nil) -- d := &protocol.PublishDiagnosticsParams{} -- env.OnceMet( -- Diagnostics(env.AtRegexp("a/go.mod", `require`), WithMessage("can be upgraded")), -- ReadDiagnostics("a/go.mod", d), -- // We do not want there to be a diagnostic for b/go.mod, -- // but there may be some subtlety in timing here, where this -- // should always succeed, but may not actually test the correct -- // behavior. -- NoDiagnostics(env.AtRegexp("b/go.mod", `require`)), -- ) -- // Check for upgrades in b/go.mod and then clear them. -- env.ExecuteCodeLensCommand("b/go.mod", command.CheckUpgrades, nil) -- env.Await(Diagnostics(env.AtRegexp("b/go.mod", `require`), WithMessage("can be upgraded"))) -- env.ExecuteCodeLensCommand("b/go.mod", command.ResetGoModDiagnostics, nil) -- env.Await(NoDiagnostics(ForFile("b/go.mod"))) +- . "golang.org/x/tools/gopls/internal/test/integration" - -- // Apply the diagnostics to a/go.mod. -- env.ApplyQuickFixes("a/go.mod", d.Diagnostics) -- env.AfterChange() -- if got := env.BufferText("a/go.mod"); got != wantGoModA { -- t.Fatalf("a/go.mod upgrade failed:\n%s", compare.Text(wantGoModA, got)) -- } -- if got := env.BufferText("b/go.mod"); got != wantGoModB { -- t.Fatalf("b/go.mod changed unexpectedly:\n%s", compare.Text(wantGoModB, got)) -- } -- }) -- }) -- } --} +- "golang.org/x/tools/gopls/internal/protocol" +-) - --func TestUnusedDependenciesCodelens(t *testing.T) { -- const proxy = ` ---- golang.org/x/hello@v1.0.0/go.mod -- +-const basicProxy = ` +--- golang.org/x/hello@v1.2.3/go.mod -- -module golang.org/x/hello - -go 1.14 ---- golang.org/x/hello@v1.0.0/hi/hi.go -- +--- golang.org/x/hello@v1.2.3/hi/hi.go -- -package hi - -var Goodbye error ---- golang.org/x/unused@v1.0.0/go.mod -- --module golang.org/x/unused -- --go 1.14 ---- golang.org/x/unused@v1.0.0/nouse/nouse.go -- --package nouse -- --var NotUsed error -` - -- const shouldRemoveDep = ` +-func TestInconsistentVendoring(t *testing.T) { +- const pkgThatUsesVendoring = ` --- go.mod -- -module mod.com - -go 1.14 - --require golang.org/x/hello v1.0.0 --require golang.org/x/unused v1.0.0 +-require golang.org/x/hello v1.2.3 --- go.sum -- --golang.org/x/hello v1.0.0 h1:qbzE1/qT0/zojAMd/JcPsO2Vb9K4Bkeyq0vB2JGMmsw= --golang.org/x/hello v1.0.0/go.mod h1:WW7ER2MRNXWA6c8/4bDIek4Hc/+DofTrMaQQitGXcco= --golang.org/x/unused v1.0.0 h1:LecSbCn5P3vTcxubungSt1Pn4D/WocCaiWOPDC0y0rw= --golang.org/x/unused v1.0.0/go.mod h1:ihoW8SgWzugwwj0N2SfLfPZCxTB1QOVfhMfB5PWTQ8U= ---- main.go -- --package main +-golang.org/x/hello v1.2.3 h1:EcMp5gSkIhaTkPXp8/3+VH+IFqTpk3ZbpOhqk0Ncmho= +-golang.org/x/hello v1.2.3/go.mod h1:WW7ER2MRNXWA6c8/4bDIek4Hc/+DofTrMaQQitGXcco= +--- vendor/modules.txt -- +--- a/a1.go -- +-package a - -import "golang.org/x/hello/hi" - --func main() { +-func _() { - _ = hi.Goodbye +- var q int // hardcode a diagnostic -} -` -- WithOptions(ProxyFiles(proxy)).Run(t, shouldRemoveDep, func(t *testing.T, env *Env) { -- env.OpenFile("go.mod") -- env.ExecuteCodeLensCommand("go.mod", command.Tidy, nil) -- env.Await(env.DoneWithChangeWatchedFiles()) -- got := env.BufferText("go.mod") -- const wantGoMod = `module mod.com -- --go 1.14 +- WithOptions( +- Modes(Default), +- ProxyFiles(basicProxy), +- ).Run(t, pkgThatUsesVendoring, func(t *testing.T, env *Env) { +- env.OpenFile("a/a1.go") +- d := &protocol.PublishDiagnosticsParams{} +- env.AfterChange( +- Diagnostics(env.AtRegexp("go.mod", "module mod.com"), WithMessage("Inconsistent vendoring")), +- ReadDiagnostics("go.mod", d), +- ) +- env.ApplyQuickFixes("go.mod", d.Diagnostics) - --require golang.org/x/hello v1.0.0 --` -- if got != wantGoMod { -- t.Fatalf("go.mod tidy failed:\n%s", compare.Text(wantGoMod, got)) -- } +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/a1.go", `q int`), WithMessage("not used")), +- ) - }) -} - --func TestRegenerateCgo(t *testing.T) { -- testenv.NeedsTool(t, "cgo") -- const workspace = ` +-func TestWindowsVendoring_Issue56291(t *testing.T) { +- const src = ` --- go.mod -- --module example.com +-module mod.com - --go 1.12 ---- cgo.go -- --package x +-go 1.14 - --/* --int fortythree() { return 42; } --*/ --import "C" +-require golang.org/x/hello v1.2.3 +--- go.sum -- +-golang.org/x/hello v1.2.3 h1:EcMp5gSkIhaTkPXp8/3+VH+IFqTpk3ZbpOhqk0Ncmho= +-golang.org/x/hello v1.2.3/go.mod h1:WW7ER2MRNXWA6c8/4bDIek4Hc/+DofTrMaQQitGXcco= +--- main.go -- +-package main - --func Foo() { -- print(C.fortytwo()) +-import "golang.org/x/hello/hi" +- +-func main() { +- _ = hi.Goodbye -} -` -- Run(t, workspace, func(t *testing.T, env *Env) { -- // Open the file. We have a nonexistant symbol that will break cgo processing. -- env.OpenFile("cgo.go") -- env.AfterChange( -- Diagnostics(env.AtRegexp("cgo.go", ``), WithMessage("go list failed to return CompiledGoFiles")), -- ) -- -- // Fix the C function name. We haven't regenerated cgo, so nothing should be fixed. -- env.RegexpReplace("cgo.go", `int fortythree`, "int fortytwo") -- env.SaveBuffer("cgo.go") +- WithOptions( +- Modes(Default), +- ProxyFiles(basicProxy), +- ).Run(t, src, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- env.AfterChange(NoDiagnostics()) +- env.RunGoCommand("mod", "tidy") +- env.RunGoCommand("mod", "vendor") +- env.AfterChange(NoDiagnostics()) +- env.RegexpReplace("main.go", `import "golang.org/x/hello/hi"`, "") - env.AfterChange( -- Diagnostics(env.AtRegexp("cgo.go", ``), WithMessage("go list failed to return CompiledGoFiles")), -- ) -- -- // Regenerate cgo, fixing the diagnostic. -- env.ExecuteCodeLensCommand("cgo.go", command.RegenerateCgo, nil) -- env.OnceMet( -- CompletedWork(lsp.DiagnosticWorkTitle(lsp.FromRegenerateCgo), 1, true), -- NoDiagnostics(ForFile("cgo.go")), +- Diagnostics(env.AtRegexp("main.go", "hi.Goodbye")), - ) +- env.SaveBuffer("main.go") +- env.AfterChange(NoDiagnostics()) - }) -} -diff -urN a/gopls/internal/regtest/codelens/gcdetails_test.go b/gopls/internal/regtest/codelens/gcdetails_test.go ---- a/gopls/internal/regtest/codelens/gcdetails_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/codelens/gcdetails_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,127 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/test/integration/misc/vuln_test.go b/gopls/internal/test/integration/misc/vuln_test.go +--- a/gopls/internal/test/integration/misc/vuln_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/misc/vuln_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,946 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package codelens +-package misc - -import ( -- "runtime" +- "context" +- "encoding/json" +- "sort" - "strings" - "testing" +- "time" - -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/lsp/command" -- "golang.org/x/tools/gopls/internal/lsp/fake" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" --) +- "github.com/google/go-cmp/cmp" - --func TestGCDetails_Toggle(t *testing.T) { -- if runtime.GOOS == "android" { -- t.Skipf("the gc details code lens doesn't work on Android") -- } +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/command" +- "golang.org/x/tools/gopls/internal/test/compare" +- . "golang.org/x/tools/gopls/internal/test/integration" +- "golang.org/x/tools/gopls/internal/vulncheck" +- "golang.org/x/tools/gopls/internal/vulncheck/vulntest" +-) - -- const mod = ` +-func TestRunGovulncheckError(t *testing.T) { +- const files = ` --- go.mod -- -module mod.com - --go 1.15 ---- main.go -- --package main -- --import "fmt" -- --func main() { -- fmt.Println(42) --} +-go 1.12 +--- foo.go -- +-package foo -` -- WithOptions( -- Settings{ -- "codelenses": map[string]bool{ -- "gc_details": true, -- }, -- }, -- ).Run(t, mod, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- env.ExecuteCodeLensCommand("main.go", command.GCDetails, nil) -- d := &protocol.PublishDiagnosticsParams{} -- env.OnceMet( -- Diagnostics(AtPosition("main.go", 5, 13)), -- ReadDiagnostics("main.go", d), -- ) -- // Confirm that the diagnostics come from the gc details code lens. -- var found bool -- for _, d := range d.Diagnostics { -- if d.Severity != protocol.SeverityInformation { -- t.Fatalf("unexpected diagnostic severity %v, wanted Information", d.Severity) -- } -- if strings.Contains(d.Message, "42 escapes") { -- found = true -- } -- } -- if !found { -- t.Fatalf(`expected to find diagnostic with message "escape(42 escapes to heap)", found none`) +- Run(t, files, func(t *testing.T, env *Env) { +- cmd, err := command.NewRunGovulncheckCommand("Run Vulncheck Exp", command.VulncheckArgs{ +- URI: "/invalid/file/url", // invalid arg +- }) +- if err != nil { +- t.Fatal(err) - } - -- // Editing a buffer should cause gc_details diagnostics to disappear, since -- // they only apply to saved buffers. -- env.EditBuffer("main.go", fake.NewEdit(0, 0, 0, 0, "\n\n")) -- env.AfterChange(NoDiagnostics(ForFile("main.go"))) -- -- // Saving a buffer should re-format back to the original state, and -- // re-enable the gc_details diagnostics. -- env.SaveBuffer("main.go") -- env.AfterChange(Diagnostics(AtPosition("main.go", 5, 13))) +- params := &protocol.ExecuteCommandParams{ +- Command: command.RunGovulncheck.ID(), +- Arguments: cmd.Arguments, +- } - -- // Toggle the GC details code lens again so now it should be off. -- env.ExecuteCodeLensCommand("main.go", command.GCDetails, nil) -- env.Await(NoDiagnostics(ForFile("main.go"))) +- response, err := env.Editor.ExecuteCommand(env.Ctx, params) +- // We want an error! +- if err == nil { +- t.Errorf("got success, want invalid file URL error: %v", response) +- } - }) -} - --// Test for the crasher in golang/go#54199 --func TestGCDetails_NewFile(t *testing.T) { -- bug.PanicOnBugs = false -- const src = ` +-func TestRunGovulncheckError2(t *testing.T) { +- const files = ` --- go.mod -- --module mod.test +-module mod.com - -go 1.12 --` +--- foo.go -- +-package foo - +-func F() { // build error incomplete +-` - WithOptions( +- EnvVars{ +- "_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`. +- }, - Settings{ - "codelenses": map[string]bool{ -- "gc_details": true, +- "run_govulncheck": true, - }, - }, -- ).Run(t, src, func(t *testing.T, env *Env) { -- env.CreateBuffer("p_test.go", "") -- -- const gcDetailsCommand = "gopls." + string(command.GCDetails) -- -- hasGCDetails := func() bool { -- lenses := env.CodeLens("p_test.go") // should not crash -- for _, lens := range lenses { -- if lens.Command.Command == gcDetailsCommand { -- return true -- } -- } -- return false -- } -- -- // With an empty file, we shouldn't get the gc_details codelens because -- // there is nowhere to position it (it needs a package name). -- if hasGCDetails() { -- t.Errorf("got the gc_details codelens for an empty file") -- } -- -- // Edit to provide a package name. -- env.EditBuffer("p_test.go", fake.NewEdit(0, 0, 0, 0, "package p")) -- -- // Now we should get the gc_details codelens. -- if !hasGCDetails() { -- t.Errorf("didn't get the gc_details codelens for a valid non-empty Go file") +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("go.mod") +- var result command.RunVulncheckResult +- env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result) +- var ws WorkStatus +- env.Await( +- CompletedProgress(result.Token, &ws), +- ) +- wantEndMsg, wantMsgPart := "failed", "There are errors with the provided package patterns:" +- if ws.EndMsg != "failed" || !strings.Contains(ws.Msg, wantMsgPart) { +- t.Errorf("work status = %+v, want {EndMessage: %q, Message: %q}", ws, wantEndMsg, wantMsgPart) - } - }) -} -diff -urN a/gopls/internal/regtest/completion/completion18_test.go b/gopls/internal/regtest/completion/completion18_test.go ---- a/gopls/internal/regtest/completion/completion18_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/completion/completion18_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,124 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --//go:build go1.18 --// +build go1.18 +-const vulnsData = ` +--- GO-2022-01.yaml -- +-modules: +- - module: golang.org/amod +- versions: +- - introduced: 1.0.0 +- - fixed: 1.0.4 +- packages: +- - package: golang.org/amod/avuln +- symbols: +- - VulnData.Vuln1 +- - VulnData.Vuln2 +-description: > +- vuln in amod is found +-summary: vuln in amod +-references: +- - href: pkg.go.dev/vuln/GO-2022-01 +--- GO-2022-03.yaml -- +-modules: +- - module: golang.org/amod +- versions: +- - introduced: 1.0.0 +- - fixed: 1.0.6 +- packages: +- - package: golang.org/amod/avuln +- symbols: +- - nonExisting +-description: > +- unaffecting vulnerability is found +-summary: unaffecting vulnerability +--- GO-2022-02.yaml -- +-modules: +- - module: golang.org/bmod +- packages: +- - package: golang.org/bmod/bvuln +- symbols: +- - Vuln +-description: | +- vuln in bmod is found. +- +- This is a long description +- of this vulnerability. +-summary: vuln in bmod (no fix) +-references: +- - href: pkg.go.dev/vuln/GO-2022-03 +--- GO-2022-04.yaml -- +-modules: +- - module: golang.org/bmod +- packages: +- - package: golang.org/bmod/unused +- symbols: +- - Vuln +-description: | +- vuln in bmod/somethingelse is found +-summary: vuln in bmod/somethingelse +-references: +- - href: pkg.go.dev/vuln/GO-2022-04 +--- GOSTDLIB.yaml -- +-modules: +- - module: stdlib +- versions: +- - introduced: 1.18.0 +- packages: +- - package: archive/zip +- symbols: +- - OpenReader +-summary: vuln in GOSTDLIB +-references: +- - href: pkg.go.dev/vuln/GOSTDLIB +-` +- +-func TestRunGovulncheckStd(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.com - --package completion +-go 1.19 +--- main.go -- +-package main - -import ( -- "testing" +- "archive/zip" +- "fmt" +-) +- +-func main() { +- _, err := zip.OpenReader("file.zip") // vulnerability id: GOSTDLIB +- fmt.Println(err) +-} +-` +- +- db, err := vulntest.NewDatabase(context.Background(), []byte(vulnsData)) +- if err != nil { +- t.Fatal(err) +- } +- defer db.Clean() +- WithOptions( +- EnvVars{ +- // Let the analyzer read vulnerabilities data from the testdata/vulndb. +- "GOVULNDB": db.URI(), +- // When fetchinging stdlib package vulnerability info, +- // behave as if our go version is go1.19 for this testing. +- // The default behavior is to run `go env GOVERSION` (which isn't mutable env var). +- cache.GoVersionForVulnTest: "go1.19", +- "_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`. +- }, +- Settings{ +- "codelenses": map[string]bool{ +- "run_govulncheck": true, +- }, +- }, +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("go.mod") - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" --) +- // Run Command included in the codelens. +- var result command.RunVulncheckResult +- env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result) - --// test generic receivers --func TestGenericReceiver(t *testing.T) { +- env.OnceMet( +- CompletedProgress(result.Token, nil), +- ShownMessage("Found GOSTDLIB"), +- NoDiagnostics(ForFile("go.mod")), +- ) +- testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{ +- "go.mod": {IDs: []string{"GOSTDLIB"}, Mode: vulncheck.ModeGovulncheck}}) +- }) +-} +-func TestFetchVulncheckResultStd(t *testing.T) { - const files = ` --- go.mod -- -module mod.com @@ -111274,28853 +117744,31735 @@ diff -urN a/gopls/internal/regtest/completion/completion18_test.go b/gopls/inter -go 1.18 --- main.go -- -package main --type SyncMap[K any, V comparable] struct {} --func (s *SyncMap[K,V]) f() {} --type XX[T any] struct {} --type UU[T any] struct {} --func (s SyncMap[XX,string]) g(v UU) {} +- +-import ( +- "archive/zip" +- "fmt" +-) +- +-func main() { +- _, err := zip.OpenReader("file.zip") // vulnerability id: GOSTDLIB +- fmt.Println(err) +-} -` - -- tests := []struct { -- pat string -- want []string -- }{ -- {"s .Syn", []string{"SyncMap[K, V]"}}, -- {"Map.X", []string{}}, // This is probably wrong, Maybe "XX"? -- {"v U", []string{"UU", "uint", "uint16", "uint32", "uint64", "uint8", "uintptr"}}, // not U[T] +- db, err := vulntest.NewDatabase(context.Background(), []byte(vulnsData)) +- if err != nil { +- t.Fatal(err) - } -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- env.Await(env.DoneWithOpen()) -- for _, tst := range tests { -- loc := env.RegexpSearch("main.go", tst.pat) -- loc.Range.Start.Character += uint32(protocol.UTF16Len([]byte(tst.pat))) -- completions := env.Completion(loc) -- result := compareCompletionLabels(tst.want, completions.Items) -- if result != "" { -- t.Errorf("%s: wanted %v", result, tst.want) -- for i, g := range completions.Items { -- t.Errorf("got %d %s %s", i, g.Label, g.Detail) -- } -- } -- } +- defer db.Clean() +- WithOptions( +- EnvVars{ +- // Let the analyzer read vulnerabilities data from the testdata/vulndb. +- "GOVULNDB": db.URI(), +- // When fetchinging stdlib package vulnerability info, +- // behave as if our go version is go1.18 for this testing. +- cache.GoVersionForVulnTest: "go1.18", +- "_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`. +- }, +- Settings{"ui.diagnostic.vulncheck": "Imports"}, +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("go.mod") +- env.AfterChange( +- NoDiagnostics(ForFile("go.mod")), +- // we don't publish diagnostics for standard library vulnerability yet. +- ) +- testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{ +- "go.mod": { +- IDs: []string{"GOSTDLIB"}, +- Mode: vulncheck.ModeImports, +- }, +- }) - }) -} --func TestFuzzFunc(t *testing.T) { -- // use the example from the package documentation -- modfile := ` ---- go.mod -- --module mod.com - --go 1.18 --` -- part0 := `package foo --import "testing" --func FuzzNone(f *testing.F) { -- f.Add(12) // better not find this f.Add +-type fetchVulncheckResult struct { +- IDs []string +- Mode vulncheck.AnalysisMode -} --func FuzzHex(f *testing.F) { -- for _, seed := range [][]byte{{}, {0}, {9}, {0xa}, {0xf}, {1, 2, 3, 4}} { -- f.Ad` -- part1 := `d(seed) -- } -- f.F` -- part2 := `uzz(func(t *testing.T, in []byte) { -- enc := hex.EncodeToString(in) -- out, err := hex.DecodeString(enc) -- if err != nil { -- f.Failed() -- } -- if !bytes.Equal(in, out) { -- t.Fatalf("%v: round trip: %v, %s", in, out, f.Name()) -- } +- +-func testFetchVulncheckResult(t *testing.T, env *Env, want map[string]fetchVulncheckResult) { +- t.Helper() +- +- var result map[protocol.DocumentURI]*vulncheck.Result +- fetchCmd, err := command.NewFetchVulncheckResultCommand("fetch", command.URIArg{ +- URI: env.Sandbox.Workdir.URI("go.mod"), - }) --} --` -- data := modfile + `-- a_test.go -- --` + part0 + ` ---- b_test.go -- --` + part0 + part1 + ` ---- c_test.go -- --` + part0 + part1 + part2 +- if err != nil { +- t.Fatal(err) +- } +- env.ExecuteCommand(&protocol.ExecuteCommandParams{ +- Command: fetchCmd.Command, +- Arguments: fetchCmd.Arguments, +- }, &result) - -- tests := []struct { -- file string -- pat string -- offset uint32 // UTF16 length from the beginning of pat to what the user just typed -- want []string -- }{ -- {"a_test.go", "f.Ad", 3, []string{"Add"}}, -- {"c_test.go", " f.F", 4, []string{"Failed"}}, -- {"c_test.go", "f.N", 3, []string{"Name"}}, -- {"b_test.go", "f.F", 3, []string{"Fuzz(func(t *testing.T, a []byte)", "Fail", "FailNow", -- "Failed", "Fatal", "Fatalf"}}, +- for _, v := range want { +- sort.Strings(v.IDs) - } -- Run(t, data, func(t *testing.T, env *Env) { -- for _, test := range tests { -- env.OpenFile(test.file) -- env.Await(env.DoneWithOpen()) -- loc := env.RegexpSearch(test.file, test.pat) -- loc.Range.Start.Character += test.offset // character user just typed? will type? -- completions := env.Completion(loc) -- result := compareCompletionLabels(test.want, completions.Items) -- if result != "" { -- t.Errorf("pat %q %q", test.pat, result) -- for i, it := range completions.Items { -- t.Errorf("%d got %q %q", i, it.Label, it.Detail) -- } -- } +- got := map[string]fetchVulncheckResult{} +- for k, r := range result { +- osv := map[string]bool{} +- for _, v := range r.Findings { +- osv[v.OSV] = true - } -- }) +- ids := make([]string, 0, len(osv)) +- for id := range osv { +- ids = append(ids, id) +- } +- sort.Strings(ids) +- modfile := env.Sandbox.Workdir.RelPath(k.Path()) +- got[modfile] = fetchVulncheckResult{ +- IDs: ids, +- Mode: r.Mode, +- } +- } +- if diff := cmp.Diff(want, got); diff != "" { +- t.Errorf("fetch vulnchheck result = got %v, want %v: diff %v", got, want, diff) +- } -} -diff -urN a/gopls/internal/regtest/completion/completion_test.go b/gopls/internal/regtest/completion/completion_test.go ---- a/gopls/internal/regtest/completion/completion_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/completion/completion_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,1005 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package completion +-const workspace1 = ` +--- go.mod -- +-module golang.org/entry - --import ( -- "fmt" -- "sort" -- "strings" -- "testing" -- "time" +-go 1.18 - -- "github.com/google/go-cmp/cmp" -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/hooks" -- "golang.org/x/tools/gopls/internal/lsp/fake" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" -- "golang.org/x/tools/internal/testenv" +-require golang.org/cmod v1.1.3 +- +-require ( +- golang.org/amod v1.0.0 // indirect +- golang.org/bmod v0.5.0 // indirect -) +--- go.sum -- +-golang.org/amod v1.0.0 h1:EUQOI2m5NhQZijXZf8WimSnnWubaFNrrKUH/PopTN8k= +-golang.org/amod v1.0.0/go.mod h1:yvny5/2OtYFomKt8ax+WJGvN6pfN1pqjGnn7DQLUi6E= +-golang.org/bmod v0.5.0 h1:KgvUulMyMiYRB7suKA0x+DfWRVdeyPgVJvcishTH+ng= +-golang.org/bmod v0.5.0/go.mod h1:f6o+OhF66nz/0BBc/sbCsshyPRKMSxZIlG50B/bsM4c= +-golang.org/cmod v1.1.3 h1:PJ7rZFTk7xGAunBRDa0wDe7rZjZ9R/vr1S2QkVVCngQ= +-golang.org/cmod v1.1.3/go.mod h1:eCR8dnmvLYQomdeAZRCPgS5JJihXtqOQrpEkNj5feQA= +--- x/x.go -- +-package x - --func TestMain(m *testing.M) { -- bug.PanicOnBugs = true -- Main(m, hooks.Options) --} +-import ( +- "golang.org/cmod/c" +- "golang.org/entry/y" +-) - --const proxy = ` ---- example.com@v1.2.3/go.mod -- --module example.com +-func X() { +- c.C1().Vuln1() // vuln use: X -> Vuln1 +-} - --go 1.12 ---- example.com@v1.2.3/blah/blah.go -- --package blah +-func CallY() { +- y.Y() // vuln use: CallY -> y.Y -> bvuln.Vuln +-} - --const Name = "Blah" ---- random.org@v1.2.3/go.mod -- --module random.org +--- y/y.go -- +-package y - --go 1.12 ---- random.org@v1.2.3/blah/blah.go -- --package hello +-import "golang.org/cmod/c" - --const Name = "Hello" +-func Y() { +- c.C2()() // vuln use: Y -> bvuln.Vuln +-} -` - --func TestPackageCompletion(t *testing.T) { -- const files = ` ---- go.mod -- --module mod.com +-// cmod/c imports amod/avuln and bmod/bvuln. +-const proxy1 = ` +--- golang.org/cmod@v1.1.3/go.mod -- +-module golang.org/cmod - -go 1.12 ---- fruits/apple.go -- --package apple +--- golang.org/cmod@v1.1.3/c/c.go -- +-package c - --fun apple() int { -- return 0 +-import ( +- "golang.org/amod/avuln" +- "golang.org/bmod/bvuln" +-) +- +-type I interface { +- Vuln1() -} - ---- fruits/testfile.go -- --// this is a comment +-func C1() I { +- v := avuln.VulnData{} +- v.Vuln2() // vuln use +- return v +-} - --/* -- this is a multiline comment --*/ +-func C2() func() { +- return bvuln.Vuln +-} +--- golang.org/amod@v1.0.0/go.mod -- +-module golang.org/amod - --import "fmt" +-go 1.14 +--- golang.org/amod@v1.0.0/avuln/avuln.go -- +-package avuln - --func test() {} +-type VulnData struct {} +-func (v VulnData) Vuln1() {} +-func (v VulnData) Vuln2() {} +--- golang.org/amod@v1.0.4/go.mod -- +-module golang.org/amod - ---- fruits/testfile2.go -- --package +-go 1.14 +--- golang.org/amod@v1.0.4/avuln/avuln.go -- +-package avuln - ---- fruits/testfile3.go -- --pac ---- 123f_r.u~its-123/testfile.go -- --package +-type VulnData struct {} +-func (v VulnData) Vuln1() {} +-func (v VulnData) Vuln2() {} - ---- .invalid-dir@-name/testfile.go -- --package +--- golang.org/bmod@v0.5.0/go.mod -- +-module golang.org/bmod +- +-go 1.14 +--- golang.org/bmod@v0.5.0/bvuln/bvuln.go -- +-package bvuln +- +-func Vuln() { +- // something evil +-} +--- golang.org/bmod@v0.5.0/unused/unused.go -- +-package unused +- +-func Vuln() { +- // something evil +-} +--- golang.org/amod@v1.0.6/go.mod -- +-module golang.org/amod +- +-go 1.14 +--- golang.org/amod@v1.0.6/avuln/avuln.go -- +-package avuln +- +-type VulnData struct {} +-func (v VulnData) Vuln1() {} +-func (v VulnData) Vuln2() {} -` -- var ( -- testfile4 = "" -- testfile5 = "/*a comment*/ " -- testfile6 = "/*a comment*/\n" -- ) -- for _, tc := range []struct { -- name string -- filename string -- content *string -- triggerRegexp string -- want []string -- editRegexp string -- }{ -- { -- name: "package completion at valid position", -- filename: "fruits/testfile.go", -- triggerRegexp: "\n()", -- want: []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"}, -- editRegexp: "\n()", -- }, -- { -- name: "package completion in a comment", -- filename: "fruits/testfile.go", -- triggerRegexp: "th(i)s", -- want: nil, -- }, -- { -- name: "package completion in a multiline comment", -- filename: "fruits/testfile.go", -- triggerRegexp: `\/\*\n()`, -- want: nil, -- }, -- { -- name: "package completion at invalid position", -- filename: "fruits/testfile.go", -- triggerRegexp: "import \"fmt\"\n()", -- want: nil, -- }, -- { -- name: "package completion after keyword 'package'", -- filename: "fruits/testfile2.go", -- triggerRegexp: "package()", -- want: []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"}, -- editRegexp: "package\n", -- }, -- { -- name: "package completion with 'pac' prefix", -- filename: "fruits/testfile3.go", -- triggerRegexp: "pac()", -- want: []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"}, -- editRegexp: "pac", -- }, -- { -- name: "package completion for empty file", -- filename: "fruits/testfile4.go", -- triggerRegexp: "^$", -- content: &testfile4, -- want: []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"}, -- editRegexp: "^$", -- }, -- { -- name: "package completion without terminal newline", -- filename: "fruits/testfile5.go", -- triggerRegexp: `\*\/ ()`, -- content: &testfile5, -- want: []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"}, -- editRegexp: `\*\/ ()`, -- }, -- { -- name: "package completion on terminal newline", -- filename: "fruits/testfile6.go", -- triggerRegexp: `\*\/\n()`, -- content: &testfile6, -- want: []string{"package apple", "package apple_test", "package fruits", "package fruits_test", "package main"}, -- editRegexp: `\*\/\n()`, -- }, -- // Issue golang/go#44680 -- { -- name: "package completion for dir name with punctuation", -- filename: "123f_r.u~its-123/testfile.go", -- triggerRegexp: "package()", -- want: []string{"package fruits123", "package fruits123_test", "package main"}, -- editRegexp: "package\n", -- }, -- { -- name: "package completion for invalid dir name", -- filename: ".invalid-dir@-name/testfile.go", -- triggerRegexp: "package()", -- want: []string{"package main"}, -- editRegexp: "package\n", +- +-func vulnTestEnv(proxyData string) (*vulntest.DB, []RunOption, error) { +- db, err := vulntest.NewDatabase(context.Background(), []byte(vulnsData)) +- if err != nil { +- return nil, nil, nil +- } +- settings := Settings{ +- "codelenses": map[string]bool{ +- "run_govulncheck": true, - }, +- } +- ev := EnvVars{ +- // Let the analyzer read vulnerabilities data from the testdata/vulndb. +- "GOVULNDB": db.URI(), +- // When fetching stdlib package vulnerability info, +- // behave as if our go version is go1.18 for this testing. +- // The default behavior is to run `go env GOVERSION` (which isn't mutable env var). +- cache.GoVersionForVulnTest: "go1.18", +- "_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`. +- "GOSUMDB": "off", +- } +- return db, []RunOption{ProxyFiles(proxyData), ev, settings}, nil +-} +- +-func TestRunVulncheckPackageDiagnostics(t *testing.T) { +- db, opts0, err := vulnTestEnv(proxy1) +- if err != nil { +- t.Fatal(err) +- } +- defer db.Clean() +- +- checkVulncheckDiagnostics := func(env *Env, t *testing.T) { +- env.OpenFile("go.mod") +- +- gotDiagnostics := &protocol.PublishDiagnosticsParams{} +- env.AfterChange( +- Diagnostics(env.AtRegexp("go.mod", `golang.org/amod`)), +- ReadDiagnostics("go.mod", gotDiagnostics), +- ) +- +- testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{ +- "go.mod": { +- IDs: []string{"GO-2022-01", "GO-2022-02", "GO-2022-03"}, +- Mode: vulncheck.ModeImports, +- }, +- }) +- +- wantVulncheckDiagnostics := map[string]vulnDiagExpectation{ +- "golang.org/amod": { +- diagnostics: []vulnDiag{ +- { +- msg: "golang.org/amod has known vulnerabilities GO-2022-01, GO-2022-03.", +- severity: protocol.SeverityInformation, +- source: string(cache.Vulncheck), +- codeActions: []string{ +- "Run govulncheck to verify", +- "Upgrade to v1.0.6", +- "Upgrade to latest", +- }, +- }, +- }, +- codeActions: []string{ +- "Run govulncheck to verify", +- "Upgrade to v1.0.6", +- "Upgrade to latest", +- }, +- hover: []string{"GO-2022-01", "Fixed in v1.0.4.", "GO-2022-03"}, +- }, +- "golang.org/bmod": { +- diagnostics: []vulnDiag{ +- { +- msg: "golang.org/bmod has a vulnerability GO-2022-02.", +- severity: protocol.SeverityInformation, +- source: string(cache.Vulncheck), +- codeActions: []string{ +- "Run govulncheck to verify", +- }, +- }, +- }, +- codeActions: []string{ +- "Run govulncheck to verify", +- }, +- hover: []string{"GO-2022-02", "vuln in bmod (no fix)", "No fix is available."}, +- }, +- } +- +- for pattern, want := range wantVulncheckDiagnostics { +- modPathDiagnostics := testVulnDiagnostics(t, env, pattern, want, gotDiagnostics) +- +- gotActions := env.CodeAction("go.mod", modPathDiagnostics) +- if diff := diffCodeActions(gotActions, want.codeActions); diff != "" { +- t.Errorf("code actions for %q do not match, got %v, want %v\n%v\n", pattern, gotActions, want.codeActions, diff) +- continue +- } +- } +- } +- +- wantNoVulncheckDiagnostics := func(env *Env, t *testing.T) { +- env.OpenFile("go.mod") +- +- gotDiagnostics := &protocol.PublishDiagnosticsParams{} +- env.AfterChange( +- ReadDiagnostics("go.mod", gotDiagnostics), +- ) +- +- if len(gotDiagnostics.Diagnostics) > 0 { +- t.Errorf("Unexpected diagnostics: %v", stringify(gotDiagnostics)) +- } +- testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{}) +- } +- +- for _, tc := range []struct { +- name string +- setting Settings +- wantDiagnostics bool +- }{ +- {"imports", Settings{"ui.diagnostic.vulncheck": "Imports"}, true}, +- {"default", Settings{}, false}, +- {"invalid", Settings{"ui.diagnostic.vulncheck": "invalid"}, false}, - } { - t.Run(tc.name, func(t *testing.T) { -- Run(t, files, func(t *testing.T, env *Env) { -- if tc.content != nil { -- env.WriteWorkspaceFile(tc.filename, *tc.content) -- env.Await(env.DoneWithChangeWatchedFiles()) -- } -- env.OpenFile(tc.filename) -- completions := env.Completion(env.RegexpSearch(tc.filename, tc.triggerRegexp)) -- -- // Check that the completion item suggestions are in the range -- // of the file. {Start,End}.Line are zero-based. -- lineCount := len(strings.Split(env.BufferText(tc.filename), "\n")) -- for _, item := range completions.Items { -- if start := int(item.TextEdit.Range.Start.Line); start > lineCount { -- t.Fatalf("unexpected text edit range start line number: got %d, want <= %d", start, lineCount) -- } -- if end := int(item.TextEdit.Range.End.Line); end > lineCount { -- t.Fatalf("unexpected text edit range end line number: got %d, want <= %d", end, lineCount) -- } +- // override the settings options to enable diagnostics +- opts := append(opts0, tc.setting) +- WithOptions(opts...).Run(t, workspace1, func(t *testing.T, env *Env) { +- // TODO(hyangah): implement it, so we see GO-2022-01, GO-2022-02, and GO-2022-03. +- // Check that the actions we get when including all diagnostics at a location return the same result +- if tc.wantDiagnostics { +- checkVulncheckDiagnostics(env, t) +- } else { +- wantNoVulncheckDiagnostics(env, t) - } - -- if tc.want != nil { -- expectedLoc := env.RegexpSearch(tc.filename, tc.editRegexp) -- for _, item := range completions.Items { -- gotRng := item.TextEdit.Range -- if expectedLoc.Range != gotRng { -- t.Errorf("unexpected completion range for completion item %s: got %v, want %v", -- item.Label, gotRng, expectedLoc.Range) +- if tc.name == "imports" && tc.wantDiagnostics { +- // test we get only govulncheck-based diagnostics after "run govulncheck". +- var result command.RunVulncheckResult +- env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result) +- gotDiagnostics := &protocol.PublishDiagnosticsParams{} +- env.OnceMet( +- CompletedProgress(result.Token, nil), +- ShownMessage("Found"), +- ) +- env.OnceMet( +- Diagnostics(env.AtRegexp("go.mod", "golang.org/bmod")), +- ReadDiagnostics("go.mod", gotDiagnostics), +- ) +- // We expect only one diagnostic for GO-2022-02. +- count := 0 +- for _, diag := range gotDiagnostics.Diagnostics { +- if strings.Contains(diag.Message, "GO-2022-02") { +- count++ +- if got, want := diag.Severity, protocol.SeverityWarning; got != want { +- t.Errorf("Diagnostic for GO-2022-02 = %v, want %v", got, want) +- } - } - } -- } -- -- diff := compareCompletionLabels(tc.want, completions.Items) -- if diff != "" { -- t.Error(diff) +- if count != 1 { +- t.Errorf("Unexpected number of diagnostics about GO-2022-02 = %v, want 1:\n%+v", count, stringify(gotDiagnostics)) +- } - } - }) - }) - } -} - --func TestPackageNameCompletion(t *testing.T) { -- const files = ` ---- go.mod -- --module mod.com -- --go 1.12 ---- math/add.go -- --package ma --` -- -- want := []string{"ma", "ma_test", "main", "math", "math_test"} -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("math/add.go") -- completions := env.Completion(env.RegexpSearch("math/add.go", "package ma()")) -- -- diff := compareCompletionLabels(want, completions.Items) -- if diff != "" { -- t.Fatal(diff) -- } -- }) --} +-// TestRunGovulncheck_Expiry checks that govulncheck results expire after a +-// certain amount of time. +-func TestRunGovulncheck_Expiry(t *testing.T) { +- // For this test, set the max age to a duration smaller than the sleep below. +- defer func(prev time.Duration) { +- cache.MaxGovulncheckResultAge = prev +- }(cache.MaxGovulncheckResultAge) +- cache.MaxGovulncheckResultAge = 99 * time.Millisecond - --// TODO(rfindley): audit/clean up call sites for this helper, to ensure --// consistent test errors. --func compareCompletionLabels(want []string, gotItems []protocol.CompletionItem) string { -- var got []string -- for _, item := range gotItems { -- got = append(got, item.Label) -- if item.Label != item.InsertText && item.TextEdit == nil { -- // Label should be the same as InsertText, if InsertText is to be used -- return fmt.Sprintf("label not the same as InsertText %#v", item) -- } +- db, opts0, err := vulnTestEnv(proxy1) +- if err != nil { +- t.Fatal(err) - } +- defer db.Clean() - -- if len(got) == 0 && len(want) == 0 { -- return "" // treat nil and the empty slice as equivalent -- } +- WithOptions(opts0...).Run(t, workspace1, func(t *testing.T, env *Env) { +- env.OpenFile("go.mod") +- env.OpenFile("x/x.go") - -- if diff := cmp.Diff(want, got); diff != "" { -- return fmt.Sprintf("completion item mismatch (-want +got):\n%s", diff) -- } -- return "" +- var result command.RunVulncheckResult +- env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result) +- env.OnceMet( +- CompletedProgress(result.Token, nil), +- ShownMessage("Found"), +- ) +- // Sleep long enough for the results to expire. +- time.Sleep(100 * time.Millisecond) +- // Make an arbitrary edit to force re-diagnosis of the workspace. +- env.RegexpReplace("x/x.go", "package x", "package x ") +- env.AfterChange( +- NoDiagnostics(env.AtRegexp("go.mod", "golang.org/bmod")), +- ) +- }) -} - --func TestUnimportedCompletion(t *testing.T) { -- const mod = ` ---- go.mod -- --module mod.com -- --go 1.14 -- --require example.com v1.2.3 ---- go.sum -- --example.com v1.2.3 h1:ihBTGWGjTU3V4ZJ9OmHITkU9WQ4lGdQkMjgyLFk0FaY= --example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= ---- main.go -- --package main -- --func main() { -- _ = blah +-func stringify(a interface{}) string { +- data, _ := json.Marshal(a) +- return string(data) -} ---- main2.go -- --package main - --import "example.com/blah" +-func TestRunVulncheckWarning(t *testing.T) { +- db, opts, err := vulnTestEnv(proxy1) +- if err != nil { +- t.Fatal(err) +- } +- defer db.Clean() +- WithOptions(opts...).Run(t, workspace1, func(t *testing.T, env *Env) { +- env.OpenFile("go.mod") - --func _() { -- _ = blah.Hello --} --` -- WithOptions( -- ProxyFiles(proxy), -- ).Run(t, mod, func(t *testing.T, env *Env) { -- // Make sure the dependency is in the module cache and accessible for -- // unimported completions, and then remove it before proceeding. -- env.RemoveWorkspaceFile("main2.go") -- env.RunGoCommand("mod", "tidy") -- env.Await(env.DoneWithChangeWatchedFiles()) +- var result command.RunVulncheckResult +- env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result) +- gotDiagnostics := &protocol.PublishDiagnosticsParams{} +- env.OnceMet( +- CompletedProgress(result.Token, nil), +- ShownMessage("Found"), +- ) +- // Vulncheck diagnostics asynchronous to the vulncheck command. +- env.OnceMet( +- Diagnostics(env.AtRegexp("go.mod", `golang.org/amod`)), +- ReadDiagnostics("go.mod", gotDiagnostics), +- ) - -- // Trigger unimported completions for the example.com/blah package. -- env.OpenFile("main.go") -- env.Await(env.DoneWithOpen()) -- loc := env.RegexpSearch("main.go", "ah") -- completions := env.Completion(loc) -- if len(completions.Items) == 0 { -- t.Fatalf("no completion items") +- testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{ +- // All vulnerabilities (symbol-level, import-level, module-level) are reported. +- "go.mod": {IDs: []string{"GO-2022-01", "GO-2022-02", "GO-2022-03", "GO-2022-04"}, Mode: vulncheck.ModeGovulncheck}, +- }) +- env.OpenFile("x/x.go") +- env.OpenFile("y/y.go") +- wantDiagnostics := map[string]vulnDiagExpectation{ +- "golang.org/amod": { +- applyAction: "Upgrade to v1.0.6", +- diagnostics: []vulnDiag{ +- { +- msg: "golang.org/amod has a vulnerability used in the code: GO-2022-01.", +- severity: protocol.SeverityWarning, +- source: string(cache.Govulncheck), +- codeActions: []string{ +- "Upgrade to v1.0.4", +- "Upgrade to latest", +- "Reset govulncheck result", +- }, +- }, +- { +- msg: "golang.org/amod has a vulnerability GO-2022-03 that is not used in the code.", +- severity: protocol.SeverityInformation, +- source: string(cache.Govulncheck), +- codeActions: []string{ +- "Upgrade to v1.0.6", +- "Upgrade to latest", +- "Reset govulncheck result", +- }, +- }, +- }, +- codeActions: []string{ +- "Upgrade to v1.0.6", +- "Upgrade to latest", +- "Reset govulncheck result", +- }, +- hover: []string{"GO-2022-01", "Fixed in v1.0.4.", "GO-2022-03"}, +- }, +- "golang.org/bmod": { +- diagnostics: []vulnDiag{ +- { +- msg: "golang.org/bmod has a vulnerability used in the code: GO-2022-02.", +- severity: protocol.SeverityWarning, +- source: string(cache.Govulncheck), +- codeActions: []string{ +- "Reset govulncheck result", // no fix, but we should give an option to reset. +- }, +- }, +- }, +- codeActions: []string{ +- "Reset govulncheck result", // no fix, but we should give an option to reset. +- }, +- hover: []string{"GO-2022-02", "vuln in bmod (no fix)", "No fix is available."}, +- }, - } -- env.AcceptCompletion(loc, completions.Items[0]) // adds blah import to main.go -- env.Await(env.DoneWithChange()) - -- // Trigger completions once again for the blah.<> selector. -- env.RegexpReplace("main.go", "_ = blah", "_ = blah.") -- env.Await(env.DoneWithChange()) -- loc = env.RegexpSearch("main.go", "\n}") -- completions = env.Completion(loc) -- if len(completions.Items) != 1 { -- t.Fatalf("expected 1 completion item, got %v", len(completions.Items)) -- } -- item := completions.Items[0] -- if item.Label != "Name" { -- t.Fatalf("expected completion item blah.Name, got %v", item.Label) -- } -- env.AcceptCompletion(loc, item) +- for mod, want := range wantDiagnostics { +- modPathDiagnostics := testVulnDiagnostics(t, env, mod, want, gotDiagnostics) - -- // Await the diagnostics to add example.com/blah to the go.mod file. -- env.AfterChange( -- Diagnostics(env.AtRegexp("main.go", `"example.com/blah"`)), -- ) -- }) --} +- // Check that the actions we get when including all diagnostics at a location return the same result +- gotActions := env.CodeAction("go.mod", modPathDiagnostics) +- if diff := diffCodeActions(gotActions, want.codeActions); diff != "" { +- t.Errorf("code actions for %q do not match, expected %v, got %v\n%v\n", mod, want.codeActions, gotActions, diff) +- continue +- } - --// Test that completions still work with an undownloaded module, golang/go#43333. --func TestUndownloadedModule(t *testing.T) { -- // mod.com depends on example.com, but only in a file that's hidden by a -- // build tag, so the IWL won't download example.com. That will cause errors -- // in the go list -m call performed by the imports package. -- const files = ` ---- go.mod -- --module mod.com +- // Apply the code action matching applyAction. +- if want.applyAction == "" { +- continue +- } +- for _, action := range gotActions { +- if action.Title == want.applyAction { +- env.ApplyCodeAction(action) +- break +- } +- } +- } - --go 1.14 +- env.Await(env.DoneWithChangeWatchedFiles()) +- wantGoMod := `module golang.org/entry - --require example.com v1.2.3 ---- go.sum -- --example.com v1.2.3 h1:ihBTGWGjTU3V4ZJ9OmHITkU9WQ4lGdQkMjgyLFk0FaY= --example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= ---- useblah.go -- --// +build hidden +-go 1.18 - --package pkg --import "example.com/blah" --var _ = blah.Name ---- mainmod/mainmod.go -- --package mainmod +-require golang.org/cmod v1.1.3 - --const Name = "mainmod" +-require ( +- golang.org/amod v1.0.6 // indirect +- golang.org/bmod v0.5.0 // indirect +-) -` -- WithOptions(ProxyFiles(proxy)).Run(t, files, func(t *testing.T, env *Env) { -- env.CreateBuffer("import.go", "package pkg\nvar _ = mainmod.Name\n") -- env.SaveBuffer("import.go") -- content := env.ReadWorkspaceFile("import.go") -- if !strings.Contains(content, `import "mod.com/mainmod`) { -- t.Errorf("expected import of mod.com/mainmod in %q", content) +- if got := env.BufferText("go.mod"); got != wantGoMod { +- t.Fatalf("go.mod vulncheck fix failed:\n%s", compare.Text(wantGoMod, got)) - } - }) -} - --// Test that we can doctor the source code enough so the file is --// parseable and completion works as expected. --func TestSourceFixup(t *testing.T) { -- const files = ` +-func diffCodeActions(gotActions []protocol.CodeAction, want []string) string { +- var gotTitles []string +- for _, ca := range gotActions { +- gotTitles = append(gotTitles, ca.Title) +- } +- return cmp.Diff(want, gotTitles) +-} +- +-const workspace2 = ` --- go.mod -- --module mod.com +-module golang.org/entry - --go 1.12 ---- foo.go -- --package foo +-go 1.18 - --func _() { -- var s S -- if s. --} +-require golang.org/bmod v0.5.0 - --type S struct { -- i int +--- go.sum -- +-golang.org/bmod v0.5.0 h1:MT/ysNRGbCiURc5qThRFWaZ5+rK3pQRPo9w7dYZfMDk= +-golang.org/bmod v0.5.0/go.mod h1:k+zl+Ucu4yLIjndMIuWzD/MnOHy06wqr3rD++y0abVs= +--- x/x.go -- +-package x +- +-import "golang.org/bmod/bvuln" +- +-func F() { +- // Calls a benign func in bvuln. +- bvuln.OK() -} -` - -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("foo.go") -- completions := env.Completion(env.RegexpSearch("foo.go", `if s\.()`)) -- diff := compareCompletionLabels([]string{"i"}, completions.Items) -- if diff != "" { -- t.Fatal(diff) -- } -- }) --} +-const proxy2 = ` +--- golang.org/bmod@v0.5.0/bvuln/bvuln.go -- +-package bvuln - --func TestCompletion_Issue45510(t *testing.T) { -- const files = ` ---- go.mod -- --module mod.com +-func Vuln() {} // vulnerable. +-func OK() {} // ok. +-` - --go 1.12 ---- main.go -- --package main +-func TestGovulncheckInfo(t *testing.T) { +- db, opts, err := vulnTestEnv(proxy2) +- if err != nil { +- t.Fatal(err) +- } +- defer db.Clean() +- WithOptions(opts...).Run(t, workspace2, func(t *testing.T, env *Env) { +- env.OpenFile("go.mod") +- var result command.RunVulncheckResult +- env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result) +- gotDiagnostics := &protocol.PublishDiagnosticsParams{} +- env.OnceMet( +- CompletedProgress(result.Token, nil), +- ShownMessage("No vulnerabilities found"), // only count affecting vulnerabilities. +- ) - --func _() { -- type a *a -- var aaaa1, aaaa2 a -- var _ a = aaaa +- // Vulncheck diagnostics asynchronous to the vulncheck command. +- env.OnceMet( +- Diagnostics(env.AtRegexp("go.mod", "golang.org/bmod")), +- ReadDiagnostics("go.mod", gotDiagnostics), +- ) - -- type b a -- var bbbb1, bbbb2 b -- var _ b = bbbb --} +- testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{"go.mod": {IDs: []string{"GO-2022-02", "GO-2022-04"}, Mode: vulncheck.ModeGovulncheck}}) +- // wantDiagnostics maps a module path in the require +- // section of a go.mod to diagnostics that will be returned +- // when running vulncheck. +- wantDiagnostics := map[string]vulnDiagExpectation{ +- "golang.org/bmod": { +- diagnostics: []vulnDiag{ +- { +- msg: "golang.org/bmod has a vulnerability GO-2022-02 that is not used in the code.", +- severity: protocol.SeverityInformation, +- source: string(cache.Govulncheck), +- codeActions: []string{ +- "Reset govulncheck result", +- }, +- }, +- }, +- codeActions: []string{ +- "Reset govulncheck result", +- }, +- hover: []string{"GO-2022-02", "vuln in bmod (no fix)", "No fix is available."}, +- }, +- } - --type ( -- c *d -- d *e -- e **c --) +- var allActions []protocol.CodeAction +- for mod, want := range wantDiagnostics { +- modPathDiagnostics := testVulnDiagnostics(t, env, mod, want, gotDiagnostics) +- // Check that the actions we get when including all diagnostics at a location return the same result +- gotActions := env.CodeAction("go.mod", modPathDiagnostics) +- allActions = append(allActions, gotActions...) +- if diff := diffCodeActions(gotActions, want.codeActions); diff != "" { +- t.Errorf("code actions for %q do not match, expected %v, got %v\n%v\n", mod, want.codeActions, gotActions, diff) +- continue +- } +- } - --func _() { -- var ( -- xxxxc c -- xxxxd d -- xxxxe e -- ) +- // Clear Diagnostics by using one of the reset code actions. +- var reset protocol.CodeAction +- for _, a := range allActions { +- if a.Title == "Reset govulncheck result" { +- reset = a +- break +- } +- } +- if reset.Title != "Reset govulncheck result" { +- t.Errorf("failed to find a 'Reset govulncheck result' code action, got %v", allActions) +- } +- env.ApplyCodeAction(reset) - -- var _ c = xxxx -- var _ d = xxxx -- var _ e = xxxx +- env.Await(NoDiagnostics(ForFile("go.mod"))) +- }) -} --` -- -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") - -- tests := []struct { -- re string -- want []string -- }{ -- {`var _ a = aaaa()`, []string{"aaaa1", "aaaa2"}}, -- {`var _ b = bbbb()`, []string{"bbbb1", "bbbb2"}}, -- {`var _ c = xxxx()`, []string{"xxxxc", "xxxxd", "xxxxe"}}, -- {`var _ d = xxxx()`, []string{"xxxxc", "xxxxd", "xxxxe"}}, -- {`var _ e = xxxx()`, []string{"xxxxc", "xxxxd", "xxxxe"}}, +-// testVulnDiagnostics finds the require or module statement line for the requireMod in go.mod file +-// and runs checks if diagnostics and code actions associated with the line match expectation. +-func testVulnDiagnostics(t *testing.T, env *Env, pattern string, want vulnDiagExpectation, got *protocol.PublishDiagnosticsParams) []protocol.Diagnostic { +- t.Helper() +- loc := env.RegexpSearch("go.mod", pattern) +- var modPathDiagnostics []protocol.Diagnostic +- for _, w := range want.diagnostics { +- // Find the diagnostics at loc.start. +- var diag *protocol.Diagnostic +- for _, g := range got.Diagnostics { +- g := g +- if g.Range.Start == loc.Range.Start && w.msg == g.Message { +- modPathDiagnostics = append(modPathDiagnostics, g) +- diag = &g +- break +- } +- } +- if diag == nil { +- t.Errorf("no diagnostic at %q matching %q found\n", pattern, w.msg) +- continue +- } +- if diag.Severity != w.severity || diag.Source != w.source { +- t.Errorf("incorrect (severity, source) for %q, want (%s, %s) got (%s, %s)\n", w.msg, w.severity, w.source, diag.Severity, diag.Source) +- } +- // Check expected code actions appear. +- gotActions := env.CodeAction("go.mod", []protocol.Diagnostic{*diag}) +- if diff := diffCodeActions(gotActions, w.codeActions); diff != "" { +- t.Errorf("code actions for %q do not match, want %v, got %v\n%v\n", w.msg, w.codeActions, gotActions, diff) +- continue - } -- for _, tt := range tests { -- completions := env.Completion(env.RegexpSearch("main.go", tt.re)) -- diff := compareCompletionLabels(tt.want, completions.Items) -- if diff != "" { -- t.Errorf("%s: %s", tt.re, diff) +- } +- // Check that useful info is supplemented as hover. +- if len(want.hover) > 0 { +- hover, _ := env.Hover(loc) +- for _, part := range want.hover { +- if !strings.Contains(hover.Value, part) { +- t.Errorf("hover contents for %q do not match, want %v, got %v\n", pattern, strings.Join(want.hover, ","), hover.Value) +- break - } - } -- }) +- } +- return modPathDiagnostics -} - --func TestCompletionDeprecation(t *testing.T) { -- const files = ` ---- go.mod -- --module test.com -- --go 1.16 ---- prog.go -- --package waste --// Deprecated, use newFoof --func fooFunc() bool { -- return false +-type vulnRelatedInfo struct { +- Filename string +- Line uint32 +- Message string -} - --// Deprecated --const badPi = 3.14 -- --func doit() { -- if fooF -- panic() -- x := badP +-type vulnDiag struct { +- msg string +- severity protocol.DiagnosticSeverity +- // codeActions is a list titles of code actions that we get with this +- // diagnostics as the context. +- codeActions []string +- // relatedInfo is related info message prefixed by the file base. +- // See summarizeRelatedInfo. +- relatedInfo []vulnRelatedInfo +- // diagnostic source. +- source string -} --` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("prog.go") -- loc := env.RegexpSearch("prog.go", "if fooF") -- loc.Range.Start.Character += uint32(protocol.UTF16Len([]byte("if fooF"))) -- completions := env.Completion(loc) -- diff := compareCompletionLabels([]string{"fooFunc"}, completions.Items) -- if diff != "" { -- t.Error(diff) -- } -- if completions.Items[0].Tags == nil { -- t.Errorf("expected Tags to show deprecation %#v", completions.Items[0].Tags) -- } -- loc = env.RegexpSearch("prog.go", "= badP") -- loc.Range.Start.Character += uint32(protocol.UTF16Len([]byte("= badP"))) -- completions = env.Completion(loc) -- diff = compareCompletionLabels([]string{"badPi"}, completions.Items) -- if diff != "" { -- t.Error(diff) -- } -- if completions.Items[0].Tags == nil { -- t.Errorf("expected Tags to show deprecation %#v", completions.Items[0].Tags) -- } -- }) +- +-// vulnDiagExpectation maps a module path in the require +-// section of a go.mod to diagnostics that will be returned +-// when running vulncheck. +-type vulnDiagExpectation struct { +- // applyAction is the title of the code action to run for this module. +- // If empty, no code actions will be executed. +- applyAction string +- // diagnostics is the list of diagnostics we expect at the require line for +- // the module path. +- diagnostics []vulnDiag +- // codeActions is a list titles of code actions that we get with context +- // diagnostics. +- codeActions []string +- // hover message is the list of expected hover message parts for this go.mod require line. +- // all parts must appear in the hover message. +- hover []string -} +diff -urN a/gopls/internal/test/integration/misc/webserver_test.go b/gopls/internal/test/integration/misc/webserver_test.go +--- a/gopls/internal/test/integration/misc/webserver_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/misc/webserver_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,229 +0,0 @@ +-// Copyright 2024 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func TestUnimportedCompletion_VSCodeIssue1489(t *testing.T) { -- const src = ` ---- go.mod -- --module mod.com +-package misc - --go 1.14 +-import ( +- "html" +- "io" +- "net/http" +- "regexp" +- "strings" +- "testing" - ---- main.go -- --package main +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/command" +- . "golang.org/x/tools/gopls/internal/test/integration" +-) - --import "fmt" +-// TestWebServer exercises the web server created on demand +-// for code actions such as "View package documentation". +-func TestWebServer(t *testing.T) { +- const files = ` +--- go.mod -- +-module example.com - --func main() { -- fmt.Println("a") -- math.Sqr --} --` -- WithOptions( -- WindowsLineEndings(), -- Settings{"ui.completion.usePlaceholders": true}, -- ).Run(t, src, func(t *testing.T, env *Env) { -- // Trigger unimported completions for the mod.com package. -- env.OpenFile("main.go") -- env.Await(env.DoneWithOpen()) -- loc := env.RegexpSearch("main.go", "Sqr()") -- completions := env.Completion(loc) -- if len(completions.Items) == 0 { -- t.Fatalf("no completion items") -- } -- env.AcceptCompletion(loc, completions.Items[0]) -- env.Await(env.DoneWithChange()) -- got := env.BufferText("main.go") -- want := "package main\r\n\r\nimport (\r\n\t\"fmt\"\r\n\t\"math\"\r\n)\r\n\r\nfunc main() {\r\n\tfmt.Println(\"a\")\r\n\tmath.Sqrt(${1:x float64})\r\n}\r\n" -- if diff := cmp.Diff(want, got); diff != "" { -- t.Errorf("unimported completion (-want +got):\n%s", diff) -- } -- }) --} +--- a/a.go -- +-package a - --func TestUnimportedCompletionHasPlaceholders60269(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) // uses type params +-const A = 1 - -- // We can't express this as a marker test because it doesn't support AcceptCompletion. -- const src = ` +-// EOF +-` +- Run(t, files, func(t *testing.T, env *Env) { +- // Assert that the HTML page contains the expected const declaration. +- // (We may need to make allowances for HTML markup.) +- uri1 := viewPkgDoc(t, env, "a/a.go") +- doc1 := get(t, uri1) +- checkMatch(t, true, doc1, "const A =.*1") +- +- // Check that edits to the buffer (even unsaved) are +- // reflected in the HTML document. +- env.RegexpReplace("a/a.go", "// EOF", "func NewFunc() {}") +- env.Await(env.DoneDiagnosingChanges()) +- doc2 := get(t, uri1) +- checkMatch(t, true, doc2, "func NewFunc") +- +- // TODO(adonovan): assert some basic properties of the +- // HTML document using something like +- // golang.org/x/pkgsite/internal/testing/htmlcheck. +- +- // Grab the URL in the HTML source link for NewFunc. +- // (We don't have a DOM or JS interpreter so we have +- // to know something of the document internals here.) +- rx := regexp.MustCompile(`<h3 id='NewFunc'.*httpGET\("(.*)"\)`) +- openURL := html.UnescapeString(string(rx.FindSubmatch(doc2)[1])) +- +- // Fetch the document. Its result isn't important, +- // but it must have the side effect of another showDocument +- // downcall, this time for a "file:" URL, causing the +- // client editor to navigate to the source file. +- t.Log("extracted /open URL", openURL) +- get(t, openURL) +- +- // Check that that shown location is that of NewFunc. +- shownSource := shownDocument(t, env, "file:") +- gotLoc := protocol.Location{ +- URI: protocol.DocumentURI(shownSource.URI), // fishy conversion +- Range: *shownSource.Selection, +- } +- t.Log("showDocument(source file) URL:", gotLoc) +- wantLoc := env.RegexpSearch("a/a.go", `func ()NewFunc`) +- if gotLoc != wantLoc { +- t.Errorf("got location %v, want %v", gotLoc, wantLoc) +- } +- }) +-} +- +-func TestRenderNoPanic66449(t *testing.T) { +- // This particular input triggered a latent bug in doc.New +- // that would corrupt the AST while filtering out unexported +- // symbols such as b, causing nodeHTML to panic. +- // Now it doesn't crash. +- // +- // We also check cross-reference anchors for all symbols. +- const files = ` --- go.mod -- -module example.com --go 1.12 - --- a/a.go -- -package a - --var _ = b.F -- ---- b/b.go -- --package b +-// The 'π' suffix is to elimimate spurious matches with other HTML substrings, +-// in particular the random base64 secret tokens that appear in gopls URLs. - --func F0(a, b int, c float64) {} --func F1(int, chan *string) {} --func F2[K, V any](map[K]V, chan V) {} // missing type parameters was issue #60959 --func F3[K comparable, V any](map[K]V, chan V) {} --` -- WithOptions( -- WindowsLineEndings(), -- Settings{"ui.completion.usePlaceholders": true}, -- ).Run(t, src, func(t *testing.T, env *Env) { -- env.OpenFile("a/a.go") -- env.Await(env.DoneWithOpen()) +-var Vπ, vπ = 0, 0 +-const Cπ, cπ = 0, 0 - -- // The table lists the expected completions of b.F as they appear in Items. -- const common = "package a\r\n\r\nimport \"example.com/b\"\r\n\r\nvar _ = " -- for i, want := range []string{ -- common + "b.F0(${1:a int}, ${2:b int}, ${3:c float64})\r\n", -- common + "b.F1(${1:_ int}, ${2:_ chan *string})\r\n", -- common + "b.F2[${1:K any}, ${2:V any}](${3:_ map[K]V}, ${4:_ chan V})\r\n", -- common + "b.F3[${1:K comparable}, ${2:V any}](${3:_ map[K]V}, ${4:_ chan V})\r\n", -- } { -- loc := env.RegexpSearch("a/a.go", "b.F()") -- completions := env.Completion(loc) -- if len(completions.Items) == 0 { -- t.Fatalf("no completion items") -- } -- saved := env.BufferText("a/a.go") -- env.AcceptCompletion(loc, completions.Items[i]) -- env.Await(env.DoneWithChange()) -- got := env.BufferText("a/a.go") -- if diff := cmp.Diff(want, got); diff != "" { -- t.Errorf("%d: unimported completion (-want +got):\n%s", i, diff) -- } -- env.SetBufferContent("a/a.go", saved) // restore -- } -- }) --} +-func Fπ() +-func fπ() - --func TestPackageMemberCompletionAfterSyntaxError(t *testing.T) { -- // This test documents the current broken behavior due to golang/go#58833. -- const src = ` ---- go.mod -- --module mod.com +-type Tπ int +-type tπ int - --go 1.14 +-func (Tπ) Mπ() {} +-func (Tπ) mπ() {} - ---- main.go -- --package main +-func (tπ) Mπ() {} +-func (tπ) mπ() {} +-` +- Run(t, files, func(t *testing.T, env *Env) { +- uri1 := viewPkgDoc(t, env, "a/a.go") +- doc := get(t, uri1) +- // (Ideally our code rendering would also +- // eliminate unexported symbols...) +- checkMatch(t, true, doc, "var Vπ, vπ = .*0.*0") +- checkMatch(t, true, doc, "const Cπ, cπ = .*0.*0") +- +- // Unexported funcs/types/... must still be discarded. +- checkMatch(t, true, doc, "Fπ") +- checkMatch(t, false, doc, "fπ") +- checkMatch(t, true, doc, "Tπ") +- checkMatch(t, false, doc, "tπ") +- +- // Also, check that anchors exist (only) for exported symbols. +- // exported: +- checkMatch(t, true, doc, "<a id='Vπ'") +- checkMatch(t, true, doc, "<a id='Cπ'") +- checkMatch(t, true, doc, "<h3 id='Tπ'") +- checkMatch(t, true, doc, "<h3 id='Fπ'") +- checkMatch(t, true, doc, "<h4 id='Tπ.Mπ'") +- // unexported: +- checkMatch(t, false, doc, "<a id='vπ'") +- checkMatch(t, false, doc, "<a id='cπ'") +- checkMatch(t, false, doc, "<h3 id='tπ'") +- checkMatch(t, false, doc, "<h3 id='fπ'") +- checkMatch(t, false, doc, "<h4 id='Tπ.mπ'") +- checkMatch(t, false, doc, "<h4 id='tπ.Mπ'") +- checkMatch(t, false, doc, "<h4 id='tπ.mπ'") +- }) +-} +- +-// viewPkgDoc invokes the "View package documention" code action in +-// the specified file. It returns the URI of the document, or fails +-// the test. +-func viewPkgDoc(t *testing.T, env *Env, filename string) protocol.URI { +- env.OpenFile(filename) +- +- // Invoke the "View package documentation" code +- // action to start the server. +- var docAction *protocol.CodeAction +- actions := env.CodeAction(filename, nil) +- for _, act := range actions { +- if act.Title == "View package documentation" { +- docAction = &act +- break +- } +- } +- if docAction == nil { +- t.Fatalf("can't find action with Title 'View package documentation', only %#v", +- actions) +- } - --import "math" +- // Execute the command. +- // Its side effect should be a single showDocument request. +- params := &protocol.ExecuteCommandParams{ +- Command: docAction.Command.Command, +- Arguments: docAction.Command.Arguments, +- } +- var result command.DebuggingResult +- env.ExecuteCommand(params, &result) - --func main() { -- math.Sqrt(,0) -- math.Ldex +- doc := shownDocument(t, env, "http:") +- if doc == nil { +- t.Fatalf("no showDocument call had 'http:' prefix") +- } +- t.Log("showDocument(package doc) URL:", doc.URI) +- return doc.URI -} --` -- Run(t, src, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- env.Await(env.DoneWithOpen()) -- loc := env.RegexpSearch("main.go", "Ldex()") -- completions := env.Completion(loc) -- if len(completions.Items) == 0 { -- t.Fatalf("no completion items") +- +-// shownDocument returns the first shown document matching the URI prefix. +-// It may be nil. +-// +-// TODO(adonovan): the integration test framework +-// needs a way to reset ShownDocuments so they don't +-// accumulate, necessitating the fragile prefix hack. +-func shownDocument(t *testing.T, env *Env, prefix string) *protocol.ShowDocumentParams { +- t.Helper() +- var shown []*protocol.ShowDocumentParams +- env.Await(ShownDocuments(&shown)) +- var first *protocol.ShowDocumentParams +- for _, sd := range shown { +- if strings.HasPrefix(sd.URI, prefix) { +- if first != nil { +- t.Errorf("got multiple showDocument requests: %#v", shown) +- break +- } +- first = sd - } -- env.AcceptCompletion(loc, completions.Items[0]) -- env.Await(env.DoneWithChange()) -- got := env.BufferText("main.go") -- // The completion of math.Ldex after the syntax error on the -- // previous line is not "math.Ldexp" but "math.Ldexmath.Abs". -- // (In VSCode, "Abs" wrongly appears in the completion menu.) -- // This is a consequence of poor error recovery in the parser -- // causing "math.Ldex" to become a BadExpr. -- want := "package main\n\nimport \"math\"\n\nfunc main() {\n\tmath.Sqrt(,0)\n\tmath.Ldexmath.Abs(${1:})\n}\n" -- if diff := cmp.Diff(want, got); diff != "" { -- t.Errorf("unimported completion (-want +got):\n%s", diff) +- } +- return first +-} +- +-// get fetches the content of a document over HTTP. +-func get(t *testing.T, url string) []byte { +- t.Helper() +- resp, err := http.Get(url) +- if err != nil { +- t.Fatal(err) +- } +- defer resp.Body.Close() +- got, err := io.ReadAll(resp.Body) +- if err != nil { +- t.Fatal(err) +- } +- return got +-} +- +-// checkMatch asserts that got matches (or doesn't match, if !want) the pattern. +-func checkMatch(t *testing.T, want bool, got []byte, pattern string) { +- t.Helper() +- if regexp.MustCompile(pattern).Match(got) != want { +- if want { +- t.Errorf("input did not match wanted pattern %q; got:\n%s", pattern, got) +- } else { +- t.Errorf("input matched unwanted pattern %q; got:\n%s", pattern, got) - } -- }) +- } -} +diff -urN a/gopls/internal/test/integration/misc/workspace_symbol_test.go b/gopls/internal/test/integration/misc/workspace_symbol_test.go +--- a/gopls/internal/test/integration/misc/workspace_symbol_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/misc/workspace_symbol_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,114 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func TestCompleteAllFields(t *testing.T) { -- // This test verifies that completion results always include all struct fields. -- // See golang/go#53992. +-package misc - -- const src = ` +-import ( +- "testing" +- +- "github.com/google/go-cmp/cmp" +- . "golang.org/x/tools/gopls/internal/test/integration" +- "golang.org/x/tools/gopls/internal/settings" +-) +- +-func TestWorkspaceSymbolMissingMetadata(t *testing.T) { +- const files = ` --- go.mod -- -module mod.com - --go 1.18 -- ---- p/p.go -- +-go 1.17 +--- a.go -- -package p - --import ( -- "fmt" +-const K1 = "a.go" +--- exclude.go -- - -- . "net/http" -- . "runtime" -- . "go/types" -- . "go/parser" -- . "go/ast" --) +-//go:build exclude +-// +build exclude - --type S struct { -- a, b, c, d, e, f, g, h, i, j, k, l, m int -- n, o, p, q, r, s, t, u, v, w, x, y, z int --} +-package exclude - --func _() { -- var s S -- fmt.Println(s.) --} +-const K2 = "exclude.go" -` - -- WithOptions(Settings{ -- "completionBudget": "1ns", // must be non-zero as 0 => infinity -- }).Run(t, src, func(t *testing.T, env *Env) { -- wantFields := make(map[string]bool) -- for c := 'a'; c <= 'z'; c++ { -- wantFields[string(c)] = true -- } -- -- env.OpenFile("p/p.go") -- // Make an arbitrary edit to ensure we're not hitting the cache. -- env.EditBuffer("p/p.go", fake.NewEdit(0, 0, 0, 0, fmt.Sprintf("// current time: %v\n", time.Now()))) -- loc := env.RegexpSearch("p/p.go", `s\.()`) -- completions := env.Completion(loc) -- gotFields := make(map[string]bool) -- for _, item := range completions.Items { -- if item.Kind == protocol.FieldCompletion { -- gotFields[item.Label] = true -- } -- } +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("a.go") +- checkSymbols(env, "K", "K1") - -- if diff := cmp.Diff(wantFields, gotFields); diff != "" { -- t.Errorf("Completion(...) returned mismatching fields (-want +got):\n%s", diff) -- } +- // Opening up an ignored file will result in an overlay with missing +- // metadata, but this shouldn't break workspace symbols requests. +- env.OpenFile("exclude.go") +- checkSymbols(env, "K", "K1") - }) -} - --func TestDefinition(t *testing.T) { -- testenv.NeedsGo1Point(t, 17) // in go1.16, The FieldList in func x is not empty -- files := ` +-func TestWorkspaceSymbolSorting(t *testing.T) { +- const files = ` --- go.mod -- -module mod.com - --go 1.18 ---- a_test.go -- --package foo +-go 1.17 +--- a/a.go -- +-package a +- +-const ( +- Foo = iota +- FooBar +- Fooey +- Fooex +- Fooest +-) -` -- tests := []struct { -- line string // the sole line in the buffer after the package statement -- pat string // the pattern to search for -- want []string // expected completions -- }{ -- {"func T", "T", []string{"TestXxx(t *testing.T)", "TestMain(m *testing.M)"}}, -- {"func T()", "T", []string{"TestMain", "Test"}}, -- {"func TestM", "TestM", []string{"TestMain(m *testing.M)", "TestM(t *testing.T)"}}, -- {"func TestM()", "TestM", []string{"TestMain"}}, -- {"func TestMi", "TestMi", []string{"TestMi(t *testing.T)"}}, -- {"func TestMi()", "TestMi", nil}, -- {"func TestG", "TestG", []string{"TestG(t *testing.T)"}}, -- {"func TestG(", "TestG", nil}, -- {"func Ben", "B", []string{"BenchmarkXxx(b *testing.B)"}}, -- {"func Ben(", "Ben", []string{"Benchmark"}}, -- {"func BenchmarkFoo", "BenchmarkFoo", []string{"BenchmarkFoo(b *testing.B)"}}, -- {"func BenchmarkFoo(", "BenchmarkFoo", nil}, -- {"func Fuz", "F", []string{"FuzzXxx(f *testing.F)"}}, -- {"func Fuz(", "Fuz", []string{"Fuzz"}}, -- {"func Testx", "Testx", nil}, -- {"func TestMe(t *testing.T)", "TestMe", nil}, -- {"func Te(t *testing.T)", "Te", []string{"TestMain", "Test"}}, -- } -- fname := "a_test.go" -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile(fname) -- env.Await(env.DoneWithOpen()) -- for _, test := range tests { -- env.SetBufferContent(fname, "package foo\n"+test.line) -- loc := env.RegexpSearch(fname, test.pat) -- loc.Range.Start.Character += uint32(protocol.UTF16Len([]byte(test.pat))) -- completions := env.Completion(loc) -- if diff := compareCompletionLabels(test.want, completions.Items); diff != "" { -- t.Error(diff) -- } -- } +- +- var symbolMatcher = string(settings.SymbolFastFuzzy) +- WithOptions( +- Settings{"symbolMatcher": symbolMatcher}, +- ).Run(t, files, func(t *testing.T, env *Env) { +- checkSymbols(env, "Foo", +- "Foo", // prefer exact segment matches first +- "FooBar", // ...followed by exact word matches +- "Fooex", // shorter than Fooest, FooBar, lexically before Fooey +- "Fooey", // shorter than Fooest, Foobar +- "Fooest", +- ) - }) -} - --// Test that completing a definition replaces source text when applied, golang/go#56852. --// Note: With go <= 1.16 the completions does not add parameters and fails these tests. --func TestDefinitionReplaceRange(t *testing.T) { -- testenv.NeedsGo1Point(t, 17) -- -- const mod = ` +-func TestWorkspaceSymbolSpecialPatterns(t *testing.T) { +- const files = ` --- go.mod -- -module mod.com - -go 1.17 +--- a/a.go -- +-package a +- +-const ( +- AxxBxxCxx +- ABC +-) -` - -- tests := []struct { -- name string -- before, after string -- }{ -- { -- name: "func TestMa", -- before: ` --package foo_test +- var symbolMatcher = string(settings.SymbolFastFuzzy) +- WithOptions( +- Settings{"symbolMatcher": symbolMatcher}, +- ).Run(t, files, func(t *testing.T, env *Env) { +- checkSymbols(env, "ABC", "ABC", "AxxBxxCxx") +- checkSymbols(env, "'ABC", "ABC") +- checkSymbols(env, "^mod.com", "mod.com/a.ABC", "mod.com/a.AxxBxxCxx") +- checkSymbols(env, "^mod.com Axx", "mod.com/a.AxxBxxCxx") +- checkSymbols(env, "C$", "ABC") +- }) +-} - --func TestMa --`, -- after: ` --package foo_test +-func checkSymbols(env *Env, query string, want ...string) { +- env.T.Helper() +- var got []string +- for _, info := range env.Symbol(query) { +- got = append(got, info.Name) +- } +- if diff := cmp.Diff(got, want); diff != "" { +- env.T.Errorf("unexpected Symbol(%q) result (+want -got):\n%s", query, diff) +- } +-} +diff -urN a/gopls/internal/test/integration/modfile/modfile_test.go b/gopls/internal/test/integration/modfile/modfile_test.go +--- a/gopls/internal/test/integration/modfile/modfile_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/modfile/modfile_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,1219 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func TestMain(m *testing.M) --`, -- }, -- { -- name: "func TestSome", -- before: ` --package foo_test +-package modfile - --func TestSome --`, -- after: ` --package foo_test +-import ( +- "path/filepath" +- "runtime" +- "strings" +- "testing" - --func TestSome(t *testing.T) --`, -- }, -- { -- name: "func Bench", -- before: ` --package foo_test +- "golang.org/x/tools/gopls/internal/hooks" +- "golang.org/x/tools/gopls/internal/test/compare" +- . "golang.org/x/tools/gopls/internal/test/integration" +- "golang.org/x/tools/gopls/internal/util/bug" - --func Bench --`, -- // Note: Snippet with escaped }. -- after: ` --package foo_test +- "golang.org/x/tools/gopls/internal/protocol" +-) - --func Benchmark${1:Xxx}(b *testing.B) { -- $0 --\} --`, -- }, -- } +-func TestMain(m *testing.M) { +- bug.PanicOnBugs = true +- Main(m, hooks.Options) +-} - -- Run(t, mod, func(t *testing.T, env *Env) { -- env.CreateBuffer("foo_test.go", "") +-const workspaceProxy = ` +--- example.com@v1.2.3/go.mod -- +-module example.com - -- for _, tst := range tests { -- tst.before = strings.Trim(tst.before, "\n") -- tst.after = strings.Trim(tst.after, "\n") -- env.SetBufferContent("foo_test.go", tst.before) +-go 1.12 +--- example.com@v1.2.3/blah/blah.go -- +-package blah - -- loc := env.RegexpSearch("foo_test.go", tst.name) -- loc.Range.Start.Character = uint32(protocol.UTF16Len([]byte(tst.name))) -- completions := env.Completion(loc) -- if len(completions.Items) == 0 { -- t.Fatalf("no completion items") -- } +-func SaySomething() { +- fmt.Println("something") +-} +--- random.org@v1.2.3/go.mod -- +-module random.org - -- env.AcceptCompletion(loc, completions.Items[0]) -- env.Await(env.DoneWithChange()) -- if buf := env.BufferText("foo_test.go"); buf != tst.after { -- t.Errorf("%s:incorrect completion: got %q, want %q", tst.name, buf, tst.after) -- } -- } -- }) +-go 1.12 +--- random.org@v1.2.3/bye/bye.go -- +-package bye +- +-func Goodbye() { +- println("Bye") -} +-` - --func TestGoWorkCompletion(t *testing.T) { -- const files = ` ---- go.work -- --go 1.18 +-const proxy = ` +--- example.com@v1.2.3/go.mod -- +-module example.com - --use ./a --use ./a/ba --use ./a/b/ --use ./dir/foo --use ./dir/foobar/ ---- a/go.mod -- ---- go.mod -- ---- a/bar/go.mod -- ---- a/b/c/d/e/f/go.mod -- ---- dir/bar -- ---- dir/foobar/go.mod -- --` +-go 1.12 +--- example.com@v1.2.3/blah/blah.go -- +-package blah - -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("go.work") +-const Name = "Blah" +--- random.org@v1.2.3/go.mod -- +-module random.org - -- tests := []struct { -- re string -- want []string -- }{ -- {`use ()\.`, []string{".", "./a", "./a/bar", "./dir/foobar"}}, -- {`use \.()`, []string{"", "/a", "/a/bar", "/dir/foobar"}}, -- {`use \./()`, []string{"a", "a/bar", "dir/foobar"}}, -- {`use ./a()`, []string{"", "/b/c/d/e/f", "/bar"}}, -- {`use ./a/b()`, []string{"/c/d/e/f", "ar"}}, -- {`use ./a/b/()`, []string{`c/d/e/f`}}, -- {`use ./a/ba()`, []string{"r"}}, -- {`use ./dir/foo()`, []string{"bar"}}, -- {`use ./dir/foobar/()`, []string{}}, -- } -- for _, tt := range tests { -- completions := env.Completion(env.RegexpSearch("go.work", tt.re)) -- diff := compareCompletionLabels(tt.want, completions.Items) -- if diff != "" { -- t.Errorf("%s: %s", tt.re, diff) -- } -- } -- }) --} +-go 1.12 +--- random.org@v1.2.3/blah/blah.go -- +-package hello - --func TestBuiltinCompletion(t *testing.T) { -- const files = ` ---- go.mod -- +-const Name = "Hello" +-` +- +-func TestModFileModification(t *testing.T) { +- const untidyModule = ` +--- a/go.mod -- -module mod.com - --go 1.18 ---- a.go -- --package a +--- a/main.go -- +-package main - --func _() { -- // here +-import "example.com/blah" +- +-func main() { +- println(blah.Name) -} -` - -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("a.go") -- result := env.Completion(env.RegexpSearch("a.go", `// here`)) -- builtins := []string{ -- "any", "append", "bool", "byte", "cap", "close", -- "comparable", "complex", "complex128", "complex64", "copy", "delete", -- "error", "false", "float32", "float64", "imag", "int", "int16", "int32", -- "int64", "int8", "len", "make", "new", "panic", "print", "println", "real", -- "recover", "rune", "string", "true", "uint", "uint16", "uint32", "uint64", -- "uint8", "uintptr", "nil", -- } -- if testenv.Go1Point() >= 21 { -- builtins = append(builtins, "clear", "max", "min") -- } -- sort.Strings(builtins) -- var got []string +- runner := RunMultiple{ +- {"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, +- {"nested", WithOptions(ProxyFiles(proxy))}, +- } - -- for _, item := range result.Items { -- // TODO(rfindley): for flexibility, ignore zero while it is being -- // implemented. Remove this if/when zero lands. -- if item.Label != "zero" { -- got = append(got, item.Label) +- t.Run("basic", func(t *testing.T) { +- runner.Run(t, untidyModule, func(t *testing.T, env *Env) { +- // Open the file and make sure that the initial workspace load does not +- // modify the go.mod file. +- goModContent := env.ReadWorkspaceFile("a/go.mod") +- env.OpenFile("a/main.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/main.go", "\"example.com/blah\"")), +- ) +- if got := env.ReadWorkspaceFile("a/go.mod"); got != goModContent { +- t.Fatalf("go.mod changed on disk:\n%s", compare.Text(goModContent, got)) - } -- } -- sort.Strings(got) +- // Save the buffer, which will format and organize imports. +- // Confirm that the go.mod file still does not change. +- env.SaveBuffer("a/main.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/main.go", "\"example.com/blah\"")), +- ) +- if got := env.ReadWorkspaceFile("a/go.mod"); got != goModContent { +- t.Fatalf("go.mod changed on disk:\n%s", compare.Text(goModContent, got)) +- } +- }) +- }) - -- if diff := cmp.Diff(builtins, got); diff != "" { -- t.Errorf("Completion: unexpected mismatch (-want +got):\n%s", diff) -- } +- // Reproduce golang/go#40269 by deleting and recreating main.go. +- t.Run("delete main.go", func(t *testing.T) { +- runner.Run(t, untidyModule, func(t *testing.T, env *Env) { +- goModContent := env.ReadWorkspaceFile("a/go.mod") +- mainContent := env.ReadWorkspaceFile("a/main.go") +- env.OpenFile("a/main.go") +- env.SaveBuffer("a/main.go") +- +- // Ensure that we're done processing all the changes caused by opening +- // and saving above. If not, we may run into a file locking issue on +- // windows. +- // +- // If this proves insufficient, env.RemoveWorkspaceFile can be updated to +- // retry file lock errors on windows. +- env.AfterChange() +- env.RemoveWorkspaceFile("a/main.go") +- +- // TODO(rfindley): awaiting here shouldn't really be necessary. We should +- // be consistent eventually. +- // +- // Probably this was meant to exercise a race with the change below. +- env.AfterChange() +- +- env.WriteWorkspaceFile("a/main.go", mainContent) +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/main.go", "\"example.com/blah\"")), +- ) +- if got := env.ReadWorkspaceFile("a/go.mod"); got != goModContent { +- t.Fatalf("go.mod changed on disk:\n%s", compare.Text(goModContent, got)) +- } +- }) - }) -} - --func TestOverlayCompletion(t *testing.T) { -- const files = ` ---- go.mod -- --module foo.test +-func TestGoGetFix(t *testing.T) { +- const mod = ` +--- a/go.mod -- +-module mod.com - --go 1.18 +-go 1.12 - ---- foo/foo.go -- --package foo +--- a/main.go -- +-package main - --type Foo struct{} +-import "example.com/blah" +- +-var _ = blah.Name -` - -- Run(t, files, func(t *testing.T, env *Env) { -- env.CreateBuffer("nodisk/nodisk.go", ` --package nodisk +- const want = `module mod.com - --import ( -- "foo.test/foo" --) +-go 1.12 - --func _() { -- foo.Foo() --} --`) -- list := env.Completion(env.RegexpSearch("nodisk/nodisk.go", "foo.(Foo)")) -- want := []string{"Foo"} -- var got []string -- for _, item := range list.Items { -- got = append(got, item.Label) +-require example.com v1.2.3 +-` +- +- RunMultiple{ +- {"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, +- {"nested", WithOptions(ProxyFiles(proxy))}, +- }.Run(t, mod, func(t *testing.T, env *Env) { +- if strings.Contains(t.Name(), "workspace_module") { +- t.Skip("workspace module mode doesn't set -mod=readonly") - } -- if diff := cmp.Diff(want, got); diff != "" { -- t.Errorf("Completion: unexpected mismatch (-want +got):\n%s", diff) +- env.OpenFile("a/main.go") +- var d protocol.PublishDiagnosticsParams +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/main.go", `"example.com/blah"`)), +- ReadDiagnostics("a/main.go", &d), +- ) +- var goGetDiag protocol.Diagnostic +- for _, diag := range d.Diagnostics { +- if strings.Contains(diag.Message, "could not import") { +- goGetDiag = diag +- } +- } +- env.ApplyQuickFixes("a/main.go", []protocol.Diagnostic{goGetDiag}) +- if got := env.ReadWorkspaceFile("a/go.mod"); got != want { +- t.Fatalf("unexpected go.mod content:\n%s", compare.Text(want, got)) - } - }) -} - --// Fix for golang/go#60062: unimported completion included "golang.org/toolchain" results. --func TestToolchainCompletions(t *testing.T) { -- const files = ` ---- go.mod -- --module foo.test/foo +-// Tests that multiple missing dependencies gives good single fixes. +-func TestMissingDependencyFixes(t *testing.T) { +- const mod = ` +--- a/go.mod -- +-module mod.com - --go 1.21 +-go 1.12 - ---- foo.go -- --package foo +--- a/main.go -- +-package main - --func _() { -- os.Open --} +-import "example.com/blah" +-import "random.org/blah" - --func _() { -- strings --} +-var _, _ = blah.Name, hello.Name -` - -- const proxy = ` ---- golang.org/toolchain@v0.0.1-go1.21.1.linux-amd64/go.mod -- --module golang.org/toolchain ---- golang.org/toolchain@v0.0.1-go1.21.1.linux-amd64/src/os/os.go -- --package os +- const want = `module mod.com - --func Open() {} ---- golang.org/toolchain@v0.0.1-go1.21.1.linux-amd64/src/strings/strings.go -- --package strings +-go 1.12 - --func Join() {} +-require random.org v1.2.3 -` - -- WithOptions( -- ProxyFiles(proxy), -- ).Run(t, files, func(t *testing.T, env *Env) { -- env.RunGoCommand("mod", "download", "golang.org/toolchain@v0.0.1-go1.21.1.linux-amd64") -- env.OpenFile("foo.go") -- -- for _, pattern := range []string{"os.Open()", "string()"} { -- loc := env.RegexpSearch("foo.go", pattern) -- res := env.Completion(loc) -- for _, item := range res.Items { -- if strings.Contains(item.Detail, "golang.org/toolchain") { -- t.Errorf("Completion(...) returned toolchain item %#v", item) -- } +- RunMultiple{ +- {"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, +- {"nested", WithOptions(ProxyFiles(proxy))}, +- }.Run(t, mod, func(t *testing.T, env *Env) { +- env.OpenFile("a/main.go") +- var d protocol.PublishDiagnosticsParams +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/main.go", `"random.org/blah"`)), +- ReadDiagnostics("a/main.go", &d), +- ) +- var randomDiag protocol.Diagnostic +- for _, diag := range d.Diagnostics { +- if strings.Contains(diag.Message, "random.org") { +- randomDiag = diag - } - } +- env.ApplyQuickFixes("a/main.go", []protocol.Diagnostic{randomDiag}) +- if got := env.ReadWorkspaceFile("a/go.mod"); got != want { +- t.Fatalf("unexpected go.mod content:\n%s", compare.Text(want, got)) +- } - }) -} -diff -urN a/gopls/internal/regtest/completion/postfix_snippet_test.go b/gopls/internal/regtest/completion/postfix_snippet_test.go ---- a/gopls/internal/regtest/completion/postfix_snippet_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/completion/postfix_snippet_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,590 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package completion - --import ( -- "strings" -- "testing" +-// Tests that multiple missing dependencies gives good single fixes. +-func TestMissingDependencyFixesWithGoWork(t *testing.T) { +- const mod = ` +--- go.work -- +-go 1.18 - -- . "golang.org/x/tools/gopls/internal/lsp/regtest" +-use ( +- ./a -) -- --func TestPostfixSnippetCompletion(t *testing.T) { -- const mod = ` ---- go.mod -- +--- a/go.mod -- -module mod.com - -go 1.12 --` - -- cases := []struct { -- name string -- before, after string -- }{ -- { -- name: "sort", -- before: ` --package foo +--- a/main.go -- +-package main - --func _() { -- var foo []int -- foo.sort --} --`, -- after: ` --package foo +-import "example.com/blah" +-import "random.org/blah" - --import "sort" +-var _, _ = blah.Name, hello.Name +-` - --func _() { -- var foo []int -- sort.Slice(foo, func(i, j int) bool { -- $0 --}) --} --`, -- }, -- { -- name: "sort_renamed_sort_package", -- before: ` --package foo +- const want = `module mod.com - --import blahsort "sort" +-go 1.12 - --var j int +-require random.org v1.2.3 +-` - --func _() { -- var foo []int -- foo.sort +- RunMultiple{ +- {"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, +- {"nested", WithOptions(ProxyFiles(proxy))}, +- }.Run(t, mod, func(t *testing.T, env *Env) { +- env.OpenFile("a/main.go") +- var d protocol.PublishDiagnosticsParams +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/main.go", `"random.org/blah"`)), +- ReadDiagnostics("a/main.go", &d), +- ) +- var randomDiag protocol.Diagnostic +- for _, diag := range d.Diagnostics { +- if strings.Contains(diag.Message, "random.org") { +- randomDiag = diag +- } +- } +- env.ApplyQuickFixes("a/main.go", []protocol.Diagnostic{randomDiag}) +- if got := env.ReadWorkspaceFile("a/go.mod"); got != want { +- t.Fatalf("unexpected go.mod content:\n%s", compare.Text(want, got)) +- } +- }) -} --`, -- after: ` --package foo - --import blahsort "sort" +-func TestIndirectDependencyFix(t *testing.T) { +- const mod = ` +--- a/go.mod -- +-module mod.com - --var j int +-go 1.12 - --func _() { -- var foo []int -- blahsort.Slice(foo, func(i, j2 int) bool { -- $0 --}) --} --`, -- }, -- { -- name: "last", -- before: ` --package foo +-require example.com v1.2.3 // indirect +--- a/go.sum -- +-example.com v1.2.3 h1:ihBTGWGjTU3V4ZJ9OmHITkU9WQ4lGdQkMjgyLFk0FaY= +-example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= +--- a/main.go -- +-package main - --func _() { -- var s struct { i []int } -- s.i.last --} --`, -- after: ` --package foo +-import "example.com/blah" - --func _() { -- var s struct { i []int } -- s.i[len(s.i)-1] --} --`, -- }, -- { -- name: "reverse", -- before: ` --package foo +-func main() { +- fmt.Println(blah.Name) +-` +- const want = `module mod.com - --func _() { -- var foo []int -- foo.reverse --} --`, -- after: ` --package foo +-go 1.12 - --func _() { -- var foo []int -- for i, j := 0, len(foo)-1; i < j; i, j = i+1, j-1 { -- foo[i], foo[j] = foo[j], foo[i] --} +-require example.com v1.2.3 +-` - +- RunMultiple{ +- {"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, +- {"nested", WithOptions(ProxyFiles(proxy))}, +- }.Run(t, mod, func(t *testing.T, env *Env) { +- env.OpenFile("a/go.mod") +- var d protocol.PublishDiagnosticsParams +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/go.mod", "// indirect")), +- ReadDiagnostics("a/go.mod", &d), +- ) +- env.ApplyQuickFixes("a/go.mod", d.Diagnostics) +- if got := env.BufferText("a/go.mod"); got != want { +- t.Fatalf("unexpected go.mod content:\n%s", compare.Text(want, got)) +- } +- }) -} --`, -- }, -- { -- name: "slice_range", -- before: ` --package foo - --func _() { -- type myThing struct{} -- var foo []myThing -- foo.range --} --`, -- after: ` --package foo +-// Test to reproduce golang/go#39041. It adds a new require to a go.mod file +-// that already has an unused require. +-func TestNewDepWithUnusedDep(t *testing.T) { - --func _() { -- type myThing struct{} -- var foo []myThing -- for i, mt := range foo { -- $0 --} --} --`, -- }, -- { -- name: "append_stmt", -- before: ` --package foo +- const proxy = ` +--- github.com/esimov/caire@v1.2.5/go.mod -- +-module github.com/esimov/caire - --func _() { -- var foo []int -- foo.append --} --`, -- after: ` --package foo +-go 1.12 +--- github.com/esimov/caire@v1.2.5/caire.go -- +-package caire - --func _() { -- var foo []int -- foo = append(foo, $0) --} --`, -- }, -- { -- name: "append_expr", -- before: ` --package foo +-func RemoveTempImage() {} +--- google.golang.org/protobuf@v1.20.0/go.mod -- +-module google.golang.org/protobuf +- +-go 1.12 +--- google.golang.org/protobuf@v1.20.0/hello/hello.go -- +-package hello +-` +- const repro = ` +--- a/go.mod -- +-module mod.com - --func _() { -- var foo []int -- var _ []int = foo.append --} --`, -- after: ` --package foo +-go 1.14 - --func _() { -- var foo []int -- var _ []int = append(foo, $0) --} --`, -- }, -- { -- name: "slice_copy", -- before: ` --package foo +-require google.golang.org/protobuf v1.20.0 +--- a/go.sum -- +-github.com/esimov/caire v1.2.5 h1:OcqDII/BYxcBYj3DuwDKjd+ANhRxRqLa2n69EGje7qw= +-github.com/esimov/caire v1.2.5/go.mod h1:mXnjRjg3+WUtuhfSC1rKRmdZU9vJZyS1ZWU0qSvJhK8= +-google.golang.org/protobuf v1.20.0 h1:y9T1vAtFKQg0faFNMOxJU7WuEqPWolVkjIkU6aI8qCY= +-google.golang.org/protobuf v1.20.0/go.mod h1:FcqsytGClbtLv1ot8NvsJHjBi0h22StKVP+K/j2liKA= +--- a/main.go -- +-package main - --func _() { -- var foo []int -- foo.copy --} --`, -- after: ` --package foo +-import ( +- "github.com/esimov/caire" +-) - -func _() { -- var foo []int -- fooCopy := make([]int, len(foo)) --copy(fooCopy, foo) +- caire.RemoveTempImage() +-}` - --} --`, -- }, -- { -- name: "map_range", -- before: ` --package foo +- RunMultiple{ +- {"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, +- {"nested", WithOptions(ProxyFiles(proxy))}, +- }.Run(t, repro, func(t *testing.T, env *Env) { +- env.OpenFile("a/main.go") +- var d protocol.PublishDiagnosticsParams +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/main.go", `"github.com/esimov/caire"`)), +- ReadDiagnostics("a/main.go", &d), +- ) +- env.ApplyQuickFixes("a/main.go", d.Diagnostics) +- want := `module mod.com - --func _() { -- var foo map[string]int -- foo.range --} --`, -- after: ` --package foo +-go 1.14 - --func _() { -- var foo map[string]int -- for k, v := range foo { -- $0 --} +-require ( +- github.com/esimov/caire v1.2.5 +- google.golang.org/protobuf v1.20.0 +-) +-` +- if got := env.ReadWorkspaceFile("a/go.mod"); got != want { +- t.Fatalf("TestNewDepWithUnusedDep failed:\n%s", compare.Text(want, got)) +- } +- }) -} --`, -- }, -- { -- name: "map_clear", -- before: ` --package foo - --func _() { -- var foo map[string]int -- foo.clear --} --`, -- after: ` --package foo +-// TODO: For this test to be effective, the sandbox's file watcher must respect +-// the file watching GlobPattern in the capability registration. See +-// golang/go#39384. +-func TestModuleChangesOnDisk(t *testing.T) { +- const mod = ` +--- a/go.mod -- +-module mod.com - --func _() { -- var foo map[string]int -- for k := range foo { -- delete(foo, k) --} +-go 1.12 - --} --`, -- }, -- { -- name: "map_keys", -- before: ` --package foo +-require example.com v1.2.3 +--- a/go.sum -- +-example.com v1.2.3 h1:ihBTGWGjTU3V4ZJ9OmHITkU9WQ4lGdQkMjgyLFk0FaY= +-example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= +--- a/main.go -- +-package main - --func _() { -- var foo map[string]int -- foo.keys +-func main() { +- fmt.Println(blah.Name) +-` +- RunMultiple{ +- {"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, +- {"nested", WithOptions(ProxyFiles(proxy))}, +- }.Run(t, mod, func(t *testing.T, env *Env) { +- // With zero-config gopls, we must open a/main.go to have a View including a/go.mod. +- env.OpenFile("a/main.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/go.mod", "require")), +- ) +- env.RunGoCommandInDir("a", "mod", "tidy") +- env.AfterChange( +- NoDiagnostics(ForFile("a/go.mod")), +- ) +- }) -} --`, -- after: ` --package foo - --func _() { -- var foo map[string]int -- keys := make([]string, 0, len(foo)) --for k := range foo { -- keys = append(keys, k) --} +-// Tests golang/go#39784: a missing indirect dependency, necessary +-// due to blah@v2.0.0's incomplete go.mod file. +-func TestBadlyVersionedModule(t *testing.T) { +- const proxy = ` +--- example.com/blah/@v/v1.0.0.mod -- +-module example.com - --} --`, -- }, -- { -- name: "channel_range", -- before: ` --package foo +-go 1.12 +--- example.com/blah@v1.0.0/blah.go -- +-package blah - --func _() { -- foo := make(chan int) -- foo.range --} --`, -- after: ` --package foo +-const Name = "Blah" +--- example.com/blah/v2/@v/v2.0.0.mod -- +-module example.com - --func _() { -- foo := make(chan int) -- for e := range foo { -- $0 --} --} --`, -- }, -- { -- name: "var", -- before: ` --package foo +-go 1.12 +--- example.com/blah/v2@v2.0.0/blah.go -- +-package blah - --func foo() (int, error) { return 0, nil } +-import "example.com/blah" - --func _() { -- foo().var --} --`, -- after: ` --package foo +-var V1Name = blah.Name +-const Name = "Blah" +-` +- const files = ` +--- a/go.mod -- +-module mod.com - --func foo() (int, error) { return 0, nil } +-go 1.12 - --func _() { -- i, err := foo() --} --`, -- }, -- { -- name: "var_single_value", -- before: ` --package foo +-require example.com/blah/v2 v2.0.0 +--- a/go.sum -- +-example.com/blah v1.0.0 h1:kGPlWJbMsn1P31H9xp/q2mYI32cxLnCvauHN0AVaHnc= +-example.com/blah v1.0.0/go.mod h1:PZUQaGFeVjyDmAE8ywmLbmDn3fj4Ws8epg4oLuDzW3M= +-example.com/blah/v2 v2.0.0 h1:DNPsFPkKtTdxclRheaMCiYAoYizp6PuBzO0OmLOO0pY= +-example.com/blah/v2 v2.0.0/go.mod h1:UZiKbTwobERo/hrqFLvIQlJwQZQGxWMVY4xere8mj7w= +--- a/main.go -- +-package main - --func foo() error { return nil } +-import "example.com/blah/v2" - --func _() { -- foo().var --} --`, -- after: ` --package foo +-var _ = blah.Name +-` +- RunMultiple{ +- {"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, +- {"nested", WithOptions(ProxyFiles(proxy))}, +- }.Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("a/main.go") +- env.OpenFile("a/go.mod") +- var modDiags protocol.PublishDiagnosticsParams +- env.AfterChange( +- // We would like for the error to appear in the v2 module, but +- // as of writing non-workspace packages are not diagnosed. +- Diagnostics(env.AtRegexp("a/main.go", `"example.com/blah/v2"`), WithMessage("no required module provides")), +- Diagnostics(env.AtRegexp("a/go.mod", `require example.com/blah/v2`), WithMessage("no required module provides")), +- ReadDiagnostics("a/go.mod", &modDiags), +- ) - --func foo() error { return nil } +- env.ApplyQuickFixes("a/go.mod", modDiags.Diagnostics) +- const want = `module mod.com - --func _() { -- err := foo() +-go 1.12 +- +-require ( +- example.com/blah v1.0.0 // indirect +- example.com/blah/v2 v2.0.0 +-) +-` +- env.SaveBuffer("a/go.mod") +- env.AfterChange(NoDiagnostics(ForFile("a/main.go"))) +- if got := env.BufferText("a/go.mod"); got != want { +- t.Fatalf("suggested fixes failed:\n%s", compare.Text(want, got)) +- } +- }) -} --`, -- }, -- { -- name: "var_same_type", -- before: ` --package foo - --func foo() (int, int) { return 0, 0 } +-// Reproduces golang/go#38232. +-func TestUnknownRevision(t *testing.T) { +- if runtime.GOOS == "plan9" { +- t.Skipf("skipping test that fails for unknown reasons on plan9; see https://go.dev/issue/50477") +- } +- const unknown = ` +--- a/go.mod -- +-module mod.com - --func _() { -- foo().var --} --`, -- after: ` --package foo +-require ( +- example.com v1.2.2 +-) +--- a/main.go -- +-package main - --func foo() (int, int) { return 0, 0 } +-import "example.com/blah" - --func _() { -- i, i2 := foo() +-func main() { +- var x = blah.Name -} --`, -- }, -- { -- name: "print_scalar", -- before: ` --package foo +-` - --func _() { -- var foo int -- foo.print --} --`, -- after: ` --package foo +- runner := RunMultiple{ +- {"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, +- {"nested", WithOptions(ProxyFiles(proxy))}, +- } +- // Start from a bad state/bad IWL, and confirm that we recover. +- t.Run("bad", func(t *testing.T) { +- runner.Run(t, unknown, func(t *testing.T, env *Env) { +- env.OpenFile("a/go.mod") +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/go.mod", "example.com v1.2.2")), +- ) +- env.RegexpReplace("a/go.mod", "v1.2.2", "v1.2.3") +- env.SaveBuffer("a/go.mod") // Save to trigger diagnostics. - --import "fmt" +- d := protocol.PublishDiagnosticsParams{} +- env.AfterChange( +- // Make sure the diagnostic mentions the new version -- the old diagnostic is in the same place. +- Diagnostics(env.AtRegexp("a/go.mod", "example.com v1.2.3"), WithMessage("example.com@v1.2.3")), +- ReadDiagnostics("a/go.mod", &d), +- ) +- qfs := env.GetQuickFixes("a/go.mod", d.Diagnostics) +- if len(qfs) == 0 { +- t.Fatalf("got 0 code actions to fix %v, wanted at least 1", d.Diagnostics) +- } +- env.ApplyCodeAction(qfs[0]) // Arbitrarily pick a single fix to apply. Applying all of them seems to cause trouble in this particular test. +- env.SaveBuffer("a/go.mod") // Save to trigger diagnostics. +- env.AfterChange( +- NoDiagnostics(ForFile("a/go.mod")), +- Diagnostics(env.AtRegexp("a/main.go", "x = ")), +- ) +- }) +- }) - --func _() { -- var foo int -- fmt.Printf("foo: %v\n", foo) --} --`, -- }, -- { -- name: "print_multi", -- before: ` --package foo +- const known = ` +--- a/go.mod -- +-module mod.com - --func foo() (int, error) { return 0, nil } +-require ( +- example.com v1.2.3 +-) +--- a/go.sum -- +-example.com v1.2.3 h1:ihBTGWGjTU3V4ZJ9OmHITkU9WQ4lGdQkMjgyLFk0FaY= +-example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= +--- a/main.go -- +-package main - --func _() { -- foo().print +-import "example.com/blah" +- +-func main() { +- var x = blah.Name +-} +-` +- // Start from a good state, transform to a bad state, and confirm that we +- // still recover. +- t.Run("good", func(t *testing.T) { +- runner.Run(t, known, func(t *testing.T, env *Env) { +- env.OpenFile("a/go.mod") +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/main.go", "x = ")), +- ) +- env.RegexpReplace("a/go.mod", "v1.2.3", "v1.2.2") +- env.Editor.SaveBuffer(env.Ctx, "a/go.mod") // go.mod changes must be on disk +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/go.mod", "example.com v1.2.2")), +- ) +- env.RegexpReplace("a/go.mod", "v1.2.2", "v1.2.3") +- env.Editor.SaveBuffer(env.Ctx, "a/go.mod") // go.mod changes must be on disk +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/main.go", "x = ")), +- ) +- }) +- }) -} --`, -- after: ` --package foo - --import "fmt" +-// Confirm that an error in an indirect dependency of a requirement is surfaced +-// as a diagnostic in the go.mod file. +-func TestErrorInIndirectDependency(t *testing.T) { +- const badProxy = ` +--- example.com@v1.2.3/go.mod -- +-module example.com - --func foo() (int, error) { return 0, nil } +-go 1.12 - --func _() { -- fmt.Println(foo()) --} --`, -- }, -- { -- name: "string split", -- before: ` --package foo +-require random.org v1.2.3 // indirect +--- example.com@v1.2.3/blah/blah.go -- +-package blah - --func foo() []string { -- x := "test" -- return x.split --}`, -- after: ` --package foo +-const Name = "Blah" +--- random.org@v1.2.3/go.mod -- +-module bob.org - --import "strings" +-go 1.12 +--- random.org@v1.2.3/blah/blah.go -- +-package hello - --func foo() []string { -- x := "test" -- return strings.Split(x, "$0") --}`, -- }, -- { -- name: "string slice join", -- before: ` --package foo +-const Name = "Hello" +-` +- const module = ` +--- a/go.mod -- +-module mod.com - --func foo() string { -- x := []string{"a", "test"} -- return x.join --}`, -- after: ` --package foo +-go 1.14 - --import "strings" +-require example.com v1.2.3 +--- a/main.go -- +-package main - --func foo() string { -- x := []string{"a", "test"} -- return strings.Join(x, "$0") --}`, -- }, -- { -- name: "if not nil interface", -- before: ` --package foo +-import "example.com/blah" - --func _() { -- var foo error -- foo.ifnotnil +-func main() { +- println(blah.Name) +-} +-` +- RunMultiple{ +- {"default", WithOptions(ProxyFiles(badProxy), WorkspaceFolders("a"))}, +- {"nested", WithOptions(ProxyFiles(badProxy))}, +- }.Run(t, module, func(t *testing.T, env *Env) { +- env.OpenFile("a/go.mod") +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/go.mod", "require example.com v1.2.3")), +- ) +- }) -} --`, -- after: ` --package foo - --func _() { -- var foo error -- if foo != nil { -- $0 +-// A copy of govim's config_set_env_goflags_mod_readonly test. +-func TestGovimModReadonly(t *testing.T) { +- const mod = ` +--- go.mod -- +-module mod.com +- +-go 1.13 +--- main.go -- +-package main +- +-import "example.com/blah" +- +-func main() { +- println(blah.Name) -} +-` +- WithOptions( +- EnvVars{"GOFLAGS": "-mod=readonly"}, +- ProxyFiles(proxy), +- Modes(Default), +- ).Run(t, mod, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- original := env.ReadWorkspaceFile("go.mod") +- env.AfterChange( +- Diagnostics(env.AtRegexp("main.go", `"example.com/blah"`)), +- ) +- got := env.ReadWorkspaceFile("go.mod") +- if got != original { +- t.Fatalf("go.mod file modified:\n%s", compare.Text(original, got)) +- } +- env.RunGoCommand("get", "example.com/blah@v1.2.3") +- env.RunGoCommand("mod", "tidy") +- env.AfterChange( +- NoDiagnostics(ForFile("main.go")), +- ) +- }) -} --`, -- }, -- { -- name: "if not nil pointer", -- before: ` --package foo - --func _() { -- var foo *int -- foo.ifnotnil --} --`, -- after: ` --package foo +-func TestMultiModuleModDiagnostics(t *testing.T) { +- const mod = ` +--- go.work -- +-go 1.18 - --func _() { -- var foo *int -- if foo != nil { -- $0 --} --} --`, -- }, -- { -- name: "if not nil slice", -- before: ` --package foo +-use ( +- a +- b +-) +--- a/go.mod -- +-module moda.com - --func _() { -- var foo []int -- foo.ifnotnil --} --`, -- after: ` --package foo +-go 1.14 - --func _() { -- var foo []int -- if foo != nil { -- $0 --} --} --`, -- }, -- { -- name: "if not nil map", -- before: ` --package foo +-require ( +- example.com v1.2.3 +-) +--- a/go.sum -- +-example.com v1.2.3 h1:Yryq11hF02fEf2JlOS2eph+ICE2/ceevGV3C9dl5V/c= +-example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= +--- a/main.go -- +-package main - --func _() { -- var foo map[string]any -- foo.ifnotnil --} --`, -- after: ` --package foo +-func main() {} +--- b/go.mod -- +-module modb.com - --func _() { -- var foo map[string]any -- if foo != nil { -- $0 --} --} --`, -- }, -- { -- name: "if not nil channel", -- before: ` --package foo +-require example.com v1.2.3 - --func _() { -- var foo chan int -- foo.ifnotnil --} --`, -- after: ` --package foo +-go 1.14 +--- b/main.go -- +-package main - --func _() { -- var foo chan int -- if foo != nil { -- $0 +-import "example.com/blah" +- +-func main() { +- blah.SaySomething() -} +-` +- WithOptions( +- ProxyFiles(workspaceProxy), +- ).Run(t, mod, func(t *testing.T, env *Env) { +- env.AfterChange( +- Diagnostics( +- env.AtRegexp("a/go.mod", "example.com v1.2.3"), +- WithMessage("is not used"), +- ), +- ) +- }) -} --`, -- }, -- { -- name: "if not nil function", -- before: ` --package foo - --func _() { -- var foo func() -- foo.ifnotnil --} --`, -- after: ` --package foo +-func TestModTidyWithBuildTags(t *testing.T) { +- const mod = ` +--- go.mod -- +-module mod.com - --func _() { -- var foo func() -- if foo != nil { -- $0 --} --} --`, -- }, -- } +-go 1.14 +--- main.go -- +-// +build bob - -- r := WithOptions( -- Settings{ -- "experimentalPostfixCompletions": true, -- }, -- ) -- r.Run(t, mod, func(t *testing.T, env *Env) { -- env.CreateBuffer("foo.go", "") +-package main - -- for _, c := range cases { -- t.Run(c.name, func(t *testing.T) { -- c.before = strings.Trim(c.before, "\n") -- c.after = strings.Trim(c.after, "\n") +-import "example.com/blah" - -- env.SetBufferContent("foo.go", c.before) +-func main() { +- blah.SaySomething() +-} +-` +- WithOptions( +- ProxyFiles(workspaceProxy), +- Settings{"buildFlags": []string{"-tags", "bob"}}, +- ).Run(t, mod, func(t *testing.T, env *Env) { +- env.OnceMet( +- InitialWorkspaceLoad, +- Diagnostics(env.AtRegexp("main.go", `"example.com/blah"`)), +- ) +- }) +-} - -- loc := env.RegexpSearch("foo.go", "\n}") -- completions := env.Completion(loc) -- if len(completions.Items) != 1 { -- t.Fatalf("expected one completion, got %v", completions.Items) -- } +-func TestModTypoDiagnostic(t *testing.T) { +- const mod = ` +--- go.mod -- +-module mod.com - -- env.AcceptCompletion(loc, completions.Items[0]) +-go 1.12 +--- main.go -- +-package main - -- if buf := env.BufferText("foo.go"); buf != c.after { -- t.Errorf("\nGOT:\n%s\nEXPECTED:\n%s", buf, c.after) -- } -- }) -- } +-func main() {} +-` +- Run(t, mod, func(t *testing.T, env *Env) { +- env.OpenFile("go.mod") +- env.RegexpReplace("go.mod", "module", "modul") +- env.AfterChange( +- Diagnostics(env.AtRegexp("go.mod", "modul")), +- ) - }) -} -diff -urN a/gopls/internal/regtest/debug/debug_test.go b/gopls/internal/regtest/debug/debug_test.go ---- a/gopls/internal/regtest/debug/debug_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/debug/debug_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,101 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package debug +-func TestSumUpdateFixesDiagnostics(t *testing.T) { +- const mod = ` +--- go.mod -- +-module mod.com - --import ( -- "context" -- "encoding/json" -- "io" -- "net/http" -- "strings" -- "testing" +-go 1.12 - -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/hooks" -- "golang.org/x/tools/gopls/internal/lsp/command" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" +-require ( +- example.com v1.2.3 +-) +--- go.sum -- +--- main.go -- +-package main +- +-import ( +- "example.com/blah" -) - --func TestMain(m *testing.M) { -- Main(m, hooks.Options) +-func main() { +- println(blah.Name) -} -- --func TestBugNotification(t *testing.T) { -- // Verify that a properly configured session gets notified of a bug on the -- // server. +-` - WithOptions( -- Modes(Default), // must be in-process to receive the bug report below -- Settings{"showBugReports": true}, -- ).Run(t, "", func(t *testing.T, env *Env) { -- const desc = "got a bug" -- bug.Report(desc) -- env.Await(ShownMessage(desc)) +- ProxyFiles(workspaceProxy), +- ).Run(t, mod, func(t *testing.T, env *Env) { +- d := &protocol.PublishDiagnosticsParams{} +- env.OpenFile("go.mod") +- env.AfterChange( +- Diagnostics( +- env.AtRegexp("go.mod", `example.com v1.2.3`), +- WithMessage("go.sum is out of sync"), +- ), +- ReadDiagnostics("go.mod", d), +- ) +- env.ApplyQuickFixes("go.mod", d.Diagnostics) +- env.SaveBuffer("go.mod") // Save to trigger diagnostics. +- env.AfterChange( +- NoDiagnostics(ForFile("go.mod")), +- ) - }) -} - --// TestStartDebugging executes a gopls.start_debugging command to --// start the internal web server. --func TestStartDebugging(t *testing.T) { -- WithOptions( -- Modes(Default|Experimental), // doesn't work in Forwarded mode -- ).Run(t, "", func(t *testing.T, env *Env) { -- // Start a debugging server. -- res, err := startDebugging(env.Ctx, env.Editor.Server, &command.DebuggingArgs{ -- Addr: "", // any free port -- }) -- if err != nil { -- t.Fatalf("startDebugging: %v", err) -- } +-// This test confirms that editing a go.mod file only causes metadata +-// to be invalidated when it's saved. +-func TestGoModInvalidatesOnSave(t *testing.T) { +- const mod = ` +--- go.mod -- +-module mod.com - -- // Assert that the server requested that the -- // client show the debug page in a browser. -- debugURL := res.URLs[0] -- env.Await(ShownDocument(debugURL)) +-go 1.12 +--- main.go -- +-package main - -- // Send a request to the debug server and ensure it responds. -- resp, err := http.Get(debugURL) -- if err != nil { -- t.Fatal(err) -- } -- defer resp.Body.Close() -- data, err := io.ReadAll(resp.Body) -- if err != nil { -- t.Fatalf("reading HTTP response body: %v", err) +-func main() { +- hello() +-} +--- hello.go -- +-package main +- +-func hello() {} +-` +- WithOptions( +- // TODO(rFindley) this doesn't work in multi-module workspace mode, because +- // it keeps around the last parsing modfile. Update this test to also +- // exercise the workspace module. +- Modes(Default), +- ).Run(t, mod, func(t *testing.T, env *Env) { +- env.OpenFile("go.mod") +- env.Await(env.DoneWithOpen()) +- env.RegexpReplace("go.mod", "module", "modul") +- // Confirm that we still have metadata with only on-disk edits. +- env.OpenFile("main.go") +- loc := env.GoToDefinition(env.RegexpSearch("main.go", "hello")) +- if filepath.Base(string(loc.URI)) != "hello.go" { +- t.Fatalf("expected definition in hello.go, got %s", loc.URI) - } -- const want = "<title>GoPls" -- if !strings.Contains(string(data), want) { -- t.Errorf("GET %s response does not contain %q: <<%s>>", debugURL, want, data) +- // Confirm that we no longer have metadata when the file is saved. +- env.SaveBufferWithoutActions("go.mod") +- _, err := env.Editor.Definition(env.Ctx, env.RegexpSearch("main.go", "hello")) +- if err == nil { +- t.Fatalf("expected error, got none") - } - }) -} - --// startDebugging starts a debugging server. --// TODO(adonovan): move into command package? --func startDebugging(ctx context.Context, server protocol.Server, args *command.DebuggingArgs) (*command.DebuggingResult, error) { -- rawArgs, err := command.MarshalArgs(args) -- if err != nil { -- return nil, err -- } -- res0, err := server.ExecuteCommand(ctx, &protocol.ExecuteCommandParams{ -- Command: command.StartDebugging.ID(), -- Arguments: rawArgs, -- }) -- if err != nil { -- return nil, err -- } -- // res0 is the result of a schemaless (map[string]any) JSON decoding. -- // Re-encode and decode into the correct Go struct type. -- // TODO(adonovan): fix (*serverDispatcher).ExecuteCommand. -- data, err := json.Marshal(res0) -- if err != nil { -- return nil, err -- } -- var res *command.DebuggingResult -- if err := json.Unmarshal(data, &res); err != nil { -- return nil, err -- } -- return res, nil --} -diff -urN a/gopls/internal/regtest/diagnostics/analysis_test.go b/gopls/internal/regtest/diagnostics/analysis_test.go ---- a/gopls/internal/regtest/diagnostics/analysis_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/diagnostics/analysis_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,127 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-func TestRemoveUnusedDependency(t *testing.T) { +- const proxy = ` +--- hasdep.com@v1.2.3/go.mod -- +-module hasdep.com - --package diagnostics +-go 1.12 - --import ( -- "fmt" -- "testing" +-require example.com v1.2.3 +--- hasdep.com@v1.2.3/a/a.go -- +-package a +--- example.com@v1.2.3/go.mod -- +-module example.com - -- "golang.org/x/tools/gopls/internal/lsp/cache" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" --) +-go 1.12 +--- example.com@v1.2.3/blah/blah.go -- +-package blah - --// Test for the timeformat analyzer, following golang/vscode-go#2406. --// --// This test checks that applying the suggested fix from the analyzer resolves --// the diagnostic warning. --func TestTimeFormatAnalyzer(t *testing.T) { -- const files = ` +-const Name = "Blah" +--- random.com@v1.2.3/go.mod -- +-module random.com +- +-go 1.12 +--- random.com@v1.2.3/blah/blah.go -- +-package blah +- +-const Name = "Blah" +-` +- t.Run("almost tidied", func(t *testing.T) { +- const mod = ` --- go.mod -- -module mod.com - --go 1.18 +-go 1.12 +- +-require hasdep.com v1.2.3 +--- go.sum -- +-example.com v1.2.3 h1:ihBTGWGjTU3V4ZJ9OmHITkU9WQ4lGdQkMjgyLFk0FaY= +-example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= +-hasdep.com v1.2.3 h1:00y+N5oD+SpKoqV1zP2VOPawcW65Zb9NebANY3GSzGI= +-hasdep.com v1.2.3/go.mod h1:ePVZOlez+KZEOejfLPGL2n4i8qiAjrkhQZ4wcImqAes= --- main.go -- -package main - --import ( -- "fmt" -- "time" --) -- --func main() { -- now := time.Now() -- fmt.Println(now.Format("2006-02-01")) --}` -- -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- -- var d protocol.PublishDiagnosticsParams -- env.AfterChange( -- Diagnostics(env.AtRegexp("main.go", "2006-02-01")), -- ReadDiagnostics("main.go", &d), -- ) +-func main() {} +-` +- WithOptions( +- ProxyFiles(proxy), +- ).Run(t, mod, func(t *testing.T, env *Env) { +- env.OpenFile("go.mod") +- d := &protocol.PublishDiagnosticsParams{} +- env.AfterChange( +- Diagnostics(env.AtRegexp("go.mod", "require hasdep.com v1.2.3")), +- ReadDiagnostics("go.mod", d), +- ) +- const want = `module mod.com - -- env.ApplyQuickFixes("main.go", d.Diagnostics) -- env.AfterChange(NoDiagnostics(ForFile("main.go"))) +-go 1.12 +-` +- env.ApplyQuickFixes("go.mod", d.Diagnostics) +- if got := env.BufferText("go.mod"); got != want { +- t.Fatalf("unexpected content in go.mod:\n%s", compare.Text(want, got)) +- } +- }) - }) --} - --func TestAnalysisProgressReporting(t *testing.T) { -- const files = ` +- t.Run("not tidied", func(t *testing.T) { +- const mod = ` --- go.mod -- -module mod.com - --go 1.18 +-go 1.12 - +-require hasdep.com v1.2.3 +-require random.com v1.2.3 +--- go.sum -- +-example.com v1.2.3 h1:ihBTGWGjTU3V4ZJ9OmHITkU9WQ4lGdQkMjgyLFk0FaY= +-example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= +-hasdep.com v1.2.3 h1:00y+N5oD+SpKoqV1zP2VOPawcW65Zb9NebANY3GSzGI= +-hasdep.com v1.2.3/go.mod h1:ePVZOlez+KZEOejfLPGL2n4i8qiAjrkhQZ4wcImqAes= +-random.com v1.2.3 h1:PzYTykzqqH6+qU0dIgh9iPFbfb4Mm8zNBjWWreRKtx0= +-random.com v1.2.3/go.mod h1:8EGj+8a4Hw1clAp8vbaeHAsKE4sbm536FP7nKyXO+qQ= --- main.go -- -package main - --func main() { --}` +-func main() {} +-` +- WithOptions( +- ProxyFiles(proxy), +- ).Run(t, mod, func(t *testing.T, env *Env) { +- d := &protocol.PublishDiagnosticsParams{} +- env.OpenFile("go.mod") +- pos := env.RegexpSearch("go.mod", "require hasdep.com v1.2.3").Range.Start +- env.AfterChange( +- Diagnostics(AtPosition("go.mod", pos.Line, pos.Character)), +- ReadDiagnostics("go.mod", d), +- ) +- const want = `module mod.com - -- tests := []struct { -- setting bool -- want Expectation -- }{ -- {true, CompletedWork(cache.AnalysisProgressTitle, 1, true)}, -- {false, Not(CompletedWork(cache.AnalysisProgressTitle, 1, true))}, -- } +-go 1.12 - -- for _, test := range tests { -- t.Run(fmt.Sprint(test.setting), func(t *testing.T) { -- WithOptions( -- Settings{ -- "reportAnalysisProgressAfter": "0s", -- "analysisProgressReporting": test.setting, -- }, -- ).Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- env.AfterChange(test.want) -- }) +-require random.com v1.2.3 +-` +- var diagnostics []protocol.Diagnostic +- for _, d := range d.Diagnostics { +- if d.Range.Start.Line != pos.Line { +- continue +- } +- diagnostics = append(diagnostics, d) +- } +- env.ApplyQuickFixes("go.mod", diagnostics) +- if got := env.BufferText("go.mod"); got != want { +- t.Fatalf("unexpected content in go.mod:\n%s", compare.Text(want, got)) +- } - }) -- } +- }) -} - --// Test the embed directive analyzer. --// --// There is a fix for missing imports, but it should not trigger for other --// kinds of issues reported by the analayzer, here the variable --// declaration following the embed directive is wrong. --func TestNoSuggestedFixesForEmbedDirectiveDeclaration(t *testing.T) { -- const generated = ` +-func TestSumUpdateQuickFix(t *testing.T) { +- const mod = ` --- go.mod -- -module mod.com - --go 1.20 -- ---- foo.txt -- --FOO +-go 1.12 - +-require ( +- example.com v1.2.3 +-) +--- go.sum -- --- main.go -- -package main - --import _ "embed" -- --//go:embed foo.txt --var foo, bar string +-import ( +- "example.com/blah" +-) - -func main() { -- _ = foo +- blah.Hello() -} -` -- Run(t, generated, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- var d protocol.PublishDiagnosticsParams +- WithOptions( +- ProxyFiles(workspaceProxy), +- Modes(Default), +- ).Run(t, mod, func(t *testing.T, env *Env) { +- env.OpenFile("go.mod") +- params := &protocol.PublishDiagnosticsParams{} - env.AfterChange( -- Diagnostics(env.AtRegexp("main.go", "//go:embed")), -- ReadDiagnostics("main.go", &d), +- Diagnostics( +- env.AtRegexp("go.mod", `example.com`), +- WithMessage("go.sum is out of sync"), +- ), +- ReadDiagnostics("go.mod", params), - ) -- if fixes := env.GetQuickFixes("main.go", d.Diagnostics); len(fixes) != 0 { -- t.Errorf("got quick fixes %v, wanted none", fixes) +- env.ApplyQuickFixes("go.mod", params.Diagnostics) +- const want = `example.com v1.2.3 h1:Yryq11hF02fEf2JlOS2eph+ICE2/ceevGV3C9dl5V/c= +-example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= +-` +- if got := env.ReadWorkspaceFile("go.sum"); got != want { +- t.Fatalf("unexpected go.sum contents:\n%s", compare.Text(want, got)) - } - }) -} -diff -urN a/gopls/internal/regtest/diagnostics/builtin_test.go b/gopls/internal/regtest/diagnostics/builtin_test.go ---- a/gopls/internal/regtest/diagnostics/builtin_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/diagnostics/builtin_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,35 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package diagnostics -- --import ( -- "strings" -- "testing" -- -- . "golang.org/x/tools/gopls/internal/lsp/regtest" --) - --func TestIssue44866(t *testing.T) { -- src := ` ---- go.mod -- --module mod.com +-func TestDownloadDeps(t *testing.T) { +- const proxy = ` +--- example.com@v1.2.3/go.mod -- +-module example.com - -go 1.12 ---- a.go -- --package a - --const ( -- c = iota --) --` -- Run(t, src, func(t *testing.T, env *Env) { -- env.OpenFile("a.go") -- loc := env.GoToDefinition(env.RegexpSearch("a.go", "iota")) -- if !strings.HasSuffix(string(loc.URI), "builtin.go") { -- t.Fatalf("jumped to %q, want builtin.go", loc.URI) -- } -- env.AfterChange(NoDiagnostics(ForFile("builtin.go"))) -- }) --} -diff -urN a/gopls/internal/regtest/diagnostics/diagnostics_test.go b/gopls/internal/regtest/diagnostics/diagnostics_test.go ---- a/gopls/internal/regtest/diagnostics/diagnostics_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/diagnostics/diagnostics_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,2155 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-require random.org v1.2.3 +--- example.com@v1.2.3/blah/blah.go -- +-package blah - --package diagnostics +-import "random.org/bye" - --import ( -- "context" -- "fmt" -- "os/exec" -- "testing" +-func SaySomething() { +- bye.Goodbye() +-} +--- random.org@v1.2.3/go.mod -- +-module random.org - -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/hooks" -- "golang.org/x/tools/gopls/internal/lsp" -- "golang.org/x/tools/gopls/internal/lsp/fake" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" -- "golang.org/x/tools/internal/testenv" --) +-go 1.12 +--- random.org@v1.2.3/bye/bye.go -- +-package bye - --func TestMain(m *testing.M) { -- bug.PanicOnBugs = true -- Main(m, hooks.Options) +-func Goodbye() { +- println("Bye") -} +-` - --// Use mod.com for all go.mod files due to golang/go#35230. --const exampleProgram = ` +- const mod = ` --- go.mod -- -module mod.com - -go 1.12 +--- go.sum -- --- main.go -- -package main - --import "fmt" +-import ( +- "example.com/blah" +-) - -func main() { -- fmt.Println("Hello World.") --}` -- --func TestDiagnosticErrorInEditedFile(t *testing.T) { -- // This test is very basic: start with a clean Go program, make an error, and -- // get a diagnostic for that error. However, it also demonstrates how to -- // combine Expectations to await more complex state in the editor. -- Run(t, exampleProgram, func(t *testing.T, env *Env) { -- // Deleting the 'n' at the end of Println should generate a single error -- // diagnostic. +- blah.SaySomething() +-} +-` +- WithOptions( +- ProxyFiles(proxy), +- Modes(Default), +- ).Run(t, mod, func(t *testing.T, env *Env) { - env.OpenFile("main.go") -- env.RegexpReplace("main.go", "Printl(n)", "") +- d := &protocol.PublishDiagnosticsParams{} - env.AfterChange( -- Diagnostics(env.AtRegexp("main.go", "Printl")), -- // Assert that this test has sent no error logs to the client. This is not -- // strictly necessary for testing this regression, but is included here -- // as an example of using the NoErrorLogs() expectation. Feel free to -- // delete. -- NoErrorLogs(), +- Diagnostics( +- env.AtRegexp("main.go", `"example.com/blah"`), +- WithMessage(`could not import example.com/blah (no required module provides package "example.com/blah")`), +- ), +- ReadDiagnostics("main.go", d), +- ) +- env.ApplyQuickFixes("main.go", d.Diagnostics) +- env.AfterChange( +- NoDiagnostics(ForFile("main.go")), +- NoDiagnostics(ForFile("go.mod")), - ) - }) -} - --func TestMissingImportDiagsClearOnFirstFile(t *testing.T) { -- const onlyMod = ` +-func TestInvalidGoVersion(t *testing.T) { +- const files = ` --- go.mod -- -module mod.com - --go 1.12 +-go foo +--- main.go -- +-package main -` -- Run(t, onlyMod, func(t *testing.T, env *Env) { -- env.CreateBuffer("main.go", `package main -- --func m() { -- log.Println() --} --`) -- env.AfterChange(Diagnostics(env.AtRegexp("main.go", "log"))) -- env.SaveBuffer("main.go") -- env.AfterChange(NoDiagnostics(ForFile("main.go"))) +- Run(t, files, func(t *testing.T, env *Env) { +- env.OnceMet( +- InitialWorkspaceLoad, +- Diagnostics(env.AtRegexp("go.mod", `go foo`), WithMessage("invalid go version")), +- ) +- env.WriteWorkspaceFile("go.mod", "module mod.com \n\ngo 1.12\n") +- env.AfterChange(NoDiagnostics(ForFile("go.mod"))) - }) -} - --func TestDiagnosticErrorInNewFile(t *testing.T) { -- const brokenFile = `package main -- --const Foo = "abc +-// This is a regression test for a bug in the line-oriented implementation +-// of the "apply diffs" operation used by the fake editor. +-func TestIssue57627(t *testing.T) { +- const files = ` +--- go.work -- +-package main -` -- Run(t, brokenFile, func(t *testing.T, env *Env) { -- env.CreateBuffer("broken.go", brokenFile) -- env.AfterChange(Diagnostics(env.AtRegexp("broken.go", "\"abc"))) +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("go.work") +- env.SetBufferContent("go.work", "go 1.18\nuse moda/a") +- env.SaveBuffer("go.work") // doesn't fail - }) -} - --// badPackage contains a duplicate definition of the 'a' const. --const badPackage = ` ---- go.mod -- --module mod.com -- --go 1.12 ---- a.go -- --package consts -- --const a = 1 ---- b.go -- --package consts -- --const a = 2 +-func TestInconsistentMod(t *testing.T) { +- const proxy = ` +--- golang.org/x/mod@v0.7.0/go.mod -- +-go 1.20 +-module golang.org/x/mod +--- golang.org/x/mod@v0.7.0/a.go -- +-package mod +-func AutoQuote(string) string { return ""} +--- golang.org/x/mod@v0.9.0/go.mod -- +-go 1.20 +-module golang.org/x/mod +--- golang.org/x/mod@v0.9.0/a.go -- +-package mod +-func AutoQuote(string) string { return ""} -` +- const files = ` +--- go.work -- +-go 1.20 +-use ( +- ./a +- ./b +-) - --func TestDiagnosticClearingOnEdit(t *testing.T) { -- Run(t, badPackage, func(t *testing.T, env *Env) { -- env.OpenFile("b.go") -- env.AfterChange( -- Diagnostics(env.AtRegexp("a.go", "a = 1")), -- Diagnostics(env.AtRegexp("b.go", "a = 2")), -- ) -- -- // Fix the error by editing the const name in b.go to `b`. -- env.RegexpReplace("b.go", "(a) = 2", "b") -- env.AfterChange( -- NoDiagnostics(ForFile("a.go")), -- NoDiagnostics(ForFile("b.go")), -- ) -- }) --} +--- a/go.mod -- +-module a.mod.com +-go 1.20 +-require golang.org/x/mod v0.6.0 // yyy +-replace golang.org/x/mod v0.6.0 => golang.org/x/mod v0.7.0 +--- a/main.go -- +-package main +-import "golang.org/x/mod" +-import "fmt" +-func main() {fmt.Println(mod.AutoQuote(""))} - --func TestDiagnosticClearingOnDelete_Issue37049(t *testing.T) { -- Run(t, badPackage, func(t *testing.T, env *Env) { -- env.OpenFile("a.go") -- env.AfterChange( -- Diagnostics(env.AtRegexp("a.go", "a = 1")), -- Diagnostics(env.AtRegexp("b.go", "a = 2")), -- ) -- env.RemoveWorkspaceFile("b.go") +--- b/go.mod -- +-module b.mod.com +-go 1.20 +-require golang.org/x/mod v0.9.0 // xxx +--- b/main.go -- +-package aaa +-import "golang.org/x/mod" +-import "fmt" +-func main() {fmt.Println(mod.AutoQuote(""))} +-var A int - -- env.AfterChange( -- NoDiagnostics(ForFile("a.go")), -- NoDiagnostics(ForFile("b.go")), -- ) +--- b/c/go.mod -- +-module c.b.mod.com +-go 1.20 +-require b.mod.com v0.4.2 +-replace b.mod.com => ../ +--- b/c/main.go -- +-package main +-import "b.mod.com/aaa" +-import "fmt" +-func main() {fmt.Println(aaa.A)} +-` +- WithOptions( +- ProxyFiles(proxy), +- Modes(Default), +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("a/go.mod") +- ahints := env.InlayHints("a/go.mod") +- if len(ahints) != 1 { +- t.Errorf("expected exactly one hint, got %d: %#v", len(ahints), ahints) +- } +- env.OpenFile("b/c/go.mod") +- bhints := env.InlayHints("b/c/go.mod") +- if len(bhints) != 0 { +- t.Errorf("expected no hints, got %d: %#v", len(bhints), bhints) +- } - }) --} -- --func TestDiagnosticClearingOnClose(t *testing.T) { -- Run(t, badPackage, func(t *testing.T, env *Env) { -- env.CreateBuffer("c.go", `package consts - --const a = 3`) -- env.AfterChange( -- Diagnostics(env.AtRegexp("a.go", "a = 1")), -- Diagnostics(env.AtRegexp("b.go", "a = 2")), -- Diagnostics(env.AtRegexp("c.go", "a = 3")), -- ) -- env.CloseBuffer("c.go") -- env.AfterChange( -- Diagnostics(env.AtRegexp("a.go", "a = 1")), -- Diagnostics(env.AtRegexp("b.go", "a = 2")), -- NoDiagnostics(ForFile("c.go")), -- ) -- }) -} +diff -urN a/gopls/internal/test/integration/modfile/tempmodfile_test.go b/gopls/internal/test/integration/modfile/tempmodfile_test.go +--- a/gopls/internal/test/integration/modfile/tempmodfile_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/modfile/tempmodfile_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,41 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// Tests golang/go#37978. --func TestIssue37978(t *testing.T) { -- Run(t, exampleProgram, func(t *testing.T, env *Env) { -- // Create a new workspace-level directory and empty file. -- env.CreateBuffer("c/c.go", "") +-package modfile - -- // Write the file contents with a missing import. -- env.EditBuffer("c/c.go", protocol.TextEdit{ -- NewText: `package c +-import ( +- "testing" - --const a = http.MethodGet --`, -- }) -- env.AfterChange( -- Diagnostics(env.AtRegexp("c/c.go", "http.MethodGet")), -- ) -- // Save file, which will organize imports, adding the expected import. -- // Expect the diagnostics to clear. -- env.SaveBuffer("c/c.go") -- env.AfterChange( -- NoDiagnostics(ForFile("c/c.go")), -- ) -- }) --} +- . "golang.org/x/tools/gopls/internal/test/integration" +-) - --// Tests golang/go#38878: good a.go, bad a_test.go, remove a_test.go but its errors remain --// If the file is open in the editor, this is working as intended --// If the file is not open in the editor, the errors go away --const test38878 = ` +-// This test replaces an older, problematic test (golang/go#57784). But it has +-// been a long time since the go command would mutate go.mod files. +-// +-// TODO(golang/go#61970): the tempModfile setting should be removed entirely. +-func TestTempModfileUnchanged(t *testing.T) { +- // badMod has a go.mod file that is missing a go directive. +- const badMod = ` --- go.mod -- --module foo -- --go 1.12 ---- a.go -- --package x -- --// import "fmt" -- --func f() {} -- ---- a_test.go -- --package x -- --import "testing" -- --func TestA(t *testing.T) { -- f(3) --} +-module badmod.test/p +--- p.go -- +-package p -` - --// Tests golang/go#38878: deleting a test file should clear its errors, and --// not break the workspace. --func TestDeleteTestVariant(t *testing.T) { -- Run(t, test38878, func(t *testing.T, env *Env) { -- env.AfterChange(Diagnostics(env.AtRegexp("a_test.go", `f\((3)\)`))) -- env.RemoveWorkspaceFile("a_test.go") -- env.AfterChange(NoDiagnostics(ForFile("a_test.go"))) -- -- // Make sure the test variant has been removed from the workspace by -- // triggering a metadata load. -- env.OpenFile("a.go") -- env.RegexpReplace("a.go", `// import`, "import") -- env.AfterChange(Diagnostics(env.AtRegexp("a.go", `"fmt"`))) -- }) --} -- --// Tests golang/go#38878: deleting a test file on disk while it's still open --// should not clear its errors. --func TestDeleteTestVariant_DiskOnly(t *testing.T) { -- Run(t, test38878, func(t *testing.T, env *Env) { -- env.OpenFile("a_test.go") -- env.AfterChange(Diagnostics(AtPosition("a_test.go", 5, 3))) -- env.Sandbox.Workdir.RemoveFile(context.Background(), "a_test.go") -- env.AfterChange(Diagnostics(AtPosition("a_test.go", 5, 3))) +- WithOptions( +- Modes(Default), // no reason to test this with a remote gopls +- ProxyFiles(workspaceProxy), +- Settings{ +- "tempModfile": true, +- }, +- ).Run(t, badMod, func(t *testing.T, env *Env) { +- env.OpenFile("p.go") +- env.AfterChange() +- want := "module badmod.test/p\n" +- got := env.ReadWorkspaceFile("go.mod") +- if got != want { +- t.Errorf("go.mod content:\n%s\nwant:\n%s", got, want) +- } - }) -} +diff -urN a/gopls/internal/test/integration/options.go b/gopls/internal/test/integration/options.go +--- a/gopls/internal/test/integration/options.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/options.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,178 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// TestNoMod confirms that gopls continues to work when a user adds a go.mod --// file to their workspace. --func TestNoMod(t *testing.T) { -- const noMod = ` ---- main.go -- --package main -- --import "mod.com/bob" -- --func main() { -- bob.Hello() --} ---- bob/bob.go -- --package bob -- --func Hello() { -- var x int --} --` -- -- t.Run("manual", func(t *testing.T) { -- Run(t, noMod, func(t *testing.T, env *Env) { -- env.OnceMet( -- InitialWorkspaceLoad, -- Diagnostics(env.AtRegexp("main.go", `"mod.com/bob"`)), -- ) -- env.CreateBuffer("go.mod", `module mod.com +-package integration - -- go 1.12 --`) -- env.SaveBuffer("go.mod") -- var d protocol.PublishDiagnosticsParams -- env.AfterChange( -- NoDiagnostics(ForFile("main.go")), -- Diagnostics(env.AtRegexp("bob/bob.go", "x")), -- ReadDiagnostics("bob/bob.go", &d), -- ) -- if len(d.Diagnostics) != 1 { -- t.Fatalf("expected 1 diagnostic, got %v", len(d.Diagnostics)) -- } -- }) -- }) -- t.Run("initialized", func(t *testing.T) { -- Run(t, noMod, func(t *testing.T, env *Env) { -- env.OnceMet( -- InitialWorkspaceLoad, -- Diagnostics(env.AtRegexp("main.go", `"mod.com/bob"`)), -- ) -- env.RunGoCommand("mod", "init", "mod.com") -- env.AfterChange( -- NoDiagnostics(ForFile("main.go")), -- Diagnostics(env.AtRegexp("bob/bob.go", "x")), -- ) -- }) -- }) +-import ( +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/test/integration/fake" +-) - -- t.Run("without workspace module", func(t *testing.T) { -- WithOptions( -- Modes(Default), -- ).Run(t, noMod, func(t *testing.T, env *Env) { -- env.OnceMet( -- InitialWorkspaceLoad, -- Diagnostics(env.AtRegexp("main.go", `"mod.com/bob"`)), -- ) -- if err := env.Sandbox.RunGoCommand(env.Ctx, "", "mod", []string{"init", "mod.com"}, nil, true); err != nil { -- t.Fatal(err) -- } -- env.AfterChange( -- NoDiagnostics(ForFile("main.go")), -- Diagnostics(env.AtRegexp("bob/bob.go", "x")), -- ) -- }) -- }) +-type runConfig struct { +- editor fake.EditorConfig +- sandbox fake.SandboxConfig +- modes Mode +- noLogsOnError bool +- writeGoSum []string -} - --// Tests golang/go#38267. --func TestIssue38267(t *testing.T) { -- const testPackage = ` ---- go.mod -- --module mod.com -- --go 1.12 ---- lib.go -- --package lib +-func defaultConfig() runConfig { +- return runConfig{ +- editor: fake.EditorConfig{ +- Settings: map[string]interface{}{ +- // Shorten the diagnostic delay to speed up test execution (else we'd add +- // the default delay to each assertion about diagnostics) +- "diagnosticsDelay": "10ms", +- }, +- }, +- } +-} - --func Hello(x string) { -- _ = x +-// A RunOption augments the behavior of the test runner. +-type RunOption interface { +- set(*runConfig) -} ---- lib_test.go -- --package lib - --import "testing" +-type optionSetter func(*runConfig) - --type testStruct struct{ -- name string +-func (f optionSetter) set(opts *runConfig) { +- f(opts) -} - --func TestHello(t *testing.T) { -- testStructs := []*testStruct{ -- &testStruct{"hello"}, -- &testStruct{"goodbye"}, -- } -- for y := range testStructs { -- _ = y -- } +-// ProxyFiles configures a file proxy using the given txtar-encoded string. +-func ProxyFiles(txt string) RunOption { +- return optionSetter(func(opts *runConfig) { +- opts.sandbox.ProxyFiles = fake.UnpackTxt(txt) +- }) -} --` - -- Run(t, testPackage, func(t *testing.T, env *Env) { -- env.OpenFile("lib_test.go") -- env.AfterChange( -- Diagnostics(AtPosition("lib_test.go", 10, 2)), -- Diagnostics(AtPosition("lib_test.go", 11, 2)), -- ) -- env.OpenFile("lib.go") -- env.RegexpReplace("lib.go", "_ = x", "var y int") -- env.AfterChange( -- Diagnostics(env.AtRegexp("lib.go", "y int")), -- NoDiagnostics(ForFile("lib_test.go")), -- ) +-// WriteGoSum causes the environment to write a go.sum file for the requested +-// relative directories (via `go list -mod=mod`), before starting gopls. +-// +-// Useful for tests that use ProxyFiles, but don't care about crafting the +-// go.sum content. +-func WriteGoSum(dirs ...string) RunOption { +- return optionSetter(func(opts *runConfig) { +- opts.writeGoSum = dirs - }) -} - --// Tests golang/go#38328. --func TestPackageChange_Issue38328(t *testing.T) { -- const packageChange = ` ---- go.mod -- --module fake -- --go 1.12 ---- a.go -- --package foo --func main() {} --` -- Run(t, packageChange, func(t *testing.T, env *Env) { -- env.OpenFile("a.go") -- env.RegexpReplace("a.go", "foo", "foox") -- env.AfterChange( -- NoDiagnostics(ForFile("a.go")), -- ) +-// Modes configures the execution modes that the test should run in. +-// +-// By default, modes are configured by the test runner. If this option is set, +-// it overrides the set of default modes and the test runs in exactly these +-// modes. +-func Modes(modes Mode) RunOption { +- return optionSetter(func(opts *runConfig) { +- if opts.modes != 0 { +- panic("modes set more than once") +- } +- opts.modes = modes - }) -} - --const testPackageWithRequire = ` ---- go.mod -- --module mod.com -- --go 1.12 -- --require foo.test v1.2.3 ---- go.sum -- --foo.test v1.2.3 h1:TMA+lyd1ck0TqjSFpNe4T6cf/K6TYkoHwOOcMBMjaEw= --foo.test v1.2.3/go.mod h1:Ij3kyLIe5lzjycjh13NL8I2gX0quZuTdW0MnmlwGBL4= ---- print.go -- --package lib +-// NoLogsOnError turns off dumping the LSP logs on test failures. +-func NoLogsOnError() RunOption { +- return optionSetter(func(opts *runConfig) { +- opts.noLogsOnError = true +- }) +-} - --import ( -- "fmt" +-// WindowsLineEndings configures the editor to use windows line endings. +-func WindowsLineEndings() RunOption { +- return optionSetter(func(opts *runConfig) { +- opts.editor.WindowsLineEndings = true +- }) +-} - -- "foo.test/bar" --) +-// ClientName sets the LSP client name. +-func ClientName(name string) RunOption { +- return optionSetter(func(opts *runConfig) { +- opts.editor.ClientName = name +- }) +-} - --func PrintAnswer() { -- fmt.Printf("answer: %s", bar.Answer) +-// CapabilitiesJSON sets the capabalities json. +-func CapabilitiesJSON(capabilities []byte) RunOption { +- return optionSetter(func(opts *runConfig) { +- opts.editor.CapabilitiesJSON = capabilities +- }) -} --` - --const testPackageWithRequireProxy = ` ---- foo.test@v1.2.3/go.mod -- --module foo.test +-// Settings sets user-provided configuration for the LSP server. +-// +-// As a special case, the env setting must not be provided via Settings: use +-// EnvVars instead. +-type Settings map[string]interface{} - --go 1.12 ---- foo.test@v1.2.3/bar/const.go -- --package bar +-func (s Settings) set(opts *runConfig) { +- if opts.editor.Settings == nil { +- opts.editor.Settings = make(map[string]interface{}) +- } +- for k, v := range s { +- opts.editor.Settings[k] = v +- } +-} - --const Answer = 42 --` +-// WorkspaceFolders configures the workdir-relative workspace folders to send +-// to the LSP server. By default the editor sends a single workspace folder +-// corresponding to the workdir root. To explicitly configure no workspace +-// folders, use WorkspaceFolders with no arguments. +-func WorkspaceFolders(relFolders ...string) RunOption { +- if len(relFolders) == 0 { +- // Use an empty non-nil slice to signal explicitly no folders. +- relFolders = []string{} +- } - --func TestResolveDiagnosticWithDownload(t *testing.T) { -- WithOptions( -- ProxyFiles(testPackageWithRequireProxy), -- ).Run(t, testPackageWithRequire, func(t *testing.T, env *Env) { -- env.OpenFile("print.go") -- // Check that gopackages correctly loaded this dependency. We should get a -- // diagnostic for the wrong formatting type. -- env.AfterChange( -- Diagnostics( -- env.AtRegexp("print.go", "fmt.Printf"), -- WithMessage("wrong type int"), -- ), -- ) +- return optionSetter(func(opts *runConfig) { +- opts.editor.WorkspaceFolders = relFolders - }) -} - --func TestMissingDependency(t *testing.T) { -- Run(t, testPackageWithRequire, func(t *testing.T, env *Env) { -- env.OpenFile("print.go") -- env.Await( -- // Log messages are asynchronous to other events on the LSP stream, so we -- // can't use OnceMet or AfterChange here. -- LogMatching(protocol.Error, "initial workspace load failed", 1, false), -- ) -- }) +-// FolderSettings defines per-folder workspace settings, keyed by relative path +-// to the folder. +-// +-// Use in conjunction with WorkspaceFolders to have different settings for +-// different folders. +-type FolderSettings map[string]Settings +- +-func (fs FolderSettings) set(opts *runConfig) { +- // Re-use the Settings type, for symmetry, but translate back into maps for +- // the editor config. +- folders := make(map[string]map[string]any) +- for k, v := range fs { +- folders[k] = v +- } +- opts.editor.FolderSettings = folders -} - --// Tests golang/go#36951. --func TestAdHocPackages_Issue36951(t *testing.T) { -- const adHoc = ` ---- b/b.go -- --package b +-// EnvVars sets environment variables for the LSP session. When applying these +-// variables to the session, the special string $SANDBOX_WORKDIR is replaced by +-// the absolute path to the sandbox working directory. +-type EnvVars map[string]string - --func Hello() { -- var x int +-func (e EnvVars) set(opts *runConfig) { +- if opts.editor.Env == nil { +- opts.editor.Env = make(map[string]string) +- } +- for k, v := range e { +- opts.editor.Env[k] = v +- } -} --` -- Run(t, adHoc, func(t *testing.T, env *Env) { -- env.OpenFile("b/b.go") -- env.AfterChange( -- Diagnostics(env.AtRegexp("b/b.go", "x")), -- ) +- +-// InGOPATH configures the workspace working directory to be GOPATH, rather +-// than a separate working directory for use with modules. +-func InGOPATH() RunOption { +- return optionSetter(func(opts *runConfig) { +- opts.sandbox.InGoPath = true - }) -} - --// Tests golang/go#37984: GOPATH should be read from the go command. --func TestNoGOPATH_Issue37984(t *testing.T) { -- const files = ` ---- main.go -- --package main -- --func _() { -- fmt.Println("Hello World") --} --` -- WithOptions( -- EnvVars{ -- "GOPATH": "", -- "GO111MODULE": "off", -- }, -- ).Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- env.AfterChange(Diagnostics(env.AtRegexp("main.go", "fmt"))) -- env.SaveBuffer("main.go") -- env.AfterChange(NoDiagnostics(ForFile("main.go"))) +-// MessageResponder configures the editor to respond to +-// window/showMessageRequest messages using the provided function. +-func MessageResponder(f func(*protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error)) RunOption { +- return optionSetter(func(opts *runConfig) { +- opts.editor.MessageResponder = f - }) -} +diff -urN a/gopls/internal/test/integration/regtest.go b/gopls/internal/test/integration/regtest.go +--- a/gopls/internal/test/integration/regtest.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/regtest.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,185 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// Tests golang/go#38669. --func TestEqualInEnv_Issue38669(t *testing.T) { -- const files = ` ---- go.mod -- --module mod.com +-package integration - --go 1.12 ---- main.go -- --package main +-import ( +- "context" +- "flag" +- "fmt" +- "os" +- "runtime" +- "testing" +- "time" - --var _ = x.X ---- x/x.go -- --package x +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/cmd" +- "golang.org/x/tools/gopls/internal/settings" +- "golang.org/x/tools/internal/gocommand" +- "golang.org/x/tools/internal/memoize" +- "golang.org/x/tools/internal/testenv" +- "golang.org/x/tools/internal/tool" +-) - --var X = 0 --` -- WithOptions( -- EnvVars{"GOFLAGS": "-tags=foo"}, -- ).Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- env.OrganizeImports("main.go") -- env.AfterChange(NoDiagnostics(ForFile("main.go"))) -- }) --} +-var ( +- runSubprocessTests = flag.Bool("enable_gopls_subprocess_tests", false, "run integration tests against a gopls subprocess (default: in-process)") +- goplsBinaryPath = flag.String("gopls_test_binary", "", "path to the gopls binary for use as a remote, for use with the -enable_gopls_subprocess_tests flag") +- timeout = flag.Duration("timeout", defaultTimeout(), "if nonzero, default timeout for each integration test; defaults to GOPLS_INTEGRATION_TEST_TIMEOUT") +- skipCleanup = flag.Bool("skip_cleanup", false, "whether to skip cleaning up temp directories") +- printGoroutinesOnFailure = flag.Bool("print_goroutines", false, "whether to print goroutines info on failure") +- printLogs = flag.Bool("print_logs", false, "whether to print LSP logs") +-) - --// Tests golang/go#38467. --func TestNoSuggestedFixesForGeneratedFiles_Issue38467(t *testing.T) { -- const generated = ` ---- go.mod -- --module mod.com +-func defaultTimeout() time.Duration { +- s := os.Getenv("GOPLS_INTEGRATION_TEST_TIMEOUT") +- if s == "" { +- return 0 +- } +- d, err := time.ParseDuration(s) +- if err != nil { +- fmt.Fprintf(os.Stderr, "invalid GOPLS_INTEGRATION_TEST_TIMEOUT %q: %v\n", s, err) +- os.Exit(2) +- } +- return d +-} - --go 1.12 ---- main.go -- --package main +-var runner *Runner - --// Code generated by generator.go. DO NOT EDIT. +-// The integrationTestRunner interface abstracts the Run operation, +-// enables decorators for various optional features. +-type integrationTestRunner interface { +- Run(t *testing.T, files string, f TestFunc) +-} - --func _() { -- for i, _ := range []string{} { -- _ = i -- } +-func Run(t *testing.T, files string, f TestFunc) { +- runner.Run(t, files, f) -} --` -- Run(t, generated, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- var d protocol.PublishDiagnosticsParams -- env.AfterChange( -- Diagnostics(AtPosition("main.go", 5, 8)), -- ReadDiagnostics("main.go", &d), -- ) -- if fixes := env.GetQuickFixes("main.go", d.Diagnostics); len(fixes) != 0 { -- t.Errorf("got quick fixes %v, wanted none", fixes) -- } -- }) +- +-func WithOptions(opts ...RunOption) configuredRunner { +- return configuredRunner{opts: opts} -} - --// Expect a module/GOPATH error if there is an error in the file at startup. --// Tests golang/go#37279. --func TestBrokenWorkspace_OutsideModule(t *testing.T) { -- const noModule = ` ---- a.go -- --package foo +-type configuredRunner struct { +- opts []RunOption +-} - --import "mod.com/hello" +-func (r configuredRunner) Run(t *testing.T, files string, f TestFunc) { +- // Print a warning if the test's temporary directory is not +- // suitable as a workspace folder, as this may lead to +- // otherwise-cryptic failures. This situation typically occurs +- // when an arbitrary string (e.g. "foo.") is used as a subtest +- // name, on a platform with filename restrictions (e.g. no +- // trailing period on Windows). +- tmp := t.TempDir() +- if err := cache.CheckPathValid(tmp); err != nil { +- t.Logf("Warning: testing.T.TempDir(%s) is not valid as a workspace folder: %s", +- tmp, err) +- } - --func f() { -- hello.Goodbye() +- runner.Run(t, files, f, r.opts...) -} --` -- Run(t, noModule, func(t *testing.T, env *Env) { -- env.OpenFile("a.go") -- env.AfterChange( -- // Expect the adHocPackagesWarning. -- OutstandingWork(lsp.WorkspaceLoadFailure, "outside of a module"), -- ) -- // Deleting the import dismisses the warning. -- env.RegexpReplace("a.go", `import "mod.com/hello"`, "") -- env.AfterChange( -- NoOutstandingWork(IgnoreTelemetryPromptWork), -- ) -- }) +- +-type RunMultiple []struct { +- Name string +- Runner integrationTestRunner -} - --func TestNonGoFolder(t *testing.T) { -- const files = ` ---- hello.txt -- --hi mom --` -- for _, go111module := range []string{"on", "off", ""} { -- t.Run(fmt.Sprintf("GO111MODULE_%v", go111module), func(t *testing.T) { -- WithOptions( -- EnvVars{"GO111MODULE": go111module}, -- ).Run(t, files, func(t *testing.T, env *Env) { -- env.OnceMet( -- InitialWorkspaceLoad, -- NoOutstandingWork(IgnoreTelemetryPromptWork), -- ) -- }) +-func (r RunMultiple) Run(t *testing.T, files string, f TestFunc) { +- for _, runner := range r { +- t.Run(runner.Name, func(t *testing.T) { +- runner.Runner.Run(t, files, f) - }) - } -} - --// Tests the repro case from golang/go#38602. Diagnostics are now handled properly, --// which blocks type checking. --func TestConflictingMainPackageErrors(t *testing.T) { -- const collision = ` ---- x/x.go -- --package x +-// DefaultModes returns the default modes to run for each regression test (they +-// may be reconfigured by the tests themselves). +-func DefaultModes() Mode { +- modes := Default +- if !testing.Short() { +- modes |= Experimental | Forwarded +- } +- if *runSubprocessTests { +- modes |= SeparateProcess +- } +- return modes +-} - --import "x/hello" +-var runFromMain = false // true if Main has been called - --func Hello() { -- hello.HiThere() --} ---- x/main.go -- --package main +-// Main sets up and tears down the shared integration test state. +-func Main(m *testing.M, hook func(*settings.Options)) { +- runFromMain = true - --func main() { -- fmt.Println("") --} --` -- WithOptions( -- InGOPATH(), -- EnvVars{"GO111MODULE": "off"}, -- ).Run(t, collision, func(t *testing.T, env *Env) { -- env.OpenFile("x/x.go") -- env.AfterChange( -- Diagnostics(env.AtRegexp("x/x.go", `^`), WithMessage("found packages main (main.go) and x (x.go)")), -- Diagnostics(env.AtRegexp("x/main.go", `^`), WithMessage("found packages main (main.go) and x (x.go)")), -- ) +- // golang/go#54461: enable additional debugging around hanging Go commands. +- gocommand.DebugHangingGoCommands = true - -- // We don't recover cleanly from the errors without good overlay support. -- if testenv.Go1Point() >= 16 { -- env.RegexpReplace("x/x.go", `package x`, `package main`) -- env.AfterChange( -- Diagnostics(env.AtRegexp("x/main.go", `fmt`)), -- ) -- } -- }) --} +- // If this magic environment variable is set, run gopls instead of the test +- // suite. See the documentation for runTestAsGoplsEnvvar for more details. +- if os.Getenv(runTestAsGoplsEnvvar) == "true" { +- tool.Main(context.Background(), cmd.New(hook), os.Args[1:]) +- os.Exit(0) +- } - --const ardanLabsProxy = ` ---- github.com/ardanlabs/conf@v1.2.3/go.mod -- --module github.com/ardanlabs/conf +- if !testenv.HasExec() { +- fmt.Printf("skipping all tests: exec not supported on %s/%s\n", runtime.GOOS, runtime.GOARCH) +- os.Exit(0) +- } +- testenv.ExitIfSmallMachine() - --go 1.12 ---- github.com/ardanlabs/conf@v1.2.3/conf.go -- --package conf +- // Disable GOPACKAGESDRIVER, as it can cause spurious test failures. +- os.Setenv("GOPACKAGESDRIVER", "off") - --var ErrHelpWanted error --` +- if skipReason := checkBuilder(); skipReason != "" { +- fmt.Printf("Skipping all tests: %s\n", skipReason) +- os.Exit(0) +- } - --// Test for golang/go#38211. --func Test_Issue38211(t *testing.T) { -- const ardanLabs = ` ---- go.mod -- --module mod.com +- if err := testenv.HasTool("go"); err != nil { +- fmt.Println("Missing go command") +- os.Exit(1) +- } - --go 1.14 ---- main.go -- --package main +- flag.Parse() - --import "github.com/ardanlabs/conf" +- runner = &Runner{ +- DefaultModes: DefaultModes(), +- Timeout: *timeout, +- PrintGoroutinesOnFailure: *printGoroutinesOnFailure, +- SkipCleanup: *skipCleanup, +- OptionsHook: hook, +- store: memoize.NewStore(memoize.NeverEvict), +- } - --func main() { -- _ = conf.ErrHelpWanted --} --` -- WithOptions( -- ProxyFiles(ardanLabsProxy), -- ).Run(t, ardanLabs, func(t *testing.T, env *Env) { -- // Expect a diagnostic with a suggested fix to add -- // "github.com/ardanlabs/conf" to the go.mod file. -- env.OpenFile("go.mod") -- env.OpenFile("main.go") -- var d protocol.PublishDiagnosticsParams -- env.AfterChange( -- Diagnostics(env.AtRegexp("main.go", `"github.com/ardanlabs/conf"`)), -- ReadDiagnostics("main.go", &d), -- ) -- env.ApplyQuickFixes("main.go", d.Diagnostics) -- env.SaveBuffer("go.mod") -- env.AfterChange( -- NoDiagnostics(ForFile("main.go")), -- ) -- // Comment out the line that depends on conf and expect a -- // diagnostic and a fix to remove the import. -- env.RegexpReplace("main.go", "_ = conf.ErrHelpWanted", "//_ = conf.ErrHelpWanted") -- env.AfterChange( -- Diagnostics(env.AtRegexp("main.go", `"github.com/ardanlabs/conf"`)), -- ) -- env.SaveBuffer("main.go") -- // Expect a diagnostic and fix to remove the dependency in the go.mod. -- env.AfterChange( -- NoDiagnostics(ForFile("main.go")), -- Diagnostics(env.AtRegexp("go.mod", "require github.com/ardanlabs/conf"), WithMessage("not used in this module")), -- ReadDiagnostics("go.mod", &d), -- ) -- env.ApplyQuickFixes("go.mod", d.Diagnostics) -- env.SaveBuffer("go.mod") -- env.AfterChange( -- NoDiagnostics(ForFile("go.mod")), -- ) -- // Uncomment the lines and expect a new diagnostic for the import. -- env.RegexpReplace("main.go", "//_ = conf.ErrHelpWanted", "_ = conf.ErrHelpWanted") -- env.SaveBuffer("main.go") -- env.AfterChange( -- Diagnostics(env.AtRegexp("main.go", `"github.com/ardanlabs/conf"`)), -- ) -- }) +- runner.goplsPath = *goplsBinaryPath +- if runner.goplsPath == "" { +- var err error +- runner.goplsPath, err = os.Executable() +- if err != nil { +- panic(fmt.Sprintf("finding test binary path: %v", err)) +- } +- } +- +- dir, err := os.MkdirTemp("", "gopls-test-") +- if err != nil { +- panic(fmt.Errorf("creating temp directory: %v", err)) +- } +- runner.tempDir = dir +- +- var code int +- defer func() { +- if err := runner.Close(); err != nil { +- fmt.Fprintf(os.Stderr, "closing test runner: %v\n", err) +- // Cleanup is broken in go1.12 and earlier, and sometimes flakes on +- // Windows due to file locking, but this is OK for our CI. +- // +- // Fail on go1.13+, except for windows and android which have shutdown problems. +- if testenv.Go1Point() >= 13 && runtime.GOOS != "windows" && runtime.GOOS != "android" { +- os.Exit(1) +- } +- } +- os.Exit(code) +- }() +- code = m.Run() -} +diff -urN a/gopls/internal/test/integration/runner.go b/gopls/internal/test/integration/runner.go +--- a/gopls/internal/test/integration/runner.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/runner.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,448 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// Test for golang/go#38207. --func TestNewModule_Issue38207(t *testing.T) { -- const emptyFile = ` ---- go.mod -- --module mod.com +-package integration - --go 1.12 ---- main.go -- --` -- WithOptions( -- ProxyFiles(ardanLabsProxy), -- ).Run(t, emptyFile, func(t *testing.T, env *Env) { -- env.CreateBuffer("main.go", `package main +-import ( +- "bytes" +- "context" +- "fmt" +- "io" +- "net" +- "os" +- "os/exec" +- "path/filepath" +- "runtime" +- "runtime/pprof" +- "strings" +- "sync" +- "testing" +- "time" - --import "github.com/ardanlabs/conf" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/debug" +- "golang.org/x/tools/gopls/internal/lsprpc" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/settings" +- "golang.org/x/tools/gopls/internal/test/integration/fake" +- "golang.org/x/tools/internal/jsonrpc2" +- "golang.org/x/tools/internal/jsonrpc2/servertest" +- "golang.org/x/tools/internal/memoize" +- "golang.org/x/tools/internal/testenv" +- "golang.org/x/tools/internal/xcontext" +-) - --func main() { -- _ = conf.ErrHelpWanted --} --`) -- env.SaveBuffer("main.go") -- var d protocol.PublishDiagnosticsParams -- env.AfterChange( -- Diagnostics(env.AtRegexp("main.go", `"github.com/ardanlabs/conf"`), WithMessage("no required module")), -- ReadDiagnostics("main.go", &d), -- ) -- env.ApplyQuickFixes("main.go", d.Diagnostics) -- env.AfterChange( -- NoDiagnostics(ForFile("main.go")), -- ) -- }) --} +-// Mode is a bitmask that defines for which execution modes a test should run. +-// +-// Each mode controls several aspects of gopls' configuration: +-// - Which server options to use for gopls sessions +-// - Whether to use a shared cache +-// - Whether to use a shared server +-// - Whether to run the server in-process or in a separate process +-// +-// The behavior of each mode with respect to these aspects is summarized below. +-// TODO(rfindley, cleanup): rather than using arbitrary names for these modes, +-// we can compose them explicitly out of the features described here, allowing +-// individual tests more freedom in constructing problematic execution modes. +-// For example, a test could assert on a certain behavior when running with +-// experimental options on a separate process. Moreover, we could unify 'Modes' +-// with 'Options', and use RunMultiple rather than a hard-coded loop through +-// modes. +-// +-// Mode | Options | Shared Cache? | Shared Server? | In-process? +-// --------------------------------------------------------------------------- +-// Default | Default | Y | N | Y +-// Forwarded | Default | Y | Y | Y +-// SeparateProcess | Default | Y | Y | N +-// Experimental | Experimental | N | N | Y +-type Mode int - --// Test for golang/go#36960. --func TestNewFileBadImports_Issue36960(t *testing.T) { -- const simplePackage = ` ---- go.mod -- --module mod.com +-const ( +- // Default mode runs gopls with the default options, communicating over pipes +- // to emulate the lsp sidecar execution mode, which communicates over +- // stdin/stdout. +- // +- // It uses separate servers for each test, but a shared cache, to avoid +- // duplicating work when processing GOROOT. +- Default Mode = 1 << iota - --go 1.14 ---- a/a1.go -- --package a +- // Forwarded uses the default options, but forwards connections to a shared +- // in-process gopls server. +- Forwarded - --import "fmt" +- // SeparateProcess uses the default options, but forwards connection to an +- // external gopls daemon. +- // +- // Only supported on GOOS=linux. +- SeparateProcess - --func _() { -- fmt.Println("hi") --} --` -- Run(t, simplePackage, func(t *testing.T, env *Env) { -- env.OpenFile("a/a1.go") -- env.CreateBuffer("a/a2.go", ``) -- env.SaveBufferWithoutActions("a/a2.go") -- env.AfterChange( -- NoDiagnostics(ForFile("a/a1.go")), -- ) -- env.EditBuffer("a/a2.go", fake.NewEdit(0, 0, 0, 0, `package a`)) -- env.AfterChange( -- NoDiagnostics(ForFile("a/a1.go")), -- ) -- }) --} +- // Experimental enables all of the experimental configurations that are +- // being developed, and runs gopls in sidecar mode. +- // +- // It uses a separate cache for each test, to exercise races that may only +- // appear with cache misses. +- Experimental +-) - --// This test tries to replicate the workflow of a user creating a new x test. --// It also tests golang/go#39315. --func TestManuallyCreatingXTest(t *testing.T) { -- // Create a package that already has a test variant (in-package test). -- const testVariant = ` ---- go.mod -- --module mod.com +-func (m Mode) String() string { +- switch m { +- case Default: +- return "default" +- case Forwarded: +- return "forwarded" +- case SeparateProcess: +- return "separate process" +- case Experimental: +- return "experimental" +- default: +- return "unknown mode" +- } +-} - --go 1.15 ---- hello/hello.go -- --package hello +-// A Runner runs tests in gopls execution environments, as specified by its +-// modes. For modes that share state (for example, a shared cache or common +-// remote), any tests that execute on the same Runner will share the same +-// state. +-type Runner struct { +- // Configuration +- DefaultModes Mode // modes to run for each test +- Timeout time.Duration // per-test timeout, if set +- PrintGoroutinesOnFailure bool // whether to dump goroutines on test failure +- SkipCleanup bool // if set, don't delete test data directories when the test exits +- OptionsHook func(*settings.Options) // if set, use these options when creating gopls sessions - --func Hello() { -- var x int --} ---- hello/hello_test.go -- --package hello +- // Immutable state shared across test invocations +- goplsPath string // path to the gopls executable (for SeparateProcess mode) +- tempDir string // shared parent temp directory +- store *memoize.Store // shared store - --import "testing" +- // Lazily allocated resources +- tsOnce sync.Once +- ts *servertest.TCPServer // shared in-process test server ("forwarded" mode) - --func TestHello(t *testing.T) { -- var x int -- Hello() +- startRemoteOnce sync.Once +- remoteSocket string // unix domain socket for shared daemon ("separate process" mode) +- remoteErr error +- cancelRemote func() -} --` -- Run(t, testVariant, func(t *testing.T, env *Env) { -- // Open the file, triggering the workspace load. -- // There are errors in the code to ensure all is working as expected. -- env.OpenFile("hello/hello.go") -- env.AfterChange( -- Diagnostics(env.AtRegexp("hello/hello.go", "x")), -- Diagnostics(env.AtRegexp("hello/hello_test.go", "x")), -- ) - -- // Create an empty file with the intention of making it an x test. -- // This resembles a typical flow in an editor like VS Code, in which -- // a user would create an empty file and add content, saving -- // intermittently. -- // TODO(rstambler): There might be more edge cases here, as file -- // content can be added incrementally. -- env.CreateBuffer("hello/hello_x_test.go", ``) +-type TestFunc func(t *testing.T, env *Env) - -- // Save the empty file (no actions since formatting will fail). -- env.SaveBufferWithoutActions("hello/hello_x_test.go") +-// Run executes the test function in the default configured gopls execution +-// modes. For each a test run, a new workspace is created containing the +-// un-txtared files specified by filedata. +-func (r *Runner) Run(t *testing.T, files string, test TestFunc, opts ...RunOption) { +- // TODO(rfindley): this function has gotten overly complicated, and warrants +- // refactoring. - -- // Add the content. The missing import is for the package under test. -- env.EditBuffer("hello/hello_x_test.go", fake.NewEdit(0, 0, 0, 0, `package hello_test +- if !runFromMain { +- // Main performs various setup precondition checks. +- // While it could theoretically be made OK for a Runner to be used outside +- // of Main, it is simpler to enforce that we only use the Runner from +- // integration test suites. +- t.Fatal("integration.Runner.Run must be run from integration.Main") +- } - --import ( -- "testing" --) +- tests := []struct { +- name string +- mode Mode +- getServer func(func(*settings.Options)) jsonrpc2.StreamServer +- }{ +- {"default", Default, r.defaultServer}, +- {"forwarded", Forwarded, r.forwardedServer}, +- {"separate_process", SeparateProcess, r.separateProcessServer}, +- {"experimental", Experimental, r.experimentalServer}, +- } - --func TestHello(t *testing.T) { -- hello.Hello() --} --`)) -- // Expect a diagnostic for the missing import. Save, which should -- // trigger import organization. The diagnostic should clear. -- env.AfterChange( -- Diagnostics(env.AtRegexp("hello/hello_x_test.go", "hello.Hello")), -- ) -- env.SaveBuffer("hello/hello_x_test.go") -- env.AfterChange( -- NoDiagnostics(ForFile("hello/hello_x_test.go")), -- ) -- }) --} +- for _, tc := range tests { +- tc := tc +- config := defaultConfig() +- for _, opt := range opts { +- opt.set(&config) +- } +- modes := r.DefaultModes +- if config.modes != 0 { +- modes = config.modes +- } +- if modes&tc.mode == 0 { +- continue +- } - --// Reproduce golang/go#40690. --func TestCreateOnlyXTest(t *testing.T) { -- const mod = ` ---- go.mod -- --module mod.com +- t.Run(tc.name, func(t *testing.T) { +- // TODO(rfindley): once jsonrpc2 shutdown is fixed, we should not leak +- // goroutines in this test function. +- // stacktest.NoLeak(t) - --go 1.12 ---- foo/foo.go -- --package foo ---- foo/bar_test.go -- --` -- Run(t, mod, func(t *testing.T, env *Env) { -- env.OpenFile("foo/bar_test.go") -- env.EditBuffer("foo/bar_test.go", fake.NewEdit(0, 0, 0, 0, "package foo")) -- env.Await(env.DoneWithChange()) -- env.RegexpReplace("foo/bar_test.go", "package foo", `package foo_test +- ctx := context.Background() +- if r.Timeout != 0 { +- var cancel context.CancelFunc +- ctx, cancel = context.WithTimeout(ctx, r.Timeout) +- defer cancel() +- } else if d, ok := testenv.Deadline(t); ok { +- timeout := time.Until(d) * 19 / 20 // Leave an arbitrary 5% for cleanup. +- var cancel context.CancelFunc +- ctx, cancel = context.WithTimeout(ctx, timeout) +- defer cancel() +- } - --import "testing" +- // TODO(rfindley): do we need an instance at all? Can it be removed? +- ctx = debug.WithInstance(ctx, "off") - --func TestX(t *testing.T) { -- var x int --} --`) -- env.AfterChange( -- Diagnostics(env.AtRegexp("foo/bar_test.go", "x")), -- ) -- }) --} +- rootDir := filepath.Join(r.tempDir, filepath.FromSlash(t.Name())) +- if err := os.MkdirAll(rootDir, 0755); err != nil { +- t.Fatal(err) +- } - --func TestChangePackageName(t *testing.T) { -- const mod = ` ---- go.mod -- --module mod.com +- files := fake.UnpackTxt(files) +- if config.editor.WindowsLineEndings { +- for name, data := range files { +- files[name] = bytes.ReplaceAll(data, []byte("\n"), []byte("\r\n")) +- } +- } +- config.sandbox.Files = files +- config.sandbox.RootDir = rootDir +- sandbox, err := fake.NewSandbox(&config.sandbox) +- if err != nil { +- t.Fatal(err) +- } +- defer func() { +- if !r.SkipCleanup { +- if err := sandbox.Close(); err != nil { +- pprof.Lookup("goroutine").WriteTo(os.Stderr, 1) +- t.Errorf("closing the sandbox: %v", err) +- } +- } +- }() - --go 1.12 ---- foo/foo.go -- --package foo ---- foo/bar_test.go -- --package foo_ --` -- Run(t, mod, func(t *testing.T, env *Env) { -- env.OpenFile("foo/bar_test.go") -- env.AfterChange() -- env.RegexpReplace("foo/bar_test.go", "package foo_", "package foo_test") -- env.AfterChange( -- NoDiagnostics(ForFile("foo/bar_test.go")), -- NoDiagnostics(ForFile("foo/foo.go")), -- ) -- }) --} +- // Write the go.sum file for the requested directories, before starting the server. +- for _, dir := range config.writeGoSum { +- if err := sandbox.RunGoCommand(context.Background(), dir, "list", []string{"-mod=mod", "./..."}, []string{"GOWORK=off"}, true); err != nil { +- t.Fatal(err) +- } +- } - --func TestIgnoredFiles(t *testing.T) { -- const ws = ` ---- go.mod -- --module mod.com +- ss := tc.getServer(r.OptionsHook) - --go 1.12 ---- _foo/x.go -- --package x +- framer := jsonrpc2.NewRawStream +- ls := &loggingFramer{} +- framer = ls.framer(jsonrpc2.NewRawStream) +- ts := servertest.NewPipeServer(ss, framer) - --var _ = foo.Bar --` -- Run(t, ws, func(t *testing.T, env *Env) { -- env.OpenFile("_foo/x.go") -- env.AfterChange( -- NoDiagnostics(ForFile("_foo/x.go")), -- ) -- }) +- awaiter := NewAwaiter(sandbox.Workdir) +- const skipApplyEdits = false +- editor, err := fake.NewEditor(sandbox, config.editor).Connect(ctx, ts, awaiter.Hooks(), skipApplyEdits) +- if err != nil { +- t.Fatal(err) +- } +- env := &Env{ +- T: t, +- Ctx: ctx, +- Sandbox: sandbox, +- Editor: editor, +- Server: ts, +- Awaiter: awaiter, +- } +- defer func() { +- if t.Failed() && r.PrintGoroutinesOnFailure { +- pprof.Lookup("goroutine").WriteTo(os.Stderr, 1) +- } +- if (t.Failed() && !config.noLogsOnError) || *printLogs { +- ls.printBuffers(t.Name(), os.Stderr) +- } +- // For tests that failed due to a timeout, don't fail to shutdown +- // because ctx is done. +- // +- // There is little point to setting an arbitrary timeout for closing +- // the editor: in general we want to clean up before proceeding to the +- // next test, and if there is a deadlock preventing closing it will +- // eventually be handled by the `go test` timeout. +- if err := editor.Close(xcontext.Detach(ctx)); err != nil { +- t.Errorf("closing editor: %v", err) +- } +- }() +- // Always await the initial workspace load. +- env.Await(InitialWorkspaceLoad) +- test(t, env) +- }) +- } -} - --// Partially reproduces golang/go#38977, moving a file between packages. --// It also gets hit by some go command bug fixed in 1.15, but we don't --// care about that so much here. --func TestDeletePackage(t *testing.T) { -- const ws = ` ---- go.mod -- --module mod.com -- --go 1.15 ---- a/a.go -- --package a -- --const A = 1 +-// longBuilders maps builders that are skipped when -short is set to a +-// (possibly empty) justification. +-var longBuilders = map[string]string{ +- "openbsd-amd64-64": "golang.org/issues/42789", +- "openbsd-386-64": "golang.org/issues/42789", +- "openbsd-386-68": "golang.org/issues/42789", +- "openbsd-amd64-68": "golang.org/issues/42789", +- "darwin-amd64-10_12": "", +- "freebsd-amd64-race": "", +- "illumos-amd64": "", +- "netbsd-arm-bsiegert": "", +- "solaris-amd64-oraclerel": "", +- "windows-arm-zx2c4": "", +-} - ---- b/b.go -- --package b +-// TODO(rfindley): inline into Main. +-func checkBuilder() string { +- builder := os.Getenv("GO_BUILDER_NAME") +- if reason, ok := longBuilders[builder]; ok && testing.Short() { +- if reason != "" { +- return fmt.Sprintf("skipping %s with -short due to %s", builder, reason) +- } else { +- return fmt.Sprintf("skipping %s with -short", builder) +- } +- } +- return "" +-} - --import "mod.com/a" +-type loggingFramer struct { +- mu sync.Mutex +- buf *safeBuffer +-} - --const B = a.A +-// safeBuffer is a threadsafe buffer for logs. +-type safeBuffer struct { +- mu sync.Mutex +- buf bytes.Buffer +-} - ---- c/c.go -- --package c +-func (b *safeBuffer) Write(p []byte) (int, error) { +- b.mu.Lock() +- defer b.mu.Unlock() +- return b.buf.Write(p) +-} - --import "mod.com/a" +-func (s *loggingFramer) framer(f jsonrpc2.Framer) jsonrpc2.Framer { +- return func(nc net.Conn) jsonrpc2.Stream { +- s.mu.Lock() +- framed := false +- if s.buf == nil { +- s.buf = &safeBuffer{buf: bytes.Buffer{}} +- framed = true +- } +- s.mu.Unlock() +- stream := f(nc) +- if framed { +- return protocol.LoggingStream(stream, s.buf) +- } +- return stream +- } +-} - --const C = a.A --` -- Run(t, ws, func(t *testing.T, env *Env) { -- env.OpenFile("b/b.go") -- env.Await(env.DoneWithOpen()) -- // Delete c/c.go, the only file in package c. -- env.RemoveWorkspaceFile("c/c.go") +-func (s *loggingFramer) printBuffers(testname string, w io.Writer) { +- s.mu.Lock() +- defer s.mu.Unlock() - -- // We should still get diagnostics for files that exist. -- env.RegexpReplace("b/b.go", `a.A`, "a.Nonexistant") -- env.AfterChange( -- Diagnostics(env.AtRegexp("b/b.go", `Nonexistant`)), -- ) -- }) +- if s.buf == nil { +- return +- } +- fmt.Fprintf(os.Stderr, "#### Start Gopls Test Logs for %q\n", testname) +- s.buf.mu.Lock() +- io.Copy(w, &s.buf.buf) +- s.buf.mu.Unlock() +- fmt.Fprintf(os.Stderr, "#### End Gopls Test Logs for %q\n", testname) -} - --// This is a copy of the scenario_default/quickfix_empty_files.txt test from --// govim. Reproduces golang/go#39646. --func TestQuickFixEmptyFiles(t *testing.T) { -- const mod = ` ---- go.mod -- --module mod.com -- --go 1.12 --` -- // To fully recreate the govim tests, we create files by inserting -- // a newline, adding to the file, and then deleting the newline. -- // Wait for each event to process to avoid cancellations and force -- // package loads. -- writeGoVim := func(env *Env, name, content string) { -- env.WriteWorkspaceFile(name, "") -- env.Await(env.DoneWithChangeWatchedFiles()) +-// defaultServer handles the Default execution mode. +-func (r *Runner) defaultServer(optsHook func(*settings.Options)) jsonrpc2.StreamServer { +- return lsprpc.NewStreamServer(cache.New(r.store), false, optsHook) +-} - -- env.CreateBuffer(name, "\n") -- env.Await(env.DoneWithOpen()) +-// experimentalServer handles the Experimental execution mode. +-func (r *Runner) experimentalServer(optsHook func(*settings.Options)) jsonrpc2.StreamServer { +- options := func(o *settings.Options) { +- optsHook(o) +- o.EnableAllExperiments() +- } +- return lsprpc.NewStreamServer(cache.New(nil), false, options) +-} - -- env.EditBuffer(name, fake.NewEdit(1, 0, 1, 0, content)) -- env.Await(env.DoneWithChange()) +-// forwardedServer handles the Forwarded execution mode. +-func (r *Runner) forwardedServer(optsHook func(*settings.Options)) jsonrpc2.StreamServer { +- r.tsOnce.Do(func() { +- ctx := context.Background() +- ctx = debug.WithInstance(ctx, "off") +- ss := lsprpc.NewStreamServer(cache.New(nil), false, optsHook) +- r.ts = servertest.NewTCPServer(ctx, ss, nil) +- }) +- return newForwarder("tcp", r.ts.Addr) +-} - -- env.EditBuffer(name, fake.NewEdit(0, 0, 1, 0, "")) -- env.Await(env.DoneWithChange()) +-// runTestAsGoplsEnvvar triggers TestMain to run gopls instead of running +-// tests. It's a trick to allow tests to find a binary to use to start a gopls +-// subprocess. +-const runTestAsGoplsEnvvar = "_GOPLS_TEST_BINARY_RUN_AS_GOPLS" +- +-// separateProcessServer handles the SeparateProcess execution mode. +-func (r *Runner) separateProcessServer(optsHook func(*settings.Options)) jsonrpc2.StreamServer { +- if runtime.GOOS != "linux" { +- panic("separate process execution mode is only supported on linux") - } - -- const p = `package p; func DoIt(s string) {};` -- const main = `package main +- r.startRemoteOnce.Do(func() { +- socketDir, err := os.MkdirTemp(r.tempDir, "gopls-test-socket") +- if err != nil { +- r.remoteErr = err +- return +- } +- r.remoteSocket = filepath.Join(socketDir, "gopls-test-daemon") - --import "mod.com/p" +- // The server should be killed by when the test runner exits, but to be +- // conservative also set a listen timeout. +- args := []string{"serve", "-listen", "unix;" + r.remoteSocket, "-listen.timeout", "1m"} - --func main() { -- p.DoIt(5) --} --` -- // A simple version of the test that reproduces most of the problems it -- // exposes. -- t.Run("short", func(t *testing.T) { -- Run(t, mod, func(t *testing.T, env *Env) { -- writeGoVim(env, "p/p.go", p) -- writeGoVim(env, "main.go", main) -- env.AfterChange( -- Diagnostics(env.AtRegexp("main.go", "5")), -- ) -- }) +- ctx, cancel := context.WithCancel(context.Background()) +- cmd := exec.CommandContext(ctx, r.goplsPath, args...) +- cmd.Env = append(os.Environ(), runTestAsGoplsEnvvar+"=true") +- +- // Start the external gopls process. This is still somewhat racy, as we +- // don't know when gopls binds to the socket, but the gopls forwarder +- // client has built-in retry behavior that should mostly mitigate this +- // problem (and if it doesn't, we probably want to improve the retry +- // behavior). +- if err := cmd.Start(); err != nil { +- cancel() +- r.remoteSocket = "" +- r.remoteErr = err +- } else { +- r.cancelRemote = cancel +- // Spin off a goroutine to wait, so that we free up resources when the +- // server exits. +- go cmd.Wait() +- } - }) - -- // A full version that replicates the whole flow of the test. -- t.Run("full", func(t *testing.T) { -- Run(t, mod, func(t *testing.T, env *Env) { -- writeGoVim(env, "p/p.go", p) -- writeGoVim(env, "main.go", main) -- writeGoVim(env, "p/p_test.go", `package p +- return newForwarder("unix", r.remoteSocket) +-} - --import "testing" +-func newForwarder(network, address string) jsonrpc2.StreamServer { +- server, err := lsprpc.NewForwarder(network+";"+address, nil) +- if err != nil { +- // This should never happen, as we are passing an explicit address. +- panic(fmt.Sprintf("internal error: unable to create forwarder: %v", err)) +- } +- return server +-} - --func TestDoIt(t *testing.T) { -- DoIt(5) +-// Close cleans up resource that have been allocated to this workspace. +-func (r *Runner) Close() error { +- var errmsgs []string +- if r.ts != nil { +- if err := r.ts.Close(); err != nil { +- errmsgs = append(errmsgs, err.Error()) +- } +- } +- if r.cancelRemote != nil { +- r.cancelRemote() +- } +- if !r.SkipCleanup { +- if err := os.RemoveAll(r.tempDir); err != nil { +- errmsgs = append(errmsgs, err.Error()) +- } +- } +- if len(errmsgs) > 0 { +- return fmt.Errorf("errors closing the test runner:\n\t%s", strings.Join(errmsgs, "\n\t")) +- } +- return nil -} --`) -- writeGoVim(env, "p/x_test.go", `package p_test +diff -urN a/gopls/internal/test/integration/template/template_test.go b/gopls/internal/test/integration/template/template_test.go +--- a/gopls/internal/test/integration/template/template_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/template/template_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,231 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package template - -import ( +- "strings" - "testing" - -- "mod.com/p" +- "golang.org/x/tools/gopls/internal/hooks" +- "golang.org/x/tools/gopls/internal/protocol" +- . "golang.org/x/tools/gopls/internal/test/integration" +- "golang.org/x/tools/gopls/internal/util/bug" -) - --func TestDoIt(t *testing.T) { -- p.DoIt(5) --} --`) -- env.AfterChange( -- Diagnostics(env.AtRegexp("main.go", "5")), -- Diagnostics(env.AtRegexp("p/p_test.go", "5")), -- Diagnostics(env.AtRegexp("p/x_test.go", "5")), -- ) -- env.RegexpReplace("p/p.go", "s string", "i int") -- env.AfterChange( -- NoDiagnostics(ForFile("main.go")), -- NoDiagnostics(ForFile("p/p_test.go")), -- NoDiagnostics(ForFile("p/x_test.go")), -- ) -- }) -- }) +-func TestMain(m *testing.M) { +- bug.PanicOnBugs = true +- Main(m, hooks.Options) -} - --func TestSingleFile(t *testing.T) { -- const mod = ` +-func TestMultilineTokens(t *testing.T) { +- // 51731: panic: runtime error: slice bounds out of range [38:3] +- const files = ` --- go.mod -- -module mod.com - --go 1.13 ---- a/a.go -- --package a -- --func _() { -- var x int --} +-go 1.17 +--- hi.tmpl -- +-{{if (foÜx .X.Y)}}😀{{$A := +- "hi" +- }}{{.Z $A}}{{else}} +-{{$A.X 12}} +-{{foo (.X.Y) 23 ($A.Z)}} +-{{end}} -` - WithOptions( -- // Empty workspace folders. -- WorkspaceFolders(), -- ).Run(t, mod, func(t *testing.T, env *Env) { -- env.OpenFile("a/a.go") -- env.AfterChange( -- Diagnostics(env.AtRegexp("a/a.go", "x")), -- ) +- Settings{ +- "templateExtensions": []string{"tmpl"}, +- "semanticTokens": true, +- }, +- ).Run(t, files, func(t *testing.T, env *Env) { +- var p protocol.SemanticTokensParams +- p.TextDocument.URI = env.Sandbox.Workdir.URI("hi.tmpl") +- toks, err := env.Editor.Server.SemanticTokensFull(env.Ctx, &p) +- if err != nil { +- t.Errorf("semantic token failed: %v", err) +- } +- if toks == nil || len(toks.Data) == 0 { +- t.Errorf("got no semantic tokens") +- } - }) -} - --// Reproduces the case described in --// https://github.com/golang/go/issues/39296#issuecomment-652058883. --func TestPkgm(t *testing.T) { -- const basic = ` +-func TestTemplatesFromExtensions(t *testing.T) { +- const files = ` --- go.mod -- -module mod.com - --go 1.15 ---- foo/foo.go -- --package foo -- --import "fmt" -- --func Foo() { -- fmt.Println("") --} +-go 1.12 +--- hello.tmpl -- +-{{range .Planets}} +-Hello {{}} <-- missing body +-{{end}} -` -- Run(t, basic, func(t *testing.T, env *Env) { -- env.WriteWorkspaceFile("foo/foo_test.go", `package main -- --func main() { +- WithOptions( +- Settings{ +- "templateExtensions": []string{"tmpl"}, +- "semanticTokens": true, +- }, +- ).Run(t, files, func(t *testing.T, env *Env) { +- // TODO: can we move this diagnostic onto {{}}? +- var diags protocol.PublishDiagnosticsParams +- env.OnceMet( +- InitialWorkspaceLoad, +- Diagnostics(env.AtRegexp("hello.tmpl", "()Hello {{}}")), +- ReadDiagnostics("hello.tmpl", &diags), +- ) +- d := diags.Diagnostics // issue 50786: check for Source +- if len(d) != 1 { +- t.Errorf("expected 1 diagnostic, got %d", len(d)) +- return +- } +- if d[0].Source != "template" { +- t.Errorf("expected Source 'template', got %q", d[0].Source) +- } +- // issue 50801 (even broken templates could return some semantic tokens) +- var p protocol.SemanticTokensParams +- p.TextDocument.URI = env.Sandbox.Workdir.URI("hello.tmpl") +- toks, err := env.Editor.Server.SemanticTokensFull(env.Ctx, &p) +- if err != nil { +- t.Errorf("semantic token failed: %v", err) +- } +- if toks == nil || len(toks.Data) == 0 { +- t.Errorf("got no semantic tokens") +- } - --}`) -- env.OpenFile("foo/foo_test.go") -- env.RegexpReplace("foo/foo_test.go", `package main`, `package foo`) -- env.AfterChange(NoDiagnostics(ForFile("foo/foo.go"))) +- env.WriteWorkspaceFile("hello.tmpl", "{{range .Planets}}\nHello {{.}}\n{{end}}") +- env.AfterChange(NoDiagnostics(ForFile("hello.tmpl"))) - }) -} - --func TestClosingBuffer(t *testing.T) { -- const basic = ` +-func TestTemplatesObserveDirectoryFilters(t *testing.T) { +- const files = ` --- go.mod -- -module mod.com - --go 1.14 ---- main.go -- --package main -- --func main() {} +-go 1.12 +--- a/a.tmpl -- +-A {{}} <-- missing body +--- b/b.tmpl -- +-B {{}} <-- missing body -` -- Run(t, basic, func(t *testing.T, env *Env) { -- env.Editor.CreateBuffer(env.Ctx, "foo.go", `package main`) -- env.AfterChange() -- env.CloseBuffer("foo.go") -- env.AfterChange(NoLogMatching(protocol.Info, "packages=0")) +- +- WithOptions( +- Settings{ +- "directoryFilters": []string{"-b"}, +- "templateExtensions": []string{"tmpl"}, +- }, +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.OnceMet( +- InitialWorkspaceLoad, +- Diagnostics(env.AtRegexp("a/a.tmpl", "()A")), +- NoDiagnostics(ForFile("b/b.tmpl")), +- ) - }) -} - --// Reproduces golang/go#38424. --func TestCutAndPaste(t *testing.T) { -- const basic = ` +-func TestTemplatesFromLangID(t *testing.T) { +- const files = ` --- go.mod -- -module mod.com - --go 1.14 ---- main2.go -- --package main +-go 1.12 -` -- Run(t, basic, func(t *testing.T, env *Env) { -- env.CreateBuffer("main.go", "") -- env.Await(env.DoneWithOpen()) -- -- env.SaveBufferWithoutActions("main.go") -- env.Await(env.DoneWithSave(), env.DoneWithChangeWatchedFiles()) -- -- env.EditBuffer("main.go", fake.NewEdit(0, 0, 0, 0, `package main -- --func main() { --} --`)) -- env.Await(env.DoneWithChange()) -- -- env.SaveBuffer("main.go") -- env.Await(env.DoneWithSave(), env.DoneWithChangeWatchedFiles()) -- -- env.EditBuffer("main.go", fake.NewEdit(0, 0, 4, 0, "")) -- env.Await(env.DoneWithChange()) -- -- env.EditBuffer("main.go", fake.NewEdit(0, 0, 0, 0, `package main - --func main() { -- var x int --} --`)) +- Run(t, files, func(t *testing.T, env *Env) { +- env.CreateBuffer("hello.tmpl", "") - env.AfterChange( -- Diagnostics(env.AtRegexp("main.go", "x")), +- NoDiagnostics(ForFile("hello.tmpl")), // Don't get spurious errors for empty templates. - ) +- env.SetBufferContent("hello.tmpl", "{{range .Planets}}\nHello {{}}\n{{end}}") +- env.Await(Diagnostics(env.AtRegexp("hello.tmpl", "()Hello {{}}"))) +- env.RegexpReplace("hello.tmpl", "{{}}", "{{.}}") +- env.Await(NoDiagnostics(ForFile("hello.tmpl"))) - }) -} - --// Reproduces golang/go#39763. --func TestInvalidPackageName(t *testing.T) { -- const pkgDefault = ` +-func TestClosingTemplatesMakesDiagnosticsDisappear(t *testing.T) { +- const files = ` --- go.mod -- -module mod.com - -go 1.12 ---- main.go -- --package default -- --func main() {} +--- hello.tmpl -- +-{{range .Planets}} +-Hello {{}} <-- missing body +-{{end}} -` -- Run(t, pkgDefault, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") +- +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("hello.tmpl") - env.AfterChange( -- Diagnostics( -- env.AtRegexp("main.go", "default"), -- WithMessage("expected 'IDENT'"), -- ), +- Diagnostics(env.AtRegexp("hello.tmpl", "()Hello {{}}")), +- ) +- // Since we don't have templateExtensions configured, closing hello.tmpl +- // should make its diagnostics disappear. +- env.CloseBuffer("hello.tmpl") +- env.AfterChange( +- NoDiagnostics(ForFile("hello.tmpl")), - ) - }) -} - --// This tests the functionality of the "limitWorkspaceScope" --func TestLimitWorkspaceScope(t *testing.T) { -- const mod = ` +-func TestMultipleSuffixes(t *testing.T) { +- const files = ` --- go.mod -- -module mod.com - -go 1.12 ---- a/main.go -- --package main -- --func main() {} ---- main.go -- --package main -- --func main() { -- var x int --} +--- b.gotmpl -- +-{{define "A"}}goo{{end}} +--- a.tmpl -- +-{{template "A"}} -` +- - WithOptions( -- WorkspaceFolders("a"), -- ).Run(t, mod, func(t *testing.T, env *Env) { -- env.OpenFile("a/main.go") -- env.AfterChange( -- Diagnostics(env.AtRegexp("main.go", "x")), -- ) -- }) -- WithOptions( -- WorkspaceFolders("a"), -- Settings{"expandWorkspaceToModule": false}, -- ).Run(t, mod, func(t *testing.T, env *Env) { -- env.OpenFile("a/main.go") -- env.AfterChange( -- NoDiagnostics(ForFile("main.go")), -- ) +- Settings{ +- "templateExtensions": []string{"tmpl", "gotmpl"}, +- }, +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("a.tmpl") +- x := env.RegexpSearch("a.tmpl", `A`) +- loc := env.GoToDefinition(x) +- refs := env.References(loc) +- if len(refs) != 2 { +- t.Fatalf("got %v reference(s), want 2", len(refs)) +- } +- // make sure we got one from b.gotmpl +- want := env.Sandbox.Workdir.URI("b.gotmpl") +- if refs[0].URI != want && refs[1].URI != want { +- t.Errorf("failed to find reference to %s", shorten(want)) +- for i, r := range refs { +- t.Logf("%d: URI:%s %v", i, shorten(r.URI), r.Range) +- } +- } +- +- content, nloc := env.Hover(loc) +- if loc != nloc { +- t.Errorf("loc? got %v, wanted %v", nloc, loc) +- } +- if content.Value != "template A defined" { +- t.Errorf("got %s, wanted 'template A defined", content.Value) +- } - }) -} - --func TestSimplifyCompositeLitDiagnostic(t *testing.T) { +-// shorten long URIs +-func shorten(fn protocol.DocumentURI) string { +- if len(fn) <= 20 { +- return string(fn) +- } +- pieces := strings.Split(string(fn), "/") +- if len(pieces) < 2 { +- return string(fn) +- } +- j := len(pieces) +- return pieces[j-2] + "/" + pieces[j-1] +-} +- +-// Hover needs tests +diff -urN a/gopls/internal/test/integration/watch/setting_test.go b/gopls/internal/test/integration/watch/setting_test.go +--- a/gopls/internal/test/integration/watch/setting_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/watch/setting_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,85 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package watch +- +-import ( +- "fmt" +- "testing" +- +- . "golang.org/x/tools/gopls/internal/test/integration" +-) +- +-func TestSubdirWatchPatterns(t *testing.T) { - const files = ` --- go.mod -- --module mod.com +-module mod.test - --go 1.12 ---- main.go -- --package main +-go 1.18 +--- subdir/subdir.go -- +-package subdir +-` - --import "fmt" +- tests := []struct { +- clientName string +- subdirWatchPatterns string +- wantWatched bool +- }{ +- {"other client", "on", true}, +- {"other client", "off", false}, +- {"other client", "auto", false}, +- {"Visual Studio Code", "auto", true}, +- } - --type t struct { -- msg string +- for _, test := range tests { +- t.Run(fmt.Sprintf("%s_%s", test.clientName, test.subdirWatchPatterns), func(t *testing.T) { +- WithOptions( +- ClientName(test.clientName), +- Settings{ +- "subdirWatchPatterns": test.subdirWatchPatterns, +- }, +- ).Run(t, files, func(t *testing.T, env *Env) { +- var expectation Expectation +- if test.wantWatched { +- expectation = FileWatchMatching("subdir") +- } else { +- expectation = NoFileWatchMatching("subdir") +- } +- env.OnceMet( +- InitialWorkspaceLoad, +- expectation, +- ) +- }) +- }) +- } -} - --func main() { -- x := []t{t{"msg"}} -- fmt.Println(x) +-// This test checks that we surface errors for invalid subdir watch patterns, +-// as the triple of ("off"|"on"|"auto") may be confusing to users inclined to +-// use (true|false) or some other truthy value. +-func TestSubdirWatchPatterns_BadValues(t *testing.T) { +- tests := []struct { +- badValue interface{} +- wantMessage string +- }{ +- {true, "invalid type bool, expect string"}, +- {false, "invalid type bool, expect string"}, +- {"yes", `invalid option "yes"`}, +- } +- +- for _, test := range tests { +- t.Run(fmt.Sprint(test.badValue), func(t *testing.T) { +- WithOptions( +- Settings{ +- "subdirWatchPatterns": test.badValue, +- }, +- ).Run(t, "", func(t *testing.T, env *Env) { +- env.OnceMet( +- InitialWorkspaceLoad, +- ShownMessage(test.wantMessage), +- ) +- }) +- }) +- } -} --` +diff -urN a/gopls/internal/test/integration/watch/watch_test.go b/gopls/internal/test/integration/watch/watch_test.go +--- a/gopls/internal/test/integration/watch/watch_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/watch/watch_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,704 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package watch +- +-import ( +- "testing" +- +- "golang.org/x/tools/gopls/internal/hooks" +- . "golang.org/x/tools/gopls/internal/test/integration" +- "golang.org/x/tools/gopls/internal/util/bug" +- +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/test/integration/fake" +-) - -- WithOptions( -- Settings{"staticcheck": true}, -- ).Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- var d protocol.PublishDiagnosticsParams -- env.AfterChange( -- Diagnostics(env.AtRegexp("main.go", `t{"msg"}`), WithMessage("redundant type")), -- ReadDiagnostics("main.go", &d), -- ) -- if tags := d.Diagnostics[0].Tags; len(tags) == 0 || tags[0] != protocol.Unnecessary { -- t.Errorf("wanted Unnecessary tag on diagnostic, got %v", tags) -- } -- env.ApplyQuickFixes("main.go", d.Diagnostics) -- env.AfterChange(NoDiagnostics(ForFile("main.go"))) -- }) +-func TestMain(m *testing.M) { +- bug.PanicOnBugs = true +- Main(m, hooks.Options) -} - --// Test some secondary diagnostics --func TestSecondaryDiagnostics(t *testing.T) { -- const dir = ` +-func TestEditFile(t *testing.T) { +- const pkg = ` --- go.mod -- -module mod.com - --go 1.12 ---- main.go -- --package main --func main() { -- panic("not here") +-go 1.14 +--- a/a.go -- +-package a +- +-func _() { +- var x int -} ---- other.go -- --package main --func main() {} -` -- Run(t, dir, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- env.OpenFile("other.go") -- var mainDiags, otherDiags protocol.PublishDiagnosticsParams -- env.AfterChange( -- ReadDiagnostics("main.go", &mainDiags), -- ReadDiagnostics("other.go", &otherDiags), -- ) -- if len(mainDiags.Diagnostics) != 1 { -- t.Fatalf("main.go, got %d diagnostics, expected 1", len(mainDiags.Diagnostics)) -- } -- keep := mainDiags.Diagnostics[0] -- if len(otherDiags.Diagnostics) != 1 { -- t.Fatalf("other.go: got %d diagnostics, expected 1", len(otherDiags.Diagnostics)) -- } -- if len(otherDiags.Diagnostics[0].RelatedInformation) != 1 { -- t.Fatalf("got %d RelatedInformations, expected 1", len(otherDiags.Diagnostics[0].RelatedInformation)) -- } -- // check that the RelatedInformation matches the error from main.go -- c := otherDiags.Diagnostics[0].RelatedInformation[0] -- if c.Location.Range != keep.Range { -- t.Errorf("locations don't match. Got %v expected %v", c.Location.Range, keep.Range) -- } +- // Edit the file when it's *not open* in the workspace, and check that +- // diagnostics are updated. +- t.Run("unopened", func(t *testing.T) { +- Run(t, pkg, func(t *testing.T, env *Env) { +- env.OnceMet( +- InitialWorkspaceLoad, +- Diagnostics(env.AtRegexp("a/a.go", "x")), +- ) +- env.WriteWorkspaceFile("a/a.go", `package a; func _() {};`) +- env.AfterChange( +- NoDiagnostics(ForFile("a/a.go")), +- ) +- }) +- }) +- +- // Edit the file when it *is open* in the workspace, and check that +- // diagnostics are *not* updated. +- t.Run("opened", func(t *testing.T) { +- Run(t, pkg, func(t *testing.T, env *Env) { +- env.OpenFile("a/a.go") +- // Insert a trivial edit so that we don't automatically update the buffer +- // (see CL 267577). +- env.EditBuffer("a/a.go", fake.NewEdit(0, 0, 0, 0, " ")) +- env.AfterChange() +- env.WriteWorkspaceFile("a/a.go", `package a; func _() {};`) +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/a.go", "x")), +- ) +- }) - }) -} - --func TestOrphanedFiles(t *testing.T) { -- const files = ` +-// Edit a dependency on disk and expect a new diagnostic. +-func TestEditDependency(t *testing.T) { +- const pkg = ` --- go.mod -- -module mod.com - --go 1.12 +-go 1.14 +--- b/b.go -- +-package b +- +-func B() int { return 0 } --- a/a.go -- -package a - --func main() { -- var x int --} ---- a/a_exclude.go -- --// +build exclude -- --package a +-import ( +- "mod.com/b" +-) - -func _() { -- var x int +- _ = b.B() -} -` -- Run(t, files, func(t *testing.T, env *Env) { +- Run(t, pkg, func(t *testing.T, env *Env) { - env.OpenFile("a/a.go") +- env.AfterChange() +- env.WriteWorkspaceFile("b/b.go", `package b; func B() {};`) - env.AfterChange( -- Diagnostics(env.AtRegexp("a/a.go", "x")), +- Diagnostics(env.AtRegexp("a/a.go", "b.B")), - ) -- env.OpenFile("a/a_exclude.go") -- -- loadOnce := LogMatching(protocol.Info, "query=.*file=.*a_exclude.go", 1, false) -- -- // can't use OnceMet or AfterChange as logs are async -- env.Await(loadOnce) -- // ...but ensure that the change has been fully processed before editing. -- // Otherwise, there may be a race where the snapshot is cloned before all -- // state changes resulting from the load have been processed -- // (golang/go#61521). -- env.AfterChange() -- -- // Check that orphaned files are not reloaded, by making a change in -- // a.go file and confirming that the workspace diagnosis did not reload -- // a_exclude.go. -- // -- // This is racy (but fails open) because logs are asynchronous to other LSP -- // operations. There's a chance gopls _did_ log, and we just haven't seen -- // it yet. -- env.RegexpReplace("a/a.go", "package a", "package a // arbitrary comment") -- env.AfterChange(loadOnce) - }) -} - --func TestEnableAllExperiments(t *testing.T) { -- // Before the oldest supported Go version, gopls sends a warning to upgrade -- // Go, which fails the expectation below. -- testenv.NeedsGo1Point(t, lsp.OldestSupportedGoVersion()) -- -- const mod = ` +-// Edit both the current file and one of its dependencies on disk and +-// expect diagnostic changes. +-func TestEditFileAndDependency(t *testing.T) { +- const pkg = ` --- go.mod -- -module mod.com - --go 1.12 ---- main.go -- --package main +-go 1.14 +--- b/b.go -- +-package b - --import "bytes" +-func B() int { return 0 } +--- a/a.go -- +-package a - --func b(c bytes.Buffer) { -- _ = 1 +-import ( +- "mod.com/b" +-) +- +-func _() { +- var x int +- _ = b.B() -} -` -- WithOptions( -- Settings{"allExperiments": true}, -- ).Run(t, mod, func(t *testing.T, env *Env) { -- // Confirm that the setting doesn't cause any warnings. +- Run(t, pkg, func(t *testing.T, env *Env) { - env.OnceMet( - InitialWorkspaceLoad, -- NoShownMessage(""), // empty substring to match any message +- Diagnostics(env.AtRegexp("a/a.go", "x")), - ) -- }) --} +- env.WriteWorkspaceFiles(map[string]string{ +- "b/b.go": `package b; func B() {};`, +- "a/a.go": `package a - --func TestSwig(t *testing.T) { -- // This is fixed in Go 1.17, but not earlier. -- testenv.NeedsGo1Point(t, 17) +-import "mod.com/b" - -- if _, err := exec.LookPath("swig"); err != nil { -- t.Skip("skipping test: swig not available") -- } -- if _, err := exec.LookPath("g++"); err != nil { -- t.Skip("skipping test: g++ not available") -- } +-func _() { +- b.B() +-}`, +- }) +- env.AfterChange( +- NoDiagnostics(ForFile("a/a.go")), +- NoDiagnostics(ForFile("b/b.go")), +- ) +- }) +-} - -- const mod = ` +-// Delete a dependency and expect a new diagnostic. +-func TestDeleteDependency(t *testing.T) { +- const pkg = ` --- go.mod -- -module mod.com - --go 1.12 ---- pkg/simple/export_swig.go -- --package simple -- --func ExportSimple(x, y int) int { -- return Gcd(x, y) --} ---- pkg/simple/simple.swigcxx -- --%module simple +-go 1.14 +--- b/b.go -- +-package b - --%inline %{ --extern int gcd(int x, int y) --{ -- int g; -- g = y; -- while (x > 0) { -- g = x; -- x = y % x; -- y = g; -- } -- return g; --} --%} ---- main.go -- +-func B() int { return 0 } +--- a/a.go -- -package a - --func main() { -- var x int +-import ( +- "mod.com/b" +-) +- +-func _() { +- _ = b.B() -} -` -- Run(t, mod, func(t *testing.T, env *Env) { -- env.OnceMet( -- InitialWorkspaceLoad, -- NoDiagnostics(WithMessage("illegal character U+0023 '#'")), +- Run(t, pkg, func(t *testing.T, env *Env) { +- env.OpenFile("a/a.go") +- env.AfterChange() +- env.RemoveWorkspaceFile("b/b.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/a.go", "\"mod.com/b\"")), - ) - }) -} - --// When foo_test.go is opened, gopls will object to the borked package name. --// This test asserts that when the package name is fixed, gopls will soon after --// have no more complaints about it. --// https://github.com/golang/go/issues/41061 --func TestRenamePackage(t *testing.T) { -- const proxy = ` ---- example.com@v1.2.3/go.mod -- --module example.com -- --go 1.12 ---- example.com@v1.2.3/blah/blah.go -- --package blah -- --const Name = "Blah" ---- random.org@v1.2.3/go.mod -- --module random.org -- --go 1.12 ---- random.org@v1.2.3/blah/blah.go -- --package hello -- --const Name = "Hello" --` -- -- const contents = ` +-// Create a dependency on disk and expect the diagnostic to go away. +-func TestCreateDependency(t *testing.T) { +- const missing = ` --- go.mod -- -module mod.com - --go 1.12 ---- main.go -- --package main +-go 1.14 +--- b/b.go -- +-package b - --import "example.com/blah" +-func B() int { return 0 } +--- a/a.go -- +-package a - --func main() { -- blah.Hello() +-import ( +- "mod.com/c" +-) +- +-func _() { +- c.C() -} ---- bob.go -- --package main ---- foo/foo.go -- --package foo ---- foo/foo_test.go -- --package foo_ -` -- -- WithOptions( -- ProxyFiles(proxy), -- InGOPATH(), -- EnvVars{"GO111MODULE": "off"}, -- ).Run(t, contents, func(t *testing.T, env *Env) { -- // Simulate typing character by character. -- env.OpenFile("foo/foo_test.go") -- env.Await(env.DoneWithOpen()) -- env.RegexpReplace("foo/foo_test.go", "_", "_t") -- env.Await(env.DoneWithChange()) -- env.RegexpReplace("foo/foo_test.go", "_t", "_test") +- Run(t, missing, func(t *testing.T, env *Env) { +- env.OnceMet( +- InitialWorkspaceLoad, +- Diagnostics(env.AtRegexp("a/a.go", "\"mod.com/c\"")), +- ) +- env.WriteWorkspaceFile("c/c.go", `package c; func C() {};`) - env.AfterChange( -- NoDiagnostics(ForFile("foo/foo_test.go")), -- NoOutstandingWork(IgnoreTelemetryPromptWork), +- NoDiagnostics(ForFile("a/a.go")), - ) - }) -} - --// TestProgressBarErrors confirms that critical workspace load errors are shown --// and updated via progress reports. --func TestProgressBarErrors(t *testing.T) { -- const pkg = ` +-// Create a new dependency and add it to the file on disk. +-// This is similar to what might happen if you switch branches. +-func TestCreateAndAddDependency(t *testing.T) { +- const original = ` --- go.mod -- --modul mod.com +-module mod.com - --go 1.12 ---- main.go -- --package main --` -- Run(t, pkg, func(t *testing.T, env *Env) { -- env.OpenFile("go.mod") -- env.AfterChange( -- OutstandingWork(lsp.WorkspaceLoadFailure, "unknown directive"), -- ) -- env.EditBuffer("go.mod", fake.NewEdit(0, 0, 3, 0, `module mod.com +-go 1.14 +--- a/a.go -- +-package a - --go 1.hello --`)) -- // As of golang/go#42529, go.mod changes do not reload the workspace until -- // they are saved. -- env.SaveBufferWithoutActions("go.mod") -- env.AfterChange( -- OutstandingWork(lsp.WorkspaceLoadFailure, "invalid go version"), -- ) -- env.RegexpReplace("go.mod", "go 1.hello", "go 1.12") -- env.SaveBufferWithoutActions("go.mod") +-func _() {} +-` +- Run(t, original, func(t *testing.T, env *Env) { +- env.WriteWorkspaceFile("c/c.go", `package c; func C() {};`) +- env.WriteWorkspaceFile("a/a.go", `package a; import "mod.com/c"; func _() { c.C() }`) - env.AfterChange( -- NoOutstandingWork(IgnoreTelemetryPromptWork), +- NoDiagnostics(ForFile("a/a.go")), - ) - }) -} - --func TestDeleteDirectory(t *testing.T) { -- const mod = ` ---- bob/bob.go -- --package bob -- --func Hello() { -- var x int --} +-// Create a new file that defines a new symbol, in the same package. +-func TestCreateFile(t *testing.T) { +- const pkg = ` --- go.mod -- -module mod.com ---- cmd/main.go -- --package main - --import "mod.com/bob" +-go 1.14 +--- a/a.go -- +-package a - --func main() { -- bob.Hello() +-func _() { +- hello() -} -` -- WithOptions( -- Settings{ -- // Now that we don't watch subdirs by default (except for VS Code), -- // we must explicitly ask gopls to requests subdir watch patterns. -- "subdirWatchPatterns": "on", -- }, -- ).Run(t, mod, func(t *testing.T, env *Env) { +- Run(t, pkg, func(t *testing.T, env *Env) { - env.OnceMet( - InitialWorkspaceLoad, -- FileWatchMatching("bob"), +- Diagnostics(env.AtRegexp("a/a.go", "hello")), - ) -- env.RemoveWorkspaceFile("bob") +- env.WriteWorkspaceFile("a/a2.go", `package a; func hello() {};`) - env.AfterChange( -- Diagnostics(env.AtRegexp("cmd/main.go", `"mod.com/bob"`)), -- NoDiagnostics(ForFile("bob/bob.go")), -- NoFileWatchMatching("bob"), +- NoDiagnostics(ForFile("a/a.go")), - ) - }) -} - --// Confirms that circular imports are tested and reported. --func TestCircularImports(t *testing.T) { -- const mod = ` +-// Add a new method to an interface and implement it. +-// Inspired by the structure of internal/golang and internal/cache. +-func TestCreateImplementation(t *testing.T) { +- const pkg = ` --- go.mod -- -module mod.com - --go 1.12 ---- self/self.go -- --package self -- --import _ "mod.com/self" --func Hello() {} ---- double/a/a.go -- --package a -- --import _ "mod.com/double/b" ---- double/b/b.go -- +-go 1.14 +--- b/b.go -- -package b - --import _ "mod.com/double/a" ---- triple/a/a.go -- +-type B interface{ +- Hello() string +-} +- +-func SayHello(bee B) { +- println(bee.Hello()) +-} +--- a/a.go -- -package a - --import _ "mod.com/triple/b" ---- triple/b/b.go -- --package b +-import "mod.com/b" - --import _ "mod.com/triple/c" ---- triple/c/c.go -- --package c +-type X struct {} - --import _ "mod.com/triple/a" +-func (_ X) Hello() string { +- return "" +-} +- +-func _() { +- x := X{} +- b.SayHello(x) +-} -` -- Run(t, mod, func(t *testing.T, env *Env) { -- env.OnceMet( -- InitialWorkspaceLoad, -- Diagnostics(env.AtRegexp("self/self.go", `_ "mod.com/self"`), WithMessage("import cycle not allowed")), -- Diagnostics(env.AtRegexp("double/a/a.go", `_ "mod.com/double/b"`), WithMessage("import cycle not allowed")), -- Diagnostics(env.AtRegexp("triple/a/a.go", `_ "mod.com/triple/b"`), WithMessage("import cycle not allowed")), -- ) -- }) +- const newMethod = `package b +-type B interface{ +- Hello() string +- Bye() string -} - --// Tests golang/go#46667: deleting a problematic import path should resolve --// import cycle errors. --func TestResolveImportCycle(t *testing.T) { -- const mod = ` ---- go.mod -- --module mod.test +-func SayHello(bee B) { +- println(bee.Hello()) +-}` +- const implementation = `package a - --go 1.16 ---- a/a.go -- --package a +-import "mod.com/b" - --import "mod.test/b" +-type X struct {} - --const A = b.A --const B = 2 ---- b/b.go -- --package b +-func (_ X) Hello() string { +- return "" +-} - --import "mod.test/a" +-func (_ X) Bye() string { +- return "" +-} - --const A = 1 --const B = a.B -- ` -- Run(t, mod, func(t *testing.T, env *Env) { -- env.OpenFile("a/a.go") -- env.OpenFile("b/b.go") -- env.AfterChange( -- // The Go command sometimes tells us about only one of the import cycle -- // errors below. For robustness of this test, succeed if we get either. -- // -- // TODO(golang/go#52904): we should get *both* of these errors. -- AnyOf( -- Diagnostics(env.AtRegexp("a/a.go", `"mod.test/b"`), WithMessage("import cycle")), -- Diagnostics(env.AtRegexp("b/b.go", `"mod.test/a"`), WithMessage("import cycle")), -- ), -- ) -- env.RegexpReplace("b/b.go", `const B = a\.B`, "") -- env.SaveBuffer("b/b.go") -- env.AfterChange( -- NoDiagnostics(ForFile("a/a.go")), -- NoDiagnostics(ForFile("b/b.go")), -- ) +-func _() { +- x := X{} +- b.SayHello(x) +-}` +- +- // Add the new method before the implementation. Expect diagnostics. +- t.Run("method before implementation", func(t *testing.T) { +- Run(t, pkg, func(t *testing.T, env *Env) { +- env.WriteWorkspaceFile("b/b.go", newMethod) +- env.AfterChange( +- Diagnostics(AtPosition("a/a.go", 12, 12)), +- ) +- env.WriteWorkspaceFile("a/a.go", implementation) +- env.AfterChange( +- NoDiagnostics(ForFile("a/a.go")), +- ) +- }) +- }) +- // Add the new implementation before the new method. Expect no diagnostics. +- t.Run("implementation before method", func(t *testing.T) { +- Run(t, pkg, func(t *testing.T, env *Env) { +- env.WriteWorkspaceFile("a/a.go", implementation) +- env.AfterChange( +- NoDiagnostics(ForFile("a/a.go")), +- ) +- env.WriteWorkspaceFile("b/b.go", newMethod) +- env.AfterChange( +- NoDiagnostics(ForFile("a/a.go")), +- ) +- }) +- }) +- // Add both simultaneously. Expect no diagnostics. +- t.Run("implementation and method simultaneously", func(t *testing.T) { +- Run(t, pkg, func(t *testing.T, env *Env) { +- env.WriteWorkspaceFiles(map[string]string{ +- "a/a.go": implementation, +- "b/b.go": newMethod, +- }) +- env.AfterChange( +- NoDiagnostics(ForFile("a/a.go")), +- NoDiagnostics(ForFile("b/b.go")), +- ) +- }) - }) -} - --func TestBadImport(t *testing.T) { -- const mod = ` +-// Tests golang/go#38498. Delete a file and then force a reload. +-// Assert that we no longer try to load the file. +-func TestDeleteFiles(t *testing.T) { +- const pkg = ` --- go.mod -- -module mod.com - --go 1.12 ---- main.go -- --package main +-go 1.14 +--- a/a.go -- +-package a - --import ( -- _ "nosuchpkg" --) +-func _() { +- var _ int +-} +--- a/a_unneeded.go -- +-package a -` -- t.Run("module", func(t *testing.T) { -- Run(t, mod, func(t *testing.T, env *Env) { -- env.OnceMet( -- InitialWorkspaceLoad, -- Diagnostics(env.AtRegexp("main.go", `"nosuchpkg"`), WithMessage(`could not import nosuchpkg (no required module provides package "nosuchpkg"`)), +- t.Run("close then delete", func(t *testing.T) { +- WithOptions( +- Settings{"verboseOutput": true}, +- ).Run(t, pkg, func(t *testing.T, env *Env) { +- env.OpenFile("a/a.go") +- env.OpenFile("a/a_unneeded.go") +- env.Await( +- // Log messages are asynchronous to other events on the LSP stream, so we +- // can't use OnceMet or AfterChange here. +- LogMatching(protocol.Info, "a_unneeded.go", 1, false), +- ) +- +- // Close and delete the open file, mimicking what an editor would do. +- env.CloseBuffer("a/a_unneeded.go") +- env.RemoveWorkspaceFile("a/a_unneeded.go") +- env.RegexpReplace("a/a.go", "var _ int", "fmt.Println(\"\")") +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/a.go", "fmt")), +- ) +- env.SaveBuffer("a/a.go") +- env.Await( +- // There should only be one log message containing +- // a_unneeded.go, from the initial workspace load, which we +- // check for earlier. If there are more, there's a bug. +- LogMatching(protocol.Info, "a_unneeded.go", 1, false), +- NoDiagnostics(ForFile("a/a.go")), - ) - }) - }) -- t.Run("GOPATH", func(t *testing.T) { +- +- t.Run("delete then close", func(t *testing.T) { - WithOptions( -- InGOPATH(), -- EnvVars{"GO111MODULE": "off"}, -- Modes(Default), -- ).Run(t, mod, func(t *testing.T, env *Env) { -- env.OnceMet( -- InitialWorkspaceLoad, -- Diagnostics(env.AtRegexp("main.go", `"nosuchpkg"`), WithMessage(`cannot find package "nosuchpkg"`)), +- Settings{"verboseOutput": true}, +- ).Run(t, pkg, func(t *testing.T, env *Env) { +- env.OpenFile("a/a.go") +- env.OpenFile("a/a_unneeded.go") +- env.Await( +- LogMatching(protocol.Info, "a_unneeded.go", 1, false), +- ) +- +- // Delete and then close the file. +- env.RemoveWorkspaceFile("a/a_unneeded.go") +- env.CloseBuffer("a/a_unneeded.go") +- env.RegexpReplace("a/a.go", "var _ int", "fmt.Println(\"\")") +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/a.go", "fmt")), +- ) +- env.SaveBuffer("a/a.go") +- env.Await( +- // There should only be one log message containing +- // a_unneeded.go, from the initial workspace load, which we +- // check for earlier. If there are more, there's a bug. +- LogMatching(protocol.Info, "a_unneeded.go", 1, false), +- NoDiagnostics(ForFile("a/a.go")), - ) - }) - }) -} - --func TestNestedModules(t *testing.T) { -- const proxy = ` ---- nested.com@v1.0.0/go.mod -- --module nested.com -- --go 1.12 ---- nested.com@v1.0.0/hello/hello.go -- --package hello -- --func Hello() {} --` -- -- const nested = ` +-// This change reproduces the behavior of switching branches, with multiple +-// files being created and deleted. The key change here is the movement of a +-// symbol from one file to another in a given package through a deletion and +-// creation. To reproduce an issue with metadata invalidation in batched +-// changes, the last change in the batch is an on-disk file change that doesn't +-// require metadata invalidation. +-func TestMoveSymbol(t *testing.T) { +- const pkg = ` --- go.mod -- -module mod.com - --go 1.12 -- --require nested.com v1.0.0 ---- go.sum -- --nested.com v1.0.0 h1:I6spLE4CgFqMdBPc+wTV2asDO2QJ3tU0YAT+jkLeN1I= --nested.com v1.0.0/go.mod h1:ly53UzXQgVjSlV7wicdBB4p8BxfytuGT1Xcyv0ReJfI= +-go 1.14 --- main.go -- -package main - --import "nested.com/hello" +-import "mod.com/a" - -func main() { -- hello.Hello() +- var x int +- x = a.Hello +- println(x) -} ---- nested/go.mod -- --module nested.com -- ---- nested/hello/hello.go -- --package hello +--- a/a1.go -- +-package a - --func Hello() { -- helloHelper() --} ---- nested/hello/hello_helper.go -- --package hello +-var Hello int +--- a/a2.go -- +-package a - --func helloHelper() {} +-func _() {} -` -- WithOptions( -- ProxyFiles(proxy), -- Modes(Default), -- ).Run(t, nested, func(t *testing.T, env *Env) { -- // Expect a diagnostic in a nested module. -- env.OpenFile("nested/hello/hello.go") +- Run(t, pkg, func(t *testing.T, env *Env) { +- env.WriteWorkspaceFile("a/a3.go", "package a\n\nvar Hello int\n") +- env.RemoveWorkspaceFile("a/a1.go") +- env.WriteWorkspaceFile("a/a2.go", "package a; func _() {};") - env.AfterChange( -- Diagnostics(env.AtRegexp("nested/hello/hello.go", "helloHelper")), -- Diagnostics(env.AtRegexp("nested/hello/hello.go", "package (hello)"), WithMessage("not included in your workspace")), +- NoDiagnostics(ForFile("main.go")), - ) - }) -} - --func TestAdHocPackagesReloading(t *testing.T) { -- const nomod = ` ---- main.go -- --package main +-// Reproduce golang/go#40456. +-func TestChangeVersion(t *testing.T) { +- const proxy = ` +--- example.com@v1.2.3/go.mod -- +-module example.com - --func main() {} --` -- Run(t, nomod, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- env.RegexpReplace("main.go", "{}", "{ var x int; }") // simulate typing -- env.AfterChange(NoLogMatching(protocol.Info, "packages=1")) -- }) --} +-go 1.12 +--- example.com@v1.2.3/blah/blah.go -- +-package blah - --func TestBuildTagChange(t *testing.T) { -- const files = ` ---- go.mod -- --module mod.com +-const Name = "Blah" +- +-func X(x int) {} +--- example.com@v1.2.2/go.mod -- +-module example.com - -go 1.12 ---- foo.go -- --// decoy comment --// +build hidden --// decoy comment +--- example.com@v1.2.2/blah/blah.go -- +-package blah - --package foo --var Foo = 1 ---- bar.go -- --package foo --var Bar = Foo --` +-const Name = "Blah" - -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("foo.go") -- env.AfterChange(Diagnostics(env.AtRegexp("bar.go", `Foo`))) -- env.RegexpReplace("foo.go", `\+build`, "") -- env.AfterChange(NoDiagnostics(ForFile("bar.go"))) -- }) +-func X() {} +--- random.org@v1.2.3/go.mod -- +-module random.org - --} +-go 1.12 +--- random.org@v1.2.3/blah/blah.go -- +-package hello - --func TestIssue44736(t *testing.T) { -- const files = ` -- -- go.mod -- --module blah.com +-const Name = "Hello" +-` +- const mod = ` +--- go.mod -- +-module mod.com - --go 1.16 +-go 1.12 +- +-require example.com v1.2.2 +--- go.sum -- +-example.com v1.2.3 h1:OnPPkx+rW63kj9pgILsu12MORKhSlnFa3DVRJq1HZ7g= +-example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= --- main.go -- -package main - --import "fmt" +-import "example.com/blah" - -func main() { -- asdf -- fmt.Printf("This is a test %v") -- fdas +- blah.X() -} ---- other.go -- --package main -- -` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- env.OpenFile("other.go") -- env.AfterChange( -- Diagnostics(env.AtRegexp("main.go", "asdf")), -- Diagnostics(env.AtRegexp("main.go", "fdas")), -- ) -- env.SetBufferContent("other.go", "package main\n\nasdf") -- // The new diagnostic in other.go should not suppress diagnostics in main.go. +- WithOptions(ProxyFiles(proxy)).Run(t, mod, func(t *testing.T, env *Env) { +- env.WriteWorkspaceFiles(map[string]string{ +- "go.mod": `module mod.com +- +-go 1.12 +- +-require example.com v1.2.3 +-`, +- "main.go": `package main +- +-import ( +- "example.com/blah" +-) +- +-func main() { +- blah.X(1) +-} +-`, +- }) - env.AfterChange( -- Diagnostics(env.AtRegexp("other.go", "asdf"), WithMessage("expected declaration")), -- Diagnostics(env.AtRegexp("main.go", "asdf")), +- env.DoneWithChangeWatchedFiles(), +- NoDiagnostics(ForFile("main.go")), - ) - }) -} - --func TestInitialization(t *testing.T) { +-// Reproduces golang/go#40340. +-func TestSwitchFromGOPATHToModuleMode(t *testing.T) { - const files = ` ---- go.mod -- --module mod.com +--- foo/blah/blah.go -- +-package blah - --go 1.16 +-const Name = "" --- main.go -- -package main +- +-import "foo/blah" +- +-func main() { +- _ = blah.Name +-} -` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("go.mod") -- env.Await(env.DoneWithOpen()) -- env.RegexpReplace("go.mod", "module", "modul") -- env.SaveBufferWithoutActions("go.mod") +- WithOptions( +- InGOPATH(), +- Modes(Default), // golang/go#57521: this test is temporarily failing in 'experimental' mode +- EnvVars{"GO111MODULE": "auto"}, +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") - env.AfterChange( -- NoLogMatching(protocol.Error, "initial workspace load failed"), +- NoDiagnostics(ForFile("main.go")), - ) -- }) --} -- --// This test confirms that the view does not reinitialize when a go.mod file is --// opened. --func TestNoReinitialize(t *testing.T) { -- const files = ` ---- go.mod -- --module mod.com +- if err := env.Sandbox.RunGoCommand(env.Ctx, "", "mod", []string{"init", "mod.com"}, nil, true); err != nil { +- t.Fatal(err) +- } - --go 1.12 ---- main.go -- --package main +- // TODO(golang/go#57558, golang/go#57512): file watching is asynchronous, +- // and we must wait for the view to be reconstructed before touching +- // main.go, so that the new view "knows" about main.go. This is a bug, but +- // awaiting the change here avoids it. +- env.AfterChange() - --func main() {} --` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("go.mod") -- env.Await( -- // Check that we have only loaded "<dir>/..." once. -- // Log messages are asynchronous to other events on the LSP stream, so we -- // can't use OnceMet or AfterChange here. -- LogMatching(protocol.Info, `.*query=.*\.\.\..*`, 1, false), +- env.RegexpReplace("main.go", `"foo/blah"`, `"mod.com/foo/blah"`) +- env.AfterChange( +- NoDiagnostics(ForFile("main.go")), - ) - }) -} - --func TestLangVersion(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) // Requires types.Config.GoVersion, new in 1.18. +-// Reproduces golang/go#40487. +-func TestSwitchFromModulesToGOPATH(t *testing.T) { - const files = ` ---- go.mod -- +--- foo/go.mod -- -module mod.com - --go 1.12 ---- main.go -- +-go 1.14 +--- foo/blah/blah.go -- +-package blah +- +-const Name = "" +--- foo/main.go -- -package main - --const C = 0b10 +-import "mod.com/blah" +- +-func main() { +- _ = blah.Name +-} -` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OnceMet( -- InitialWorkspaceLoad, -- Diagnostics(env.AtRegexp("main.go", `0b10`), WithMessage("go1.13 or later")), +- WithOptions( +- InGOPATH(), +- EnvVars{"GO111MODULE": "auto"}, +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("foo/main.go") +- env.RemoveWorkspaceFile("foo/go.mod") +- env.AfterChange( +- Diagnostics(env.AtRegexp("foo/main.go", `"mod.com/blah"`)), - ) -- env.WriteWorkspaceFile("go.mod", "module mod.com \n\ngo 1.13\n") +- env.RegexpReplace("foo/main.go", `"mod.com/blah"`, `"foo/blah"`) - env.AfterChange( -- NoDiagnostics(ForFile("main.go")), +- NoDiagnostics(ForFile("foo/main.go")), - ) - }) -} - --func TestNoQuickFixForUndeclaredConstraint(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) +-func TestNewSymbolInTestVariant(t *testing.T) { - const files = ` --- go.mod -- -module mod.com - --go 1.18 ---- main.go -- --package main +-go 1.12 +--- a/a.go -- +-package a - --func F[T C](_ T) { +-func bob() {} +--- a/a_test.go -- +-package a +- +-import "testing" +- +-func TestBob(t *testing.T) { +- bob() -} -` -- - Run(t, files, func(t *testing.T, env *Env) { -- var d protocol.PublishDiagnosticsParams -- env.OnceMet( -- InitialWorkspaceLoad, -- Diagnostics(env.AtRegexp("main.go", `C`)), -- ReadDiagnostics("main.go", &d), -- ) -- if fixes := env.GetQuickFixes("main.go", d.Diagnostics); len(fixes) != 0 { -- t.Errorf("got quick fixes %v, wanted none", fixes) -- } -- }) --} +- // Add a new symbol to the package under test and use it in the test +- // variant. Expect no diagnostics. +- env.WriteWorkspaceFiles(map[string]string{ +- "a/a.go": `package a - --func TestEditGoDirective(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) -- const files = ` ---- go.mod -- --module mod.com +-func bob() {} +-func george() {} +-`, +- "a/a_test.go": `package a - --go 1.16 ---- main.go -- --package main +-import "testing" - --func F[T any](_ T) { +-func TestAll(t *testing.T) { +- bob() +- george() -} --` -- Run(t, files, func(_ *testing.T, env *Env) { // Create a new workspace-level directory and empty file. -- var d protocol.PublishDiagnosticsParams -- env.OnceMet( -- InitialWorkspaceLoad, -- Diagnostics(env.AtRegexp("main.go", `T any`), WithMessage("type parameter")), -- ReadDiagnostics("main.go", &d), -- ) -- -- env.ApplyQuickFixes("main.go", d.Diagnostics) +-`, +- }) - env.AfterChange( -- NoDiagnostics(ForFile("main.go")), +- NoDiagnostics(ForFile("a/a.go")), +- NoDiagnostics(ForFile("a/a_test.go")), - ) -- }) --} -- --func TestEditGoDirectiveWorkspace(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) -- const files = ` ---- go.mod -- --module mod.com -- --go 1.16 ---- go.work -- --go 1.18 +- // Now, add a new file to the test variant and use its symbol in the +- // original test file. Expect no diagnostics. +- env.WriteWorkspaceFiles(map[string]string{ +- "a/a_test.go": `package a - --use . ---- main.go -- --package main +-import "testing" - --func F[T any](_ T) { +-func TestAll(t *testing.T) { +- bob() +- george() +- hi() -} --` -- Run(t, files, func(_ *testing.T, env *Env) { // Create a new workspace-level directory and empty file. -- var d protocol.PublishDiagnosticsParams +-`, +- "a/a2_test.go": `package a - -- // We should have a diagnostic because generics are not supported at 1.16. -- env.OnceMet( -- InitialWorkspaceLoad, -- Diagnostics(env.AtRegexp("main.go", `T any`), WithMessage("type parameter")), -- ReadDiagnostics("main.go", &d), -- ) +-import "testing" - -- // This diagnostic should have a quick fix to edit the go version. -- env.ApplyQuickFixes("main.go", d.Diagnostics) +-func hi() {} - -- // Once the edit is applied, the problematic diagnostics should be -- // resolved. +-func TestSomething(t *testing.T) {} +-`, +- }) - env.AfterChange( -- NoDiagnostics(ForFile("main.go")), +- NoDiagnostics(ForFile("a/a_test.go")), +- NoDiagnostics(ForFile("a/a2_test.go")), - ) - }) -} +diff -urN a/gopls/internal/test/integration/workspace/adhoc_test.go b/gopls/internal/test/integration/workspace/adhoc_test.go +--- a/gopls/internal/test/integration/workspace/adhoc_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/workspace/adhoc_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,39 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package workspace +- +-import ( +- "testing" +- +- . "golang.org/x/tools/gopls/internal/test/integration" +-) +- +-// Test for golang/go#57209: editing a file in an ad-hoc package should not +-// trigger conflicting diagnostics. +-func TestAdhoc_Edits(t *testing.T) { +- const files = ` +--- a.go -- +-package foo +- +-const X = 1 - --// This test demonstrates that analysis facts are correctly propagated --// across packages. --func TestInterpackageAnalysis(t *testing.T) { -- const src = ` ---- go.mod -- --module example.com ---- a/a.go -- --package a +--- b.go -- +-package foo - --import "example.com/b" +-// import "errors" - --func _() { -- new(b.B).Printf("%d", "s") // printf error +-const Y = X +-` +- +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("b.go") +- +- for i := 0; i < 10; i++ { +- env.RegexpReplace("b.go", `// import "errors"`, `import "errors"`) +- env.RegexpReplace("b.go", `import "errors"`, `// import "errors"`) +- env.AfterChange(NoDiagnostics()) +- } +- }) -} +diff -urN a/gopls/internal/test/integration/workspace/broken_test.go b/gopls/internal/test/integration/workspace/broken_test.go +--- a/gopls/internal/test/integration/workspace/broken_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/workspace/broken_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,265 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - ---- b/b.go -- --package b +-package workspace - --import "example.com/c" +-import ( +- "strings" +- "testing" - --type B struct{} +- "golang.org/x/tools/gopls/internal/server" +- . "golang.org/x/tools/gopls/internal/test/integration" +- "golang.org/x/tools/internal/testenv" +-) - --func (B) Printf(format string, args ...interface{}) { -- c.MyPrintf(format, args...) --} +-// This file holds various tests for UX with respect to broken workspaces. +-// +-// TODO: consolidate other tests here. +-// +-// TODO: write more tests: +-// - an explicit GOWORK value that doesn't exist +-// - using modules and/or GOWORK inside of GOPATH? - ---- c/c.go -- --package c +-// Test for golang/go#53933 +-func TestBrokenWorkspace_DuplicateModules(t *testing.T) { +- // The go command error message was improved in Go 1.20 to mention multiple +- // modules. +- testenv.NeedsGo1Point(t, 20) - --import "fmt" +- // This proxy module content is replaced by the workspace, but is still +- // required for module resolution to function in the Go command. +- const proxy = ` +--- example.com/foo@v0.0.1/go.mod -- +-module example.com/foo - --func MyPrintf(format string, args ...interface{}) { -- fmt.Printf(format, args...) --} +-go 1.12 -` -- Run(t, src, func(t *testing.T, env *Env) { -- env.OpenFile("a/a.go") -- env.AfterChange( -- Diagnostics( -- env.AtRegexp("a/a.go", "new.*Printf"), -- WithMessage("format %d has arg \"s\" of wrong type string"), -- ), -- ) -- }) --} - --// This test ensures that only Analyzers with RunDespiteErrors=true --// are invoked on a package that would not compile, even if the errors --// are distant and localized. --func TestErrorsThatPreventAnalysis(t *testing.T) { - const src = ` ---- go.mod -- --module example.com ---- a/a.go -- --package a +--- go.work -- +-go 1.18 - --import "fmt" --import "sync" --import _ "example.com/b" +-use ( +- ./package1 +- ./package1/vendor/example.com/foo +- ./package2 +- ./package2/vendor/example.com/foo +-) - --func _() { -- // The copylocks analyzer (RunDespiteErrors, FactTypes={}) does run. -- var mu sync.Mutex -- mu2 := mu // copylocks error, reported -- _ = &mu2 +--- package1/go.mod -- +-module mod.test - -- // The printf analyzer (!RunDespiteErrors, FactTypes!={}) does not run: -- // (c, printf) failed because of type error in c -- // (b, printf) and (a, printf) do not run because of failed prerequisites. -- fmt.Printf("%d", "s") // printf error, unreported +-go 1.18 - -- // The bools analyzer (!RunDespiteErrors, FactTypes={}) does not run: -- var cond bool -- _ = cond != true && cond != true // bools error, unreported +-require example.com/foo v0.0.1 +--- package1/main.go -- +-package main +- +-import "example.com/foo" +- +-func main() { +- _ = foo.CompleteMe -} +--- package1/vendor/example.com/foo/go.mod -- +-module example.com/foo - ---- b/b.go -- --package b +-go 1.18 +--- package1/vendor/example.com/foo/foo.go -- +-package foo - --import _ "example.com/c" +-const CompleteMe = 111 +--- package2/go.mod -- +-module mod2.test - ---- c/c.go -- --package c +-go 1.18 - --var _ = 1 / "" // type error +-require example.com/foo v0.0.1 +--- package2/main.go -- +-package main +- +-import "example.com/foo" +- +-func main() { +- _ = foo.CompleteMe +-} +--- package2/vendor/example.com/foo/go.mod -- +-module example.com/foo +- +-go 1.18 +--- package2/vendor/example.com/foo/foo.go -- +-package foo - +-const CompleteMe = 222 -` -- Run(t, src, func(t *testing.T, env *Env) { -- var diags protocol.PublishDiagnosticsParams -- env.OpenFile("a/a.go") +- +- WithOptions( +- ProxyFiles(proxy), +- ).Run(t, src, func(t *testing.T, env *Env) { +- env.OpenFile("package1/main.go") - env.AfterChange( -- Diagnostics(env.AtRegexp("a/a.go", "mu2 := (mu)"), WithMessage("assignment copies lock value")), -- ReadDiagnostics("a/a.go", &diags)) +- OutstandingWork(server.WorkspaceLoadFailure, `module example.com/foo appears multiple times in workspace`), +- ) - -- // Assert that there were no other diagnostics. -- // In particular: -- // - "fmt.Printf" does not trigger a [printf] finding; -- // - "cond != true" does not trigger a [bools] finding. -- // -- // We use this check in preference to NoDiagnosticAtRegexp -- // as it is robust in case of minor mistakes in the position -- // regexp, and because it reports unexpected diagnostics. -- if got, want := len(diags.Diagnostics), 1; got != want { -- t.Errorf("got %d diagnostics in a/a.go, want %d:", got, want) -- for i, diag := range diags.Diagnostics { -- t.Logf("Diagnostics[%d] = %+v", i, diag) -- } +- // Remove the redundant vendored copy of example.com. +- env.WriteWorkspaceFile("go.work", `go 1.18 +- use ( +- ./package1 +- ./package2 +- ./package2/vendor/example.com/foo +- ) +- `) +- env.AfterChange(NoOutstandingWork(IgnoreTelemetryPromptWork)) +- +- // Check that definitions in package1 go to the copy vendored in package2. +- location := string(env.GoToDefinition(env.RegexpSearch("package1/main.go", "CompleteMe")).URI) +- const wantLocation = "package2/vendor/example.com/foo/foo.go" +- if !strings.HasSuffix(location, wantLocation) { +- t.Errorf("got definition of CompleteMe at %q, want %q", location, wantLocation) - } - }) -} - --// This test demonstrates the deprecated symbol analyzer --// produces deprecation notices with expected severity and tags. --func TestDeprecatedAnalysis(t *testing.T) { -- const src = ` +-// Test for golang/go#43186: correcting the module path should fix errors +-// without restarting gopls. +-func TestBrokenWorkspace_WrongModulePath(t *testing.T) { +- const files = ` --- go.mod -- --module example.com ---- a/a.go -- --package a +-module mod.testx - --import "example.com/b" +-go 1.18 +--- p/internal/foo/foo.go -- +-package foo - --func _() { -- new(b.B).Obsolete() // deprecated --} +-const C = 1 +--- p/internal/bar/bar.go -- +-package bar - ---- b/b.go -- --package b +-import "mod.test/p/internal/foo" - --type B struct{} +-const D = foo.C + 1 +--- p/internal/bar/bar_test.go -- +-package bar_test - --// Deprecated: use New instead. --func (B) Obsolete() {} +-import ( +- "mod.test/p/internal/foo" +- . "mod.test/p/internal/bar" +-) - --func (B) New() {} +-const E = D + foo.C +--- p/internal/baz/baz_test.go -- +-package baz_test +- +-import ( +- named "mod.test/p/internal/bar" +-) +- +-const F = named.D - 3 -` -- Run(t, src, func(t *testing.T, env *Env) { -- env.OpenFile("a/a.go") +- +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("p/internal/bar/bar.go") - env.AfterChange( -- Diagnostics( -- env.AtRegexp("a/a.go", "new.*Obsolete"), -- WithMessage("use New instead."), -- WithSeverityTags("deprecated", protocol.SeverityHint, []protocol.DiagnosticTag{protocol.Deprecated}), -- ), +- Diagnostics(env.AtRegexp("p/internal/bar/bar.go", "\"mod.test/p/internal/foo\"")), - ) +- env.OpenFile("go.mod") +- env.RegexpReplace("go.mod", "mod.testx", "mod.test") +- env.SaveBuffer("go.mod") // saving triggers a reload +- env.AfterChange(NoDiagnostics()) - }) -} - --func TestDiagnosticsOnlyOnSaveFile(t *testing.T) { -- const onlyMod = ` ---- go.mod -- --module mod.com +-func TestMultipleModules_Warning(t *testing.T) { +- t.Skip("temporary skip for golang/go#57979: revisit after zero-config logic is in place") - --go 1.12 ---- main.go -- --package main +- msgForVersion := func(ver int) string { +- if ver >= 18 { +- return `gopls was not able to find modules in your workspace.` +- } else { +- return `gopls requires a module at the root of your workspace.` +- } +- } - --func main() { -- Foo() --} ---- foo.go -- --package main +- const modules = ` +--- a/go.mod -- +-module a.com - --func Foo() {} +-go 1.12 +--- a/a.go -- +-package a +--- a/empty.go -- +-// an empty file +--- b/go.mod -- +-module b.com +- +-go 1.12 +--- b/b.go -- +-package b -` -- WithOptions( -- Settings{ -- "diagnosticsTrigger": "Save", -- }, -- ).Run(t, onlyMod, func(t *testing.T, env *Env) { -- env.OpenFile("foo.go") -- env.RegexpReplace("foo.go", "(Foo)", "Bar") // Makes reference to Foo undefined/undeclared. -- env.AfterChange(NoDiagnostics()) // No diagnostics update until file save. +- for _, go111module := range []string{"on", "auto"} { +- t.Run("GO111MODULE="+go111module, func(t *testing.T) { +- WithOptions( +- Modes(Default), +- EnvVars{"GO111MODULE": go111module}, +- ).Run(t, modules, func(t *testing.T, env *Env) { +- ver := env.GoVersion() +- msg := msgForVersion(ver) +- env.OpenFile("a/a.go") +- env.OpenFile("a/empty.go") +- env.OpenFile("b/go.mod") +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/a.go", "package a")), +- Diagnostics(env.AtRegexp("b/go.mod", "module b.com")), +- OutstandingWork(server.WorkspaceLoadFailure, msg), +- ) - -- env.SaveBuffer("foo.go") -- // Compiler's error message about undeclared names vary depending on the version, -- // but must be explicit about the problematic name. -- env.AfterChange(Diagnostics(env.AtRegexp("main.go", "Foo"), WithMessage("Foo"))) +- // Changing the workspace folders to the valid modules should resolve +- // the workspace errors and diagnostics. +- // +- // TODO(rfindley): verbose work tracking doesn't follow changing the +- // workspace folder, therefore we can't invoke AfterChange here. +- env.ChangeWorkspaceFolders("a", "b") +- env.Await( +- NoDiagnostics(ForFile("a/a.go")), +- NoDiagnostics(ForFile("b/go.mod")), +- NoOutstandingWork(IgnoreTelemetryPromptWork), +- ) - -- env.OpenFile("main.go") -- env.RegexpReplace("main.go", "(Foo)", "Bar") -- // No diagnostics update until file save. That results in outdated diagnostic. -- env.AfterChange(Diagnostics(env.AtRegexp("main.go", "Bar"), WithMessage("Foo"))) +- env.ChangeWorkspaceFolders(".") - -- env.SaveBuffer("main.go") -- env.AfterChange(NoDiagnostics()) +- // TODO(rfindley): when GO111MODULE=auto, we need to open or change a +- // file here in order to detect a critical error. This is because gopls +- // has forgotten about a/a.go, and therefore doesn't hit the heuristic +- // "all packages are command-line-arguments". +- // +- // This is broken, and could be fixed by adjusting the heuristic to +- // account for the scenario where there are *no* workspace packages, or +- // (better) trying to get workspace packages for each open file. See +- // also golang/go#54261. +- env.OpenFile("b/b.go") +- env.AfterChange( +- // TODO(rfindley): fix these missing diagnostics. +- // Diagnostics(env.AtRegexp("a/a.go", "package a")), +- // Diagnostics(env.AtRegexp("b/go.mod", "module b.com")), +- Diagnostics(env.AtRegexp("b/b.go", "package b")), +- OutstandingWork(server.WorkspaceLoadFailure, msg), +- ) +- }) +- }) +- } +- +- // Expect no warning if GO111MODULE=auto in a directory in GOPATH. +- t.Run("GOPATH_GO111MODULE_auto", func(t *testing.T) { +- WithOptions( +- Modes(Default), +- EnvVars{"GO111MODULE": "auto"}, +- InGOPATH(), +- ).Run(t, modules, func(t *testing.T, env *Env) { +- env.OpenFile("a/a.go") +- env.AfterChange( +- NoDiagnostics(ForFile("a/a.go")), +- NoOutstandingWork(IgnoreTelemetryPromptWork), +- ) +- }) - }) -} -diff -urN a/gopls/internal/regtest/diagnostics/golist_test.go b/gopls/internal/regtest/diagnostics/golist_test.go ---- a/gopls/internal/regtest/diagnostics/golist_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/diagnostics/golist_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,71 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/test/integration/workspace/directoryfilters_test.go b/gopls/internal/test/integration/workspace/directoryfilters_test.go +--- a/gopls/internal/test/integration/workspace/directoryfilters_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/workspace/directoryfilters_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,207 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package diagnostics +-package workspace - -import ( +- "sort" +- "strings" - "testing" - -- . "golang.org/x/tools/gopls/internal/lsp/regtest" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/internal/testenv" +- . "golang.org/x/tools/gopls/internal/test/integration" -) - --func TestGoListErrors(t *testing.T) { -- testenv.NeedsTool(t, "cgo") -- -- const src = ` ---- go.mod -- --module a.com -- --go 1.18 ---- a/a.go -- --package a -- --import ---- c/c.go -- --package c -- --/* --int fortythree() { return 42; } --*/ --import "C" +-// This file contains regression tests for the directoryFilters setting. +-// +-// TODO: +-// - consolidate some of these tests into a single test +-// - add more tests for changing directory filters - --func Foo() { -- print(C.fortytwo()) +-func TestDirectoryFilters(t *testing.T) { +- WithOptions( +- ProxyFiles(workspaceProxy), +- WorkspaceFolders("pkg"), +- Settings{ +- "directoryFilters": []string{"-inner"}, +- }, +- ).Run(t, workspaceModule, func(t *testing.T, env *Env) { +- syms := env.Symbol("Hi") +- sort.Slice(syms, func(i, j int) bool { return syms[i].ContainerName < syms[j].ContainerName }) +- for _, s := range syms { +- if strings.Contains(s.ContainerName, "inner") { +- t.Errorf("WorkspaceSymbol: found symbol %q with container %q, want \"inner\" excluded", s.Name, s.ContainerName) +- } +- } +- }) -} ---- p/p.go -- --package p -- --import "a.com/q" - --const P = q.Q + 1 ---- q/q.go -- --package q +-func TestDirectoryFiltersLoads(t *testing.T) { +- // exclude, and its error, should be excluded from the workspace. +- const files = ` +--- go.mod -- +-module example.com - --import "a.com/p" +-go 1.12 +--- exclude/exclude.go -- +-package exclude - --const Q = p.P + 1 +-const _ = Nonexistant -` - -- Run(t, src, func(t *testing.T, env *Env) { +- WithOptions( +- Settings{"directoryFilters": []string{"-exclude"}}, +- ).Run(t, files, func(t *testing.T, env *Env) { - env.OnceMet( - InitialWorkspaceLoad, -- Diagnostics( -- env.AtRegexp("a/a.go", "import\n()"), -- FromSource(string(source.ParseError)), -- ), -- Diagnostics( -- AtPosition("c/c.go", 0, 0), -- FromSource(string(source.ListError)), -- WithMessage("may indicate failure to perform cgo processing"), -- ), -- Diagnostics( -- env.AtRegexp("p/p.go", `"a.com/q"`), -- FromSource(string(source.ListError)), -- WithMessage("import cycle not allowed"), -- ), +- NoDiagnostics(ForFile("exclude/x.go")), - ) - }) -} -diff -urN a/gopls/internal/regtest/diagnostics/invalidation_test.go b/gopls/internal/regtest/diagnostics/invalidation_test.go ---- a/gopls/internal/regtest/diagnostics/invalidation_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/diagnostics/invalidation_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,111 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package diagnostics -- --import ( -- "fmt" -- "testing" -- -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" --) - --// Test for golang/go#50267: diagnostics should be re-sent after a file is --// opened. --func TestDiagnosticsAreResentAfterCloseOrOpen(t *testing.T) { +-func TestDirectoryFiltersTransitiveDep(t *testing.T) { +- // Even though exclude is excluded from the workspace, it should +- // still be importable as a non-workspace package. - const files = ` --- go.mod -- --module mod.com +-module example.com - --go 1.16 ---- main.go -- --package main +-go 1.12 +--- include/include.go -- +-package include +-import "example.com/exclude" - --func _() { -- x := 2 --} +-const _ = exclude.X +--- exclude/exclude.go -- +-package exclude +- +-const _ = Nonexistant // should be ignored, since this is a non-workspace package +-const X = 1 -` -- Run(t, files, func(_ *testing.T, env *Env) { // Create a new workspace-level directory and empty file. -- env.OpenFile("main.go") -- var afterOpen protocol.PublishDiagnosticsParams -- env.AfterChange( -- ReadDiagnostics("main.go", &afterOpen), -- ) -- env.CloseBuffer("main.go") -- var afterClose protocol.PublishDiagnosticsParams -- env.AfterChange( -- ReadDiagnostics("main.go", &afterClose), -- ) -- if afterOpen.Version == afterClose.Version { -- t.Errorf("publishDiagnostics: got the same version after closing (%d) as after opening", afterOpen.Version) -- } -- env.OpenFile("main.go") -- var afterReopen protocol.PublishDiagnosticsParams -- env.AfterChange( -- ReadDiagnostics("main.go", &afterReopen), +- +- WithOptions( +- Settings{"directoryFilters": []string{"-exclude"}}, +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.OnceMet( +- InitialWorkspaceLoad, +- NoDiagnostics(ForFile("exclude/exclude.go")), // filtered out +- NoDiagnostics(ForFile("include/include.go")), // successfully builds - ) -- if afterReopen.Version == afterClose.Version { -- t.Errorf("pubslishDiagnostics: got the same version after reopening (%d) as after closing", afterClose.Version) +- }) +-} +- +-// Test for golang/go#46438: support for '**' in directory filters. +-func TestDirectoryFilters_Wildcard(t *testing.T) { +- filters := []string{"-**/bye"} +- WithOptions( +- ProxyFiles(workspaceProxy), +- WorkspaceFolders("pkg"), +- Settings{ +- "directoryFilters": filters, +- }, +- ).Run(t, workspaceModule, func(t *testing.T, env *Env) { +- syms := env.Symbol("Bye") +- sort.Slice(syms, func(i, j int) bool { return syms[i].ContainerName < syms[j].ContainerName }) +- for _, s := range syms { +- if strings.Contains(s.ContainerName, "bye") { +- t.Errorf("WorkspaceSymbol: found symbol %q with container %q with filters %v", s.Name, s.ContainerName, filters) +- } - } - }) -} - --// Test for the "chattyDiagnostics" setting: we should get re-published --// diagnostics after every file change, even if diagnostics did not change. --func TestChattyDiagnostics(t *testing.T) { +-// Test for golang/go#52993: wildcard directoryFilters should apply to +-// goimports scanning as well. +-func TestDirectoryFilters_ImportScanning(t *testing.T) { - const files = ` --- go.mod -- --module mod.com +-module mod.test - --go 1.16 +-go 1.12 --- main.go -- -package main - --func _() { -- x := 2 +-func main() { +- bye.Goodbye() +- hi.Hello() -} +--- p/bye/bye.go -- +-package bye - --// Irrelevant comment #0 +-func Goodbye() {} +--- hi/hi.go -- +-package hi +- +-func Hello() {} -` - - WithOptions( - Settings{ -- "chattyDiagnostics": true, +- "directoryFilters": []string{"-**/bye", "-hi"}, - }, -- ).Run(t, files, func(_ *testing.T, env *Env) { // Create a new workspace-level directory and empty file. -- +- ).Run(t, files, func(t *testing.T, env *Env) { - env.OpenFile("main.go") -- var d protocol.PublishDiagnosticsParams -- env.AfterChange( -- ReadDiagnostics("main.go", &d), -- ) -- -- if len(d.Diagnostics) != 1 { -- t.Fatalf("len(Diagnostics) = %d, want 1", len(d.Diagnostics)) -- } -- msg := d.Diagnostics[0].Message -- -- for i := 0; i < 5; i++ { -- before := d.Version -- env.RegexpReplace("main.go", "Irrelevant comment #.", fmt.Sprintf("Irrelevant comment #%d", i)) -- env.AfterChange( -- ReadDiagnostics("main.go", &d), -- ) -- -- if d.Version == before { -- t.Errorf("after change, got version %d, want new version", d.Version) -- } -- -- // As a sanity check, make sure we have the same diagnostic. -- if len(d.Diagnostics) != 1 { -- t.Fatalf("len(Diagnostics) = %d, want 1", len(d.Diagnostics)) -- } -- newMsg := d.Diagnostics[0].Message -- if newMsg != msg { -- t.Errorf("after change, got message %q, want %q", newMsg, msg) -- } +- beforeSave := env.BufferText("main.go") +- env.OrganizeImports("main.go") +- got := env.BufferText("main.go") +- if got != beforeSave { +- t.Errorf("after organizeImports code action, got modified buffer:\n%s", got) - } - }) -} -diff -urN a/gopls/internal/regtest/diagnostics/undeclared_test.go b/gopls/internal/regtest/diagnostics/undeclared_test.go ---- a/gopls/internal/regtest/diagnostics/undeclared_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/diagnostics/undeclared_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,73 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package diagnostics - --import ( -- "testing" +-// Test for golang/go#52993: non-wildcard directoryFilters should still be +-// applied relative to the workspace folder, not the module root. +-func TestDirectoryFilters_MultiRootImportScanning(t *testing.T) { +- const files = ` +--- go.work -- +-go 1.18 - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" +-use ( +- a +- b -) +--- a/go.mod -- +-module mod1.test - --func TestUndeclaredDiagnostics(t *testing.T) { -- src := ` ---- go.mod -- --module mod.com -- --go 1.12 ---- a/a.go -- --package a +-go 1.18 +--- a/main.go -- +-package main - --func _() int { -- return x +-func main() { +- hi.Hi() -} ---- b/b.go -- --package b +--- a/hi/hi.go -- +-package hi - --func _() int { -- var y int -- y = y -- return y +-func Hi() {} +--- b/go.mod -- +-module mod2.test +- +-go 1.18 +--- b/main.go -- +-package main +- +-func main() { +- hi.Hi() -} --` -- Run(t, src, func(t *testing.T, env *Env) { -- isUnnecessary := func(diag protocol.Diagnostic) bool { -- for _, tag := range diag.Tags { -- if tag == protocol.Unnecessary { -- return true -- } -- } -- return false -- } +--- b/hi/hi.go -- +-package hi - -- // 'x' is undeclared, but still necessary. -- env.OpenFile("a/a.go") -- var adiags protocol.PublishDiagnosticsParams -- env.AfterChange( -- Diagnostics(env.AtRegexp("a/a.go", "x")), -- ReadDiagnostics("a/a.go", &adiags), -- ) -- if got := len(adiags.Diagnostics); got != 1 { -- t.Errorf("len(Diagnostics) = %d, want 1", got) -- } -- if diag := adiags.Diagnostics[0]; isUnnecessary(diag) { -- t.Errorf("%v tagged unnecessary, want necessary", diag) -- } +-func Hi() {} +-` - -- // 'y = y' is pointless, and should be detected as unnecessary. -- env.OpenFile("b/b.go") -- var bdiags protocol.PublishDiagnosticsParams -- env.AfterChange( -- Diagnostics(env.AtRegexp("b/b.go", "y = y")), -- ReadDiagnostics("b/b.go", &bdiags), -- ) -- if got := len(bdiags.Diagnostics); got != 1 { -- t.Errorf("len(Diagnostics) = %d, want 1", got) -- } -- if diag := bdiags.Diagnostics[0]; !isUnnecessary(diag) { -- t.Errorf("%v tagged necessary, want unnecessary", diag) +- WithOptions( +- Settings{ +- "directoryFilters": []string{"-hi"}, // this test fails with -**/hi +- }, +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("a/main.go") +- beforeSave := env.BufferText("a/main.go") +- env.OrganizeImports("a/main.go") +- got := env.BufferText("a/main.go") +- if got == beforeSave { +- t.Errorf("after organizeImports code action, got identical buffer:\n%s", got) - } - }) -} -diff -urN a/gopls/internal/regtest/inlayhints/inlayhints_test.go b/gopls/internal/regtest/inlayhints/inlayhints_test.go ---- a/gopls/internal/regtest/inlayhints/inlayhints_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/inlayhints/inlayhints_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,69 +0,0 @@ +diff -urN a/gopls/internal/test/integration/workspace/fromenv_test.go b/gopls/internal/test/integration/workspace/fromenv_test.go +--- a/gopls/internal/test/integration/workspace/fromenv_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/workspace/fromenv_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,76 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. --package inlayhint +- +-package workspace - -import ( +- "fmt" +- "path/filepath" - "testing" - -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/hooks" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" -- "golang.org/x/tools/gopls/internal/lsp/source" +- . "golang.org/x/tools/gopls/internal/test/integration" -) - --func TestMain(m *testing.M) { -- bug.PanicOnBugs = true -- Main(m, hooks.Options) --} +-// Test that setting go.work via environment variables or settings works. +-func TestUseGoWorkOutsideTheWorkspace(t *testing.T) { +- // As discussed in +- // https://github.com/golang/go/issues/59458#issuecomment-1513794691, we must +- // use \-separated paths in go.work use directives for this test to work +- // correctly on windows. +- var files = fmt.Sprintf(` +--- work/a/go.mod -- +-module a.com - --func TestEnablingInlayHints(t *testing.T) { -- const workspace = ` ---- go.mod -- --module inlayHint.test -go 1.12 ---- lib.go -- --package lib --type Number int --const ( -- Zero Number = iota -- One -- Two +--- work/a/a.go -- +-package a +--- work/b/go.mod -- +-module b.com +- +-go 1.12 +--- work/b/b.go -- +-package b +- +-func _() { +- x := 1 // unused +-} +--- other/c/go.mod -- +-module c.com +- +-go 1.18 +--- other/c/c.go -- +-package c +--- config/go.work -- +-go 1.18 +- +-use ( +- %s +- %s +- %s -) --` -- tests := []struct { -- label string -- enabled map[string]bool -- wantInlayHint bool -- }{ -- { -- label: "default", -- wantInlayHint: false, -- }, -- { -- label: "enable const", -- enabled: map[string]bool{source.ConstantValues: true}, -- wantInlayHint: true, -- }, -- { -- label: "enable parameter names", -- enabled: map[string]bool{source.ParameterNames: true}, -- wantInlayHint: false, -- }, -- } -- for _, test := range tests { -- t.Run(test.label, func(t *testing.T) { -- WithOptions( -- Settings{ -- "hints": test.enabled, -- }, -- ).Run(t, workspace, func(t *testing.T, env *Env) { -- env.OpenFile("lib.go") -- lens := env.InlayHints("lib.go") -- if gotInlayHint := len(lens) > 0; gotInlayHint != test.wantInlayHint { -- t.Errorf("got inlayHint: %t, want %t", gotInlayHint, test.wantInlayHint) -- } -- }) -- }) -- } +-`, +- filepath.Join("$SANDBOX_WORKDIR", "work", "a"), +- filepath.Join("$SANDBOX_WORKDIR", "work", "b"), +- filepath.Join("$SANDBOX_WORKDIR", "other", "c"), +- ) +- +- WithOptions( +- WorkspaceFolders("work"), // use a nested workspace dir, so that GOWORK is outside the workspace +- EnvVars{"GOWORK": filepath.Join("$SANDBOX_WORKDIR", "config", "go.work")}, +- ).Run(t, files, func(t *testing.T, env *Env) { +- // When we have an explicit GOWORK set, we should get a file watch request. +- env.OnceMet( +- InitialWorkspaceLoad, +- FileWatchMatching(`other`), +- FileWatchMatching(`config.go\.work`), +- ) +- env.Await(FileWatchMatching(`config.go\.work`)) +- // Even though work/b is not open, we should get its diagnostics as it is +- // included in the workspace. +- env.OpenFile("work/a/a.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("work/b/b.go", "x := 1"), WithMessage("not used")), +- ) +- }) -} -diff -urN a/gopls/internal/regtest/marker/marker_test.go b/gopls/internal/regtest/marker/marker_test.go ---- a/gopls/internal/regtest/marker/marker_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/marker_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,30 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/test/integration/workspace/goversion_test.go b/gopls/internal/test/integration/workspace/goversion_test.go +--- a/gopls/internal/test/integration/workspace/goversion_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/workspace/goversion_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,62 +0,0 @@ +-// Copyright 2024 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package marker +-package workspace - -import ( +- "flag" - "os" +- "runtime" - "testing" - -- "golang.org/x/tools/gopls/internal/bug" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" -- "golang.org/x/tools/internal/testenv" +- . "golang.org/x/tools/gopls/internal/test/integration" -) - --func TestMain(m *testing.M) { -- bug.PanicOnBugs = true -- testenv.ExitIfSmallMachine() -- os.Exit(m.Run()) --} -- --// Note: we use a separate package for the marker tests so that we can easily --// compare their performance to the existing marker tests in ./internal/lsp. -- --// TestMarkers runs the marker tests from the testdata directory. --// --// See RunMarkerTests for details on how marker tests work. --func TestMarkers(t *testing.T) { -- RunMarkerTests(t, "testdata") --} -diff -urN a/gopls/internal/regtest/marker/testdata/codeaction/extract_method.txt b/gopls/internal/regtest/marker/testdata/codeaction/extract_method.txt ---- a/gopls/internal/regtest/marker/testdata/codeaction/extract_method.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/codeaction/extract_method.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,244 +0,0 @@ --This test exercises function and method extraction. -- ---- flags -- ---ignore_extra_diags -- ---- basic.go -- --package extract -- --//@codeactionedit(A_XLessThanYP, "refactor.extract", meth1, "Extract method") --//@codeactionedit(A_XLessThanYP, "refactor.extract", func1, "Extract function") --//@codeactionedit(A_AddP1, "refactor.extract", meth2, "Extract method") --//@codeactionedit(A_AddP1, "refactor.extract", func2, "Extract function") --//@codeactionedit(A_AddP2, "refactor.extract", meth3, "Extract method") --//@codeactionedit(A_AddP2, "refactor.extract", func3, "Extract function") --//@codeactionedit(A_XLessThanY, "refactor.extract", meth4, "Extract method") --//@codeactionedit(A_XLessThanY, "refactor.extract", func4, "Extract function") --//@codeactionedit(A_Add1, "refactor.extract", meth5, "Extract method") --//@codeactionedit(A_Add1, "refactor.extract", func5, "Extract function") --//@codeactionedit(A_Add2, "refactor.extract", meth6, "Extract method") --//@codeactionedit(A_Add2, "refactor.extract", func6, "Extract function") -- --type A struct { -- x int -- y int --} -- --func (a *A) XLessThanYP() bool { -- return a.x < a.y //@loc(A_XLessThanYP, re`return.*a\.y`) --} -- --func (a *A) AddP() int { -- sum := a.x + a.y //@loc(A_AddP1, re`sum.*a\.y`) -- return sum //@loc(A_AddP2, re`return.*sum`) --} -- --func (a A) XLessThanY() bool { -- return a.x < a.y //@loc(A_XLessThanY, re`return.*a\.y`) --} -- --func (a A) Add() int { -- sum := a.x + a.y //@loc(A_Add1, re`sum.*a\.y`) -- return sum //@loc(A_Add2, re`return.*sum`) --} -- ---- @func1/basic.go -- ----- before --+++ after --@@ -22 +22,5 @@ --- return a.x < a.y //@loc(A_XLessThanYP, re`return.*a\.y`) --+ return newFunction(a) //@loc(A_XLessThanYP, re`return.*a\.y`) --+} --+ --+func newFunction(a *A) bool { --+ return a.x < a.y ---- @func2/basic.go -- ----- before --+++ after --@@ -26,2 +26,7 @@ --- sum := a.x + a.y //@loc(A_AddP1, re`sum.*a\.y`) --+ sum := newFunction(a) //@loc(A_AddP1, re`sum.*a\.y`) --- return sum //@loc(A_AddP2, re`return.*sum`) --+ return sum //@loc(A_AddP2, re`return.*sum`) --+} --+ --+func newFunction(a *A) int { --+ sum := a.x + a.y --+ return sum ---- @func3/basic.go -- ----- before --+++ after --@@ -27 +27,5 @@ --- return sum //@loc(A_AddP2, re`return.*sum`) --+ return newFunction(sum) //@loc(A_AddP2, re`return.*sum`) --+} --+ --+func newFunction(sum int) int { --+ return sum ---- @func4/basic.go -- ----- before --+++ after --@@ -31 +31,5 @@ --- return a.x < a.y //@loc(A_XLessThanY, re`return.*a\.y`) --+ return newFunction(a) //@loc(A_XLessThanY, re`return.*a\.y`) --+} --+ --+func newFunction(a A) bool { --+ return a.x < a.y ---- @func5/basic.go -- ----- before --+++ after --@@ -35 +35 @@ --- sum := a.x + a.y //@loc(A_Add1, re`sum.*a\.y`) --+ sum := newFunction(a) //@loc(A_Add1, re`sum.*a\.y`) --@@ -39 +39,5 @@ --+func newFunction(a A) int { --+ sum := a.x + a.y --+ return sum --+} --+ ---- @func6/basic.go -- ----- before --+++ after --@@ -36 +36 @@ --- return sum //@loc(A_Add2, re`return.*sum`) --+ return newFunction(sum) //@loc(A_Add2, re`return.*sum`) --@@ -39 +39,4 @@ --+func newFunction(sum int) int { --+ return sum --+} --+ ---- @meth1/basic.go -- ----- before --+++ after --@@ -22 +22,5 @@ --- return a.x < a.y //@loc(A_XLessThanYP, re`return.*a\.y`) --+ return a.newMethod() //@loc(A_XLessThanYP, re`return.*a\.y`) --+} --+ --+func (a *A) newMethod() bool { --+ return a.x < a.y ---- @meth2/basic.go -- ----- before --+++ after --@@ -26,2 +26,7 @@ --- sum := a.x + a.y //@loc(A_AddP1, re`sum.*a\.y`) --+ sum := a.newMethod() //@loc(A_AddP1, re`sum.*a\.y`) --- return sum //@loc(A_AddP2, re`return.*sum`) --+ return sum //@loc(A_AddP2, re`return.*sum`) --+} --+ --+func (a *A) newMethod() int { --+ sum := a.x + a.y --+ return sum ---- @meth3/basic.go -- ----- before --+++ after --@@ -27 +27,5 @@ --- return sum //@loc(A_AddP2, re`return.*sum`) --+ return a.newMethod(sum) //@loc(A_AddP2, re`return.*sum`) --+} --+ --+func (*A) newMethod(sum int) int { --+ return sum ---- @meth4/basic.go -- ----- before --+++ after --@@ -31 +31,5 @@ --- return a.x < a.y //@loc(A_XLessThanY, re`return.*a\.y`) --+ return a.newMethod() //@loc(A_XLessThanY, re`return.*a\.y`) --+} --+ --+func (a A) newMethod() bool { --+ return a.x < a.y ---- @meth5/basic.go -- ----- before --+++ after --@@ -35 +35 @@ --- sum := a.x + a.y //@loc(A_Add1, re`sum.*a\.y`) --+ sum := a.newMethod() //@loc(A_Add1, re`sum.*a\.y`) --@@ -39 +39,5 @@ --+func (a A) newMethod() int { --+ sum := a.x + a.y --+ return sum --+} --+ ---- @meth6/basic.go -- ----- before --+++ after --@@ -36 +36 @@ --- return sum //@loc(A_Add2, re`return.*sum`) --+ return a.newMethod(sum) //@loc(A_Add2, re`return.*sum`) --@@ -39 +39,4 @@ --+func (A) newMethod(sum int) int { --+ return sum --+} --+ ---- context.go -- --package extract -- --import "context" -- --//@codeactionedit(B_AddP, "refactor.extract", contextMeth1, "Extract method") --//@codeactionedit(B_AddP, "refactor.extract", contextFunc1, "Extract function") --//@codeactionedit(B_LongList, "refactor.extract", contextMeth2, "Extract method") --//@codeactionedit(B_LongList, "refactor.extract", contextFunc2, "Extract function") -- --type B struct { -- x int -- y int --} +-var go121bin = flag.String("go121bin", "", "bin directory containing go 1.21 or later") - --func (b *B) AddP(ctx context.Context) (int, error) { -- sum := b.x + b.y -- return sum, ctx.Err() //@loc(B_AddP, re`return.*ctx\.Err\(\)`) --} +-// TODO(golang/go#65917): delete this test once we no longer support building +-// gopls with older Go versions. +-func TestCanHandlePatchVersions(t *testing.T) { +- // This test verifies the fixes for golang/go#66195 and golang/go#66636 -- +- // that gopls does not crash when encountering a go version with a patch +- // number in the go.mod file. +- // +- // This is tricky to test, because the regression requires that gopls is +- // built with an older go version, and then the environment is upgraded to +- // have a more recent go. To set up this scenario, the test requires a path +- // to a bin directory containing go1.21 or later. +- if *go121bin == "" { +- t.Skip("-go121bin directory is not set") +- } - --func (b *B) LongList(ctx context.Context) (int, error) { -- p1 := 1 -- p2 := 1 -- p3 := 1 -- return p1 + p2 + p3, ctx.Err() //@loc(B_LongList, re`return.*ctx\.Err\(\)`) --} ---- @contextMeth1/context.go -- ----- before --+++ after --@@ -17 +17,5 @@ --- return sum, ctx.Err() //@loc(B_AddP, re`return.*ctx\.Err\(\)`) --+ return b.newMethod(ctx, sum) //@loc(B_AddP, re`return.*ctx\.Err\(\)`) --+} --+ --+func (*B) newMethod(ctx context.Context, sum int) (int, error) { --+ return sum, ctx.Err() ---- @contextMeth2/context.go -- ----- before --+++ after --@@ -24 +24 @@ --- return p1 + p2 + p3, ctx.Err() //@loc(B_LongList, re`return.*ctx\.Err\(\)`) --+ return b.newMethod(ctx, p1, p2, p3) //@loc(B_LongList, re`return.*ctx\.Err\(\)`) --@@ -26 +26,4 @@ --+ --+func (*B) newMethod(ctx context.Context, p1 int, p2 int, p3 int) (int, error) { --+ return p1 + p2 + p3, ctx.Err() --+} ---- @contextFunc2/context.go -- ----- before --+++ after --@@ -24 +24 @@ --- return p1 + p2 + p3, ctx.Err() //@loc(B_LongList, re`return.*ctx\.Err\(\)`) --+ return newFunction(ctx, p1, p2, p3) //@loc(B_LongList, re`return.*ctx\.Err\(\)`) --@@ -26 +26,4 @@ --+ --+func newFunction(ctx context.Context, p1 int, p2 int, p3 int) (int, error) { --+ return p1 + p2 + p3, ctx.Err() --+} ---- @contextFunc1/context.go -- ----- before --+++ after --@@ -17 +17,5 @@ --- return sum, ctx.Err() //@loc(B_AddP, re`return.*ctx\.Err\(\)`) --+ return newFunction(ctx, sum) //@loc(B_AddP, re`return.*ctx\.Err\(\)`) --+} --+ --+func newFunction(ctx context.Context, sum int) (int, error) { --+ return sum, ctx.Err() -diff -urN a/gopls/internal/regtest/marker/testdata/codeaction/extract_variable.txt b/gopls/internal/regtest/marker/testdata/codeaction/extract_variable.txt ---- a/gopls/internal/regtest/marker/testdata/codeaction/extract_variable.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/codeaction/extract_variable.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,83 +0,0 @@ --This test checks the behavior of the 'extract variable' code action. +- if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { +- t.Skip("requires linux or darwin") // for PATH separator +- } - ---- flags -- ---ignore_extra_diags +- path := os.Getenv("PATH") +- t.Setenv("PATH", *go121bin+":"+path) - ---- basic_lit.go -- --package extract +- const files = ` +--- go.mod -- +-module example.com/bar - --func _() { -- var _ = 1 + 2 //@codeactionedit("1", "refactor.extract", basic_lit1) -- var _ = 3 + 4 //@codeactionedit("3 + 4", "refactor.extract", basic_lit2) --} +-go 1.21.1 - ---- @basic_lit1/basic_lit.go -- ----- before --+++ after --@@ -3,2 +3,3 @@ ---func _() { --+func _() { --+ x := 1 --- var _ = 1 + 2 //@codeactionedit("1", "refactor.extract", basic_lit1) --+ var _ = x + 2 //@codeactionedit("1", "refactor.extract", basic_lit1) ---- @basic_lit2/basic_lit.go -- ----- before --+++ after --@@ -5 +5,2 @@ --- var _ = 3 + 4 //@codeactionedit("3 + 4", "refactor.extract", basic_lit2) --+ x := 3 + 4 --+ var _ = x //@codeactionedit("3 + 4", "refactor.extract", basic_lit2) ---- func_call.go -- --package extract +--- p.go -- +-package bar - --import "strconv" +-type I interface { string } +-` - --func _() { -- x0 := append([]int{}, 1) //@codeactionedit("append([]int{}, 1)", "refactor.extract", func_call1) -- str := "1" -- b, err := strconv.Atoi(str) //@codeactionedit("strconv.Atoi(str)", "refactor.extract", func_call2) +- WithOptions( +- EnvVars{ +- "PATH": path, +- }, +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("p.go") +- env.AfterChange( +- NoDiagnostics(ForFile("p.go")), +- ) +- }) -} +diff -urN a/gopls/internal/test/integration/workspace/metadata_test.go b/gopls/internal/test/integration/workspace/metadata_test.go +--- a/gopls/internal/test/integration/workspace/metadata_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/workspace/metadata_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,238 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - ---- @func_call1/func_call.go -- ----- before --+++ after --@@ -6 +6,2 @@ --- x0 := append([]int{}, 1) //@codeactionedit("append([]int{}, 1)", "refactor.extract", func_call1) --+ x := append([]int{}, 1) --+ x0 := x //@codeactionedit("append([]int{}, 1)", "refactor.extract", func_call1) ---- @func_call2/func_call.go -- ----- before --+++ after --@@ -8 +8,2 @@ --- b, err := strconv.Atoi(str) //@codeactionedit("strconv.Atoi(str)", "refactor.extract", func_call2) --+ x, x1 := strconv.Atoi(str) --+ b, err := x, x1 //@codeactionedit("strconv.Atoi(str)", "refactor.extract", func_call2) ---- scope.go -- --package extract +-package workspace - --import "go/ast" +-import ( +- "strings" +- "testing" - --func _() { -- x0 := 0 -- if true { -- y := ast.CompositeLit{} //@codeactionedit("ast.CompositeLit{}", "refactor.extract", scope1) -- } -- if true { -- x1 := !false //@codeactionedit("!false", "refactor.extract", scope2) -- } --} +- . "golang.org/x/tools/gopls/internal/test/integration" +-) - ---- @scope1/scope.go -- ----- before --+++ after --@@ -8 +8,2 @@ --- y := ast.CompositeLit{} //@codeactionedit("ast.CompositeLit{}", "refactor.extract", scope1) --+ x := ast.CompositeLit{} --+ y := x //@codeactionedit("ast.CompositeLit{}", "refactor.extract", scope1) ---- @scope2/scope.go -- ----- before --+++ after --@@ -11 +11,2 @@ --- x1 := !false //@codeactionedit("!false", "refactor.extract", scope2) --+ x := !false --+ x1 := x //@codeactionedit("!false", "refactor.extract", scope2) -diff -urN a/gopls/internal/regtest/marker/testdata/codeaction/fill_struct.txt b/gopls/internal/regtest/marker/testdata/codeaction/fill_struct.txt ---- a/gopls/internal/regtest/marker/testdata/codeaction/fill_struct.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/codeaction/fill_struct.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,630 +0,0 @@ --This test checks the behavior of the 'fill struct' code action. +-// TODO(rfindley): move workspace tests related to metadata bugs into this +-// file. - ---- flags -- ---ignore_extra_diags +-func TestFixImportDecl(t *testing.T) { +- const src = ` +--- go.mod -- +-module mod.test +- +-go 1.12 +--- p.go -- +-package p +- +-import ( +- _ "fmt" - +-const C = 42 +-` +- +- Run(t, src, func(t *testing.T, env *Env) { +- env.OpenFile("p.go") +- env.RegexpReplace("p.go", "\"fmt\"", "\"fmt\"\n)") +- env.AfterChange( +- NoDiagnostics(ForFile("p.go")), +- ) +- }) +-} +- +-// Test that moving ignoring a file via build constraints causes diagnostics to +-// be resolved. +-func TestIgnoreFile(t *testing.T) { +- const src = ` --- go.mod -- --module golang.org/lsptests/fillstruct +-module mod.test - --go 1.18 +-go 1.12 +--- foo.go -- +-package main - ---- data/data.go -- --package data +-func main() {} +--- bar.go -- +-package main - --type B struct { -- ExportedInt int -- unexportedInt int +-func main() {} +- ` +- +- Run(t, src, func(t *testing.T, env *Env) { +- env.OpenFile("foo.go") +- env.OpenFile("bar.go") +- env.OnceMet( +- env.DoneWithOpen(), +- Diagnostics(env.AtRegexp("foo.go", "func (main)")), +- Diagnostics(env.AtRegexp("bar.go", "func (main)")), +- ) +- +- // Ignore bar.go. This should resolve diagnostics. +- env.RegexpReplace("bar.go", "package main", "//go:build ignore\n\npackage main") +- +- // To make this test pass with experimentalUseInvalidMetadata, we could make +- // an arbitrary edit that invalidates the snapshot, at which point the +- // orphaned diagnostics will be invalidated. +- // +- // But of course, this should not be necessary: we should invalidate stale +- // information when fresh metadata arrives. +- // env.RegexpReplace("foo.go", "package main", "package main // test") +- env.AfterChange( +- NoDiagnostics(ForFile("foo.go")), +- NoDiagnostics(ForFile("bar.go")), +- ) +- +- // If instead of 'ignore' (which gopls treats as a standalone package) we +- // used a different build tag, we should get a warning about having no +- // packages for bar.go +- env.RegexpReplace("bar.go", "ignore", "excluded") +- env.AfterChange( +- Diagnostics(env.AtRegexp("bar.go", "package (main)"), WithMessage("excluded due to its build tags")), +- ) +- }) -} - ---- a.go -- --package fillstruct +-func TestReinitializeRepeatedly(t *testing.T) { +- const multiModule = ` +--- go.work -- +-go 1.18 +- +-use ( +- moda/a +- modb +-) +--- moda/a/go.mod -- +-module a.com +- +-require b.com v1.2.3 +--- moda/a/go.sum -- +-b.com v1.2.3 h1:tXrlXP0rnjRpKNmkbLYoWBdq0ikb3C3bKK9//moAWBI= +-b.com v1.2.3/go.mod h1:D+J7pfFBZK5vdIdZEFquR586vKKIkqG7Qjw9AxG5BQ8= +--- moda/a/a.go -- +-package a - -import ( -- "golang.org/lsptests/fillstruct/data" +- "b.com/b" -) - --type basicStruct struct { -- foo int +-func main() { +- var x int +- _ = b.Hello() +- // AAA -} +--- modb/go.mod -- +-module b.com - --var _ = basicStruct{} //@codeactionedit("}", "refactor.rewrite", a1) +--- modb/b/b.go -- +-package b - --type twoArgStruct struct { -- foo int -- bar string +-func Hello() int { +- var x int -} +-` +- WithOptions( +- ProxyFiles(workspaceModuleProxy), +- Settings{ +- // For this test, we want workspace diagnostics to start immediately +- // during change processing. +- "diagnosticsDelay": "0", +- }, +- ).Run(t, multiModule, func(t *testing.T, env *Env) { +- env.OpenFile("moda/a/a.go") +- env.AfterChange() - --var _ = twoArgStruct{} //@codeactionedit("}", "refactor.rewrite", a2) -- --type nestedStruct struct { -- bar string -- basic basicStruct +- // This test verifies that we fully process workspace reinitialization +- // (which allows GOPROXY), even when the reinitialized snapshot is +- // invalidated by subsequent changes. +- // +- // First, update go.work to remove modb. This will cause reinitialization +- // to fetch b.com from the proxy. +- env.WriteWorkspaceFile("go.work", "go 1.18\nuse moda/a") +- // Next, wait for gopls to start processing the change. Because we've set +- // diagnosticsDelay to zero, this will start diagnosing the workspace (and +- // try to reinitialize on the snapshot context). +- env.Await(env.StartedChangeWatchedFiles()) +- // Finally, immediately make a file change to cancel the previous +- // operation. This is racy, but will usually cause initialization to be +- // canceled. +- env.RegexpReplace("moda/a/a.go", "AAA", "BBB") +- env.AfterChange() +- // Now, to satisfy a definition request, gopls will try to reload moda. But +- // without access to the proxy (because this is no longer a +- // reinitialization), this loading will fail. +- loc := env.GoToDefinition(env.RegexpSearch("moda/a/a.go", "Hello")) +- got := env.Sandbox.Workdir.URIToPath(loc.URI) +- if want := "b.com@v1.2.3/b/b.go"; !strings.HasSuffix(got, want) { +- t.Errorf("expected %s, got %v", want, got) +- } +- }) -} - --var _ = nestedStruct{} //@codeactionedit("}", "refactor.rewrite", a3) +-// Test for golang/go#59458. With lazy module loading, we may not need +-// transitively required modules. +-func TestNestedModuleLoading_Issue59458(t *testing.T) { +- // In this test, module b.com/nested requires b.com/other, which in turn +- // requires b.com, but b.com/nested does not reach b.com through the package +- // graph. Therefore, b.com/nested does not need b.com on 1.17 and later, +- // thanks to graph pruning. +- // +- // We verify that we can load b.com/nested successfully. Previously, we +- // couldn't, because loading the pattern b.com/nested/... matched the module +- // b.com, which exists in the module graph but does not have a go.sum entry. - --var _ = data.B{} //@codeactionedit("}", "refactor.rewrite", a4) ---- @a1/a.go -- ----- before --+++ after --@@ -11 +11,3 @@ ---var _ = basicStruct{} //@codeactionedit("}", "refactor.rewrite", a1) --+var _ = basicStruct{ --+ foo: 0, --+} //@codeactionedit("}", "refactor.rewrite", a1) ---- @a2/a.go -- ----- before --+++ after --@@ -18 +18,4 @@ ---var _ = twoArgStruct{} //@codeactionedit("}", "refactor.rewrite", a2) --+var _ = twoArgStruct{ --+ foo: 0, --+ bar: "", --+} //@codeactionedit("}", "refactor.rewrite", a2) ---- @a3/a.go -- ----- before --+++ after --@@ -25 +25,4 @@ ---var _ = nestedStruct{} //@codeactionedit("}", "refactor.rewrite", a3) --+var _ = nestedStruct{ --+ bar: "", --+ basic: basicStruct{}, --+} //@codeactionedit("}", "refactor.rewrite", a3) ---- @a4/a.go -- ----- before --+++ after --@@ -27 +27,3 @@ ---var _ = data.B{} //@codeactionedit("}", "refactor.rewrite", a4) --+var _ = data.B{ --+ ExportedInt: 0, --+} //@codeactionedit("}", "refactor.rewrite", a4) ---- a2.go -- --package fillstruct +- const proxy = ` +--- b.com@v1.2.3/go.mod -- +-module b.com - --type typedStruct struct { -- m map[string]int -- s []int -- c chan int -- c1 <-chan int -- a [2]string --} +-go 1.18 +--- b.com@v1.2.3/b/b.go -- +-package b - --var _ = typedStruct{} //@codeactionedit("}", "refactor.rewrite", a21) +-func Hello() {} - --type funStruct struct { -- fn func(i int) int --} +--- b.com/other@v1.4.6/go.mod -- +-module b.com/other - --var _ = funStruct{} //@codeactionedit("}", "refactor.rewrite", a22) +-go 1.18 - --type funStructCompex struct { -- fn func(i int, s string) (string, int) --} +-require b.com v1.2.3 +--- b.com/other@v1.4.6/go.sun -- +-b.com v1.2.3 h1:AGjCxWRJLUuJiZ21IUTByr9buoa6+B6Qh5LFhVLKpn4= +--- b.com/other@v1.4.6/bar/bar.go -- +-package bar - --var _ = funStructCompex{} //@codeactionedit("}", "refactor.rewrite", a23) +-import "b.com/b" - --type funStructEmpty struct { -- fn func() +-func _() { +- b.Hello() -} +--- b.com/other@v1.4.6/foo/foo.go -- +-package foo - --var _ = funStructEmpty{} //@codeactionedit("}", "refactor.rewrite", a24) +-const Foo = 0 +-` - ---- @a21/a2.go -- ----- before --+++ after --@@ -11 +11,7 @@ ---var _ = typedStruct{} //@codeactionedit("}", "refactor.rewrite", a21) --+var _ = typedStruct{ --+ m: map[string]int{}, --+ s: []int{}, --+ c: make(chan int), --+ c1: make(<-chan int), --+ a: [2]string{}, --+} //@codeactionedit("}", "refactor.rewrite", a21) ---- @a22/a2.go -- ----- before --+++ after --@@ -17 +17,4 @@ ---var _ = funStruct{} //@codeactionedit("}", "refactor.rewrite", a22) --+var _ = funStruct{ --+ fn: func(i int) int { --+ }, --+} //@codeactionedit("}", "refactor.rewrite", a22) ---- @a23/a2.go -- ----- before --+++ after --@@ -23 +23,4 @@ ---var _ = funStructCompex{} //@codeactionedit("}", "refactor.rewrite", a23) --+var _ = funStructCompex{ --+ fn: func(i int, s string) (string, int) { --+ }, --+} //@codeactionedit("}", "refactor.rewrite", a23) ---- @a24/a2.go -- ----- before --+++ after --@@ -29 +29,4 @@ ---var _ = funStructEmpty{} //@codeactionedit("}", "refactor.rewrite", a24) --+var _ = funStructEmpty{ --+ fn: func() { --+ }, --+} //@codeactionedit("}", "refactor.rewrite", a24) ---- a3.go -- --package fillstruct +- const files = ` +--- go.mod -- +-module b.com/nested +- +-go 1.18 +- +-require b.com/other v1.4.6 +--- go.sum -- +-b.com/other v1.4.6 h1:pHXSzGsk6DamYXp9uRdDB9A/ZQqAN9it+JudU0sBf94= +-b.com/other v1.4.6/go.mod h1:T0TYuGdAHw4p/l0+1P/yhhYHfZRia7PaadNVDu58OWM= +--- nested.go -- +-package nested +- +-import "b.com/other/foo" +- +-const C = foo.Foo +-` +- WithOptions( +- ProxyFiles(proxy), +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.OnceMet( +- InitialWorkspaceLoad, +- NoDiagnostics(), +- ) +- }) +-} +diff -urN a/gopls/internal/test/integration/workspace/misspelling_test.go b/gopls/internal/test/integration/workspace/misspelling_test.go +--- a/gopls/internal/test/integration/workspace/misspelling_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/workspace/misspelling_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,80 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package workspace - -import ( -- "go/ast" -- "go/token" +- "runtime" +- "testing" +- +- . "golang.org/x/tools/gopls/internal/test/integration" +- "golang.org/x/tools/gopls/internal/test/compare" -) - --type Foo struct { -- A int --} +-// Test for golang/go#57081. +-func TestFormattingMisspelledURI(t *testing.T) { +- if runtime.GOOS != "windows" && runtime.GOOS != "darwin" { +- t.Skip("golang/go#57081 only reproduces on case-insensitive filesystems.") +- } +- const files = ` +--- go.mod -- +-module mod.test - --type Bar struct { -- X *Foo -- Y *Foo --} +-go 1.19 +--- foo.go -- +-package foo - --var _ = Bar{} //@codeactionedit("}", "refactor.rewrite", a31) +-const C = 2 // extra space is intentional +-` - --type importedStruct struct { -- m map[*ast.CompositeLit]ast.Field -- s []ast.BadExpr -- a [3]token.Token -- c chan ast.EmptyStmt -- fn func(ast_decl ast.DeclStmt) ast.Ellipsis -- st ast.CompositeLit --} +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("Foo.go") +- env.FormatBuffer("Foo.go") +- want := env.BufferText("Foo.go") - --var _ = importedStruct{} //@codeactionedit("}", "refactor.rewrite", a32) +- if want == "" { +- t.Fatalf("Foo.go is empty") +- } - --type pointerBuiltinStruct struct { -- b *bool -- s *string -- i *int +- // In golang/go#57081, we observed that if overlay cases don't match, gopls +- // will find (and format) the on-disk contents rather than the overlay, +- // resulting in invalid edits. +- // +- // Verify that this doesn't happen, by confirming that formatting is +- // idempotent. +- env.FormatBuffer("Foo.go") +- got := env.BufferText("Foo.go") +- if diff := compare.Text(want, got); diff != "" { +- t.Errorf("invalid content after second formatting:\n%s", diff) +- } +- }) -} - --var _ = pointerBuiltinStruct{} //@codeactionedit("}", "refactor.rewrite", a33) +-// Test that we can find packages for open files with different spelling on +-// case-insensitive file systems. +-func TestPackageForMisspelledURI(t *testing.T) { +- t.Skip("golang/go#57081: this test fails because the Go command does not load Foo.go correctly") +- if runtime.GOOS != "windows" && runtime.GOOS != "darwin" { +- t.Skip("golang/go#57081 only reproduces on case-insensitive filesystems.") +- } +- const files = ` +--- go.mod -- +-module mod.test - --var _ = []ast.BasicLit{ -- {}, //@codeactionedit("}", "refactor.rewrite", a34) --} +-go 1.19 +--- foo.go -- +-package foo - --var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite", a35) ---- @a31/a3.go -- ----- before --+++ after --@@ -17 +17,4 @@ ---var _ = Bar{} //@codeactionedit("}", "refactor.rewrite", a31) --+var _ = Bar{ --+ X: &Foo{}, --+ Y: &Foo{}, --+} //@codeactionedit("}", "refactor.rewrite", a31) ---- @a32/a3.go -- ----- before --+++ after --@@ -28 +28,9 @@ ---var _ = importedStruct{} //@codeactionedit("}", "refactor.rewrite", a32) --+var _ = importedStruct{ --+ m: map[*ast.CompositeLit]ast.Field{}, --+ s: []ast.BadExpr{}, --+ a: [3]token.Token{}, --+ c: make(chan ast.EmptyStmt), --+ fn: func(ast_decl ast.DeclStmt) ast.Ellipsis { --+ }, --+ st: ast.CompositeLit{}, --+} //@codeactionedit("}", "refactor.rewrite", a32) ---- @a33/a3.go -- ----- before --+++ after --@@ -36 +36,5 @@ ---var _ = pointerBuiltinStruct{} //@codeactionedit("}", "refactor.rewrite", a33) --+var _ = pointerBuiltinStruct{ --+ b: new(bool), --+ s: new(string), --+ i: new(int), --+} //@codeactionedit("}", "refactor.rewrite", a33) ---- @a34/a3.go -- ----- before --+++ after --@@ -39 +39,5 @@ --- {}, //@codeactionedit("}", "refactor.rewrite", a34) --+ { --+ ValuePos: 0, --+ Kind: 0, --+ Value: "", --+ }, //@codeactionedit("}", "refactor.rewrite", a34) ---- @a35/a3.go -- ----- before --+++ after --@@ -42 +42,5 @@ ---var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite", a35) --+var _ = []ast.BasicLit{{ --+ ValuePos: 0, --+ Kind: 0, --+ Value: "", --+}} //@codeactionedit("}", "refactor.rewrite", a35) ---- a4.go -- --package fillstruct +-const C = D +--- bar.go -- +-package foo +- +-const D = 2 +-` +- +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("Foo.go") +- env.AfterChange(NoDiagnostics()) +- }) +-} +diff -urN a/gopls/internal/test/integration/workspace/multi_folder_test.go b/gopls/internal/test/integration/workspace/multi_folder_test.go +--- a/gopls/internal/test/integration/workspace/multi_folder_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/workspace/multi_folder_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,128 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --import "go/ast" +-package workspace - --type iStruct struct { -- X int --} +-import ( +- "testing" - --type sStruct struct { -- str string +- . "golang.org/x/tools/gopls/internal/test/integration" +-) +- +-// TODO(rfindley): update the marker tests to support the concept of multiple +-// workspace folders, and move this there. +-func TestMultiView_Diagnostics(t *testing.T) { +- // In the past, gopls would only diagnose one View at a time +- // (the last to have changed). +- // +- // This test verifies that gopls can maintain diagnostics for multiple Views. +- const files = ` +- +--- a/go.mod -- +-module golang.org/lsptests/a +- +-go 1.20 +--- a/a.go -- +-package a +- +-func _() { +- x := 1 // unused -} +--- b/go.mod -- +-module golang.org/lsptests/b - --type multiFill struct { -- num int -- strin string -- arr []int +-go 1.20 +--- b/b.go -- +-package b +- +-func _() { +- y := 2 // unused -} +-` - --type assignStruct struct { -- n ast.Node +- WithOptions( +- WorkspaceFolders("a", "b"), +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.OnceMet( +- InitialWorkspaceLoad, +- Diagnostics(env.AtRegexp("a/a.go", "x")), +- Diagnostics(env.AtRegexp("b/b.go", "y")), +- ) +- }) -} - --func fill() { -- var x int -- var _ = iStruct{} //@codeactionedit("}", "refactor.rewrite", a41) +-func TestMultiView_LocalReplace(t *testing.T) { +- // This is a regression test for #66145, where gopls attempted to load a +- // package in a locally replaced module as a workspace package, resulting in +- // spurious import diagnostics because the module graph had been pruned. - -- var s string -- var _ = sStruct{} //@codeactionedit("}", "refactor.rewrite", a42) +- const proxy = ` +--- example.com/c@v1.2.3/go.mod -- +-module example.com/c - -- var n int -- _ = []int{} -- if true { -- arr := []int{1, 2} -- } -- var _ = multiFill{} //@codeactionedit("}", "refactor.rewrite", a43) +-go 1.20 - -- var node *ast.CompositeLit -- var _ = assignStruct{} //@codeactionedit("}", "refactor.rewrite", a45) --} +--- example.com/c@v1.2.3/c.go -- +-package c - ---- @a41/a4.go -- ----- before --+++ after --@@ -25 +25,3 @@ --- var _ = iStruct{} //@codeactionedit("}", "refactor.rewrite", a41) --+ var _ = iStruct{ --+ X: x, --+ } //@codeactionedit("}", "refactor.rewrite", a41) ---- @a42/a4.go -- ----- before --+++ after --@@ -28 +28,3 @@ --- var _ = sStruct{} //@codeactionedit("}", "refactor.rewrite", a42) --+ var _ = sStruct{ --+ str: s, --+ } //@codeactionedit("}", "refactor.rewrite", a42) ---- @a43/a4.go -- ----- before --+++ after --@@ -35 +35,5 @@ --- var _ = multiFill{} //@codeactionedit("}", "refactor.rewrite", a43) --+ var _ = multiFill{ --+ num: n, --+ strin: s, --+ arr: []int{}, --+ } //@codeactionedit("}", "refactor.rewrite", a43) ---- @a45/a4.go -- ----- before --+++ after --@@ -38 +38,3 @@ --- var _ = assignStruct{} //@codeactionedit("}", "refactor.rewrite", a45) --+ var _ = assignStruct{ --+ n: node, --+ } //@codeactionedit("}", "refactor.rewrite", a45) ---- fill_struct.go -- --package fillstruct +-const C = 3 - --type StructA struct { -- unexportedIntField int -- ExportedIntField int -- MapA map[int]string -- Array []int -- StructB --} +-` +- // In the past, gopls would only diagnose one View at a time +- // (the last to have changed). +- // +- // This test verifies that gopls can maintain diagnostics for multiple Views. +- const files = ` +--- a/go.mod -- +-module golang.org/lsptests/a - --type StructA2 struct { -- B *StructB --} +-go 1.20 - --type StructA3 struct { -- B StructB --} +-require golang.org/lsptests/b v1.2.3 - --func fill() { -- a := StructA{} //@codeactionedit("}", "refactor.rewrite", fill_struct1) -- b := StructA2{} //@codeactionedit("}", "refactor.rewrite", fill_struct2) -- c := StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct3) -- if true { -- _ = StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct4) -- } --} +-replace golang.org/lsptests/b => ../b - ---- @fill_struct1/fill_struct.go -- ----- before --+++ after --@@ -20 +20,7 @@ --- a := StructA{} //@codeactionedit("}", "refactor.rewrite", fill_struct1) --+ a := StructA{ --+ unexportedIntField: 0, --+ ExportedIntField: 0, --+ MapA: map[int]string{}, --+ Array: []int{}, --+ StructB: StructB{}, --+ } //@codeactionedit("}", "refactor.rewrite", fill_struct1) ---- @fill_struct2/fill_struct.go -- ----- before --+++ after --@@ -21 +21,3 @@ --- b := StructA2{} //@codeactionedit("}", "refactor.rewrite", fill_struct2) --+ b := StructA2{ --+ B: &StructB{}, --+ } //@codeactionedit("}", "refactor.rewrite", fill_struct2) ---- @fill_struct3/fill_struct.go -- ----- before --+++ after --@@ -22 +22,3 @@ --- c := StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct3) --+ c := StructA3{ --+ B: StructB{}, --+ } //@codeactionedit("}", "refactor.rewrite", fill_struct3) ---- @fill_struct4/fill_struct.go -- ----- before --+++ after --@@ -24 +24,3 @@ --- _ = StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct4) --+ _ = StructA3{ --+ B: StructB{}, --+ } //@codeactionedit("}", "refactor.rewrite", fill_struct4) ---- fill_struct_anon.go -- --package fillstruct +--- a/a.go -- +-package a - --type StructAnon struct { -- a struct{} -- b map[string]interface{} -- c map[string]struct { -- d int -- e bool -- } --} +-import "golang.org/lsptests/b" - --func fill() { -- _ := StructAnon{} //@codeactionedit("}", "refactor.rewrite", fill_struct_anon) --} ---- @fill_struct_anon/fill_struct_anon.go -- ----- before --+++ after --@@ -13 +13,5 @@ --- _ := StructAnon{} //@codeactionedit("}", "refactor.rewrite", fill_struct_anon) --+ _ := StructAnon{ --+ a: struct{}{}, --+ b: map[string]interface{}{}, --+ c: map[string]struct{d int; e bool}{}, --+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_anon) ---- fill_struct_nested.go -- --package fillstruct +-const A = b.B - 1 - --type StructB struct { -- StructC --} +--- b/go.mod -- +-module golang.org/lsptests/b - --type StructC struct { -- unexportedInt int --} +-go 1.20 - --func nested() { -- c := StructB{ -- StructC: StructC{}, //@codeactionedit("}", "refactor.rewrite", fill_nested) -- } +-require example.com/c v1.2.3 +- +--- b/go.sum -- +-example.com/c v1.2.3 h1:hsOPhoHQLZPEn7l3kNya3fR3SfqW0/rafZMP8ave6fg= +-example.com/c v1.2.3/go.mod h1:4uG6Y5qX88LrEd4KfRoiguHZIbdLKUEHD1wXqPyrHcA= +--- b/b.go -- +-package b +- +-const B = 2 +- +--- b/unrelated/u.go -- +-package unrelated +- +-import "example.com/c" +- +-const U = c.C +-` +- +- WithOptions( +- WorkspaceFolders("a", "b"), +- ProxyFiles(proxy), +- ).Run(t, files, func(t *testing.T, env *Env) { +- // Opening unrelated first ensures that when we compute workspace packages +- // for the "a" workspace, it includes the unrelated package, which will be +- // unloadable from a as there is no a/go.sum. +- env.OpenFile("b/unrelated/u.go") +- env.AfterChange() +- env.OpenFile("a/a.go") +- env.AfterChange(NoDiagnostics()) +- }) -} +diff -urN a/gopls/internal/test/integration/workspace/quickfix_test.go b/gopls/internal/test/integration/workspace/quickfix_test.go +--- a/gopls/internal/test/integration/workspace/quickfix_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/workspace/quickfix_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,458 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - ---- @fill_nested/fill_struct_nested.go -- ----- before --+++ after --@@ -13 +13,3 @@ --- StructC: StructC{}, //@codeactionedit("}", "refactor.rewrite", fill_nested) --+ StructC: StructC{ --+ unexportedInt: 0, --+ }, //@codeactionedit("}", "refactor.rewrite", fill_nested) ---- fill_struct_package.go -- --package fillstruct +-package workspace - -import ( -- h2 "net/http" +- "strings" +- "testing" - -- "golang.org/lsptests/fillstruct/data" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/test/compare" +- +- . "golang.org/x/tools/gopls/internal/test/integration" -) - --func unexported() { -- a := data.B{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package1) -- _ = h2.Client{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package2) --} ---- @fill_struct_package1/fill_struct_package.go -- ----- before --+++ after --@@ -10 +10,3 @@ --- a := data.B{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package1) --+ a := data.B{ --+ ExportedInt: 0, --+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_package1) ---- @fill_struct_package2/fill_struct_package.go -- ----- before --+++ after --@@ -11 +11,7 @@ --- _ = h2.Client{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package2) --+ _ = h2.Client{ --+ Transport: nil, --+ CheckRedirect: func(req *h2.Request, via []*h2.Request) error { --+ }, --+ Jar: nil, --+ Timeout: 0, --+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_package2) ---- fill_struct_partial.go -- --package fillstruct +-func TestQuickFix_UseModule(t *testing.T) { +- t.Skip("temporary skip for golang/go#57979: with zero-config gopls these files are no longer orphaned") - --type StructPartialA struct { -- PrefilledInt int -- UnfilledInt int -- StructPartialB --} +- const files = ` +--- go.work -- +-go 1.20 - --type StructPartialB struct { -- PrefilledInt int -- UnfilledInt int --} +-use ( +- ./a +-) +--- a/go.mod -- +-module mod.com/a - --func fill() { -- a := StructPartialA{ -- PrefilledInt: 5, -- } //@codeactionedit("}", "refactor.rewrite", fill_struct_partial1) -- b := StructPartialB{ -- /* this comment should disappear */ -- PrefilledInt: 7, // This comment should be blown away. -- /* As should -- this one */ -- } //@codeactionedit("}", "refactor.rewrite", fill_struct_partial2) --} +-go 1.18 - ---- @fill_struct_partial1/fill_struct_partial.go -- ----- before --+++ after --@@ -16 +16,3 @@ --- PrefilledInt: 5, --+ PrefilledInt: 5, --+ UnfilledInt: 0, --+ StructPartialB: StructPartialB{}, ---- @fill_struct_partial2/fill_struct_partial.go -- ----- before --+++ after --@@ -19,4 +19,2 @@ --- /* this comment should disappear */ --+ PrefilledInt: 7, --- PrefilledInt: 7, // This comment should be blown away. --- /* As should --- this one */ --+ UnfilledInt: 0, ---- fill_struct_spaces.go -- --package fillstruct +--- a/main.go -- +-package main - --type StructD struct { -- ExportedIntField int --} +-import "mod.com/a/lib" - --func spaces() { -- d := StructD{} //@codeactionedit("}", "refactor.rewrite", fill_struct_spaces) +-func main() { +- _ = lib.C -} - ---- @fill_struct_spaces/fill_struct_spaces.go -- ----- before --+++ after --@@ -8 +8,3 @@ --- d := StructD{} //@codeactionedit("}", "refactor.rewrite", fill_struct_spaces) --+ d := StructD{ --+ ExportedIntField: 0, --+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_spaces) ---- fill_struct_unsafe.go -- --package fillstruct +--- a/lib/lib.go -- +-package lib - --import "unsafe" +-const C = "b" +--- b/go.mod -- +-module mod.com/b - --type unsafeStruct struct { -- x int -- p unsafe.Pointer --} +-go 1.18 - --func fill() { -- _ := unsafeStruct{} //@codeactionedit("}", "refactor.rewrite", fill_struct_unsafe) +--- b/main.go -- +-package main +- +-import "mod.com/b/lib" +- +-func main() { +- _ = lib.C -} - ---- @fill_struct_unsafe/fill_struct_unsafe.go -- ----- before --+++ after --@@ -11 +11,4 @@ --- _ := unsafeStruct{} //@codeactionedit("}", "refactor.rewrite", fill_struct_unsafe) --+ _ := unsafeStruct{ --+ x: 0, --+ p: nil, --+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_unsafe) ---- typeparams.go -- --package fillstruct +--- b/lib/lib.go -- +-package lib - --type emptyStructWithTypeParams[A any] struct{} +-const C = "b" +-` - --var _ = emptyStructWithTypeParams[int]{} // no suggested fix +- for _, title := range []string{ +- "Use this module", +- "Use all modules", +- } { +- t.Run(title, func(t *testing.T) { +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("b/main.go") +- var d protocol.PublishDiagnosticsParams +- env.AfterChange(ReadDiagnostics("b/main.go", &d)) +- fixes := env.GetQuickFixes("b/main.go", d.Diagnostics) +- var toApply []protocol.CodeAction +- for _, fix := range fixes { +- if strings.Contains(fix.Title, title) { +- toApply = append(toApply, fix) +- } +- } +- if len(toApply) != 1 { +- t.Fatalf("codeAction: got %d quick fixes matching %q, want 1; got: %v", len(toApply), title, toApply) +- } +- env.ApplyCodeAction(toApply[0]) +- env.AfterChange(NoDiagnostics()) +- want := `go 1.20 - --type basicStructWithTypeParams[T any] struct { -- foo T +-use ( +- ./a +- ./b +-) +-` +- got := env.ReadWorkspaceFile("go.work") +- if diff := compare.Text(want, got); diff != "" { +- t.Errorf("unexpeced go.work content:\n%s", diff) +- } +- }) +- }) +- } -} - --var _ = basicStructWithTypeParams[int]{} //@codeactionedit("}", "refactor.rewrite", typeparams1) +-func TestQuickFix_AddGoWork(t *testing.T) { +- t.Skip("temporary skip for golang/go#57979: with zero-config gopls these files are no longer orphaned") - --type twoArgStructWithTypeParams[F, B any] struct { -- foo F -- bar B --} +- const files = ` +--- a/go.mod -- +-module mod.com/a - --var _ = twoArgStructWithTypeParams[string, int]{} //@codeactionedit("}", "refactor.rewrite", typeparams2) +-go 1.18 - --var _ = twoArgStructWithTypeParams[int, string]{ -- bar: "bar", --} //@codeactionedit("}", "refactor.rewrite", typeparams3) +--- a/main.go -- +-package main - --type nestedStructWithTypeParams struct { -- bar string -- basic basicStructWithTypeParams[int] +-import "mod.com/a/lib" +- +-func main() { +- _ = lib.C -} - --var _ = nestedStructWithTypeParams{} //@codeactionedit("}", "refactor.rewrite", typeparams4) +--- a/lib/lib.go -- +-package lib - --func _[T any]() { -- type S struct{ t T } -- _ = S{} //@codeactionedit("}", "refactor.rewrite", typeparams5) +-const C = "b" +--- b/go.mod -- +-module mod.com/b +- +-go 1.18 +- +--- b/main.go -- +-package main +- +-import "mod.com/b/lib" +- +-func main() { +- _ = lib.C -} ---- @typeparams1/typeparams.go -- ----- before --+++ after --@@ -11 +11,3 @@ ---var _ = basicStructWithTypeParams[int]{} //@codeactionedit("}", "refactor.rewrite", typeparams1) --+var _ = basicStructWithTypeParams[int]{ --+ foo: 0, --+} //@codeactionedit("}", "refactor.rewrite", typeparams1) ---- @typeparams2/typeparams.go -- ----- before --+++ after --@@ -18 +18,4 @@ ---var _ = twoArgStructWithTypeParams[string, int]{} //@codeactionedit("}", "refactor.rewrite", typeparams2) --+var _ = twoArgStructWithTypeParams[string, int]{ --+ foo: "", --+ bar: 0, --+} //@codeactionedit("}", "refactor.rewrite", typeparams2) ---- @typeparams3/typeparams.go -- ----- before --+++ after --@@ -20 +20,2 @@ ---var _ = twoArgStructWithTypeParams[int, string]{ --+var _ = twoArgStructWithTypeParams[int, string]{ --+ foo: 0, ---- @typeparams4/typeparams.go -- ----- before --+++ after --@@ -29 +29,4 @@ ---var _ = nestedStructWithTypeParams{} //@codeactionedit("}", "refactor.rewrite", typeparams4) --+var _ = nestedStructWithTypeParams{ --+ bar: "", --+ basic: basicStructWithTypeParams{}, --+} //@codeactionedit("}", "refactor.rewrite", typeparams4) ---- @typeparams5/typeparams.go -- ----- before --+++ after --@@ -33 +33,3 @@ --- _ = S{} //@codeactionedit("}", "refactor.rewrite", typeparams5) --+ _ = S{ --+ t: *new(T), --+ } //@codeactionedit("}", "refactor.rewrite", typeparams5) -diff -urN a/gopls/internal/regtest/marker/testdata/codeaction/functionextraction_issue44813.txt b/gopls/internal/regtest/marker/testdata/codeaction/functionextraction_issue44813.txt ---- a/gopls/internal/regtest/marker/testdata/codeaction/functionextraction_issue44813.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/codeaction/functionextraction_issue44813.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,42 +0,0 @@ --This test verifies the fix for golang/go#44813: extraction failure when there --are blank identifiers. - ---- go.mod -- --module mod.test/extract +--- b/lib/lib.go -- +-package lib - --go 1.18 +-const C = "b" +-` - ---- p.go -- --package extract +- tests := []struct { +- name string +- file string +- title string +- want string // expected go.work content, excluding go directive line +- }{ +- { +- "use b", +- "b/main.go", +- "Add a go.work file using this module", +- ` +-use ./b +-`, +- }, +- { +- "use a", +- "a/main.go", +- "Add a go.work file using this module", +- ` +-use ./a +-`, +- }, +- { +- "use all", +- "a/main.go", +- "Add a go.work file using all modules", +- ` +-use ( +- ./a +- ./b +-) +-`, +- }, +- } - --import "fmt" +- for _, test := range tests { +- t.Run(test.name, func(t *testing.T) { +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile(test.file) +- var d protocol.PublishDiagnosticsParams +- env.AfterChange(ReadDiagnostics(test.file, &d)) +- fixes := env.GetQuickFixes(test.file, d.Diagnostics) +- var toApply []protocol.CodeAction +- for _, fix := range fixes { +- if strings.Contains(fix.Title, test.title) { +- toApply = append(toApply, fix) +- } +- } +- if len(toApply) != 1 { +- t.Fatalf("codeAction: got %d quick fixes matching %q, want 1; got: %v", len(toApply), test.title, toApply) +- } +- env.ApplyCodeAction(toApply[0]) +- env.AfterChange( +- NoDiagnostics(ForFile(test.file)), +- ) - --func main() { -- x := []rune{} //@codeaction("x", end, "refactor.extract", ext) -- s := "HELLO" -- for _, c := range s { -- x = append(x, c) -- } //@loc(end, "}") -- fmt.Printf("%x\n", x) +- got := env.ReadWorkspaceFile("go.work") +- // Ignore the `go` directive, which we assume is on the first line of +- // the go.work file. This allows the test to be independent of go version. +- got = strings.Join(strings.Split(got, "\n")[1:], "\n") +- if diff := compare.Text(test.want, got); diff != "" { +- t.Errorf("unexpected go.work content:\n%s", diff) +- } +- }) +- }) +- } -} - ---- @ext/p.go -- --package extract +-func TestQuickFix_UnsavedGoWork(t *testing.T) { +- t.Skip("temporary skip for golang/go#57979: with zero-config gopls these files are no longer orphaned") - --import "fmt" +- const files = ` +--- go.work -- +-go 1.21 - --func main() { -- //@codeaction("x", end, "refactor.extract", ext) -- x := newFunction() //@loc(end, "}") -- fmt.Printf("%x\n", x) --} +-use ( +- ./a +-) +--- a/go.mod -- +-module mod.com/a - --func newFunction() []rune { -- x := []rune{} -- s := "HELLO" -- for _, c := range s { -- x = append(x, c) -- } -- return x --} +-go 1.18 - -diff -urN a/gopls/internal/regtest/marker/testdata/codeaction/functionextraction.txt b/gopls/internal/regtest/marker/testdata/codeaction/functionextraction.txt ---- a/gopls/internal/regtest/marker/testdata/codeaction/functionextraction.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/codeaction/functionextraction.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,583 +0,0 @@ --This test verifies various behaviors of function extraction. +--- a/main.go -- +-package main - ---- go.mod -- --module mod.test/extract +-func main() {} +--- b/go.mod -- +-module mod.com/b - -go 1.18 - ---- basic.go -- --package extract +--- b/main.go -- +-package main - --func _() { //@codeaction("{", closeBracket, "refactor.extract", outer) -- a := 1 //@codeaction("a", end, "refactor.extract", inner) -- _ = a + 4 //@loc(end, "4") --} //@loc(closeBracket, "}") +-func main() {} +-` - ---- @inner/basic.go -- --package extract +- for _, title := range []string{ +- "Use this module", +- "Use all modules", +- } { +- t.Run(title, func(t *testing.T) { +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("go.work") +- env.OpenFile("b/main.go") +- env.RegexpReplace("go.work", "go 1.21", "go 1.21 // arbitrary comment") +- var d protocol.PublishDiagnosticsParams +- env.AfterChange(ReadDiagnostics("b/main.go", &d)) +- fixes := env.GetQuickFixes("b/main.go", d.Diagnostics) +- var toApply []protocol.CodeAction +- for _, fix := range fixes { +- if strings.Contains(fix.Title, title) { +- toApply = append(toApply, fix) +- } +- } +- if len(toApply) != 1 { +- t.Fatalf("codeAction: got %d quick fixes matching %q, want 1; got: %v", len(toApply), title, toApply) +- } +- fix := toApply[0] +- err := env.Editor.ApplyCodeAction(env.Ctx, fix) +- if err == nil { +- t.Fatalf("codeAction(%q) succeeded unexpectedly", fix.Title) +- } - --func _() { //@codeaction("{", closeBracket, "refactor.extract", outer) -- //@codeaction("a", end, "refactor.extract", inner) -- newFunction() //@loc(end, "4") +- if got := err.Error(); !strings.Contains(got, "must save") { +- t.Errorf("codeAction(%q) returned error %q, want containing \"must save\"", fix.Title, err) +- } +- }) +- }) +- } -} - --func newFunction() { -- a := 1 -- _ = a + 4 --} //@loc(closeBracket, "}") +-func TestQuickFix_GOWORKOff(t *testing.T) { +- t.Skip("temporary skip for golang/go#57979: with zero-config gopls these files are no longer orphaned") - ---- @outer/basic.go -- --package extract +- const files = ` +--- go.work -- +-go 1.21 - --func _() { //@codeaction("{", closeBracket, "refactor.extract", outer) -- //@codeaction("a", end, "refactor.extract", inner) -- newFunction() //@loc(end, "4") --} +-use ( +- ./a +-) +--- a/go.mod -- +-module mod.com/a - --func newFunction() { -- a := 1 -- _ = a + 4 --} //@loc(closeBracket, "}") +-go 1.18 - ---- return.go -- --package extract +--- a/main.go -- +-package main - --func _() bool { -- x := 1 -- if x == 0 { //@codeaction("if", ifend, "refactor.extract", return) -- return true -- } //@loc(ifend, "}") -- return false --} +-func main() {} +--- b/go.mod -- +-module mod.com/b - ---- @return/return.go -- --package extract +-go 1.18 - --func _() bool { -- x := 1 -- //@codeaction("if", ifend, "refactor.extract", return) -- shouldReturn, returnValue := newFunction(x) -- if shouldReturn { -- return returnValue -- } //@loc(ifend, "}") -- return false --} +--- b/main.go -- +-package main - --func newFunction(x int) (bool, bool) { -- if x == 0 { -- return true, true -- } -- return false, false --} +-func main() {} +-` - ---- return_nonnested.go -- --package extract +- for _, title := range []string{ +- "Use this module", +- "Use all modules", +- } { +- t.Run(title, func(t *testing.T) { +- WithOptions( +- EnvVars{"GOWORK": "off"}, +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("go.work") +- env.OpenFile("b/main.go") +- var d protocol.PublishDiagnosticsParams +- env.AfterChange(ReadDiagnostics("b/main.go", &d)) +- fixes := env.GetQuickFixes("b/main.go", d.Diagnostics) +- var toApply []protocol.CodeAction +- for _, fix := range fixes { +- if strings.Contains(fix.Title, title) { +- toApply = append(toApply, fix) +- } +- } +- if len(toApply) != 1 { +- t.Fatalf("codeAction: got %d quick fixes matching %q, want 1; got: %v", len(toApply), title, toApply) +- } +- fix := toApply[0] +- err := env.Editor.ApplyCodeAction(env.Ctx, fix) +- if err == nil { +- t.Fatalf("codeAction(%q) succeeded unexpectedly", fix.Title) +- } - --func _() bool { -- x := 1 //@codeaction("x", rnnEnd, "refactor.extract", rnn) -- if x == 0 { -- return true +- if got := err.Error(); !strings.Contains(got, "GOWORK=off") { +- t.Errorf("codeAction(%q) returned error %q, want containing \"GOWORK=off\"", fix.Title, err) +- } +- }) +- }) - } -- return false //@loc(rnnEnd, "false") -} - ---- @rnn/return_nonnested.go -- --package extract +-func TestStubMethods64087(t *testing.T) { +- // We can't use the @fix or @suggestedfixerr or @codeactionerr +- // because the error now reported by the corrected logic +- // is internal and silently causes no fix to be offered. +- // +- // See also the similar TestStubMethods64545 below. - --func _() bool { -- //@codeaction("x", rnnEnd, "refactor.extract", rnn) -- return newFunction() //@loc(rnnEnd, "false") --} +- const files = ` +-This is a regression test for a panic (issue #64087) in stub methods. - --func newFunction() bool { -- x := 1 -- if x == 0 { -- return true -- } -- return false --} +-The illegal expression int("") caused a "cannot convert" error that +-spuriously triggered the "stub methods" in a function whose return +-statement had too many operands, leading to an out-of-bounds index. - ---- return_complex.go -- --package extract +--- go.mod -- +-module mod.com +-go 1.18 - --import "fmt" +--- a.go -- +-package a - --func _() (int, string, error) { -- x := 1 -- y := "hello" -- z := "bye" //@codeaction("z", rcEnd, "refactor.extract", rc) -- if y == z { -- return x, y, fmt.Errorf("same") -- } else if false { -- z = "hi" -- return x, z, nil -- } //@loc(rcEnd, "}") -- return x, z, nil +-func f() error { +- return nil, myerror{int("")} -} - ---- @rc/return_complex.go -- --package extract +-type myerror struct{any} +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("a.go") - --import "fmt" +- // Expect a "wrong result count" diagnostic. +- var d protocol.PublishDiagnosticsParams +- env.AfterChange(ReadDiagnostics("a.go", &d)) +- +- // In no particular order, we expect: +- // "...too many return values..." (compiler) +- // "...cannot convert..." (compiler) +- // and possibly: +- // "...too many return values..." (fillreturns) +- // We check only for the first of these. +- found := false +- for i, diag := range d.Diagnostics { +- t.Logf("Diagnostics[%d] = %q (%s)", i, diag.Message, diag.Source) +- if strings.Contains(diag.Message, "too many return") { +- found = true +- } +- } +- if !found { +- t.Fatalf("Expected WrongResultCount diagnostic not found.") +- } - --func _() (int, string, error) { -- x := 1 -- y := "hello" -- //@codeaction("z", rcEnd, "refactor.extract", rc) -- z, shouldReturn, returnValue, returnValue1, returnValue2 := newFunction(y, x) -- if shouldReturn { -- return returnValue, returnValue1, returnValue2 -- } //@loc(rcEnd, "}") -- return x, z, nil --} +- // GetQuickFixes should not panic (the original bug). +- fixes := env.GetQuickFixes("a.go", d.Diagnostics) - --func newFunction(y string, x int) (string, bool, int, string, error) { -- z := "bye" -- if y == z { -- return "", true, x, y, fmt.Errorf("same") -- } else if false { -- z = "hi" -- return "", true, x, z, nil -- } -- return z, false, 0, "", nil +- // We should not be offered a "stub methods" fix. +- for _, fix := range fixes { +- if strings.Contains(fix.Title, "Implement error") { +- t.Errorf("unexpected 'stub methods' fix: %#v", fix) +- } +- } +- }) -} - ---- return_complex_nonnested.go -- --package extract +-func TestStubMethods64545(t *testing.T) { +- // We can't use the @fix or @suggestedfixerr or @codeactionerr +- // because the error now reported by the corrected logic +- // is internal and silently causes no fix to be offered. +- // +- // TODO(adonovan): we may need to generalize this test and +- // TestStubMethods64087 if this happens a lot. - --import "fmt" +- const files = ` +-This is a regression test for a panic (issue #64545) in stub methods. - --func _() (int, string, error) { -- x := 1 -- y := "hello" -- z := "bye" //@codeaction("z", rcnnEnd, "refactor.extract", rcnn) -- if y == z { -- return x, y, fmt.Errorf("same") -- } else if false { -- z = "hi" -- return x, z, nil -- } -- return x, z, nil //@loc(rcnnEnd, "nil") --} +-The illegal expression int("") caused a "cannot convert" error that +-spuriously triggered the "stub methods" in a function whose var +-spec had no RHS values, leading to an out-of-bounds index. - ---- @rcnn/return_complex_nonnested.go -- --package extract +--- go.mod -- +-module mod.com +-go 1.18 - --import "fmt" +--- a.go -- +-package a - --func _() (int, string, error) { -- x := 1 -- y := "hello" -- //@codeaction("z", rcnnEnd, "refactor.extract", rcnn) -- return newFunction(y, x) //@loc(rcnnEnd, "nil") --} +-var _ [int("")]byte +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("a.go") - --func newFunction(y string, x int) (int, string, error) { -- z := "bye" -- if y == z { -- return x, y, fmt.Errorf("same") -- } else if false { -- z = "hi" -- return x, z, nil -- } -- return x, z, nil --} +- // Expect a "cannot convert" diagnostic, and perhaps others. +- var d protocol.PublishDiagnosticsParams +- env.AfterChange(ReadDiagnostics("a.go", &d)) - ---- return_func_lit.go -- --package extract +- found := false +- for i, diag := range d.Diagnostics { +- t.Logf("Diagnostics[%d] = %q (%s)", i, diag.Message, diag.Source) +- if strings.Contains(diag.Message, "cannot convert") { +- found = true +- } +- } +- if !found { +- t.Fatalf("Expected 'cannot convert' diagnostic not found.") +- } - --import "go/ast" +- // GetQuickFixes should not panic (the original bug). +- fixes := env.GetQuickFixes("a.go", d.Diagnostics) - --func _() { -- ast.Inspect(ast.NewIdent("a"), func(n ast.Node) bool { -- if n == nil { //@codeaction("if", rflEnd, "refactor.extract", rfl) -- return true -- } //@loc(rflEnd, "}") -- return false +- // We should not be offered a "stub methods" fix. +- for _, fix := range fixes { +- if strings.Contains(fix.Title, "Implement error") { +- t.Errorf("unexpected 'stub methods' fix: %#v", fix) +- } +- } - }) -} +diff -urN a/gopls/internal/test/integration/workspace/standalone_test.go b/gopls/internal/test/integration/workspace/standalone_test.go +--- a/gopls/internal/test/integration/workspace/standalone_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/workspace/standalone_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,206 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - ---- @rfl/return_func_lit.go -- --package extract +-package workspace - --import "go/ast" +-import ( +- "sort" +- "testing" - --func _() { -- ast.Inspect(ast.NewIdent("a"), func(n ast.Node) bool { -- //@codeaction("if", rflEnd, "refactor.extract", rfl) -- shouldReturn, returnValue := newFunction(n) -- if shouldReturn { -- return returnValue -- } //@loc(rflEnd, "}") -- return false -- }) --} +- "github.com/google/go-cmp/cmp" +- "golang.org/x/tools/gopls/internal/protocol" +- . "golang.org/x/tools/gopls/internal/test/integration" +-) - --func newFunction(n ast.Node) (bool, bool) { -- if n == nil { -- return true, true -- } -- return false, false --} +-func TestStandaloneFiles(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.test - ---- return_func_lit_nonnested.go -- --package extract +-go 1.16 +--- lib/lib.go -- +-package lib - --import "go/ast" +-const K = 0 - --func _() { -- ast.Inspect(ast.NewIdent("a"), func(n ast.Node) bool { -- if n == nil { //@codeaction("if", rflnnEnd, "refactor.extract", rflnn) -- return true -- } -- return false //@loc(rflnnEnd, "false") -- }) +-type I interface { +- M() -} +--- lib/ignore.go -- +-//go:build ignore +-// +build ignore - ---- @rflnn/return_func_lit_nonnested.go -- --package extract +-package main - --import "go/ast" +-import ( +- "mod.test/lib" +-) - --func _() { -- ast.Inspect(ast.NewIdent("a"), func(n ast.Node) bool { -- //@codeaction("if", rflnnEnd, "refactor.extract", rflnn) -- return newFunction(n) //@loc(rflnnEnd, "false") -- }) --} +-const K = 1 - --func newFunction(n ast.Node) bool { -- if n == nil { -- return true -- } -- return false +-type Mer struct{} +-func (Mer) M() +- +-func main() { +- println(lib.K + K) -} +-` +- WithOptions( +- // On Go 1.17 and earlier, this test fails with +- // experimentalWorkspaceModule. Not investigated, as +- // experimentalWorkspaceModule will be removed. +- Modes(Default), +- ).Run(t, files, func(t *testing.T, env *Env) { +- // Initially, gopls should not know about the standalone file as it hasn't +- // been opened. Therefore, we should only find one symbol 'K'. +- // +- // (The choice of "K" is a little sleazy: it was originally "C" until +- // we started adding "unsafe" to the workspace unconditionally, which +- // caused a spurious match of "unsafe.Slice". But in practice every +- // workspace depends on unsafe.) +- syms := env.Symbol("K") +- if got, want := len(syms), 1; got != want { +- t.Errorf("got %d symbols, want %d (%+v)", got, want, syms) +- } - ---- return_init.go -- --package extract +- // Similarly, we should only find one reference to "K", and no +- // implementations of I. +- checkLocations := func(method string, gotLocations []protocol.Location, wantFiles ...string) { +- var gotFiles []string +- for _, l := range gotLocations { +- gotFiles = append(gotFiles, env.Sandbox.Workdir.URIToPath(l.URI)) +- } +- sort.Strings(gotFiles) +- sort.Strings(wantFiles) +- if diff := cmp.Diff(wantFiles, gotFiles); diff != "" { +- t.Errorf("%s(...): unexpected locations (-want +got):\n%s", method, diff) +- } +- } - --func _() string { -- x := 1 -- if x == 0 { //@codeaction("if", riEnd, "refactor.extract", ri) -- x = 3 -- return "a" -- } //@loc(riEnd, "}") -- x = 2 -- return "b" --} +- env.OpenFile("lib/lib.go") +- env.AfterChange(NoDiagnostics()) - ---- @ri/return_init.go -- --package extract +- // Replacing K with D should not cause any workspace diagnostics, since we +- // haven't yet opened the standalone file. +- env.RegexpReplace("lib/lib.go", "K", "D") +- env.AfterChange(NoDiagnostics()) +- env.RegexpReplace("lib/lib.go", "D", "K") +- env.AfterChange(NoDiagnostics()) - --func _() string { -- x := 1 -- //@codeaction("if", riEnd, "refactor.extract", ri) -- shouldReturn, returnValue := newFunction(x) -- if shouldReturn { -- return returnValue -- } //@loc(riEnd, "}") -- x = 2 -- return "b" --} +- refs := env.References(env.RegexpSearch("lib/lib.go", "K")) +- checkLocations("References", refs, "lib/lib.go") - --func newFunction(x int) (bool, string) { -- if x == 0 { -- x = 3 -- return true, "a" -- } -- return false, "" --} +- impls := env.Implementations(env.RegexpSearch("lib/lib.go", "I")) +- checkLocations("Implementations", impls) // no implementations +- +- // Opening the standalone file should not result in any diagnostics. +- env.OpenFile("lib/ignore.go") +- env.AfterChange(NoDiagnostics()) +- +- // Having opened the standalone file, we should find its symbols in the +- // workspace. +- syms = env.Symbol("K") +- if got, want := len(syms), 2; got != want { +- t.Fatalf("got %d symbols, want %d", got, want) +- } +- +- foundMainK := false +- var symNames []string +- for _, sym := range syms { +- symNames = append(symNames, sym.Name) +- if sym.Name == "main.K" { +- foundMainK = true +- } +- } +- if !foundMainK { +- t.Errorf("WorkspaceSymbol(\"K\") = %v, want containing main.K", symNames) +- } +- +- // We should resolve workspace definitions in the standalone file. +- fileLoc := env.GoToDefinition(env.RegexpSearch("lib/ignore.go", "lib.(K)")) +- file := env.Sandbox.Workdir.URIToPath(fileLoc.URI) +- if got, want := file, "lib/lib.go"; got != want { +- t.Errorf("GoToDefinition(lib.K) = %v, want %v", got, want) +- } - ---- return_init_nonnested.go -- --package extract +- // ...as well as intra-file definitions +- loc := env.GoToDefinition(env.RegexpSearch("lib/ignore.go", "\\+ (K)")) +- wantLoc := env.RegexpSearch("lib/ignore.go", "const (K)") +- if loc != wantLoc { +- t.Errorf("GoToDefinition(K) = %v, want %v", loc, wantLoc) +- } - --func _() string { -- x := 1 -- if x == 0 { //@codeaction("if", rinnEnd, "refactor.extract", rinn) -- x = 3 -- return "a" -- } -- x = 2 -- return "b" //@loc(rinnEnd, "\"b\"") --} +- // Renaming "lib.K" to "lib.D" should cause a diagnostic in the standalone +- // file. +- env.RegexpReplace("lib/lib.go", "K", "D") +- env.AfterChange(Diagnostics(env.AtRegexp("lib/ignore.go", "lib.(K)"))) - ---- @rinn/return_init_nonnested.go -- --package extract +- // Undoing the replacement should fix diagnostics +- env.RegexpReplace("lib/lib.go", "D", "K") +- env.AfterChange(NoDiagnostics()) - --func _() string { -- x := 1 -- //@codeaction("if", rinnEnd, "refactor.extract", rinn) -- return newFunction(x) //@loc(rinnEnd, "\"b\"") --} +- // Now that our workspace has no errors, we should be able to find +- // references and rename. +- refs = env.References(env.RegexpSearch("lib/lib.go", "K")) +- checkLocations("References", refs, "lib/lib.go", "lib/ignore.go") - --func newFunction(x int) string { -- if x == 0 { -- x = 3 -- return "a" -- } -- x = 2 -- return "b" --} +- impls = env.Implementations(env.RegexpSearch("lib/lib.go", "I")) +- checkLocations("Implementations", impls, "lib/ignore.go") - ---- args_returns.go -- --package extract +- // Renaming should rename in the standalone package. +- env.Rename(env.RegexpSearch("lib/lib.go", "K"), "D") +- env.RegexpSearch("lib/ignore.go", "lib.D") +- }) +-} - --func _() { -- a := 1 -- a = 5 //@codeaction("a", araend, "refactor.extract", ara) -- a = a + 2 //@loc(araend, "2") +-func TestStandaloneFiles_Configuration(t *testing.T) { +- const files = ` +--- go.mod -- +-module mod.test - -- b := a * 2 //@codeaction("b", arbend, "refactor.extract", arb) -- _ = b + 4 //@loc(arbend, "4") --} +-go 1.18 +--- lib.go -- +-package lib // without this package, files are loaded as command-line-arguments +--- ignore.go -- +-//go:build ignore +-// +build ignore - ---- @ara/args_returns.go -- --package extract +-package main - --func _() { -- a := 1 -- //@codeaction("a", araend, "refactor.extract", ara) -- a = newFunction(a) //@loc(araend, "2") +-// An arbitrary comment. - -- b := a * 2 //@codeaction("b", arbend, "refactor.extract", arb) -- _ = b + 4 //@loc(arbend, "4") --} +-func main() {} +--- standalone.go -- +-//go:build standalone +-// +build standalone - --func newFunction(a int) int { -- a = 5 -- a = a + 2 -- return a --} +-package main - ---- @arb/args_returns.go -- --package extract +-func main() {} +-` - --func _() { -- a := 1 -- a = 5 //@codeaction("a", araend, "refactor.extract", ara) -- a = a + 2 //@loc(araend, "2") +- WithOptions( +- Settings{ +- "standaloneTags": []string{"standalone", "script"}, +- }, +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("ignore.go") +- env.OpenFile("standalone.go") - -- //@codeaction("b", arbend, "refactor.extract", arb) -- newFunction(a) //@loc(arbend, "4") --} +- env.AfterChange( +- Diagnostics(env.AtRegexp("ignore.go", "package (main)")), +- NoDiagnostics(ForFile("standalone.go")), +- ) - --func newFunction(a int) { -- b := a * 2 -- _ = b + 4 +- cfg := env.Editor.Config() +- cfg.Settings = map[string]interface{}{ +- "standaloneTags": []string{"ignore"}, +- } +- env.ChangeConfiguration(cfg) +- env.AfterChange( +- NoDiagnostics(ForFile("ignore.go")), +- Diagnostics(env.AtRegexp("standalone.go", "package (main)")), +- ) +- }) -} +diff -urN a/gopls/internal/test/integration/workspace/std_test.go b/gopls/internal/test/integration/workspace/std_test.go +--- a/gopls/internal/test/integration/workspace/std_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/workspace/std_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,73 +0,0 @@ +-// Copyright 2024 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - ---- scope.go -- --package extract +-package workspace - --func _() { -- newFunction := 1 -- a := newFunction //@codeaction("a", "newFunction", "refactor.extract", scope) -- _ = a // avoid diagnostic --} +-import ( +- "os/exec" +- "path/filepath" +- "runtime" +- "strings" +- "testing" - --func newFunction1() int { -- return 1 --} +- . "golang.org/x/tools/gopls/internal/test/integration" +-) - ---- @scope/scope.go -- --package extract +-func TestStdWorkspace(t *testing.T) { +- // This test checks that we actually load workspace packages when opening +- // GOROOT. +- // +- // In golang/go#65801, we failed to do this because go/packages returns nil +- // Module for std and cmd. +- // +- // Because this test loads std as a workspace, it may be slow on smaller +- // builders. +- if testing.Short() { +- t.Skip("skipping with -short: loads GOROOT") +- } - --func _() { -- newFunction := 1 -- a := newFunction2(newFunction) //@codeaction("a", "newFunction", "refactor.extract", scope) -- _ = a // avoid diagnostic --} +- // The test also fails on Windows because an absolute path does not match +- // (likely a misspelling due to slashes). +- // TODO(rfindley): investigate and fix this on windows. +- if runtime.GOOS == "windows" { +- t.Skip("skipping on windows: fails to misspelled paths") +- } - --func newFunction2(newFunction int) int { -- a := newFunction -- return a +- // Query GOROOT. This is slightly more precise than e.g. runtime.GOROOT, as +- // it queries the go command in the environment. +- goroot, err := exec.Command("go", "env", "GOROOT").Output() +- if err != nil { +- t.Fatal(err) +- } +- stdDir := filepath.Join(strings.TrimSpace(string(goroot)), "src") +- WithOptions( +- Modes(Default), // This test may be slow. No reason to run it multiple times. +- WorkspaceFolders(stdDir), +- ).Run(t, "", func(t *testing.T, env *Env) { +- // Find parser.ParseFile. Query with `'` to get an exact match. +- syms := env.Symbol("'go/parser.ParseFile") +- if len(syms) != 1 { +- t.Fatalf("got %d symbols, want exactly 1. Symbols:\n%v", len(syms), syms) +- } +- parserPath := syms[0].Location.URI.Path() +- env.OpenFile(parserPath) +- +- // Find the reference to ast.File from the signature of ParseFile. This +- // helps guard against matching a comment. +- astFile := env.RegexpSearch(parserPath, `func ParseFile\(.*ast\.(File)`) +- refs := env.References(astFile) +- +- // If we've successfully loaded workspace packages for std, we should find +- // a reference in go/types. +- foundGoTypesReference := false +- for _, ref := range refs { +- if strings.Contains(string(ref.URI), "go/types") { +- foundGoTypesReference = true +- } +- } +- if !foundGoTypesReference { +- t.Errorf("references(ast.File) did not return a go/types reference. Refs:\n%v", refs) +- } +- }) -} +diff -urN a/gopls/internal/test/integration/workspace/vendor_test.go b/gopls/internal/test/integration/workspace/vendor_test.go +--- a/gopls/internal/test/integration/workspace/vendor_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/workspace/vendor_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,67 +0,0 @@ +-// Copyright 2024 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func newFunction1() int { -- return 1 --} +-package workspace - ---- smart_initialization.go -- --package extract +-import ( +- "testing" - --func _() { -- var a []int -- a = append(a, 2) //@codeaction("a", siEnd, "refactor.extract", si) -- b := 4 //@loc(siEnd, "4") -- a = append(a, b) --} +- . "golang.org/x/tools/gopls/internal/test/integration" +-) - ---- @si/smart_initialization.go -- --package extract +-func TestWorkspacePackagesExcludesVendor(t *testing.T) { +- // This test verifies that packages in the vendor directory are not workspace +- // packages. This would be an easy mistake for gopls to make, since mod +- // vendoring excludes go.mod files, and therefore the nearest go.mod file for +- // vendored packages is often the workspace mod file. +- const proxy = ` +--- other.com/b@v1.0.0/go.mod -- +-module other.com/b - --func _() { -- var a []int -- //@codeaction("a", siEnd, "refactor.extract", si) -- a, b := newFunction(a) //@loc(siEnd, "4") -- a = append(a, b) --} +-go 1.18 - --func newFunction(a []int) ([]int, int) { -- a = append(a, 2) -- b := 4 -- return a, b --} +--- other.com/b@v1.0.0/b.go -- +-package b - ---- smart_return.go -- --package extract +-type B int - -func _() { -- var b []int -- var a int -- a = 2 //@codeaction("a", srEnd, "refactor.extract", sr) -- b = []int{} -- b = append(b, a) //@loc(srEnd, ")") -- b[0] = 1 +- var V int // unused -} +-` +- const src = ` +--- go.mod -- +-module example.com/a +-go 1.14 +-require other.com/b v1.0.0 - ---- @sr/smart_return.go -- --package extract +--- go.sum -- +-other.com/b v1.0.0 h1:ct1+0RPozzMvA2rSYnVvIfr/GDHcd7oVnw147okdi3g= +-other.com/b v1.0.0/go.mod h1:bfTSZo/4ZtAQJWBYScopwW6n9Ctfsl2mi8nXsqjDXR8= - --func _() { -- var b []int -- var a int -- //@codeaction("a", srEnd, "refactor.extract", sr) -- b = newFunction(a, b) //@loc(srEnd, ")") -- b[0] = 1 --} +--- a.go -- +-package a - --func newFunction(a int, b []int) []int { -- a = 2 -- b = []int{} -- b = append(b, a) -- return b --} +-import "other.com/b" - ---- unnecessary_param.go -- --package extract +-var _ b.B - --func _() { -- var b []int -- a := 2 //@codeaction("a", upEnd, "refactor.extract", up) -- b = []int{} -- b = append(b, a) //@loc(upEnd, ")") -- b[0] = 1 -- if a == 2 { -- return -- } +-` +- WithOptions( +- ProxyFiles(proxy), +- Modes(Default), +- ).Run(t, src, func(t *testing.T, env *Env) { +- env.RunGoCommand("mod", "vendor") +- // Uncomment for updated go.sum contents. +- // env.DumpGoSum(".") +- env.OpenFile("a.go") +- env.AfterChange( +- NoDiagnostics(), // as b is not a workspace package +- ) +- env.GoToDefinition(env.RegexpSearch("a.go", `b\.(B)`)) +- env.AfterChange( +- Diagnostics(env.AtRegexp("vendor/other.com/b/b.go", "V"), WithMessage("not used")), +- ) +- }) -} +diff -urN a/gopls/internal/test/integration/workspace/workspace_test.go b/gopls/internal/test/integration/workspace/workspace_test.go +--- a/gopls/internal/test/integration/workspace/workspace_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/workspace/workspace_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,1355 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - ---- @up/unnecessary_param.go -- --package extract -- --func _() { -- var b []int -- //@codeaction("a", upEnd, "refactor.extract", up) -- a, b := newFunction(b) //@loc(upEnd, ")") -- b[0] = 1 -- if a == 2 { -- return -- } --} +-package workspace - --func newFunction(b []int) (int, []int) { -- a := 2 -- b = []int{} -- b = append(b, a) -- return a, b --} +-import ( +- "context" +- "fmt" +- "sort" +- "strings" +- "testing" - ---- comment.go -- --package extract +- "github.com/google/go-cmp/cmp" +- "golang.org/x/tools/gopls/internal/hooks" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/test/integration/fake" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/gopls/internal/util/goversion" +- "golang.org/x/tools/internal/gocommand" +- "golang.org/x/tools/internal/testenv" - --func _() { -- a := /* comment in the middle of a line */ 1 //@codeaction("a", commentEnd, "refactor.extract", comment1) -- // Comment on its own line //@codeaction("Comment", commentEnd, "refactor.extract", comment2) -- _ = a + 4 //@loc(commentEnd, "4"),codeaction("_", lastComment, "refactor.extract", comment3) -- // Comment right after 3 + 4 +- . "golang.org/x/tools/gopls/internal/test/integration" +-) - -- // Comment after with space //@loc(lastComment, "Comment") +-func TestMain(m *testing.M) { +- bug.PanicOnBugs = true +- Main(m, hooks.Options) -} - ---- @comment1/comment.go -- --package extract +-const workspaceProxy = ` +--- example.com@v1.2.3/go.mod -- +-module example.com - --func _() { -- /* comment in the middle of a line */ -- //@codeaction("a", commentEnd, "refactor.extract", comment1) -- // Comment on its own line //@codeaction("Comment", commentEnd, "refactor.extract", comment2) -- newFunction() //@loc(commentEnd, "4"),codeaction("_", lastComment, "refactor.extract", comment3) -- // Comment right after 3 + 4 +-go 1.12 +--- example.com@v1.2.3/blah/blah.go -- +-package blah - -- // Comment after with space //@loc(lastComment, "Comment") +-import "fmt" +- +-func SaySomething() { +- fmt.Println("something") -} +--- random.org@v1.2.3/go.mod -- +-module random.org - --func newFunction() { -- a := 1 +-go 1.12 +--- random.org@v1.2.3/bye/bye.go -- +-package bye - -- _ = a + 4 +-func Goodbye() { +- println("Bye") -} +-` - ---- @comment2/comment.go -- --package extract +-// TODO: Add a replace directive. +-const workspaceModule = ` +--- pkg/go.mod -- +-module mod.com - --func _() { -- a := /* comment in the middle of a line */ 1 //@codeaction("a", commentEnd, "refactor.extract", comment1) -- // Comment on its own line //@codeaction("Comment", commentEnd, "refactor.extract", comment2) -- newFunction(a) //@loc(commentEnd, "4"),codeaction("_", lastComment, "refactor.extract", comment3) -- // Comment right after 3 + 4 +-go 1.14 - -- // Comment after with space //@loc(lastComment, "Comment") --} +-require ( +- example.com v1.2.3 +- random.org v1.2.3 +-) +--- pkg/go.sum -- +-example.com v1.2.3 h1:veRD4tUnatQRgsULqULZPjeoBGFr2qBhevSCZllD2Ds= +-example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= +-random.org v1.2.3 h1:+JE2Fkp7gS0zsHXGEQJ7hraom3pNTlkxC4b2qPfA+/Q= +-random.org v1.2.3/go.mod h1:E9KM6+bBX2g5ykHZ9H27w16sWo3QwgonyjM44Dnej3I= +--- pkg/main.go -- +-package main - --func newFunction(a int) { -- _ = a + 4 +-import ( +- "example.com/blah" +- "mod.com/inner" +- "random.org/bye" +-) +- +-func main() { +- blah.SaySomething() +- inner.Hi() +- bye.Goodbye() -} +--- pkg/main2.go -- +-package main - ---- @comment3/comment.go -- --package extract +-import "fmt" - -func _() { -- a := /* comment in the middle of a line */ 1 //@codeaction("a", commentEnd, "refactor.extract", comment1) -- // Comment on its own line //@codeaction("Comment", commentEnd, "refactor.extract", comment2) -- newFunction(a) //@loc(commentEnd, "4"),codeaction("_", lastComment, "refactor.extract", comment3) -- // Comment right after 3 + 4 -- -- // Comment after with space //@loc(lastComment, "Comment") --} -- --func newFunction(a int) { -- _ = a + 4 +- fmt.Print("%s") -} +--- pkg/inner/inner.go -- +-package inner - ---- redefine.go -- --package extract -- --import "strconv" +-import "example.com/blah" - --func _() { -- i, err := strconv.Atoi("1") -- u, err := strconv.Atoi("2") //@codeaction("u", ")", "refactor.extract", redefine) -- if i == u || err == nil { -- return -- } +-func Hi() { +- blah.SaySomething() -} +--- goodbye/bye/bye.go -- +-package bye - ---- @redefine/redefine.go -- --package extract +-func Bye() {} +--- goodbye/go.mod -- +-module random.org - --import "strconv" +-go 1.12 +-` - --func _() { -- i, err := strconv.Atoi("1") -- u, err := newFunction() //@codeaction("u", ")", "refactor.extract", redefine) -- if i == u || err == nil { -- return +-// Confirm that find references returns all of the references in the module, +-// regardless of what the workspace root is. +-func TestReferences(t *testing.T) { +- for _, tt := range []struct { +- name, rootPath string +- }{ +- { +- name: "module root", +- rootPath: "pkg", +- }, +- { +- name: "subdirectory", +- rootPath: "pkg/inner", +- }, +- } { +- t.Run(tt.name, func(t *testing.T) { +- opts := []RunOption{ProxyFiles(workspaceProxy)} +- if tt.rootPath != "" { +- opts = append(opts, WorkspaceFolders(tt.rootPath)) +- } +- WithOptions(opts...).Run(t, workspaceModule, func(t *testing.T, env *Env) { +- f := "pkg/inner/inner.go" +- env.OpenFile(f) +- locations := env.References(env.RegexpSearch(f, `SaySomething`)) +- want := 3 +- if got := len(locations); got != want { +- t.Fatalf("expected %v locations, got %v", want, got) +- } +- }) +- }) - } -} - --func newFunction() (int, error) { -- u, err := strconv.Atoi("2") -- return u, err +-// Make sure that analysis diagnostics are cleared for the whole package when +-// the only opened file is closed. This test was inspired by the experience in +-// VS Code, where clicking on a reference result triggers a +-// textDocument/didOpen without a corresponding textDocument/didClose. +-func TestClearAnalysisDiagnostics(t *testing.T) { +- WithOptions( +- ProxyFiles(workspaceProxy), +- WorkspaceFolders("pkg/inner"), +- ).Run(t, workspaceModule, func(t *testing.T, env *Env) { +- env.OpenFile("pkg/main.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("pkg/main2.go", "fmt.Print")), +- ) +- env.CloseBuffer("pkg/main.go") +- env.AfterChange( +- NoDiagnostics(ForFile("pkg/main2.go")), +- ) +- }) -} - -diff -urN a/gopls/internal/regtest/marker/testdata/codeaction/imports.txt b/gopls/internal/regtest/marker/testdata/codeaction/imports.txt ---- a/gopls/internal/regtest/marker/testdata/codeaction/imports.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/codeaction/imports.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,175 +0,0 @@ --This test verifies the behavior of the 'source.organizeImports' code action. -- ---- go.mod -- --module mod.test/imports -- --go 1.18 -- ---- add.go -- --package imports //@codeaction("imports", "", "source.organizeImports", add) -- --import ( -- "fmt" --) -- --func _() { -- fmt.Println("") -- bytes.NewBuffer(nil) //@diag("bytes", re"(undeclared|undefined)") +-// TestReloadOnlyOnce checks that changes to the go.mod file do not result in +-// redundant package loads (golang/go#54473). +-// +-// Note that this test may be fragile, as it depends on specific structure to +-// log messages around reinitialization. Nevertheless, it is important for +-// guarding against accidentally duplicate reloading. +-func TestReloadOnlyOnce(t *testing.T) { +- WithOptions( +- ProxyFiles(workspaceProxy), +- WorkspaceFolders("pkg"), +- ).Run(t, workspaceModule, func(t *testing.T, env *Env) { +- dir := env.Sandbox.Workdir.URI("goodbye").Path() +- goModWithReplace := fmt.Sprintf(`%s +-replace random.org => %s +-`, env.ReadWorkspaceFile("pkg/go.mod"), dir) +- env.WriteWorkspaceFile("pkg/go.mod", goModWithReplace) +- env.Await( +- LogMatching(protocol.Info, `packages\.Load #\d+\n`, 2, false), +- ) +- }) -} - ---- @add/add.go -- --package imports //@codeaction("imports", "", "source.organizeImports", add) -- --import ( -- "bytes" -- "fmt" --) -- --func _() { -- fmt.Println("") -- bytes.NewBuffer(nil) //@diag("bytes", re"(undeclared|undefined)") --} +-const workspaceModuleProxy = ` +--- example.com@v1.2.3/go.mod -- +-module example.com - ---- good.go -- --package imports //@codeactionerr("imports", "", "source.organizeImports", re"found 0 CodeActions") +-go 1.12 +--- example.com@v1.2.3/blah/blah.go -- +-package blah - -import "fmt" - --func _() { --fmt.Println("") +-func SaySomething() { +- fmt.Println("something") -} +--- b.com@v1.2.3/go.mod -- +-module b.com - ---- issue35458.go -- +-go 1.12 +--- b.com@v1.2.3/b/b.go -- +-package b - +-func Hello() {} +-` - +-const multiModule = ` +--- moda/a/go.mod -- +-module a.com - +-require b.com v1.2.3 +--- moda/a/go.sum -- +-b.com v1.2.3 h1:tXrlXP0rnjRpKNmkbLYoWBdq0ikb3C3bKK9//moAWBI= +-b.com v1.2.3/go.mod h1:D+J7pfFBZK5vdIdZEFquR586vKKIkqG7Qjw9AxG5BQ8= +--- moda/a/a.go -- +-package a - +-import ( +- "b.com/b" +-) - --// package doc --package imports //@codeaction("imports", "", "source.organizeImports", issue35458) +-func main() { +- var x int +- _ = b.Hello() +-} +--- modb/go.mod -- +-module b.com - +--- modb/b/b.go -- +-package b - +-func Hello() int { +- var x int +-} +-` - +-func TestAutomaticWorkspaceModule_Interdependent(t *testing.T) { +- WithOptions( +- ProxyFiles(workspaceModuleProxy), +- ).Run(t, multiModule, func(t *testing.T, env *Env) { +- env.RunGoCommand("work", "init") +- env.RunGoCommand("work", "use", "-r", ".") +- env.AfterChange( +- Diagnostics(env.AtRegexp("moda/a/a.go", "x")), +- Diagnostics(env.AtRegexp("modb/b/b.go", "x")), +- NoDiagnostics(env.AtRegexp("moda/a/a.go", `"b.com/b"`)), +- ) +- }) +-} - +-func TestWorkspaceVendoring(t *testing.T) { +- testenv.NeedsGo1Point(t, 22) +- WithOptions( +- ProxyFiles(workspaceModuleProxy), +- ).Run(t, multiModule, func(t *testing.T, env *Env) { +- env.RunGoCommand("work", "init") +- env.RunGoCommand("work", "use", "moda/a") +- env.AfterChange() +- env.OpenFile("moda/a/a.go") +- env.RunGoCommand("work", "vendor") +- env.AfterChange() +- loc := env.GoToDefinition(env.RegexpSearch("moda/a/a.go", "b.(Hello)")) +- const want = "vendor/b.com/b/b.go" +- if got := env.Sandbox.Workdir.URIToPath(loc.URI); got != want { +- t.Errorf("Definition: got location %q, want %q", got, want) +- } +- }) +-} - +-func TestModuleWithExclude(t *testing.T) { +- const proxy = ` +--- c.com@v1.2.3/go.mod -- +-module c.com - --func _() { -- println("Hello, world!") --} +-go 1.12 - +-require b.com v1.2.3 +--- c.com@v1.2.3/blah/blah.go -- +-package blah - +-import "fmt" - +-func SaySomething() { +- fmt.Println("something") +-} +--- b.com@v1.2.3/go.mod -- +-module b.com - +-go 1.12 +--- b.com@v1.2.4/b/b.go -- +-package b - +-func Hello() {} +--- b.com@v1.2.4/go.mod -- +-module b.com - +-go 1.12 +-` +- const files = ` +--- go.mod -- +-module a.com - +-require c.com v1.2.3 - ---- @issue35458/issue35458.go -- --// package doc --package imports //@codeaction("imports", "", "source.organizeImports", issue35458) +-exclude b.com v1.2.3 +--- go.sum -- +-c.com v1.2.3 h1:n07Dz9fYmpNqvZMwZi5NEqFcSHbvLa9lacMX+/g25tw= +-c.com v1.2.3/go.mod h1:/4TyYgU9Nu5tA4NymP5xyqE8R2VMzGD3TbJCwCOvHAg= +--- main.go -- +-package a - +-func main() { +- var x int +-} +-` +- WithOptions( +- ProxyFiles(proxy), +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.OnceMet( +- InitialWorkspaceLoad, +- Diagnostics(env.AtRegexp("main.go", "x")), +- ) +- }) +-} - +-// This change tests that the version of the module used changes after it has +-// been deleted from the workspace. +-// +-// TODO(golang/go#55331): delete this placeholder along with experimental +-// workspace module. +-func TestDeleteModule_Interdependent(t *testing.T) { +- const multiModule = ` +--- go.work -- +-go 1.18 - +-use ( +- moda/a +- modb +-) +--- moda/a/go.mod -- +-module a.com - +-require b.com v1.2.3 +--- moda/a/go.sum -- +-b.com v1.2.3 h1:tXrlXP0rnjRpKNmkbLYoWBdq0ikb3C3bKK9//moAWBI= +-b.com v1.2.3/go.mod h1:D+J7pfFBZK5vdIdZEFquR586vKKIkqG7Qjw9AxG5BQ8= +--- moda/a/a.go -- +-package a - +-import ( +- "b.com/b" +-) - --func _() { -- println("Hello, world!") +-func main() { +- var x int +- _ = b.Hello() -} +--- modb/go.mod -- +-module b.com - +--- modb/b/b.go -- +-package b - +-func Hello() int { +- var x int +-} +-` +- WithOptions( +- ProxyFiles(workspaceModuleProxy), +- ).Run(t, multiModule, func(t *testing.T, env *Env) { +- env.OpenFile("moda/a/a.go") +- env.Await(env.DoneWithOpen()) - +- originalLoc := env.GoToDefinition(env.RegexpSearch("moda/a/a.go", "Hello")) +- original := env.Sandbox.Workdir.URIToPath(originalLoc.URI) +- if want := "modb/b/b.go"; !strings.HasSuffix(original, want) { +- t.Errorf("expected %s, got %v", want, original) +- } +- env.CloseBuffer(original) +- env.AfterChange() - +- env.RemoveWorkspaceFile("modb/b/b.go") +- env.RemoveWorkspaceFile("modb/go.mod") +- env.WriteWorkspaceFile("go.work", "go 1.18\nuse moda/a") +- env.AfterChange() - +- gotLoc := env.GoToDefinition(env.RegexpSearch("moda/a/a.go", "Hello")) +- got := env.Sandbox.Workdir.URIToPath(gotLoc.URI) +- if want := "b.com@v1.2.3/b/b.go"; !strings.HasSuffix(got, want) { +- t.Errorf("expected %s, got %v", want, got) +- } +- }) +-} - +-// Tests that the version of the module used changes after it has been added +-// to the workspace. +-func TestCreateModule_Interdependent(t *testing.T) { +- const multiModule = ` +--- go.work -- +-go 1.18 - +-use ( +- moda/a +-) +--- moda/a/go.mod -- +-module a.com - ---- multi.go -- --package imports //@codeaction("imports", "", "source.organizeImports", multi) -- --import "fmt" +-require b.com v1.2.3 +--- moda/a/go.sum -- +-b.com v1.2.3 h1:tXrlXP0rnjRpKNmkbLYoWBdq0ikb3C3bKK9//moAWBI= +-b.com v1.2.3/go.mod h1:D+J7pfFBZK5vdIdZEFquR586vKKIkqG7Qjw9AxG5BQ8= +--- moda/a/a.go -- +-package a - --import "bytes" //@diag("\"bytes\"", re"not used") +-import ( +- "b.com/b" +-) - --func _() { -- fmt.Println("") +-func main() { +- var x int +- _ = b.Hello() -} +-` +- WithOptions( +- ProxyFiles(workspaceModuleProxy), +- ).Run(t, multiModule, func(t *testing.T, env *Env) { +- env.OpenFile("moda/a/a.go") +- loc := env.GoToDefinition(env.RegexpSearch("moda/a/a.go", "Hello")) +- original := env.Sandbox.Workdir.URIToPath(loc.URI) +- if want := "b.com@v1.2.3/b/b.go"; !strings.HasSuffix(original, want) { +- t.Errorf("expected %s, got %v", want, original) +- } +- env.CloseBuffer(original) +- env.WriteWorkspaceFiles(map[string]string{ +- "go.work": `go 1.18 - ---- @multi/multi.go -- --package imports //@codeaction("imports", "", "source.organizeImports", multi) -- --import "fmt" -- --//@diag("\"bytes\"", re"not used") +-use ( +- moda/a +- modb +-) +-`, +- "modb/go.mod": "module b.com", +- "modb/b/b.go": `package b - --func _() { -- fmt.Println("") +-func Hello() int { +- var x int +-} +-`, +- }) +- env.AfterChange(Diagnostics(env.AtRegexp("modb/b/b.go", "x"))) +- gotLoc := env.GoToDefinition(env.RegexpSearch("moda/a/a.go", "Hello")) +- got := env.Sandbox.Workdir.URIToPath(gotLoc.URI) +- if want := "modb/b/b.go"; !strings.HasSuffix(got, want) { +- t.Errorf("expected %s, got %v", want, original) +- } +- }) -} - ---- needs.go -- --package imports //@codeaction("package", "", "source.organizeImports", needs) +-// This test confirms that a gopls workspace can recover from initialization +-// with one invalid module. +-func TestOneBrokenModule(t *testing.T) { +- const multiModule = ` +--- go.work -- +-go 1.18 - --func goodbye() { -- fmt.Printf("HI") //@diag("fmt", re"(undeclared|undefined)") -- log.Printf("byeeeee") //@diag("log", re"(undeclared|undefined)") --} +-use ( +- moda/a +- modb +-) +--- moda/a/go.mod -- +-module a.com - ---- @needs/needs.go -- --package imports //@codeaction("package", "", "source.organizeImports", needs) +-require b.com v1.2.3 +- +--- moda/a/a.go -- +-package a - -import ( -- "fmt" -- "log" +- "b.com/b" -) - --func goodbye() { -- fmt.Printf("HI") //@diag("fmt", re"(undeclared|undefined)") -- log.Printf("byeeeee") //@diag("log", re"(undeclared|undefined)") +-func main() { +- var x int +- _ = b.Hello() -} +--- modb/go.mod -- +-modul b.com // typo here - ---- remove.go -- --package imports //@codeaction("package", "", "source.organizeImports", remove) +--- modb/b/b.go -- +-package b - --import ( -- "bytes" //@diag("\"bytes\"", re"not used") -- "fmt" --) +-func Hello() int { +- var x int +-} +-` +- WithOptions( +- ProxyFiles(workspaceModuleProxy), +- ).Run(t, multiModule, func(t *testing.T, env *Env) { +- env.OpenFile("modb/go.mod") +- env.AfterChange( +- Diagnostics(AtPosition("modb/go.mod", 0, 0)), +- ) +- env.RegexpReplace("modb/go.mod", "modul", "module") +- env.SaveBufferWithoutActions("modb/go.mod") +- env.AfterChange( +- Diagnostics(env.AtRegexp("modb/b/b.go", "x")), +- ) +- }) +-} - --func _() { -- fmt.Println("") +-// TestBadGoWork exercises the panic from golang/vscode-go#2121. +-func TestBadGoWork(t *testing.T) { +- const files = ` +--- go.work -- +-use ./bar +--- bar/go.mod -- +-module example.com/bar +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("go.work") +- }) -} - ---- @remove/remove.go -- --package imports //@codeaction("package", "", "source.organizeImports", remove) +-func TestUseGoWork(t *testing.T) { +- // This test validates certain functionality related to using a go.work +- // file to specify workspace modules. +- const multiModule = ` +--- moda/a/go.mod -- +-module a.com +- +-require b.com v1.2.3 +--- moda/a/go.sum -- +-b.com v1.2.3 h1:tXrlXP0rnjRpKNmkbLYoWBdq0ikb3C3bKK9//moAWBI= +-b.com v1.2.3/go.mod h1:D+J7pfFBZK5vdIdZEFquR586vKKIkqG7Qjw9AxG5BQ8= +--- moda/a/a.go -- +-package a - -import ( -- "fmt" +- "b.com/b" -) - --func _() { -- fmt.Println("") +-func main() { +- var x int +- _ = b.Hello() -} +--- modb/go.mod -- +-module b.com - ---- removeall.go -- --package imports //@codeaction("package", "", "source.organizeImports", removeall) +-require example.com v1.2.3 +--- modb/go.sum -- +-example.com v1.2.3 h1:Yryq11hF02fEf2JlOS2eph+ICE2/ceevGV3C9dl5V/c= +-example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= +--- modb/b/b.go -- +-package b - --import ( -- "bytes" //@diag("\"bytes\"", re"not used") -- "fmt" //@diag("\"fmt\"", re"not used") +-func Hello() int { +- var x int +-} +--- go.work -- +-go 1.17 - +-use ( +- ./moda/a -) +-` +- WithOptions( +- ProxyFiles(workspaceModuleProxy), +- Settings{ +- "subdirWatchPatterns": "on", +- }, +- ).Run(t, multiModule, func(t *testing.T, env *Env) { +- // Initially, the go.work should cause only the a.com module to be loaded, +- // so we shouldn't get any file watches for modb. Further validate this by +- // jumping to a definition in b.com and ensuring that we go to the module +- // cache. +- env.OnceMet( +- InitialWorkspaceLoad, +- NoFileWatchMatching("modb"), +- ) +- env.OpenFile("moda/a/a.go") +- env.Await(env.DoneWithOpen()) - --func _() { --} +- // To verify which modules are loaded, we'll jump to the definition of +- // b.Hello. +- checkHelloLocation := func(want string) error { +- loc := env.GoToDefinition(env.RegexpSearch("moda/a/a.go", "Hello")) +- file := env.Sandbox.Workdir.URIToPath(loc.URI) +- if !strings.HasSuffix(file, want) { +- return fmt.Errorf("expected %s, got %v", want, file) +- } +- return nil +- } - ---- @removeall/removeall.go -- --package imports //@codeaction("package", "", "source.organizeImports", removeall) +- // Initially this should be in the module cache, as b.com is not replaced. +- if err := checkHelloLocation("b.com@v1.2.3/b/b.go"); err != nil { +- t.Fatal(err) +- } - --//@diag("\"fmt\"", re"not used") +- // Now, modify the go.work file on disk to activate the b.com module in +- // the workspace. +- env.WriteWorkspaceFile("go.work", ` +-go 1.17 - --func _() { --} +-use ( +- ./moda/a +- ./modb +-) +-`) - ---- twolines.go -- --package imports --func main() {} //@codeactionerr("main", "", "source.organizeImports", re"found 0") -diff -urN a/gopls/internal/regtest/marker/testdata/codeaction/infertypeargs.txt b/gopls/internal/regtest/marker/testdata/codeaction/infertypeargs.txt ---- a/gopls/internal/regtest/marker/testdata/codeaction/infertypeargs.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/codeaction/infertypeargs.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,38 +0,0 @@ --This test verifies the infertypeargs refactoring. +- // As of golang/go#54069, writing go.work to the workspace triggers a +- // workspace reload, and new file watches. +- env.AfterChange( +- Diagnostics(env.AtRegexp("modb/b/b.go", "x")), +- // TODO(golang/go#60340): we don't get a file watch yet, because +- // updateWatchedDirectories runs before snapshot.load. Instead, we get it +- // after the next change (the didOpen below). +- // FileWatchMatching("modb"), +- ) - ---- flags -- ---min_go=go1.18 +- // Jumping to definition should now go to b.com in the workspace. +- if err := checkHelloLocation("modb/b/b.go"); err != nil { +- t.Fatal(err) +- } - ---- go.mod -- --module mod.test/infertypeargs +- // Now, let's modify the go.work *overlay* (not on disk), and verify that +- // this change is only picked up once it is saved. +- env.OpenFile("go.work") +- env.AfterChange( +- // TODO(golang/go#60340): delete this expectation in favor of +- // the commented-out expectation above, once we fix the evaluation order +- // of file watches. We should not have to wait for a second change to get +- // the correct watches. +- FileWatchMatching("modb"), +- ) +- env.SetBufferContent("go.work", `go 1.17 - --go 1.18 +-use ( +- ./moda/a +-)`) - ---- p.go -- --package infertypeargs +- // Simply modifying the go.work file does not cause a reload, so we should +- // still jump within the workspace. +- // +- // TODO: should editing the go.work above cause modb diagnostics to be +- // suppressed? +- env.Await(env.DoneWithChange()) +- if err := checkHelloLocation("modb/b/b.go"); err != nil { +- t.Fatal(err) +- } - --func app[S interface{ ~[]E }, E interface{}](s S, e E) S { -- return append(s, e) --} +- // Saving should reload the workspace. +- env.SaveBufferWithoutActions("go.work") +- if err := checkHelloLocation("b.com@v1.2.3/b/b.go"); err != nil { +- t.Fatal(err) +- } - --func _() { -- _ = app[[]int] -- _ = app[[]int, int] -- _ = app[[]int]([]int{}, 0) //@codeaction("app", ")", "refactor.rewrite", infer) -- _ = app([]int{}, 0) --} +- // This fails if guarded with a OnceMet(DoneWithSave(), ...), because it is +- // delayed (and therefore not synchronous with the change). +- // +- // Note: this check used to assert on NoDiagnostics, but with zero-config +- // gopls we still have diagnostics. +- env.Await(Diagnostics(ForFile("modb/go.mod"), WithMessage("example.com is not used"))) - ---- @infer/p.go -- --package infertypeargs +- // Test Formatting. +- env.SetBufferContent("go.work", `go 1.18 +- use ( - --func app[S interface{ ~[]E }, E interface{}](s S, e E) S { -- return append(s, e) --} - --func _() { -- _ = app[[]int] -- _ = app[[]int, int] -- _ = app([]int{}, 0) //@codeaction("app", ")", "refactor.rewrite", infer) -- _ = app([]int{}, 0) +- +- ./moda/a +-) +-`) // TODO(matloob): For some reason there's a "start position 7:0 is out of bounds" error when the ")" is on the last character/line in the file. Rob probably knows what's going on. +- env.SaveBuffer("go.work") +- env.Await(env.DoneWithSave()) +- gotWorkContents := env.ReadWorkspaceFile("go.work") +- wantWorkContents := `go 1.18 +- +-use ( +- ./moda/a +-) +-` +- if gotWorkContents != wantWorkContents { +- t.Fatalf("formatted contents of workspace: got %q; want %q", gotWorkContents, wantWorkContents) +- } +- }) -} - -diff -urN a/gopls/internal/regtest/marker/testdata/codeaction/inline.txt b/gopls/internal/regtest/marker/testdata/codeaction/inline.txt ---- a/gopls/internal/regtest/marker/testdata/codeaction/inline.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/codeaction/inline.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,23 +0,0 @@ --This is a minimal test of the refactor.inline code action. -- ---- go.mod -- --module testdata/codeaction +-func TestUseGoWorkDiagnosticMissingModule(t *testing.T) { +- const files = ` +--- go.work -- -go 1.18 - ---- a/a.go -- --package a -- --func _() { -- println(add(1, 2)) //@codeaction("add", ")", "refactor.inline", inline) +-use ./foo +--- bar/go.mod -- +-module example.com/bar +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("go.work") +- env.AfterChange( +- Diagnostics(env.AtRegexp("go.work", "use"), WithMessage("directory ./foo does not contain a module")), +- ) +- // The following tests is a regression test against an issue where we weren't +- // copying the workFile struct field on workspace when a new one was created in +- // (*workspace).invalidate. Set the buffer content to a working file so that +- // invalidate recognizes the workspace to be change and copies over the workspace +- // struct, and then set the content back to the old contents to make sure +- // the diagnostic still shows up. +- env.SetBufferContent("go.work", "go 1.18 \n\n use ./bar\n") +- env.AfterChange( +- NoDiagnostics(env.AtRegexp("go.work", "use")), +- ) +- env.SetBufferContent("go.work", "go 1.18 \n\n use ./foo\n") +- env.AfterChange( +- Diagnostics(env.AtRegexp("go.work", "use"), WithMessage("directory ./foo does not contain a module")), +- ) +- }) -} - --func add(x, y int) int { return x + y } -- ---- @inline/a/a.go -- --package a +-func TestUseGoWorkDiagnosticSyntaxError(t *testing.T) { +- const files = ` +--- go.work -- +-go 1.18 - --func _() { -- println(1 + 2) //@codeaction("add", ")", "refactor.inline", inline) +-usa ./foo +-replace +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("go.work") +- env.AfterChange( +- Diagnostics(env.AtRegexp("go.work", "usa"), WithMessage("unknown directive: usa")), +- Diagnostics(env.AtRegexp("go.work", "replace"), WithMessage("usage: replace")), +- ) +- }) -} - --func add(x, y int) int { return x + y } -diff -urN a/gopls/internal/regtest/marker/testdata/codeaction/removeparam_formatting.txt b/gopls/internal/regtest/marker/testdata/codeaction/removeparam_formatting.txt ---- a/gopls/internal/regtest/marker/testdata/codeaction/removeparam_formatting.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/codeaction/removeparam_formatting.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,55 +0,0 @@ --This test exercises behavior of change signature refactoring with respect to --comments. -- --Currently, inline comments around arguments or parameters are dropped, which is --probably acceptable. Fixing this is likely intractible without fixing comment --representation in the AST. -- ---- go.mod -- --module unused.mod -- +-func TestUseGoWorkHover(t *testing.T) { +- const files = ` +--- go.work -- -go 1.18 - ---- a/a.go -- --package a +-use ./foo +-use ( +- ./bar +- ./bar/baz +-) +--- foo/go.mod -- +-module example.com/foo +--- bar/go.mod -- +-module example.com/bar +--- bar/baz/go.mod -- +-module example.com/bar/baz +-` +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("go.work") - --// A doc comment. --func A(x /* used parameter */, unused int /* unused parameter */ ) int { //@codeaction("unused", "unused", "refactor.rewrite", a) -- // about to return -- return x // returning -- // just returned --} +- tcs := map[string]string{ +- `\./foo`: "example.com/foo", +- `(?m)\./bar$`: "example.com/bar", +- `\./bar/baz`: "example.com/bar/baz", +- } - --// This function makes calls. --func _() { -- // about to call -- A(one() /* used arg */, 2 /* unused arg */) // calling -- // just called +- for hoverRE, want := range tcs { +- got, _ := env.Hover(env.RegexpSearch("go.work", hoverRE)) +- if got.Value != want { +- t.Errorf(`hover on %q: got %q, want %q`, hoverRE, got, want) +- } +- } +- }) -} - --func one() int { -- // I should be unaffected! -- return 1 --} +-func TestExpandToGoWork(t *testing.T) { +- const workspace = ` +--- moda/a/go.mod -- +-module a.com - ---- @a/a/a.go -- +-require b.com v1.2.3 +--- moda/a/a.go -- -package a - --// A doc comment. --func A(x int) int { //@codeaction("unused", "unused", "refactor.rewrite", a) -- // about to return -- return x // returning -- // just returned --} +-import ( +- "b.com/b" +-) - --// This function makes calls. --func _() { -- // about to call -- A(one()) // calling -- // just called +-func main() { +- var x int +- _ = b.Hello() -} +--- modb/go.mod -- +-module b.com - --func one() int { -- // I should be unaffected! -- return 1 +-require example.com v1.2.3 +--- modb/b/b.go -- +-package b +- +-func Hello() int { +- var x int -} -diff -urN a/gopls/internal/regtest/marker/testdata/codeaction/removeparam_funcvalue.txt b/gopls/internal/regtest/marker/testdata/codeaction/removeparam_funcvalue.txt ---- a/gopls/internal/regtest/marker/testdata/codeaction/removeparam_funcvalue.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/codeaction/removeparam_funcvalue.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,19 +0,0 @@ --This test exercises change signature refactoring handling of function values. +--- go.work -- +-go 1.17 - --TODO(rfindley): use a literalization strategy to allow these references. +-use ( +- ./moda/a +- ./modb +-) +-` +- WithOptions( +- WorkspaceFolders("moda/a"), +- ).Run(t, workspace, func(t *testing.T, env *Env) { +- env.OpenFile("moda/a/a.go") +- env.Await(env.DoneWithOpen()) +- loc := env.GoToDefinition(env.RegexpSearch("moda/a/a.go", "Hello")) +- file := env.Sandbox.Workdir.URIToPath(loc.URI) +- want := "modb/b/b.go" +- if !strings.HasSuffix(file, want) { +- t.Errorf("expected %s, got %v", want, file) +- } +- }) +-} - +-func TestInnerGoWork(t *testing.T) { +- // This test checks that gopls honors a go.work file defined +- // inside a go module (golang/go#63917). +- const workspace = ` --- go.mod -- --module unused.mod +-module a.com - +-require b.com v1.2.3 +--- a/go.work -- -go 1.18 - +-use ( +- .. +- ../b +-) --- a/a.go -- -package a - --func A(x, unused int) int { //@codeactionerr("unused", "unused", "refactor.rewrite", re"non-call function reference") -- return x --} -- --func _() { -- _ = A --} -diff -urN a/gopls/internal/regtest/marker/testdata/codeaction/removeparam_imports.txt b/gopls/internal/regtest/marker/testdata/codeaction/removeparam_imports.txt ---- a/gopls/internal/regtest/marker/testdata/codeaction/removeparam_imports.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/codeaction/removeparam_imports.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,160 +0,0 @@ --This test checks the behavior of removing a parameter with respect to various --import scenarios. -- ---- go.mod -- --module mod.test -- --go 1.21 -- +-import "b.com/b" - ---- a/a1.go -- --package a +-var _ = b.B +--- b/go.mod -- +-module b.com/b - --import "mod.test/b" +--- b/b.go -- +-package b - --func _() { -- b.B(<-b.Chan, <-b.Chan) +-const B = 0 +-` +- WithOptions( +- // This doesn't work if we open the outer module. I'm not sure it should, +- // since the go.work file does not apply to the entire module, just a +- // subdirectory. +- WorkspaceFolders("a"), +- ).Run(t, workspace, func(t *testing.T, env *Env) { +- env.OpenFile("a/a.go") +- loc := env.GoToDefinition(env.RegexpSearch("a/a.go", "b.(B)")) +- got := env.Sandbox.Workdir.URIToPath(loc.URI) +- want := "b/b.go" +- if got != want { +- t.Errorf("Definition(b.B): got %q, want %q", got, want) +- } +- }) -} - ---- a/a2.go -- --package a +-func TestNonWorkspaceFileCreation(t *testing.T) { +- const files = ` +--- work/go.mod -- +-module mod.com - --import "mod.test/b" +-go 1.12 +--- work/x.go -- +-package x +-` - --func _() { -- b.B(<-b.Chan, <-b.Chan) -- b.B(<-b.Chan, <-b.Chan) +- const code = ` +-package foo +-import "fmt" +-var _ = fmt.Printf +-` +- WithOptions( +- WorkspaceFolders("work"), // so that outside/... is outside the workspace +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.CreateBuffer("outside/foo.go", "") +- env.EditBuffer("outside/foo.go", fake.NewEdit(0, 0, 0, 0, code)) +- env.GoToDefinition(env.RegexpSearch("outside/foo.go", `Printf`)) +- }) -} - ---- a/a3.go -- --package a +-func TestGoWork_V2Module(t *testing.T) { +- // When using a go.work, we must have proxy content even if it is replaced. +- const proxy = ` +--- b.com/v2@v2.1.9/go.mod -- +-module b.com/v2 - --import "mod.test/b" +-go 1.12 +--- b.com/v2@v2.1.9/b/b.go -- +-package b - --func _() { -- b.B(<-b.Chan, <-b.Chan) +-func Ciao()() int { +- return 0 -} +-` - --func _() { -- b.B(<-b.Chan, <-b.Chan) --} +- const multiModule = ` +--- go.work -- +-go 1.18 - ---- a/a4.go -- +-use ( +- moda/a +- modb +- modb/v2 +- modc +-) +--- moda/a/go.mod -- +-module a.com +- +-require b.com/v2 v2.1.9 +--- moda/a/a.go -- -package a - --// TODO(rfindley/adonovan): inlining here adds an additional import of --// mod.test/b. Can we do better? -import ( -- . "mod.test/b" +- "b.com/v2/b" -) - --func _() { -- B(<-Chan, <-Chan) +-func main() { +- var x int +- _ = b.Hi() -} +--- modb/go.mod -- +-module b.com - ---- b/b.go -- +--- modb/b/b.go -- -package b - --import "mod.test/c" -- --var Chan chan c.C -- --func B(x, y c.C) { //@codeaction("x", "x", "refactor.rewrite", b) +-func Hello() int { +- var x int -} +--- modb/v2/go.mod -- +-module b.com/v2 - ---- c/c.go -- --package c -- --type C int -- ---- d/d.go -- --package d +--- modb/v2/b/b.go -- +-package b - --// Removing the parameter should remove this import. --import "mod.test/c" +-func Hi() int { +- var x int +-} +--- modc/go.mod -- +-module gopkg.in/yaml.v1 // test gopkg.in versions +--- modc/main.go -- +-package main - --func D(x c.C) { //@codeaction("x", "x", "refactor.rewrite", d) +-func main() { +- var x int -} +-` - --func _() { -- D(1) +- WithOptions( +- ProxyFiles(proxy), +- ).Run(t, multiModule, func(t *testing.T, env *Env) { +- env.OnceMet( +- InitialWorkspaceLoad, +- // TODO(rfindley): assert on the full set of diagnostics here. We +- // should ensure that we don't have a diagnostic at b.Hi in a.go. +- Diagnostics(env.AtRegexp("moda/a/a.go", "x")), +- Diagnostics(env.AtRegexp("modb/b/b.go", "x")), +- Diagnostics(env.AtRegexp("modb/v2/b/b.go", "x")), +- Diagnostics(env.AtRegexp("modc/main.go", "x")), +- ) +- }) -} - ---- @b/a/a1.go -- --package a +-// Confirm that a fix for a tidy module will correct all modules in the +-// workspace. +-func TestMultiModule_OneBrokenModule(t *testing.T) { +- // In the earlier 'experimental workspace mode', gopls would aggregate go.sum +- // entries for the workspace module, allowing it to correctly associate +- // missing go.sum with diagnostics. With go.work files, this doesn't work: +- // the go.command will happily write go.work.sum. +- t.Skip("golang/go#57509: go.mod diagnostics do not work in go.work mode") +- const files = ` +--- go.work -- +-go 1.18 - --import ( -- "mod.test/b" -- "mod.test/c" +-use ( +- a +- b -) +--- go.work.sum -- +--- a/go.mod -- +-module a.com - --func _() { -- var _ c.C = <-b.Chan -- b.B(<-b.Chan) --} ---- @b/a/a2.go -- --package a +-go 1.12 +--- a/main.go -- +-package main +--- b/go.mod -- +-module b.com - --import ( -- "mod.test/b" -- "mod.test/c" +-go 1.12 +- +-require ( +- example.com v1.2.3 -) +--- b/go.sum -- +--- b/main.go -- +-package b - --func _() { -- var _ c.C = <-b.Chan -- b.B(<-b.Chan) -- var _ c.C = <-b.Chan -- b.B(<-b.Chan) +-import "example.com/blah" +- +-func main() { +- blah.Hello() +-} +-` +- WithOptions( +- ProxyFiles(workspaceProxy), +- ).Run(t, files, func(t *testing.T, env *Env) { +- params := &protocol.PublishDiagnosticsParams{} +- env.OpenFile("b/go.mod") +- env.AfterChange( +- Diagnostics( +- env.AtRegexp("go.mod", `example.com v1.2.3`), +- WithMessage("go.sum is out of sync"), +- ), +- ReadDiagnostics("b/go.mod", params), +- ) +- for _, d := range params.Diagnostics { +- if !strings.Contains(d.Message, "go.sum is out of sync") { +- continue +- } +- actions := env.GetQuickFixes("b/go.mod", []protocol.Diagnostic{d}) +- if len(actions) != 2 { +- t.Fatalf("expected 2 code actions, got %v", len(actions)) +- } +- env.ApplyQuickFixes("b/go.mod", []protocol.Diagnostic{d}) +- } +- env.AfterChange( +- NoDiagnostics(ForFile("b/go.mod")), +- ) +- }) -} ---- @b/a/a3.go -- --package a - --import ( -- "mod.test/b" -- "mod.test/c" --) +-// Tests the fix for golang/go#52500. +-func TestChangeTestVariant_Issue52500(t *testing.T) { +- const src = ` +--- go.mod -- +-module mod.test - --func _() { -- var _ c.C = <-b.Chan -- b.B(<-b.Chan) --} +-go 1.12 +--- main_test.go -- +-package main_test - --func _() { -- var _ c.C = <-b.Chan -- b.B(<-b.Chan) --} ---- @b/a/a4.go -- --package a +-type Server struct{} - --// TODO(rfindley/adonovan): inlining here adds an additional import of --// mod.test/b. Can we do better? --import ( -- "mod.test/b" -- . "mod.test/b" -- "mod.test/c" --) +-const mainConst = otherConst +--- other_test.go -- +-package main_test - --func _() { -- var _ c.C = <-Chan -- b.B(<-Chan) --} ---- @b/b/b.go -- --package b +-const otherConst = 0 - --import "mod.test/c" +-func (Server) Foo() {} +-` - --var Chan chan c.C +- Run(t, src, func(t *testing.T, env *Env) { +- env.OpenFile("other_test.go") +- env.RegexpReplace("other_test.go", "main_test", "main") - --func B(y c.C) { //@codeaction("x", "x", "refactor.rewrite", b) +- // For this test to function, it is necessary to wait on both of the +- // expectations below: the bug is that when switching the package name in +- // other_test.go from main->main_test, metadata for main_test is not marked +- // as invalid. So we need to wait for the metadata of main_test.go to be +- // updated before moving other_test.go back to the main_test package. +- env.Await( +- Diagnostics(env.AtRegexp("other_test.go", "Server")), +- Diagnostics(env.AtRegexp("main_test.go", "otherConst")), +- ) +- env.RegexpReplace("other_test.go", "main", "main_test") +- env.AfterChange( +- NoDiagnostics(ForFile("other_test.go")), +- NoDiagnostics(ForFile("main_test.go")), +- ) +- +- // This will cause a test failure if other_test.go is not in any package. +- _ = env.GoToDefinition(env.RegexpSearch("other_test.go", "Server")) +- }) -} ---- @d/d/d.go -- --package d - --// Removing the parameter should remove this import. +-// Test for golang/go#48929. +-func TestClearNonWorkspaceDiagnostics(t *testing.T) { +- const ws = ` +--- go.work -- +-go 1.18 - --func D() { //@codeaction("x", "x", "refactor.rewrite", d) +-use ( +- ./b +-) +--- a/go.mod -- +-module a +- +-go 1.17 +--- a/main.go -- +-package main +- +-func main() { +- var V string -} +--- b/go.mod -- +-module b - --func _() { -- D() +-go 1.17 +--- b/main.go -- +-package b +- +-import ( +- _ "fmt" +-) +-` +- Run(t, ws, func(t *testing.T, env *Env) { +- env.OpenFile("b/main.go") +- env.AfterChange( +- NoDiagnostics(ForFile("a/main.go")), +- ) +- env.OpenFile("a/main.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/main.go", "V"), WithMessage("not used")), +- ) +- // Here, diagnostics are added because of zero-config gopls. +- // In the past, they were added simply due to diagnosing changed files. +- // (see TestClearNonWorkspaceDiagnostics_NoView below for a +- // reimplementation of that test). +- if got, want := len(env.Views()), 2; got != want { +- t.Errorf("after opening a/main.go, got %d views, want %d", got, want) +- } +- env.CloseBuffer("a/main.go") +- env.AfterChange( +- NoDiagnostics(ForFile("a/main.go")), +- ) +- if got, want := len(env.Views()), 1; got != want { +- t.Errorf("after closing a/main.go, got %d views, want %d", got, want) +- } +- }) -} -diff -urN a/gopls/internal/regtest/marker/testdata/codeaction/removeparam.txt b/gopls/internal/regtest/marker/testdata/codeaction/removeparam.txt ---- a/gopls/internal/regtest/marker/testdata/codeaction/removeparam.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/codeaction/removeparam.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,246 +0,0 @@ --This test exercises the refactoring to remove unused parameters. - ---- go.mod -- --module unused.mod +-// This test is like TestClearNonWorkspaceDiagnostics, but bypasses the +-// zero-config algorithm by opening a nested workspace folder. +-// +-// We should still compute diagnostics correctly for open packages. +-func TestClearNonWorkspaceDiagnostics_NoView(t *testing.T) { +- const ws = ` +--- a/go.mod -- +-module example.com/a - -go 1.18 - ---- a/a.go -- --package a +-require example.com/b v1.2.3 - --func A(x, unused int) int { //@codeaction("unused", "unused", "refactor.rewrite", a) -- return x --} +-replace example.com/b => ../b - ---- @a/a/a.go -- +--- a/a.go -- -package a - --func A(x int) int { //@codeaction("unused", "unused", "refactor.rewrite", a) -- return x --} -- ---- a/a2.go -- --package a +-import "example.com/b" - -func _() { -- A(1, 2) +- V := b.B // unused -} - ---- a/a_test.go -- --package a +--- b/go.mod -- +-module b - --func _() { -- A(1, 2) --} +-go 1.18 - ---- a/a_x_test.go -- --package a_test +--- b/b.go -- +-package b - --import "unused.mod/a" +-const B = 2 - -func _() { -- a.A(1, 2) +- var V int // unused -} - ---- b/b.go -- +--- b/b2.go -- -package b - --import "unused.mod/a" +-const B2 = B - --func f() int { -- return 1 --} +--- c/c.go -- +-package main - --func g() int { -- return 2 +-func main() { +- var V int // unused -} +-` +- WithOptions( +- WorkspaceFolders("a"), +- ).Run(t, ws, func(t *testing.T, env *Env) { +- env.OpenFile("a/a.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/a.go", "V"), WithMessage("not used")), +- NoDiagnostics(ForFile("b/b.go")), +- NoDiagnostics(ForFile("c/c.go")), +- ) +- env.OpenFile("b/b.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/a.go", "V"), WithMessage("not used")), +- Diagnostics(env.AtRegexp("b/b.go", "V"), WithMessage("not used")), +- NoDiagnostics(ForFile("c/c.go")), +- ) - --func _() { -- a.A(f(), 1) --} +- // Opening b/b.go should not result in a new view, because b is not +- // contained in a workspace folder. +- // +- // Yet we should get diagnostics for b, because it is open. +- if got, want := len(env.Views()), 1; got != want { +- t.Errorf("after opening b/b.go, got %d views, want %d", got, want) +- } +- env.CloseBuffer("b/b.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/a.go", "V"), WithMessage("not used")), +- NoDiagnostics(ForFile("b/b.go")), +- NoDiagnostics(ForFile("c/c.go")), +- ) - ---- @a/a/a2.go -- --package a +- // We should get references in the b package. +- bUse := env.RegexpSearch("a/a.go", `b\.(B)`) +- refs := env.References(bUse) +- wantRefs := []string{"a/a.go", "b/b.go", "b/b2.go"} +- var gotRefs []string +- for _, ref := range refs { +- gotRefs = append(gotRefs, env.Sandbox.Workdir.URIToPath(ref.URI)) +- } +- sort.Strings(gotRefs) +- if diff := cmp.Diff(wantRefs, gotRefs); diff != "" { +- t.Errorf("references(b.B) mismatch (-want +got)\n%s", diff) +- } - --func _() { -- A(1) --} ---- @a/a/a_test.go -- --package a +- // Opening c/c.go should also not result in a new view, yet we should get +- // orphaned file diagnostics. +- env.OpenFile("c/c.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/a.go", "V"), WithMessage("not used")), +- NoDiagnostics(ForFile("b/b.go")), +- Diagnostics(env.AtRegexp("c/c.go", "V"), WithMessage("not used")), +- ) +- if got, want := len(env.Views()), 1; got != want { +- t.Errorf("after opening b/b.go, got %d views, want %d", got, want) +- } - --func _() { -- A(1) +- env.CloseBuffer("c/c.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/a.go", "V"), WithMessage("not used")), +- NoDiagnostics(ForFile("b/b.go")), +- NoDiagnostics(ForFile("c/c.go")), +- ) +- env.CloseBuffer("a/a.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("a/a.go", "V"), WithMessage("not used")), +- NoDiagnostics(ForFile("b/b.go")), +- NoDiagnostics(ForFile("c/c.go")), +- ) +- }) -} ---- @a/a/a_x_test.go -- --package a_test - --import "unused.mod/a" +-// Test that we don't get a version warning when the Go version in PATH is +-// supported. +-func TestOldGoNotification_SupportedVersion(t *testing.T) { +- v := goVersion(t) +- if v < goversion.OldestSupported() { +- t.Skipf("go version 1.%d is unsupported", v) +- } - --func _() { -- a.A(1) +- Run(t, "", func(t *testing.T, env *Env) { +- env.OnceMet( +- InitialWorkspaceLoad, +- NoShownMessage("upgrade"), +- ) +- }) -} ---- @a/b/b.go -- --package b - --import "unused.mod/a" +-// Test that we do get a version warning when the Go version in PATH is +-// unsupported, though this test may never execute if we stop running CI at +-// legacy Go versions (see also TestOldGoNotification_Fake) +-func TestOldGoNotification_UnsupportedVersion(t *testing.T) { +- v := goVersion(t) +- if v >= goversion.OldestSupported() { +- t.Skipf("go version 1.%d is supported", v) +- } - --func f() int { -- return 1 +- Run(t, "", func(t *testing.T, env *Env) { +- env.Await( +- // Note: cannot use OnceMet(InitialWorkspaceLoad, ...) here, as the +- // upgrade message may race with the IWL. +- ShownMessage("Please upgrade"), +- ) +- }) -} - --func g() int { -- return 2 --} +-func TestOldGoNotification_Fake(t *testing.T) { +- // Get the Go version from path, and make sure it's unsupported. +- // +- // In the future we'll stop running CI on legacy Go versions. By mutating the +- // oldest supported Go version here, we can at least ensure that the +- // ShowMessage pop-up works. +- ctx := context.Background() +- version, err := gocommand.GoVersion(ctx, gocommand.Invocation{}, &gocommand.Runner{}) +- if err != nil { +- t.Fatal(err) +- } +- defer func(t []goversion.Support) { +- goversion.Supported = t +- }(goversion.Supported) +- goversion.Supported = []goversion.Support{ +- {GoVersion: version, InstallGoplsVersion: "v1.0.0"}, +- } - --func _() { -- a.A(f()) +- Run(t, "", func(t *testing.T, env *Env) { +- env.Await( +- // Note: cannot use OnceMet(InitialWorkspaceLoad, ...) here, as the +- // upgrade message may race with the IWL. +- ShownMessage("Please upgrade"), +- ) +- }) -} ---- field/field.go -- --package field - --func Field(x int, field int) { //@codeaction("int", "int", "refactor.rewrite", field) +-// goVersion returns the version of the Go command in PATH. +-func goVersion(t *testing.T) int { +- t.Helper() +- ctx := context.Background() +- goversion, err := gocommand.GoVersion(ctx, gocommand.Invocation{}, &gocommand.Runner{}) +- if err != nil { +- t.Fatal(err) +- } +- return goversion -} - --func _() { -- Field(1, 2) +-func TestGoworkMutation(t *testing.T) { +- WithOptions( +- ProxyFiles(workspaceModuleProxy), +- ).Run(t, multiModule, func(t *testing.T, env *Env) { +- env.RunGoCommand("work", "init") +- env.RunGoCommand("work", "use", "-r", ".") +- env.AfterChange( +- Diagnostics(env.AtRegexp("moda/a/a.go", "x")), +- Diagnostics(env.AtRegexp("modb/b/b.go", "x")), +- NoDiagnostics(env.AtRegexp("moda/a/a.go", `b\.Hello`)), +- ) +- env.RunGoCommand("work", "edit", "-dropuse", "modb") +- env.Await( +- Diagnostics(env.AtRegexp("moda/a/a.go", "x")), +- NoDiagnostics(env.AtRegexp("modb/b/b.go", "x")), +- Diagnostics(env.AtRegexp("moda/a/a.go", `b\.Hello`)), +- ) +- }) -} ---- @field/field/field.go -- --package field +diff -urN a/gopls/internal/test/integration/workspace/zero_config_test.go b/gopls/internal/test/integration/workspace/zero_config_test.go +--- a/gopls/internal/test/integration/workspace/zero_config_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/workspace/zero_config_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,326 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func Field(field int) { //@codeaction("int", "int", "refactor.rewrite", field) --} +-package workspace - --func _() { -- Field(2) --} ---- ellipsis/ellipsis.go -- --package ellipsis +-import ( +- "strings" +- "testing" - --func Ellipsis(...any) { //@codeaction("any", "any", "refactor.rewrite", ellipsis) --} +- "github.com/google/go-cmp/cmp" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/protocol/command" - --func _() { -- // TODO(rfindley): investigate the broken formatting resulting from these inlinings. -- Ellipsis() -- Ellipsis(1) -- Ellipsis(1, 2) -- Ellipsis(1, f(), g()) -- Ellipsis(h()) -- Ellipsis(i()...) --} +- . "golang.org/x/tools/gopls/internal/test/integration" +-) - --func f() int --func g() int --func h() (int, int) --func i() []any +-func TestAddAndRemoveGoWork(t *testing.T) { +- // Use a workspace with a module in the root directory to exercise the case +- // where a go.work is added to the existing root directory. This verifies +- // that we're detecting changes to the module source, not just the root +- // directory. +- const nomod = ` +--- go.mod -- +-module a.com - ---- @ellipsis/ellipsis/ellipsis.go -- --package ellipsis +-go 1.16 +--- main.go -- +-package main - --func Ellipsis() { //@codeaction("any", "any", "refactor.rewrite", ellipsis) --} +-func main() {} +--- b/go.mod -- +-module b.com - --func _() { -- // TODO(rfindley): investigate the broken formatting resulting from these inlinings. -- Ellipsis() -- Ellipsis() -- Ellipsis() -- var _ []any = []any{1, f(), g()} -- Ellipsis() -- func(_ ...any) { -- Ellipsis() -- }(h()) -- var _ []any = i() -- Ellipsis() --} +-go 1.16 +--- b/main.go -- +-package main - --func f() int --func g() int --func h() (int, int) --func i() []any ---- ellipsis2/ellipsis2.go -- --package ellipsis2 +-func main() {} +-` +- WithOptions( +- Modes(Default), +- ).Run(t, nomod, func(t *testing.T, env *Env) { +- env.OpenFile("main.go") +- env.OpenFile("b/main.go") - --func Ellipsis2(_, _ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2) --} +- summary := func(typ cache.ViewType, root, folder string) command.View { +- return command.View{ +- Type: typ.String(), +- Root: env.Sandbox.Workdir.URI(root), +- Folder: env.Sandbox.Workdir.URI(folder), +- } +- } +- checkViews := func(want ...command.View) { +- got := env.Views() +- if diff := cmp.Diff(want, got); diff != "" { +- t.Errorf("SummarizeViews() mismatch (-want +got):\n%s", diff) +- } +- } - --func _() { -- Ellipsis2(1,2,3) -- Ellipsis2(h()) -- Ellipsis2(1,2, []int{3, 4}...) --} +- // Zero-config gopls makes this work. +- env.AfterChange( +- NoDiagnostics(ForFile("main.go")), +- NoDiagnostics(env.AtRegexp("b/main.go", "package (main)")), +- ) +- checkViews(summary(cache.GoModView, ".", "."), summary(cache.GoModView, "b", ".")) - --func h() (int, int) +- env.WriteWorkspaceFile("go.work", `go 1.16 - ---- @ellipsis2/ellipsis2/ellipsis2.go -- --package ellipsis2 +-use ( +- . +- b +-) +-`) +- env.AfterChange(NoDiagnostics()) +- checkViews(summary(cache.GoWorkView, ".", ".")) - --func Ellipsis2(_ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2) --} +- // Removing the go.work file should put us back where we started. +- env.RemoveWorkspaceFile("go.work") - --func _() { -- Ellipsis2(2, []int{3}...) -- func(_, blank0 int, rest ...int) { -- Ellipsis2(blank0, rest...) -- }(h()) -- Ellipsis2(2, []int{3, 4}...) --} +- // Again, zero-config gopls makes this work. +- env.AfterChange( +- NoDiagnostics(ForFile("main.go")), +- NoDiagnostics(env.AtRegexp("b/main.go", "package (main)")), +- ) +- checkViews(summary(cache.GoModView, ".", "."), summary(cache.GoModView, "b", ".")) - --func h() (int, int) ---- overlapping/overlapping.go -- --package overlapping +- // Close and reopen b, to ensure the views are adjusted accordingly. +- env.CloseBuffer("b/main.go") +- env.AfterChange() +- checkViews(summary(cache.GoModView, ".", ".")) - --func Overlapping(i int) int { //@codeactionerr(re"(i) int", re"(i) int", "refactor.rewrite", re"overlapping") -- return 0 +- env.OpenFile("b/main.go") +- env.AfterChange() +- checkViews(summary(cache.GoModView, ".", "."), summary(cache.GoModView, "b", ".")) +- }) -} - --func _() { -- x := Overlapping(Overlapping(0)) -- _ = x --} +-func TestOpenAndClosePorts(t *testing.T) { +- // This test checks that as we open and close files requiring a different +- // port, the set of Views is adjusted accordingly. +- const files = ` +--- go.mod -- +-module a.com/a - ---- effects/effects.go -- --package effects +-go 1.20 - --func effects(x, y int) int { //@codeaction("y", "y", "refactor.rewrite", effects) -- return x --} +--- a_linux.go -- +-package a - --func f() int --func g() int +--- a_darwin.go -- +-package a - --func _() { -- effects(f(), g()) -- effects(f(), g()) --} ---- @effects/effects/effects.go -- --package effects +--- a_windows.go -- +-package a +-` - --func effects(x int) int { //@codeaction("y", "y", "refactor.rewrite", effects) -- return x +- WithOptions( +- EnvVars{ +- "GOOS": "linux", // assume that linux is the default GOOS +- }, +- ).Run(t, files, func(t *testing.T, env *Env) { +- summary := func(envOverlay ...string) command.View { +- return command.View{ +- Type: cache.GoModView.String(), +- Root: env.Sandbox.Workdir.URI("."), +- Folder: env.Sandbox.Workdir.URI("."), +- EnvOverlay: envOverlay, +- } +- } +- checkViews := func(want ...command.View) { +- got := env.Views() +- if diff := cmp.Diff(want, got); diff != "" { +- t.Errorf("SummarizeViews() mismatch (-want +got):\n%s", diff) +- } +- } +- checkViews(summary()) +- env.OpenFile("a_linux.go") +- checkViews(summary()) +- env.OpenFile("a_darwin.go") +- checkViews( +- summary(), +- summary("GOARCH=amd64", "GOOS=darwin"), +- ) +- env.OpenFile("a_windows.go") +- checkViews( +- summary(), +- summary("GOARCH=amd64", "GOOS=darwin"), +- summary("GOARCH=amd64", "GOOS=windows"), +- ) +- env.CloseBuffer("a_darwin.go") +- checkViews( +- summary(), +- summary("GOARCH=amd64", "GOOS=windows"), +- ) +- env.CloseBuffer("a_linux.go") +- checkViews( +- summary(), +- summary("GOARCH=amd64", "GOOS=windows"), +- ) +- env.CloseBuffer("a_windows.go") +- checkViews(summary()) +- }) -} - --func f() int --func g() int +-func TestCriticalErrorsInOrphanedFiles(t *testing.T) { +- // This test checks that as we open and close files requiring a different +- // port, the set of Views is adjusted accordingly. +- const files = ` +--- go.mod -- +-modul golang.org/lsptests/broken - --func _() { -- var x, _ int = f(), g() -- effects(x) -- { -- var x, _ int = f(), g() -- effects(x) -- } --} ---- recursive/recursive.go -- --package recursive +-go 1.20 - --func Recursive(x int) int { //@codeaction("x", "x", "refactor.rewrite", recursive) -- return Recursive(1) --} +--- a.go -- +-package broken - ---- @recursive/recursive/recursive.go -- --package recursive +-const C = 0 +-` - --func Recursive() int { //@codeaction("x", "x", "refactor.rewrite", recursive) -- return Recursive() +- Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("a.go") +- env.AfterChange( +- Diagnostics(env.AtRegexp("go.mod", "modul")), +- Diagnostics(env.AtRegexp("a.go", "broken"), WithMessage("initialization failed")), +- ) +- }) -} -diff -urN a/gopls/internal/regtest/marker/testdata/codelens/generate.txt b/gopls/internal/regtest/marker/testdata/codelens/generate.txt ---- a/gopls/internal/regtest/marker/testdata/codelens/generate.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/codelens/generate.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,9 +0,0 @@ --This test exercises the "generate" codelens. - ---- generate.go -- --//@codelenses() +-func TestGoModReplace(t *testing.T) { +- // This test checks that we treat locally replaced modules as workspace +- // modules, according to the "includeReplaceInWorkspace" setting. +- const files = ` +--- moda/go.mod -- +-module golang.org/a - --package generate +-require golang.org/b v1.2.3 - --//go:generate echo Hi //@ codelens("//go:generate", "run go generate"), codelens("//go:generate", "run go generate ./...") --//go:generate echo I shall have no CodeLens -diff -urN a/gopls/internal/regtest/marker/testdata/codelens/test.txt b/gopls/internal/regtest/marker/testdata/codelens/test.txt ---- a/gopls/internal/regtest/marker/testdata/codelens/test.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/codelens/test.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,31 +0,0 @@ --This file tests codelenses for test functions. +-replace golang.org/b => ../modb - --TODO: for some reason these code lens have zero width. Does that affect their --utility/visibility in various LSP clients? +-go 1.20 - ---- settings.json -- --{ -- "codelenses": { -- "test": true -- } --} +--- moda/a.go -- +-package a - ---- p_test.go -- --//@codelenses() +-import "golang.org/b" - --package codelens //@codelens(re"()package codelens", "run file benchmarks") +-const A = b.B - --import "testing" +--- modb/go.mod -- +-module golang.org/b - --func TestMain(m *testing.M) {} // no code lens for TestMain +-go 1.20 - --func TestFuncWithCodeLens(t *testing.T) { //@codelens(re"()func", "run test") --} +--- modb/b.go -- +-package b - --func thisShouldNotHaveACodeLens(t *testing.T) { --} +-const B = 1 +-` - --func BenchmarkFuncWithCodeLens(b *testing.B) { //@codelens(re"()func", "run benchmark") +- for useReplace, expectation := range map[bool]Expectation{ +- true: FileWatchMatching("modb"), +- false: NoFileWatchMatching("modb"), +- } { +- WithOptions( +- WorkspaceFolders("moda"), +- Settings{ +- "includeReplaceInWorkspace": useReplace, +- }, +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.OnceMet( +- InitialWorkspaceLoad, +- expectation, +- ) +- }) +- } -} - --func helper() {} // expect no code lens -diff -urN a/gopls/internal/regtest/marker/testdata/completion/address.txt b/gopls/internal/regtest/marker/testdata/completion/address.txt ---- a/gopls/internal/regtest/marker/testdata/completion/address.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/address.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,92 +0,0 @@ --This test exercises the reference and dereference completion modifiers. +-func TestDisableZeroConfig(t *testing.T) { +- // This test checks that we treat locally replaced modules as workspace +- // modules, according to the "includeReplaceInWorkspace" setting. +- const files = ` +--- moda/go.mod -- +-module golang.org/a - --TODO: remove the need to set "literalCompletions" here, as this is one of the --few places this setting is needed. +-go 1.20 - ---- flags -- ---ignore_extra_diags +--- moda/a.go -- +-package a +- +--- modb/go.mod -- +-module golang.org/b +- +-go 1.20 +- +--- modb/b.go -- +-package b +- +-` +- +- WithOptions( +- Settings{"zeroConfig": false}, +- ).Run(t, files, func(t *testing.T, env *Env) { +- env.OpenFile("moda/a.go") +- env.OpenFile("modb/b.go") +- env.AfterChange() +- if got := env.Views(); len(got) != 1 || got[0].Type != cache.AdHocView.String() { +- t.Errorf("Views: got %v, want one adhoc view", got) +- } +- }) +-} - +-func TestVendorExcluded(t *testing.T) { +- // Test that we don't create Views for vendored modules. +- // +- // We construct the vendor directory manually here, as `go mod vendor` will +- // omit the go.mod file. This synthesizes the setup of Kubernetes, where the +- // entire module is vendored through a symlinked directory. +- const src = ` --- go.mod -- --module golang.org/lsptests +-module example.com/a - -go 1.18 - ---- address/address.go -- --package address -- --func wantsPtr(*int) {} --func wantsVariadicPtr(...*int) {} +-require other.com/b v1.0.0 - --func wantsVariadic(...int) {} +--- a.go -- +-package a +-import "other.com/b" +-var _ b.B - --type foo struct{ c int } //@item(addrFieldC, "c", "int", "field") +--- vendor/modules.txt -- +-# other.com/b v1.0.0 +-## explicit; go 1.14 +-other.com/b - --func _() { -- var ( -- a string //@item(addrA, "a", "string", "var") -- b int //@item(addrB, "b", "int", "var") -- ) +--- vendor/other.com/b/go.mod -- +-module other.com/b +-go 1.14 - -- wantsPtr() //@rank(")", addrB, addrA),snippet(")", addrB, "&b") -- wantsPtr(&b) //@snippet(")", addrB, "b") +--- vendor/other.com/b/b.go -- +-package b +-type B int - -- wantsVariadicPtr() //@rank(")", addrB, addrA),snippet(")", addrB, "&b") +-func _() { +- var V int // unused +-} +-` +- WithOptions( +- Modes(Default), +- ).Run(t, src, func(t *testing.T, env *Env) { +- env.OpenFile("a.go") +- env.AfterChange(NoDiagnostics()) +- loc := env.GoToDefinition(env.RegexpSearch("a.go", `b\.(B)`)) +- if !strings.Contains(string(loc.URI), "/vendor/") { +- t.Fatalf("Definition(b.B) = %v, want vendored location", loc.URI) +- } +- env.AfterChange( +- Diagnostics(env.AtRegexp("vendor/other.com/b/b.go", "V"), WithMessage("not used")), +- ) - -- var s foo -- s.c //@item(addrDeepC, "s.c", "int", "field") -- wantsPtr() //@snippet(")", addrDeepC, "&s.c") -- wantsPtr(s) //@snippet(")", addrDeepC, "&s.c") -- wantsPtr(&s) //@snippet(")", addrDeepC, "s.c") +- if views := env.Views(); len(views) != 1 { +- t.Errorf("After opening /vendor/, got %d views, want 1. Views:\n%v", len(views), views) +- } +- }) +-} +diff -urN a/gopls/internal/test/integration/wrappers.go b/gopls/internal/test/integration/wrappers.go +--- a/gopls/internal/test/integration/wrappers.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/integration/wrappers.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,592 +0,0 @@ +-// Copyright 2020 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- // don't add "&" in item (it gets added as an additional edit) -- wantsPtr(&s.c) //@snippet(")", addrFieldC, "c") +-package integration - -- // check dereferencing as well -- var c *int //@item(addrCPtr, "c", "*int", "var") -- var _ int = _ //@rank("_ //", addrCPtr, addrA),snippet("_ //", addrCPtr, "*c") +-import ( +- "encoding/json" +- "path" - -- wantsVariadic() //@rank(")", addrCPtr, addrA),snippet(")", addrCPtr, "*c") +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/protocol/command" +- "golang.org/x/tools/gopls/internal/test/integration/fake" +- "golang.org/x/tools/internal/xcontext" +-) - -- var d **int //@item(addrDPtr, "d", "**int", "var") -- var _ int = _ //@rank("_ //", addrDPtr, addrA),snippet("_ //", addrDPtr, "**d") +-// RemoveWorkspaceFile deletes a file on disk but does nothing in the +-// editor. It calls t.Fatal on any error. +-func (e *Env) RemoveWorkspaceFile(name string) { +- e.T.Helper() +- if err := e.Sandbox.Workdir.RemoveFile(e.Ctx, name); err != nil { +- e.T.Fatal(err) +- } +-} - -- type namedPtr *int -- var np namedPtr //@item(addrNamedPtr, "np", "namedPtr", "var") +-// ReadWorkspaceFile reads a file from the workspace, calling t.Fatal on any +-// error. +-func (e *Env) ReadWorkspaceFile(name string) string { +- e.T.Helper() +- content, err := e.Sandbox.Workdir.ReadFile(name) +- if err != nil { +- e.T.Fatal(err) +- } +- return string(content) +-} - -- var _ int = _ //@rank("_ //", addrNamedPtr, addrA) +-// WriteWorkspaceFile writes a file to disk but does nothing in the editor. +-// It calls t.Fatal on any error. +-func (e *Env) WriteWorkspaceFile(name, content string) { +- e.T.Helper() +- if err := e.Sandbox.Workdir.WriteFile(e.Ctx, name, content); err != nil { +- e.T.Fatal(err) +- } +-} - -- // don't get tripped up by recursive pointer type -- type dontMessUp *dontMessUp //@item(dontMessUp, "dontMessUp", "*dontMessUp", "type") -- var dmu *dontMessUp //@item(addrDMU, "dmu", "*dontMessUp", "var") +-// WriteWorkspaceFiles deletes a file on disk but does nothing in the +-// editor. It calls t.Fatal on any error. +-func (e *Env) WriteWorkspaceFiles(files map[string]string) { +- e.T.Helper() +- if err := e.Sandbox.Workdir.WriteFiles(e.Ctx, files); err != nil { +- e.T.Fatal(err) +- } +-} - -- var _ int = dmu //@complete(" //", addrDMU, dontMessUp) +-// ListFiles lists relative paths to files in the given directory. +-// It calls t.Fatal on any error. +-func (e *Env) ListFiles(dir string) []string { +- e.T.Helper() +- paths, err := e.Sandbox.Workdir.ListFiles(dir) +- if err != nil { +- e.T.Fatal(err) +- } +- return paths -} - --func (f foo) ptr() *foo { return &f } +-// OpenFile opens a file in the editor, calling t.Fatal on any error. +-func (e *Env) OpenFile(name string) { +- e.T.Helper() +- if err := e.Editor.OpenFile(e.Ctx, name); err != nil { +- e.T.Fatal(err) +- } +-} - --func _() { -- getFoo := func() foo { return foo{} } +-// CreateBuffer creates a buffer in the editor, calling t.Fatal on any error. +-func (e *Env) CreateBuffer(name string, content string) { +- e.T.Helper() +- if err := e.Editor.CreateBuffer(e.Ctx, name, content); err != nil { +- e.T.Fatal(err) +- } +-} - -- // not addressable -- getFoo().c //@item(addrGetFooC, "getFoo().c", "int", "field") +-// BufferText returns the current buffer contents for the file with the given +-// relative path, calling t.Fatal if the file is not open in a buffer. +-func (e *Env) BufferText(name string) string { +- e.T.Helper() +- text, ok := e.Editor.BufferText(name) +- if !ok { +- e.T.Fatalf("buffer %q is not open", name) +- } +- return text +-} - -- // addressable -- getFoo().ptr().c //@item(addrGetFooPtrC, "getFoo().ptr().c", "int", "field") +-// CloseBuffer closes an editor buffer without saving, calling t.Fatal on any +-// error. +-func (e *Env) CloseBuffer(name string) { +- e.T.Helper() +- if err := e.Editor.CloseBuffer(e.Ctx, name); err != nil { +- e.T.Fatal(err) +- } +-} - -- wantsPtr() //@snippet(")", addrGetFooPtrC, "&getFoo().ptr().c") -- wantsPtr(&g) //@snippet(")", addrGetFooPtrC, "getFoo().ptr().c") +-// EditBuffer applies edits to an editor buffer, calling t.Fatal on any error. +-func (e *Env) EditBuffer(name string, edits ...protocol.TextEdit) { +- e.T.Helper() +- if err := e.Editor.EditBuffer(e.Ctx, name, edits); err != nil { +- e.T.Fatal(err) +- } -} - --type nested struct { -- f foo +-func (e *Env) SetBufferContent(name string, content string) { +- e.T.Helper() +- if err := e.Editor.SetBufferContent(e.Ctx, name, content); err != nil { +- e.T.Fatal(err) +- } -} - --func _() { -- getNested := func() nested { return nested{} } +-// ReadFile returns the file content for name that applies to the current +-// editing session: if the file is open, it returns its buffer content, +-// otherwise it returns on disk content. +-func (e *Env) FileContent(name string) string { +- e.T.Helper() +- text, ok := e.Editor.BufferText(name) +- if ok { +- return text +- } +- return e.ReadWorkspaceFile(name) +-} - -- getNested().f.c //@item(addrNestedC, "getNested().f.c", "int", "field") -- getNested().f.ptr().c //@item(addrNestedPtrC, "getNested().f.ptr().c", "int", "field") +-// RegexpSearch returns the starting position of the first match for re in the +-// buffer specified by name, calling t.Fatal on any error. It first searches +-// for the position in open buffers, then in workspace files. +-func (e *Env) RegexpSearch(name, re string) protocol.Location { +- e.T.Helper() +- loc, err := e.Editor.RegexpSearch(name, re) +- if err == fake.ErrUnknownBuffer { +- loc, err = e.Sandbox.Workdir.RegexpSearch(name, re) +- } +- if err != nil { +- e.T.Fatalf("RegexpSearch: %v, %v for %q", name, err, re) +- } +- return loc +-} - -- // addrNestedC is not addressable, so rank lower -- wantsPtr(getNestedfc) //@complete(")", addrNestedPtrC, addrNestedC) +-// RegexpReplace replaces the first group in the first match of regexpStr with +-// the replace text, calling t.Fatal on any error. +-func (e *Env) RegexpReplace(name, regexpStr, replace string) { +- e.T.Helper() +- if err := e.Editor.RegexpReplace(e.Ctx, name, regexpStr, replace); err != nil { +- e.T.Fatalf("RegexpReplace: %v", err) +- } -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/anon.txt b/gopls/internal/regtest/marker/testdata/completion/anon.txt ---- a/gopls/internal/regtest/marker/testdata/completion/anon.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/anon.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,37 +0,0 @@ --This test checks completion related to anonymous structs. - ---- flags -- ---ignore_extra_diags +-// SaveBuffer saves an editor buffer, calling t.Fatal on any error. +-func (e *Env) SaveBuffer(name string) { +- e.T.Helper() +- if err := e.Editor.SaveBuffer(e.Ctx, name); err != nil { +- e.T.Fatal(err) +- } +-} - ---- settings.json -- --{ -- "deepCompletion": false +-func (e *Env) SaveBufferWithoutActions(name string) { +- e.T.Helper() +- if err := e.Editor.SaveBufferWithoutActions(e.Ctx, name); err != nil { +- e.T.Fatal(err) +- } -} - ---- anon.go -- --package anon +-// GoToDefinition goes to definition in the editor, calling t.Fatal on any +-// error. It returns the path and position of the resulting jump. +-// +-// TODO(rfindley): rename this to just 'Definition'. +-func (e *Env) GoToDefinition(loc protocol.Location) protocol.Location { +- e.T.Helper() +- loc, err := e.Editor.Definition(e.Ctx, loc) +- if err != nil { +- e.T.Fatal(err) +- } +- return loc +-} - --// Literal completion results. --/* int() */ //@item(int, "int()", "int", "var") +-func (e *Env) TypeDefinition(loc protocol.Location) protocol.Location { +- e.T.Helper() +- loc, err := e.Editor.TypeDefinition(e.Ctx, loc) +- if err != nil { +- e.T.Fatal(err) +- } +- return loc +-} - --func _() { -- for _, _ := range []struct { -- i, j int //@item(anonI, "i", "int", "field"),item(anonJ, "j", "int", "field") -- }{ -- { -- i: 1, -- //@complete("", anonJ) -- }, -- { -- //@complete("", anonI, anonJ, int) -- }, -- } { -- continue +-// FormatBuffer formats the editor buffer, calling t.Fatal on any error. +-func (e *Env) FormatBuffer(name string) { +- e.T.Helper() +- if err := e.Editor.FormatBuffer(e.Ctx, name); err != nil { +- e.T.Fatal(err) - } +-} - -- s := struct{ f int }{ } //@item(anonF, "f", "int", "field"),item(structS, "s", "struct{...}", "var"),complete(" }", anonF, int) +-// OrganizeImports processes the source.organizeImports codeAction, calling +-// t.Fatal on any error. +-func (e *Env) OrganizeImports(name string) { +- e.T.Helper() +- if err := e.Editor.OrganizeImports(e.Ctx, name); err != nil { +- e.T.Fatal(err) +- } +-} - -- _ = map[struct{ x int }]int{ //@item(anonX, "x", "int", "field") -- struct{ x int }{ }: 1, //@complete(" }", anonX, int, structS) +-// ApplyQuickFixes processes the quickfix codeAction, calling t.Fatal on any error. +-func (e *Env) ApplyQuickFixes(path string, diagnostics []protocol.Diagnostic) { +- e.T.Helper() +- loc := protocol.Location{URI: e.Sandbox.Workdir.URI(path)} // zero Range => whole file +- if err := e.Editor.ApplyQuickFixes(e.Ctx, loc, diagnostics); err != nil { +- e.T.Fatal(err) - } -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/append.txt b/gopls/internal/regtest/marker/testdata/completion/append.txt ---- a/gopls/internal/regtest/marker/testdata/completion/append.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/append.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,56 +0,0 @@ --This test checks behavior of completion within append expressions. - ---- flags -- ---ignore_extra_diags +-// ApplyCodeAction applies the given code action. +-func (e *Env) ApplyCodeAction(action protocol.CodeAction) { +- e.T.Helper() +- if err := e.Editor.ApplyCodeAction(e.Ctx, action); err != nil { +- e.T.Fatal(err) +- } +-} - ---- go.mod -- --module golang.org/lsptests/append +-// GetQuickFixes returns the available quick fix code actions. +-func (e *Env) GetQuickFixes(path string, diagnostics []protocol.Diagnostic) []protocol.CodeAction { +- e.T.Helper() +- loc := protocol.Location{URI: e.Sandbox.Workdir.URI(path)} // zero Range => whole file +- actions, err := e.Editor.GetQuickFixes(e.Ctx, loc, diagnostics) +- if err != nil { +- e.T.Fatal(err) +- } +- return actions +-} - --go 1.18 +-// Hover in the editor, calling t.Fatal on any error. +-func (e *Env) Hover(loc protocol.Location) (*protocol.MarkupContent, protocol.Location) { +- e.T.Helper() +- c, loc, err := e.Editor.Hover(e.Ctx, loc) +- if err != nil { +- e.T.Fatal(err) +- } +- return c, loc +-} - ---- append.go -- --package append +-func (e *Env) DocumentLink(name string) []protocol.DocumentLink { +- e.T.Helper() +- links, err := e.Editor.DocumentLink(e.Ctx, name) +- if err != nil { +- e.T.Fatal(err) +- } +- return links +-} - --func foo([]string) {} --func bar(...string) {} +-func (e *Env) DocumentHighlight(loc protocol.Location) []protocol.DocumentHighlight { +- e.T.Helper() +- highlights, err := e.Editor.DocumentHighlight(e.Ctx, loc) +- if err != nil { +- e.T.Fatal(err) +- } +- return highlights +-} - --func _() { -- var ( -- aInt []int //@item(appendInt, "aInt", "[]int", "var") -- aStrings []string //@item(appendStrings, "aStrings", "[]string", "var") -- aString string //@item(appendString, "aString", "string", "var") -- ) +-// RunGenerate runs "go generate" in the given dir, calling t.Fatal on any error. +-// It waits for the generate command to complete and checks for file changes +-// before returning. +-func (e *Env) RunGenerate(dir string) { +- e.T.Helper() +- if err := e.Editor.RunGenerate(e.Ctx, dir); err != nil { +- e.T.Fatal(err) +- } +- e.Await(NoOutstandingWork(IgnoreTelemetryPromptWork)) +- // Ideally the editor.Workspace would handle all synthetic file watching, but +- // we help it out here as we need to wait for the generate command to +- // complete before checking the filesystem. +- e.CheckForFileChanges() +-} - -- append(aStrings, a) //@rank(")", appendString, appendInt) -- var _ interface{} = append(aStrings, a) //@rank(")", appendString, appendInt) -- var _ []string = append(oops, a) //@rank(")", appendString, appendInt) +-// RunGoCommand runs the given command in the sandbox's default working +-// directory. +-func (e *Env) RunGoCommand(verb string, args ...string) { +- e.T.Helper() +- if err := e.Sandbox.RunGoCommand(e.Ctx, "", verb, args, nil, true); err != nil { +- e.T.Fatal(err) +- } +-} - -- foo(append()) //@rank("))", appendStrings, appendInt),rank("))", appendStrings, appendString) -- foo(append([]string{}, a)) //@rank("))", appendStrings, appendInt),rank("))", appendString, appendInt),snippet("))", appendStrings, "aStrings...") -- foo(append([]string{}, "", a)) //@rank("))", appendString, appendInt),rank("))", appendString, appendStrings) +-// RunGoCommandInDir is like RunGoCommand, but executes in the given +-// relative directory of the sandbox. +-func (e *Env) RunGoCommandInDir(dir, verb string, args ...string) { +- e.T.Helper() +- if err := e.Sandbox.RunGoCommand(e.Ctx, dir, verb, args, nil, true); err != nil { +- e.T.Fatal(err) +- } +-} - -- // Don't add "..." to append() argument. -- bar(append()) //@snippet("))", appendStrings, "aStrings") +-// RunGoCommandInDirWithEnv is like RunGoCommand, but executes in the given +-// relative directory of the sandbox with the given additional environment variables. +-func (e *Env) RunGoCommandInDirWithEnv(dir string, env []string, verb string, args ...string) { +- e.T.Helper() +- if err := e.Sandbox.RunGoCommand(e.Ctx, dir, verb, args, env, true); err != nil { +- e.T.Fatal(err) +- } +-} - -- type baz struct{} -- baz{} //@item(appendBazLiteral, "baz{}", "", "var") -- var bazzes []baz //@item(appendBazzes, "bazzes", "[]baz", "var") -- var bazzy baz //@item(appendBazzy, "bazzy", "baz", "var") -- bazzes = append(bazzes, ba) //@rank(")", appendBazzy, appendBazLiteral, appendBazzes) +-// GoVersion checks the version of the go command. +-// It returns the X in Go 1.X. +-func (e *Env) GoVersion() int { +- e.T.Helper() +- v, err := e.Sandbox.GoVersion(e.Ctx) +- if err != nil { +- e.T.Fatal(err) +- } +- return v +-} - -- var b struct{ b []baz } -- b.b //@item(appendNestedBaz, "b.b", "[]baz", "field") -- b.b = append(b.b, b) //@rank(")", appendBazzy, appendBazLiteral, appendNestedBaz) +-// DumpGoSum prints the correct go.sum contents for dir in txtar format, +-// for use in creating integration tests. +-func (e *Env) DumpGoSum(dir string) { +- e.T.Helper() - -- var aStringsPtr *[]string //@item(appendStringsPtr, "aStringsPtr", "*[]string", "var") -- foo(append([]string{}, a)) //@snippet("))", appendStringsPtr, "*aStringsPtr...") +- if err := e.Sandbox.RunGoCommand(e.Ctx, dir, "list", []string{"-mod=mod", "./..."}, nil, true); err != nil { +- e.T.Fatal(err) +- } +- sumFile := path.Join(dir, "go.sum") +- e.T.Log("\n\n-- " + sumFile + " --\n" + e.ReadWorkspaceFile(sumFile)) +- e.T.Fatal("see contents above") +-} - -- foo(append([]string{}, *a)) //@snippet("))", appendStringsPtr, "aStringsPtr...") +-// CheckForFileChanges triggers a manual poll of the workspace for any file +-// changes since creation, or since last polling. It is a workaround for the +-// lack of true file watching support in the fake workspace. +-func (e *Env) CheckForFileChanges() { +- e.T.Helper() +- if err := e.Sandbox.Workdir.CheckForFileChanges(e.Ctx); err != nil { +- e.T.Fatal(err) +- } -} - ---- append2.go -- --package append +-// CodeLens calls textDocument/codeLens for the given path, calling t.Fatal on +-// any error. +-func (e *Env) CodeLens(path string) []protocol.CodeLens { +- e.T.Helper() +- lens, err := e.Editor.CodeLens(e.Ctx, path) +- if err != nil { +- e.T.Fatal(err) +- } +- return lens +-} - --func _() { -- _ = append(a, struct) //@complete(")") +-// ExecuteCodeLensCommand executes the command for the code lens matching the +-// given command name. +-func (e *Env) ExecuteCodeLensCommand(path string, cmd command.Command, result interface{}) { +- e.T.Helper() +- lenses := e.CodeLens(path) +- var lens protocol.CodeLens +- var found bool +- for _, l := range lenses { +- if l.Command.Command == cmd.ID() { +- lens = l +- found = true +- } +- } +- if !found { +- e.T.Fatalf("found no command with the ID %s", cmd.ID()) +- } +- e.ExecuteCommand(&protocol.ExecuteCommandParams{ +- Command: lens.Command.Command, +- Arguments: lens.Command.Arguments, +- }, result) -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/assign.txt b/gopls/internal/regtest/marker/testdata/completion/assign.txt ---- a/gopls/internal/regtest/marker/testdata/completion/assign.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/assign.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,47 +0,0 @@ --This test checks that completion considers assignability when ranking results. - ---- flags -- ---ignore_extra_diags +-func (e *Env) ExecuteCommand(params *protocol.ExecuteCommandParams, result interface{}) { +- e.T.Helper() +- response, err := e.Editor.ExecuteCommand(e.Ctx, params) +- if err != nil { +- e.T.Fatal(err) +- } +- if result == nil { +- return +- } +- // Hack: The result of an executeCommand request will be unmarshaled into +- // maps. Re-marshal and unmarshal into the type we expect. +- // +- // This could be improved by generating a jsonrpc2 command client from the +- // command.Interface, but that should only be done if we're consolidating +- // this part of the tsprotocol generation. +- data, err := json.Marshal(response) +- if err != nil { +- e.T.Fatal(err) +- } +- if err := json.Unmarshal(data, result); err != nil { +- e.T.Fatal(err) +- } +-} - ---- go.mod -- --module golang.org/lsptests/assign +-// Views returns the server's views. +-func (e *Env) Views() []command.View { +- var summaries []command.View +- cmd, err := command.NewViewsCommand("") +- if err != nil { +- e.T.Fatal(err) +- } +- e.ExecuteCommand(&protocol.ExecuteCommandParams{ +- Command: cmd.Command, +- Arguments: cmd.Arguments, +- }, &summaries) +- return summaries +-} - --go 1.18 +-// StartProfile starts a CPU profile with the given name, using the +-// gopls.start_profile custom command. It calls t.Fatal on any error. +-// +-// The resulting stop function must be called to stop profiling (using the +-// gopls.stop_profile custom command). +-func (e *Env) StartProfile() (stop func() string) { +- // TODO(golang/go#61217): revisit the ergonomics of these command APIs. +- // +- // This would be a lot simpler if we generated params constructors. +- args, err := command.MarshalArgs(command.StartProfileArgs{}) +- if err != nil { +- e.T.Fatal(err) +- } +- params := &protocol.ExecuteCommandParams{ +- Command: command.StartProfile.ID(), +- Arguments: args, +- } +- var result command.StartProfileResult +- e.ExecuteCommand(params, &result) - ---- settings.json -- --{ -- "completeUnimported": false +- return func() string { +- stopArgs, err := command.MarshalArgs(command.StopProfileArgs{}) +- if err != nil { +- e.T.Fatal(err) +- } +- stopParams := &protocol.ExecuteCommandParams{ +- Command: command.StopProfile.ID(), +- Arguments: stopArgs, +- } +- var result command.StopProfileResult +- e.ExecuteCommand(stopParams, &result) +- return result.File +- } -} - ---- assign.go -- --package assign -- --import "golang.org/lsptests/assign/internal/secret" +-// InlayHints calls textDocument/inlayHints for the given path, calling t.Fatal on +-// any error. +-func (e *Env) InlayHints(path string) []protocol.InlayHint { +- e.T.Helper() +- hints, err := e.Editor.InlayHint(e.Ctx, path) +- if err != nil { +- e.T.Fatal(err) +- } +- return hints +-} - --func _() { -- secret.Hello() -- var ( -- myInt int //@item(assignInt, "myInt", "int", "var") -- myStr string //@item(assignStr, "myStr", "string", "var") -- ) +-// Symbol calls workspace/symbol +-func (e *Env) Symbol(query string) []protocol.SymbolInformation { +- e.T.Helper() +- ans, err := e.Editor.Symbols(e.Ctx, query) +- if err != nil { +- e.T.Fatal(err) +- } +- return ans +-} - -- var _ string = my //@rank(" //", assignStr, assignInt) -- var _ string = //@rank(" //", assignStr, assignInt) +-// References wraps Editor.References, calling t.Fatal on any error. +-func (e *Env) References(loc protocol.Location) []protocol.Location { +- e.T.Helper() +- locations, err := e.Editor.References(e.Ctx, loc) +- if err != nil { +- e.T.Fatal(err) +- } +- return locations -} - --func _() { -- var a string = a //@complete(" //") +-// Rename wraps Editor.Rename, calling t.Fatal on any error. +-func (e *Env) Rename(loc protocol.Location, newName string) { +- e.T.Helper() +- if err := e.Editor.Rename(e.Ctx, loc, newName); err != nil { +- e.T.Fatal(err) +- } -} - --func _() { -- fooBar := fooBa //@complete(" //"),item(assignFooBar, "fooBar", "", "var") -- abc, fooBar := 123, fooBa //@complete(" //", assignFooBar) -- { -- fooBar := fooBa //@complete(" //", assignFooBar) +-// Implementations wraps Editor.Implementations, calling t.Fatal on any error. +-func (e *Env) Implementations(loc protocol.Location) []protocol.Location { +- e.T.Helper() +- locations, err := e.Editor.Implementations(e.Ctx, loc) +- if err != nil { +- e.T.Fatal(err) - } +- return locations -} - ---- internal/secret/secret.go -- --package secret +-// RenameFile wraps Editor.RenameFile, calling t.Fatal on any error. +-func (e *Env) RenameFile(oldPath, newPath string) { +- e.T.Helper() +- if err := e.Editor.RenameFile(e.Ctx, oldPath, newPath); err != nil { +- e.T.Fatal(err) +- } +-} - --func Hello() {} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/bad.txt b/gopls/internal/regtest/marker/testdata/completion/bad.txt ---- a/gopls/internal/regtest/marker/testdata/completion/bad.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/bad.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,68 +0,0 @@ --This test exercises completion in the presence of type errors. +-// SignatureHelp wraps Editor.SignatureHelp, calling t.Fatal on error +-func (e *Env) SignatureHelp(loc protocol.Location) *protocol.SignatureHelp { +- e.T.Helper() +- sighelp, err := e.Editor.SignatureHelp(e.Ctx, loc) +- if err != nil { +- e.T.Fatal(err) +- } +- return sighelp +-} - --Note: this test was ported from the old marker tests, which did not enable --unimported completion. Enabling it causes matches in e.g. crypto/rand. +-// Completion executes a completion request on the server. +-func (e *Env) Completion(loc protocol.Location) *protocol.CompletionList { +- e.T.Helper() +- completions, err := e.Editor.Completion(e.Ctx, loc) +- if err != nil { +- e.T.Fatal(err) +- } +- return completions +-} - ---- settings.json -- --{ -- "completeUnimported": false +-// AcceptCompletion accepts a completion for the given item at the given +-// position. +-func (e *Env) AcceptCompletion(loc protocol.Location, item protocol.CompletionItem) { +- e.T.Helper() +- if err := e.Editor.AcceptCompletion(e.Ctx, loc, item); err != nil { +- e.T.Fatal(err) +- } -} - ---- go.mod -- --module bad.test +-// CodeAction calls textDocument/codeAction for the given path, and calls +-// t.Fatal if there are errors. +-func (e *Env) CodeAction(path string, diagnostics []protocol.Diagnostic) []protocol.CodeAction { +- e.T.Helper() +- loc := protocol.Location{URI: e.Sandbox.Workdir.URI(path)} // no Range => whole file +- actions, err := e.Editor.CodeAction(e.Ctx, loc, diagnostics) +- if err != nil { +- e.T.Fatal(err) +- } +- return actions +-} - --go 1.18 +-// ChangeConfiguration updates the editor config, calling t.Fatal on any error. +-func (e *Env) ChangeConfiguration(newConfig fake.EditorConfig) { +- e.T.Helper() +- if err := e.Editor.ChangeConfiguration(e.Ctx, newConfig); err != nil { +- e.T.Fatal(err) +- } +-} - ---- bad/bad0.go -- --package bad +-// ChangeWorkspaceFolders updates the editor workspace folders, calling t.Fatal +-// on any error. +-func (e *Env) ChangeWorkspaceFolders(newFolders ...string) { +- e.T.Helper() +- if err := e.Editor.ChangeWorkspaceFolders(e.Ctx, newFolders); err != nil { +- e.T.Fatal(err) +- } +-} - --func stuff() { //@item(stuff, "stuff", "func()", "func") -- x := "heeeeyyyy" -- random2(x) //@diag("x", re"cannot use x \\(variable of type string\\) as int value in argument to random2") -- random2(1) //@complete("dom", random, random2, random3) -- y := 3 //@diag("y", re"y declared (and|but) not used") +-// SemanticTokensFull invokes textDocument/semanticTokens/full, calling t.Fatal +-// on any error. +-func (e *Env) SemanticTokensFull(path string) []fake.SemanticToken { +- e.T.Helper() +- toks, err := e.Editor.SemanticTokensFull(e.Ctx, path) +- if err != nil { +- e.T.Fatal(err) +- } +- return toks -} - --type bob struct { //@item(bob, "bob", "struct{...}", "struct") -- x int +-// SemanticTokensRange invokes textDocument/semanticTokens/range, calling t.Fatal +-// on any error. +-func (e *Env) SemanticTokensRange(loc protocol.Location) []fake.SemanticToken { +- e.T.Helper() +- toks, err := e.Editor.SemanticTokensRange(e.Ctx, loc) +- if err != nil { +- e.T.Fatal(err) +- } +- return toks -} - --func _() { -- var q int -- _ = &bob{ -- f: q, //@diag("f: q", re"unknown field f in struct literal") +-// Close shuts down the editor session and cleans up the sandbox directory, +-// calling t.Error on any error. +-func (e *Env) Close() { +- ctx := xcontext.Detach(e.Ctx) +- if err := e.Editor.Close(ctx); err != nil { +- e.T.Errorf("closing editor: %v", err) +- } +- if err := e.Sandbox.Close(); err != nil { +- e.T.Errorf("cleaning up sandbox: %v", err) - } -} +diff -urN a/gopls/internal/test/marker/doc.go b/gopls/internal/test/marker/doc.go +--- a/gopls/internal/test/marker/doc.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/doc.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,361 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - ---- bad/bad1.go -- --package bad +-/* +-Package marker defines a framework for running "marker" tests, each +-defined by a file in the testdata subdirectory. +- +-Use this command to run the tests: +- +- $ go test ./gopls/internal/test/marker [-update] +- +-A marker test uses the '//@' marker syntax of the x/tools/go/expect package +-to annotate source code with various information such as locations and +-arguments of LSP operations to be executed by the test. The syntax following +-'@' is parsed as a comma-separated list of ordinary Go function calls, for +-example +- +- //@foo(a, "b", 3),bar(0) +- +-and delegates to a corresponding function to perform LSP-related operations. +-See the Marker types documentation below for a list of supported markers. +- +-Each call argument is converted to the type of the corresponding parameter of +-the designated function. The conversion logic may use the surrounding context, +-such as the position or nearby text. See the Argument conversion section below +-for the full set of special conversions. As a special case, the blank +-identifier '_' is treated as the zero value of the parameter type. +- +-The test runner collects test cases by searching the given directory for +-files with the .txt extension. Each file is interpreted as a txtar archive, +-which is extracted to a temporary directory. The relative path to the .txt +-file is used as the subtest name. The preliminary section of the file +-(before the first archive entry) is a free-form comment. +- +-# Special files +- +-There are several types of file within the test archive that are given special +-treatment by the test runner: +- +- - "skip": the presence of this file causes the test to be skipped, with +- the file content used as the skip message. +- +- - "flags": this file is treated as a whitespace-separated list of flags +- that configure the MarkerTest instance. Supported flags: +- -min_go=go1.20 sets the minimum Go version for the test; +- -cgo requires that CGO_ENABLED is set and the cgo tool is available +- -write_sumfile=a,b,c instructs the test runner to generate go.sum files +- in these directories before running the test. +- -skip_goos=a,b,c instructs the test runner to skip the test for the +- listed GOOS values. +- -skip_goarch=a,b,c does the same for GOARCH. +- -ignore_extra_diags suppresses errors for unmatched diagnostics +- TODO(rfindley): using build constraint expressions for -skip_go{os,arch} would +- be clearer. +- -filter_builtins=false disables the filtering of builtins from +- completion results. +- -filter_keywords=false disables the filtering of keywords from +- completion results. +- TODO(rfindley): support flag values containing whitespace. +- +- - "settings.json": this file is parsed as JSON, and used as the +- session configuration (see gopls/doc/settings.md) +- +- - "capabilities.json": this file is parsed as JSON client capabilities, +- and applied as an overlay over the default editor client capabilities. +- see https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#clientCapabilities +- for more details. +- +- - "env": this file is parsed as a list of VAR=VALUE fields specifying the +- editor environment. +- +- - Golden files: Within the archive, file names starting with '@' are +- treated as "golden" content, and are not written to disk, but instead are +- made available to test methods expecting an argument of type *Golden, +- using the identifier following '@'. For example, if the first parameter of +- Foo were of type *Golden, the test runner would convert the identifier a +- in the call @foo(a, "b", 3) into a *Golden by collecting golden file +- data starting with "@a/". As a special case, for tests that only need one +- golden file, the data contained in the file "@a" is indexed in the *Golden +- value by the empty string "". +- +- - proxy files: any file starting with proxy/ is treated as a Go proxy +- file. If present, these files are written to a separate temporary +- directory and GOPROXY is set to file://<proxy directory>. +- +-# Marker types +- +-Markers are of two kinds. A few are "value markers" (e.g. @item), which are +-processed in a first pass and each computes a value that may be referred to +-by name later. Most are "action markers", which are processed in a second +-pass and take some action such as testing an LSP operation; they may refer +-to values computed by value markers. +- +-The following markers are supported within marker tests: +- +- - acceptcompletion(location, label, golden): specifies that accepting the +- completion candidate produced at the given location with provided label +- results in the given golden state. +- +- - codeaction(start, end, kind, golden, ...titles): specifies a code action +- to request for the given range. To support multi-line ranges, the range +- is defined to be between start.Start and end.End. The golden directory +- contains changed file content after the code action is applied. +- If titles are provided, they are used to filter the matching code +- action. +- +- TODO(rfindley): consolidate with codeactionedit, via a @loc2 marker that +- allows binding multi-line locations. +- +- - codeactionedit(range, kind, golden, ...titles): a shorter form of +- codeaction. Invokes a code action of the given kind for the given +- in-line range, and compares the resulting formatted unified *edits* +- (notably, not the full file content) with the golden directory. +- +- - codeactionerr(start, end, kind, wantError): specifies a codeaction that +- fails with an error that matches the expectation. +- +- - codelens(location, title): specifies that a codelens is expected at the +- given location, with given title. Must be used in conjunction with +- @codelenses. +- +- - codelenses(): specifies that textDocument/codeLens should be run for the +- current document, with results compared to the @codelens annotations in +- the current document. +- +- - complete(location, ...items): specifies expected completion results at +- the given location. Must be used in conjunction with @item. +- +- - diag(location, regexp): specifies an expected diagnostic matching the +- given regexp at the given location. The test runner requires +- a 1:1 correspondence between observed diagnostics and diag annotations. +- The diagnostics source and kind fields are ignored, to reduce fuss. +- +- The specified location must match the start position of the diagnostic, +- but end positions are ignored. +- +- TODO(adonovan): in the older marker framework, the annotation asserted +- two additional fields (source="compiler", kind="error"). Restore them? +- +- - def(src, dst location): performs a textDocument/definition request at +- the src location, and check the result points to the dst location. +- +- - documentLink(golden): asserts that textDocument/documentLink returns +- links as described by the golden file. +- +- - foldingrange(golden): performs a textDocument/foldingRange for the +- current document, and compare with the golden content, which is the +- original source annotated with numbered tags delimiting the resulting +- ranges (e.g. <1 kind="..."> ... </1>). +- +- - format(golden): performs a textDocument/format request for the enclosing +- file, and compare against the named golden file. If the formatting +- request succeeds, the golden file must contain the resulting formatted +- source. If the formatting request fails, the golden file must contain +- the error message. +- +- - highlight(src location, dsts ...location): makes a +- textDocument/highlight request at the given src location, which should +- highlight the provided dst locations. +- +- - hover(src, dst location, sm stringMatcher): performs a textDocument/hover +- at the src location, and checks that the result is the dst location, with +- matching hover content. +- +- - hovererr(src, sm stringMatcher): performs a textDocument/hover at the src +- location, and checks that the error matches the given stringMatcher. +- +- - implementations(src location, want ...location): makes a +- textDocument/implementation query at the src location and +- checks that the resulting set of locations matches want. +- +- - incomingcalls(src location, want ...location): makes a +- callHierarchy/incomingCalls query at the src location, and checks that +- the set of call.From locations matches want. +- +- - item(label, details, kind): defines a completion item with the provided +- fields. This information is not positional, and therefore @item markers +- may occur anywhere in the source. Used in conjunction with @complete, +- snippet, or rank. +- +- TODO(rfindley): rethink whether floating @item annotations are the best +- way to specify completion results. +- +- - loc(name, location): specifies the name for a location in the source. These +- locations may be referenced by other markers. +- +- - outgoingcalls(src location, want ...location): makes a +- callHierarchy/outgoingCalls query at the src location, and checks that +- the set of call.To locations matches want. +- +- - preparerename(src, spn, placeholder): asserts that a textDocument/prepareRename +- request at the src location expands to the spn location, with given +- placeholder. If placeholder is "", this is treated as a negative +- assertion and prepareRename should return nil. +- +- - rename(location, new, golden): specifies a renaming of the +- identifier at the specified location to the new name. +- The golden directory contains the transformed files. +- +- - renameerr(location, new, wantError): specifies a renaming that +- fails with an error that matches the expectation. +- +- - signature(location, label, active): specifies that +- signatureHelp at the given location should match the provided string, with +- the active parameter (an index) highlighted. +- +- - suggestedfix(location, regexp, golden): like diag, the location and +- regexp identify an expected diagnostic. This diagnostic must +- to have exactly one associated code action of the specified kind. +- This action is executed for its editing effects on the source files. +- Like rename, the golden directory contains the expected transformed files. +- +- - suggestedfixerr(location, regexp, kind, wantError): specifies that the +- suggestedfix operation should fail with an error that matches the expectation. +- (Failures in the computation to offer a fix do not generally result +- in LSP errors, so this marker is not appropriate for testing them.) +- +- - rank(location, ...completionItem): executes a textDocument/completion +- request at the given location, and verifies that each expected +- completion item occurs in the results, in the expected order. Other +- unexpected completion items may occur in the results. +- TODO(rfindley): this exists for compatibility with the old marker tests. +- Replace this with rankl, and rename. +- A "!" prefix on a label asserts that the symbol is not a +- completion candidate. +- +- - rankl(location, ...label): like rank, but only cares about completion +- item labels. +- +- - refs(location, want ...location): executes a textDocument/references +- request at the first location and asserts that the result is the set of +- 'want' locations. The first want location must be the declaration +- (assumedly unique). +- +- - snippet(location, completionItem, snippet): executes a +- textDocument/completion request at the location, and searches for a +- result with label matching that of the provided completion item +- (TODO(rfindley): accept a label rather than a completion item). Check +- the result snippet matches the provided snippet. +- +- - symbol(golden): makes a textDocument/documentSymbol request +- for the enclosing file, formats the response with one symbol +- per line, sorts it, and compares against the named golden file. +- Each line is of the form: +- +- dotted.symbol.name kind "detail" +n lines +- +- where the "+n lines" part indicates that the declaration spans +- several lines. The test otherwise makes no attempt to check +- location information. There is no point to using more than one +- @symbol marker in a given file. +- +- - token(location, tokenType, mod): makes a textDocument/semanticTokens/range +- request at the given location, and asserts that the result includes +- exactly one token with the given token type and modifier string. +- +- - workspacesymbol(query, golden): makes a workspace/symbol request for the +- given query, formats the response with one symbol per line, and compares +- against the named golden file. As workspace symbols are by definition a +- workspace-wide request, the location of the workspace symbol marker does +- not matter. Each line is of the form: +- +- location name kind +- +-# Argument conversion +- +-Marker arguments are first parsed by the go/expect package, which accepts +-the following tokens as defined by the Go spec: +- - string, int64, float64, and rune literals +- - true and false +- - nil +- - identifiers (type expect.Identifier) +- - regular expressions, denoted the two tokens re"abc" (type *regexp.Regexp) +- +-These values are passed as arguments to the corresponding parameter of the +-test function. Additional value conversions may occur for these argument -> +-parameter type pairs: +- - string->regexp: the argument is parsed as a regular expressions. +- - string->location: the argument is converted to the location of the first +- instance of the argument in the partial line preceding the note. +- - regexp->location: the argument is converted to the location of the first +- match for the argument in the partial line preceding the note. If the +- regular expression contains exactly one subgroup, the position of the +- subgroup is used rather than the position of the submatch. +- - name->location: the argument is replaced by the named location. +- - name->Golden: the argument is used to look up golden content prefixed by +- @<argument>. +- - {string,regexp,identifier}->stringMatcher: a stringMatcher type +- specifies an expected string, either in the form of a substring +- that must be present, a regular expression that it must match, or an +- identifier (e.g. foo) such that the archive entry @foo exists and +- contains the exact expected string. +- stringMatchers are used by some markers to match positive results +- (outputs) and by other markers to match error messages. +- +-# Example +- +-Here is a complete example: +- +- This test checks hovering over constants. +- +- -- a.go -- +- package a +- +- const abc = 0x2a //@hover("b", "abc", abc),hover(" =", "abc", abc) +- +- -- @abc -- +- ```go +- const abc untyped int = 42 +- ``` +- +- @hover("b", "abc", abc),hover(" =", "abc", abc) +- +-In this example, the @hover annotation tells the test runner to run the +-hoverMarker function, which has parameters: +- +- (mark marker, src, dsc protocol.Location, g *Golden). +- +-The first argument holds the test context, including fake editor with open +-files, and sandboxed directory. +- +-Argument converters translate the "b" and "abc" arguments into locations by +-interpreting each one as a substring (or as a regular expression, if of the +-form re"a|b") and finding the location of its first occurrence on the preceding +-portion of the line, and the abc identifier into a the golden content contained +-in the file @abc. Then the hoverMarker method executes a textDocument/hover LSP +-request at the src position, and ensures the result spans "abc", with the +-markdown content from @abc. (Note that the markdown content includes the expect +-annotation as the doc comment.) +- +-The next hover on the same line asserts the same result, but initiates the +-hover immediately after "abc" in the source. This tests that we find the +-preceding identifier when hovering. +- +-# Updating golden files +- +-To update golden content in the test archive, it is easier to regenerate +-content automatically rather than edit it by hand. To do this, run the +-tests with the -update flag. Only tests that actually run will be updated. +- +-In some cases, golden content will vary by Go version (for example, gopls +-produces different markdown at Go versions before the 1.19 go/doc update). +-By convention, the golden content in test archives should match the output +-at Go tip. Each test function can normalize golden content for older Go +-versions. +- +-Note that -update does not cause missing @diag or @loc markers to be added. +- +-# TODO +- +- - Rename the files .txtar. +- - Provide some means by which locations in the standard library +- (or builtin.go) can be named, so that, for example, we can we +- can assert that MyError implements the built-in error type. +- - If possible, improve handling for optional arguments. Rather than have +- multiple variations of a marker, it would be nice to support a more +- flexible signature: can codeaction, codeactionedit, codeactionerr, and +- suggestedfix be consolidated? +-*/ +-package marker +diff -urN a/gopls/internal/test/marker/marker_test.go b/gopls/internal/test/marker/marker_test.go +--- a/gopls/internal/test/marker/marker_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/marker_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,2326 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// See #36637 --type stateFunc func() stateFunc //@item(stateFunc, "stateFunc", "func() stateFunc", "type") +-package marker - --var a unknown //@item(global_a, "a", "unknown", "var"),diag("unknown", re"(undeclared name|undefined): unknown") +-// This file defines the marker test framework. +-// See doc.go for extensive documentation. - --func random() int { //@item(random, "random", "func() int", "func") -- //@complete("", global_a, bob, random, random2, random3, stateFunc, stuff) -- return 0 --} +-import ( +- "bytes" +- "context" +- "encoding/json" +- "flag" +- "fmt" +- "go/token" +- "go/types" +- "io/fs" +- "log" +- "os" +- "path" +- "path/filepath" +- "reflect" +- "regexp" +- "runtime" +- "sort" +- "strings" +- "testing" - --func random2(y int) int { //@item(random2, "random2", "func(y int) int", "func"),item(bad_y_param, "y", "int", "var") -- x := 6 //@item(x, "x", "int", "var"),diag("x", re"x declared (and|but) not used") -- var q blah //@item(q, "q", "blah", "var"),diag("q", re"q declared (and|but) not used"),diag("blah", re"(undeclared name|undefined): blah") -- var t **blob //@item(t, "t", "**blob", "var"),diag("t", re"t declared (and|but) not used"),diag("blob", re"(undeclared name|undefined): blob") -- //@complete("", q, t, x, bad_y_param, global_a, bob, random, random2, random3, stateFunc, stuff) +- "github.com/google/go-cmp/cmp" - -- return y --} +- "golang.org/x/tools/go/expect" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/debug" +- "golang.org/x/tools/gopls/internal/hooks" +- "golang.org/x/tools/gopls/internal/lsprpc" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/gopls/internal/test/compare" +- "golang.org/x/tools/gopls/internal/test/integration" +- "golang.org/x/tools/gopls/internal/test/integration/fake" +- "golang.org/x/tools/gopls/internal/util/bug" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/gopls/internal/util/slices" +- "golang.org/x/tools/internal/diff" +- "golang.org/x/tools/internal/diff/myers" +- "golang.org/x/tools/internal/jsonrpc2" +- "golang.org/x/tools/internal/jsonrpc2/servertest" +- "golang.org/x/tools/internal/testenv" +- "golang.org/x/tools/txtar" +-) - --func random3(y ...int) { //@item(random3, "random3", "func(y ...int)", "func"),item(y_variadic_param, "y", "[]int", "var") -- //@complete("", y_variadic_param, global_a, bob, random, random2, random3, stateFunc, stuff) +-var update = flag.Bool("update", false, "if set, update test data during marker tests") - -- var ch chan (favType1) //@item(ch, "ch", "chan (favType1)", "var"),diag("ch", re"ch declared (and|but) not used"),diag("favType1", re"(undeclared name|undefined): favType1") -- var m map[keyType]int //@item(m, "m", "map[keyType]int", "var"),diag("m", re"m declared (and|but) not used"),diag("keyType", re"(undeclared name|undefined): keyType") -- var arr []favType2 //@item(arr, "arr", "[]favType2", "var"),diag("arr", re"arr declared (and|but) not used"),diag("favType2", re"(undeclared name|undefined): favType2") -- var fn1 func() badResult //@item(fn1, "fn1", "func() badResult", "var"),diag("fn1", re"fn1 declared (and|but) not used"),diag("badResult", re"(undeclared name|undefined): badResult") -- var fn2 func(badParam) //@item(fn2, "fn2", "func(badParam)", "var"),diag("fn2", re"fn2 declared (and|but) not used"),diag("badParam", re"(undeclared name|undefined): badParam") -- //@complete("", arr, ch, fn1, fn2, m, y_variadic_param, global_a, bob, random, random2, random3, stateFunc, stuff) +-func TestMain(m *testing.M) { +- bug.PanicOnBugs = true +- testenv.ExitIfSmallMachine() +- // Disable GOPACKAGESDRIVER, as it can cause spurious test failures. +- os.Setenv("GOPACKAGESDRIVER", "off") +- os.Exit(m.Run()) -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/basic_lit.txt b/gopls/internal/regtest/marker/testdata/completion/basic_lit.txt ---- a/gopls/internal/regtest/marker/testdata/completion/basic_lit.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/basic_lit.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,19 +0,0 @@ --This test checks completion related to basic literals. - ---- flags -- ---ignore_extra_diags +-// Test runs the marker tests from the testdata directory. +-// +-// See package documentation for details on how marker tests work. +-// +-// These tests were inspired by (and in many places copied from) a previous +-// iteration of the marker tests built on top of the packagestest framework. +-// Key design decisions motivating this reimplementation are as follows: +-// - The old tests had a single global session, causing interaction at a +-// distance and several awkward workarounds. +-// - The old tests could not be safely parallelized, because certain tests +-// manipulated the server options +-// - Relatedly, the old tests did not have a logic grouping of assertions into +-// a single unit, resulting in clusters of files serving clusters of +-// entangled assertions. +-// - The old tests used locations in the source as test names and as the +-// identity of golden content, meaning that a single edit could change the +-// name of an arbitrary number of subtests, and making it difficult to +-// manually edit golden content. +-// - The old tests did not hew closely to LSP concepts, resulting in, for +-// example, each marker implementation doing its own position +-// transformations, and inventing its own mechanism for configuration. +-// - The old tests had an ad-hoc session initialization process. The integration +-// test environment has had more time devoted to its initialization, and has a +-// more convenient API. +-// - The old tests lacked documentation, and often had failures that were hard +-// to understand. By starting from scratch, we can revisit these aspects. +-func Test(t *testing.T) { +- if testing.Short() { +- builder := os.Getenv("GO_BUILDER_NAME") +- // Note that HasPrefix(builder, "darwin-" only matches legacy builders. +- // LUCI builder names start with x_tools-goN.NN. +- // We want to exclude solaris on both legacy and LUCI builders, as +- // it is timing out. +- if strings.HasPrefix(builder, "darwin-") || strings.Contains(builder, "solaris") { +- t.Skip("golang/go#64473: skipping with -short: this test is too slow on darwin and solaris builders") +- } +- } +- // The marker tests must be able to run go/packages.Load. +- testenv.NeedsGoPackages(t) - ---- basiclit.go -- --package basiclit +- const dir = "testdata" +- tests, err := loadMarkerTests(dir) +- if err != nil { +- t.Fatal(err) +- } - --func _() { -- var a int // something for lexical completions +- // Opt: use a shared cache. +- cache := cache.New(nil) - -- _ = "hello." //@complete(".") +- for _, test := range tests { +- test := test +- t.Run(test.name, func(t *testing.T) { +- t.Parallel() +- if test.skipReason != "" { +- t.Skip(test.skipReason) +- } +- if slices.Contains(test.skipGOOS, runtime.GOOS) { +- t.Skipf("skipping on %s due to -skip_goos", runtime.GOOS) +- } +- if slices.Contains(test.skipGOARCH, runtime.GOARCH) { +- t.Skipf("skipping on %s due to -skip_goos", runtime.GOOS) +- } - -- _ = 1 //@complete(" //") +- // TODO(rfindley): it may be more useful to have full support for build +- // constraints. +- if test.minGoVersion != "" { +- var go1point int +- if _, err := fmt.Sscanf(test.minGoVersion, "go1.%d", &go1point); err != nil { +- t.Fatalf("parsing -min_go version: %v", err) +- } +- testenv.NeedsGo1Point(t, go1point) +- } +- if test.cgo { +- testenv.NeedsTool(t, "cgo") +- } +- config := fake.EditorConfig{ +- Settings: test.settings, +- CapabilitiesJSON: test.capabilities, +- Env: test.env, +- } +- if _, ok := config.Settings["diagnosticsDelay"]; !ok { +- if config.Settings == nil { +- config.Settings = make(map[string]any) +- } +- config.Settings["diagnosticsDelay"] = "10ms" +- } +- // inv: config.Settings != nil - -- _ = 1. //@complete(".") +- run := &markerTestRun{ +- test: test, +- env: newEnv(t, cache, test.files, test.proxyFiles, test.writeGoSum, config), +- settings: config.Settings, +- values: make(map[expect.Identifier]any), +- diags: make(map[protocol.Location][]protocol.Diagnostic), +- extraNotes: make(map[protocol.DocumentURI]map[string][]*expect.Note), +- } +- // TODO(rfindley): make it easier to clean up the integration test environment. +- defer run.env.Editor.Shutdown(context.Background()) // ignore error +- defer run.env.Sandbox.Close() // ignore error - -- _ = 'a' //@complete("' ") --} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/builtins.txt b/gopls/internal/regtest/marker/testdata/completion/builtins.txt ---- a/gopls/internal/regtest/marker/testdata/completion/builtins.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/builtins.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,118 +0,0 @@ --This test checks completion of Go builtins. +- // Open all files so that we operate consistently with LSP clients, and +- // (pragmatically) so that we have a Mapper available via the fake +- // editor. +- // +- // This also allows avoiding mutating the editor state in tests. +- for file := range test.files { +- run.env.OpenFile(file) +- } +- // Wait for the didOpen notifications to be processed, then collect +- // diagnostics. +- var diags map[string]*protocol.PublishDiagnosticsParams +- run.env.AfterChange(integration.ReadAllDiagnostics(&diags)) +- for path, params := range diags { +- uri := run.env.Sandbox.Workdir.URI(path) +- for _, diag := range params.Diagnostics { +- loc := protocol.Location{ +- URI: uri, +- Range: protocol.Range{ +- Start: diag.Range.Start, +- End: diag.Range.Start, // ignore end positions +- }, +- } +- run.diags[loc] = append(run.diags[loc], diag) +- } +- } - ---- flags -- ---ignore_extra_diags ---filter_builtins=false +- var markers []marker +- for _, note := range test.notes { +- mark := marker{run: run, note: note} +- if fn, ok := valueMarkerFuncs[note.Name]; ok { +- fn(mark) +- } else if _, ok := actionMarkerFuncs[note.Name]; ok { +- markers = append(markers, mark) // save for later +- } else { +- uri := mark.uri() +- if run.extraNotes[uri] == nil { +- run.extraNotes[uri] = make(map[string][]*expect.Note) +- } +- run.extraNotes[uri][note.Name] = append(run.extraNotes[uri][note.Name], note) +- } +- } - ---- builtin_args.go -- --package builtins +- // Invoke each remaining marker in the test. +- for _, mark := range markers { +- actionMarkerFuncs[mark.note.Name](mark) +- } - --func _() { -- var ( -- aSlice []int //@item(builtinSlice, "aSlice", "[]int", "var") -- aMap map[string]int //@item(builtinMap, "aMap", "map[string]int", "var") -- aString string //@item(builtinString, "aString", "string", "var") -- aArray [0]int //@item(builtinArray, "aArray", "[0]int", "var") -- aArrayPtr *[0]int //@item(builtinArrayPtr, "aArrayPtr", "*[0]int", "var") -- aChan chan int //@item(builtinChan, "aChan", "chan int", "var") -- aPtr *int //@item(builtinPtr, "aPtr", "*int", "var") -- aInt int //@item(builtinInt, "aInt", "int", "var") -- ) +- // Any remaining (un-eliminated) diagnostics are an error. +- if !test.ignoreExtraDiags { +- for loc, diags := range run.diags { +- for _, diag := range diags { +- t.Errorf("%s: unexpected diagnostic: %q", run.fmtLoc(loc), diag.Message) +- } +- } +- } - -- type ( -- aSliceType []int //@item(builtinSliceType, "aSliceType", "[]int", "type") -- aChanType chan int //@item(builtinChanType, "aChanType", "chan int", "type") -- aMapType map[string]int //@item(builtinMapType, "aMapType", "map[string]int", "type") -- ) +- // TODO(rfindley): use these for whole-file marker tests. +- for uri, extras := range run.extraNotes { +- for name, extra := range extras { +- if len(extra) > 0 { +- t.Errorf("%s: %d unused %q markers", run.env.Sandbox.Workdir.URIToPath(uri), len(extra), name) +- } +- } +- } - -- close() //@rank(")", builtinChan, builtinSlice) +- formatted, err := formatTest(test) +- if err != nil { +- t.Errorf("formatTest: %v", err) +- } else if *update { +- filename := filepath.Join(dir, test.name) +- if err := os.WriteFile(filename, formatted, 0644); err != nil { +- t.Error(err) +- } +- } else if !t.Failed() { +- // Verify that the testdata has not changed. +- // +- // Only check this if the test hasn't already failed, otherwise we'd +- // report duplicate mismatches of golden data. +- // Otherwise, verify that formatted content matches. +- if diff := compare.NamedText("formatted", "on-disk", string(formatted), string(test.content)); diff != "" { +- t.Errorf("formatted test does not match on-disk content:\n%s", diff) +- } +- } +- }) +- } - -- append() //@rank(")", builtinSlice, builtinChan) +- if abs, err := filepath.Abs(dir); err == nil && t.Failed() { +- t.Logf("(Filenames are relative to %s.)", abs) +- } +-} - -- var _ []byte = append([]byte(nil), ""...) //@rank(") //") +-// A marker holds state for the execution of a single @marker +-// annotation in the source. +-type marker struct { +- run *markerTestRun +- note *expect.Note +-} - -- copy() //@rank(")", builtinSlice, builtinChan) -- copy(aSlice, aS) //@rank(")", builtinSlice, builtinString) -- copy(aS, aSlice) //@rank(",", builtinSlice, builtinString) +-// ctx returns the mark context. +-func (m marker) ctx() context.Context { return m.run.env.Ctx } - -- delete() //@rank(")", builtinMap, builtinChan) -- delete(aMap, aS) //@rank(")", builtinString, builtinSlice) +-// T returns the testing.TB for this mark. +-func (m marker) T() testing.TB { return m.run.env.T } - -- aMapFunc := func() map[int]int { //@item(builtinMapFunc, "aMapFunc", "func() map[int]int", "var") -- return nil -- } -- delete() //@rank(")", builtinMapFunc, builtinSlice) +-// server returns the LSP server for the marker test run. +-func (m marker) editor() *fake.Editor { return m.run.env.Editor } - -- len() //@rank(")", builtinSlice, builtinInt),rank(")", builtinMap, builtinInt),rank(")", builtinString, builtinInt),rank(")", builtinArray, builtinInt),rank(")", builtinArrayPtr, builtinPtr),rank(")", builtinChan, builtinInt) +-// server returns the LSP server for the marker test run. +-func (m marker) server() protocol.Server { return m.run.env.Editor.Server } - -- cap() //@rank(")", builtinSlice, builtinMap),rank(")", builtinArray, builtinString),rank(")", builtinArrayPtr, builtinPtr),rank(")", builtinChan, builtinInt) +-// uri returns the URI of the file containing the marker. +-func (mark marker) uri() protocol.DocumentURI { +- return mark.run.env.Sandbox.Workdir.URI(mark.run.test.fset.File(mark.note.Pos).Name()) +-} - -- make() //@rank(")", builtinMapType, int),rank(")", builtinChanType, int),rank(")", builtinSliceType, int),rank(")", builtinMapType, int) -- make(aSliceType, a) //@rank(")", builtinInt, builtinSlice) +-// document returns a protocol.TextDocumentIdentifier for the current file. +-func (mark marker) document() protocol.TextDocumentIdentifier { +- return protocol.TextDocumentIdentifier{URI: mark.uri()} +-} - -- type myInt int -- var mi myInt //@item(builtinMyInt, "mi", "myInt", "var") -- make(aSliceType, m) //@snippet(")", builtinMyInt, "mi") +-// path returns the relative path to the file containing the marker. +-func (mark marker) path() string { +- return mark.run.env.Sandbox.Workdir.RelPath(mark.run.test.fset.File(mark.note.Pos).Name()) +-} - -- var _ []int = make() //@rank(")", builtinSliceType, builtinMapType) +-// mapper returns a *protocol.Mapper for the current file. +-func (mark marker) mapper() *protocol.Mapper { +- mapper, err := mark.editor().Mapper(mark.path()) +- if err != nil { +- mark.T().Fatalf("failed to get mapper for current mark: %v", err) +- } +- return mapper +-} - -- type myStruct struct{} //@item(builtinStructType, "myStruct", "struct{...}", "struct") -- var _ *myStruct = new() //@rank(")", builtinStructType, int) +-// errorf reports an error with a prefix indicating the position of the marker note. +-// +-// It formats the error message using mark.sprintf. +-func (mark marker) errorf(format string, args ...any) { +- mark.T().Helper() +- msg := mark.sprintf(format, args...) +- // TODO(adonovan): consider using fmt.Fprintf(os.Stderr)+t.Fail instead of +- // t.Errorf to avoid reporting uninteresting positions in the Go source of +- // the driver. However, this loses the order of stderr wrt "FAIL: TestFoo" +- // subtest dividers. +- mark.T().Errorf("%s: %s", mark.run.fmtPos(mark.note.Pos), msg) +-} - -- for k := range a { //@rank(" {", builtinSlice, builtinInt),rank(" {", builtinString, builtinInt),rank(" {", builtinChan, builtinInt),rank(" {", builtinArray, builtinInt),rank(" {", builtinArrayPtr, builtinInt),rank(" {", builtinMap, builtinInt), +-// valueMarkerFunc returns a wrapper around a function that allows it to be +-// called during the processing of value markers (e.g. @value(v, 123)) with marker +-// arguments converted to function parameters. The provided function's first +-// parameter must be of type 'marker', and it must return a value. +-// +-// Unlike action markers, which are executed for actions such as test +-// assertions, value markers are all evaluated first, and each computes +-// a value that is recorded by its identifier, which is the marker's first +-// argument. These values may be referred to from an action marker by +-// this identifier, e.g. @action(... , v, ...). +-// +-// For example, given a fn with signature +-// +-// func(mark marker, label, details, kind string) CompletionItem +-// +-// The result of valueMarkerFunc can associated with @item notes, and invoked +-// as follows: +-// +-// //@item(FooCompletion, "Foo", "func() int", "func") +-// +-// The provided fn should not mutate the test environment. +-func valueMarkerFunc(fn any) func(marker) { +- ftype := reflect.TypeOf(fn) +- if ftype.NumIn() == 0 || ftype.In(0) != markerType { +- panic(fmt.Sprintf("value marker function %#v must accept marker as its first argument", ftype)) +- } +- if ftype.NumOut() != 1 { +- panic(fmt.Sprintf("value marker function %#v must have exactly 1 result", ftype)) +- } +- +- return func(mark marker) { +- if len(mark.note.Args) == 0 || !is[expect.Identifier](mark.note.Args[0]) { +- mark.errorf("first argument to a value marker function must be an identifier") +- return +- } +- id := mark.note.Args[0].(expect.Identifier) +- if alt, ok := mark.run.values[id]; ok { +- mark.errorf("%s already declared as %T", id, alt) +- return +- } +- args := append([]any{mark}, mark.note.Args[1:]...) +- argValues, err := convertArgs(mark, ftype, args) +- if err != nil { +- mark.errorf("converting args: %v", err) +- return +- } +- results := reflect.ValueOf(fn).Call(argValues) +- mark.run.values[id] = results[0].Interface() +- } +-} +- +-// actionMarkerFunc returns a wrapper around a function that allows it to be +-// called during the processing of action markers (e.g. @action("abc", 123)) +-// with marker arguments converted to function parameters. The provided +-// function's first parameter must be of type 'marker', and it must not return +-// any values. +-// +-// The provided fn should not mutate the test environment. +-func actionMarkerFunc(fn any) func(marker) { +- ftype := reflect.TypeOf(fn) +- if ftype.NumIn() == 0 || ftype.In(0) != markerType { +- panic(fmt.Sprintf("action marker function %#v must accept marker as its first argument", ftype)) +- } +- if ftype.NumOut() != 0 { +- panic(fmt.Sprintf("action marker function %#v cannot have results", ftype)) +- } +- +- return func(mark marker) { +- args := append([]any{mark}, mark.note.Args...) +- argValues, err := convertArgs(mark, ftype, args) +- if err != nil { +- mark.errorf("converting args: %v", err) +- return +- } +- reflect.ValueOf(fn).Call(argValues) - } +-} - -- for k, v := range a { //@rank(" {", builtinSlice, builtinChan) +-func convertArgs(mark marker, ftype reflect.Type, args []any) ([]reflect.Value, error) { +- var ( +- argValues []reflect.Value +- pnext int // next param index +- p reflect.Type // current param +- ) +- for i, arg := range args { +- if i < ftype.NumIn() { +- p = ftype.In(pnext) +- pnext++ +- } else if p == nil || !ftype.IsVariadic() { +- // The actual number of arguments expected by the mark varies, depending +- // on whether this is a value marker or an action marker. +- // +- // Since this error indicates a bug, probably OK to have an imprecise +- // error message here. +- return nil, fmt.Errorf("too many arguments to %s", mark.note.Name) +- } +- elemType := p +- if ftype.IsVariadic() && pnext == ftype.NumIn() { +- elemType = p.Elem() +- } +- var v reflect.Value +- if id, ok := arg.(expect.Identifier); ok && id == "_" { +- v = reflect.Zero(elemType) +- } else { +- a, err := convert(mark, arg, elemType) +- if err != nil { +- return nil, err +- } +- v = reflect.ValueOf(a) +- } +- argValues = append(argValues, v) +- } +- // Check that we have sufficient arguments. If the function is variadic, we +- // do not need arguments for the final parameter. +- if pnext < ftype.NumIn()-1 || pnext == ftype.NumIn()-1 && !ftype.IsVariadic() { +- // Same comment as above: OK to be vague here. +- return nil, fmt.Errorf("not enough arguments to %s", mark.note.Name) - } +- return argValues, nil +-} - -- <-a //@rank(" //", builtinChan, builtinInt) +-// is reports whether arg is a T. +-func is[T any](arg any) bool { +- _, ok := arg.(T) +- return ok -} - ---- builtin_types.go -- --package builtins +-// Supported value marker functions. See [valueMarkerFunc] for more details. +-var valueMarkerFuncs = map[string]func(marker){ +- "loc": valueMarkerFunc(locMarker), +- "item": valueMarkerFunc(completionItemMarker), +-} - --func _() { -- var _ []bool //@item(builtinBoolSliceType, "[]bool", "[]bool", "type") +-// Supported action marker functions. See [actionMarkerFunc] for more details. +-var actionMarkerFuncs = map[string]func(marker){ +- "acceptcompletion": actionMarkerFunc(acceptCompletionMarker), +- "codeaction": actionMarkerFunc(codeActionMarker), +- "codeactionedit": actionMarkerFunc(codeActionEditMarker), +- "codeactionerr": actionMarkerFunc(codeActionErrMarker), +- "codelenses": actionMarkerFunc(codeLensesMarker), +- "complete": actionMarkerFunc(completeMarker), +- "def": actionMarkerFunc(defMarker), +- "diag": actionMarkerFunc(diagMarker), +- "documentlink": actionMarkerFunc(documentLinkMarker), +- "foldingrange": actionMarkerFunc(foldingRangeMarker), +- "format": actionMarkerFunc(formatMarker), +- "highlight": actionMarkerFunc(highlightMarker), +- "hover": actionMarkerFunc(hoverMarker), +- "hovererr": actionMarkerFunc(hoverErrMarker), +- "implementation": actionMarkerFunc(implementationMarker), +- "incomingcalls": actionMarkerFunc(incomingCallsMarker), +- "inlayhints": actionMarkerFunc(inlayhintsMarker), +- "outgoingcalls": actionMarkerFunc(outgoingCallsMarker), +- "preparerename": actionMarkerFunc(prepareRenameMarker), +- "rank": actionMarkerFunc(rankMarker), +- "rankl": actionMarkerFunc(ranklMarker), +- "refs": actionMarkerFunc(refsMarker), +- "rename": actionMarkerFunc(renameMarker), +- "renameerr": actionMarkerFunc(renameErrMarker), +- "selectionrange": actionMarkerFunc(selectionRangeMarker), +- "signature": actionMarkerFunc(signatureMarker), +- "snippet": actionMarkerFunc(snippetMarker), +- "suggestedfix": actionMarkerFunc(suggestedfixMarker), +- "suggestedfixerr": actionMarkerFunc(suggestedfixErrMarker), +- "symbol": actionMarkerFunc(symbolMarker), +- "token": actionMarkerFunc(tokenMarker), +- "typedef": actionMarkerFunc(typedefMarker), +- "workspacesymbol": actionMarkerFunc(workspaceSymbolMarker), +-} - -- var _ []bool = make() //@rank(")", builtinBoolSliceType, int) +-// markerTest holds all the test data extracted from a test txtar archive. +-// +-// See the documentation for RunMarkerTests for more information on the archive +-// format. +-type markerTest struct { +- name string // relative path to the txtar file in the testdata dir +- fset *token.FileSet // fileset used for parsing notes +- content []byte // raw test content +- archive *txtar.Archive // original test archive +- settings map[string]any // gopls settings +- capabilities []byte // content of capabilities.json file +- env map[string]string // editor environment +- proxyFiles map[string][]byte // proxy content +- files map[string][]byte // data files from the archive (excluding special files) +- notes []*expect.Note // extracted notes from data files +- golden map[expect.Identifier]*Golden // extracted golden content, by identifier name - -- var _ []bool = make([], 0) //@rank(",", bool, int) +- skipReason string // the skip reason extracted from the "skip" archive file +- flags []string // flags extracted from the special "flags" archive file. - -- var _ [][]bool = make([][], 0) //@rank(",", bool, int) +- // Parsed flags values. +- minGoVersion string +- cgo bool +- writeGoSum []string // comma separated dirs to write go sum for +- skipGOOS []string // comma separated GOOS values to skip +- skipGOARCH []string // comma separated GOARCH values to skip +- ignoreExtraDiags bool +- filterBuiltins bool +- filterKeywords bool -} - ---- builtins.go -- --package builtins -- --// Definitions of builtin completion items that are still used in tests. +-// flagSet returns the flagset used for parsing the special "flags" file in the +-// test archive. +-func (t *markerTest) flagSet() *flag.FlagSet { +- flags := flag.NewFlagSet(t.name, flag.ContinueOnError) +- flags.StringVar(&t.minGoVersion, "min_go", "", "if set, the minimum go1.X version required for this test") +- flags.BoolVar(&t.cgo, "cgo", false, "if set, requires cgo (both the cgo tool and CGO_ENABLED=1)") +- flags.Var((*stringListValue)(&t.writeGoSum), "write_sumfile", "if set, write the sumfile for these directories") +- flags.Var((*stringListValue)(&t.skipGOOS), "skip_goos", "if set, skip this test on these GOOS values") +- flags.Var((*stringListValue)(&t.skipGOARCH), "skip_goarch", "if set, skip this test on these GOARCH values") +- flags.BoolVar(&t.ignoreExtraDiags, "ignore_extra_diags", false, "if set, suppress errors for unmatched diagnostics") +- flags.BoolVar(&t.filterBuiltins, "filter_builtins", true, "if set, filter builtins from completion results") +- flags.BoolVar(&t.filterKeywords, "filter_keywords", true, "if set, filter keywords from completion results") +- return flags +-} - --/* bool */ //@item(bool, "bool", "", "type") --/* complex(r float64, i float64) */ //@item(complex, "complex", "func(r float64, i float64) complex128", "func") --/* float32 */ //@item(float32, "float32", "", "type") --/* float64 */ //@item(float64, "float64", "", "type") --/* imag(c complex128) float64 */ //@item(imag, "imag", "func(c complex128) float64", "func") --/* int */ //@item(int, "int", "", "type") --/* iota */ //@item(iota, "iota", "", "const") --/* string */ //@item(string, "string", "", "type") --/* true */ //@item(_true, "true", "", "const") +-// stringListValue implements flag.Value. +-type stringListValue []string - ---- constants.go -- --package builtins +-func (l *stringListValue) Set(s string) error { +- if s != "" { +- for _, d := range strings.Split(s, ",") { +- *l = append(*l, strings.TrimSpace(d)) +- } +- } +- return nil +-} - --func _() { -- const ( -- foo = iota //@complete(" //", iota) -- ) +-func (l stringListValue) String() string { +- return strings.Join([]string(l), ",") +-} - -- iota //@complete(" //") +-func (t *markerTest) getGolden(id expect.Identifier) *Golden { +- golden, ok := t.golden[id] +- // If there was no golden content for this identifier, we must create one +- // to handle the case where -update is set: we need a place to store +- // the updated content. +- if !ok { +- golden = &Golden{id: id} - -- var iota int //@item(iotaVar, "iota", "int", "var") +- // TODO(adonovan): the separation of markerTest (the +- // static aspects) from markerTestRun (the dynamic +- // ones) is evidently bogus because here we modify +- // markerTest during execution. Let's merge the two. +- t.golden[id] = golden +- } +- return golden +-} - -- iota //@complete(" //", iotaVar) +-// Golden holds extracted golden content for a single @<name> prefix. +-// +-// When -update is set, golden captures the updated golden contents for later +-// writing. +-type Golden struct { +- id expect.Identifier +- data map[string][]byte // key "" => @id itself +- updated map[string][]byte -} - --func _() { -- var twoRedUpEnd bool //@item(TRUEVar, "twoRedUpEnd", "bool", "var") +-// Get returns golden content for the given name, which corresponds to the +-// relative path following the golden prefix @<name>/. For example, to access +-// the content of @foo/path/to/result.json from the Golden associated with +-// @foo, name should be "path/to/result.json". +-// +-// If -update is set, the given update function will be called to get the +-// updated golden content that should be written back to testdata. +-// +-// Marker functions must use this method instead of accessing data entries +-// directly otherwise the -update operation will delete those entries. +-// +-// TODO(rfindley): rethink the logic here. We may want to separate Get and Set, +-// and not delete golden content that isn't set. +-func (g *Golden) Get(t testing.TB, name string, updated []byte) ([]byte, bool) { +- if existing, ok := g.updated[name]; ok { +- // Multiple tests may reference the same golden data, but if they do they +- // must agree about its expected content. +- if diff := compare.NamedText("existing", "updated", string(existing), string(updated)); diff != "" { +- t.Errorf("conflicting updates for golden data %s/%s:\n%s", g.id, name, diff) +- } +- } +- if g.updated == nil { +- g.updated = make(map[string][]byte) +- } +- g.updated[name] = updated +- if *update { +- return updated, true +- } - -- var _ bool = true //@rank(" //", _true, TRUEVar) +- res, ok := g.data[name] +- return res, ok -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/casesensitive.txt b/gopls/internal/regtest/marker/testdata/completion/casesensitive.txt ---- a/gopls/internal/regtest/marker/testdata/completion/casesensitive.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/casesensitive.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,24 +0,0 @@ --This test exercises the caseSensitive completion matcher. - ---- flags -- ---ignore_extra_diags +-// loadMarkerTests walks the given dir looking for .txt files, which it +-// interprets as a txtar archive. +-// +-// See the documentation for RunMarkerTests for more details on the test data +-// archive. +-func loadMarkerTests(dir string) ([]*markerTest, error) { +- var tests []*markerTest +- err := filepath.WalkDir(dir, func(path string, _ fs.DirEntry, err error) error { +- if strings.HasSuffix(path, ".txt") { +- content, err := os.ReadFile(path) +- if err != nil { +- return err +- } - ---- settings.json -- --{ -- "completeUnimported": false, -- "matcher": "caseSensitive" +- name := strings.TrimPrefix(path, dir+string(filepath.Separator)) +- test, err := loadMarkerTest(name, content) +- if err != nil { +- return fmt.Errorf("%s: %v", path, err) +- } +- tests = append(tests, test) +- } +- return err +- }) +- return tests, err -} - ---- casesensitive.go -- --package casesensitive -- --func _() { -- var lower int //@item(lower, "lower", "int", "var") -- var Upper int //@item(upper, "Upper", "int", "var") +-func loadMarkerTest(name string, content []byte) (*markerTest, error) { +- archive := txtar.Parse(content) +- if len(archive.Files) == 0 { +- return nil, fmt.Errorf("txtar file has no '-- filename --' sections") +- } +- if bytes.Contains(archive.Comment, []byte("\n-- ")) { +- // This check is conservative, but the comment is only a comment. +- return nil, fmt.Errorf("ill-formed '-- filename --' header in comment") +- } +- test := &markerTest{ +- name: name, +- fset: token.NewFileSet(), +- content: content, +- archive: archive, +- files: make(map[string][]byte), +- golden: make(map[expect.Identifier]*Golden), +- } +- for _, file := range archive.Files { +- switch { +- case file.Name == "skip": +- reason := strings.ReplaceAll(string(file.Data), "\n", " ") +- reason = strings.TrimSpace(reason) +- test.skipReason = reason - -- l //@complete(" //", lower) -- U //@complete(" //", upper) +- case file.Name == "flags": +- test.flags = strings.Fields(string(file.Data)) - -- L //@complete(" //") -- u //@complete(" //") --} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/cast.txt b/gopls/internal/regtest/marker/testdata/completion/cast.txt ---- a/gopls/internal/regtest/marker/testdata/completion/cast.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/cast.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,17 +0,0 @@ --This test checks completion related to casts. +- case file.Name == "settings.json": +- if err := json.Unmarshal(file.Data, &test.settings); err != nil { +- return nil, err +- } - ---- flags -- ---ignore_extra_diags +- case file.Name == "capabilities.json": +- test.capabilities = file.Data // lazily unmarshalled by the editor - ---- cast.go -- --package cast +- case file.Name == "env": +- test.env = make(map[string]string) +- fields := strings.Fields(string(file.Data)) +- for _, field := range fields { +- key, value, ok := strings.Cut(field, "=") +- if !ok { +- return nil, fmt.Errorf("env vars must be formatted as var=value, got %q", field) +- } +- test.env[key] = value +- } - --func _() { -- foo := struct{x int}{x: 1} //@item(x_field, "x", "int", "field") -- _ = float64(foo.x) //@complete("x", x_field) --} +- case strings.HasPrefix(file.Name, "@"): // golden content +- idstring, name, _ := strings.Cut(file.Name[len("@"):], "/") +- id := expect.Identifier(idstring) +- // Note that a file.Name of just "@id" gives (id, name) = ("id", ""). +- if _, ok := test.golden[id]; !ok { +- test.golden[id] = &Golden{ +- id: id, +- data: make(map[string][]byte), +- } +- } +- test.golden[id].data[name] = file.Data - --func _() { -- foo := struct{x int}{x: 1} -- _ = float64(foo. //@complete(" /", x_field) --} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/channel.txt b/gopls/internal/regtest/marker/testdata/completion/channel.txt ---- a/gopls/internal/regtest/marker/testdata/completion/channel.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/channel.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,36 +0,0 @@ --This test checks completion related to channels. +- case strings.HasPrefix(file.Name, "proxy/"): +- name := file.Name[len("proxy/"):] +- if test.proxyFiles == nil { +- test.proxyFiles = make(map[string][]byte) +- } +- test.proxyFiles[name] = file.Data - ---- flags -- ---ignore_extra_diags +- default: // ordinary file content +- notes, err := expect.Parse(test.fset, file.Name, file.Data) +- if err != nil { +- return nil, fmt.Errorf("parsing notes in %q: %v", file.Name, err) +- } - ---- settings.json -- --{ -- "completeUnimported": false --} +- // Reject common misspelling: "// @mark". +- // TODO(adonovan): permit "// @" within a string. Detect multiple spaces. +- if i := bytes.Index(file.Data, []byte("// @")); i >= 0 { +- line := 1 + bytes.Count(file.Data[:i], []byte("\n")) +- return nil, fmt.Errorf("%s:%d: unwanted space before marker (// @)", file.Name, line) +- } - ---- channel.go -- --package channel +- // The 'go list' command doesn't work correct with modules named +- // testdata", so don't allow it as a module name (golang/go#65406). +- // (Otherwise files within it will end up in an ad hoc +- // package, "command-line-arguments/$TMPDIR/...".) +- if filepath.Base(file.Name) == "go.mod" && +- bytes.Contains(file.Data, []byte("module testdata")) { +- return nil, fmt.Errorf("'testdata' is not a valid module name") +- } - --func _() { -- var ( -- aa = "123" //@item(channelAA, "aa", "string", "var") -- ab = 123 //@item(channelAB, "ab", "int", "var") -- ) +- test.notes = append(test.notes, notes...) +- test.files[file.Name] = file.Data +- } - -- { -- type myChan chan int -- var mc myChan -- mc <- a //@complete(" //", channelAB, channelAA) +- // Print a warning if we see what looks like "-- filename --" +- // without the second "--". It's not necessarily wrong, +- // but it should almost never appear in our test inputs. +- if bytes.Contains(file.Data, []byte("\n-- ")) { +- log.Printf("ill-formed '-- filename --' header in %s?", file.Name) +- } - } - -- { -- var ac chan int //@item(channelAC, "ac", "chan int", "var") -- a <- a //@complete(" <-", channelAC, channelAA, channelAB) +- // Parse flags after loading files, as they may have been set by the "flags" +- // file. +- if err := test.flagSet().Parse(test.flags); err != nil { +- return nil, fmt.Errorf("parsing flags: %v", err) - } - -- { -- var foo chan int //@item(channelFoo, "foo", "chan int", "var") -- wantsInt := func(int) {} //@item(channelWantsInt, "wantsInt", "func(int)", "var") -- wantsInt(<-) //@rank(")", channelFoo, channelAB) -- } +- return test, nil -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/comment.txt b/gopls/internal/regtest/marker/testdata/completion/comment.txt ---- a/gopls/internal/regtest/marker/testdata/completion/comment.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/comment.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,81 +0,0 @@ --This test checks behavior of completion within comments. -- ---- flags -- ---ignore_extra_diags -- ---- go.mod -- --module golang.org/lsptests/comment -- --go 1.18 -- ---- p.go -- --package comment_completion -- --var p bool - --//@complete(re"$") +-// formatTest formats the test as a txtar archive. +-func formatTest(test *markerTest) ([]byte, error) { +- arch := &txtar.Archive{ +- Comment: test.archive.Comment, +- } - --func _() { -- var a int +- updatedGolden := make(map[string][]byte) +- for id, g := range test.golden { +- for name, data := range g.updated { +- filename := "@" + path.Join(string(id), name) // name may be "" +- updatedGolden[filename] = data +- } +- } - -- switch a { -- case 1: -- //@complete(re"$") -- _ = a +- // Preserve the original ordering of archive files. +- for _, file := range test.archive.Files { +- switch file.Name { +- // Preserve configuration files exactly as they were. They must have parsed +- // if we got this far. +- case "skip", "flags", "settings.json", "capabilities.json", "env": +- arch.Files = append(arch.Files, file) +- default: +- if _, ok := test.files[file.Name]; ok { // ordinary file +- arch.Files = append(arch.Files, file) +- } else if strings.HasPrefix(file.Name, "proxy/") { // proxy file +- arch.Files = append(arch.Files, file) +- } else if data, ok := updatedGolden[file.Name]; ok { // golden file +- arch.Files = append(arch.Files, txtar.File{Name: file.Name, Data: data}) +- delete(updatedGolden, file.Name) +- } +- } - } - -- var b chan int -- select { -- case <-b: -- //@complete(re"$") -- _ = b +- // ...followed by any new golden files. +- var newGoldenFiles []txtar.File +- for filename, data := range updatedGolden { +- // TODO(rfindley): it looks like this implicitly removes trailing newlines +- // from golden content. Is there any way to fix that? Perhaps we should +- // just make the diff tolerant of missing newlines? +- newGoldenFiles = append(newGoldenFiles, txtar.File{Name: filename, Data: data}) - } +- // Sort new golden files lexically. +- sort.Slice(newGoldenFiles, func(i, j int) bool { +- return newGoldenFiles[i].Name < newGoldenFiles[j].Name +- }) +- arch.Files = append(arch.Files, newGoldenFiles...) - -- var ( -- //@complete(re"$") -- _ = a -- ) +- return txtar.Format(arch), nil -} - --// //@complete(" ", variableC) --var C string //@item(variableC, "C", "string", "var") //@complete(" ", variableC) +-// newEnv creates a new environment for a marker test. +-// +-// TODO(rfindley): simplify and refactor the construction of testing +-// environments across integration tests, marker tests, and benchmarks. +-func newEnv(t *testing.T, cache *cache.Cache, files, proxyFiles map[string][]byte, writeGoSum []string, config fake.EditorConfig) *integration.Env { +- sandbox, err := fake.NewSandbox(&fake.SandboxConfig{ +- RootDir: t.TempDir(), +- Files: files, +- ProxyFiles: proxyFiles, +- }) +- if err != nil { +- t.Fatal(err) +- } - --// //@complete(" ", constant) --const Constant = "example" //@item(constant, "Constant", "string", "const") //@complete(" ", constant) +- for _, dir := range writeGoSum { +- if err := sandbox.RunGoCommand(context.Background(), dir, "list", []string{"-mod=mod", "..."}, []string{"GOWORK=off"}, true); err != nil { +- t.Fatal(err) +- } +- } - --// //@complete(" ", structType, fieldB, fieldA) --type StructType struct { //@item(structType, "StructType", "struct{...}", "struct") //@complete(" ", structType, fieldA, fieldB) -- // //@complete(" ", fieldA, structType, fieldB) -- A string //@item(fieldA, "A", "string", "field") //@complete(" ", fieldA, structType, fieldB) -- b int //@item(fieldB, "b", "int", "field") //@complete(" ", fieldB, structType, fieldA) --} +- // Put a debug instance in the context to prevent logging to stderr. +- // See associated TODO in runner.go: we should revisit this pattern. +- ctx := context.Background() +- ctx = debug.WithInstance(ctx, "off") - --// //@complete(" ", method, structRecv, paramX, resultY, fieldB, fieldA) --func (structType *StructType) Method(X int) (Y int) { //@item(structRecv, "structType", "*StructType", "var"),item(method, "Method", "func(X int) (Y int)", "method"),item(paramX, "X", "int", "var"),item(resultY, "Y", "int", "var") -- // //@complete(" ", method, structRecv, paramX, resultY, fieldB, fieldA) -- return +- awaiter := integration.NewAwaiter(sandbox.Workdir) +- ss := lsprpc.NewStreamServer(cache, false, hooks.Options) +- server := servertest.NewPipeServer(ss, jsonrpc2.NewRawStream) +- const skipApplyEdits = true // capture edits but don't apply them +- editor, err := fake.NewEditor(sandbox, config).Connect(ctx, server, awaiter.Hooks(), skipApplyEdits) +- if err != nil { +- sandbox.Close() // ignore error +- t.Fatal(err) +- } +- if err := awaiter.Await(ctx, integration.InitialWorkspaceLoad); err != nil { +- sandbox.Close() // ignore error +- t.Fatal(err) +- } +- return &integration.Env{ +- T: t, +- Ctx: ctx, +- Editor: editor, +- Sandbox: sandbox, +- Awaiter: awaiter, +- } -} - --// //@complete(" ", newType) --type NewType string //@item(newType, "NewType", "string", "type") //@complete(" ", newType) +-// A markerTestRun holds the state of one run of a marker test archive. +-type markerTestRun struct { +- test *markerTest +- env *integration.Env +- settings map[string]any - --// //@complete(" ", testInterface, testA, testB) --type TestInterface interface { //@item(testInterface, "TestInterface", "interface{...}", "interface") -- // //@complete(" ", testA, testInterface, testB) -- TestA(L string) (M int) //@item(testA, "TestA", "func(L string) (M int)", "method"),item(paramL, "L", "var", "string"),item(resM, "M", "var", "int") //@complete(" ", testA, testInterface, testB) -- TestB(N int) bool //@item(testB, "TestB", "func(N int) bool", "method"),item(paramN, "N", "var", "int") //@complete(" ", testB, testInterface, testA) --} +- // Collected information. +- // Each @diag/@suggestedfix marker eliminates an entry from diags. +- values map[expect.Identifier]any +- diags map[protocol.Location][]protocol.Diagnostic // diagnostics by position; location end == start - --// //@complete(" ", function) --func Function() int { //@item(function, "Function", "func() int", "func") //@complete(" ", function) -- // //@complete(" ", function) -- return 0 +- // Notes that weren't associated with a top-level marker func. They may be +- // consumed by another marker (e.g. @codelenses collects @codelens markers). +- // Any notes that aren't consumed are flagged as an error. +- extraNotes map[protocol.DocumentURI]map[string][]*expect.Note -} - --// This tests multiline block comments and completion with prefix --// Lorem Ipsum Multili//@complete("Multi", multiline) --// Lorem ipsum dolor sit ametom --func Multiline() int { //@item(multiline, "Multiline", "func() int", "func") -- // //@complete(" ", multiline) -- return 0 +-// sprintf returns a formatted string after applying pre-processing to +-// arguments of the following types: +-// - token.Pos: formatted using (*markerTestRun).fmtPos +-// - protocol.Location: formatted using (*markerTestRun).fmtLoc +-func (c *marker) sprintf(format string, args ...any) string { +- if false { +- _ = fmt.Sprintf(format, args...) // enable vet printf checker +- } +- var args2 []any +- for _, arg := range args { +- switch arg := arg.(type) { +- case token.Pos: +- args2 = append(args2, c.run.fmtPos(arg)) +- case protocol.Location: +- args2 = append(args2, c.run.fmtLoc(arg)) +- default: +- args2 = append(args2, arg) +- } +- } +- return fmt.Sprintf(format, args2...) -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/complit.txt b/gopls/internal/regtest/marker/testdata/completion/complit.txt ---- a/gopls/internal/regtest/marker/testdata/completion/complit.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/complit.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,104 +0,0 @@ --This test checks completion related to composite literals. - ---- flags -- ---ignore_extra_diags -- ---- settings.json -- --{ -- "completeUnimported": false +-// fmtLoc formats the given pos in the context of the test, using +-// archive-relative paths for files and including the line number in the full +-// archive file. +-func (run *markerTestRun) fmtPos(pos token.Pos) string { +- file := run.test.fset.File(pos) +- if file == nil { +- run.env.T.Errorf("position %d not in test fileset", pos) +- return "<invalid location>" +- } +- m, err := run.env.Editor.Mapper(file.Name()) +- if err != nil { +- run.env.T.Errorf("%s", err) +- return "<invalid location>" +- } +- loc, err := m.PosLocation(file, pos, pos) +- if err != nil { +- run.env.T.Errorf("Mapper(%s).PosLocation failed: %v", file.Name(), err) +- } +- return run.fmtLoc(loc) -} - ---- complit.go -- --package complit -- --// Literal completion results. --/* int() */ //@item(int, "int()", "int", "var") -- --// general completions -- --type position struct { //@item(structPosition, "position", "struct{...}", "struct") -- X, Y int //@item(fieldX, "X", "int", "field"),item(fieldY, "Y", "int", "field") +-// fmtLoc formats the given location in the context of the test, using +-// archive-relative paths for files and including the line number in the full +-// archive file. +-func (run *markerTestRun) fmtLoc(loc protocol.Location) string { +- formatted := run.fmtLocDetails(loc, true) +- if formatted == "" { +- run.env.T.Errorf("unable to find %s in test archive", loc) +- return "<invalid location>" +- } +- return formatted -} - --func _() { -- _ = position{ -- //@complete("", fieldX, fieldY, int, structPosition) +-// See fmtLoc. If includeTxtPos is not set, the position in the full archive +-// file is omitted. +-// +-// If the location cannot be found within the archive, fmtLocDetails returns "". +-func (run *markerTestRun) fmtLocDetails(loc protocol.Location, includeTxtPos bool) string { +- if loc == (protocol.Location{}) { +- return "" - } -- _ = position{ -- X: 1, -- //@complete("", fieldY) +- lines := bytes.Count(run.test.archive.Comment, []byte("\n")) +- var name string +- for _, f := range run.test.archive.Files { +- lines++ // -- separator -- +- uri := run.env.Sandbox.Workdir.URI(f.Name) +- if uri == loc.URI { +- name = f.Name +- break +- } +- lines += bytes.Count(f.Data, []byte("\n")) - } -- _ = position{ -- //@complete("", fieldX) -- Y: 1, +- if name == "" { +- return "" - } -- _ = []*position{ -- { -- //@complete("", fieldX, fieldY, int, structPosition) -- }, +- m, err := run.env.Editor.Mapper(name) +- if err != nil { +- run.env.T.Errorf("internal error: %v", err) +- return "<invalid location>" +- } +- start, end, err := m.RangeOffsets(loc.Range) +- if err != nil { +- run.env.T.Errorf("error formatting location %s: %v", loc, err) +- return "<invalid location>" - } --} -- --func _() { - var ( -- aa string //@item(aaVar, "aa", "string", "var") -- ab int //@item(abVar, "ab", "int", "var") +- startLine, startCol8 = m.OffsetLineCol8(start) +- endLine, endCol8 = m.OffsetLineCol8(end) - ) -- -- _ = map[int]int{ -- a: a, //@complete(":", abVar, aaVar),complete(",", abVar, aaVar) +- innerSpan := fmt.Sprintf("%d:%d", startLine, startCol8) // relative to the embedded file +- outerSpan := fmt.Sprintf("%d:%d", lines+startLine, startCol8) // relative to the archive file +- if start != end { +- if endLine == startLine { +- innerSpan += fmt.Sprintf("-%d", endCol8) +- outerSpan += fmt.Sprintf("-%d", endCol8) +- } else { +- innerSpan += fmt.Sprintf("-%d:%d", endLine, endCol8) +- outerSpan += fmt.Sprintf("-%d:%d", lines+endLine, endCol8) +- } - } - -- _ = map[int]int{ -- //@complete("", abVar, int, aaVar, structPosition) +- if includeTxtPos { +- return fmt.Sprintf("%s:%s (%s:%s)", name, innerSpan, run.test.name, outerSpan) +- } else { +- return fmt.Sprintf("%s:%s", name, innerSpan) - } +-} - -- _ = []string{a: ""} //@complete(":", abVar, aaVar) -- _ = [1]string{a: ""} //@complete(":", abVar, aaVar) +-// ---- converters ---- - -- _ = position{X: a} //@complete("}", abVar, aaVar) -- _ = position{a} //@complete("}", abVar, aaVar) -- _ = position{a, } //@complete("}", abVar, int, aaVar, structPosition) +-// converter is the signature of argument converters. +-// A converter should return an error rather than calling marker.errorf(). +-// +-// type converter func(marker, any) (any, error) - -- _ = []int{a} //@complete("}", abVar, aaVar) -- _ = [1]int{a} //@complete("}", abVar, aaVar) +-// Types with special conversions. +-var ( +- goldenType = reflect.TypeOf(&Golden{}) +- locationType = reflect.TypeOf(protocol.Location{}) +- markerType = reflect.TypeOf(marker{}) +- stringMatcherType = reflect.TypeOf(stringMatcher{}) +-) - -- type myStruct struct { -- AA int //@item(fieldAA, "AA", "int", "field") -- AB string //@item(fieldAB, "AB", "string", "field") +-func convert(mark marker, arg any, paramType reflect.Type) (any, error) { +- // Handle stringMatcher and golden parameters before resolving identifiers, +- // because golden content lives in a separate namespace from other +- // identifiers. +- switch paramType { +- case stringMatcherType: +- return convertStringMatcher(mark, arg) +- case goldenType: +- id, ok := arg.(expect.Identifier) +- if !ok { +- return nil, fmt.Errorf("invalid input type %T: golden key must be an identifier", arg) +- } +- return mark.run.test.getGolden(id), nil +- } +- if id, ok := arg.(expect.Identifier); ok { +- if arg, ok := mark.run.values[id]; ok { +- if !reflect.TypeOf(arg).AssignableTo(paramType) { +- return nil, fmt.Errorf("cannot convert %v (%T) to %s", arg, arg, paramType) +- } +- return arg, nil +- } +- } +- if paramType == locationType { +- return convertLocation(mark, arg) - } +- if reflect.TypeOf(arg).AssignableTo(paramType) { +- return arg, nil // no conversion required +- } +- return nil, fmt.Errorf("cannot convert %v (%T) to %s", arg, arg, paramType) +-} - -- _ = myStruct{ -- AB: a, //@complete(",", aaVar, abVar) +-// convertLocation converts a string or regexp argument into the protocol +-// location corresponding to the first position of the string (or first match +-// of the regexp) in the line preceding the note. +-func convertLocation(mark marker, arg any) (protocol.Location, error) { +- switch arg := arg.(type) { +- case string: +- startOff, preceding, m, err := linePreceding(mark.run, mark.note.Pos) +- if err != nil { +- return protocol.Location{}, err +- } +- idx := bytes.Index(preceding, []byte(arg)) +- if idx < 0 { +- return protocol.Location{}, fmt.Errorf("substring %q not found in %q", arg, preceding) +- } +- off := startOff + idx +- return m.OffsetLocation(off, off+len(arg)) +- case *regexp.Regexp: +- return findRegexpInLine(mark.run, mark.note.Pos, arg) +- default: +- return protocol.Location{}, fmt.Errorf("cannot convert argument type %T to location (must be a string to match the preceding line)", arg) - } +-} - -- var s myStruct +-// findRegexpInLine searches the partial line preceding pos for a match for the +-// regular expression re, returning a location spanning the first match. If re +-// contains exactly one subgroup, the position of this subgroup match is +-// returned rather than the position of the full match. +-func findRegexpInLine(run *markerTestRun, pos token.Pos, re *regexp.Regexp) (protocol.Location, error) { +- startOff, preceding, m, err := linePreceding(run, pos) +- if err != nil { +- return protocol.Location{}, err +- } - -- _ = map[int]string{1: "" + s.A} //@complete("}", fieldAB, fieldAA) -- _ = map[int]string{1: (func(i int) string { return "" })(s.A)} //@complete(")}", fieldAA, fieldAB) -- _ = map[int]string{1: func() string { s.A }} //@complete(" }", fieldAA, fieldAB) +- matches := re.FindSubmatchIndex(preceding) +- if len(matches) == 0 { +- return protocol.Location{}, fmt.Errorf("no match for regexp %q found in %q", re, string(preceding)) +- } +- var start, end int +- switch len(matches) { +- case 2: +- // no subgroups: return the range of the regexp expression +- start, end = matches[0], matches[1] +- case 4: +- // one subgroup: return its range +- start, end = matches[2], matches[3] +- default: +- return protocol.Location{}, fmt.Errorf("invalid location regexp %q: expect either 0 or 1 subgroups, got %d", re, len(matches)/2-1) +- } - -- _ = position{s.A} //@complete("}", fieldAA, fieldAB) +- return m.OffsetLocation(start+startOff, end+startOff) +-} - -- var X int //@item(varX, "X", "int", "var") -- _ = position{X} //@complete("}", fieldX, varX) +-func linePreceding(run *markerTestRun, pos token.Pos) (int, []byte, *protocol.Mapper, error) { +- file := run.test.fset.File(pos) +- posn := safetoken.Position(file, pos) +- lineStart := file.LineStart(posn.Line) +- startOff, endOff, err := safetoken.Offsets(file, lineStart, pos) +- if err != nil { +- return 0, nil, nil, err +- } +- m, err := run.env.Editor.Mapper(file.Name()) +- if err != nil { +- return 0, nil, nil, err +- } +- return startOff, m.Content[startOff:endOff], m, nil -} - --func _() { -- type foo struct{} //@item(complitFoo, "foo", "struct{...}", "struct") +-// convertStringMatcher converts a string, regexp, or identifier +-// argument into a stringMatcher. The string is a substring of the +-// expected error, the regexp is a pattern than matches the expected +-// error, and the identifier is a golden file containing the expected +-// error. +-func convertStringMatcher(mark marker, arg any) (stringMatcher, error) { +- switch arg := arg.(type) { +- case string: +- return stringMatcher{substr: arg}, nil +- case *regexp.Regexp: +- return stringMatcher{pattern: arg}, nil +- case expect.Identifier: +- golden := mark.run.test.getGolden(arg) +- return stringMatcher{golden: golden}, nil +- default: +- return stringMatcher{}, fmt.Errorf("cannot convert %T to wantError (want: string, regexp, or identifier)", arg) +- } +-} - -- var _ *foo = &fo{} //@snippet("{", complitFoo, "foo") -- var _ *foo = fo{} //@snippet("{", complitFoo, "&foo") +-// A stringMatcher represents an expectation of a specific string value. +-// +-// It may be indicated in one of three ways, in 'expect' notation: +-// - an identifier 'foo', to compare (exactly) with the contents of the golden +-// section @foo; +-// - a pattern expression re"ab.*c", to match against a regular expression; +-// - a string literal "abc", to check for a substring. +-type stringMatcher struct { +- golden *Golden +- pattern *regexp.Regexp +- substr string +-} - -- struct { a, b *foo }{ -- a: &fo{}, //@rank("{", complitFoo) -- b: fo{}, //@snippet("{", complitFoo, "&foo") +-func (sc stringMatcher) String() string { +- if sc.golden != nil { +- return fmt.Sprintf("content from @%s entry", sc.golden.id) +- } else if sc.pattern != nil { +- return fmt.Sprintf("content matching %#q", sc.pattern) +- } else { +- return fmt.Sprintf("content with substring %q", sc.substr) - } -} - --func _() { -- _ := position{ -- X: 1, //@complete("X", fieldX),complete(" 1", int, structPosition) -- Y: , //@complete(":", fieldY),complete(" ,", int, structPosition) +-// checkErr asserts that the given error matches the stringMatcher's expectations. +-func (sc stringMatcher) checkErr(mark marker, err error) { +- if err == nil { +- mark.errorf("@%s succeeded unexpectedly, want %v", mark.note.Name, sc) +- return - } +- sc.check(mark, err.Error()) -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/constant.txt b/gopls/internal/regtest/marker/testdata/completion/constant.txt ---- a/gopls/internal/regtest/marker/testdata/completion/constant.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/constant.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,20 +0,0 @@ --This test checks completion related to constants. - ---- flags -- ---ignore_extra_diags +-// check asserts that the given content matches the stringMatcher's expectations. +-func (sc stringMatcher) check(mark marker, got string) { +- if sc.golden != nil { +- compareGolden(mark, []byte(got), sc.golden) +- } else if sc.pattern != nil { +- // Content must match the regular expression pattern. +- if !sc.pattern.MatchString(got) { +- mark.errorf("got %q, does not match pattern %#q", got, sc.pattern) +- } - ---- constant.go -- --package constant +- } else if !strings.Contains(got, sc.substr) { +- // Content must contain the expected substring. +- mark.errorf("got %q, want substring %q", got, sc.substr) +- } +-} - --const x = 1 //@item(constX, "x", "int", "const") +-// checkChangedFiles compares the files changed by an operation with their expected (golden) state. +-func checkChangedFiles(mark marker, changed map[string][]byte, golden *Golden) { +- // Check changed files match expectations. +- for filename, got := range changed { +- if want, ok := golden.Get(mark.T(), filename, got); !ok { +- mark.errorf("%s: unexpected change to file %s; got:\n%s", +- mark.note.Name, filename, got) - --const ( -- a int = iota << 2 //@item(constA, "a", "int", "const") -- b //@item(constB, "b", "int", "const") -- c //@item(constC, "c", "int", "const") --) +- } else if string(got) != string(want) { +- mark.errorf("%s: wrong file content for %s: got:\n%s\nwant:\n%s\ndiff:\n%s", +- mark.note.Name, filename, got, want, +- compare.Bytes(want, got)) +- } +- } - --func _() { -- const y = "hi" //@item(constY, "y", "string", "const") -- //@complete("", constY, constA, constB, constC, constX) +- // Report unmet expectations. +- for filename := range golden.data { +- if _, ok := changed[filename]; !ok { +- want, _ := golden.Get(mark.T(), filename, nil) +- mark.errorf("%s: missing change to file %s; want:\n%s", +- mark.note.Name, filename, want) +- } +- } -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/danglingstmt.txt b/gopls/internal/regtest/marker/testdata/completion/danglingstmt.txt ---- a/gopls/internal/regtest/marker/testdata/completion/danglingstmt.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/danglingstmt.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,158 +0,0 @@ --This test checks that completion works as expected in the presence of --incomplete statements that may affect parser recovery. - ---- flags -- ---ignore_extra_diags +-// checkDiffs computes unified diffs for each changed file, and compares with +-// the diff content stored in the given golden directory. +-func checkDiffs(mark marker, changed map[string][]byte, golden *Golden) { +- diffs := make(map[string]string) +- for name, after := range changed { +- before := mark.run.env.FileContent(name) +- // TODO(golang/go#64023): switch back to diff.Strings. +- // The attached issue is only one obstacle to switching. +- // Another is that different diff algorithms produce +- // different results, so if we commit diffs in test +- // expectations, then we need to either (1) state +- // which diff implementation they use and never change +- // it, or (2) don't compare diffs, but instead apply +- // the "want" diff and check that it produces the +- // "got" output. Option 2 is more robust, as it allows +- // the test expectation to use any valid diff. +- edits := myers.ComputeEdits(before, string(after)) +- d, err := diff.ToUnified("before", "after", before, edits, 0) +- if err != nil { +- // Can't happen: edits are consistent. +- log.Fatalf("internal error in diff.ToUnified: %v", err) +- } +- // Trim the unified header from diffs, as it is unnecessary and repetitive. +- difflines := strings.Split(d, "\n") +- if len(difflines) >= 2 && strings.HasPrefix(difflines[1], "+++") { +- diffs[name] = strings.Join(difflines[2:], "\n") +- } else { +- diffs[name] = d +- } +- } +- // Check changed files match expectations. +- for filename, got := range diffs { +- if want, ok := golden.Get(mark.T(), filename, []byte(got)); !ok { +- mark.errorf("%s: unexpected change to file %s; got diff:\n%s", +- mark.note.Name, filename, got) - ---- go.mod -- --module golang.org/lsptests/dangling +- } else if got != string(want) { +- mark.errorf("%s: wrong diff for %s:\n\ngot:\n%s\n\nwant:\n%s\n", +- mark.note.Name, filename, got, want) +- } +- } +- // Report unmet expectations. +- for filename := range golden.data { +- if _, ok := changed[filename]; !ok { +- want, _ := golden.Get(mark.T(), filename, nil) +- mark.errorf("%s: missing change to file %s; want:\n%s", +- mark.note.Name, filename, want) +- } +- } +-} - --go 1.18 +-// ---- marker functions ---- - ---- settings.json -- --{ -- "completeUnimported": false, -- "deepCompletion": false +-// TODO(rfindley): consolidate documentation of these markers. They are already +-// documented above, so much of the documentation here is redundant. +- +-// completionItem is a simplified summary of a completion item. +-type completionItem struct { +- Label, Detail, Kind, Documentation string -} - ---- dangling_for.go -- --package danglingstmt +-func completionItemMarker(mark marker, label string, other ...string) completionItem { +- if len(other) > 3 { +- mark.errorf("too many arguments to @item: expect at most 4") +- } +- item := completionItem{ +- Label: label, +- } +- if len(other) > 0 { +- item.Detail = other[0] +- } +- if len(other) > 1 { +- item.Kind = other[1] +- } +- if len(other) > 2 { +- item.Documentation = other[2] +- } +- return item +-} - --func _() { -- for bar //@rank(" //", danglingBar) +-func rankMarker(mark marker, src protocol.Location, items ...completionItem) { +- // Separate positive and negative items (expectations). +- var pos, neg []completionItem +- for _, item := range items { +- if strings.HasPrefix(item.Label, "!") { +- neg = append(neg, item) +- } else { +- pos = append(pos, item) +- } +- } +- +- // Collect results that are present in items, preserving their order. +- list := mark.run.env.Completion(src) +- var got []string +- for _, g := range list.Items { +- for _, w := range pos { +- if g.Label == w.Label { +- got = append(got, g.Label) +- break +- } +- } +- for _, w := range neg { +- if g.Label == w.Label[len("!"):] { +- mark.errorf("got unwanted completion: %s", g.Label) +- break +- } +- } +- } +- var want []string +- for _, w := range pos { +- want = append(want, w.Label) +- } +- if diff := cmp.Diff(want, got); diff != "" { +- mark.errorf("completion rankings do not match (-want +got):\n%s", diff) +- } -} - --func bar() bool { //@item(danglingBar, "bar", "func() bool", "func") -- return true +-func ranklMarker(mark marker, src protocol.Location, labels ...string) { +- // Separate positive and negative labels (expectations). +- var pos, neg []string +- for _, label := range labels { +- if strings.HasPrefix(label, "!") { +- neg = append(neg, label[len("!"):]) +- } else { +- pos = append(pos, label) +- } +- } +- +- // Collect results that are present in items, preserving their order. +- list := mark.run.env.Completion(src) +- var got []string +- for _, g := range list.Items { +- if slices.Contains(pos, g.Label) { +- got = append(got, g.Label) +- } else if slices.Contains(neg, g.Label) { +- mark.errorf("got unwanted completion: %s", g.Label) +- } +- } +- if diff := cmp.Diff(pos, got); diff != "" { +- mark.errorf("completion rankings do not match (-want +got):\n%s", diff) +- } -} - ---- dangling_for_init.go -- --package danglingstmt +-func snippetMarker(mark marker, src protocol.Location, item completionItem, want string) { +- list := mark.run.env.Completion(src) +- var ( +- found bool +- got string +- all []string // for errors +- ) +- items := filterBuiltinsAndKeywords(mark, list.Items) +- for _, i := range items { +- all = append(all, i.Label) +- if i.Label == item.Label { +- found = true +- if i.TextEdit != nil { +- got = i.TextEdit.NewText +- } +- break +- } +- } +- if !found { +- mark.errorf("no completion item found matching %s (got: %v)", item.Label, all) +- return +- } +- if got != want { +- mark.errorf("snippets do not match: got %q, want %q", got, want) +- } +-} - --func _() { -- for i := bar //@rank(" //", danglingBar2) +-// completeMarker implements the @complete marker, running +-// textDocument/completion at the given src location and asserting that the +-// results match the expected results. +-func completeMarker(mark marker, src protocol.Location, want ...completionItem) { +- list := mark.run.env.Completion(src) +- items := filterBuiltinsAndKeywords(mark, list.Items) +- var got []completionItem +- for i, item := range items { +- simplified := completionItem{ +- Label: item.Label, +- Detail: item.Detail, +- Kind: fmt.Sprint(item.Kind), +- } +- if item.Documentation != nil { +- switch v := item.Documentation.Value.(type) { +- case string: +- simplified.Documentation = v +- case protocol.MarkupContent: +- simplified.Documentation = strings.TrimSpace(v.Value) // trim newlines +- } +- } +- // Support short-hand notation: if Detail, Kind, or Documentation are omitted from the +- // item, don't match them. +- if i < len(want) { +- if want[i].Detail == "" { +- simplified.Detail = "" +- } +- if want[i].Kind == "" { +- simplified.Kind = "" +- } +- if want[i].Documentation == "" { +- simplified.Documentation = "" +- } +- } +- got = append(got, simplified) +- } +- if len(want) == 0 { +- want = nil // got is nil if empty +- } +- if diff := cmp.Diff(want, got); diff != "" { +- mark.errorf("Completion(...) returned unexpect results (-want +got):\n%s", diff) +- } -} - --func bar2() int { //@item(danglingBar2, "bar2", "func() int", "func") -- return 0 +-// filterBuiltinsAndKeywords filters out builtins and keywords from completion +-// results. +-// +-// It over-approximates, and does not detect if builtins are shadowed. +-func filterBuiltinsAndKeywords(mark marker, items []protocol.CompletionItem) []protocol.CompletionItem { +- keep := 0 +- for _, item := range items { +- if mark.run.test.filterKeywords && item.Kind == protocol.KeywordCompletion { +- continue +- } +- if mark.run.test.filterBuiltins && types.Universe.Lookup(item.Label) != nil { +- continue +- } +- items[keep] = item +- keep++ +- } +- return items[:keep] -} - ---- dangling_for_init_cond.go -- --package danglingstmt +-// acceptCompletionMarker implements the @acceptCompletion marker, running +-// textDocument/completion at the given src location and accepting the +-// candidate with the given label. The resulting source must match the provided +-// golden content. +-func acceptCompletionMarker(mark marker, src protocol.Location, label string, golden *Golden) { +- list := mark.run.env.Completion(src) +- var selected *protocol.CompletionItem +- for _, item := range list.Items { +- if item.Label == label { +- selected = &item +- break +- } +- } +- if selected == nil { +- mark.errorf("Completion(...) did not return an item labeled %q", label) +- return +- } +- filename := mark.path() +- mapper := mark.mapper() +- patched, _, err := protocol.ApplyEdits(mapper, append([]protocol.TextEdit{*selected.TextEdit}, selected.AdditionalTextEdits...)) - --func _() { -- for i := bar3(); i > bar //@rank(" //", danglingBar3) +- if err != nil { +- mark.errorf("ApplyProtocolEdits failed: %v", err) +- return +- } +- changes := map[string][]byte{filename: patched} +- // Check the file state. +- checkChangedFiles(mark, changes, golden) -} - --func bar3() int { //@item(danglingBar3, "bar3", "func() int", "func") -- return 0 +-// defMarker implements the @def marker, running textDocument/definition at +-// the given src location and asserting that there is exactly one resulting +-// location, matching dst. +-// +-// TODO(rfindley): support a variadic destination set. +-func defMarker(mark marker, src, dst protocol.Location) { +- got := mark.run.env.GoToDefinition(src) +- if got != dst { +- mark.errorf("definition location does not match:\n\tgot: %s\n\twant %s", +- mark.run.fmtLoc(got), mark.run.fmtLoc(dst)) +- } -} - ---- dangling_for_init_cond_post.go -- --package danglingstmt -- --func _() { -- for i := bar4(); i > bar4(); i += bar //@rank(" //", danglingBar4) +-func typedefMarker(mark marker, src, dst protocol.Location) { +- got := mark.run.env.TypeDefinition(src) +- if got != dst { +- mark.errorf("type definition location does not match:\n\tgot: %s\n\twant %s", +- mark.run.fmtLoc(got), mark.run.fmtLoc(dst)) +- } -} - --func bar4() int { //@item(danglingBar4, "bar4", "func() int", "func") -- return 0 +-func foldingRangeMarker(mark marker, g *Golden) { +- env := mark.run.env +- ranges, err := mark.server().FoldingRange(env.Ctx, &protocol.FoldingRangeParams{ +- TextDocument: mark.document(), +- }) +- if err != nil { +- mark.errorf("foldingRange failed: %v", err) +- return +- } +- var edits []protocol.TextEdit +- insert := func(line, char uint32, text string) { +- pos := protocol.Position{Line: line, Character: char} +- edits = append(edits, protocol.TextEdit{ +- Range: protocol.Range{ +- Start: pos, +- End: pos, +- }, +- NewText: text, +- }) +- } +- for i, rng := range ranges { +- insert(rng.StartLine, rng.StartCharacter, fmt.Sprintf("<%d kind=%q>", i, rng.Kind)) +- insert(rng.EndLine, rng.EndCharacter, fmt.Sprintf("</%d>", i)) +- } +- filename := mark.path() +- mapper, err := env.Editor.Mapper(filename) +- if err != nil { +- mark.errorf("Editor.Mapper(%s) failed: %v", filename, err) +- return +- } +- got, _, err := protocol.ApplyEdits(mapper, edits) +- if err != nil { +- mark.errorf("ApplyProtocolEdits failed: %v", err) +- return +- } +- want, _ := g.Get(mark.T(), "", got) +- if diff := compare.Bytes(want, got); diff != "" { +- mark.errorf("foldingRange mismatch:\n%s", diff) +- } -} - ---- dangling_if.go -- --package danglingstmt +-// formatMarker implements the @format marker. +-func formatMarker(mark marker, golden *Golden) { +- edits, err := mark.server().Formatting(mark.ctx(), &protocol.DocumentFormattingParams{ +- TextDocument: mark.document(), +- }) +- var got []byte +- if err != nil { +- got = []byte(err.Error() + "\n") // all golden content is newline terminated +- } else { +- env := mark.run.env +- filename := mark.path() +- mapper, err := env.Editor.Mapper(filename) +- if err != nil { +- mark.errorf("Editor.Mapper(%s) failed: %v", filename, err) +- } - --func _() { -- if foo //@rank(" //", danglingFoo) --} +- got, _, err = protocol.ApplyEdits(mapper, edits) +- if err != nil { +- mark.errorf("ApplyProtocolEdits failed: %v", err) +- return +- } +- } - --func foo() bool { //@item(danglingFoo, "foo", "func() bool", "func") -- return true +- compareGolden(mark, got, golden) -} - ---- dangling_if_eof.go -- --package danglingstmt -- --func bar5() bool { //@item(danglingBar5, "bar5", "func() bool", "func") -- return true --} +-func highlightMarker(mark marker, src protocol.Location, dsts ...protocol.Location) { +- highlights := mark.run.env.DocumentHighlight(src) +- var got []protocol.Range +- for _, h := range highlights { +- got = append(got, h.Range) +- } - --func _() { -- if b //@rank(" //", danglingBar5) +- var want []protocol.Range +- for _, d := range dsts { +- want = append(want, d.Range) +- } - ---- dangling_if_init.go -- --package danglingstmt +- sortRanges := func(s []protocol.Range) { +- sort.Slice(s, func(i, j int) bool { +- return protocol.CompareRange(s[i], s[j]) < 0 +- }) +- } - --func _() { -- if i := foo //@rank(" //", danglingFoo2) --} +- sortRanges(got) +- sortRanges(want) - --func foo2() bool { //@item(danglingFoo2, "foo2", "func() bool", "func") -- return true +- if diff := cmp.Diff(want, got); diff != "" { +- mark.errorf("DocumentHighlight(%v) mismatch (-want +got):\n%s", src, diff) +- } -} - ---- dangling_if_init_cond.go -- --package danglingstmt -- --func _() { -- if i := 123; foo //@rank(" //", danglingFoo3) +-func hoverMarker(mark marker, src, dst protocol.Location, sc stringMatcher) { +- content, gotDst := mark.run.env.Hover(src) +- if gotDst != dst { +- mark.errorf("hover location does not match:\n\tgot: %s\n\twant %s)", mark.run.fmtLoc(gotDst), mark.run.fmtLoc(dst)) +- } +- gotMD := "" +- if content != nil { +- gotMD = content.Value +- } +- sc.check(mark, gotMD) -} - --func foo3() bool { //@item(danglingFoo3, "foo3", "func() bool", "func") -- return true +-func hoverErrMarker(mark marker, src protocol.Location, em stringMatcher) { +- _, _, err := mark.editor().Hover(mark.ctx(), src) +- em.checkErr(mark, err) -} - ---- dangling_multiline_if.go -- --package danglingstmt +-// locMarker implements the @loc marker. It is executed before other +-// markers, so that locations are available. +-func locMarker(mark marker, loc protocol.Location) protocol.Location { return loc } - --func walrus() bool { //@item(danglingWalrus, "walrus", "func() bool", "func") -- return true +-// diagMarker implements the @diag marker. It eliminates diagnostics from +-// the observed set in mark.test. +-func diagMarker(mark marker, loc protocol.Location, re *regexp.Regexp) { +- if _, ok := removeDiagnostic(mark, loc, re); !ok { +- mark.errorf("no diagnostic at %v matches %q", loc, re) +- } -} - --func _() { -- if true && -- walrus //@complete(" //", danglingWalrus) +-// removeDiagnostic looks for a diagnostic matching loc at the given position. +-// +-// If found, it returns (diag, true), and eliminates the matched diagnostic +-// from the unmatched set. +-// +-// If not found, it returns (protocol.Diagnostic{}, false). +-func removeDiagnostic(mark marker, loc protocol.Location, re *regexp.Regexp) (protocol.Diagnostic, bool) { +- loc.Range.End = loc.Range.Start // diagnostics ignore end position. +- diags := mark.run.diags[loc] +- for i, diag := range diags { +- if re.MatchString(diag.Message) { +- mark.run.diags[loc] = append(diags[:i], diags[i+1:]...) +- return diag, true +- } +- } +- return protocol.Diagnostic{}, false -} - ---- dangling_selector_1.go -- --package danglingstmt +-// renameMarker implements the @rename(location, new, golden) marker. +-func renameMarker(mark marker, loc protocol.Location, newName string, golden *Golden) { +- changed, err := rename(mark.run.env, loc, newName) +- if err != nil { +- mark.errorf("rename failed: %v. (Use @renameerr for expected errors.)", err) +- return +- } +- checkDiffs(mark, changed, golden) +-} - --func _() { -- x. //@rank(" //", danglingI) +-// renameErrMarker implements the @renamererr(location, new, error) marker. +-func renameErrMarker(mark marker, loc protocol.Location, newName string, wantErr stringMatcher) { +- _, err := rename(mark.run.env, loc, newName) +- wantErr.checkErr(mark, err) -} - --var x struct { i int } //@item(danglingI, "i", "int", "field") +-func selectionRangeMarker(mark marker, loc protocol.Location, g *Golden) { +- ranges, err := mark.server().SelectionRange(mark.ctx(), &protocol.SelectionRangeParams{ +- TextDocument: mark.document(), +- Positions: []protocol.Position{loc.Range.Start}, +- }) +- if err != nil { +- mark.errorf("SelectionRange failed: %v", err) +- return +- } +- var buf bytes.Buffer +- m := mark.mapper() +- for i, path := range ranges { +- fmt.Fprintf(&buf, "Ranges %d:", i) +- rng := path +- for { +- s, e, err := m.RangeOffsets(rng.Range) +- if err != nil { +- mark.errorf("RangeOffsets failed: %v", err) +- return +- } - ---- dangling_selector_2.go -- --package danglingstmt +- var snippet string +- if e-s < 30 { +- snippet = string(m.Content[s:e]) +- } else { +- snippet = string(m.Content[s:s+15]) + "..." + string(m.Content[e-15:e]) +- } - --// TODO: re-enable this test, which was broken when the foo package was removed. --// (we can replicate the relevant definitions in the new marker test) --// import "golang.org/lsptests/foo" +- fmt.Fprintf(&buf, "\n\t%v %q", rng.Range, strings.ReplaceAll(snippet, "\n", "\\n")) - --func _() { -- foo. // rank(" //", Foo) -- var _ = []string{foo.} // rank("}", Foo) +- if rng.Parent == nil { +- break +- } +- rng = *rng.Parent +- } +- buf.WriteRune('\n') +- } +- compareGolden(mark, buf.Bytes(), g) -} - ---- dangling_switch_init.go -- --package danglingstmt -- --func _() { -- switch i := baz //@rank(" //", danglingBaz) +-func tokenMarker(mark marker, loc protocol.Location, tokenType, mod string) { +- tokens := mark.run.env.SemanticTokensRange(loc) +- if len(tokens) != 1 { +- mark.errorf("got %d tokens, want 1", len(tokens)) +- return +- } +- tok := tokens[0] +- if tok.TokenType != tokenType { +- mark.errorf("token type = %q, want %q", tok.TokenType, tokenType) +- } +- if tok.Mod != mod { +- mark.errorf("token mod = %q, want %q", tok.Mod, mod) +- } -} - --func baz() int { //@item(danglingBaz, "baz", "func() int", "func") -- return 0 +-func signatureMarker(mark marker, src protocol.Location, label string, active int64) { +- got := mark.run.env.SignatureHelp(src) +- if label == "" { +- // A null result is expected. +- // (There's no point having a @signatureerr marker +- // because the server handler suppresses all errors.) +- if got != nil && len(got.Signatures) > 0 { +- mark.errorf("signatureHelp = %v, want 0 signatures", got) +- } +- return +- } +- if got == nil || len(got.Signatures) != 1 { +- mark.errorf("signatureHelp = %v, want exactly 1 signature", got) +- return +- } +- if got := got.Signatures[0].Label; got != label { +- mark.errorf("signatureHelp: got label %q, want %q", got, label) +- } +- if got := int64(got.ActiveParameter); got != active { +- mark.errorf("signatureHelp: got active parameter %d, want %d", got, active) +- } -} - ---- dangling_switch_init_tag.go -- --package danglingstmt +-// rename returns the new contents of the files that would be modified +-// by renaming the identifier at loc to newName. +-func rename(env *integration.Env, loc protocol.Location, newName string) (map[string][]byte, error) { +- // We call Server.Rename directly, instead of +- // env.Editor.Rename(env.Ctx, loc, newName) +- // to isolate Rename from PrepareRename, and because we don't +- // want to modify the file system in a scenario with multiple +- // @rename markers. - --func _() { -- switch i := 0; baz //@rank(" //", danglingBaz2) --} +- editMap, err := env.Editor.Server.Rename(env.Ctx, &protocol.RenameParams{ +- TextDocument: protocol.TextDocumentIdentifier{URI: loc.URI}, +- Position: loc.Range.Start, +- NewName: newName, +- }) +- if err != nil { +- return nil, err +- } - --func baz2() int { //@item(danglingBaz2, "baz2", "func() int", "func") -- return 0 +- fileChanges := make(map[string][]byte) +- if err := applyDocumentChanges(env, editMap.DocumentChanges, fileChanges); err != nil { +- return nil, fmt.Errorf("applying document changes: %v", err) +- } +- return fileChanges, nil -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/deep2.txt b/gopls/internal/regtest/marker/testdata/completion/deep2.txt ---- a/gopls/internal/regtest/marker/testdata/completion/deep2.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/deep2.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,65 +0,0 @@ --This test exercises deep completion. -- --It was originally bundled with deep.go, but is split into a separate test as --the new marker tests do not permit mutating server options for individual --marks. -- ---- flags -- ---ignore_extra_diags - ---- go.mod -- --module golang.org/lsptests -- --go 1.18 +-// applyDocumentChanges applies the given document changes to the editor buffer +-// content, recording the resulting contents in the fileChanges map. It is an +-// error for a change to an edit a file that is already present in the +-// fileChanges map. +-func applyDocumentChanges(env *integration.Env, changes []protocol.DocumentChanges, fileChanges map[string][]byte) error { +- getMapper := func(path string) (*protocol.Mapper, error) { +- if _, ok := fileChanges[path]; ok { +- return nil, fmt.Errorf("internal error: %s is already edited", path) +- } +- return env.Editor.Mapper(path) +- } - ---- deep/deep2.go -- --package deep +- for _, change := range changes { +- if change.RenameFile != nil { +- // rename +- oldFile := env.Sandbox.Workdir.URIToPath(change.RenameFile.OldURI) +- mapper, err := getMapper(oldFile) +- if err != nil { +- return err +- } +- newFile := env.Sandbox.Workdir.URIToPath(change.RenameFile.NewURI) +- fileChanges[newFile] = mapper.Content +- } else { +- // edit +- filename := env.Sandbox.Workdir.URIToPath(change.TextDocumentEdit.TextDocument.URI) +- mapper, err := getMapper(filename) +- if err != nil { +- return err +- } +- patched, _, err := protocol.ApplyEdits(mapper, protocol.AsTextEdits(change.TextDocumentEdit.Edits)) +- if err != nil { +- return err +- } +- fileChanges[filename] = patched +- } +- } - --type foo struct { -- b bar +- return nil -} - --func (f foo) bar() bar { -- return f.b --} +-func codeActionMarker(mark marker, start, end protocol.Location, actionKind string, g *Golden, titles ...string) { +- // Request the range from start.Start to end.End. +- loc := start +- loc.Range.End = end.Range.End - --func (f foo) barPtr() *bar { -- return &f.b +- // Apply the fix it suggests. +- changed, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil, titles) +- if err != nil { +- mark.errorf("codeAction failed: %v", err) +- return +- } +- +- // Check the file state. +- checkChangedFiles(mark, changed, g) -} - --type bar struct{} +-func codeActionEditMarker(mark marker, loc protocol.Location, actionKind string, g *Golden, titles ...string) { +- changed, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil, titles) +- if err != nil { +- mark.errorf("codeAction failed: %v", err) +- return +- } - --func (b bar) valueReceiver() int { -- return 0 +- checkDiffs(mark, changed, g) -} - --func (b *bar) ptrReceiver() int { -- return 0 +-func codeActionErrMarker(mark marker, start, end protocol.Location, actionKind string, wantErr stringMatcher) { +- loc := start +- loc.Range.End = end.Range.End +- _, err := codeAction(mark.run.env, loc.URI, loc.Range, actionKind, nil, nil) +- wantErr.checkErr(mark, err) -} - --func _() { -- var ( -- i int -- f foo -- ) +-// codeLensesMarker runs the @codelenses() marker, collecting @codelens marks +-// in the current file and comparing with the result of the +-// textDocument/codeLens RPC. +-func codeLensesMarker(mark marker) { +- type codeLens struct { +- Range protocol.Range +- Title string +- } - -- f.bar().valueReceiver //@item(deepBarValue, "f.bar().valueReceiver", "func() int", "method") -- f.barPtr().ptrReceiver //@item(deepBarPtrPtr, "f.barPtr().ptrReceiver", "func() int", "method") -- f.barPtr().valueReceiver //@item(deepBarPtrValue, "f.barPtr().valueReceiver", "func() int", "method") +- lenses := mark.run.env.CodeLens(mark.path()) +- var got []codeLens +- for _, lens := range lenses { +- title := "" +- if lens.Command != nil { +- title = lens.Command.Title +- } +- got = append(got, codeLens{lens.Range, title}) +- } - -- i = fbar //@complete(" //", deepBarValue, deepBarPtrPtr, deepBarPtrValue) --} +- var want []codeLens +- mark.consumeExtraNotes("codelens", actionMarkerFunc(func(_ marker, loc protocol.Location, title string) { +- want = append(want, codeLens{loc.Range, title}) +- })) - --func (b baz) Thing() struct{ val int } { -- return b.thing --} +- for _, s := range [][]codeLens{got, want} { +- sort.Slice(s, func(i, j int) bool { +- li, lj := s[i], s[j] +- if c := protocol.CompareRange(li.Range, lj.Range); c != 0 { +- return c < 0 +- } +- return li.Title < lj.Title +- }) +- } - --type baz struct { -- thing struct{ val int } +- if diff := cmp.Diff(want, got); diff != "" { +- mark.errorf("codelenses: unexpected diff (-want +got):\n%s", diff) +- } -} - --func (b baz) _() { -- b.Thing().val //@item(deepBazMethVal, "b.Thing().val", "int", "field") -- b.thing.val //@item(deepBazFieldVal, "b.thing.val", "int", "field") -- var _ int = bval //@rank(" //", deepBazFieldVal, deepBazMethVal) --} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/deep.txt b/gopls/internal/regtest/marker/testdata/completion/deep.txt ---- a/gopls/internal/regtest/marker/testdata/completion/deep.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/deep.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,110 +0,0 @@ --This test exercises deep completion. +-func documentLinkMarker(mark marker, g *Golden) { +- var b bytes.Buffer +- links := mark.run.env.DocumentLink(mark.path()) +- for _, l := range links { +- if l.Target == nil { +- mark.errorf("%s: nil link target", l.Range) +- continue +- } +- loc := protocol.Location{URI: mark.uri(), Range: l.Range} +- fmt.Fprintln(&b, mark.run.fmtLocDetails(loc, false), *l.Target) +- } - ---- settings.json -- --{ -- "completeUnimported": false, -- "matcher": "caseInsensitive" +- compareGolden(mark, b.Bytes(), g) -} - ---- flags -- ---ignore_extra_diags -- ---- go.mod -- --module golang.org/lsptests -- --go 1.18 -- ---- deep/deep.go -- --package deep -- --import "context" -- --type deepA struct { -- b deepB //@item(deepBField, "b", "deepB", "field") --} +-// consumeExtraNotes runs the provided func for each extra note with the given +-// name, and deletes all matching notes. +-func (mark marker) consumeExtraNotes(name string, f func(marker)) { +- uri := mark.uri() +- notes := mark.run.extraNotes[uri][name] +- delete(mark.run.extraNotes[uri], name) - --type deepB struct { +- for _, note := range notes { +- f(marker{run: mark.run, note: note}) +- } -} - --func wantsDeepB(deepB) {} +-// suggestedfixMarker implements the @suggestedfix(location, regexp, +-// kind, golden) marker. It acts like @diag(location, regexp), to set +-// the expectation of a diagnostic, but then it applies the first code +-// action of the specified kind suggested by the matched diagnostic. +-func suggestedfixMarker(mark marker, loc protocol.Location, re *regexp.Regexp, golden *Golden) { +- loc.Range.End = loc.Range.Start // diagnostics ignore end position. +- // Find and remove the matching diagnostic. +- diag, ok := removeDiagnostic(mark, loc, re) +- if !ok { +- mark.errorf("no diagnostic at %v matches %q", loc, re) +- return +- } - --func _() { -- var a deepA //@item(deepAVar, "a", "deepA", "var") -- a.b //@item(deepABField, "a.b", "deepB", "field") -- wantsDeepB(a) //@complete(")", deepABField, deepAVar) +- // Apply the fix it suggests. +- changed, err := codeAction(mark.run.env, loc.URI, diag.Range, "quickfix", &diag, nil) +- if err != nil { +- mark.errorf("suggestedfix failed: %v. (Use @suggestedfixerr for expected errors.)", err) +- return +- } - -- deepA{a} //@snippet("}", deepABField, "a.b") +- // Check the file state. +- checkDiffs(mark, changed, golden) -} - --func wantsContext(context.Context) {} -- --func _() { -- context.Background() //@item(ctxBackground, "context.Background", "func() context.Context", "func", "Background returns a non-nil, empty Context.") -- context.TODO() //@item(ctxTODO, "context.TODO", "func() context.Context", "func", "TODO returns a non-nil, empty Context.") -- -- wantsContext(c) //@rank(")", ctxBackground),rank(")", ctxTODO) --} +-func suggestedfixErrMarker(mark marker, loc protocol.Location, re *regexp.Regexp, wantErr stringMatcher) { +- loc.Range.End = loc.Range.Start // diagnostics ignore end position. +- // Find and remove the matching diagnostic. +- diag, ok := removeDiagnostic(mark, loc, re) +- if !ok { +- mark.errorf("no diagnostic at %v matches %q", loc, re) +- return +- } - --func _() { -- var cork struct{ err error } -- cork.err //@item(deepCorkErr, "cork.err", "error", "field") -- context //@item(deepContextPkg, "context", "\"context\"", "package") -- var _ error = co // rank(" //", deepCorkErr, deepContextPkg) +- // Apply the fix it suggests. +- _, err := codeAction(mark.run.env, loc.URI, diag.Range, "quickfix", &diag, nil) +- wantErr.checkErr(mark, err) -} - --func _() { -- // deepCircle is circular. -- type deepCircle struct { -- *deepCircle +-// codeAction executes a textDocument/codeAction request for the specified +-// location and kind. If diag is non-nil, it is used as the code action +-// context. +-// +-// The resulting map contains resulting file contents after the code action is +-// applied. Currently, this function does not support code actions that return +-// edits directly; it only supports code action commands. +-func codeAction(env *integration.Env, uri protocol.DocumentURI, rng protocol.Range, actionKind string, diag *protocol.Diagnostic, titles []string) (map[string][]byte, error) { +- changes, err := codeActionChanges(env, uri, rng, actionKind, diag, titles) +- if err != nil { +- return nil, err - } -- var circle deepCircle //@item(deepCircle, "circle", "deepCircle", "var") -- circle.deepCircle //@item(deepCircleField, "circle.deepCircle", "*deepCircle", "field") -- var _ deepCircle = circ //@complete(" //", deepCircle, deepCircleField),snippet(" //", deepCircleField, "*circle.deepCircle") +- fileChanges := make(map[string][]byte) +- if err := applyDocumentChanges(env, changes, fileChanges); err != nil { +- return nil, fmt.Errorf("applying document changes: %v", err) +- } +- return fileChanges, nil -} - --func _() { -- type deepEmbedC struct { -- } -- type deepEmbedB struct { -- deepEmbedC +-// codeActionChanges executes a textDocument/codeAction request for the +-// specified location and kind, and captures the resulting document changes. +-// If diag is non-nil, it is used as the code action context. +-// If titles is non-empty, the code action title must be present among the provided titles. +-func codeActionChanges(env *integration.Env, uri protocol.DocumentURI, rng protocol.Range, actionKind string, diag *protocol.Diagnostic, titles []string) ([]protocol.DocumentChanges, error) { +- // Request all code actions that apply to the diagnostic. +- // (The protocol supports filtering using Context.Only={actionKind} +- // but we can give a better error if we don't filter.) +- params := &protocol.CodeActionParams{ +- TextDocument: protocol.TextDocumentIdentifier{URI: uri}, +- Range: rng, +- Context: protocol.CodeActionContext{ +- Only: nil, // => all kinds +- }, - } -- type deepEmbedA struct { -- deepEmbedB +- if diag != nil { +- params.Context.Diagnostics = []protocol.Diagnostic{*diag} - } - -- wantsC := func(deepEmbedC) {} -- -- var a deepEmbedA //@item(deepEmbedA, "a", "deepEmbedA", "var") -- a.deepEmbedB //@item(deepEmbedB, "a.deepEmbedB", "deepEmbedB", "field") -- a.deepEmbedC //@item(deepEmbedC, "a.deepEmbedC", "deepEmbedC", "field") -- wantsC(a) //@complete(")", deepEmbedC, deepEmbedA, deepEmbedB) --} +- actions, err := env.Editor.Server.CodeAction(env.Ctx, params) +- if err != nil { +- return nil, err +- } - --func _() { -- type nested struct { -- a int -- n *nested //@item(deepNestedField, "n", "*nested", "field") +- // Find the sole candidates CodeAction of the specified kind (e.g. refactor.rewrite). +- var candidates []protocol.CodeAction +- for _, act := range actions { +- if act.Kind == protocol.CodeActionKind(actionKind) { +- if len(titles) > 0 { +- for _, f := range titles { +- if act.Title == f { +- candidates = append(candidates, act) +- break +- } +- } +- } else { +- candidates = append(candidates, act) +- } +- } +- } +- if len(candidates) != 1 { +- for _, act := range actions { +- env.T.Logf("found CodeAction Kind=%s Title=%q", act.Kind, act.Title) +- } +- return nil, fmt.Errorf("found %d CodeActions of kind %s matching filters %v for this diagnostic, want 1", len(candidates), actionKind, titles) - } +- action := candidates[0] - -- nested{ -- a: 123, //@complete(" //", deepNestedField) +- // Apply the codeAction. +- // +- // Spec: +- // "If a code action provides an edit and a command, first the edit is +- // executed and then the command." +- // An action may specify an edit and/or a command, to be +- // applied in that order. But since applyDocumentChanges(env, +- // action.Edit.DocumentChanges) doesn't compose, for now we +- // assert that actions return one or the other. +- +- // Resolve code action edits first if the client has resolve support +- // and the code action has no edits. +- if action.Edit == nil { +- editSupport, err := env.Editor.EditResolveSupport() +- if err != nil { +- return nil, err +- } +- if editSupport { +- resolved, err := env.Editor.Server.ResolveCodeAction(env.Ctx, &action) +- if err != nil { +- return nil, err +- } +- action.Edit = resolved.Edit +- } - } --} - --func _() { -- var a struct { -- b struct { -- c int +- if action.Edit != nil { +- if action.Edit.Changes != nil { +- env.T.Errorf("internal error: discarding unexpected CodeAction{Kind=%s, Title=%q}.Edit.Changes", action.Kind, action.Title) +- } +- if action.Edit.DocumentChanges != nil { +- if action.Command != nil { +- env.T.Errorf("internal error: discarding unexpected CodeAction{Kind=%s, Title=%q}.Command", action.Kind, action.Title) +- } +- return action.Edit.DocumentChanges, nil - } -- d int - } - -- a.d //@item(deepAD, "a.d", "int", "field") -- a.b.c //@item(deepABC, "a.b.c", "int", "field") -- a.b //@item(deepAB, "a.b", "struct{...}", "field") -- a //@item(deepA, "a", "struct{...}", "var") +- if action.Command != nil { +- // This is a typical CodeAction command: +- // +- // Title: "Implement error" +- // Command: gopls.apply_fix +- // Arguments: [{"Fix":"stub_methods","URI":".../a.go","Range":...}}] +- // +- // The client makes an ExecuteCommand RPC to the server, +- // which dispatches it to the ApplyFix handler. +- // ApplyFix dispatches to the "stub_methods" suggestedfix hook (the meat). +- // The server then makes an ApplyEdit RPC to the client, +- // whose Awaiter hook gathers the edits instead of applying them. - -- // "a.d" should be ranked above the deeper "a.b.c" -- var i int -- i = a //@complete(" //", deepAD, deepABC, deepA, deepAB) --} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/errors.txt b/gopls/internal/regtest/marker/testdata/completion/errors.txt ---- a/gopls/internal/regtest/marker/testdata/completion/errors.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/errors.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,33 +0,0 @@ --This test checks completion related to errors. +- _ = env.Awaiter.TakeDocumentChanges() // reset (assuming Env is confined to this thread) - ---- flags -- ---ignore_extra_diags +- if _, err := env.Editor.Server.ExecuteCommand(env.Ctx, &protocol.ExecuteCommandParams{ +- Command: action.Command.Command, +- Arguments: action.Command.Arguments, +- }); err != nil { +- return nil, err +- } +- return env.Awaiter.TakeDocumentChanges(), nil +- } - ---- settings.json -- --{ -- "deepCompletion": false +- return nil, nil -} - ---- go.mod -- --module golang.org/lsptests -- --go 1.18 -- ---- errors.go -- --package errors +-// refsMarker implements the @refs marker. +-func refsMarker(mark marker, src protocol.Location, want ...protocol.Location) { +- refs := func(includeDeclaration bool, want []protocol.Location) error { +- got, err := mark.server().References(mark.ctx(), &protocol.ReferenceParams{ +- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(src), +- Context: protocol.ReferenceContext{ +- IncludeDeclaration: includeDeclaration, +- }, +- }) +- if err != nil { +- return err +- } - --import ( -- "golang.org/lsptests/types" --) +- return compareLocations(mark, got, want) +- } - --func _() { -- bob.Bob() //@complete(".") -- types.b //@complete(" //", Bob_interface) +- for _, includeDeclaration := range []bool{false, true} { +- // Ignore first 'want' location if we didn't request the declaration. +- // TODO(adonovan): don't assume a single declaration: +- // there may be >1 if corresponding methods are considered. +- want := want +- if !includeDeclaration && len(want) > 0 { +- want = want[1:] +- } +- if err := refs(includeDeclaration, want); err != nil { +- mark.errorf("refs(includeDeclaration=%t) failed: %v", +- includeDeclaration, err) +- } +- } -} - ---- types/types.go -- --package types -- --type Bob interface { //@item(Bob_interface, "Bob", "interface{...}", "interface") -- Bobby() +-// implementationMarker implements the @implementation marker. +-func implementationMarker(mark marker, src protocol.Location, want ...protocol.Location) { +- got, err := mark.server().Implementation(mark.ctx(), &protocol.ImplementationParams{ +- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(src), +- }) +- if err != nil { +- mark.errorf("implementation at %s failed: %v", src, err) +- return +- } +- if err := compareLocations(mark, got, want); err != nil { +- mark.errorf("implementation: %v", err) +- } -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/field_list.txt b/gopls/internal/regtest/marker/testdata/completion/field_list.txt ---- a/gopls/internal/regtest/marker/testdata/completion/field_list.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/field_list.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,38 +0,0 @@ --This test checks completion related to field lists. -- ---- flags -- ---ignore_extra_diags - ---- settings.json -- --{ -- "completeUnimported": false +-func itemLocation(item protocol.CallHierarchyItem) protocol.Location { +- return protocol.Location{ +- URI: item.URI, +- Range: item.Range, +- } -} - ---- field_list.go -- --package fieldlist -- --var myInt int //@item(flVar, "myInt", "int", "var") --type myType int //@item(flType, "myType", "int", "type") -- --func (my) _() {} //@complete(") _", flType) --func (my my) _() {} //@complete(" my)"),complete(") _", flType) -- --func (myType) _() {} //@complete(") {", flType) -- --func (myType) _(my my) {} //@complete(" my)"),complete(") {", flType) -- --func (myType) _() my {} //@complete(" {", flType) -- --func (myType) _() (my my) {} //@complete(" my"),complete(") {", flType) -- --func _() { -- var _ struct { -- //@complete("", flType) -- m my //@complete(" my"),complete(" //", flType) +-func incomingCallsMarker(mark marker, src protocol.Location, want ...protocol.Location) { +- getCalls := func(item protocol.CallHierarchyItem) ([]protocol.Location, error) { +- calls, err := mark.server().IncomingCalls(mark.ctx(), &protocol.CallHierarchyIncomingCallsParams{Item: item}) +- if err != nil { +- return nil, err +- } +- var locs []protocol.Location +- for _, call := range calls { +- locs = append(locs, itemLocation(call.From)) +- } +- return locs, nil - } +- callHierarchy(mark, src, getCalls, want) +-} - -- var _ interface { -- //@complete("", flType) -- m() my //@complete("("),complete(" //", flType) +-func outgoingCallsMarker(mark marker, src protocol.Location, want ...protocol.Location) { +- getCalls := func(item protocol.CallHierarchyItem) ([]protocol.Location, error) { +- calls, err := mark.server().OutgoingCalls(mark.ctx(), &protocol.CallHierarchyOutgoingCallsParams{Item: item}) +- if err != nil { +- return nil, err +- } +- var locs []protocol.Location +- for _, call := range calls { +- locs = append(locs, itemLocation(call.To)) +- } +- return locs, nil - } +- callHierarchy(mark, src, getCalls, want) -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/foobarbaz.txt b/gopls/internal/regtest/marker/testdata/completion/foobarbaz.txt ---- a/gopls/internal/regtest/marker/testdata/completion/foobarbaz.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/foobarbaz.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,541 +0,0 @@ --This test ports some arbitrary tests from the old marker framework, that were --*mostly* about completion. - ---- flags -- ---ignore_extra_diags ---min_go=go1.20 +-type callHierarchyFunc = func(protocol.CallHierarchyItem) ([]protocol.Location, error) - ---- settings.json -- --{ -- "completeUnimported": false, -- "deepCompletion": false, -- "experimentalPostfixCompletions": false +-func callHierarchy(mark marker, src protocol.Location, getCalls callHierarchyFunc, want []protocol.Location) { +- items, err := mark.server().PrepareCallHierarchy(mark.ctx(), &protocol.CallHierarchyPrepareParams{ +- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(src), +- }) +- if err != nil { +- mark.errorf("PrepareCallHierarchy failed: %v", err) +- return +- } +- if nitems := len(items); nitems != 1 { +- mark.errorf("PrepareCallHierarchy returned %d items, want exactly 1", nitems) +- return +- } +- if loc := itemLocation(items[0]); loc != src { +- mark.errorf("PrepareCallHierarchy found call %v, want %v", loc, src) +- return +- } +- calls, err := getCalls(items[0]) +- if err != nil { +- mark.errorf("call hierarchy failed: %v", err) +- return +- } +- if calls == nil { +- calls = []protocol.Location{} +- } +- // TODO(rfindley): why aren't call hierarchy results stable? +- sortLocs := func(locs []protocol.Location) { +- sort.Slice(locs, func(i, j int) bool { +- return protocol.CompareLocation(locs[i], locs[j]) < 0 +- }) +- } +- sortLocs(want) +- sortLocs(calls) +- if d := cmp.Diff(want, calls); d != "" { +- mark.errorf("call hierarchy: unexpected results (-want +got):\n%s", d) +- } -} - ---- go.mod -- --module foobar.test -- --go 1.18 -- ---- foo/foo.go -- --package foo //@loc(PackageFoo, "foo"),item(PackageFooItem, "foo", "\"foobar.test/foo\"", "package") +-func inlayhintsMarker(mark marker, g *Golden) { +- hints := mark.run.env.InlayHints(mark.path()) - --type StructFoo struct { //@loc(StructFooLoc, "StructFoo"), item(StructFoo, "StructFoo", "struct{...}", "struct") -- Value int //@item(Value, "Value", "int", "field") --} +- // Map inlay hints to text edits. +- edits := make([]protocol.TextEdit, len(hints)) +- for i, hint := range hints { +- var paddingLeft, paddingRight string +- if hint.PaddingLeft { +- paddingLeft = " " +- } +- if hint.PaddingRight { +- paddingRight = " " +- } +- edits[i] = protocol.TextEdit{ +- Range: protocol.Range{Start: hint.Position, End: hint.Position}, +- NewText: fmt.Sprintf("<%s%s%s>", paddingLeft, hint.Label[0].Value, paddingRight), +- } +- } - --// Pre-set this marker, as we don't have a "source" for it in this package. --/* Error() */ //@item(Error, "Error", "func() string", "method") +- m := mark.mapper() +- got, _, err := protocol.ApplyEdits(m, edits) +- if err != nil { +- mark.errorf("ApplyProtocolEdits: %v", err) +- return +- } - --func Foo() { //@item(Foo, "Foo", "func()", "func") -- var err error -- err.Error() //@complete("E", Error) +- compareGolden(mark, got, g) -} - --func _() { -- var sFoo StructFoo //@complete("t", StructFoo) -- if x := sFoo; x.Value == 1 { //@complete("V", Value), typedef("sFoo", StructFooLoc) +-func prepareRenameMarker(mark marker, src, spn protocol.Location, placeholder string) { +- params := &protocol.PrepareRenameParams{ +- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(src), +- } +- got, err := mark.server().PrepareRename(mark.ctx(), params) +- if err != nil { +- mark.T().Fatal(err) +- } +- if placeholder == "" { +- if got != nil { +- mark.errorf("PrepareRename(...) = %v, want nil", got) +- } - return - } +- want := &protocol.PrepareRenameResult{Range: spn.Range, Placeholder: placeholder} +- if diff := cmp.Diff(want, got); diff != "" { +- mark.errorf("mismatching PrepareRename result:\n%s", diff) +- } -} - --func _() { -- shadowed := 123 -- { -- shadowed := "hi" //@item(shadowed, "shadowed", "string", "var") -- sha //@complete("a", shadowed), diag("sha", re"(undefined|undeclared)") -- _ = shadowed +-// symbolMarker implements the @symbol marker. +-func symbolMarker(mark marker, golden *Golden) { +- // Retrieve information about all symbols in this file. +- symbols, err := mark.server().DocumentSymbol(mark.ctx(), &protocol.DocumentSymbolParams{ +- TextDocument: protocol.TextDocumentIdentifier{URI: mark.uri()}, +- }) +- if err != nil { +- mark.errorf("DocumentSymbol request failed: %v", err) +- return - } --} - --type IntFoo int //@loc(IntFooLoc, "IntFoo"), item(IntFoo, "IntFoo", "int", "type") +- // Format symbols one per line, sorted (in effect) by first column, a dotted name. +- var lines []string +- for _, symbol := range symbols { +- // Each result element is a union of (legacy) +- // SymbolInformation and (new) DocumentSymbol, +- // so we ascertain which one and then transcode. +- data, err := json.Marshal(symbol) +- if err != nil { +- mark.T().Fatal(err) +- } +- if _, ok := symbol.(map[string]any)["location"]; ok { +- // This case is not reached because Editor initialization +- // enables HierarchicalDocumentSymbolSupport. +- // TODO(adonovan): test this too. +- var sym protocol.SymbolInformation +- if err := json.Unmarshal(data, &sym); err != nil { +- mark.T().Fatal(err) +- } +- mark.errorf("fake Editor doesn't support SymbolInformation") - ---- bar/bar.go -- --package bar +- } else { +- var sym protocol.DocumentSymbol // new hierarchical hotness +- if err := json.Unmarshal(data, &sym); err != nil { +- mark.T().Fatal(err) +- } - --import ( -- "foobar.test/foo" //@item(foo, "foo", "\"foobar.test/foo\"", "package") --) +- // Print each symbol in the response tree. +- var visit func(sym protocol.DocumentSymbol, prefix []string) +- visit = func(sym protocol.DocumentSymbol, prefix []string) { +- var out strings.Builder +- out.WriteString(strings.Join(prefix, ".")) +- fmt.Fprintf(&out, " %q", sym.Detail) +- if delta := sym.Range.End.Line - sym.Range.Start.Line; delta > 0 { +- fmt.Fprintf(&out, " +%d lines", delta) +- } +- lines = append(lines, out.String()) - --func helper(i foo.IntFoo) {} //@item(helper, "helper", "func(i foo.IntFoo)", "func") +- for _, child := range sym.Children { +- visit(child, append(prefix, child.Name)) +- } +- } +- visit(sym, []string{sym.Name}) +- } +- } +- sort.Strings(lines) +- lines = append(lines, "") // match trailing newline in .txtar file +- got := []byte(strings.Join(lines, "\n")) - --func _() { -- help //@complete("l", helper) -- _ = foo.StructFoo{} //@complete("S", IntFoo, StructFoo) +- // Compare with golden. +- want, ok := golden.Get(mark.T(), "", got) +- if !ok { +- mark.errorf("%s: missing golden file @%s", mark.note.Name, golden.id) +- } else if diff := cmp.Diff(string(got), string(want)); diff != "" { +- mark.errorf("%s: unexpected output: got:\n%s\nwant:\n%s\ndiff:\n%s", +- mark.note.Name, got, want, diff) +- } -} - --// Bar is a function. --func Bar() { //@item(Bar, "Bar", "func()", "func", "Bar is a function.") -- foo.Foo() //@complete("F", Foo, IntFoo, StructFoo) -- var _ foo.IntFoo //@complete("I", IntFoo, StructFoo) -- foo.() //@complete("(", Foo, IntFoo, StructFoo), diag(")", re"expected type") +-// compareLocations returns an error message if got and want are not +-// the same set of locations. The marker is used only for fmtLoc. +-func compareLocations(mark marker, got, want []protocol.Location) error { +- toStrings := func(locs []protocol.Location) []string { +- strs := make([]string, len(locs)) +- for i, loc := range locs { +- strs[i] = mark.run.fmtLoc(loc) +- } +- sort.Strings(strs) +- return strs +- } +- if diff := cmp.Diff(toStrings(want), toStrings(got)); diff != "" { +- return fmt.Errorf("incorrect result locations: (got %d, want %d):\n%s", +- len(got), len(want), diff) +- } +- return nil -} - --// These items weren't present in the old marker tests (due to settings), but --// we may as well include them. --//@item(intConversion, "int()"), item(fooFoo, "foo.Foo") --//@item(fooIntFoo, "foo.IntFoo"), item(fooStructFoo, "foo.StructFoo") -- --func _() { -- var Valentine int //@item(Valentine, "Valentine", "int", "var") -- -- _ = foo.StructFoo{ //@diag("foo", re"unkeyed fields") -- Valu //@complete(" //", Value) +-func workspaceSymbolMarker(mark marker, query string, golden *Golden) { +- params := &protocol.WorkspaceSymbolParams{ +- Query: query, - } -- _ = foo.StructFoo{ //@diag("foo", re"unkeyed fields") -- Va //@complete("a", Value, Valentine) - +- gotSymbols, err := mark.server().Symbol(mark.ctx(), params) +- if err != nil { +- mark.errorf("Symbol(%q) failed: %v", query, err) +- return - } -- _ = foo.StructFoo{ -- Value: 5, //@complete("a", Value) -- } -- _ = foo.StructFoo{ -- //@complete("//", Value, Valentine, intConversion, foo, helper, Bar) +- var got bytes.Buffer +- for _, s := range gotSymbols { +- // Omit the txtar position of the symbol location; otherwise edits to the +- // txtar archive lead to unexpected failures. +- loc := mark.run.fmtLocDetails(s.Location, false) +- // TODO(rfindley): can we do better here, by detecting if the location is +- // relative to GOROOT? +- if loc == "" { +- loc = "<unknown>" +- } +- fmt.Fprintf(&got, "%s %s %s\n", loc, s.Name, s.Kind) - } -- _ = foo.StructFoo{ -- Value: Valen //@complete("le", Valentine) +- +- compareGolden(mark, got.Bytes(), golden) +-} +- +-// compareGolden compares the content of got with that of g.Get(""), reporting +-// errors on any mismatch. +-// +-// TODO(rfindley): use this helper in more places. +-func compareGolden(mark marker, got []byte, g *Golden) { +- want, ok := g.Get(mark.T(), "", got) +- if !ok { +- mark.errorf("missing golden file @%s", g.id) +- return - } -- _ = foo.StructFoo{ -- Value: //@complete(" //", Valentine, intConversion, foo, helper, Bar) +- // Normalize newline termination: archive files (i.e. Golden content) can't +- // contain non-newline terminated files, except in the special case where the +- // file is completely empty. +- // +- // Note that txtar partitions a contiguous byte slice, so we must copy before +- // appending. +- normalize := func(s []byte) []byte { +- if n := len(s); n > 0 && s[n-1] != '\n' { +- s = append(s[:n:n], '\n') // don't mutate array +- } +- return s - } -- _ = foo.StructFoo{ -- Value: //@complete(" ", Valentine, intConversion, foo, helper, Bar) +- got = normalize(got) +- want = normalize(want) +- if diff := compare.Bytes(want, got); diff != "" { +- mark.errorf("%s does not match @%s:\n%s", mark.note.Name, g.id, diff) - } -} +diff -urN a/gopls/internal/test/marker/testdata/callhierarchy/callhierarchy.txt b/gopls/internal/test/marker/testdata/callhierarchy/callhierarchy.txt +--- a/gopls/internal/test/marker/testdata/callhierarchy/callhierarchy.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/callhierarchy/callhierarchy.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,94 +0,0 @@ +-This test checks call hierarchy queries. +- +--ignore_extra_diags due to the initialization cycle. +- +--- flags -- +--ignore_extra_diags +- +--- go.mod -- +-module golang.org/lsptests/callhierarchy - ---- baz/baz.go -- --package baz +--- incoming/incoming.go -- +-package incoming - --import ( -- "foobar.test/bar" +-import "golang.org/lsptests/callhierarchy" - -- f "foobar.test/foo" --) +-// A is exported to test incoming calls across packages +-func A() { //@loc(incomingA, "A") +- callhierarchy.D() +-} - --var FooStruct f.StructFoo +--- outgoing/outgoing.go -- +-package outgoing - --func Baz() { -- defer bar.Bar() //@complete("B", Bar) -- // TODO: Test completion here. -- defer bar.B //@diag(re"bar.B()", re"must be function call") -- var x f.IntFoo //@complete("n", IntFoo), typedef("x", IntFooLoc) -- bar.Bar() //@complete("B", Bar) +-// B is exported to test outgoing calls across packages +-func B() { //@loc(outgoingB, "B") -} - --func _() { -- bob := f.StructFoo{Value: 5} -- if x := bob. //@complete(" //", Value) -- switch true == false { -- case true: -- if x := bob. //@complete(" //", Value) -- case false: -- } -- if x := bob.Va //@complete("a", Value) -- switch true == true { -- default: -- } +--- hierarchy.go -- +-package callhierarchy +- +-import "golang.org/lsptests/callhierarchy/outgoing" +- +-func a() { //@loc(hierarchyA, "a") +- D() -} - ---- arraytype/arraytype.go -- --package arraytype +-func b() { //@loc(hierarchyB, "b") +- D() +-} - --import ( -- "foobar.test/foo" --) +-// C is an exported function +-func C() { //@loc(hierarchyC, "C") +- D() +- D() +-} - --func _() { -- var ( -- val string //@item(atVal, "val", "string", "var") -- ) +-// To test hierarchy across function literals +-var x = func() { //@loc(hierarchyLiteral, "func"),loc(hierarchyLiteralOut, "x") +- D() +-} - -- [] //@complete(" //", atVal, PackageFooItem) +-// D is exported to test incoming/outgoing calls across packages +-func D() { //@loc(hierarchyD, "D"),incomingcalls(hierarchyD, hierarchyA, hierarchyB, hierarchyC, hierarchyLiteral, incomingA),outgoingcalls(hierarchyD, hierarchyE, hierarchyF, hierarchyG, hierarchyLiteralOut, outgoingB, hierarchyFoo, hierarchyH, hierarchyI, hierarchyJ, hierarchyK) +- e() +- x() +- F() +- outgoing.B() +- foo := func() {} //@loc(hierarchyFoo, "foo"),incomingcalls(hierarchyFoo, hierarchyD),outgoingcalls(hierarchyFoo) +- foo() - -- []val //@complete(" //") +- func() { +- g() +- }() - -- []foo.StructFoo //@complete(" //", StructFoo) +- var i Interface = impl{} +- i.H() +- i.I() - -- []foo.StructFoo(nil) //@complete("(", StructFoo) +- s := Struct{} +- s.J() +- s.K() +-} - -- []*foo.StructFoo //@complete(" //", StructFoo) +-func e() {} //@loc(hierarchyE, "e") - -- [...]foo.StructFoo //@complete(" //", StructFoo) +-// F is an exported function +-func F() {} //@loc(hierarchyF, "F") - -- [2][][4]foo.StructFoo //@complete(" //", StructFoo) +-func g() {} //@loc(hierarchyG, "g") - -- []struct { f []foo.StructFoo } //@complete(" }", StructFoo) +-type Interface interface { +- H() //@loc(hierarchyH, "H") +- I() //@loc(hierarchyI, "I") -} - --func _() { -- type myInt int //@item(atMyInt, "myInt", "int", "type") +-type impl struct{} - -- var mark []myInt //@item(atMark, "mark", "[]myInt", "var") +-func (i impl) H() {} +-func (i impl) I() {} - -- var s []myInt //@item(atS, "s", "[]myInt", "var") -- s = []m //@complete(" //", atMyInt) +-type Struct struct { +- J func() //@loc(hierarchyJ, "J") +- K func() //@loc(hierarchyK, "K") +-} +diff -urN a/gopls/internal/test/marker/testdata/codeaction/change_quote.txt b/gopls/internal/test/marker/testdata/codeaction/change_quote.txt +--- a/gopls/internal/test/marker/testdata/codeaction/change_quote.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codeaction/change_quote.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,69 +0,0 @@ +-This test checks the behavior of the 'change quote' code action. - -- var a [1]myInt -- a = [1]m //@complete(" //", atMyInt) +--- flags -- +--ignore_extra_diags - -- var ds [][]myInt -- ds = [][]m //@complete(" //", atMyInt) --} +--- go.mod -- +-module golang.org/lsptests/changequote - --func _() { -- var b [0]byte //@item(atByte, "b", "[0]byte", "var") -- var _ []byte = b //@snippet(" //", atByte, "b[:]") --} +-go 1.18 - ---- badstmt/badstmt.go -- --package badstmt +--- a.go -- +-package changequote - -import ( -- "foobar.test/foo" +- "fmt" -) - --// (The syntax error causes suppression of diagnostics for type errors. --// See issue #59888.) -- --func _(x int) { -- defer foo.F //@complete(" //", Foo, IntFoo, StructFoo) -- defer foo.F //@complete(" //", Foo, IntFoo, StructFoo) +-func foo() { +- var s string +- s = "hello" //@codeactionedit(`"`, "refactor.rewrite", a1, "Convert to raw string literal") +- s = `hello` //@codeactionedit("`", "refactor.rewrite", a2, "Convert to interpreted string literal") +- s = "hello\tworld" //@codeactionedit(`"`, "refactor.rewrite", a3, "Convert to raw string literal") +- s = `hello world` //@codeactionedit("`", "refactor.rewrite", a4, "Convert to interpreted string literal") +- s = "hello\nworld" //@codeactionedit(`"`, "refactor.rewrite", a5, "Convert to raw string literal") +- // add a comment to avoid affect diff compute +- s = `hello +-world` //@codeactionedit("`", "refactor.rewrite", a6, "Convert to interpreted string literal") +- s = "hello\"world" //@codeactionedit(`"`, "refactor.rewrite", a7, "Convert to raw string literal") +- s = `hello"world` //@codeactionedit("`", "refactor.rewrite", a8, "Convert to interpreted string literal") +- s = "hello\x1bworld" //@codeactionerr(`"`, "", "refactor.rewrite", re"found 0 CodeActions") +- s = "hello`world" //@codeactionerr(`"`, "", "refactor.rewrite", re"found 0 CodeActions") +- s = "hello\x7fworld" //@codeactionerr(`"`, "", "refactor.rewrite", re"found 0 CodeActions") +- fmt.Println(s) -} - --func _() { -- switch true { -- case true: -- go foo.F //@complete(" //", Foo, IntFoo, StructFoo) -- } --} +--- @a1/a.go -- +-@@ -9 +9 @@ +-- s = "hello" //@codeactionedit(`"`, "refactor.rewrite", a1, "Convert to raw string literal") +-+ s = `hello` //@codeactionedit(`"`, "refactor.rewrite", a1, "Convert to raw string literal") +--- @a2/a.go -- +-@@ -10 +10 @@ +-- s = `hello` //@codeactionedit("`", "refactor.rewrite", a2, "Convert to interpreted string literal") +-+ s = "hello" //@codeactionedit("`", "refactor.rewrite", a2, "Convert to interpreted string literal") +--- @a3/a.go -- +-@@ -11 +11 @@ +-- s = "hello\tworld" //@codeactionedit(`"`, "refactor.rewrite", a3, "Convert to raw string literal") +-+ s = `hello world` //@codeactionedit(`"`, "refactor.rewrite", a3, "Convert to raw string literal") +--- @a4/a.go -- +-@@ -12 +12 @@ +-- s = `hello world` //@codeactionedit("`", "refactor.rewrite", a4, "Convert to interpreted string literal") +-+ s = "hello\tworld" //@codeactionedit("`", "refactor.rewrite", a4, "Convert to interpreted string literal") +--- @a5/a.go -- +-@@ -13 +13,2 @@ +-- s = "hello\nworld" //@codeactionedit(`"`, "refactor.rewrite", a5, "Convert to raw string literal") +-+ s = `hello +-+world` //@codeactionedit(`"`, "refactor.rewrite", a5, "Convert to raw string literal") +--- @a6/a.go -- +-@@ -15,2 +15 @@ +-- s = `hello +--world` //@codeactionedit("`", "refactor.rewrite", a6, "Convert to interpreted string literal") +-+ s = "hello\nworld" //@codeactionedit("`", "refactor.rewrite", a6, "Convert to interpreted string literal") +--- @a7/a.go -- +-@@ -17 +17 @@ +-- s = "hello\"world" //@codeactionedit(`"`, "refactor.rewrite", a7, "Convert to raw string literal") +-+ s = `hello"world` //@codeactionedit(`"`, "refactor.rewrite", a7, "Convert to raw string literal") +--- @a8/a.go -- +-@@ -18 +18 @@ +-- s = `hello"world` //@codeactionedit("`", "refactor.rewrite", a8, "Convert to interpreted string literal") +-+ s = "hello\"world" //@codeactionedit("`", "refactor.rewrite", a8, "Convert to interpreted string literal") +diff -urN a/gopls/internal/test/marker/testdata/codeaction/extract_method.txt b/gopls/internal/test/marker/testdata/codeaction/extract_method.txt +--- a/gopls/internal/test/marker/testdata/codeaction/extract_method.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codeaction/extract_method.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,218 +0,0 @@ +-This test exercises function and method extraction. - --func _() { -- defer func() { -- foo.F //@complete(" //", Foo, IntFoo, StructFoo), snippet(" //", Foo, "Foo()") +--- flags -- +--ignore_extra_diags - -- foo. //@rank(" //", Foo) -- } --} +--- basic.go -- +-package extract - ---- badstmt/badstmt_2.go -- --package badstmt +-//@codeactionedit(A_XLessThanYP, "refactor.extract", meth1, "Extract method") +-//@codeactionedit(A_XLessThanYP, "refactor.extract", func1, "Extract function") +-//@codeactionedit(A_AddP1, "refactor.extract", meth2, "Extract method") +-//@codeactionedit(A_AddP1, "refactor.extract", func2, "Extract function") +-//@codeactionedit(A_AddP2, "refactor.extract", meth3, "Extract method") +-//@codeactionedit(A_AddP2, "refactor.extract", func3, "Extract function") +-//@codeactionedit(A_XLessThanY, "refactor.extract", meth4, "Extract method") +-//@codeactionedit(A_XLessThanY, "refactor.extract", func4, "Extract function") +-//@codeactionedit(A_Add1, "refactor.extract", meth5, "Extract method") +-//@codeactionedit(A_Add1, "refactor.extract", func5, "Extract function") +-//@codeactionedit(A_Add2, "refactor.extract", meth6, "Extract method") +-//@codeactionedit(A_Add2, "refactor.extract", func6, "Extract function") - --import ( -- "foobar.test/foo" --) +-type A struct { +- x int +- y int +-} - --func _() { -- defer func() { foo. } //@rank(" }", Foo) +-func (a *A) XLessThanYP() bool { +- return a.x < a.y //@loc(A_XLessThanYP, re`return.*a\.y`) -} - ---- badstmt/badstmt_3.go -- --package badstmt +-func (a *A) AddP() int { +- sum := a.x + a.y //@loc(A_AddP1, re`sum.*a\.y`) +- return sum //@loc(A_AddP2, re`return.*sum`) +-} - --import ( -- "foobar.test/foo" --) +-func (a A) XLessThanY() bool { +- return a.x < a.y //@loc(A_XLessThanY, re`return.*a\.y`) +-} - --func _() { -- go foo. //@rank(" //", Foo, IntFoo), snippet(" //", Foo, "Foo()") +-func (a A) Add() int { +- sum := a.x + a.y //@loc(A_Add1, re`sum.*a\.y`) +- return sum //@loc(A_Add2, re`return.*sum`) -} - ---- badstmt/badstmt_4.go -- --package badstmt +--- @func1/basic.go -- +-@@ -22 +22 @@ +-- return a.x < a.y //@loc(A_XLessThanYP, re`return.*a\.y`) +-+ return newFunction(a) //@loc(A_XLessThanYP, re`return.*a\.y`) +-@@ -25 +25,4 @@ +-+func newFunction(a *A) bool { +-+ return a.x < a.y +-+} +-+ +--- @func2/basic.go -- +-@@ -26 +26 @@ +-- sum := a.x + a.y //@loc(A_AddP1, re`sum.*a\.y`) +-+ sum := newFunction(a) //@loc(A_AddP1, re`sum.*a\.y`) +-@@ -30 +30,5 @@ +-+func newFunction(a *A) int { +-+ sum := a.x + a.y +-+ return sum +-+} +-+ +--- @func3/basic.go -- +-@@ -27 +27 @@ +-- return sum //@loc(A_AddP2, re`return.*sum`) +-+ return newFunction(sum) //@loc(A_AddP2, re`return.*sum`) +-@@ -30 +30,4 @@ +-+func newFunction(sum int) int { +-+ return sum +-+} +-+ +--- @func4/basic.go -- +-@@ -31 +31 @@ +-- return a.x < a.y //@loc(A_XLessThanY, re`return.*a\.y`) +-+ return newFunction(a) //@loc(A_XLessThanY, re`return.*a\.y`) +-@@ -34 +34,4 @@ +-+func newFunction(a A) bool { +-+ return a.x < a.y +-+} +-+ +--- @func5/basic.go -- +-@@ -35 +35 @@ +-- sum := a.x + a.y //@loc(A_Add1, re`sum.*a\.y`) +-+ sum := newFunction(a) //@loc(A_Add1, re`sum.*a\.y`) +-@@ -39 +39,5 @@ +-+func newFunction(a A) int { +-+ sum := a.x + a.y +-+ return sum +-+} +-+ +--- @func6/basic.go -- +-@@ -36 +36 @@ +-- return sum //@loc(A_Add2, re`return.*sum`) +-+ return newFunction(sum) //@loc(A_Add2, re`return.*sum`) +-@@ -39 +39,4 @@ +-+func newFunction(sum int) int { +-+ return sum +-+} +-+ +--- @meth1/basic.go -- +-@@ -22 +22 @@ +-- return a.x < a.y //@loc(A_XLessThanYP, re`return.*a\.y`) +-+ return a.newMethod() //@loc(A_XLessThanYP, re`return.*a\.y`) +-@@ -25 +25,4 @@ +-+func (a *A) newMethod() bool { +-+ return a.x < a.y +-+} +-+ +--- @meth2/basic.go -- +-@@ -26 +26 @@ +-- sum := a.x + a.y //@loc(A_AddP1, re`sum.*a\.y`) +-+ sum := a.newMethod() //@loc(A_AddP1, re`sum.*a\.y`) +-@@ -30 +30,5 @@ +-+func (a *A) newMethod() int { +-+ sum := a.x + a.y +-+ return sum +-+} +-+ +--- @meth3/basic.go -- +-@@ -27 +27 @@ +-- return sum //@loc(A_AddP2, re`return.*sum`) +-+ return a.newMethod(sum) //@loc(A_AddP2, re`return.*sum`) +-@@ -30 +30,4 @@ +-+func (*A) newMethod(sum int) int { +-+ return sum +-+} +-+ +--- @meth4/basic.go -- +-@@ -31 +31 @@ +-- return a.x < a.y //@loc(A_XLessThanY, re`return.*a\.y`) +-+ return a.newMethod() //@loc(A_XLessThanY, re`return.*a\.y`) +-@@ -34 +34,4 @@ +-+func (a A) newMethod() bool { +-+ return a.x < a.y +-+} +-+ +--- @meth5/basic.go -- +-@@ -35 +35 @@ +-- sum := a.x + a.y //@loc(A_Add1, re`sum.*a\.y`) +-+ sum := a.newMethod() //@loc(A_Add1, re`sum.*a\.y`) +-@@ -39 +39,5 @@ +-+func (a A) newMethod() int { +-+ sum := a.x + a.y +-+ return sum +-+} +-+ +--- @meth6/basic.go -- +-@@ -36 +36 @@ +-- return sum //@loc(A_Add2, re`return.*sum`) +-+ return a.newMethod(sum) //@loc(A_Add2, re`return.*sum`) +-@@ -39 +39,4 @@ +-+func (A) newMethod(sum int) int { +-+ return sum +-+} +-+ +--- context.go -- +-package extract - --import ( -- "foobar.test/foo" --) +-import "context" - --func _() { -- go func() { -- defer foo. //@rank(" //", Foo, IntFoo) -- } +-//@codeactionedit(B_AddP, "refactor.extract", contextMeth1, "Extract method") +-//@codeactionedit(B_AddP, "refactor.extract", contextFunc1, "Extract function") +-//@codeactionedit(B_LongList, "refactor.extract", contextMeth2, "Extract method") +-//@codeactionedit(B_LongList, "refactor.extract", contextFunc2, "Extract function") +- +-type B struct { +- x int +- y int -} - ---- selector/selector.go -- --package selector +-func (b *B) AddP(ctx context.Context) (int, error) { +- sum := b.x + b.y +- return sum, ctx.Err() //@loc(B_AddP, re`return.*ctx\.Err\(\)`) +-} - --import ( -- "foobar.test/bar" --) +-func (b *B) LongList(ctx context.Context) (int, error) { +- p1 := 1 +- p2 := 1 +- p3 := 1 +- return p1 + p2 + p3, ctx.Err() //@loc(B_LongList, re`return.*ctx\.Err\(\)`) +-} +--- @contextMeth1/context.go -- +-@@ -17 +17 @@ +-- return sum, ctx.Err() //@loc(B_AddP, re`return.*ctx\.Err\(\)`) +-+ return b.newMethod(ctx, sum) //@loc(B_AddP, re`return.*ctx\.Err\(\)`) +-@@ -20 +20,4 @@ +-+func (*B) newMethod(ctx context.Context, sum int) (int, error) { +-+ return sum, ctx.Err() +-+} +-+ +--- @contextMeth2/context.go -- +-@@ -24 +24 @@ +-- return p1 + p2 + p3, ctx.Err() //@loc(B_LongList, re`return.*ctx\.Err\(\)`) +-+ return b.newMethod(ctx, p1, p2, p3) //@loc(B_LongList, re`return.*ctx\.Err\(\)`) +-@@ -26 +26,4 @@ +-+ +-+func (*B) newMethod(ctx context.Context, p1 int, p2 int, p3 int) (int, error) { +-+ return p1 + p2 + p3, ctx.Err() +-+} +--- @contextFunc2/context.go -- +-@@ -24 +24 @@ +-- return p1 + p2 + p3, ctx.Err() //@loc(B_LongList, re`return.*ctx\.Err\(\)`) +-+ return newFunction(ctx, p1, p2, p3) //@loc(B_LongList, re`return.*ctx\.Err\(\)`) +-@@ -26 +26,4 @@ +-+ +-+func newFunction(ctx context.Context, p1 int, p2 int, p3 int) (int, error) { +-+ return p1 + p2 + p3, ctx.Err() +-+} +--- @contextFunc1/context.go -- +-@@ -17 +17 @@ +-- return sum, ctx.Err() //@loc(B_AddP, re`return.*ctx\.Err\(\)`) +-+ return newFunction(ctx, sum) //@loc(B_AddP, re`return.*ctx\.Err\(\)`) +-@@ -20 +20,4 @@ +-+func newFunction(ctx context.Context, sum int) (int, error) { +-+ return sum, ctx.Err() +-+} +-+ +diff -urN a/gopls/internal/test/marker/testdata/codeaction/extract_variable_resolve.txt b/gopls/internal/test/marker/testdata/codeaction/extract_variable_resolve.txt +--- a/gopls/internal/test/marker/testdata/codeaction/extract_variable_resolve.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codeaction/extract_variable_resolve.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,81 +0,0 @@ +-This test checks the behavior of the 'extract variable' code action, with resolve support. +-See extract_variable.txt for the same test without resolve support. - --type S struct { -- B, A, C int //@item(Bf, "B", "int", "field"),item(Af, "A", "int", "field"),item(Cf, "C", "int", "field") +--- capabilities.json -- +-{ +- "textDocument": { +- "codeAction": { +- "dataSupport": true, +- "resolveSupport": { +- "properties": ["edit"] +- } +- } +- } -} +--- flags -- +--ignore_extra_diags +- +--- basic_lit.go -- +-package extract - -func _() { -- _ = S{}.; //@complete(";", Af, Bf, Cf) +- var _ = 1 + 2 //@codeactionedit("1", "refactor.extract", basic_lit1) +- var _ = 3 + 4 //@codeactionedit("3 + 4", "refactor.extract", basic_lit2) -} - --type bob struct { a int } //@item(a, "a", "int", "field") --type george struct { b int } --type jack struct { c int } //@item(c, "c", "int", "field") --type jill struct { d int } +--- @basic_lit1/basic_lit.go -- +-@@ -4 +4,2 @@ +-- var _ = 1 + 2 //@codeactionedit("1", "refactor.extract", basic_lit1) +-+ x := 1 +-+ var _ = x + 2 //@codeactionedit("1", "refactor.extract", basic_lit1) +--- @basic_lit2/basic_lit.go -- +-@@ -5 +5,2 @@ +-- var _ = 3 + 4 //@codeactionedit("3 + 4", "refactor.extract", basic_lit2) +-+ x := 3 + 4 +-+ var _ = x //@codeactionedit("3 + 4", "refactor.extract", basic_lit2) +--- func_call.go -- +-package extract - --func (b *bob) george() *george {} //@item(george, "george", "func() *george", "method") --func (g *george) jack() *jack {} --func (j *jack) jill() *jill {} //@item(jill, "jill", "func() *jill", "method") +-import "strconv" - -func _() { -- b := &bob{} -- y := b.george(). -- jack(); -- y.; //@complete(";", c, jill) +- x0 := append([]int{}, 1) //@codeactionedit("append([]int{}, 1)", "refactor.extract", func_call1) +- str := "1" +- b, err := strconv.Atoi(str) //@codeactionedit("strconv.Atoi(str)", "refactor.extract", func_call2) -} - --func _() { -- bar. //@complete(" /", Bar) -- x := 5 +--- @func_call1/func_call.go -- +-@@ -6 +6,2 @@ +-- x0 := append([]int{}, 1) //@codeactionedit("append([]int{}, 1)", "refactor.extract", func_call1) +-+ x := append([]int{}, 1) +-+ x0 := x //@codeactionedit("append([]int{}, 1)", "refactor.extract", func_call1) +--- @func_call2/func_call.go -- +-@@ -8 +8,2 @@ +-- b, err := strconv.Atoi(str) //@codeactionedit("strconv.Atoi(str)", "refactor.extract", func_call2) +-+ x, x1 := strconv.Atoi(str) +-+ b, err := x, x1 //@codeactionedit("strconv.Atoi(str)", "refactor.extract", func_call2) +--- scope.go -- +-package extract - -- var b *bob -- b. //@complete(" /", a, george) -- y, z := 5, 6 +-import "go/ast" - -- b. //@complete(" /", a, george) -- y, z, a, b, c := 5, 6 +-func _() { +- x0 := 0 +- if true { +- y := ast.CompositeLit{} //@codeactionedit("ast.CompositeLit{}", "refactor.extract", scope1) +- } +- if true { +- x1 := !false //@codeactionedit("!false", "refactor.extract", scope2) +- } -} - --func _() { -- bar. //@complete(" /", Bar) -- bar.Bar() +--- @scope1/scope.go -- +-@@ -8 +8,2 @@ +-- y := ast.CompositeLit{} //@codeactionedit("ast.CompositeLit{}", "refactor.extract", scope1) +-+ x := ast.CompositeLit{} +-+ y := x //@codeactionedit("ast.CompositeLit{}", "refactor.extract", scope1) +--- @scope2/scope.go -- +-@@ -11 +11,2 @@ +-- x1 := !false //@codeactionedit("!false", "refactor.extract", scope2) +-+ x := !false +-+ x1 := x //@codeactionedit("!false", "refactor.extract", scope2) +diff -urN a/gopls/internal/test/marker/testdata/codeaction/extract_variable.txt b/gopls/internal/test/marker/testdata/codeaction/extract_variable.txt +--- a/gopls/internal/test/marker/testdata/codeaction/extract_variable.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codeaction/extract_variable.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,70 +0,0 @@ +-This test checks the behavior of the 'extract variable' code action. +-See extract_variable_resolve.txt for the same test with resolve support. - -- bar. //@complete(" /", Bar) -- go f() --} +--- flags -- +--ignore_extra_diags +- +--- basic_lit.go -- +-package extract - -func _() { -- var b *bob -- if y != b. //@complete(" /", a, george) -- z := 5 +- var _ = 1 + 2 //@codeactionedit("1", "refactor.extract", basic_lit1) +- var _ = 3 + 4 //@codeactionedit("3 + 4", "refactor.extract", basic_lit2) +-} - -- if z + y + 1 + b. //@complete(" /", a, george) -- r, s, t := 4, 5 +--- @basic_lit1/basic_lit.go -- +-@@ -4 +4,2 @@ +-- var _ = 1 + 2 //@codeactionedit("1", "refactor.extract", basic_lit1) +-+ x := 1 +-+ var _ = x + 2 //@codeactionedit("1", "refactor.extract", basic_lit1) +--- @basic_lit2/basic_lit.go -- +-@@ -5 +5,2 @@ +-- var _ = 3 + 4 //@codeactionedit("3 + 4", "refactor.extract", basic_lit2) +-+ x := 3 + 4 +-+ var _ = x //@codeactionedit("3 + 4", "refactor.extract", basic_lit2) +--- func_call.go -- +-package extract - -- if y != b. //@complete(" /", a, george) -- z = 5 +-import "strconv" - -- if z + y + 1 + b. //@complete(" /", a, george) -- r = 4 +-func _() { +- x0 := append([]int{}, 1) //@codeactionedit("append([]int{}, 1)", "refactor.extract", func_call1) +- str := "1" +- b, err := strconv.Atoi(str) //@codeactionedit("strconv.Atoi(str)", "refactor.extract", func_call2) -} - ---- literal_snippets/literal_snippets.go -- --package literal_snippets +--- @func_call1/func_call.go -- +-@@ -6 +6,2 @@ +-- x0 := append([]int{}, 1) //@codeactionedit("append([]int{}, 1)", "refactor.extract", func_call1) +-+ x := append([]int{}, 1) +-+ x0 := x //@codeactionedit("append([]int{}, 1)", "refactor.extract", func_call1) +--- @func_call2/func_call.go -- +-@@ -8 +8,2 @@ +-- b, err := strconv.Atoi(str) //@codeactionedit("strconv.Atoi(str)", "refactor.extract", func_call2) +-+ x, x1 := strconv.Atoi(str) +-+ b, err := x, x1 //@codeactionedit("strconv.Atoi(str)", "refactor.extract", func_call2) +--- scope.go -- +-package extract - --import ( -- "bytes" -- "context" -- "go/ast" -- "net/http" -- "sort" +-import "go/ast" - -- "golang.org/lsptests/foo" --) +-func _() { +- x0 := 0 +- if true { +- y := ast.CompositeLit{} //@codeactionedit("ast.CompositeLit{}", "refactor.extract", scope1) +- } +- if true { +- x1 := !false //@codeactionedit("!false", "refactor.extract", scope2) +- } +-} +- +--- @scope1/scope.go -- +-@@ -8 +8,2 @@ +-- y := ast.CompositeLit{} //@codeactionedit("ast.CompositeLit{}", "refactor.extract", scope1) +-+ x := ast.CompositeLit{} +-+ y := x //@codeactionedit("ast.CompositeLit{}", "refactor.extract", scope1) +--- @scope2/scope.go -- +-@@ -11 +11,2 @@ +-- x1 := !false //@codeactionedit("!false", "refactor.extract", scope2) +-+ x := !false +-+ x1 := x //@codeactionedit("!false", "refactor.extract", scope2) +diff -urN a/gopls/internal/test/marker/testdata/codeaction/extract-variadic-63287.txt b/gopls/internal/test/marker/testdata/codeaction/extract-variadic-63287.txt +--- a/gopls/internal/test/marker/testdata/codeaction/extract-variadic-63287.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codeaction/extract-variadic-63287.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,28 +0,0 @@ +-This test exercises extract on a variadic function. +-It is a regression test for bug #63287 in which +-the final paramater's "..." would go missing. - --func _() { -- []int{} //@item(litIntSlice, "[]int{}", "", "var") -- &[]int{} //@item(litIntSliceAddr, "&[]int{}", "", "var") -- make([]int, 0) //@item(makeIntSlice, "make([]int, 0)", "", "func") +--- go.mod -- +-module example.com +-go 1.18 - -- var _ *[]int = in //@snippet(" //", litIntSliceAddr, "&[]int{$0\\}") -- var _ **[]int = in //@complete(" //") +--- a/a.go -- +-package a - -- var slice []int -- slice = i //@snippet(" //", litIntSlice, "[]int{$0\\}") -- slice = m //@snippet(" //", makeIntSlice, "make([]int, ${1:})") --} +-//@codeactionedit(block, "refactor.extract", out, "Extract function") - -func _() { -- type namedInt []int -- -- namedInt{} //@item(litNamedSlice, "namedInt{}", "", "var") -- make(namedInt, 0) //@item(makeNamedSlice, "make(namedInt, 0)", "", "func") -- -- var namedSlice namedInt -- namedSlice = n //@snippet(" //", litNamedSlice, "namedInt{$0\\}") -- namedSlice = m //@snippet(" //", makeNamedSlice, "make(namedInt, ${1:})") +- var logf func(string, ...any) +- { println(logf) } //@loc(block, re`{.*}`) -} - --func _() { -- make(chan int) //@item(makeChan, "make(chan int)", "", "func") +--- @out/a/a.go -- +-@@ -7 +7 @@ +-- { println(logf) } //@loc(block, re`{.*}`) +-+ { newFunction(logf) } //@loc(block, re`{.*}`) +-@@ -10 +10,4 @@ +-+func newFunction(logf func( string, ...any)) { +-+ println(logf) +-+} +-+ +--- end -- +diff -urN a/gopls/internal/test/marker/testdata/codeaction/fill_struct_resolve.txt b/gopls/internal/test/marker/testdata/codeaction/fill_struct_resolve.txt +--- a/gopls/internal/test/marker/testdata/codeaction/fill_struct_resolve.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codeaction/fill_struct_resolve.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,586 +0,0 @@ +-This test checks the behavior of the 'fill struct' code action, with resolve support. +-See fill_struct.txt for same test without resolve support. - -- var ch chan int -- ch = m //@snippet(" //", makeChan, "make(chan int)") +--- capabilities.json -- +-{ +- "textDocument": { +- "codeAction": { +- "dataSupport": true, +- "resolveSupport": { +- "properties": ["edit"] +- } +- } +- } -} +--- flags -- +--ignore_extra_diags - --func _() { -- map[string]struct{}{} //@item(litMap, "map[string]struct{}{}", "", "var") -- make(map[string]struct{}) //@item(makeMap, "make(map[string]struct{})", "", "func") +--- go.mod -- +-module golang.org/lsptests/fillstruct - -- var m map[string]struct{} -- m = m //@snippet(" //", litMap, "map[string]struct{\\}{$0\\}") -- m = m //@snippet(" //", makeMap, "make(map[string]struct{\\})") +-go 1.18 - -- struct{}{} //@item(litEmptyStruct, "struct{}{}", "", "var") +--- data/data.go -- +-package data - -- m["hi"] = s //@snippet(" //", litEmptyStruct, "struct{\\}{\\}") +-type B struct { +- ExportedInt int +- unexportedInt int -} - --func _() { -- type myStruct struct{ i int } //@item(myStructType, "myStruct", "struct{...}", "struct") +--- a.go -- +-package fillstruct - -- myStruct{} //@item(litStruct, "myStruct{}", "", "var") -- &myStruct{} //@item(litStructPtr, "&myStruct{}", "", "var") +-import ( +- "golang.org/lsptests/fillstruct/data" +-) - -- var ms myStruct -- ms = m //@snippet(" //", litStruct, "myStruct{$0\\}") +-type basicStruct struct { +- foo int +-} - -- var msPtr *myStruct -- msPtr = m //@snippet(" //", litStructPtr, "&myStruct{$0\\}") +-var _ = basicStruct{} //@codeactionedit("}", "refactor.rewrite", a1) - -- msPtr = &m //@snippet(" //", litStruct, "myStruct{$0\\}") +-type twoArgStruct struct { +- foo int +- bar string +-} - -- type myStructCopy struct { i int } //@item(myStructCopyType, "myStructCopy", "struct{...}", "struct") +-var _ = twoArgStruct{} //@codeactionedit("}", "refactor.rewrite", a2) - -- // Don't offer literal completion for convertible structs. -- ms = myStruct //@complete(" //", litStruct, myStructType, myStructCopyType) +-type nestedStruct struct { +- bar string +- basic basicStruct -} - --type myImpl struct{} +-var _ = nestedStruct{} //@codeactionedit("}", "refactor.rewrite", a3) - --func (myImpl) foo() {} +-var _ = data.B{} //@codeactionedit("}", "refactor.rewrite", a4) +--- @a1/a.go -- +-@@ -11 +11,3 @@ +--var _ = basicStruct{} //@codeactionedit("}", "refactor.rewrite", a1) +-+var _ = basicStruct{ +-+ foo: 0, +-+} //@codeactionedit("}", "refactor.rewrite", a1) +--- @a2/a.go -- +-@@ -18 +18,4 @@ +--var _ = twoArgStruct{} //@codeactionedit("}", "refactor.rewrite", a2) +-+var _ = twoArgStruct{ +-+ foo: 0, +-+ bar: "", +-+} //@codeactionedit("}", "refactor.rewrite", a2) +--- @a3/a.go -- +-@@ -25 +25,4 @@ +--var _ = nestedStruct{} //@codeactionedit("}", "refactor.rewrite", a3) +-+var _ = nestedStruct{ +-+ bar: "", +-+ basic: basicStruct{}, +-+} //@codeactionedit("}", "refactor.rewrite", a3) +--- @a4/a.go -- +-@@ -27 +27,3 @@ +--var _ = data.B{} //@codeactionedit("}", "refactor.rewrite", a4) +-+var _ = data.B{ +-+ ExportedInt: 0, +-+} //@codeactionedit("}", "refactor.rewrite", a4) +--- a2.go -- +-package fillstruct - --func (*myImpl) bar() {} +-type typedStruct struct { +- m map[string]int +- s []int +- c chan int +- c1 <-chan int +- a [2]string +-} - --type myBasicImpl string +-var _ = typedStruct{} //@codeactionedit("}", "refactor.rewrite", a21) - --func (myBasicImpl) foo() {} +-type funStruct struct { +- fn func(i int) int +-} - --func _() { -- type myIntf interface { -- foo() -- } +-var _ = funStruct{} //@codeactionedit("}", "refactor.rewrite", a22) - -- myImpl{} //@item(litImpl, "myImpl{}", "", "var") +-type funStructComplex struct { +- fn func(i int, s string) (string, int) +-} - -- var mi myIntf -- mi = m //@snippet(" //", litImpl, "myImpl{\\}") +-var _ = funStructComplex{} //@codeactionedit("}", "refactor.rewrite", a23) - -- myBasicImpl() //@item(litBasicImpl, "myBasicImpl()", "string", "var") +-type funStructEmpty struct { +- fn func() +-} - -- mi = m //@snippet(" //", litBasicImpl, "myBasicImpl($0)") +-var _ = funStructEmpty{} //@codeactionedit("}", "refactor.rewrite", a24) - -- // only satisfied by pointer to myImpl -- type myPtrIntf interface { -- bar() -- } +--- @a21/a2.go -- +-@@ -11 +11,7 @@ +--var _ = typedStruct{} //@codeactionedit("}", "refactor.rewrite", a21) +-+var _ = typedStruct{ +-+ m: map[string]int{}, +-+ s: []int{}, +-+ c: make(chan int), +-+ c1: make(<-chan int), +-+ a: [2]string{}, +-+} //@codeactionedit("}", "refactor.rewrite", a21) +--- @a22/a2.go -- +-@@ -17 +17,4 @@ +--var _ = funStruct{} //@codeactionedit("}", "refactor.rewrite", a22) +-+var _ = funStruct{ +-+ fn: func(i int) int { +-+ }, +-+} //@codeactionedit("}", "refactor.rewrite", a22) +--- @a23/a2.go -- +-@@ -23 +23,4 @@ +--var _ = funStructComplex{} //@codeactionedit("}", "refactor.rewrite", a23) +-+var _ = funStructComplex{ +-+ fn: func(i int, s string) (string, int) { +-+ }, +-+} //@codeactionedit("}", "refactor.rewrite", a23) +--- @a24/a2.go -- +-@@ -29 +29,4 @@ +--var _ = funStructEmpty{} //@codeactionedit("}", "refactor.rewrite", a24) +-+var _ = funStructEmpty{ +-+ fn: func() { +-+ }, +-+} //@codeactionedit("}", "refactor.rewrite", a24) +--- a3.go -- +-package fillstruct - -- &myImpl{} //@item(litImplPtr, "&myImpl{}", "", "var") +-import ( +- "go/ast" +- "go/token" +-) - -- var mpi myPtrIntf -- mpi = m //@snippet(" //", litImplPtr, "&myImpl{\\}") +-type Foo struct { +- A int -} - --func _() { -- var s struct{ i []int } //@item(litSliceField, "i", "[]int", "field") -- var foo []int -- // no literal completions after selector -- foo = s.i //@complete(" //", litSliceField) +-type Bar struct { +- X *Foo +- Y *Foo -} - --func _() { -- type myStruct struct{ i int } //@item(litMyStructType, "myStruct", "struct{...}", "struct") -- myStruct{} //@item(litMyStruct, "myStruct{}", "", "var") +-var _ = Bar{} //@codeactionedit("}", "refactor.rewrite", a31) - -- foo := func(s string, args ...myStruct) {} -- // Don't give literal slice candidate for variadic arg. -- // Do give literal candidates for variadic element. -- foo("", myStruct) //@complete(")", litMyStruct, litMyStructType) +-type importedStruct struct { +- m map[*ast.CompositeLit]ast.Field +- s []ast.BadExpr +- a [3]token.Token +- c chan ast.EmptyStmt +- fn func(ast_decl ast.DeclStmt) ast.Ellipsis +- st ast.CompositeLit -} - --func _() { -- Buffer{} //@item(litBuffer, "Buffer{}", "", "var") +-var _ = importedStruct{} //@codeactionedit("}", "refactor.rewrite", a32) - -- var b *bytes.Buffer -- b = bytes.Bu //@snippet(" //", litBuffer, "Buffer{\\}") +-type pointerBuiltinStruct struct { +- b *bool +- s *string +- i *int -} - --func _() { -- _ = "func(...) {}" //@item(litFunc, "func(...) {}", "", "var") -- -- // no literal "func" completions -- http.Handle("", fun) //@complete(")") -- -- var namedReturn func(s string) (b bool) -- namedReturn = f //@snippet(" //", litFunc, "func(s string) (b bool) {$0\\}") -- -- var multiReturn func() (bool, int) -- multiReturn = f //@snippet(" //", litFunc, "func() (bool, int) {$0\\}") -- -- var multiNamedReturn func() (b bool, i int) -- multiNamedReturn = f //@snippet(" //", litFunc, "func() (b bool, i int) {$0\\}") -- -- var duplicateParams func(myImpl, int, myImpl) -- duplicateParams = f //@snippet(" //", litFunc, "func(mi1 myImpl, i int, mi2 myImpl) {$0\\}") -- -- type aliasImpl = myImpl -- var aliasParams func(aliasImpl) aliasImpl -- aliasParams = f //@snippet(" //", litFunc, "func(ai aliasImpl) aliasImpl {$0\\}") -- -- const two = 2 -- var builtinTypes func([]int, [two]bool, map[string]string, struct{ i int }, interface{ foo() }, <-chan int) -- builtinTypes = f //@snippet(" //", litFunc, "func(i1 []int, b [two]bool, m map[string]string, s struct{ i int \\}, i2 interface{ foo() \\}, c <-chan int) {$0\\}") -- -- var _ func(ast.Node) = f //@snippet(" //", litFunc, "func(n ast.Node) {$0\\}") -- var _ func(error) = f //@snippet(" //", litFunc, "func(err error) {$0\\}") -- var _ func(context.Context) = f //@snippet(" //", litFunc, "func(ctx context.Context) {$0\\}") +-var _ = pointerBuiltinStruct{} //@codeactionedit("}", "refactor.rewrite", a33) - -- type context struct {} -- var _ func(context) = f //@snippet(" //", litFunc, "func(ctx context) {$0\\}") +-var _ = []ast.BasicLit{ +- {}, //@codeactionedit("}", "refactor.rewrite", a34) -} - --func _() { -- float64() //@item(litFloat64, "float64()", "float64", "var") -- -- // don't complete to "&float64()" -- var _ *float64 = float64 //@complete(" //") +-var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite", a35) +--- @a31/a3.go -- +-@@ -17 +17,4 @@ +--var _ = Bar{} //@codeactionedit("}", "refactor.rewrite", a31) +-+var _ = Bar{ +-+ X: &Foo{}, +-+ Y: &Foo{}, +-+} //@codeactionedit("}", "refactor.rewrite", a31) +--- @a32/a3.go -- +-@@ -28 +28,9 @@ +--var _ = importedStruct{} //@codeactionedit("}", "refactor.rewrite", a32) +-+var _ = importedStruct{ +-+ m: map[*ast.CompositeLit]ast.Field{}, +-+ s: []ast.BadExpr{}, +-+ a: [3]token.Token{}, +-+ c: make(chan ast.EmptyStmt), +-+ fn: func(ast_decl ast.DeclStmt) ast.Ellipsis { +-+ }, +-+ st: ast.CompositeLit{}, +-+} //@codeactionedit("}", "refactor.rewrite", a32) +--- @a33/a3.go -- +-@@ -36 +36,5 @@ +--var _ = pointerBuiltinStruct{} //@codeactionedit("}", "refactor.rewrite", a33) +-+var _ = pointerBuiltinStruct{ +-+ b: new(bool), +-+ s: new(string), +-+ i: new(int), +-+} //@codeactionedit("}", "refactor.rewrite", a33) +--- @a34/a3.go -- +-@@ -39 +39,5 @@ +-- {}, //@codeactionedit("}", "refactor.rewrite", a34) +-+ { +-+ ValuePos: 0, +-+ Kind: 0, +-+ Value: "", +-+ }, //@codeactionedit("}", "refactor.rewrite", a34) +--- @a35/a3.go -- +-@@ -42 +42,5 @@ +--var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite", a35) +-+var _ = []ast.BasicLit{{ +-+ ValuePos: 0, +-+ Kind: 0, +-+ Value: "", +-+}} //@codeactionedit("}", "refactor.rewrite", a35) +--- a4.go -- +-package fillstruct - -- var f float64 -- f = fl //@complete(" //", litFloat64),snippet(" //", litFloat64, "float64($0)") +-import "go/ast" - -- type myInt int -- myInt() //@item(litMyInt, "myInt()", "", "var") +-type iStruct struct { +- X int +-} - -- var mi myInt -- mi = my //@snippet(" //", litMyInt, "myInt($0)") +-type sStruct struct { +- str string -} - --func _() { -- type ptrStruct struct { -- p *ptrStruct -- } +-type multiFill struct { +- num int +- strin string +- arr []int +-} - -- ptrStruct{} //@item(litPtrStruct, "ptrStruct{}", "", "var") +-type assignStruct struct { +- n ast.Node +-} - -- ptrStruct{ -- p: &ptrSt, //@rank(",", litPtrStruct) -- } +-func fill() { +- var x int +- var _ = iStruct{} //@codeactionedit("}", "refactor.rewrite", a41) - -- &ptrStruct{} //@item(litPtrStructPtr, "&ptrStruct{}", "", "var") +- var s string +- var _ = sStruct{} //@codeactionedit("}", "refactor.rewrite", a42) - -- &ptrStruct{ -- p: ptrSt, //@rank(",", litPtrStructPtr) +- var n int +- _ = []int{} +- if true { +- arr := []int{1, 2} - } --} +- var _ = multiFill{} //@codeactionedit("}", "refactor.rewrite", a43) - --func _() { -- f := func(...[]int) {} -- f() //@snippet(")", litIntSlice, "[]int{$0\\}") +- var node *ast.CompositeLit +- var _ = assignStruct{} //@codeactionedit("}", "refactor.rewrite", a45) -} - +--- @a41/a4.go -- +-@@ -25 +25,3 @@ +-- var _ = iStruct{} //@codeactionedit("}", "refactor.rewrite", a41) +-+ var _ = iStruct{ +-+ X: x, +-+ } //@codeactionedit("}", "refactor.rewrite", a41) +--- @a42/a4.go -- +-@@ -28 +28,3 @@ +-- var _ = sStruct{} //@codeactionedit("}", "refactor.rewrite", a42) +-+ var _ = sStruct{ +-+ str: s, +-+ } //@codeactionedit("}", "refactor.rewrite", a42) +--- @a43/a4.go -- +-@@ -35 +35,5 @@ +-- var _ = multiFill{} //@codeactionedit("}", "refactor.rewrite", a43) +-+ var _ = multiFill{ +-+ num: n, +-+ strin: s, +-+ arr: []int{}, +-+ } //@codeactionedit("}", "refactor.rewrite", a43) +--- @a45/a4.go -- +-@@ -38 +38,3 @@ +-- var _ = assignStruct{} //@codeactionedit("}", "refactor.rewrite", a45) +-+ var _ = assignStruct{ +-+ n: node, +-+ } //@codeactionedit("}", "refactor.rewrite", a45) +--- fill_struct.go -- +-package fillstruct - --func _() { -- // don't complete to "untyped int()" -- []int{}[untyped] //@complete("] //") +-type StructA struct { +- unexportedIntField int +- ExportedIntField int +- MapA map[int]string +- Array []int +- StructB -} - --type Tree[T any] struct{} -- --func (tree Tree[T]) Do(f func(s T)) {} -- --func _() { -- var t Tree[string] -- t.Do(fun) //@complete(")", litFunc), snippet(")", litFunc, "func(s string) {$0\\}") +-type StructA2 struct { +- B *StructB -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/func_rank.txt b/gopls/internal/regtest/marker/testdata/completion/func_rank.txt ---- a/gopls/internal/regtest/marker/testdata/completion/func_rank.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/func_rank.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,83 +0,0 @@ --This test checks various ranking of completion results within function call --context. -- ---- flags -- ---ignore_extra_diags - ---- settings.json -- --{ -- "completeUnimported": false, -- "deepCompletion": false +-type StructA3 struct { +- B StructB -} - ---- func_rank.go -- --package func_rank -- --import "net/http" -- --var stringAVar = "var" //@item(stringAVar, "stringAVar", "string", "var") --func stringBFunc() string { return "str" } //@item(stringBFunc, "stringBFunc", "func() string", "func") --type stringer struct{} //@item(stringer, "stringer", "struct{...}", "struct") -- --func _() stringer //@complete("tr", stringer) -- --func _(val stringer) {} //@complete("tr", stringer) -- --func (stringer) _() {} //@complete("tr", stringer) -- --func _() { -- var s struct { -- AA int //@item(rankAA, "AA", "int", "field") -- AB string //@item(rankAB, "AB", "string", "field") -- AC int //@item(rankAC, "AC", "int", "field") +-func fill() { +- a := StructA{} //@codeactionedit("}", "refactor.rewrite", fill_struct1) +- b := StructA2{} //@codeactionedit("}", "refactor.rewrite", fill_struct2) +- c := StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct3) +- if true { +- _ = StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct4) - } -- fnStr := func(string) {} -- fnStr(s.A) //@complete(")", rankAB, rankAA, rankAC) -- fnStr("" + s.A) //@complete(")", rankAB, rankAA, rankAC) -- -- fnInt := func(int) {} -- fnInt(-s.A) //@complete(")", rankAA, rankAC, rankAB) -- -- // no expected type -- fnInt(func() int { s.A }) //@complete(" }", rankAA, rankAB, rankAC) -- fnInt(s.A()) //@complete("()", rankAA, rankAC, rankAB) -- fnInt([]int{}[s.A]) //@complete("])", rankAA, rankAC, rankAB) -- fnInt([]int{}[:s.A]) //@complete("])", rankAA, rankAC, rankAB) -- -- fnInt(s.A.(int)) //@complete(".(", rankAA, rankAC, rankAB) -- -- fnPtr := func(*string) {} -- fnPtr(&s.A) //@complete(")", rankAB, rankAA, rankAC) +-} - -- var aaPtr *string //@item(rankAAPtr, "aaPtr", "*string", "var") -- var abPtr *int //@item(rankABPtr, "abPtr", "*int", "var") -- fnInt(*a) //@complete(")", rankABPtr, rankAAPtr, stringAVar) +--- @fill_struct1/fill_struct.go -- +-@@ -20 +20,7 @@ +-- a := StructA{} //@codeactionedit("}", "refactor.rewrite", fill_struct1) +-+ a := StructA{ +-+ unexportedIntField: 0, +-+ ExportedIntField: 0, +-+ MapA: map[int]string{}, +-+ Array: []int{}, +-+ StructB: StructB{}, +-+ } //@codeactionedit("}", "refactor.rewrite", fill_struct1) +--- @fill_struct2/fill_struct.go -- +-@@ -21 +21,3 @@ +-- b := StructA2{} //@codeactionedit("}", "refactor.rewrite", fill_struct2) +-+ b := StructA2{ +-+ B: &StructB{}, +-+ } //@codeactionedit("}", "refactor.rewrite", fill_struct2) +--- @fill_struct3/fill_struct.go -- +-@@ -22 +22,3 @@ +-- c := StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct3) +-+ c := StructA3{ +-+ B: StructB{}, +-+ } //@codeactionedit("}", "refactor.rewrite", fill_struct3) +--- @fill_struct4/fill_struct.go -- +-@@ -24 +24,3 @@ +-- _ = StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct4) +-+ _ = StructA3{ +-+ B: StructB{}, +-+ } //@codeactionedit("}", "refactor.rewrite", fill_struct4) +--- fill_struct_anon.go -- +-package fillstruct - -- _ = func() string { -- return s.A //@complete(" //", rankAB, rankAA, rankAC) +-type StructAnon struct { +- a struct{} +- b map[string]interface{} +- c map[string]struct { +- d int +- e bool - } -} - --type foo struct { -- fooPrivateField int //@item(rankFooPrivField, "fooPrivateField", "int", "field") -- FooPublicField int //@item(rankFooPubField, "FooPublicField", "int", "field") +-func fill() { +- _ := StructAnon{} //@codeactionedit("}", "refactor.rewrite", fill_struct_anon) -} +--- @fill_struct_anon/fill_struct_anon.go -- +-@@ -13 +13,5 @@ +-- _ := StructAnon{} //@codeactionedit("}", "refactor.rewrite", fill_struct_anon) +-+ _ := StructAnon{ +-+ a: struct{}{}, +-+ b: map[string]interface{}{}, +-+ c: map[string]struct{d int; e bool}{}, +-+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_anon) +--- fill_struct_nested.go -- +-package fillstruct - --func (foo) fooPrivateMethod() int { //@item(rankFooPrivMeth, "fooPrivateMethod", "func() int", "method") -- return 0 +-type StructB struct { +- StructC -} - --func (foo) FooPublicMethod() int { //@item(rankFooPubMeth, "FooPublicMethod", "func() int", "method") -- return 0 +-type StructC struct { +- unexportedInt int -} - --func _() { -- var _ int = foo{}. //@rank(" //", rankFooPrivField, rankFooPubField),rank(" //", rankFooPrivMeth, rankFooPubMeth),rank(" //", rankFooPrivField, rankFooPrivMeth) +-func nested() { +- c := StructB{ +- StructC: StructC{}, //@codeactionedit("}", "refactor.rewrite", fill_nested) +- } -} - --func _() { -- HandleFunc //@item(httpHandleFunc, "HandleFunc", "func(pattern string, handler func(http.ResponseWriter, *http.Request))", "func") -- HandlerFunc //@item(httpHandlerFunc, "HandlerFunc", "func(http.ResponseWriter, *http.Request)", "type") +--- @fill_nested/fill_struct_nested.go -- +-@@ -13 +13,3 @@ +-- StructC: StructC{}, //@codeactionedit("}", "refactor.rewrite", fill_nested) +-+ StructC: StructC{ +-+ unexportedInt: 0, +-+ }, //@codeactionedit("}", "refactor.rewrite", fill_nested) +--- fill_struct_package.go -- +-package fillstruct - -- http.HandleFunc //@rank(" //", httpHandleFunc, httpHandlerFunc) --} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/func_sig.txt b/gopls/internal/regtest/marker/testdata/completion/func_sig.txt ---- a/gopls/internal/regtest/marker/testdata/completion/func_sig.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/func_sig.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,15 +0,0 @@ --This test checks completion related to function signatures. +-import ( +- h2 "net/http" - ---- flags -- ---ignore_extra_diags +- "golang.org/lsptests/fillstruct/data" +-) - ---- func_sig.go -- --package funcsig +-func unexported() { +- a := data.B{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package1) +- _ = h2.Client{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package2) +-} +--- @fill_struct_package1/fill_struct_package.go -- +-@@ -10 +10,3 @@ +-- a := data.B{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package1) +-+ a := data.B{ +-+ ExportedInt: 0, +-+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_package1) +--- @fill_struct_package2/fill_struct_package.go -- +-@@ -11 +11,7 @@ +-- _ = h2.Client{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package2) +-+ _ = h2.Client{ +-+ Transport: nil, +-+ CheckRedirect: func(req *h2.Request, via []*h2.Request) error { +-+ }, +-+ Jar: nil, +-+ Timeout: 0, +-+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_package2) +--- fill_struct_partial.go -- +-package fillstruct - --type someType int //@item(sigSomeType, "someType", "int", "type") +-type StructPartialA struct { +- PrefilledInt int +- UnfilledInt int +- StructPartialB +-} - --// Don't complete "foo" in signature. --func (foo someType) _() { //@item(sigFoo, "foo", "someType", "var"),complete(") {", sigSomeType) +-type StructPartialB struct { +- PrefilledInt int +- UnfilledInt int +-} - -- //@complete("", sigFoo, sigSomeType) +-func fill() { +- a := StructPartialA{ +- PrefilledInt: 5, +- } //@codeactionedit("}", "refactor.rewrite", fill_struct_partial1) +- b := StructPartialB{ +- /* this comment should disappear */ +- PrefilledInt: 7, // This comment should be blown away. +- /* As should +- this one */ +- } //@codeactionedit("}", "refactor.rewrite", fill_struct_partial2) -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/func_snippets.txt b/gopls/internal/regtest/marker/testdata/completion/func_snippets.txt ---- a/gopls/internal/regtest/marker/testdata/completion/func_snippets.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/func_snippets.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,32 +0,0 @@ --This test exercises function snippets using generics. - ---- flags -- ---ignore_extra_diags +--- @fill_struct_partial1/fill_struct_partial.go -- +-@@ -16 +16,3 @@ +-- PrefilledInt: 5, +-+ PrefilledInt: 5, +-+ UnfilledInt: 0, +-+ StructPartialB: StructPartialB{}, +--- @fill_struct_partial2/fill_struct_partial.go -- +-@@ -19,4 +19,2 @@ +-- /* this comment should disappear */ +-- PrefilledInt: 7, // This comment should be blown away. +-- /* As should +-- this one */ +-+ PrefilledInt: 7, +-+ UnfilledInt: 0, +--- fill_struct_spaces.go -- +-package fillstruct - ---- settings.json -- --{ -- "usePlaceholders": true +-type StructD struct { +- ExportedIntField int -} - ---- go.mod -- --module golang.org/lsptests/snippets -- --go 1.18 +-func spaces() { +- d := StructD{} //@codeactionedit("}", "refactor.rewrite", fill_struct_spaces) +-} - ---- funcsnippets.go -- --package snippets +--- @fill_struct_spaces/fill_struct_spaces.go -- +-@@ -8 +8,3 @@ +-- d := StructD{} //@codeactionedit("}", "refactor.rewrite", fill_struct_spaces) +-+ d := StructD{ +-+ ExportedIntField: 0, +-+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_spaces) +--- fill_struct_unsafe.go -- +-package fillstruct - --type SyncMap[K comparable, V any] struct{} +-import "unsafe" - --func NewSyncMap[K comparable, V any]() (result *SyncMap[K, V]) { //@item(NewSyncMap, "NewSyncMap", "", "") -- return +-type unsafeStruct struct { +- x int +- p unsafe.Pointer -} - --func Identity[P ~int](p P) P { //@item(Identity, "Identity", "", "") -- return p +-func fill() { +- _ := unsafeStruct{} //@codeactionedit("}", "refactor.rewrite", fill_struct_unsafe) -} - --func _() { -- _ = NewSyncM //@snippet(" //", NewSyncMap, "NewSyncMap[${1:K comparable}, ${2:V any}]()") -- _ = Identi //@snippet(" //", Identity, "Identity[${1:P ~int}](${2:p P})") --} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/func_value.txt b/gopls/internal/regtest/marker/testdata/completion/func_value.txt ---- a/gopls/internal/regtest/marker/testdata/completion/func_value.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/func_value.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,33 +0,0 @@ --This test checks completion related to function values. +--- @fill_struct_unsafe/fill_struct_unsafe.go -- +-@@ -11 +11,4 @@ +-- _ := unsafeStruct{} //@codeactionedit("}", "refactor.rewrite", fill_struct_unsafe) +-+ _ := unsafeStruct{ +-+ x: 0, +-+ p: nil, +-+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_unsafe) +--- typeparams.go -- +-package fillstruct - ---- flags -- ---ignore_extra_diags +-type emptyStructWithTypeParams[A any] struct{} - ---- func_value.go -- --package funcvalue +-var _ = emptyStructWithTypeParams[int]{} // no suggested fix - --func fooFunc() int { //@item(fvFooFunc, "fooFunc", "func() int", "func") -- return 0 +-type basicStructWithTypeParams[T any] struct { +- foo T -} - --var _ = fooFunc() //@item(fvFooFuncCall, "fooFunc", "func() int", "func") +-var _ = basicStructWithTypeParams[int]{} //@codeactionedit("}", "refactor.rewrite", typeparams1) - --var fooVar = func() int { //@item(fvFooVar, "fooVar", "func() int", "var") -- return 0 +-type twoArgStructWithTypeParams[F, B any] struct { +- foo F +- bar B -} - --var _ = fooVar() //@item(fvFooVarCall, "fooVar", "func() int", "var") +-var _ = twoArgStructWithTypeParams[string, int]{} //@codeactionedit("}", "refactor.rewrite", typeparams2) - --type myFunc func() int +-var _ = twoArgStructWithTypeParams[int, string]{ +- bar: "bar", +-} //@codeactionedit("}", "refactor.rewrite", typeparams3) - --var fooType myFunc = fooVar //@item(fvFooType, "fooType", "myFunc", "var") +-type nestedStructWithTypeParams struct { +- bar string +- basic basicStructWithTypeParams[int] +-} - --var _ = fooType() //@item(fvFooTypeCall, "fooType", "func() int", "var") +-var _ = nestedStructWithTypeParams{} //@codeactionedit("}", "refactor.rewrite", typeparams4) - --func _() { -- var f func() int -- f = foo //@complete(" //", fvFooFunc, fvFooType, fvFooVar) +-func _[T any]() { +- type S struct{ t T } +- _ = S{} //@codeactionedit("}", "refactor.rewrite", typeparams5) +-} +--- @typeparams1/typeparams.go -- +-@@ -11 +11,3 @@ +--var _ = basicStructWithTypeParams[int]{} //@codeactionedit("}", "refactor.rewrite", typeparams1) +-+var _ = basicStructWithTypeParams[int]{ +-+ foo: 0, +-+} //@codeactionedit("}", "refactor.rewrite", typeparams1) +--- @typeparams2/typeparams.go -- +-@@ -18 +18,4 @@ +--var _ = twoArgStructWithTypeParams[string, int]{} //@codeactionedit("}", "refactor.rewrite", typeparams2) +-+var _ = twoArgStructWithTypeParams[string, int]{ +-+ foo: "", +-+ bar: 0, +-+} //@codeactionedit("}", "refactor.rewrite", typeparams2) +--- @typeparams3/typeparams.go -- +-@@ -21 +21 @@ +-+ foo: 0, +--- @typeparams4/typeparams.go -- +-@@ -29 +29,4 @@ +--var _ = nestedStructWithTypeParams{} //@codeactionedit("}", "refactor.rewrite", typeparams4) +-+var _ = nestedStructWithTypeParams{ +-+ bar: "", +-+ basic: basicStructWithTypeParams{}, +-+} //@codeactionedit("}", "refactor.rewrite", typeparams4) +--- @typeparams5/typeparams.go -- +-@@ -33 +33,3 @@ +-- _ = S{} //@codeactionedit("}", "refactor.rewrite", typeparams5) +-+ _ = S{ +-+ t: *new(T), +-+ } //@codeactionedit("}", "refactor.rewrite", typeparams5) +--- issue63921.go -- +-package fillstruct - -- var i int -- i = foo //@complete(" //", fvFooFuncCall, fvFooTypeCall, fvFooVarCall) +-// Test for golang/go#63921: fillstruct panicked with invalid fields. +-type invalidStruct struct { +- F int +- Undefined -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/fuzzy.txt b/gopls/internal/regtest/marker/testdata/completion/fuzzy.txt ---- a/gopls/internal/regtest/marker/testdata/completion/fuzzy.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/fuzzy.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,55 +0,0 @@ --This test exercises fuzzy completion matching. +- +-func _() { +- // Note: the golden content for issue63921 is empty: fillstruct produces no +- // edits, but does not panic. +- invalidStruct{} //@codeactionedit("}", "refactor.rewrite", issue63921) +-} +diff -urN a/gopls/internal/test/marker/testdata/codeaction/fill_struct.txt b/gopls/internal/test/marker/testdata/codeaction/fill_struct.txt +--- a/gopls/internal/test/marker/testdata/codeaction/fill_struct.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codeaction/fill_struct.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,575 +0,0 @@ +-This test checks the behavior of the 'fill struct' code action. +-See fill_struct_resolve.txt for same test with resolve support. - --- flags -- --ignore_extra_diags - --- go.mod -- --module golang.org/lsptests +-module golang.org/lsptests/fillstruct - -go 1.18 - ---- fuzzy/fuzzy.go -- --package fuzzy -- --func _() { -- var a struct { -- fabar int -- fooBar string -- } -- -- a.fabar //@item(fuzzFabarField, "a.fabar", "int", "field") -- a.fooBar //@item(fuzzFooBarField, "a.fooBar", "string", "field") -- -- afa //@complete(" //", fuzzFabarField, fuzzFooBarField) -- afb //@complete(" //", fuzzFooBarField, fuzzFabarField) -- -- fab //@complete(" //", fuzzFabarField) -- -- var myString string -- myString = af //@complete(" //", fuzzFooBarField, fuzzFabarField) +--- data/data.go -- +-package data - -- var b struct { -- c struct { -- d struct { -- e struct { -- abc string -- } -- abc float32 -- } -- abc bool -- } -- abc int -- } +-type B struct { +- ExportedInt int +- unexportedInt int +-} - -- b.abc //@item(fuzzABCInt, "b.abc", "int", "field") -- b.c.abc //@item(fuzzABCbool, "b.c.abc", "bool", "field") -- b.c.d.abc //@item(fuzzABCfloat, "b.c.d.abc", "float32", "field") -- b.c.d.e.abc //@item(fuzzABCstring, "b.c.d.e.abc", "string", "field") +--- a.go -- +-package fillstruct - -- // in depth order by default -- abc //@complete(" //", fuzzABCInt, fuzzABCbool, fuzzABCfloat) +-import ( +- "golang.org/lsptests/fillstruct/data" +-) - -- // deep candidate that matches expected type should still ranked first -- var s string -- s = abc //@complete(" //", fuzzABCstring, fuzzABCInt, fuzzABCbool) +-type basicStruct struct { +- foo int -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/index.txt b/gopls/internal/regtest/marker/testdata/completion/index.txt ---- a/gopls/internal/regtest/marker/testdata/completion/index.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/index.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,36 +0,0 @@ --This test checks completion related to index expressions. - ---- flags -- ---ignore_extra_diags +-var _ = basicStruct{} //@codeactionedit("}", "refactor.rewrite", a1) - ---- settings.json -- --{ -- "completeUnimported": false +-type twoArgStruct struct { +- foo int +- bar string -} - ---- index.go -- --package index -- --func _() { -- var ( -- aa = "123" //@item(indexAA, "aa", "string", "var") -- ab = 123 //@item(indexAB, "ab", "int", "var") -- ) +-var _ = twoArgStruct{} //@codeactionedit("}", "refactor.rewrite", a2) - -- var foo [1]int -- foo[a] //@complete("]", indexAB, indexAA) -- foo[:a] //@complete("]", indexAB, indexAA) -- a[:a] //@complete("[", indexAA, indexAB) -- a[a] //@complete("[", indexAA, indexAB) +-type nestedStruct struct { +- bar string +- basic basicStruct +-} - -- var bar map[string]int -- bar[a] //@complete("]", indexAA, indexAB) +-var _ = nestedStruct{} //@codeactionedit("}", "refactor.rewrite", a3) - -- type myMap map[string]int -- var baz myMap -- baz[a] //@complete("]", indexAA, indexAB) +-var _ = data.B{} //@codeactionedit("}", "refactor.rewrite", a4) +--- @a1/a.go -- +-@@ -11 +11,3 @@ +--var _ = basicStruct{} //@codeactionedit("}", "refactor.rewrite", a1) +-+var _ = basicStruct{ +-+ foo: 0, +-+} //@codeactionedit("}", "refactor.rewrite", a1) +--- @a2/a.go -- +-@@ -18 +18,4 @@ +--var _ = twoArgStruct{} //@codeactionedit("}", "refactor.rewrite", a2) +-+var _ = twoArgStruct{ +-+ foo: 0, +-+ bar: "", +-+} //@codeactionedit("}", "refactor.rewrite", a2) +--- @a3/a.go -- +-@@ -25 +25,4 @@ +--var _ = nestedStruct{} //@codeactionedit("}", "refactor.rewrite", a3) +-+var _ = nestedStruct{ +-+ bar: "", +-+ basic: basicStruct{}, +-+} //@codeactionedit("}", "refactor.rewrite", a3) +--- @a4/a.go -- +-@@ -27 +27,3 @@ +--var _ = data.B{} //@codeactionedit("}", "refactor.rewrite", a4) +-+var _ = data.B{ +-+ ExportedInt: 0, +-+} //@codeactionedit("}", "refactor.rewrite", a4) +--- a2.go -- +-package fillstruct - -- type myInt int -- var mi myInt //@item(indexMyInt, "mi", "myInt", "var") -- foo[m] //@snippet("]", indexMyInt, "mi") +-type typedStruct struct { +- m map[string]int +- s []int +- c chan int +- c1 <-chan int +- a [2]string -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/interfacerank.txt b/gopls/internal/regtest/marker/testdata/completion/interfacerank.txt ---- a/gopls/internal/regtest/marker/testdata/completion/interfacerank.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/interfacerank.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,36 +0,0 @@ --This test checks that completion ranking accounts for interface assignability. - ---- flags -- ---ignore_extra_diags +-var _ = typedStruct{} //@codeactionedit("}", "refactor.rewrite", a21) - ---- settings.json -- --{ -- "completeUnimported": false, -- "deepCompletion": false +-type funStruct struct { +- fn func(i int) int -} - ---- p.go -- -- --package interfacerank +-var _ = funStruct{} //@codeactionedit("}", "refactor.rewrite", a22) - --type foo interface { -- foo() +-type funStructComplex struct { +- fn func(i int, s string) (string, int) -} - --type fooImpl int +-var _ = funStructComplex{} //@codeactionedit("}", "refactor.rewrite", a23) - --func (*fooImpl) foo() {} +-type funStructEmpty struct { +- fn func() +-} - --func wantsFoo(foo) {} +-var _ = funStructEmpty{} //@codeactionedit("}", "refactor.rewrite", a24) - --func _() { -- var ( -- aa string //@item(irAA, "aa", "string", "var") -- ab *fooImpl //@item(irAB, "ab", "*fooImpl", "var") -- ) +--- @a21/a2.go -- +-@@ -11 +11,7 @@ +--var _ = typedStruct{} //@codeactionedit("}", "refactor.rewrite", a21) +-+var _ = typedStruct{ +-+ m: map[string]int{}, +-+ s: []int{}, +-+ c: make(chan int), +-+ c1: make(<-chan int), +-+ a: [2]string{}, +-+} //@codeactionedit("}", "refactor.rewrite", a21) +--- @a22/a2.go -- +-@@ -17 +17,4 @@ +--var _ = funStruct{} //@codeactionedit("}", "refactor.rewrite", a22) +-+var _ = funStruct{ +-+ fn: func(i int) int { +-+ }, +-+} //@codeactionedit("}", "refactor.rewrite", a22) +--- @a23/a2.go -- +-@@ -23 +23,4 @@ +--var _ = funStructComplex{} //@codeactionedit("}", "refactor.rewrite", a23) +-+var _ = funStructComplex{ +-+ fn: func(i int, s string) (string, int) { +-+ }, +-+} //@codeactionedit("}", "refactor.rewrite", a23) +--- @a24/a2.go -- +-@@ -29 +29,4 @@ +--var _ = funStructEmpty{} //@codeactionedit("}", "refactor.rewrite", a24) +-+var _ = funStructEmpty{ +-+ fn: func() { +-+ }, +-+} //@codeactionedit("}", "refactor.rewrite", a24) +--- a3.go -- +-package fillstruct - -- wantsFoo(a) //@complete(")", irAB, irAA) +-import ( +- "go/ast" +- "go/token" +-) - -- var ac fooImpl //@item(irAC, "ac", "fooImpl", "var") -- wantsFoo(&a) //@complete(")", irAC, irAA, irAB) +-type Foo struct { +- A int -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/issue56505.txt b/gopls/internal/regtest/marker/testdata/completion/issue56505.txt ---- a/gopls/internal/regtest/marker/testdata/completion/issue56505.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/issue56505.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,13 +0,0 @@ --Test for golang/go#56505: completion on variables of type *error should not --panic. -- ---- flags -- ---ignore_extra_diags -- ---- issue.go -- --package issues - --func _() { -- var e *error -- e.x //@complete(" //") +-type Bar struct { +- X *Foo +- Y *Foo -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/issue59096.txt b/gopls/internal/regtest/marker/testdata/completion/issue59096.txt ---- a/gopls/internal/regtest/marker/testdata/completion/issue59096.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/issue59096.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,20 +0,0 @@ --This test exercises the panic in golang/go#59096: completing at a syntactic --type-assert expression was panicking because gopls was translating it into --a (malformed) selector expr. -- ---- go.mod -- --module example.com - ---- a/a.go -- --package a +-var _ = Bar{} //@codeactionedit("}", "refactor.rewrite", a31) - --func _() { -- b.(foo) //@complete(re"b.()", B), diag("b", re"(undefined|undeclared name): b") +-type importedStruct struct { +- m map[*ast.CompositeLit]ast.Field +- s []ast.BadExpr +- a [3]token.Token +- c chan ast.EmptyStmt +- fn func(ast_decl ast.DeclStmt) ast.Ellipsis +- st ast.CompositeLit -} - --//@item(B, "B", "const (from \"example.com/b\")", "const") -- ---- b/b.go -- --package b +-var _ = importedStruct{} //@codeactionedit("}", "refactor.rewrite", a32) - --const B = 0 -diff -urN a/gopls/internal/regtest/marker/testdata/completion/issue60545.txt b/gopls/internal/regtest/marker/testdata/completion/issue60545.txt ---- a/gopls/internal/regtest/marker/testdata/completion/issue60545.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/issue60545.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,28 +0,0 @@ --This test checks that unimported completion is case-insensitive. +-type pointerBuiltinStruct struct { +- b *bool +- s *string +- i *int +-} - ---- go.mod -- --module mod.test +-var _ = pointerBuiltinStruct{} //@codeactionedit("}", "refactor.rewrite", a33) - --go 1.18 +-var _ = []ast.BasicLit{ +- {}, //@codeactionedit("}", "refactor.rewrite", a34) +-} - ---- main.go -- --package main +-var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite", a35) +--- @a31/a3.go -- +-@@ -17 +17,4 @@ +--var _ = Bar{} //@codeactionedit("}", "refactor.rewrite", a31) +-+var _ = Bar{ +-+ X: &Foo{}, +-+ Y: &Foo{}, +-+} //@codeactionedit("}", "refactor.rewrite", a31) +--- @a32/a3.go -- +-@@ -28 +28,9 @@ +--var _ = importedStruct{} //@codeactionedit("}", "refactor.rewrite", a32) +-+var _ = importedStruct{ +-+ m: map[*ast.CompositeLit]ast.Field{}, +-+ s: []ast.BadExpr{}, +-+ a: [3]token.Token{}, +-+ c: make(chan ast.EmptyStmt), +-+ fn: func(ast_decl ast.DeclStmt) ast.Ellipsis { +-+ }, +-+ st: ast.CompositeLit{}, +-+} //@codeactionedit("}", "refactor.rewrite", a32) +--- @a33/a3.go -- +-@@ -36 +36,5 @@ +--var _ = pointerBuiltinStruct{} //@codeactionedit("}", "refactor.rewrite", a33) +-+var _ = pointerBuiltinStruct{ +-+ b: new(bool), +-+ s: new(string), +-+ i: new(int), +-+} //@codeactionedit("}", "refactor.rewrite", a33) +--- @a34/a3.go -- +-@@ -39 +39,5 @@ +-- {}, //@codeactionedit("}", "refactor.rewrite", a34) +-+ { +-+ ValuePos: 0, +-+ Kind: 0, +-+ Value: "", +-+ }, //@codeactionedit("}", "refactor.rewrite", a34) +--- @a35/a3.go -- +-@@ -42 +42,5 @@ +--var _ = []ast.BasicLit{{}} //@codeactionedit("}", "refactor.rewrite", a35) +-+var _ = []ast.BasicLit{{ +-+ ValuePos: 0, +-+ Kind: 0, +-+ Value: "", +-+}} //@codeactionedit("}", "refactor.rewrite", a35) +--- a4.go -- +-package fillstruct - --//@item(Print, "Print", "func (from \"fmt\")", "func") --//@item(Printf, "Printf", "func (from \"fmt\")", "func") --//@item(Println, "Println", "func (from \"fmt\")", "func") +-import "go/ast" - --func main() { -- fmt.p //@complete(re"fmt.p()", Print, Printf, Println), diag("fmt", re"(undefined|undeclared)") +-type iStruct struct { +- X int -} - ---- other.go -- --package main -- --// Including another package that imports "fmt" causes completion to use the --// existing metadata, which is the codepath leading to golang/go#60545. --import "fmt" -- --func _() { -- fmt.Println() +-type sStruct struct { +- str string -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/issue62141.txt b/gopls/internal/regtest/marker/testdata/completion/issue62141.txt ---- a/gopls/internal/regtest/marker/testdata/completion/issue62141.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/issue62141.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,39 +0,0 @@ --This test checks that we don't suggest completion to an untyped conversion such --as "untyped float(abcdef)". -- ---- main.go -- --package main -- --func main() { -- abcdef := 32 //@diag("abcdef", re"not used") -- x := 1.0 / abcd //@acceptcompletion(re"abcd()", "abcdef", int), diag("x", re"not used"), diag("abcd", re"(undefined|undeclared)") - -- // Verify that we don't suggest converting compatible untyped constants. -- const untypedConst = 42 -- y := 1.1 / untypedC //@acceptcompletion(re"untypedC()", "untypedConst", untyped), diag("y", re"not used"), diag("untypedC", re"(undefined|undeclared)") +-type multiFill struct { +- num int +- strin string +- arr []int -} - ---- @int/main.go -- --package main -- --func main() { -- abcdef := 32 //@diag("abcdef", re"not used") -- x := 1.0 / float64(abcdef) //@acceptcompletion(re"abcd()", "abcdef", int), diag("x", re"not used"), diag("abcd", re"(undefined|undeclared)") -- -- // Verify that we don't suggest converting compatible untyped constants. -- const untypedConst = 42 -- y := 1.1 / untypedC //@acceptcompletion(re"untypedC()", "untypedConst", untyped), diag("y", re"not used"), diag("untypedC", re"(undefined|undeclared)") +-type assignStruct struct { +- n ast.Node -} - ---- @untyped/main.go -- --package main -- --func main() { -- abcdef := 32 //@diag("abcdef", re"not used") -- x := 1.0 / abcd //@acceptcompletion(re"abcd()", "abcdef", int), diag("x", re"not used"), diag("abcd", re"(undefined|undeclared)") +-func fill() { +- var x int +- var _ = iStruct{} //@codeactionedit("}", "refactor.rewrite", a41) - -- // Verify that we don't suggest converting compatible untyped constants. -- const untypedConst = 42 -- y := 1.1 / untypedConst //@acceptcompletion(re"untypedC()", "untypedConst", untyped), diag("y", re"not used"), diag("untypedC", re"(undefined|undeclared)") --} +- var s string +- var _ = sStruct{} //@codeactionedit("}", "refactor.rewrite", a42) - -diff -urN a/gopls/internal/regtest/marker/testdata/completion/issue62560.txt b/gopls/internal/regtest/marker/testdata/completion/issue62560.txt ---- a/gopls/internal/regtest/marker/testdata/completion/issue62560.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/issue62560.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,19 +0,0 @@ --This test verifies that completion of package members in unimported packages --reflects their fuzzy score, even when those members are present in the --transitive import graph of the main module. (For technical reasons, this was --the nature of the regression in golang/go#62560.) +- var n int +- _ = []int{} +- if true { +- arr := []int{1, 2} +- } +- var _ = multiFill{} //@codeactionedit("}", "refactor.rewrite", a43) - ---- go.mod -- --module mod.test +- var node *ast.CompositeLit +- var _ = assignStruct{} //@codeactionedit("}", "refactor.rewrite", a45) +-} - ---- foo/foo.go -- --package foo +--- @a41/a4.go -- +-@@ -25 +25,3 @@ +-- var _ = iStruct{} //@codeactionedit("}", "refactor.rewrite", a41) +-+ var _ = iStruct{ +-+ X: x, +-+ } //@codeactionedit("}", "refactor.rewrite", a41) +--- @a42/a4.go -- +-@@ -28 +28,3 @@ +-- var _ = sStruct{} //@codeactionedit("}", "refactor.rewrite", a42) +-+ var _ = sStruct{ +-+ str: s, +-+ } //@codeactionedit("}", "refactor.rewrite", a42) +--- @a43/a4.go -- +-@@ -35 +35,5 @@ +-- var _ = multiFill{} //@codeactionedit("}", "refactor.rewrite", a43) +-+ var _ = multiFill{ +-+ num: n, +-+ strin: s, +-+ arr: []int{}, +-+ } //@codeactionedit("}", "refactor.rewrite", a43) +--- @a45/a4.go -- +-@@ -38 +38,3 @@ +-- var _ = assignStruct{} //@codeactionedit("}", "refactor.rewrite", a45) +-+ var _ = assignStruct{ +-+ n: node, +-+ } //@codeactionedit("}", "refactor.rewrite", a45) +--- fill_struct.go -- +-package fillstruct - --func _() { -- json.U //@rankl(re"U()", "Unmarshal", "InvalidUTF8Error"), diag("json", re"(undefined|undeclared)") +-type StructA struct { +- unexportedIntField int +- ExportedIntField int +- MapA map[int]string +- Array []int +- StructB -} - ---- bar/bar.go -- --package bar -- --import _ "encoding/json" -diff -urN a/gopls/internal/regtest/marker/testdata/completion/issue62676.txt b/gopls/internal/regtest/marker/testdata/completion/issue62676.txt ---- a/gopls/internal/regtest/marker/testdata/completion/issue62676.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/issue62676.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,63 +0,0 @@ --This test verifies that unimported completion respects the usePlaceholders setting. +-type StructA2 struct { +- B *StructB +-} - ---- flags -- ---ignore_extra_diags +-type StructA3 struct { +- B StructB +-} - ---- settings.json -- --{ -- "usePlaceholders": false +-func fill() { +- a := StructA{} //@codeactionedit("}", "refactor.rewrite", fill_struct1) +- b := StructA2{} //@codeactionedit("}", "refactor.rewrite", fill_struct2) +- c := StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct3) +- if true { +- _ = StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct4) +- } -} - ---- go.mod -- --module mod.test +--- @fill_struct1/fill_struct.go -- +-@@ -20 +20,7 @@ +-- a := StructA{} //@codeactionedit("}", "refactor.rewrite", fill_struct1) +-+ a := StructA{ +-+ unexportedIntField: 0, +-+ ExportedIntField: 0, +-+ MapA: map[int]string{}, +-+ Array: []int{}, +-+ StructB: StructB{}, +-+ } //@codeactionedit("}", "refactor.rewrite", fill_struct1) +--- @fill_struct2/fill_struct.go -- +-@@ -21 +21,3 @@ +-- b := StructA2{} //@codeactionedit("}", "refactor.rewrite", fill_struct2) +-+ b := StructA2{ +-+ B: &StructB{}, +-+ } //@codeactionedit("}", "refactor.rewrite", fill_struct2) +--- @fill_struct3/fill_struct.go -- +-@@ -22 +22,3 @@ +-- c := StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct3) +-+ c := StructA3{ +-+ B: StructB{}, +-+ } //@codeactionedit("}", "refactor.rewrite", fill_struct3) +--- @fill_struct4/fill_struct.go -- +-@@ -24 +24,3 @@ +-- _ = StructA3{} //@codeactionedit("}", "refactor.rewrite", fill_struct4) +-+ _ = StructA3{ +-+ B: StructB{}, +-+ } //@codeactionedit("}", "refactor.rewrite", fill_struct4) +--- fill_struct_anon.go -- +-package fillstruct - --go 1.21 +-type StructAnon struct { +- a struct{} +- b map[string]interface{} +- c map[string]struct { +- d int +- e bool +- } +-} - ---- foo/foo.go -- --package foo +-func fill() { +- _ := StructAnon{} //@codeactionedit("}", "refactor.rewrite", fill_struct_anon) +-} +--- @fill_struct_anon/fill_struct_anon.go -- +-@@ -13 +13,5 @@ +-- _ := StructAnon{} //@codeactionedit("}", "refactor.rewrite", fill_struct_anon) +-+ _ := StructAnon{ +-+ a: struct{}{}, +-+ b: map[string]interface{}{}, +-+ c: map[string]struct{d int; e bool}{}, +-+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_anon) +--- fill_struct_nested.go -- +-package fillstruct - --func _() { -- // This uses goimports-based completion; TODO: this should insert snippets. -- os.Open //@acceptcompletion(re"Open()", "Open", open) +-type StructB struct { +- StructC -} - --func _() { -- // This uses metadata-based completion. -- errors.New //@acceptcompletion(re"New()", "New", new) +-type StructC struct { +- unexportedInt int -} - ---- bar/bar.go -- --package bar +-func nested() { +- c := StructB{ +- StructC: StructC{}, //@codeactionedit("}", "refactor.rewrite", fill_nested) +- } +-} - --import _ "errors" // important: doesn't transitively import os. +--- @fill_nested/fill_struct_nested.go -- +-@@ -13 +13,3 @@ +-- StructC: StructC{}, //@codeactionedit("}", "refactor.rewrite", fill_nested) +-+ StructC: StructC{ +-+ unexportedInt: 0, +-+ }, //@codeactionedit("}", "refactor.rewrite", fill_nested) +--- fill_struct_package.go -- +-package fillstruct - ---- @new/foo/foo.go -- --package foo +-import ( +- h2 "net/http" - --import "errors" +- "golang.org/lsptests/fillstruct/data" +-) - --func _() { -- // This uses goimports-based completion; TODO: this should insert snippets. -- os.Open //@acceptcompletion(re"Open()", "Open", open) +-func unexported() { +- a := data.B{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package1) +- _ = h2.Client{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package2) -} +--- @fill_struct_package1/fill_struct_package.go -- +-@@ -10 +10,3 @@ +-- a := data.B{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package1) +-+ a := data.B{ +-+ ExportedInt: 0, +-+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_package1) +--- @fill_struct_package2/fill_struct_package.go -- +-@@ -11 +11,7 @@ +-- _ = h2.Client{} //@codeactionedit("}", "refactor.rewrite", fill_struct_package2) +-+ _ = h2.Client{ +-+ Transport: nil, +-+ CheckRedirect: func(req *h2.Request, via []*h2.Request) error { +-+ }, +-+ Jar: nil, +-+ Timeout: 0, +-+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_package2) +--- fill_struct_partial.go -- +-package fillstruct - --func _() { -- // This uses metadata-based completion. -- errors.New(${1:}) //@acceptcompletion(re"New()", "New", new) +-type StructPartialA struct { +- PrefilledInt int +- UnfilledInt int +- StructPartialB -} - ---- @open/foo/foo.go -- --package foo -- --import "os" -- --func _() { -- // This uses goimports-based completion; TODO: this should insert snippets. -- os.Open //@acceptcompletion(re"Open()", "Open", open) +-type StructPartialB struct { +- PrefilledInt int +- UnfilledInt int -} - --func _() { -- // This uses metadata-based completion. -- errors.New //@acceptcompletion(re"New()", "New", new) +-func fill() { +- a := StructPartialA{ +- PrefilledInt: 5, +- } //@codeactionedit("}", "refactor.rewrite", fill_struct_partial1) +- b := StructPartialB{ +- /* this comment should disappear */ +- PrefilledInt: 7, // This comment should be blown away. +- /* As should +- this one */ +- } //@codeactionedit("}", "refactor.rewrite", fill_struct_partial2) -} - -diff -urN a/gopls/internal/regtest/marker/testdata/completion/keywords.txt b/gopls/internal/regtest/marker/testdata/completion/keywords.txt ---- a/gopls/internal/regtest/marker/testdata/completion/keywords.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/keywords.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,166 +0,0 @@ --This test checks completion of Go keywords. -- ---- flags -- ---ignore_extra_diags ---filter_keywords=false +--- @fill_struct_partial1/fill_struct_partial.go -- +-@@ -16 +16,3 @@ +-- PrefilledInt: 5, +-+ PrefilledInt: 5, +-+ UnfilledInt: 0, +-+ StructPartialB: StructPartialB{}, +--- @fill_struct_partial2/fill_struct_partial.go -- +-@@ -19,4 +19,2 @@ +-- /* this comment should disappear */ +-- PrefilledInt: 7, // This comment should be blown away. +-- /* As should +-- this one */ +-+ PrefilledInt: 7, +-+ UnfilledInt: 0, +--- fill_struct_spaces.go -- +-package fillstruct - ---- settings.json -- --{ -- "completeUnimported": false, -- "matcher": "caseInsensitive", -- "experimentalPostfixCompletions": false +-type StructD struct { +- ExportedIntField int -} - ---- keywords.go -- --package keywords -- --//@rank("", type),rank("", func),rank("", var),rank("", const),rank("", import) -- --func _() { -- var test int //@rank(" //", int, interface) -- var tChan chan int -- var _ m //@complete(" //", map) -- var _ f //@complete(" //", func) -- var _ c //@complete(" //", chan) -- -- var _ str //@rank(" //", string, struct) -- -- type _ int //@rank(" //", interface, int) +-func spaces() { +- d := StructD{} //@codeactionedit("}", "refactor.rewrite", fill_struct_spaces) +-} - -- type _ str //@rank(" //", struct, string) +--- @fill_struct_spaces/fill_struct_spaces.go -- +-@@ -8 +8,3 @@ +-- d := StructD{} //@codeactionedit("}", "refactor.rewrite", fill_struct_spaces) +-+ d := StructD{ +-+ ExportedIntField: 0, +-+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_spaces) +--- fill_struct_unsafe.go -- +-package fillstruct - -- switch test { -- case 1: // TODO: trying to complete case here will break because the parser won't return *ast.Ident -- b //@complete(" //", break) -- case 2: -- f //@complete(" //", fallthrough, for) -- r //@complete(" //", return) -- d //@complete(" //", default, defer) -- c //@complete(" //", case, const) -- } +-import "unsafe" - -- switch test.(type) { -- case fo: //@complete(":") -- case int: -- b //@complete(" //", break) -- case int32: -- f //@complete(" //", for) -- d //@complete(" //", default, defer) -- r //@complete(" //", return) -- c //@complete(" //", case, const) -- } +-type unsafeStruct struct { +- x int +- p unsafe.Pointer +-} - -- select { -- case <-tChan: -- b //@complete(" //", break) -- c //@complete(" //", case, const) -- } +-func fill() { +- _ := unsafeStruct{} //@codeactionedit("}", "refactor.rewrite", fill_struct_unsafe) +-} - -- for index := 0; index < test; index++ { -- c //@complete(" //", const, continue) -- b //@complete(" //", break) -- } +--- @fill_struct_unsafe/fill_struct_unsafe.go -- +-@@ -11 +11,4 @@ +-- _ := unsafeStruct{} //@codeactionedit("}", "refactor.rewrite", fill_struct_unsafe) +-+ _ := unsafeStruct{ +-+ x: 0, +-+ p: nil, +-+ } //@codeactionedit("}", "refactor.rewrite", fill_struct_unsafe) +--- typeparams.go -- +-package fillstruct - -- for range []int{} { -- c //@complete(" //", const, continue) -- b //@complete(" //", break) -- } +-type emptyStructWithTypeParams[A any] struct{} - -- // Test function level keywords +-var _ = emptyStructWithTypeParams[int]{} // no suggested fix - -- //Using 2 characters to test because map output order is random -- sw //@complete(" //", switch) -- se //@complete(" //", select) +-type basicStructWithTypeParams[T any] struct { +- foo T +-} - -- f //@complete(" //", for) -- d //@complete(" //", defer) -- g //@rank(" //", go),rank(" //", goto) -- r //@complete(" //", return) -- i //@complete(" //", if) -- e //@complete(" //", else) -- v //@complete(" //", var) -- c //@complete(" //", const) +-var _ = basicStructWithTypeParams[int]{} //@codeactionedit("}", "refactor.rewrite", typeparams1) - -- for i := r //@complete(" //", range) +-type twoArgStructWithTypeParams[F, B any] struct { +- foo F +- bar B -} - --/* package */ //@item(package, "package", "", "keyword") --/* import */ //@item(import, "import", "", "keyword") --/* func */ //@item(func, "func", "", "keyword") --/* type */ //@item(type, "type", "", "keyword") --/* var */ //@item(var, "var", "", "keyword") --/* const */ //@item(const, "const", "", "keyword") --/* break */ //@item(break, "break", "", "keyword") --/* default */ //@item(default, "default", "", "keyword") --/* case */ //@item(case, "case", "", "keyword") --/* defer */ //@item(defer, "defer", "", "keyword") --/* go */ //@item(go, "go", "", "keyword") --/* for */ //@item(for, "for", "", "keyword") --/* if */ //@item(if, "if", "", "keyword") --/* else */ //@item(else, "else", "", "keyword") --/* switch */ //@item(switch, "switch", "", "keyword") --/* select */ //@item(select, "select", "", "keyword") --/* fallthrough */ //@item(fallthrough, "fallthrough", "", "keyword") --/* continue */ //@item(continue, "continue", "", "keyword") --/* return */ //@item(return, "return", "", "keyword") --/* goto */ //@item(goto, "goto", "", "keyword") --/* struct */ //@item(struct, "struct", "", "keyword") --/* interface */ //@item(interface, "interface", "", "keyword") --/* map */ //@item(map, "map", "", "keyword") --/* chan */ //@item(chan, "chan", "", "keyword") --/* range */ //@item(range, "range", "", "keyword") --/* string */ //@item(string, "string", "", "type") --/* int */ //@item(int, "int", "", "type") -- ---- accidental_keywords.go -- --package keywords +-var _ = twoArgStructWithTypeParams[string, int]{} //@codeactionedit("}", "refactor.rewrite", typeparams2) - --// non-matching candidate - shouldn't show up as completion --var apple = "apple" +-var _ = twoArgStructWithTypeParams[int, string]{ +- bar: "bar", +-} //@codeactionedit("}", "refactor.rewrite", typeparams3) - --func _() { -- foo.bar() // insert some extra statements to exercise our AST surgery -- variance := 123 //@item(kwVariance, "variance", "int", "var") -- foo.bar() -- println(var) //@complete(")", kwVariance) +-type nestedStructWithTypeParams struct { +- bar string +- basic basicStructWithTypeParams[int] -} - --func _() { -- foo.bar() -- var s struct { variance int } //@item(kwVarianceField, "variance", "int", "field") -- foo.bar() -- s.var //@complete(" //", kwVarianceField) --} +-var _ = nestedStructWithTypeParams{} //@codeactionedit("}", "refactor.rewrite", typeparams4) - --func _() { -- channel := 123 //@item(kwChannel, "channel", "int", "var") -- chan //@complete(" //", kwChannel) -- foo.bar() +-func _[T any]() { +- type S struct{ t T } +- _ = S{} //@codeactionedit("}", "refactor.rewrite", typeparams5) -} +--- @typeparams1/typeparams.go -- +-@@ -11 +11,3 @@ +--var _ = basicStructWithTypeParams[int]{} //@codeactionedit("}", "refactor.rewrite", typeparams1) +-+var _ = basicStructWithTypeParams[int]{ +-+ foo: 0, +-+} //@codeactionedit("}", "refactor.rewrite", typeparams1) +--- @typeparams2/typeparams.go -- +-@@ -18 +18,4 @@ +--var _ = twoArgStructWithTypeParams[string, int]{} //@codeactionedit("}", "refactor.rewrite", typeparams2) +-+var _ = twoArgStructWithTypeParams[string, int]{ +-+ foo: "", +-+ bar: 0, +-+} //@codeactionedit("}", "refactor.rewrite", typeparams2) +--- @typeparams3/typeparams.go -- +-@@ -21 +21 @@ +-+ foo: 0, +--- @typeparams4/typeparams.go -- +-@@ -29 +29,4 @@ +--var _ = nestedStructWithTypeParams{} //@codeactionedit("}", "refactor.rewrite", typeparams4) +-+var _ = nestedStructWithTypeParams{ +-+ bar: "", +-+ basic: basicStructWithTypeParams{}, +-+} //@codeactionedit("}", "refactor.rewrite", typeparams4) +--- @typeparams5/typeparams.go -- +-@@ -33 +33,3 @@ +-- _ = S{} //@codeactionedit("}", "refactor.rewrite", typeparams5) +-+ _ = S{ +-+ t: *new(T), +-+ } //@codeactionedit("}", "refactor.rewrite", typeparams5) +--- issue63921.go -- +-package fillstruct - --func _() { -- foo.bar() -- var typeName string //@item(kwTypeName, "typeName", "string", "var") -- foo.bar() -- type //@complete(" //", kwTypeName) +-// Test for golang/go#63921: fillstruct panicked with invalid fields. +-type invalidStruct struct { +- F int +- Undefined -} ---- empty_select.go -- --package keywords - -func _() { -- select { -- c //@complete(" //", case) -- } +- // Note: the golden content for issue63921 is empty: fillstruct produces no +- // edits, but does not panic. +- invalidStruct{} //@codeactionedit("}", "refactor.rewrite", issue63921) -} ---- empty_switch.go -- --package keywords -- --func _() { -- switch { -- //@complete("", case, default) -- } +diff -urN a/gopls/internal/test/marker/testdata/codeaction/fill_switch_resolve.txt b/gopls/internal/test/marker/testdata/codeaction/fill_switch_resolve.txt +--- a/gopls/internal/test/marker/testdata/codeaction/fill_switch_resolve.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codeaction/fill_switch_resolve.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,116 +0,0 @@ +-This test checks the behavior of the 'fill switch' code action, with resolve support. +-See fill_switch.txt for same test without resolve support. - -- switch test.(type) { -- d //@complete(" //", default) +--- capabilities.json -- +-{ +- "textDocument": { +- "codeAction": { +- "dataSupport": true, +- "resolveSupport": { +- "properties": ["edit"] +- } +- } - } -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/labels.txt b/gopls/internal/regtest/marker/testdata/completion/labels.txt ---- a/gopls/internal/regtest/marker/testdata/completion/labels.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/labels.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,55 +0,0 @@ --This test checks completion of labels. -- --- flags -- --ignore_extra_diags - ---- labels.go -- --package labels +--- go.mod -- +-module golang.org/lsptests/fillswitch - --func _() { -- goto F //@complete(" //", label1, label5) +-go 1.18 - --Foo1: //@item(label1, "Foo1", "label", "const") -- for a, b := range []int{} { -- Foo2: //@item(label2, "Foo2", "label", "const") -- switch { -- case true: -- break F //@complete(" //", label2, label1) +--- data/data.go -- +-package data - -- continue F //@complete(" //", label1) +-type TypeB int - -- { -- FooUnjumpable: -- } +-const ( +- TypeBOne TypeB = iota +- TypeBTwo +- TypeBThree +-) - -- goto F //@complete(" //", label1, label2, label4, label5) +--- a.go -- +-package fillswitch - -- func() { -- goto F //@complete(" //", label3) +-import ( +- "golang.org/lsptests/fillswitch/data" +-) - -- break F //@complete(" //") +-type typeA int - -- continue F //@complete(" //") +-const ( +- typeAOne typeA = iota +- typeATwo +- typeAThree +-) - -- Foo3: //@item(label3, "Foo3", "label", "const") -- }() -- } +-type notification interface { +- isNotification() +-} - -- Foo4: //@item(label4, "Foo4", "label", "const") -- switch interface{}(a).(type) { -- case int: -- break F //@complete(" //", label4, label1) -- } +-type notificationOne struct{} +- +-func (notificationOne) isNotification() {} +- +-type notificationTwo struct{} +- +-func (notificationTwo) isNotification() {} +- +-func doSwitch() { +- var b data.TypeB +- switch b { +- case data.TypeBOne: //@codeactionedit(":", "refactor.rewrite", a1) - } - -- break F //@complete(" //") +- var a typeA +- switch a { +- case typeAThree: //@codeactionedit(":", "refactor.rewrite", a2) +- } - -- continue F //@complete(" //") +- var n notification +- switch n.(type) { //@codeactionedit("{", "refactor.rewrite", a3) +- } - --Foo5: //@item(label5, "Foo5", "label", "const") -- for { -- break F //@complete(" //", label5) +- switch nt := n.(type) { //@codeactionedit("{", "refactor.rewrite", a4) - } - -- return +- var s struct { +- a typeA +- } +- +- switch s.a { +- case typeAThree: //@codeactionedit(":", "refactor.rewrite", a5) +- } -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/lit.txt b/gopls/internal/regtest/marker/testdata/completion/lit.txt ---- a/gopls/internal/regtest/marker/testdata/completion/lit.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/lit.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,49 +0,0 @@ +--- @a1/a.go -- +-@@ -31 +31,4 @@ +-+ case data.TypeBThree: +-+ case data.TypeBTwo: +-+ default: +-+ panic(fmt.Sprintf("unexpected data.TypeB: %#v", b)) +--- @a2/a.go -- +-@@ -36 +36,4 @@ +-+ case typeAOne: +-+ case typeATwo: +-+ default: +-+ panic(fmt.Sprintf("unexpected fillswitch.typeA: %#v", a)) +--- @a3/a.go -- +-@@ -40 +40,4 @@ +-+ case notificationOne: +-+ case notificationTwo: +-+ default: +-+ panic(fmt.Sprintf("unexpected fillswitch.notification: %#v", n)) +--- @a4/a.go -- +-@@ -43 +43,4 @@ +-+ case notificationOne: +-+ case notificationTwo: +-+ default: +-+ panic(fmt.Sprintf("unexpected fillswitch.notification: %#v", nt)) +--- @a5/a.go -- +-@@ -51 +51,4 @@ +-+ case typeAOne: +-+ case typeATwo: +-+ default: +-+ panic(fmt.Sprintf("unexpected fillswitch.typeA: %#v", s.a)) +diff -urN a/gopls/internal/test/marker/testdata/codeaction/fill_switch.txt b/gopls/internal/test/marker/testdata/codeaction/fill_switch.txt +--- a/gopls/internal/test/marker/testdata/codeaction/fill_switch.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codeaction/fill_switch.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,105 +0,0 @@ +-This test checks the behavior of the 'fill switch' code action. +-See fill_switch_resolve.txt for same test with resolve support. - --- flags -- --ignore_extra_diags - --- go.mod -- --module mod.test +-module golang.org/lsptests/fillswitch - -go 1.18 - ---- foo/foo.go -- --package foo -- --type StructFoo struct{ F int } -- ---- a.go -- --package a -- --import "mod.test/foo" -- --func _() { -- StructFoo{} //@item(litStructFoo, "StructFoo{}", "struct{...}", "struct") +--- data/data.go -- +-package data - -- var sfp *foo.StructFoo -- // Don't insert the "&" before "StructFoo{}". -- sfp = foo.Str //@snippet(" //", litStructFoo, "StructFoo{$0\\}") +-type TypeB int - -- var sf foo.StructFoo -- sf = foo.Str //@snippet(" //", litStructFoo, "StructFoo{$0\\}") -- sf = foo. //@snippet(" //", litStructFoo, "StructFoo{$0\\}") --} +-const ( +- TypeBOne TypeB = iota +- TypeBTwo +- TypeBThree +-) - ---- http.go -- --package a +--- a.go -- +-package fillswitch - -import ( -- "net/http" -- "sort" +- "golang.org/lsptests/fillswitch/data" -) - --func _() { -- sort.Slice(nil, fun) //@snippet(")", litFunc, "func(i, j int) bool {$0\\}") +-type typeA int - -- http.HandleFunc("", f) //@snippet(")", litFunc, "func(w http.ResponseWriter, r *http.Request) {$0\\}") +-const ( +- typeAOne typeA = iota +- typeATwo +- typeAThree +-) - -- //@item(litFunc, "func(...) {}", "", "var") -- http.HandlerFunc() //@item(handlerFunc, "http.HandlerFunc()", "", "var") -- http.Handle("", http.HandlerFunc()) //@snippet("))", litFunc, "func(w http.ResponseWriter, r *http.Request) {$0\\}") -- http.Handle("", h) //@snippet(")", handlerFunc, "http.HandlerFunc($0)") +-type notification interface { +- isNotification() -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/maps.txt b/gopls/internal/regtest/marker/testdata/completion/maps.txt ---- a/gopls/internal/regtest/marker/testdata/completion/maps.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/maps.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,29 +0,0 @@ --This test checks completion of map keys and values. - ---- flags -- ---ignore_extra_diags +-type notificationOne struct{} - ---- settings.json -- --{ -- "completeUnimported": false --} +-func (notificationOne) isNotification() {} - ---- maps.go -- --package maps +-type notificationTwo struct{} - --func _() { -- var aVar int //@item(mapVar, "aVar", "int", "var") +-func (notificationTwo) isNotification() {} - -- // not comparabale -- type aSlice []int //@item(mapSliceType, "aSlice", "[]int", "type") +-func doSwitch() { +- var b data.TypeB +- switch b { +- case data.TypeBOne: //@codeactionedit(":", "refactor.rewrite", a1) +- } - -- *aSlice //@item(mapSliceTypePtr, "*aSlice", "[]int", "type") +- var a typeA +- switch a { +- case typeAThree: //@codeactionedit(":", "refactor.rewrite", a2) +- } - -- // comparable -- type aStruct struct{} //@item(mapStructType, "aStruct", "struct{...}", "struct") +- var n notification +- switch n.(type) { //@codeactionedit("{", "refactor.rewrite", a3) +- } - -- map[]a{} //@complete("]", mapSliceType, mapStructType),snippet("]", mapSliceType, "*aSlice") +- switch nt := n.(type) { //@codeactionedit("{", "refactor.rewrite", a4) +- } - -- map[a]a{} //@complete("]", mapSliceType, mapStructType) -- map[a]a{} //@complete("{", mapSliceType, mapStructType) +- var s struct { +- a typeA +- } +- +- switch s.a { +- case typeAThree: //@codeactionedit(":", "refactor.rewrite", a5) +- } -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/multi_return.txt b/gopls/internal/regtest/marker/testdata/completion/multi_return.txt ---- a/gopls/internal/regtest/marker/testdata/completion/multi_return.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/multi_return.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,55 +0,0 @@ --This test checks various ranking of completion results related to functions --with multiple return values. +--- @a1/a.go -- +-@@ -31 +31,4 @@ +-+ case data.TypeBThree: +-+ case data.TypeBTwo: +-+ default: +-+ panic(fmt.Sprintf("unexpected data.TypeB: %#v", b)) +--- @a2/a.go -- +-@@ -36 +36,4 @@ +-+ case typeAOne: +-+ case typeATwo: +-+ default: +-+ panic(fmt.Sprintf("unexpected fillswitch.typeA: %#v", a)) +--- @a3/a.go -- +-@@ -40 +40,4 @@ +-+ case notificationOne: +-+ case notificationTwo: +-+ default: +-+ panic(fmt.Sprintf("unexpected fillswitch.notification: %#v", n)) +--- @a4/a.go -- +-@@ -43 +43,4 @@ +-+ case notificationOne: +-+ case notificationTwo: +-+ default: +-+ panic(fmt.Sprintf("unexpected fillswitch.notification: %#v", nt)) +--- @a5/a.go -- +-@@ -51 +51,4 @@ +-+ case typeAOne: +-+ case typeATwo: +-+ default: +-+ panic(fmt.Sprintf("unexpected fillswitch.typeA: %#v", s.a)) +diff -urN a/gopls/internal/test/marker/testdata/codeaction/functionextraction_issue44813.txt b/gopls/internal/test/marker/testdata/codeaction/functionextraction_issue44813.txt +--- a/gopls/internal/test/marker/testdata/codeaction/functionextraction_issue44813.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codeaction/functionextraction_issue44813.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,42 +0,0 @@ +-This test verifies the fix for golang/go#44813: extraction failure when there +-are blank identifiers. - ---- flags -- ---ignore_extra_diags +--- go.mod -- +-module mod.test/extract - ---- multireturn.go -- --package multireturn +-go 1.18 - --func f0() {} //@item(multiF0, "f0", "func()", "func") +--- p.go -- +-package extract - --func f1(int) int { return 0 } //@item(multiF1, "f1", "func(int) int", "func") +-import "fmt" - --func f2(int, int) (int, int) { return 0, 0 } //@item(multiF2, "f2", "func(int, int) (int, int)", "func") +-func main() { +- x := []rune{} //@codeaction("x", end, "refactor.extract", ext) +- s := "HELLO" +- for _, c := range s { +- x = append(x, c) +- } //@loc(end, "}") +- fmt.Printf("%x\n", x) +-} - --func f2Str(string, string) (string, string) { return "", "" } //@item(multiF2Str, "f2Str", "func(string, string) (string, string)", "func") +--- @ext/p.go -- +-package extract - --func f3(int, int, int) (int, int, int) { return 0, 0, 0 } //@item(multiF3, "f3", "func(int, int, int) (int, int, int)", "func") +-import "fmt" - --func _() { -- _ := f //@rank(" //", multiF1, multiF2) +-func main() { +- //@codeaction("x", end, "refactor.extract", ext) +- x := newFunction() //@loc(end, "}") +- fmt.Printf("%x\n", x) +-} - -- _, _ := f //@rank(" //", multiF2, multiF0),rank(" //", multiF1, multiF0) +-func newFunction() []rune { +- x := []rune{} +- s := "HELLO" +- for _, c := range s { +- x = append(x, c) +- } +- return x +-} - -- _, _ := _, f //@rank(" //", multiF1, multiF2),rank(" //", multiF1, multiF0) +diff -urN a/gopls/internal/test/marker/testdata/codeaction/functionextraction.txt b/gopls/internal/test/marker/testdata/codeaction/functionextraction.txt +--- a/gopls/internal/test/marker/testdata/codeaction/functionextraction.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codeaction/functionextraction.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,583 +0,0 @@ +-This test verifies various behaviors of function extraction. - -- _, _ := f, abc //@rank(", abc", multiF1, multiF2) +--- go.mod -- +-module mod.test/extract - -- f1() //@rank(")", multiF1, multiF0) -- f1(f) //@rank(")", multiF1, multiF2) -- f2(f) //@rank(")", multiF2, multiF3),rank(")", multiF1, multiF3) -- f2(1, f) //@rank(")", multiF1, multiF2),rank(")", multiF1, multiF0) -- f2(1, ) //@rank(")", multiF1, multiF2),rank(")", multiF1, multiF0) -- f2Str() //@rank(")", multiF2Str, multiF2) +-go 1.18 - -- var i int -- i, _ := f //@rank(" //", multiF2, multiF2Str) +--- basic.go -- +-package extract - -- var s string -- _, s := f //@rank(" //", multiF2Str, multiF2) +-func _() { //@codeaction("{", closeBracket, "refactor.extract", outer) +- a := 1 //@codeaction("a", end, "refactor.extract", inner) +- _ = a + 4 //@loc(end, "4") +-} //@loc(closeBracket, "}") - -- banana, s = f //@rank(" //", multiF2, multiF3) +--- @inner/basic.go -- +-package extract - -- var variadic func(int, ...int) -- variadic() //@rank(")", multiF1, multiF0),rank(")", multiF2, multiF0),rank(")", multiF3, multiF0) +-func _() { //@codeaction("{", closeBracket, "refactor.extract", outer) +- //@codeaction("a", end, "refactor.extract", inner) +- newFunction() //@loc(end, "4") -} - --func _() { -- var baz func(...interface{}) +-func newFunction() { +- a := 1 +- _ = a + 4 +-} //@loc(closeBracket, "}") - -- var otterNap func() (int, int) //@item(multiTwo, "otterNap", "func() (int, int)", "var") -- var one int //@item(multiOne, "one", "int", "var") +--- @outer/basic.go -- +-package extract - -- baz(on) //@rank(")", multiOne, multiTwo) +-func _() { //@codeaction("{", closeBracket, "refactor.extract", outer) +- //@codeaction("a", end, "refactor.extract", inner) +- newFunction() //@loc(end, "4") -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/nested_complit.txt b/gopls/internal/regtest/marker/testdata/completion/nested_complit.txt ---- a/gopls/internal/regtest/marker/testdata/completion/nested_complit.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/nested_complit.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,23 +0,0 @@ --This test checks completion of nested composite literals; - --TODO(rfindley): investigate an un-skip the disabled test below. +-func newFunction() { +- a := 1 +- _ = a + 4 +-} //@loc(closeBracket, "}") - ---- flags -- ---ignore_extra_diags +--- return.go -- +-package extract - ---- nested_complit.go -- --package nested_complit +-func _() bool { +- x := 1 +- if x == 0 { //@codeaction("if", ifend, "refactor.extract", return) +- return true +- } //@loc(ifend, "}") +- return false +-} - --type ncFoo struct {} //@item(structNCFoo, "ncFoo", "struct{...}", "struct") +--- @return/return.go -- +-package extract - --type ncBar struct { //@item(structNCBar, "ncBar", "struct{...}", "struct") -- baz []ncFoo +-func _() bool { +- x := 1 +- //@codeaction("if", ifend, "refactor.extract", return) +- shouldReturn, returnValue := newFunction(x) +- if shouldReturn { +- return returnValue +- } //@loc(ifend, "}") +- return false -} - --func _() { -- []ncFoo{} //@item(litNCFoo, "[]ncFoo{}", "", "var") -- _ := ncBar{ -- // disabled - see issue #54822 -- baz: [] // complete(" //", structNCFoo, structNCBar) +-func newFunction(x int) (bool, bool) { +- if x == 0 { +- return true, true - } +- return false, false -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/postfix.txt b/gopls/internal/regtest/marker/testdata/completion/postfix.txt ---- a/gopls/internal/regtest/marker/testdata/completion/postfix.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/postfix.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,51 +0,0 @@ --These tests check that postfix completions do and do not show up in certain --cases. Tests for the postfix completion contents are implemented as ad-hoc --regtests. -- ---- flags -- ---ignore_extra_diags -- ---- go.mod -- --module golang.org/lsptests/snippets -- --go 1.18 -- ---- postfix.go -- --package snippets -- --func _() { -- var foo []int -- foo.append //@rank(" //", postfixAppend) -- -- []int{}.append //@complete(" //") -- -- []int{}.last //@complete(" //") -- -- /* copy! */ //@item(postfixCopy, "copy!", "duplicate slice", "snippet") -- -- foo.copy //@rank(" //", postfixCopy) -- -- var s struct{ i []int } -- s.i.copy //@rank(" //", postfixCopy) - -- var _ []int = s.i.copy //@complete(" //") +--- return_nonnested.go -- +-package extract - -- var blah func() []int -- blah().append //@complete(" //") +-func _() bool { +- x := 1 //@codeaction("x", rnnEnd, "refactor.extract", rnn) +- if x == 0 { +- return true +- } +- return false //@loc(rnnEnd, "false") -} - --func _() { -- /* append! */ //@item(postfixAppend, "append!", "append and re-assign slice", "snippet") -- /* last! */ //@item(postfixLast, "last!", "s[len(s)-1]", "snippet") -- /* print! */ //@item(postfixPrint, "print!", "print to stdout", "snippet") -- /* range! */ //@item(postfixRange, "range!", "range over slice", "snippet") -- /* reverse! */ //@item(postfixReverse, "reverse!", "reverse slice", "snippet") -- /* sort! */ //@item(postfixSort, "sort!", "sort.Slice()", "snippet") -- /* var! */ //@item(postfixVar, "var!", "assign to variable", "snippet") -- /* ifnotnil! */ //@item(postfixIfNotNil, "ifnotnil!", "if expr != nil", "snippet") -- -- var foo []int -- foo. //@complete(" //", postfixAppend, postfixCopy, postfixIfNotNil, postfixLast, postfixPrint, postfixRange, postfixReverse, postfixSort, postfixVar) +--- @rnn/return_nonnested.go -- +-package extract - -- foo = nil +-func _() bool { +- //@codeaction("x", rnnEnd, "refactor.extract", rnn) +- return newFunction() //@loc(rnnEnd, "false") -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/printf.txt b/gopls/internal/regtest/marker/testdata/completion/printf.txt ---- a/gopls/internal/regtest/marker/testdata/completion/printf.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/printf.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,39 +0,0 @@ --This test checks various ranking of completion results related to printf. - ---- flags -- ---ignore_extra_diags +-func newFunction() bool { +- x := 1 +- if x == 0 { +- return true +- } +- return false +-} - ---- printf.go -- --package printf +--- return_complex.go -- +-package extract - -import "fmt" - --func myPrintf(string, ...interface{}) {} -- --func _() { -- var ( -- aInt int //@item(printfInt, "aInt", "int", "var") -- aFloat float64 //@item(printfFloat, "aFloat", "float64", "var") -- aString string //@item(printfString, "aString", "string", "var") -- aBytes []byte //@item(printfBytes, "aBytes", "[]byte", "var") -- aStringer fmt.Stringer //@item(printfStringer, "aStringer", "fmt.Stringer", "var") -- aError error //@item(printfError, "aError", "error", "var") -- aBool bool //@item(printfBool, "aBool", "bool", "var") -- ) -- -- myPrintf("%d", a) //@rank(")", printfInt, printfFloat) -- myPrintf("%s", a) //@rank(")", printfString, printfInt),rank(")", printfBytes, printfInt),rank(")", printfStringer, printfInt),rank(")", printfError, printfInt) -- myPrintf("%w", a) //@rank(")", printfError, printfInt) -- myPrintf("%x %[1]b", a) //@rank(")", printfInt, printfString) +-func _() (int, string, error) { +- x := 1 +- y := "hello" +- z := "bye" //@codeaction("z", rcEnd, "refactor.extract", rc) +- if y == z { +- return x, y, fmt.Errorf("same") +- } else if false { +- z = "hi" +- return x, z, nil +- } //@loc(rcEnd, "}") +- return x, z, nil +-} - -- fmt.Printf("%t", a) //@rank(")", printfBool, printfInt) +--- @rc/return_complex.go -- +-package extract - -- fmt.Fprintf(nil, "%f", a) //@rank(")", printfFloat, printfInt) +-import "fmt" - -- fmt.Sprintf("%[2]q %[1]*.[3]*[4]f", -- a, //@rank(",", printfInt, printfFloat) -- a, //@rank(",", printfString, printfFloat) -- a, //@rank(",", printfInt, printfFloat) -- a, //@rank(",", printfFloat, printfInt) -- ) +-func _() (int, string, error) { +- x := 1 +- y := "hello" +- //@codeaction("z", rcEnd, "refactor.extract", rc) +- z, shouldReturn, returnValue, returnValue1, returnValue2 := newFunction(y, x) +- if shouldReturn { +- return returnValue, returnValue1, returnValue2 +- } //@loc(rcEnd, "}") +- return x, z, nil -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/rank.txt b/gopls/internal/regtest/marker/testdata/completion/rank.txt ---- a/gopls/internal/regtest/marker/testdata/completion/rank.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/rank.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,212 +0,0 @@ --This test checks various ranking of completion results. -- ---- flags -- ---ignore_extra_diags - ---- settings.json -- --{ -- "completeUnimported": false, -- "deepCompletion": false +-func newFunction(y string, x int) (string, bool, int, string, error) { +- z := "bye" +- if y == z { +- return "", true, x, y, fmt.Errorf("same") +- } else if false { +- z = "hi" +- return "", true, x, z, nil +- } +- return z, false, 0, "", nil -} - ---- go.mod -- --module golang.org/lsptests/rank -- --go 1.18 -- ---- struct/struct_rank.go -- --package struct_rank +--- return_complex_nonnested.go -- +-package extract - --type foo struct { -- c int //@item(c_rank, "c", "int", "field") -- b int //@item(b_rank, "b", "int", "field") -- a int //@item(a_rank, "a", "int", "field") --} +-import "fmt" - --func f() { -- foo := foo{} //@rank("}", c_rank, b_rank, a_rank) +-func _() (int, string, error) { +- x := 1 +- y := "hello" +- z := "bye" //@codeaction("z", rcnnEnd, "refactor.extract", rcnn) +- if y == z { +- return x, y, fmt.Errorf("same") +- } else if false { +- z = "hi" +- return x, z, nil +- } +- return x, z, nil //@loc(rcnnEnd, "nil") -} - ---- assign_rank.go -- --package rank -- --// Literal completion results. --/* int() */ //@item(int, "int()", "int", "var") --/* string() */ //@item(string, "string()", "string", "var") +--- @rcnn/return_complex_nonnested.go -- +-package extract - --var ( -- apple int = 3 //@item(apple, "apple", "int", "var") -- pear string = "hello" //@item(pear, "pear", "string", "var") --) +-import "fmt" - --func _() { -- orange := 1 //@item(orange, "orange", "int", "var") -- grape := "hello" //@item(grape, "grape", "string", "var") -- orange, grape = 2, "hello" //@complete(" \"", grape, pear, string, orange, apple) +-func _() (int, string, error) { +- x := 1 +- y := "hello" +- //@codeaction("z", rcnnEnd, "refactor.extract", rcnn) +- return newFunction(y, x) //@loc(rcnnEnd, "nil") -} - --func _() { -- var pineapple int //@item(pineapple, "pineapple", "int", "var") -- pineapple = 1 //@complete(" 1", pineapple, apple, int, pear) -- -- y := //@complete(" /", pineapple, apple, pear) +-func newFunction(y string, x int) (int, string, error) { +- z := "bye" +- if y == z { +- return x, y, fmt.Errorf("same") +- } else if false { +- z = "hi" +- return x, z, nil +- } +- return x, z, nil -} - ---- binexpr_rank.go -- --package rank +--- return_func_lit.go -- +-package extract - --func _() { -- _ = 5 + ; //@complete(" ;", apple, pear) -- y := + 5; //@complete(" +", apple, pear) +-import "go/ast" - -- if 6 == {} //@complete(" {", apple, pear) +-func _() { +- ast.Inspect(ast.NewIdent("a"), func(n ast.Node) bool { +- if n == nil { //@codeaction("if", rflEnd, "refactor.extract", rfl) +- return true +- } //@loc(rflEnd, "}") +- return false +- }) -} - ---- boolexpr_rank.go -- --package rank +--- @rfl/return_func_lit.go -- +-package extract +- +-import "go/ast" - -func _() { -- someRandomBoolFunc := func() bool { //@item(boolExprFunc, "someRandomBoolFunc", "func() bool", "var") -- return true -- } +- ast.Inspect(ast.NewIdent("a"), func(n ast.Node) bool { +- //@codeaction("if", rflEnd, "refactor.extract", rfl) +- shouldReturn, returnValue := newFunction(n) +- if shouldReturn { +- return returnValue +- } //@loc(rflEnd, "}") +- return false +- }) +-} - -- var foo, bar int //@item(boolExprBar, "bar", "int", "var") -- if foo == 123 && b { //@rank(" {", boolExprBar, boolExprFunc) +-func newFunction(n ast.Node) (bool, bool) { +- if n == nil { +- return true, true - } +- return false, false -} - ---- convert_rank.go -- --package rank -- --import "time" +--- return_func_lit_nonnested.go -- +-package extract - --// Copied from the old builtins.go, which has been ported to the new marker tests. --/* complex(r float64, i float64) */ //@item(complex, "complex", "func(r float64, i float64) complex128", "func") +-import "go/ast" - -func _() { -- type strList []string -- wantsStrList := func(strList) {} -- -- var ( -- convA string //@item(convertA, "convA", "string", "var") -- convB []string //@item(convertB, "convB", "[]string", "var") -- ) -- wantsStrList(strList(conv)) //@complete("))", convertB, convertA) +- ast.Inspect(ast.NewIdent("a"), func(n ast.Node) bool { +- if n == nil { //@codeaction("if", rflnnEnd, "refactor.extract", rflnn) +- return true +- } +- return false //@loc(rflnnEnd, "false") +- }) -} - --func _() { -- type myInt int -- -- const ( -- convC = "hi" //@item(convertC, "convC", "string", "const") -- convD = 123 //@item(convertD, "convD", "int", "const") -- convE int = 123 //@item(convertE, "convE", "int", "const") -- convF string = "there" //@item(convertF, "convF", "string", "const") -- convG myInt = 123 //@item(convertG, "convG", "myInt", "const") -- ) -- -- var foo int -- foo = conv //@rank(" //", convertE, convertD) -- -- var mi myInt -- mi = conv //@rank(" //", convertG, convertD, convertE) -- mi + conv //@rank(" //", convertG, convertD, convertE) -- -- 1 + conv //@rank(" //", convertD, convertC),rank(" //", convertE, convertC),rank(" //", convertG, convertC) -- -- type myString string -- var ms myString -- ms = conv //@rank(" //", convertC, convertF) +--- @rflnn/return_func_lit_nonnested.go -- +-package extract - -- type myUint uint32 -- var mu myUint -- mu = conv //@rank(" //", convertD, convertE) +-import "go/ast" - -- // don't downrank constants when assigning to interface{} -- var _ interface{} = c //@rank(" //", convertD, complex) +-func _() { +- ast.Inspect(ast.NewIdent("a"), func(n ast.Node) bool { +- //@codeaction("if", rflnnEnd, "refactor.extract", rflnn) +- return newFunction(n) //@loc(rflnnEnd, "false") +- }) +-} - -- var _ time.Duration = conv //@rank(" //", convertD, convertE),snippet(" //", convertE, "time.Duration(convE)") +-func newFunction(n ast.Node) bool { +- if n == nil { +- return true +- } +- return false +-} - -- var convP myInt //@item(convertP, "convP", "myInt", "var") -- var _ *int = conv //@snippet(" //", convertP, "(*int)(&convP)") +--- return_init.go -- +-package extract - -- var ff float64 //@item(convertFloat, "ff", "float64", "var") -- f == convD //@snippet(" =", convertFloat, "ff") +-func _() string { +- x := 1 +- if x == 0 { //@codeaction("if", riEnd, "refactor.extract", ri) +- x = 3 +- return "a" +- } //@loc(riEnd, "}") +- x = 2 +- return "b" -} - ---- switch_rank.go -- --package rank +--- @ri/return_init.go -- +-package extract - --import "time" +-func _() string { +- x := 1 +- //@codeaction("if", riEnd, "refactor.extract", ri) +- shouldReturn, returnValue := newFunction(x) +- if shouldReturn { +- return returnValue +- } //@loc(riEnd, "}") +- x = 2 +- return "b" +-} - --func _() { -- switch pear { -- case _: //@rank("_", pear, apple) +-func newFunction(x int) (bool, string) { +- if x == 0 { +- x = 3 +- return true, "a" - } +- return false, "" +-} - -- time.Monday //@item(timeMonday, "time.Monday", "time.Weekday", "const"),item(monday ,"Monday", "time.Weekday", "const") -- time.Friday //@item(timeFriday, "time.Friday", "time.Weekday", "const"),item(friday ,"Friday", "time.Weekday", "const") +--- return_init_nonnested.go -- +-package extract - -- now := time.Now() -- now.Weekday //@item(nowWeekday, "now.Weekday", "func() time.Weekday", "method") +-func _() string { +- x := 1 +- if x == 0 { //@codeaction("if", rinnEnd, "refactor.extract", rinn) +- x = 3 +- return "a" +- } +- x = 2 +- return "b" //@loc(rinnEnd, "\"b\"") +-} - -- then := time.Now() -- then.Weekday //@item(thenWeekday, "then.Weekday", "func() time.Weekday", "method") +--- @rinn/return_init_nonnested.go -- +-package extract - -- switch time.Weekday(0) { -- case time.Monday, time.Tuesday: -- case time.Wednesday, time.Thursday: -- case time.Saturday, time.Sunday: -- // TODO: these tests were disabled because they require deep completion -- // (which would break other tests) -- case t: // rank(":", timeFriday, timeMonday) -- case time.: //@rank(":", friday, monday) +-func _() string { +- x := 1 +- //@codeaction("if", rinnEnd, "refactor.extract", rinn) +- return newFunction(x) //@loc(rinnEnd, "\"b\"") +-} - -- case now.Weekday(): -- case week: // rank(":", thenWeekday, nowWeekday) +-func newFunction(x int) string { +- if x == 0 { +- x = 3 +- return "a" - } +- x = 2 +- return "b" -} - ---- type_assert_rank.go -- --package rank +--- args_returns.go -- +-package extract - -func _() { -- type flower int //@item(flower, "flower", "int", "type") -- var fig string //@item(fig, "fig", "string", "var") +- a := 1 +- a = 5 //@codeaction("a", araend, "refactor.extract", ara) +- a = a + 2 //@loc(araend, "2") - -- _ = interface{}(nil).(f) //@complete(") //", flower) +- b := a * 2 //@codeaction("b", arbend, "refactor.extract", arb) +- _ = b + 4 //@loc(arbend, "4") -} - ---- type_switch_rank.go -- --package rank -- --import ( -- "fmt" -- "go/ast" --) +--- @ara/args_returns.go -- +-package extract - -func _() { -- type basket int //@item(basket, "basket", "int", "type") -- var banana string //@item(banana, "banana", "string", "var") +- a := 1 +- //@codeaction("a", araend, "refactor.extract", ara) +- a = newFunction(a) //@loc(araend, "2") - -- switch interface{}(pear).(type) { -- case b: //@complete(":", basket) -- b //@complete(" //", banana, basket) -- } +- b := a * 2 //@codeaction("b", arbend, "refactor.extract", arb) +- _ = b + 4 //@loc(arbend, "4") +-} - -- Ident //@item(astIdent, "Ident", "struct{...}", "struct") -- IfStmt //@item(astIfStmt, "IfStmt", "struct{...}", "struct") +-func newFunction(a int) int { +- a = 5 +- a = a + 2 +- return a +-} - -- switch ast.Node(nil).(type) { -- case *ast.Ident: -- case *ast.I: //@rank(":", astIfStmt, astIdent) -- } +--- @arb/args_returns.go -- +-package extract - -- Stringer //@item(fmtStringer, "Stringer", "interface{...}", "interface") -- GoStringer //@item(fmtGoStringer, "GoStringer", "interface{...}", "interface") +-func _() { +- a := 1 +- a = 5 //@codeaction("a", araend, "refactor.extract", ara) +- a = a + 2 //@loc(araend, "2") - -- switch interface{}(nil).(type) { -- case fmt.Stringer: //@rank(":", fmtStringer, fmtGoStringer) -- } +- //@codeaction("b", arbend, "refactor.extract", arb) +- newFunction(a) //@loc(arbend, "4") -} - -diff -urN a/gopls/internal/regtest/marker/testdata/completion/snippet_placeholder.txt b/gopls/internal/regtest/marker/testdata/completion/snippet_placeholder.txt ---- a/gopls/internal/regtest/marker/testdata/completion/snippet_placeholder.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/snippet_placeholder.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,83 +0,0 @@ --This test checks basic completion snippet support, using placeholders. -- --Unlike the old marker tests, the new marker tests assume static configuration --(as defined by settings.json), and therefore there is duplication between this --test and snippet.txt. This is a price we pay so that we don't have to mutate --the server during testing. +-func newFunction(a int) { +- b := a * 2 +- _ = b + 4 +-} - ---- flags -- ---ignore_extra_diags +--- scope.go -- +-package extract - ---- settings.json -- --{ -- "usePlaceholders": true +-func _() { +- newFunction := 1 +- a := newFunction //@codeaction("a", "newFunction", "refactor.extract", scope) +- _ = a // avoid diagnostic -} - ---- go.mod -- --module golang.org/lsptests/snippet -- ---- snippet.go -- --package snippets +-func newFunction1() int { +- return 1 +-} - --// Pre-set this marker, as we don't have a "source" for it in this package. --/* Error() */ //@item(Error, "Error", "func() string", "method") +--- @scope/scope.go -- +-package extract - --type AliasType = int //@item(sigAliasType, "AliasType", "AliasType", "type") +-func _() { +- newFunction := 1 +- a := newFunction2(newFunction) //@codeaction("a", "newFunction", "refactor.extract", scope) +- _ = a // avoid diagnostic +-} - --func foo(i int, b bool) {} //@item(snipFoo, "foo", "func(i int, b bool)", "func") --func bar(fn func()) func() {} //@item(snipBar, "bar", "func(fn func())", "func") --func baz(at AliasType, b bool) {} //@item(snipBaz, "baz", "func(at AliasType, b bool)", "func") +-func newFunction2(newFunction int) int { +- a := newFunction +- return a +-} - --type Foo struct { -- Bar int //@item(snipFieldBar, "Bar", "int", "field") -- Func func(at AliasType) error //@item(snipFieldFunc, "Func", "func(at AliasType) error", "field") +-func newFunction1() int { +- return 1 -} - --func (Foo) Baz() func() {} //@item(snipMethodBaz, "Baz", "func() func()", "method") --func (Foo) BazBar() func() {} //@item(snipMethodBazBar, "BazBar", "func() func()", "method") --func (Foo) BazBaz(at AliasType) func() {} //@item(snipMethodBazBaz, "BazBaz", "func(at AliasType) func()", "method") +--- smart_initialization.go -- +-package extract - -func _() { -- f //@snippet(" //", snipFoo, "foo(${1:i int}, ${2:b bool})") -- -- bar //@snippet(" //", snipBar, "bar(${1:fn func()})") -- -- baz //@snippet(" //", snipBaz, "baz(${1:at AliasType}, ${2:b bool})") -- baz() //@signature("(", "baz(at AliasType, b bool)", 0) +- var a []int +- a = append(a, 2) //@codeaction("a", siEnd, "refactor.extract", si) +- b := 4 //@loc(siEnd, "4") +- a = append(a, b) +-} - -- bar(nil) //@snippet("(", snipBar, "bar") -- bar(ba) //@snippet(")", snipBar, "bar(${1:fn func()})") -- var f Foo -- bar(f.Ba) //@snippet(")", snipMethodBaz, "Baz()") -- (bar)(nil) //@snippet(")", snipBar, "bar(${1:fn func()})") -- (f.Ba)() //@snippet(")", snipMethodBaz, "Baz()") +--- @si/smart_initialization.go -- +-package extract - -- Foo{ -- B //@snippet(" //", snipFieldBar, "Bar: ${1:int},") -- } +-func _() { +- var a []int +- //@codeaction("a", siEnd, "refactor.extract", si) +- a, b := newFunction(a) //@loc(siEnd, "4") +- a = append(a, b) +-} - -- Foo{ -- F //@snippet(" //", snipFieldFunc, "Func: ${1:func(at AliasType) error},") -- } +-func newFunction(a []int) ([]int, int) { +- a = append(a, 2) +- b := 4 +- return a, b +-} - -- Foo{B} //@snippet("}", snipFieldBar, "Bar: ${1:int}") -- Foo{} //@snippet("}", snipFieldBar, "Bar: ${1:int}") +--- smart_return.go -- +-package extract - -- Foo{Foo{}.B} //@snippet("} ", snipFieldBar, "Bar") +-func _() { +- var b []int +- var a int +- a = 2 //@codeaction("a", srEnd, "refactor.extract", sr) +- b = []int{} +- b = append(b, a) //@loc(srEnd, ")") +- b[0] = 1 +-} - -- var err error -- err.Error() //@snippet("E", Error, "Error()") -- f.Baz() //@snippet("B", snipMethodBaz, "Baz()") +--- @sr/smart_return.go -- +-package extract - -- f.Baz() //@snippet("(", snipMethodBazBar, "BazBar") +-func _() { +- var b []int +- var a int +- //@codeaction("a", srEnd, "refactor.extract", sr) +- b = newFunction(a, b) //@loc(srEnd, ")") +- b[0] = 1 +-} - -- f.Baz() //@snippet("B", snipMethodBazBaz, "BazBaz(${1:at AliasType})") +-func newFunction(a int, b []int) []int { +- a = 2 +- b = []int{} +- b = append(b, a) +- return b -} - +--- unnecessary_param.go -- +-package extract +- -func _() { -- type bar struct { -- a int -- b float64 //@item(snipBarB, "b", "field") +- var b []int +- a := 2 //@codeaction("a", upEnd, "refactor.extract", up) +- b = []int{} +- b = append(b, a) //@loc(upEnd, ")") +- b[0] = 1 +- if a == 2 { +- return - } -- bar{b} //@snippet("}", snipBarB, "b: ${1:float64}") -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/snippet.txt b/gopls/internal/regtest/marker/testdata/completion/snippet.txt ---- a/gopls/internal/regtest/marker/testdata/completion/snippet.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/snippet.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,77 +0,0 @@ --This test checks basic completion snippet support. -- ---- flags -- ---ignore_extra_diags -- ---- go.mod -- --module golang.org/lsptests/snippet -- ---- snippet.go -- --package snippets -- --// Pre-set this marker, as we don't have a "source" for it in this package. --// The comment is used to create a synthetic completion item. --// --// TODO(rfindley): allow completion markers to refer to ad-hoc items inline, --// without this trick. --/* Error() */ //@item(Error, "Error", "func() string", "method") - --type AliasType = int //@item(sigAliasType, "AliasType", "AliasType", "type") +--- @up/unnecessary_param.go -- +-package extract - --func foo(i int, b bool) {} //@item(snipFoo, "foo", "func(i int, b bool)", "func") --func bar(fn func()) func() {} //@item(snipBar, "bar", "func(fn func())", "func") --func baz(at AliasType, b bool) {} //@item(snipBaz, "baz", "func(at AliasType, b bool)", "func") +-func _() { +- var b []int +- //@codeaction("a", upEnd, "refactor.extract", up) +- a, b := newFunction(b) //@loc(upEnd, ")") +- b[0] = 1 +- if a == 2 { +- return +- } +-} - --type Foo struct { -- Bar int //@item(snipFieldBar, "Bar", "int", "field") -- Func func(at AliasType) error //@item(snipFieldFunc, "Func", "func(at AliasType) error", "field") +-func newFunction(b []int) (int, []int) { +- a := 2 +- b = []int{} +- b = append(b, a) +- return a, b -} - --func (Foo) Baz() func() {} //@item(snipMethodBaz, "Baz", "func() func()", "method") --func (Foo) BazBar() func() {} //@item(snipMethodBazBar, "BazBar", "func() func()", "method") --func (Foo) BazBaz(at AliasType) func() {} //@item(snipMethodBazBaz, "BazBaz", "func(at AliasType) func()", "method") +--- comment.go -- +-package extract - -func _() { -- f //@snippet(" //", snipFoo, "foo(${1:})") +- a := /* comment in the middle of a line */ 1 //@codeaction("a", commentEnd, "refactor.extract", comment1) +- // Comment on its own line //@codeaction("Comment", commentEnd, "refactor.extract", comment2) +- _ = a + 4 //@loc(commentEnd, "4"),codeaction("_", lastComment, "refactor.extract", comment3) +- // Comment right after 3 + 4 - -- bar //@snippet(" //", snipBar, "bar(${1:})") +- // Comment after with space //@loc(lastComment, "Comment") +-} - -- baz //@snippet(" //", snipBaz, "baz(${1:})") -- baz() //@signature("(", "baz(at AliasType, b bool)", 0) +--- @comment1/comment.go -- +-package extract - -- bar(nil) //@snippet("(", snipBar, "bar") -- bar(ba) //@snippet(")", snipBar, "bar(${1:})") -- var f Foo -- bar(f.Ba) //@snippet(")", snipMethodBaz, "Baz()") -- (bar)(nil) //@snippet(")", snipBar, "bar(${1:})") -- (f.Ba)() //@snippet(")", snipMethodBaz, "Baz()") +-func _() { +- /* comment in the middle of a line */ +- //@codeaction("a", commentEnd, "refactor.extract", comment1) +- // Comment on its own line //@codeaction("Comment", commentEnd, "refactor.extract", comment2) +- newFunction() //@loc(commentEnd, "4"),codeaction("_", lastComment, "refactor.extract", comment3) +- // Comment right after 3 + 4 - -- Foo{ -- B //@snippet(" //", snipFieldBar, "Bar: ${1:},") -- } +- // Comment after with space //@loc(lastComment, "Comment") +-} - -- Foo{ -- F //@snippet(" //", snipFieldFunc, "Func: ${1:},") -- } +-func newFunction() { +- a := 1 - -- Foo{B} //@snippet("}", snipFieldBar, "Bar: ${1:}") -- Foo{} //@snippet("}", snipFieldBar, "Bar: ${1:}") +- _ = a + 4 +-} - -- Foo{Foo{}.B} //@snippet("} ", snipFieldBar, "Bar") +--- @comment2/comment.go -- +-package extract - -- var err error -- err.Error() //@snippet("E", Error, "Error()") -- f.Baz() //@snippet("B", snipMethodBaz, "Baz()") +-func _() { +- a := /* comment in the middle of a line */ 1 //@codeaction("a", commentEnd, "refactor.extract", comment1) +- // Comment on its own line //@codeaction("Comment", commentEnd, "refactor.extract", comment2) +- newFunction(a) //@loc(commentEnd, "4"),codeaction("_", lastComment, "refactor.extract", comment3) +- // Comment right after 3 + 4 - -- f.Baz() //@snippet("(", snipMethodBazBar, "BazBar") +- // Comment after with space //@loc(lastComment, "Comment") +-} - -- f.Baz() //@snippet("B", snipMethodBazBaz, "BazBaz(${1:})") +-func newFunction(a int) { +- _ = a + 4 -} - +--- @comment3/comment.go -- +-package extract +- -func _() { -- type bar struct { -- a int -- b float64 //@item(snipBarB, "b", "float64") -- } -- bar{b} //@snippet("}", snipBarB, "b: ${1:}") --} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/statements.txt b/gopls/internal/regtest/marker/testdata/completion/statements.txt ---- a/gopls/internal/regtest/marker/testdata/completion/statements.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/statements.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,121 +0,0 @@ --This test exercises completion around various statements. +- a := /* comment in the middle of a line */ 1 //@codeaction("a", commentEnd, "refactor.extract", comment1) +- // Comment on its own line //@codeaction("Comment", commentEnd, "refactor.extract", comment2) +- newFunction(a) //@loc(commentEnd, "4"),codeaction("_", lastComment, "refactor.extract", comment3) +- // Comment right after 3 + 4 - ---- flags -- ---ignore_extra_diags +- // Comment after with space //@loc(lastComment, "Comment") +-} - ---- settings.json -- --{ -- "usePlaceholders": true +-func newFunction(a int) { +- _ = a + 4 -} - ---- go.mod -- --module golang.org/lsptests/statements +--- redefine.go -- +-package extract - ---- append.go -- --package statements +-import "strconv" - -func _() { -- type mySlice []int -- -- var ( -- abc []int //@item(stmtABC, "abc", "[]int", "var") -- abcdef mySlice //@item(stmtABCDEF, "abcdef", "mySlice", "var") -- ) -- -- /* abcdef = append(abcdef, ) */ //@item(stmtABCDEFAssignAppend, "abcdef = append(abcdef, )", "", "func") +- i, err := strconv.Atoi("1") +- u, err := strconv.Atoi("2") //@codeaction("u", ")", "refactor.extract", redefine) +- if i == u || err == nil { +- return +- } +-} - -- // don't offer "abc = append(abc, )" because "abc" isn't necessarily -- // better than "abcdef". -- abc //@complete(" //", stmtABC, stmtABCDEF) +--- @redefine/redefine.go -- +-package extract - -- abcdef //@complete(" //", stmtABCDEF, stmtABCDEFAssignAppend) +-import "strconv" - -- /* append(abc, ) */ //@item(stmtABCAppend, "append(abc, )", "", "func") +-func _() { +- i, err := strconv.Atoi("1") +- u, err := newFunction() //@codeaction("u", ")", "refactor.extract", redefine) +- if i == u || err == nil { +- return +- } +-} - -- abc = app //@snippet(" //", stmtABCAppend, "append(abc, ${1:})") +-func newFunction() (int, error) { +- u, err := strconv.Atoi("2") +- return u, err -} - --func _() { -- var s struct{ xyz []int } +diff -urN a/gopls/internal/test/marker/testdata/codeaction/grouplines.txt b/gopls/internal/test/marker/testdata/codeaction/grouplines.txt +--- a/gopls/internal/test/marker/testdata/codeaction/grouplines.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codeaction/grouplines.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,206 +0,0 @@ +-This test exercises the refactoring of putting arguments, return values, and composite literal elements into a +-single line. - -- /* xyz = append(s.xyz, ) */ //@item(stmtXYZAppend, "xyz = append(s.xyz, )", "", "func") +--- go.mod -- +-module unused.mod - -- s.x //@snippet(" //", stmtXYZAppend, "xyz = append(s.xyz, ${1:})") +-go 1.18 - -- /* s.xyz = append(s.xyz, ) */ //@item(stmtDeepXYZAppend, "s.xyz = append(s.xyz, )", "", "func") +--- func_arg/func_arg.go -- +-package func_arg - -- sx //@snippet(" //", stmtDeepXYZAppend, "s.xyz = append(s.xyz, ${1:})") +-func A( +- a string, +- b, c int64, +- x int /*@codeaction("x", "x", "refactor.rewrite", func_arg)*/, +- y int, +-) (r1 string, r2, r3 int64, r4 int, r5 int) { +- return a, b, c, x, y -} - --func _() { -- var foo [][]int +--- @func_arg/func_arg/func_arg.go -- +-package func_arg - -- /* append(foo[0], ) */ //@item(stmtFooAppend, "append(foo[0], )", "", "func") +-func A(a string, b, c int64, x int /*@codeaction("x", "x", "refactor.rewrite", func_arg)*/, y int) (r1 string, r2, r3 int64, r4 int, r5 int) { +- return a, b, c, x, y +-} - -- foo[0] = app //@complete(" //", stmtFooAppend),snippet(" //", stmtFooAppend, "append(foo[0], ${1:})") +--- func_ret/func_ret.go -- +-package func_ret +- +-func A(a string, b, c int64, x int, y int) ( +- r1 string /*@codeaction("r1", "r1", "refactor.rewrite", func_ret)*/, +- r2, r3 int64, +- r4 int, +- r5 int, +-) { +- return a, b, c, x, y -} - ---- if_err_check_return.go -- --package statements +--- @func_ret/func_ret/func_ret.go -- +-package func_ret - --import ( -- "bytes" -- "io" -- "os" --) +-func A(a string, b, c int64, x int, y int) (r1 string /*@codeaction("r1", "r1", "refactor.rewrite", func_ret)*/, r2, r3 int64, r4 int, r5 int) { +- return a, b, c, x, y +-} - --func one() (int, float32, io.Writer, *int, []int, bytes.Buffer, error) { -- /* if err != nil { return err } */ //@item(stmtOneIfErrReturn, "if err != nil { return err }", "", "") -- /* err != nil { return err } */ //@item(stmtOneErrReturn, "err != nil { return err }", "", "") +--- functype_arg/functype_arg.go -- +-package functype_arg - -- _, err := os.Open("foo") -- //@snippet("", stmtOneIfErrReturn, "if err != nil {\n\treturn 0, 0, nil, nil, nil, bytes.Buffer{\\}, ${1:err}\n\\}") +-type A func( +- a string, +- b, c int64, +- x int /*@codeaction("x", "x", "refactor.rewrite", functype_arg)*/, +- y int, +-) (r1 string, r2, r3 int64, r4 int, r5 int) - -- _, err = os.Open("foo") -- i //@snippet(" //", stmtOneIfErrReturn, "if err != nil {\n\treturn 0, 0, nil, nil, nil, bytes.Buffer{\\}, ${1:err}\n\\}") +--- @functype_arg/functype_arg/functype_arg.go -- +-package functype_arg - -- _, err = os.Open("foo") -- if er //@snippet(" //", stmtOneErrReturn, "err != nil {\n\treturn 0, 0, nil, nil, nil, bytes.Buffer{\\}, ${1:err}\n\\}") +-type A func(a string, b, c int64, x int /*@codeaction("x", "x", "refactor.rewrite", functype_arg)*/, y int) (r1 string, r2, r3 int64, r4 int, r5 int) - -- _, err = os.Open("foo") -- if //@snippet(" //", stmtOneIfErrReturn, "if err != nil {\n\treturn 0, 0, nil, nil, nil, bytes.Buffer{\\}, ${1:err}\n\\}") +--- functype_ret/functype_ret.go -- +-package functype_ret - -- _, err = os.Open("foo") -- if //@snippet("//", stmtOneIfErrReturn, "if err != nil {\n\treturn 0, 0, nil, nil, nil, bytes.Buffer{\\}, ${1:err}\n\\}") --} +-type A func(a string, b, c int64, x int, y int) ( +- r1 string /*@codeaction("r1", "r1", "refactor.rewrite", functype_ret)*/, +- r2, r3 int64, +- r4 int, +- r5 int, +-) - ---- if_err_check_return2.go -- --package statements +--- @functype_ret/functype_ret/functype_ret.go -- +-package functype_ret - --import "os" +-type A func(a string, b, c int64, x int, y int) (r1 string /*@codeaction("r1", "r1", "refactor.rewrite", functype_ret)*/, r2, r3 int64, r4 int, r5 int) - --func two() error { -- var s struct{ err error } +--- func_call/func_call.go -- +-package func_call - -- /* if s.err != nil { return s.err } */ //@item(stmtTwoIfErrReturn, "if s.err != nil { return s.err }", "", "") +-import "fmt" - -- _, s.err = os.Open("foo") -- //@snippet("", stmtTwoIfErrReturn, "if s.err != nil {\n\treturn ${1:s.err}\n\\}") +-func a() { +- fmt.Println( +- 1 /*@codeaction("1", "1", "refactor.rewrite", func_call)*/, +- 2, +- 3, +- fmt.Sprintf("hello %d", 4), +- ) -} - ---- if_err_check_test.go -- --package statements -- --import ( -- "os" -- "testing" --) +--- @func_call/func_call/func_call.go -- +-package func_call - --func TestErr(t *testing.T) { -- /* if err != nil { t.Fatal(err) } */ //@item(stmtOneIfErrTFatal, "if err != nil { t.Fatal(err) }", "", "") +-import "fmt" - -- _, err := os.Open("foo") -- //@snippet("", stmtOneIfErrTFatal, "if err != nil {\n\tt.Fatal(err)\n\\}") +-func a() { +- fmt.Println(1 /*@codeaction("1", "1", "refactor.rewrite", func_call)*/, 2, 3, fmt.Sprintf("hello %d", 4)) -} - --func BenchmarkErr(b *testing.B) { -- /* if err != nil { b.Fatal(err) } */ //@item(stmtOneIfErrBFatal, "if err != nil { b.Fatal(err) }", "", "") +--- indent/indent.go -- +-package indent - -- _, err := os.Open("foo") -- //@snippet("", stmtOneIfErrBFatal, "if err != nil {\n\tb.Fatal(err)\n\\}") --} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/testy.txt b/gopls/internal/regtest/marker/testdata/completion/testy.txt ---- a/gopls/internal/regtest/marker/testdata/completion/testy.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/testy.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,61 +0,0 @@ +-import "fmt" - ---- flags -- ---ignore_extra_diags +-func a() { +- fmt.Println( +- 1, +- 2, +- 3, +- fmt.Sprintf( +- "hello %d" /*@codeaction("hello", "hello", "refactor.rewrite", indent, "Join parameters into one line")*/, +- 4, +- )) +-} - ---- go.mod -- --module testy.test +--- @indent/indent/indent.go -- +-package indent - --go 1.18 +-import "fmt" - ---- types/types.go -- --package types +-func a() { +- fmt.Println( +- 1, +- 2, +- 3, +- fmt.Sprintf("hello %d" /*@codeaction("hello", "hello", "refactor.rewrite", indent, "Join parameters into one line")*/, 4)) +-} - +--- structelts/structelts.go -- +-package structelts - ---- signature/signature.go -- --package signature +-type A struct{ +- a int +- b int +-} - --type Alias = int +-func a() { +- _ = A{ +- a: 1, +- b: 2 /*@codeaction("b", "b", "refactor.rewrite", structelts)*/, +- } +-} - ---- snippets/snippets.go -- --package snippets +--- @structelts/structelts/structelts.go -- +-package structelts - --import ( -- "testy.test/signature" -- t "testy.test/types" --) +-type A struct{ +- a int +- b int +-} - --func X(_ map[signature.Alias]t.CoolAlias) (map[signature.Alias]t.CoolAlias) { -- return nil +-func a() { +- _ = A{a: 1, b: 2 /*@codeaction("b", "b", "refactor.rewrite", structelts)*/} -} - ---- testy/testy.go -- --package testy +--- sliceelts/sliceelts.go -- +-package sliceelts - --func a() { //@item(funcA, "a", "func()", "func") -- //@complete("", funcA) +-func a() { +- _ = []int{ +- 1 /*@codeaction("1", "1", "refactor.rewrite", sliceelts)*/, +- 2, +- } -} - +--- @sliceelts/sliceelts/sliceelts.go -- +-package sliceelts - ---- testy/testy_test.go -- --package testy +-func a() { +- _ = []int{1 /*@codeaction("1", "1", "refactor.rewrite", sliceelts)*/, 2} +-} - --import ( -- "testing" +--- mapelts/mapelts.go -- +-package mapelts - -- sig "testy.test/signature" -- "testy.test/snippets" --) +-func a() { +- _ = map[string]int{ +- "a": 1 /*@codeaction("1", "1", "refactor.rewrite", mapelts)*/, +- "b": 2, +- } +-} - --func TestSomething(t *testing.T) { //@item(TestSomething, "TestSomething(t *testing.T)", "", "func") -- var x int //@loc(testyX, "x"), diag("x", re"x declared (and|but) not used") -- a() //@loc(testyA, "a") +--- @mapelts/mapelts/mapelts.go -- +-package mapelts +- +-func a() { +- _ = map[string]int{"a": 1 /*@codeaction("1", "1", "refactor.rewrite", mapelts)*/, "b": 2} -} - --func _() { -- _ = snippets.X(nil) //@signature("nil", "X(_ map[sig.Alias]types.CoolAlias) map[sig.Alias]types.CoolAlias", 0) -- var _ sig.Alias +--- starcomment/starcomment.go -- +-package starcomment +- +-func A( +- /*1*/ x /*2*/ string /*3*/ /*@codeaction("x", "x", "refactor.rewrite", starcomment)*/, +- /*4*/ y /*5*/ int /*6*/, +-) (string, int) { +- return x, y -} - --func issue63578(err error) { -- err.Error() //@signature(")", "Error()", 0) +--- @starcomment/starcomment/starcomment.go -- +-package starcomment +- +-func A(/*1*/ x /*2*/ string /*3*/ /*@codeaction("x", "x", "refactor.rewrite", starcomment)*/, /*4*/ y /*5*/ int /*6*/) (string, int) { +- return x, y -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/type_assert.txt b/gopls/internal/regtest/marker/testdata/completion/type_assert.txt ---- a/gopls/internal/regtest/marker/testdata/completion/type_assert.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/type_assert.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,30 +0,0 @@ --This test checks completion related to type assertions. - ---- flags -- ---ignore_extra_diags +diff -urN a/gopls/internal/test/marker/testdata/codeaction/import-shadows-builtin.txt b/gopls/internal/test/marker/testdata/codeaction/import-shadows-builtin.txt +--- a/gopls/internal/test/marker/testdata/codeaction/import-shadows-builtin.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codeaction/import-shadows-builtin.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,55 +0,0 @@ +-This is a regression test for bug #63592 in "organize imports" whereby +-the new imports would shadow predeclared names. - ---- type_assert.go -- --package typeassert +-In the original example, the conflict was between predeclared error +-type and the unfortunately named package github.com/coreos/etcd/error, +-but this example uses a package with the ludicrous name of complex128. - --type abc interface { //@item(abcIntf, "abc", "interface{...}", "interface") -- abc() --} +-The new behavior is that we will not attempt to import packages +-that shadow predeclared names. (Ideally we would do that only if +-the predeclared name is actually referenced in the file, which +-complex128 happens to be in this example, but that's a trickier +-analysis than the internal/imports package is game for.) - --type abcImpl struct{} //@item(abcImpl, "abcImpl", "struct{...}", "struct") --func (abcImpl) abc() +-The name complex127 works as usual. - --type abcPtrImpl struct{} //@item(abcPtrImpl, "abcPtrImpl", "struct{...}", "struct") --func (*abcPtrImpl) abc() +--- go.mod -- +-module example.com +-go 1.18 - --type abcNotImpl struct{} //@item(abcNotImpl, "abcNotImpl", "struct{...}", "struct") +--- complex128/a.go -- +-package complex128 - --func _() { -- var a abc -- switch a.(type) { -- case ab: //@complete(":", abcImpl, abcPtrImpl, abcIntf, abcNotImpl) -- case *ab: //@complete(":", abcImpl, abcPtrImpl, abcIntf, abcNotImpl) -- } +-var V int - -- a.(ab) //@complete(")", abcImpl, abcPtrImpl, abcIntf, abcNotImpl) -- a.(*ab) //@complete(")", abcImpl, abcPtrImpl, abcIntf, abcNotImpl) --} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/type_mods.txt b/gopls/internal/regtest/marker/testdata/completion/type_mods.txt ---- a/gopls/internal/regtest/marker/testdata/completion/type_mods.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/type_mods.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,27 +0,0 @@ --This test check completion snippets with type modifiers. +--- complex127/a.go -- +-package complex127 - ---- flags -- ---ignore_extra_diags +-var V int - ---- typemods.go -- --package typemods +--- main.go -- +-package main - --func fooFunc() func() int { //@item(modFooFunc, "fooFunc", "func() func() int", "func") -- return func() int { -- return 0 -- } --} +-import () //@codeaction("import", "", "source.organizeImports", out) - --func fooPtr() *int { //@item(modFooPtr, "fooPtr", "func() *int", "func") -- return nil +-func main() { +- complex128.V() //@diag("V", re"type complex128 has no field") +- complex127.V() //@diag("complex127", re"(undeclared|undefined)") -} - -func _() { -- var _ int = foo //@snippet(" //", modFooFunc, "fooFunc()()"),snippet(" //", modFooPtr, "*fooPtr()") +- var _ complex128 = 1 + 2i -} +--- @out/main.go -- +-package main - --func _() { -- var m map[int][]chan int //@item(modMapChanPtr, "m", "map[int]chan *int", "var") +-import "example.com/complex127" //@codeaction("import", "", "source.organizeImports", out) - -- var _ int = m //@snippet(" //", modMapChanPtr, "<-m[${1:}][${2:}]") +-func main() { +- complex128.V() //@diag("V", re"type complex128 has no field") +- complex127.V() //@diag("complex127", re"(undeclared|undefined)") -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/type_params.txt b/gopls/internal/regtest/marker/testdata/completion/type_params.txt ---- a/gopls/internal/regtest/marker/testdata/completion/type_params.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/type_params.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,71 +0,0 @@ --This test checks various ranking of completion results related to type --parameters. - ---- flags -- ---ignore_extra_diags +-func _() { +- var _ complex128 = 1 + 2i +-} +diff -urN a/gopls/internal/test/marker/testdata/codeaction/imports.txt b/gopls/internal/test/marker/testdata/codeaction/imports.txt +--- a/gopls/internal/test/marker/testdata/codeaction/imports.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codeaction/imports.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,175 +0,0 @@ +-This test verifies the behavior of the 'source.organizeImports' code action. - ---- type_params.go -- --package typeparams +--- go.mod -- +-module mod.test/imports - --// Copied from the old builtins.go, which has been ported to the new marker tests. --/* string */ //@item(string, "string", "", "type") --/* float32 */ //@item(float32, "float32", "", "type") --/* float64 */ //@item(float64, "float64", "", "type") --/* int */ //@item(int, "int", "", "type") +-go 1.18 - --func one[a int | string]() {} --func two[a int | string, b float64 | int]() {} +--- add.go -- +-package imports //@codeaction("imports", "", "source.organizeImports", add) +- +-import ( +- "fmt" +-) - -func _() { -- one[]() //@rank("]", string, float64) -- two[]() //@rank("]", int, float64) -- two[int, f]() //@rank("]", float64, float32) +- fmt.Println("") +- bytes.NewBuffer(nil) //@diag("bytes", re"(undeclared|undefined)") -} - --func slices[a []int | []float64]() {} //@item(tpInts, "[]int", "[]int", "type"),item(tpFloats, "[]float64", "[]float64", "type") +--- @add/add.go -- +-package imports //@codeaction("imports", "", "source.organizeImports", add) +- +-import ( +- "bytes" +- "fmt" +-) - -func _() { -- slices[]() //@rank("]", tpInts),rank("]", tpFloats) +- fmt.Println("") +- bytes.NewBuffer(nil) //@diag("bytes", re"(undeclared|undefined)") -} - --type s[a int | string] struct{} +--- good.go -- +-package imports //@codeactionerr("imports", "", "source.organizeImports", re"found 0 CodeActions") +- +-import "fmt" - -func _() { -- s[]{} //@rank("]", int, float64) +-fmt.Println("") -} - --func takesGeneric[a int | string](s[a]) { -- "s[a]{}" //@item(tpInScopeLit, "s[a]{}", "", "var") -- takesGeneric() //@rank(")", tpInScopeLit),snippet(")", tpInScopeLit, "s[a]{\\}") --} +--- issue35458.go -- - --func _() { -- s[int]{} //@item(tpInstLit, "s[int]{}", "", "var") -- takesGeneric[int]() //@rank(")", tpInstLit),snippet(")", tpInstLit, "s[int]{\\}") - -- "s[...]{}" //@item(tpUninstLit, "s[...]{}", "", "var") -- takesGeneric() //@rank(")", tpUninstLit),snippet(")", tpUninstLit, "s[${1:}]{\\}") --} - --func returnTP[A int | float64](a A) A { //@item(returnTP, "returnTP", "something", "func") -- return a --} - --func _() { -- // disabled - see issue #54822 -- var _ int = returnTP // snippet(" //", returnTP, "returnTP[${1:}](${2:})") - -- var aa int //@item(tpInt, "aa", "int", "var") -- var ab float64 //@item(tpFloat, "ab", "float64", "var") -- returnTP[int](a) //@rank(")", tpInt, tpFloat) --} +-// package doc +-package imports //@codeaction("imports", "", "source.organizeImports", issue35458) +- +- +- +- - --func takesFunc[T any](func(T) T) { -- var _ func(t T) T = f //@snippet(" //", tpLitFunc, "func(t T) T {$0\\}") --} - -func _() { -- _ = "func(...) {}" //@item(tpLitFunc, "func(...) {}", "", "var") -- takesFunc() //@snippet(")", tpLitFunc, "func(${1:}) ${2:} {$0\\}") -- takesFunc[int]() //@snippet(")", tpLitFunc, "func(i int) int {$0\\}") +- println("Hello, world!") -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/unimported.txt b/gopls/internal/regtest/marker/testdata/completion/unimported.txt ---- a/gopls/internal/regtest/marker/testdata/completion/unimported.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/unimported.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,88 +0,0 @@ - ---- flags -- ---ignore_extra_diags - ---- go.mod -- --module unimported.test - --go 1.18 - ---- unimported/export_test.go -- --package unimported - --var TestExport int //@item(testexport, "TestExport", "var (from \"unimported.test/unimported\")", "var") - ---- signature/signature.go -- --package signature - --func Foo() {} - ---- foo/foo.go -- --package foo +--- @issue35458/issue35458.go -- +-// package doc +-package imports //@codeaction("imports", "", "source.organizeImports", issue35458) - --type StructFoo struct{ F int } - ---- baz/baz.go -- --package baz - --import ( -- f "unimported.test/foo" --) - --var FooStruct f.StructFoo - ---- unimported/unimported.go -- --package unimported - -func _() { -- http //@complete("p", http, httptest, httptrace, httputil) -- // container/ring is extremely unlikely to be imported by anything, so shouldn't have type information. -- ring.Ring //@complete(re"R()ing", ringring) -- signature.Foo //@complete("Foo", signaturefoo) -- -- context.Bac //@complete(" //", contextBackground) +- println("Hello, world!") -} - --// Create markers for unimported std lib packages. Only for use by this test. --/* http */ //@item(http, "http", "\"net/http\"", "package") --/* httptest */ //@item(httptest, "httptest", "\"net/http/httptest\"", "package") --/* httptrace */ //@item(httptrace, "httptrace", "\"net/http/httptrace\"", "package") --/* httputil */ //@item(httputil, "httputil", "\"net/http/httputil\"", "package") -- --/* ring.Ring */ //@item(ringring, "Ring", "(from \"container/ring\")", "var") -- --/* signature.Foo */ //@item(signaturefoo, "Foo", "func (from \"unimported.test/signature\")", "func") - --/* context.Background */ //@item(contextBackground, "Background", "func (from \"context\")", "func") - --// Now that we no longer type-check imported completions, --// we don't expect the context.Background().Err method (see golang/go#58663). --/* context.Background().Err */ //@item(contextBackgroundErr, "Background().Err", "func (from \"context\")", "method") - ---- unimported/unimported_cand_type.go -- --package unimported - --import ( -- _ "context" - -- "unimported.test/baz" --) - --func _() { -- foo.StructFoo{} //@item(litFooStructFoo, "foo.StructFoo{}", "struct{...}", "struct") - -- // We get the literal completion for "foo.StructFoo{}" even though we haven't -- // imported "foo" yet. -- baz.FooStruct = f //@snippet(" //", litFooStructFoo, "foo.StructFoo{$0\\}") --} +--- multi.go -- +-package imports //@codeaction("imports", "", "source.organizeImports", multi) - ---- unimported/x_test.go -- --package unimported_test +-import "fmt" - --import ( -- "testing" --) +-import "bytes" //@diag("\"bytes\"", re"not used") - --func TestSomething(t *testing.T) { -- _ = unimported.TestExport //@complete("TestExport", testexport) +-func _() { +- fmt.Println("") -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/unresolved.txt b/gopls/internal/regtest/marker/testdata/completion/unresolved.txt ---- a/gopls/internal/regtest/marker/testdata/completion/unresolved.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/unresolved.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,16 +0,0 @@ --This test verifies gopls does not crash on fake "resolved" types. - ---- flags -- ---ignore_extra_diags +--- @multi/multi.go -- +-package imports //@codeaction("imports", "", "source.organizeImports", multi) - ---- settings.json -- --{ -- "completeUnimported": false --} +-import "fmt" - ---- unresolved.go -- --package unresolved +-//@diag("\"bytes\"", re"not used") - --func foo(interface{}) { -- foo(func(i, j f //@complete(" //") +-func _() { +- fmt.Println("") -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/unsafe.txt b/gopls/internal/regtest/marker/testdata/completion/unsafe.txt ---- a/gopls/internal/regtest/marker/testdata/completion/unsafe.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/unsafe.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,24 +0,0 @@ --This test checks completion of symbols in the 'unsafe' package. - ---- flags -- ---ignore_extra_diags +--- needs.go -- +-package imports //@codeaction("package", "", "source.organizeImports", needs) - ---- settings.json -- --{ -- "matcher": "caseinsensitive" +-func goodbye() { +- fmt.Printf("HI") //@diag("fmt", re"(undeclared|undefined)") +- log.Printf("byeeeee") //@diag("log", re"(undeclared|undefined)") -} - ---- unsafe.go -- --package unsafe +--- @needs/needs.go -- +-package imports //@codeaction("package", "", "source.organizeImports", needs) - -import ( -- "unsafe" +- "fmt" +- "log" -) - --// Pre-set this marker, as we don't have a "source" for it in this package. --/* unsafe.Sizeof */ //@item(Sizeof, "Sizeof", "invalid type", "text") -- --func _() { -- x := struct{}{} -- _ = unsafe.Sizeof(x) //@complete("z", Sizeof) +-func goodbye() { +- fmt.Printf("HI") //@diag("fmt", re"(undeclared|undefined)") +- log.Printf("byeeeee") //@diag("log", re"(undeclared|undefined)") -} -diff -urN a/gopls/internal/regtest/marker/testdata/completion/variadic.txt b/gopls/internal/regtest/marker/testdata/completion/variadic.txt ---- a/gopls/internal/regtest/marker/testdata/completion/variadic.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/completion/variadic.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,67 +0,0 @@ --This test checks completion related to variadic functions. -- ---- flags -- ---ignore_extra_diags - ---- variadic.go -- --package variadic -- --func foo(i int, strs ...string) {} +--- remove.go -- +-package imports //@codeaction("package", "", "source.organizeImports", remove) - --func bar() []string { //@item(vFunc, "bar", "func() []string", "func") -- return nil --} +-import ( +- "bytes" //@diag("\"bytes\"", re"not used") +- "fmt" +-) - -func _() { -- var ( -- i int //@item(vInt, "i", "int", "var") -- s string //@item(vStr, "s", "string", "var") -- ss []string //@item(vStrSlice, "ss", "[]string", "var") -- v interface{} //@item(vIntf, "v", "interface{}", "var") -- ) -- -- foo() //@rank(")", vInt, vStr),rank(")", vInt, vStrSlice) -- foo(123, ) //@rank(")", vStr, vInt),rank(")", vStrSlice, vInt) -- foo(123, "", ) //@rank(")", vStr, vInt),rank(")", vStr, vStrSlice) -- foo(123, s, "") //@rank(", \"", vStr, vStrSlice) -- -- // snippet will add the "..." for you -- foo(123, ) //@snippet(")", vStrSlice, "ss..."),snippet(")", vFunc, "bar()..."),snippet(")", vStr, "s") -- -- // don't add "..." for interface{} -- foo(123, ) //@snippet(")", vIntf, "v") +- fmt.Println("") -} - --func qux(...func()) {} --func f() {} //@item(vVarArg, "f", "func()", "func") +--- @remove/remove.go -- +-package imports //@codeaction("package", "", "source.organizeImports", remove) - --func _() { -- qux(f) //@snippet(")", vVarArg, "f") --} +-import ( +- "fmt" +-) - -func _() { -- foo(0, []string{}...) //@complete(")") +- fmt.Println("") -} - ---- variadic_intf.go -- --package variadic +--- removeall.go -- +-package imports //@codeaction("package", "", "source.organizeImports", removeall) - --type baz interface { -- baz() --} +-import ( +- "bytes" //@diag("\"bytes\"", re"not used") +- "fmt" //@diag("\"fmt\"", re"not used") - --func wantsBaz(...baz) {} +-) - --type bazImpl int +-func _() { +-} - --func (bazImpl) baz() {} +--- @removeall/removeall.go -- +-package imports //@codeaction("package", "", "source.organizeImports", removeall) - --func _() { -- var ( -- impls []bazImpl //@item(vImplSlice, "impls", "[]bazImpl", "var") -- impl bazImpl //@item(vImpl, "impl", "bazImpl", "var") -- bazes []baz //@item(vIntfSlice, "bazes", "[]baz", "var") -- ) +-//@diag("\"fmt\"", re"not used") - -- wantsBaz() //@rank(")", vImpl, vImplSlice),rank(")", vIntfSlice, vImplSlice) +-func _() { -} -diff -urN a/gopls/internal/regtest/marker/testdata/definition/cgo.txt b/gopls/internal/regtest/marker/testdata/definition/cgo.txt ---- a/gopls/internal/regtest/marker/testdata/definition/cgo.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/definition/cgo.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,62 +0,0 @@ --This test is ported from the old marker tests. --It tests hover and definition for cgo declarations. - ---- flags -- ---cgo +--- twolines.go -- +-package imports +-func main() {} //@codeactionerr("main", "", "source.organizeImports", re"found 0") +diff -urN a/gopls/internal/test/marker/testdata/codeaction/infertypeargs.txt b/gopls/internal/test/marker/testdata/codeaction/infertypeargs.txt +--- a/gopls/internal/test/marker/testdata/codeaction/infertypeargs.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codeaction/infertypeargs.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,25 +0,0 @@ +-This test verifies the infertypeargs refactoring. - --- go.mod -- --module cgo.test +-module mod.test/infertypeargs - -go 1.18 - ---- cgo/cgo.go -- --package cgo +--- p.go -- +-package infertypeargs - --/* --#include <stdio.h> --#include <stdlib.h> +-func app[S interface{ ~[]E }, E interface{}](s S, e E) S { +- return append(s, e) +-} - --void myprint(char* s) { -- printf("%s\n", s); +-func _() { +- _ = app[[]int] +- _ = app[[]int, int] +- _ = app[[]int]([]int{}, 0) //@suggestedfix("[[]int]", re"unnecessary type arguments", infer) +- _ = app([]int{}, 0) -} --*/ --import "C" - --import ( -- "fmt" -- "unsafe" --) +--- @infer/p.go -- +-@@ -10 +10 @@ +-- _ = app[[]int]([]int{}, 0) //@suggestedfix("[[]int]", re"unnecessary type arguments", infer) +-+ _ = app([]int{}, 0) //@suggestedfix("[[]int]", re"unnecessary type arguments", infer) +diff -urN a/gopls/internal/test/marker/testdata/codeaction/inline_resolve.txt b/gopls/internal/test/marker/testdata/codeaction/inline_resolve.txt +--- a/gopls/internal/test/marker/testdata/codeaction/inline_resolve.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codeaction/inline_resolve.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,35 +0,0 @@ +-This is a minimal test of the refactor.inline code actions, with resolve support. +-See inline.txt for same test without resolve support. - --func Example() { //@loc(cgoexample, "Example"), item(cgoexampleItem, "Example", "func()", "func") -- fmt.Println() -- cs := C.CString("Hello from stdio\n") -- C.myprint(cs) -- C.free(unsafe.Pointer(cs)) +--- capabilities.json -- +-{ +- "textDocument": { +- "codeAction": { +- "dataSupport": true, +- "resolveSupport": { +- "properties": ["edit"] +- } +- } +- } -} +--- go.mod -- +-module example.com/codeaction +-go 1.18 +- +--- a/a.go -- +-package a - -func _() { -- Example() //@hover("ample", "Example", hoverExample), def("ample", cgoexample), complete("ample", cgoexampleItem) +- println(add(1, 2)) //@codeaction("add", ")", "refactor.inline", inline) -} - ---- @hoverExample/hover.md -- --```go --func Example() --``` -- --[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/cgo.test/cgo#Example) ---- usecgo/usecgo.go -- --package cgoimport +-func add(x, y int) int { return x + y } - --import ( -- "cgo.test/cgo" --) +--- @inline/a/a.go -- +-package a - -func _() { -- cgo.Example() //@hover("ample", "Example", hoverImportedExample), def("ample", cgoexample), complete("ample", cgoexampleItem) +- println(1 + 2) //@codeaction("add", ")", "refactor.inline", inline) -} ---- @hoverImportedExample/hover.md -- --```go --func cgo.Example() --``` - --[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/cgo.test/cgo#Example) -diff -urN a/gopls/internal/regtest/marker/testdata/definition/embed.txt b/gopls/internal/regtest/marker/testdata/definition/embed.txt ---- a/gopls/internal/regtest/marker/testdata/definition/embed.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/definition/embed.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,254 +0,0 @@ --This test checks definition and hover operations over embedded fields and methods. +-func add(x, y int) int { return x + y } +diff -urN a/gopls/internal/test/marker/testdata/codeaction/inline.txt b/gopls/internal/test/marker/testdata/codeaction/inline.txt +--- a/gopls/internal/test/marker/testdata/codeaction/inline.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codeaction/inline.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,24 +0,0 @@ +-This is a minimal test of the refactor.inline code action, without resolve support. +-See inline_resolve.txt for same test with resolve support. - --- go.mod -- --module mod.com -- +-module example.com/codeaction -go 1.18 - --- a/a.go -- -package a - --type A string //@loc(AString, "A") +-func _() { +- println(add(1, 2)) //@codeaction("add", ")", "refactor.inline", inline) +-} - --func (_ A) Hi() {} //@loc(AHi, "Hi") +-func add(x, y int) int { return x + y } - --type S struct { -- Field int //@loc(SField, "Field") -- R // embed a struct -- H // embed an interface --} +--- @inline/a/a.go -- +-package a - --type R struct { -- Field2 int //@loc(RField2, "Field2") +-func _() { +- println(1 + 2) //@codeaction("add", ")", "refactor.inline", inline) -} - --func (_ R) Hey() {} //@loc(RHey, "Hey") +-func add(x, y int) int { return x + y } +diff -urN a/gopls/internal/test/marker/testdata/codeaction/invertif.txt b/gopls/internal/test/marker/testdata/codeaction/invertif.txt +--- a/gopls/internal/test/marker/testdata/codeaction/invertif.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codeaction/invertif.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,218 +0,0 @@ +-This test exercises the 'invert if condition' code action. - --type H interface { //@loc(H, "H") -- Goodbye() //@loc(HGoodbye, "Goodbye") --} +--- p.go -- +-package invertif - --type I interface { //@loc(I, "I") -- B() //@loc(IB, "B") -- J +-import ( +- "fmt" +- "os" +-) +- +-func Boolean() { +- b := true +- if b { //@codeactionedit("if b", "refactor.rewrite", boolean) +- fmt.Println("A") +- } else { +- fmt.Println("B") +- } -} - --type J interface { //@loc(J, "J") -- Hello() //@loc(JHello, "Hello") +-func BooleanFn() { +- if os.IsPathSeparator('X') { //@codeactionedit("if os.IsPathSeparator('X')", "refactor.rewrite", boolean_fn) +- fmt.Println("A") +- } else { +- fmt.Println("B") +- } -} - ---- b/b.go -- --package b +-// Note that the comment here jumps to the wrong location. +-func DontRemoveParens() { +- a := false +- b := true +- if !(a || +- b) { //@codeactionedit("b", "refactor.rewrite", dont_remove_parens) +- fmt.Println("A") +- } else { +- fmt.Println("B") +- } +-} - --import "mod.com/a" //@loc(AImport, re"\".*\"") +-func ElseIf() { +- // No inversion expected when there's not else clause +- if len(os.Args) > 2 { +- fmt.Println("A") +- } - --type embed struct { -- F int //@loc(F, "F") +- // No inversion expected for else-if, that would become unreadable +- if len(os.Args) > 2 { +- fmt.Println("A") +- } else if os.Args[0] == "X" { //@codeactionedit(re"if os.Args.0. == .X.", "refactor.rewrite", else_if) +- fmt.Println("B") +- } else { +- fmt.Println("C") +- } -} - --func (embed) M() //@loc(M, "M") -- --type Embed struct { -- embed -- *a.A -- a.I -- a.S +-func GreaterThan() { +- if len(os.Args) > 2 { //@codeactionedit("i", "refactor.rewrite", greater_than) +- fmt.Println("A") +- } else { +- fmt.Println("B") +- } -} - --func _() { -- e := Embed{} -- e.Hi() //@def("Hi", AHi),hover("Hi", "Hi", AHi) -- e.B() //@def("B", IB),hover("B", "B", IB) -- _ = e.Field //@def("Field", SField),hover("Field", "Field", SField) -- _ = e.Field2 //@def("Field2", RField2),hover("Field2", "Field2", RField2) -- e.Hello() //@def("Hello", JHello),hover("Hello", "Hello",JHello) -- e.Hey() //@def("Hey", RHey),hover("Hey", "Hey", RHey) -- e.Goodbye() //@def("Goodbye", HGoodbye),hover("Goodbye", "Goodbye", HGoodbye) -- e.M() //@def("M", M),hover("M", "M", M) -- _ = e.F //@def("F", F),hover("F", "F", F) +-func NotBoolean() { +- b := true +- if !b { //@codeactionedit("if !b", "refactor.rewrite", not_boolean) +- fmt.Println("A") +- } else { +- fmt.Println("B") +- } -} - --type aAlias = a.A //@loc(aAlias, "aAlias") +-func RemoveElse() { +- if true { //@codeactionedit("if true", "refactor.rewrite", remove_else) +- fmt.Println("A") +- } else { +- fmt.Println("B") +- return +- } - --type S1 struct { //@loc(S1, "S1") -- F1 int //@loc(S1F1, "F1") -- S2 //@loc(S1S2, "S2"),def("S2", S2),hover("S2", "S2", S2) -- a.A //@def("A", AString),hover("A", "A", aA) -- aAlias //@def("a", aAlias),hover("a", "aAlias", aAlias) +- fmt.Println("C") -} - --type S2 struct { //@loc(S2, "S2") -- F1 string //@loc(S2F1, "F1") -- F2 int //@loc(S2F2, "F2") -- *a.A //@def("A", AString),def("a",AImport) +-func RemoveParens() { +- b := true +- if !(b) { //@codeactionedit("if", "refactor.rewrite", remove_parens) +- fmt.Println("A") +- } else { +- fmt.Println("B") +- } -} - --type S3 struct { -- F1 struct { -- a.A //@def("A", AString) +-func Semicolon() { +- if _, err := fmt.Println("x"); err != nil { //@codeactionedit("if", "refactor.rewrite", semicolon) +- fmt.Println("A") +- } else { +- fmt.Println("B") - } -} - --func Bar() { -- var x S1 //@def("S1", S1),hover("S1", "S1", S1) -- _ = x.S2 //@def("S2", S1S2),hover("S2", "S2", S1S2) -- _ = x.F1 //@def("F1", S1F1),hover("F1", "F1", S1F1) -- _ = x.F2 //@def("F2", S2F2),hover("F2", "F2", S2F2) -- _ = x.S2.F1 //@def("F1", S2F1),hover("F1", "F1", S2F1) +-func SemicolonAnd() { +- if n, err := fmt.Println("x"); err != nil && n > 0 { //@codeactionedit("f", "refactor.rewrite", semicolon_and) +- fmt.Println("A") +- } else { +- fmt.Println("B") +- } -} - ---- b/c.go -- --package b -- --var _ = S1{ //@def("S1", S1),hover("S1", "S1", S1) -- F1: 99, //@def("F1", S1F1),hover("F1", "F1", S1F1) +-func SemicolonOr() { +- if n, err := fmt.Println("x"); err != nil || n < 5 { //@codeactionedit(re"if n, err := fmt.Println..x..; err != nil .. n < 5", "refactor.rewrite", semicolon_or) +- fmt.Println("A") +- } else { +- fmt.Println("B") +- } -} - ---- @AHi/hover.md -- --```go --func (a.A).Hi() --``` -- --[`(a.A).Hi` on pkg.go.dev](https://pkg.go.dev/mod.com/a#A.Hi) ---- @F/hover.md -- --```go --field F int --``` -- --@loc(F, "F") -- -- --[`(b.Embed).F` on pkg.go.dev](https://pkg.go.dev/mod.com/b#Embed.F) ---- @HGoodbye/hover.md -- --```go --func (a.H).Goodbye() --``` -- --@loc(HGoodbye, "Goodbye") +--- @boolean/p.go -- +-@@ -10,3 +10 @@ +-- if b { //@codeactionedit("if b", "refactor.rewrite", boolean) +-- fmt.Println("A") +-- } else { +-+ if !b { +-@@ -14 +12,2 @@ +-+ } else { //@codeactionedit("if b", "refactor.rewrite", boolean) +-+ fmt.Println("A") +--- @boolean_fn/p.go -- +-@@ -18,3 +18 @@ +-- if os.IsPathSeparator('X') { //@codeactionedit("if os.IsPathSeparator('X')", "refactor.rewrite", boolean_fn) +-- fmt.Println("A") +-- } else { +-+ if !os.IsPathSeparator('X') { +-@@ -22 +20,2 @@ +-+ } else { //@codeactionedit("if os.IsPathSeparator('X')", "refactor.rewrite", boolean_fn) +-+ fmt.Println("A") +--- @dont_remove_parens/p.go -- +-@@ -29,4 +29,2 @@ +-- if !(a || +-- b) { //@codeactionedit("b", "refactor.rewrite", dont_remove_parens) +-- fmt.Println("A") +-- } else { +-+ if (a || +-+ b) { +-@@ -34 +32,2 @@ +-+ } else { //@codeactionedit("b", "refactor.rewrite", dont_remove_parens) +-+ fmt.Println("A") +--- @else_if/p.go -- +-@@ -46,3 +46 @@ +-- } else if os.Args[0] == "X" { //@codeactionedit(re"if os.Args.0. == .X.", "refactor.rewrite", else_if) +-- fmt.Println("B") +-- } else { +-+ } else if os.Args[0] != "X" { +-@@ -50 +48,2 @@ +-+ } else { //@codeactionedit(re"if os.Args.0. == .X.", "refactor.rewrite", else_if) +-+ fmt.Println("B") +--- @greater_than/p.go -- +-@@ -54,3 +54 @@ +-- if len(os.Args) > 2 { //@codeactionedit("i", "refactor.rewrite", greater_than) +-- fmt.Println("A") +-- } else { +-+ if len(os.Args) <= 2 { +-@@ -58 +56,2 @@ +-+ } else { //@codeactionedit("i", "refactor.rewrite", greater_than) +-+ fmt.Println("A") +--- @not_boolean/p.go -- +-@@ -63,3 +63 @@ +-- if !b { //@codeactionedit("if !b", "refactor.rewrite", not_boolean) +-- fmt.Println("A") +-- } else { +-+ if b { +-@@ -67 +65,2 @@ +-+ } else { //@codeactionedit("if !b", "refactor.rewrite", not_boolean) +-+ fmt.Println("A") +--- @remove_else/p.go -- +-@@ -71,3 +71 @@ +-- if true { //@codeactionedit("if true", "refactor.rewrite", remove_else) +-- fmt.Println("A") +-- } else { +-+ if false { +-@@ -78 +76,3 @@ +-+ //@codeactionedit("if true", "refactor.rewrite", remove_else) +-+ fmt.Println("A") +-+ +--- @remove_parens/p.go -- +-@@ -83,3 +83 @@ +-- if !(b) { //@codeactionedit("if", "refactor.rewrite", remove_parens) +-- fmt.Println("A") +-- } else { +-+ if b { +-@@ -87 +85,2 @@ +-+ } else { //@codeactionedit("if", "refactor.rewrite", remove_parens) +-+ fmt.Println("A") +--- @semicolon/p.go -- +-@@ -91,3 +91 @@ +-- if _, err := fmt.Println("x"); err != nil { //@codeactionedit("if", "refactor.rewrite", semicolon) +-- fmt.Println("A") +-- } else { +-+ if _, err := fmt.Println("x"); err == nil { +-@@ -95 +93,2 @@ +-+ } else { //@codeactionedit("if", "refactor.rewrite", semicolon) +-+ fmt.Println("A") +--- @semicolon_and/p.go -- +-@@ -99,3 +99 @@ +-- if n, err := fmt.Println("x"); err != nil && n > 0 { //@codeactionedit("f", "refactor.rewrite", semicolon_and) +-- fmt.Println("A") +-- } else { +-+ if n, err := fmt.Println("x"); err == nil || n <= 0 { +-@@ -103 +101,2 @@ +-+ } else { //@codeactionedit("f", "refactor.rewrite", semicolon_and) +-+ fmt.Println("A") +--- @semicolon_or/p.go -- +-@@ -107,3 +107 @@ +-- if n, err := fmt.Println("x"); err != nil || n < 5 { //@codeactionedit(re"if n, err := fmt.Println..x..; err != nil .. n < 5", "refactor.rewrite", semicolon_or) +-- fmt.Println("A") +-- } else { +-+ if n, err := fmt.Println("x"); err == nil && n >= 5 { +-@@ -111 +109,2 @@ +-+ } else { //@codeactionedit(re"if n, err := fmt.Println..x..; err != nil .. n < 5", "refactor.rewrite", semicolon_or) +-+ fmt.Println("A") +diff -urN a/gopls/internal/test/marker/testdata/codeaction/issue64558.txt b/gopls/internal/test/marker/testdata/codeaction/issue64558.txt +--- a/gopls/internal/test/marker/testdata/codeaction/issue64558.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codeaction/issue64558.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,14 +0,0 @@ +-Test of an inlining failure due to an ill-typed input program (#64558). - +--- go.mod -- +-module example.com +-go 1.18 - --[`(a.H).Goodbye` on pkg.go.dev](https://pkg.go.dev/mod.com/a#H.Goodbye) ---- @IB/hover.md -- --```go --func (a.I).B() --``` +--- a/a.go -- +-package a - --@loc(IB, "B") +-func _() { +- f(1, 2) //@ diag("2", re"too many arguments"), codeactionerr("f", ")", "refactor.inline", re`inlining failed \("args/params mismatch"\), likely because inputs were ill-typed`) +-} - +-func f(int) {} +diff -urN a/gopls/internal/test/marker/testdata/codeaction/removeparam_formatting.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam_formatting.txt +--- a/gopls/internal/test/marker/testdata/codeaction/removeparam_formatting.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codeaction/removeparam_formatting.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,55 +0,0 @@ +-This test exercises behavior of change signature refactoring with respect to +-comments. - --[`(a.I).B` on pkg.go.dev](https://pkg.go.dev/mod.com/a#I.B) ---- @JHello/hover.md -- --```go --func (a.J).Hello() --``` +-Currently, inline comments around arguments or parameters are dropped, which is +-probably acceptable. Fixing this is likely intractible without fixing comment +-representation in the AST. - --@loc(JHello, "Hello") +--- go.mod -- +-module unused.mod - +-go 1.18 - --[`(a.J).Hello` on pkg.go.dev](https://pkg.go.dev/mod.com/a#J.Hello) ---- @M/hover.md -- --```go --func (embed).M() --``` +--- a/a.go -- +-package a - --[`(b.Embed).M` on pkg.go.dev](https://pkg.go.dev/mod.com/b#Embed.M) ---- @RField2/hover.md -- --```go --field Field2 int --``` +-// A doc comment. +-func A(x /* used parameter */, unused int /* unused parameter */ ) int { //@codeaction("unused", "unused", "refactor.rewrite", a) +- // about to return +- return x // returning +- // just returned +-} - --@loc(RField2, "Field2") +-// This function makes calls. +-func _() { +- // about to call +- A(one() /* used arg */, 2 /* unused arg */) // calling +- // just called +-} - +-func one() int { +- // I should be unaffected! +- return 1 +-} - --[`(a.R).Field2` on pkg.go.dev](https://pkg.go.dev/mod.com/a#R.Field2) ---- @RHey/hover.md -- --```go --func (a.R).Hey() --``` +--- @a/a/a.go -- +-package a - --[`(a.R).Hey` on pkg.go.dev](https://pkg.go.dev/mod.com/a#R.Hey) ---- @S1/hover.md -- --```go --type S1 struct { -- F1 int //@loc(S1F1, "F1") -- S2 //@loc(S1S2, "S2"),def("S2", S2),hover("S2", "S2", S2) -- a.A //@def("A", AString),hover("A", "A", aA) -- aAlias //@def("a", aAlias),hover("a", "aAlias", aAlias) +-// A doc comment. +-func A(x int) int { //@codeaction("unused", "unused", "refactor.rewrite", a) +- // about to return +- return x // returning +- // just returned -} --``` - --[`b.S1` on pkg.go.dev](https://pkg.go.dev/mod.com/b#S1) ---- @S1F1/hover.md -- --```go --field F1 int --``` +-// This function makes calls. +-func _() { +- // about to call +- A(one()) // calling +- // just called +-} - --@loc(S1F1, "F1") +-func one() int { +- // I should be unaffected! +- return 1 +-} +diff -urN a/gopls/internal/test/marker/testdata/codeaction/removeparam_funcvalue.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam_funcvalue.txt +--- a/gopls/internal/test/marker/testdata/codeaction/removeparam_funcvalue.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codeaction/removeparam_funcvalue.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,19 +0,0 @@ +-This test exercises change signature refactoring handling of function values. - +-TODO(rfindley): use a literalization strategy to allow these references. - --[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/mod.com/b#S1.F1) ---- @S1S2/hover.md -- --```go --field S2 S2 --``` +--- go.mod -- +-module unused.mod - --@loc(S1S2, "S2"),def("S2", S2),hover("S2", "S2", S2) +-go 1.18 - +--- a/a.go -- +-package a - --[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/mod.com/b#S1.S2) ---- @S2/hover.md -- --```go --type S2 struct { -- F1 string //@loc(S2F1, "F1") -- F2 int //@loc(S2F2, "F2") -- *a.A //@def("A", AString),def("a",AImport) +-func A(x, unused int) int { //@codeactionerr("unused", "unused", "refactor.rewrite", re"non-call function reference") +- return x -} --``` -- --[`b.S2` on pkg.go.dev](https://pkg.go.dev/mod.com/b#S2) ---- @S2F1/hover.md -- --```go --field F1 string --``` -- --@loc(S2F1, "F1") -- -- --[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/mod.com/b#S2.F1) ---- @S2F2/hover.md -- --```go --field F2 int --``` - --@loc(S2F2, "F2") +-func _() { +- _ = A +-} +diff -urN a/gopls/internal/test/marker/testdata/codeaction/removeparam_imports.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam_imports.txt +--- a/gopls/internal/test/marker/testdata/codeaction/removeparam_imports.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codeaction/removeparam_imports.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,160 +0,0 @@ +-This test checks the behavior of removing a parameter with respect to various +-import scenarios. - +--- go.mod -- +-module mod.test - --[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/mod.com/b#S2.F2) ---- @SField/hover.md -- --```go --field Field int --``` +-go 1.21 - --@loc(SField, "Field") - +--- a/a1.go -- +-package a - --[`(a.S).Field` on pkg.go.dev](https://pkg.go.dev/mod.com/a#S.Field) ---- @aA/hover.md -- --```go --type A string +-import "mod.test/b" - --func (a.A).Hi() --``` +-func _() { +- b.B(<-b.Chan, <-b.Chan) +-} - --@loc(AString, "A") +--- a/a2.go -- +-package a - +-import "mod.test/b" - --[`a.A` on pkg.go.dev](https://pkg.go.dev/mod.com/a#A) ---- @aAlias/hover.md -- --```go --type aAlias = a.A +-func _() { +- b.B(<-b.Chan, <-b.Chan) +- b.B(<-b.Chan, <-b.Chan) +-} - --func (a.A).Hi() --``` +--- a/a3.go -- +-package a - --@loc(aAlias, "aAlias") -diff -urN a/gopls/internal/regtest/marker/testdata/definition/import.txt b/gopls/internal/regtest/marker/testdata/definition/import.txt ---- a/gopls/internal/regtest/marker/testdata/definition/import.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/definition/import.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,52 +0,0 @@ --This test checks definition and hover over imports. ---- go.mod -- --module mod.com +-import "mod.test/b" - --go 1.18 ---- foo/foo.go -- --package foo +-func _() { +- b.B(<-b.Chan, <-b.Chan) +-} - --type Foo struct{} +-func _() { +- b.B(<-b.Chan, <-b.Chan) +-} - --// DoFoo does foo. --func DoFoo() {} //@loc(DoFoo, "DoFoo") ---- bar/bar.go -- --package bar +--- a/a4.go -- +-package a - +-// TODO(rfindley/adonovan): inlining here adds an additional import of +-// mod.test/b. Can we do better? -import ( -- myFoo "mod.com/foo" //@loc(myFoo, "myFoo") +- . "mod.test/b" -) - --var _ *myFoo.Foo //@def("myFoo", myFoo),hover("myFoo", "myFoo", myFoo) ---- bar/dotimport.go -- --package bar -- --import . "mod.com/foo" -- -func _() { -- // variable of type foo.Foo -- var _ Foo //@hover("_", "_", FooVar) -- -- DoFoo() //@hover("DoFoo", "DoFoo", DoFoo) +- B(<-Chan, <-Chan) -} ---- @DoFoo/hover.md -- --```go --func DoFoo() --``` -- --DoFoo does foo. - +--- b/b.go -- +-package b - --[`foo.DoFoo` on pkg.go.dev](https://pkg.go.dev/mod.com/foo#DoFoo) ---- @FooVar/hover.md -- --```go --var _ Foo --``` -- --variable of type foo.Foo ---- @myFoo/hover.md -- --```go --package myFoo ("mod.com/foo") --``` +-import "mod.test/c" - --[`myFoo` on pkg.go.dev](https://pkg.go.dev/mod.com/foo) -diff -urN a/gopls/internal/regtest/marker/testdata/definition/misc.txt b/gopls/internal/regtest/marker/testdata/definition/misc.txt ---- a/gopls/internal/regtest/marker/testdata/definition/misc.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/definition/misc.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,230 +0,0 @@ --This test exercises miscellaneous definition and hover requests. ---- go.mod -- --module mod.com +-var Chan chan c.C - --go 1.16 ---- a.go -- --package a //@loc(aPackage, re"package (a)"),hover(aPackage, aPackage, aPackage) +-func B(x, y c.C) { //@codeaction("x", "x", "refactor.rewrite", b) +-} - --var ( -- // x is a variable. -- x string //@loc(x, "x"),hover(x, x, hoverx) --) +--- c/c.go -- +-package c - --// Constant block. When I hover on h, I should see this comment. --const ( -- // When I hover on g, I should see this comment. -- g = 1 //@hover("g", "g", hoverg) +-type C int - -- h = 2 //@hover("h", "h", hoverh) --) +--- d/d.go -- +-package d - --// z is a variable too. --var z string //@loc(z, "z"),hover(z, z, hoverz) +-// Removing the parameter should remove this import. +-import "mod.test/c" - --func AStuff() { //@loc(AStuff, "AStuff") -- x := 5 -- Random2(x) //@def("dom2", Random2) -- Random() //@def("()", Random) +-func D(x c.C) { //@codeaction("x", "x", "refactor.rewrite", d) -} - --type H interface { //@loc(H, "H") -- Goodbye() +-func _() { +- D(1) -} - --type I interface { //@loc(I, "I") -- B() -- J --} +--- @b/a/a1.go -- +-package a - --type J interface { //@loc(J, "J") -- Hello() --} +-import ( +- "mod.test/b" +- "mod.test/c" +-) - -func _() { -- // 1st type declaration block -- type ( -- a struct { //@hover("a", "a", hoverDeclBlocka) -- x string -- } -- ) -- -- // 2nd type declaration block -- type ( -- // b has a comment -- b struct{} //@hover("b", "b", hoverDeclBlockb) -- ) -- -- // 3rd type declaration block -- type ( -- // c is a struct -- c struct { //@hover("c", "c", hoverDeclBlockc) -- f string -- } +- var _ c.C = <-b.Chan +- b.B(<-b.Chan) +-} +--- @b/a/a2.go -- +-package a - -- d string //@hover("d", "d", hoverDeclBlockd) -- ) +-import ( +- "mod.test/b" +- "mod.test/c" +-) - -- type ( -- e struct { //@hover("e", "e", hoverDeclBlocke) -- f float64 -- } // e has a comment -- ) +-func _() { +- var _ c.C = <-b.Chan +- b.B(<-b.Chan) +- var _ c.C = <-b.Chan +- b.B(<-b.Chan) -} +--- @b/a/a3.go -- +-package a - --var ( -- hh H //@hover("H", "H", hoverH) -- ii I //@hover("I", "I", hoverI) -- jj J //@hover("J", "J", hoverJ) +-import ( +- "mod.test/b" +- "mod.test/c" -) ---- a_test.go -- +- +-func _() { +- var _ c.C = <-b.Chan +- b.B(<-b.Chan) +-} +- +-func _() { +- var _ c.C = <-b.Chan +- b.B(<-b.Chan) +-} +--- @b/a/a4.go -- -package a - +-// TODO(rfindley/adonovan): inlining here adds an additional import of +-// mod.test/b. Can we do better? -import ( -- "testing" +- "mod.test/b" +- . "mod.test/b" +- "mod.test/c" -) - --func TestA(t *testing.T) { //@hover("TestA", "TestA", hoverTestA) +-func _() { +- var _ c.C = <-Chan +- b.B(<-Chan) -} ---- random.go -- --package a +--- @b/b/b.go -- +-package b - --func Random() int { //@loc(Random, "Random") -- y := 6 + 7 -- return y +-import "mod.test/c" +- +-var Chan chan c.C +- +-func B(y c.C) { //@codeaction("x", "x", "refactor.rewrite", b) -} +--- @d/d/d.go -- +-package d - --func Random2(y int) int { //@loc(Random2, "Random2"),loc(RandomParamY, "y") -- return y //@def("y", RandomParamY),hover("y", "y", hovery) +-// Removing the parameter should remove this import. +- +-func D() { //@codeaction("x", "x", "refactor.rewrite", d) -} - --type Pos struct { -- x, y int //@loc(PosX, "x"),loc(PosY, "y") +-func _() { +- D() -} +diff -urN a/gopls/internal/test/marker/testdata/codeaction/removeparam_method.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam_method.txt +--- a/gopls/internal/test/marker/testdata/codeaction/removeparam_method.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codeaction/removeparam_method.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,123 +0,0 @@ +-This test verifies that gopls can remove unused parameters from methods. - --// Typ has a comment. Its fields do not. --type Typ struct{ field string } //@loc(TypField, "field") +-Specifically, check +-1. basic removal of unused parameters, when the receiver is named, locally and +- across package boundaries +-2. handling of unnamed receivers - --func _() { -- x := &Typ{} -- _ = x.field //@def("field", TypField),hover("field", "field", hoverfield) +--- go.mod -- +-module example.com/rm +- +-go 1.20 +- +--- basic.go -- +-package rm +- +-type Basic int +- +-func (t Basic) Foo(x int) { //@codeaction("x", "x", "refactor.rewrite", basic) -} - --func (p *Pos) Sum() int { //@loc(PosSum, "Sum") -- return p.x + p.y //@hover("x", "x", hoverpx) +-func _(b Basic) { +- b.Foo(1) +- // TODO(rfindley): methodexprs should not get rewritten as methods. +- Basic.Foo(1, 2) -} - +--- basicuse/p.go -- +-package basicuse +- +-import "example.com/rm" +- -func _() { -- var p Pos -- _ = p.Sum() //@def("()", PosSum),hover("()", `Sum`, hoverSum) +- x := new(rm.Basic) +- x.Foo(sideEffects()) +- rm.Basic.Foo(1,2) -} ---- @aPackage/hover.md -- ---- @hoverDeclBlocka/hover.md -- --```go --type a struct { -- x string --} --``` - --1st type declaration block ---- @hoverDeclBlockb/hover.md -- --```go --type b struct{} --``` +-func sideEffects() int - --b has a comment ---- @hoverDeclBlockc/hover.md -- --```go --type c struct { -- f string --} --``` +--- @basic/basic.go -- +-package rm - --c is a struct ---- @hoverDeclBlockd/hover.md -- --```go --type d string --``` +-type Basic int - --3rd type declaration block ---- @hoverDeclBlocke/hover.md -- --```go --type e struct { -- f float64 +-func (t Basic) Foo() { //@codeaction("x", "x", "refactor.rewrite", basic) -} --``` - --e has a comment ---- @hoverH/hover.md -- --```go --type H interface { -- Goodbye() +-func _(b Basic) { +- b.Foo() +- // TODO(rfindley): methodexprs should not get rewritten as methods. +- Basic(1).Foo() -} --``` +--- @basic/basicuse/p.go -- +-package basicuse - --[`a.H` on pkg.go.dev](https://pkg.go.dev/mod.com#H) ---- @hoverI/hover.md -- --```go --type I interface { -- B() -- J --} --``` +-import "example.com/rm" - --[`a.I` on pkg.go.dev](https://pkg.go.dev/mod.com#I) ---- @hoverJ/hover.md -- --```go --type J interface { -- Hello() +-func _() { +- x := new(rm.Basic) +- var ( +- t rm.Basic = *x +- _ int = sideEffects() +- ) +- t.Foo() +- rm.Basic(1).Foo() -} --``` - --[`a.J` on pkg.go.dev](https://pkg.go.dev/mod.com#J) ---- @hoverSum/hover.md -- --```go --func (*Pos).Sum() int --``` +-func sideEffects() int +--- missingrecv.go -- +-package rm - --[`(a.Pos).Sum` on pkg.go.dev](https://pkg.go.dev/mod.com#Pos.Sum) ---- @hoverTestA/hover.md -- --```go --func TestA(t *testing.T) --``` ---- @hoverfield/hover.md -- --```go --field field string --``` ---- @hoverg/hover.md -- --```go --const g untyped int = 1 --``` +-type Missing struct{} - --When I hover on g, I should see this comment. ---- @hoverh/hover.md -- --```go --const h untyped int = 2 --``` +-var r2 int - --Constant block. When I hover on h, I should see this comment. ---- @hoverpx/hover.md -- --```go --field x int --``` +-func (Missing) M(a, b, c, r0 int) (r1 int) { //@codeaction("b", "b", "refactor.rewrite", missingrecv) +- return a + c +-} - --@loc(PosX, "x"),loc(PosY, "y") ---- @hoverx/hover.md -- --```go --var x string --``` +-func _() { +- m := &Missing{} +- _ = m.M(1, 2, 3, 4) +-} - --x is a variable. ---- @hovery/hover.md -- --```go --var y int --``` ---- @hoverz/hover.md -- --```go --var z string --``` +--- missingrecvuse/p.go -- +-package missingrecvuse - --z is a variable too. -diff -urN a/gopls/internal/regtest/marker/testdata/diagnostics/addgowork.txt b/gopls/internal/regtest/marker/testdata/diagnostics/addgowork.txt ---- a/gopls/internal/regtest/marker/testdata/diagnostics/addgowork.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/diagnostics/addgowork.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,50 +0,0 @@ --This test demonstrates diagnostics for adding a go.work file. +-import "example.com/rm" - --Quick-fixes change files on disk, so are tested by regtests. +-func _() { +- x := rm.Missing{} +- x.M(1, sideEffects(), 3, 4) +-} - --TODO(rfindley): improve the "cannot find package" import errors. +-func sideEffects() int - ---- skip -- --Skipping due to go.dev/issue/60584#issuecomment-1622238115. --There appears to be a real race in the critical error logic causing this test --to flake with high frequency. +--- @missingrecv/missingrecv.go -- +-package rm - ---- flags -- ---min_go=go1.18 +-type Missing struct{} - ---- a/go.mod -- --module mod.com/a +-var r2 int - --go 1.18 +-func (Missing) M(a, c, r0 int) (r1 int) { //@codeaction("b", "b", "refactor.rewrite", missingrecv) +- return a + c +-} - ---- a/main.go -- --package main //@diag("main", re"add a go.work file") +-func _() { +- m := &Missing{} +- _ = (*m).M(1, 3, 4) +-} +--- @missingrecv/missingrecvuse/p.go -- +-package missingrecvuse - --import "mod.com/a/lib" //@diag("\"mod.com", re"cannot find package") +-import "example.com/rm" - --func main() { -- _ = lib.C +-func _() { +- x := rm.Missing{} +- var _ int = sideEffects() +- x.M(1, 3, 4) -} - ---- a/lib/lib.go -- --package lib //@diag("lib", re"add a go.work file") +-func sideEffects() int +diff -urN a/gopls/internal/test/marker/testdata/codeaction/removeparam_resolve.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam_resolve.txt +--- a/gopls/internal/test/marker/testdata/codeaction/removeparam_resolve.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codeaction/removeparam_resolve.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,258 +0,0 @@ +-This test exercises the refactoring to remove unused parameters, with resolve support. +-See removeparam.txt for same test without resolve support. - --const C = "b" ---- b/go.mod -- --module mod.com/b +--- capabilities.json -- +-{ +- "textDocument": { +- "codeAction": { +- "dataSupport": true, +- "resolveSupport": { +- "properties": ["edit"] +- } +- } +- } +-} +--- go.mod -- +-module unused.mod - -go 1.18 - ---- b/main.go -- --package main //@diag("main", re"add a go.work file") +--- a/a.go -- +-package a - --import "mod.com/b/lib" //@diag("\"mod.com", re"cannot find package") +-func A(x, unused int) int { //@codeaction("unused", "unused", "refactor.rewrite", a) +- return x +-} - --func main() { -- _ = lib.C +--- @a/a/a.go -- +-package a +- +-func A(x int) int { //@codeaction("unused", "unused", "refactor.rewrite", a) +- return x -} - ---- b/lib/lib.go -- --package lib //@diag("lib", re"add a go.work file") +--- a/a2.go -- +-package a - --const C = "b" -diff -urN a/gopls/internal/regtest/marker/testdata/diagnostics/analyzers.txt b/gopls/internal/regtest/marker/testdata/diagnostics/analyzers.txt ---- a/gopls/internal/regtest/marker/testdata/diagnostics/analyzers.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/diagnostics/analyzers.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,57 +0,0 @@ --Test of warning diagnostics from various analyzers: --copylocks, printf, slog, tests, timeformat, and nilness. +-func _() { +- A(1, 2) +-} - ---- go.mod -- --module example.com --go 1.12 +--- a/a_test.go -- +-package a - ---- flags -- ---min_go=go1.21 +-func _() { +- A(1, 2) +-} - ---- bad_test.go -- --package analyzer +--- a/a_x_test.go -- +-package a_test - --import ( -- "fmt" -- "log/slog" -- "sync" -- "testing" -- "time" --) +-import "unused.mod/a" - --// copylocks -func _() { -- var x sync.Mutex -- _ = x //@diag("x", re"assignment copies lock value to _: sync.Mutex") +- a.A(1, 2) -} - --// printf --func _() { -- printfWrapper("%s") //@diag(re`printfWrapper\(.*\)`, re"example.com.printfWrapper format %s reads arg #1, but call has 0 args") +--- b/b.go -- +-package b +- +-import "unused.mod/a" +- +-func f() int { +- return 1 -} - --func printfWrapper(format string, args ...interface{}) { -- fmt.Printf(format, args...) +-func g() int { +- return 2 -} - --// slog -func _() { -- slog.Info("msg", 1) //@diag("1", re`slog.Info arg "1" should be a string or a slog.Attr`) +- a.A(f(), 1) -} - --// tests --func Testbad(t *testing.T) { //@diag("", re"Testbad has malformed name: first letter after 'Test' must not be lowercase") --} +--- @a/a/a2.go -- +-package a - --// timeformat -func _() { -- now := time.Now() -- fmt.Println(now.Format("2006-02-01")) //@diag("2006-02-01", re"2006-02-01 should be 2006-01-02") +- A(1) -} +--- @a/a/a_test.go -- +-package a - --// nilness --func _(ptr *int) { -- if ptr == nil { -- _ = *ptr //@diag("*ptr", re"nil dereference in load") -- } +-func _() { +- A(1) -} -diff -urN a/gopls/internal/regtest/marker/testdata/diagnostics/excludedfile.txt b/gopls/internal/regtest/marker/testdata/diagnostics/excludedfile.txt ---- a/gopls/internal/regtest/marker/testdata/diagnostics/excludedfile.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/diagnostics/excludedfile.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,38 +0,0 @@ --This test demonstrates diagnostics for various forms of file exclusion. +--- @a/a/a_x_test.go -- +-package a_test - --Skip on plan9, an arbitrary GOOS, so that we can exercise GOOS exclusions --resulting from file suffixes. +-import "unused.mod/a" - ---- flags -- ---min_go=go1.18 ---skip_goos=plan9 +-func _() { +- a.A(1) +-} +--- @a/b/b.go -- +-package b - ---- go.work -- --go 1.21 +-import "unused.mod/a" - --use ( -- ./a --) ---- a/go.mod -- --module mod.com/a +-func f() int { +- return 1 +-} - --go 1.18 +-func g() int { +- return 2 +-} - ---- a/a.go -- --package a +-func _() { +- a.A(f()) +-} +--- field/field.go -- +-package field - ---- a/a_plan9.go -- --package a //@diag(re"package (a)", re"excluded due to its GOOS/GOARCH") +-func Field(x int, field int) { //@codeaction("int", "int", "refactor.rewrite", field, "Refactor: remove unused parameter") +-} - ---- a/a_ignored.go -- --//go:build skip --package a //@diag(re"package (a)", re"excluded due to its build tags") +-func _() { +- Field(1, 2) +-} +--- @field/field/field.go -- +-package field - ---- b/go.mod -- --module mod.com/b +-func Field(field int) { //@codeaction("int", "int", "refactor.rewrite", field, "Refactor: remove unused parameter") +-} - --go 1.18 +-func _() { +- Field(2) +-} +--- ellipsis/ellipsis.go -- +-package ellipsis - ---- b/b.go -- --package b //@diag(re"package (b)", re"add this module to your go.work") +-func Ellipsis(...any) { //@codeaction("any", "any", "refactor.rewrite", ellipsis) +-} - -diff -urN a/gopls/internal/regtest/marker/testdata/diagnostics/generated.txt b/gopls/internal/regtest/marker/testdata/diagnostics/generated.txt ---- a/gopls/internal/regtest/marker/testdata/diagnostics/generated.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/diagnostics/generated.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,21 +0,0 @@ --Test of "undeclared" diagnostic in generated code. +-func _() { +- // TODO(rfindley): investigate the broken formatting resulting from these inlinings. +- Ellipsis() +- Ellipsis(1) +- Ellipsis(1, 2) +- Ellipsis(1, f(), g()) +- Ellipsis(h()) +- Ellipsis(i()...) +-} - ---- go.mod -- --module example.com --go 1.12 +-func f() int +-func g() int +-func h() (int, int) +-func i() []any - ---- generated.go -- --package generated +--- @ellipsis/ellipsis/ellipsis.go -- +-package ellipsis - --// Code generated by generator.go. DO NOT EDIT. +-func Ellipsis() { //@codeaction("any", "any", "refactor.rewrite", ellipsis) +-} - -func _() { -- var y int //@diag("y", re"y declared (and|but) not used") +- // TODO(rfindley): investigate the broken formatting resulting from these inlinings. +- Ellipsis() +- Ellipsis() +- Ellipsis() +- var _ []any = []any{1, f(), g()} +- Ellipsis() +- func(_ ...any) { +- Ellipsis() +- }(h()) +- var _ []any = i() +- Ellipsis() -} - ---- generator.go -- --package generated +-func f() int +-func g() int +-func h() (int, int) +-func i() []any +--- ellipsis2/ellipsis2.go -- +-package ellipsis2 +- +-func Ellipsis2(_, _ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2, "Refactor: remove unused parameter") +-} - -func _() { -- var x int //@diag("x", re"x declared (and|but) not used") +- Ellipsis2(1,2,3) +- Ellipsis2(h()) +- Ellipsis2(1,2, []int{3, 4}...) -} -diff -urN a/gopls/internal/regtest/marker/testdata/diagnostics/issue56943.txt b/gopls/internal/regtest/marker/testdata/diagnostics/issue56943.txt ---- a/gopls/internal/regtest/marker/testdata/diagnostics/issue56943.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/diagnostics/issue56943.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,22 +0,0 @@ --This test verifies that we produce diagnostics related to mismatching --unexported interface methods in non-workspace packages. - --Previously, we would fail to produce a diagnostic because we trimmed the AST. --See golang/go#56943. ---- main.go -- --package main +-func h() (int, int) - --import ( -- "go/ast" -- "go/token" --) +--- @ellipsis2/ellipsis2/ellipsis2.go -- +-package ellipsis2 - --func main() { -- var a int //@diag(re"(a) int", re"a declared.*not used") -- var _ ast.Expr = node{} //@diag("node{}", re"missing.*exprNode") +-func Ellipsis2(_ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2, "Refactor: remove unused parameter") -} - --type node struct{} -- --func (node) Pos() token.Pos { return 0 } --func (node) End() token.Pos { return 0 } -diff -urN a/gopls/internal/regtest/marker/testdata/diagnostics/issue59005.txt b/gopls/internal/regtest/marker/testdata/diagnostics/issue59005.txt ---- a/gopls/internal/regtest/marker/testdata/diagnostics/issue59005.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/diagnostics/issue59005.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,20 +0,0 @@ --This test verifies that we don't drop type checking errors on the floor when we --fail to compute positions for their related errors. +-func _() { +- Ellipsis2(2, []int{3}...) +- func(_, blank0 int, rest ...int) { +- Ellipsis2(blank0, rest...) +- }(h()) +- Ellipsis2(2, []int{3, 4}...) +-} - ---- go.mod -- --module play.ground +-func h() (int, int) +--- overlapping/overlapping.go -- +-package overlapping - ---- p.go -- --package p +-func Overlapping(i int) int { //@codeactionerr(re"(i) int", re"(i) int", "refactor.rewrite", re"overlapping") +- return 0 +-} - --import ( -- . "play.ground/foo" --) +-func _() { +- x := Overlapping(Overlapping(0)) +- _ = x +-} - --const C = 1 //@diag("C", re"C already declared through dot-import") --var _ = C +--- effects/effects.go -- +-package effects - ---- foo/foo.go -- --package foo +-func effects(x, y int) int { //@codeaction("y", "y", "refactor.rewrite", effects), diag("y", re"unused") +- return x +-} - --const C = 2 -diff -urN a/gopls/internal/regtest/marker/testdata/diagnostics/issue60544.txt b/gopls/internal/regtest/marker/testdata/diagnostics/issue60544.txt ---- a/gopls/internal/regtest/marker/testdata/diagnostics/issue60544.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/diagnostics/issue60544.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,13 +0,0 @@ --This test exercises a crash due to treatment of "comparable" in methodset --calculation (golang/go#60544). +-func f() int +-func g() int - ---min_go is 1.19 as the error message changed at this Go version. ---- flags -- ---min_go=go1.19 +-func _() { +- effects(f(), g()) +- effects(f(), g()) +-} +--- @effects/effects/effects.go -- +-package effects - ---- main.go -- --package main +-func effects(x int) int { //@codeaction("y", "y", "refactor.rewrite", effects), diag("y", re"unused") +- return x +-} - --type X struct{} +-func f() int +-func g() int - --func (X) test(x comparable) {} //@diag("comparable", re"outside a type constraint") -diff -urN a/gopls/internal/regtest/marker/testdata/diagnostics/issue60605.txt b/gopls/internal/regtest/marker/testdata/diagnostics/issue60605.txt ---- a/gopls/internal/regtest/marker/testdata/diagnostics/issue60605.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/diagnostics/issue60605.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,12 +0,0 @@ --This test verifies that we can export constants with unknown kind. --Previously, the exporter would panic while attempting to convert such constants --to their target type (float64, in this case). +-func _() { +- var x, _ int = f(), g() +- effects(x) +- { +- var x, _ int = f(), g() +- effects(x) +- } +-} +--- recursive/recursive.go -- +-package recursive - ---- go.mod -- --module mod.txt/p +-func Recursive(x int) int { //@codeaction("x", "x", "refactor.rewrite", recursive) +- return Recursive(1) +-} - --go 1.20 ---- p.go -- --package p +--- @recursive/recursive/recursive.go -- +-package recursive - --const EPSILON float64 = 1e- //@diag(re"1e-()", re"exponent has no digits") -diff -urN a/gopls/internal/regtest/marker/testdata/diagnostics/parseerr.txt b/gopls/internal/regtest/marker/testdata/diagnostics/parseerr.txt ---- a/gopls/internal/regtest/marker/testdata/diagnostics/parseerr.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/diagnostics/parseerr.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,27 +0,0 @@ +-func Recursive() int { //@codeaction("x", "x", "refactor.rewrite", recursive) +- return Recursive() +-} +diff -urN a/gopls/internal/test/marker/testdata/codeaction/removeparam_satisfies.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam_satisfies.txt +--- a/gopls/internal/test/marker/testdata/codeaction/removeparam_satisfies.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codeaction/removeparam_satisfies.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,62 +0,0 @@ +-This test verifies that gopls can remove unused parameters from methods, +-when that method satisfies an interface. - --This test exercises diagnostics produced for syntax errors. +-For now, we just update static calls. In the future, we should compute the set +-of dynamic calls that must change (and therefore, the set of concrete functions +-that must be modified), in order to produce the desired outcome for our users. - --Because parser error recovery can be quite lossy, diagnostics --for type errors are suppressed in files with syntax errors; --see issue #59888. But diagnostics are reported for type errors --in well-formed files of the same package. +-Doing so would be more complicated, so for now this test simply records the +-current behavior. - --- go.mod -- --module example.com --go 1.12 -- ---- bad.go -- --package p +-module example.com/rm - --func f() { -- append("") // no diagnostic for type error in file containing syntax error --} +-go 1.20 - --func .() {} //@diag(re"func ().", re"expected 'IDENT', found '.'") +--- p.go -- +-package rm - ---- good.go -- --package p +-type T int - --func g() { -- append("") //@diag(re`""`, re"a slice") +-func (t T) Foo(x int) { //@codeaction("x", "x", "refactor.rewrite", basic) -} -diff -urN a/gopls/internal/regtest/marker/testdata/diagnostics/rundespiteerrors.txt b/gopls/internal/regtest/marker/testdata/diagnostics/rundespiteerrors.txt ---- a/gopls/internal/regtest/marker/testdata/diagnostics/rundespiteerrors.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/diagnostics/rundespiteerrors.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,27 +0,0 @@ --This test verifies that analyzers without RunDespiteErrors are not --executed on a package containing type errors (see issue #54762). - --We require go1.18 because the range of the `1 + ""` go/types error --changed then, and the new @diag marker is quite particular. +--- use/use.go -- +-package use - ---- go.mod -- --module example.com --go 1.12 +-import "example.com/rm" - ---- flags -- ---min_go=go1.18 +-type Fooer interface{ +- Foo(int) +-} - ---- a.go -- --package a +-var _ Fooer = rm.T(0) - -func _() { -- // A type error. -- _ = 1 + "" //@diag(`1 + ""`, re"mismatched types|cannot convert") +- var x rm.T +- x.Foo(1) +-} +--- @basic/p.go -- +-package rm - -- // A violation of an analyzer for which RunDespiteErrors=false: -- // no (simplifyrange, warning) diagnostic is produced; the diag -- // comment is merely illustrative. -- for _ = range "" { //diag("for _", "simplify range expression", ) +-type T int - -- } +-func (t T) Foo() { //@codeaction("x", "x", "refactor.rewrite", basic) -} -diff -urN a/gopls/internal/regtest/marker/testdata/diagnostics/typeerr.txt b/gopls/internal/regtest/marker/testdata/diagnostics/typeerr.txt ---- a/gopls/internal/regtest/marker/testdata/diagnostics/typeerr.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/diagnostics/typeerr.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,30 +0,0 @@ - --This test exercises diagnostics produced for type errors --in the absence of syntax errors. +--- @basic/use/use.go -- +-package use - --The type error was chosen to exercise the 'nonewvars' type-error analyzer. --(The 'undeclaredname' analyzer depends on the text of the go/types --"undeclared name" error, which changed in go1.20.) +-import "example.com/rm" - --The append() type error was also carefully chosen to have text and --position that are invariant across all versions of Go run by the builders. +-type Fooer interface { +- Foo(int) +-} +- +-var _ Fooer = rm.T(0) +- +-func _() { +- var x rm.T +- var t rm.T = x +- t.Foo() +-} +diff -urN a/gopls/internal/test/marker/testdata/codeaction/removeparam.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam.txt +--- a/gopls/internal/test/marker/testdata/codeaction/removeparam.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codeaction/removeparam.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,247 +0,0 @@ +-This test exercises the refactoring to remove unused parameters. +-See removeparam_resolve.txt for same test with resolve support. - --- go.mod -- --module example.com --go 1.12 +-module unused.mod - ---- typeerr.go -- --package a +-go 1.18 - --func f(x int) { -- append("") //@diag(re`""`, re"a slice") +--- a/a.go -- +-package a - -- x := 123 //@diag(re"x := 123", re"no new variables"), suggestedfix(re"():", re"no new variables", fix) +-func A(x, unused int) int { //@codeaction("unused", "unused", "refactor.rewrite", a) +- return x -} - ---- @fix/typeerr.go -- ----- before --+++ after --@@ -6 +6 @@ --- x := 123 //@diag(re"x := 123", re"no new variables"), suggestedfix(re"():", re"no new variables", fix) --+ x = 123 //@diag(re"x := 123", re"no new variables"), suggestedfix(re"():", re"no new variables", fix) -diff -urN a/gopls/internal/regtest/marker/testdata/diagnostics/useinternal.txt b/gopls/internal/regtest/marker/testdata/diagnostics/useinternal.txt ---- a/gopls/internal/regtest/marker/testdata/diagnostics/useinternal.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/diagnostics/useinternal.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,21 +0,0 @@ --This test checks a diagnostic for invalid use of internal packages. -- --This list error changed in Go 1.21. +--- @a/a/a.go -- +-package a - ---- flags -- ---min_go=go1.21 +-func A(x int) int { //@codeaction("unused", "unused", "refactor.rewrite", a) +- return x +-} - ---- go.mod -- --module bad.test +--- a/a2.go -- +-package a - --go 1.18 +-func _() { +- A(1, 2) +-} - ---- assign/internal/secret/secret.go -- --package secret +--- a/a_test.go -- +-package a - --func Hello() {} +-func _() { +- A(1, 2) +-} - ---- bad/bad.go -- --package bad +--- a/a_x_test.go -- +-package a_test - --import _ "bad.test/assign/internal/secret" //@diag("\"bad.test/assign/internal/secret\"", re"could not import bad.test/assign/internal/secret \\(invalid use of internal package \"bad.test/assign/internal/secret\"\\)"),diag("_", re"use of internal package bad.test/assign/internal/secret not allowed") -diff -urN a/gopls/internal/regtest/marker/testdata/diagnostics/usemodule.txt b/gopls/internal/regtest/marker/testdata/diagnostics/usemodule.txt ---- a/gopls/internal/regtest/marker/testdata/diagnostics/usemodule.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/diagnostics/usemodule.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,51 +0,0 @@ --This test demonstrates diagnostics for a module that is missing from the --go.work file. +-import "unused.mod/a" - --Quick-fixes change files on disk, so are tested by regtests. +-func _() { +- a.A(1, 2) +-} - ---- flags -- ---min_go=go1.18 +--- b/b.go -- +-package b - ---- go.work -- --go 1.21 +-import "unused.mod/a" - --use ( -- ./a --) +-func f() int { +- return 1 +-} - ---- a/go.mod -- --module mod.com/a +-func g() int { +- return 2 +-} - --go 1.18 +-func _() { +- a.A(f(), 1) +-} - ---- a/main.go -- --package main +--- @a/a/a2.go -- +-package a - --import "mod.com/a/lib" +-func _() { +- A(1) +-} +--- @a/a/a_test.go -- +-package a - --func main() { -- _ = lib.C +-func _() { +- A(1) -} +--- @a/a/a_x_test.go -- +-package a_test - ---- a/lib/lib.go -- --package lib +-import "unused.mod/a" - --const C = "b" ---- b/go.mod -- --module mod.com/b +-func _() { +- a.A(1) +-} +--- @a/b/b.go -- +-package b - --go 1.18 +-import "unused.mod/a" - ---- b/main.go -- --package main //@diag("main", re"add this module to your go.work") +-func f() int { +- return 1 +-} - --import "mod.com/b/lib" //@diag("\"mod.com", re"not included in a workspace module") +-func g() int { +- return 2 +-} - --func main() { -- _ = lib.C +-func _() { +- a.A(f()) -} +--- field/field.go -- +-package field - ---- b/lib/lib.go -- --package lib //@diag("lib", re"add this module to your go.work") +-func Field(x int, field int) { //@codeaction("int", "int", "refactor.rewrite", field, "Refactor: remove unused parameter") +-} - --const C = "b" -diff -urN a/gopls/internal/regtest/marker/testdata/fixedbugs/issue59318.txt b/gopls/internal/regtest/marker/testdata/fixedbugs/issue59318.txt ---- a/gopls/internal/regtest/marker/testdata/fixedbugs/issue59318.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/fixedbugs/issue59318.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,22 +0,0 @@ --This test verifies that we can load multiple orphaned files as --command-line-arguments packages. +-func _() { +- Field(1, 2) +-} +--- @field/field/field.go -- +-package field - --Previously, we would load only one because go/packages returns at most one --command-line-arguments package per query. +-func Field(field int) { //@codeaction("int", "int", "refactor.rewrite", field, "Refactor: remove unused parameter") +-} - ---- a/main.go -- --package main +-func _() { +- Field(2) +-} +--- ellipsis/ellipsis.go -- +-package ellipsis - --func main() { -- var a int //@diag(re"var (a)", re"not used") +-func Ellipsis(...any) { //@codeaction("any", "any", "refactor.rewrite", ellipsis) -} ---- b/main.go -- --package main - --func main() { -- var b int //@diag(re"var (b)", re"not used") +-func _() { +- // TODO(rfindley): investigate the broken formatting resulting from these inlinings. +- Ellipsis() +- Ellipsis(1) +- Ellipsis(1, 2) +- Ellipsis(1, f(), g()) +- Ellipsis(h()) +- Ellipsis(i()...) -} ---- c/go.mod -- --module c.com // The existence of this module avoids a workspace error. - --go 1.18 -diff -urN a/gopls/internal/regtest/marker/testdata/fixedbugs/issue59944.txt b/gopls/internal/regtest/marker/testdata/fixedbugs/issue59944.txt ---- a/gopls/internal/regtest/marker/testdata/fixedbugs/issue59944.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/fixedbugs/issue59944.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,33 +0,0 @@ --This test verifies that gopls does not panic when encountering the go/types --bug described in golang/go#59944: the Bindingf function is not included in --the methodset of its receiver type. +-func f() int +-func g() int +-func h() (int, int) +-func i() []any - --Adapted from the code in question from the issue. +--- @ellipsis/ellipsis/ellipsis.go -- +-package ellipsis - ---- flags -- ---cgo +-func Ellipsis() { //@codeaction("any", "any", "refactor.rewrite", ellipsis) +-} - ---- go.mod -- --module example.com +-func _() { +- // TODO(rfindley): investigate the broken formatting resulting from these inlinings. +- Ellipsis() +- Ellipsis() +- Ellipsis() +- var _ []any = []any{1, f(), g()} +- Ellipsis() +- func(_ ...any) { +- Ellipsis() +- }(h()) +- var _ []any = i() +- Ellipsis() +-} - --go 1.12 +-func f() int +-func g() int +-func h() (int, int) +-func i() []any +--- ellipsis2/ellipsis2.go -- +-package ellipsis2 - ---- cgo.go -- --package x +-func Ellipsis2(_, _ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2, "Refactor: remove unused parameter") +-} - --import "fmt" +-func _() { +- Ellipsis2(1,2,3) +- Ellipsis2(h()) +- Ellipsis2(1,2, []int{3, 4}...) +-} - --/* --struct layout { -- int field; --}; --*/ --import "C" +-func h() (int, int) - --type Layout = C.struct_layout +--- @ellipsis2/ellipsis2/ellipsis2.go -- +-package ellipsis2 - --// Bindingf is a printf wrapper. This was necessary to trigger the panic in --// objectpath while encoding facts. --func (l *Layout) Bindingf(format string, args ...interface{}) { -- fmt.Printf(format, args...) +-func Ellipsis2(_ int, rest ...int) { //@codeaction("_", "_", "refactor.rewrite", ellipsis2, "Refactor: remove unused parameter") -} -diff -urN a/gopls/internal/regtest/marker/testdata/foldingrange/a_lineonly.txt b/gopls/internal/regtest/marker/testdata/foldingrange/a_lineonly.txt ---- a/gopls/internal/regtest/marker/testdata/foldingrange/a_lineonly.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/foldingrange/a_lineonly.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,163 +0,0 @@ --This test checks basic behavior of the textDocument/foldingRange, when the --editor only supports line folding. - ---- capabilities.json -- --{ -- "textDocument": { -- "foldingRange": { -- "lineFoldingOnly": true -- } -- } +-func _() { +- Ellipsis2(2, []int{3}...) +- func(_, blank0 int, rest ...int) { +- Ellipsis2(blank0, rest...) +- }(h()) +- Ellipsis2(2, []int{3, 4}...) -} ---- a.go -- --package folding //@foldingrange(raw) - --import ( -- "fmt" -- _ "log" --) +-func h() (int, int) +--- overlapping/overlapping.go -- +-package overlapping - --import _ "os" +-func Overlapping(i int) int { //@codeactionerr(re"(i) int", re"(i) int", "refactor.rewrite", re"overlapping") +- return 0 +-} - --// bar is a function. --// With a multiline doc comment. --func bar() string { -- /* This is a single line comment */ -- switch { -- case true: -- if true { -- fmt.Println("true") -- } else { -- fmt.Println("false") -- } -- case false: -- fmt.Println("false") -- default: -- fmt.Println("default") -- } -- /* This is a multiline -- block -- comment */ +-func _() { +- x := Overlapping(Overlapping(0)) +- _ = x +-} - -- /* This is a multiline -- block -- comment */ -- // Followed by another comment. -- _ = []int{ -- 1, -- 2, -- 3, -- } -- _ = [2]string{"d", -- "e", -- } -- _ = map[string]int{ -- "a": 1, -- "b": 2, -- "c": 3, -- } -- type T struct { -- f string -- g int -- h string -- } -- _ = T{ -- f: "j", -- g: 4, -- h: "i", -- } -- x, y := make(chan bool), make(chan bool) -- select { -- case val := <-x: -- if val { -- fmt.Println("true from x") -- } else { -- fmt.Println("false from x") -- } -- case <-y: -- fmt.Println("y") -- default: -- fmt.Println("default") -- } -- // This is a multiline comment -- // that is not a doc comment. -- return ` --this string --is not indented` +--- effects/effects.go -- +-package effects +- +-func effects(x, y int) int { //@ diag("y", re"unused"), codeaction("y", "y", "refactor.rewrite", effects) +- return x -} ---- @raw -- --package folding //@foldingrange(raw) - --import (<0 kind="imports"> -- "fmt" -- _ "log"</0> --) +-func f() int +-func g() int - --import _ "os" +-func _() { +- effects(f(), g()) +- effects(f(), g()) +-} +--- @effects/effects/effects.go -- +-package effects - --// bar is a function.<1 kind="comment"> --// With a multiline doc comment.</1> --func bar() string {<2 kind=""> -- /* This is a single line comment */ -- switch {<3 kind=""> -- case true:<4 kind=""> -- if true {<5 kind=""> -- fmt.Println("true")</5> -- } else {<6 kind=""> -- fmt.Println("false")</6> -- }</4> -- case false:<7 kind=""> -- fmt.Println("false")</7> -- default:<8 kind=""> -- fmt.Println("default")</3></8> -- } -- /* This is a multiline<9 kind="comment"> -- block -- comment */</9> +-func effects(x int) int { //@ diag("y", re"unused"), codeaction("y", "y", "refactor.rewrite", effects) +- return x +-} - -- /* This is a multiline<10 kind="comment"> -- block -- comment */ -- // Followed by another comment.</10> -- _ = []int{<11 kind=""> -- 1, -- 2, -- 3</11>, -- } -- _ = [2]string{"d", -- "e", -- } -- _ = map[string]int{<12 kind=""> -- "a": 1, -- "b": 2, -- "c": 3</12>, -- } -- type T struct {<13 kind=""> -- f string -- g int -- h string</13> -- } -- _ = T{<14 kind=""> -- f: "j", -- g: 4, -- h: "i"</14>, -- } -- x, y := make(chan bool), make(chan bool) -- select {<15 kind=""> -- case val := <-x:<16 kind=""> -- if val {<17 kind=""> -- fmt.Println("true from x")</17> -- } else {<18 kind=""> -- fmt.Println("false from x")</18> -- }</16> -- case <-y:<19 kind=""> -- fmt.Println("y")</19> -- default:<20 kind=""> -- fmt.Println("default")</15></20> +-func f() int +-func g() int +- +-func _() { +- var x, _ int = f(), g() +- effects(x) +- { +- var x, _ int = f(), g() +- effects(x) - } -- // This is a multiline comment<21 kind="comment"> -- // that is not a doc comment.</21> -- return <22 kind="">` --this string --is not indented`</2></22> -} -diff -urN a/gopls/internal/regtest/marker/testdata/foldingrange/a.txt b/gopls/internal/regtest/marker/testdata/foldingrange/a.txt ---- a/gopls/internal/regtest/marker/testdata/foldingrange/a.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/foldingrange/a.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,154 +0,0 @@ --This test checks basic behavior of textDocument/foldingRange. +--- recursive/recursive.go -- +-package recursive - ---- a.go -- --package folding //@foldingrange(raw) +-func Recursive(x int) int { //@codeaction("x", "x", "refactor.rewrite", recursive) +- return Recursive(1) +-} - --import ( -- "fmt" -- _ "log" --) +--- @recursive/recursive/recursive.go -- +-package recursive - --import _ "os" +-func Recursive() int { //@codeaction("x", "x", "refactor.rewrite", recursive) +- return Recursive() +-} +diff -urN a/gopls/internal/test/marker/testdata/codeaction/removeparam_witherrs.txt b/gopls/internal/test/marker/testdata/codeaction/removeparam_witherrs.txt +--- a/gopls/internal/test/marker/testdata/codeaction/removeparam_witherrs.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codeaction/removeparam_witherrs.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,11 +0,0 @@ +-This test checks that we can't remove parameters for packages with errors. - --// bar is a function. --// With a multiline doc comment. --func bar() string { -- /* This is a single line comment */ -- switch { -- case true: -- if true { -- fmt.Println("true") -- } else { -- fmt.Println("false") -- } -- case false: -- fmt.Println("false") -- default: -- fmt.Println("default") -- } -- /* This is a multiline -- block -- comment */ +--- p.go -- +-package p - -- /* This is a multiline -- block -- comment */ -- // Followed by another comment. -- _ = []int{ -- 1, -- 2, -- 3, -- } -- _ = [2]string{"d", -- "e", -- } -- _ = map[string]int{ -- "a": 1, -- "b": 2, -- "c": 3, -- } -- type T struct { -- f string -- g int -- h string -- } -- _ = T{ -- f: "j", -- g: 4, -- h: "i", -- } -- x, y := make(chan bool), make(chan bool) -- select { -- case val := <-x: -- if val { -- fmt.Println("true from x") -- } else { -- fmt.Println("false from x") -- } -- case <-y: -- fmt.Println("y") -- default: -- fmt.Println("default") -- } -- // This is a multiline comment -- // that is not a doc comment. -- return ` --this string --is not indented` +-func foo(unused int) { //@codeactionerr("unused", "unused", "refactor.rewrite", re"found 0") +-} +- +-func _() { +- foo("") //@diag(`""`, re"cannot use") -} ---- @raw -- --package folding //@foldingrange(raw) +diff -urN a/gopls/internal/test/marker/testdata/codeaction/splitlines.txt b/gopls/internal/test/marker/testdata/codeaction/splitlines.txt +--- a/gopls/internal/test/marker/testdata/codeaction/splitlines.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codeaction/splitlines.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,223 +0,0 @@ +-This test exercises the refactoring of putting arguments, return values, and composite literal elements +-into separate lines. - --import (<0 kind="imports"> -- "fmt" -- _ "log" --</0>) +--- go.mod -- +-module unused.mod - --import _ "os" +-go 1.18 - --// bar is a function.<1 kind="comment"> --// With a multiline doc comment.</1> --func bar(<2 kind=""></2>) string {<3 kind=""> -- /* This is a single line comment */ -- switch {<4 kind=""> -- case true:<5 kind=""> -- if true {<6 kind=""> -- fmt.Println(<7 kind="">"true"</7>) -- </6>} else {<8 kind=""> -- fmt.Println(<9 kind="">"false"</9>) -- </8>}</5> -- case false:<10 kind=""> -- fmt.Println(<11 kind="">"false"</11>)</10> -- default:<12 kind=""> -- fmt.Println(<13 kind="">"default"</13>)</12> -- </4>} -- /* This is a multiline<14 kind="comment"> -- block -- comment */</14> +--- func_arg/func_arg.go -- +-package func_arg - -- /* This is a multiline<15 kind="comment"> -- block -- comment */ -- // Followed by another comment.</15> -- _ = []int{<16 kind=""> -- 1, -- 2, -- 3, -- </16>} -- _ = [2]string{<17 kind="">"d", -- "e", -- </17>} -- _ = map[string]int{<18 kind=""> -- "a": 1, -- "b": 2, -- "c": 3, -- </18>} -- type T struct {<19 kind=""> -- f string -- g int -- h string -- </19>} -- _ = T{<20 kind=""> -- f: "j", -- g: 4, -- h: "i", -- </20>} -- x, y := make(<21 kind="">chan bool</21>), make(<22 kind="">chan bool</22>) -- select {<23 kind=""> -- case val := <-x:<24 kind=""> -- if val {<25 kind=""> -- fmt.Println(<26 kind="">"true from x"</26>) -- </25>} else {<27 kind=""> -- fmt.Println(<28 kind="">"false from x"</28>) -- </27>}</24> -- case <-y:<29 kind=""> -- fmt.Println(<30 kind="">"y"</30>)</29> -- default:<31 kind=""> -- fmt.Println(<32 kind="">"default"</32>)</31> -- </23>} -- // This is a multiline comment<33 kind="comment"> -- // that is not a doc comment.</33> -- return <34 kind="">` --this string --is not indented`</34> --</3>} -diff -urN a/gopls/internal/regtest/marker/testdata/foldingrange/bad.txt b/gopls/internal/regtest/marker/testdata/foldingrange/bad.txt ---- a/gopls/internal/regtest/marker/testdata/foldingrange/bad.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/foldingrange/bad.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,41 +0,0 @@ --This test verifies behavior of textDocument/foldingRange in the presence of --unformatted syntax. +-func A(a string, b, c int64, x int, y int) (r1 string, r2, r3 int64, r4 int, r5 int) { //@codeaction("x", "x", "refactor.rewrite", func_arg) +- return a, b, c, x, y +-} - ---- a.go -- --package folding //@foldingrange(raw) +--- @func_arg/func_arg/func_arg.go -- +-package func_arg - --import ( "fmt" -- _ "log" --) +-func A( +- a string, +- b, c int64, +- x int, +- y int, +-) (r1 string, r2, r3 int64, r4 int, r5 int) { //@codeaction("x", "x", "refactor.rewrite", func_arg) +- return a, b, c, x, y +-} - --import ( -- _ "os" ) -- --// badBar is a function. --func badBar() string { x := true -- if x { -- // This is the only foldable thing in this file when lineFoldingOnly -- fmt.Println("true") -- } else { -- fmt.Println("false") } -- return "" +--- func_ret/func_ret.go -- +-package func_ret +- +-func A(a string, b, c int64, x int, y int) (r1 string, r2, r3 int64, r4 int, r5 int) { //@codeaction("r1", "r1", "refactor.rewrite", func_ret) +- return a, b, c, x, y -} ---- @raw -- --package folding //@foldingrange(raw) - --import (<0 kind="imports"> "fmt" -- _ "log" --</0>) +--- @func_ret/func_ret/func_ret.go -- +-package func_ret - --import (<1 kind="imports"> -- _ "os" </1>) -- --// badBar is a function. --func badBar(<2 kind=""></2>) string {<3 kind=""> x := true -- if x {<4 kind=""> -- // This is the only foldable thing in this file when lineFoldingOnly -- fmt.Println(<5 kind="">"true"</5>) -- </4>} else {<6 kind=""> -- fmt.Println(<7 kind="">"false"</7>) </6>} -- return "" --</3>} -diff -urN a/gopls/internal/regtest/marker/testdata/format/format.txt b/gopls/internal/regtest/marker/testdata/format/format.txt ---- a/gopls/internal/regtest/marker/testdata/format/format.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/format/format.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,80 +0,0 @@ --This test checks basic behavior of textDocument/formatting requests. +-func A(a string, b, c int64, x int, y int) ( +- r1 string, +- r2, r3 int64, +- r4 int, +- r5 int, +-) { //@codeaction("r1", "r1", "refactor.rewrite", func_ret) +- return a, b, c, x, y +-} - ---- go.mod -- --module mod.com +--- functype_arg/functype_arg.go -- +-package functype_arg - --go 1.18 ---- good.go -- --package format //@format(good) +-type A func(a string, b, c int64, x int, y int) (r1 string, r2, r3 int64, r4 int, r5 int) //@codeaction("x", "x", "refactor.rewrite", functype_arg) - --import ( -- "log" --) +--- @functype_arg/functype_arg/functype_arg.go -- +-package functype_arg - --func goodbye() { -- log.Printf("byeeeee") +-type A func( +- a string, +- b, c int64, +- x int, +- y int, +-) (r1 string, r2, r3 int64, r4 int, r5 int) //@codeaction("x", "x", "refactor.rewrite", functype_arg) +- +--- functype_ret/functype_ret.go -- +-package functype_ret +- +-type A func(a string, b, c int64, x int, y int) (r1 string, r2, r3 int64, r4 int, r5 int) //@codeaction("r1", "r1", "refactor.rewrite", functype_ret) +- +--- @functype_ret/functype_ret/functype_ret.go -- +-package functype_ret +- +-type A func(a string, b, c int64, x int, y int) ( +- r1 string, +- r2, r3 int64, +- r4 int, +- r5 int, +-) //@codeaction("r1", "r1", "refactor.rewrite", functype_ret) +- +--- func_call/func_call.go -- +-package func_call +- +-import "fmt" +- +-func a() { +- fmt.Println(1, 2, 3, fmt.Sprintf("hello %d", 4)) //@codeaction("1", "1", "refactor.rewrite", func_call) -} - ---- @good -- --package format //@format(good) +--- @func_call/func_call/func_call.go -- +-package func_call - --import ( -- "log" --) +-import "fmt" - --func goodbye() { -- log.Printf("byeeeee") +-func a() { +- fmt.Println( +- 1, +- 2, +- 3, +- fmt.Sprintf("hello %d", 4), +- ) //@codeaction("1", "1", "refactor.rewrite", func_call) -} ---- bad.go -- --package format //@format(bad) - --import ( -- "runtime" -- "fmt" -- "log" --) +--- indent/indent.go -- +-package indent - --func hello() { +-import "fmt" - +-func a() { +- fmt.Println(1, 2, 3, fmt.Sprintf("hello %d", 4)) //@codeaction("hello", "hello", "refactor.rewrite", indent, "Split parameters into separate lines") +-} - +--- @indent/indent/indent.go -- +-package indent - +-import "fmt" - -- var x int //@diag("x", re"x declared (and|but) not used") +-func a() { +- fmt.Println(1, 2, 3, fmt.Sprintf( +- "hello %d", +- 4, +- )) //@codeaction("hello", "hello", "refactor.rewrite", indent, "Split parameters into separate lines") -} - --func hi() { -- runtime.GOROOT() -- fmt.Printf("") +--- indent2/indent2.go -- +-package indent2 - -- log.Printf("") +-import "fmt" +- +-func a() { +- fmt. +- Println(1, 2, 3, fmt.Sprintf("hello %d", 4)) //@codeaction("1", "1", "refactor.rewrite", indent2, "Split parameters into separate lines") -} ---- @bad -- --package format //@format(bad) - --import ( -- "fmt" -- "log" -- "runtime" --) +--- @indent2/indent2/indent2.go -- +-package indent2 - --func hello() { +-import "fmt" - -- var x int //@diag("x", re"x declared (and|but) not used") +-func a() { +- fmt. +- Println( +- 1, +- 2, +- 3, +- fmt.Sprintf("hello %d", 4), +- ) //@codeaction("1", "1", "refactor.rewrite", indent2, "Split parameters into separate lines") -} - --func hi() { -- runtime.GOROOT() -- fmt.Printf("") +--- structelts/structelts.go -- +-package structelts - -- log.Printf("") +-type A struct{ +- a int +- b int -} ---- newline.go -- --package format //@format(newline) --func _() {} ---- @newline -- --package format //@format(newline) --func _() {} ---- oneline.go -- --package format //@format(oneline) ---- @oneline -- --package format //@format(oneline) -diff -urN a/gopls/internal/regtest/marker/testdata/format/issue59554.txt b/gopls/internal/regtest/marker/testdata/format/issue59554.txt ---- a/gopls/internal/regtest/marker/testdata/format/issue59554.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/format/issue59554.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,33 +0,0 @@ --Test case for golang/go#59554: data corruption on formatting due to line --directives. - --Note that gofumpt is needed for this test case, as it reformats var decls into --short var decls. +-func a() { +- _ = A{a: 1, b: 2} //@codeaction("b", "b", "refactor.rewrite", structelts) +-} - --Note that gofumpt requires Go 1.18. +--- @structelts/structelts/structelts.go -- +-package structelts - ---- flags -- ---min_go=go1.18 +-type A struct{ +- a int +- b int +-} - ---- settings.json -- --{ -- "formatting.gofumpt": true +-func a() { +- _ = A{ +- a: 1, +- b: 2, +- } //@codeaction("b", "b", "refactor.rewrite", structelts) -} ---- main.go -- --package main //@format(main) - --func Match(data []byte) int { --//line :1 -- var idx = ^uint(0) -- _ = idx -- return -1 +--- sliceelts/sliceelts.go -- +-package sliceelts +- +-func a() { +- _ = []int{1, 2} //@codeaction("1", "1", "refactor.rewrite", sliceelts) -} ---- @main -- --package main //@format(main) - --func Match(data []byte) int { --//line :1 -- idx := ^uint(0) -- _ = idx -- return -1 +--- @sliceelts/sliceelts/sliceelts.go -- +-package sliceelts +- +-func a() { +- _ = []int{ +- 1, +- 2, +- } //@codeaction("1", "1", "refactor.rewrite", sliceelts) -} -diff -urN a/gopls/internal/regtest/marker/testdata/format/noparse.txt b/gopls/internal/regtest/marker/testdata/format/noparse.txt ---- a/gopls/internal/regtest/marker/testdata/format/noparse.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/format/noparse.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,27 +0,0 @@ --This test checks that formatting does not run on code that has parse errors. - ---- parse.go -- --package noparse_format //@format(parse) +--- mapelts/mapelts.go -- +-package mapelts - --func _() { --f() //@diag("f", re"(undefined|undeclared name): f") +-func a() { +- _ = map[string]int{"a": 1, "b": 2} //@codeaction("1", "1", "refactor.rewrite", mapelts) -} ---- @parse -- --package noparse_format //@format(parse) - --func _() { -- f() //@diag("f", re"(undefined|undeclared name): f") +--- @mapelts/mapelts/mapelts.go -- +-package mapelts +- +-func a() { +- _ = map[string]int{ +- "a": 1, +- "b": 2, +- } //@codeaction("1", "1", "refactor.rewrite", mapelts) -} ---- noparse.go -- --package noparse_format //@format(noparse) - --// The nonewvars expectation asserts that the go/analysis framework ran. +--- starcomment/starcomment.go -- +-package starcomment - --func what() { -- var hi func() -- if { hi() //@diag(re"(){", re".*missing.*") -- } -- hi := nil +-func A(/*1*/ x /*2*/ string /*3*/, /*4*/ y /*5*/ int /*6*/) (string, int) { //@codeaction("x", "x", "refactor.rewrite", starcomment) +- return x, y -} ---- @noparse -- --7:5: missing condition in if statement -diff -urN a/gopls/internal/regtest/marker/testdata/highlight/highlight.txt b/gopls/internal/regtest/marker/testdata/highlight/highlight.txt ---- a/gopls/internal/regtest/marker/testdata/highlight/highlight.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/highlight/highlight.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,158 +0,0 @@ --This test checks basic functionality of the textDocument/highlight request. - ---- highlights.go -- --package highlights +--- @starcomment/starcomment/starcomment.go -- +-package starcomment - --import ( -- "fmt" //@loc(fmtImp, "\"fmt\""),highlight(fmtImp, fmtImp, fmt1, fmt2, fmt3, fmt4) -- h2 "net/http" //@loc(hImp, "h2"),highlight(hImp, hImp, hUse) -- "sort" --) +-func A( +- /*1*/ x /*2*/ string /*3*/, +- /*4*/ y /*5*/ int /*6*/, +-) (string, int) { //@codeaction("x", "x", "refactor.rewrite", starcomment) +- return x, y +-} - --type F struct{ bar int } //@loc(barDeclaration, "bar"),highlight(barDeclaration, barDeclaration, bar1, bar2, bar3) +diff -urN a/gopls/internal/test/marker/testdata/codelens/generate.txt b/gopls/internal/test/marker/testdata/codelens/generate.txt +--- a/gopls/internal/test/marker/testdata/codelens/generate.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codelens/generate.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,9 +0,0 @@ +-This test exercises the "generate" codelens. - --func _() F { -- return F{ -- bar: 123, //@loc(bar1, "bar"),highlight(bar1, barDeclaration, bar1, bar2, bar3) +--- generate.go -- +-//@codelenses() +- +-package generate +- +-//go:generate echo Hi //@ codelens("//go:generate", "run go generate"), codelens("//go:generate", "run go generate ./...") +-//go:generate echo I shall have no CodeLens +diff -urN a/gopls/internal/test/marker/testdata/codelens/test.txt b/gopls/internal/test/marker/testdata/codelens/test.txt +--- a/gopls/internal/test/marker/testdata/codelens/test.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/codelens/test.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,32 +0,0 @@ +-This file tests codelenses for test functions. +- +-TODO: for some reason these code lens have zero width. Does that affect their +-utility/visibility in various LSP clients? +- +--- settings.json -- +-{ +- "codelenses": { +- "test": true - } -} - --var foo = F{bar: 52} //@loc(fooDeclaration, "foo"),loc(bar2, "bar"),highlight(fooDeclaration, fooDeclaration, fooUse),highlight(bar2, barDeclaration, bar1, bar2, bar3) +--- p_test.go -- +-//@codelenses() - --func Print() { //@loc(printFunc, "Print"),highlight(printFunc, printFunc, printTest) -- _ = h2.Client{} //@loc(hUse, "h2"),highlight(hUse, hImp, hUse) +-package codelens //@codelens(re"()package codelens", "run file benchmarks") - -- fmt.Println(foo) //@loc(fooUse, "foo"),highlight(fooUse, fooDeclaration, fooUse),loc(fmt1, "fmt"),highlight(fmt1, fmtImp, fmt1, fmt2, fmt3, fmt4) -- fmt.Print("yo") //@loc(printSep, "Print"),highlight(printSep, printSep, print1, print2),loc(fmt2, "fmt"),highlight(fmt2, fmtImp, fmt1, fmt2, fmt3, fmt4) +-import "testing" +- +-func TestMain(m *testing.M) {} // no code lens for TestMain +- +-func TestFuncWithCodeLens(t *testing.T) { //@codelens(re"()func", "run test") -} - --func (x *F) Inc() { //@loc(xRightDecl, "x"),loc(xLeftDecl, " *"),highlight(xRightDecl, xRightDecl, xUse),highlight(xLeftDecl, xRightDecl, xUse) -- x.bar++ //@loc(xUse, "x"),loc(bar3, "bar"),highlight(xUse, xRightDecl, xUse),highlight(bar3, barDeclaration, bar1, bar2, bar3) +-func thisShouldNotHaveACodeLens(t *testing.T) { //@diag("t ", re"unused parameter") +- println() // nonempty body => "unused parameter" -} - --func testFunctions() { -- fmt.Print("main start") //@loc(print1, "Print"),highlight(print1, printSep, print1, print2),loc(fmt3, "fmt"),highlight(fmt3, fmtImp, fmt1, fmt2, fmt3, fmt4) -- fmt.Print("ok") //@loc(print2, "Print"),highlight(print2, printSep, print1, print2),loc(fmt4, "fmt"),highlight(fmt4, fmtImp, fmt1, fmt2, fmt3, fmt4) -- Print() //@loc(printTest, "Print"),highlight(printTest, printFunc, printTest) +-func BenchmarkFuncWithCodeLens(b *testing.B) { //@codelens(re"()func", "run benchmark") -} - --// DocumentHighlight is undefined, so its uses below are type errors. --// Nevertheless, document highlighting should still work. --//@diag(doc1, re"undefined|undeclared"), diag(doc2, re"undefined|undeclared"), diag(doc3, re"undefined|undeclared") +-func helper() {} // expect no code lens +diff -urN a/gopls/internal/test/marker/testdata/completion/address.txt b/gopls/internal/test/marker/testdata/completion/address.txt +--- a/gopls/internal/test/marker/testdata/completion/address.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/address.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,92 +0,0 @@ +-This test exercises the reference and dereference completion modifiers. - --func toProtocolHighlight(rngs []int) []DocumentHighlight { //@loc(doc1, "DocumentHighlight"),loc(docRet1, "[]DocumentHighlight"),highlight(doc1, docRet1, doc1, doc2, doc3, result) -- result := make([]DocumentHighlight, 0, len(rngs)) //@loc(doc2, "DocumentHighlight"),highlight(doc2, doc1, doc2, doc3) -- for _, rng := range rngs { -- result = append(result, DocumentHighlight{ //@loc(doc3, "DocumentHighlight"),highlight(doc3, doc1, doc2, doc3) -- Range: rng, -- }) -- } -- return result //@loc(result, "result") +-TODO: remove the need to set "literalCompletions" here, as this is one of the +-few places this setting is needed. +- +--- flags -- +--ignore_extra_diags +- +--- go.mod -- +-module golang.org/lsptests +- +-go 1.18 +- +--- address/address.go -- +-package address +- +-func wantsPtr(*int) {} +-func wantsVariadicPtr(...*int) {} +- +-func wantsVariadic(...int) {} +- +-type foo struct{ c int } //@item(addrFieldC, "c", "int", "field") +- +-func _() { +- var ( +- a string //@item(addrA, "a", "string", "var") +- b int //@item(addrB, "b", "int", "var") +- ) +- +- wantsPtr() //@rank(")", addrB, addrA),snippet(")", addrB, "&b") +- wantsPtr(&b) //@snippet(")", addrB, "b") +- +- wantsVariadicPtr() //@rank(")", addrB, addrA),snippet(")", addrB, "&b") +- +- var s foo +- s.c //@item(addrDeepC, "s.c", "int", "field") +- wantsPtr() //@snippet(")", addrDeepC, "&s.c") +- wantsPtr(s) //@snippet(")", addrDeepC, "&s.c") +- wantsPtr(&s) //@snippet(")", addrDeepC, "s.c") +- +- // don't add "&" in item (it gets added as an additional edit) +- wantsPtr(&s.c) //@snippet(")", addrFieldC, "c") +- +- // check dereferencing as well +- var c *int //@item(addrCPtr, "c", "*int", "var") +- var _ int = _ //@rank("_ //", addrCPtr, addrA),snippet("_ //", addrCPtr, "*c") +- +- wantsVariadic() //@rank(")", addrCPtr, addrA),snippet(")", addrCPtr, "*c") +- +- var d **int //@item(addrDPtr, "d", "**int", "var") +- var _ int = _ //@rank("_ //", addrDPtr, addrA),snippet("_ //", addrDPtr, "**d") +- +- type namedPtr *int +- var np namedPtr //@item(addrNamedPtr, "np", "namedPtr", "var") +- +- var _ int = _ //@rank("_ //", addrNamedPtr, addrA) +- +- // don't get tripped up by recursive pointer type +- type dontMessUp *dontMessUp //@item(dontMessUp, "dontMessUp", "*dontMessUp", "type") +- var dmu *dontMessUp //@item(addrDMU, "dmu", "*dontMessUp", "var") +- +- var _ int = dmu //@complete(" //", addrDMU, dontMessUp) -} - --func testForLoops() { -- for i := 0; i < 10; i++ { //@loc(forDecl1, "for"),highlight(forDecl1, forDecl1, brk1, cont1) -- if i > 8 { -- break //@loc(brk1, "break"),highlight(brk1, forDecl1, brk1, cont1) -- } -- if i < 2 { -- for j := 1; j < 10; j++ { //@loc(forDecl2, "for"),highlight(forDecl2, forDecl2, cont2) -- if j < 3 { -- for k := 1; k < 10; k++ { //@loc(forDecl3, "for"),highlight(forDecl3, forDecl3, cont3) -- if k < 3 { -- continue //@loc(cont3, "continue"),highlight(cont3, forDecl3, cont3) -- } -- } -- continue //@loc(cont2, "continue"),highlight(cont2, forDecl2, cont2) -- } -- } -- continue //@loc(cont1, "continue"),highlight(cont1, forDecl1, brk1, cont1) -- } -- } +-func (f foo) ptr() *foo { return &f } - -- arr := []int{} -- for i := range arr { //@loc(forDecl4, "for"),highlight(forDecl4, forDecl4, brk4, cont4) -- if i > 8 { -- break //@loc(brk4, "break"),highlight(brk4, forDecl4, brk4, cont4) -- } -- if i < 4 { -- continue //@loc(cont4, "continue"),highlight(cont4, forDecl4, brk4, cont4) -- } -- } +-func _() { +- getFoo := func() foo { return foo{} } - --Outer: -- for i := 0; i < 10; i++ { //@loc(forDecl5, "for"),highlight(forDecl5, forDecl5, brk5, brk6, brk8) -- break //@loc(brk5, "break"),highlight(brk5, forDecl5, brk5, brk6, brk8) -- for { //@loc(forDecl6, "for"),highlight(forDecl6, forDecl6, cont5), diag("for", re"unreachable") -- if i == 1 { -- break Outer //@loc(brk6, "break Outer"),highlight(brk6, forDecl5, brk5, brk6, brk8) -- } -- switch i { //@loc(switch1, "switch"),highlight(switch1, switch1, brk7) -- case 5: -- break //@loc(brk7, "break"),highlight(brk7, switch1, brk7) -- case 6: -- continue //@loc(cont5, "continue"),highlight(cont5, forDecl6, cont5) -- case 7: -- break Outer //@loc(brk8, "break Outer"),highlight(brk8, forDecl5, brk5, brk6, brk8) -- } -- } -- } +- // not addressable +- getFoo().c //@item(addrGetFooC, "getFoo().c", "int", "field") +- +- // addressable +- getFoo().ptr().c //@item(addrGetFooPtrC, "getFoo().ptr().c", "int", "field") +- +- wantsPtr() //@snippet(")", addrGetFooPtrC, "&getFoo().ptr().c") +- wantsPtr(&g) //@snippet(")", addrGetFooPtrC, "getFoo().ptr().c") -} - --func testSwitch() { -- var i, j int +-type nested struct { +- f foo +-} - --L1: -- for { //@loc(forDecl7, "for"),highlight(forDecl7, forDecl7, brk10, cont6) -- L2: -- switch i { //@loc(switch2, "switch"),highlight(switch2, switch2, brk11, brk12, brk13) -- case 1: -- switch j { //@loc(switch3, "switch"),highlight(switch3, switch3, brk9) -- case 1: -- break //@loc(brk9, "break"),highlight(brk9, switch3, brk9) -- case 2: -- break L1 //@loc(brk10, "break L1"),highlight(brk10, forDecl7, brk10, cont6) -- case 3: -- break L2 //@loc(brk11, "break L2"),highlight(brk11, switch2, brk11, brk12, brk13) -- default: -- continue //@loc(cont6, "continue"),highlight(cont6, forDecl7, brk10, cont6) -- } -- case 2: -- break //@loc(brk12, "break"),highlight(brk12, switch2, brk11, brk12, brk13) -- default: -- break L2 //@loc(brk13, "break L2"),highlight(brk13, switch2, brk11, brk12, brk13) -- } -- } +-func _() { +- getNested := func() nested { return nested{} } +- +- getNested().f.c //@item(addrNestedC, "getNested().f.c", "int", "field") +- getNested().f.ptr().c //@item(addrNestedPtrC, "getNested().f.ptr().c", "int", "field") +- +- // addrNestedC is not addressable, so rank lower +- wantsPtr(getNestedfc) //@complete(")", addrNestedPtrC, addrNestedC) -} +diff -urN a/gopls/internal/test/marker/testdata/completion/anon.txt b/gopls/internal/test/marker/testdata/completion/anon.txt +--- a/gopls/internal/test/marker/testdata/completion/anon.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/anon.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,37 +0,0 @@ +-This test checks completion related to anonymous structs. - --func testReturn() bool { //@loc(func1, "func"),loc(bool1, "bool"),highlight(func1, func1, fullRet11, fullRet12),highlight(bool1, bool1, false1, bool2, true1) -- if 1 < 2 { -- return false //@loc(ret11, "return"),loc(fullRet11, "return false"),loc(false1, "false"),highlight(ret11, func1, fullRet11, fullRet12) -- } -- candidates := []int{} -- sort.SliceStable(candidates, func(i, j int) bool { //@loc(func2, "func"),loc(bool2, "bool"),highlight(func2, func2, fullRet2) -- return candidates[i] > candidates[j] //@loc(ret2, "return"),loc(fullRet2, "return candidates[i] > candidates[j]"),highlight(ret2, func2, fullRet2) -- }) -- return true //@loc(ret12, "return"),loc(fullRet12, "return true"),loc(true1, "true"),highlight(ret12, func1, fullRet11, fullRet12) +--- flags -- +--ignore_extra_diags +- +--- settings.json -- +-{ +- "deepCompletion": false -} - --func testReturnFields() float64 { //@loc(retVal1, "float64"),highlight(retVal1, retVal1, retVal11, retVal21) -- if 1 < 2 { -- return 20.1 //@loc(retVal11, "20.1"),highlight(retVal11, retVal1, retVal11, retVal21) +--- anon.go -- +-package anon +- +-// Literal completion results. +-/* int() */ //@item(int, "int()", "int", "var") +- +-func _() { +- for _, _ := range []struct { +- i, j int //@item(anonI, "i", "int", "field"),item(anonJ, "j", "int", "field") +- }{ +- { +- i: 1, +- //@complete("", anonJ) +- }, +- { +- //@complete("", anonI, anonJ, int) +- }, +- } { +- continue - } -- z := 4.3 //@loc(zDecl, "z") -- return z //@loc(retVal21, "z"),highlight(retVal21, retVal1, retVal11, zDecl, retVal21) --} - --func testReturnMultipleFields() (float32, string) { //@loc(retVal31, "float32"),loc(retVal32, "string"),highlight(retVal31, retVal31, retVal41, retVal51),highlight(retVal32, retVal32, retVal42, retVal52) -- y := "im a var" //@loc(yDecl, "y"), -- if 1 < 2 { -- return 20.1, y //@loc(retVal41, "20.1"),loc(retVal42, "y"),highlight(retVal41, retVal31, retVal41, retVal51),highlight(retVal42, retVal32, yDecl, retVal42, retVal52) +- s := struct{ f int }{ } //@item(anonF, "f", "int", "field"),item(structS, "s", "struct{...}", "var"),complete(" }", anonF, int) +- +- _ = map[struct{ x int }]int{ //@item(anonX, "x", "int", "field") +- struct{ x int }{ }: 1, //@complete(" }", anonX, int, structS) - } -- return 4.9, "test" //@loc(retVal51, "4.9"),loc(retVal52, "\"test\""),highlight(retVal51, retVal31, retVal41, retVal51),highlight(retVal52, retVal32, retVal42, retVal52) -} +diff -urN a/gopls/internal/test/marker/testdata/completion/append.txt b/gopls/internal/test/marker/testdata/completion/append.txt +--- a/gopls/internal/test/marker/testdata/completion/append.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/append.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,56 +0,0 @@ +-This test checks behavior of completion within append expressions. - --func testReturnFunc() int32 { //@loc(retCall, "int32") -- mulch := 1 //@loc(mulchDec, "mulch"),highlight(mulchDec, mulchDec, mulchRet) -- return int32(mulch) //@loc(mulchRet, "mulch"),loc(retFunc, "int32"),loc(retTotal, "int32(mulch)"),highlight(mulchRet, mulchDec, mulchRet),highlight(retFunc, retCall, retFunc, retTotal) --} -diff -urN a/gopls/internal/regtest/marker/testdata/highlight/issue60435.txt b/gopls/internal/regtest/marker/testdata/highlight/issue60435.txt ---- a/gopls/internal/regtest/marker/testdata/highlight/issue60435.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/highlight/issue60435.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,15 +0,0 @@ --This is a regression test for issue 60435: --Highlighting "net/http" shouldn't have any effect --on an import path that contains it as a substring, --such as httptest. +--- flags -- +--ignore_extra_diags - ---- highlights.go -- --package highlights +--- go.mod -- +-module golang.org/lsptests/append - --import ( -- "net/http" //@loc(httpImp, `"net/http"`) -- "net/http/httptest" //@loc(httptestImp, `"net/http/httptest"`) --) +-go 1.18 - --var _ = httptest.NewRequest --var _ = http.NewRequest //@loc(here, "http"), highlight(here, here, httpImp) -diff -urN a/gopls/internal/regtest/marker/testdata/hover/basiclit.txt b/gopls/internal/regtest/marker/testdata/hover/basiclit.txt ---- a/gopls/internal/regtest/marker/testdata/hover/basiclit.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/hover/basiclit.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,81 +0,0 @@ --This test checks gopls behavior when hovering over basic literals. ---- basiclit.go -- --package basiclit +--- append.go -- +-package append +- +-func foo([]string) {} +-func bar(...string) {} - -func _() { -- _ = 'a' //@hover("'a'", "'a'", latinA) -- _ = 0x61 //@hover("0x61", "0x61", latinAHex) +- var ( +- aInt []int //@item(appendInt, "aInt", "[]int", "var") +- aStrings []string //@item(appendStrings, "aStrings", "[]string", "var") +- aString string //@item(appendString, "aString", "string", "var") +- ) - -- _ = '\u2211' //@hover("'\\u2211'", "'\\u2211'", summation) -- _ = 0x2211 //@hover("0x2211", "0x2211", summationHex) -- _ = "foo \u2211 bar" //@hover("\\u2211", "\\u2211", summation) +- append(aStrings, a) //@rank(")", appendString, appendInt) +- var _ interface{} = append(aStrings, a) //@rank(")", appendString, appendInt) +- var _ []string = append(oops, a) //@rank(")", appendString, appendInt) - -- _ = '\a' //@hover("'\\a'", "'\\a'", control) -- _ = "foo \a bar" //@hover("\\a", "\\a", control) +- foo(append()) //@rank("))", appendStrings, appendInt),rank("))", appendStrings, appendString) +- foo(append([]string{}, a)) //@rank("))", appendStrings, appendInt),rank("))", appendString, appendInt),snippet("))", appendStrings, "aStrings...") +- foo(append([]string{}, "", a)) //@rank("))", appendString, appendInt),rank("))", appendString, appendStrings) - -- _ = '\U0001F30A' //@hover("'\\U0001F30A'", "'\\U0001F30A'", waterWave) -- _ = 0x0001F30A //@hover("0x0001F30A", "0x0001F30A", waterWaveHex) -- _ = 0X0001F30A //@hover("0X0001F30A", "0X0001F30A", waterWaveHex) -- _ = "foo \U0001F30A bar" //@hover("\\U0001F30A", "\\U0001F30A", waterWave) +- // Don't add "..." to append() argument. +- bar(append()) //@snippet("))", appendStrings, "aStrings") - -- _ = '\x7E' //@hover("'\\x7E'", "'\\x7E'", tilde) -- _ = "foo \x7E bar" //@hover("\\x7E", "\\x7E", tilde) -- _ = "foo \a bar" //@hover("\\a", "\\a", control) +- type baz struct{} +- baz{} //@item(appendBazLiteral, "baz{}", "", "var") +- var bazzes []baz //@item(appendBazzes, "bazzes", "[]baz", "var") +- var bazzy baz //@item(appendBazzy, "bazzy", "baz", "var") +- bazzes = append(bazzes, ba) //@rank(")", appendBazzy, appendBazLiteral, appendBazzes) - -- _ = '\173' //@hover("'\\173'", "'\\173'", leftCurly) -- _ = "foo \173 bar" //@hover("\\173","\\173", leftCurly) -- _ = "foo \173 bar \u2211 baz" //@hover("\\173","\\173", leftCurly) -- _ = "foo \173 bar \u2211 baz" //@hover("\\u2211","\\u2211", summation) -- _ = "foo\173bar\u2211baz" //@hover("\\173","\\173", leftCurly) -- _ = "foo\173bar\u2211baz" //@hover("\\u2211","\\u2211", summation) +- var b struct{ b []baz } +- b.b //@item(appendNestedBaz, "b.b", "[]baz", "field") +- b.b = append(b.b, b) //@rank(")", appendBazzy, appendBazLiteral, appendNestedBaz) - -- // search for runes in string only if there is an escaped sequence -- _ = "hello" //@hover(`"hello"`, _, _) +- var aStringsPtr *[]string //@item(appendStringsPtr, "aStringsPtr", "*[]string", "var") +- foo(append([]string{}, a)) //@snippet("))", appendStringsPtr, "*aStringsPtr...") - -- // incorrect escaped rune sequences -- _ = '\0' //@hover("'\\0'", _, _),diag(re`\\0()'`, re"illegal character") -- _ = '\u22111' //@hover("'\\u22111'", _, _) -- _ = '\U00110000' //@hover("'\\U00110000'", _, _) -- _ = '\u12e45'//@hover("'\\u12e45'", _, _) -- _ = '\xa' //@hover("'\\xa'", _, _) -- _ = 'aa' //@hover("'aa'", _, _) +- foo(append([]string{}, *a)) //@snippet("))", appendStringsPtr, "aStringsPtr...") +-} - -- // other basic lits -- _ = 1 //@hover("1", _, _) -- _ = 1.2 //@hover("1.2", _, _) -- _ = 1.2i //@hover("1.2i", _, _) -- _ = 0123 //@hover("0123", _, _) -- _ = 0b1001 //@hover("0b", "0b1001", binaryNumber) -- _ = 0B1001 //@hover("0B", "0B1001", binaryNumber) -- _ = 0o77 //@hover("0o", "0o77", octalNumber) -- _ = 0O77 //@hover("0O", "0O77", octalNumber) -- _ = 0x1234567890 //@hover("0x1234567890", "0x1234567890", hexNumber) -- _ = 0X1234567890 //@hover("0X1234567890", "0X1234567890", hexNumber) -- _ = 0x1000000000000000000 //@hover("0x1", "0x1000000000000000000", bigHex) --) ---- @bigHex/hover.md -- --4722366482869645213696 ---- @binaryNumber/hover.md -- --9 ---- @control/hover.md -- --U+0007, control ---- @hexNumber/hover.md -- --78187493520 ---- @latinA/hover.md -- --'a', U+0061, LATIN SMALL LETTER A ---- @latinAHex/hover.md -- --97, 'a', U+0061, LATIN SMALL LETTER A ---- @leftCurly/hover.md -- --'{', U+007B, LEFT CURLY BRACKET ---- @octalNumber/hover.md -- --63 ---- @summation/hover.md -- --'∑', U+2211, N-ARY SUMMATION ---- @summationHex/hover.md -- --8721, '∑', U+2211, N-ARY SUMMATION ---- @tilde/hover.md -- --'~', U+007E, TILDE ---- @waterWave/hover.md -- --'🌊', U+1F30A, WATER WAVE ---- @waterWaveHex/hover.md -- --127754, '🌊', U+1F30A, WATER WAVE -diff -urN a/gopls/internal/regtest/marker/testdata/hover/const.txt b/gopls/internal/regtest/marker/testdata/hover/const.txt ---- a/gopls/internal/regtest/marker/testdata/hover/const.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/hover/const.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,157 +0,0 @@ --This test checks hovering over constants. +--- append2.go -- +-package append +- +-func _() { +- _ = append(a, struct) //@complete(")") +-} +diff -urN a/gopls/internal/test/marker/testdata/completion/assign.txt b/gopls/internal/test/marker/testdata/completion/assign.txt +--- a/gopls/internal/test/marker/testdata/completion/assign.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/assign.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,47 +0,0 @@ +-This test checks that completion considers assignability when ranking results. - --- flags -- ---min_go=go1.17 +--ignore_extra_diags - --- go.mod -- --module mod.com +-module golang.org/lsptests/assign - --go 1.17 +-go 1.18 - ---- c.go -- --package c +--- settings.json -- +-{ +- "completeUnimported": false +-} - --import ( -- "math" -- "time" --) +--- assign.go -- +-package assign - --const X = 0 //@hover("X", "X", bX) +-import "golang.org/lsptests/assign/internal/secret" - --// dur is a constant of type time.Duration. --const dur = 15*time.Minute + 10*time.Second + 350*time.Millisecond //@hover("dur", "dur", dur) +-func _() { +- secret.Hello() +- var ( +- myInt int //@item(assignInt, "myInt", "int", "var") +- myStr string //@item(assignStr, "myStr", "string", "var") +- ) - --// MaxFloat32 is used in another package. --const MaxFloat32 = 0x1p127 * (1 + (1 - 0x1p-23)) +- var _ string = my //@rank(" //", assignStr, assignInt) +- var _ string = //@rank(" //", assignStr, assignInt) +-} - --// Numbers. -func _() { -- const hex, bin = 0xe34e, 0b1001001 +- var a string = a //@complete(" //") +-} - -- const ( -- // no inline comment -- decimal = 153 +-func _() { +- fooBar := fooBa //@complete(" //"),item(assignFooBar, "fooBar", "", "var") +- abc, fooBar := 123, fooBa //@complete(" //", assignFooBar) +- { +- fooBar := fooBa //@complete(" //", assignFooBar) +- } +-} - -- numberWithUnderscore int64 = 10_000_000_000 -- octal = 0o777 -- expr = 2 << (0b111&0b101 - 2) -- boolean = (55 - 3) == (26 * 2) -- ) +--- internal/secret/secret.go -- +-package secret - -- _ = decimal //@hover("decimal", "decimal", decimalConst) -- _ = hex //@hover("hex", "hex", hexConst) -- _ = bin //@hover("bin", "bin", binConst) -- _ = numberWithUnderscore //@hover("numberWithUnderscore", "numberWithUnderscore", numberWithUnderscoreConst) -- _ = octal //@hover("octal", "octal", octalConst) -- _ = expr //@hover("expr", "expr", exprConst) -- _ = boolean //@hover("boolean", "boolean", boolConst) +-func Hello() {} +diff -urN a/gopls/internal/test/marker/testdata/completion/bad.txt b/gopls/internal/test/marker/testdata/completion/bad.txt +--- a/gopls/internal/test/marker/testdata/completion/bad.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/bad.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,68 +0,0 @@ +-This test exercises completion in the presence of type errors. - -- const ln10 = 2.30258509299404568401799145468436420760110148862877297603332790 +-Note: this test was ported from the old marker tests, which did not enable +-unimported completion. Enabling it causes matches in e.g. crypto/rand. - -- _ = ln10 //@hover("ln10", "ln10", ln10Const) +--- settings.json -- +-{ +- "completeUnimported": false -} - --// Iota. --func _() { -- const ( -- a = 1 << iota -- b -- ) +--- go.mod -- +-module bad.test - -- _ = a //@hover("a", "a", aIota) -- _ = b //@hover("b", "b", bIota) +-go 1.18 +- +--- bad/bad0.go -- +-package bad +- +-func stuff() { //@item(stuff, "stuff", "func()", "func") +- x := "heeeeyyyy" +- random2(x) //@diag("x", re"cannot use x \\(variable of type string\\) as int value in argument to random2") +- random2(1) //@complete("dom", random, random2, random3) +- y := 3 //@diag("y", re"y.*declared (and|but) not used") +-} +- +-type bob struct { //@item(bob, "bob", "struct{...}", "struct") +- x int -} - --// Strings. -func _() { -- const ( -- str = "hello" + " " + "world" -- longStr = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur eget ipsum non nunc --molestie mattis id quis augue. Mauris dictum tincidunt ipsum, in auctor arcu congue eu. --Morbi hendrerit fringilla libero commodo varius. Vestibulum in enim rutrum, rutrum tellus --aliquet, luctus enim. Nunc sem ex, consectetur id porta nec, placerat vel urna.` -- ) +- var q int +- _ = &bob{ +- f: q, //@diag("f: q", re"unknown field f in struct literal") +- } +-} - -- _ = str //@hover("str", "str", strConst) -- _ = longStr //@hover("longStr", "longStr", longStrConst) +--- bad/bad1.go -- +-package bad +- +-// See #36637 +-type stateFunc func() stateFunc //@item(stateFunc, "stateFunc", "func() stateFunc", "type") +- +-var a unknown //@item(global_a, "a", "unknown", "var"),diag("unknown", re"(undeclared name|undefined): unknown") +- +-func random() int { //@item(random, "random", "func() int", "func") +- //@complete("", global_a, bob, random, random2, random3, stateFunc, stuff) +- return 0 -} - --// Constants from other packages. --func _() { -- _ = math.Log2E //@hover("Log2E", "Log2E", log2eConst) +-func random2(y int) int { //@item(random2, "random2", "func(y int) int", "func"),item(bad_y_param, "y", "int", "var") +- x := 6 //@item(x, "x", "int", "var"),diag("x", re"x.*declared (and|but) not used") +- var q blah //@item(q, "q", "blah", "var"),diag("q", re"q.*declared (and|but) not used"),diag("blah", re"(undeclared name|undefined): blah") +- var t **blob //@item(t, "t", "**blob", "var"),diag("t", re"t.*declared (and|but) not used"),diag("blob", re"(undeclared name|undefined): blob") +- //@complete("", q, t, x, bad_y_param, global_a, bob, random, random2, random3, stateFunc, stuff) +- +- return y -} - ---- @bX/hover.md -- --```go --const X untyped int = 0 --``` +-func random3(y ...int) { //@item(random3, "random3", "func(y ...int)", "func"),item(y_variadic_param, "y", "[]int", "var") +- //@complete("", y_variadic_param, global_a, bob, random, random2, random3, stateFunc, stuff) - --@hover("X", "X", bX) +- var ch chan (favType1) //@item(ch, "ch", "chan (favType1)", "var"),diag("ch", re"ch.*declared (and|but) not used"),diag("favType1", re"(undeclared name|undefined): favType1") +- var m map[keyType]int //@item(m, "m", "map[keyType]int", "var"),diag("m", re"m.*declared (and|but) not used"),diag("keyType", re"(undeclared name|undefined): keyType") +- var arr []favType2 //@item(arr, "arr", "[]favType2", "var"),diag("arr", re"arr.*declared (and|but) not used"),diag("favType2", re"(undeclared name|undefined): favType2") +- var fn1 func() badResult //@item(fn1, "fn1", "func() badResult", "var"),diag("fn1", re"fn1.*declared (and|but) not used"),diag("badResult", re"(undeclared name|undefined): badResult") +- var fn2 func(badParam) //@item(fn2, "fn2", "func(badParam)", "var"),diag("fn2", re"fn2.*declared (and|but) not used"),diag("badParam", re"(undeclared name|undefined): badParam") +- //@complete("", arr, ch, fn1, fn2, m, y_variadic_param, global_a, bob, random, random2, random3, stateFunc, stuff) +-} +diff -urN a/gopls/internal/test/marker/testdata/completion/basic_lit.txt b/gopls/internal/test/marker/testdata/completion/basic_lit.txt +--- a/gopls/internal/test/marker/testdata/completion/basic_lit.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/basic_lit.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,19 +0,0 @@ +-This test checks completion related to basic literals. - +--- flags -- +--ignore_extra_diags - --[`c.X` on pkg.go.dev](https://pkg.go.dev/mod.com#X) ---- @dur/hover.md -- --```go --const dur time.Duration = 15*time.Minute + 10*time.Second + 350*time.Millisecond // 15m10.35s --``` +--- basiclit.go -- +-package basiclit - --dur is a constant of type time.Duration. ---- @decimalConst/hover.md -- --```go --const decimal untyped int = 153 --``` +-func _() { +- var a int // something for lexical completions - --no inline comment ---- @hexConst/hover.md -- --```go --const hex untyped int = 0xe34e // 58190 --``` ---- @binConst/hover.md -- --```go --const bin untyped int = 0b1001001 // 73 --``` ---- @numberWithUnderscoreConst/hover.md -- --```go --const numberWithUnderscore int64 = 10_000_000_000 // 10000000000 --``` ---- @octalConst/hover.md -- --```go --const octal untyped int = 0o777 // 511 --``` ---- @exprConst/hover.md -- --```go --const expr untyped int = 2 << (0b111&0b101 - 2) // 16 --``` ---- @boolConst/hover.md -- --```go --const boolean untyped bool = (55 - 3) == (26 * 2) // true --``` ---- @ln10Const/hover.md -- --```go --const ln10 untyped float = 2.30258509299404568401799145468436420760110148862877297603332790 // 2.30259 --``` ---- @aIota/hover.md -- --```go --const a untyped int = 1 << iota // 1 --``` ---- @bIota/hover.md -- --```go --const b untyped int = 2 --``` ---- @strConst/hover.md -- --```go --const str untyped string = "hello world" --``` ---- @longStrConst/hover.md -- --```go --const longStr untyped string = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur e... --``` ---- @log2eConst/hover.md -- --```go --const math.Log2E untyped float = 1 / Ln2 // 1.4427 --``` +- _ = "hello." //@complete(".") - --Mathematical constants. +- _ = 1 //@complete(" //") - +- _ = 1. //@complete(".") - --[`math.Log2E` on pkg.go.dev](https://pkg.go.dev/math#Log2E) -diff -urN a/gopls/internal/regtest/marker/testdata/hover/generics.txt b/gopls/internal/regtest/marker/testdata/hover/generics.txt ---- a/gopls/internal/regtest/marker/testdata/hover/generics.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/hover/generics.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,76 +0,0 @@ --This file contains tests for hovering over generic Go code. +- _ = 'a' //@complete("' ") +-} +diff -urN a/gopls/internal/test/marker/testdata/completion/builtins.txt b/gopls/internal/test/marker/testdata/completion/builtins.txt +--- a/gopls/internal/test/marker/testdata/completion/builtins.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/builtins.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,118 +0,0 @@ +-This test checks completion of Go builtins. - --- flags -- ---min_go=go1.18 +--ignore_extra_diags +--filter_builtins=false - ---- go.mod -- --// A go.mod is require for correct pkgsite links. --// TODO(rfindley): don't link to ad-hoc or command-line-arguments packages! --module mod.com +--- builtin_args.go -- +-package builtins - --go 1.18 +-func _() { +- var ( +- aSlice []int //@item(builtinSlice, "aSlice", "[]int", "var") +- aMap map[string]int //@item(builtinMap, "aMap", "map[string]int", "var") +- aString string //@item(builtinString, "aString", "string", "var") +- aArray [0]int //@item(builtinArray, "aArray", "[0]int", "var") +- aArrayPtr *[0]int //@item(builtinArrayPtr, "aArrayPtr", "*[0]int", "var") +- aChan chan int //@item(builtinChan, "aChan", "chan int", "var") +- aPtr *int //@item(builtinPtr, "aPtr", "*int", "var") +- aInt int //@item(builtinInt, "aInt", "int", "var") +- ) - ---- generics.go -- --package generics +- type ( +- aSliceType []int //@item(builtinSliceType, "aSliceType", "[]int", "type") +- aChanType chan int //@item(builtinChanType, "aChanType", "chan int", "type") +- aMapType map[string]int //@item(builtinMapType, "aMapType", "map[string]int", "type") +- ) - --type value[T any] struct { //hover("lue", "value", value),hover("T", "T", valueT) -- val T //@hover("T", "T", valuevalT) -- Q int //@hover("Q", "Q", valueQ) --} +- close() //@rank(")", builtinChan, builtinSlice) - --type Value[T any] struct { //@hover("T", "T", ValueT) -- val T //@hover("T", "T", ValuevalT) -- Q int //@hover("Q", "Q", ValueQ) --} +- append() //@rank(")", builtinSlice, builtinChan) - --// disabled - see issue #54822 --func F[P interface{ ~int | string }]() { // hover("P","P",Ptparam) -- // disabled - see issue #54822 -- var _ P // hover("P","P",Pvar) --} +- var _ []byte = append([]byte(nil), ""...) //@rank(") //") - ---- inferred.go -- --package generics +- copy() //@rank(")", builtinSlice, builtinChan) +- copy(aSlice, aS) //@rank(")", builtinSlice, builtinString) +- copy(aS, aSlice) //@rank(",", builtinSlice, builtinString) - --func app[S interface{ ~[]E }, E interface{}](s S, e E) S { -- return append(s, e) +- delete() //@rank(")", builtinMap, builtinChan) +- delete(aMap, aS) //@rank(")", builtinString, builtinSlice) +- +- aMapFunc := func() map[int]int { //@item(builtinMapFunc, "aMapFunc", "func() map[int]int", "var") +- return nil +- } +- delete() //@rank(")", builtinMapFunc, builtinSlice) +- +- len() //@rank(")", builtinSlice, builtinInt),rank(")", builtinMap, builtinInt),rank(")", builtinString, builtinInt),rank(")", builtinArray, builtinInt),rank(")", builtinArrayPtr, builtinPtr),rank(")", builtinChan, builtinInt) +- +- cap() //@rank(")", builtinSlice, builtinMap),rank(")", builtinArray, builtinString),rank(")", builtinArrayPtr, builtinPtr),rank(")", builtinChan, builtinInt) +- +- make() //@rank(")", builtinMapType, int),rank(")", builtinChanType, int),rank(")", builtinSliceType, int),rank(")", builtinMapType, int) +- make(aSliceType, a) //@rank(")", builtinInt, builtinSlice) +- +- type myInt int +- var mi myInt //@item(builtinMyInt, "mi", "myInt", "var") +- make(aSliceType, m) //@snippet(")", builtinMyInt, "mi") +- +- var _ []int = make() //@rank(")", builtinSliceType, builtinMapType) +- +- type myStruct struct{} //@item(builtinStructType, "myStruct", "struct{...}", "struct") +- var _ *myStruct = new() //@rank(")", builtinStructType, int) +- +- for k := range a { //@rank(" {", builtinSlice, builtinInt),rank(" {", builtinString, builtinInt),rank(" {", builtinChan, builtinInt),rank(" {", builtinArray, builtinInt),rank(" {", builtinArrayPtr, builtinInt),rank(" {", builtinMap, builtinInt), +- } +- +- for k, v := range a { //@rank(" {", builtinSlice, builtinChan) +- } +- +- <-a //@rank(" //", builtinChan, builtinInt) -} - +--- builtin_types.go -- +-package builtins +- -func _() { -- _ = app[[]int] //@hover("app", "app", appint) -- _ = app[[]int, int] //@hover("app", "app", appint) -- _ = app[[]int]([]int{}, 0) //@hover("app", "app", appint) -- _ = app([]int{}, 0) //@hover("app", "app", appint) --} +- var _ []bool //@item(builtinBoolSliceType, "[]bool", "[]bool", "type") - ---- @ValueQ/hover.md -- --```go --field Q int --``` +- var _ []bool = make() //@rank(")", builtinBoolSliceType, int) - --@hover("Q", "Q", ValueQ) +- var _ []bool = make([], 0) //@rank(",", bool, int) - +- var _ [][]bool = make([][], 0) //@rank(",", bool, int) +-} - --[`(generics.Value).Q` on pkg.go.dev](https://pkg.go.dev/mod.com#Value.Q) ---- @ValueT/hover.md -- --```go --type parameter T any --``` ---- @ValuevalT/hover.md -- --```go --type parameter T any --``` ---- @appint/hover.md -- --```go --func app(s []int, e int) []int // func[S interface{~[]E}, E interface{}](s S, e E) S --``` ---- @valueQ/hover.md -- --```go --field Q int --``` +--- builtins.go -- +-package builtins - --@hover("Q", "Q", valueQ) ---- @valuevalT/hover.md -- --```go --type parameter T any --``` -diff -urN a/gopls/internal/regtest/marker/testdata/hover/godef.txt b/gopls/internal/regtest/marker/testdata/hover/godef.txt ---- a/gopls/internal/regtest/marker/testdata/hover/godef.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/hover/godef.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,406 +0,0 @@ --This test was ported from 'godef' in the old marker tests. --It tests various hover and definition requests. +-// Definitions of builtin completion items that are still used in tests. - ---- flags -- ---min_go=go1.20 +-/* bool */ //@item(bool, "bool", "", "type") +-/* complex(r float64, i float64) */ //@item(complex, "complex", "func(r float64, i float64) complex128", "func") +-/* float32 */ //@item(float32, "float32", "", "type") +-/* float64 */ //@item(float64, "float64", "", "type") +-/* imag(c complex128) float64 */ //@item(imag, "imag", "func(c complex128) float64", "func") +-/* int */ //@item(int, "int", "", "type") +-/* iota */ //@item(iota, "iota", "", "const") +-/* string */ //@item(string, "string", "", "type") +-/* true */ //@item(_true, "true", "", "const") - ---- go.mod -- --module godef.test +--- constants.go -- +-package builtins - --go 1.18 +-func _() { +- const ( +- foo = iota //@complete(" //", iota) +- ) - ---- a/a_x_test.go -- --package a_test +- iota //@complete(" //") - --import ( -- "testing" --) +- var iota int //@item(iotaVar, "iota", "int", "var") - --func TestA2(t *testing.T) { //@hover("TestA2", "TestA2", TestA2) -- Nonexistant() //@diag("Nonexistant", re"(undeclared name|undefined): Nonexistant") +- iota //@complete(" //", iotaVar) -} - ---- @TestA2/hover.md -- --```go --func TestA2(t *testing.T) --``` ---- @ember/hover.md -- --```go --field Member string --``` +-func _() { +- var twoRedUpEnd bool //@item(TRUEVar, "twoRedUpEnd", "bool", "var") - --@loc(Member, "Member") +- var _ bool = true //@rank(" //", _true, TRUEVar) +-} +diff -urN a/gopls/internal/test/marker/testdata/completion/casesensitive.txt b/gopls/internal/test/marker/testdata/completion/casesensitive.txt +--- a/gopls/internal/test/marker/testdata/completion/casesensitive.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/casesensitive.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,24 +0,0 @@ +-This test exercises the caseSensitive completion matcher. - +--- flags -- +--ignore_extra_diags - --[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/godef.test/a#Thing.Member) ---- a/d.go -- --package a //@hover("a", _, a) +--- settings.json -- +-{ +- "completeUnimported": false, +- "matcher": "caseSensitive" +-} - --import "fmt" +--- casesensitive.go -- +-package casesensitive +- +-func _() { +- var lower int //@item(lower, "lower", "int", "var") +- var Upper int //@item(upper, "Upper", "int", "var") +- +- l //@complete(" //", lower) +- U //@complete(" //", upper) - --type Thing struct { //@loc(Thing, "Thing") -- Member string //@loc(Member, "Member") +- L //@complete(" //") +- u //@complete(" //") -} +diff -urN a/gopls/internal/test/marker/testdata/completion/cast.txt b/gopls/internal/test/marker/testdata/completion/cast.txt +--- a/gopls/internal/test/marker/testdata/completion/cast.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/cast.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,17 +0,0 @@ +-This test checks completion related to casts. - --var Other Thing //@loc(Other, "Other") +--- flags -- +--ignore_extra_diags - --func Things(val []string) []Thing { //@loc(Things, "Things") -- return nil --} +--- cast.go -- +-package cast - --func (t Thing) Method(i int) string { //@loc(Method, "Method") -- return t.Member +-func _() { +- foo := struct{x int}{x: 1} //@item(x_field, "x", "int", "field") +- _ = float64(foo.x) //@complete("x", x_field) -} - --func (t Thing) Method3() { +-func _() { +- foo := struct{x int}{x: 1} +- _ = float64(foo. //@complete(" /", x_field) -} +diff -urN a/gopls/internal/test/marker/testdata/completion/channel.txt b/gopls/internal/test/marker/testdata/completion/channel.txt +--- a/gopls/internal/test/marker/testdata/completion/channel.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/channel.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,36 +0,0 @@ +-This test checks completion related to channels. - --func (t *Thing) Method2(i int, j int) (error, string) { -- return nil, t.Member --} +--- flags -- +--ignore_extra_diags - --func (t *Thing) private() { +--- settings.json -- +-{ +- "completeUnimported": false -} - --func useThings() { -- t := Thing{ //@hover("ing", "Thing", ing) -- Member: "string", //@hover("ember", "Member", ember), def("ember", Member) +--- channel.go -- +-package channel +- +-func _() { +- var ( +- aa = "123" //@item(channelAA, "aa", "string", "var") +- ab = 123 //@item(channelAB, "ab", "int", "var") +- ) +- +- { +- type myChan chan int +- var mc myChan +- mc <- a //@complete(" //", channelAB, channelAA) - } -- fmt.Print(t.Member) //@hover("ember", "Member", ember), def("ember", Member) -- fmt.Print(Other) //@hover("ther", "Other", ther), def("ther", Other) -- Things(nil) //@hover("ings", "Things", ings), def("ings", Things) -- t.Method(0) //@hover("eth", "Method", eth), def("eth", Method) --} - --type NextThing struct { //@loc(NextThing, "NextThing") -- Thing -- Value int --} +- { +- var ac chan int //@item(channelAC, "ac", "chan int", "var") +- a <- a //@complete(" <-", channelAC, channelAA, channelAB) +- } - --func (n NextThing) another() string { -- return n.Member +- { +- var foo chan int //@item(channelFoo, "foo", "chan int", "var") +- wantsInt := func(int) {} //@item(channelWantsInt, "wantsInt", "func(int)", "var") +- wantsInt(<-) //@rank(")", channelFoo, channelAB) +- } -} +diff -urN a/gopls/internal/test/marker/testdata/completion/comment.txt b/gopls/internal/test/marker/testdata/completion/comment.txt +--- a/gopls/internal/test/marker/testdata/completion/comment.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/comment.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,81 +0,0 @@ +-This test checks behavior of completion within comments. - --// Shadows Thing.Method3 --func (n *NextThing) Method3() int { -- return n.Value --} +--- flags -- +--ignore_extra_diags - --var nextThing NextThing //@hover("NextThing", "NextThing", NextThing), def("NextThing", NextThing) +--- go.mod -- +-module golang.org/lsptests/comment - ---- @ings/hover.md -- --```go --func Things(val []string) []Thing --``` +-go 1.18 - --[`a.Things` on pkg.go.dev](https://pkg.go.dev/godef.test/a#Things) ---- @ther/hover.md -- --```go --var Other Thing --``` +--- p.go -- +-package comment_completion - --@loc(Other, "Other") +-var p bool - +-//@complete(re"$") - --[`a.Other` on pkg.go.dev](https://pkg.go.dev/godef.test/a#Other) ---- @a/hover.md -- ---- @ing/hover.md -- --```go --type Thing struct { -- Member string //@loc(Member, "Member") --} +-func _() { +- var a int - --func (Thing).Method(i int) string --func (*Thing).Method2(i int, j int) (error, string) --func (Thing).Method3() --func (*Thing).private() --``` +- switch a { +- case 1: +- //@complete(re"$") +- _ = a +- } - --[`a.Thing` on pkg.go.dev](https://pkg.go.dev/godef.test/a#Thing) ---- @NextThing/hover.md -- --```go --type NextThing struct { -- Thing -- Value int +- var b chan int +- select { +- case <-b: +- //@complete(re"$") +- _ = b +- } +- +- var ( +- //@complete(re"$") +- _ = a +- ) -} - --func (*NextThing).Method3() int --func (NextThing).another() string --``` +-// //@complete(" ", variableC) +-var C string //@item(variableC, "C", "string", "var") //@complete(" ", variableC) - --[`a.NextThing` on pkg.go.dev](https://pkg.go.dev/godef.test/a#NextThing) ---- @eth/hover.md -- --```go --func (Thing).Method(i int) string --``` +-// //@complete(" ", constant) +-const Constant = "example" //@item(constant, "Constant", "string", "const") //@complete(" ", constant) - --[`(a.Thing).Method` on pkg.go.dev](https://pkg.go.dev/godef.test/a#Thing.Method) ---- a/f.go -- --// Package a is a package for testing go to definition. --package a +-// //@complete(" ", structType, fieldB, fieldA) +-type StructType struct { //@item(structType, "StructType", "struct{...}", "struct") //@complete(" ", structType, fieldA, fieldB) +- // //@complete(" ", fieldA, structType, fieldB) +- A string //@item(fieldA, "A", "string", "field") //@complete(" ", fieldA, structType, fieldB) +- b int //@item(fieldB, "b", "int", "field") //@complete(" ", fieldB, structType, fieldA) +-} - --import "fmt" +-// //@complete(" ", method, structRecv, paramX, resultY, fieldB, fieldA) +-func (structType *StructType) Method(X int) (Y int) { //@item(structRecv, "structType", "*StructType", "var"),item(method, "Method", "func(X int) (Y int)", "method"),item(paramX, "X", "int", "var"),item(resultY, "Y", "int", "var") +- // //@complete(" ", method, structRecv, paramX, resultY, fieldB, fieldA) +- return +-} - --func TypeStuff() { -- var x string +-// //@complete(" ", newType) +-type NewType string //@item(newType, "NewType", "string", "type") //@complete(" ", newType) - -- switch y := interface{}(x).(type) { //@loc(y, "y"), hover("y", "y", y) , def("y", y) -- case int: //@loc(intY, "int") -- fmt.Printf("%v", y) //@hover("y", "y", inty), def("y", y) -- case string: //@loc(stringY, "string") -- fmt.Printf("%v", y) //@hover("y", "y", stringy), def("y", y) -- } +-// //@complete(" ", testInterface, testA, testB) +-type TestInterface interface { //@item(testInterface, "TestInterface", "interface{...}", "interface") +- // //@complete(" ", testA, testInterface, testB) +- TestA(L string) (M int) //@item(testA, "TestA", "func(L string) (M int)", "method"),item(paramL, "L", "var", "string"),item(resM, "M", "var", "int") //@complete(" ", testA, testInterface, testB) +- TestB(N int) bool //@item(testB, "TestB", "func(N int) bool", "method"),item(paramN, "N", "var", "int") //@complete(" ", testB, testInterface, testA) +-} - +-// //@complete(" ", function) +-func Function() int { //@item(function, "Function", "func() int", "func") //@complete(" ", function) +- // //@complete(" ", function) +- return 0 -} ---- @inty/hover.md -- --```go --var y int --``` ---- @stringy/hover.md -- --```go --var y string --``` ---- @y/hover.md -- --```go --var y interface{} --``` ---- a/h.go -- --package a - --func _() { -- type s struct { -- nested struct { -- // nested number -- number int64 //@loc(nestedNumber, "number") -- } -- nested2 []struct { -- // nested string -- str string //@loc(nestedString, "str") -- } -- x struct { -- x struct { -- x struct { -- x struct { -- x struct { -- // nested map -- m map[string]float64 //@loc(nestedMap, "m") -- } -- } -- } -- } -- } -- } +-// This tests multiline block comments and completion with prefix +-// Lorem Ipsum Multili//@complete("Multi", multiline) +-// Lorem ipsum dolor sit ametom +-func Multiline() int { //@item(multiline, "Multiline", "func() int", "func") +- // //@complete(" ", multiline) +- return 0 +-} +diff -urN a/gopls/internal/test/marker/testdata/completion/complit.txt b/gopls/internal/test/marker/testdata/completion/complit.txt +--- a/gopls/internal/test/marker/testdata/completion/complit.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/complit.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,104 +0,0 @@ +-This test checks completion related to composite literals. - -- var t s -- _ = t.nested.number //@hover("number", "number", nestedNumber), def("number", nestedNumber) -- _ = t.nested2[0].str //@hover("str", "str", nestedString), def("str", nestedString) -- _ = t.x.x.x.x.x.m //@hover("m", "m", nestedMap), def("m", nestedMap) +--- flags -- +--ignore_extra_diags +- +--- settings.json -- +-{ +- "completeUnimported": false -} - --func _() { -- var s struct { -- // a field -- a int //@loc(structA, "a") -- // b nested struct -- b struct { //@loc(structB, "b") -- // c field of nested struct -- c int //@loc(structC, "c") -- } -- } -- _ = s.a //@def("a", structA) -- _ = s.b //@def("b", structB) -- _ = s.b.c //@def("c", structC) +--- complit.go -- +-package complit - -- var arr []struct { -- // d field -- d int //@loc(arrD, "d") -- // e nested struct -- e struct { //@loc(arrE, "e") -- // f field of nested struct -- f int //@loc(arrF, "f") -- } -- } -- _ = arr[0].d //@def("d", arrD) -- _ = arr[0].e //@def("e", arrE) -- _ = arr[0].e.f //@def("f", arrF) +-// Literal completion results. +-/* int() */ //@item(int, "int()", "int", "var") - -- var complex []struct { -- c <-chan map[string][]struct { -- // h field -- h int //@loc(complexH, "h") -- // i nested struct -- i struct { //@loc(complexI, "i") -- // j field of nested struct -- j int //@loc(complexJ, "j") -- } -- } -- } -- _ = (<-complex[0].c)["0"][0].h //@def("h", complexH) -- _ = (<-complex[0].c)["0"][0].i //@def("i", complexI) -- _ = (<-complex[0].c)["0"][0].i.j //@def("j", complexJ) +-// general completions - -- var mapWithStructKey map[struct { //@diag("struct", re"invalid map key") -- // X key field -- x []string //@loc(mapStructKeyX, "x") -- }]int -- for k := range mapWithStructKey { -- _ = k.x //@def("x", mapStructKeyX) -- } +-type position struct { //@item(structPosition, "position", "struct{...}", "struct") +- X, Y int //@item(fieldX, "X", "int", "field"),item(fieldY, "Y", "int", "field") +-} - -- var mapWithStructKeyAndValue map[struct { -- // Y key field -- y string //@loc(mapStructKeyY, "y") -- }]struct { -- // X value field -- x string //@loc(mapStructValueX, "x") +-func _() { +- _ = position{ +- //@complete("", fieldX, fieldY, int, structPosition) - } -- for k, v := range mapWithStructKeyAndValue { -- // TODO: we don't show docs for y field because both map key and value -- // are structs. And in this case, we parse only map value -- _ = k.y //@hover("y", "y", hoverStructKeyY), def("y", mapStructKeyY) -- _ = v.x //@hover("x", "x", hoverStructKeyX), def("x", mapStructValueX) +- _ = position{ +- X: 1, +- //@complete("", fieldY) - } -- -- var i []map[string]interface { -- // open method comment -- open() error //@loc(openMethod, "open") +- _ = position{ +- //@complete("", fieldX) +- Y: 1, +- } +- _ = []*position{ +- { +- //@complete("", fieldX, fieldY, int, structPosition) +- }, - } -- i[0]["1"].open() //@hover("pen","open", openMethod), def("open", openMethod) -} - -func _() { -- test := struct { -- // test description -- desc string //@loc(testDescription, "desc") -- }{} -- _ = test.desc //@def("desc", testDescription) -- -- for _, tt := range []struct { -- // test input -- in map[string][]struct { //@loc(testInput, "in") -- // test key -- key string //@loc(testInputKey, "key") -- // test value -- value interface{} //@loc(testInputValue, "value") -- } -- result struct { -- v <-chan struct { -- // expected test value -- value int //@loc(testResultValue, "value") -- } -- } -- }{} { -- _ = tt.in //@def("in", testInput) -- _ = tt.in["0"][0].key //@def("key", testInputKey) -- _ = tt.in["0"][0].value //@def("value", testInputValue) +- var ( +- aa string //@item(aaVar, "aa", "string", "var") +- ab int //@item(abVar, "ab", "int", "var") +- ) - -- _ = (<-tt.result.v).value //@def("value", testResultValue) +- _ = map[int]int{ +- a: a, //@complete(":", abVar, aaVar),complete(",", abVar, aaVar) - } --} - --func _() { -- getPoints := func() []struct { -- // X coord -- x int //@loc(returnX, "x") -- // Y coord -- y int //@loc(returnY, "y") -- } { -- return nil +- _ = map[int]int{ +- //@complete("", abVar, int, aaVar, structPosition) - } - -- r := getPoints() -- _ = r[0].x //@def("x", returnX) -- _ = r[0].y //@def("y", returnY) --} ---- @hoverStructKeyX/hover.md -- --```go --field x string --``` -- --X value field ---- @hoverStructKeyY/hover.md -- --```go --field y string --``` +- _ = []string{a: ""} //@complete(":", abVar, aaVar) +- _ = [1]string{a: ""} //@complete(":", abVar, aaVar) - --Y key field ---- @nestedNumber/hover.md -- --```go --field number int64 --``` +- _ = position{X: a} //@complete("}", abVar, aaVar) +- _ = position{a} //@complete("}", abVar, aaVar) +- _ = position{a, } //@complete("}", abVar, int, aaVar, structPosition) - --nested number ---- @nestedString/hover.md -- --```go --field str string --``` +- _ = []int{a} //@complete("}", abVar, aaVar) +- _ = [1]int{a} //@complete("}", abVar, aaVar) - --nested string ---- @openMethod/hover.md -- --```go --func (interface).open() error --``` +- type myStruct struct { +- AA int //@item(fieldAA, "AA", "int", "field") +- AB string //@item(fieldAB, "AB", "string", "field") +- } - --open method comment ---- @nestedMap/hover.md -- --```go --field m map[string]float64 --``` +- _ = myStruct{ +- AB: a, //@complete(",", aaVar, abVar) +- } - --nested map ---- b/e.go -- --package b +- var s myStruct - --import ( -- "fmt" +- _ = map[int]string{1: "" + s.A} //@complete("}", fieldAB, fieldAA) +- _ = map[int]string{1: (func(i int) string { return "" })(s.A)} //@complete(")}", fieldAA, fieldAB) +- _ = map[int]string{1: func() string { s.A }} //@complete(" }", fieldAA, fieldAB) - -- "godef.test/a" --) +- _ = position{s.A} //@complete("}", fieldAA, fieldAB) - --func useThings() { -- t := a.Thing{} //@loc(bStructType, "ing") -- fmt.Print(t.Member) //@loc(bMember, "ember") -- fmt.Print(a.Other) //@loc(bVar, "ther") -- a.Things(nil) //@loc(bFunc, "ings") +- var X int //@item(varX, "X", "int", "var") +- _ = position{X} //@complete("}", fieldX, varX) -} - --/*@ --def(bStructType, Thing) --def(bMember, Member) --def(bVar, Other) --def(bFunc, Things) --*/ -- -func _() { -- var x interface{} -- switch x := x.(type) { //@hover("x", "x", xInterface) -- case string: //@loc(eString, "string") -- fmt.Println(x) //@hover("x", "x", xString) -- case int: //@loc(eInt, "int") -- fmt.Println(x) //@hover("x", "x", xInt) -- } --} ---- @xInt/hover.md -- --```go --var x int --``` ---- @xInterface/hover.md -- --```go --var x interface{} --``` ---- @xString/hover.md -- --```go --var x string --``` ---- broken/unclosedIf.go -- --package broken +- type foo struct{} //@item(complitFoo, "foo", "struct{...}", "struct") - --import "fmt" +- var _ *foo = &fo{} //@snippet("{", complitFoo, "foo") +- var _ *foo = fo{} //@snippet("{", complitFoo, "&foo") - --func unclosedIf() { -- if false { -- var myUnclosedIf string //@loc(myUnclosedIf, "myUnclosedIf") -- fmt.Printf("s = %v\n", myUnclosedIf) //@def("my", myUnclosedIf) +- struct { a, b *foo }{ +- a: &fo{}, //@rank("{", complitFoo) +- b: fo{}, //@snippet("{", complitFoo, "&foo") +- } -} - --func _() {} //@diag("_", re"expected") -diff -urN a/gopls/internal/regtest/marker/testdata/hover/goprivate.txt b/gopls/internal/regtest/marker/testdata/hover/goprivate.txt ---- a/gopls/internal/regtest/marker/testdata/hover/goprivate.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/hover/goprivate.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,27 +0,0 @@ --This test checks that links in hover obey GOPRIVATE. ---- env -- --GOPRIVATE=mod.com ---- go.mod -- --module mod.com ---- p.go -- --package p -- --// T should not be linked, as it is private. --type T struct{} //@hover("T", "T", T) ---- lib/lib.go -- --package lib +-func _() { +- _ := position{ +- X: 1, //@complete("X", fieldX),complete(" 1", int, structPosition) +- Y: , //@complete(":", fieldY),complete(" ,", int, structPosition) +- } +-} +diff -urN a/gopls/internal/test/marker/testdata/completion/constant.txt b/gopls/internal/test/marker/testdata/completion/constant.txt +--- a/gopls/internal/test/marker/testdata/completion/constant.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/constant.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,20 +0,0 @@ +-This test checks completion related to constants. - --// GOPRIVATE should also match nested packages. --type L struct{} //@hover("L", "L", L) ---- @L/hover.md -- --```go --type L struct{} --``` +--- flags -- +--ignore_extra_diags - --GOPRIVATE should also match nested packages. ---- @T/hover.md -- --```go --type T struct{} --``` +--- constant.go -- +-package constant - --T should not be linked, as it is private. -diff -urN a/gopls/internal/regtest/marker/testdata/hover/hover.txt b/gopls/internal/regtest/marker/testdata/hover/hover.txt ---- a/gopls/internal/regtest/marker/testdata/hover/hover.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/hover/hover.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,29 +0,0 @@ --This test demonstrates some features of the new marker test runner. ---- a.go -- --package a +-const x = 1 //@item(constX, "x", "int", "const") - --const abc = 0x2a //@hover("b", "abc", abc),hover(" =", "abc", abc) ---- typeswitch.go -- --package a +-const ( +- a int = iota << 2 //@item(constA, "a", "int", "const") +- b //@item(constB, "b", "int", "const") +- c //@item(constC, "c", "int", "const") +-) - -func _() { -- var y interface{} -- switch x := y.(type) { //@hover("x", "x", x) -- case int: -- println(x) //@hover("x", "x", xint),hover(")", "x", xint) -- } --} ---- @abc/hover.md -- --```go --const abc untyped int = 0x2a // 42 --``` -- --@hover("b", "abc", abc),hover(" =", "abc", abc) ---- @x/hover.md -- --```go --var x interface{} --``` ---- @xint/hover.md -- --```go --var x int --``` -diff -urN a/gopls/internal/regtest/marker/testdata/hover/linkable_generics.txt b/gopls/internal/regtest/marker/testdata/hover/linkable_generics.txt ---- a/gopls/internal/regtest/marker/testdata/hover/linkable_generics.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/hover/linkable_generics.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,145 +0,0 @@ --This file contains tests for documentation links to generic code in hover. +- const y = "hi" //@item(constY, "y", "string", "const") +- //@complete("", constY, constA, constB, constC, constX) +-} +diff -urN a/gopls/internal/test/marker/testdata/completion/danglingstmt.txt b/gopls/internal/test/marker/testdata/completion/danglingstmt.txt +--- a/gopls/internal/test/marker/testdata/completion/danglingstmt.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/danglingstmt.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,158 +0,0 @@ +-This test checks that completion works as expected in the presence of +-incomplete statements that may affect parser recovery. - --- flags -- ---min_go=go1.18 +--ignore_extra_diags - --- go.mod -- --module mod.com +-module golang.org/lsptests/dangling - --go 1.19 +-go 1.18 - ---- a.go -- --package a +--- settings.json -- +-{ +- "completeUnimported": false, +- "deepCompletion": false +-} - --import "mod.com/generic" +--- dangling_for.go -- +-package danglingstmt - -func _() { -- // Hovering over instantiated object should produce accurate type -- // information, but link to the generic declarations. +- for bar //@rank(" //", danglingBar) +-} - -- var x generic.GT[int] //@hover("GT", "GT", xGT) -- _ = x.F //@hover("x", "x", x),hover("F", "F", xF) +-func bar() bool { //@item(danglingBar, "bar", "func() bool", "func") +- return true +-} - -- f := generic.GF[int] //@hover("GF", "GF", fGF) -- _ = f //@hover("f", "f", f) +--- dangling_for_init.go -- +-package danglingstmt +- +-func _() { +- for i := bar //@rank(" //", danglingBar2) -} - ---- generic/generic.go -- --package generic +-func bar2() int { //@item(danglingBar2, "bar2", "func() int", "func") +- return 0 +-} - --// Hovering over type parameters should link to documentation. --// --// TODO(rfindley): should it? We should probably link to the type. --type GT[P any] struct{ //@hover("GT", "GT", GT),hover("P", "P", GTP) -- F P //@hover("F", "F", F),hover("P", "P", FP) +--- dangling_for_init_cond.go -- +-package danglingstmt +- +-func _() { +- for i := bar3(); i > bar //@rank(" //", danglingBar3) -} - --func (GT[P]) M(p P) { //@hover("GT", "GT", GTrecv),hover("M","M", M),hover(re"p (P)", re"p (P)", pP) +-func bar3() int { //@item(danglingBar3, "bar3", "func() int", "func") +- return 0 -} - --func GF[P any] (p P) { //@hover("GF", "GF", GF) +--- dangling_for_init_cond_post.go -- +-package danglingstmt +- +-func _() { +- for i := bar4(); i > bar4(); i += bar //@rank(" //", danglingBar4) -} - ---- @F/hover.md -- --```go --field F P --``` +-func bar4() int { //@item(danglingBar4, "bar4", "func() int", "func") +- return 0 +-} - --@hover("F", "F", F),hover("P", "P", FP) +--- dangling_if.go -- +-package danglingstmt - +-func _() { +- if foo //@rank(" //", danglingFoo) +-} - --[`(generic.GT).F` on pkg.go.dev](https://pkg.go.dev/mod.com/generic#GT.F) ---- @FP/hover.md -- --```go --type parameter P any --``` ---- @GF/hover.md -- --```go --func GF[P any](p P) --``` +-func foo() bool { //@item(danglingFoo, "foo", "func() bool", "func") +- return true +-} - --[`generic.GF` on pkg.go.dev](https://pkg.go.dev/mod.com/generic#GF) ---- @GT/hover.md -- --```go --type GT[P any] struct { -- F P //@hover("F", "F", F),hover("P", "P", FP) +--- dangling_if_eof.go -- +-package danglingstmt +- +-func bar5() bool { //@item(danglingBar5, "bar5", "func() bool", "func") +- return true -} - --func (GT[P]).M(p P) --``` +-func _() { +- if b //@rank(" //", danglingBar5) - --Hovering over type parameters should link to documentation. +--- dangling_if_init.go -- +-package danglingstmt - --TODO(rfindley): should it? We should probably link to the type. +-func _() { +- if i := foo //@rank(" //", danglingFoo2) +-} - +-func foo2() bool { //@item(danglingFoo2, "foo2", "func() bool", "func") +- return true +-} - --[`generic.GT` on pkg.go.dev](https://pkg.go.dev/mod.com/generic#GT) ---- @GTP/hover.md -- --```go --type parameter P any --``` ---- @GTrecv/hover.md -- --```go --type GT[P any] struct { -- F P //@hover("F", "F", F),hover("P", "P", FP) +--- dangling_if_init_cond.go -- +-package danglingstmt +- +-func _() { +- if i := 123; foo //@rank(" //", danglingFoo3) -} - --func (GT[P]).M(p P) --``` +-func foo3() bool { //@item(danglingFoo3, "foo3", "func() bool", "func") +- return true +-} - --Hovering over type parameters should link to documentation. +--- dangling_multiline_if.go -- +-package danglingstmt - --TODO(rfindley): should it? We should probably link to the type. +-func walrus() bool { //@item(danglingWalrus, "walrus", "func() bool", "func") +- return true +-} +- +-func _() { +- if true && +- walrus //@complete(" //", danglingWalrus) +-} - +--- dangling_selector_1.go -- +-package danglingstmt - --[`generic.GT` on pkg.go.dev](https://pkg.go.dev/mod.com/generic#GT) ---- @M/hover.md -- --```go --func (GT[P]).M(p P) --``` +-func _() { +- x. //@rank(" //", danglingI) +-} - --[`(generic.GT).M` on pkg.go.dev](https://pkg.go.dev/mod.com/generic#GT.M) ---- @f/hover.md -- --```go --var f func(p int) --``` ---- @fGF/hover.md -- --```go --func generic.GF(p int) // func[P any](p P) --``` +-var x struct { i int } //@item(danglingI, "i", "int", "field") - --[`generic.GF` on pkg.go.dev](https://pkg.go.dev/mod.com/generic#GF) ---- @pP/hover.md -- --```go --type parameter P any --``` ---- @x/hover.md -- --```go --var x generic.GT[int] --``` +--- dangling_selector_2.go -- +-package danglingstmt - --@hover("GT", "GT", xGT) ---- @xF/hover.md -- --```go --field F int --``` +-// TODO: re-enable this test, which was broken when the foo package was removed. +-// (we can replicate the relevant definitions in the new marker test) +-// import "golang.org/lsptests/foo" - --@hover("F", "F", F),hover("P", "P", FP) +-func _() { +- foo. // rank(" //", Foo) +- var _ = []string{foo.} // rank("}", Foo) +-} - +--- dangling_switch_init.go -- +-package danglingstmt - --[`(generic.GT).F` on pkg.go.dev](https://pkg.go.dev/mod.com/generic#GT.F) ---- @xGT/hover.md -- --```go --type GT[P any] struct { -- F P //@hover("F", "F", F),hover("P", "P", FP) +-func _() { +- switch i := baz //@rank(" //", danglingBaz) -} - --func (generic.GT[P]).M(p P) --``` +-func baz() int { //@item(danglingBaz, "baz", "func() int", "func") +- return 0 +-} - --Hovering over type parameters should link to documentation. +--- dangling_switch_init_tag.go -- +-package danglingstmt - --TODO(rfindley): should it? We should probably link to the type. +-func _() { +- switch i := 0; baz //@rank(" //", danglingBaz2) +-} +- +-func baz2() int { //@item(danglingBaz2, "baz2", "func() int", "func") +- return 0 +-} +diff -urN a/gopls/internal/test/marker/testdata/completion/deep2.txt b/gopls/internal/test/marker/testdata/completion/deep2.txt +--- a/gopls/internal/test/marker/testdata/completion/deep2.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/deep2.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,65 +0,0 @@ +-This test exercises deep completion. - +-It was originally bundled with deep.go, but is split into a separate test as +-the new marker tests do not permit mutating server options for individual +-marks. - --[`generic.GT` on pkg.go.dev](https://pkg.go.dev/mod.com/generic#GT) -diff -urN a/gopls/internal/regtest/marker/testdata/hover/linkable.txt b/gopls/internal/regtest/marker/testdata/hover/linkable.txt ---- a/gopls/internal/regtest/marker/testdata/hover/linkable.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/hover/linkable.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,120 +0,0 @@ --This test checks that we correctly determine pkgsite links for various --identifiers. +--- flags -- +--ignore_extra_diags - --We should only produce links that work, meaning the object is reachable via the --package's public API. --- go.mod -- --module mod.com +-module golang.org/lsptests - -go 1.18 ---- p.go -- --package p - --type E struct { -- Embed int +--- deep/deep2.go -- +-package deep +- +-type foo struct { +- b bar -} - --// T is in the package scope, and so should be linkable. --type T struct{ //@hover("T", "T", T) -- // Only exported fields should be linkable +-func (f foo) bar() bar { +- return f.b +-} - -- f int //@hover("f", "f", f) -- F int //@hover("F", "F", F) +-func (f foo) barPtr() *bar { +- return &f.b +-} - -- E +-type bar struct{} - -- // TODO(rfindley): is the link here correct? It ignores N. -- N struct { -- // Nested fields should also be linkable. -- Nested int //@hover("Nested", "Nested", Nested) -- } +-func (b bar) valueReceiver() int { +- return 0 -} --// M is an exported method, and so should be linkable. --func (T) M() {} - --// m is not exported, and so should not be linkable. --func (T) m() {} +-func (b *bar) ptrReceiver() int { +- return 0 +-} - -func _() { -- var t T +- var ( +- i int +- f foo +- ) - -- // Embedded fields should be linkable. -- _ = t.Embed //@hover("Embed", "Embed", Embed) +- f.bar().valueReceiver //@item(deepBarValue, "f.bar().valueReceiver", "func() int", "method") +- f.barPtr().ptrReceiver //@item(deepBarPtrPtr, "f.barPtr().ptrReceiver", "func() int", "method") +- f.barPtr().valueReceiver //@item(deepBarPtrValue, "f.barPtr().valueReceiver", "func() int", "method") - -- // Local variables should not be linkable, even if they are capitalized. -- var X int //@hover("X", "X", X) -- _ = X +- i = fbar //@complete(" //", deepBarValue, deepBarPtrPtr, deepBarPtrValue) +-} - -- // Local types should not be linkable, even if they are capitalized. -- type Local struct { //@hover("Local", "Local", Local) -- E -- } +-func (b baz) Thing() struct{ val int } { +- return b.thing +-} - -- // But the embedded field should still be linkable. -- var l Local -- _ = l.Embed //@hover("Embed", "Embed", Embed) +-type baz struct { +- thing struct{ val int } -} ---- @Embed/hover.md -- --```go --field Embed int --``` - --[`(p.E).Embed` on pkg.go.dev](https://pkg.go.dev/mod.com#E.Embed) ---- @F/hover.md -- --```go --field F int --``` +-func (b baz) _() { +- b.Thing().val //@item(deepBazMethVal, "b.Thing().val", "int", "field") +- b.thing.val //@item(deepBazFieldVal, "b.thing.val", "int", "field") +- var _ int = bval //@rank(" //", deepBazFieldVal, deepBazMethVal) +-} +diff -urN a/gopls/internal/test/marker/testdata/completion/deep.txt b/gopls/internal/test/marker/testdata/completion/deep.txt +--- a/gopls/internal/test/marker/testdata/completion/deep.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/deep.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,110 +0,0 @@ +-This test exercises deep completion. - --@hover("F", "F", F) +--- settings.json -- +-{ +- "completeUnimported": false, +- "matcher": "caseInsensitive" +-} - +--- flags -- +--ignore_extra_diags - --[`(p.T).F` on pkg.go.dev](https://pkg.go.dev/mod.com#T.F) ---- @Local/hover.md -- --```go --type Local struct { -- E --} --``` +--- go.mod -- +-module golang.org/lsptests - --Local types should not be linkable, even if they are capitalized. ---- @Nested/hover.md -- --```go --field Nested int --``` +-go 1.18 - --Nested fields should also be linkable. ---- @T/hover.md -- --```go --type T struct { -- f int //@hover("f", "f", f) -- F int //@hover("F", "F", F) +--- deep/deep.go -- +-package deep - -- E +-import "context" - -- // TODO(rfindley): is the link here correct? It ignores N. -- N struct { -- // Nested fields should also be linkable. -- Nested int //@hover("Nested", "Nested", Nested) -- } +-type deepA struct { +- b deepB //@item(deepBField, "b", "deepB", "field") -} - --func (T).M() --func (T).m() --``` +-type deepB struct { +-} - --T is in the package scope, and so should be linkable. +-func wantsDeepB(deepB) {} - +-func _() { +- var a deepA //@item(deepAVar, "a", "deepA", "var") +- a.b //@item(deepABField, "a.b", "deepB", "field") +- wantsDeepB(a) //@complete(")", deepABField, deepAVar) - --[`p.T` on pkg.go.dev](https://pkg.go.dev/mod.com#T) ---- @X/hover.md -- --```go --var X int --``` +- deepA{a} //@snippet("}", deepABField, "a.b") +-} - --Local variables should not be linkable, even if they are capitalized. ---- @f/hover.md -- --```go --field f int --``` +-func wantsContext(context.Context) {} - --@hover("f", "f", f) -diff -urN a/gopls/internal/regtest/marker/testdata/hover/linkname.txt b/gopls/internal/regtest/marker/testdata/hover/linkname.txt ---- a/gopls/internal/regtest/marker/testdata/hover/linkname.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/hover/linkname.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,29 +0,0 @@ --This test check hover on the 2nd argument in go:linkname directives. ---- go.mod -- --module mod.com +-func _() { +- context.Background() //@item(ctxBackground, "context.Background", "func() context.Context", "func", "Background returns a non-nil, empty Context.") +- context.TODO() //@item(ctxTODO, "context.TODO", "func() context.Context", "func", "TODO returns a non-nil, empty Context.") - ---- upper/upper.go -- --package upper +- wantsContext(c) //@rank(")", ctxBackground),rank(")", ctxTODO) +-} - --import ( -- _ "unsafe" -- _ "mod.com/lower" --) +-func _() { +- var cork struct{ err error } +- cork.err //@item(deepCorkErr, "cork.err", "error", "field") +- context //@item(deepContextPkg, "context", "\"context\"", "package") +- var _ error = co // rank(" //", deepCorkErr, deepContextPkg) +-} - --//go:linkname foo mod.com/lower.bar //@hover("mod.com/lower.bar", "mod.com/lower.bar", bar) --func foo() string +-func _() { +- // deepCircle is circular. +- type deepCircle struct { +- *deepCircle +- } +- var circle deepCircle //@item(deepCircle, "circle", "deepCircle", "var") +- circle.deepCircle //@item(deepCircleField, "circle.deepCircle", "*deepCircle", "field") +- var _ deepCircle = circ //@complete(" //", deepCircle, deepCircleField),snippet(" //", deepCircleField, "*circle.deepCircle") +-} - ---- lower/lower.go -- --package lower +-func _() { +- type deepEmbedC struct { +- } +- type deepEmbedB struct { +- deepEmbedC +- } +- type deepEmbedA struct { +- deepEmbedB +- } - --// bar does foo. --func bar() string { -- return "foo by bar" +- wantsC := func(deepEmbedC) {} +- +- var a deepEmbedA //@item(deepEmbedA, "a", "deepEmbedA", "var") +- a.deepEmbedB //@item(deepEmbedB, "a.deepEmbedB", "deepEmbedB", "field") +- a.deepEmbedC //@item(deepEmbedC, "a.deepEmbedC", "deepEmbedC", "field") +- wantsC(a) //@complete(")", deepEmbedC, deepEmbedA, deepEmbedB) -} - ---- @bar/hover.md -- --```go --func bar() string --``` +-func _() { +- type nested struct { +- a int +- n *nested //@item(deepNestedField, "n", "*nested", "field") +- } - --bar does foo. -diff -urN a/gopls/internal/regtest/marker/testdata/hover/std.txt b/gopls/internal/regtest/marker/testdata/hover/std.txt ---- a/gopls/internal/regtest/marker/testdata/hover/std.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/hover/std.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,80 +0,0 @@ --This test checks hover results for built-in or standard library symbols. +- nested{ +- a: 123, //@complete(" //", deepNestedField) +- } +-} - --It uses synopsis documentation as full documentation for some of these --built-ins varies across Go versions, where as it just so happens that the --synopsis does not. +-func _() { +- var a struct { +- b struct { +- c int +- } +- d int +- } +- +- a.d //@item(deepAD, "a.d", "int", "field") +- a.b.c //@item(deepABC, "a.b.c", "int", "field") +- a.b //@item(deepAB, "a.b", "struct{...}", "field") +- a //@item(deepA, "a", "struct{...}", "var") +- +- // "a.d" should be ranked above the deeper "a.b.c" +- var i int +- i = a //@complete(" //", deepAD, deepABC, deepA, deepAB) +-} +diff -urN a/gopls/internal/test/marker/testdata/completion/errors.txt b/gopls/internal/test/marker/testdata/completion/errors.txt +--- a/gopls/internal/test/marker/testdata/completion/errors.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/errors.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,33 +0,0 @@ +-This test checks completion related to errors. +- +--- flags -- +--ignore_extra_diags - --In the future we may need to limit this test to the latest Go version to avoid --documentation churn. --- settings.json -- -{ -- "hoverKind": "SynopsisDocumentation" +- "deepCompletion": false -} +- --- go.mod -- --module mod.com +-module golang.org/lsptests - -go 1.18 ---- std.go -- --package std +- +--- errors.go -- +-package errors - -import ( -- "fmt" -- "go/types" -- "sync" +- "golang.org/lsptests/types" -) - -func _() { -- var err error //@loc(err, "err") -- fmt.Printf("%v", err) //@def("err", err) -- -- var _ string //@hover("string", "string", hoverstring) -- _ = make([]int, 0) //@hover("make", "make", hovermake) +- bob.Bob() //@complete(".") +- types.b //@complete(" //", Bob_interface) +-} - -- var mu sync.Mutex -- mu.Lock() //@hover("Lock", "Lock", hoverLock) +--- types/types.go -- +-package types - -- var typ *types.Named //@hover("types", "types", hoverTypes) -- typ.Obj().Name() //@hover("Name", "Name", hoverName) +-type Bob interface { //@item(Bob_interface, "Bob", "interface{...}", "interface") +- Bobby() -} ---- @hoverLock/hover.md -- --```go --func (*sync.Mutex).Lock() --``` +diff -urN a/gopls/internal/test/marker/testdata/completion/field_list.txt b/gopls/internal/test/marker/testdata/completion/field_list.txt +--- a/gopls/internal/test/marker/testdata/completion/field_list.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/field_list.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,38 +0,0 @@ +-This test checks completion related to field lists. - --Lock locks m. +--- flags -- +--ignore_extra_diags - +--- settings.json -- +-{ +- "completeUnimported": false +-} - --[`(sync.Mutex).Lock` on pkg.go.dev](https://pkg.go.dev/sync#Mutex.Lock) ---- @hoverName/hover.md -- --```go --func (*types.object).Name() string --``` +--- field_list.go -- +-package fieldlist - --Name returns the object's (package-local, unqualified) name. +-var myInt int //@item(flVar, "myInt", "int", "var") +-type myType int //@item(flType, "myType", "int", "type") - +-func (my) _() {} //@complete(") _", flType) +-func (my my) _() {} //@complete(" my)"),complete(") _", flType) - --[`(types.TypeName).Name` on pkg.go.dev](https://pkg.go.dev/go/types#TypeName.Name) ---- @hoverTypes/hover.md -- --```go --package types ("go/types") --``` +-func (myType) _() {} //@complete(") {", flType) - --[`types` on pkg.go.dev](https://pkg.go.dev/go/types) ---- @hovermake/hover.md -- --```go --func make(t Type, size ...int) Type --``` +-func (myType) _(my my) {} //@complete(" my)"),complete(") {", flType) - --The make built-in function allocates and initializes an object of type slice, map, or chan (only). +-func (myType) _() my {} //@complete(" {", flType) - +-func (myType) _() (my my) {} //@complete(" my"),complete(") {", flType) - --[`make` on pkg.go.dev](https://pkg.go.dev/builtin#make) ---- @hoverstring/hover.md -- --```go --type string string --``` +-func _() { +- var _ struct { +- //@complete("", flType) +- m my //@complete(" my"),complete(" //", flType) +- } - --string is the set of all strings of 8-bit bytes, conventionally but not necessarily representing UTF-8-encoded text. +- var _ interface { +- //@complete("", flType) +- m() my //@complete("("),complete(" //", flType) +- } +-} +diff -urN a/gopls/internal/test/marker/testdata/completion/foobarbaz.txt b/gopls/internal/test/marker/testdata/completion/foobarbaz.txt +--- a/gopls/internal/test/marker/testdata/completion/foobarbaz.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/foobarbaz.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,541 +0,0 @@ +-This test ports some arbitrary tests from the old marker framework, that were +-*mostly* about completion. - +--- flags -- +--ignore_extra_diags +--min_go=go1.20 - --[`string` on pkg.go.dev](https://pkg.go.dev/builtin#string) -diff -urN a/gopls/internal/regtest/marker/testdata/implementation/basic.txt b/gopls/internal/regtest/marker/testdata/implementation/basic.txt ---- a/gopls/internal/regtest/marker/testdata/implementation/basic.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/implementation/basic.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,73 +0,0 @@ --Basic test of implementation query. +--- settings.json -- +-{ +- "completeUnimported": false, +- "deepCompletion": false, +- "experimentalPostfixCompletions": false +-} - --- go.mod -- --module example.com --go 1.12 -- ---- implementation/implementation.go -- --package implementation +-module foobar.test - --import "example.com/other" +-go 1.18 - --type ImpP struct{} //@loc(ImpP, "ImpP"),implementation("ImpP", Laugher, OtherLaugher) +--- foo/foo.go -- +-package foo //@loc(PackageFoo, "foo"),item(PackageFooItem, "foo", "\"foobar.test/foo\"", "package") - --func (*ImpP) Laugh() { //@loc(LaughP, "Laugh"),implementation("Laugh", Laugh, OtherLaugh) +-type StructFoo struct { //@loc(StructFooLoc, "StructFoo"), item(StructFoo, "StructFoo", "struct{...}", "struct") +- Value int //@item(Value, "Value", "int", "field") -} - --type ImpS struct{} //@loc(ImpS, "ImpS"),implementation("ImpS", Laugher, OtherLaugher) +-// Pre-set this marker, as we don't have a "source" for it in this package. +-/* Error() */ //@item(Error, "Error", "func() string", "method") - --func (ImpS) Laugh() { //@loc(LaughS, "Laugh"),implementation("Laugh", Laugh, OtherLaugh) +-func Foo() { //@item(Foo, "Foo", "func()", "func") +- var err error +- err.Error() //@complete("E", Error) -} - --type Laugher interface { //@loc(Laugher, "Laugher"),implementation("Laugher", ImpP, OtherImpP, ImpS, OtherImpS, embedsImpP) -- Laugh() //@loc(Laugh, "Laugh"),implementation("Laugh", LaughP, OtherLaughP, LaughS, OtherLaughS) +-func _() { +- var sFoo StructFoo //@complete("t", StructFoo) +- if x := sFoo; x.Value == 1 { //@complete("V", Value), typedef("sFoo", StructFooLoc) +- return +- } -} - --type Foo struct { //@implementation("Foo", Joker) -- other.Foo +-func _() { +- shadowed := 123 +- { +- shadowed := "hi" //@item(shadowed, "shadowed", "string", "var") +- sha //@complete("a", shadowed), diag("sha", re"(undefined|undeclared)") +- _ = shadowed +- } -} - --type Joker interface { //@loc(Joker, "Joker") -- Joke() //@loc(Joke, "Joke"),implementation("Joke", ImpJoker) --} +-type IntFoo int //@loc(IntFooLoc, "IntFoo"), item(IntFoo, "IntFoo", "int", "type") - --type cryer int //@implementation("cryer", Cryer) +--- bar/bar.go -- +-package bar - --func (cryer) Cry(other.CryType) {} //@loc(CryImpl, "Cry"),implementation("Cry", Cry) +-import ( +- "foobar.test/foo" //@item(foo, "foo", "\"foobar.test/foo\"", "package") +-) - --type Empty interface{} //@implementation("Empty") +-func helper(i foo.IntFoo) {} //@item(helper, "helper", "func(i foo.IntFoo)", "func") - --var _ interface{ Joke() } //@implementation("Joke", ImpJoker) +-func _() { +- help //@complete("l", helper) +- _ = foo.StructFoo{} //@complete("S", IntFoo, StructFoo) +-} - --type embedsImpP struct { //@loc(embedsImpP, "embedsImpP") -- ImpP //@implementation("ImpP", Laugher, OtherLaugher) +-// Bar is a function. +-func Bar() { //@item(Bar, "Bar", "func()", "func", "Bar is a function.") +- foo.Foo() //@complete("F", Foo, IntFoo, StructFoo) +- var _ foo.IntFoo //@complete("I", IntFoo, StructFoo) +- foo.() //@complete("(", Foo, IntFoo, StructFoo), diag(")", re"expected type") -} - ---- other/other.go -- --package other +-// These items weren't present in the old marker tests (due to settings), but +-// we may as well include them. +-//@item(intConversion, "int()"), item(fooFoo, "foo.Foo") +-//@item(fooIntFoo, "foo.IntFoo"), item(fooStructFoo, "foo.StructFoo") - --type ImpP struct{} //@loc(OtherImpP, "ImpP") +-func _() { +- var Valentine int //@item(Valentine, "Valentine", "int", "var") - --func (*ImpP) Laugh() { //@loc(OtherLaughP, "Laugh") +- _ = foo.StructFoo{ //@diag("foo", re"unkeyed fields") +- Valu //@complete(" //", Value) +- } +- _ = foo.StructFoo{ //@diag("foo", re"unkeyed fields") +- Va //@complete("a", Value, Valentine) +- +- } +- _ = foo.StructFoo{ +- Value: 5, //@complete("a", Value) +- } +- _ = foo.StructFoo{ +- //@complete("//", Value, Valentine, intConversion, foo, helper, Bar) +- } +- _ = foo.StructFoo{ +- Value: Valen //@complete("le", Valentine) +- } +- _ = foo.StructFoo{ +- Value: //@complete(" //", Valentine, intConversion, foo, helper, Bar) +- } +- _ = foo.StructFoo{ +- Value: //@complete(" ", Valentine, intConversion, foo, helper, Bar) +- } -} - --type ImpS struct{} //@loc(OtherImpS, "ImpS") +--- baz/baz.go -- +-package baz - --func (ImpS) Laugh() { //@loc(OtherLaughS, "Laugh") --} +-import ( +- "foobar.test/bar" - --type ImpI interface { //@loc(OtherLaugher, "ImpI") -- Laugh() //@loc(OtherLaugh, "Laugh") --} +- f "foobar.test/foo" +-) - --type Foo struct { //@implementation("Foo", Joker) +-var FooStruct f.StructFoo +- +-func Baz() { +- defer bar.Bar() //@complete("B", Bar) +- // TODO: Test completion here. +- defer bar.B //@diag(re"bar.B()", re"must be function call") +- var x f.IntFoo //@complete("n", IntFoo), typedef("x", IntFooLoc) +- bar.Bar() //@complete("B", Bar) -} - --func (Foo) Joke() { //@loc(ImpJoker, "Joke"),implementation("Joke", Joke) +-func _() { +- bob := f.StructFoo{Value: 5} +- if x := bob. //@complete(" //", Value) +- switch true == false { +- case true: +- if x := bob. //@complete(" //", Value) +- case false: +- } +- if x := bob.Va //@complete("a", Value) +- switch true == true { +- default: +- } -} - --type CryType int +--- arraytype/arraytype.go -- +-package arraytype - --type Cryer interface { //@loc(Cryer, "Cryer") -- Cry(CryType) //@loc(Cry, "Cry"),implementation("Cry", CryImpl) --} -diff -urN a/gopls/internal/regtest/marker/testdata/implementation/generics.txt b/gopls/internal/regtest/marker/testdata/implementation/generics.txt ---- a/gopls/internal/regtest/marker/testdata/implementation/generics.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/implementation/generics.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,34 +0,0 @@ --Test of 'implementation' query on generic types. +-import ( +- "foobar.test/foo" +-) - ---- flags -- ---min_go=go1.18 +-func _() { +- var ( +- val string //@item(atVal, "val", "string", "var") +- ) - ---- go.mod -- --module example.com --go 1.18 +- [] //@complete(" //", atVal, PackageFooItem) - ---- implementation/implementation.go -- --package implementation +- []val //@complete(" //") - --type GenIface[T any] interface { //@loc(GenIface, "GenIface"),implementation("GenIface", GC) -- F(int, string, T) //@loc(GenIfaceF, "F"),implementation("F", GCF) --} +- []foo.StructFoo //@complete(" //", StructFoo) - --type GenConc[U any] int //@loc(GenConc, "GenConc"),implementation("GenConc", GI) +- []foo.StructFoo(nil) //@complete("(", StructFoo) - --func (GenConc[V]) F(int, string, V) {} //@loc(GenConcF, "F"),implementation("F", GIF) +- []*foo.StructFoo //@complete(" //", StructFoo) - --type GenConcString struct{ GenConc[string] } //@loc(GenConcString, "GenConcString"),implementation(GenConcString, GIString) +- [...]foo.StructFoo //@complete(" //", StructFoo) - ---- other/other.go -- --package other +- [2][][4]foo.StructFoo //@complete(" //", StructFoo) - --type GI[T any] interface { //@loc(GI, "GI"),implementation("GI", GenConc) -- F(int, string, T) //@loc(GIF, "F"),implementation("F", GenConcF) +- []struct { f []foo.StructFoo } //@complete(" }", StructFoo) -} - --type GIString GI[string] //@loc(GIString, "GIString"),implementation("GIString", GenConcString) +-func _() { +- type myInt int //@item(atMyInt, "myInt", "int", "type") - --type GC[U any] int //@loc(GC, "GC"),implementation("GC", GenIface) +- var mark []myInt //@item(atMark, "mark", "[]myInt", "var") - --func (GC[V]) F(int, string, V) {} //@loc(GCF, "F"),implementation("F", GenIfaceF) -diff -urN a/gopls/internal/regtest/marker/testdata/implementation/issue43655.txt b/gopls/internal/regtest/marker/testdata/implementation/issue43655.txt ---- a/gopls/internal/regtest/marker/testdata/implementation/issue43655.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/implementation/issue43655.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,22 +0,0 @@ --This test verifies that we fine implementations of the built-in error interface. +- var s []myInt //@item(atS, "s", "[]myInt", "var") +- s = []m //@complete(" //", atMyInt) - ---- go.mod -- --module example.com --go 1.12 +- var a [1]myInt +- a = [1]m //@complete(" //", atMyInt) - ---- p.go -- --package p +- var ds [][]myInt +- ds = [][]m //@complete(" //", atMyInt) +-} - --type errA struct{ error } //@loc(errA, "errA") +-func _() { +- var b [0]byte //@item(atByte, "b", "[0]byte", "var") +- var _ []byte = b //@snippet(" //", atByte, "b[:]") +-} - --type errB struct{} //@loc(errB, "errB") --func (errB) Error() string{ return "" } //@loc(errBError, "Error") +--- badstmt/badstmt.go -- +-package badstmt - --type notAnError struct{} --func (notAnError) Error() int { return 0 } +-import ( +- "foobar.test/foo" +-) - --func _() { -- var _ error //@implementation("error", errA, errB) -- var a errA -- _ = a.Error //@implementation("Error", errBError) +-// (The syntax error causes suppression of diagnostics for type errors. +-// See issue #59888.) +- +-func _(x int) { +- defer foo.F //@complete(" //", Foo, IntFoo, StructFoo) +- defer foo.F //@complete(" //", Foo, IntFoo, StructFoo) -} -diff -urN a/gopls/internal/regtest/marker/testdata/links/links.txt b/gopls/internal/regtest/marker/testdata/links/links.txt ---- a/gopls/internal/regtest/marker/testdata/links/links.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/links/links.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,47 +0,0 @@ --This test verifies behavior of textDocument/documentLink. - ---- go.mod -- --module golang.org/lsptests +-func _() { +- switch true { +- case true: +- go foo.F //@complete(" //", Foo, IntFoo, StructFoo) +- } +-} - --go 1.18 ---- foo/foo.go -- --package foo +-func _() { +- defer func() { +- foo.F //@complete(" //", Foo, IntFoo, StructFoo), snippet(" //", Foo, "Foo()") - --type StructFoo struct {} +- foo. //@rank(" //", Foo) +- } +-} - ---- links/links.go -- --package links //@documentlink(links) +--- badstmt/badstmt_2.go -- +-package badstmt - -import ( -- "fmt" -- -- "golang.org/lsptests/foo" -- -- _ "database/sql" +- "foobar.test/foo" -) - --var ( -- _ fmt.Formatter -- _ foo.StructFoo -- _ errors.Formatter //@diag("errors", re"(undeclared|undefined)") --) +-func _() { +- defer func() { foo. } //@rank(" }", Foo) +-} - --// Foo function --func Foo() string { -- /*https://example.com/comment */ +--- badstmt/badstmt_3.go -- +-package badstmt - -- url := "https://example.com/string_literal" -- return url +-import ( +- "foobar.test/foo" +-) - -- // TODO(golang/go#1234): Link the relevant issue. -- // TODO(microsoft/vscode-go#12): Another issue. +-func _() { +- go foo. //@rank(" //", Foo, IntFoo), snippet(" //", Foo, "Foo()") -} - ---- @links -- --links/links.go:4:3-6 https://pkg.go.dev/fmt --links/links.go:6:3-26 https://pkg.go.dev/golang.org/lsptests/foo --links/links.go:8:5-17 https://pkg.go.dev/database/sql --links/links.go:21:10-44 https://example.com/string_literal --links/links.go:19:4-31 https://example.com/comment --links/links.go:24:10-24 https://github.com/golang/go/issues/1234 --links/links.go:25:10-32 https://github.com/microsoft/vscode-go/issues/12 -diff -urN a/gopls/internal/regtest/marker/testdata/references/crosspackage.txt b/gopls/internal/regtest/marker/testdata/references/crosspackage.txt ---- a/gopls/internal/regtest/marker/testdata/references/crosspackage.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/references/crosspackage.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,37 +0,0 @@ --Test of basic cross-package references. -- ---- go.mod -- --module example.com --go 1.12 +--- badstmt/badstmt_4.go -- +-package badstmt - ---- a/a.go -- --package a +-import ( +- "foobar.test/foo" +-) - --type X struct { -- Y int //@loc(typeXY, "Y") +-func _() { +- go func() { +- defer foo. //@rank(" //", Foo, IntFoo) +- } -} - ---- b/b.go -- --package b +--- selector/selector.go -- +-package selector - --import "example.com/a" +-import ( +- "foobar.test/bar" +-) - --func GetXes() []a.X { -- return []a.X{ -- { -- Y: 1, //@loc(GetXesY, "Y"), refs("Y", typeXY, GetXesY, anotherXY) -- }, -- } +-type S struct { +- B, A, C int //@item(Bf, "B", "int", "field"),item(Af, "A", "int", "field"),item(Cf, "C", "int", "field") -} - ---- c/c.go -- --package c +-func _() { +- _ = S{}.; //@complete(";", Af, Bf, Cf) +-} - --import "example.com/b" +-type bob struct { a int } //@item(a, "a", "int", "field") +-type george struct { b int } +-type jack struct { c int } //@item(c, "c", "int", "field") +-type jill struct { d int } +- +-func (b *bob) george() *george {} //@item(george, "george", "func() *george", "method") +-func (g *george) jack() *jack {} +-func (j *jack) jill() *jill {} //@item(jill, "jill", "func() *jill", "method") - -func _() { -- xes := b.GetXes() -- for _, x := range xes { //@loc(defX, "x") -- _ = x.Y //@loc(useX, "x"), loc(anotherXY, "Y"), refs("Y", typeXY, anotherXY, GetXesY), refs(".", defX, useX), refs("x", defX, useX) -- } +- b := &bob{} +- y := b.george(). +- jack(); +- y.; //@complete(";", c, jill) -} -diff -urN a/gopls/internal/regtest/marker/testdata/references/imports.txt b/gopls/internal/regtest/marker/testdata/references/imports.txt ---- a/gopls/internal/regtest/marker/testdata/references/imports.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/references/imports.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,17 +0,0 @@ --Test of references to local package names (imports). -- ---- go.mod -- --module example.com --go 1.12 - ---- a/a.go -- --package a +-func _() { +- bar. //@complete(" /", Bar) +- x := 5 - --import "os" //@loc(osDef, `"os"`), refs("os", osDef, osUse) +- var b *bob +- b. //@complete(" /", a, george) +- y, z := 5, 6 - --import fmt2 "fmt" //@loc(fmt2Def, `fmt2`), refs("fmt2", fmt2Def, fmt2Use) +- b. //@complete(" /", a, george) +- y, z, a, b, c := 5, 6 +-} - -func _() { -- os.Getwd() //@loc(osUse, "os") -- fmt2.Println() //@loc(fmt2Use, "fmt2") +- bar. //@complete(" /", Bar) +- bar.Bar() +- +- bar. //@complete(" /", Bar) +- go f() -} -diff -urN a/gopls/internal/regtest/marker/testdata/references/interfaces.txt b/gopls/internal/regtest/marker/testdata/references/interfaces.txt ---- a/gopls/internal/regtest/marker/testdata/references/interfaces.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/references/interfaces.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,42 +0,0 @@ --Test of references applied to concrete and interface types that are --related by assignability. The result includes references to both. - ---- go.mod -- --module example.com --go 1.12 +-func _() { +- var b *bob +- if y != b. //@complete(" /", a, george) +- z := 5 - ---- a/a.go -- --package a +- if z + y + 1 + b. //@complete(" /", a, george) +- r, s, t := 4, 5 - --type first interface { -- common() //@loc(firCommon, "common"), refs("common", firCommon, xCommon, zCommon) -- firstMethod() //@loc(firMethod, "firstMethod"), refs("firstMethod", firMethod, xfMethod, zfMethod) --} +- if y != b. //@complete(" /", a, george) +- z = 5 - --type second interface { -- common() //@loc(secCommon, "common"), refs("common", secCommon, yCommon, zCommon) -- secondMethod() //@loc(secMethod, "secondMethod"), refs("secondMethod", secMethod, ysMethod, zsMethod) +- if z + y + 1 + b. //@complete(" /", a, george) +- r = 4 -} - --type s struct {} -- --func (*s) common() {} //@loc(sCommon, "common"), refs("common", sCommon, xCommon, yCommon, zCommon) +--- literal_snippets/literal_snippets.go -- +-package literal_snippets - --func (*s) firstMethod() {} //@loc(sfMethod, "firstMethod"), refs("firstMethod", sfMethod, xfMethod, zfMethod) +-import ( +- "bytes" +- "context" +- "go/ast" +- "net/http" +- "sort" - --func (*s) secondMethod() {} //@loc(ssMethod, "secondMethod"), refs("secondMethod", ssMethod, ysMethod, zsMethod) +- "golang.org/lsptests/foo" +-) - --func main() { -- var x first = &s{} -- var y second = &s{} +-func _() { +- []int{} //@item(litIntSlice, "[]int{}", "", "var") +- &[]int{} //@item(litIntSliceAddr, "&[]int{}", "", "var") +- make([]int, 0) //@item(makeIntSlice, "make([]int, 0)", "", "func") - -- x.common() //@loc(xCommon, "common"), refs("common", firCommon, xCommon, zCommon) -- x.firstMethod() //@loc(xfMethod, "firstMethod"), refs("firstMethod", firMethod, xfMethod, zfMethod) -- y.common() //@loc(yCommon, "common"), refs("common", secCommon, yCommon, zCommon) -- y.secondMethod() //@loc(ysMethod, "secondMethod"), refs("secondMethod", secMethod, ysMethod, zsMethod) +- var _ *[]int = in //@snippet(" //", litIntSliceAddr, "&[]int{$0\\}") +- var _ **[]int = in //@complete(" //") - -- var z *s = &s{} -- z.firstMethod() //@loc(zfMethod, "firstMethod"), refs("firstMethod", sfMethod, xfMethod, zfMethod) -- z.secondMethod() //@loc(zsMethod, "secondMethod"), refs("secondMethod", ssMethod, ysMethod, zsMethod) -- z.common() //@loc(zCommon, "common"), refs("common", sCommon, xCommon, yCommon, zCommon) +- var slice []int +- slice = i //@snippet(" //", litIntSlice, "[]int{$0\\}") +- slice = m //@snippet(" //", makeIntSlice, "make([]int, ${1:})") -} -diff -urN a/gopls/internal/regtest/marker/testdata/references/intrapackage.txt b/gopls/internal/regtest/marker/testdata/references/intrapackage.txt ---- a/gopls/internal/regtest/marker/testdata/references/intrapackage.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/references/intrapackage.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,36 +0,0 @@ --Basic test of references within a single package. -- ---- go.mod -- --module example.com --go 1.12 - ---- a/a.go -- --package a +-func _() { +- type namedInt []int - --type i int //@loc(decli, "i"), refs("i", decli, argi, returni, embeddedi) +- namedInt{} //@item(litNamedSlice, "namedInt{}", "", "var") +- make(namedInt, 0) //@item(makeNamedSlice, "make(namedInt, 0)", "", "func") - --func _(_ i) []bool { //@loc(argi, "i") -- return nil +- var namedSlice namedInt +- namedSlice = n //@snippet(" //", litNamedSlice, "namedInt{$0\\}") +- namedSlice = m //@snippet(" //", makeNamedSlice, "make(namedInt, ${1:})") -} - --func _(_ []byte) i { //@loc(returni, "i") -- return 0 +-func _() { +- make(chan int) //@item(makeChan, "make(chan int)", "", "func") +- +- var ch chan int +- ch = m //@snippet(" //", makeChan, "make(chan int)") -} - --var q string //@loc(declq, "q"), refs("q", declq, assignq, bobq) +-func _() { +- map[string]struct{}{} //@item(litMap, "map[string]struct{}{}", "", "var") +- make(map[string]struct{}) //@item(makeMap, "make(map[string]struct{})", "", "func") - --var Q string //@loc(declQ, "Q"), refs("Q", declQ) +- var m map[string]struct{} +- m = m //@snippet(" //", litMap, "map[string]struct{\\}{$0\\}") +- m = m //@snippet(" //", makeMap, "make(map[string]struct{\\})") - --func _() { -- q = "hello" //@loc(assignq, "q") -- bob := func(_ string) {} -- bob(q) //@loc(bobq, "q") --} +- struct{}{} //@item(litEmptyStruct, "struct{}{}", "", "var") - --type e struct { -- i //@loc(embeddedi, "i"), refs("i", embeddedi, embeddediref) +- m["hi"] = s //@snippet(" //", litEmptyStruct, "struct{\\}{\\}") -} - -func _() { -- _ = e{}.i //@loc(embeddediref, "i") --} -diff -urN a/gopls/internal/regtest/marker/testdata/references/issue58506.txt b/gopls/internal/regtest/marker/testdata/references/issue58506.txt ---- a/gopls/internal/regtest/marker/testdata/references/issue58506.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/references/issue58506.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,56 +0,0 @@ --Regression test for 'references' bug golang/go#58506. +- type myStruct struct{ i int } //@item(myStructType, "myStruct", "struct{...}", "struct") - --The 'references' query below, applied to method A.F, implicitly uses --the 'implementation' operation. The correct response includes two --references to B.F, one from package b and one from package d. --However, the incremental 'implementation' algorithm had a bug that --cause it to fail to report the reference from package b. +- myStruct{} //@item(litStruct, "myStruct{}", "", "var") +- &myStruct{} //@item(litStructPtr, "&myStruct{}", "", "var") - --The reason was that the incremental implementation uses different --algorithms for the local and global cases (with disjoint results), and --that when it discovered that type A satisfies interface B and thus --that B.F must be included among the global search targets, the --implementation forgot to also search package b for local references --to B.F. +- var ms myStruct +- ms = m //@snippet(" //", litStruct, "myStruct{$0\\}") - ---- go.mod -- --module example.com --go 1.12 +- var msPtr *myStruct +- msPtr = m //@snippet(" //", litStructPtr, "&myStruct{$0\\}") - ---- a/a.go -- --package a +- msPtr = &m //@snippet(" //", litStruct, "myStruct{$0\\}") - --type A int +- type myStructCopy struct { i int } //@item(myStructCopyType, "myStructCopy", "struct{...}", "struct") - --func (A) F() {} //@loc(refa, "F"), refs("F", refa, refb, refd) +- // Don't offer literal completion for convertible structs. +- ms = myStruct //@complete(" //", litStruct, myStructType, myStructCopyType) +-} - ---- b/b.go -- --package b +-type myImpl struct{} - --import ( -- "example.com/a" -- "example.com/c" --) +-func (myImpl) foo() {} +- +-func (*myImpl) bar() {} +- +-type myBasicImpl string +- +-func (myBasicImpl) foo() {} +- +-func _() { +- type myIntf interface { +- foo() +- } - --type B interface{ F() } +- myImpl{} //@item(litImpl, "myImpl{}", "", "var") - --var _ B = a.A(0) --var _ B = c.C(0) +- var mi myIntf +- mi = m //@snippet(" //", litImpl, "myImpl{\\}") - --var _ = B.F //@loc(refb, "F") +- myBasicImpl() //@item(litBasicImpl, "myBasicImpl()", "string", "var") - ---- c/c.go -- --package c +- mi = m //@snippet(" //", litBasicImpl, "myBasicImpl($0)") - --type C int +- // only satisfied by pointer to myImpl +- type myPtrIntf interface { +- bar() +- } - --// Even though C.F is "rename coupled" to A.F by B.F, --// it should not be among the results. --func (C) F() {} +- &myImpl{} //@item(litImplPtr, "&myImpl{}", "", "var") - ---- d/d.go -- --package d +- var mpi myPtrIntf +- mpi = m //@snippet(" //", litImplPtr, "&myImpl{\\}") +-} - --import "example.com/b" +-func _() { +- var s struct{ i []int } //@item(litSliceField, "i", "[]int", "field") +- var foo []int +- // no literal completions after selector +- foo = s.i //@complete(" //", litSliceField) +-} - --var _ interface{} = b.B.F //@loc(refd, "F") -diff -urN a/gopls/internal/regtest/marker/testdata/references/issue59851.txt b/gopls/internal/regtest/marker/testdata/references/issue59851.txt ---- a/gopls/internal/regtest/marker/testdata/references/issue59851.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/references/issue59851.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,29 +0,0 @@ --Regression test for 'references' bug golang/go#59851. +-func _() { +- type myStruct struct{ i int } //@item(litMyStructType, "myStruct", "struct{...}", "struct") +- myStruct{} //@item(litMyStruct, "myStruct{}", "", "var") - ---- go.mod -- --module example.com --go 1.12 +- foo := func(s string, args ...myStruct) {} +- // Don't give literal slice candidate for variadic arg. +- // Do give literal candidates for variadic element. +- foo("", myStruct) //@complete(")", litMyStruct, litMyStructType) +-} - ---- a/a.go -- --package a +-func _() { +- Buffer{} //@item(litBuffer, "Buffer{}", "", "var") - --type Iface interface { -- Method() +- var b *bytes.Buffer +- b = bytes.Bu //@snippet(" //", litBuffer, "Buffer{\\}") -} - --type implOne struct{} -- --func (implOne) Method() {} //@loc(def1, "Method"), refs(def1, def1, ref1, iref, ireftest) +-func _() { +- _ = "func(...) {}" //@item(litFunc, "func(...) {}", "", "var") - --var _ = implOne.Method //@loc(ref1, "Method") --var _ = Iface(nil).Method //@loc(iref, "Method") +- // no literal "func" completions +- http.Handle("", fun) //@complete(")") - ---- a/a_test.go -- --package a +- var namedReturn func(s string) (b bool) +- namedReturn = f //@snippet(" //", litFunc, "func(s string) (b bool) {$0\\}") - --type implTwo struct{} +- var multiReturn func() (bool, int) +- multiReturn = f //@snippet(" //", litFunc, "func() (bool, int) {$0\\}") - --func (implTwo) Method() {} //@loc(def2, "Method"), refs(def2, def2, iref, ref2, ireftest) +- var multiNamedReturn func() (b bool, i int) +- multiNamedReturn = f //@snippet(" //", litFunc, "func() (b bool, i int) {$0\\}") - --var _ = implTwo.Method //@loc(ref2, "Method") --var _ = Iface(nil).Method //@loc(ireftest, "Method") -diff -urN a/gopls/internal/regtest/marker/testdata/references/issue60369.txt b/gopls/internal/regtest/marker/testdata/references/issue60369.txt ---- a/gopls/internal/regtest/marker/testdata/references/issue60369.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/references/issue60369.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,29 +0,0 @@ --Regression test for 'references' bug golang/go#60369: a references --query on the embedded type name T in struct{p.T} instead reports all --references to the package name p. +- var duplicateParams func(myImpl, int, myImpl) +- duplicateParams = f //@snippet(" //", litFunc, "func(mi1 myImpl, i int, mi2 myImpl) {$0\\}") - --The bug was fixed in release go1.21 of go/types. +- type aliasImpl = myImpl +- var aliasParams func(aliasImpl) aliasImpl +- aliasParams = f //@snippet(" //", litFunc, "func(ai aliasImpl) aliasImpl {$0\\}") - ---- flags -- ---min_go=go1.21 +- const two = 2 +- var builtinTypes func([]int, [two]bool, map[string]string, struct{ i int }, interface{ foo() }, <-chan int) +- builtinTypes = f //@snippet(" //", litFunc, "func(i1 []int, b [two]bool, m map[string]string, s struct{ i int \\}, i2 interface{ foo() \\}, c <-chan int) {$0\\}") - ---- go.mod -- --module example.com --go 1.12 +- var _ func(ast.Node) = f //@snippet(" //", litFunc, "func(n ast.Node) {$0\\}") +- var _ func(error) = f //@snippet(" //", litFunc, "func(err error) {$0\\}") +- var _ func(context.Context) = f //@snippet(" //", litFunc, "func(ctx context.Context) {$0\\}") - ---- a/a.go -- --package a +- type context struct {} +- var _ func(context) = f //@snippet(" //", litFunc, "func(ctx context) {$0\\}") +-} - --type A struct{} --const C = 0 +-func _() { +- float64() //@item(litFloat64, "float64()", "float64", "var") - ---- b/b.go -- --package b +- // don't complete to "&float64()" +- var _ *float64 = float64 //@complete(" //") - --import a "example.com/a" //@loc(adef, "a") --type s struct { -- a.A //@loc(Aref1, "A"), loc(aref1, "a"), refs(Aref1, Aref1, Aref3), refs(aref1, adef, aref1, aref2, aref3) --} --var _ a.A //@loc(aref2, re" (a)"), loc(Aref2, "A") --var _ = s{}.A //@loc(Aref3, "A") --const c = a.C //@loc(aref3, "a") -diff -urN a/gopls/internal/regtest/marker/testdata/references/issue60622.txt b/gopls/internal/regtest/marker/testdata/references/issue60622.txt ---- a/gopls/internal/regtest/marker/testdata/references/issue60622.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/references/issue60622.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,25 +0,0 @@ --Regression test for 'references' bug golang/go#60622: --references to methods of generics were missing. +- var f float64 +- f = fl //@complete(" //", litFloat64),snippet(" //", litFloat64, "float64($0)") - ---- flags -- ---min_go=go1.18 +- type myInt int +- myInt() //@item(litMyInt, "myInt()", "", "var") - ---- go.mod -- --module example.com --go 1.18 +- var mi myInt +- mi = my //@snippet(" //", litMyInt, "myInt($0)") +-} - ---- a/a.go -- --package a +-func _() { +- type ptrStruct struct { +- p *ptrStruct +- } - --type G[T any] struct{} +- ptrStruct{} //@item(litPtrStruct, "ptrStruct{}", "", "var") - --func (G[T]) M() {} //@loc(Mdef, "M"), refs(Mdef, Mdef, Mref) +- ptrStruct{ +- p: &ptrSt, //@rank(",", litPtrStruct) +- } - ---- b/b.go -- --package b +- &ptrStruct{} //@item(litPtrStructPtr, "&ptrStruct{}", "", "var") - --import "example.com/a" +- &ptrStruct{ +- p: ptrSt, //@rank(",", litPtrStructPtr) +- } +-} - -func _() { -- new(a.G[int]).M() //@loc(Mref, "M") +- f := func(...[]int) {} +- f() //@snippet(")", litIntSlice, "[]int{$0\\}") -} -diff -urN a/gopls/internal/regtest/marker/testdata/references/issue60676.txt b/gopls/internal/regtest/marker/testdata/references/issue60676.txt ---- a/gopls/internal/regtest/marker/testdata/references/issue60676.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/references/issue60676.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,70 +0,0 @@ --This test verifies that even after importing from export data, the references --algorithm is able to find all references to struct fields or methods that are --shared by types from multiple packages. See golang/go#60676. -- --Note that the marker test runner awaits the initial workspace load, so export --data should be populated at the time references are requested. - ---- flags -- ---min_go=go1.18 - ---- go.mod -- --module mod.test +-func _() { +- // don't complete to "untyped int()" +- []int{}[untyped] //@complete("] //") +-} - --go 1.18 +-type Tree[T any] struct{} - ---- a/a.go -- --package a +-func (tree Tree[T]) Do(f func(s T)) {} - --type A struct { -- F int //@loc(FDef, "F") -- E //@loc(EDef, "E") +-func _() { +- var t Tree[string] +- t.Do(fun) //@complete(")", litFunc), snippet(")", litFunc, "func(s string) {$0\\}") -} +diff -urN a/gopls/internal/test/marker/testdata/completion/func_rank.txt b/gopls/internal/test/marker/testdata/completion/func_rank.txt +--- a/gopls/internal/test/marker/testdata/completion/func_rank.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/func_rank.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,83 +0,0 @@ +-This test checks various ranking of completion results within function call +-context. - --type E struct { -- G string //@loc(GDef, "G") --} +--- flags -- +--ignore_extra_diags - --type AI interface { -- M() //@loc(MDef, "M") -- EI -- error +--- settings.json -- +-{ +- "completeUnimported": false, +- "deepCompletion": false -} - --type EI interface { -- N() //@loc(NDef, "N") --} +--- func_rank.go -- +-package func_rank - --type T[P any] struct{ f P } +-import "net/http" - --type Error error +-var stringAVar = "var" //@item(stringAVar, "stringAVar", "string", "var") +-func stringBFunc() string { return "str" } //@item(stringBFunc, "stringBFunc", "func() string", "func") +-type stringer struct{} //@item(stringer, "stringer", "struct{...}", "struct") - +-func _() stringer //@complete("tr", stringer) - ---- b/b.go -- --package b +-func _(val stringer) {} //@complete("tr", stringer) - --import "mod.test/a" +-func (stringer) _() {} //@complete("tr", stringer) - --type B a.A +-func _() { +- var s struct { +- AA int //@item(rankAA, "AA", "int", "field") +- AB string //@item(rankAB, "AB", "string", "field") +- AC int //@item(rankAC, "AC", "int", "field") +- } +- fnStr := func(string) {} +- fnStr(s.A) //@complete(")", rankAB, rankAA, rankAC) +- fnStr("" + s.A) //@complete(")", rankAB, rankAA, rankAC) - --type BI a.AI +- fnInt := func(int) {} +- fnInt(-s.A) //@complete(")", rankAA, rankAC, rankAB) - --type T a.T[int] // must not panic +- // no expected type +- fnInt(func() int { s.A }) //@complete(" }", rankAA, rankAB, rankAC) +- fnInt(s.A()) //@complete("()", rankAA, rankAC, rankAB) +- fnInt([]int{}[s.A]) //@complete("])", rankAA, rankAC, rankAB) +- fnInt([]int{}[:s.A]) //@complete("])", rankAA, rankAC, rankAB) - ---- c/c.go -- --package c +- fnInt(s.A.(int)) //@complete(".(", rankAA, rankAC, rankAB) - --import "mod.test/b" +- fnPtr := func(*string) {} +- fnPtr(&s.A) //@complete(")", rankAB, rankAA, rankAC) - --func _() { -- x := b.B{ -- F: 42, //@refs("F", FDef, "F") +- var aaPtr *string //@item(rankAAPtr, "aaPtr", "*string", "var") +- var abPtr *int //@item(rankABPtr, "abPtr", "*int", "var") +- fnInt(*a) //@complete(")", rankABPtr, rankAAPtr, stringAVar) +- +- _ = func() string { +- return s.A //@complete(" //", rankAB, rankAA, rankAC) - } -- x.G = "hi" //@refs("G", GDef, "G") -- _ = x.E //@refs("E", EDef, "E") -} - --func _(y b.BI) { -- _ = y.M //@refs("M", MDef, "M") -- _ = y.N //@refs("N", NDef, "N") +-type foo struct { +- fooPrivateField int //@item(rankFooPrivField, "fooPrivateField", "int", "field") +- FooPublicField int //@item(rankFooPubField, "FooPublicField", "int", "field") -} -diff -urN a/gopls/internal/regtest/marker/testdata/references/issue61618.txt b/gopls/internal/regtest/marker/testdata/references/issue61618.txt ---- a/gopls/internal/regtest/marker/testdata/references/issue61618.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/references/issue61618.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,39 +0,0 @@ --Regression test for 'references' bug golang/go#61618: --references to instantiated fields were missing. - ---- flags -- ---min_go=go1.18 +-func (foo) fooPrivateMethod() int { //@item(rankFooPrivMeth, "fooPrivateMethod", "func() int", "method") +- return 0 +-} - ---- go.mod -- --module example.com --go 1.18 +-func (foo) FooPublicMethod() int { //@item(rankFooPubMeth, "FooPublicMethod", "func() int", "method") +- return 0 +-} - ---- a.go -- --package a +-func _() { +- var _ int = foo{}. //@rank(" //", rankFooPrivField, rankFooPubField),rank(" //", rankFooPrivMeth, rankFooPubMeth),rank(" //", rankFooPrivField, rankFooPrivMeth) +-} - --// This file is adapted from the example in the issue. +-func _() { +- HandleFunc //@item(httpHandleFunc, "HandleFunc", "func(pattern string, handler func(http.ResponseWriter, *http.Request))", "func") +- HandlerFunc //@item(httpHandlerFunc, "HandlerFunc", "func(http.ResponseWriter, *http.Request)", "type") - --type builder[S ~[]F, F ~string] struct { -- name string -- elements S //@loc(def, "elements"), refs(def, def, assign, use) -- elemData map[F][]ElemData[F] +- http.HandleFunc //@rank(" //", httpHandleFunc, httpHandlerFunc) -} +diff -urN a/gopls/internal/test/marker/testdata/completion/func_sig.txt b/gopls/internal/test/marker/testdata/completion/func_sig.txt +--- a/gopls/internal/test/marker/testdata/completion/func_sig.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/func_sig.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,15 +0,0 @@ +-This test checks completion related to function signatures. - --type ElemData[F ~string] struct { -- Name F --} +--- flags -- +--ignore_extra_diags - --type BuilderImpl[S ~[]F, F ~string] struct{ builder[S, F] } +--- func_sig.go -- +-package funcsig - --func NewBuilderImpl[S ~[]F, F ~string](name string) *BuilderImpl[S, F] { -- impl := &BuilderImpl[S,F]{ -- builder[S, F]{ -- name: name, -- elements: S{}, //@loc(assign, "elements"), refs(assign, def, assign, use) -- elemData: map[F][]ElemData[F]{}, -- }, -- } +-type someType int //@item(sigSomeType, "someType", "int", "type") - -- _ = impl.elements //@loc(use, "elements"), refs(use, def, assign, use) -- return impl --} -diff -urN a/gopls/internal/regtest/marker/testdata/references/shadow.txt b/gopls/internal/regtest/marker/testdata/references/shadow.txt ---- a/gopls/internal/regtest/marker/testdata/references/shadow.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/references/shadow.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,17 +0,0 @@ --Test of references in the presence of shadowing. +-// Don't complete "foo" in signature. +-func (foo someType) _() { //@item(sigFoo, "foo", "someType", "var"),complete(") {", sigSomeType) - ---- go.mod -- --module example.com --go 1.12 +- //@complete("", sigFoo, sigSomeType) +-} +diff -urN a/gopls/internal/test/marker/testdata/completion/func_snippets.txt b/gopls/internal/test/marker/testdata/completion/func_snippets.txt +--- a/gopls/internal/test/marker/testdata/completion/func_snippets.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/func_snippets.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,32 +0,0 @@ +-This test exercises function snippets using generics. - ---- a/a.go -- --package a +--- flags -- +--ignore_extra_diags - --func _() { -- x := 123 //@loc(x1, "x"), refs("x", x1, x1ref) -- _ = x //@loc(x1ref, "x") -- { -- x := "hi" //@loc(x2, "x"), refs("x", x2, x2ref) -- _ = x //@loc(x2ref, "x") -- } +--- settings.json -- +-{ +- "usePlaceholders": true -} -diff -urN a/gopls/internal/regtest/marker/testdata/references/test.txt b/gopls/internal/regtest/marker/testdata/references/test.txt ---- a/gopls/internal/regtest/marker/testdata/references/test.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/references/test.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,29 +0,0 @@ --Test of references between the extra files of a test variant --and the regular package. - --- go.mod -- --module example.com --go 1.12 +-module golang.org/lsptests/snippets - ---- a/a.go -- --package a +-go 1.18 - --func fn() {} //@loc(def, "fn"), refs("fn", def, use) +--- funcsnippets.go -- +-package snippets - --type t struct { g int } //@loc(gdef, "g") --type u struct { t } +-type SyncMap[K comparable, V any] struct{} - --var _ = new(u).g //@loc(gref, "g"), refs("g", gdef, gref) --// TODO(adonovan): fix: gref2 and gdef2 are missing. +-func NewSyncMap[K comparable, V any]() (result *SyncMap[K, V]) { //@item(NewSyncMap, "NewSyncMap", "", "") +- return +-} - ---- a/a_test.go -- --package a +-func Identity[P ~int](p P) P { //@item(Identity, "Identity", "", "") +- return p +-} - -func _() { -- fn() //@loc(use, "fn") -- -- _ = new(u).g //@loc(gref2, "g"), refs("g", gdef2, gref, gref2) +- _ = NewSyncM //@snippet(" //", NewSyncMap, "NewSyncMap[${1:K comparable}, ${2:V any}]()") +- _ = Identi //@snippet(" //", Identity, "Identity(${1:p P})") -} +diff -urN a/gopls/internal/test/marker/testdata/completion/func_value.txt b/gopls/internal/test/marker/testdata/completion/func_value.txt +--- a/gopls/internal/test/marker/testdata/completion/func_value.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/func_value.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,33 +0,0 @@ +-This test checks completion related to function values. - --// This declaration changes the meaning of u.t in the test. --func (u) g() {} //@loc(gdef2, "g") -diff -urN a/gopls/internal/regtest/marker/testdata/references/typeswitch.txt b/gopls/internal/regtest/marker/testdata/references/typeswitch.txt ---- a/gopls/internal/regtest/marker/testdata/references/typeswitch.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/references/typeswitch.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,18 +0,0 @@ --Tests of reference to implicit type switch vars, which are --a special case in go/types.Info{Def,Use,Implicits}. -- ---- go.mod -- --module example.com --go 1.12 +--- flags -- +--ignore_extra_diags - ---- a/a.go -- --package a +--- func_value.go -- +-package funcvalue - --func _(x interface{}) { -- switch y := x.(type) { //@loc(yDecl, "y"), refs("y", yDecl, yInt, yDefault) -- case int: -- println(y) //@loc(yInt, "y"), refs("y", yDecl, yInt, yDefault) -- default: -- println(y) //@loc(yDefault, "y") -- } +-func fooFunc() int { //@item(fvFooFunc, "fooFunc", "func() int", "func") +- return 0 -} -diff -urN a/gopls/internal/regtest/marker/testdata/rename/basic.txt b/gopls/internal/regtest/marker/testdata/rename/basic.txt ---- a/gopls/internal/regtest/marker/testdata/rename/basic.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/rename/basic.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,38 +0,0 @@ --This test performs basic coverage of 'rename' within a single package. - ---- basic.go -- --package p +-var _ = fooFunc() //@item(fvFooFuncCall, "fooFunc", "func() int", "func") - --func f(x int) { println(x) } //@rename("x", "y", xToy) +-var fooVar = func() int { //@item(fvFooVar, "fooVar", "func() int", "var") +- return 0 +-} - ---- @xToy/basic.go -- --package p +-var _ = fooVar() //@item(fvFooVarCall, "fooVar", "func() int", "var") - --func f(y int) { println(y) } //@rename("x", "y", xToy) +-type myFunc func() int - ---- alias.go -- --package p +-var fooType myFunc = fooVar //@item(fvFooType, "fooType", "myFunc", "var") - --// from golang/go#61625 --type LongNameHere struct{} --type A = LongNameHere //@rename("A", "B", AToB) --func Foo() A +-var _ = fooType() //@item(fvFooTypeCall, "fooType", "func() int", "var") - ---- errors.go -- --package p +-func _() { +- var f func() int +- f = foo //@complete(" //", fvFooFunc, fvFooType, fvFooVar) - --func _(x []int) { //@renameerr("_", "blank", `can't rename "_"`) -- x = append(x, 1) //@renameerr("append", "blank", "built in and cannot be renamed") -- x = nil //@renameerr("nil", "blank", "built in and cannot be renamed") -- x = nil //@renameerr("x", "x", "old and new names are the same: x") -- _ = 1 //@renameerr("1", "x", "no identifier found") +- var i int +- i = foo //@complete(" //", fvFooFuncCall, fvFooTypeCall, fvFooVarCall) -} +diff -urN a/gopls/internal/test/marker/testdata/completion/fuzzy.txt b/gopls/internal/test/marker/testdata/completion/fuzzy.txt +--- a/gopls/internal/test/marker/testdata/completion/fuzzy.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/fuzzy.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,55 +0,0 @@ +-This test exercises fuzzy completion matching. - ---- @AToB/alias.go -- --package p +--- flags -- +--ignore_extra_diags - --// from golang/go#61625 --type LongNameHere struct{} --type B = LongNameHere //@rename("A", "B", AToB) --func Foo() B +--- go.mod -- +-module golang.org/lsptests - -diff -urN a/gopls/internal/regtest/marker/testdata/rename/conflict.txt b/gopls/internal/regtest/marker/testdata/rename/conflict.txt ---- a/gopls/internal/regtest/marker/testdata/rename/conflict.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/rename/conflict.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,59 +0,0 @@ --This test exercises some renaming conflict scenarios --and ensures that the errors are informative. +-go 1.18 - ---- go.mod -- --module example.com --go 1.12 +--- fuzzy/fuzzy.go -- +-package fuzzy - ---- super/p.go -- --package super +-func _() { +- var a struct { +- fabar int +- fooBar string +- } - --var x int +- a.fabar //@item(fuzzFabarField, "a.fabar", "int", "field") +- a.fooBar //@item(fuzzFooBarField, "a.fooBar", "string", "field") - --func f(y int) { -- println(x) -- println(y) //@renameerr("y", "x", errSuperBlockConflict) --} +- afa //@complete(" //", fuzzFabarField, fuzzFooBarField) +- afb //@complete(" //", fuzzFooBarField, fuzzFabarField) - ---- @errSuperBlockConflict -- --super/p.go:5:8: renaming this var "y" to "x" --super/p.go:6:10: would shadow this reference --super/p.go:3:5: to the var declared here ---- sub/p.go -- --package sub +- fab //@complete(" //", fuzzFabarField) - --var a int +- var myString string +- myString = af //@complete(" //", fuzzFooBarField, fuzzFabarField) - --func f2(b int) { -- println(a) //@renameerr("a", "b", errSubBlockConflict) -- println(b) --} +- var b struct { +- c struct { +- d struct { +- e struct { +- abc string +- } +- abc float32 +- } +- abc bool +- } +- abc int +- } - ---- @errSubBlockConflict -- --sub/p.go:3:5: renaming this var "a" to "b" --sub/p.go:6:10: would cause this reference to become shadowed --sub/p.go:5:9: by this intervening var definition ---- pkgname/p.go -- --package pkgname +- b.abc //@item(fuzzABCInt, "b.abc", "int", "field") +- b.c.abc //@item(fuzzABCbool, "b.c.abc", "bool", "field") +- b.c.d.abc //@item(fuzzABCfloat, "b.c.d.abc", "float32", "field") +- b.c.d.e.abc //@item(fuzzABCstring, "b.c.d.e.abc", "string", "field") - --import e1 "errors" //@renameerr("e1", "errors", errImportConflict) --import "errors" +- // in depth order by default +- abc //@complete(" //", fuzzABCInt, fuzzABCbool, fuzzABCfloat) - --var _ = errors.New --var _ = e1.New +- // deep candidate that matches expected type should still ranked first +- var s string +- s = abc //@complete(" //", fuzzABCstring, fuzzABCInt, fuzzABCbool) +-} +diff -urN a/gopls/internal/test/marker/testdata/completion/imported-std.txt b/gopls/internal/test/marker/testdata/completion/imported-std.txt +--- a/gopls/internal/test/marker/testdata/completion/imported-std.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/imported-std.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,61 +0,0 @@ +-Test of imported completions respecting the effective Go version of the file. - ---- @errImportConflict -- --pkgname/p.go:3:8: renaming this imported package name "e1" to "errors" --pkgname/p.go:4:8: conflicts with imported package name in same block ---- pkgname2/p1.go -- --package pkgname2 --var x int +-(See "un-" prefixed file for same test of unimported completions.) - ---- pkgname2/p2.go -- --package pkgname2 --import "errors" //@renameerr("errors", "x", errImportConflict2) --var _ = errors.New +-These symbols below were introduced to go/types in go1.22: - ---- @errImportConflict2 -- --pkgname2/p2.go:2:8: renaming this imported package name "errors" to "x" would conflict --pkgname2/p1.go:2:5: with this package member var -diff -urN a/gopls/internal/regtest/marker/testdata/rename/embed.txt b/gopls/internal/regtest/marker/testdata/rename/embed.txt ---- a/gopls/internal/regtest/marker/testdata/rename/embed.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/rename/embed.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,36 +0,0 @@ --This test exercises renaming of types used as embedded fields. +- Alias +- Info.FileVersions +- (Checker).PkgNameOf +- +-The underlying logic depends on versions.FileVersion, which only +-behaves correctly in go1.22. (When go1.22 is assured, we can remove +-the min_go flag but leave the test inputs unchanged.) +- +--- flags -- +--ignore_extra_diags -min_go=go1.22 - --- go.mod -- -module example.com --go 1.12 +- +-go 1.21 - --- a/a.go -- -package a - --type A int //@rename("A", "A2", type) +-import "go/ast" +-import "go/token" +-import "go/types" - ---- b/b.go -- --package b +-// package-level decl +-var _ = types.Sat //@rankl("Sat", "Satisfies") +-var _ = types.Ali //@rankl("Ali", "!Alias") - --import "example.com/a" +-// field +-var _ = new(types.Info).Use //@rankl("Use", "Uses") +-var _ = new(types.Info).Fil //@rankl("Fil", "!FileVersions") - --type B struct { a.A } //@renameerr("A", "A3", errAnonField) +-// method +-var _ = new(types.Checker).Obje //@rankl("Obje", "ObjectOf") +-var _ = new(types.Checker).PkgN //@rankl("PkgN", "!PkgNameOf") - --var _ = new(B).A //@renameerr("A", "A4", errAnonField) +--- b/b.go -- +-//go:build go1.22 - ---- @errAnonField -- --can't rename embedded fields: rename the type directly or name the field ---- @type/a/a.go -- -package a - --type A2 int //@rename("A", "A2", type) -- ---- @type/b/b.go -- --package b +-import "go/ast" +-import "go/token" +-import "go/types" - --import "example.com/a" +-// package-level decl +-var _ = types.Sat //@rankl("Sat", "Satisfies") +-var _ = types.Ali //@rankl("Ali", "Alias") - --type B struct { a.A2 } //@renameerr("A", "A3", errAnonField) +-// field +-var _ = new(types.Info).Use //@rankl("Use", "Uses") +-var _ = new(types.Info).Fil //@rankl("Fil", "FileVersions") - --var _ = new(B).A2 //@renameerr("A", "A4", errAnonField) +-// method +-var _ = new(types.Checker).Obje //@rankl("Obje", "ObjectOf") +-var _ = new(types.Checker).PkgN //@rankl("PkgN", "PkgNameOf") +diff -urN a/gopls/internal/test/marker/testdata/completion/index.txt b/gopls/internal/test/marker/testdata/completion/index.txt +--- a/gopls/internal/test/marker/testdata/completion/index.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/index.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,36 +0,0 @@ +-This test checks completion related to index expressions. - -diff -urN a/gopls/internal/regtest/marker/testdata/rename/generics.txt b/gopls/internal/regtest/marker/testdata/rename/generics.txt ---- a/gopls/internal/regtest/marker/testdata/rename/generics.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/rename/generics.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,240 +0,0 @@ --This test exercises various renaming features on generic code. +--- flags -- +--ignore_extra_diags - --Fixed bugs: +--- settings.json -- +-{ +- "completeUnimported": false +-} - --- golang/go#61614: renaming a method of a type in a package that uses type -- parameter composite lits used to panic, because previous iterations of the -- satisfy analysis did not account for this language feature. +--- index.go -- +-package index - --- golang/go#61635: renaming type parameters did not work when they were -- capitalized and the package was imported by another package. +-func _() { +- var ( +- aa = "123" //@item(indexAA, "aa", "string", "var") +- ab = 123 //@item(indexAB, "ab", "int", "var") +- ) - ---- flags -- ---min_go=go1.18 +- var foo [1]int +- foo[a] //@complete("]", indexAB, indexAA) +- foo[:a] //@complete("]", indexAB, indexAA) +- a[:a] //@complete("[", indexAA, indexAB) +- a[a] //@complete("[", indexAA, indexAB) - ---- go.mod -- --module example.com --go 1.20 +- var bar map[string]int +- bar[a] //@complete("]", indexAA, indexAB) - ---- a.go -- --package a +- type myMap map[string]int +- var baz myMap +- baz[a] //@complete("]", indexAA, indexAB) - --type I int +- type myInt int +- var mi myInt //@item(indexMyInt, "mi", "myInt", "var") +- foo[m] //@snippet("]", indexMyInt, "mi") +-} +diff -urN a/gopls/internal/test/marker/testdata/completion/interfacerank.txt b/gopls/internal/test/marker/testdata/completion/interfacerank.txt +--- a/gopls/internal/test/marker/testdata/completion/interfacerank.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/interfacerank.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,36 +0,0 @@ +-This test checks that completion ranking accounts for interface assignability. - --func (I) m() {} //@rename("m", "M", mToM) +--- flags -- +--ignore_extra_diags - --func _[P ~[]int]() { -- _ = P{} +--- settings.json -- +-{ +- "completeUnimported": false, +- "deepCompletion": false -} - ---- @mToM/a.go -- --package a -- --type I int +--- p.go -- - --func (I) M() {} //@rename("m", "M", mToM) +-package interfacerank - --func _[P ~[]int]() { -- _ = P{} +-type foo interface { +- foo() -} - ---- g.go -- --package a +-type fooImpl int - --type S[P any] struct { //@rename("P", "Q", PToQ) -- P P -- F func(P) P --} +-func (*fooImpl) foo() {} - --func F[R any](r R) { -- var _ R //@rename("R", "S", RToS) --} +-func wantsFoo(foo) {} - ---- @PToQ/g.go -- --package a +-func _() { +- var ( +- aa string //@item(irAA, "aa", "string", "var") +- ab *fooImpl //@item(irAB, "ab", "*fooImpl", "var") +- ) - --type S[Q any] struct { //@rename("P", "Q", PToQ) -- P Q -- F func(Q) Q --} +- wantsFoo(a) //@complete(")", irAB, irAA) - --func F[R any](r R) { -- var _ R //@rename("R", "S", RToS) +- var ac fooImpl //@item(irAC, "ac", "fooImpl", "var") +- wantsFoo(&a) //@complete(")", irAC, irAA, irAB) -} +diff -urN a/gopls/internal/test/marker/testdata/completion/issue51783.txt b/gopls/internal/test/marker/testdata/completion/issue51783.txt +--- a/gopls/internal/test/marker/testdata/completion/issue51783.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/issue51783.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,47 +0,0 @@ +-Regression test for "completion gives unneeded generic type +-instantiation snippet", #51783. - ---- @RToS/g.go -- --package a +-Type parameters that can be inferred from the arguments +-are not part of the offered completion snippet. - --type S[P any] struct { //@rename("P", "Q", PToQ) -- P P -- F func(P) P --} +--- flags -- +--ignore_extra_diags - --func F[S any](r S) { -- var _ S //@rename("R", "S", RToS) --} +--- a.go -- +-package a - ---- issue61635/p.go -- --package issue61635 +-// identity has a single simple type parameter. +-// The completion omits the instantiation. +-func identity[T any](x T) T - --type builder[S ~[]F, F ~string] struct { //@rename("S", "T", SToT) -- name string -- elements S -- elemData map[F][]ElemData[F] -- // other fields... --} +-// clone has a second type parameter that is nonetheless constrained by the parameter. +-// The completion omits the instantiation. +-func clone[S ~[]E, E any](s S) S - --type ElemData[F ~string] struct { -- Name F -- // other fields... --} +-// unconstrained has a type parameter constrained only by the result. +-// The completion suggests instantiation. +-func unconstrained[X, Y any](x X) Y - --type BuilderImpl[S ~[]F, F ~string] struct{ builder[S, F] } +-// partial has three type parameters, +-// only the last two of which may be omitted as they +-// are constrained by the arguments. +-func partial[R any, S ~[]E, E any](s S) R - ---- importer/i.go -- --package importer +-//@item(identity, "identity", "details", "kind") +-//@item(clone, "clone", "details", "kind") +-//@item(unconstrained, "unconstrained", "details", "kind") +-//@item(partial, "partial", "details", "kind") - --import "example.com/issue61635" // importing is necessary to repro golang/go#61635 +-func _() { +- _ = identity //@snippet("identity", identity, "identity(${1:})") - --var _ issue61635.ElemData[string] +- _ = clone //@snippet("clone", clone, "clone(${1:})") - ---- @SToT/issue61635/p.go -- --package issue61635 +- _ = unconstrained //@snippet("unconstrained", unconstrained, "unconstrained[${1:}](${2:})") - --type builder[T ~[]F, F ~string] struct { //@rename("S", "T", SToT) -- name string -- elements T -- elemData map[F][]ElemData[F] -- // other fields... --} +- _ = partial //@snippet("partial", partial, "partial[${1:}](${2:})") - --type ElemData[F ~string] struct { -- Name F -- // other fields... +- // Result-type inference permits us to omit Y in this (rare) case, +- // but completion doesn't support that. +- var _ int = unconstrained //@snippet("unconstrained", unconstrained, "unconstrained[${1:}](${2:})") -} +diff -urN a/gopls/internal/test/marker/testdata/completion/issue56505.txt b/gopls/internal/test/marker/testdata/completion/issue56505.txt +--- a/gopls/internal/test/marker/testdata/completion/issue56505.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/issue56505.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,13 +0,0 @@ +-Test for golang/go#56505: completion on variables of type *error should not +-panic. - --type BuilderImpl[S ~[]F, F ~string] struct{ builder[S, F] } +--- flags -- +--ignore_extra_diags - ---- instances/type.go -- --package instances +--- issue.go -- +-package issues - --type R[P any] struct { //@rename("R", "u", Rtou) -- Next *R[P] //@rename("R", "s", RTos) +-func _() { +- var e *error +- e.x //@complete(" //") -} +diff -urN a/gopls/internal/test/marker/testdata/completion/issue59096.txt b/gopls/internal/test/marker/testdata/completion/issue59096.txt +--- a/gopls/internal/test/marker/testdata/completion/issue59096.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/issue59096.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,20 +0,0 @@ +-This test exercises the panic in golang/go#59096: completing at a syntactic +-type-assert expression was panicking because gopls was translating it into +-a (malformed) selector expr. - --func (rv R[P]) Do(R[P]) R[P] { //@rename("Do", "Do1", DoToDo1) -- var x R[P] -- return rv.Do(x) //@rename("Do", "Do2", DoToDo2) --} +--- go.mod -- +-module example.com +- +--- a/a.go -- +-package a - -func _() { -- var x R[int] //@rename("R", "r", RTor) -- x = x.Do(x) +- b.(foo) //@complete(re"b.()", B), diag("b", re"(undefined|undeclared name): b") -} - ---- @RTos/instances/type.go -- --package instances +-//@item(B, "B", "const (from \"example.com/b\")", "const") - --type s[P any] struct { //@rename("R", "u", Rtou) -- Next *s[P] //@rename("R", "s", RTos) --} +--- b/b.go -- +-package b - --func (rv s[P]) Do(s[P]) s[P] { //@rename("Do", "Do1", DoToDo1) -- var x s[P] -- return rv.Do(x) //@rename("Do", "Do2", DoToDo2) --} +-const B = 0 +diff -urN a/gopls/internal/test/marker/testdata/completion/issue60545.txt b/gopls/internal/test/marker/testdata/completion/issue60545.txt +--- a/gopls/internal/test/marker/testdata/completion/issue60545.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/issue60545.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,28 +0,0 @@ +-This test checks that unimported completion is case-insensitive. - --func _() { -- var x s[int] //@rename("R", "r", RTor) -- x = x.Do(x) --} +--- go.mod -- +-module mod.test - ---- @Rtou/instances/type.go -- --package instances +-go 1.18 - --type u[P any] struct { //@rename("R", "u", Rtou) -- Next *u[P] //@rename("R", "s", RTos) --} +--- main.go -- +-package main - --func (rv u[P]) Do(u[P]) u[P] { //@rename("Do", "Do1", DoToDo1) -- var x u[P] -- return rv.Do(x) //@rename("Do", "Do2", DoToDo2) --} +-//@item(Print, "Print", "func (from \"fmt\")", "func") +-//@item(Printf, "Printf", "func (from \"fmt\")", "func") +-//@item(Println, "Println", "func (from \"fmt\")", "func") - --func _() { -- var x u[int] //@rename("R", "r", RTor) -- x = x.Do(x) +-func main() { +- fmt.p //@complete(re"fmt.p()", Print, Printf, Println), diag("fmt", re"(undefined|undeclared)") -} - ---- @DoToDo1/instances/type.go -- --package instances -- --type R[P any] struct { //@rename("R", "u", Rtou) -- Next *R[P] //@rename("R", "s", RTos) --} +--- other.go -- +-package main - --func (rv R[P]) Do1(R[P]) R[P] { //@rename("Do", "Do1", DoToDo1) -- var x R[P] -- return rv.Do1(x) //@rename("Do", "Do2", DoToDo2) --} +-// Including another package that imports "fmt" causes completion to use the +-// existing metadata, which is the codepath leading to golang/go#60545. +-import "fmt" - -func _() { -- var x R[int] //@rename("R", "r", RTor) -- x = x.Do1(x) +- fmt.Println() -} +diff -urN a/gopls/internal/test/marker/testdata/completion/issue62141.txt b/gopls/internal/test/marker/testdata/completion/issue62141.txt +--- a/gopls/internal/test/marker/testdata/completion/issue62141.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/issue62141.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,39 +0,0 @@ +-This test checks that we don't suggest completion to an untyped conversion such +-as "untyped float(abcdef)". - ---- @DoToDo2/instances/type.go -- --package instances +--- main.go -- +-package main - --type R[P any] struct { //@rename("R", "u", Rtou) -- Next *R[P] //@rename("R", "s", RTos) --} +-func main() { +- abcdef := 32 //@diag("abcdef", re"not used") +- x := 1.0 / abcd //@acceptcompletion(re"abcd()", "abcdef", int), diag("x", re"not used"), diag("abcd", re"(undefined|undeclared)") - --func (rv R[P]) Do2(R[P]) R[P] { //@rename("Do", "Do1", DoToDo1) -- var x R[P] -- return rv.Do2(x) //@rename("Do", "Do2", DoToDo2) +- // Verify that we don't suggest converting compatible untyped constants. +- const untypedConst = 42 +- y := 1.1 / untypedC //@acceptcompletion(re"untypedC()", "untypedConst", untyped), diag("y", re"not used"), diag("untypedC", re"(undefined|undeclared)") -} - --func _() { -- var x R[int] //@rename("R", "r", RTor) -- x = x.Do2(x) --} +--- @int/main.go -- +-package main - ---- instances/func.go -- --package instances +-func main() { +- abcdef := 32 //@diag("abcdef", re"not used") +- x := 1.0 / float64(abcdef) //@acceptcompletion(re"abcd()", "abcdef", int), diag("x", re"not used"), diag("abcd", re"(undefined|undeclared)") - --func Foo[P any](p P) { //@rename("Foo", "Bar", FooToBar) -- Foo(p) //@rename("Foo", "Baz", FooToBaz) +- // Verify that we don't suggest converting compatible untyped constants. +- const untypedConst = 42 +- y := 1.1 / untypedC //@acceptcompletion(re"untypedC()", "untypedConst", untyped), diag("y", re"not used"), diag("untypedC", re"(undefined|undeclared)") -} - ---- @FooToBar/instances/func.go -- --package instances +--- @untyped/main.go -- +-package main +- +-func main() { +- abcdef := 32 //@diag("abcdef", re"not used") +- x := 1.0 / abcd //@acceptcompletion(re"abcd()", "abcdef", int), diag("x", re"not used"), diag("abcd", re"(undefined|undeclared)") - --func Bar[P any](p P) { //@rename("Foo", "Bar", FooToBar) -- Bar(p) //@rename("Foo", "Baz", FooToBaz) +- // Verify that we don't suggest converting compatible untyped constants. +- const untypedConst = 42 +- y := 1.1 / untypedConst //@acceptcompletion(re"untypedC()", "untypedConst", untyped), diag("y", re"not used"), diag("untypedC", re"(undefined|undeclared)") -} - ---- @FooToBaz/instances/func.go -- --package instances +diff -urN a/gopls/internal/test/marker/testdata/completion/issue62560.txt b/gopls/internal/test/marker/testdata/completion/issue62560.txt +--- a/gopls/internal/test/marker/testdata/completion/issue62560.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/issue62560.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,19 +0,0 @@ +-This test verifies that completion of package members in unimported packages +-reflects their fuzzy score, even when those members are present in the +-transitive import graph of the main module. (For technical reasons, this was +-the nature of the regression in golang/go#62560.) - --func Baz[P any](p P) { //@rename("Foo", "Bar", FooToBar) -- Baz(p) //@rename("Foo", "Baz", FooToBaz) --} +--- go.mod -- +-module mod.test - ---- @RTor/instances/type.go -- --package instances +--- foo/foo.go -- +-package foo - --type r[P any] struct { //@rename("R", "u", Rtou) -- Next *r[P] //@rename("R", "s", RTos) +-func _() { +- json.U //@rankl(re"U()", "Unmarshal", "InvalidUTF8Error"), diag("json", re"(undefined|undeclared)") -} - --func (rv r[P]) Do(r[P]) r[P] { //@rename("Do", "Do1", DoToDo1) -- var x r[P] -- return rv.Do(x) //@rename("Do", "Do2", DoToDo2) --} +--- bar/bar.go -- +-package bar - --func _() { -- var x r[int] //@rename("R", "r", RTor) -- x = x.Do(x) +-import _ "encoding/json" +diff -urN a/gopls/internal/test/marker/testdata/completion/issue62676.txt b/gopls/internal/test/marker/testdata/completion/issue62676.txt +--- a/gopls/internal/test/marker/testdata/completion/issue62676.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/issue62676.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,63 +0,0 @@ +-This test verifies that unimported completion respects the usePlaceholders setting. +- +--- flags -- +--ignore_extra_diags +- +--- settings.json -- +-{ +- "usePlaceholders": false -} - -diff -urN a/gopls/internal/regtest/marker/testdata/rename/issue43616.txt b/gopls/internal/regtest/marker/testdata/rename/issue43616.txt ---- a/gopls/internal/regtest/marker/testdata/rename/issue43616.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/rename/issue43616.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,19 +0,0 @@ --This test verifies the fix for golang/go#43616: renaming mishandles embedded --fields. +--- go.mod -- +-module mod.test - ---- p.go -- --package issue43616 +-go 1.21 - --type foo int //@rename("foo", "bar", fooToBar),preparerename("oo","foo","foo") +--- foo/foo.go -- +-package foo - --var x struct{ foo } //@renameerr("foo", "baz", "rename the type directly") +-func _() { +- // This uses goimports-based completion; TODO: this should insert snippets. +- os.Open //@acceptcompletion(re"Open()", "Open", open) +-} - --var _ = x.foo //@renameerr("foo", "quux", "rename the type directly") ---- @fooToBar/p.go -- --package issue43616 +-func _() { +- // This uses metadata-based completion. +- errors.New //@acceptcompletion(re"New()", "New", new) +-} - --type bar int //@rename("foo", "bar", fooToBar),preparerename("oo","foo","foo") +--- bar/bar.go -- +-package bar - --var x struct{ bar } //@renameerr("foo", "baz", "rename the type directly") +-import _ "errors" // important: doesn't transitively import os. - --var _ = x.bar //@renameerr("foo", "quux", "rename the type directly") -diff -urN a/gopls/internal/regtest/marker/testdata/rename/issue60789.txt b/gopls/internal/regtest/marker/testdata/rename/issue60789.txt ---- a/gopls/internal/regtest/marker/testdata/rename/issue60789.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/rename/issue60789.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,36 +0,0 @@ +--- @new/foo/foo.go -- +-package foo - --This test renames an exported method of an unexported type, --which is an edge case for objectpath, since it computes a path --from a syntax package that is no good when applied to an --export data package. +-import "errors" - --See issue #60789. +-func _() { +- // This uses goimports-based completion; TODO: this should insert snippets. +- os.Open //@acceptcompletion(re"Open()", "Open", open) +-} - ---- go.mod -- --module example.com --go 1.12 +-func _() { +- // This uses metadata-based completion. +- errors.New(${1:}) //@acceptcompletion(re"New()", "New", new) +-} - ---- a/a.go -- --package a +--- @open/foo/foo.go -- +-package foo - --type unexported int --func (unexported) F() {} //@rename("F", "G", fToG) +-import "os" - --var _ = unexported(0).F +-func _() { +- // This uses goimports-based completion; TODO: this should insert snippets. +- os.Open //@acceptcompletion(re"Open()", "Open", open) +-} - ---- b/b.go -- --package b +-func _() { +- // This uses metadata-based completion. +- errors.New //@acceptcompletion(re"New()", "New", new) +-} - --// The existence of this package is sufficient to exercise --// the bug even though it cannot reference a.unexported. +diff -urN a/gopls/internal/test/marker/testdata/completion/keywords.txt b/gopls/internal/test/marker/testdata/completion/keywords.txt +--- a/gopls/internal/test/marker/testdata/completion/keywords.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/keywords.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,166 +0,0 @@ +-This test checks completion of Go keywords. - --import _ "example.com/a" +--- flags -- +--ignore_extra_diags +--filter_keywords=false - ---- @fToG/a/a.go -- --package a +--- settings.json -- +-{ +- "completeUnimported": false, +- "matcher": "caseInsensitive", +- "experimentalPostfixCompletions": false +-} - --type unexported int --func (unexported) G() {} //@rename("F", "G", fToG) +--- keywords.go -- +-package keywords - --var _ = unexported(0).G +-//@rank("", type),rank("", func),rank("", var),rank("", const),rank("", import) - -diff -urN a/gopls/internal/regtest/marker/testdata/rename/issue61294.txt b/gopls/internal/regtest/marker/testdata/rename/issue61294.txt ---- a/gopls/internal/regtest/marker/testdata/rename/issue61294.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/rename/issue61294.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,29 +0,0 @@ +-func _() { +- var test int //@rank(" //", int, interface) +- var tChan chan int +- var _ m //@complete(" //", map) +- var _ f //@complete(" //", func) +- var _ c //@complete(" //", chan) - --This test renames a parameter var whose name is the same as a --package-level var, which revealed a bug in isLocal. +- var _ str //@rank(" //", string, struct) - --This is a regression test for issue #61294. +- type _ int //@rank(" //", interface, int) - ---- go.mod -- --module example.com --go 1.18 +- type _ str //@rank(" //", struct, string) - ---- a/a.go -- --package a +- switch test { +- case 1: // TODO: trying to complete case here will break because the parser won't return *ast.Ident +- b //@complete(" //", break) +- case 2: +- f //@complete(" //", fallthrough, for) +- r //@complete(" //", return) +- d //@complete(" //", default, defer) +- c //@complete(" //", case, const) +- } - --func One() +- switch test.(type) { +- case fo: //@complete(":") +- case int: +- b //@complete(" //", break) +- case int32: +- f //@complete(" //", for) +- d //@complete(" //", default, defer) +- r //@complete(" //", return) +- c //@complete(" //", case, const) +- } - --func Two(One int) //@rename("One", "Three", OneToThree) +- select { +- case <-tChan: +- b //@complete(" //", break) +- c //@complete(" //", case, const) +- } - ---- b/b.go -- --package b +- for index := 0; index < test; index++ { +- c //@complete(" //", const, continue) +- b //@complete(" //", break) +- } - --import _ "example.com/a" +- for range []int{} { +- c //@complete(" //", const, continue) +- b //@complete(" //", break) +- } - ---- @OneToThree/a/a.go -- --package a +- // Test function level keywords - --func One() +- //Using 2 characters to test because map output order is random +- sw //@complete(" //", switch) +- se //@complete(" //", select) - --func Two(Three int) //@rename("One", "Three", OneToThree) +- f //@complete(" //", for) +- d //@complete(" //", defer) +- g //@rank(" //", go),rank(" //", goto) +- r //@complete(" //", return) +- i //@complete(" //", if) +- e //@complete(" //", else) +- v //@complete(" //", var) +- c //@complete(" //", const) - -diff -urN a/gopls/internal/regtest/marker/testdata/rename/issue61640.txt b/gopls/internal/regtest/marker/testdata/rename/issue61640.txt ---- a/gopls/internal/regtest/marker/testdata/rename/issue61640.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/rename/issue61640.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,47 +0,0 @@ --This test verifies that gopls can rename instantiated fields. +- for i := r //@complete(" //", range) +-} - ---- flags -- ---min_go=go1.18 +-/* package */ //@item(package, "package", "", "keyword") +-/* import */ //@item(import, "import", "", "keyword") +-/* func */ //@item(func, "func", "", "keyword") +-/* type */ //@item(type, "type", "", "keyword") +-/* var */ //@item(var, "var", "", "keyword") +-/* const */ //@item(const, "const", "", "keyword") +-/* break */ //@item(break, "break", "", "keyword") +-/* default */ //@item(default, "default", "", "keyword") +-/* case */ //@item(case, "case", "", "keyword") +-/* defer */ //@item(defer, "defer", "", "keyword") +-/* go */ //@item(go, "go", "", "keyword") +-/* for */ //@item(for, "for", "", "keyword") +-/* if */ //@item(if, "if", "", "keyword") +-/* else */ //@item(else, "else", "", "keyword") +-/* switch */ //@item(switch, "switch", "", "keyword") +-/* select */ //@item(select, "select", "", "keyword") +-/* fallthrough */ //@item(fallthrough, "fallthrough", "", "keyword") +-/* continue */ //@item(continue, "continue", "", "keyword") +-/* return */ //@item(return, "return", "", "keyword") +-/* goto */ //@item(goto, "goto", "", "keyword") +-/* struct */ //@item(struct, "struct", "", "keyword") +-/* interface */ //@item(interface, "interface", "", "keyword") +-/* map */ //@item(map, "map", "", "keyword") +-/* chan */ //@item(chan, "chan", "", "keyword") +-/* range */ //@item(range, "range", "", "keyword") +-/* string */ //@item(string, "string", "", "type") +-/* int */ //@item(int, "int", "", "type") - ---- a.go -- --package a +--- accidental_keywords.go -- +-package keywords - --// This file is adapted from the example in the issue. +-// non-matching candidate - shouldn't show up as completion +-var apple = "apple" - --type builder[S ~[]int] struct { -- elements S //@rename("elements", "elements2", OneToTwo) +-func _() { +- foo.bar() // insert some extra statements to exercise our AST surgery +- variance := 123 //@item(kwVariance, "variance", "int", "var") +- foo.bar() +- println(var) //@complete(")", kwVariance) +-} +- +-func _() { +- foo.bar() +- var s struct { variance int } //@item(kwVarianceField, "variance", "int", "field") +- foo.bar() +- s.var //@complete(" //", kwVarianceField) -} - --type BuilderImpl[S ~[]int] struct{ builder[S] } +-func _() { +- channel := 123 //@item(kwChannel, "channel", "int", "var") +- chan //@complete(" //", kwChannel) +- foo.bar() +-} - --func NewBuilderImpl[S ~[]int](name string) *BuilderImpl[S] { -- impl := &BuilderImpl[S]{ -- builder[S]{ -- elements: S{}, -- }, -- } +-func _() { +- foo.bar() +- var typeName string //@item(kwTypeName, "typeName", "string", "var") +- foo.bar() +- type //@complete(" //", kwTypeName) +-} +--- empty_select.go -- +-package keywords - -- _ = impl.elements -- return impl +-func _() { +- select { +- c //@complete(" //", case) +- } -} ---- @OneToTwo/a.go -- --package a +--- empty_switch.go -- +-package keywords - --// This file is adapted from the example in the issue. +-func _() { +- switch { +- //@complete("", case, default) +- } - --type builder[S ~[]int] struct { -- elements2 S //@rename("elements", "elements2", OneToTwo) +- switch test.(type) { +- d //@complete(" //", default) +- } -} +diff -urN a/gopls/internal/test/marker/testdata/completion/labels.txt b/gopls/internal/test/marker/testdata/completion/labels.txt +--- a/gopls/internal/test/marker/testdata/completion/labels.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/labels.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,55 +0,0 @@ +-This test checks completion of labels. - --type BuilderImpl[S ~[]int] struct{ builder[S] } -- --func NewBuilderImpl[S ~[]int](name string) *BuilderImpl[S] { -- impl := &BuilderImpl[S]{ -- builder[S]{ -- elements2: S{}, -- }, -- } +--- flags -- +--ignore_extra_diags - -- _ = impl.elements2 -- return impl --} -diff -urN a/gopls/internal/regtest/marker/testdata/rename/issue61813.txt b/gopls/internal/regtest/marker/testdata/rename/issue61813.txt ---- a/gopls/internal/regtest/marker/testdata/rename/issue61813.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/rename/issue61813.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,18 +0,0 @@ --This test exercises the panic reported in golang/go#61813. +--- labels.go -- +-package labels - ---- p.go -- --package p +-func _() { +- goto F //@complete(" //", label1, label5) - --type P struct{} +-Foo1: //@item(label1, "Foo1", "label", "const") +- for a, b := range []int{} { +- Foo2: //@item(label2, "Foo2", "label", "const") +- switch { +- case true: +- break F //@complete(" //", label2, label1) - --func (P) M() {} //@rename("M", "N", MToN) +- continue F //@complete(" //", label1) - --var x = []*P{{}} ---- @MToN/p.go -- --package p +- { +- FooUnjumpable: +- } - --type P struct{} +- goto F //@complete(" //", label1, label2, label4, label5) - --func (P) N() {} //@rename("M", "N", MToN) +- func() { +- goto F //@complete(" //", label3) - --var x = []*P{{}} -diff -urN a/gopls/internal/regtest/marker/testdata/rename/methods.txt b/gopls/internal/regtest/marker/testdata/rename/methods.txt ---- a/gopls/internal/regtest/marker/testdata/rename/methods.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/rename/methods.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,67 +0,0 @@ --This test exercises renaming of interface methods. +- break F //@complete(" //") - --The golden is currently wrong due to https://github.com/golang/go/issues/58506: --the reference to B.F in package b should be renamed too. +- continue F //@complete(" //") - ---- go.mod -- --module example.com --go 1.12 +- Foo3: //@item(label3, "Foo3", "label", "const") +- }() +- } - ---- a/a.go -- --package a +- Foo4: //@item(label4, "Foo4", "label", "const") +- switch interface{}(a).(type) { +- case int: +- break F //@complete(" //", label4, label1) +- } +- } - --type A int +- break F //@complete(" //") - --func (A) F() {} //@renameerr("F", "G", errAfToG) +- continue F //@complete(" //") - ---- b/b.go -- --package b +-Foo5: //@item(label5, "Foo5", "label", "const") +- for { +- break F //@complete(" //", label5) +- } - --import "example.com/a" --import "example.com/c" +- return +-} +diff -urN a/gopls/internal/test/marker/testdata/completion/lit.txt b/gopls/internal/test/marker/testdata/completion/lit.txt +--- a/gopls/internal/test/marker/testdata/completion/lit.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/lit.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,49 +0,0 @@ - --type B interface { F() } //@rename("F", "G", BfToG) +--- flags -- +--ignore_extra_diags - --var _ B = a.A(0) --var _ B = c.C(0) +--- go.mod -- +-module mod.test - ---- c/c.go -- --package c +-go 1.18 - --type C int +--- foo/foo.go -- +-package foo - --func (C) F() {} //@renameerr("F", "G", errCfToG) +-type StructFoo struct{ F int } - ---- d/d.go -- --package d +--- a.go -- +-package a - --import "example.com/b" +-import "mod.test/foo" - --var _ = b.B.F +-func _() { +- StructFoo{} //@item(litStructFoo, "StructFoo{}", "struct{...}", "struct") - ---- @errAfToG -- --a/a.go:5:10: renaming this method "F" to "G" --b/b.go:6:6: would make example.com/a.A no longer assignable to interface B --b/b.go:6:20: (rename example.com/b.B.F if you intend to change both types) ---- @BfToG/b/b.go -- --package b +- var sfp *foo.StructFoo +- // Don't insert the "&" before "StructFoo{}". +- sfp = foo.Str //@snippet(" //", litStructFoo, "StructFoo{$0\\}") - --import "example.com/a" --import "example.com/c" +- var sf foo.StructFoo +- sf = foo.Str //@snippet(" //", litStructFoo, "StructFoo{$0\\}") +- sf = foo. //@snippet(" //", litStructFoo, "StructFoo{$0\\}") +-} - --type B interface { G() } //@rename("F", "G", BfToG) +--- http.go -- +-package a - --var _ B = a.A(0) --var _ B = c.C(0) +-import ( +- "net/http" +- "sort" +-) - ---- @BfToG/d/d.go -- --package d +-func _() { +- sort.Slice(nil, fun) //@snippet(")", litFunc, "func(i, j int) bool {$0\\}") - --import "example.com/b" +- http.HandleFunc("", f) //@snippet(")", litFunc, "func(w http.ResponseWriter, r *http.Request) {$0\\}") - --var _ = b.B.G +- //@item(litFunc, "func(...) {}", "", "var") +- http.HandlerFunc() //@item(handlerFunc, "http.HandlerFunc()", "", "var") +- http.Handle("", http.HandlerFunc()) //@snippet("))", litFunc, "func(w http.ResponseWriter, r *http.Request) {$0\\}") +- http.Handle("", h) //@snippet(")", handlerFunc, "http.HandlerFunc($0)") +-} +diff -urN a/gopls/internal/test/marker/testdata/completion/maps.txt b/gopls/internal/test/marker/testdata/completion/maps.txt +--- a/gopls/internal/test/marker/testdata/completion/maps.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/maps.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,29 +0,0 @@ +-This test checks completion of map keys and values. - ---- @errCfToG -- --c/c.go:5:10: renaming this method "F" to "G" --b/b.go:6:6: would make example.com/c.C no longer assignable to interface B --b/b.go:6:20: (rename example.com/b.B.F if you intend to change both types) -diff -urN a/gopls/internal/regtest/marker/testdata/rename/prepare.txt b/gopls/internal/regtest/marker/testdata/rename/prepare.txt ---- a/gopls/internal/regtest/marker/testdata/rename/prepare.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/rename/prepare.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,62 +0,0 @@ --This test verifies the behavior of textDocument/prepareRename. +--- flags -- +--ignore_extra_diags - --- settings.json -- -{ -- "deepCompletion": false +- "completeUnimported": false -} - ---- go.mod -- --module golang.org/lsptests +--- maps.go -- +-package maps - --go 1.18 ---- types/types.go -- --package types +-func _() { +- var aVar int //@item(mapVar, "aVar", "int", "var") - --type CoolAlias = int //@item(CoolAlias, "CoolAlias", "int", "type") +- // not comparabale +- type aSlice []int //@item(mapSliceType, "aSlice", "[]int", "type") - --type X struct { //@item(X_struct, "X", "struct{...}", "struct") -- x int --} +- *aSlice //@item(mapSliceTypePtr, "*aSlice", "[]int", "type") - --type Y struct { //@item(Y_struct, "Y", "struct{...}", "struct") -- y int --} +- // comparable +- type aStruct struct{} //@item(mapStructType, "aStruct", "struct{...}", "struct") - +- map[]a{} //@complete("]", mapSliceType, mapStructType),snippet("]", mapSliceType, "*aSlice") - --type Bob interface { //@item(Bob_interface, "Bob", "interface{...}", "interface") -- Bobby() +- map[a]a{} //@complete("]", mapSliceType, mapStructType) +- map[a]a{} //@complete("{", mapSliceType, mapStructType) -} +diff -urN a/gopls/internal/test/marker/testdata/completion/multi_return.txt b/gopls/internal/test/marker/testdata/completion/multi_return.txt +--- a/gopls/internal/test/marker/testdata/completion/multi_return.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/multi_return.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,55 +0,0 @@ +-This test checks various ranking of completion results related to functions +-with multiple return values. - --func (*X) Bobby() {} --func (*Y) Bobby() {} -- ---- good/good0.go -- --package good +--- flags -- +--ignore_extra_diags - --func stuff() { //@item(good_stuff, "stuff", "func()", "func"),preparerename("stu", "stuff", "stuff") -- x := 5 -- random2(x) //@preparerename("dom", "random2", "random2") --} +--- multireturn.go -- +-package multireturn - ---- good/good1.go -- --package good +-func f0() {} //@item(multiF0, "f0", "func()", "func") - --import ( -- "golang.org/lsptests/types" //@item(types_import, "types", "\"golang.org/lsptests/types\"", "package") --) +-func f1(int) int { return 0 } //@item(multiF1, "f1", "func(int) int", "func") - --func random() int { //@item(good_random, "random", "func() int", "func") -- _ = "random() int" //@preparerename("random", "", "") -- y := 6 + 7 //@preparerename("7", "", "") -- return y //@preparerename("return", "","") --} +-func f2(int, int) (int, int) { return 0, 0 } //@item(multiF2, "f2", "func(int, int) (int, int)", "func") - --func random2(y int) int { //@item(good_random2, "random2", "func(y int) int", "func"),item(good_y_param, "y", "int", "var") -- //@complete("", good_y_param, types_import, good_random, good_random2, good_stuff) -- var b types.Bob = &types.X{} //@preparerename("ypes","types", "types") -- if _, ok := b.(*types.X); ok { //@complete("X", X_struct, Y_struct, Bob_interface, CoolAlias) -- _ = 0 // suppress "empty branch" diagnostic -- } +-func f2Str(string, string) (string, string) { return "", "" } //@item(multiF2Str, "f2Str", "func(string, string) (string, string)", "func") - -- return y --} -diff -urN a/gopls/internal/regtest/marker/testdata/rename/typeswitch.txt b/gopls/internal/regtest/marker/testdata/rename/typeswitch.txt ---- a/gopls/internal/regtest/marker/testdata/rename/typeswitch.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/rename/typeswitch.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,26 +0,0 @@ --This test covers the special case of renaming a type switch var. +-func f3(int, int, int) (int, int, int) { return 0, 0, 0 } //@item(multiF3, "f3", "func(int, int, int) (int, int, int)", "func") - ---- p.go -- --package p +-func _() { +- _ := f //@rank(" //", multiF1, multiF2) - --func _(x interface{}) { -- switch y := x.(type) { //@rename("y", "z", yToZ) -- case string: -- print(y) //@rename("y", "z", yToZ) -- default: -- print(y) //@rename("y", "z", yToZ) -- } --} +- _, _ := f //@rank(" //", multiF2, multiF0),rank(" //", multiF1, multiF0) - ---- @yToZ/p.go -- --package p +- _, _ := _, f //@rank(" //", multiF1, multiF2),rank(" //", multiF1, multiF0) - --func _(x interface{}) { -- switch z := x.(type) { //@rename("y", "z", yToZ) -- case string: -- print(z) //@rename("y", "z", yToZ) -- default: -- print(z) //@rename("y", "z", yToZ) -- } --} +- _, _ := f, abc //@rank(", abc", multiF1, multiF2) - -diff -urN a/gopls/internal/regtest/marker/testdata/rename/unexported.txt b/gopls/internal/regtest/marker/testdata/rename/unexported.txt ---- a/gopls/internal/regtest/marker/testdata/rename/unexported.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/rename/unexported.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,25 +0,0 @@ +- f1() //@rank(")", multiF1, multiF0) +- f1(f) //@rank(")", multiF1, multiF2) +- f2(f) //@rank(")", multiF2, multiF3),rank(")", multiF1, multiF3) +- f2(1, f) //@rank(")", multiF1, multiF2),rank(")", multiF1, multiF0) +- f2(1, ) //@rank(")", multiF1, multiF2),rank(")", multiF1, multiF0) +- f2Str() //@rank(")", multiF2Str, multiF2) - --This test attempts to rename a.S.X to x, which would make it --inaccessible from its external test package. The rename tool --should report an error rather than wrecking the program. --See issue #59403. +- var i int +- i, _ := f //@rank(" //", multiF2, multiF2Str) - ---- go.mod -- --module example.com --go 1.12 +- var s string +- _, s := f //@rank(" //", multiF2Str, multiF2) - ---- a/a.go -- --package a +- banana, s = f //@rank(" //", multiF2, multiF3) - --var S struct{ X int } //@renameerr("X", "x", oops) +- var variadic func(int, ...int) +- variadic() //@rank(")", multiF1, multiF0),rank(")", multiF2, multiF0),rank(")", multiF3, multiF0) +-} - ---- a/a_test.go -- --package a_test +-func _() { +- var baz func(...interface{}) - --import "example.com/a" +- var otterNap func() (int, int) //@item(multiTwo, "otterNap", "func() (int, int)", "var") +- var one int //@item(multiOne, "one", "int", "var") - --var Y = a.S.X +- baz(on) //@rank(")", multiOne, multiTwo) +-} +diff -urN a/gopls/internal/test/marker/testdata/completion/nested_complit.txt b/gopls/internal/test/marker/testdata/completion/nested_complit.txt +--- a/gopls/internal/test/marker/testdata/completion/nested_complit.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/nested_complit.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,26 +0,0 @@ +-This test checks completion of nested composite literals; - ---- @oops -- --a/a.go:3:15: renaming "X" to "x" would make it unexported --a/a_test.go:5:13: breaking references from packages such as "example.com/a_test" -diff -urN a/gopls/internal/regtest/marker/testdata/signature/generic.txt b/gopls/internal/regtest/marker/testdata/signature/generic.txt ---- a/gopls/internal/regtest/marker/testdata/signature/generic.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/signature/generic.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,21 +0,0 @@ --This test checks signature help on generic signatures. +-Parser recovery changed in Go 1.20, so this test requires at least that +-version for consistency. - ---- g.go -- --package g +--- flags -- +--ignore_extra_diags +--min_go=go1.20 - --type M[K comparable, V any] map[K]V +--- nested_complit.go -- +-package nested_complit - --// golang/go#61189: signatureHelp must handle pointer receivers. --func (m *M[K, V]) Get(k K) V { -- return (*m)[k] --} +-type ncFoo struct {} //@item(structNCFoo, "ncFoo", "struct{...}", "struct") - --func Get[K comparable, V any](m M[K, V], k K) V { -- return m[k] +-type ncBar struct { //@item(structNCBar, "ncBar", "struct{...}", "struct") +- baz []ncFoo -} - -func _() { -- var m M[int, string] -- _ = m.Get(0) //@signature("(", "Get(k int) string", 0) -- _ = Get(m, 0) //@signature("0", "Get(m M[int, string], k int) string", 1) +- _ = []ncFoo{} //@item(litNCFoo, "[]ncFoo{}", "", "var") +- _ = make([]ncFoo, 0) //@item(makeNCFoo, "make([]ncFoo, 0)", "", "func") +- +- _ := ncBar{ +- baz: [] //@complete(" //", litNCFoo, makeNCFoo, structNCBar, structNCFoo) +- } -} -diff -urN a/gopls/internal/regtest/marker/testdata/signature/signature.txt b/gopls/internal/regtest/marker/testdata/signature/signature.txt ---- a/gopls/internal/regtest/marker/testdata/signature/signature.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/signature/signature.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,206 +0,0 @@ --This test exercises basic tests for signature help. +diff -urN a/gopls/internal/test/marker/testdata/completion/postfix_placeholder.txt b/gopls/internal/test/marker/testdata/completion/postfix_placeholder.txt +--- a/gopls/internal/test/marker/testdata/completion/postfix_placeholder.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/postfix_placeholder.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,83 +0,0 @@ +-These tests check that postfix completions when enable usePlaceholders - --- flags -- --ignore_extra_diags - +--- settings.json -- +-{ +- "usePlaceholders": true +-} +- --- go.mod -- --module golang.org/lsptests +-module golang.org/lsptests/snippets - -go 1.18 - ---- signature/signature.go -- --// Package signature has tests for signature help. --package signature +--- postfix.go -- +-package snippets - -import ( -- "bytes" -- "encoding/json" -- "math/big" +- "strconv" -) - --func Foo(a string, b int) (c bool) { -- return --} +-func _() { +- /* for! */ //@item(postfixFor, "for!", "range over slice by index", "snippet") +- /* forr! */ //@item(postfixForr, "forr!", "range over slice by index and value", "snippet") +- /* range! */ //@item(postfixRange, "range!", "range over slice", "snippet") +- /* var! */ //@item(postfixVar, "var!", "assign to variable", "snippet") - --func Bar(float64, ...byte) { +- var foo []int +- +- foo.fo //@snippet(" //", postfixFor, "for ${1:i} := range foo {\n\t$0\n}") +- foo.forr //@snippet(" //", postfixForr, "for ${1:i}, ${2:v} := range foo {\n\t$0\n}") +- foo.rang //@snippet(" //", postfixRange, "for ${1:i}, ${2:v} := range foo {\n\t$0\n}") +- foo.va //@snippet(" //", postfixVar, "${1:i} := foo") -} - --type myStruct struct{} +-func _() { +- /* for! */ //@item(postfixForMap, "for!", "range over map by key", "snippet") +- /* forr! */ //@item(postfixForrMap, "forr!", "range over map by key and value", "snippet") +- /* range! */ //@item(postfixRangeMap, "range!", "range over map", "snippet") - --func (*myStruct) foo(e *json.Decoder) (*big.Int, error) { -- return nil, nil --} +- var foo map[int]int - --type MyType struct{} +- foo.fo //@snippet(" //", postfixFor, "for ${1:k} := range foo {\n\t$0\n}") +- foo.forr //@snippet(" //", postfixForr, "for ${1:k}, ${2:v} := range foo {\n\t$0\n}") +- foo.rang //@snippet(" //", postfixRange, "for ${1:k}, ${2:v} := range foo {\n\t$0\n}") +-} - --type MyFunc func(foo int) string +-func _() { +- /* for! */ //@item(postfixForChannel, "for!", "range over channel", "snippet") +- /* range! */ //@item(postfixRangeChannel, "range!", "range over channel", "snippet") - --type Alias = int --type OtherAlias = int --type StringAlias = string +- var foo chan int - --func AliasSlice(a []*Alias) (b Alias) { return 0 } --func AliasMap(a map[*Alias]StringAlias) (b, c map[*Alias]StringAlias) { return nil, nil } --func OtherAliasMap(a, b map[Alias]OtherAlias) map[Alias]OtherAlias { return nil } +- foo.fo //@snippet(" //", postfixForChannel, "for ${1:e} := range foo {\n\t$0\n}") +- foo.rang //@snippet(" //", postfixRangeChannel, "for ${1:e} := range foo {\n\t$0\n}") +-} - --func Qux() { -- Foo("foo", 123) //@signature("(", "Foo(a string, b int) (c bool)", 0) -- Foo("foo", 123) //@signature("123", "Foo(a string, b int) (c bool)", 1) -- Foo("foo", 123) //@signature(",", "Foo(a string, b int) (c bool)", 0) -- Foo("foo", 123) //@signature(" 1", "Foo(a string, b int) (c bool)", 1) -- Foo("foo", 123) //@signature(")", "Foo(a string, b int) (c bool)", 1) +-type T struct { +- Name string +-} - -- Bar(13.37, 0x13) //@signature("13.37", "Bar(float64, ...byte)", 0) -- Bar(13.37, 0x37) //@signature("0x37", "Bar(float64, ...byte)", 1) -- Bar(13.37, 1, 2, 3, 4) //@signature("4", "Bar(float64, ...byte)", 1) +-func _() (string, T, map[string]string, error) { +- /* iferr! */ //@item(postfixIfErr, "iferr!", "check error and return", "snippet") +- /* variferr! */ //@item(postfixVarIfErr, "variferr!", "assign variables and check error", "snippet") +- /* var! */ //@item(postfixVars, "var!", "assign to variables", "snippet") - -- fn := func(hi, there string) func(i int) rune { -- return func(int) rune { return 0 } -- } - -- fn("hi", "there") //@signature("hi", "", 0) -- fn("hi", "there") //@signature(",", "fn(hi string, there string) func(i int) rune", 0) -- fn("hi", "there")(1) //@signature("1", "func(i int) rune", 0) +- var err error +- err.iferr //@snippet(" //", postfixIfErr, "if err != nil {\n\treturn \"\", T{}, nil, ${1:err}\n}\n") +- strconv.Atoi("32").iferr //@snippet(" //", postfixIfErr, "if _, err := strconv.Atoi(\"32\"); err != nil {\n\treturn \"\", T{}, nil, ${1:err}\n}\n") +- strconv.Atoi("32").variferr //@snippet(" //", postfixVarIfErr, "${1:i}, ${2:err} := strconv.Atoi(\"32\")\nif ${2:err} != nil {\n\treturn \"\", T{}, nil, ${3:${2:err}}\n}\n") +- +- // test function return multiple errors +- var foo func() (error, error) +- foo().iferr //@snippet(" //", postfixIfErr, "if _, err := foo(); err != nil {\n\treturn \"\", T{}, nil, ${1:err}\n}\n") +- foo().variferr //@snippet(" //", postfixVarIfErr, "${1:err2}, ${2:err} := foo()\nif ${2:err} != nil {\n\treturn \"\", T{}, nil, ${3:${2:err}}\n}\n") +- +- // test function just return error +- var bar func() error +- bar().iferr //@snippet(" //", postfixIfErr, "if err := bar(); err != nil {\n\treturn \"\", T{}, nil, ${1:err}\n}\n") +- bar().variferr //@snippet(" //", postfixVarIfErr, "${1:err2} := bar()\nif ${1:err2} != nil {\n\treturn \"\", T{}, nil, ${2:${1:err2}}\n}\n") +-} +diff -urN a/gopls/internal/test/marker/testdata/completion/postfix.txt b/gopls/internal/test/marker/testdata/completion/postfix.txt +--- a/gopls/internal/test/marker/testdata/completion/postfix.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/postfix.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,131 +0,0 @@ +-These tests check that postfix completions do and do not show up in certain +-cases. Tests for the postfix completion contents are implemented as ad-hoc +-integration tests. - -- fnPtr := &fn -- (*fnPtr)("hi", "there") //@signature(",", "func(hi string, there string) func(i int) rune", 0) +--- flags -- +--ignore_extra_diags - -- var fnIntf interface{} = Foo -- fnIntf.(func(string, int) bool)("hi", 123) //@signature("123", "func(string, int) bool", 1) +--- go.mod -- +-module golang.org/lsptests/snippets - -- (&bytes.Buffer{}).Next(2) //@signature("2", "Next(n int) []byte", 0) +-go 1.18 - -- myFunc := MyFunc(func(n int) string { return "" }) -- myFunc(123) //@signature("123", "myFunc(foo int) string", 0) +--- postfix.go -- +-package snippets - -- var ms myStruct -- ms.foo(nil) //@signature("nil", "foo(e *json.Decoder) (*big.Int, error)", 0) +-import ( +- "strconv" +-) - -- _ = make([]int, 1, 2) //@signature("2", "make(t Type, size ...int) Type", 1) +-func _() { +- var foo []int +- foo.append //@rank(" //", postfixAppend) - -- Foo(myFunc(123), 456) //@signature("myFunc", "Foo(a string, b int) (c bool)", 0) -- Foo(myFunc(123), 456) //@signature("123", "myFunc(foo int) string", 0) +- []int{}.append //@complete(" //") - -- panic("oops!") //@signature(")", "panic(v any)", 0) -- println("hello", "world") //@signature(",", "println(args ...Type)", 0) +- []int{}.last //@complete(" //") - -- Hello(func() { -- //@signature("//", "", 0) -- }) - -- AliasSlice() //@signature(")", "AliasSlice(a []*Alias) (b Alias)", 0) -- AliasMap() //@signature(")", "AliasMap(a map[*Alias]StringAlias) (b map[*Alias]StringAlias, c map[*Alias]StringAlias)", 0) -- OtherAliasMap() //@signature(")", "OtherAliasMap(a map[Alias]OtherAlias, b map[Alias]OtherAlias) map[Alias]OtherAlias", 0) --} +- foo.copy //@rank(" //", postfixCopy) - --func Hello(func()) {} +- var s struct{ i []int } +- s.i.copy //@rank(" //", postfixCopy) - ---- signature/signature2.go -- --package signature +- var _ []int = s.i.copy //@complete(" //") - --func _() { -- Foo(//@signature("//", "Foo(a string, b int) (c bool)", 0) +- var blah func() []int +- blah().append //@complete(" //") -} - ---- signature/signature3.go -- --package signature -- -func _() { -- Foo("hello",//@signature("//", "Foo(a string, b int) (c bool)", 1) +- /* append! */ //@item(postfixAppend, "append!", "append and re-assign slice", "snippet") +- /* copy! */ //@item(postfixCopy, "copy!", "duplicate slice", "snippet") +- /* for! */ //@item(postfixFor, "for!", "range over slice by index", "snippet") +- /* forr! */ //@item(postfixForr, "forr!", "range over slice by index and value", "snippet") +- /* last! */ //@item(postfixLast, "last!", "s[len(s)-1]", "snippet") +- /* len! */ //@item(postfixLen, "len!", "len(s)", "snippet") +- /* print! */ //@item(postfixPrint, "print!", "print to stdout", "snippet") +- /* range! */ //@item(postfixRange, "range!", "range over slice", "snippet") +- /* reverse! */ //@item(postfixReverse, "reverse!", "reverse slice", "snippet") +- /* sort! */ //@item(postfixSort, "sort!", "sort.Slice()", "snippet") +- /* var! */ //@item(postfixVar, "var!", "assign to variable", "snippet") +- /* ifnotnil! */ //@item(postfixIfNotNil, "ifnotnil!", "if expr != nil", "snippet") +- +- var foo []int +- foo. //@complete(" //", postfixAppend, postfixCopy, postfixFor, postfixForr, postfixIfNotNil, postfixLast, postfixLen, postfixPrint, postfixRange, postfixReverse, postfixSort, postfixVar) +- foo = nil +- +- foo.append //@snippet(" //", postfixAppend, "foo = append(foo, $0)") +- foo.copy //snippet(" //", postfixCopy, "fooCopy := make([]int, len(foo))\ncopy($fooCopy, foo)\n") +- foo.fo //@snippet(" //", postfixFor, "for ${1:} := range foo {\n\t$0\n}") +- foo.forr //@snippet(" //", postfixForr, "for ${1:}, ${2:} := range foo {\n\t$0\n}") +- foo.last //@snippet(" //", postfixLast, "foo[len(foo)-1]") +- foo.len //@snippet(" //", postfixLen, "len(foo)") +- foo.print //@snippet(" //", postfixPrint, `fmt.Printf("foo: %v\n", foo)`) +- foo.rang //@snippet(" //", postfixRange, "for ${1:}, ${2:} := range foo {\n\t$0\n}") +- foo.reverse //@snippet(" //", postfixReverse, "slices.Reverse(foo)") +- foo.sort //@snippet(" //", postfixSort, "sort.Slice(foo, func(i, j int) bool {\n\t$0\n})") +- foo.va //@snippet(" //", postfixVar, "${1:} := foo") +- foo.ifnotnil //@snippet(" //", postfixIfNotNil, "if foo != nil {\n\t$0\n}") -} - ---- signature/signature_test.go -- --package signature_test +-func _() { +- /* for! */ //@item(postfixForMap, "for!", "range over map by key", "snippet") +- /* forr! */ //@item(postfixForrMap, "forr!", "range over map by key and value", "snippet") +- /* range! */ //@item(postfixRangeMap, "range!", "range over map", "snippet") +- /* clear! */ //@item(postfixClear, "clear!", "clear map contents", "snippet") +- /* keys! */ //@item(postfixKeys, "keys!", "create slice of keys", "snippet") - --import ( -- "testing" +- var foo map[int]int +- foo. //@complete(" //", postfixClear, postfixForMap, postfixForrMap, postfixIfNotNil, postfixKeys, postfixLen, postfixPrint, postfixRangeMap, postfixVar) - -- sig "golang.org/lsptests/signature" --) +- foo = nil - --func TestSignature(t *testing.T) { -- sig.AliasSlice() //@signature(")", "AliasSlice(a []*sig.Alias) (b sig.Alias)", 0) -- sig.AliasMap() //@signature(")", "AliasMap(a map[*sig.Alias]sig.StringAlias) (b map[*sig.Alias]sig.StringAlias, c map[*sig.Alias]sig.StringAlias)", 0) -- sig.OtherAliasMap() //@signature(")", "OtherAliasMap(a map[sig.Alias]sig.OtherAlias, b map[sig.Alias]sig.OtherAlias) map[sig.Alias]sig.OtherAlias", 0) +- foo.fo //@snippet(" //", postfixFor, "for ${1:} := range foo {\n\t$0\n}") +- foo.forr //@snippet(" //", postfixForr, "for ${1:}, ${2:} := range foo {\n\t$0\n}") +- foo.rang //@snippet(" //", postfixRange, "for ${1:}, ${2:} := range foo {\n\t$0\n}") +- foo.clear //@snippet(" //", postfixClear, "for k := range foo {\n\tdelete(foo, k)\n}\n") +- foo.keys //@snippet(" //", postfixKeys, "keys := make([]int, 0, len(foo))\nfor k := range foo {\n\tkeys = append(keys, k)\n}\n") -} - ---- snippets/snippets.go -- --package snippets -- --import ( -- "golang.org/lsptests/signature" --) +-func _() { +- /* for! */ //@item(postfixForChannel, "for!", "range over channel", "snippet") +- /* range! */ //@item(postfixRangeChannel, "range!", "range over channel", "snippet") - --type CoolAlias = int //@item(CoolAlias, "CoolAlias", "int", "type") +- var foo chan int +- foo. //@complete(" //", postfixForChannel, postfixIfNotNil, postfixLen, postfixPrint, postfixRangeChannel, postfixVar) - --type structy struct { -- x signature.MyType --} +- foo = nil - --func X(_ map[signature.Alias]CoolAlias) (map[signature.Alias]CoolAlias) { -- return nil +- foo.fo //@snippet(" //", postfixForChannel, "for ${1:} := range foo {\n\t$0\n}") +- foo.rang //@snippet(" //", postfixRangeChannel, "for ${1:} := range foo {\n\t$0\n}") -} - --func _() { -- X() //@signature(")", "X(_ map[signature.Alias]CoolAlias) map[signature.Alias]CoolAlias", 0) -- _ = signature.MyType{} //@item(literalMyType, "signature.MyType{}", "", "var") -- s := structy{ -- x: //@snippet(" //", literalMyType, "signature.MyType{\\}") -- } +-type T struct { +- Name string -} - ---- importedcomplit/importedcomplit.go -- --package importedcomplit -- --import ( -- // TODO(rfindley): re-enable after moving to new framework -- // "golang.org/lsptests/foo" +-func _() (string, T, map[string]string, error) { +- /* iferr! */ //@item(postfixIfErr, "iferr!", "check error and return", "snippet") +- /* variferr! */ //@item(postfixVarIfErr, "variferr!", "assign variables and check error", "snippet") +- /* var! */ //@item(postfixVars, "var!", "assign to variables", "snippet") - -- // import completions (separate blocks to avoid comment alignment) -- "crypto/elli" //@complete("\" //", cryptoImport) +- strconv.Atoi("32"). //@complete(" //", postfixIfErr, postfixPrint, postfixVars, postfixVarIfErr) - -- "fm" //@complete("\" //", fmtImport) +- var err error +- err.iferr //@snippet(" //", postfixIfErr, "if err != nil {\n\treturn \"\", T{}, nil, ${1:}\n}\n") - -- "go/pars" //@complete("\" //", parserImport) +- strconv.Atoi("32").iferr //@snippet(" //", postfixIfErr, "if _, err := strconv.Atoi(\"32\"); err != nil {\n\treturn \"\", T{}, nil, ${1:}\n}\n") - -- namedParser "go/pars" //@complete("\" //", parserImport) +- strconv.Atoi("32").variferr //@snippet(" //", postfixVarIfErr, "${1:}, ${2:} := strconv.Atoi(\"32\")\nif ${2:} != nil {\n\treturn \"\", T{}, nil, ${3:}\n}\n") - -- "golang.org/lspte" //@complete("\" //", lsptestsImport) +- // test function return multiple errors +- var foo func() (error, error) +- foo().iferr //@snippet(" //", postfixIfErr, "if _, err := foo(); err != nil {\n\treturn \"\", T{}, nil, ${1:}\n}\n") +- foo().variferr //@snippet(" //", postfixVarIfErr, "${1:}, ${2:} := foo()\nif ${2:} != nil {\n\treturn \"\", T{}, nil, ${3:}\n}\n") - -- "golang.org/lsptests/sign" //@complete("\" //", signatureImport) +- // test function just return error +- var bar func() error +- bar().iferr //@snippet(" //", postfixIfErr, "if err := bar(); err != nil {\n\treturn \"\", T{}, nil, ${1:}\n}\n") +- bar().variferr //@snippet(" //", postfixVarIfErr, "${1:} := bar()\nif ${1:} != nil {\n\treturn \"\", T{}, nil, ${2:}\n}\n") +-} +diff -urN a/gopls/internal/test/marker/testdata/completion/printf.txt b/gopls/internal/test/marker/testdata/completion/printf.txt +--- a/gopls/internal/test/marker/testdata/completion/printf.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/printf.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,39 +0,0 @@ +-This test checks various ranking of completion results related to printf. - -- "golang.org/lsptests/sign" //@complete("ests", lsptestsImport) +--- flags -- +--ignore_extra_diags - -- "golang.org/lsptests/signa" //@complete("na\" //", signatureImport) --) +--- printf.go -- +-package printf - --func _() { -- var V int //@item(icVVar, "V", "int", "var") +-import "fmt" - -- // TODO(rfindley): re-enable after moving to new framework -- // _ = foo.StructFoo{V} // complete("}", Value, icVVar) --} +-func myPrintf(string, ...interface{}) {} - -func _() { - var ( -- aa string //@item(icAAVar, "aa", "string", "var") -- ab int //@item(icABVar, "ab", "int", "var") +- aInt int //@item(printfInt, "aInt", "int", "var") +- aFloat float64 //@item(printfFloat, "aFloat", "float64", "var") +- aString string //@item(printfString, "aString", "string", "var") +- aBytes []byte //@item(printfBytes, "aBytes", "[]byte", "var") +- aStringer fmt.Stringer //@item(printfStringer, "aStringer", "fmt.Stringer", "var") +- aError error //@item(printfError, "aError", "error", "var") +- aBool bool //@item(printfBool, "aBool", "bool", "var") - ) - -- // TODO(rfindley): re-enable after moving to new framework -- // _ = foo.StructFoo{a} // complete("}", abVar, aaVar) +- myPrintf("%d", a) //@rank(")", printfInt, printfFloat) +- myPrintf("%s", a) //@rank(")", printfString, printfInt),rank(")", printfBytes, printfInt),rank(")", printfStringer, printfInt),rank(")", printfError, printfInt) +- myPrintf("%w", a) //@rank(")", printfError, printfInt) +- myPrintf("%x %[1]b", a) //@rank(")", printfInt, printfString) - -- var s struct { -- AA string //@item(icFieldAA, "AA", "string", "field") -- AB int //@item(icFieldAB, "AB", "int", "field") -- } +- fmt.Printf("%t", a) //@rank(")", printfBool, printfInt) - -- // TODO(rfindley): re-enable after moving to new framework -- //_ = foo.StructFoo{s.} // complete("}", icFieldAB, icFieldAA) +- fmt.Fprintf(nil, "%f", a) //@rank(")", printfFloat, printfInt) +- +- fmt.Sprintf("%[2]q %[1]*.[3]*[4]f", +- a, //@rank(",", printfInt, printfFloat) +- a, //@rank(",", printfString, printfFloat) +- a, //@rank(",", printfInt, printfFloat) +- a, //@rank(",", printfFloat, printfInt) +- ) -} +diff -urN a/gopls/internal/test/marker/testdata/completion/rank.txt b/gopls/internal/test/marker/testdata/completion/rank.txt +--- a/gopls/internal/test/marker/testdata/completion/rank.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/rank.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,212 +0,0 @@ +-This test checks various ranking of completion results. - --/* "fmt" */ //@item(fmtImport, "fmt", "\"fmt\"", "package") --/* "go/parser" */ //@item(parserImport, "parser", "\"go/parser\"", "package") --/* "golang.org/lsptests/signature" */ //@item(signatureImport, "signature", "\"golang.org/lsptests/signature\"", "package") --/* "golang.org/lsptests/" */ //@item(lsptestsImport, "lsptests/", "\"golang.org/lsptests/\"", "package") --/* "crypto/elliptic" */ //@item(cryptoImport, "elliptic", "\"crypto/elliptic\"", "package") -diff -urN a/gopls/internal/regtest/marker/testdata/stubmethods/basic.txt b/gopls/internal/regtest/marker/testdata/stubmethods/basic.txt ---- a/gopls/internal/regtest/marker/testdata/stubmethods/basic.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/stubmethods/basic.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,23 +0,0 @@ --This test exercises basic 'stub methods' functionality. +--- flags -- +--ignore_extra_diags +- +--- settings.json -- +-{ +- "completeUnimported": false, +- "deepCompletion": false +-} - --- go.mod -- --module example.com --go 1.12 +-module golang.org/lsptests/rank - ---- a/a.go -- --package a +-go 1.18 - --type C int +--- struct/struct_rank.go -- +-package struct_rank - --var _ error = C(0) //@suggestedfix(re"C.0.", re"missing method Error", stub) ---- @stub/a/a.go -- ----- before --+++ after --@@ -3 +3,6 @@ ---type C int --+type C int --+ --+// Error implements error. --+func (C) Error() string { --+ panic("unimplemented") --+} -diff -urN a/gopls/internal/regtest/marker/testdata/stubmethods/issue61693.txt b/gopls/internal/regtest/marker/testdata/stubmethods/issue61693.txt ---- a/gopls/internal/regtest/marker/testdata/stubmethods/issue61693.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/stubmethods/issue61693.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,30 +0,0 @@ --This test exercises stub methods functionality with variadic parameters. +-type foo struct { +- c int //@item(c_rank, "c", "int", "field") +- b int //@item(b_rank, "b", "int", "field") +- a int //@item(a_rank, "a", "int", "field") +-} - --In golang/go#61693 stubmethods was panicking in this case. +-func f() { +- foo := foo{} //@rank("}", c_rank, b_rank, a_rank) +-} - ---- go.mod -- --module mod.com +--- assign_rank.go -- +-package rank - --go 1.18 ---- main.go -- --package main +-// Literal completion results. +-/* int() */ //@item(int, "int()", "int", "var") +-/* string() */ //@item(string, "string()", "string", "var") - --type C int +-var ( +- apple int = 3 //@item(apple, "apple", "int", "var") +- pear string = "hello" //@item(pear, "pear", "string", "var") +-) - --func F(err ...error) {} +-func _() { +- orange := 1 //@item(orange, "orange", "int", "var") +- grape := "hello" //@item(grape, "grape", "string", "var") +- orange, grape = 2, "hello" //@complete(" \"", grape, pear, string, orange, apple) +-} - -func _() { -- var x error -- F(x, C(0)) //@suggestedfix(re"C.0.", re"missing method Error", stub) +- var pineapple int //@item(pineapple, "pineapple", "int", "var") +- pineapple = 1 //@complete(" 1", pineapple, apple, int, pear) +- +- y := //@complete(" /", pineapple, apple, pear) -} ---- @stub/main.go -- ----- before --+++ after --@@ -3 +3,6 @@ ---type C int --+type C int --+ --+// Error implements error. --+func (C) Error() string { --+ panic("unimplemented") --+} -diff -urN a/gopls/internal/regtest/marker/testdata/stubmethods/issue61830.txt b/gopls/internal/regtest/marker/testdata/stubmethods/issue61830.txt ---- a/gopls/internal/regtest/marker/testdata/stubmethods/issue61830.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/stubmethods/issue61830.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,28 +0,0 @@ --This test verifies that method stubbing qualifies types relative to the current --package. - ---- p.go -- --package p +--- binexpr_rank.go -- +-package rank - --import "io" +-func _() { +- _ = 5 + ; //@complete(" ;", apple, pear) +- y := + 5; //@complete(" +", apple, pear) - --type B struct{} +- if 6 == {} //@complete(" {", apple, pear) +-} - --type I interface { -- M(io.Reader, B) +--- boolexpr_rank.go -- +-package rank +- +-func _() { +- someRandomBoolFunc := func() bool { //@item(boolExprFunc, "someRandomBoolFunc", "func() bool", "var") +- return true +- } +- +- var foo, bar int //@item(boolExprBar, "bar", "int", "var") +- if foo == 123 && b { //@rank(" {", boolExprBar, boolExprFunc) +- } -} - --type A struct{} +--- convert_rank.go -- +-package rank - --var _ I = &A{} //@suggestedfix(re"&A..", re"missing method M", stub) ---- @stub/p.go -- ----- before --+++ after --@@ -11 +11,6 @@ ---type A struct{} --+type A struct{} --+ --+// M implements I. --+func (*A) M(io.Reader, B) { --+ panic("unimplemented") --+} -diff -urN a/gopls/internal/regtest/marker/testdata/suggestedfix/self_assignment.txt b/gopls/internal/regtest/marker/testdata/suggestedfix/self_assignment.txt ---- a/gopls/internal/regtest/marker/testdata/suggestedfix/self_assignment.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/suggestedfix/self_assignment.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,21 +0,0 @@ --Test of the suggested fix to remove unnecessary assignments. +-import "time" - ---- a.go -- --package suggestedfix +-// Copied from the old builtins.go, which has been ported to the new marker tests. +-/* complex(r float64, i float64) */ //@item(complex, "complex", "func(r float64, i float64) complex128", "func") - --import ( -- "log" --) +-func _() { +- type strList []string +- wantsStrList := func(strList) {} - --func goodbye() { -- s := "hiiiiiii" -- s = s //@suggestedfix("s = s", re"self-assignment", fix) -- log.Print(s) +- var ( +- convA string //@item(convertA, "convA", "string", "var") +- convB []string //@item(convertB, "convB", "[]string", "var") +- ) +- wantsStrList(strList(conv)) //@complete("))", convertB, convertA) -} - ---- @fix/a.go -- ----- before --+++ after --@@ -9 +9 @@ --- s = s //@suggestedfix("s = s", re"self-assignment", fix) --+ //@suggestedfix("s = s", re"self-assignment", fix) -diff -urN a/gopls/internal/regtest/marker/testdata/suggestedfix/undeclared.txt b/gopls/internal/regtest/marker/testdata/suggestedfix/undeclared.txt ---- a/gopls/internal/regtest/marker/testdata/suggestedfix/undeclared.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/suggestedfix/undeclared.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,54 +0,0 @@ --Tests of suggested fixes for "undeclared name" diagnostics, --which are of ("compiler", "error") type. +-func _() { +- type myInt int - ---- go.mod -- --module example.com --go 1.12 +- const ( +- convC = "hi" //@item(convertC, "convC", "string", "const") +- convD = 123 //@item(convertD, "convD", "int", "const") +- convE int = 123 //@item(convertE, "convE", "int", "const") +- convF string = "there" //@item(convertF, "convF", "string", "const") +- convG myInt = 123 //@item(convertG, "convG", "myInt", "const") +- ) - ---- a.go -- --package p +- var foo int +- foo = conv //@rank(" //", convertE, convertD) - --func a() { -- z, _ := 1+y, 11 //@suggestedfix("y", re"(undeclared name|undefined): y", a) -- _ = z --} +- var mi myInt +- mi = conv //@rank(" //", convertG, convertD, convertE) +- mi + conv //@rank(" //", convertG, convertD, convertE) - ---- @a/a.go -- ----- before --+++ after --@@ -3 +3,2 @@ ---func a() { --+func a() { --+ y := ---- b.go -- --package p +- 1 + conv //@rank(" //", convertD, convertC),rank(" //", convertE, convertC),rank(" //", convertG, convertC) - --func b() { -- if 100 < 90 { -- } else if 100 > n+2 { //@suggestedfix("n", re"(undeclared name|undefined): n", b) -- } --} +- type myString string +- var ms myString +- ms = conv //@rank(" //", convertC, convertF) - ---- @b/b.go -- ----- before --+++ after --@@ -3 +3,2 @@ ---func b() { --+func b() { --+ n := ---- c.go -- --package p +- type myUint uint32 +- var mu myUint +- mu = conv //@rank(" //", convertD, convertE) - --func c() { -- for i < 200 { //@suggestedfix("i", re"(undeclared name|undefined): i", c) -- } -- r() //@diag("r", re"(undeclared name|undefined): r") --} +- // don't downrank constants when assigning to interface{} +- var _ interface{} = c //@rank(" //", convertD, complex) - ---- @c/c.go -- ----- before --+++ after --@@ -3 +3,2 @@ ---func c() { --+func c() { --+ i := -diff -urN a/gopls/internal/regtest/marker/testdata/suggestedfix/unusedrequire_gowork.txt b/gopls/internal/regtest/marker/testdata/suggestedfix/unusedrequire_gowork.txt ---- a/gopls/internal/regtest/marker/testdata/suggestedfix/unusedrequire_gowork.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/suggestedfix/unusedrequire_gowork.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,55 +0,0 @@ --This test checks the suggested fix to remove unused require statements from --go.mod files, when a go.work file is used. +- var _ time.Duration = conv //@rank(" //", convertD, convertE),snippet(" //", convertE, "time.Duration(convE)") - --Note that unlike unusedrequire.txt, we need not write go.sum files when --a go.work file is used. +- var convP myInt //@item(convertP, "convP", "myInt", "var") +- var _ *int = conv //@snippet(" //", convertP, "(*int)(&convP)") - ---- flags -- ---min_go=go1.18 +- var ff float64 //@item(convertFloat, "ff", "float64", "var") +- f == convD //@snippet(" =", convertFloat, "ff") +-} - ---- proxy/example.com@v1.0.0/x.go -- --package pkg --const X = 1 +--- switch_rank.go -- +-package rank - ---- go.work -- --go 1.21 +-import "time" - --use ( -- ./a -- ./b --) ---- a/go.mod -- --module mod.com/a +-func _() { +- switch pear { +- case _: //@rank("_", pear, apple) +- } - --go 1.14 +- time.Monday //@item(timeMonday, "time.Monday", "time.Weekday", "const"),item(monday ,"Monday", "time.Weekday", "const") +- time.Friday //@item(timeFriday, "time.Friday", "time.Weekday", "const"),item(friday ,"Friday", "time.Weekday", "const") - --require example.com v1.0.0 //@suggestedfix("require", re"not used", a) +- now := time.Now() +- now.Weekday //@item(nowWeekday, "now.Weekday", "func() time.Weekday", "method") - ---- @a/a/go.mod -- ----- before --+++ after --@@ -4,3 +4 @@ --- ---require example.com v1.0.0 //@suggestedfix("require", re"not used", a) --- ---- a/main.go -- --package main --func main() {} +- then := time.Now() +- then.Weekday //@item(thenWeekday, "then.Weekday", "func() time.Weekday", "method") - ---- b/go.mod -- --module mod.com/b +- switch time.Weekday(0) { +- case time.Monday, time.Tuesday: +- case time.Wednesday, time.Thursday: +- case time.Saturday, time.Sunday: +- // TODO: these tests were disabled because they require deep completion +- // (which would break other tests) +- case t: // rank(":", timeFriday, timeMonday) +- case time.: //@rank(":", friday, monday) - --go 1.14 +- case now.Weekday(): +- case week: // rank(":", thenWeekday, nowWeekday) +- } +-} - --require example.com v1.0.0 //@suggestedfix("require", re"not used", b) +--- type_assert_rank.go -- +-package rank - ---- @b/b/go.mod -- ----- before --+++ after --@@ -4,3 +4 @@ --- ---require example.com v1.0.0 //@suggestedfix("require", re"not used", b) --- ---- b/main.go -- --package main --func main() {} -diff -urN a/gopls/internal/regtest/marker/testdata/suggestedfix/unusedrequire.txt b/gopls/internal/regtest/marker/testdata/suggestedfix/unusedrequire.txt ---- a/gopls/internal/regtest/marker/testdata/suggestedfix/unusedrequire.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/suggestedfix/unusedrequire.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,27 +0,0 @@ --This test checks the suggested fix to remove unused require statements from --go.mod files. +-func _() { +- type flower int //@item(flower, "flower", "int", "type") +- var fig string //@item(fig, "fig", "string", "var") - ---- flags -- ---write_sumfile=a +- _ = interface{}(nil).(f) //@complete(") //", flower) +-} - ---- proxy/example.com@v1.0.0/x.go -- --package pkg --const X = 1 +--- type_switch_rank.go -- +-package rank - ---- a/go.mod -- --module mod.com +-import ( +- "fmt" +- "go/ast" +-) - --go 1.14 +-func _() { +- type basket int //@item(basket, "basket", "int", "type") +- var banana string //@item(banana, "banana", "string", "var") - --require example.com v1.0.0 //@suggestedfix("require", re"not used", a) +- switch interface{}(pear).(type) { +- case b: //@complete(":", basket) +- b //@complete(" //", banana, basket) +- } - ---- @a/a/go.mod -- ----- before --+++ after --@@ -4,3 +4 @@ --- ---require example.com v1.0.0 //@suggestedfix("require", re"not used", a) --- ---- a/main.go -- --package main --func main() {} -diff -urN a/gopls/internal/regtest/marker/testdata/symbol/basic.txt b/gopls/internal/regtest/marker/testdata/symbol/basic.txt ---- a/gopls/internal/regtest/marker/testdata/symbol/basic.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/symbol/basic.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,114 +0,0 @@ --Basic tests of textDocument/documentSymbols. +- Ident //@item(astIdent, "Ident", "struct{...}", "struct") +- IfStmt //@item(astIfStmt, "IfStmt", "struct{...}", "struct") - ---- symbol.go -- --package main +- switch ast.Node(nil).(type) { +- case *ast.Ident: +- case *ast.I: //@rank(":", astIfStmt, astIdent) +- } - --//@symbol(want) +- Stringer //@item(fmtStringer, "Stringer", "interface{...}", "interface") +- GoStringer //@item(fmtGoStringer, "GoStringer", "interface{...}", "interface") - --import "io" +- switch interface{}(nil).(type) { +- case fmt.Stringer: //@rank(":", fmtStringer, fmtGoStringer) +- } +-} - --var _ = 1 +diff -urN a/gopls/internal/test/marker/testdata/completion/snippet_placeholder.txt b/gopls/internal/test/marker/testdata/completion/snippet_placeholder.txt +--- a/gopls/internal/test/marker/testdata/completion/snippet_placeholder.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/snippet_placeholder.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,83 +0,0 @@ +-This test checks basic completion snippet support, using placeholders. - --var x = 42 +-Unlike the old marker tests, the new marker tests assume static configuration +-(as defined by settings.json), and therefore there is duplication between this +-test and snippet.txt. This is a price we pay so that we don't have to mutate +-the server during testing. - --var nested struct { -- nestedField struct { -- f int -- } +--- flags -- +--ignore_extra_diags +- +--- settings.json -- +-{ +- "usePlaceholders": true -} - --const y = 43 +--- go.mod -- +-module golang.org/lsptests/snippet - --type Number int +--- snippet.go -- +-package snippets - --type Alias = string +-// Pre-set this marker, as we don't have a "source" for it in this package. +-/* Error() */ //@item(Error, "Error", "func() string", "method") - --type NumberAlias = Number +-type AliasType = int //@item(sigAliasType, "AliasType", "AliasType", "type") - --type ( -- Boolean bool -- BoolAlias = bool --) +-func foo(i int, b bool) {} //@item(snipFoo, "foo", "func(i int, b bool)", "func") +-func bar(fn func()) func() {} //@item(snipBar, "bar", "func(fn func())", "func") +-func baz(at AliasType, b bool) {} //@item(snipBaz, "baz", "func(at AliasType, b bool)", "func") - -type Foo struct { -- Quux -- W io.Writer -- Bar int -- baz string -- funcField func(int) int +- Bar int //@item(snipFieldBar, "Bar", "int", "field") +- Func func(at AliasType) error //@item(snipFieldFunc, "Func", "func(at AliasType) error", "field") -} - --type Quux struct { -- X, Y float64 --} +-func (Foo) Baz() func() {} //@item(snipMethodBaz, "Baz", "func() func()", "method") +-func (Foo) BazBar() func() {} //@item(snipMethodBazBar, "BazBar", "func() func()", "method") +-func (Foo) BazBaz(at AliasType) func() {} //@item(snipMethodBazBaz, "BazBaz", "func(at AliasType) func()", "method") - --type EmptyStruct struct{} +-func _() { +- f //@snippet(" //", snipFoo, "foo(${1:i int}, ${2:b bool})") - --func (f Foo) Baz() string { -- return f.baz --} +- bar //@snippet(" //", snipBar, "bar(${1:fn func()})") - --func _() {} +- baz //@snippet(" //", snipBaz, "baz(${1:at AliasType}, ${2:b bool})") +- baz() //@signature("(", "baz(at AliasType, b bool)", 0) - --func (q *Quux) Do() {} +- bar(nil) //@snippet("(", snipBar, "bar") +- bar(ba) //@snippet(")", snipBar, "bar(${1:fn func()})") +- var f Foo +- bar(f.Ba) //@snippet(")", snipMethodBaz, "Baz()") +- (bar)(nil) //@snippet(")", snipBar, "bar(${1:fn func()})") +- (f.Ba)() //@snippet(")", snipMethodBaz, "Baz()") - --func main() { --} +- Foo{ +- B //@snippet(" //", snipFieldBar, "Bar: ${1:int},") +- } - --type Stringer interface { -- String() string --} +- Foo{ +- F //@snippet(" //", snipFieldFunc, "Func: ${1:func(at AliasType) error},") +- } - --type ABer interface { -- B() -- A() string --} +- Foo{B} //@snippet("}", snipFieldBar, "Bar: ${1:int}") +- Foo{} //@snippet("}", snipFieldBar, "Bar: ${1:int}") - --type WithEmbeddeds interface { -- Do() -- ABer -- io.Writer --} +- Foo{Foo{}.B} //@snippet("} ", snipFieldBar, "Bar") - --type EmptyInterface interface{} +- var err error +- err.Error() //@snippet("E", Error, "Error()") +- f.Baz() //@snippet("B", snipMethodBaz, "Baz()") - --func Dunk() int { return 0 } +- f.Baz() //@snippet("(", snipMethodBazBar, "BazBar") - --func dunk() {} +- f.Baz() //@snippet("B", snipMethodBazBaz, "BazBaz(${1:at AliasType})") +-} - ---- @want -- --(*Quux).Do "func()" --(Foo).Baz "func() string" +2 lines --ABer "interface{...}" +3 lines --ABer.A "func() string" --ABer.B "func()" --Alias "string" --BoolAlias "bool" --Boolean "bool" --Dunk "func() int" --EmptyInterface "interface{}" --EmptyStruct "struct{}" --Foo "struct{...}" +6 lines --Foo.Bar "int" --Foo.Quux "Quux" --Foo.W "io.Writer" --Foo.baz "string" --Foo.funcField "func(int) int" --Number "int" --NumberAlias "Number" --Quux "struct{...}" +2 lines --Quux.X "float64" --Quux.Y "float64" --Stringer "interface{...}" +2 lines --Stringer.String "func() string" --WithEmbeddeds "interface{...}" +4 lines --WithEmbeddeds.ABer "ABer" --WithEmbeddeds.Do "func()" --WithEmbeddeds.Writer "io.Writer" --dunk "func()" --main "func()" +1 lines --nested "struct{...}" +4 lines --nested.nestedField "struct{...}" +2 lines --nested.nestedField.f "int" --x "" --y "" -diff -urN a/gopls/internal/regtest/marker/testdata/symbol/generic.txt b/gopls/internal/regtest/marker/testdata/symbol/generic.txt ---- a/gopls/internal/regtest/marker/testdata/symbol/generic.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/symbol/generic.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,29 +0,0 @@ --Basic tests of textDocument/documentSymbols with generics. +-func _() { +- type bar struct { +- a int +- b float64 //@item(snipBarB, "b", "field") +- } +- bar{b} //@snippet("}", snipBarB, "b: ${1:float64}") +-} +diff -urN a/gopls/internal/test/marker/testdata/completion/snippet.txt b/gopls/internal/test/marker/testdata/completion/snippet.txt +--- a/gopls/internal/test/marker/testdata/completion/snippet.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/snippet.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,77 +0,0 @@ +-This test checks basic completion snippet support. - --- flags -- ---min_go=go1.18 +--ignore_extra_diags - ---- symbol.go -- --//@symbol(want) +--- go.mod -- +-module golang.org/lsptests/snippet - --//go:build go1.18 --// +build go1.18 +--- snippet.go -- +-package snippets - --package main +-// Pre-set this marker, as we don't have a "source" for it in this package. +-// The comment is used to create a synthetic completion item. +-// +-// TODO(rfindley): allow completion markers to refer to ad-hoc items inline, +-// without this trick. +-/* Error() */ //@item(Error, "Error", "func() string", "method") - --type T[P any] struct { -- F P --} +-type AliasType = int //@item(sigAliasType, "AliasType", "AliasType", "type") - --type Constraint interface { -- ~int | struct{ int } -- interface{ M() } +-func foo(i int, b bool) {} //@item(snipFoo, "foo", "func(i int, b bool)", "func") +-func bar(fn func()) func() {} //@item(snipBar, "bar", "func(fn func())", "func") +-func baz(at AliasType, b bool) {} //@item(snipBaz, "baz", "func(at AliasType, b bool)", "func") +- +-type Foo struct { +- Bar int //@item(snipFieldBar, "Bar", "int", "field") +- Func func(at AliasType) error //@item(snipFieldFunc, "Func", "func(at AliasType) error", "field") -} - ---- @want -- --Constraint "interface{...}" +3 lines --Constraint.interface{...} "" --Constraint.interface{...}.M "func()" --Constraint.~int | struct{int} "" --T "struct{...}" +2 lines --T.F "P" -diff -urN a/gopls/internal/regtest/marker/testdata/typedef/typedef.txt b/gopls/internal/regtest/marker/testdata/typedef/typedef.txt ---- a/gopls/internal/regtest/marker/testdata/typedef/typedef.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/typedef/typedef.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,68 +0,0 @@ --This test exercises the textDocument/typeDefinition action. +-func (Foo) Baz() func() {} //@item(snipMethodBaz, "Baz", "func() func()", "method") +-func (Foo) BazBar() func() {} //@item(snipMethodBazBar, "BazBar", "func() func()", "method") +-func (Foo) BazBaz(at AliasType) func() {} //@item(snipMethodBazBaz, "BazBaz", "func(at AliasType) func()", "method") - ---- typedef.go -- --package typedef +-func _() { +- f //@snippet(" //", snipFoo, "foo(${1:})") - --type Struct struct { //@loc(Struct, "Struct"), -- Field string --} +- bar //@snippet(" //", snipBar, "bar(${1:})") - --type Int int //@loc(Int, "Int") +- baz //@snippet(" //", snipBaz, "baz(${1:})") +- baz() //@signature("(", "baz(at AliasType, b bool)", 0) - --func _() { -- var ( -- value Struct -- point *Struct -- ) -- _ = value //@typedef("value", Struct) -- _ = point //@typedef("point", Struct) +- bar(nil) //@snippet("(", snipBar, "bar") +- bar(ba) //@snippet(")", snipBar, "bar(${1:})") +- var f Foo +- bar(f.Ba) //@snippet(")", snipMethodBaz, "Baz()") +- (bar)(nil) //@snippet(")", snipBar, "bar(${1:})") +- (f.Ba)() //@snippet(")", snipMethodBaz, "Baz()") - -- var ( -- array [3]Struct -- slice []Struct -- ch chan Struct -- complex [3]chan *[5][]Int -- ) -- _ = array //@typedef("array", Struct) -- _ = slice //@typedef("slice", Struct) -- _ = ch //@typedef("ch", Struct) -- _ = complex //@typedef("complex", Int) +- Foo{ +- B //@snippet(" //", snipFieldBar, "Bar: ${1:},") +- } - -- var s struct { -- x struct { -- xx struct { -- field1 []Struct -- field2 []Int -- } -- } +- Foo{ +- F //@snippet(" //", snipFieldFunc, "Func: ${1:},") - } -- _ = s.x.xx.field1 //@typedef("field1", Struct) -- _ = s.x.xx.field2 //@typedef("field2", Int) --} - --func F1() Int { return 0 } --func F2() (Int, float64) { return 0, 0 } --func F3() (Struct, int, bool, error) { return Struct{}, 0, false, nil } --func F4() (**int, Int, bool, *error) { return nil, 0, false, nil } --func F5() (int, float64, error, Struct) { return 0, 0, nil, Struct{} } --func F6() (int, float64, ***Struct, error) { return 0, 0, nil, nil } +- Foo{B} //@snippet("}", snipFieldBar, "Bar: ${1:}") +- Foo{} //@snippet("}", snipFieldBar, "Bar: ${1:}") - --func _() { -- F1() //@typedef("F1", Int) -- F2() //@typedef("F2", Int) -- F3() //@typedef("F3", Struct) -- F4() //@typedef("F4", Int) -- F5() //@typedef("F5", Struct) -- F6() //@typedef("F6", Struct) +- Foo{Foo{}.B} //@snippet("} ", snipFieldBar, "Bar") - -- f := func() Int { return 0 } -- f() //@typedef("f", Int) +- var err error +- err.Error() //@snippet("E", Error, "Error()") +- f.Baz() //@snippet("B", snipMethodBaz, "Baz()") +- +- f.Baz() //@snippet("(", snipMethodBazBar, "BazBar") +- +- f.Baz() //@snippet("B", snipMethodBazBaz, "BazBaz(${1:})") -} - --// https://github.com/golang/go/issues/38589#issuecomment-620350922 -func _() { -- type myFunc func(int) Int //@loc(myFunc, "myFunc") -- -- var foo myFunc -- _ = foo() //@typedef("foo", myFunc), diag(")", re"not enough arguments") +- type bar struct { +- a int +- b float64 //@item(snipBarB, "b", "float64") +- } +- bar{b} //@snippet("}", snipBarB, "b: ${1:}") -} -diff -urN a/gopls/internal/regtest/marker/testdata/workspacesymbol/allscope.txt b/gopls/internal/regtest/marker/testdata/workspacesymbol/allscope.txt ---- a/gopls/internal/regtest/marker/testdata/workspacesymbol/allscope.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/workspacesymbol/allscope.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,30 +0,0 @@ --This test verifies behavior when "symbolScope" is set to "all". +diff -urN a/gopls/internal/test/marker/testdata/completion/statements.txt b/gopls/internal/test/marker/testdata/completion/statements.txt +--- a/gopls/internal/test/marker/testdata/completion/statements.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/statements.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,134 +0,0 @@ +-This test exercises completion around various statements. +- +--- flags -- +--ignore_extra_diags - --- settings.json -- -{ -- "symbolStyle": "full", -- "symbolMatcher": "casesensitive", -- "symbolScope": "all" +- "usePlaceholders": true -} - --- go.mod -- --module mod.test/symbols +-module golang.org/lsptests/statements - --go 1.18 +--- append.go -- +-package statements - ---- query.go -- --package symbols +-func _() { +- type mySlice []int - --//@workspacesymbol("fmt.Println", println) +- var ( +- abc []int //@item(stmtABC, "abc", "[]int", "var") +- abcdef mySlice //@item(stmtABCDEF, "abcdef", "mySlice", "var") +- ) - ---- fmt/fmt.go -- --package fmt +- /* abcdef = append(abcdef, ) */ //@item(stmtABCDEFAssignAppend, "abcdef = append(abcdef, )", "", "func") - --import "fmt" +- // don't offer "abc = append(abc, )" because "abc" isn't necessarily +- // better than "abcdef". +- abc //@complete(" //", stmtABC, stmtABCDEF) - --func Println(s string) { -- fmt.Println(s) --} ---- @println -- --fmt/fmt.go:5:6-13 mod.test/symbols/fmt.Println Function --<unknown> fmt.Println Function -diff -urN a/gopls/internal/regtest/marker/testdata/workspacesymbol/caseinsensitive.txt b/gopls/internal/regtest/marker/testdata/workspacesymbol/caseinsensitive.txt ---- a/gopls/internal/regtest/marker/testdata/workspacesymbol/caseinsensitive.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/workspacesymbol/caseinsensitive.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,26 +0,0 @@ --This file contains test for symbol matches using the caseinsensitive matcher. +- abcdef //@complete(" //", stmtABCDEF, stmtABCDEFAssignAppend) - ---- settings.json -- --{ -- "symbolMatcher": "caseinsensitive" +- /* append(abc, ) */ //@item(stmtABCAppend, "append(abc, )", "", "func") +- +- abc = app //@snippet(" //", stmtABCAppend, "append(abc, ${1:})") -} - ---- go.mod -- --module mod.test/caseinsensitive +-func _() { +- var s struct{ xyz []int } - --go 1.18 +- /* xyz = append(s.xyz, ) */ //@item(stmtXYZAppend, "xyz = append(s.xyz, )", "", "func") - ---- p.go -- --package caseinsensitive +- s.x //@snippet(" //", stmtXYZAppend, "xyz = append(s.xyz, ${1:})") - --//@workspacesymbol("", blank) --//@workspacesymbol("randomgophervar", randomgophervar) +- /* s.xyz = append(s.xyz, ) */ //@item(stmtDeepXYZAppend, "s.xyz = append(s.xyz, )", "", "func") - --var RandomGopherVariableA int --var randomgopherVariableB int --var RandomGopherOtherVariable int +- sx //@snippet(" //", stmtDeepXYZAppend, "s.xyz = append(s.xyz, ${1:})") +-} - ---- @blank -- ---- @randomgophervar -- --p.go:6:5-26 RandomGopherVariableA Variable --p.go:7:5-26 randomgopherVariableB Variable -diff -urN a/gopls/internal/regtest/marker/testdata/workspacesymbol/casesensitive.txt b/gopls/internal/regtest/marker/testdata/workspacesymbol/casesensitive.txt ---- a/gopls/internal/regtest/marker/testdata/workspacesymbol/casesensitive.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/workspacesymbol/casesensitive.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,116 +0,0 @@ --This file contains tests for symbol matches using the casesensitive matcher. +-func _() { +- var foo [][]int - --For historical reasons, it also verifies general behavior of the symbol search. +- /* append(foo[0], ) */ //@item(stmtFooAppend, "append(foo[0], )", "", "func") - ---- settings.json -- --{ -- "symbolMatcher": "casesensitive" +- foo[0] = app //@complete(" //", stmtFooAppend),snippet(" //", stmtFooAppend, "append(foo[0], ${1:})") -} - ---- go.mod -- --module mod.test/casesensitive +--- if_err_check_return.go -- +-package statements - --go 1.18 +-import ( +- "bytes" +- "io" +- "os" +-) - ---- main.go -- --package main +-func one() (int, float32, io.Writer, *int, []int, bytes.Buffer, error) { +- /* if err != nil { return err } */ //@item(stmtOneIfErrReturn, "if err != nil { return err }", "", "") +- /* err != nil { return err } */ //@item(stmtOneErrReturn, "err != nil { return err }", "", "") - --//@workspacesymbol("main.main", main) --//@workspacesymbol("p.Message", Message) --//@workspacesymbol("main.myvar", myvar) --//@workspacesymbol("main.myType", myType) --//@workspacesymbol("main.myType.Blahblah", blahblah) --//@workspacesymbol("main.myStruct", myStruct) --//@workspacesymbol("main.myStruct.myStructField", myStructField) --//@workspacesymbol("main.myInterface", myInterface) --//@workspacesymbol("main.myInterface.DoSomeCoolStuff", DoSomeCoolStuff) --//@workspacesymbol("main.embed.myStruct", embeddedStruct) --//@workspacesymbol("main.embed.nestedStruct.nestedStruct2.int", int) --//@workspacesymbol("main.embed.nestedInterface.myInterface", nestedInterface) --//@workspacesymbol("main.embed.nestedInterface.nestedMethod", nestedMethod) --//@workspacesymbol("dunk", dunk) --//@workspacesymbol("Dunk", Dunk) +- _, err := os.Open("foo") +- //@snippet("", stmtOneIfErrReturn, "if err != nil {\n\treturn 0, 0, nil, nil, nil, bytes.Buffer{\\}, ${1:err}\n\\}") - --import ( -- "encoding/json" -- "fmt" --) +- _, err = os.Open("foo") +- i //@snippet(" //", stmtOneIfErrReturn, "if err != nil {\n\treturn 0, 0, nil, nil, nil, bytes.Buffer{\\}, ${1:err}\n\\}") - --func main() { // function -- fmt.Println("Hello") +- _, err = os.Open("foo") +- if er //@snippet(" //", stmtOneErrReturn, "err != nil {\n\treturn 0, 0, nil, nil, nil, bytes.Buffer{\\}, ${1:err}\n\\}") +- +- _, err = os.Open("foo") +- if //@snippet(" //", stmtOneIfErrReturn, "if err != nil {\n\treturn 0, 0, nil, nil, nil, bytes.Buffer{\\}, ${1:err}\n\\}") +- +- _, err = os.Open("foo") +- if //@snippet("//", stmtOneIfErrReturn, "if err != nil {\n\treturn 0, 0, nil, nil, nil, bytes.Buffer{\\}, ${1:err}\n\\}") -} - --var myvar int // variable +--- if_err_check_return2.go -- +-package statements - --type myType string // basic type +-import "os" - --type myDecoder json.Decoder // to use the encoding/json import +-func two() error { +- var s struct{ err error } - --func (m *myType) Blahblah() {} // method +- /* if s.err != nil { return s.err } */ //@item(stmtTwoIfErrReturn, "if s.err != nil { return s.err }", "", "") - --type myStruct struct { // struct type -- myStructField int // struct field +- _, s.err = os.Open("foo") +- //@snippet("", stmtTwoIfErrReturn, "if s.err != nil {\n\treturn ${1:s.err}\n\\}") -} - --type myInterface interface { // interface -- DoSomeCoolStuff() string // interface method --} +--- if_err_check_test.go -- +-package statements - --type embed struct { -- myStruct +-import ( +- "os" +- "testing" +-) - -- nestedStruct struct { -- nestedField int +-func TestErr(t *testing.T) { +- /* if err != nil { t.Fatal(err) } */ //@item(stmtOneIfErrTFatal, "if err != nil { t.Fatal(err) }", "", "") - -- nestedStruct2 struct { -- int -- } -- } +- _, err := os.Open("foo") +- //@snippet("", stmtOneIfErrTFatal, "if err != nil {\n\tt.Fatal(err)\n\\}") +-} - -- nestedInterface interface { -- myInterface -- nestedMethod() -- } +-func BenchmarkErr(b *testing.B) { +- /* if err != nil { b.Fatal(err) } */ //@item(stmtOneIfErrBFatal, "if err != nil { b.Fatal(err) }", "", "") +- +- _, err := os.Open("foo") +- //@snippet("", stmtOneIfErrBFatal, "if err != nil {\n\tb.Fatal(err)\n\\}") -} - --func Dunk() int { return 0 } +--- return.go -- +-package statements - --func dunk() {} +-//@item(stmtReturnZeroValues, `return 0, "", nil`) - ---- p/p.go -- --package p +-func foo() (int, string, error) { +- ret //@snippet(" ", stmtReturnZeroValues, "return ${1:0}, ${2:\"\"}, ${3:nil}") +-} - --const Message = "Hello World." // constant ---- @DoSomeCoolStuff -- --main.go:41:2-17 main.myInterface.DoSomeCoolStuff Method ---- @Dunk -- --main.go:61:6-10 Dunk Function ---- @Message -- --p/p.go:3:7-14 p.Message Constant ---- @blahblah -- --main.go:34:18-26 main.myType.Blahblah Method ---- @dunk -- --main.go:63:6-10 dunk Function ---- @int -- --main.go:51:4-7 main.embed.nestedStruct.nestedStruct2.int Field ---- @main -- --main.go:24:6-10 main.main Function ---- @myInterface -- --main.go:40:6-17 main.myInterface Interface --main.go:41:2-17 main.myInterface.DoSomeCoolStuff Method ---- @myStruct -- --main.go:36:6-14 main.myStruct Struct --main.go:37:2-15 main.myStruct.myStructField Field ---- @myStructField -- --main.go:37:2-15 main.myStruct.myStructField Field ---- @myType -- --main.go:30:6-12 main.myType Class --main.go:34:18-26 main.myType.Blahblah Method ---- @myvar -- --main.go:28:5-10 main.myvar Variable ---- @nestedInterface -- --main.go:56:3-14 main.embed.nestedInterface.myInterface Interface ---- @nestedMethod -- --main.go:57:3-15 main.embed.nestedInterface.nestedMethod Method ---- @embeddedStruct -- --main.go:45:2-10 main.embed.myStruct Field -diff -urN a/gopls/internal/regtest/marker/testdata/workspacesymbol/issue44806.txt b/gopls/internal/regtest/marker/testdata/workspacesymbol/issue44806.txt ---- a/gopls/internal/regtest/marker/testdata/workspacesymbol/issue44806.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/workspacesymbol/issue44806.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,27 +0,0 @@ --This test verifies the fix for the crash encountered in golang/go#44806. +-func bar() (int, string, error) { +- return //@snippet(" ", stmtReturnZeroValues, "return ${1:0}, ${2:\"\"}, ${3:nil}") +-} +diff -urN a/gopls/internal/test/marker/testdata/completion/testy.txt b/gopls/internal/test/marker/testdata/completion/testy.txt +--- a/gopls/internal/test/marker/testdata/completion/testy.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/testy.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,61 +0,0 @@ +- +--- flags -- +--ignore_extra_diags - --- go.mod -- --module mod.test/symbol +-module testy.test - -go 1.18 ---- symbol.go -- --package symbol - --//@workspacesymbol("m", m) +--- types/types.go -- +-package types - --type T struct{} - --// We should accept all valid receiver syntax when scanning symbols. --func (*(T)) m1() {} --func (*T) m2() {} --func (T) m3() {} --func ((T)) m4() {} --func ((*T)) m5() {} +--- signature/signature.go -- +-package signature - ---- @m -- --symbol.go:8:13-15 T.m1 Method --symbol.go:9:11-13 T.m2 Method --symbol.go:10:10-12 T.m3 Method --symbol.go:11:12-14 T.m4 Method --symbol.go:12:13-15 T.m5 Method --symbol.go:5:6-7 symbol.T Struct -diff -urN a/gopls/internal/regtest/marker/testdata/workspacesymbol/workspacesymbol.txt b/gopls/internal/regtest/marker/testdata/workspacesymbol/workspacesymbol.txt ---- a/gopls/internal/regtest/marker/testdata/workspacesymbol/workspacesymbol.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/workspacesymbol/workspacesymbol.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,72 +0,0 @@ --This test contains tests for basic functionality of the workspace/symbol --request. +-type Alias = int - --TODO(rfindley): add a test for the legacy 'fuzzy' symbol matcher using setting ("symbolMatcher": "fuzzy"). This test uses the default matcher ("fastFuzzy"). +--- snippets/snippets.go -- +-package snippets - ---- go.mod -- --module mod.test/symbols +-import ( +- "testy.test/signature" +- t "testy.test/types" +-) - --go 1.18 +-func X(_ map[signature.Alias]t.CoolAlias) (map[signature.Alias]t.CoolAlias) { +- return nil +-} - ---- query.go -- --package symbols +--- testy/testy.go -- +-package testy - --//@workspacesymbol("rgop", rgop) --//@workspacesymbol("randoma", randoma) --//@workspacesymbol("randomb", randomb) +-func a() { //@item(funcA, "a", "func()", "func") +- //@complete("", funcA) +-} - ---- a/a.go -- --package a - --var RandomGopherVariableA = "a" +--- testy/testy_test.go -- +-package testy - --const RandomGopherConstantA = "a" +-import ( +- "testing" - --const ( -- randomgopherinvariable = iota +- sig "testy.test/signature" +- "testy.test/snippets" -) - ---- a/a_test.go -- --package a +-func TestSomething(t *testing.T) { //@item(TestSomething, "TestSomething(t *testing.T)", "", "func") +- var x int //@loc(testyX, "x"), diag("x", re"x.*declared (and|but) not used") +- a() //@loc(testyA, "a") +-} - --var RandomGopherTestVariableA = "a" +-func _() { +- _ = snippets.X(nil) //@signature("nil", "X(_ map[sig.Alias]types.CoolAlias) map[sig.Alias]types.CoolAlias", 0) +- var _ sig.Alias +-} - ---- a/a_x_test.go -- --package a_test +-func issue63578(err error) { +- err.Error() //@signature(")", "Error()", 0) +-} +diff -urN a/gopls/internal/test/marker/testdata/completion/type_assert.txt b/gopls/internal/test/marker/testdata/completion/type_assert.txt +--- a/gopls/internal/test/marker/testdata/completion/type_assert.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/type_assert.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,30 +0,0 @@ +-This test checks completion related to type assertions. - --var RandomGopherXTestVariableA = "a" +--- flags -- +--ignore_extra_diags - ---- b/b.go -- --package b +--- type_assert.go -- +-package typeassert - --var RandomGopherVariableB = "b" +-type abc interface { //@item(abcIntf, "abc", "interface{...}", "interface") +- abc() +-} - --type RandomGopherStructB struct { -- Bar int +-type abcImpl struct{} //@item(abcImpl, "abcImpl", "struct{...}", "struct") +-func (abcImpl) abc() +- +-type abcPtrImpl struct{} //@item(abcPtrImpl, "abcPtrImpl", "struct{...}", "struct") +-func (*abcPtrImpl) abc() +- +-type abcNotImpl struct{} //@item(abcNotImpl, "abcNotImpl", "struct{...}", "struct") +- +-func _() { +- var a abc +- switch a.(type) { +- case ab: //@complete(":", abcImpl, abcPtrImpl, abcIntf, abcNotImpl) +- case *ab: //@complete(":", abcImpl, abcPtrImpl, abcIntf, abcNotImpl) +- } +- +- a.(ab) //@complete(")", abcImpl, abcPtrImpl, abcIntf, abcNotImpl) +- a.(*ab) //@complete(")", abcImpl, abcPtrImpl, abcIntf, abcNotImpl) -} +diff -urN a/gopls/internal/test/marker/testdata/completion/type_mods.txt b/gopls/internal/test/marker/testdata/completion/type_mods.txt +--- a/gopls/internal/test/marker/testdata/completion/type_mods.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/type_mods.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,27 +0,0 @@ +-This test check completion snippets with type modifiers. - ---- @rgop -- --b/b.go:5:6-25 RandomGopherStructB Struct --a/a.go:5:7-28 RandomGopherConstantA Constant --a/a.go:3:5-26 RandomGopherVariableA Variable --b/b.go:3:5-26 RandomGopherVariableB Variable --a/a_test.go:3:5-30 RandomGopherTestVariableA Variable --a/a_x_test.go:3:5-31 RandomGopherXTestVariableA Variable --a/a.go:8:2-24 randomgopherinvariable Constant --b/b.go:6:2-5 RandomGopherStructB.Bar Field ---- @randoma -- --a/a.go:5:7-28 RandomGopherConstantA Constant --a/a.go:3:5-26 RandomGopherVariableA Variable --b/b.go:3:5-26 RandomGopherVariableB Variable --a/a.go:8:2-24 randomgopherinvariable Constant --a/a_test.go:3:5-30 RandomGopherTestVariableA Variable --a/a_x_test.go:3:5-31 RandomGopherXTestVariableA Variable --b/b.go:6:2-5 RandomGopherStructB.Bar Field ---- @randomb -- --b/b.go:5:6-25 RandomGopherStructB Struct --a/a.go:3:5-26 RandomGopherVariableA Variable --b/b.go:3:5-26 RandomGopherVariableB Variable --a/a.go:8:2-24 randomgopherinvariable Constant --a/a_test.go:3:5-30 RandomGopherTestVariableA Variable --a/a_x_test.go:3:5-31 RandomGopherXTestVariableA Variable --b/b.go:6:2-5 RandomGopherStructB.Bar Field -diff -urN a/gopls/internal/regtest/marker/testdata/workspacesymbol/wsscope.txt b/gopls/internal/regtest/marker/testdata/workspacesymbol/wsscope.txt ---- a/gopls/internal/regtest/marker/testdata/workspacesymbol/wsscope.txt 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/marker/testdata/workspacesymbol/wsscope.txt 1970-01-01 00:00:00.000000000 +0000 -@@ -1,29 +0,0 @@ --This test verifies behavior when "symbolScope" is set to "workspace". +--- flags -- +--ignore_extra_diags - ---- settings.json -- --{ -- "symbolStyle": "full", -- "symbolMatcher": "casesensitive", -- "symbolScope": "workspace" +--- typemods.go -- +-package typemods +- +-func fooFunc() func() int { //@item(modFooFunc, "fooFunc", "func() func() int", "func") +- return func() int { +- return 0 +- } +-} +- +-func fooPtr() *int { //@item(modFooPtr, "fooPtr", "func() *int", "func") +- return nil +-} +- +-func _() { +- var _ int = foo //@snippet(" //", modFooFunc, "fooFunc()()"),snippet(" //", modFooPtr, "*fooPtr()") +-} +- +-func _() { +- var m map[int][]chan int //@item(modMapChanPtr, "m", "map[int]chan *int", "var") +- +- var _ int = m //@snippet(" //", modMapChanPtr, "<-m[${1:}][${2:}]") -} +diff -urN a/gopls/internal/test/marker/testdata/completion/type_params.txt b/gopls/internal/test/marker/testdata/completion/type_params.txt +--- a/gopls/internal/test/marker/testdata/completion/type_params.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/type_params.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,70 +0,0 @@ +-This test checks various ranking of completion results related to type +-parameters. - ---- go.mod -- --module mod.test/symbols +--- flags -- +--ignore_extra_diags - --go 1.18 +--- type_params.go -- +-package typeparams - ---- query.go -- --package symbols +-// Copied from the old builtins.go, which has been ported to the new marker tests. +-/* string */ //@item(string, "string", "", "type") +-/* float32 */ //@item(float32, "float32", "", "type") +-/* float64 */ //@item(float64, "float64", "", "type") +-/* int */ //@item(int, "int", "", "type") - --//@workspacesymbol("fmt.Println", println) +-func one[a int | string]() {} +-func two[a int | string, b float64 | int]() {} - ---- fmt/fmt.go -- --package fmt +-func _() { +- one[]() //@rank("]", string, float64) +- two[]() //@rank("]", int, float64) +- two[int, f]() //@rank("]", float64, float32) +-} - --import "fmt" +-func slices[a []int | []float64]() {} //@item(tpInts, "[]int", "[]int", "type"),item(tpFloats, "[]float64", "[]float64", "type") - --func Println(s string) { -- fmt.Println(s) +-func _() { +- slices[]() //@rank("]", tpInts),rank("]", tpFloats) -} ---- @println -- --fmt/fmt.go:5:6-13 mod.test/symbols/fmt.Println Function -diff -urN a/gopls/internal/regtest/misc/call_hierarchy_test.go b/gopls/internal/regtest/misc/call_hierarchy_test.go ---- a/gopls/internal/regtest/misc/call_hierarchy_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/misc/call_hierarchy_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,35 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. --package misc - --import ( -- "testing" +-type s[a int | string] struct{} - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" --) +-func _() { +- s[]{} //@rank("]", int, float64) +-} - --// Test for golang/go#49125 --func TestCallHierarchy_Issue49125(t *testing.T) { -- const files = ` ---- go.mod -- --module mod.com +-func takesGeneric[a int | string](s[a]) { +- "s[a]{}" //@item(tpInScopeLit, "s[a]{}", "", "var") +- takesGeneric() //@rank(")", tpInScopeLit),snippet(")", tpInScopeLit, "s[a]{\\}") +-} - --go 1.12 ---- p.go -- --package pkg --` -- // TODO(rfindley): this could probably just be a marker test. -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("p.go") -- loc := env.RegexpSearch("p.go", "pkg") +-func _() { +- s[int]{} //@item(tpInstLit, "s[int]{}", "", "var") +- takesGeneric[int]() //@rank(")", tpInstLit),snippet(")", tpInstLit, "s[int]{\\}") - -- var params protocol.CallHierarchyPrepareParams -- params.TextDocument.URI = loc.URI -- params.Position = loc.Range.Start +- "s[...]{}" //@item(tpUninstLit, "s[...]{}", "", "var") +- takesGeneric() //@rank(")", tpUninstLit),snippet(")", tpUninstLit, "s[${1:}]{\\}") +-} - -- // Check that this doesn't panic. -- env.Editor.Server.PrepareCallHierarchy(env.Ctx, ¶ms) -- }) +-func returnTP[A int | float64](a A) A { //@item(returnTP, "returnTP", "something", "func") +- return a -} -diff -urN a/gopls/internal/regtest/misc/configuration_test.go b/gopls/internal/regtest/misc/configuration_test.go ---- a/gopls/internal/regtest/misc/configuration_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/misc/configuration_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,157 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package misc +-func _() { +- var _ int = returnTP //@snippet(" //", returnTP, "returnTP(${1:})") - --import ( -- "testing" +- var aa int //@item(tpInt, "aa", "int", "var") +- var ab float64 //@item(tpFloat, "ab", "float64", "var") +- returnTP[int](a) //@rank(")", tpInt, tpFloat) +-} +- +-func takesFunc[T any](func(T) T) { +- var _ func(t T) T = f //@snippet(" //", tpLitFunc, "func(t T) T {$0\\}") +-} - -- . "golang.org/x/tools/gopls/internal/lsp/regtest" +-func _() { +- _ = "func(...) {}" //@item(tpLitFunc, "func(...) {}", "", "var") +- takesFunc() //@snippet(")", tpLitFunc, "func(${1:}) ${2:} {$0\\}") +- takesFunc[int]() //@snippet(")", tpLitFunc, "func(i int) int {$0\\}") +-} +diff -urN a/gopls/internal/test/marker/testdata/completion/unimported-std.txt b/gopls/internal/test/marker/testdata/completion/unimported-std.txt +--- a/gopls/internal/test/marker/testdata/completion/unimported-std.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/unimported-std.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,49 +0,0 @@ +-Test of unimported completions respecting the effective Go version of the file. - -- "golang.org/x/tools/internal/testenv" --) +-(See unprefixed file for same test of imported completions.) - --// Test that enabling and disabling produces the expected results of showing --// and hiding staticcheck analysis results. --func TestChangeConfiguration(t *testing.T) { -- // Staticcheck only supports Go versions >= 1.19. -- // Note: keep this in sync with TestStaticcheckWarning. Below this version we -- // should get an error when setting staticcheck configuration. -- testenv.NeedsGo1Point(t, 19) +-These symbols below were introduced to go/types in go1.22: +- +- Alias +- Info.FileVersions +- (Checker).PkgNameOf +- +-The underlying logic depends on versions.FileVersion, which only +-behaves correctly in go1.22. (When go1.22 is assured, we can remove +-the min_go flag but leave the test inputs unchanged.) +- +--- flags -- +--ignore_extra_diags -min_go=go1.22 - -- const files = ` --- go.mod -- --module mod.com +-module example.com +- +-go 1.21 - --go 1.12 --- a/a.go -- -package a - --import "errors" +-// package-level func +-var _ = types.Sat //@rankl("Sat", "Satisfies") +-var _ = types.Ali //@rankl("Ali", "!Alias") - --// FooErr should be called ErrFoo (ST1012) --var FooErr = errors.New("foo") --` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("a/a.go") -- env.AfterChange( -- NoDiagnostics(ForFile("a/a.go")), -- ) -- cfg := env.Editor.Config() -- cfg.Settings = map[string]interface{}{ -- "staticcheck": true, -- } -- env.ChangeConfiguration(cfg) -- env.AfterChange( -- Diagnostics(env.AtRegexp("a/a.go", "var (FooErr)")), -- ) -- }) --} +-// (We don't offer completions of methods +-// of types from unimported packages, so the fact that +-// we don't implement std version filtering isn't evident.) - --// TestMajorOptionsChange is like TestChangeConfiguration, but modifies an --// an open buffer before making a major (but inconsequential) change that --// causes gopls to recreate the view. --// --// Gopls should not get confused about buffer content when recreating the view. --func TestMajorOptionsChange(t *testing.T) { -- testenv.NeedsGo1Point(t, 19) // needs staticcheck +-// field +-var _ = new(types.Info).Use //@rankl("Use", "!Uses") +-var _ = new(types.Info).Fil //@rankl("Fil", "!FileVersions") - -- const files = ` ---- go.mod -- --module mod.com +-// method +-var _ = new(types.Checker).Obje //@rankl("Obje", "!ObjectOf") +-var _ = new(types.Checker).PkgN //@rankl("PkgN", "!PkgNameOf") - --go 1.12 ---- a/a.go -- --package a +--- b/b.go -- +-//go:build go1.22 - --import "errors" +-package a - --var ErrFoo = errors.New("foo") --` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("a/a.go") -- // Introduce a staticcheck diagnostic. It should be detected when we enable -- // staticcheck later. -- env.RegexpReplace("a/a.go", "ErrFoo", "FooErr") -- env.AfterChange( -- NoDiagnostics(ForFile("a/a.go")), -- ) -- cfg := env.Editor.Config() -- // Any change to environment recreates the view, but this should not cause -- // gopls to get confused about the content of a/a.go: we should get the -- // staticcheck diagnostic below. -- cfg.Env = map[string]string{ -- "AN_ARBITRARY_VAR": "FOO", -- } -- cfg.Settings = map[string]interface{}{ -- "staticcheck": true, -- } -- env.ChangeConfiguration(cfg) -- env.AfterChange( -- Diagnostics(env.AtRegexp("a/a.go", "var (FooErr)")), -- ) -- }) --} +-// package-level decl +-var _ = types.Sat //@rankl("Sat", "Satisfies") +-var _ = types.Ali //@rankl("Ali", "Alias") +diff -urN a/gopls/internal/test/marker/testdata/completion/unimported.txt b/gopls/internal/test/marker/testdata/completion/unimported.txt +--- a/gopls/internal/test/marker/testdata/completion/unimported.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/unimported.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,88 +0,0 @@ - --func TestStaticcheckWarning(t *testing.T) { -- // Note: keep this in sync with TestChangeConfiguration. -- testenv.SkipAfterGo1Point(t, 16) +--- flags -- +--ignore_extra_diags - -- const files = ` --- go.mod -- --module mod.com +-module unimported.test - --go 1.12 ---- a/a.go -- --package a +-go 1.18 - --import "errors" +--- unimported/export_test.go -- +-package unimported - --// FooErr should be called ErrFoo (ST1012) --var FooErr = errors.New("foo") --` +-var TestExport int //@item(testexport, "TestExport", "var (from \"unimported.test/unimported\")", "var") - -- WithOptions( -- Settings{"staticcheck": true}, -- ).Run(t, files, func(t *testing.T, env *Env) { -- env.OnceMet( -- InitialWorkspaceLoad, -- ShownMessage("staticcheck is not supported"), -- ) -- }) --} +--- signature/signature.go -- +-package signature - --func TestGofumptWarning(t *testing.T) { -- testenv.SkipAfterGo1Point(t, 17) +-func Foo() {} - -- WithOptions( -- Settings{"gofumpt": true}, -- ).Run(t, "", func(t *testing.T, env *Env) { -- env.OnceMet( -- InitialWorkspaceLoad, -- ShownMessage("gofumpt is not supported"), -- ) -- }) --} +--- foo/foo.go -- +-package foo - --func TestDeprecatedSettings(t *testing.T) { -- WithOptions( -- Settings{ -- "experimentalUseInvalidMetadata": true, -- "experimentalWatchedFileDelay": "1s", -- "experimentalWorkspaceModule": true, -- "tempModfile": true, -- "expandWorkspaceToModule": false, -- }, -- ).Run(t, "", func(t *testing.T, env *Env) { -- env.OnceMet( -- InitialWorkspaceLoad, -- ShownMessage("experimentalWorkspaceModule"), -- ShownMessage("experimentalUseInvalidMetadata"), -- ShownMessage("experimentalWatchedFileDelay"), -- ShownMessage("https://go.dev/issue/63537"), // issue to remove tempModfile -- ShownMessage("https://go.dev/issue/63536"), // issue to remove expandWorkspaceToModule -- ) -- }) --} -diff -urN a/gopls/internal/regtest/misc/debugserver_test.go b/gopls/internal/regtest/misc/debugserver_test.go ---- a/gopls/internal/regtest/misc/debugserver_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/misc/debugserver_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,46 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-type StructFoo struct{ F int } - --package misc +--- baz/baz.go -- +-package baz - -import ( -- "net/http" -- "testing" +- f "unimported.test/foo" +-) - -- "golang.org/x/tools/gopls/internal/lsp/command" -- "golang.org/x/tools/gopls/internal/lsp/protocol" +-var FooStruct f.StructFoo - -- . "golang.org/x/tools/gopls/internal/lsp/regtest" --) +--- unimported/unimported.go -- +-package unimported - --func TestStartDebugging(t *testing.T) { -- WithOptions( -- Modes(Forwarded), -- ).Run(t, "", func(t *testing.T, env *Env) { -- args, err := command.MarshalArgs(command.DebuggingArgs{}) -- if err != nil { -- t.Fatal(err) -- } -- params := &protocol.ExecuteCommandParams{ -- Command: command.StartDebugging.ID(), -- Arguments: args, -- } -- var result command.DebuggingResult -- env.ExecuteCommand(params, &result) -- if got, want := len(result.URLs), 2; got != want { -- t.Fatalf("got %d urls, want %d; urls: %#v", got, want, result.URLs) -- } -- for i, u := range result.URLs { -- resp, err := http.Get(u) -- if err != nil { -- t.Errorf("getting url #%d (%q): %v", i, u, err) -- continue -- } -- defer resp.Body.Close() -- if got, want := resp.StatusCode, http.StatusOK; got != want { -- t.Errorf("debug server #%d returned HTTP %d, want %d", i, got, want) -- } -- } -- }) +-func _() { +- http //@complete("p", http, httptest, httptrace, httputil) +- // container/ring is extremely unlikely to be imported by anything, so shouldn't have type information. +- ring.Ring //@complete(re"R()ing", ringring) +- signature.Foo //@complete("Foo", signaturefoo) +- +- context.Bac //@complete(" //", contextBackground) -} -diff -urN a/gopls/internal/regtest/misc/definition_test.go b/gopls/internal/regtest/misc/definition_test.go ---- a/gopls/internal/regtest/misc/definition_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/misc/definition_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,571 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package misc +-// Create markers for unimported std lib packages. Only for use by this test. +-/* http */ //@item(http, "http", "\"net/http\"", "package") +-/* httptest */ //@item(httptest, "httptest", "\"net/http/httptest\"", "package") +-/* httptrace */ //@item(httptrace, "httptrace", "\"net/http/httptrace\"", "package") +-/* httputil */ //@item(httputil, "httputil", "\"net/http/httputil\"", "package") +- +-/* ring.Ring */ //@item(ringring, "Ring", "(from \"container/ring\")", "var") +- +-/* signature.Foo */ //@item(signaturefoo, "Foo", "func (from \"unimported.test/signature\")", "func") +- +-/* context.Background */ //@item(contextBackground, "Background", "func (from \"context\")", "func") +- +-// Now that we no longer type-check imported completions, +-// we don't expect the context.Background().Err method (see golang/go#58663). +-/* context.Background().Err */ //@item(contextBackgroundErr, "Background().Err", "func (from \"context\")", "method") +- +--- unimported/unimported_cand_type.go -- +-package unimported - -import ( -- "os" -- "path" -- "path/filepath" -- "strings" -- "testing" +- _ "context" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" -- "golang.org/x/tools/gopls/internal/lsp/tests/compare" +- "unimported.test/baz" -) - --const internalDefinition = ` ---- go.mod -- --module mod.com +-func _() { +- foo.StructFoo{} //@item(litFooStructFoo, "foo.StructFoo{}", "struct{...}", "struct") - --go 1.12 ---- main.go -- --package main +- // We get the literal completion for "foo.StructFoo{}" even though we haven't +- // imported "foo" yet. +- baz.FooStruct = f //@snippet(" //", litFooStructFoo, "foo.StructFoo{$0\\}") +-} - --import "fmt" +--- unimported/x_test.go -- +-package unimported_test - --func main() { -- fmt.Println(message) +-import ( +- "testing" +-) +- +-func TestSomething(t *testing.T) { +- _ = unimported.TestExport //@complete("TestExport", testexport) -} ---- const.go -- --package main +diff -urN a/gopls/internal/test/marker/testdata/completion/unresolved.txt b/gopls/internal/test/marker/testdata/completion/unresolved.txt +--- a/gopls/internal/test/marker/testdata/completion/unresolved.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/unresolved.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,16 +0,0 @@ +-This test verifies gopls does not crash on fake "resolved" types. - --const message = "Hello World." --` +--- flags -- +--ignore_extra_diags - --func TestGoToInternalDefinition(t *testing.T) { -- Run(t, internalDefinition, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- loc := env.GoToDefinition(env.RegexpSearch("main.go", "message")) -- name := env.Sandbox.Workdir.URIToPath(loc.URI) -- if want := "const.go"; name != want { -- t.Errorf("GoToDefinition: got file %q, want %q", name, want) -- } -- if want := env.RegexpSearch("const.go", "message"); loc != want { -- t.Errorf("GoToDefinition: got location %v, want %v", loc, want) -- } -- }) +--- settings.json -- +-{ +- "completeUnimported": false -} - --const linknameDefinition = ` ---- go.mod -- --module mod.com -- ---- upper/upper.go -- --package upper +--- unresolved.go -- +-package unresolved - --import ( -- _ "unsafe" +-func foo(interface{}) { +- foo(func(i, j f //@complete(" //") +-} +diff -urN a/gopls/internal/test/marker/testdata/completion/unsafe.txt b/gopls/internal/test/marker/testdata/completion/unsafe.txt +--- a/gopls/internal/test/marker/testdata/completion/unsafe.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/unsafe.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,24 +0,0 @@ +-This test checks completion of symbols in the 'unsafe' package. - -- _ "mod.com/middle" --) +--- flags -- +--ignore_extra_diags - --//go:linkname foo mod.com/lower.bar --func foo() string +--- settings.json -- +-{ +- "matcher": "caseinsensitive" +-} - ---- middle/middle.go -- --package middle +--- unsafe.go -- +-package unsafe - -import ( -- _ "mod.com/lower" +- "unsafe" -) - ---- lower/lower.s -- +-// Pre-set this marker, as we don't have a "source" for it in this package. +-/* unsafe.Sizeof */ //@item(Sizeof, "Sizeof", "invalid type", "text") - ---- lower/lower.go -- --package lower +-func _() { +- x := struct{}{} +- _ = unsafe.Sizeof(x) //@complete("z", Sizeof) +-} +diff -urN a/gopls/internal/test/marker/testdata/completion/variadic.txt b/gopls/internal/test/marker/testdata/completion/variadic.txt +--- a/gopls/internal/test/marker/testdata/completion/variadic.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/completion/variadic.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,67 +0,0 @@ +-This test checks completion related to variadic functions. - --func bar() string { -- return "bar as foo" --}` +--- flags -- +--ignore_extra_diags - --func TestGoToLinknameDefinition(t *testing.T) { -- Run(t, linknameDefinition, func(t *testing.T, env *Env) { -- env.OpenFile("upper/upper.go") +--- variadic.go -- +-package variadic - -- // Jump from directives 2nd arg. -- start := env.RegexpSearch("upper/upper.go", `lower.bar`) -- loc := env.GoToDefinition(start) -- name := env.Sandbox.Workdir.URIToPath(loc.URI) -- if want := "lower/lower.go"; name != want { -- t.Errorf("GoToDefinition: got file %q, want %q", name, want) -- } -- if want := env.RegexpSearch("lower/lower.go", `bar`); loc != want { -- t.Errorf("GoToDefinition: got position %v, want %v", loc, want) -- } -- }) +-func foo(i int, strs ...string) {} +- +-func bar() []string { //@item(vFunc, "bar", "func() []string", "func") +- return nil -} - --const linknameDefinitionReverse = ` ---- go.mod -- --module mod.com +-func _() { +- var ( +- i int //@item(vInt, "i", "int", "var") +- s string //@item(vStr, "s", "string", "var") +- ss []string //@item(vStrSlice, "ss", "[]string", "var") +- v interface{} //@item(vIntf, "v", "interface{}", "var") +- ) - ---- upper/upper.s -- +- foo() //@rank(")", vInt, vStr),rank(")", vInt, vStrSlice) +- foo(123, ) //@rank(")", vStr, vInt),rank(")", vStrSlice, vInt) +- foo(123, "", ) //@rank(")", vStr, vInt),rank(")", vStr, vStrSlice) +- foo(123, s, "") //@rank(", \"", vStr, vStrSlice) - ---- upper/upper.go -- --package upper +- // snippet will add the "..." for you +- foo(123, ) //@snippet(")", vStrSlice, "ss..."),snippet(")", vFunc, "bar()..."),snippet(")", vStr, "s") - --import ( -- _ "mod.com/middle" --) +- // don't add "..." for interface{} +- foo(123, ) //@snippet(")", vIntf, "v") +-} - --func foo() string +-func qux(...func()) {} +-func f() {} //@item(vVarArg, "f", "func()", "func") - ---- middle/middle.go -- --package middle +-func _() { +- qux(f) //@snippet(")", vVarArg, "f") +-} - --import ( -- _ "mod.com/lower" --) +-func _() { +- foo(0, []string{}...) //@complete(")") +-} - ---- lower/lower.go -- --package lower +--- variadic_intf.go -- +-package variadic - --import _ "unsafe" +-type baz interface { +- baz() +-} - --//go:linkname bar mod.com/upper.foo --func bar() string { -- return "bar as foo" --}` +-func wantsBaz(...baz) {} - --func TestGoToLinknameDefinitionInReverseDep(t *testing.T) { -- Run(t, linknameDefinitionReverse, func(t *testing.T, env *Env) { -- env.OpenFile("lower/lower.go") +-type bazImpl int - -- // Jump from directives 2nd arg. -- start := env.RegexpSearch("lower/lower.go", `upper.foo`) -- loc := env.GoToDefinition(start) -- name := env.Sandbox.Workdir.URIToPath(loc.URI) -- if want := "upper/upper.go"; name != want { -- t.Errorf("GoToDefinition: got file %q, want %q", name, want) -- } -- if want := env.RegexpSearch("upper/upper.go", `foo`); loc != want { -- t.Errorf("GoToDefinition: got position %v, want %v", loc, want) -- } -- }) +-func (bazImpl) baz() {} +- +-func _() { +- var ( +- impls []bazImpl //@item(vImplSlice, "impls", "[]bazImpl", "var") +- impl bazImpl //@item(vImpl, "impl", "bazImpl", "var") +- bazes []baz //@item(vIntfSlice, "bazes", "[]baz", "var") +- ) +- +- wantsBaz() //@rank(")", vImpl, vImplSlice),rank(")", vIntfSlice, vImplSlice) +-} +diff -urN a/gopls/internal/test/marker/testdata/configuration/static.txt b/gopls/internal/test/marker/testdata/configuration/static.txt +--- a/gopls/internal/test/marker/testdata/configuration/static.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/configuration/static.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,41 +0,0 @@ +-This test confirms that gopls honors configuration even if the client does not +-support dynamic configuration. +- +--- capabilities.json -- +-{ +- "configuration": false +-} +- +--- settings.json -- +-{ +- "usePlaceholders": true, +- "analyses": { +- "composites": false +- } -} - --// The linkname directive connects two packages not related in the import graph. --const linknameDefinitionDisconnected = ` --- go.mod -- --module mod.com +-module example.com/config +- +-go 1.18 - --- a/a.go -- -package a - --import ( -- _ "unsafe" --) +-import "example.com/config/b" - --//go:linkname foo mod.com/b.bar --func foo() string +-func Identity[P ~int](p P) P { //@item(Identity, "Identity", "", "") +- return p +-} +- +-func _() { +- _ = b.B{2} +- _ = Identi //@snippet(" //", Identity, "Identity(${1:p P})"), diag("Ident", re"(undefined|undeclared)") +-} - --- b/b.go -- -package b - --func bar() string { -- return "bar as foo" --}` -- --func TestGoToLinknameDefinitionDisconnected(t *testing.T) { -- Run(t, linknameDefinitionDisconnected, func(t *testing.T, env *Env) { -- env.OpenFile("a/a.go") -- -- // Jump from directives 2nd arg. -- start := env.RegexpSearch("a/a.go", `b.bar`) -- loc := env.GoToDefinition(start) -- name := env.Sandbox.Workdir.URIToPath(loc.URI) -- if want := "b/b.go"; name != want { -- t.Errorf("GoToDefinition: got file %q, want %q", name, want) -- } -- if want := env.RegexpSearch("b/b.go", `bar`); loc != want { -- t.Errorf("GoToDefinition: got position %v, want %v", loc, want) -- } -- }) +-type B struct { +- F int -} +diff -urN a/gopls/internal/test/marker/testdata/definition/cgo.txt b/gopls/internal/test/marker/testdata/definition/cgo.txt +--- a/gopls/internal/test/marker/testdata/definition/cgo.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/definition/cgo.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,62 +0,0 @@ +-This test is ported from the old marker tests. +-It tests hover and definition for cgo declarations. +- +--- flags -- +--cgo - --const stdlibDefinition = ` --- go.mod -- --module mod.com +-module cgo.test - --go 1.12 ---- main.go -- --package main +-go 1.18 - --import "fmt" +--- cgo/cgo.go -- +-package cgo - --func main() { -- fmt.Printf() --}` +-/* +-#include <stdio.h> +-#include <stdlib.h> - --func TestGoToStdlibDefinition_Issue37045(t *testing.T) { -- Run(t, stdlibDefinition, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- loc := env.GoToDefinition(env.RegexpSearch("main.go", `fmt.(Printf)`)) -- name := env.Sandbox.Workdir.URIToPath(loc.URI) -- if got, want := path.Base(name), "print.go"; got != want { -- t.Errorf("GoToDefinition: got file %q, want %q", name, want) -- } +-void myprint(char* s) { +- printf("%s\n", s); +-} +-*/ +-import "C" - -- // Test that we can jump to definition from outside our workspace. -- // See golang.org/issues/37045. -- newLoc := env.GoToDefinition(loc) -- newName := env.Sandbox.Workdir.URIToPath(newLoc.URI) -- if newName != name { -- t.Errorf("GoToDefinition is not idempotent: got %q, want %q", newName, name) -- } -- if newLoc != loc { -- t.Errorf("GoToDefinition is not idempotent: got %v, want %v", newLoc, loc) -- } -- }) +-import ( +- "fmt" +- "unsafe" +-) +- +-func Example() { //@loc(cgoexample, "Example"), item(cgoexampleItem, "Example", "func()", "func") +- fmt.Println() +- cs := C.CString("Hello from stdio\n") +- C.myprint(cs) +- C.free(unsafe.Pointer(cs)) -} - --func TestUnexportedStdlib_Issue40809(t *testing.T) { -- Run(t, stdlibDefinition, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- loc := env.GoToDefinition(env.RegexpSearch("main.go", `fmt.(Printf)`)) -- name := env.Sandbox.Workdir.URIToPath(loc.URI) +-func _() { +- Example() //@hover("ample", "Example", hoverExample), def("ample", cgoexample), complete("ample", cgoexampleItem) +-} - -- loc = env.RegexpSearch(name, `:=\s*(newPrinter)\(\)`) +--- @hoverExample -- +-```go +-func Example() +-``` - -- // Check that we can find references on a reference -- refs := env.References(loc) -- if len(refs) < 5 { -- t.Errorf("expected 5+ references to newPrinter, found: %#v", refs) -- } +-[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/cgo.test/cgo#Example) +--- usecgo/usecgo.go -- +-package cgoimport - -- loc = env.GoToDefinition(loc) -- content, _ := env.Hover(loc) -- if !strings.Contains(content.Value, "newPrinter") { -- t.Fatal("definition of newPrinter went to the incorrect place") -- } -- // And on the definition too. -- refs = env.References(loc) -- if len(refs) < 5 { -- t.Errorf("expected 5+ references to newPrinter, found: %#v", refs) -- } -- }) +-import ( +- "cgo.test/cgo" +-) +- +-func _() { +- cgo.Example() //@hover("ample", "Example", hoverImportedExample), def("ample", cgoexample), complete("ample", cgoexampleItem) -} +--- @hoverImportedExample -- +-```go +-func cgo.Example() +-``` +- +-[`cgo.Example` on pkg.go.dev](https://pkg.go.dev/cgo.test/cgo#Example) +diff -urN a/gopls/internal/test/marker/testdata/definition/embed.txt b/gopls/internal/test/marker/testdata/definition/embed.txt +--- a/gopls/internal/test/marker/testdata/definition/embed.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/definition/embed.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,275 +0,0 @@ +-This test checks definition and hover operations over embedded fields and methods. +- +-Its size expectations assume a 64-bit machine, +-and correct sizes information requires go1.21. +- +--- flags -- +--skip_goarch=386,arm +--min_go=go1.21 - --// Test the hover on an error's Error function. --// This can't be done via the marker tests because Error is a builtin. --func TestHoverOnError(t *testing.T) { -- const mod = ` --- go.mod -- -module mod.com - --go 1.12 ---- main.go -- --package main +-go 1.18 - --func main() { -- var err error -- err.Error() --}` -- Run(t, mod, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- content, _ := env.Hover(env.RegexpSearch("main.go", "Error")) -- if content == nil { -- t.Fatalf("nil hover content for Error") -- } -- want := "```go\nfunc (error).Error() string\n```" -- if content.Value != want { -- t.Fatalf("hover failed:\n%s", compare.Text(want, content.Value)) -- } -- }) +--- a/a.go -- +-package a +- +-type A string //@loc(AString, "A") +- +-func (_ A) Hi() {} //@loc(AHi, "Hi") +- +-type S struct { +- Field int //@loc(SField, "Field") +- R // embed a struct +- H // embed an interface -} - --func TestImportShortcut(t *testing.T) { -- const mod = ` ---- go.mod -- --module mod.com +-type R struct { +- Field2 int //@loc(RField2, "Field2") +-} - --go 1.12 ---- main.go -- --package main +-func (r R) Hey() {} //@loc(RHey, "Hey") - --import "fmt" +-type H interface { //@loc(H, "H") +- Goodbye() //@loc(HGoodbye, "Goodbye") +-} - --func main() {} --` -- for _, tt := range []struct { -- wantLinks int -- importShortcut string -- }{ -- {1, "Link"}, -- {0, "Definition"}, -- {1, "Both"}, -- } { -- t.Run(tt.importShortcut, func(t *testing.T) { -- WithOptions( -- Settings{"importShortcut": tt.importShortcut}, -- ).Run(t, mod, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- loc := env.GoToDefinition(env.RegexpSearch("main.go", `"fmt"`)) -- if loc == (protocol.Location{}) { -- t.Fatalf("expected definition, got none") -- } -- links := env.DocumentLink("main.go") -- if len(links) != tt.wantLinks { -- t.Fatalf("expected %v links, got %v", tt.wantLinks, len(links)) -- } -- }) -- }) -- } +-type I interface { //@loc(I, "I") +- B() //@loc(IB, "B") +- J -} - --func TestGoToTypeDefinition_Issue38589(t *testing.T) { -- const mod = ` ---- go.mod -- --module mod.com +-type J interface { //@loc(J, "J") +- Hello() //@loc(JHello, "Hello") +-} - --go 1.12 ---- main.go -- --package main +--- b/b.go -- +-package b - --type Int int +-import "mod.com/a" //@loc(AImport, re"\".*\"") - --type Struct struct{} +-type embed struct { +- F int //@loc(F, "F") +-} - --func F1() {} --func F2() (int, error) { return 0, nil } --func F3() (**Struct, bool, *Int, error) { return nil, false, nil, nil } --func F4() (**Struct, bool, *float64, error) { return nil, false, nil, nil } +-func (embed) M() //@loc(M, "M") - --func main() {} --` +-type Embed struct { +- embed +- *a.A +- a.I +- a.S +-} - -- for _, tt := range []struct { -- re string -- wantError bool -- wantTypeRe string -- }{ -- {re: `F1`, wantError: true}, -- {re: `F2`, wantError: true}, -- {re: `F3`, wantError: true}, -- {re: `F4`, wantError: false, wantTypeRe: `type (Struct)`}, -- } { -- t.Run(tt.re, func(t *testing.T) { -- Run(t, mod, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") +-func _() { +- e := Embed{} +- e.Hi() //@def("Hi", AHi),hover("Hi", "Hi", AHi) +- e.B() //@def("B", IB),hover("B", "B", IB) +- _ = e.Field //@def("Field", SField),hover("Field", "Field", SField) +- _ = e.Field2 //@def("Field2", RField2),hover("Field2", "Field2", RField2) +- e.Hello() //@def("Hello", JHello),hover("Hello", "Hello",JHello) +- e.Hey() //@def("Hey", RHey),hover("Hey", "Hey", RHey) +- e.Goodbye() //@def("Goodbye", HGoodbye),hover("Goodbye", "Goodbye", HGoodbye) +- e.M() //@def("M", M),hover("M", "M", M) +- _ = e.F //@def("F", F),hover("F", "F", F) +-} - -- loc, err := env.Editor.TypeDefinition(env.Ctx, env.RegexpSearch("main.go", tt.re)) -- if tt.wantError { -- if err == nil { -- t.Fatal("expected error, got nil") -- } -- return -- } -- if err != nil { -- t.Fatalf("expected nil error, got %s", err) -- } +-type aAlias = a.A //@loc(aAlias, "aAlias") - -- typeLoc := env.RegexpSearch("main.go", tt.wantTypeRe) -- if loc != typeLoc { -- t.Errorf("invalid pos: want %+v, got %+v", typeLoc, loc) -- } -- }) -- }) -- } +-type S1 struct { //@loc(S1, "S1") +- F1 int //@loc(S1F1, "F1") +- S2 //@loc(S1S2, "S2"),def("S2", S2),hover("S2", "S2", S2) +- a.A //@def("A", AString),hover("A", "A", aA) +- aAlias //@def("a", aAlias),hover("a", "aAlias", aAlias) -} - --func TestGoToTypeDefinition_Issue60544(t *testing.T) { -- const mod = ` ---- go.mod -- --module mod.com +-type S2 struct { //@loc(S2, "S2") +- F1 string //@loc(S2F1, "F1") +- F2 int //@loc(S2F2, "F2") +- *a.A //@def("A", AString),def("a",AImport) +-} - --go 1.19 ---- main.go -- --package main +-type S3 struct { +- F1 struct { +- a.A //@def("A", AString) +- } +-} - --func F[T comparable]() {} --` +-func Bar() { +- var x S1 //@def("S1", S1),hover("S1", "S1", S1) +- _ = x.S2 //@def("S2", S1S2),hover("S2", "S2", S1S2) +- _ = x.F1 //@def("F1", S1F1),hover("F1", "F1", S1F1) +- _ = x.F2 //@def("F2", S2F2),hover("F2", "F2", S2F2) +- _ = x.S2.F1 //@def("F1", S2F1),hover("F1", "F1", S2F1) +-} - -- Run(t, mod, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") +--- b/c.go -- +-package b - -- _ = env.TypeDefinition(env.RegexpSearch("main.go", "comparable")) // must not panic -- }) +-var _ = S1{ //@def("S1", S1),hover("S1", "S1", S1) +- F1: 99, //@def("F1", S1F1),hover("F1", "F1", S1F1) -} - --// Test for golang/go#47825. --func TestImportTestVariant(t *testing.T) { -- const mod = ` ---- go.mod -- --module mod.com +--- @AHi -- +-```go +-func (a.A) Hi() +-``` - --go 1.12 ---- client/test/role.go -- --package test +-[`(a.A).Hi` on pkg.go.dev](https://pkg.go.dev/mod.com/a#A.Hi) +--- @F -- +-```go +-field F int +-``` - --import _ "mod.com/client" +-@loc(F, "F") - --type RoleSetup struct{} ---- client/client_role_test.go -- --package client_test - --import ( -- "testing" -- _ "mod.com/client" -- ctest "mod.com/client/test" --) +-[`(b.Embed).F` on pkg.go.dev](https://pkg.go.dev/mod.com/b#Embed.F) +--- @HGoodbye -- +-```go +-func (a.H) Goodbye() +-``` - --func TestClient(t *testing.T) { -- _ = ctest.RoleSetup{} --} ---- client/client_test.go -- --package client +-@loc(HGoodbye, "Goodbye") - --import "testing" - --func TestClient(t *testing.T) {} ---- client.go -- --package client --` -- Run(t, mod, func(t *testing.T, env *Env) { -- env.OpenFile("client/client_role_test.go") -- env.GoToDefinition(env.RegexpSearch("client/client_role_test.go", "RoleSetup")) -- }) --} +-[`(a.H).Goodbye` on pkg.go.dev](https://pkg.go.dev/mod.com/a#H.Goodbye) +--- @IB -- +-```go +-func (a.I) B() +-``` - --// This test exercises a crashing pattern from golang/go#49223. --func TestGoToCrashingDefinition_Issue49223(t *testing.T) { -- Run(t, "", func(t *testing.T, env *Env) { -- params := &protocol.DefinitionParams{} -- params.TextDocument.URI = protocol.DocumentURI("fugitive%3A///Users/user/src/mm/ems/.git//0/pkg/domain/treasury/provider.go") -- params.Position.Character = 18 -- params.Position.Line = 0 -- env.Editor.Server.Definition(env.Ctx, params) -- }) --} +-@loc(IB, "B") - --// TestVendoringInvalidatesMetadata ensures that gopls uses the --// correct metadata even after an external 'go mod vendor' command --// causes packages to move; see issue #55995. --// See also TestImplementationsInVendor, which tests the same fix. --func TestVendoringInvalidatesMetadata(t *testing.T) { -- t.Skip("golang/go#56169: file watching does not capture vendor dirs") - -- const proxy = ` ---- other.com/b@v1.0.0/go.mod -- --module other.com/b --go 1.14 +-[`(a.I).B` on pkg.go.dev](https://pkg.go.dev/mod.com/a#I.B) +--- @JHello -- +-```go +-func (a.J) Hello() +-``` - ---- other.com/b@v1.0.0/b.go -- --package b --const K = 0 --` -- const src = ` ---- go.mod -- --module example.com/a --go 1.14 --require other.com/b v1.0.0 +-@loc(JHello, "Hello") - ---- go.sum -- --other.com/b v1.0.0 h1:1wb3PMGdet5ojzrKl+0iNksRLnOM9Jw+7amBNqmYwqk= --other.com/b v1.0.0/go.mod h1:TgHQFucl04oGT+vrUm/liAzukYHNxCwKNkQZEyn3m9g= - ---- a.go -- --package a --import "other.com/b" --const _ = b.K +-[`(a.J).Hello` on pkg.go.dev](https://pkg.go.dev/mod.com/a#J.Hello) +--- @M -- +-```go +-func (embed) M() +-``` - --` -- WithOptions( -- ProxyFiles(proxy), -- Modes(Default), // fails in 'experimental' mode -- ).Run(t, src, func(t *testing.T, env *Env) { -- // Enable to debug go.sum mismatch, which may appear as -- // "module lookup disabled by GOPROXY=off", confusingly. -- if false { -- env.DumpGoSum(".") -- } +-[`(b.Embed).M` on pkg.go.dev](https://pkg.go.dev/mod.com/b#Embed.M) +--- @RField2 -- +-```go +-field Field2 int +-``` - -- env.OpenFile("a.go") -- refLoc := env.RegexpSearch("a.go", "K") // find "b.K" reference +-@loc(RField2, "Field2") - -- // Initially, b.K is defined in the module cache. -- gotLoc := env.GoToDefinition(refLoc) -- gotFile := env.Sandbox.Workdir.URIToPath(gotLoc.URI) -- wantCache := filepath.ToSlash(env.Sandbox.GOPATH()) + "/pkg/mod/other.com/b@v1.0.0/b.go" -- if gotFile != wantCache { -- t.Errorf("GoToDefinition, before: got file %q, want %q", gotFile, wantCache) -- } - -- // Run 'go mod vendor' outside the editor. -- if err := env.Sandbox.RunGoCommand(env.Ctx, ".", "mod", []string{"vendor"}, nil, true); err != nil { -- t.Fatalf("go mod vendor: %v", err) -- } +-[`(a.R).Field2` on pkg.go.dev](https://pkg.go.dev/mod.com/a#R.Field2) +--- @RHey -- +-```go +-func (r a.R) Hey() +-``` - -- // Synchronize changes to watched files. -- env.Await(env.DoneWithChangeWatchedFiles()) +-[`(a.R).Hey` on pkg.go.dev](https://pkg.go.dev/mod.com/a#R.Hey) +--- @S1 -- +-```go +-type S1 struct { +- F1 int //@loc(S1F1, "F1") +- S2 //@loc(S1S2, "S2"),def("S2", S2),hover("S2", "S2", S2) +- a.A //@def("A", AString),hover("A", "A", aA) +- aAlias //@def("a", aAlias),hover("a", "aAlias", aAlias) +-} +-``` - -- // Now, b.K is defined in the vendor tree. -- gotLoc = env.GoToDefinition(refLoc) -- wantVendor := "vendor/other.com/b/b.go" -- if gotFile != wantVendor { -- t.Errorf("GoToDefinition, after go mod vendor: got file %q, want %q", gotFile, wantVendor) -- } +-```go +-// Embedded fields: +-F2 int // through S2 +-``` - -- // Delete the vendor tree. -- if err := os.RemoveAll(env.Sandbox.Workdir.AbsPath("vendor")); err != nil { -- t.Fatal(err) -- } -- // Notify the server of the deletion. -- if err := env.Sandbox.Workdir.CheckForFileChanges(env.Ctx); err != nil { -- t.Fatal(err) -- } +-[`b.S1` on pkg.go.dev](https://pkg.go.dev/mod.com/b#S1) +--- @S1F1 -- +-```go +-field F1 int +-``` - -- // Synchronize again. -- env.Await(env.DoneWithChangeWatchedFiles()) +-@loc(S1F1, "F1") - -- // b.K is once again defined in the module cache. -- gotLoc = env.GoToDefinition(gotLoc) -- gotFile = env.Sandbox.Workdir.URIToPath(gotLoc.URI) -- if gotFile != wantCache { -- t.Errorf("GoToDefinition, after rm -rf vendor: got file %q, want %q", gotFile, wantCache) -- } -- }) +- +-[`(b.S1).F1` on pkg.go.dev](https://pkg.go.dev/mod.com/b#S1.F1) +--- @S1S2 -- +-```go +-field S2 S2 +-``` +- +-@loc(S1S2, "S2"),def("S2", S2),hover("S2", "S2", S2) +- +- +-[`(b.S1).S2` on pkg.go.dev](https://pkg.go.dev/mod.com/b#S1.S2) +--- @S2 -- +-```go +-type S2 struct { // size=32 (0x20) +- F1 string //@loc(S2F1, "F1") +- F2 int //@loc(S2F2, "F2") +- *a.A //@def("A", AString),def("a",AImport) -} +-``` - --const embedDefinition = ` ---- go.mod -- --module mod.com -- ---- main.go -- --package main -- --import ( -- "embed" --) +-```go +-func (a.A) Hi() +-``` - --//go:embed *.txt --var foo embed.FS +-[`b.S2` on pkg.go.dev](https://pkg.go.dev/mod.com/b#S2) +--- @S2F1 -- +-```go +-field F1 string +-``` - --func main() {} +-@loc(S2F1, "F1") - ---- skip.sql -- --SKIP - ---- foo.txt -- --FOO +-[`(b.S2).F1` on pkg.go.dev](https://pkg.go.dev/mod.com/b#S2.F1) +--- @S2F2 -- +-```go +-field F2 int +-``` - ---- skip.bat -- --SKIP --` +-@loc(S2F2, "F2") - --func TestGoToEmbedDefinition(t *testing.T) { -- Run(t, embedDefinition, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") - -- start := env.RegexpSearch("main.go", `\*.txt`) -- loc := env.GoToDefinition(start) +-[`(b.S2).F2` on pkg.go.dev](https://pkg.go.dev/mod.com/b#S2.F2) +--- @SField -- +-```go +-field Field int +-``` - -- name := env.Sandbox.Workdir.URIToPath(loc.URI) -- if want := "foo.txt"; name != want { -- t.Errorf("GoToDefinition: got file %q, want %q", name, want) -- } -- }) --} -diff -urN a/gopls/internal/regtest/misc/embed_test.go b/gopls/internal/regtest/misc/embed_test.go ---- a/gopls/internal/regtest/misc/embed_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/misc/embed_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,40 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. --package misc +-@loc(SField, "Field") - --import ( -- "testing" - -- . "golang.org/x/tools/gopls/internal/lsp/regtest" --) +-[`(a.S).Field` on pkg.go.dev](https://pkg.go.dev/mod.com/a#S.Field) +--- @aA -- +-```go +-type A string // size=16 (0x10) +-``` - --func TestMissingPatternDiagnostic(t *testing.T) { -- const files = ` ---- go.mod -- --module example.com ---- x.go -- --package x +-@loc(AString, "A") - --import ( -- _ "embed" --) - --// Issue 47436 --func F() {} +-```go +-func (a.A) Hi() +-``` - --//go:embed NONEXISTENT --var foo string --` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("x.go") -- env.AfterChange( -- Diagnostics( -- env.AtRegexp("x.go", `NONEXISTENT`), -- WithMessage("no matching files found"), -- ), -- ) -- env.RegexpReplace("x.go", `NONEXISTENT`, "x.go") -- env.AfterChange(NoDiagnostics(ForFile("x.go"))) -- }) --} -diff -urN a/gopls/internal/regtest/misc/extract_test.go b/gopls/internal/regtest/misc/extract_test.go ---- a/gopls/internal/regtest/misc/extract_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/misc/extract_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,65 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. --package misc +-[`a.A` on pkg.go.dev](https://pkg.go.dev/mod.com/a#A) +--- @aAlias -- +-```go +-type aAlias = a.A // size=16 (0x10) +-``` - --import ( -- "testing" +-@loc(aAlias, "aAlias") - -- . "golang.org/x/tools/gopls/internal/lsp/regtest" -- "golang.org/x/tools/gopls/internal/lsp/tests/compare" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" --) +-```go +-func (a.A) Hi() +-``` +diff -urN a/gopls/internal/test/marker/testdata/definition/import.txt b/gopls/internal/test/marker/testdata/definition/import.txt +--- a/gopls/internal/test/marker/testdata/definition/import.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/definition/import.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,53 +0,0 @@ +-This test checks definition and hover over imports. - --func TestExtractFunction(t *testing.T) { -- const files = ` --- go.mod -- -module mod.com - --go 1.12 ---- main.go -- --package main +-go 1.18 +--- foo/foo.go -- +-package foo - --func Foo() int { -- a := 5 -- return a --} --` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- loc := env.RegexpSearch("main.go", `a := 5\n.*return a`) -- actions, err := env.Editor.CodeAction(env.Ctx, loc, nil) -- if err != nil { -- t.Fatal(err) -- } +-type Foo struct{} - -- // Find the extract function code action. -- var extractFunc *protocol.CodeAction -- for _, action := range actions { -- if action.Kind == protocol.RefactorExtract && action.Title == "Extract function" { -- extractFunc = &action -- break -- } -- } -- if extractFunc == nil { -- t.Fatal("could not find extract function action") -- } +-// DoFoo does foo. +-func DoFoo() {} //@loc(DoFoo, "DoFoo") +--- bar/bar.go -- +-package bar - -- env.ApplyCodeAction(*extractFunc) -- want := `package main +-import ( +- myFoo "mod.com/foo" //@loc(myFoo, "myFoo") +-) - --func Foo() int { -- return newFunction() --} +-var _ *myFoo.Foo //@def("myFoo", myFoo),hover("myFoo", "myFoo", myFoo) +--- bar/dotimport.go -- +-package bar - --func newFunction() int { -- a := 5 -- return a --} --` -- if got := env.BufferText("main.go"); got != want { -- t.Fatalf("TestFillStruct failed:\n%s", compare.Text(want, got)) -- } -- }) --} -diff -urN a/gopls/internal/regtest/misc/failures_test.go b/gopls/internal/regtest/misc/failures_test.go ---- a/gopls/internal/regtest/misc/failures_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/misc/failures_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,82 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-import . "mod.com/foo" - --package misc +-func _() { +- // variable of type foo.Foo +- var _ Foo //@hover("_", "_", FooVar) - --import ( -- "testing" +- DoFoo() //@hover("DoFoo", "DoFoo", DoFoo) +-} +--- @DoFoo -- +-```go +-func DoFoo() +-``` - -- . "golang.org/x/tools/gopls/internal/lsp/regtest" -- "golang.org/x/tools/gopls/internal/lsp/tests/compare" --) +-DoFoo does foo. - --// This is a slight variant of TestHoverOnError in definition_test.go --// that includes a line directive, which makes no difference since --// gopls ignores line directives. --func TestHoverFailure(t *testing.T) { -- const mod = ` ---- go.mod -- --module mod.com - --go 1.12 ---- a.y -- --DWIM(main) +-[`foo.DoFoo` on pkg.go.dev](https://pkg.go.dev/mod.com/foo#DoFoo) +--- @FooVar -- +-```go +-var _ Foo +-``` - ---- main.go -- --//line a.y:1 --package main +-variable of type foo.Foo +--- @myFoo -- +-```go +-package myFoo ("mod.com/foo") +-``` - --func main() { -- var err error -- err.Error() --}` -- Run(t, mod, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- content, _ := env.Hover(env.RegexpSearch("main.go", "Error")) -- if content == nil { -- t.Fatalf("Hover('Error') returned nil") -- } -- want := "```go\nfunc (error).Error() string\n```" -- if content.Value != want { -- t.Fatalf("wrong Hover('Error') content:\n%s", compare.Text(want, content.Value)) -- } -- }) --} +-[`myFoo` on pkg.go.dev](https://pkg.go.dev/mod.com/foo) +diff -urN a/gopls/internal/test/marker/testdata/definition/misc.txt b/gopls/internal/test/marker/testdata/definition/misc.txt +--- a/gopls/internal/test/marker/testdata/definition/misc.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/definition/misc.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,241 +0,0 @@ +-This test exercises miscellaneous definition and hover requests. +- +-Its size expectations assume a 64-bit machine. - --// This test demonstrates a case where gopls is not at all confused by --// line directives, because it completely ignores them. --func TestFailingDiagnosticClearingOnEdit(t *testing.T) { -- // badPackageDup contains a duplicate definition of the 'a' const. -- // This is a minor variant of TestDiagnosticClearingOnEdit from -- // diagnostics_test.go, with a line directive, which makes no difference. -- const badPackageDup = ` --- go.mod -- -module mod.com - --go 1.12 ---- a.go -- --package consts -- --const a = 1 ---- b.go -- --package consts --//line gen.go:5 --const a = 2 --` -- -- Run(t, badPackageDup, func(t *testing.T, env *Env) { -- env.OpenFile("b.go") -- env.AfterChange( -- Diagnostics(env.AtRegexp("b.go", `a = 2`), WithMessage("a redeclared")), -- Diagnostics(env.AtRegexp("a.go", `a = 1`), WithMessage("other declaration")), -- ) +-go 1.16 - -- // Fix the error by editing the const name in b.go to `b`. -- env.RegexpReplace("b.go", "(a) = 2", "b") -- env.AfterChange( -- NoDiagnostics(ForFile("a.go")), -- NoDiagnostics(ForFile("b.go")), -- ) -- }) --} -diff -urN a/gopls/internal/regtest/misc/fix_test.go b/gopls/internal/regtest/misc/fix_test.go ---- a/gopls/internal/regtest/misc/fix_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/misc/fix_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,136 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +--- flags -- +--skip_goarch=386,arm - --package misc +--- a.go -- +-package a //@loc(aPackage, re"package (a)"),hover(aPackage, aPackage, aPackage) - --import ( -- "testing" +-var ( +- // x is a variable. +- x string //@loc(x, "x"),hover(x, x, hoverx) +-) - -- . "golang.org/x/tools/gopls/internal/lsp/regtest" -- "golang.org/x/tools/gopls/internal/lsp/tests/compare" +-// Constant block. When I hover on h, I should see this comment. +-const ( +- // When I hover on g, I should see this comment. +- g = 1 //@hover("g", "g", hoverg) - -- "golang.org/x/tools/gopls/internal/lsp/protocol" +- h = 2 //@hover("h", "h", hoverh) -) - --// A basic test for fillstruct, now that it uses a command. --func TestFillStruct(t *testing.T) { -- const basic = ` ---- go.mod -- --module mod.com -- --go 1.14 ---- main.go -- --package main +-// z is a variable too. +-var z string //@loc(z, "z"),hover(z, z, hoverz) - --type Info struct { -- WordCounts map[string]int -- Words []string +-func AStuff() { //@loc(AStuff, "AStuff") +- x := 5 +- Random2(x) //@def("dom2", Random2) +- Random() //@def("()", Random) -} - --func Foo() { -- _ = Info{} +-type H interface { //@loc(H, "H") +- Goodbye() -} --` -- Run(t, basic, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- if err := env.Editor.RefactorRewrite(env.Ctx, env.RegexpSearch("main.go", "Info{}")); err != nil { -- t.Fatal(err) -- } -- want := `package main - --type Info struct { -- WordCounts map[string]int -- Words []string +-type I interface { //@loc(I, "I") +- B() +- J -} - --func Foo() { -- _ = Info{ -- WordCounts: map[string]int{}, -- Words: []string{}, -- } --} --` -- if got := env.BufferText("main.go"); got != want { -- t.Fatalf("TestFillStruct failed:\n%s", compare.Text(want, got)) -- } -- }) +-type J interface { //@loc(J, "J") +- Hello() -} - --func TestFillReturns(t *testing.T) { -- const files = ` ---- go.mod -- --module mod.com +-func _() { +- // 1st type declaration block +- type ( +- a struct { //@hover("a", "a", hoverDeclBlocka) +- x string +- } +- ) - --go 1.12 ---- main.go -- --package main +- // 2nd type declaration block +- type ( +- // b has a comment +- b struct{} //@hover("b", "b", hoverDeclBlockb) +- ) - --func Foo() error { -- return --} --` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- var d protocol.PublishDiagnosticsParams -- env.AfterChange( -- // The error message here changed in 1.18; "return values" covers both forms. -- Diagnostics(env.AtRegexp("main.go", `return`), WithMessage("return values")), -- ReadDiagnostics("main.go", &d), -- ) -- codeActions := env.CodeAction("main.go", d.Diagnostics) -- if len(codeActions) != 2 { -- t.Fatalf("expected 2 code actions, got %v", len(codeActions)) -- } -- var foundQuickFix, foundFixAll bool -- for _, a := range codeActions { -- if a.Kind == protocol.QuickFix { -- foundQuickFix = true -- } -- if a.Kind == protocol.SourceFixAll { -- foundFixAll = true -- } -- } -- if !foundQuickFix { -- t.Fatalf("expected quickfix code action, got none") -- } -- if !foundFixAll { -- t.Fatalf("expected fixall code action, got none") +- // 3rd type declaration block +- type ( +- // c is a struct +- c struct { //@hover("c", "c", hoverDeclBlockc) +- f string - } -- env.ApplyQuickFixes("main.go", d.Diagnostics) -- env.AfterChange(NoDiagnostics(ForFile("main.go"))) -- }) +- +- d string //@hover("d", "d", hoverDeclBlockd) +- ) +- +- type ( +- e struct { //@hover("e", "e", hoverDeclBlocke) +- f float64 +- } // e has a comment +- ) -} - --func TestUnusedParameter_Issue63755(t *testing.T) { -- // This test verifies the fix for #63755, where codeActions panicked on parameters -- // of functions with no function body. +-var ( +- hh H //@hover("H", "H", hoverH) +- ii I //@hover("I", "I", hoverI) +- jj J //@hover("J", "J", hoverJ) +-) +--- a_test.go -- +-package a - -- // We should not detect parameters as unused for external functions. +-import ( +- "testing" +-) - -- const files = ` ---- go.mod -- --module unused.mod +-func TestA(t *testing.T) { //@hover("TestA", "TestA", hoverTestA) +-} +--- random.go -- +-package a - --go 1.18 +-func Random() int { //@loc(Random, "Random") +- y := 6 + 7 +- return y +-} - ---- external.go -- --package external +-func Random2(y int) int { //@loc(Random2, "Random2"),loc(RandomParamY, "y") +- return y //@def("y", RandomParamY),hover("y", "y", hovery) +-} +- +-type Pos struct { +- x, y int //@loc(PosX, "x"),loc(PosY, "y") +-} - --func External(z int) //@codeaction("refactor.rewrite", "z", "z", recursive) +-// Typ has a comment. Its fields do not. +-type Typ struct{ field string } //@loc(TypField, "field") - -func _() { -- External(1) --} -- ` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("external.go") -- actions, err := env.Editor.CodeAction(env.Ctx, env.RegexpSearch("external.go", "z"), nil) -- if err != nil { -- t.Fatal(err) -- } -- if len(actions) > 0 { -- t.Errorf("CodeAction(): got %d code actions, want 0", len(actions)) -- } -- }) +- x := &Typ{} +- _ = x.field //@def("field", TypField),hover("field", "field", hoverfield) -} -diff -urN a/gopls/internal/regtest/misc/formatting_test.go b/gopls/internal/regtest/misc/formatting_test.go ---- a/gopls/internal/regtest/misc/formatting_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/misc/formatting_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,395 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package misc +-func (p *Pos) Sum() int { //@loc(PosSum, "Sum") +- return p.x + p.y //@hover("x", "x", hoverpx) +-} - --import ( -- "strings" -- "testing" +-func _() { +- var p Pos +- _ = p.Sum() //@def("()", PosSum),hover("()", `Sum`, hoverSum) +-} +--- @aPackage -- +--- @hoverDeclBlocka -- +-```go +-type a struct { // size=16 (0x10) +- x string +-} +-``` - -- . "golang.org/x/tools/gopls/internal/lsp/regtest" -- "golang.org/x/tools/gopls/internal/lsp/tests/compare" -- "golang.org/x/tools/internal/testenv" --) +-1st type declaration block +--- @hoverDeclBlockb -- +-```go +-type b struct{} // size=0 +-``` - --const unformattedProgram = ` ---- main.go -- --package main --import "fmt" --func main( ) { -- fmt.Println("Hello World.") +-b has a comment +--- @hoverDeclBlockc -- +-```go +-type c struct { // size=16 (0x10) +- f string -} ---- main.go.golden -- --package main +-``` - --import "fmt" +-c is a struct +--- @hoverDeclBlockd -- +-```go +-type d string // size=16 (0x10) +-``` - --func main() { -- fmt.Println("Hello World.") +-3rd type declaration block +--- @hoverDeclBlocke -- +-```go +-type e struct { // size=8 +- f float64 -} --` +-``` - --func TestFormatting(t *testing.T) { -- Run(t, unformattedProgram, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- env.FormatBuffer("main.go") -- got := env.BufferText("main.go") -- want := env.ReadWorkspaceFile("main.go.golden") -- if got != want { -- t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) -- } -- }) +-e has a comment +--- @hoverH -- +-```go +-type H interface { +- Goodbye() -} +-``` - --// Tests golang/go#36824. --func TestFormattingOneLine36824(t *testing.T) { -- const onelineProgram = ` ---- a.go -- --package main; func f() {} +-[`a.H` on pkg.go.dev](https://pkg.go.dev/mod.com#H) +--- @hoverI -- +-```go +-type I interface { +- B() +- J +-} +-``` - ---- a.go.formatted -- --package main +-```go +-func (J) Hello() +-``` - --func f() {} --` -- Run(t, onelineProgram, func(t *testing.T, env *Env) { -- env.OpenFile("a.go") -- env.FormatBuffer("a.go") -- got := env.BufferText("a.go") -- want := env.ReadWorkspaceFile("a.go.formatted") -- if got != want { -- t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) -- } -- }) +-[`a.I` on pkg.go.dev](https://pkg.go.dev/mod.com#I) +--- @hoverJ -- +-```go +-type J interface { +- Hello() -} +-``` - --// Tests golang/go#36824. --func TestFormattingOneLineImports36824(t *testing.T) { -- const onelineProgramA = ` ---- a.go -- --package x; func f() {fmt.Println()} +-[`a.J` on pkg.go.dev](https://pkg.go.dev/mod.com#J) +--- @hoverSum -- +-```go +-func (p *Pos) Sum() int +-``` - ---- a.go.imported -- --package x +-[`(a.Pos).Sum` on pkg.go.dev](https://pkg.go.dev/mod.com#Pos.Sum) +--- @hoverTestA -- +-```go +-func TestA(t *testing.T) +-``` +--- @hoverfield -- +-```go +-field field string +-``` +--- @hoverg -- +-```go +-const g untyped int = 1 +-``` - --import "fmt" +-When I hover on g, I should see this comment. +--- @hoverh -- +-```go +-const h untyped int = 2 +-``` - --func f() { fmt.Println() } --` -- Run(t, onelineProgramA, func(t *testing.T, env *Env) { -- env.OpenFile("a.go") -- env.OrganizeImports("a.go") -- got := env.BufferText("a.go") -- want := env.ReadWorkspaceFile("a.go.imported") -- if got != want { -- t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) -- } -- }) --} +-Constant block. When I hover on h, I should see this comment. +--- @hoverpx -- +-```go +-field x int +-``` - --func TestFormattingOneLineRmImports36824(t *testing.T) { -- const onelineProgramB = ` ---- a.go -- --package x; import "os"; func f() {} +-@loc(PosX, "x"),loc(PosY, "y") +--- @hoverx -- +-```go +-var x string +-``` - ---- a.go.imported -- --package x +-x is a variable. +--- @hovery -- +-```go +-var y int +-``` +--- @hoverz -- +-```go +-var z string +-``` - --func f() {} --` -- Run(t, onelineProgramB, func(t *testing.T, env *Env) { -- env.OpenFile("a.go") -- env.OrganizeImports("a.go") -- got := env.BufferText("a.go") -- want := env.ReadWorkspaceFile("a.go.imported") -- if got != want { -- t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) -- } -- }) --} +-z is a variable too. +diff -urN a/gopls/internal/test/marker/testdata/definition/standalone.txt b/gopls/internal/test/marker/testdata/definition/standalone.txt +--- a/gopls/internal/test/marker/testdata/definition/standalone.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/definition/standalone.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,42 +0,0 @@ +-This test checks the behavior of standalone packages, in particular documenting +-our failure to support test files as standalone packages (golang/go#64233). - --const disorganizedProgram = ` ---- main.go -- --package main +--- go.mod -- +-module golang.org/lsptests/a - --import ( -- "fmt" -- "errors" --) --func main( ) { -- fmt.Println(errors.New("bad")) --} ---- main.go.organized -- --package main +-go 1.20 - --import ( -- "errors" -- "fmt" --) --func main( ) { -- fmt.Println(errors.New("bad")) --} ---- main.go.formatted -- +--- a.go -- +-package a +- +-func F() {} //@loc(F, "F") +- +--- standalone.go -- +-//go:build ignore -package main - --import ( -- "errors" -- "fmt" --) +-import "golang.org/lsptests/a" - -func main() { -- fmt.Println(errors.New("bad")) --} --` -- --func TestOrganizeImports(t *testing.T) { -- Run(t, disorganizedProgram, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- env.OrganizeImports("main.go") -- got := env.BufferText("main.go") -- want := env.ReadWorkspaceFile("main.go.organized") -- if got != want { -- t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) -- } -- }) +- a.F() //@def("F", F) -} - --func TestFormattingOnSave(t *testing.T) { -- Run(t, disorganizedProgram, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- env.SaveBuffer("main.go") -- got := env.BufferText("main.go") -- want := env.ReadWorkspaceFile("main.go.formatted") -- if got != want { -- t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) -- } -- }) --} +--- standalone_test.go -- +-//go:build ignore +-package main //@diag("main", re"No packages found") - --// Tests various possibilities for comments in files with CRLF line endings. --// Import organization in these files has historically been a source of bugs. --func TestCRLFLineEndings(t *testing.T) { -- for _, tt := range []struct { -- issue, input, want string -- }{ -- { -- issue: "41057", -- want: `package main +-import "golang.org/lsptests/a" - --/* --Hi description --*/ --func Hi() { +-func main() { +- a.F() //@hovererr("F", "no package") -} --`, -- }, -- { -- issue: "42646", -- want: `package main -- --import ( -- "fmt" --) - --/* --func upload(c echo.Context) error { -- if err := r.ParseForm(); err != nil { -- fmt.Fprintf(w, "ParseForm() err: %v", err) -- return -- } -- fmt.Fprintf(w, "POST request successful") -- path_ver := r.FormValue("path_ver") -- ukclin_ver := r.FormValue("ukclin_ver") +--- standalone_x_test.go -- +-//go:build ignore +-package main_test //@diag("main", re"No packages found") - -- fmt.Fprintf(w, "Name = %s\n", path_ver) -- fmt.Fprintf(w, "Address = %s\n", ukclin_ver) --} --*/ +-import "golang.org/lsptests/a" - -func main() { -- const server_port = 8080 -- fmt.Printf("port: %d\n", server_port) +- a.F() //@hovererr("F", "no package") -} --`, -- }, -- { -- issue: "42923", -- want: `package main +diff -urN a/gopls/internal/test/marker/testdata/diagnostics/addgowork.txt b/gopls/internal/test/marker/testdata/diagnostics/addgowork.txt +--- a/gopls/internal/test/marker/testdata/diagnostics/addgowork.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/diagnostics/addgowork.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,51 +0,0 @@ +-This test demonstrates diagnostics for adding a go.work file. - --// Line 1. --// aa --type Tree struct { -- arr []string --} --`, -- }, -- { -- issue: "47200", -- input: `package main +-Quick-fixes change files on disk, so are tested by integration tests. - --import "fmt" +-TODO(rfindley): improve the "cannot find package" import errors. - --func main() { -- math.Sqrt(9) -- fmt.Println("hello") --} --`, -- want: `package main +--- skip -- +-These diagnostics are no longer produced, because in golang/go#57979 +-(zero-config gopls) we made gopls function independent of a go.work file. +-Preserving this test as we may want to re-enable the code actions go manage +-a go.work file. - --import ( -- "fmt" -- "math" --) +-Note that in go.dev/issue/60584#issuecomment-1622238115, this test was flaky. +-However, critical error logic has since been rewritten. - --func main() { -- math.Sqrt(9) -- fmt.Println("hello") --} --`, -- }, -- } { -- t.Run(tt.issue, func(t *testing.T) { -- Run(t, "-- main.go --", func(t *testing.T, env *Env) { -- input := tt.input -- if input == "" { -- input = tt.want -- } -- crlf := strings.ReplaceAll(input, "\n", "\r\n") -- env.CreateBuffer("main.go", crlf) -- env.Await(env.DoneWithOpen()) -- env.OrganizeImports("main.go") -- got := env.BufferText("main.go") -- got = strings.ReplaceAll(got, "\r\n", "\n") // convert everything to LF for simplicity -- if tt.want != got { -- t.Errorf("unexpected content after save:\n%s", compare.Text(tt.want, got)) -- } -- }) -- }) -- } --} +--- a/go.mod -- +-module mod.com/a - --func TestFormattingOfGeneratedFile_Issue49555(t *testing.T) { -- const input = ` ---- main.go -- --// Code generated by generator.go. DO NOT EDIT. +-go 1.18 - --package main +--- a/main.go -- +-package main //@diag("main", re"add a go.work file") - --import "fmt" +-import "mod.com/a/lib" //@diag("\"mod.com", re"cannot find package") - -func main() { +- _ = lib.C +-} - +--- a/lib/lib.go -- +-package lib //@diag("lib", re"add a go.work file") - +-const C = "b" +--- b/go.mod -- +-module mod.com/b - +-go 1.18 - -- fmt.Print("hello") --} --` +--- b/main.go -- +-package main //@diag("main", re"add a go.work file") - -- Run(t, input, func(t *testing.T, env *Env) { -- wantErrSuffix := "file is generated" +-import "mod.com/b/lib" //@diag("\"mod.com", re"cannot find package") - -- env.OpenFile("main.go") -- err := env.Editor.FormatBuffer(env.Ctx, "main.go") -- if err == nil { -- t.Fatal("expected error, got nil") -- } -- // Check only the suffix because an error contains a dynamic path to main.go -- if !strings.HasSuffix(err.Error(), wantErrSuffix) { -- t.Fatalf("unexpected error %q, want suffix %q", err.Error(), wantErrSuffix) -- } -- }) +-func main() { +- _ = lib.C -} - --func TestGofumptFormatting(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) +--- b/lib/lib.go -- +-package lib //@diag("lib", re"add a go.work file") +- +-const C = "b" +diff -urN a/gopls/internal/test/marker/testdata/diagnostics/analyzers.txt b/gopls/internal/test/marker/testdata/diagnostics/analyzers.txt +--- a/gopls/internal/test/marker/testdata/diagnostics/analyzers.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/diagnostics/analyzers.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,68 +0,0 @@ +-Test of warning diagnostics from various analyzers: +-copylocks, printf, slog, tests, timeformat, and nilness. - -- // Exercise some gofumpt formatting rules: -- // - No empty lines following an assignment operator -- // - Octal integer literals should use the 0o prefix on modules using Go -- // 1.13 and later. Requires LangVersion to be correctly resolved. -- // - std imports must be in a separate group at the top. Requires ModulePath -- // to be correctly resolved. -- const input = ` --- go.mod -- --module foo +-module example.com +-go 1.12 - --go 1.17 ---- foo.go -- --package foo +--- flags -- +--min_go=go1.21 +- +--- bad_test.go -- +-package analyzer - -import ( -- "foo/bar" - "fmt" +- "sync" +- "testing" +- "time" -) - --const perm = 0755 -- --func foo() { -- foo := -- "bar" -- fmt.Println(foo, bar.Bar) +-// copylocks +-func _() { +- var x sync.Mutex +- _ = x //@diag("x", re"assignment copies lock value to _: sync.Mutex") -} ---- foo.go.formatted -- --package foo - --import ( -- "fmt" +-// printf +-func _() { +- printfWrapper("%s") //@diag(re`printfWrapper\(.*\)`, re"example.com.printfWrapper format %s reads arg #1, but call has 0 args") +-} - -- "foo/bar" --) +-func printfWrapper(format string, args ...interface{}) { +- fmt.Printf(format, args...) +-} - --const perm = 0o755 +-// tests +-func Testbad(t *testing.T) { //@diag("", re"Testbad has malformed name: first letter after 'Test' must not be lowercase") +-} - --func foo() { -- foo := "bar" -- fmt.Println(foo, bar.Bar) +-// timeformat +-func _() { +- now := time.Now() +- fmt.Println(now.Format("2006-02-01")) //@diag("2006-02-01", re"2006-02-01 should be 2006-01-02") -} ---- bar/bar.go -- --package bar - --const Bar = 42 --` +-// nilness +-func _(ptr *int) { +- if ptr == nil { +- _ = *ptr //@diag("*ptr", re"nil dereference in load") +- } +-} - -- WithOptions( -- Settings{ -- "gofumpt": true, -- }, -- ).Run(t, input, func(t *testing.T, env *Env) { -- env.OpenFile("foo.go") -- env.FormatBuffer("foo.go") -- got := env.BufferText("foo.go") -- want := env.ReadWorkspaceFile("foo.go.formatted") -- if got != want { -- t.Errorf("unexpected formatting result:\n%s", compare.Text(want, got)) -- } -- }) +-// unusedwrite +-func _(s struct{x int}) { +- s.x = 1 //@diag("x", re"unused write to field x") -} - --func TestGofumpt_Issue61692(t *testing.T) { -- testenv.NeedsGo1Point(t, 21) +--- bad_test_go121.go -- +-//go:build go1.21 - -- const input = ` ---- go.mod -- --module foo +-package analyzer - --go 1.21rc3 ---- foo.go -- --package foo +-import "log/slog" - +-// slog -func _() { -- foo := -- "bar" --} --` -- -- WithOptions( -- Settings{ -- "gofumpt": true, -- }, -- ).Run(t, input, func(t *testing.T, env *Env) { -- env.OpenFile("foo.go") -- env.FormatBuffer("foo.go") // golang/go#61692: must not panic -- }) +- slog.Info("msg", 1) //@diag("1", re`slog.Info arg "1" should be a string or a slog.Attr`) -} -diff -urN a/gopls/internal/regtest/misc/generate_test.go b/gopls/internal/regtest/misc/generate_test.go ---- a/gopls/internal/regtest/misc/generate_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/misc/generate_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,71 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --// TODO(rfindley): figure out why go generate fails on android builders. -- --//go:build !android --// +build !android +diff -urN a/gopls/internal/test/marker/testdata/diagnostics/excludedfile.txt b/gopls/internal/test/marker/testdata/diagnostics/excludedfile.txt +--- a/gopls/internal/test/marker/testdata/diagnostics/excludedfile.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/diagnostics/excludedfile.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,36 +0,0 @@ +-This test demonstrates diagnostics for various forms of file exclusion. - --package misc +-Note: this test used to also check the errors when a file was excluded due to +-an inactive module, or mismatching GOOS/GOARCH, comment, but with zero-config +-gopls (golang/go#57979) and improved build tag support (golang/go#29202), we no +-longer get these errors. - --import ( -- "testing" +--- go.work -- +-go 1.21 - -- . "golang.org/x/tools/gopls/internal/lsp/regtest" +-use ( +- ./a -) +--- a/go.mod -- +-module mod.com/a - --func TestGenerateProgress(t *testing.T) { -- const generatedWorkspace = ` ---- go.mod -- --module fake.test -- --go 1.14 ---- generate.go -- --// +build ignore +-go 1.18 - --package main +--- a/a.go -- +-package a - --import ( -- "os" --) +--- a/a_plan9.go -- +-package a // Not excluded, due to improved build tag support. - --func main() { -- os.WriteFile("generated.go", []byte("package " + os.Args[1] + "\n\nconst Answer = 21"), 0644) --} +--- a/a_ignored.go -- +-//go:build skip +-package a //@diag(re"package (a)", re"excluded due to its build tags") - ---- lib1/lib.go -- --package lib1 +--- b/go.mod -- +-module mod.com/b - --//` + `go:generate go run ../generate.go lib1 +-go 1.18 - ---- lib2/lib.go -- --package lib2 +--- b/b.go -- +-package b // Not excluded, due to zero-config gopls. - --//` + `go:generate go run ../generate.go lib2 +diff -urN a/gopls/internal/test/marker/testdata/diagnostics/generated.txt b/gopls/internal/test/marker/testdata/diagnostics/generated.txt +--- a/gopls/internal/test/marker/testdata/diagnostics/generated.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/diagnostics/generated.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,21 +0,0 @@ +-Test of "undeclared" diagnostic in generated code. - ---- main.go -- --package main +--- go.mod -- +-module example.com +-go 1.12 - --import ( -- "fake.test/lib1" -- "fake.test/lib2" --) +--- generated.go -- +-package generated - --func main() { -- println(lib1.Answer + lib2.Answer) --} --` +-// Code generated by generator.go. DO NOT EDIT. - -- Run(t, generatedWorkspace, func(t *testing.T, env *Env) { -- env.OnceMet( -- InitialWorkspaceLoad, -- Diagnostics(env.AtRegexp("main.go", "lib1.(Answer)")), -- ) -- env.RunGenerate("./lib1") -- env.RunGenerate("./lib2") -- env.AfterChange( -- NoDiagnostics(ForFile("main.go")), -- ) -- }) +-func _() { +- var y int //@diag("y", re"y.*declared (and|but) not used") -} -diff -urN a/gopls/internal/regtest/misc/highlight_test.go b/gopls/internal/regtest/misc/highlight_test.go ---- a/gopls/internal/regtest/misc/highlight_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/misc/highlight_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,153 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package misc - --import ( -- "sort" -- "testing" +--- generator.go -- +-package generated - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" --) +-func _() { +- var x int //@diag("x", re"x.*declared (and|but) not used") +-} +diff -urN a/gopls/internal/test/marker/testdata/diagnostics/initcycle.txt b/gopls/internal/test/marker/testdata/diagnostics/initcycle.txt +--- a/gopls/internal/test/marker/testdata/diagnostics/initcycle.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/diagnostics/initcycle.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,17 +0,0 @@ +-This test verifies that gopls spreads initialization cycle errors across +-multiple declarations. - --func TestWorkspacePackageHighlight(t *testing.T) { -- const mod = ` ---- go.mod -- --module mod.com +-We set -ignore_extra_diags due to golang/go#65877: gopls produces redundant +-diagnostics for initialization cycles. - --go 1.12 ---- main.go -- --package main +--- flags -- +--ignore_extra_diags - --func main() { -- var A string = "A" -- x := "x-" + A -- println(A, x) --}` +--- p.go -- +-package p - -- Run(t, mod, func(t *testing.T, env *Env) { -- const file = "main.go" -- env.OpenFile(file) -- loc := env.GoToDefinition(env.RegexpSearch(file, `var (A) string`)) +-var X = Y //@diag("X", re"initialization cycle") - -- checkHighlights(env, loc, 3) -- }) --} +-var Y = Z //@diag("Y", re"initialization cycle") - --func TestStdPackageHighlight_Issue43511(t *testing.T) { -- const mod = ` ---- go.mod -- --module mod.com +-var Z = X //@diag("Z", re"initialization cycle") +diff -urN a/gopls/internal/test/marker/testdata/diagnostics/issue56943.txt b/gopls/internal/test/marker/testdata/diagnostics/issue56943.txt +--- a/gopls/internal/test/marker/testdata/diagnostics/issue56943.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/diagnostics/issue56943.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,22 +0,0 @@ +-This test verifies that we produce diagnostics related to mismatching +-unexported interface methods in non-workspace packages. - --go 1.12 +-Previously, we would fail to produce a diagnostic because we trimmed the AST. +-See golang/go#56943. --- main.go -- -package main - --import "fmt" +-import ( +- "go/ast" +- "go/token" +-) - -func main() { -- fmt.Printf() --}` -- -- Run(t, mod, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- defLoc := env.GoToDefinition(env.RegexpSearch("main.go", `fmt\.(Printf)`)) -- file := env.Sandbox.Workdir.URIToPath(defLoc.URI) -- loc := env.RegexpSearch(file, `func Printf\((format) string`) -- -- checkHighlights(env, loc, 2) -- }) +- var a int //@diag(re"(a) int", re"a.*declared.*not used") +- var _ ast.Expr = node{} //@diag("node{}", re"missing.*exprNode") -} - --func TestThirdPartyPackageHighlight_Issue43511(t *testing.T) { -- const proxy = ` ---- example.com@v1.2.3/go.mod -- --module example.com -- --go 1.12 ---- example.com@v1.2.3/global/global.go -- --package global +-type node struct{} - --const A = 1 +-func (node) Pos() token.Pos { return 0 } +-func (node) End() token.Pos { return 0 } +diff -urN a/gopls/internal/test/marker/testdata/diagnostics/issue59005.txt b/gopls/internal/test/marker/testdata/diagnostics/issue59005.txt +--- a/gopls/internal/test/marker/testdata/diagnostics/issue59005.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/diagnostics/issue59005.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,20 +0,0 @@ +-This test verifies that we don't drop type checking errors on the floor when we +-fail to compute positions for their related errors. - --func foo() { -- _ = A --} +--- go.mod -- +-module play.ground - --func bar() int { -- return A + A --} ---- example.com@v1.2.3/local/local.go -- --package local +--- p.go -- +-package p - --func foo() int { -- const b = 2 +-import ( +- . "play.ground/foo" +-) - -- return b * b * (b+1) + b --}` +-const C = 1 //@diag("C", re"C already declared through dot-import") +-var _ = C - -- const mod = ` ---- go.mod -- --module mod.com +--- foo/foo.go -- +-package foo - --go 1.12 +-const C = 2 +diff -urN a/gopls/internal/test/marker/testdata/diagnostics/issue60544.txt b/gopls/internal/test/marker/testdata/diagnostics/issue60544.txt +--- a/gopls/internal/test/marker/testdata/diagnostics/issue60544.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/diagnostics/issue60544.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,9 +0,0 @@ +-This test exercises a crash due to treatment of "comparable" in methodset +-calculation (golang/go#60544). - --require example.com v1.2.3 ---- go.sum -- --example.com v1.2.3 h1:WFzrgiQJwEDJNLDUOV1f9qlasQkvzXf2UNLaNIqbWsI= --example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= --- main.go -- -package main - --import ( -- _ "example.com/global" -- _ "example.com/local" --) +-type X struct{} - --func main() {}` +-func (X) test(x comparable) {} //@diag("comparable", re"outside a type constraint") +diff -urN a/gopls/internal/test/marker/testdata/diagnostics/issue60605.txt b/gopls/internal/test/marker/testdata/diagnostics/issue60605.txt +--- a/gopls/internal/test/marker/testdata/diagnostics/issue60605.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/diagnostics/issue60605.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,12 +0,0 @@ +-This test verifies that we can export constants with unknown kind. +-Previously, the exporter would panic while attempting to convert such constants +-to their target type (float64, in this case). - -- WithOptions( -- ProxyFiles(proxy), -- ).Run(t, mod, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") +--- go.mod -- +-module mod.txt/p - -- defLoc := env.GoToDefinition(env.RegexpSearch("main.go", `"example.com/global"`)) -- file := env.Sandbox.Workdir.URIToPath(defLoc.URI) -- loc := env.RegexpSearch(file, `const (A)`) -- checkHighlights(env, loc, 4) +-go 1.20 +--- p.go -- +-package p - -- defLoc = env.GoToDefinition(env.RegexpSearch("main.go", `"example.com/local"`)) -- file = env.Sandbox.Workdir.URIToPath(defLoc.URI) -- loc = env.RegexpSearch(file, `const (b)`) -- checkHighlights(env, loc, 5) -- }) --} +-const EPSILON float64 = 1e- //@diag(re"1e-()", re"exponent has no digits") +diff -urN a/gopls/internal/test/marker/testdata/diagnostics/issue64547.txt b/gopls/internal/test/marker/testdata/diagnostics/issue64547.txt +--- a/gopls/internal/test/marker/testdata/diagnostics/issue64547.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/diagnostics/issue64547.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,14 +0,0 @@ +-This test checks the fix for golang/go#64547: the lostcancel analyzer reports +-diagnostics that overflow the file. - --func checkHighlights(env *Env, loc protocol.Location, highlightCount int) { -- t := env.T -- t.Helper() +--- p.go -- +-package p - -- highlights := env.DocumentHighlight(loc) -- if len(highlights) != highlightCount { -- t.Fatalf("expected %v highlight(s), got %v", highlightCount, len(highlights)) -- } +-import "context" - -- references := env.References(loc) -- if len(highlights) != len(references) { -- t.Fatalf("number of highlights and references is expected to be equal: %v != %v", len(highlights), len(references)) +-func _() { +- _, cancel := context.WithCancel(context.Background()) //@diag("_, cancel", re"not used on all paths") +- if false { +- cancel() - } +-} //@diag("}", re"may be reached without using the cancel") +diff -urN a/gopls/internal/test/marker/testdata/diagnostics/osarch_suffix.txt b/gopls/internal/test/marker/testdata/diagnostics/osarch_suffix.txt +--- a/gopls/internal/test/marker/testdata/diagnostics/osarch_suffix.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/diagnostics/osarch_suffix.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,46 +0,0 @@ +-This test verifies that we add an [os,arch] suffix to each diagnostic +-that doesn't appear in the default build (=runtime.{GOOS,GOARCH}). - -- sort.Slice(highlights, func(i, j int) bool { -- return protocol.CompareRange(highlights[i].Range, highlights[j].Range) < 0 -- }) -- sort.Slice(references, func(i, j int) bool { -- return protocol.CompareRange(references[i].Range, references[j].Range) < 0 -- }) -- for i := range highlights { -- if highlights[i].Range != references[i].Range { -- t.Errorf("highlight and reference ranges are expected to be equal: %v != %v", highlights[i].Range, references[i].Range) -- } -- } --} -diff -urN a/gopls/internal/regtest/misc/hover_test.go b/gopls/internal/regtest/misc/hover_test.go ---- a/gopls/internal/regtest/misc/hover_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/misc/hover_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,493 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-See golang/go#65496. - --package misc +-The two p/*.go files below are written to trigger the same diagnostic +-(range, message, source, etc) but varying only by URI. - --import ( -- "fmt" -- "strings" -- "testing" +-In the q test, a single location in the common code q.go has two +-diagnostics, one of which is tagged. - -- "golang.org/x/tools/gopls/internal/lsp/fake" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" -- "golang.org/x/tools/internal/testenv" --) +-This test would fail on openbsd/mips64 because it will be +-the same as the default build, so we skip that platform. - --func TestHoverUnexported(t *testing.T) { -- const proxy = ` ---- golang.org/x/structs@v1.0.0/go.mod -- --module golang.org/x/structs +--- flags -- +--skip_goos=openbsd - --go 1.12 +--- go.mod -- +-module example.com - ---- golang.org/x/structs@v1.0.0/types.go -- --package structs +--- p/p.go -- +-package p - --type Mixed struct { -- // Exported comment -- Exported int -- unexported string --} +-var _ fmt.Stringer //@diag("fmt", re"unde.*: fmt$") - --func printMixed(m Mixed) { -- println(m) --} --` -- const mod = ` ---- go.mod -- --module mod.com +--- p/p_openbsd_mips64.go -- +-package p - --go 1.12 +-var _ fmt.Stringer //@diag("fmt", re"unde.*: fmt \\[openbsd,mips64\\]") - --require golang.org/x/structs v1.0.0 ---- go.sum -- --golang.org/x/structs v1.0.0 h1:Ito/a7hBYZaNKShFrZKjfBA/SIPvmBrcPCBWPx5QeKk= --golang.org/x/structs v1.0.0/go.mod h1:47gkSIdo5AaQaWJS0upVORsxfEr1LL1MWv9dmYF3iq4= ---- main.go -- --package main +--- q/q_default.go -- +-//+build !openbsd && !mips64 - --import "golang.org/x/structs" +-package q - --func main() { -- var m structs.Mixed -- _ = m.Exported --} --` +-func f(int) int - -- // TODO: use a nested workspace folder here. -- WithOptions( -- ProxyFiles(proxy), -- ).Run(t, mod, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- mixedLoc := env.RegexpSearch("main.go", "Mixed") -- got, _ := env.Hover(mixedLoc) -- if !strings.Contains(got.Value, "unexported") { -- t.Errorf("Workspace hover: missing expected field 'unexported'. Got:\n%q", got.Value) -- } +--- q/q_openbsd_mips64.go -- +-package q - -- cacheLoc := env.GoToDefinition(mixedLoc) -- cacheFile := env.Sandbox.Workdir.URIToPath(cacheLoc.URI) -- argLoc := env.RegexpSearch(cacheFile, "printMixed.*(Mixed)") -- got, _ = env.Hover(argLoc) -- if !strings.Contains(got.Value, "unexported") { -- t.Errorf("Non-workspace hover: missing expected field 'unexported'. Got:\n%q", got.Value) -- } +-func f(string) int - -- exportedFieldLoc := env.RegexpSearch("main.go", "Exported") -- got, _ = env.Hover(exportedFieldLoc) -- if !strings.Contains(got.Value, "comment") { -- t.Errorf("Workspace hover: missing comment for field 'Exported'. Got:\n%q", got.Value) -- } -- }) --} +--- q/q.go -- +-package q - --func TestHoverIntLiteral(t *testing.T) { -- const source = ` ---- main.go -- --package main +-var _ = f() //@ diag(")", re`.*want \(string\) \[openbsd,mips64\]`), diag(")", re`.*want \(int\)$`) +diff -urN a/gopls/internal/test/marker/testdata/diagnostics/parseerr.txt b/gopls/internal/test/marker/testdata/diagnostics/parseerr.txt +--- a/gopls/internal/test/marker/testdata/diagnostics/parseerr.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/diagnostics/parseerr.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,27 +0,0 @@ - --var ( -- bigBin = 0b1001001 --) +-This test exercises diagnostics produced for syntax errors. - --var hex = 0xe34e +-Because parser error recovery can be quite lossy, diagnostics +-for type errors are suppressed in files with syntax errors; +-see issue #59888. But diagnostics are reported for type errors +-in well-formed files of the same package. - --func main() { --} --` -- Run(t, source, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- hexExpected := "58190" -- got, _ := env.Hover(env.RegexpSearch("main.go", "0xe")) -- if got != nil && !strings.Contains(got.Value, hexExpected) { -- t.Errorf("Hover: missing expected field '%s'. Got:\n%q", hexExpected, got.Value) -- } +--- go.mod -- +-module example.com +-go 1.12 - -- binExpected := "73" -- got, _ = env.Hover(env.RegexpSearch("main.go", "0b1")) -- if got != nil && !strings.Contains(got.Value, binExpected) { -- t.Errorf("Hover: missing expected field '%s'. Got:\n%q", binExpected, got.Value) -- } -- }) +--- bad.go -- +-package p +- +-func f() { +- append("") // no diagnostic for type error in file containing syntax error -} - --// Tests that hovering does not trigger the panic in golang/go#48249. --func TestPanicInHoverBrokenCode(t *testing.T) { -- // Note: this test can not be expressed as a marker test, as it must use -- // content without a trailing newline. -- const source = ` ---- main.go -- --package main +-func .() {} //@diag(re"func ().", re"expected 'IDENT', found '.'") - --type Example struct` -- Run(t, source, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- env.Editor.Hover(env.Ctx, env.RegexpSearch("main.go", "Example")) -- }) +--- good.go -- +-package p +- +-func g() { +- append("") //@diag(re`""`, re"a slice") -} +diff -urN a/gopls/internal/test/marker/testdata/diagnostics/rundespiteerrors.txt b/gopls/internal/test/marker/testdata/diagnostics/rundespiteerrors.txt +--- a/gopls/internal/test/marker/testdata/diagnostics/rundespiteerrors.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/diagnostics/rundespiteerrors.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,21 +0,0 @@ +-This test verifies that analyzers without RunDespiteErrors are not +-executed on a package containing type errors (see issue #54762). - --func TestHoverRune_48492(t *testing.T) { -- const files = ` --- go.mod -- --module mod.com +-module example.com +-go 1.12 - --go 1.18 ---- main.go -- --package main --` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- env.EditBuffer("main.go", fake.NewEdit(0, 0, 1, 0, "package main\nfunc main() {\nconst x = `\nfoo\n`\n}")) -- env.Editor.Hover(env.Ctx, env.RegexpSearch("main.go", "foo")) -- }) --} +--- a.go -- +-package a +- +-func _() { +- // A type error. +- _ = 1 + "" //@diag(`1 + ""`, re"mismatched types|cannot convert") +- +- // A violation of an analyzer for which RunDespiteErrors=false: +- // no (simplifyrange, warning) diagnostic is produced; the diag +- // comment is merely illustrative. +- for _ = range "" { //diag("for _", "simplify range expression", ) - --func TestHoverImport(t *testing.T) { -- const packageDoc1 = "Package lib1 hover documentation" -- const packageDoc2 = "Package lib2 hover documentation" -- tests := []struct { -- hoverPackage string -- want string -- wantError bool -- }{ -- { -- "mod.com/lib1", -- packageDoc1, -- false, -- }, -- { -- "mod.com/lib2", -- packageDoc2, -- false, -- }, -- { -- "mod.com/lib3", -- "", -- false, -- }, -- { -- "mod.com/lib4", -- "", -- true, -- }, - } -- source := fmt.Sprintf(` ---- go.mod -- --module mod.com +-} +diff -urN a/gopls/internal/test/marker/testdata/diagnostics/stdversion.txt b/gopls/internal/test/marker/testdata/diagnostics/stdversion.txt +--- a/gopls/internal/test/marker/testdata/diagnostics/stdversion.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/diagnostics/stdversion.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,89 +0,0 @@ +-Test of "too new" diagnostics from the stdversion analyzer. - --go 1.12 ---- lib1/a.go -- --// %s --package lib1 +-This test references go1.21 symbols from std, but the analyzer itself +-depends on the go1.22 behavior of versions.FileVersion. - --const C = 1 +-See also go/analysis/passes/stdversion/testdata/test.txtar, +-which runs the same test in the analysistest framework. - ---- lib1/b.go -- --package lib1 +--- flags -- +--min_go=go1.22 - --const D = 1 +--- go.mod -- +-module example.com - ---- lib2/a.go -- --// %s --package lib2 +-go 1.21 - --const E = 1 +--- a/a.go -- +-package a - ---- lib3/a.go -- --package lib3 +-import "go/types" - --const F = 1 +-func _() { +- // old package-level type +- var _ types.Info // ok: defined by go1.0 - ---- main.go -- --package main +- // new field of older type +- _ = new(types.Info).FileVersions //@diag("FileVersions", re`types.FileVersions requires go1.22 or later \(module is go1.21\)`) - --import ( -- "mod.com/lib1" -- "mod.com/lib2" -- "mod.com/lib3" -- "mod.com/lib4" --) +- // new method of older type +- _ = new(types.Info).PkgNameOf //@diag("PkgNameOf", re`types.PkgNameOf requires go1.22 or later \(module is go1.21\)`) - --func main() { -- println("Hello") --} -- `, packageDoc1, packageDoc2) -- Run(t, source, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- for _, test := range tests { -- got, _, err := env.Editor.Hover(env.Ctx, env.RegexpSearch("main.go", test.hoverPackage)) -- if test.wantError { -- if err == nil { -- t.Errorf("Hover(%q) succeeded unexpectedly", test.hoverPackage) -- } -- } else if !strings.Contains(got.Value, test.want) { -- t.Errorf("Hover(%q): got:\n%q\nwant:\n%q", test.hoverPackage, got.Value, test.want) -- } -- } -- }) +- // new package-level type +- var a types.Alias //@diag("Alias", re`types.Alias requires go1.22 or later \(module is go1.21\)`) +- +- // new method of new type +- a.Underlying() // no diagnostic -} - --// for x/tools/gopls: unhandled named anchor on the hover #57048 --func TestHoverTags(t *testing.T) { -- const source = ` ---- go.mod -- --module mod.com +--- sub/go.mod -- +-module example.com/sub - --go 1.19 +-go 1.21 - ---- lib/a.go -- +--- sub/sub.go -- +-package sub - --// variety of execution modes. --// --// # Test package setup --// --// The regression test package uses a couple of uncommon patterns to reduce --package lib +-import "go/types" - ---- a.go -- -- package main -- import "mod.com/lib" +-func _() { +- // old package-level type +- var _ types.Info // ok: defined by go1.0 - -- const A = 1 +- // new field of older type +- _ = new(types.Info).FileVersions //@diag("FileVersions", re`types.FileVersions requires go1.22 or later \(module is go1.21\)`) - --} --` -- Run(t, source, func(t *testing.T, env *Env) { -- t.Run("tags", func(t *testing.T) { -- env.OpenFile("a.go") -- z := env.RegexpSearch("a.go", "lib") -- t.Logf("%#v", z) -- got, _ := env.Hover(env.RegexpSearch("a.go", "lib")) -- if strings.Contains(got.Value, "{#hdr-") { -- t.Errorf("Hover: got {#hdr- tag:\n%q", got) -- } -- }) -- }) --} +- // new method of older type +- _ = new(types.Info).PkgNameOf //@diag("PkgNameOf", re`types.PkgNameOf requires go1.22 or later \(module is go1.21\)`) - --// This is a regression test for Go issue #57625. --func TestHoverModMissingModuleStmt(t *testing.T) { -- const source = ` ---- go.mod -- --go 1.16 --` -- Run(t, source, func(t *testing.T, env *Env) { -- env.OpenFile("go.mod") -- env.Hover(env.RegexpSearch("go.mod", "go")) // no panic -- }) --} +- // new package-level type +- var a types.Alias //@diag("Alias", re`types.Alias requires go1.22 or later \(module is go1.21\)`) - --func TestHoverCompletionMarkdown(t *testing.T) { -- testenv.NeedsGo1Point(t, 19) -- const source = ` ---- go.mod -- --module mod.com --go 1.19 ---- main.go -- --package main --// Just says [hello]. --// --// [hello]: https://en.wikipedia.org/wiki/Hello --func Hello() string { -- Hello() //Here -- return "hello" +- // new method of new type +- a.Underlying() // no diagnostic -} --` -- Run(t, source, func(t *testing.T, env *Env) { -- // Hover, Completion, and SignatureHelp should all produce markdown -- // check that the markdown for SignatureHelp and Completion are -- // the same, and contained in that for Hover (up to trailing \n) -- env.OpenFile("main.go") -- loc := env.RegexpSearch("main.go", "func (Hello)") -- hover, _ := env.Hover(loc) -- hoverContent := hover.Value - -- loc = env.RegexpSearch("main.go", "//Here") -- loc.Range.Start.Character -= 3 // Hello(_) //Here -- completions := env.Completion(loc) -- signatures := env.SignatureHelp(loc) +--- sub/tagged.go -- +-//go:build go1.22 - -- if len(completions.Items) != 1 { -- t.Errorf("got %d completions, expected 1", len(completions.Items)) -- } -- if len(signatures.Signatures) != 1 { -- t.Errorf("got %d signatures, expected 1", len(signatures.Signatures)) -- } -- item := completions.Items[0].Documentation.Value -- var itemContent string -- if x, ok := item.(protocol.MarkupContent); !ok || x.Kind != protocol.Markdown { -- t.Fatalf("%#v is not markdown", item) -- } else { -- itemContent = strings.Trim(x.Value, "\n") -- } -- sig := signatures.Signatures[0].Documentation.Value -- var sigContent string -- if x, ok := sig.(protocol.MarkupContent); !ok || x.Kind != protocol.Markdown { -- t.Fatalf("%#v is not markdown", item) -- } else { -- sigContent = x.Value -- } -- if itemContent != sigContent { -- t.Errorf("item:%q not sig:%q", itemContent, sigContent) -- } -- if !strings.Contains(hoverContent, itemContent) { -- t.Errorf("hover:%q does not containt sig;%q", hoverContent, sigContent) -- } -- }) --} +-package sub - --// Test that the generated markdown contains links for Go references. --// https://github.com/golang/go/issues/58352 --func TestHoverLinks(t *testing.T) { -- testenv.NeedsGo1Point(t, 19) -- const input = ` ---- go.mod -- --go 1.19 --module mod.com ---- main.go -- --package main --// [fmt] --var A int --// [fmt.Println] --var B int --// [golang.org/x/tools/go/packages.Package.String] --var C int --` -- var tests = []struct { -- pat string -- ans string -- }{ -- {"A", "fmt"}, -- {"B", "fmt#Println"}, -- {"C", "golang.org/x/tools/go/packages#Package.String"}, -- } -- for _, test := range tests { -- Run(t, input, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- loc := env.RegexpSearch("main.go", test.pat) -- hover, _ := env.Hover(loc) -- hoverContent := hover.Value -- want := fmt.Sprintf("%s/%s", "https://pkg.go.dev", test.ans) -- if !strings.Contains(hoverContent, want) { -- t.Errorf("hover:%q does not contain link %q", hoverContent, want) -- } -- }) -- } +-import "go/types" +- +-func _() { +- // old package-level type +- var _ types.Info +- +- // new field of older type +- _ = new(types.Info).FileVersions +- +- // new method of older type +- _ = new(types.Info).PkgNameOf +- +- // new package-level type +- var a types.Alias +- +- // new method of new type +- a.Underlying() -} - --const linknameHover = ` +diff -urN a/gopls/internal/test/marker/testdata/diagnostics/strangefiles.txt b/gopls/internal/test/marker/testdata/diagnostics/strangefiles.txt +--- a/gopls/internal/test/marker/testdata/diagnostics/strangefiles.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/diagnostics/strangefiles.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,21 +0,0 @@ +-This test checks diagnostics on files that are strange for one reason or +-another. +- +-Note(rfindley): ported from the old marker tests. I'm not sure why these were +-written originally. +- +--ignore_extra_diags is required because the marker framework fails for +-noparse.go, and we therefore can't match the EOF error. +- +--- flags -- +--ignore_extra_diags +- --- go.mod -- --module mod.com +-module golang.org/lsptests - ---- upper/upper.go -- --package upper +-go 1.18 +--- %percent/perc%ent.go -- +-package percent //@diag("percent", re"No packages") - --import ( -- _ "unsafe" -- _ "mod.com/lower" --) +--- noparse/noparse.go -- - --//go:linkname foo mod.com/lower.bar --func foo() string +diff -urN a/gopls/internal/test/marker/testdata/diagnostics/typeerr.txt b/gopls/internal/test/marker/testdata/diagnostics/typeerr.txt +--- a/gopls/internal/test/marker/testdata/diagnostics/typeerr.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/diagnostics/typeerr.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,28 +0,0 @@ - ---- lower/lower.go -- --package lower +-This test exercises diagnostics produced for type errors +-in the absence of syntax errors. - --// bar does foo. --func bar() string { -- return "foo by bar" --}` +-The type error was chosen to exercise the 'nonewvars' type-error analyzer. +-(The 'undeclaredname' analyzer depends on the text of the go/types +-"undeclared name" error, which changed in go1.20.) - --func TestHoverLinknameDirective(t *testing.T) { -- Run(t, linknameHover, func(t *testing.T, env *Env) { -- // Jump from directives 2nd arg. -- env.OpenFile("upper/upper.go") -- from := env.RegexpSearch("upper/upper.go", `lower.bar`) +-The append() type error was also carefully chosen to have text and +-position that are invariant across all versions of Go run by the builders. - -- hover, _ := env.Hover(from) -- content := hover.Value +--- go.mod -- +-module example.com +-go 1.12 - -- expect := "bar does foo" -- if !strings.Contains(content, expect) { -- t.Errorf("hover: %q does not contain: %q", content, expect) -- } -- }) +--- typeerr.go -- +-package a +- +-func f(x int) { +- append("") //@diag(re`""`, re"a slice") +- +- x := 123 //@diag(re"x := 123", re"no new variables"), suggestedfix(re"():", re"no new variables", fix) -} - --func TestHoverGoWork_Issue60821(t *testing.T) { -- const files = ` +--- @fix/typeerr.go -- +-@@ -6 +6 @@ +-- x := 123 //@diag(re"x := 123", re"no new variables"), suggestedfix(re"():", re"no new variables", fix) +-+ x = 123 //@diag(re"x := 123", re"no new variables"), suggestedfix(re"():", re"no new variables", fix) +diff -urN a/gopls/internal/test/marker/testdata/diagnostics/useinternal.txt b/gopls/internal/test/marker/testdata/diagnostics/useinternal.txt +--- a/gopls/internal/test/marker/testdata/diagnostics/useinternal.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/diagnostics/useinternal.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,21 +0,0 @@ +-This test checks a diagnostic for invalid use of internal packages. +- +-This list error changed in Go 1.21. +- +--- flags -- +--min_go=go1.21 +- +--- go.mod -- +-module bad.test +- +-go 1.18 +- +--- assign/internal/secret/secret.go -- +-package secret +- +-func Hello() {} +- +--- bad/bad.go -- +-package bad +- +-import _ "bad.test/assign/internal/secret" //@diag("\"bad.test/assign/internal/secret\"", re"could not import bad.test/assign/internal/secret \\(invalid use of internal package \"bad.test/assign/internal/secret\"\\)"),diag("_", re"use of internal package bad.test/assign/internal/secret not allowed") +diff -urN a/gopls/internal/test/marker/testdata/diagnostics/usemodule.txt b/gopls/internal/test/marker/testdata/diagnostics/usemodule.txt +--- a/gopls/internal/test/marker/testdata/diagnostics/usemodule.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/diagnostics/usemodule.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,52 +0,0 @@ +-This test demonstrates diagnostics for a module that is missing from the +-go.work file. +- +-Quick-fixes change files on disk, so are tested by integration tests. +- +--- skip -- +-Temporary skip due to golang/go#57979, with zero-config gopls, these modules +-are no longer orphaned. +- --- go.work -- --go 1.19 +-go 1.21 - -use ( -- moda -- modb +- ./a -) ---- moda/go.mod -- - --` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("go.work") -- // Neither of the requests below should crash gopls. -- _, _, _ = env.Editor.Hover(env.Ctx, env.RegexpSearch("go.work", "moda")) -- _, _, _ = env.Editor.Hover(env.Ctx, env.RegexpSearch("go.work", "modb")) -- }) --} +--- a/go.mod -- +-module mod.com/a - --const embedHover = ` ---- go.mod -- --module mod.com --go 1.19 ---- main.go -- --package main +-go 1.18 - --import "embed" +--- a/main.go -- +-package main - --//go:embed *.txt --var foo embed.FS +-import "mod.com/a/lib" - -func main() { +- _ = lib.C -} ---- foo.txt -- --FOO ---- bar.txt -- --BAR ---- baz.txt -- --BAZ ---- other.sql -- --SKIPPED ---- dir.txt/skip.txt -- --SKIPPED --` - --func TestHoverEmbedDirective(t *testing.T) { -- testenv.NeedsGo1Point(t, 19) -- Run(t, embedHover, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- from := env.RegexpSearch("main.go", `\*.txt`) +--- a/lib/lib.go -- +-package lib - -- got, _ := env.Hover(from) -- if got == nil { -- t.Fatalf("hover over //go:embed arg not found") -- } -- content := got.Value +-const C = "b" +--- b/go.mod -- +-module mod.com/b - -- wants := []string{"foo.txt", "bar.txt", "baz.txt"} -- for _, want := range wants { -- if !strings.Contains(content, want) { -- t.Errorf("hover: %q does not contain: %q", content, want) -- } -- } +-go 1.18 - -- // A directory should never be matched, even if it happens to have a matching name. -- // Content in subdirectories should not match on only one asterisk. -- skips := []string{"other.sql", "dir.txt", "skip.txt"} -- for _, skip := range skips { -- if strings.Contains(content, skip) { -- t.Errorf("hover: %q should not contain: %q", content, skip) -- } -- } -- }) +--- b/main.go -- +-package main //@diag("main", re"add this module to your go.work") +- +-import "mod.com/b/lib" //@diag("\"mod.com", re"not included in a workspace module") +- +-func main() { +- _ = lib.C -} -diff -urN a/gopls/internal/regtest/misc/imports_test.go b/gopls/internal/regtest/misc/imports_test.go ---- a/gopls/internal/regtest/misc/imports_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/misc/imports_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,286 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package misc +--- b/lib/lib.go -- +-package lib //@diag("lib", re"add this module to your go.work") - --import ( -- "os" -- "path/filepath" -- "strings" -- "testing" +-const C = "b" +diff -urN a/gopls/internal/test/marker/testdata/fixedbugs/issue59318.txt b/gopls/internal/test/marker/testdata/fixedbugs/issue59318.txt +--- a/gopls/internal/test/marker/testdata/fixedbugs/issue59318.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/fixedbugs/issue59318.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,20 +0,0 @@ +-Previously, this test verifies that we can load multiple orphaned files as +-command-line-arguments packages. In the distant past, we would load only one +-because go/packages returns at most one command-line-arguments package per +-query. - -- . "golang.org/x/tools/gopls/internal/lsp/regtest" -- "golang.org/x/tools/gopls/internal/lsp/tests/compare" +-With zero-config gopls, these packages are successfully loaded as ad-hoc +-packages. - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/internal/testenv" --) +--- a/main.go -- +-package main +- +-func main() { +- var a int //@diag(re"var (a)", re"not used") +-} +--- b/main.go -- +-package main +- +-func main() { +- var b int //@diag(re"var (b)", re"not used") +-} +diff -urN a/gopls/internal/test/marker/testdata/fixedbugs/issue59944.txt b/gopls/internal/test/marker/testdata/fixedbugs/issue59944.txt +--- a/gopls/internal/test/marker/testdata/fixedbugs/issue59944.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/fixedbugs/issue59944.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,33 +0,0 @@ +-This test verifies that gopls does not panic when encountering the go/types +-bug described in golang/go#59944: the Bindingf function is not included in +-the methodset of its receiver type. +- +-Adapted from the code in question from the issue. +- +--- flags -- +--cgo - --// Tests golang/go#38815. --func TestIssue38815(t *testing.T) { -- const needs = ` --- go.mod -- --module foo +-module example.com - -go 1.12 ---- a.go -- +- +--- cgo.go -- +-package x +- +-import "fmt" +- +-/* +-struct layout { +- int field; +-}; +-*/ +-import "C" +- +-type Layout = C.struct_layout +- +-// Bindingf is a printf wrapper. This was necessary to trigger the panic in +-// objectpath while encoding facts. +-func (l *Layout) Bindingf(format string, args ...interface{}) { +- fmt.Printf(format, args...) +-} +diff -urN a/gopls/internal/test/marker/testdata/fixedbugs/issue66109.txt b/gopls/internal/test/marker/testdata/fixedbugs/issue66109.txt +--- a/gopls/internal/test/marker/testdata/fixedbugs/issue66109.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/fixedbugs/issue66109.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,25 +0,0 @@ +-This test exercises the crash in golang/go#66109: a dangling reference due to +-test variants of a command-line-arguments package. +- +--- flags -- +--min_go=go1.22 +- +--- go.mod -- +-module example.com/tools +- +-go 1.22 +- +--- tools_test.go -- +-//go:build tools +- +-package tools //@diag("tools", re"No packages found") +- +-import ( +- _ "example.com/tools/tool" +-) +- +--- tool/tool.go -- -package main --func f() {} --` -- const ntest = `package main --func TestZ(t *testing.T) { -- f() +- +-func main() { -} --` -- const want = `package main +diff -urN a/gopls/internal/test/marker/testdata/fixedbugs/issue66250.txt b/gopls/internal/test/marker/testdata/fixedbugs/issue66250.txt +--- a/gopls/internal/test/marker/testdata/fixedbugs/issue66250.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/fixedbugs/issue66250.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,17 +0,0 @@ +-This bug checks the fix for golang/go#66250. Package references should not +-crash when one package file lacks a package name. - --import "testing" +-TODO(rfindley): the -ignore_extra_diags flag is only necessary because of +-problems matching diagnostics in the broken file, likely due to poor parser +-recovery. - --func TestZ(t *testing.T) { -- f() +--- flags -- +--ignore_extra_diags +- +--- a.go -- +-package x //@refs("x", "x") +- +--- b.go -- +- +-func _() { -} --` +diff -urN a/gopls/internal/test/marker/testdata/foldingrange/a_lineonly.txt b/gopls/internal/test/marker/testdata/foldingrange/a_lineonly.txt +--- a/gopls/internal/test/marker/testdata/foldingrange/a_lineonly.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/foldingrange/a_lineonly.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,163 +0,0 @@ +-This test checks basic behavior of the textDocument/foldingRange, when the +-editor only supports line folding. - -- // it was returning -- // "package main\nimport \"testing\"\npackage main..." -- Run(t, needs, func(t *testing.T, env *Env) { -- env.CreateBuffer("a_test.go", ntest) -- env.SaveBuffer("a_test.go") -- got := env.BufferText("a_test.go") -- if want != got { -- t.Errorf("got\n%q, wanted\n%q", got, want) +--- capabilities.json -- +-{ +- "textDocument": { +- "foldingRange": { +- "lineFoldingOnly": true - } -- }) +- } -} -- --func TestIssue59124(t *testing.T) { -- const stuff = ` ---- go.mod -- --module foo --go 1.19 --- a.go -- --//line foo.y:102 --package main +-package folding //@foldingrange(raw) - --import "fmt" +-import ( +- "fmt" +- _ "log" +-) - --//this comment is necessary for failure --func a() { -- fmt.Println("hello") --} --` -- Run(t, stuff, func(t *testing.T, env *Env) { -- env.OpenFile("a.go") -- was := env.BufferText("a.go") -- env.Await(NoDiagnostics()) -- env.OrganizeImports("a.go") -- is := env.BufferText("a.go") -- if diff := compare.Text(was, is); diff != "" { -- t.Errorf("unexpected diff after organizeImports:\n%s", diff) +-import _ "os" +- +-// bar is a function. +-// With a multiline doc comment. +-func bar() string { +- /* This is a single line comment */ +- switch { +- case true: +- if true { +- fmt.Println("true") +- } else { +- fmt.Println("false") - } -- }) +- case false: +- fmt.Println("false") +- default: +- fmt.Println("default") +- } +- /* This is a multiline +- block +- comment */ +- +- /* This is a multiline +- block +- comment */ +- // Followed by another comment. +- _ = []int{ +- 1, +- 2, +- 3, +- } +- _ = [2]string{"d", +- "e", +- } +- _ = map[string]int{ +- "a": 1, +- "b": 2, +- "c": 3, +- } +- type T struct { +- f string +- g int +- h string +- } +- _ = T{ +- f: "j", +- g: 4, +- h: "i", +- } +- x, y := make(chan bool), make(chan bool) +- select { +- case val := <-x: +- if val { +- fmt.Println("true from x") +- } else { +- fmt.Println("false from x") +- } +- case <-y: +- fmt.Println("y") +- default: +- fmt.Println("default") +- } +- // This is a multiline comment +- // that is not a doc comment. +- return ` +-this string +-is not indented` -} +--- @raw -- +-package folding //@foldingrange(raw) - --func TestVim1(t *testing.T) { -- const vim1 = `package main +-import (<0 kind="imports"> +- "fmt" +- _ "log"</0> +-) - --import "fmt" +-import _ "os" - --var foo = 1 --var bar = 2 +-// bar is a function.<1 kind="comment"> +-// With a multiline doc comment.</1> +-func bar() string {<2 kind=""> +- /* This is a single line comment */ +- switch {<3 kind=""> +- case true:<4 kind=""> +- if true {<5 kind=""> +- fmt.Println("true")</5> +- } else {<6 kind=""> +- fmt.Println("false")</6> +- }</4> +- case false:<7 kind=""> +- fmt.Println("false")</7> +- default:<8 kind=""> +- fmt.Println("default")</3></8> +- } +- /* This is a multiline<9 kind="comment"> +- block +- comment */</9> - --func main() { -- fmt.Printf("This is a test %v\n", foo) -- fmt.Printf("This is another test %v\n", foo) -- fmt.Printf("This is also a test %v\n", foo) +- /* This is a multiline<10 kind="comment"> +- block +- comment */ +- // Followed by another comment.</10> +- _ = []int{<11 kind=""> +- 1, +- 2, +- 3</11>, +- } +- _ = [2]string{"d", +- "e", +- } +- _ = map[string]int{<12 kind=""> +- "a": 1, +- "b": 2, +- "c": 3</12>, +- } +- type T struct {<13 kind=""> +- f string +- g int +- h string</13> +- } +- _ = T{<14 kind=""> +- f: "j", +- g: 4, +- h: "i"</14>, +- } +- x, y := make(chan bool), make(chan bool) +- select {<15 kind=""> +- case val := <-x:<16 kind=""> +- if val {<17 kind=""> +- fmt.Println("true from x")</17> +- } else {<18 kind=""> +- fmt.Println("false from x")</18> +- }</16> +- case <-y:<19 kind=""> +- fmt.Println("y")</19> +- default:<20 kind=""> +- fmt.Println("default")</15></20> +- } +- // This is a multiline comment<21 kind="comment"> +- // that is not a doc comment.</21> +- return <22 kind="">` +-this string +-is not indented`</2></22> -} --` +diff -urN a/gopls/internal/test/marker/testdata/foldingrange/a.txt b/gopls/internal/test/marker/testdata/foldingrange/a.txt +--- a/gopls/internal/test/marker/testdata/foldingrange/a.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/foldingrange/a.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,154 +0,0 @@ +-This test checks basic behavior of textDocument/foldingRange. - -- // The file remains unchanged, but if there are any CodeActions returned, they confuse vim. -- // Therefore check for no CodeActions -- Run(t, "", func(t *testing.T, env *Env) { -- env.CreateBuffer("main.go", vim1) -- env.OrganizeImports("main.go") -- actions := env.CodeAction("main.go", nil) -- if len(actions) > 0 { -- got := env.BufferText("main.go") -- t.Errorf("unexpected actions %#v", actions) -- if got == vim1 { -- t.Errorf("no changes") -- } else { -- t.Errorf("got\n%q", got) -- t.Errorf("was\n%q", vim1) -- } +--- a.go -- +-package folding //@foldingrange(raw) +- +-import ( +- "fmt" +- _ "log" +-) +- +-import _ "os" +- +-// bar is a function. +-// With a multiline doc comment. +-func bar() string { +- /* This is a single line comment */ +- switch { +- case true: +- if true { +- fmt.Println("true") +- } else { +- fmt.Println("false") +- } +- case false: +- fmt.Println("false") +- default: +- fmt.Println("default") +- } +- /* This is a multiline +- block +- comment */ +- +- /* This is a multiline +- block +- comment */ +- // Followed by another comment. +- _ = []int{ +- 1, +- 2, +- 3, +- } +- _ = [2]string{"d", +- "e", +- } +- _ = map[string]int{ +- "a": 1, +- "b": 2, +- "c": 3, +- } +- type T struct { +- f string +- g int +- h string +- } +- _ = T{ +- f: "j", +- g: 4, +- h: "i", +- } +- x, y := make(chan bool), make(chan bool) +- select { +- case val := <-x: +- if val { +- fmt.Println("true from x") +- } else { +- fmt.Println("false from x") - } -- }) +- case <-y: +- fmt.Println("y") +- default: +- fmt.Println("default") +- } +- // This is a multiline comment +- // that is not a doc comment. +- return ` +-this string +-is not indented` -} +--- @raw -- +-package folding //@foldingrange(raw) - --func TestVim2(t *testing.T) { -- const vim2 = `package main -- --import ( +-import (<0 kind="imports"> - "fmt" +- _ "log" +-</0>) - -- "example.com/blah" -- -- "rubbish.com/useless" --) -- --func main() { -- fmt.Println(blah.Name, useless.Name) --} --` -- -- Run(t, "", func(t *testing.T, env *Env) { -- env.CreateBuffer("main.go", vim2) -- env.OrganizeImports("main.go") -- actions := env.CodeAction("main.go", nil) -- if len(actions) > 0 { -- t.Errorf("unexpected actions %#v", actions) -- } -- }) --} -- --func TestGOMODCACHE(t *testing.T) { -- const proxy = ` ---- example.com@v1.2.3/go.mod -- --module example.com +-import _ "os" - --go 1.12 ---- example.com@v1.2.3/x/x.go -- --package x +-// bar is a function.<1 kind="comment"> +-// With a multiline doc comment.</1> +-func bar(<2 kind=""></2>) string {<3 kind=""> +- /* This is a single line comment */ +- switch {<4 kind=""> +- case true:<5 kind=""> +- if true {<6 kind=""> +- fmt.Println(<7 kind="">"true"</7>) +- </6>} else {<8 kind=""> +- fmt.Println(<9 kind="">"false"</9>) +- </8>}</5> +- case false:<10 kind=""> +- fmt.Println(<11 kind="">"false"</11>)</10> +- default:<12 kind=""> +- fmt.Println(<13 kind="">"default"</13>)</12> +- </4>} +- /* This is a multiline<14 kind="comment"> +- block +- comment */</14> - --const X = 1 ---- example.com@v1.2.3/y/y.go -- --package y +- /* This is a multiline<15 kind="comment"> +- block +- comment */ +- // Followed by another comment.</15> +- _ = []int{<16 kind=""> +- 1, +- 2, +- 3, +- </16>} +- _ = [2]string{<17 kind="">"d", +- "e", +- </17>} +- _ = map[string]int{<18 kind=""> +- "a": 1, +- "b": 2, +- "c": 3, +- </18>} +- type T struct {<19 kind=""> +- f string +- g int +- h string +- </19>} +- _ = T{<20 kind=""> +- f: "j", +- g: 4, +- h: "i", +- </20>} +- x, y := make(<21 kind="">chan bool</21>), make(<22 kind="">chan bool</22>) +- select {<23 kind=""> +- case val := <-x:<24 kind=""> +- if val {<25 kind=""> +- fmt.Println(<26 kind="">"true from x"</26>) +- </25>} else {<27 kind=""> +- fmt.Println(<28 kind="">"false from x"</28>) +- </27>}</24> +- case <-y:<29 kind=""> +- fmt.Println(<30 kind="">"y"</30>)</29> +- default:<31 kind=""> +- fmt.Println(<32 kind="">"default"</32>)</31> +- </23>} +- // This is a multiline comment<33 kind="comment"> +- // that is not a doc comment.</33> +- return <34 kind="">` +-this string +-is not indented`</34> +-</3>} +diff -urN a/gopls/internal/test/marker/testdata/foldingrange/bad.txt b/gopls/internal/test/marker/testdata/foldingrange/bad.txt +--- a/gopls/internal/test/marker/testdata/foldingrange/bad.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/foldingrange/bad.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,41 +0,0 @@ +-This test verifies behavior of textDocument/foldingRange in the presence of +-unformatted syntax. - --const Y = 2 --` -- const files = ` ---- go.mod -- --module mod.com +--- a.go -- +-package folding //@foldingrange(raw) - --go 1.12 +-import ( "fmt" +- _ "log" +-) - --require example.com v1.2.3 ---- go.sum -- --example.com v1.2.3 h1:6vTQqzX+pnwngZF1+5gcO3ZEWmix1jJ/h+pWS8wUxK0= --example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= ---- main.go -- --package main +-import ( +- _ "os" ) +- +-// badBar is a function. +-func badBar() string { x := true +- if x { +- // This is the only foldable thing in this file when lineFoldingOnly +- fmt.Println("true") +- } else { +- fmt.Println("false") } +- return "" +-} +--- @raw -- +-package folding //@foldingrange(raw) - --import "example.com/x" +-import (<0 kind="imports"> "fmt" +- _ "log" +-</0>) - --var _, _ = x.X, y.Y --` -- modcache, err := os.MkdirTemp("", "TestGOMODCACHE-modcache") -- if err != nil { -- t.Fatal(err) -- } -- defer os.RemoveAll(modcache) -- WithOptions( -- EnvVars{"GOMODCACHE": modcache}, -- ProxyFiles(proxy), -- ).Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- env.AfterChange(Diagnostics(env.AtRegexp("main.go", `y.Y`))) -- env.SaveBuffer("main.go") -- env.AfterChange(NoDiagnostics(ForFile("main.go"))) -- loc := env.GoToDefinition(env.RegexpSearch("main.go", `y.(Y)`)) -- path := env.Sandbox.Workdir.URIToPath(loc.URI) -- if !strings.HasPrefix(path, filepath.ToSlash(modcache)) { -- t.Errorf("found module dependency outside of GOMODCACHE: got %v, wanted subdir of %v", path, filepath.ToSlash(modcache)) -- } -- }) --} +-import (<1 kind="imports"> +- _ "os" </1>) +- +-// badBar is a function. +-func badBar(<2 kind=""></2>) string {<3 kind=""> x := true +- if x {<4 kind=""> +- // This is the only foldable thing in this file when lineFoldingOnly +- fmt.Println(<5 kind="">"true"</5>) +- </4>} else {<6 kind=""> +- fmt.Println(<7 kind="">"false"</7>) </6>} +- return "" +-</3>} +diff -urN a/gopls/internal/test/marker/testdata/format/format.txt b/gopls/internal/test/marker/testdata/format/format.txt +--- a/gopls/internal/test/marker/testdata/format/format.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/format/format.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,80 +0,0 @@ +-This test checks basic behavior of textDocument/formatting requests. - --// Tests golang/go#40685. --func TestAcceptImportsQuickFixTestVariant(t *testing.T) { -- const pkg = ` --- go.mod -- -module mod.com - --go 1.12 ---- a/a.go -- --package a +-go 1.18 +--- good.go -- +-package format //@format(good) - -import ( -- "fmt" +- "log" -) - --func _() { -- fmt.Println("") -- os.Stat("") +-func goodbye() { +- log.Printf("byeeeee") -} ---- a/a_test.go -- --package a +- +--- @good -- +-package format //@format(good) - -import ( -- "os" -- "testing" +- "log" -) - --func TestA(t *testing.T) { -- os.Stat("") --} --` -- Run(t, pkg, func(t *testing.T, env *Env) { -- env.OpenFile("a/a.go") -- var d protocol.PublishDiagnosticsParams -- env.AfterChange( -- Diagnostics(env.AtRegexp("a/a.go", "os.Stat")), -- ReadDiagnostics("a/a.go", &d), -- ) -- env.ApplyQuickFixes("a/a.go", d.Diagnostics) -- env.AfterChange( -- NoDiagnostics(ForFile("a/a.go")), -- ) -- }) +-func goodbye() { +- log.Printf("byeeeee") -} +--- bad.go -- +-package format //@format(bad) - --// Test for golang/go#52784 --func TestGoWorkImports(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) -- const pkg = ` ---- go.work -- --go 1.19 -- --use ( -- ./caller -- ./mod +-import ( +- "runtime" +- "fmt" +- "log" -) ---- caller/go.mod -- --module caller.com -- --go 1.18 -- --require mod.com v0.0.0 - --replace mod.com => ../mod ---- caller/caller.go -- --package main +-func hello() { - --func main() { -- a.Test() --} ---- mod/go.mod -- --module mod.com - --go 1.18 ---- mod/a/a.go -- --package a - --func Test() { --} --` -- Run(t, pkg, func(t *testing.T, env *Env) { -- env.OpenFile("caller/caller.go") -- env.AfterChange(Diagnostics(env.AtRegexp("caller/caller.go", "a.Test"))) - -- // Saving caller.go should trigger goimports, which should find a.Test in -- // the mod.com module, thanks to the go.work file. -- env.SaveBuffer("caller/caller.go") -- env.AfterChange(NoDiagnostics(ForFile("caller/caller.go"))) -- }) +- var x int //@diag("x", re"x.*declared (and|but) not used") -} -diff -urN a/gopls/internal/regtest/misc/import_test.go b/gopls/internal/regtest/misc/import_test.go ---- a/gopls/internal/regtest/misc/import_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/misc/import_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,133 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package misc -- --import ( -- "testing" - -- "github.com/google/go-cmp/cmp" -- "golang.org/x/tools/gopls/internal/lsp/command" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" -- "golang.org/x/tools/gopls/internal/lsp/tests/compare" --) -- --func TestAddImport(t *testing.T) { -- const before = `package main -- --import "fmt" +-func hi() { +- runtime.NumCPU() +- fmt.Printf("") - --func main() { -- fmt.Println("hello world") +- log.Printf("") -} --` -- -- const want = `package main +--- @bad -- +-package format //@format(bad) - -import ( -- "bytes" - "fmt" +- "log" +- "runtime" -) - --func main() { -- fmt.Println("hello world") --} --` +-func hello() { - -- Run(t, "", func(t *testing.T, env *Env) { -- env.CreateBuffer("main.go", before) -- cmd, err := command.NewAddImportCommand("Add Import", command.AddImportArgs{ -- URI: env.Sandbox.Workdir.URI("main.go"), -- ImportPath: "bytes", -- }) -- if err != nil { -- t.Fatal(err) -- } -- env.ExecuteCommand(&protocol.ExecuteCommandParams{ -- Command: "gopls.add_import", -- Arguments: cmd.Arguments, -- }, nil) -- got := env.BufferText("main.go") -- if got != want { -- t.Fatalf("gopls.add_import failed\n%s", compare.Text(want, got)) -- } -- }) +- var x int //@diag("x", re"x.*declared (and|but) not used") -} - --func TestListImports(t *testing.T) { -- const files = ` ---- go.mod -- --module mod.com -- --go 1.12 ---- foo.go -- --package foo --const C = 1 ---- import_strings_test.go -- --package foo --import ( -- x "strings" -- "testing" --) +-func hi() { +- runtime.NumCPU() +- fmt.Printf("") - --func TestFoo(t *testing.T) {} ---- import_testing_test.go -- --package foo +- log.Printf("") +-} +--- newline.go -- +-package format //@format(newline) +-func _() {} +--- @newline -- +-package format //@format(newline) +-func _() {} +--- oneline.go -- +-package format //@format(oneline) +--- @oneline -- +-package format //@format(oneline) +diff -urN a/gopls/internal/test/marker/testdata/format/issue59554.txt b/gopls/internal/test/marker/testdata/format/issue59554.txt +--- a/gopls/internal/test/marker/testdata/format/issue59554.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/format/issue59554.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,34 +0,0 @@ +-Test case for golang/go#59554: data corruption on formatting due to line +-directives. - --import "testing" +-Note that gofumpt is needed for this test case, as it reformats var decls into +-short var decls. - --func TestFoo2(t *testing.T) {} --` -- tests := []struct { -- filename string -- want command.ListImportsResult -- }{ -- { -- filename: "import_strings_test.go", -- want: command.ListImportsResult{ -- Imports: []command.FileImport{ -- {Name: "x", Path: "strings"}, -- {Path: "testing"}, -- }, -- PackageImports: []command.PackageImport{ -- {Path: "strings"}, -- {Path: "testing"}, -- }, -- }, -- }, -- { -- filename: "import_testing_test.go", -- want: command.ListImportsResult{ -- Imports: []command.FileImport{ -- {Path: "testing"}, -- }, -- PackageImports: []command.PackageImport{ -- {Path: "strings"}, -- {Path: "testing"}, -- }, -- }, -- }, -- } +-Note that gofumpt requires Go 1.20. - -- Run(t, files, func(t *testing.T, env *Env) { -- for _, tt := range tests { -- cmd, err := command.NewListImportsCommand("List Imports", command.URIArg{ -- URI: env.Sandbox.Workdir.URI(tt.filename), -- }) -- if err != nil { -- t.Fatal(err) -- } -- var result command.ListImportsResult -- env.ExecuteCommand(&protocol.ExecuteCommandParams{ -- Command: command.ListImports.ID(), -- Arguments: cmd.Arguments, -- }, &result) -- if diff := cmp.Diff(tt.want, result); diff != "" { -- t.Errorf("unexpected list imports result for %q (-want +got):\n%s", tt.filename, diff) -- } -- } +--- flags -- +--min_go=go1.20 - -- }) +--- settings.json -- +-{ +- "formatting.gofumpt": true -} -diff -urN a/gopls/internal/regtest/misc/link_test.go b/gopls/internal/regtest/misc/link_test.go ---- a/gopls/internal/regtest/misc/link_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/misc/link_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,96 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package misc -- --import ( -- "strings" -- "testing" - -- . "golang.org/x/tools/gopls/internal/lsp/regtest" --) +--- main.go -- +-package main //@format(main) - --func TestHoverAndDocumentLink(t *testing.T) { -- const program = ` ---- go.mod -- --module mod.test +-func Match(data []byte) int { +-//line :1 +- var idx = ^uint(0) +- _ = idx +- return -1 +-} +--- @main -- +-package main //@format(main) - --go 1.12 +-func Match(data []byte) int { +-//line :1 +- idx := ^uint(0) +- _ = idx +- return -1 +-} +diff -urN a/gopls/internal/test/marker/testdata/format/noparse.txt b/gopls/internal/test/marker/testdata/format/noparse.txt +--- a/gopls/internal/test/marker/testdata/format/noparse.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/format/noparse.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,27 +0,0 @@ +-This test checks that formatting does not run on code that has parse errors. - --require import.test v1.2.3 ---- go.sum -- --import.test v1.2.3 h1:Mu4N9BICLJFxwwn8YNg6T3frkFWW1O7evXvo0HiRjBc= --import.test v1.2.3/go.mod h1:KooCN1g237upRg7irU7F+3oADn5tVClU8YYW4I1xhMk= ---- main.go -- --package main +--- parse.go -- +-package noparse_format //@format(parse) - --import "import.test/pkg" +-func _() { +-f() //@diag("f", re"(undefined|undeclared name): f") +-} +--- @parse -- +-package noparse_format //@format(parse) - --func main() { -- // Issue 43990: this is not a link that most users can open from an LSP -- // client: mongodb://not.a.link.com -- println(pkg.Hello) --}` +-func _() { +- f() //@diag("f", re"(undefined|undeclared name): f") +-} +--- noparse.go -- +-package noparse_format //@format(noparse) - -- const proxy = ` ---- import.test@v1.2.3/go.mod -- --module import.test +-// The nonewvars expectation asserts that the go/analysis framework ran. - --go 1.12 ---- import.test@v1.2.3/pkg/const.go -- --package pkg +-func what() { +- var hi func() +- if { hi() //@diag(re"(){", re".*missing.*") +- } +- hi := nil +-} +--- @noparse -- +-7:5: missing condition in if statement +diff -urN a/gopls/internal/test/marker/testdata/highlight/controlflow.txt b/gopls/internal/test/marker/testdata/highlight/controlflow.txt +--- a/gopls/internal/test/marker/testdata/highlight/controlflow.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/highlight/controlflow.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,71 +0,0 @@ +-This test verifies document highlighting for control flow. - --const Hello = "Hello" --` -- WithOptions( -- ProxyFiles(proxy), -- ).Run(t, program, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- env.OpenFile("go.mod") +--- go.mod -- +-module mod.com - -- modLink := "https://pkg.go.dev/mod/import.test@v1.2.3" -- pkgLink := "https://pkg.go.dev/import.test@v1.2.3/pkg" +-go 1.18 - -- // First, check that we get the expected links via hover and documentLink. -- content, _ := env.Hover(env.RegexpSearch("main.go", "pkg.Hello")) -- if content == nil || !strings.Contains(content.Value, pkgLink) { -- t.Errorf("hover: got %v in main.go, want contains %q", content, pkgLink) -- } -- content, _ = env.Hover(env.RegexpSearch("go.mod", "import.test")) -- if content == nil || !strings.Contains(content.Value, pkgLink) { -- t.Errorf("hover: got %v in go.mod, want contains %q", content, pkgLink) -- } -- links := env.DocumentLink("main.go") -- if len(links) != 1 || *links[0].Target != pkgLink { -- t.Errorf("documentLink: got links %+v for main.go, want one link with target %q", links, pkgLink) -- } -- links = env.DocumentLink("go.mod") -- if len(links) != 1 || *links[0].Target != modLink { -- t.Errorf("documentLink: got links %+v for go.mod, want one link with target %q", links, modLink) -- } +--- p.go -- +-package p - -- // Then change the environment to make these links private. -- cfg := env.Editor.Config() -- cfg.Env = map[string]string{"GOPRIVATE": "import.test"} -- env.ChangeConfiguration(cfg) +--- issue60589.go -- +-package p - -- // Finally, verify that the links are gone. -- content, _ = env.Hover(env.RegexpSearch("main.go", "pkg.Hello")) -- if content == nil || strings.Contains(content.Value, pkgLink) { -- t.Errorf("hover: got %v in main.go, want non-empty hover without %q", content, pkgLink) -- } -- content, _ = env.Hover(env.RegexpSearch("go.mod", "import.test")) -- if content == nil || strings.Contains(content.Value, modLink) { -- t.Errorf("hover: got %v in go.mod, want contains %q", content, modLink) -- } -- links = env.DocumentLink("main.go") -- if len(links) != 0 { -- t.Errorf("documentLink: got %d document links for main.go, want 0\nlinks: %v", len(links), links) -- } -- links = env.DocumentLink("go.mod") -- if len(links) != 0 { -- t.Errorf("documentLink: got %d document links for go.mod, want 0\nlinks: %v", len(links), links) -- } -- }) +-// This test verifies that control flow lighlighting correctly +-// accounts for multi-name result parameters. +-// In golang/go#60589, it did not. +- +-func _() (foo int, bar, baz string) { //@ loc(func, "func"), loc(foo, "foo"), loc(fooint, "foo int"), loc(int, "int"), loc(bar, "bar"), loc(beforebaz, " baz"), loc(baz, "baz"), loc(barbazstring, "bar, baz string"), loc(beforestring, re`() string`), loc(string, "string") +- return 0, "1", "2" //@ loc(return, `return 0, "1", "2"`), loc(l0, "0"), loc(l1, `"1"`), loc(l2, `"2"`) -} -diff -urN a/gopls/internal/regtest/misc/misc_test.go b/gopls/internal/regtest/misc/misc_test.go ---- a/gopls/internal/regtest/misc/misc_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/misc/misc_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,18 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package misc +-// Assertions, expressed here to avoid clutter above. +-// Note that when the cursor is over the field type, there is some +-// (likely harmless) redundancy. - --import ( -- "testing" +-//@ highlight(func, func, return) +-//@ highlight(foo, foo, l0) +-//@ highlight(int, fooint, int, l0) +-//@ highlight(bar, bar, l1) +-//@ highlight(beforebaz) +-//@ highlight(baz, baz, l2) +-//@ highlight(beforestring, baz, l2) +-//@ highlight(string, barbazstring, string, l1, l2) +-//@ highlight(l0, foo, l0) +-//@ highlight(l1, bar, l1) +-//@ highlight(l2, baz, l2) - -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/hooks" -- "golang.org/x/tools/gopls/internal/lsp/regtest" --) +-// Check that duplicate result names do not cause +-// inaccurate highlighting. - --func TestMain(m *testing.M) { -- bug.PanicOnBugs = true -- regtest.Main(m, hooks.Options) +-func _() (x, x int32) { //@ loc(x1, re`\((x)`), loc(x2, re`(x) int`), diag(x1, re"redeclared"), diag(x2, re"redeclared") +- return 1, 2 //@ loc(one, "1"), loc(two, "2") -} -diff -urN a/gopls/internal/regtest/misc/multiple_adhoc_test.go b/gopls/internal/regtest/misc/multiple_adhoc_test.go ---- a/gopls/internal/regtest/misc/multiple_adhoc_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/misc/multiple_adhoc_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,44 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package misc - --import ( -- "testing" +-//@ highlight(one, one, x1) +-//@ highlight(two, two, x2) +-//@ highlight(x1, x1, one) +-//@ highlight(x2, x2, two) - -- . "golang.org/x/tools/gopls/internal/lsp/regtest" --) +--- issue65516.go -- +-package p - --func TestMultipleAdHocPackages(t *testing.T) { -- Run(t, ` ---- a/a.go -- --package main +-// This test checks that gopls doesn't crash while highlighting +-// functions with no body (golang/go#65516). - --import "fmt" +-func Foo() (int, string) //@highlight("int", "int"), highlight("func", "func") - --func main() { -- fmt.Println("") --} ---- a/b.go -- --package main +--- issue65952.go -- +-package p - --import "fmt" +-// This test checks that gopls doesn't crash while highlighting +-// return values in functions with no results. - --func main() () { -- fmt.Println("") +-func _() { +- return 0 //@highlight("0", "0"), diag("0", re"too many return") -} --`, func(t *testing.T, env *Env) { -- env.OpenFile("a/a.go") -- if list := env.Completion(env.RegexpSearch("a/a.go", "Println")); list == nil || len(list.Items) == 0 { -- t.Fatal("expected completions, got none") -- } -- env.OpenFile("a/b.go") -- if list := env.Completion(env.RegexpSearch("a/b.go", "Println")); list == nil || len(list.Items) == 0 { -- t.Fatal("expected completions, got none") -- } -- if list := env.Completion(env.RegexpSearch("a/a.go", "Println")); list == nil || len(list.Items) == 0 { -- t.Fatal("expected completions, got none") -- } -- }) +- +-func _() () { +- // TODO(golang/go#65966): fix the triplicate diagnostics here. +- return 0 //@highlight("0", "0"), diag("0", re"too many return"), diag("0", re"too many return"), diag("0", re"too many return") -} -diff -urN a/gopls/internal/regtest/misc/prompt_test.go b/gopls/internal/regtest/misc/prompt_test.go ---- a/gopls/internal/regtest/misc/prompt_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/misc/prompt_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,231 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +diff -urN a/gopls/internal/test/marker/testdata/highlight/highlight.txt b/gopls/internal/test/marker/testdata/highlight/highlight.txt +--- a/gopls/internal/test/marker/testdata/highlight/highlight.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/highlight/highlight.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,158 +0,0 @@ +-This test checks basic functionality of the textDocument/highlight request. - --package misc +--- highlights.go -- +-package highlights - -import ( -- "fmt" -- "os" -- "path/filepath" -- "regexp" -- "testing" -- -- "golang.org/x/tools/gopls/internal/lsp" -- "golang.org/x/tools/gopls/internal/lsp/command" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" +- "fmt" //@loc(fmtImp, "\"fmt\""),highlight(fmtImp, fmtImp, fmt1, fmt2, fmt3, fmt4) +- h2 "net/http" //@loc(hImp, "h2"),highlight(hImp, hImp, hUse) +- "sort" -) - --// Test that gopls prompts for telemetry only when it is supposed to. --func TestTelemetryPrompt_Conditions(t *testing.T) { -- const src = ` ---- go.mod -- --module mod.com +-type F struct{ bar int } //@loc(barDeclaration, "bar"),highlight(barDeclaration, barDeclaration, bar1, bar2, bar3) - --go 1.12 ---- main.go -- --package main +-func _() F { +- return F{ +- bar: 123, //@loc(bar1, "bar"),highlight(bar1, barDeclaration, bar1, bar2, bar3) +- } +-} - --func main() { +-var foo = F{bar: 52} //@loc(fooDeclaration, "foo"),loc(bar2, "bar"),highlight(fooDeclaration, fooDeclaration, fooUse),highlight(bar2, barDeclaration, bar1, bar2, bar3) +- +-func Print() { //@loc(printFunc, "Print"),highlight(printFunc, printFunc, printTest) +- _ = h2.Client{} //@loc(hUse, "h2"),highlight(hUse, hImp, hUse) +- +- fmt.Println(foo) //@loc(fooUse, "foo"),highlight(fooUse, fooDeclaration, fooUse),loc(fmt1, "fmt"),highlight(fmt1, fmtImp, fmt1, fmt2, fmt3, fmt4) +- fmt.Print("yo") //@loc(printSep, "Print"),highlight(printSep, printSep, print1, print2),loc(fmt2, "fmt"),highlight(fmt2, fmtImp, fmt1, fmt2, fmt3, fmt4) -} --` - -- for _, enabled := range []bool{true, false} { -- t.Run(fmt.Sprintf("telemetryPrompt=%v", enabled), func(t *testing.T) { -- for _, initialMode := range []string{"", "off", "on"} { -- t.Run(fmt.Sprintf("initial_mode=%s", initialMode), func(t *testing.T) { -- modeFile := filepath.Join(t.TempDir(), "mode") -- if initialMode != "" { -- if err := os.WriteFile(modeFile, []byte(initialMode), 0666); err != nil { -- t.Fatal(err) -- } -- } -- WithOptions( -- Modes(Default), // no need to run this in all modes -- EnvVars{ -- lsp.GoplsConfigDirEnvvar: t.TempDir(), -- lsp.FakeTelemetryModefileEnvvar: modeFile, -- }, -- Settings{ -- "telemetryPrompt": enabled, -- }, -- ).Run(t, src, func(t *testing.T, env *Env) { -- wantPrompt := enabled && (initialMode == "" || initialMode == "off") -- expectation := ShownMessageRequest(".*Would you like to enable Go telemetry?") -- if !wantPrompt { -- expectation = Not(expectation) -- } -- env.OnceMet( -- CompletedWork(lsp.TelemetryPromptWorkTitle, 1, true), -- expectation, -- ) -- }) -- }) -- } -- }) -- } +-func (x *F) Inc() { //@loc(xRightDecl, "x"),loc(xLeftDecl, " *"),highlight(xRightDecl, xRightDecl, xUse),highlight(xLeftDecl, xRightDecl, xUse) +- x.bar++ //@loc(xUse, "x"),loc(bar3, "bar"),highlight(xUse, xRightDecl, xUse),highlight(bar3, barDeclaration, bar1, bar2, bar3) -} - --// Test that responding to the telemetry prompt results in the expected state. --func TestTelemetryPrompt_Response(t *testing.T) { -- const src = ` ---- go.mod -- --module mod.com +-func testFunctions() { +- fmt.Print("main start") //@loc(print1, "Print"),highlight(print1, printSep, print1, print2),loc(fmt3, "fmt"),highlight(fmt3, fmtImp, fmt1, fmt2, fmt3, fmt4) +- fmt.Print("ok") //@loc(print2, "Print"),highlight(print2, printSep, print1, print2),loc(fmt4, "fmt"),highlight(fmt4, fmtImp, fmt1, fmt2, fmt3, fmt4) +- Print() //@loc(printTest, "Print"),highlight(printTest, printFunc, printTest) +-} - --go 1.12 ---- main.go -- --package main +-// DocumentHighlight is undefined, so its uses below are type errors. +-// Nevertheless, document highlighting should still work. +-//@diag(doc1, re"undefined|undeclared"), diag(doc2, re"undefined|undeclared"), diag(doc3, re"undefined|undeclared") - --func main() { +-func toProtocolHighlight(rngs []int) []DocumentHighlight { //@loc(doc1, "DocumentHighlight"),loc(docRet1, "[]DocumentHighlight"),highlight(doc1, docRet1, doc1, doc2, doc3, result) +- result := make([]DocumentHighlight, 0, len(rngs)) //@loc(doc2, "DocumentHighlight"),highlight(doc2, doc1, doc2, doc3) +- for _, rng := range rngs { +- result = append(result, DocumentHighlight{ //@loc(doc3, "DocumentHighlight"),highlight(doc3, doc1, doc2, doc3) +- Range: rng, +- }) +- } +- return result //@loc(result, "result") -} --` - -- tests := []struct { -- response string // response to choose for the telemetry dialog -- wantMode string // resulting telemetry mode -- wantMsg string // substring contained in the follow-up popup (if empty, no popup is expected) -- }{ -- {lsp.TelemetryYes, "on", "uploading is now enabled"}, -- {lsp.TelemetryNo, "", ""}, -- {"", "", ""}, -- } -- for _, test := range tests { -- t.Run(fmt.Sprintf("response=%s", test.response), func(t *testing.T) { -- modeFile := filepath.Join(t.TempDir(), "mode") -- msgRE := regexp.MustCompile(".*Would you like to enable Go telemetry?") -- respond := func(m *protocol.ShowMessageRequestParams) (*protocol.MessageActionItem, error) { -- if msgRE.MatchString(m.Message) { -- for _, item := range m.Actions { -- if item.Title == test.response { -- return &item, nil +-func testForLoops() { +- for i := 0; i < 10; i++ { //@loc(forDecl1, "for"),highlight(forDecl1, forDecl1, brk1, cont1) +- if i > 8 { +- break //@loc(brk1, "break"),highlight(brk1, forDecl1, brk1, cont1) +- } +- if i < 2 { +- for j := 1; j < 10; j++ { //@loc(forDecl2, "for"),highlight(forDecl2, forDecl2, cont2) +- if j < 3 { +- for k := 1; k < 10; k++ { //@loc(forDecl3, "for"),highlight(forDecl3, forDecl3, cont3) +- if k < 3 { +- continue //@loc(cont3, "continue"),highlight(cont3, forDecl3, cont3) - } - } -- if test.response != "" { -- t.Errorf("action item %q not found", test.response) -- } +- continue //@loc(cont2, "continue"),highlight(cont2, forDecl2, cont2) - } -- return nil, nil - } -- WithOptions( -- Modes(Default), // no need to run this in all modes -- EnvVars{ -- lsp.GoplsConfigDirEnvvar: t.TempDir(), -- lsp.FakeTelemetryModefileEnvvar: modeFile, -- }, -- Settings{ -- "telemetryPrompt": true, -- }, -- MessageResponder(respond), -- ).Run(t, src, func(t *testing.T, env *Env) { -- var postConditions []Expectation -- if test.wantMsg != "" { -- postConditions = append(postConditions, ShownMessage(test.wantMsg)) -- } -- env.OnceMet( -- CompletedWork(lsp.TelemetryPromptWorkTitle, 1, true), -- postConditions..., -- ) -- gotMode := "" -- if contents, err := os.ReadFile(modeFile); err == nil { -- gotMode = string(contents) -- } else if !os.IsNotExist(err) { -- t.Fatal(err) -- } -- if gotMode != test.wantMode { -- t.Errorf("after prompt, mode=%s, want %s", gotMode, test.wantMode) -- } -- }) -- }) +- continue //@loc(cont1, "continue"),highlight(cont1, forDecl1, brk1, cont1) +- } - } --} - --// Test that we stop asking about telemetry after the user ignores the question --// 5 times. --func TestTelemetryPrompt_GivingUp(t *testing.T) { -- const src = ` ---- go.mod -- --module mod.com -- --go 1.12 ---- main.go -- --package main +- arr := []int{} +- for i := range arr { //@loc(forDecl4, "for"),highlight(forDecl4, forDecl4, brk4, cont4) +- if i > 8 { +- break //@loc(brk4, "break"),highlight(brk4, forDecl4, brk4, cont4) +- } +- if i < 4 { +- continue //@loc(cont4, "continue"),highlight(cont4, forDecl4, brk4, cont4) +- } +- } - --func main() { +-Outer: +- for i := 0; i < 10; i++ { //@loc(forDecl5, "for"),highlight(forDecl5, forDecl5, brk5, brk6, brk8) +- break //@loc(brk5, "break"),highlight(brk5, forDecl5, brk5, brk6, brk8) +- for { //@loc(forDecl6, "for"),highlight(forDecl6, forDecl6, cont5), diag("for", re"unreachable") +- if i == 1 { +- break Outer //@loc(brk6, "break Outer"),highlight(brk6, forDecl5, brk5, brk6, brk8) +- } +- switch i { //@loc(switch1, "switch"),highlight(switch1, switch1, brk7) +- case 5: +- break //@loc(brk7, "break"),highlight(brk7, switch1, brk7) +- case 6: +- continue //@loc(cont5, "continue"),highlight(cont5, forDecl6, cont5) +- case 7: +- break Outer //@loc(brk8, "break Outer"),highlight(brk8, forDecl5, brk5, brk6, brk8) +- } +- } +- } -} --` -- -- // For this test, we want to share state across gopls sessions. -- modeFile := filepath.Join(t.TempDir(), "mode") -- configDir := t.TempDir() - -- const maxPrompts = 5 // internal prompt limit defined by gopls +-func testSwitch() { +- var i, j int - -- for i := 0; i < maxPrompts+1; i++ { -- WithOptions( -- Modes(Default), // no need to run this in all modes -- EnvVars{ -- lsp.GoplsConfigDirEnvvar: configDir, -- lsp.FakeTelemetryModefileEnvvar: modeFile, -- }, -- Settings{ -- "telemetryPrompt": true, -- }, -- ).Run(t, src, func(t *testing.T, env *Env) { -- wantPrompt := i < maxPrompts -- expectation := ShownMessageRequest(".*Would you like to enable Go telemetry?") -- if !wantPrompt { -- expectation = Not(expectation) +-L1: +- for { //@loc(forDecl7, "for"),highlight(forDecl7, forDecl7, brk10, cont6) +- L2: +- switch i { //@loc(switch2, "switch"),highlight(switch2, switch2, brk11, brk12, brk13) +- case 1: +- switch j { //@loc(switch3, "switch"),highlight(switch3, switch3, brk9) +- case 1: +- break //@loc(brk9, "break"),highlight(brk9, switch3, brk9) +- case 2: +- break L1 //@loc(brk10, "break L1"),highlight(brk10, forDecl7, brk10, cont6) +- case 3: +- break L2 //@loc(brk11, "break L2"),highlight(brk11, switch2, brk11, brk12, brk13) +- default: +- continue //@loc(cont6, "continue"),highlight(cont6, forDecl7, brk10, cont6) - } -- env.OnceMet( -- CompletedWork(lsp.TelemetryPromptWorkTitle, 1, true), -- expectation, -- ) -- }) +- case 2: +- break //@loc(brk12, "break"),highlight(brk12, switch2, brk11, brk12, brk13) +- default: +- break L2 //@loc(brk13, "break L2"),highlight(brk13, switch2, brk11, brk12, brk13) +- } - } -} - --// Test that gopls prompts for telemetry only when it is supposed to. --func TestTelemetryPrompt_Conditions2(t *testing.T) { -- const src = ` ---- go.mod -- --module mod.com +-func testReturn() bool { //@loc(func1, "func"),loc(bool1, "bool"),highlight(func1, func1, fullRet11, fullRet12),highlight(bool1, bool1, false1, bool2, true1) +- if 1 < 2 { +- return false //@loc(ret11, "return"),loc(fullRet11, "return false"),loc(false1, "false"),highlight(ret11, func1, fullRet11, fullRet12) +- } +- candidates := []int{} +- sort.SliceStable(candidates, func(i, j int) bool { //@loc(func2, "func"),loc(bool2, "bool"),highlight(func2, func2, fullRet2) +- return candidates[i] > candidates[j] //@loc(ret2, "return"),loc(fullRet2, "return candidates[i] > candidates[j]"),highlight(ret2, func2, fullRet2) +- }) +- return true //@loc(ret12, "return"),loc(fullRet12, "return true"),loc(true1, "true"),highlight(ret12, func1, fullRet11, fullRet12) +-} - --go 1.12 ---- main.go -- --package main +-func testReturnFields() float64 { //@loc(retVal1, "float64"),highlight(retVal1, retVal1, retVal11, retVal21) +- if 1 < 2 { +- return 20.1 //@loc(retVal11, "20.1"),highlight(retVal11, retVal1, retVal11, retVal21) +- } +- z := 4.3 //@loc(zDecl, "z") +- return z //@loc(retVal21, "z"),highlight(retVal21, retVal1, retVal11, zDecl, retVal21) +-} - --func main() { +-func testReturnMultipleFields() (float32, string) { //@loc(retVal31, "float32"),loc(retVal32, "string"),highlight(retVal31, retVal31, retVal41, retVal51),highlight(retVal32, retVal32, retVal42, retVal52) +- y := "im a var" //@loc(yDecl, "y"), +- if 1 < 2 { +- return 20.1, y //@loc(retVal41, "20.1"),loc(retVal42, "y"),highlight(retVal41, retVal31, retVal41, retVal51),highlight(retVal42, retVal32, yDecl, retVal42, retVal52) +- } +- return 4.9, "test" //@loc(retVal51, "4.9"),loc(retVal52, "\"test\""),highlight(retVal51, retVal31, retVal41, retVal51),highlight(retVal52, retVal32, retVal42, retVal52) -} --` -- modeFile := filepath.Join(t.TempDir(), "mode") -- WithOptions( -- Modes(Default), // no need to run this in all modes -- EnvVars{ -- lsp.GoplsConfigDirEnvvar: t.TempDir(), -- lsp.FakeTelemetryModefileEnvvar: modeFile, -- }, -- Settings{ -- // off because we are testing -- // if we can trigger the prompt with command. -- "telemetryPrompt": false, -- }, -- ).Run(t, src, func(t *testing.T, env *Env) { -- cmd, err := command.NewMaybePromptForTelemetryCommand("prompt") -- if err != nil { -- t.Fatal(err) +- +-func testReturnFunc() int32 { //@loc(retCall, "int32") +- mulch := 1 //@loc(mulchDec, "mulch"),highlight(mulchDec, mulchDec, mulchRet) +- return int32(mulch) //@loc(mulchRet, "mulch"),loc(retFunc, "int32"),loc(retTotal, "int32(mulch)"),highlight(mulchRet, mulchDec, mulchRet),highlight(retFunc, retCall, retFunc, retTotal) +-} +diff -urN a/gopls/internal/test/marker/testdata/highlight/issue60435.txt b/gopls/internal/test/marker/testdata/highlight/issue60435.txt +--- a/gopls/internal/test/marker/testdata/highlight/issue60435.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/highlight/issue60435.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,15 +0,0 @@ +-This is a regression test for issue 60435: +-Highlighting "net/http" shouldn't have any effect +-on an import path that contains it as a substring, +-such as httptest. +- +--- highlights.go -- +-package highlights +- +-import ( +- "net/http" //@loc(httpImp, `"net/http"`) +- "net/http/httptest" //@loc(httptestImp, `"net/http/httptest"`) +-) +- +-var _ = httptest.NewRequest +-var _ = http.NewRequest //@loc(here, "http"), highlight(here, here, httpImp) +diff -urN a/gopls/internal/test/marker/testdata/highlight/switchbreak.txt b/gopls/internal/test/marker/testdata/highlight/switchbreak.txt +--- a/gopls/internal/test/marker/testdata/highlight/switchbreak.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/highlight/switchbreak.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,21 +0,0 @@ +-This is a regression test for issue 65752: a break in a switch should +-highlight the switch, not the enclosing loop. +- +--- a.go -- +-package a +- +-func _(x any) { +- for { +- // type switch +- switch x.(type) { //@loc(tswitch, "switch") +- default: +- break //@highlight("break", tswitch, "break") - } -- var result error -- env.ExecuteCommand(&protocol.ExecuteCommandParams{ -- Command: cmd.Command, -- }, &result) -- if result != nil { -- t.Fatal(err) +- +- // value switch +- switch { //@loc(vswitch, "switch") +- default: +- break //@highlight("break", vswitch, "break") - } -- expectation := ShownMessageRequest(".*Would you like to enable Go telemetry?") -- env.OnceMet( -- CompletedWork(lsp.TelemetryPromptWorkTitle, 2, true), -- expectation, -- ) -- }) +- } -} -diff -urN a/gopls/internal/regtest/misc/references_test.go b/gopls/internal/regtest/misc/references_test.go ---- a/gopls/internal/regtest/misc/references_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/misc/references_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,581 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +diff -urN a/gopls/internal/test/marker/testdata/hover/basiclit.txt b/gopls/internal/test/marker/testdata/hover/basiclit.txt +--- a/gopls/internal/test/marker/testdata/hover/basiclit.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/hover/basiclit.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,82 +0,0 @@ +-This test checks gopls behavior when hovering over basic literals. +- +--- basiclit.go -- +-package basiclit +- +-func _() { +- _ = 'a' //@hover("'a'", "'a'", latinA) +- _ = 0x61 //@hover("0x61", "0x61", latinAHex) +- +- _ = '\u2211' //@hover("'\\u2211'", "'\\u2211'", summation) +- _ = 0x2211 //@hover("0x2211", "0x2211", summationHex) +- _ = "foo \u2211 bar" //@hover("\\u2211", "\\u2211", summation) +- +- _ = '\a' //@hover("'\\a'", "'\\a'", control) +- _ = "foo \a bar" //@hover("\\a", "\\a", control) +- +- _ = '\U0001F30A' //@hover("'\\U0001F30A'", "'\\U0001F30A'", waterWave) +- _ = 0x0001F30A //@hover("0x0001F30A", "0x0001F30A", waterWaveHex) +- _ = 0X0001F30A //@hover("0X0001F30A", "0X0001F30A", waterWaveHex) +- _ = "foo \U0001F30A bar" //@hover("\\U0001F30A", "\\U0001F30A", waterWave) +- +- _ = '\x7E' //@hover("'\\x7E'", "'\\x7E'", tilde) +- _ = "foo \x7E bar" //@hover("\\x7E", "\\x7E", tilde) +- _ = "foo \a bar" //@hover("\\a", "\\a", control) +- +- _ = '\173' //@hover("'\\173'", "'\\173'", leftCurly) +- _ = "foo \173 bar" //@hover("\\173","\\173", leftCurly) +- _ = "foo \173 bar \u2211 baz" //@hover("\\173","\\173", leftCurly) +- _ = "foo \173 bar \u2211 baz" //@hover("\\u2211","\\u2211", summation) +- _ = "foo\173bar\u2211baz" //@hover("\\173","\\173", leftCurly) +- _ = "foo\173bar\u2211baz" //@hover("\\u2211","\\u2211", summation) +- +- // search for runes in string only if there is an escaped sequence +- _ = "hello" //@hover(`"hello"`, _, _) +- +- // incorrect escaped rune sequences +- _ = '\0' //@hover("'\\0'", _, _),diag(re`\\0()'`, re"illegal character") +- _ = '\u22111' //@hover("'\\u22111'", _, _) +- _ = '\U00110000' //@hover("'\\U00110000'", _, _) +- _ = '\u12e45'//@hover("'\\u12e45'", _, _) +- _ = '\xa' //@hover("'\\xa'", _, _) +- _ = 'aa' //@hover("'aa'", _, _) +- +- // other basic lits +- _ = 1 //@hover("1", _, _) +- _ = 1.2 //@hover("1.2", _, _) +- _ = 1.2i //@hover("1.2i", _, _) +- _ = 0123 //@hover("0123", _, _) +- _ = 0b1001 //@hover("0b", "0b1001", binaryNumber) +- _ = 0B1001 //@hover("0B", "0B1001", binaryNumber) +- _ = 0o77 //@hover("0o", "0o77", octalNumber) +- _ = 0O77 //@hover("0O", "0O77", octalNumber) +- _ = 0x1234567890 //@hover("0x1234567890", "0x1234567890", hexNumber) +- _ = 0X1234567890 //@hover("0X1234567890", "0X1234567890", hexNumber) +- _ = 0x1000000000000000000 //@hover("0x1", "0x1000000000000000000", bigHex) +-) +--- @bigHex -- +-4722366482869645213696 +--- @binaryNumber -- +-9 +--- @control -- +-U+0007, control +--- @hexNumber -- +-78187493520 +--- @latinA -- +-'a', U+0061, LATIN SMALL LETTER A +--- @latinAHex -- +-97, 'a', U+0061, LATIN SMALL LETTER A +--- @leftCurly -- +-'{', U+007B, LEFT CURLY BRACKET +--- @octalNumber -- +-63 +--- @summation -- +-'∑', U+2211, N-ARY SUMMATION +--- @summationHex -- +-8721, '∑', U+2211, N-ARY SUMMATION +--- @tilde -- +-'~', U+007E, TILDE +--- @waterWave -- +-'🌊', U+1F30A, WATER WAVE +--- @waterWaveHex -- +-127754, '🌊', U+1F30A, WATER WAVE +diff -urN a/gopls/internal/test/marker/testdata/hover/const.txt b/gopls/internal/test/marker/testdata/hover/const.txt +--- a/gopls/internal/test/marker/testdata/hover/const.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/hover/const.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,154 +0,0 @@ +-This test checks hovering over constants. +- +--- go.mod -- +-module mod.com - --package misc +-go 1.17 - --import ( -- "fmt" -- "os" -- "path/filepath" -- "reflect" -- "sort" -- "strings" -- "testing" +--- c.go -- +-package c - -- "github.com/google/go-cmp/cmp" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/regtest" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" -- "golang.org/x/tools/internal/testenv" +-import ( +- "math" +- "time" -) - --func TestStdlibReferences(t *testing.T) { -- const files = ` ---- go.mod -- --module mod.com +-const X = 0 //@hover("X", "X", bX) - --go 1.12 ---- main.go -- --package main +-// dur is a constant of type time.Duration. +-const dur = 15*time.Minute + 10*time.Second + 350*time.Millisecond //@hover("dur", "dur", dur) - --import "fmt" +-// MaxFloat32 is used in another package. +-const MaxFloat32 = 0x1p127 * (1 + (1 - 0x1p-23)) - --func main() { -- fmt.Print() --} --` +-// Numbers. +-func _() { +- const hex, bin = 0xe34e, 0b1001001 - -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- loc := env.GoToDefinition(env.RegexpSearch("main.go", `fmt.(Print)`)) -- refs, err := env.Editor.References(env.Ctx, loc) -- if err != nil { -- t.Fatal(err) -- } -- if len(refs) != 2 { -- // TODO(adonovan): make this assertion less maintainer-hostile. -- t.Fatalf("got %v reference(s), want 2", len(refs)) -- } -- // The first reference is guaranteed to be the definition. -- if got, want := refs[1].URI, env.Sandbox.Workdir.URI("main.go"); got != want { -- t.Errorf("found reference in %v, wanted %v", got, want) -- } -- }) --} +- const ( +- // no inline comment +- decimal = 153 - --// This is a regression test for golang/go#48400 (a panic). --func TestReferencesOnErrorMethod(t *testing.T) { -- // Ideally this would actually return the correct answer, -- // instead of merely failing gracefully. -- const files = ` ---- go.mod -- --module mod.com +- numberWithUnderscore int64 = 10_000_000_000 +- octal = 0o777 +- expr = 2 << (0b111&0b101 - 2) +- boolean = (55 - 3) == (26 * 2) +- ) - --go 1.12 ---- main.go -- --package main +- _ = decimal //@hover("decimal", "decimal", decimalConst) +- _ = hex //@hover("hex", "hex", hexConst) +- _ = bin //@hover("bin", "bin", binConst) +- _ = numberWithUnderscore //@hover("numberWithUnderscore", "numberWithUnderscore", numberWithUnderscoreConst) +- _ = octal //@hover("octal", "octal", octalConst) +- _ = expr //@hover("expr", "expr", exprConst) +- _ = boolean //@hover("boolean", "boolean", boolConst) - --type t interface { -- error +- const ln10 = 2.30258509299404568401799145468436420760110148862877297603332790 +- +- _ = ln10 //@hover("ln10", "ln10", ln10Const) -} - --type s struct{} +-// Iota. +-func _() { +- const ( +- a = 1 << iota +- b +- ) - --func (*s) Error() string { -- return "" +- _ = a //@hover("a", "a", aIota) +- _ = b //@hover("b", "b", bIota) -} - +-// Strings. -func _() { -- var s s -- _ = s.Error() +- const ( +- str = "hello" + " " + "world" +- longStr = `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur eget ipsum non nunc +-molestie mattis id quis augue. Mauris dictum tincidunt ipsum, in auctor arcu congue eu. +-Morbi hendrerit fringilla libero commodo varius. Vestibulum in enim rutrum, rutrum tellus +-aliquet, luctus enim. Nunc sem ex, consectetur id porta nec, placerat vel urna.` +- ) +- +- _ = str //@hover("str", "str", strConst) +- _ = longStr //@hover("longStr", "longStr", longStrConst) -} --` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- loc := env.GoToDefinition(env.RegexpSearch("main.go", `Error`)) -- refs, err := env.Editor.References(env.Ctx, loc) -- if err != nil { -- t.Fatalf("references on (*s).Error failed: %v", err) -- } -- // TODO(adonovan): this test is crying out for marker support in regtests. -- var buf strings.Builder -- for _, ref := range refs { -- fmt.Fprintf(&buf, "%s %s\n", env.Sandbox.Workdir.URIToPath(ref.URI), ref.Range) -- } -- got := buf.String() -- want := "main.go 8:10-8:15\n" + // (*s).Error decl -- "main.go 14:7-14:12\n" // s.Error() call -- if diff := cmp.Diff(want, got); diff != "" { -- t.Errorf("unexpected references on (*s).Error (-want +got):\n%s", diff) -- } -- }) +- +-// Constants from other packages. +-func _() { +- _ = math.Log2E //@hover("Log2E", "Log2E", log2eConst) -} - --func TestDefsRefsBuiltins(t *testing.T) { -- testenv.NeedsGo1Point(t, 17) // for unsafe.{Add,Slice} -- // TODO(adonovan): add unsafe.{SliceData,String,StringData} in later go versions. -- const files = ` ---- go.mod -- --module example.com --go 1.16 +--- @bX -- +-```go +-const X untyped int = 0 +-``` - ---- a.go -- --package a +-@hover("X", "X", bX) - --import "unsafe" - --const _ = iota --var _ error --var _ int --var _ = append() --var _ = unsafe.Pointer(nil) --var _ = unsafe.Add(nil, nil) --var _ = unsafe.Sizeof(0) --var _ = unsafe.Alignof(0) --var _ = unsafe.Slice(nil, 0) --` +-[`c.X` on pkg.go.dev](https://pkg.go.dev/mod.com#X) +--- @dur -- +-```go +-const dur time.Duration = 15*time.Minute + 10*time.Second + 350*time.Millisecond // 15m10.35s +-``` - -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("a.go") -- for _, name := range strings.Fields( -- "iota error int nil append iota Pointer Sizeof Alignof Add Slice") { -- loc := env.RegexpSearch("a.go", `\b`+name+`\b`) +-dur is a constant of type time.Duration. +--- @decimalConst -- +-```go +-const decimal untyped int = 153 +-``` - -- // definition -> {builtin,unsafe}.go -- def := env.GoToDefinition(loc) -- if (!strings.HasSuffix(string(def.URI), "builtin.go") && -- !strings.HasSuffix(string(def.URI), "unsafe.go")) || -- def.Range.Start.Line == 0 { -- t.Errorf("definition(%q) = %v, want {builtin,unsafe}.go", -- name, def) -- } +-no inline comment +--- @hexConst -- +-```go +-const hex untyped int = 0xe34e // 58190 +-``` +--- @binConst -- +-```go +-const bin untyped int = 0b1001001 // 73 +-``` +--- @numberWithUnderscoreConst -- +-```go +-const numberWithUnderscore int64 = 10_000_000_000 // 10000000000 +-``` +--- @octalConst -- +-```go +-const octal untyped int = 0o777 // 511 +-``` +--- @exprConst -- +-```go +-const expr untyped int = 2 << (0b111&0b101 - 2) // 16 +-``` +--- @boolConst -- +-```go +-const boolean untyped bool = (55 - 3) == (26 * 2) // true +-``` +--- @ln10Const -- +-```go +-const ln10 untyped float = 2.30258509299404568401799145468436420760110148862877297603332790 // 2.30259 +-``` +--- @aIota -- +-```go +-const a untyped int = 1 << iota // 1 +-``` +--- @bIota -- +-```go +-const b untyped int = 2 +-``` +--- @strConst -- +-```go +-const str untyped string = "hello world" +-``` +--- @longStrConst -- +-```go +-const longStr untyped string = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur e... +-``` +--- @log2eConst -- +-```go +-const math.Log2E untyped float = 1 / Ln2 // 1.4427 +-``` - -- // "references to (builtin "Foo"|unsafe.Foo) are not supported" -- _, err := env.Editor.References(env.Ctx, loc) -- gotErr := fmt.Sprint(err) -- if !strings.Contains(gotErr, "references to") || -- !strings.Contains(gotErr, "not supported") || -- !strings.Contains(gotErr, name) { -- t.Errorf("references(%q) error: got %q, want %q", -- name, gotErr, "references to ... are not supported") -- } -- } -- }) --} +-Mathematical constants. - --func TestPackageReferences(t *testing.T) { -- tests := []struct { -- packageName string -- wantRefCount int -- wantFiles []string -- }{ -- { -- "lib1", -- 3, -- []string{ -- "main.go", -- "lib1/a.go", -- "lib1/b.go", -- }, -- }, -- { -- "lib2", -- 2, -- []string{ -- "main.go", -- "lib2/a.go", -- }, -- }, -- } - -- const files = ` +-[`math.Log2E` on pkg.go.dev](https://pkg.go.dev/math#Log2E) +diff -urN a/gopls/internal/test/marker/testdata/hover/embed.txt b/gopls/internal/test/marker/testdata/hover/embed.txt +--- a/gopls/internal/test/marker/testdata/hover/embed.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/hover/embed.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,57 +0,0 @@ +-This test checks that hover reports accessible embedded fields +-(after the doc comment and before the accessible methods). +- --- go.mod -- --module mod.com +-module example.com - -go 1.18 ---- lib1/a.go -- --package lib1 - --const A = 1 +--- q/q.go -- +-package q - ---- lib1/b.go -- --package lib1 +-type Q struct { +- One int +- two string +- q2[chan int] +-} - --const B = 1 +-type q2[T any] struct { +- Three *T +- four string +-} - ---- lib2/a.go -- --package lib2 +--- p.go -- +-package p - --const C = 1 +-import "example.com/q" - ---- main.go -- --package main +-// doc +-type P struct { +- q.Q +-} - --import ( -- "mod.com/lib1" -- "mod.com/lib2" --) +-func (P) m() {} - --func main() { -- println("Hello") --} --` -- Run(t, files, func(t *testing.T, env *Env) { -- for _, test := range tests { -- file := fmt.Sprintf("%s/a.go", test.packageName) -- env.OpenFile(file) -- loc := env.RegexpSearch(file, test.packageName) -- refs := env.References(loc) -- if len(refs) != test.wantRefCount { -- // TODO(adonovan): make this assertion less maintainer-hostile. -- t.Fatalf("got %v reference(s), want %d", len(refs), test.wantRefCount) -- } -- var refURIs []string -- for _, ref := range refs { -- refURIs = append(refURIs, string(ref.URI)) -- } -- for _, base := range test.wantFiles { -- hasBase := false -- for _, ref := range refURIs { -- if strings.HasSuffix(ref, base) { -- hasBase = true -- break -- } -- } -- if !hasBase { -- t.Fatalf("got [%v], want reference ends with \"%v\"", strings.Join(refURIs, ","), base) -- } -- } -- } -- }) +-var p P //@hover("P", "P", P) +- +--- @P -- +-```go +-type P struct { +- q.Q -} +-``` - --// Test for golang/go#43144. --// --// Verify that we search for references and implementations in intermediate --// test variants. --func TestReferencesInTestVariants(t *testing.T) { -- const files = ` ---- go.mod -- --module foo.mod +-doc - --go 1.12 ---- foo/foo.go -- --package foo - --import "foo.mod/bar" +-```go +-// Embedded fields: +-One int // through Q +-Three *chan int // through Q.q2 +-``` - --const Foo = 42 +-```go +-func (P) m() +-``` - --type T int --type InterfaceM interface{ M() } --type InterfaceF interface{ F() } +-[`p.P` on pkg.go.dev](https://pkg.go.dev/example.com#P) +diff -urN a/gopls/internal/test/marker/testdata/hover/generics.txt b/gopls/internal/test/marker/testdata/hover/generics.txt +--- a/gopls/internal/test/marker/testdata/hover/generics.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/hover/generics.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,85 +0,0 @@ +-This file contains tests for hovering over generic Go code. - --func _() { -- _ = bar.Blah --} +-Requires go1.20+ for the new go/doc/comment package, and a change in Go 1.20 +-that affected the formatting of constraint interfaces. - ---- foo/foo_test.go -- --package foo +--- flags -- +--min_go=go1.20 - --type Fer struct{} --func (Fer) F() {} +--- go.mod -- +-// A go.mod is require for correct pkgsite links. +-// TODO(rfindley): don't link to ad-hoc or command-line-arguments packages! +-module mod.com - ---- bar/bar.go -- --package bar +-go 1.18 - --var Blah = 123 +--- generics.go -- +-package generics - ---- bar/bar_test.go -- --package bar +-type value[T any] struct { //hover("lue", "value", value),hover("T", "T", valueT) +- val T //@hover("T", "T", valuevalT) +- Q int64 //@hover("Q", "Q", valueQ) +-} - --type Mer struct{} --func (Mer) M() {} +-type Value[T any] struct { //@hover("T", "T", ValueT) +- val T //@hover("T", "T", ValuevalT) +- Q int64 //@hover("Q", "Q", ValueQ) +-} - --func TestBar() { -- _ = Blah +-func F[P interface{ ~int | string }]() { //@hover("P", "P", Ptparam) +- var _ P //@hover("P","P",Pvar) -} ---- bar/bar_x_test.go -- --package bar_test - --import ( -- "foo.mod/bar" -- "foo.mod/foo" --) +--- inferred.go -- +-package generics - --type Mer struct{} --func (Mer) M() {} +-func app[S interface{ ~[]E }, E interface{}](s S, e E) S { +- return append(s, e) +-} - -func _() { -- _ = bar.Blah -- _ = foo.Foo +- _ = app[[]int] //@hover("app", "app", appint) +- _ = app[[]int, int] //@hover("app", "app", appint) +- _ = app[[]int]([]int{}, 0) //@hover("app", "app", appint), diag("[[]int]", re"unnecessary") +- _ = app([]int{}, 0) //@hover("app", "app", appint) -} --` - -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("foo/foo.go") +--- @ValueQ -- +-```go +-field Q int64 // size=8 +-``` - -- refTests := []struct { -- re string -- wantRefs []string -- }{ -- // Blah is referenced: -- // - inside the foo.mod/bar (ordinary) package -- // - inside the foo.mod/bar [foo.mod/bar.test] test variant package -- // - from the foo.mod/bar_test [foo.mod/bar.test] x_test package -- // - from the foo.mod/foo package -- {"Blah", []string{"bar/bar.go:3", "bar/bar_test.go:7", "bar/bar_x_test.go:12", "foo/foo.go:12"}}, +-@hover("Q", "Q", ValueQ) - -- // Foo is referenced in bar_x_test.go via the intermediate test variant -- // foo.mod/foo [foo.mod/bar.test]. -- {"Foo", []string{"bar/bar_x_test.go:13", "foo/foo.go:5"}}, -- } - -- for _, test := range refTests { -- loc := env.RegexpSearch("foo/foo.go", test.re) -- refs := env.References(loc) +-[`(generics.Value).Q` on pkg.go.dev](https://pkg.go.dev/mod.com#Value.Q) +--- @ValueT -- +-```go +-type parameter T any +-``` +--- @ValuevalT -- +-```go +-type parameter T any +-``` +--- @appint -- +-```go +-func app(s []int, e int) []int // func[S interface{~[]E}, E interface{}](s S, e E) S +-``` +--- @valueQ -- +-```go +-field Q int64 // size=8 +-``` - -- got := fileLocations(env, refs) -- if diff := cmp.Diff(test.wantRefs, got); diff != "" { -- t.Errorf("References(%q) returned unexpected diff (-want +got):\n%s", test.re, diff) -- } -- } +-@hover("Q", "Q", valueQ) +--- @valuevalT -- +-```go +-type parameter T any +-``` +--- @Ptparam -- +-```go +-type parameter P interface{~int | string} +-``` +--- @Pvar -- +-```go +-type parameter P interface{~int | string} +-``` +diff -urN a/gopls/internal/test/marker/testdata/hover/godef.txt b/gopls/internal/test/marker/testdata/hover/godef.txt +--- a/gopls/internal/test/marker/testdata/hover/godef.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/hover/godef.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,426 +0,0 @@ +-This test was ported from 'godef' in the old marker tests. +-It tests various hover and definition requests. - -- implTests := []struct { -- re string -- wantImpls []string -- }{ -- // InterfaceM is implemented both in foo.mod/bar [foo.mod/bar.test] (which -- // doesn't import foo), and in foo.mod/bar_test [foo.mod/bar.test], which -- // imports the test variant of foo. -- {"InterfaceM", []string{"bar/bar_test.go:3", "bar/bar_x_test.go:8"}}, +-Requires go1.19+ for the new go/doc/comment package. - -- // A search within the ordinary package to should find implementations -- // (Fer) within the augmented test package. -- {"InterfaceF", []string{"foo/foo_test.go:3"}}, -- } +-TODO(adonovan): figure out why this test also fails +-without -min_go=go1.20. Or just wait... - -- for _, test := range implTests { -- loc := env.RegexpSearch("foo/foo.go", test.re) -- impls := env.Implementations(loc) +--- flags -- +--min_go=go1.19 - -- got := fileLocations(env, impls) -- if diff := cmp.Diff(test.wantImpls, got); diff != "" { -- t.Errorf("Implementations(%q) returned unexpected diff (-want +got):\n%s", test.re, diff) -- } -- } -- }) --} +--- flags -- +--min_go=go1.20 - --// This is a regression test for Issue #56169, in which interface --// implementations in vendored modules were not found. The actual fix --// was the same as for #55995; see TestVendoringInvalidatesMetadata. --func TestImplementationsInVendor(t *testing.T) { -- t.Skip("golang/go#56169: file watching does not capture vendor dirs") +--- go.mod -- +-module godef.test - -- const proxy = ` ---- other.com/b@v1.0.0/go.mod -- --module other.com/b --go 1.14 +-go 1.18 - ---- other.com/b@v1.0.0/b.go -- --package b --type B int --func (B) F() {} --` -- const src = ` ---- go.mod -- --module example.com/a --go 1.14 --require other.com/b v1.0.0 +--- a/a_x_test.go -- +-package a_test - ---- go.sum -- --other.com/b v1.0.0 h1:9WyCKS+BLAMRQM0CegP6zqP2beP+ShTbPaARpNY31II= --other.com/b v1.0.0/go.mod h1:TgHQFucl04oGT+vrUm/liAzukYHNxCwKNkQZEyn3m9g= +-import ( +- "testing" +-) - ---- a.go -- --package a --import "other.com/b" --type I interface { F() } --var _ b.B +-func TestA2(t *testing.T) { //@hover("TestA2", "TestA2", TestA2) +- Nonexistant() //@diag("Nonexistant", re"(undeclared name|undefined): Nonexistant") +-} +- +--- @TestA2 -- +-```go +-func TestA2(t *testing.T) +-``` +--- @ember -- +-```go +-field Member string +-``` +- +-@loc(Member, "Member") +- +- +-[`(a.Thing).Member` on pkg.go.dev](https://pkg.go.dev/godef.test/a#Thing.Member) +--- a/d.go -- +-package a //@hover("a", _, a) - --` -- WithOptions( -- ProxyFiles(proxy), -- Modes(Default), // fails in 'experimental' mode -- ).Run(t, src, func(t *testing.T, env *Env) { -- // Enable to debug go.sum mismatch, which may appear as -- // "module lookup disabled by GOPROXY=off", confusingly. -- if false { -- env.DumpGoSum(".") -- } +-import "fmt" - -- checkVendor := func(locs []protocol.Location, wantVendor bool) { -- if len(locs) != 1 { -- t.Errorf("got %d locations, want 1", len(locs)) -- } else if strings.Contains(string(locs[0].URI), "/vendor/") != wantVendor { -- t.Errorf("got location %s, wantVendor=%t", locs[0], wantVendor) -- } -- } +-type Thing struct { //@loc(Thing, "Thing") +- Member string //@loc(Member, "Member") +-} - -- env.OpenFile("a.go") -- refLoc := env.RegexpSearch("a.go", "I") // find "I" reference +-var Other Thing //@loc(Other, "Other") - -- // Initially, a.I has one implementation b.B in -- // the module cache, not the vendor tree. -- checkVendor(env.Implementations(refLoc), false) +-func Things(val []string) []Thing { //@loc(Things, "Things") +- return nil +-} - -- // Run 'go mod vendor' outside the editor. -- if err := env.Sandbox.RunGoCommand(env.Ctx, ".", "mod", []string{"vendor"}, nil, true); err != nil { -- t.Fatalf("go mod vendor: %v", err) -- } +-func (t Thing) Method(i int) string { //@loc(Method, "Method") +- return t.Member +-} - -- // Synchronize changes to watched files. -- env.Await(env.DoneWithChangeWatchedFiles()) +-func (t Thing) Method3() { +-} - -- // Now, b.B is found in the vendor tree. -- checkVendor(env.Implementations(refLoc), true) +-func (t *Thing) Method2(i int, j int) (error, string) { +- return nil, t.Member +-} - -- // Delete the vendor tree. -- if err := os.RemoveAll(env.Sandbox.Workdir.AbsPath("vendor")); err != nil { -- t.Fatal(err) -- } -- // Notify the server of the deletion. -- if err := env.Sandbox.Workdir.CheckForFileChanges(env.Ctx); err != nil { -- t.Fatal(err) -- } +-func (t *Thing) private() { +-} - -- // Synchronize again. -- env.Await(env.DoneWithChangeWatchedFiles()) +-func useThings() { +- t := Thing{ //@hover("ing", "Thing", ing) +- Member: "string", //@hover("ember", "Member", ember), def("ember", Member) +- } +- fmt.Print(t.Member) //@hover("ember", "Member", ember), def("ember", Member) +- fmt.Print(Other) //@hover("ther", "Other", ther), def("ther", Other) +- Things(nil) //@hover("ings", "Things", ings), def("ings", Things) +- t.Method(0) //@hover("eth", "Method", eth), def("eth", Method) +-} - -- // b.B is once again defined in the module cache. -- checkVendor(env.Implementations(refLoc), false) -- }) +-type NextThing struct { //@loc(NextThing, "NextThing") +- Thing +- Value int -} - --// This test can't be expressed as a marker test because the marker --// test framework opens all files (which is a bit of a hack), creating --// a <command-line-arguments> package for packages that otherwise --// wouldn't be found from the go.work file. --func TestReferencesFromWorkspacePackages59674(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) // for go.work support -- const src = ` ---- a/go.mod -- --module example.com/a --go 1.12 +-func (n NextThing) another() string { +- return n.Member +-} - ---- b/go.mod -- --module example.com/b --go 1.12 +-// Shadows Thing.Method3 +-func (n *NextThing) Method3() int { +- return n.Value +-} - ---- c/go.mod -- --module example.com/c --go 1.12 +-var nextThing NextThing //@hover("NextThing", "NextThing", NextThing), def("NextThing", NextThing) - ---- lib/go.mod -- --module example.com/lib --go 1.12 +--- @ings -- +-```go +-func Things(val []string) []Thing +-``` - ---- go.work -- --use ./a --use ./b --// don't use ./c --use ./lib +-[`a.Things` on pkg.go.dev](https://pkg.go.dev/godef.test/a#Things) +--- @ther -- +-```go +-var Other Thing +-``` - ---- a/a.go -- --package a +-@loc(Other, "Other") - --import "example.com/lib" - --var _ = lib.F // query here +-[`a.Other` on pkg.go.dev](https://pkg.go.dev/godef.test/a#Other) +--- @a -- +--- @ing -- +-```go +-type Thing struct { +- Member string //@loc(Member, "Member") +-} +-``` - ---- b/b.go -- --package b +-```go +-func (t Thing) Method(i int) string +-func (t *Thing) Method2(i int, j int) (error, string) +-func (t Thing) Method3() +-func (t *Thing) private() +-``` - --import "example.com/lib" +-[`a.Thing` on pkg.go.dev](https://pkg.go.dev/godef.test/a#Thing) +--- @NextThing -- +-```go +-type NextThing struct { +- Thing +- Value int +-} +-``` - --var _ = lib.F // also found by references +-```go +-// Embedded fields: +-Member string // through Thing +-``` - ---- c/c.go -- --package c +-```go +-func (t Thing) Method(i int) string +-func (t *Thing) Method2(i int, j int) (error, string) +-func (n *NextThing) Method3() int +-func (n NextThing) another() string +-func (t *Thing) private() +-``` - --import "example.com/lib" +-[`a.NextThing` on pkg.go.dev](https://pkg.go.dev/godef.test/a#NextThing) +--- @eth -- +-```go +-func (t Thing) Method(i int) string +-``` - --var _ = lib.F // this reference should not be reported +-[`(a.Thing).Method` on pkg.go.dev](https://pkg.go.dev/godef.test/a#Thing.Method) +--- a/f.go -- +-// Package a is a package for testing go to definition. +-package a - ---- lib/lib.go -- --package lib +-import "fmt" - --func F() {} // declaration --` -- Run(t, src, func(t *testing.T, env *Env) { -- env.OpenFile("a/a.go") -- refLoc := env.RegexpSearch("a/a.go", "F") -- got := fileLocations(env, env.References(refLoc)) -- want := []string{"a/a.go:5", "b/b.go:5", "lib/lib.go:3"} -- if diff := cmp.Diff(want, got); diff != "" { -- t.Errorf("incorrect References (-want +got):\n%s", diff) -- } -- }) --} +-func TypeStuff() { +- var x string - --// Test an 'implementation' query on a type that implements 'error'. --// (Unfortunately builtin locations cannot be expressed using @loc --// in the marker test framework.) --func TestImplementationsOfError(t *testing.T) { -- const src = ` ---- go.mod -- --module example.com --go 1.12 +- switch y := interface{}(x).(type) { //@loc(y, "y"), hover("y", "y", y) , def("y", y) +- case int: //@loc(intY, "int") +- fmt.Printf("%v", y) //@hover("y", "y", inty), def("y", y) +- case string: //@loc(stringY, "string") +- fmt.Printf("%v", y) //@hover("y", "y", stringy), def("y", y) +- } - ---- a.go -- +-} +--- @inty -- +-```go +-var y int +-``` +--- @stringy -- +-```go +-var y string +-``` +--- @y -- +-```go +-var y interface{} +-``` +--- a/h.go -- -package a - --type Error2 interface { -- Error() string +-func _() { +- type s struct { +- nested struct { +- // nested number +- number int64 //@loc(nestedNumber, "number") +- } +- nested2 []struct { +- // nested string +- str string //@loc(nestedString, "str") +- } +- x struct { +- x struct { +- x struct { +- x struct { +- x struct { +- // nested map +- m map[string]float64 //@loc(nestedMap, "m") +- } +- } +- } +- } +- } +- } +- +- var t s +- _ = t.nested.number //@hover("number", "number", nestedNumber), def("number", nestedNumber) +- _ = t.nested2[0].str //@hover("str", "str", nestedString), def("str", nestedString) +- _ = t.x.x.x.x.x.m //@hover("m", "m", nestedMap), def("m", nestedMap) -} - --type MyError int --func (MyError) Error() string { return "" } +-func _() { +- var s struct { +- // a field +- a int //@loc(structA, "a") +- // b nested struct +- b struct { //@loc(structB, "b") +- // c field of nested struct +- c int //@loc(structC, "c") +- } +- } +- _ = s.a //@def("a", structA) +- _ = s.b //@def("b", structB) +- _ = s.b.c //@def("c", structC) - --type MyErrorPtr int --func (*MyErrorPtr) Error() string { return "" } --` -- Run(t, src, func(t *testing.T, env *Env) { -- env.OpenFile("a.go") +- var arr []struct { +- // d field +- d int //@loc(arrD, "d") +- // e nested struct +- e struct { //@loc(arrE, "e") +- // f field of nested struct +- f int //@loc(arrF, "f") +- } +- } +- _ = arr[0].d //@def("d", arrD) +- _ = arr[0].e //@def("e", arrE) +- _ = arr[0].e.f //@def("f", arrF) - -- for _, test := range []struct { -- re string -- want []string -- }{ -- // error type -- {"Error2", []string{"a.go:10", "a.go:7", "std:builtin/builtin.go"}}, -- {"MyError", []string{"a.go:3", "std:builtin/builtin.go"}}, -- {"MyErrorPtr", []string{"a.go:3", "std:builtin/builtin.go"}}, -- // error.Error method -- {"(Error).. string", []string{"a.go:11", "a.go:8", "std:builtin/builtin.go"}}, -- {"MyError. (Error)", []string{"a.go:4", "std:builtin/builtin.go"}}, -- {"MyErrorPtr. (Error)", []string{"a.go:4", "std:builtin/builtin.go"}}, -- } { -- matchLoc := env.RegexpSearch("a.go", test.re) -- impls := env.Implementations(matchLoc) -- got := fileLocations(env, impls) -- if !reflect.DeepEqual(got, test.want) { -- t.Errorf("Implementations(%q) = %q, want %q", -- test.re, got, test.want) +- var complex []struct { +- c <-chan map[string][]struct { +- // h field +- h int //@loc(complexH, "h") +- // i nested struct +- i struct { //@loc(complexI, "i") +- // j field of nested struct +- j int //@loc(complexJ, "j") - } - } -- }) +- } +- _ = (<-complex[0].c)["0"][0].h //@def("h", complexH) +- _ = (<-complex[0].c)["0"][0].i //@def("i", complexI) +- _ = (<-complex[0].c)["0"][0].i.j //@def("j", complexJ) +- +- var mapWithStructKey map[struct { //@diag("struct", re"invalid map key") +- // X key field +- x []string //@loc(mapStructKeyX, "x") +- }]int +- for k := range mapWithStructKey { +- _ = k.x //@def("x", mapStructKeyX) +- } +- +- var mapWithStructKeyAndValue map[struct { +- // Y key field +- y string //@loc(mapStructKeyY, "y") +- }]struct { +- // X value field +- x string //@loc(mapStructValueX, "x") +- } +- for k, v := range mapWithStructKeyAndValue { +- // TODO: we don't show docs for y field because both map key and value +- // are structs. And in this case, we parse only map value +- _ = k.y //@hover("y", "y", hoverStructKeyY), def("y", mapStructKeyY) +- _ = v.x //@hover("x", "x", hoverStructKeyX), def("x", mapStructValueX) +- } +- +- var i []map[string]interface { +- // open method comment +- open() error //@loc(openMethod, "open") +- } +- i[0]["1"].open() //@hover("pen","open", openMethod), def("open", openMethod) -} - --// fileLocations returns a new sorted array of the --// relative file name and line number of each location. --// Duplicates are not removed. --// Standard library filenames are abstracted for robustness. --func fileLocations(env *regtest.Env, locs []protocol.Location) []string { -- got := make([]string, 0, len(locs)) -- for _, loc := range locs { -- path := env.Sandbox.Workdir.URIToPath(loc.URI) // (slashified) -- if i := strings.LastIndex(path, "/src/"); i >= 0 && filepath.IsAbs(path) { -- // Absolute path with "src" segment: assume it's in GOROOT. -- // Strip directory and don't add line/column since they are fragile. -- path = "std:" + path[i+len("/src/"):] -- } else { -- path = fmt.Sprintf("%s:%d", path, loc.Range.Start.Line+1) +-func _() { +- test := struct { +- // test description +- desc string //@loc(testDescription, "desc") +- }{} +- _ = test.desc //@def("desc", testDescription) +- +- for _, tt := range []struct { +- // test input +- in map[string][]struct { //@loc(testInput, "in") +- // test key +- key string //@loc(testInputKey, "key") +- // test value +- value interface{} //@loc(testInputValue, "value") - } -- got = append(got, path) +- result struct { +- v <-chan struct { +- // expected test value +- value int //@loc(testResultValue, "value") +- } +- } +- }{} { +- _ = tt.in //@def("in", testInput) +- _ = tt.in["0"][0].key //@def("key", testInputKey) +- _ = tt.in["0"][0].value //@def("value", testInputValue) +- +- _ = (<-tt.result.v).value //@def("value", testResultValue) - } -- sort.Strings(got) -- return got -} -diff -urN a/gopls/internal/regtest/misc/rename_test.go b/gopls/internal/regtest/misc/rename_test.go ---- a/gopls/internal/regtest/misc/rename_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/misc/rename_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,935 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package misc +-func _() { +- getPoints := func() []struct { +- // X coord +- x int //@loc(returnX, "x") +- // Y coord +- y int //@loc(returnY, "y") +- } { +- return nil +- } +- +- r := getPoints() +- _ = r[0].x //@def("x", returnX) +- _ = r[0].y //@def("y", returnY) +-} +--- @hoverStructKeyX -- +-```go +-field x string +-``` +- +-X value field +--- @hoverStructKeyY -- +-```go +-field y string +-``` +- +-Y key field +--- @nestedNumber -- +-```go +-field number int64 +-``` +- +-nested number +--- @nestedString -- +-```go +-field str string +-``` +- +-nested string +--- @openMethod -- +-```go +-func (interface) open() error +-``` +- +-open method comment +--- @nestedMap -- +-```go +-field m map[string]float64 +-``` +- +-nested map +--- b/e.go -- +-package b - -import ( - "fmt" -- "strings" -- "testing" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" -- "golang.org/x/tools/gopls/internal/lsp/tests/compare" -- "golang.org/x/tools/internal/testenv" +- "godef.test/a" -) - --func TestPrepareRenameMainPackage(t *testing.T) { -- const files = ` +-func useThings() { +- t := a.Thing{} //@loc(bStructType, "ing") +- fmt.Print(t.Member) //@loc(bMember, "ember") +- fmt.Print(a.Other) //@loc(bVar, "ther") +- a.Things(nil) //@loc(bFunc, "ings") +-} +- +-/*@ +-def(bStructType, Thing) +-def(bMember, Member) +-def(bVar, Other) +-def(bFunc, Things) +-*/ +- +-func _() { +- var x interface{} +- switch x := x.(type) { //@hover("x", "x", xInterface) +- case string: //@loc(eString, "string") +- fmt.Println(x) //@hover("x", "x", xString) +- case int: //@loc(eInt, "int") +- fmt.Println(x) //@hover("x", "x", xInt) +- } +-} +--- @xInt -- +-```go +-var x int +-``` +--- @xInterface -- +-```go +-var x interface{} +-``` +--- @xString -- +-```go +-var x string +-``` +--- broken/unclosedIf.go -- +-package broken +- +-import "fmt" +- +-func unclosedIf() { +- if false { +- var myUnclosedIf string //@loc(myUnclosedIf, "myUnclosedIf") +- fmt.Printf("s = %v\n", myUnclosedIf) //@def("my", myUnclosedIf) +-} +- +-func _() {} //@diag("_", re"expected") +diff -urN a/gopls/internal/test/marker/testdata/hover/goprivate.txt b/gopls/internal/test/marker/testdata/hover/goprivate.txt +--- a/gopls/internal/test/marker/testdata/hover/goprivate.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/hover/goprivate.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,28 +0,0 @@ +-This test checks that links in hover obey GOPRIVATE. +- +--- env -- +-GOPRIVATE=mod.com --- go.mod -- -module mod.com +--- p.go -- +-package p - --go 1.18 ---- main.go -- --package main +-// T should not be linked, as it is private. +-type T struct{} //@hover("T", "T", T) +--- lib/lib.go -- +-package lib - --import ( -- "fmt" --) +-// GOPRIVATE should also match nested packages. +-type L struct{} //@hover("L", "L", L) +--- @L -- +-```go +-type L struct{} // size=0 +-``` - --func main() { -- fmt.Println(1) --} --` -- const wantErr = "can't rename package \"main\"" -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- loc := env.RegexpSearch("main.go", `main`) -- params := &protocol.PrepareRenameParams{ -- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), -- } -- _, err := env.Editor.Server.PrepareRename(env.Ctx, params) -- if err == nil { -- t.Errorf("missing can't rename package main error from PrepareRename") -- } +-GOPRIVATE should also match nested packages. +--- @T -- +-```go +-type T struct{} // size=0 +-``` - -- if err.Error() != wantErr { -- t.Errorf("got %v, want %v", err.Error(), wantErr) -- } -- }) +-T should not be linked, as it is private. +diff -urN a/gopls/internal/test/marker/testdata/hover/hover.txt b/gopls/internal/test/marker/testdata/hover/hover.txt +--- a/gopls/internal/test/marker/testdata/hover/hover.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/hover/hover.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,30 +0,0 @@ +-This test demonstrates some features of the new marker test runner. +- +--- a.go -- +-package a +- +-const abc = 0x2a //@hover("b", "abc", abc),hover(" =", "abc", abc) +--- typeswitch.go -- +-package a +- +-func _() { +- var y interface{} +- switch x := y.(type) { //@hover("x", "x", x) +- case int: +- println(x) //@hover("x", "x", xint),hover(")", "x", xint) +- } -} +--- @abc -- +-```go +-const abc untyped int = 0x2a // 42 +-``` +- +-@hover("b", "abc", abc),hover(" =", "abc", abc) +--- @x -- +-```go +-var x interface{} +-``` +--- @xint -- +-```go +-var x int +-``` +diff -urN a/gopls/internal/test/marker/testdata/hover/issues.txt b/gopls/internal/test/marker/testdata/hover/issues.txt +--- a/gopls/internal/test/marker/testdata/hover/issues.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/hover/issues.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,22 +0,0 @@ +-This test verifies fixes for various issues reported for hover. - --// Test case for golang/go#56227 --func TestRenameWithUnsafeSlice(t *testing.T) { -- testenv.NeedsGo1Point(t, 17) // unsafe.Slice was added in Go 1.17 -- const files = ` --- go.mod -- --module mod.com +-module golang.org/lsptests - --go 1.18 ---- p.go -- --package p +--- issue64239/p.go -- +-package issue64239 +- +-// golang/go#64239: hover fails for objects in the unsafe package. - -import "unsafe" - --type T struct{} +-var _ = unsafe.Sizeof(struct{}{}) //@hover("Sizeof", "Sizeof", "`Sizeof` on pkg.go.dev") - --func (T) M() {} +--- issue64237/p.go -- +-package issue64237 - --func _() { -- x := [3]int{1, 2, 3} -- ptr := unsafe.Pointer(&x) -- _ = unsafe.Slice((*int)(ptr), 3) --} --` +-// golang/go#64237: hover panics for broken imports. - -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("p.go") -- env.Rename(env.RegexpSearch("p.go", "M"), "N") // must not panic -- }) --} +-import "golang.org/lsptests/nonexistant" //@diag("\"golang", re"could not import") - --func TestPrepareRenameWithNoPackageDeclaration(t *testing.T) { -- const files = ` --go 1.14 ---- lib/a.go -- --import "fmt" +-var _ = nonexistant.Value //@hovererr("nonexistant", "no package data") +diff -urN a/gopls/internal/test/marker/testdata/hover/linkable_generics.txt b/gopls/internal/test/marker/testdata/hover/linkable_generics.txt +--- a/gopls/internal/test/marker/testdata/hover/linkable_generics.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/hover/linkable_generics.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,148 +0,0 @@ +-This file contains tests for documentation links to generic code in hover. - --const A = 1 +--- go.mod -- +-module mod.com - --func bar() { -- fmt.Println("Bar") --} +-go 1.19 - ---- main.go -- --package main +--- a.go -- +-package a - --import "fmt" +-import "mod.com/generic" - --func main() { -- fmt.Println("Hello") --} --` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("lib/a.go") -- err := env.Editor.Rename(env.Ctx, env.RegexpSearch("lib/a.go", "fmt"), "fmt1") -- if got, want := fmt.Sprint(err), "no identifier found"; got != want { -- t.Errorf("Rename: got error %v, want %v", got, want) -- } -- }) --} +-func _() { +- // Hovering over instantiated object should produce accurate type +- // information, but link to the generic declarations. - --func TestPrepareRenameFailWithUnknownModule(t *testing.T) { -- testenv.NeedsGo1Point(t, 17) -- const files = ` --go 1.14 ---- lib/a.go -- --package lib +- var x generic.GT[int] //@hover("GT", "GT", xGT) +- _ = x.F //@hover("x", "x", x),hover("F", "F", xF) - --const A = 1 +- f := generic.GF[int] //@hover("GF", "GF", fGF) +- _ = f //@hover("f", "f", f) +-} - ---- main.go -- --package main +--- generic/generic.go -- +-package generic - --import ( -- "mod.com/lib" --) +-// Hovering over type parameters should link to documentation. +-// +-// TODO(rfindley): should it? We should probably link to the type. +-type GT[P any] struct{ //@hover("GT", "GT", GT),hover("P", "P", GTP) +- F P //@hover("F", "F", F),hover("P", "P", FP) +-} - --func main() { -- println("Hello") +-func (GT[P]) M(p P) { //@hover("GT", "GT", GTrecv),hover("M","M", M),hover(re"p (P)", re"p (P)", pP) -} --` -- const wantErr = "can't rename package: missing module information for package" -- Run(t, files, func(t *testing.T, env *Env) { -- loc := env.RegexpSearch("lib/a.go", "lib") -- params := &protocol.PrepareRenameParams{ -- TextDocumentPositionParams: protocol.LocationTextDocumentPositionParams(loc), -- } -- _, err := env.Editor.Server.PrepareRename(env.Ctx, params) -- if err == nil || !strings.Contains(err.Error(), wantErr) { -- t.Errorf("missing cannot rename packages with unknown module from PrepareRename") -- } -- }) +- +-func GF[P any] (p P) { //@hover("GF", "GF", GF) -} - --// This test ensures that each import of a renamed package --// is also renamed if it would otherwise create a conflict. --func TestRenamePackageWithConflicts(t *testing.T) { -- testenv.NeedsGo1Point(t, 17) -- const files = ` ---- go.mod -- --module mod.com +--- @F -- +-```go +-field F P +-``` - --go 1.18 ---- lib/a.go -- --package lib +-@hover("F", "F", F),hover("P", "P", FP) - --const A = 1 - ---- lib/nested/a.go -- --package nested +-[`(generic.GT).F` on pkg.go.dev](https://pkg.go.dev/mod.com/generic#GT.F) +--- @FP -- +-```go +-type parameter P any +-``` +--- @GF -- +-```go +-func GF[P any](p P) +-``` - --const B = 1 +-[`generic.GF` on pkg.go.dev](https://pkg.go.dev/mod.com/generic#GF) +--- @GT -- +-```go +-type GT[P any] struct { +- F P //@hover("F", "F", F),hover("P", "P", FP) +-} +-``` - ---- lib/x/a.go -- --package nested1 +-Hovering over type parameters should link to documentation. - --const C = 1 +-TODO(rfindley): should it? We should probably link to the type. - ---- main.go -- --package main - --import ( -- "mod.com/lib" -- "mod.com/lib/nested" -- nested1 "mod.com/lib/x" --) +-```go +-func (GT[P]) M(p P) +-``` - --func main() { -- println("Hello") +-[`generic.GT` on pkg.go.dev](https://pkg.go.dev/mod.com/generic#GT) +--- @GTP -- +-```go +-type parameter P any +-``` +--- @GTrecv -- +-```go +-type GT[P any] struct { +- F P //@hover("F", "F", F),hover("P", "P", FP) -} --` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("lib/a.go") -- env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested") +-``` - -- // Check if the new package name exists. -- env.RegexpSearch("nested/a.go", "package nested") -- env.RegexpSearch("main.go", `nested2 "mod.com/nested"`) -- env.RegexpSearch("main.go", "mod.com/nested/nested") -- env.RegexpSearch("main.go", `nested1 "mod.com/nested/x"`) -- }) --} +-Hovering over type parameters should link to documentation. - --func TestRenamePackageWithAlias(t *testing.T) { -- testenv.NeedsGo1Point(t, 17) -- const files = ` ---- go.mod -- --module mod.com +-TODO(rfindley): should it? We should probably link to the type. - --go 1.18 ---- lib/a.go -- --package lib - --const A = 1 +-```go +-func (GT[P]) M(p P) +-``` - ---- lib/nested/a.go -- --package nested +-[`generic.GT` on pkg.go.dev](https://pkg.go.dev/mod.com/generic#GT) +--- @M -- +-```go +-func (GT[P]) M(p P) +-``` - --const B = 1 +-[`(generic.GT).M` on pkg.go.dev](https://pkg.go.dev/mod.com/generic#GT.M) +--- @f -- +-```go +-var f func(p int) +-``` +--- @fGF -- +-```go +-func generic.GF(p int) // func[P any](p P) +-``` - ---- main.go -- --package main +-[`generic.GF` on pkg.go.dev](https://pkg.go.dev/mod.com/generic#GF) +--- @pP -- +-```go +-type parameter P any +-``` +--- @x -- +-```go +-var x generic.GT[int] +-``` - --import ( -- "mod.com/lib" -- lib1 "mod.com/lib/nested" --) +-@hover("GT", "GT", xGT) +--- @xF -- +-```go +-field F int +-``` - --func main() { -- println("Hello") --} --` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("lib/a.go") -- env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested") +-@hover("F", "F", F),hover("P", "P", FP) - -- // Check if the new package name exists. -- env.RegexpSearch("nested/a.go", "package nested") -- env.RegexpSearch("main.go", "mod.com/nested") -- env.RegexpSearch("main.go", `lib1 "mod.com/nested/nested"`) -- }) +- +-[`(generic.GT).F` on pkg.go.dev](https://pkg.go.dev/mod.com/generic#GT.F) +--- @xGT -- +-```go +-type GT[P any] struct { +- F P //@hover("F", "F", F),hover("P", "P", FP) -} +-``` - --func TestRenamePackageWithDifferentDirectoryPath(t *testing.T) { -- testenv.NeedsGo1Point(t, 17) -- const files = ` ---- go.mod -- --module mod.com +-Hovering over type parameters should link to documentation. - --go 1.18 ---- lib/a.go -- --package lib +-TODO(rfindley): should it? We should probably link to the type. - --const A = 1 - ---- lib/nested/a.go -- --package foo +-```go +-func (generic.GT[P]) M(p P) +-``` - --const B = 1 +-[`generic.GT` on pkg.go.dev](https://pkg.go.dev/mod.com/generic#GT) +diff -urN a/gopls/internal/test/marker/testdata/hover/linkable.txt b/gopls/internal/test/marker/testdata/hover/linkable.txt +--- a/gopls/internal/test/marker/testdata/hover/linkable.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/hover/linkable.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,134 +0,0 @@ +-This test checks that we correctly determine pkgsite links for various +-identifiers. - ---- main.go -- --package main +-We should only produce links that work, meaning the object is reachable via the +-package's public API. - --import ( -- "mod.com/lib" -- foo "mod.com/lib/nested" --) +--- go.mod -- +-module mod.com - --func main() { -- println("Hello") --} --` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("lib/a.go") -- env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested") +-go 1.18 +--- p.go -- +-package p - -- // Check if the new package name exists. -- env.RegexpSearch("nested/a.go", "package nested") -- env.RegexpSearch("main.go", "mod.com/nested") -- env.RegexpSearch("main.go", `foo "mod.com/nested/nested"`) -- }) +-type E struct { +- Embed int64 -} - --func TestRenamePackage(t *testing.T) { -- testenv.NeedsGo1Point(t, 17) -- const files = ` ---- go.mod -- --module mod.com +-// T is in the package scope, and so should be linkable. +-type T struct{ //@hover("T", "T", T) +- // Only exported fields should be linkable - --go 1.18 ---- lib/a.go -- --package lib +- f int64 //@hover("f", "f", f) +- F int64 //@hover("F", "F", F) - --const A = 1 +- E - ---- lib/b.go -- --package lib +- // TODO(rfindley): is the link here correct? It ignores N. +- N struct { +- // Nested fields should also be linkable. +- Nested int64 //@hover("Nested", "Nested", Nested) +- } +-} +-// M is an exported method, and so should be linkable. +-func (T) M() {} - --const B = 1 +-// m is not exported, and so should not be linkable. +-func (T) m() {} - ---- lib/nested/a.go -- --package nested +-func _() { +- var t T - --const C = 1 +- // Embedded fields should be linkable. +- _ = t.Embed //@hover("Embed", "Embed", Embed) - ---- main.go -- --package main +- // Local variables should not be linkable, even if they are capitalized. +- var X int64 //@hover("X", "X", X) +- _ = X - --import ( -- "mod.com/lib" -- "mod.com/lib/nested" --) +- // Local types should not be linkable, even if they are capitalized. +- type Local struct { //@hover("Local", "Local", Local) +- E +- } - --func main() { -- println("Hello") +- // But the embedded field should still be linkable. +- var l Local +- _ = l.Embed //@hover("Embed", "Embed", Embed) -} --` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("lib/a.go") -- env.Rename(env.RegexpSearch("lib/a.go", "lib"), "lib1") +--- @Embed -- +-```go +-field Embed int64 +-``` - -- // Check if the new package name exists. -- env.RegexpSearch("lib1/a.go", "package lib1") -- env.RegexpSearch("lib1/b.go", "package lib1") -- env.RegexpSearch("main.go", "mod.com/lib1") -- env.RegexpSearch("main.go", "mod.com/lib1/nested") -- }) --} +-[`(p.E).Embed` on pkg.go.dev](https://pkg.go.dev/mod.com#E.Embed) +--- @F -- +-```go +-field F int64 // size=8, offset=8 +-``` - --// Test for golang/go#47564. --func TestRenameInTestVariant(t *testing.T) { -- const files = ` ---- go.mod -- --module mod.com +-@hover("F", "F", F) - --go 1.12 ---- stringutil/stringutil.go -- --package stringutil - --func Identity(s string) string { -- return s +-[`(p.T).F` on pkg.go.dev](https://pkg.go.dev/mod.com#T.F) +--- @Local -- +-```go +-type Local struct { // size=8 +- E -} ---- stringutil/stringutil_test.go -- --package stringutil +-``` - --func TestIdentity(t *testing.T) { -- if got := Identity("foo"); got != "foo" { -- t.Errorf("bad") -- } --} ---- main.go -- --package main +-Local types should not be linkable, even if they are capitalized. - --import ( -- "fmt" - -- "mod.com/stringutil" --) +-```go +-// Embedded fields: +-Embed int64 // through E +-``` +--- @Nested -- +-```go +-field Nested int64 // size=8, offset=0 +-``` - --func main() { -- fmt.Println(stringutil.Identity("hello world")) --} --` +-Nested fields should also be linkable. +--- @T -- +-```go +-type T struct { // size=32 (0x20) +- f int64 //@hover("f", "f", f) +- F int64 //@hover("F", "F", F) - -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- env.Rename(env.RegexpSearch("main.go", `stringutil\.(Identity)`), "Identityx") -- env.OpenFile("stringutil/stringutil_test.go") -- text := env.BufferText("stringutil/stringutil_test.go") -- if !strings.Contains(text, "Identityx") { -- t.Errorf("stringutil/stringutil_test.go: missing expected token `Identityx` after rename:\n%s", text) -- } -- }) +- E +- +- // TODO(rfindley): is the link here correct? It ignores N. +- N struct { +- // Nested fields should also be linkable. +- Nested int64 //@hover("Nested", "Nested", Nested) +- } -} +-``` - --// This is a test that rename operation initiated by the editor function as expected. --func TestRenameFileFromEditor(t *testing.T) { -- const files = ` ---- go.mod -- --module mod.com +-T is in the package scope, and so should be linkable. - --go 1.16 ---- a/a.go -- --package a - --const X = 1 ---- a/x.go -- --package a +-```go +-// Embedded fields: +-Embed int64 // through E +-``` - --const X = 2 ---- b/b.go -- --package b --` +-```go +-func (T) M() +-func (T) m() +-``` - -- Run(t, files, func(t *testing.T, env *Env) { -- // Rename files and verify that diagnostics are affected accordingly. +-[`p.T` on pkg.go.dev](https://pkg.go.dev/mod.com#T) +--- @X -- +-```go +-var X int64 +-``` - -- // Initially, we should have diagnostics on both X's, for their duplicate declaration. -- env.OnceMet( -- InitialWorkspaceLoad, -- Diagnostics(env.AtRegexp("a/a.go", "X")), -- Diagnostics(env.AtRegexp("a/x.go", "X")), -- ) +-Local variables should not be linkable, even if they are capitalized. +--- @f -- +-```go +-field f int64 // size=8, offset=0 +-``` - -- // Moving x.go should make the diagnostic go away. -- env.RenameFile("a/x.go", "b/x.go") -- env.AfterChange( -- NoDiagnostics(ForFile("a/a.go")), // no more duplicate declarations -- Diagnostics(env.AtRegexp("b/b.go", "package")), // as package names mismatch -- ) +-@hover("f", "f", f) +diff -urN a/gopls/internal/test/marker/testdata/hover/linkname.txt b/gopls/internal/test/marker/testdata/hover/linkname.txt +--- a/gopls/internal/test/marker/testdata/hover/linkname.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/hover/linkname.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,30 +0,0 @@ +-This test check hover on the 2nd argument in go:linkname directives. - -- // Renaming should also work on open buffers. -- env.OpenFile("b/x.go") +--- go.mod -- +-module mod.com - -- // Moving x.go back to a/ should cause the diagnostics to reappear. -- env.RenameFile("b/x.go", "a/x.go") -- env.AfterChange( -- Diagnostics(env.AtRegexp("a/a.go", "X")), -- Diagnostics(env.AtRegexp("a/x.go", "X")), -- ) +--- upper/upper.go -- +-package upper - -- // Renaming the entire directory should move both the open and closed file. -- env.RenameFile("a", "x") -- env.AfterChange( -- Diagnostics(env.AtRegexp("x/a.go", "X")), -- Diagnostics(env.AtRegexp("x/x.go", "X")), -- ) +-import ( +- _ "unsafe" +- _ "mod.com/lower" +-) - -- // As a sanity check, verify that x/x.go is open. -- if text := env.BufferText("x/x.go"); text == "" { -- t.Fatal("got empty buffer for x/x.go") -- } -- }) +-//go:linkname foo mod.com/lower.bar //@hover("mod.com/lower.bar", "mod.com/lower.bar", bar) +-func foo() string +- +--- lower/lower.go -- +-package lower +- +-// bar does foo. +-func bar() string { +- return "foo by bar" -} - --func TestRenamePackage_Tests(t *testing.T) { -- testenv.NeedsGo1Point(t, 17) -- const files = ` ---- go.mod -- --module mod.com +--- @bar -- +-```go +-func bar() string +-``` - --go 1.18 ---- lib/a.go -- --package lib +-bar does foo. +diff -urN a/gopls/internal/test/marker/testdata/hover/methods.txt b/gopls/internal/test/marker/testdata/hover/methods.txt +--- a/gopls/internal/test/marker/testdata/hover/methods.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/hover/methods.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,71 +0,0 @@ +-This test checks the formatting of the list of accessible methods. - --const A = 1 +-Observe that: +-- interface methods that appear in the syntax are not repeated +- in the method set of the type; +-- promoted methods of structs are shown; +-- receiver variables are correctly named; +-- receiver variables have a pointer type if appropriate; +-- only accessible methods are shown. - ---- lib/b.go -- --package lib +--- go.mod -- +-module example.com - --const B = 1 +--- lib/lib.go -- +-package lib - ---- lib/a_test.go -- --package lib_test +-type I interface { +- A() +- b() +- J +-} - --import ( -- "mod.com/lib" -- "fmt --) +-type J interface { C() } - --const C = 1 +-type S struct { I } +-func (s S) A() {} +-func (s S) b() {} +-func (s *S) PA() {} +-func (s *S) pb() {} - ---- lib/b_test.go -- --package lib +--- a/a.go -- +-package a - --import ( -- "fmt --) +-import "example.com/lib" - --const D = 1 +-var _ lib.I //@hover("I", "I", I) +-var _ lib.J //@hover("J", "J", J) +-var _ lib.S //@hover("S", "S", S) - ---- lib/nested/a.go -- --package nested +--- @I -- +-```go +-type I interface { +- A() +- b() +- J +-} +-``` - --const D = 1 +-```go +-func (lib.J) C() +-``` - ---- main.go -- --package main +-[`lib.I` on pkg.go.dev](https://pkg.go.dev/example.com/lib#I) +--- @J -- +-```go +-type J interface{ C() } +-``` - --import ( -- "mod.com/lib" -- "mod.com/lib/nested" --) +-[`lib.J` on pkg.go.dev](https://pkg.go.dev/example.com/lib#J) +--- @S -- +-```go +-type S struct{ I } +-``` - --func main() { -- println("Hello") --} --` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("lib/a.go") -- env.Rename(env.RegexpSearch("lib/a.go", "lib"), "lib1") +-```go +-func (s lib.S) A() +-func (lib.J) C() +-func (s *lib.S) PA() +-``` - -- // Check if the new package name exists. -- env.RegexpSearch("lib1/a.go", "package lib1") -- env.RegexpSearch("lib1/b.go", "package lib1") -- env.RegexpSearch("main.go", "mod.com/lib1") -- env.RegexpSearch("main.go", "mod.com/lib1/nested") +-[`lib.S` on pkg.go.dev](https://pkg.go.dev/example.com/lib#S) +diff -urN a/gopls/internal/test/marker/testdata/hover/sizeoffset.txt b/gopls/internal/test/marker/testdata/hover/sizeoffset.txt +--- a/gopls/internal/test/marker/testdata/hover/sizeoffset.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/hover/sizeoffset.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,117 +0,0 @@ +-This test checks that hover reports the sizes of vars/types, +-and the offsets of struct fields. - -- // Check if the test package is renamed -- env.RegexpSearch("lib1/a_test.go", "package lib1_test") -- env.RegexpSearch("lib1/b_test.go", "package lib1") -- }) --} +-Notes: +-- this only works on the declaring identifier, not on refs. +-- the size of a type is undefined if it depends on type parameters. +-- the offset of a field is undefined if it or any preceding field +- has undefined size/alignment. +-- the test's size expectations assumes a 64-bit machine. +-- requires go1.22 because size information was inaccurate before. - --func TestRenamePackage_NestedModule(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) -- const files = ` ---- go.work -- --go 1.18 --use ( -- . -- ./foo/bar -- ./foo/baz --) +--- flags -- +--skip_goarch=386,arm +--min_go=go1.22 - --- go.mod -- --module mod.com +-module example.com - -go 1.18 +--- a.go -- +-package a - --require ( -- mod.com/foo/bar v0.0.0 --) +-type T struct { //@ hover("T", "T", T) +- a int //@ hover("a", "a", a) +- U U //@ hover("U", "U", U) +- y, z int //@ hover("y", "y", y), hover("z", "z", z) +-} - --replace ( -- mod.com/foo/bar => ./foo/bar -- mod.com/foo/baz => ./foo/baz --) ---- foo/foo.go -- --package foo +-type U struct { +- slice []string +-} - --import "fmt" +-type G[T any] struct { +- p T //@ hover("p", "p", p) +- q int //@ hover("q", "q", q) +-} - --func Bar() { -- fmt.Println("In foo before renamed to foox.") +-var _ struct { +- Gint G[int] //@ hover("Gint", "Gint", Gint) +- Gstring G[string] //@ hover("Gstring", "Gstring", Gstring) -} - ---- foo/bar/go.mod -- --module mod.com/foo/bar +-type wasteful struct { //@ hover("wasteful", "wasteful", wasteful) +- a bool +- b [2]string +- c bool +-} - ---- foo/bar/bar.go -- --package bar +--- @T -- +-```go +-type T struct { // size=48 (0x30) +- a int //@ hover("a", "a", a) +- U U //@ hover("U", "U", U) +- y, z int //@ hover("y", "y", y), hover("z", "z", z) +-} +-``` - --const Msg = "Hi from package bar" +-[`a.T` on pkg.go.dev](https://pkg.go.dev/example.com#T) +--- @wasteful -- +-```go +-type wasteful struct { // size=48 (0x30) (29% wasted) +- a bool +- b [2]string +- c bool +-} +-``` +--- @a -- +-```go +-field a int // size=8, offset=0 +-``` - ---- foo/baz/go.mod -- --module mod.com/foo/baz +-@ hover("a", "a", a) +--- @U -- +-```go +-field U U // size=24 (0x18), offset=8 +-``` - ---- foo/baz/baz.go -- --package baz +-@ hover("U", "U", U) - --const Msg = "Hi from package baz" - ---- main.go -- --package main +-[`(a.T).U` on pkg.go.dev](https://pkg.go.dev/example.com#T.U) +--- @y -- +-```go +-field y int // size=8, offset=32 (0x20) +-``` - --import ( -- "fmt" -- "mod.com/foo/bar" -- "mod.com/foo/baz" -- "mod.com/foo" --) +-@ hover("y", "y", y), hover("z", "z", z) +--- @z -- +-```go +-field z int // size=8, offset=40 (0x28) +-``` - --func main() { -- foo.Bar() -- fmt.Println(bar.Msg) -- fmt.Println(baz.Msg) --} --` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("foo/foo.go") -- env.Rename(env.RegexpSearch("foo/foo.go", "foo"), "foox") +-@ hover("y", "y", y), hover("z", "z", z) +--- @p -- +-```go +-field p T +-``` - -- env.RegexpSearch("foox/foo.go", "package foox") -- env.OpenFile("foox/bar/bar.go") -- env.OpenFile("foox/bar/go.mod") +-@ hover("p", "p", p) +--- @q -- +-```go +-field q int // size=8 +-``` - -- env.RegexpSearch("main.go", "mod.com/foo/bar") -- env.RegexpSearch("main.go", "mod.com/foox") -- env.RegexpSearch("main.go", "foox.Bar()") +-@ hover("q", "q", q) +--- @Gint -- +-```go +-field Gint G[int] // size=16 (0x10), offset=0 +-``` - -- env.RegexpSearch("go.mod", "./foox/bar") -- env.RegexpSearch("go.mod", "./foox/baz") -- }) +-@ hover("Gint", "Gint", Gint) +--- @Gstring -- +-```go +-field Gstring G[string] // size=24 (0x18), offset=16 (0x10) +-``` +- +-@ hover("Gstring", "Gstring", Gstring) +diff -urN a/gopls/internal/test/marker/testdata/hover/std.txt b/gopls/internal/test/marker/testdata/hover/std.txt +--- a/gopls/internal/test/marker/testdata/hover/std.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/hover/std.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,83 +0,0 @@ +-This test checks hover results for built-in or standard library symbols. +- +-It uses synopsis documentation as full documentation for some of these +-built-ins varies across Go versions, where as it just so happens that the +-synopsis does not. +- +-In the future we may need to limit this test to the latest Go version to avoid +-documentation churn. +- +--- settings.json -- +-{ +- "hoverKind": "SynopsisDocumentation" -} - --func TestRenamePackage_DuplicateImport(t *testing.T) { -- testenv.NeedsGo1Point(t, 17) -- const files = ` --- go.mod -- -module mod.com - -go 1.18 ---- lib/a.go -- --package lib -- --const A = 1 -- ---- lib/nested/a.go -- --package nested - --const B = 1 -- ---- main.go -- --package main +--- std.go -- +-package std - -import ( -- "mod.com/lib" -- lib1 "mod.com/lib" -- lib2 "mod.com/lib/nested" +- "fmt" +- "go/types" +- "sync" -) - --func main() { -- println("Hello") --} --` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("lib/a.go") -- env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested") -- -- // Check if the new package name exists. -- env.RegexpSearch("nested/a.go", "package nested") -- env.RegexpSearch("main.go", "mod.com/nested") -- env.RegexpSearch("main.go", `lib1 "mod.com/nested"`) -- env.RegexpSearch("main.go", `lib2 "mod.com/nested/nested"`) -- }) --} +-func _() { +- var err error //@loc(err, "err") +- fmt.Printf("%v", err) //@def("err", err) - --func TestRenamePackage_DuplicateBlankImport(t *testing.T) { -- testenv.NeedsGo1Point(t, 17) -- const files = ` ---- go.mod -- --module mod.com +- var _ string //@hover("string", "string", hoverstring) +- _ = make([]int, 0) //@hover("make", "make", hovermake) - --go 1.18 ---- lib/a.go -- --package lib +- var mu sync.Mutex +- mu.Lock() //@hover("Lock", "Lock", hoverLock) - --const A = 1 +- var typ *types.Named //@hover("types", "types", hoverTypes) +- typ.Obj().Name() //@hover("Name", "Name", hoverName) +-} +--- @hoverLock -- +-```go +-func (m *sync.Mutex) Lock() +-``` - ---- lib/nested/a.go -- --package nested +-Lock locks m. - --const B = 1 - ---- main.go -- --package main +-[`(sync.Mutex).Lock` on pkg.go.dev](https://pkg.go.dev/sync#Mutex.Lock) +--- @hoverName -- +-```go +-func (obj *types.object) Name() string +-``` - --import ( -- "mod.com/lib" -- _ "mod.com/lib" -- lib1 "mod.com/lib/nested" --) +-Name returns the object's (package-local, unqualified) name. - --func main() { -- println("Hello") --} --` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("lib/a.go") -- env.Rename(env.RegexpSearch("lib/a.go", "lib"), "nested") - -- // Check if the new package name exists. -- env.RegexpSearch("nested/a.go", "package nested") -- env.RegexpSearch("main.go", "mod.com/nested") -- env.RegexpSearch("main.go", `_ "mod.com/nested"`) -- env.RegexpSearch("main.go", `lib1 "mod.com/nested/nested"`) -- }) --} +-[`(types.TypeName).Name` on pkg.go.dev](https://pkg.go.dev/go/types#TypeName.Name) +--- @hoverTypes -- +-```go +-package types ("go/types") +-``` - --func TestRenamePackage_TestVariant(t *testing.T) { -- const files = ` ---- go.mod -- --module mod.com +-[`types` on pkg.go.dev](https://pkg.go.dev/go/types) +--- @hovermake -- +-```go +-func make(t Type, size ...int) Type +-``` - --go 1.12 ---- foo/foo.go -- --package foo +-The make built-in function allocates and initializes an object of type slice, map, or chan (only). - --const Foo = 42 ---- bar/bar.go -- --package bar - --import "mod.com/foo" +-[`make` on pkg.go.dev](https://pkg.go.dev/builtin#make) +--- @hoverstring -- +-```go +-type string string +-``` - --const Bar = foo.Foo ---- bar/bar_test.go -- --package bar +-string is the set of all strings of 8-bit bytes, conventionally but not necessarily representing UTF-8-encoded text. - --import "mod.com/foo" - --const Baz = foo.Foo ---- testdata/bar/bar.go -- --package bar +-[`string` on pkg.go.dev](https://pkg.go.dev/builtin#string) +diff -urN a/gopls/internal/test/marker/testdata/implementation/basic.txt b/gopls/internal/test/marker/testdata/implementation/basic.txt +--- a/gopls/internal/test/marker/testdata/implementation/basic.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/implementation/basic.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,73 +0,0 @@ +-Basic test of implementation query. - --import "mod.com/foox" +--- go.mod -- +-module example.com +-go 1.12 - --const Bar = foox.Foo ---- testdata/bar/bar_test.go -- --package bar +--- implementation/implementation.go -- +-package implementation - --import "mod.com/foox" +-import "example.com/other" - --const Baz = foox.Foo --` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("foo/foo.go") -- env.Rename(env.RegexpSearch("foo/foo.go", "package (foo)"), "foox") +-type ImpP struct{} //@loc(ImpP, "ImpP"),implementation("ImpP", Laugher, OtherLaugher) - -- checkTestdata(t, env) -- }) +-func (*ImpP) Laugh() { //@loc(LaughP, "Laugh"),implementation("Laugh", Laugh, OtherLaugh) -} - --func TestRenamePackage_IntermediateTestVariant(t *testing.T) { -- // In this test set up, we have the following import edges: -- // bar_test -> baz -> foo -> bar -- // bar_test -> foo -> bar -- // bar_test -> bar -- // -- // As a consequence, bar_x_test.go is in the reverse closure of both -- // `foo [bar.test]` and `baz [bar.test]`. This test confirms that we don't -- // produce duplicate edits in this case. -- const files = ` ---- go.mod -- --module foo.mod +-type ImpS struct{} //@loc(ImpS, "ImpS"),implementation("ImpS", Laugher, OtherLaugher) - --go 1.12 ---- foo/foo.go -- --package foo +-func (ImpS) Laugh() { //@loc(LaughS, "Laugh"),implementation("Laugh", Laugh, OtherLaugh) +-} - --import "foo.mod/bar" +-type Laugher interface { //@loc(Laugher, "Laugher"),implementation("Laugher", ImpP, OtherImpP, ImpS, OtherImpS, embedsImpP) +- Laugh() //@loc(Laugh, "Laugh"),implementation("Laugh", LaughP, OtherLaughP, LaughS, OtherLaughS) +-} - --const Foo = 42 +-type Foo struct { //@implementation("Foo", Joker) +- other.Foo +-} - --const _ = bar.Bar ---- baz/baz.go -- --package baz +-type Joker interface { //@loc(Joker, "Joker") +- Joke() //@loc(Joke, "Joke"),implementation("Joke", ImpJoker) +-} - --import "foo.mod/foo" +-type cryer int //@implementation("cryer", Cryer) - --const Baz = foo.Foo ---- bar/bar.go -- --package bar +-func (cryer) Cry(other.CryType) {} //@loc(CryImpl, "Cry"),implementation("Cry", Cry) - --var Bar = 123 ---- bar/bar_test.go -- --package bar +-type Empty interface{} //@implementation("Empty") - --const _ = Bar ---- bar/bar_x_test.go -- --package bar_test +-var _ interface{ Joke() } //@implementation("Joke", ImpJoker) - --import ( -- "foo.mod/bar" -- "foo.mod/baz" -- "foo.mod/foo" --) +-type embedsImpP struct { //@loc(embedsImpP, "embedsImpP") +- ImpP //@implementation("ImpP", Laugher, OtherLaugher) +-} - --const _ = bar.Bar + baz.Baz + foo.Foo ---- testdata/foox/foo.go -- --package foox +--- other/other.go -- +-package other - --import "foo.mod/bar" +-type ImpP struct{} //@loc(OtherImpP, "ImpP") - --const Foo = 42 +-func (*ImpP) Laugh() { //@loc(OtherLaughP, "Laugh") +-} - --const _ = bar.Bar ---- testdata/baz/baz.go -- --package baz +-type ImpS struct{} //@loc(OtherImpS, "ImpS") - --import "foo.mod/foox" +-func (ImpS) Laugh() { //@loc(OtherLaughS, "Laugh") +-} - --const Baz = foox.Foo ---- testdata/bar/bar_x_test.go -- --package bar_test +-type ImpI interface { //@loc(OtherLaugher, "ImpI") +- Laugh() //@loc(OtherLaugh, "Laugh") +-} - --import ( -- "foo.mod/bar" -- "foo.mod/baz" -- "foo.mod/foox" --) +-type Foo struct { //@implementation("Foo", Joker) +-} - --const _ = bar.Bar + baz.Baz + foox.Foo --` +-func (Foo) Joke() { //@loc(ImpJoker, "Joke"),implementation("Joke", Joke) +-} - -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("foo/foo.go") -- env.Rename(env.RegexpSearch("foo/foo.go", "package (foo)"), "foox") +-type CryType int - -- checkTestdata(t, env) -- }) +-type Cryer interface { //@loc(Cryer, "Cryer") +- Cry(CryType) //@loc(Cry, "Cry"),implementation("Cry", CryImpl) -} +diff -urN a/gopls/internal/test/marker/testdata/implementation/generics.txt b/gopls/internal/test/marker/testdata/implementation/generics.txt +--- a/gopls/internal/test/marker/testdata/implementation/generics.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/implementation/generics.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,31 +0,0 @@ +-Test of 'implementation' query on generic types. - --func TestRenamePackage_Nesting(t *testing.T) { -- testenv.NeedsGo1Point(t, 17) -- const files = ` --- go.mod -- --module mod.com -- +-module example.com -go 1.18 ---- lib/a.go -- --package lib - --import "mod.com/lib/nested" -- --const A = 1 + nested.B ---- lib/nested/a.go -- --package nested +--- implementation/implementation.go -- +-package implementation - --const B = 1 ---- other/other.go -- --package other +-type GenIface[T any] interface { //@loc(GenIface, "GenIface"),implementation("GenIface", GC) +- F(int, string, T) //@loc(GenIfaceF, "F"),implementation("F", GCF) +-} - --import ( -- "mod.com/lib" -- "mod.com/lib/nested" --) +-type GenConc[U any] int //@loc(GenConc, "GenConc"),implementation("GenConc", GI) - --const C = lib.A + nested.B ---- testdata/libx/a.go -- --package libx +-func (GenConc[V]) F(int, string, V) {} //@loc(GenConcF, "F"),implementation("F", GIF) - --import "mod.com/libx/nested" +-type GenConcString struct{ GenConc[string] } //@loc(GenConcString, "GenConcString"),implementation(GenConcString, GIString) - --const A = 1 + nested.B ---- testdata/other/other.go -- +--- other/other.go -- -package other - --import ( -- "mod.com/libx" -- "mod.com/libx/nested" --) +-type GI[T any] interface { //@loc(GI, "GI"),implementation("GI", GenConc) +- F(int, string, T) //@loc(GIF, "F"),implementation("F", GenConcF) +-} - --const C = libx.A + nested.B --` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("lib/a.go") -- env.Rename(env.RegexpSearch("lib/a.go", "package (lib)"), "libx") +-type GIString GI[string] //@loc(GIString, "GIString"),implementation("GIString", GenConcString) - -- checkTestdata(t, env) -- }) --} +-type GC[U any] int //@loc(GC, "GC"),implementation("GC", GenIface) +- +-func (GC[V]) F(int, string, V) {} //@loc(GCF, "F"),implementation("F", GenIfaceF) +diff -urN a/gopls/internal/test/marker/testdata/implementation/issue43655.txt b/gopls/internal/test/marker/testdata/implementation/issue43655.txt +--- a/gopls/internal/test/marker/testdata/implementation/issue43655.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/implementation/issue43655.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,22 +0,0 @@ +-This test verifies that we fine implementations of the built-in error interface. - --func TestRenamePackage_InvalidName(t *testing.T) { -- testenv.NeedsGo1Point(t, 17) -- const files = ` --- go.mod -- --module mod.com +-module example.com +-go 1.12 - --go 1.18 ---- lib/a.go -- --package lib +--- p.go -- +-package p - --import "mod.com/lib/nested" +-type errA struct{ error } //@loc(errA, "errA") - --const A = 1 + nested.B --` +-type errB struct{} //@loc(errB, "errB") +-func (errB) Error() string{ return "" } //@loc(errBError, "Error") - -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("lib/a.go") -- loc := env.RegexpSearch("lib/a.go", "package (lib)") +-type notAnError struct{} +-func (notAnError) Error() int { return 0 } - -- for _, badName := range []string{"$$$", "lib_test"} { -- if err := env.Editor.Rename(env.Ctx, loc, badName); err == nil { -- t.Errorf("Rename(lib, libx) succeeded, want non-nil error") -- } -- } -- }) +-func _() { +- var _ error //@implementation("error", errA, errB) +- var a errA +- _ = a.Error //@implementation("Error", errBError) -} +diff -urN a/gopls/internal/test/marker/testdata/inlayhints/inlayhints.txt b/gopls/internal/test/marker/testdata/inlayhints/inlayhints.txt +--- a/gopls/internal/test/marker/testdata/inlayhints/inlayhints.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/inlayhints/inlayhints.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,405 +0,0 @@ - --func TestRenamePackage_InternalPackage(t *testing.T) { -- testenv.NeedsGo1Point(t, 17) -- const files = ` ---- go.mod -- --module mod.com +--- flags -- +--ignore_extra_diags - --go 1.18 ---- lib/a.go -- --package lib +--- settings.json -- +-{ +- "hints": { +- "assignVariableTypes": true, +- "compositeLiteralFields": true, +- "compositeLiteralTypes": true, +- "constantValues": true, +- "functionTypeParameters": true, +- "parameterNames": true, +- "rangeVariabletypes": true +- } +-} +- +--- composite_literals.go -- +-package inlayHint //@inlayhints(complit) +- +-import "fmt" +- +-func fieldNames() { +- for _, c := range []struct { +- in, want string +- }{ +- struct{ in, want string }{"Hello, world", "dlrow ,olleH"}, +- {"Hello, 世界", "界世 ,olleH"}, +- {"", ""}, +- } { +- fmt.Println(c.in == c.want) +- } +-} +- +-func fieldNamesPointers() { +- for _, c := range []*struct { +- in, want string +- }{ +- &struct{ in, want string }{"Hello, world", "dlrow ,olleH"}, +- {"Hello, 世界", "界世 ,olleH"}, +- {"", ""}, +- } { +- fmt.Println(c.in == c.want) +- } +-} - --import ( -- "fmt" -- "mod.com/lib/internal/x" --) +--- @complit -- +-package inlayHint //@inlayhints(complit) - --const A = 1 +-import "fmt" - --func print() { -- fmt.Println(x.B) +-func fieldNames() { +- for _, c := range []struct { +- in, want string +- }{ +- struct{ in, want string }{<in: >"Hello, world", <want: >"dlrow ,olleH"}, +- <struct{in string; want string}>{<in: >"Hello, 世界", <want: >"界世 ,olleH"}, +- <struct{in string; want string}>{<in: >"", <want: >""}, +- } { +- fmt.Println(<a...: >c.in == c.want) +- } -} - ---- lib/internal/x/a.go -- --package x +-func fieldNamesPointers() { +- for _, c := range []*struct { +- in, want string +- }{ +- &struct{ in, want string }{<in: >"Hello, world", <want: >"dlrow ,olleH"}, +- <&struct{in string; want string}>{<in: >"Hello, 世界", <want: >"界世 ,olleH"}, +- <&struct{in string; want string}>{<in: >"", <want: >""}, +- } { +- fmt.Println(<a...: >c.in == c.want) +- } +-} - --const B = 1 +--- constant_values.go -- +-package inlayHint //@inlayhints(values) - ---- main.go -- --package main +-const True = true - --import "mod.com/lib" +-type Kind int - --func main() { -- lib.print() --} --` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("lib/internal/x/a.go") -- env.Rename(env.RegexpSearch("lib/internal/x/a.go", "x"), "utils") +-const ( +- KindNone Kind = iota +- KindPrint +- KindPrintf +- KindErrorf +-) - -- // Check if the new package name exists. -- env.RegexpSearch("lib/a.go", "mod.com/lib/internal/utils") -- env.RegexpSearch("lib/a.go", "utils.B") +-const ( +- u = iota * 4 +- v float64 = iota * 42 +- w = iota * 42 +-) - -- // Check if the test package is renamed -- env.RegexpSearch("lib/internal/utils/a.go", "package utils") +-const ( +- a, b = 1, 2 +- c, d +- e, f = 5 * 5, "hello" + "world" +- g, h +- i, j = true, f +-) - -- env.OpenFile("lib/a.go") -- env.Rename(env.RegexpSearch("lib/a.go", "lib"), "lib1") +-// No hint +-const ( +- Int = 3 +- Float = 3.14 +- Bool = true +- Rune = '3' +- Complex = 2.7i +- String = "Hello, world!" +-) - -- // Check if the new package name exists. -- env.RegexpSearch("lib1/a.go", "package lib1") -- env.RegexpSearch("lib1/a.go", "mod.com/lib1/internal/utils") -- env.RegexpSearch("main.go", `import "mod.com/lib1"`) -- env.RegexpSearch("main.go", "lib1.print()") -- }) --} +-var ( +- varInt = 3 +- varFloat = 3.14 +- varBool = true +- varRune = '3' + '4' +- varComplex = 2.7i +- varString = "Hello, world!" +-) - --// checkTestdata checks that current buffer contents match their corresponding --// expected content in the testdata directory. --func checkTestdata(t *testing.T, env *Env) { -- t.Helper() -- files := env.ListFiles("testdata") -- if len(files) == 0 { -- t.Fatal("no files in testdata directory") -- } -- for _, file := range files { -- suffix := strings.TrimPrefix(file, "testdata/") -- got := env.BufferText(suffix) -- want := env.ReadWorkspaceFile(file) -- if diff := compare.Text(want, got); diff != "" { -- t.Errorf("Rename: unexpected buffer content for %s (-want +got):\n%s", suffix, diff) -- } -- } --} -diff -urN a/gopls/internal/regtest/misc/semantictokens_test.go b/gopls/internal/regtest/misc/semantictokens_test.go ---- a/gopls/internal/regtest/misc/semantictokens_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/misc/semantictokens_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,204 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +--- @values -- +-package inlayHint //@inlayhints(values) - --package misc +-const True = true - --import ( -- "testing" +-type Kind int - -- "github.com/google/go-cmp/cmp" -- "golang.org/x/tools/gopls/internal/lsp/fake" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" -- "golang.org/x/tools/internal/typeparams" +-const ( +- KindNone Kind = iota< = 0> +- KindPrint< = 1> +- KindPrintf< = 2> +- KindErrorf< = 3> -) - --func TestBadURICrash_VSCodeIssue1498(t *testing.T) { -- const src = ` ---- go.mod -- --module example.com +-const ( +- u = iota * 4< = 0> +- v float64 = iota * 42< = 42> +- w = iota * 42< = 84> +-) - --go 1.12 +-const ( +- a, b = 1, 2 +- c, d< = 1, 2> +- e, f = 5 * 5, "hello" + "world"< = 25, "helloworld"> +- g, h< = 25, "helloworld"> +- i, j = true, f< = true, "helloworld"> +-) - ---- main.go -- --package main +-// No hint +-const ( +- Int = 3 +- Float = 3.14 +- Bool = true +- Rune = '3' +- Complex = 2.7i +- String = "Hello, world!" +-) - --func main() {} +-var ( +- varInt = 3 +- varFloat = 3.14 +- varBool = true +- varRune = '3' + '4' +- varComplex = 2.7i +- varString = "Hello, world!" +-) - --` -- WithOptions( -- Modes(Default), -- Settings{"allExperiments": true}, -- ).Run(t, src, func(t *testing.T, env *Env) { -- params := &protocol.SemanticTokensParams{} -- const badURI = "http://foo" -- params.TextDocument.URI = badURI -- // This call panicked in the past: golang/vscode-go#1498. -- if _, err := env.Editor.Server.SemanticTokensFull(env.Ctx, params); err != nil { -- // Requests to an invalid URI scheme shouldn't result in an error, we -- // simply don't support this so return empty result. This could be -- // changed, but for now assert on the current behavior. -- t.Errorf("SemanticTokensFull(%q): %v", badURI, err) -- } -- }) --} +--- parameter_names.go -- +-package inlayHint //@inlayhints(parameters) - --// fix bug involving type parameters and regular parameters --// (golang/vscode-go#2527) --func TestSemantic_2527(t *testing.T) { -- // these are the expected types of identifiers in text order -- want := []fake.SemanticToken{ -- {Token: "package", TokenType: "keyword"}, -- {Token: "foo", TokenType: "namespace"}, -- {Token: "// Deprecated (for testing)", TokenType: "comment"}, -- {Token: "func", TokenType: "keyword"}, -- {Token: "Add", TokenType: "function", Mod: "definition deprecated"}, -- {Token: "T", TokenType: "typeParameter", Mod: "definition"}, -- {Token: "int", TokenType: "type", Mod: "defaultLibrary"}, -- {Token: "target", TokenType: "parameter", Mod: "definition"}, -- {Token: "T", TokenType: "typeParameter"}, -- {Token: "l", TokenType: "parameter", Mod: "definition"}, -- {Token: "T", TokenType: "typeParameter"}, -- {Token: "T", TokenType: "typeParameter"}, -- {Token: "return", TokenType: "keyword"}, -- {Token: "append", TokenType: "function", Mod: "defaultLibrary"}, -- {Token: "l", TokenType: "parameter"}, -- {Token: "target", TokenType: "parameter"}, -- {Token: "for", TokenType: "keyword"}, -- {Token: "range", TokenType: "keyword"}, -- {Token: "l", TokenType: "parameter"}, -- {Token: "// test coverage", TokenType: "comment"}, -- {Token: "return", TokenType: "keyword"}, -- {Token: "nil", TokenType: "variable", Mod: "readonly defaultLibrary"}, -- } -- src := ` ---- go.mod -- --module example.com +-import "fmt" - --go 1.19 ---- main.go -- --package foo --// Deprecated (for testing) --func Add[T int](target T, l []T) []T { -- return append(l, target) -- for range l {} // test coverage -- return nil +-func hello(name string) string { +- return "Hello " + name -} --` -- WithOptions( -- Modes(Default), -- Settings{"semanticTokens": true}, -- ).Run(t, src, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- env.AfterChange( -- Diagnostics(env.AtRegexp("main.go", "for range")), -- ) -- seen, err := env.Editor.SemanticTokens(env.Ctx, "main.go") -- if err != nil { -- t.Fatal(err) -- } -- if x := cmp.Diff(want, seen); x != "" { -- t.Errorf("Semantic tokens do not match (-want +got):\n%s", x) -- } -- }) - +-func helloWorld() string { +- return hello("World") -} - --// fix inconsistency in TypeParameters --// https://github.com/golang/go/issues/57619 --func TestSemantic_57619(t *testing.T) { -- if !typeparams.Enabled { -- t.Skip("type parameters are needed for this test") -- } -- src := ` ---- go.mod -- --module example.com +-type foo struct{} - --go 1.19 ---- main.go -- --package foo --type Smap[K int, V any] struct { -- Store map[K]V --} --func (s *Smap[K, V]) Get(k K) (V, bool) { -- v, ok := s.Store[k] -- return v, ok +-func (*foo) bar(baz string, qux int) int { +- if baz != "" { +- return qux + 1 +- } +- return qux -} --func New[K int, V any]() Smap[K, V] { -- return Smap[K, V]{Store: make(map[K]V)} +- +-func kase(foo int, bar bool, baz ...string) { +- fmt.Println(foo, bar, baz) -} --` -- WithOptions( -- Modes(Default), -- Settings{"semanticTokens": true}, -- ).Run(t, src, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- seen, err := env.Editor.SemanticTokens(env.Ctx, "main.go") -- if err != nil { -- t.Fatal(err) -- } -- for i, s := range seen { -- if (s.Token == "K" || s.Token == "V") && s.TokenType != "typeParameter" { -- t.Errorf("%d: expected K and V to be type parameters, but got %v", i, s) -- } -- } -- }) +- +-func kipp(foo string, bar, baz string) { +- fmt.Println(foo, bar, baz) -} - --func TestSemanticGoDirectives(t *testing.T) { -- src := ` ---- go.mod -- --module example.com +-func plex(foo, bar string, baz string) { +- fmt.Println(foo, bar, baz) +-} - --go 1.19 ---- main.go -- --package foo +-func tars(foo string, bar, baz string) { +- fmt.Println(foo, bar, baz) +-} - --//go:linkname now time.Now --func now() +-func foobar() { +- var x foo +- x.bar("", 1) +- kase(0, true, "c", "d", "e") +- kipp("a", "b", "c") +- plex("a", "b", "c") +- tars("a", "b", "c") +- foo, bar, baz := "a", "b", "c" +- kipp(foo, bar, baz) +- plex("a", bar, baz) +- tars(foo+foo, (bar), "c") - --//go:noinline --func foo() {} +-} - --// Mentioning go:noinline should not tokenize. +--- @parameters -- +-package inlayHint //@inlayhints(parameters) - --//go:notadirective --func bar() {} --` -- want := []fake.SemanticToken{ -- {Token: "package", TokenType: "keyword"}, -- {Token: "foo", TokenType: "namespace"}, +-import "fmt" - -- {Token: "//", TokenType: "comment"}, -- {Token: "go:linkname", TokenType: "namespace"}, -- {Token: "now time.Now", TokenType: "comment"}, -- {Token: "func", TokenType: "keyword"}, -- {Token: "now", TokenType: "function", Mod: "definition"}, +-func hello(name string) string { +- return "Hello " + name +-} - -- {Token: "//", TokenType: "comment"}, -- {Token: "go:noinline", TokenType: "namespace"}, -- {Token: "func", TokenType: "keyword"}, -- {Token: "foo", TokenType: "function", Mod: "definition"}, +-func helloWorld() string { +- return hello(<name: >"World") +-} - -- {Token: "// Mentioning go:noinline should not tokenize.", TokenType: "comment"}, +-type foo struct{} - -- {Token: "//go:notadirective", TokenType: "comment"}, -- {Token: "func", TokenType: "keyword"}, -- {Token: "bar", TokenType: "function", Mod: "definition"}, +-func (*foo) bar(baz string, qux int) int { +- if baz != "" { +- return qux + 1 - } -- -- WithOptions( -- Modes(Default), -- Settings{"semanticTokens": true}, -- ).Run(t, src, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- seen, err := env.Editor.SemanticTokens(env.Ctx, "main.go") -- if err != nil { -- t.Fatal(err) -- } -- if x := cmp.Diff(want, seen); x != "" { -- t.Errorf("Semantic tokens do not match (-want +got):\n%s", x) -- } -- }) +- return qux -} -diff -urN a/gopls/internal/regtest/misc/settings_test.go b/gopls/internal/regtest/misc/settings_test.go ---- a/gopls/internal/regtest/misc/settings_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/misc/settings_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,32 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package misc -- --import ( -- "testing" -- -- . "golang.org/x/tools/gopls/internal/lsp/regtest" --) - --func TestEmptyDirectoryFilters_Issue51843(t *testing.T) { -- const src = ` ---- go.mod -- --module mod.com -- --go 1.12 ---- main.go -- --package main -- --func main() { +-func kase(foo int, bar bool, baz ...string) { +- fmt.Println(<a...: >foo, bar, baz) -} --` - -- WithOptions( -- Settings{"directoryFilters": []string{""}}, -- ).Run(t, src, func(t *testing.T, env *Env) { -- // No need to do anything. Issue golang/go#51843 is triggered by the empty -- // directory filter above. -- }) +-func kipp(foo string, bar, baz string) { +- fmt.Println(<a...: >foo, bar, baz) -} -diff -urN a/gopls/internal/regtest/misc/shared_test.go b/gopls/internal/regtest/misc/shared_test.go ---- a/gopls/internal/regtest/misc/shared_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/misc/shared_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,72 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package misc - --import ( -- "testing" +-func plex(foo, bar string, baz string) { +- fmt.Println(<a...: >foo, bar, baz) +-} - -- "golang.org/x/tools/gopls/internal/lsp/fake" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" --) +-func tars(foo string, bar, baz string) { +- fmt.Println(<a...: >foo, bar, baz) +-} - --// Smoke test that simultaneous editing sessions in the same workspace works. --func TestSimultaneousEdits(t *testing.T) { -- const sharedProgram = ` ---- go.mod -- --module mod +-func foobar() { +- var x foo +- x.bar(<baz: >"", <qux: >1) +- kase(<foo: >0, <bar: >true, <baz...: >"c", "d", "e") +- kipp(<foo: >"a", <bar: >"b", <baz: >"c") +- plex(<foo: >"a", <bar: >"b", <baz: >"c") +- tars(<foo: >"a", <bar: >"b", <baz: >"c") +- foo< string>, bar< string>, baz< string> := "a", "b", "c" +- kipp(foo, bar, baz) +- plex(<foo: >"a", bar, baz) +- tars(<foo: >foo+foo, <bar: >(bar), <baz: >"c") - --go 1.12 ---- main.go -- --package main +-} - --import "fmt" +--- type_params.go -- +-package inlayHint //@inlayhints(typeparams) - -func main() { -- fmt.Println("Hello World.") --}` +- ints := map[string]int64{ +- "first": 34, +- "second": 12, +- } - -- WithOptions( -- Modes(DefaultModes()&(Forwarded|SeparateProcess)), -- ).Run(t, sharedProgram, func(t *testing.T, env1 *Env) { -- // Create a second test session connected to the same workspace and server -- // as the first. -- awaiter := NewAwaiter(env1.Sandbox.Workdir) -- const skipApplyEdits = false -- editor, err := fake.NewEditor(env1.Sandbox, env1.Editor.Config()).Connect(env1.Ctx, env1.Server, awaiter.Hooks(), skipApplyEdits) -- if err != nil { -- t.Fatal(err) -- } -- env2 := &Env{ -- T: t, -- Ctx: env1.Ctx, -- Sandbox: env1.Sandbox, -- Server: env1.Server, -- Editor: editor, -- Awaiter: awaiter, -- } -- env2.Await(InitialWorkspaceLoad) -- // In editor #1, break fmt.Println as before. -- env1.OpenFile("main.go") -- env1.RegexpReplace("main.go", "Printl(n)", "") -- // In editor #2 remove the closing brace. -- env2.OpenFile("main.go") -- env2.RegexpReplace("main.go", "\\)\n(})", "") +- floats := map[string]float64{ +- "first": 35.98, +- "second": 26.99, +- } - -- // Now check that we got different diagnostics in each environment. -- env1.AfterChange(Diagnostics(env1.AtRegexp("main.go", "Printl"))) -- env2.AfterChange(Diagnostics(env2.AtRegexp("main.go", "$"))) +- SumIntsOrFloats[string, int64](ints) +- SumIntsOrFloats[string, float64](floats) - -- // Now close editor #2, and verify that operation in editor #1 is -- // unaffected. -- if err := env2.Editor.Close(env2.Ctx); err != nil { -- t.Errorf("closing second editor: %v", err) -- } +- SumIntsOrFloats(ints) +- SumIntsOrFloats(floats) - -- env1.RegexpReplace("main.go", "Printl", "Println") -- env1.AfterChange( -- NoDiagnostics(ForFile("main.go")), -- ) -- }) +- SumNumbers(ints) +- SumNumbers(floats) -} -diff -urN a/gopls/internal/regtest/misc/signature_help_test.go b/gopls/internal/regtest/misc/signature_help_test.go ---- a/gopls/internal/regtest/misc/signature_help_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/misc/signature_help_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,69 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package misc -- --import ( -- "testing" -- -- "github.com/google/go-cmp/cmp" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" --) -- --func TestSignatureHelpInNonWorkspacePackage(t *testing.T) { -- const files = ` ---- a/go.mod -- --module a.com - --go 1.18 ---- a/a/a.go -- --package a +-type Number interface { +- int64 | float64 +-} - --func DoSomething(int) {} +-func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V { +- var s V +- for _, v := range m { +- s += v +- } +- return s +-} - --func _() { -- DoSomething() +-func SumNumbers[K comparable, V Number](m map[K]V) V { +- var s V +- for _, v := range m { +- s += v +- } +- return s -} ---- b/go.mod -- --module b.com --go 1.18 - --require a.com v1.0.0 +--- @typeparams -- +-package inlayHint //@inlayhints(typeparams) - --replace a.com => ../a ---- b/b/b.go -- --package b +-func main() { +- ints< map[string]int64> := map[string]int64{ +- "first": 34, +- "second": 12, +- } - --import "a.com/a" +- floats< map[string]float64> := map[string]float64{ +- "first": 35.98, +- "second": 26.99, +- } - --func _() { -- a.DoSomething() --} --` +- SumIntsOrFloats[string, int64](<m: >ints) +- SumIntsOrFloats[string, float64](<m: >floats) - -- WithOptions( -- WorkspaceFolders("a"), -- ).Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("a/a/a.go") -- env.OpenFile("b/b/b.go") -- signatureHelp := func(filename string) *protocol.SignatureHelp { -- loc := env.RegexpSearch(filename, `DoSomething\(()\)`) -- var params protocol.SignatureHelpParams -- params.TextDocument.URI = loc.URI -- params.Position = loc.Range.Start -- help, err := env.Editor.Server.SignatureHelp(env.Ctx, ¶ms) -- if err != nil { -- t.Fatal(err) -- } -- return help -- } -- ahelp := signatureHelp("a/a/a.go") -- bhelp := signatureHelp("b/b/b.go") +- SumIntsOrFloats<[string, int64]>(<m: >ints) +- SumIntsOrFloats<[string, float64]>(<m: >floats) - -- if diff := cmp.Diff(ahelp, bhelp); diff != "" { -- t.Fatal(diff) -- } -- }) +- SumNumbers<[string, int64]>(<m: >ints) +- SumNumbers<[string, float64]>(<m: >floats) -} -diff -urN a/gopls/internal/regtest/misc/staticcheck_test.go b/gopls/internal/regtest/misc/staticcheck_test.go ---- a/gopls/internal/regtest/misc/staticcheck_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/misc/staticcheck_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,110 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package misc -- --import ( -- "testing" -- -- "golang.org/x/tools/internal/testenv" - -- . "golang.org/x/tools/gopls/internal/lsp/regtest" --) +-type Number interface { +- int64 | float64 +-} - --func TestStaticcheckGenerics(t *testing.T) { -- testenv.NeedsGo1Point(t, 19) // generics were introduced in Go 1.18, staticcheck requires go1.19+ +-func SumIntsOrFloats[K comparable, V int64 | float64](m map[K]V) V { +- var s V +- for _, v := range m { +- s += v +- } +- return s +-} - -- const files = ` ---- go.mod -- --module mod.com +-func SumNumbers[K comparable, V Number](m map[K]V) V { +- var s V +- for _, v := range m { +- s += v +- } +- return s +-} - --go 1.18 ---- a/a.go -- --package a +--- variable_types.go -- +-package inlayHint //@inlayhints(vartypes) - --import ( -- "errors" -- "sort" -- "strings" --) +-func assignTypes() { +- i, j := 0, len([]string{})-1 +- println(i, j) +-} - --func Zero[P any]() P { -- var p P -- return p +-func rangeTypes() { +- for k, v := range []string{} { +- println(k, v) +- } -} - --type Inst[P any] struct { -- Field P +-func funcLitType() { +- myFunc := func(a string) string { return "" } -} - --func testGenerics[P *T, T any](p P) { -- // Calls to instantiated functions should not break checks. -- slice := Zero[string]() -- sort.Slice(slice, func(i, j int) bool { -- return slice[i] < slice[j] -- }) +-func compositeLitType() { +- foo := map[string]interface{}{"": ""} +-} - -- // Usage of instantiated fields should not break checks. -- g := Inst[string]{"hello"} -- g.Field = strings.TrimLeft(g.Field, "12234") +--- @vartypes -- +-package inlayHint //@inlayhints(vartypes) - -- // Use of type parameters should not break checks. -- var q P -- p = q // SA4009: p is overwritten before its first use -- q = &*p // SA4001: &* will be simplified +-func assignTypes() { +- i< int>, j< int> := 0, len([]string{})-1 +- println(i, j) -} - +-func rangeTypes() { +- for k, v := range []string{} { +- println(k, v) +- } +-} - --// FooErr should be called ErrFoo (ST1012) --var FooErr error = errors.New("foo") --` +-func funcLitType() { +- myFunc< func(a string) string> := func(a string) string { return "" } +-} - -- WithOptions( -- Settings{"staticcheck": true}, -- ).Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("a/a.go") -- env.AfterChange( -- Diagnostics(env.AtRegexp("a/a.go", "sort.Slice"), FromSource("sortslice")), -- Diagnostics(env.AtRegexp("a/a.go", "sort.Slice.(slice)"), FromSource("SA1028")), -- Diagnostics(env.AtRegexp("a/a.go", "var (FooErr)"), FromSource("ST1012")), -- Diagnostics(env.AtRegexp("a/a.go", `"12234"`), FromSource("SA1024")), -- Diagnostics(env.AtRegexp("a/a.go", "testGenerics.*(p P)"), FromSource("SA4009")), -- Diagnostics(env.AtRegexp("a/a.go", "q = (&\\*p)"), FromSource("SA4001")), -- ) -- }) +-func compositeLitType() { +- foo< map[string]interface{}> := map[string]interface{}{"": ""} -} - --// Test for golang/go#56270: an analysis with related info should not panic if --// analysis.RelatedInformation.End is not set. --func TestStaticcheckRelatedInfo(t *testing.T) { -- testenv.NeedsGo1Point(t, 19) // staticcheck is only supported at Go 1.19+ -- const files = ` +diff -urN a/gopls/internal/test/marker/testdata/links/links.txt b/gopls/internal/test/marker/testdata/links/links.txt +--- a/gopls/internal/test/marker/testdata/links/links.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/links/links.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,47 +0,0 @@ +-This test verifies behavior of textDocument/documentLink. +- --- go.mod -- --module mod.test +-module golang.org/lsptests - -go 1.18 ---- p.go -- --package p +--- foo/foo.go -- +-package foo +- +-type StructFoo struct {} +- +--- links/links.go -- +-package links //@documentlink(links) - -import ( - "fmt" +- +- "golang.org/lsptests/foo" +- +- _ "database/sql" -) - --func Foo(enabled interface{}) { -- if enabled, ok := enabled.(bool); ok { -- } else { -- _ = fmt.Sprintf("invalid type %T", enabled) // enabled is always bool here -- } --} --` +-var ( +- _ fmt.Formatter +- _ foo.StructFoo +- _ errors.Formatter //@diag("errors", re"(undeclared|undefined)") +-) - -- WithOptions( -- Settings{"staticcheck": true}, -- ).Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("p.go") -- env.AfterChange( -- Diagnostics(env.AtRegexp("p.go", ", (enabled)"), FromSource("SA9008")), -- ) -- }) --} -diff -urN a/gopls/internal/regtest/misc/vendor_test.go b/gopls/internal/regtest/misc/vendor_test.go ---- a/gopls/internal/regtest/misc/vendor_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/misc/vendor_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,103 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-// Foo function +-func Foo() string { +- /*https://example.com/comment */ - --package misc +- url := "https://example.com/string_literal" +- return url - --import ( -- "testing" +- // TODO(golang/go#1234): Link the relevant issue. +- // TODO(microsoft/vscode-go#12): Another issue. +-} - -- . "golang.org/x/tools/gopls/internal/lsp/regtest" +--- @links -- +-links/links.go:4:3-6 https://pkg.go.dev/fmt +-links/links.go:6:3-26 https://pkg.go.dev/golang.org/lsptests/foo +-links/links.go:8:5-17 https://pkg.go.dev/database/sql +-links/links.go:21:10-44 https://example.com/string_literal +-links/links.go:19:4-31 https://example.com/comment +-links/links.go:24:10-24 https://github.com/golang/go/issues/1234 +-links/links.go:25:10-32 https://github.com/microsoft/vscode-go/issues/12 +diff -urN a/gopls/internal/test/marker/testdata/references/crosspackage.txt b/gopls/internal/test/marker/testdata/references/crosspackage.txt +--- a/gopls/internal/test/marker/testdata/references/crosspackage.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/references/crosspackage.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,37 +0,0 @@ +-Test of basic cross-package references. - -- "golang.org/x/tools/gopls/internal/lsp/protocol" --) +--- go.mod -- +-module example.com +-go 1.12 - --const basicProxy = ` ---- golang.org/x/hello@v1.2.3/go.mod -- --module golang.org/x/hello +--- a/a.go -- +-package a - --go 1.14 ---- golang.org/x/hello@v1.2.3/hi/hi.go -- --package hi +-type X struct { +- Y int //@loc(typeXY, "Y") +-} - --var Goodbye error --` +--- b/b.go -- +-package b - --func TestInconsistentVendoring(t *testing.T) { -- const pkgThatUsesVendoring = ` ---- go.mod -- --module mod.com +-import "example.com/a" - --go 1.14 +-func GetXes() []a.X { +- return []a.X{ +- { +- Y: 1, //@loc(GetXesY, "Y"), refs("Y", typeXY, GetXesY, anotherXY) +- }, +- } +-} - --require golang.org/x/hello v1.2.3 ---- go.sum -- --golang.org/x/hello v1.2.3 h1:EcMp5gSkIhaTkPXp8/3+VH+IFqTpk3ZbpOhqk0Ncmho= --golang.org/x/hello v1.2.3/go.mod h1:WW7ER2MRNXWA6c8/4bDIek4Hc/+DofTrMaQQitGXcco= ---- vendor/modules.txt -- ---- a/a1.go -- --package a +--- c/c.go -- +-package c - --import "golang.org/x/hello/hi" +-import "example.com/b" - -func _() { -- _ = hi.Goodbye -- var q int // hardcode a diagnostic --} --` -- WithOptions( -- Modes(Default), -- ProxyFiles(basicProxy), -- ).Run(t, pkgThatUsesVendoring, func(t *testing.T, env *Env) { -- env.OpenFile("a/a1.go") -- d := &protocol.PublishDiagnosticsParams{} -- env.OnceMet( -- InitialWorkspaceLoad, -- Diagnostics(env.AtRegexp("go.mod", "module mod.com"), WithMessage("Inconsistent vendoring")), -- ReadDiagnostics("go.mod", d), -- ) -- env.ApplyQuickFixes("go.mod", d.Diagnostics) -- -- env.AfterChange( -- Diagnostics(env.AtRegexp("a/a1.go", `q int`), WithMessage("not used")), -- ) -- }) +- xes := b.GetXes() +- for _, x := range xes { //@loc(defX, "x") +- _ = x.Y //@loc(useX, "x"), loc(anotherXY, "Y"), refs("Y", typeXY, anotherXY, GetXesY), refs(".", defX, useX), refs("x", defX, useX) +- } -} +diff -urN a/gopls/internal/test/marker/testdata/references/imports.txt b/gopls/internal/test/marker/testdata/references/imports.txt +--- a/gopls/internal/test/marker/testdata/references/imports.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/references/imports.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,17 +0,0 @@ +-Test of references to local package names (imports). - --func TestWindowsVendoring_Issue56291(t *testing.T) { -- const src = ` --- go.mod -- --module mod.com +-module example.com +-go 1.12 - --go 1.14 +--- a/a.go -- +-package a - --require golang.org/x/hello v1.2.3 ---- go.sum -- --golang.org/x/hello v1.2.3 h1:EcMp5gSkIhaTkPXp8/3+VH+IFqTpk3ZbpOhqk0Ncmho= --golang.org/x/hello v1.2.3/go.mod h1:WW7ER2MRNXWA6c8/4bDIek4Hc/+DofTrMaQQitGXcco= ---- main.go -- --package main +-import "os" //@loc(osDef, `"os"`), refs("os", osDef, osUse) - --import "golang.org/x/hello/hi" +-import fmt2 "fmt" //@loc(fmt2Def, `fmt2`), refs("fmt2", fmt2Def, fmt2Use) - --func main() { -- _ = hi.Goodbye --} --` -- WithOptions( -- Modes(Default), -- ProxyFiles(basicProxy), -- ).Run(t, src, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- env.AfterChange(NoDiagnostics()) -- env.RunGoCommand("mod", "tidy") -- env.RunGoCommand("mod", "vendor") -- env.AfterChange(NoDiagnostics()) -- env.RegexpReplace("main.go", `import "golang.org/x/hello/hi"`, "") -- env.AfterChange( -- Diagnostics(env.AtRegexp("main.go", "hi.Goodbye")), -- ) -- env.SaveBuffer("main.go") -- env.AfterChange(NoDiagnostics()) -- }) +-func _() { +- os.Getwd() //@loc(osUse, "os") +- fmt2.Println() //@loc(fmt2Use, "fmt2") -} -diff -urN a/gopls/internal/regtest/misc/vuln_test.go b/gopls/internal/regtest/misc/vuln_test.go ---- a/gopls/internal/regtest/misc/vuln_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/misc/vuln_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,952 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --//go:build go1.18 --// +build go1.18 +diff -urN a/gopls/internal/test/marker/testdata/references/interfaces.txt b/gopls/internal/test/marker/testdata/references/interfaces.txt +--- a/gopls/internal/test/marker/testdata/references/interfaces.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/references/interfaces.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,42 +0,0 @@ +-Test of references applied to concrete and interface types that are +-related by assignability. The result includes references to both. - --package misc +--- go.mod -- +-module example.com +-go 1.12 - --import ( -- "context" -- "encoding/json" -- "sort" -- "strings" -- "testing" +--- a/a.go -- +-package a - -- "github.com/google/go-cmp/cmp" +-type first interface { +- common() //@loc(firCommon, "common"), refs("common", firCommon, xCommon, zCommon) +- firstMethod() //@loc(firMethod, "firstMethod"), refs("firstMethod", firMethod, xfMethod, zfMethod) +-} - -- "golang.org/x/tools/gopls/internal/lsp/command" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" -- "golang.org/x/tools/gopls/internal/lsp/source" -- "golang.org/x/tools/gopls/internal/lsp/tests/compare" -- "golang.org/x/tools/gopls/internal/vulncheck" -- "golang.org/x/tools/gopls/internal/vulncheck/scan" -- "golang.org/x/tools/gopls/internal/vulncheck/vulntest" -- "golang.org/x/tools/internal/testenv" --) +-type second interface { +- common() //@loc(secCommon, "common"), refs("common", secCommon, yCommon, zCommon) +- secondMethod() //@loc(secMethod, "secondMethod"), refs("secondMethod", secMethod, ysMethod, zsMethod) +-} - --func TestRunGovulncheckError(t *testing.T) { -- const files = ` ---- go.mod -- --module mod.com +-type s struct {} - --go 1.12 ---- foo.go -- --package foo --` -- Run(t, files, func(t *testing.T, env *Env) { -- cmd, err := command.NewRunGovulncheckCommand("Run Vulncheck Exp", command.VulncheckArgs{ -- URI: "/invalid/file/url", // invalid arg -- }) -- if err != nil { -- t.Fatal(err) -- } +-func (*s) common() {} //@loc(sCommon, "common"), refs("common", sCommon, xCommon, yCommon, zCommon) - -- params := &protocol.ExecuteCommandParams{ -- Command: command.RunGovulncheck.ID(), -- Arguments: cmd.Arguments, -- } +-func (*s) firstMethod() {} //@loc(sfMethod, "firstMethod"), refs("firstMethod", sfMethod, xfMethod, zfMethod) - -- response, err := env.Editor.ExecuteCommand(env.Ctx, params) -- // We want an error! -- if err == nil { -- t.Errorf("got success, want invalid file URL error: %v", response) -- } -- }) --} +-func (*s) secondMethod() {} //@loc(ssMethod, "secondMethod"), refs("secondMethod", ssMethod, ysMethod, zsMethod) - --func TestRunGovulncheckError2(t *testing.T) { -- const files = ` ---- go.mod -- --module mod.com +-func main() { +- var x first = &s{} +- var y second = &s{} - --go 1.12 ---- foo.go -- --package foo +- x.common() //@loc(xCommon, "common"), refs("common", firCommon, xCommon, zCommon) +- x.firstMethod() //@loc(xfMethod, "firstMethod"), refs("firstMethod", firMethod, xfMethod, zfMethod) +- y.common() //@loc(yCommon, "common"), refs("common", secCommon, yCommon, zCommon) +- y.secondMethod() //@loc(ysMethod, "secondMethod"), refs("secondMethod", secMethod, ysMethod, zsMethod) - --func F() { // build error incomplete --` -- WithOptions( -- EnvVars{ -- "_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`. -- }, -- Settings{ -- "codelenses": map[string]bool{ -- "run_govulncheck": true, -- }, -- }, -- ).Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("go.mod") -- var result command.RunVulncheckResult -- env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result) -- var ws WorkStatus -- env.Await( -- CompletedProgress(result.Token, &ws), -- ) -- wantEndMsg, wantMsgPart := "failed", "There are errors with the provided package patterns:" -- if ws.EndMsg != "failed" || !strings.Contains(ws.Msg, wantMsgPart) { -- t.Errorf("work status = %+v, want {EndMessage: %q, Message: %q}", ws, wantEndMsg, wantMsgPart) -- } -- }) +- var z *s = &s{} +- z.firstMethod() //@loc(zfMethod, "firstMethod"), refs("firstMethod", sfMethod, xfMethod, zfMethod) +- z.secondMethod() //@loc(zsMethod, "secondMethod"), refs("secondMethod", ssMethod, ysMethod, zsMethod) +- z.common() //@loc(zCommon, "common"), refs("common", sCommon, xCommon, yCommon, zCommon) -} +diff -urN a/gopls/internal/test/marker/testdata/references/intrapackage.txt b/gopls/internal/test/marker/testdata/references/intrapackage.txt +--- a/gopls/internal/test/marker/testdata/references/intrapackage.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/references/intrapackage.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,36 +0,0 @@ +-Basic test of references within a single package. - --const vulnsData = ` ---- GO-2022-01.yaml -- --modules: -- - module: golang.org/amod -- versions: -- - introduced: 1.0.0 -- - fixed: 1.0.4 -- packages: -- - package: golang.org/amod/avuln -- symbols: -- - VulnData.Vuln1 -- - VulnData.Vuln2 --description: > -- vuln in amod is found --summary: vuln in amod --references: -- - href: pkg.go.dev/vuln/GO-2022-01 ---- GO-2022-03.yaml -- --modules: -- - module: golang.org/amod -- versions: -- - introduced: 1.0.0 -- - fixed: 1.0.6 -- packages: -- - package: golang.org/amod/avuln -- symbols: -- - nonExisting --description: > -- unaffecting vulnerability is found --summary: unaffecting vulnerability ---- GO-2022-02.yaml -- --modules: -- - module: golang.org/bmod -- packages: -- - package: golang.org/bmod/bvuln -- symbols: -- - Vuln --description: | -- vuln in bmod is found. -- -- This is a long description -- of this vulnerability. --summary: vuln in bmod (no fix) --references: -- - href: pkg.go.dev/vuln/GO-2022-03 ---- GO-2022-04.yaml -- --modules: -- - module: golang.org/bmod -- packages: -- - package: golang.org/bmod/unused -- symbols: -- - Vuln --description: | -- vuln in bmod/somethingelse is found --summary: vuln in bmod/somethingelse --references: -- - href: pkg.go.dev/vuln/GO-2022-04 ---- GOSTDLIB.yaml -- --modules: -- - module: stdlib -- versions: -- - introduced: 1.18.0 -- packages: -- - package: archive/zip -- symbols: -- - OpenReader --summary: vuln in GOSTDLIB --references: -- - href: pkg.go.dev/vuln/GOSTDLIB --` -- --func TestRunGovulncheckStd(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) -- const files = ` --- go.mod -- --module mod.com +-module example.com +-go 1.12 - --go 1.18 ---- main.go -- --package main +--- a/a.go -- +-package a - --import ( -- "archive/zip" -- "fmt" --) +-type i int //@loc(decli, "i"), refs("i", decli, argi, returni, embeddedi) - --func main() { -- _, err := zip.OpenReader("file.zip") // vulnerability id: GOSTDLIB -- fmt.Println(err) +-func _(_ i) []bool { //@loc(argi, "i") +- return nil -} --` - -- db, err := vulntest.NewDatabase(context.Background(), []byte(vulnsData)) -- if err != nil { -- t.Fatal(err) -- } -- defer db.Clean() -- WithOptions( -- EnvVars{ -- // Let the analyzer read vulnerabilities data from the testdata/vulndb. -- "GOVULNDB": db.URI(), -- // When fetchinging stdlib package vulnerability info, -- // behave as if our go version is go1.18 for this testing. -- // The default behavior is to run `go env GOVERSION` (which isn't mutable env var). -- scan.GoVersionForVulnTest: "go1.18", -- "_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`. -- }, -- Settings{ -- "codelenses": map[string]bool{ -- "run_govulncheck": true, -- }, -- }, -- ).Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("go.mod") +-func _(_ []byte) i { //@loc(returni, "i") +- return 0 +-} - -- // Test CodeLens is present. -- lenses := env.CodeLens("go.mod") +-var q string //@loc(declq, "q"), refs("q", declq, assignq, bobq) - -- const wantCommand = "gopls." + string(command.RunGovulncheck) -- var gotCodelens = false -- var lens protocol.CodeLens -- for _, l := range lenses { -- if l.Command.Command == wantCommand { -- gotCodelens = true -- lens = l -- break -- } -- } -- if !gotCodelens { -- t.Fatal("got no vulncheck codelens") -- } -- // Run Command included in the codelens. -- var result command.RunVulncheckResult -- env.ExecuteCommand(&protocol.ExecuteCommandParams{ -- Command: lens.Command.Command, -- Arguments: lens.Command.Arguments, -- }, &result) +-var Q string //@loc(declQ, "Q"), refs("Q", declQ) - -- env.OnceMet( -- CompletedProgress(result.Token, nil), -- ShownMessage("Found GOSTDLIB"), -- NoDiagnostics(ForFile("go.mod")), -- ) -- testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{ -- "go.mod": {IDs: []string{"GOSTDLIB"}, Mode: vulncheck.ModeGovulncheck}}) -- }) +-func _() { +- q = "hello" //@loc(assignq, "q") +- bob := func(_ string) {} +- bob(q) //@loc(bobq, "q") -} - --func TestFetchVulncheckResultStd(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) -- const files = ` +-type e struct { +- i //@loc(embeddedi, "i"), refs("i", embeddedi, embeddediref) +-} +- +-func _() { +- _ = e{}.i //@loc(embeddediref, "i") +-} +diff -urN a/gopls/internal/test/marker/testdata/references/issue58506.txt b/gopls/internal/test/marker/testdata/references/issue58506.txt +--- a/gopls/internal/test/marker/testdata/references/issue58506.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/references/issue58506.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,56 +0,0 @@ +-Regression test for 'references' bug golang/go#58506. +- +-The 'references' query below, applied to method A.F, implicitly uses +-the 'implementation' operation. The correct response includes two +-references to B.F, one from package b and one from package d. +-However, the incremental 'implementation' algorithm had a bug that +-cause it to fail to report the reference from package b. +- +-The reason was that the incremental implementation uses different +-algorithms for the local and global cases (with disjoint results), and +-that when it discovered that type A satisfies interface B and thus +-that B.F must be included among the global search targets, the +-implementation forgot to also search package b for local references +-to B.F. +- --- go.mod -- --module mod.com +-module example.com +-go 1.12 - --go 1.18 ---- main.go -- --package main +--- a/a.go -- +-package a +- +-type A int +- +-func (A) F() {} //@loc(refa, "F"), refs("F", refa, refb, refd) +- +--- b/b.go -- +-package b - -import ( -- "archive/zip" -- "fmt" +- "example.com/a" +- "example.com/c" -) - --func main() { -- _, err := zip.OpenReader("file.zip") // vulnerability id: GOSTDLIB -- fmt.Println(err) --} --` +-type B interface{ F() } - -- db, err := vulntest.NewDatabase(context.Background(), []byte(vulnsData)) -- if err != nil { -- t.Fatal(err) -- } -- defer db.Clean() -- WithOptions( -- EnvVars{ -- // Let the analyzer read vulnerabilities data from the testdata/vulndb. -- "GOVULNDB": db.URI(), -- // When fetchinging stdlib package vulnerability info, -- // behave as if our go version is go1.18 for this testing. -- scan.GoVersionForVulnTest: "go1.18", -- "_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`. -- }, -- Settings{"ui.diagnostic.vulncheck": "Imports"}, -- ).Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("go.mod") -- env.AfterChange( -- NoDiagnostics(ForFile("go.mod")), -- // we don't publish diagnostics for standard library vulnerability yet. -- ) -- testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{ -- "go.mod": { -- IDs: []string{"GOSTDLIB"}, -- Mode: vulncheck.ModeImports, -- }, -- }) -- }) --} +-var _ B = a.A(0) +-var _ B = c.C(0) - --type fetchVulncheckResult struct { -- IDs []string -- Mode vulncheck.AnalysisMode --} +-var _ = B.F //@loc(refb, "F") - --func testFetchVulncheckResult(t *testing.T, env *Env, want map[string]fetchVulncheckResult) { -- t.Helper() +--- c/c.go -- +-package c - -- var result map[protocol.DocumentURI]*vulncheck.Result -- fetchCmd, err := command.NewFetchVulncheckResultCommand("fetch", command.URIArg{ -- URI: env.Sandbox.Workdir.URI("go.mod"), -- }) -- if err != nil { -- t.Fatal(err) -- } -- env.ExecuteCommand(&protocol.ExecuteCommandParams{ -- Command: fetchCmd.Command, -- Arguments: fetchCmd.Arguments, -- }, &result) +-type C int - -- for _, v := range want { -- sort.Strings(v.IDs) -- } -- got := map[string]fetchVulncheckResult{} -- for k, r := range result { -- osv := map[string]bool{} -- for _, v := range r.Findings { -- osv[v.OSV] = true -- } -- ids := make([]string, 0, len(osv)) -- for id := range osv { -- ids = append(ids, id) -- } -- sort.Strings(ids) -- modfile := env.Sandbox.Workdir.RelPath(k.SpanURI().Filename()) -- got[modfile] = fetchVulncheckResult{ -- IDs: ids, -- Mode: r.Mode, -- } -- } -- if diff := cmp.Diff(want, got); diff != "" { -- t.Errorf("fetch vulnchheck result = got %v, want %v: diff %v", got, want, diff) -- } --} +-// Even though C.F is "rename coupled" to A.F by B.F, +-// it should not be among the results. +-func (C) F() {} - --const workspace1 = ` ---- go.mod -- --module golang.org/entry +--- d/d.go -- +-package d - --go 1.18 +-import "example.com/b" - --require golang.org/cmod v1.1.3 +-var _ interface{} = b.B.F //@loc(refd, "F") +diff -urN a/gopls/internal/test/marker/testdata/references/issue59851.txt b/gopls/internal/test/marker/testdata/references/issue59851.txt +--- a/gopls/internal/test/marker/testdata/references/issue59851.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/references/issue59851.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,29 +0,0 @@ +-Regression test for 'references' bug golang/go#59851. - --require ( -- golang.org/amod v1.0.0 // indirect -- golang.org/bmod v0.5.0 // indirect --) ---- go.sum -- --golang.org/amod v1.0.0 h1:EUQOI2m5NhQZijXZf8WimSnnWubaFNrrKUH/PopTN8k= --golang.org/amod v1.0.0/go.mod h1:yvny5/2OtYFomKt8ax+WJGvN6pfN1pqjGnn7DQLUi6E= --golang.org/bmod v0.5.0 h1:KgvUulMyMiYRB7suKA0x+DfWRVdeyPgVJvcishTH+ng= --golang.org/bmod v0.5.0/go.mod h1:f6o+OhF66nz/0BBc/sbCsshyPRKMSxZIlG50B/bsM4c= --golang.org/cmod v1.1.3 h1:PJ7rZFTk7xGAunBRDa0wDe7rZjZ9R/vr1S2QkVVCngQ= --golang.org/cmod v1.1.3/go.mod h1:eCR8dnmvLYQomdeAZRCPgS5JJihXtqOQrpEkNj5feQA= ---- x/x.go -- --package x +--- go.mod -- +-module example.com +-go 1.12 - --import ( -- "golang.org/cmod/c" -- "golang.org/entry/y" --) +--- a/a.go -- +-package a - --func X() { -- c.C1().Vuln1() // vuln use: X -> Vuln1 +-type Iface interface { +- Method() -} - --func CallY() { -- y.Y() // vuln use: CallY -> y.Y -> bvuln.Vuln --} +-type implOne struct{} - ---- y/y.go -- --package y +-func (implOne) Method() {} //@loc(def1, "Method"), refs(def1, def1, ref1, iref, ireftest) - --import "golang.org/cmod/c" +-var _ = implOne.Method //@loc(ref1, "Method") +-var _ = Iface(nil).Method //@loc(iref, "Method") - --func Y() { -- c.C2()() // vuln use: Y -> bvuln.Vuln --} --` +--- a/a_test.go -- +-package a - --// cmod/c imports amod/avuln and bmod/bvuln. --const proxy1 = ` ---- golang.org/cmod@v1.1.3/go.mod -- --module golang.org/cmod +-type implTwo struct{} +- +-func (implTwo) Method() {} //@loc(def2, "Method"), refs(def2, def2, iref, ref2, ireftest) +- +-var _ = implTwo.Method //@loc(ref2, "Method") +-var _ = Iface(nil).Method //@loc(ireftest, "Method") +diff -urN a/gopls/internal/test/marker/testdata/references/issue60369.txt b/gopls/internal/test/marker/testdata/references/issue60369.txt +--- a/gopls/internal/test/marker/testdata/references/issue60369.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/references/issue60369.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,29 +0,0 @@ +-Regression test for 'references' bug golang/go#60369: a references +-query on the embedded type name T in struct{p.T} instead reports all +-references to the package name p. +- +-The bug was fixed in release go1.21 of go/types. - +--- flags -- +--min_go=go1.21 +- +--- go.mod -- +-module example.com -go 1.12 ---- golang.org/cmod@v1.1.3/c/c.go -- --package c - --import ( -- "golang.org/amod/avuln" -- "golang.org/bmod/bvuln" --) +--- a/a.go -- +-package a - --type I interface { -- Vuln1() --} +-type A struct{} +-const C = 0 - --func C1() I { -- v := avuln.VulnData{} -- v.Vuln2() // vuln use -- return v +--- b/b.go -- +-package b +- +-import a "example.com/a" //@loc(adef, "a") +-type s struct { +- a.A //@loc(Aref1, "A"), loc(aref1, "a"), refs(Aref1, Aref1, Aref3), refs(aref1, adef, aref1, aref2, aref3) -} +-var _ a.A //@loc(aref2, re" (a)"), loc(Aref2, "A") +-var _ = s{}.A //@loc(Aref3, "A") +-const c = a.C //@loc(aref3, "a") +diff -urN a/gopls/internal/test/marker/testdata/references/issue60622.txt b/gopls/internal/test/marker/testdata/references/issue60622.txt +--- a/gopls/internal/test/marker/testdata/references/issue60622.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/references/issue60622.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,22 +0,0 @@ +-Regression test for 'references' bug golang/go#60622: +-references to methods of generics were missing. - --func C2() func() { -- return bvuln.Vuln +--- go.mod -- +-module example.com +-go 1.18 +- +--- a/a.go -- +-package a +- +-type G[T any] struct{} +- +-func (G[T]) M() {} //@loc(Mdef, "M"), refs(Mdef, Mdef, Mref) +- +--- b/b.go -- +-package b +- +-import "example.com/a" +- +-func _() { +- new(a.G[int]).M() //@loc(Mref, "M") -} ---- golang.org/amod@v1.0.0/go.mod -- --module golang.org/amod +diff -urN a/gopls/internal/test/marker/testdata/references/issue60676.txt b/gopls/internal/test/marker/testdata/references/issue60676.txt +--- a/gopls/internal/test/marker/testdata/references/issue60676.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/references/issue60676.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,68 +0,0 @@ +-This test verifies that even after importing from export data, the references +-algorithm is able to find all references to struct fields or methods that are +-shared by types from multiple packages. See golang/go#60676. - --go 1.14 ---- golang.org/amod@v1.0.0/avuln/avuln.go -- --package avuln +-Note that the marker test runner awaits the initial workspace load, so export +-data should be populated at the time references are requested. - --type VulnData struct {} --func (v VulnData) Vuln1() {} --func (v VulnData) Vuln2() {} ---- golang.org/amod@v1.0.4/go.mod -- --module golang.org/amod +--- go.mod -- +-module mod.test - --go 1.14 ---- golang.org/amod@v1.0.4/avuln/avuln.go -- --package avuln +-go 1.18 - --type VulnData struct {} --func (v VulnData) Vuln1() {} --func (v VulnData) Vuln2() {} +--- a/a.go -- +-package a - ---- golang.org/bmod@v0.5.0/go.mod -- --module golang.org/bmod +-type A struct { +- F int //@loc(FDef, "F") +- E //@loc(EDef, "E") +-} - --go 1.14 ---- golang.org/bmod@v0.5.0/bvuln/bvuln.go -- --package bvuln +-type E struct { +- G string //@loc(GDef, "G") +-} - --func Vuln() { -- // something evil +-type AI interface { +- M() //@loc(MDef, "M") +- EI +- error -} ---- golang.org/bmod@v0.5.0/unused/unused.go -- --package unused - --func Vuln() { -- // something evil +-type EI interface { +- N() //@loc(NDef, "N") -} ---- golang.org/amod@v1.0.6/go.mod -- --module golang.org/amod - --go 1.14 ---- golang.org/amod@v1.0.6/avuln/avuln.go -- --package avuln +-type T[P any] struct{ f P } - --type VulnData struct {} --func (v VulnData) Vuln1() {} --func (v VulnData) Vuln2() {} --` +-type Error error - --func vulnTestEnv(vulnsDB, proxyData string) (*vulntest.DB, []RunOption, error) { -- db, err := vulntest.NewDatabase(context.Background(), []byte(vulnsData)) -- if err != nil { -- return nil, nil, nil -- } -- settings := Settings{ -- "codelenses": map[string]bool{ -- "run_govulncheck": true, -- }, -- } -- ev := EnvVars{ -- // Let the analyzer read vulnerabilities data from the testdata/vulndb. -- "GOVULNDB": db.URI(), -- // When fetching stdlib package vulnerability info, -- // behave as if our go version is go1.18 for this testing. -- // The default behavior is to run `go env GOVERSION` (which isn't mutable env var). -- scan.GoVersionForVulnTest: "go1.18", -- "_GOPLS_TEST_BINARY_RUN_AS_GOPLS": "true", // needed to run `gopls vulncheck`. -- "GOSUMDB": "off", +- +--- b/b.go -- +-package b +- +-import "mod.test/a" +- +-type B a.A +- +-type BI a.AI +- +-type T a.T[int] // must not panic +- +--- c/c.go -- +-package c +- +-import "mod.test/b" +- +-func _() { +- x := b.B{ +- F: 42, //@refs("F", FDef, "F", Fuse) - } -- return db, []RunOption{ProxyFiles(proxyData), ev, settings}, nil +- x.G = "hi" //@refs("G", GDef, "G") +- _ = x.E //@refs("E", EDef, "E") +- _ = x.F //@loc(Fuse, "F") -} - --func TestRunVulncheckPackageDiagnostics(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) +-func _(y b.BI) { +- _ = y.M //@refs("M", MDef, "M") +- _ = y.N //@refs("N", NDef, "N") +-} +diff -urN a/gopls/internal/test/marker/testdata/references/issue61618.txt b/gopls/internal/test/marker/testdata/references/issue61618.txt +--- a/gopls/internal/test/marker/testdata/references/issue61618.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/references/issue61618.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,36 +0,0 @@ +-Regression test for 'references' bug golang/go#61618: +-references to instantiated fields were missing. - -- db, opts0, err := vulnTestEnv(vulnsData, proxy1) -- if err != nil { -- t.Fatal(err) -- } -- defer db.Clean() +--- go.mod -- +-module example.com +-go 1.18 - -- checkVulncheckDiagnostics := func(env *Env, t *testing.T) { -- env.OpenFile("go.mod") +--- a.go -- +-package a - -- gotDiagnostics := &protocol.PublishDiagnosticsParams{} -- env.AfterChange( -- Diagnostics(env.AtRegexp("go.mod", `golang.org/amod`)), -- ReadDiagnostics("go.mod", gotDiagnostics), -- ) +-// This file is adapted from the example in the issue. - -- testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{ -- "go.mod": { -- IDs: []string{"GO-2022-01", "GO-2022-02", "GO-2022-03"}, -- Mode: vulncheck.ModeImports, -- }, -- }) +-type builder[S ~[]F, F ~string] struct { +- name string +- elements S //@loc(def, "elements"), refs(def, def, assign, use) +- elemData map[F][]ElemData[F] +-} - -- wantVulncheckDiagnostics := map[string]vulnDiagExpectation{ -- "golang.org/amod": { -- diagnostics: []vulnDiag{ -- { -- msg: "golang.org/amod has known vulnerabilities GO-2022-01, GO-2022-03.", -- severity: protocol.SeverityInformation, -- source: string(source.Vulncheck), -- codeActions: []string{ -- "Run govulncheck to verify", -- "Upgrade to v1.0.6", -- "Upgrade to latest", -- }, -- }, -- }, -- codeActions: []string{ -- "Run govulncheck to verify", -- "Upgrade to v1.0.6", -- "Upgrade to latest", -- }, -- hover: []string{"GO-2022-01", "Fixed in v1.0.4.", "GO-2022-03"}, -- }, -- "golang.org/bmod": { -- diagnostics: []vulnDiag{ -- { -- msg: "golang.org/bmod has a vulnerability GO-2022-02.", -- severity: protocol.SeverityInformation, -- source: string(source.Vulncheck), -- codeActions: []string{ -- "Run govulncheck to verify", -- }, -- }, -- }, -- codeActions: []string{ -- "Run govulncheck to verify", -- }, -- hover: []string{"GO-2022-02", "vuln in bmod (no fix)", "No fix is available."}, -- }, -- } +-type ElemData[F ~string] struct { +- Name F +-} +- +-type BuilderImpl[S ~[]F, F ~string] struct{ builder[S, F] } +- +-func NewBuilderImpl[S ~[]F, F ~string](name string) *BuilderImpl[S, F] { +- impl := &BuilderImpl[S,F]{ +- builder[S, F]{ +- name: name, +- elements: S{}, //@loc(assign, "elements"), refs(assign, def, assign, use) +- elemData: map[F][]ElemData[F]{}, +- }, +- } +- +- _ = impl.elements //@loc(use, "elements"), refs(use, def, assign, use) +- return impl +-} +diff -urN a/gopls/internal/test/marker/testdata/references/shadow.txt b/gopls/internal/test/marker/testdata/references/shadow.txt +--- a/gopls/internal/test/marker/testdata/references/shadow.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/references/shadow.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,17 +0,0 @@ +-Test of references in the presence of shadowing. +- +--- go.mod -- +-module example.com +-go 1.12 - -- for pattern, want := range wantVulncheckDiagnostics { -- modPathDiagnostics := testVulnDiagnostics(t, env, pattern, want, gotDiagnostics) +--- a/a.go -- +-package a - -- gotActions := env.CodeAction("go.mod", modPathDiagnostics) -- if diff := diffCodeActions(gotActions, want.codeActions); diff != "" { -- t.Errorf("code actions for %q do not match, got %v, want %v\n%v\n", pattern, gotActions, want.codeActions, diff) -- continue -- } -- } +-func _() { +- x := 123 //@loc(x1, "x"), refs("x", x1, x1ref) +- _ = x //@loc(x1ref, "x") +- { +- x := "hi" //@loc(x2, "x"), refs("x", x2, x2ref) +- _ = x //@loc(x2ref, "x") - } +-} +diff -urN a/gopls/internal/test/marker/testdata/references/test.txt b/gopls/internal/test/marker/testdata/references/test.txt +--- a/gopls/internal/test/marker/testdata/references/test.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/references/test.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,29 +0,0 @@ +-Test of references between the extra files of a test variant +-and the regular package. - -- wantNoVulncheckDiagnostics := func(env *Env, t *testing.T) { -- env.OpenFile("go.mod") +--- go.mod -- +-module example.com +-go 1.12 - -- gotDiagnostics := &protocol.PublishDiagnosticsParams{} -- env.AfterChange( -- ReadDiagnostics("go.mod", gotDiagnostics), -- ) +--- a/a.go -- +-package a - -- if len(gotDiagnostics.Diagnostics) > 0 { -- t.Errorf("Unexpected diagnostics: %v", stringify(gotDiagnostics)) -- } -- testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{}) -- } +-func fn() {} //@loc(def, "fn"), refs("fn", def, use) - -- for _, tc := range []struct { -- name string -- setting Settings -- wantDiagnostics bool -- }{ -- {"imports", Settings{"ui.diagnostic.vulncheck": "Imports"}, true}, -- {"default", Settings{}, false}, -- {"invalid", Settings{"ui.diagnostic.vulncheck": "invalid"}, false}, -- } { -- t.Run(tc.name, func(t *testing.T) { -- // override the settings options to enable diagnostics -- opts := append(opts0, tc.setting) -- WithOptions(opts...).Run(t, workspace1, func(t *testing.T, env *Env) { -- // TODO(hyangah): implement it, so we see GO-2022-01, GO-2022-02, and GO-2022-03. -- // Check that the actions we get when including all diagnostics at a location return the same result -- if tc.wantDiagnostics { -- checkVulncheckDiagnostics(env, t) -- } else { -- wantNoVulncheckDiagnostics(env, t) -- } +-type t struct { g int } //@loc(gdef, "g") +-type u struct { t } - -- if tc.name == "imports" && tc.wantDiagnostics { -- // test we get only govulncheck-based diagnostics after "run govulncheck". -- var result command.RunVulncheckResult -- env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result) -- gotDiagnostics := &protocol.PublishDiagnosticsParams{} -- env.OnceMet( -- CompletedProgress(result.Token, nil), -- ShownMessage("Found"), -- ) -- env.OnceMet( -- Diagnostics(env.AtRegexp("go.mod", "golang.org/bmod")), -- ReadDiagnostics("go.mod", gotDiagnostics), -- ) -- // We expect only one diagnostic for GO-2022-02. -- count := 0 -- for _, diag := range gotDiagnostics.Diagnostics { -- if strings.Contains(diag.Message, "GO-2022-02") { -- count++ -- if got, want := diag.Severity, protocol.SeverityWarning; got != want { -- t.Errorf("Diagnostic for GO-2022-02 = %v, want %v", got, want) -- } -- } -- } -- if count != 1 { -- t.Errorf("Unexpected number of diagnostics about GO-2022-02 = %v, want 1:\n%+v", count, stringify(gotDiagnostics)) -- } -- } -- }) -- }) -- } --} +-var _ = new(u).g //@loc(gref, "g"), refs("g", gdef, gref) +-// TODO(adonovan): fix: gref2 and gdef2 are missing. - --func stringify(a interface{}) string { -- data, _ := json.Marshal(a) -- return string(data) +--- a/a_test.go -- +-package a +- +-func _() { +- fn() //@loc(use, "fn") +- +- _ = new(u).g //@loc(gref2, "g"), refs("g", gdef2, gref, gref2) -} - --func TestRunVulncheckWarning(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) +-// This declaration changes the meaning of u.t in the test. +-func (u) g() {} //@loc(gdef2, "g") +diff -urN a/gopls/internal/test/marker/testdata/references/typeswitch.txt b/gopls/internal/test/marker/testdata/references/typeswitch.txt +--- a/gopls/internal/test/marker/testdata/references/typeswitch.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/references/typeswitch.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,18 +0,0 @@ +-Tests of reference to implicit type switch vars, which are +-a special case in go/types.Info{Def,Use,Implicits}. - -- db, opts, err := vulnTestEnv(vulnsData, proxy1) -- if err != nil { -- t.Fatal(err) +--- go.mod -- +-module example.com +-go 1.12 +- +--- a/a.go -- +-package a +- +-func _(x interface{}) { +- switch y := x.(type) { //@loc(yDecl, "y"), refs("y", yDecl, yInt, yDefault) +- case int: +- println(y) //@loc(yInt, "y"), refs("y", yDecl, yInt, yDefault) +- default: +- println(y) //@loc(yDefault, "y") - } -- defer db.Clean() -- WithOptions(opts...).Run(t, workspace1, func(t *testing.T, env *Env) { -- env.OpenFile("go.mod") +-} +diff -urN a/gopls/internal/test/marker/testdata/rename/bad.txt b/gopls/internal/test/marker/testdata/rename/bad.txt +--- a/gopls/internal/test/marker/testdata/rename/bad.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/rename/bad.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,19 +0,0 @@ +-This test checks that rename fails in the presence of errors. - -- var result command.RunVulncheckResult -- env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result) -- gotDiagnostics := &protocol.PublishDiagnosticsParams{} -- env.OnceMet( -- CompletedProgress(result.Token, nil), -- ShownMessage("Found"), -- ) -- // Vulncheck diagnostics asynchronous to the vulncheck command. -- env.OnceMet( -- Diagnostics(env.AtRegexp("go.mod", `golang.org/amod`)), -- ReadDiagnostics("go.mod", gotDiagnostics), -- ) +--- go.mod -- +-module golang.org/lsptests/bad - -- testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{ -- "go.mod": {IDs: []string{"GO-2022-01", "GO-2022-02", "GO-2022-03"}, Mode: vulncheck.ModeGovulncheck}, -- }) -- env.OpenFile("x/x.go") -- env.OpenFile("y/y.go") -- wantDiagnostics := map[string]vulnDiagExpectation{ -- "golang.org/amod": { -- applyAction: "Upgrade to v1.0.6", -- diagnostics: []vulnDiag{ -- { -- msg: "golang.org/amod has a vulnerability used in the code: GO-2022-01.", -- severity: protocol.SeverityWarning, -- source: string(source.Govulncheck), -- codeActions: []string{ -- "Upgrade to v1.0.4", -- "Upgrade to latest", -- "Reset govulncheck result", -- }, -- }, -- { -- msg: "golang.org/amod has a vulnerability GO-2022-03 that is not used in the code.", -- severity: protocol.SeverityInformation, -- source: string(source.Govulncheck), -- codeActions: []string{ -- "Upgrade to v1.0.6", -- "Upgrade to latest", -- "Reset govulncheck result", -- }, -- }, -- }, -- codeActions: []string{ -- "Upgrade to v1.0.6", -- "Upgrade to latest", -- "Reset govulncheck result", -- }, -- hover: []string{"GO-2022-01", "Fixed in v1.0.4.", "GO-2022-03"}, -- }, -- "golang.org/bmod": { -- diagnostics: []vulnDiag{ -- { -- msg: "golang.org/bmod has a vulnerability used in the code: GO-2022-02.", -- severity: protocol.SeverityWarning, -- source: string(source.Govulncheck), -- codeActions: []string{ -- "Reset govulncheck result", // no fix, but we should give an option to reset. -- }, -- }, -- }, -- codeActions: []string{ -- "Reset govulncheck result", // no fix, but we should give an option to reset. -- }, -- hover: []string{"GO-2022-02", "vuln in bmod (no fix)", "No fix is available."}, -- }, -- } +-go 1.18 - -- for mod, want := range wantDiagnostics { -- modPathDiagnostics := testVulnDiagnostics(t, env, mod, want, gotDiagnostics) +--- bad.go -- +-package bad - -- // Check that the actions we get when including all diagnostics at a location return the same result -- gotActions := env.CodeAction("go.mod", modPathDiagnostics) -- if diff := diffCodeActions(gotActions, want.codeActions); diff != "" { -- t.Errorf("code actions for %q do not match, expected %v, got %v\n%v\n", mod, want.codeActions, gotActions, diff) -- continue -- } +-type myStruct struct { +-} - -- // Apply the code action matching applyAction. -- if want.applyAction == "" { -- continue -- } -- for _, action := range gotActions { -- if action.Title == want.applyAction { -- env.ApplyCodeAction(action) -- break -- } -- } -- } +-func (s *myStruct) sFunc() bool { //@renameerr("sFunc", "rFunc", re"not possible") +- return s.Bad //@diag("Bad", re"no field or method") +-} - -- env.Await(env.DoneWithChangeWatchedFiles()) -- wantGoMod := `module golang.org/entry +--- bad_test.go -- +-package bad +diff -urN a/gopls/internal/test/marker/testdata/rename/basic.txt b/gopls/internal/test/marker/testdata/rename/basic.txt +--- a/gopls/internal/test/marker/testdata/rename/basic.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/rename/basic.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,35 +0,0 @@ +-This test performs basic coverage of 'rename' within a single package. - --go 1.18 +--- basic.go -- +-package p - --require golang.org/cmod v1.1.3 +-func f(x int) { println(x) } //@rename("x", "y", xToy) - --require ( -- golang.org/amod v1.0.6 // indirect -- golang.org/bmod v0.5.0 // indirect --) --` -- if got := env.BufferText("go.mod"); got != wantGoMod { -- t.Fatalf("go.mod vulncheck fix failed:\n%s", compare.Text(wantGoMod, got)) -- } -- }) --} +--- @xToy/basic.go -- +-@@ -3 +3 @@ +--func f(x int) { println(x) } //@rename("x", "y", xToy) +-+func f(y int) { println(y) } //@rename("x", "y", xToy) +--- alias.go -- +-package p - --func diffCodeActions(gotActions []protocol.CodeAction, want []string) string { -- var gotTitles []string -- for _, ca := range gotActions { -- gotTitles = append(gotTitles, ca.Title) -- } -- return cmp.Diff(want, gotTitles) +-// from golang/go#61625 +-type LongNameHere struct{} +-type A = LongNameHere //@rename("A", "B", AToB) +-func Foo() A +- +--- errors.go -- +-package p +- +-func _(x []int) { //@renameerr("_", "blank", `can't rename "_"`) +- x = append(x, 1) //@renameerr("append", "blank", "built in and cannot be renamed") +- x = nil //@renameerr("nil", "blank", "built in and cannot be renamed") +- x = nil //@renameerr("x", "x", "old and new names are the same: x") +- _ = 1 //@renameerr("1", "x", "no identifier found") -} - --const workspace2 = ` +--- @AToB/alias.go -- +-@@ -5,2 +5,2 @@ +--type A = LongNameHere //@rename("A", "B", AToB) +--func Foo() A +-+type B = LongNameHere //@rename("A", "B", AToB) +-+func Foo() B +diff -urN a/gopls/internal/test/marker/testdata/rename/conflict.txt b/gopls/internal/test/marker/testdata/rename/conflict.txt +--- a/gopls/internal/test/marker/testdata/rename/conflict.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/rename/conflict.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,59 +0,0 @@ +-This test exercises some renaming conflict scenarios +-and ensures that the errors are informative. +- --- go.mod -- --module golang.org/entry +-module example.com +-go 1.12 - --go 1.18 +--- super/p.go -- +-package super - --require golang.org/bmod v0.5.0 +-var x int - ---- go.sum -- --golang.org/bmod v0.5.0 h1:MT/ysNRGbCiURc5qThRFWaZ5+rK3pQRPo9w7dYZfMDk= --golang.org/bmod v0.5.0/go.mod h1:k+zl+Ucu4yLIjndMIuWzD/MnOHy06wqr3rD++y0abVs= ---- x/x.go -- --package x +-func f(y int) { +- println(x) +- println(y) //@renameerr("y", "x", errSuperBlockConflict) +-} - --import "golang.org/bmod/bvuln" +--- @errSuperBlockConflict -- +-super/p.go:5:8: renaming this var "y" to "x" +-super/p.go:6:10: would shadow this reference +-super/p.go:3:5: to the var declared here +--- sub/p.go -- +-package sub - --func F() { -- // Calls a benign func in bvuln. -- bvuln.OK() +-var a int +- +-func f2(b int) { +- println(a) //@renameerr("a", "b", errSubBlockConflict) +- println(b) -} --` - --const proxy2 = ` ---- golang.org/bmod@v0.5.0/bvuln/bvuln.go -- --package bvuln +--- @errSubBlockConflict -- +-sub/p.go:3:5: renaming this var "a" to "b" +-sub/p.go:6:10: would cause this reference to become shadowed +-sub/p.go:5:9: by this intervening var definition +--- pkgname/p.go -- +-package pkgname - --func Vuln() {} // vulnerable. --func OK() {} // ok. --` +-import e1 "errors" //@renameerr("e1", "errors", errImportConflict) +-import "errors" - --func TestGovulncheckInfo(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) +-var _ = errors.New +-var _ = e1.New - -- db, opts, err := vulnTestEnv(vulnsData, proxy2) -- if err != nil { -- t.Fatal(err) -- } -- defer db.Clean() -- WithOptions(opts...).Run(t, workspace2, func(t *testing.T, env *Env) { -- env.OpenFile("go.mod") -- var result command.RunVulncheckResult -- env.ExecuteCodeLensCommand("go.mod", command.RunGovulncheck, &result) -- gotDiagnostics := &protocol.PublishDiagnosticsParams{} -- env.OnceMet( -- CompletedProgress(result.Token, nil), -- ShownMessage("No vulnerabilities found"), // only count affecting vulnerabilities. -- ) +--- @errImportConflict -- +-pkgname/p.go:3:8: renaming this imported package name "e1" to "errors" +-pkgname/p.go:4:8: conflicts with imported package name in same block +--- pkgname2/p1.go -- +-package pkgname2 +-var x int - -- // Vulncheck diagnostics asynchronous to the vulncheck command. -- env.OnceMet( -- Diagnostics(env.AtRegexp("go.mod", "golang.org/bmod")), -- ReadDiagnostics("go.mod", gotDiagnostics), -- ) +--- pkgname2/p2.go -- +-package pkgname2 +-import "errors" //@renameerr("errors", "x", errImportConflict2) +-var _ = errors.New - -- testFetchVulncheckResult(t, env, map[string]fetchVulncheckResult{"go.mod": {IDs: []string{"GO-2022-02"}, Mode: vulncheck.ModeGovulncheck}}) -- // wantDiagnostics maps a module path in the require -- // section of a go.mod to diagnostics that will be returned -- // when running vulncheck. -- wantDiagnostics := map[string]vulnDiagExpectation{ -- "golang.org/bmod": { -- diagnostics: []vulnDiag{ -- { -- msg: "golang.org/bmod has a vulnerability GO-2022-02 that is not used in the code.", -- severity: protocol.SeverityInformation, -- source: string(source.Govulncheck), -- codeActions: []string{ -- "Reset govulncheck result", -- }, -- }, -- }, -- codeActions: []string{ -- "Reset govulncheck result", -- }, -- hover: []string{"GO-2022-02", "vuln in bmod (no fix)", "No fix is available."}, -- }, -- } +--- @errImportConflict2 -- +-pkgname2/p2.go:2:8: renaming this imported package name "errors" to "x" would conflict +-pkgname2/p1.go:2:5: with this package member var +diff -urN a/gopls/internal/test/marker/testdata/rename/crosspkg.txt b/gopls/internal/test/marker/testdata/rename/crosspkg.txt +--- a/gopls/internal/test/marker/testdata/rename/crosspkg.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/rename/crosspkg.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,72 +0,0 @@ +-This test checks cross-package renaming. - -- var allActions []protocol.CodeAction -- for mod, want := range wantDiagnostics { -- modPathDiagnostics := testVulnDiagnostics(t, env, mod, want, gotDiagnostics) -- // Check that the actions we get when including all diagnostics at a location return the same result -- gotActions := env.CodeAction("go.mod", modPathDiagnostics) -- allActions = append(allActions, gotActions...) -- if diff := diffCodeActions(gotActions, want.codeActions); diff != "" { -- t.Errorf("code actions for %q do not match, expected %v, got %v\n%v\n", mod, want.codeActions, gotActions, diff) -- continue -- } -- } +--- go.mod -- +-module golang.org/lsptests/rename - -- // Clear Diagnostics by using one of the reset code actions. -- var reset protocol.CodeAction -- for _, a := range allActions { -- if a.Title == "Reset govulncheck result" { -- reset = a -- break -- } -- } -- if reset.Title != "Reset govulncheck result" { -- t.Errorf("failed to find a 'Reset govulncheck result' code action, got %v", allActions) -- } -- env.ApplyCodeAction(reset) +-go 1.18 - -- env.Await(NoDiagnostics(ForFile("go.mod"))) -- }) --} +--- crosspkg/crosspkg.go -- +-package crosspkg - --// testVulnDiagnostics finds the require or module statement line for the requireMod in go.mod file --// and runs checks if diagnostics and code actions associated with the line match expectation. --func testVulnDiagnostics(t *testing.T, env *Env, pattern string, want vulnDiagExpectation, got *protocol.PublishDiagnosticsParams) []protocol.Diagnostic { -- t.Helper() -- loc := env.RegexpSearch("go.mod", pattern) -- var modPathDiagnostics []protocol.Diagnostic -- for _, w := range want.diagnostics { -- // Find the diagnostics at loc.start. -- var diag *protocol.Diagnostic -- for _, g := range got.Diagnostics { -- g := g -- if g.Range.Start == loc.Range.Start && w.msg == g.Message { -- modPathDiagnostics = append(modPathDiagnostics, g) -- diag = &g -- break -- } -- } -- if diag == nil { -- t.Errorf("no diagnostic at %q matching %q found\n", pattern, w.msg) -- continue -- } -- if diag.Severity != w.severity || diag.Source != w.source { -- t.Errorf("incorrect (severity, source) for %q, want (%s, %s) got (%s, %s)\n", w.msg, w.severity, w.source, diag.Severity, diag.Source) -- } -- // Check expected code actions appear. -- gotActions := env.CodeAction("go.mod", []protocol.Diagnostic{*diag}) -- if diff := diffCodeActions(gotActions, w.codeActions); diff != "" { -- t.Errorf("code actions for %q do not match, want %v, got %v\n%v\n", w.msg, w.codeActions, gotActions, diff) -- continue -- } -- } -- // Check that useful info is supplemented as hover. -- if len(want.hover) > 0 { -- hover, _ := env.Hover(loc) -- for _, part := range want.hover { -- if !strings.Contains(hover.Value, part) { -- t.Errorf("hover contents for %q do not match, want %v, got %v\n", pattern, strings.Join(want.hover, ","), hover.Value) -- break -- } -- } -- } -- return modPathDiagnostics --} +-func Foo() { //@rename("Foo", "Dolphin", FooToDolphin) - --type vulnRelatedInfo struct { -- Filename string -- Line uint32 -- Message string -} - --type vulnDiag struct { -- msg string -- severity protocol.DiagnosticSeverity -- // codeActions is a list titles of code actions that we get with this -- // diagnostics as the context. -- codeActions []string -- // relatedInfo is related info message prefixed by the file base. -- // See summarizeRelatedInfo. -- relatedInfo []vulnRelatedInfo -- // diagnostic source. -- source string --} +-var Bar int //@rename("Bar", "Tomato", BarToTomato) - --func (i vulnRelatedInfo) less(j vulnRelatedInfo) bool { -- if i.Filename != j.Filename { -- return i.Filename < j.Filename -- } -- if i.Line != j.Line { -- return i.Line < j.Line -- } -- return i.Message < j.Message --} +--- crosspkg/another/another.go -- +-package another - --// vulnDiagExpectation maps a module path in the require --// section of a go.mod to diagnostics that will be returned --// when running vulncheck. --type vulnDiagExpectation struct { -- // applyAction is the title of the code action to run for this module. -- // If empty, no code actions will be executed. -- applyAction string -- // diagnostics is the list of diagnostics we expect at the require line for -- // the module path. -- diagnostics []vulnDiag -- // codeActions is a list titles of code actions that we get with context -- // diagnostics. -- codeActions []string -- // hover message is the list of expected hover message parts for this go.mod require line. -- // all parts must appear in the hover message. -- hover []string +-type ( +- I interface{ F() } +- C struct{ I } +-) +- +-func (C) g() +- +-func _() { +- var x I = C{} +- x.F() //@rename("F", "G", FToG) -} -diff -urN a/gopls/internal/regtest/misc/workspace_symbol_test.go b/gopls/internal/regtest/misc/workspace_symbol_test.go ---- a/gopls/internal/regtest/misc/workspace_symbol_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/misc/workspace_symbol_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,114 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package misc +--- crosspkg/other/other.go -- +-package other - --import ( -- "testing" +-import "golang.org/lsptests/rename/crosspkg" - -- "github.com/google/go-cmp/cmp" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" -- "golang.org/x/tools/gopls/internal/lsp/source" --) +-func Other() { +- crosspkg.Bar //@diag("crosspkg", re"not used") +- crosspkg.Foo() //@rename("Foo", "Flamingo", FooToFlamingo) +-} +- +--- @BarToTomato/crosspkg/crosspkg.go -- +-@@ -7 +7 @@ +--var Bar int //@rename("Bar", "Tomato", BarToTomato) +-+var Tomato int //@rename("Bar", "Tomato", BarToTomato) +--- @BarToTomato/crosspkg/other/other.go -- +-@@ -6 +6 @@ +-- crosspkg.Bar //@diag("crosspkg", re"not used") +-+ crosspkg.Tomato //@diag("crosspkg", re"not used") +--- @FToG/crosspkg/another/another.go -- +-@@ -4 +4 @@ +-- I interface{ F() } +-+ I interface{ G() } +-@@ -12 +12 @@ +-- x.F() //@rename("F", "G", FToG) +-+ x.G() //@rename("F", "G", FToG) +--- @FooToDolphin/crosspkg/crosspkg.go -- +-@@ -3 +3 @@ +--func Foo() { //@rename("Foo", "Dolphin", FooToDolphin) +-+func Dolphin() { //@rename("Foo", "Dolphin", FooToDolphin) +--- @FooToDolphin/crosspkg/other/other.go -- +-@@ -7 +7 @@ +-- crosspkg.Foo() //@rename("Foo", "Flamingo", FooToFlamingo) +-+ crosspkg.Dolphin() //@rename("Foo", "Flamingo", FooToFlamingo) +--- @FooToFlamingo/crosspkg/crosspkg.go -- +-@@ -3 +3 @@ +--func Foo() { //@rename("Foo", "Dolphin", FooToDolphin) +-+func Flamingo() { //@rename("Foo", "Dolphin", FooToDolphin) +--- @FooToFlamingo/crosspkg/other/other.go -- +-@@ -7 +7 @@ +-- crosspkg.Foo() //@rename("Foo", "Flamingo", FooToFlamingo) +-+ crosspkg.Flamingo() //@rename("Foo", "Flamingo", FooToFlamingo) +diff -urN a/gopls/internal/test/marker/testdata/rename/doclink.txt b/gopls/internal/test/marker/testdata/rename/doclink.txt +--- a/gopls/internal/test/marker/testdata/rename/doclink.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/rename/doclink.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,180 +0,0 @@ +-This test checks that doc links are also handled correctly (golang/go#64495). - --func TestWorkspaceSymbolMissingMetadata(t *testing.T) { -- const files = ` --- go.mod -- --module mod.com +-module example.com +- +-go 1.21 +- +--- a/a.go -- +-package a +- +-// Foo just for test [Foo] +-// reference others objects [A] [B] [C] [C.F] [C.PF] +-func Foo() {} //@rename("Foo", "Bar", FooToBar) - --go 1.17 ---- a.go -- --package p +-const A = 1 //@rename("A", "AA", AToAA) - --const K1 = "a.go" ---- exclude.go -- +-var B = 1 //@rename("B", "BB", BToBB) - --//go:build exclude --// +build exclude +-type C int //@rename("C", "CC", CToCC) - --package exclude +-func (C) F() {} //@rename("F", "FF", FToFF) - --const K2 = "exclude.go" --` +-func (*C) PF() {} //@rename("PF", "PFF", PFToPFF) - -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("a.go") -- checkSymbols(env, "K", "K1") +-// D just for test [*D] +-type D int //@rename("D", "DD", DToDD) - -- // Opening up an ignored file will result in an overlay with missing -- // metadata, but this shouldn't break workspace symbols requests. -- env.OpenFile("exclude.go") -- checkSymbols(env, "K", "K1") -- }) +-// E test generic type doc link [E] [E.Foo] +-type E[T any] struct { //@rename("E", "EE", EToEE) +- Field T -} - --func TestWorkspaceSymbolSorting(t *testing.T) { -- const files = ` ---- go.mod -- --module mod.com -- --go 1.17 ---- a/a.go -- --package a +-func (E[T]) Foo() {} //@rename("Foo", "Bar", EFooToEBar) - --const ( -- Foo = iota -- FooBar -- Fooey -- Fooex -- Fooest --) --` +--- b/b.go -- +-package b - -- var symbolMatcher = string(source.SymbolFastFuzzy) -- WithOptions( -- Settings{"symbolMatcher": symbolMatcher}, -- ).Run(t, files, func(t *testing.T, env *Env) { -- checkSymbols(env, "Foo", -- "Foo", // prefer exact segment matches first -- "FooBar", // ...followed by exact word matches -- "Fooex", // shorter than Fooest, FooBar, lexically before Fooey -- "Fooey", // shorter than Fooest, Foobar -- "Fooest", -- ) -- }) --} +-import aa "example.com/a" //@rename("aa", "a", pkgRename) +- +-// FooBar just for test [aa.Foo] [aa.A] [aa.B] [aa.C] [aa.C.F] [aa.C.PF] +-// reference pointer type [*aa.D] +-// reference generic type links [aa.E] [aa.E.Foo] +-func FooBar() { +- aa.Foo() +- var e aa.E[int] +- e.Foo() +-} +- +- +--- @FooToBar/a/a.go -- +-@@ -3 +3 @@ +--// Foo just for test [Foo] +-+// Bar just for test [Bar] +-@@ -5 +5 @@ +--func Foo() {} //@rename("Foo", "Bar", FooToBar) +-+func Bar() {} //@rename("Foo", "Bar", FooToBar) +--- @FooToBar/b/b.go -- +-@@ -5 +5 @@ +--// FooBar just for test [aa.Foo] [aa.A] [aa.B] [aa.C] [aa.C.F] [aa.C.PF] +-+// FooBar just for test [aa.Bar] [aa.A] [aa.B] [aa.C] [aa.C.F] [aa.C.PF] +-@@ -9 +9 @@ +-- aa.Foo() +-+ aa.Bar() +--- @AToAA/a/a.go -- +-@@ -4 +4 @@ +--// reference others objects [A] [B] [C] [C.F] [C.PF] +-+// reference others objects [AA] [B] [C] [C.F] [C.PF] +-@@ -7 +7 @@ +--const A = 1 //@rename("A", "AA", AToAA) +-+const AA = 1 //@rename("A", "AA", AToAA) +--- @AToAA/b/b.go -- +-@@ -5 +5 @@ +--// FooBar just for test [aa.Foo] [aa.A] [aa.B] [aa.C] [aa.C.F] [aa.C.PF] +-+// FooBar just for test [aa.Foo] [aa.AA] [aa.B] [aa.C] [aa.C.F] [aa.C.PF] +--- @BToBB/a/a.go -- +-@@ -4 +4 @@ +--// reference others objects [A] [B] [C] [C.F] [C.PF] +-+// reference others objects [A] [BB] [C] [C.F] [C.PF] +-@@ -9 +9 @@ +--var B = 1 //@rename("B", "BB", BToBB) +-+var BB = 1 //@rename("B", "BB", BToBB) +--- @BToBB/b/b.go -- +-@@ -5 +5 @@ +--// FooBar just for test [aa.Foo] [aa.A] [aa.B] [aa.C] [aa.C.F] [aa.C.PF] +-+// FooBar just for test [aa.Foo] [aa.A] [aa.BB] [aa.C] [aa.C.F] [aa.C.PF] +--- @CToCC/a/a.go -- +-@@ -4 +4 @@ +--// reference others objects [A] [B] [C] [C.F] [C.PF] +-+// reference others objects [A] [B] [CC] [CC.F] [CC.PF] +-@@ -11 +11 @@ +--type C int //@rename("C", "CC", CToCC) +-+type CC int //@rename("C", "CC", CToCC) +-@@ -13 +13 @@ +--func (C) F() {} //@rename("F", "FF", FToFF) +-+func (CC) F() {} //@rename("F", "FF", FToFF) +-@@ -15 +15 @@ +--func (*C) PF() {} //@rename("PF", "PFF", PFToPFF) +-+func (*CC) PF() {} //@rename("PF", "PFF", PFToPFF) +--- @CToCC/b/b.go -- +-@@ -5 +5 @@ +--// FooBar just for test [aa.Foo] [aa.A] [aa.B] [aa.C] [aa.C.F] [aa.C.PF] +-+// FooBar just for test [aa.Foo] [aa.A] [aa.B] [aa.CC] [aa.CC.F] [aa.CC.PF] +--- @FToFF/a/a.go -- +-@@ -4 +4 @@ +--// reference others objects [A] [B] [C] [C.F] [C.PF] +-+// reference others objects [A] [B] [C] [C.FF] [C.PF] +-@@ -13 +13 @@ +--func (C) F() {} //@rename("F", "FF", FToFF) +-+func (C) FF() {} //@rename("F", "FF", FToFF) +--- @FToFF/b/b.go -- +-@@ -5 +5 @@ +--// FooBar just for test [aa.Foo] [aa.A] [aa.B] [aa.C] [aa.C.F] [aa.C.PF] +-+// FooBar just for test [aa.Foo] [aa.A] [aa.B] [aa.C] [aa.C.FF] [aa.C.PF] +--- @PFToPFF/a/a.go -- +-@@ -4 +4 @@ +--// reference others objects [A] [B] [C] [C.F] [C.PF] +-+// reference others objects [A] [B] [C] [C.F] [C.PFF] +-@@ -15 +15 @@ +--func (*C) PF() {} //@rename("PF", "PFF", PFToPFF) +-+func (*C) PFF() {} //@rename("PF", "PFF", PFToPFF) +--- @PFToPFF/b/b.go -- +-@@ -5 +5 @@ +--// FooBar just for test [aa.Foo] [aa.A] [aa.B] [aa.C] [aa.C.F] [aa.C.PF] +-+// FooBar just for test [aa.Foo] [aa.A] [aa.B] [aa.C] [aa.C.F] [aa.C.PFF] +--- @pkgRename/b/b.go -- +-@@ -3 +3 @@ +--import aa "example.com/a" //@rename("aa", "a", pkgRename) +-+import "example.com/a" //@rename("aa", "a", pkgRename) +-@@ -5,3 +5,3 @@ +--// FooBar just for test [aa.Foo] [aa.A] [aa.B] [aa.C] [aa.C.F] [aa.C.PF] +--// reference pointer type [*aa.D] +--// reference generic type links [aa.E] [aa.E.Foo] +-+// FooBar just for test [a.Foo] [a.A] [a.B] [a.C] [a.C.F] [a.C.PF] +-+// reference pointer type [*a.D] +-+// reference generic type links [a.E] [a.E.Foo] +-@@ -9,2 +9,2 @@ +-- aa.Foo() +-- var e aa.E[int] +-+ a.Foo() +-+ var e a.E[int] +--- @DToDD/a/a.go -- +-@@ -17,2 +17,2 @@ +--// D just for test [*D] +--type D int //@rename("D", "DD", DToDD) +-+// DD just for test [*DD] +-+type DD int //@rename("D", "DD", DToDD) +--- @DToDD/b/b.go -- +-@@ -6 +6 @@ +--// reference pointer type [*aa.D] +-+// reference pointer type [*aa.DD] +--- @EToEE/a/a.go -- +-@@ -20,2 +20,2 @@ +--// E test generic type doc link [E] [E.Foo] +--type E[T any] struct { //@rename("E", "EE", EToEE) +-+// EE test generic type doc link [EE] [EE.Foo] +-+type EE[T any] struct { //@rename("E", "EE", EToEE) +-@@ -25 +25 @@ +--func (E[T]) Foo() {} //@rename("Foo", "Bar", EFooToEBar) +-+func (EE[T]) Foo() {} //@rename("Foo", "Bar", EFooToEBar) +--- @EToEE/b/b.go -- +-@@ -7 +7 @@ +--// reference generic type links [aa.E] [aa.E.Foo] +-+// reference generic type links [aa.EE] [aa.EE.Foo] +-@@ -10 +10 @@ +-- var e aa.E[int] +-+ var e aa.EE[int] +--- @EFooToEBar/a/a.go -- +-@@ -20 +20 @@ +--// E test generic type doc link [E] [E.Foo] +-+// E test generic type doc link [E] [E.Bar] +-@@ -25 +25 @@ +--func (E[T]) Foo() {} //@rename("Foo", "Bar", EFooToEBar) +-+func (E[T]) Bar() {} //@rename("Foo", "Bar", EFooToEBar) +--- @EFooToEBar/b/b.go -- +-@@ -7 +7 @@ +--// reference generic type links [aa.E] [aa.E.Foo] +-+// reference generic type links [aa.E] [aa.E.Bar] +-@@ -11 +11 @@ +-- e.Foo() +-+ e.Bar() +diff -urN a/gopls/internal/test/marker/testdata/rename/embed.txt b/gopls/internal/test/marker/testdata/rename/embed.txt +--- a/gopls/internal/test/marker/testdata/rename/embed.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/rename/embed.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,33 +0,0 @@ +-This test exercises renaming of types used as embedded fields. - --func TestWorkspaceSymbolSpecialPatterns(t *testing.T) { -- const files = ` --- go.mod -- --module mod.com +-module example.com +-go 1.12 - --go 1.17 --- a/a.go -- -package a - --const ( -- AxxBxxCxx -- ABC --) --` +-type A int //@rename("A", "A2", type) - -- var symbolMatcher = string(source.SymbolFastFuzzy) -- WithOptions( -- Settings{"symbolMatcher": symbolMatcher}, -- ).Run(t, files, func(t *testing.T, env *Env) { -- checkSymbols(env, "ABC", "ABC", "AxxBxxCxx") -- checkSymbols(env, "'ABC", "ABC") -- checkSymbols(env, "^mod.com", "mod.com/a.ABC", "mod.com/a.AxxBxxCxx") -- checkSymbols(env, "^mod.com Axx", "mod.com/a.AxxBxxCxx") -- checkSymbols(env, "C$", "ABC") -- }) --} +--- b/b.go -- +-package b - --func checkSymbols(env *Env, query string, want ...string) { -- env.T.Helper() -- var got []string -- for _, info := range env.Symbol(query) { -- got = append(got, info.Name) -- } -- if diff := cmp.Diff(got, want); diff != "" { -- env.T.Errorf("unexpected Symbol(%q) result (+want -got):\n%s", query, diff) -- } --} -diff -urN a/gopls/internal/regtest/modfile/modfile_test.go b/gopls/internal/regtest/modfile/modfile_test.go ---- a/gopls/internal/regtest/modfile/modfile_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/modfile/modfile_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,1222 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-import "example.com/a" - --package modfile +-type B struct { a.A } //@renameerr("A", "A3", errAnonField) - --import ( -- "path/filepath" -- "runtime" -- "strings" -- "testing" +-var _ = new(B).A //@renameerr("A", "A4", errAnonField) - -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/hooks" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" -- "golang.org/x/tools/gopls/internal/lsp/tests/compare" +--- @errAnonField -- +-can't rename embedded fields: rename the type directly or name the field +--- @type/a/a.go -- +-@@ -3 +3 @@ +--type A int //@rename("A", "A2", type) +-+type A2 int //@rename("A", "A2", type) +--- @type/b/b.go -- +-@@ -5 +5 @@ +--type B struct { a.A } //@renameerr("A", "A3", errAnonField) +-+type B struct { a.A2 } //@renameerr("A", "A3", errAnonField) +-@@ -7 +7 @@ +--var _ = new(B).A //@renameerr("A", "A4", errAnonField) +-+var _ = new(B).A2 //@renameerr("A", "A4", errAnonField) +diff -urN a/gopls/internal/test/marker/testdata/rename/generics_basic.txt b/gopls/internal/test/marker/testdata/rename/generics_basic.txt +--- a/gopls/internal/test/marker/testdata/rename/generics_basic.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/rename/generics_basic.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,107 +0,0 @@ +-This test exercise basic renaming of generic code. +- +--- embedded.go -- +-package a - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/internal/testenv" --) +-type foo[P any] int //@rename("foo", "bar", fooTobar) - --func TestMain(m *testing.M) { -- bug.PanicOnBugs = true -- Main(m, hooks.Options) --} +-var x struct{ foo[int] } - --const workspaceProxy = ` ---- example.com@v1.2.3/go.mod -- --module example.com +-var _ = x.foo - --go 1.12 ---- example.com@v1.2.3/blah/blah.go -- --package blah +--- @fooTobar/embedded.go -- +-@@ -3 +3 @@ +--type foo[P any] int //@rename("foo", "bar", fooTobar) +-+type bar[P any] int //@rename("foo", "bar", fooTobar) +-@@ -5 +5 @@ +--var x struct{ foo[int] } +-+var x struct{ bar[int] } +-@@ -7 +7 @@ +--var _ = x.foo +-+var _ = x.bar +--- generics.go -- +-package a - --func SaySomething() { -- fmt.Println("something") +-type G[P any] struct { +- F int -} ---- random.org@v1.2.3/go.mod -- --module random.org - --go 1.12 ---- random.org@v1.2.3/bye/bye.go -- --package bye +-func (G[_]) M() {} - --func Goodbye() { -- println("Bye") +-func F[P any](P) { +- var p P //@rename("P", "Q", PToQ) +- _ = p -} --` -- --const proxy = ` ---- example.com@v1.2.3/go.mod -- --module example.com -- --go 1.12 ---- example.com@v1.2.3/blah/blah.go -- --package blah - --const Name = "Blah" ---- random.org@v1.2.3/go.mod -- --module random.org -- --go 1.12 ---- random.org@v1.2.3/blah/blah.go -- --package hello -- --const Name = "Hello" --` +-func _() { +- var x G[int] //@rename("G", "H", GToH) +- _ = x.F //@rename("F", "K", FToK) +- x.M() //@rename("M", "N", MToN) - --func TestModFileModification(t *testing.T) { -- const untidyModule = ` ---- a/go.mod -- --module mod.com +- var y G[string] +- _ = y.F +- y.M() +-} - ---- a/main.go -- --package main +--- @FToK/generics.go -- +-@@ -4 +4 @@ +-- F int +-+ K int +-@@ -16 +16 @@ +-- _ = x.F //@rename("F", "K", FToK) +-+ _ = x.K //@rename("F", "K", FToK) +-@@ -20 +20 @@ +-- _ = y.F +-+ _ = y.K +--- @GToH/generics.go -- +-@@ -3 +3 @@ +--type G[P any] struct { +-+type H[P any] struct { +-@@ -7 +7 @@ +--func (G[_]) M() {} +-+func (H[_]) M() {} +-@@ -15 +15 @@ +-- var x G[int] //@rename("G", "H", GToH) +-+ var x H[int] //@rename("G", "H", GToH) +-@@ -19 +19 @@ +-- var y G[string] +-+ var y H[string] +--- @MToN/generics.go -- +-@@ -7 +7 @@ +--func (G[_]) M() {} +-+func (G[_]) N() {} +-@@ -17 +17 @@ +-- x.M() //@rename("M", "N", MToN) +-+ x.N() //@rename("M", "N", MToN) +-@@ -21 +21 @@ +-- y.M() +-+ y.N() +--- @PToQ/generics.go -- +-@@ -9,2 +9,2 @@ +--func F[P any](P) { +-- var p P //@rename("P", "Q", PToQ) +-+func F[Q any](Q) { +-+ var p Q //@rename("P", "Q", PToQ) +--- unions.go -- +-package a - --import "example.com/blah" +-type T string //@rename("T", "R", TToR) - --func main() { -- println(blah.Name) +-type C interface { +- T | ~int //@rename("T", "S", TToS) -} --` - -- runner := RunMultiple{ -- {"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, -- {"nested", WithOptions(ProxyFiles(proxy))}, -- } +--- @TToR/unions.go -- +-@@ -3 +3 @@ +--type T string //@rename("T", "R", TToR) +-+type R string //@rename("T", "R", TToR) +-@@ -6 +6 @@ +-- T | ~int //@rename("T", "S", TToS) +-+ R | ~int //@rename("T", "S", TToS) +--- @TToS/unions.go -- +-@@ -3 +3 @@ +--type T string //@rename("T", "R", TToR) +-+type S string //@rename("T", "R", TToR) +-@@ -6 +6 @@ +-- T | ~int //@rename("T", "S", TToS) +-+ S | ~int //@rename("T", "S", TToS) +diff -urN a/gopls/internal/test/marker/testdata/rename/generics.txt b/gopls/internal/test/marker/testdata/rename/generics.txt +--- a/gopls/internal/test/marker/testdata/rename/generics.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/rename/generics.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,185 +0,0 @@ +-This test exercises various renaming features on generic code. - -- t.Run("basic", func(t *testing.T) { -- runner.Run(t, untidyModule, func(t *testing.T, env *Env) { -- // Open the file and make sure that the initial workspace load does not -- // modify the go.mod file. -- goModContent := env.ReadWorkspaceFile("a/go.mod") -- env.OpenFile("a/main.go") -- env.AfterChange( -- Diagnostics(env.AtRegexp("a/main.go", "\"example.com/blah\"")), -- ) -- if got := env.ReadWorkspaceFile("a/go.mod"); got != goModContent { -- t.Fatalf("go.mod changed on disk:\n%s", compare.Text(goModContent, got)) -- } -- // Save the buffer, which will format and organize imports. -- // Confirm that the go.mod file still does not change. -- env.SaveBuffer("a/main.go") -- env.AfterChange( -- Diagnostics(env.AtRegexp("a/main.go", "\"example.com/blah\"")), -- ) -- if got := env.ReadWorkspaceFile("a/go.mod"); got != goModContent { -- t.Fatalf("go.mod changed on disk:\n%s", compare.Text(goModContent, got)) -- } -- }) -- }) +-Fixed bugs: - -- // Reproduce golang/go#40269 by deleting and recreating main.go. -- t.Run("delete main.go", func(t *testing.T) { -- runner.Run(t, untidyModule, func(t *testing.T, env *Env) { -- goModContent := env.ReadWorkspaceFile("a/go.mod") -- mainContent := env.ReadWorkspaceFile("a/main.go") -- env.OpenFile("a/main.go") -- env.SaveBuffer("a/main.go") +-- golang/go#61614: renaming a method of a type in a package that uses type +- parameter composite lits used to panic, because previous iterations of the +- satisfy analysis did not account for this language feature. - -- // Ensure that we're done processing all the changes caused by opening -- // and saving above. If not, we may run into a file locking issue on -- // windows. -- // -- // If this proves insufficient, env.RemoveWorkspaceFile can be updated to -- // retry file lock errors on windows. -- env.AfterChange() -- env.RemoveWorkspaceFile("a/main.go") +-- golang/go#61635: renaming type parameters did not work when they were +- capitalized and the package was imported by another package. - -- // TODO(rfindley): awaiting here shouldn't really be necessary. We should -- // be consistent eventually. -- // -- // Probably this was meant to exercise a race with the change below. -- env.AfterChange() +--- go.mod -- +-module example.com +-go 1.20 - -- env.WriteWorkspaceFile("a/main.go", mainContent) -- env.AfterChange( -- Diagnostics(env.AtRegexp("a/main.go", "\"example.com/blah\"")), -- ) -- if got := env.ReadWorkspaceFile("a/go.mod"); got != goModContent { -- t.Fatalf("go.mod changed on disk:\n%s", compare.Text(goModContent, got)) -- } -- }) -- }) --} +--- a.go -- +-package a - --func TestGoGetFix(t *testing.T) { -- const mod = ` ---- a/go.mod -- --module mod.com +-type I int - --go 1.12 +-func (I) m() {} //@rename("m", "M", mToM) - ---- a/main.go -- --package main +-func _[P ~[]int]() { +- _ = P{} +-} - --import "example.com/blah" +--- @mToM/a.go -- +-@@ -5 +5 @@ +--func (I) m() {} //@rename("m", "M", mToM) +-+func (I) M() {} //@rename("m", "M", mToM) +--- g.go -- +-package a - --var _ = blah.Name --` +-type S[P any] struct { //@rename("P", "Q", PToQ) +- P P +- F func(P) P +-} - -- const want = `module mod.com +-func F[R any](r R) { +- var _ R //@rename("R", "S", RToS) +-} - --go 1.12 +--- @PToQ/g.go -- +-@@ -3,3 +3,3 @@ +--type S[P any] struct { //@rename("P", "Q", PToQ) +-- P P +-- F func(P) P +-+type S[Q any] struct { //@rename("P", "Q", PToQ) +-+ P Q +-+ F func(Q) Q +--- @RToS/g.go -- +-@@ -8,2 +8,2 @@ +--func F[R any](r R) { +-- var _ R //@rename("R", "S", RToS) +-+func F[S any](r S) { +-+ var _ S //@rename("R", "S", RToS) +--- issue61635/p.go -- +-package issue61635 - --require example.com v1.2.3 --` +-type builder[S ~[]F, F ~string] struct { //@rename("S", "T", SToT) +- name string +- elements S +- elemData map[F][]ElemData[F] +- // other fields... +-} - -- RunMultiple{ -- {"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, -- {"nested", WithOptions(ProxyFiles(proxy))}, -- }.Run(t, mod, func(t *testing.T, env *Env) { -- if strings.Contains(t.Name(), "workspace_module") { -- t.Skip("workspace module mode doesn't set -mod=readonly") -- } -- env.OpenFile("a/main.go") -- var d protocol.PublishDiagnosticsParams -- env.AfterChange( -- Diagnostics(env.AtRegexp("a/main.go", `"example.com/blah"`)), -- ReadDiagnostics("a/main.go", &d), -- ) -- var goGetDiag protocol.Diagnostic -- for _, diag := range d.Diagnostics { -- if strings.Contains(diag.Message, "could not import") { -- goGetDiag = diag -- } -- } -- env.ApplyQuickFixes("a/main.go", []protocol.Diagnostic{goGetDiag}) -- if got := env.ReadWorkspaceFile("a/go.mod"); got != want { -- t.Fatalf("unexpected go.mod content:\n%s", compare.Text(want, got)) -- } -- }) +-type ElemData[F ~string] struct { +- Name F +- // other fields... -} - --// Tests that multiple missing dependencies gives good single fixes. --func TestMissingDependencyFixes(t *testing.T) { -- const mod = ` ---- a/go.mod -- --module mod.com +-type BuilderImpl[S ~[]F, F ~string] struct{ builder[S, F] } - --go 1.12 +--- importer/i.go -- +-package importer - ---- a/main.go -- --package main +-import "example.com/issue61635" // importing is necessary to repro golang/go#61635 - --import "example.com/blah" --import "random.org/blah" +-var _ issue61635.ElemData[string] - --var _, _ = blah.Name, hello.Name --` +--- @SToT/issue61635/p.go -- +-@@ -3 +3 @@ +--type builder[S ~[]F, F ~string] struct { //@rename("S", "T", SToT) +-+type builder[T ~[]F, F ~string] struct { //@rename("S", "T", SToT) +-@@ -5 +5 @@ +-- elements S +-+ elements T +--- instances/type.go -- +-package instances - -- const want = `module mod.com +-type R[P any] struct { //@rename("R", "u", Rtou) +- Next *R[P] //@rename("R", "s", RTos) +-} - --go 1.12 +-func (rv R[P]) Do(R[P]) R[P] { //@rename("Do", "Do1", DoToDo1) +- var x R[P] +- return rv.Do(x) //@rename("Do", "Do2", DoToDo2) +-} - --require random.org v1.2.3 --` +-func _() { +- var x R[int] //@rename("R", "r", RTor) +- x = x.Do(x) +-} - -- RunMultiple{ -- {"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, -- {"nested", WithOptions(ProxyFiles(proxy))}, -- }.Run(t, mod, func(t *testing.T, env *Env) { -- env.OpenFile("a/main.go") -- var d protocol.PublishDiagnosticsParams -- env.AfterChange( -- Diagnostics(env.AtRegexp("a/main.go", `"random.org/blah"`)), -- ReadDiagnostics("a/main.go", &d), -- ) -- var randomDiag protocol.Diagnostic -- for _, diag := range d.Diagnostics { -- if strings.Contains(diag.Message, "random.org") { -- randomDiag = diag -- } -- } -- env.ApplyQuickFixes("a/main.go", []protocol.Diagnostic{randomDiag}) -- if got := env.ReadWorkspaceFile("a/go.mod"); got != want { -- t.Fatalf("unexpected go.mod content:\n%s", compare.Text(want, got)) -- } -- }) +--- @RTos/instances/type.go -- +-@@ -3,2 +3,2 @@ +--type R[P any] struct { //@rename("R", "u", Rtou) +-- Next *R[P] //@rename("R", "s", RTos) +-+type s[P any] struct { //@rename("R", "u", Rtou) +-+ Next *s[P] //@rename("R", "s", RTos) +-@@ -7,2 +7,2 @@ +--func (rv R[P]) Do(R[P]) R[P] { //@rename("Do", "Do1", DoToDo1) +-- var x R[P] +-+func (rv s[P]) Do(s[P]) s[P] { //@rename("Do", "Do1", DoToDo1) +-+ var x s[P] +-@@ -13 +13 @@ +-- var x R[int] //@rename("R", "r", RTor) +-+ var x s[int] //@rename("R", "r", RTor) +--- @Rtou/instances/type.go -- +-@@ -3,2 +3,2 @@ +--type R[P any] struct { //@rename("R", "u", Rtou) +-- Next *R[P] //@rename("R", "s", RTos) +-+type u[P any] struct { //@rename("R", "u", Rtou) +-+ Next *u[P] //@rename("R", "s", RTos) +-@@ -7,2 +7,2 @@ +--func (rv R[P]) Do(R[P]) R[P] { //@rename("Do", "Do1", DoToDo1) +-- var x R[P] +-+func (rv u[P]) Do(u[P]) u[P] { //@rename("Do", "Do1", DoToDo1) +-+ var x u[P] +-@@ -13 +13 @@ +-- var x R[int] //@rename("R", "r", RTor) +-+ var x u[int] //@rename("R", "r", RTor) +--- @DoToDo1/instances/type.go -- +-@@ -7 +7 @@ +--func (rv R[P]) Do(R[P]) R[P] { //@rename("Do", "Do1", DoToDo1) +-+func (rv R[P]) Do1(R[P]) R[P] { //@rename("Do", "Do1", DoToDo1) +-@@ -9 +9 @@ +-- return rv.Do(x) //@rename("Do", "Do2", DoToDo2) +-+ return rv.Do1(x) //@rename("Do", "Do2", DoToDo2) +-@@ -14 +14 @@ +-- x = x.Do(x) +-+ x = x.Do1(x) +--- @DoToDo2/instances/type.go -- +-@@ -7 +7 @@ +--func (rv R[P]) Do(R[P]) R[P] { //@rename("Do", "Do1", DoToDo1) +-+func (rv R[P]) Do2(R[P]) R[P] { //@rename("Do", "Do1", DoToDo1) +-@@ -9 +9 @@ +-- return rv.Do(x) //@rename("Do", "Do2", DoToDo2) +-+ return rv.Do2(x) //@rename("Do", "Do2", DoToDo2) +-@@ -14 +14 @@ +-- x = x.Do(x) +-+ x = x.Do2(x) +--- instances/func.go -- +-package instances +- +-func Foo[P any](p P) { //@rename("Foo", "Bar", FooToBar) +- Foo(p) //@rename("Foo", "Baz", FooToBaz) -} - --// Tests that multiple missing dependencies gives good single fixes. --func TestMissingDependencyFixesWithGoWork(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) -- const mod = ` ---- go.work -- --go 1.18 +--- @FooToBar/instances/func.go -- +-@@ -3,2 +3,2 @@ +--func Foo[P any](p P) { //@rename("Foo", "Bar", FooToBar) +-- Foo(p) //@rename("Foo", "Baz", FooToBaz) +-+func Bar[P any](p P) { //@rename("Foo", "Bar", FooToBar) +-+ Bar(p) //@rename("Foo", "Baz", FooToBaz) +--- @FooToBaz/instances/func.go -- +-@@ -3,2 +3,2 @@ +--func Foo[P any](p P) { //@rename("Foo", "Bar", FooToBar) +-- Foo(p) //@rename("Foo", "Baz", FooToBaz) +-+func Baz[P any](p P) { //@rename("Foo", "Bar", FooToBar) +-+ Baz(p) //@rename("Foo", "Baz", FooToBaz) +--- @RTor/instances/type.go -- +-@@ -3,2 +3,2 @@ +--type R[P any] struct { //@rename("R", "u", Rtou) +-- Next *R[P] //@rename("R", "s", RTos) +-+type r[P any] struct { //@rename("R", "u", Rtou) +-+ Next *r[P] //@rename("R", "s", RTos) +-@@ -7,2 +7,2 @@ +--func (rv R[P]) Do(R[P]) R[P] { //@rename("Do", "Do1", DoToDo1) +-- var x R[P] +-+func (rv r[P]) Do(r[P]) r[P] { //@rename("Do", "Do1", DoToDo1) +-+ var x r[P] +-@@ -13 +13 @@ +-- var x R[int] //@rename("R", "r", RTor) +-+ var x r[int] //@rename("R", "r", RTor) +diff -urN a/gopls/internal/test/marker/testdata/rename/issue39614.txt b/gopls/internal/test/marker/testdata/rename/issue39614.txt +--- a/gopls/internal/test/marker/testdata/rename/issue39614.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/rename/issue39614.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,18 +0,0 @@ - --use ( -- ./a --) ---- a/go.mod -- --module mod.com +--- flags -- +--ignore_extra_diags - --go 1.12 +--- p.go -- +-package issue39614 - ---- a/main.go -- --package main +-func fn() { +- var foo bool //@rename("foo", "bar", fooTobar) +- make(map[string]bool +- if true { +- } +-} - --import "example.com/blah" --import "random.org/blah" +--- @fooTobar/p.go -- +-@@ -4 +4 @@ +-- var foo bool //@rename("foo", "bar", fooTobar) +-+ var bar bool //@rename("foo", "bar", fooTobar) +diff -urN a/gopls/internal/test/marker/testdata/rename/issue42134.txt b/gopls/internal/test/marker/testdata/rename/issue42134.txt +--- a/gopls/internal/test/marker/testdata/rename/issue42134.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/rename/issue42134.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,80 +0,0 @@ +-Regression test for #42134, +-"rename fails to update doc comment for local variable of function type" - --var _, _ = blah.Name, hello.Name --` +--- 1.go -- +-package issue42134 - -- const want = `module mod.com +-func _() { +- // foo computes things. +- foo := func() {} - --go 1.12 +- foo() //@rename("foo", "bar", fooTobar) +-} +--- @fooTobar/1.go -- +-@@ -4,2 +4,2 @@ +-- // foo computes things. +-- foo := func() {} +-+ // bar computes things. +-+ bar := func() {} +-@@ -7 +7 @@ +-- foo() //@rename("foo", "bar", fooTobar) +-+ bar() //@rename("foo", "bar", fooTobar) +--- 2.go -- +-package issue42134 - --require random.org v1.2.3 --` +-import "fmt" - -- RunMultiple{ -- {"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, -- {"nested", WithOptions(ProxyFiles(proxy))}, -- }.Run(t, mod, func(t *testing.T, env *Env) { -- env.OpenFile("a/main.go") -- var d protocol.PublishDiagnosticsParams -- env.AfterChange( -- Diagnostics(env.AtRegexp("a/main.go", `"random.org/blah"`)), -- ReadDiagnostics("a/main.go", &d), -- ) -- var randomDiag protocol.Diagnostic -- for _, diag := range d.Diagnostics { -- if strings.Contains(diag.Message, "random.org") { -- randomDiag = diag -- } -- } -- env.ApplyQuickFixes("a/main.go", []protocol.Diagnostic{randomDiag}) -- if got := env.ReadWorkspaceFile("a/go.mod"); got != want { -- t.Fatalf("unexpected go.mod content:\n%s", compare.Text(want, got)) -- } -- }) +-func _() { +- // minNumber is a min number. +- // Second line. +- minNumber := min(1, 2) +- fmt.Println(minNumber) //@rename("minNumber", "res", minNumberTores) -} - --func TestIndirectDependencyFix(t *testing.T) { -- const mod = ` ---- a/go.mod -- --module mod.com +-func min(a, b int) int { return a + b } +--- @minNumberTores/2.go -- +-@@ -6 +6 @@ +-- // minNumber is a min number. +-+ // res is a min number. +-@@ -8,2 +8,2 @@ +-- minNumber := min(1, 2) +-- fmt.Println(minNumber) //@rename("minNumber", "res", minNumberTores) +-+ res := min(1, 2) +-+ fmt.Println(res) //@rename("minNumber", "res", minNumberTores) +--- 3.go -- +-package issue42134 - --go 1.12 +-func _() { +- /* +- tests contains test cases +- */ +- tests := []struct { //@rename("tests", "testCases", testsTotestCases) +- in, out string +- }{} +- _ = tests +-} +--- @testsTotestCases/3.go -- +-@@ -5 +5 @@ +-- tests contains test cases +-+ testCases contains test cases +-@@ -7 +7 @@ +-- tests := []struct { //@rename("tests", "testCases", testsTotestCases) +-+ testCases := []struct { //@rename("tests", "testCases", testsTotestCases) +-@@ -10 +10 @@ +-- _ = tests +-+ _ = testCases +--- 4.go -- +-package issue42134 - --require example.com v1.2.3 // indirect ---- a/go.sum -- --example.com v1.2.3 h1:ihBTGWGjTU3V4ZJ9OmHITkU9WQ4lGdQkMjgyLFk0FaY= --example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= ---- a/main.go -- --package main +-func _() { +- // a is equal to 5. Comment must stay the same - --import "example.com/blah" +- a := 5 +- _ = a //@rename("a", "b", aTob) +-} +--- @aTob/4.go -- +-@@ -6,2 +6,2 @@ +-- a := 5 +-- _ = a //@rename("a", "b", aTob) +-+ b := 5 +-+ _ = b //@rename("a", "b", aTob) +diff -urN a/gopls/internal/test/marker/testdata/rename/issue43616.txt b/gopls/internal/test/marker/testdata/rename/issue43616.txt +--- a/gopls/internal/test/marker/testdata/rename/issue43616.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/rename/issue43616.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,21 +0,0 @@ +-This test verifies the fix for golang/go#43616: renaming mishandles embedded +-fields. - --func main() { -- fmt.Println(blah.Name) --` -- const want = `module mod.com +--- p.go -- +-package issue43616 - --go 1.12 +-type foo int //@rename("foo", "bar", fooToBar),preparerename("oo","foo","foo") - --require example.com v1.2.3 --` +-var x struct{ foo } //@renameerr("foo", "baz", "rename the type directly") - -- RunMultiple{ -- {"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, -- {"nested", WithOptions(ProxyFiles(proxy))}, -- }.Run(t, mod, func(t *testing.T, env *Env) { -- env.OpenFile("a/go.mod") -- var d protocol.PublishDiagnosticsParams -- env.AfterChange( -- Diagnostics(env.AtRegexp("a/go.mod", "// indirect")), -- ReadDiagnostics("a/go.mod", &d), -- ) -- env.ApplyQuickFixes("a/go.mod", d.Diagnostics) -- if got := env.BufferText("a/go.mod"); got != want { -- t.Fatalf("unexpected go.mod content:\n%s", compare.Text(want, got)) -- } -- }) --} +-var _ = x.foo //@renameerr("foo", "quux", "rename the type directly") +--- @fooToBar/p.go -- +-@@ -3 +3 @@ +--type foo int //@rename("foo", "bar", fooToBar),preparerename("oo","foo","foo") +-+type bar int //@rename("foo", "bar", fooToBar),preparerename("oo","foo","foo") +-@@ -5 +5 @@ +--var x struct{ foo } //@renameerr("foo", "baz", "rename the type directly") +-+var x struct{ bar } //@renameerr("foo", "baz", "rename the type directly") +-@@ -7 +7 @@ +--var _ = x.foo //@renameerr("foo", "quux", "rename the type directly") +-+var _ = x.bar //@renameerr("foo", "quux", "rename the type directly") +diff -urN a/gopls/internal/test/marker/testdata/rename/issue60752.txt b/gopls/internal/test/marker/testdata/rename/issue60752.txt +--- a/gopls/internal/test/marker/testdata/rename/issue60752.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/rename/issue60752.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,57 +0,0 @@ - --// Test to reproduce golang/go#39041. It adds a new require to a go.mod file --// that already has an unused require. --func TestNewDepWithUnusedDep(t *testing.T) { +-This test renames a receiver, type parameter, parameter or result var +-whose name matches a package-level decl. Prior to go1.22, this used to +-cause a spurious shadowing error because of an edge case in the +-behavior of types.Scope for function parameters and results. - -- const proxy = ` ---- github.com/esimov/caire@v1.2.5/go.mod -- --module github.com/esimov/caire +-This is a regression test for issue #60752, a bug in the type checker. - --go 1.12 ---- github.com/esimov/caire@v1.2.5/caire.go -- --package caire +--- flags -- +--min_go=go1.22 +- +--- go.mod -- +-module example.com +-go 1.18 - --func RemoveTempImage() {} ---- google.golang.org/protobuf@v1.20.0/go.mod -- --module google.golang.org/protobuf +--- a/type.go -- +-package a - --go 1.12 ---- google.golang.org/protobuf@v1.20.0/hello/hello.go -- --package hello --` -- const repro = ` ---- a/go.mod -- --module mod.com +-type t int - --go 1.14 +--- a/recv.go -- +-package a - --require google.golang.org/protobuf v1.20.0 ---- a/go.sum -- --github.com/esimov/caire v1.2.5 h1:OcqDII/BYxcBYj3DuwDKjd+ANhRxRqLa2n69EGje7qw= --github.com/esimov/caire v1.2.5/go.mod h1:mXnjRjg3+WUtuhfSC1rKRmdZU9vJZyS1ZWU0qSvJhK8= --google.golang.org/protobuf v1.20.0 h1:y9T1vAtFKQg0faFNMOxJU7WuEqPWolVkjIkU6aI8qCY= --google.golang.org/protobuf v1.20.0/go.mod h1:FcqsytGClbtLv1ot8NvsJHjBi0h22StKVP+K/j2liKA= ---- a/main.go -- --package main +-func (v t) _() {} //@ rename("v", "t", recv) - --import ( -- "github.com/esimov/caire" --) +--- a/param.go -- +-package a - --func _() { -- caire.RemoveTempImage() --}` +-func _(v t) {} //@ rename("v", "t", param) - -- RunMultiple{ -- {"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, -- {"nested", WithOptions(ProxyFiles(proxy))}, -- }.Run(t, repro, func(t *testing.T, env *Env) { -- env.OpenFile("a/main.go") -- var d protocol.PublishDiagnosticsParams -- env.AfterChange( -- Diagnostics(env.AtRegexp("a/main.go", `"github.com/esimov/caire"`)), -- ReadDiagnostics("a/main.go", &d), -- ) -- env.ApplyQuickFixes("a/main.go", d.Diagnostics) -- want := `module mod.com +--- a/result.go -- +-package a - --go 1.14 +-func _() (v t) { return } //@ rename("v", "t", result) - --require ( -- github.com/esimov/caire v1.2.5 -- google.golang.org/protobuf v1.20.0 --) --` -- if got := env.ReadWorkspaceFile("a/go.mod"); got != want { -- t.Fatalf("TestNewDepWithUnusedDep failed:\n%s", compare.Text(want, got)) -- } -- }) --} +--- a/typeparam.go -- +-package a - --// TODO: For this test to be effective, the sandbox's file watcher must respect --// the file watching GlobPattern in the capability registration. See --// golang/go#39384. --func TestModuleChangesOnDisk(t *testing.T) { -- const mod = ` ---- a/go.mod -- --module mod.com +-func _[v t]() {} //@ renameerr("v", "t", re"would shadow (.|\n)*type.go:3:6") - --go 1.12 +--- b/b.go -- +-package b - --require example.com v1.2.3 ---- a/go.sum -- --example.com v1.2.3 h1:ihBTGWGjTU3V4ZJ9OmHITkU9WQ4lGdQkMjgyLFk0FaY= --example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= ---- a/main.go -- --package main +-import _ "example.com/a" - --func main() { -- fmt.Println(blah.Name) --` -- RunMultiple{ -- {"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, -- {"nested", WithOptions(ProxyFiles(proxy))}, -- }.Run(t, mod, func(t *testing.T, env *Env) { -- env.OnceMet( -- InitialWorkspaceLoad, -- Diagnostics(env.AtRegexp("a/go.mod", "require")), -- ) -- env.RunGoCommandInDir("a", "mod", "tidy") -- env.AfterChange( -- NoDiagnostics(ForFile("a/go.mod")), -- ) -- }) --} +--- @param/a/param.go -- +-@@ -3 +3 @@ +--func _(v t) {} //@ rename("v", "t", param) +-+func _(t t) {} //@ rename("v", "t", param) +--- @recv/a/recv.go -- +-@@ -3 +3 @@ +--func (v t) _() {} //@ rename("v", "t", recv) +-+func (t t) _() {} //@ rename("v", "t", recv) +--- @result/a/result.go -- +-@@ -3 +3 @@ +--func _() (v t) { return } //@ rename("v", "t", result) +-+func _() (t t) { return } //@ rename("v", "t", result) +diff -urN a/gopls/internal/test/marker/testdata/rename/issue60789.txt b/gopls/internal/test/marker/testdata/rename/issue60789.txt +--- a/gopls/internal/test/marker/testdata/rename/issue60789.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/rename/issue60789.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,35 +0,0 @@ - --// Tests golang/go#39784: a missing indirect dependency, necessary --// due to blah@v2.0.0's incomplete go.mod file. --func TestBadlyVersionedModule(t *testing.T) { -- const proxy = ` ---- example.com/blah/@v/v1.0.0.mod -- --module example.com +-This test renames an exported method of an unexported type, +-which is an edge case for objectpath, since it computes a path +-from a syntax package that is no good when applied to an +-export data package. - --go 1.12 ---- example.com/blah@v1.0.0/blah.go -- --package blah +-See issue #60789. - --const Name = "Blah" ---- example.com/blah/v2/@v/v2.0.0.mod -- +--- go.mod -- -module example.com -- -go 1.12 ---- example.com/blah/v2@v2.0.0/blah.go -- --package blah - --import "example.com/blah" +--- a/a.go -- +-package a - --var V1Name = blah.Name --const Name = "Blah" --` -- const files = ` ---- a/go.mod -- --module mod.com +-type unexported int +-func (unexported) F() {} //@rename("F", "G", fToG) - --go 1.12 +-var _ = unexported(0).F - --require example.com/blah/v2 v2.0.0 ---- a/go.sum -- --example.com/blah v1.0.0 h1:kGPlWJbMsn1P31H9xp/q2mYI32cxLnCvauHN0AVaHnc= --example.com/blah v1.0.0/go.mod h1:PZUQaGFeVjyDmAE8ywmLbmDn3fj4Ws8epg4oLuDzW3M= --example.com/blah/v2 v2.0.0 h1:DNPsFPkKtTdxclRheaMCiYAoYizp6PuBzO0OmLOO0pY= --example.com/blah/v2 v2.0.0/go.mod h1:UZiKbTwobERo/hrqFLvIQlJwQZQGxWMVY4xere8mj7w= ---- a/main.go -- --package main +--- b/b.go -- +-package b - --import "example.com/blah/v2" +-// The existence of this package is sufficient to exercise +-// the bug even though it cannot reference a.unexported. - --var _ = blah.Name --` -- RunMultiple{ -- {"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, -- {"nested", WithOptions(ProxyFiles(proxy))}, -- }.Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("a/main.go") -- env.OpenFile("a/go.mod") -- var modDiags protocol.PublishDiagnosticsParams -- env.AfterChange( -- // We would like for the error to appear in the v2 module, but -- // as of writing non-workspace packages are not diagnosed. -- Diagnostics(env.AtRegexp("a/main.go", `"example.com/blah/v2"`), WithMessage("cannot find module providing")), -- Diagnostics(env.AtRegexp("a/go.mod", `require example.com/blah/v2`), WithMessage("cannot find module providing")), -- ReadDiagnostics("a/go.mod", &modDiags), -- ) +-import _ "example.com/a" - -- env.ApplyQuickFixes("a/go.mod", modDiags.Diagnostics) -- const want = `module mod.com +--- @fToG/a/a.go -- +-@@ -4 +4 @@ +--func (unexported) F() {} //@rename("F", "G", fToG) +-+func (unexported) G() {} //@rename("F", "G", fToG) +-@@ -6 +6 @@ +--var _ = unexported(0).F +-+var _ = unexported(0).G +diff -urN a/gopls/internal/test/marker/testdata/rename/issue61294.txt b/gopls/internal/test/marker/testdata/rename/issue61294.txt +--- a/gopls/internal/test/marker/testdata/rename/issue61294.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/rename/issue61294.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,26 +0,0 @@ - --go 1.12 +-This test renames a parameter var whose name is the same as a +-package-level var, which revealed a bug in isLocal. - --require ( -- example.com/blah v1.0.0 // indirect -- example.com/blah/v2 v2.0.0 --) --` -- env.SaveBuffer("a/go.mod") -- env.AfterChange(NoDiagnostics(ForFile("a/main.go"))) -- if got := env.BufferText("a/go.mod"); got != want { -- t.Fatalf("suggested fixes failed:\n%s", compare.Text(want, got)) -- } -- }) --} +-This is a regression test for issue #61294. - --// Reproduces golang/go#38232. --func TestUnknownRevision(t *testing.T) { -- if runtime.GOOS == "plan9" { -- t.Skipf("skipping test that fails for unknown reasons on plan9; see https://go.dev/issue/50477") -- } -- const unknown = ` ---- a/go.mod -- --module mod.com +--- go.mod -- +-module example.com +-go 1.18 - --require ( -- example.com v1.2.2 --) ---- a/main.go -- --package main +--- a/a.go -- +-package a - --import "example.com/blah" +-func One() - --func main() { -- var x = blah.Name --} --` +-func Two(One int) //@rename("One", "Three", OneToThree) - -- runner := RunMultiple{ -- {"default", WithOptions(ProxyFiles(proxy), WorkspaceFolders("a"))}, -- {"nested", WithOptions(ProxyFiles(proxy))}, -- } -- // Start from a bad state/bad IWL, and confirm that we recover. -- t.Run("bad", func(t *testing.T) { -- runner.Run(t, unknown, func(t *testing.T, env *Env) { -- env.OpenFile("a/go.mod") -- env.AfterChange( -- Diagnostics(env.AtRegexp("a/go.mod", "example.com v1.2.2")), -- ) -- env.RegexpReplace("a/go.mod", "v1.2.2", "v1.2.3") -- env.SaveBuffer("a/go.mod") // Save to trigger diagnostics. +--- b/b.go -- +-package b - -- d := protocol.PublishDiagnosticsParams{} -- env.AfterChange( -- // Make sure the diagnostic mentions the new version -- the old diagnostic is in the same place. -- Diagnostics(env.AtRegexp("a/go.mod", "example.com v1.2.3"), WithMessage("example.com@v1.2.3")), -- ReadDiagnostics("a/go.mod", &d), -- ) -- qfs := env.GetQuickFixes("a/go.mod", d.Diagnostics) -- if len(qfs) == 0 { -- t.Fatalf("got 0 code actions to fix %v, wanted at least 1", d.Diagnostics) -- } -- env.ApplyCodeAction(qfs[0]) // Arbitrarily pick a single fix to apply. Applying all of them seems to cause trouble in this particular test. -- env.SaveBuffer("a/go.mod") // Save to trigger diagnostics. -- env.AfterChange( -- NoDiagnostics(ForFile("a/go.mod")), -- Diagnostics(env.AtRegexp("a/main.go", "x = ")), -- ) -- }) -- }) +-import _ "example.com/a" - -- const known = ` ---- a/go.mod -- --module mod.com +--- @OneToThree/a/a.go -- +-@@ -5 +5 @@ +--func Two(One int) //@rename("One", "Three", OneToThree) +-+func Two(Three int) //@rename("One", "Three", OneToThree) +diff -urN a/gopls/internal/test/marker/testdata/rename/issue61640.txt b/gopls/internal/test/marker/testdata/rename/issue61640.txt +--- a/gopls/internal/test/marker/testdata/rename/issue61640.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/rename/issue61640.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,33 +0,0 @@ +-This test verifies that gopls can rename instantiated fields. - --require ( -- example.com v1.2.3 --) ---- a/go.sum -- --example.com v1.2.3 h1:ihBTGWGjTU3V4ZJ9OmHITkU9WQ4lGdQkMjgyLFk0FaY= --example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= ---- a/main.go -- --package main +--- a.go -- +-package a - --import "example.com/blah" +-// This file is adapted from the example in the issue. - --func main() { -- var x = blah.Name --} --` -- // Start from a good state, transform to a bad state, and confirm that we -- // still recover. -- t.Run("good", func(t *testing.T) { -- runner.Run(t, known, func(t *testing.T, env *Env) { -- env.OpenFile("a/go.mod") -- env.AfterChange( -- Diagnostics(env.AtRegexp("a/main.go", "x = ")), -- ) -- env.RegexpReplace("a/go.mod", "v1.2.3", "v1.2.2") -- env.Editor.SaveBuffer(env.Ctx, "a/go.mod") // go.mod changes must be on disk -- env.AfterChange( -- Diagnostics(env.AtRegexp("a/go.mod", "example.com v1.2.2")), -- ) -- env.RegexpReplace("a/go.mod", "v1.2.2", "v1.2.3") -- env.Editor.SaveBuffer(env.Ctx, "a/go.mod") // go.mod changes must be on disk -- env.AfterChange( -- Diagnostics(env.AtRegexp("a/main.go", "x = ")), -- ) -- }) -- }) +-type builder[S ~[]int] struct { +- elements S //@rename("elements", "elements2", OneToTwo) -} - --// Confirm that an error in an indirect dependency of a requirement is surfaced --// as a diagnostic in the go.mod file. --func TestErrorInIndirectDependency(t *testing.T) { -- const badProxy = ` ---- example.com@v1.2.3/go.mod -- --module example.com +-type BuilderImpl[S ~[]int] struct{ builder[S] } - --go 1.12 +-func NewBuilderImpl[S ~[]int](name string) *BuilderImpl[S] { +- impl := &BuilderImpl[S]{ +- builder[S]{ +- elements: S{}, +- }, +- } - --require random.org v1.2.3 // indirect ---- example.com@v1.2.3/blah/blah.go -- --package blah +- _ = impl.elements +- return impl +-} +--- @OneToTwo/a.go -- +-@@ -6 +6 @@ +-- elements S //@rename("elements", "elements2", OneToTwo) +-+ elements2 S //@rename("elements", "elements2", OneToTwo) +-@@ -14 +14 @@ +-- elements: S{}, +-+ elements2: S{}, +-@@ -18 +18 @@ +-- _ = impl.elements +-+ _ = impl.elements2 +diff -urN a/gopls/internal/test/marker/testdata/rename/issue61813.txt b/gopls/internal/test/marker/testdata/rename/issue61813.txt +--- a/gopls/internal/test/marker/testdata/rename/issue61813.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/rename/issue61813.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,14 +0,0 @@ +-This test exercises the panic reported in golang/go#61813. - --const Name = "Blah" ---- random.org@v1.2.3/go.mod -- --module bob.org +--- p.go -- +-package p - --go 1.12 ---- random.org@v1.2.3/blah/blah.go -- --package hello +-type P struct{} - --const Name = "Hello" --` -- const module = ` ---- a/go.mod -- --module mod.com +-func (P) M() {} //@rename("M", "N", MToN) - --go 1.14 +-var x = []*P{{}} +--- @MToN/p.go -- +-@@ -5 +5 @@ +--func (P) M() {} //@rename("M", "N", MToN) +-+func (P) N() {} //@rename("M", "N", MToN) +diff -urN a/gopls/internal/test/marker/testdata/rename/methods.txt b/gopls/internal/test/marker/testdata/rename/methods.txt +--- a/gopls/internal/test/marker/testdata/rename/methods.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/rename/methods.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,57 +0,0 @@ +-This test exercises renaming of interface methods. - --require example.com v1.2.3 ---- a/main.go -- --package main +-The golden is currently wrong due to https://github.com/golang/go/issues/58506: +-the reference to B.F in package b should be renamed too. - --import "example.com/blah" +--- go.mod -- +-module example.com +-go 1.12 - --func main() { -- println(blah.Name) --} --` -- RunMultiple{ -- {"default", WithOptions(ProxyFiles(badProxy), WorkspaceFolders("a"))}, -- {"nested", WithOptions(ProxyFiles(badProxy))}, -- }.Run(t, module, func(t *testing.T, env *Env) { -- env.OpenFile("a/go.mod") -- env.AfterChange( -- Diagnostics(env.AtRegexp("a/go.mod", "require example.com v1.2.3")), -- ) -- }) --} +--- a/a.go -- +-package a - --// A copy of govim's config_set_env_goflags_mod_readonly test. --func TestGovimModReadonly(t *testing.T) { -- const mod = ` ---- go.mod -- --module mod.com +-type A int - --go 1.13 ---- main.go -- --package main +-func (A) F() {} //@renameerr("F", "G", errAfToG) - --import "example.com/blah" +--- b/b.go -- +-package b - --func main() { -- println(blah.Name) --} --` -- WithOptions( -- EnvVars{"GOFLAGS": "-mod=readonly"}, -- ProxyFiles(proxy), -- Modes(Default), -- ).Run(t, mod, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- original := env.ReadWorkspaceFile("go.mod") -- env.AfterChange( -- Diagnostics(env.AtRegexp("main.go", `"example.com/blah"`)), -- ) -- got := env.ReadWorkspaceFile("go.mod") -- if got != original { -- t.Fatalf("go.mod file modified:\n%s", compare.Text(original, got)) -- } -- env.RunGoCommand("get", "example.com/blah@v1.2.3") -- env.RunGoCommand("mod", "tidy") -- env.AfterChange( -- NoDiagnostics(ForFile("main.go")), -- ) -- }) --} +-import "example.com/a" +-import "example.com/c" - --func TestMultiModuleModDiagnostics(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) // uses go.work -- const mod = ` ---- go.work -- --go 1.18 +-type B interface { F() } //@rename("F", "G", BfToG) - --use ( -- a -- b --) ---- a/go.mod -- --module moda.com +-var _ B = a.A(0) +-var _ B = c.C(0) - --go 1.14 +--- c/c.go -- +-package c - --require ( -- example.com v1.2.3 --) ---- a/go.sum -- --example.com v1.2.3 h1:Yryq11hF02fEf2JlOS2eph+ICE2/ceevGV3C9dl5V/c= --example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= ---- a/main.go -- --package main +-type C int - --func main() {} ---- b/go.mod -- --module modb.com +-func (C) F() {} //@renameerr("F", "G", errCfToG) - --require example.com v1.2.3 +--- d/d.go -- +-package d - --go 1.14 ---- b/main.go -- --package main +-import "example.com/b" - --import "example.com/blah" +-var _ = b.B.F - --func main() { -- blah.SaySomething() --} --` -- WithOptions( -- ProxyFiles(workspaceProxy), -- ).Run(t, mod, func(t *testing.T, env *Env) { -- env.AfterChange( -- Diagnostics( -- env.AtRegexp("a/go.mod", "example.com v1.2.3"), -- WithMessage("is not used"), -- ), -- ) -- }) +--- @errAfToG -- +-a/a.go:5:10: renaming this method "F" to "G" +-b/b.go:6:6: would make example.com/a.A no longer assignable to interface B +-b/b.go:6:20: (rename example.com/b.B.F if you intend to change both types) +--- @BfToG/b/b.go -- +-@@ -6 +6 @@ +--type B interface { F() } //@rename("F", "G", BfToG) +-+type B interface { G() } //@rename("F", "G", BfToG) +--- @BfToG/d/d.go -- +-@@ -5 +5 @@ +--var _ = b.B.F +-+var _ = b.B.G +--- @errCfToG -- +-c/c.go:5:10: renaming this method "F" to "G" +-b/b.go:6:6: would make example.com/c.C no longer assignable to interface B +-b/b.go:6:20: (rename example.com/b.B.F if you intend to change both types) +diff -urN a/gopls/internal/test/marker/testdata/rename/prepare.txt b/gopls/internal/test/marker/testdata/rename/prepare.txt +--- a/gopls/internal/test/marker/testdata/rename/prepare.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/rename/prepare.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,62 +0,0 @@ +-This test verifies the behavior of textDocument/prepareRename. +- +--- settings.json -- +-{ +- "deepCompletion": false -} - --func TestModTidyWithBuildTags(t *testing.T) { -- const mod = ` --- go.mod -- --module mod.com -- --go 1.14 ---- main.go -- --// +build bob +-module golang.org/lsptests - --package main +-go 1.18 +--- types/types.go -- +-package types - --import "example.com/blah" +-type CoolAlias = int //@item(CoolAlias, "CoolAlias", "int", "type") - --func main() { -- blah.SaySomething() --} --` -- WithOptions( -- ProxyFiles(workspaceProxy), -- Settings{"buildFlags": []string{"-tags", "bob"}}, -- ).Run(t, mod, func(t *testing.T, env *Env) { -- env.OnceMet( -- InitialWorkspaceLoad, -- Diagnostics(env.AtRegexp("main.go", `"example.com/blah"`)), -- ) -- }) +-type X struct { //@item(X_struct, "X", "struct{...}", "struct") +- x int -} - --func TestModTypoDiagnostic(t *testing.T) { -- const mod = ` ---- go.mod -- --module mod.com +-type Y struct { //@item(Y_struct, "Y", "struct{...}", "struct") +- y int +-} - --go 1.12 ---- main.go -- --package main - --func main() {} --` -- Run(t, mod, func(t *testing.T, env *Env) { -- env.OpenFile("go.mod") -- env.RegexpReplace("go.mod", "module", "modul") -- env.AfterChange( -- Diagnostics(env.AtRegexp("go.mod", "modul")), -- ) -- }) +-type Bob interface { //@item(Bob_interface, "Bob", "interface{...}", "interface") +- Bobby() -} - --func TestSumUpdateFixesDiagnostics(t *testing.T) { -- const mod = ` ---- go.mod -- --module mod.com +-func (*X) Bobby() {} +-func (*Y) Bobby() {} +- +--- good/good0.go -- +-package good - --go 1.12 +-func stuff() { //@item(good_stuff, "stuff", "func()", "func"),preparerename("stu", "stuff", "stuff") +- x := 5 +- random2(x) //@preparerename("dom", "random2", "random2") +-} - --require ( -- example.com v1.2.3 --) ---- go.sum -- ---- main.go -- --package main +--- good/good1.go -- +-package good - -import ( -- "example.com/blah" +- "golang.org/lsptests/types" //@item(types_import, "types", "\"golang.org/lsptests/types\"", "package") -) - --func main() { -- println(blah.Name) +-func random() int { //@item(good_random, "random", "func() int", "func") +- _ = "random() int" //@preparerename("random", "", "") +- y := 6 + 7 //@preparerename("7", "", "") +- return y //@preparerename("return", "","") -} --` -- WithOptions( -- ProxyFiles(workspaceProxy), -- ).Run(t, mod, func(t *testing.T, env *Env) { -- d := &protocol.PublishDiagnosticsParams{} -- env.OpenFile("go.mod") -- env.AfterChange( -- Diagnostics( -- env.AtRegexp("go.mod", `example.com v1.2.3`), -- WithMessage("go.sum is out of sync"), -- ), -- ReadDiagnostics("go.mod", d), -- ) -- env.ApplyQuickFixes("go.mod", d.Diagnostics) -- env.SaveBuffer("go.mod") // Save to trigger diagnostics. -- env.AfterChange( -- NoDiagnostics(ForFile("go.mod")), -- ) -- }) +- +-func random2(y int) int { //@item(good_random2, "random2", "func(y int) int", "func"),item(good_y_param, "y", "int", "var") +- //@complete("", good_y_param, types_import, good_random, good_random2, good_stuff) +- var b types.Bob = &types.X{} //@preparerename("ypes","types", "types") +- if _, ok := b.(*types.X); ok { //@complete("X", X_struct, Y_struct, Bob_interface, CoolAlias) +- _ = 0 // suppress "empty branch" diagnostic +- } +- +- return y -} +diff -urN a/gopls/internal/test/marker/testdata/rename/random.txt b/gopls/internal/test/marker/testdata/rename/random.txt +--- a/gopls/internal/test/marker/testdata/rename/random.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/rename/random.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,238 +0,0 @@ +-This test ports some "random" rename tests from the old marker tests. +- +--- flags -- +--ignore_extra_diags - --// This test confirms that editing a go.mod file only causes metadata --// to be invalidated when it's saved. --func TestGoModInvalidatesOnSave(t *testing.T) { -- const mod = ` --- go.mod -- --module mod.com +-module golang.org/lsptests/rename - --go 1.12 ---- main.go -- --package main +-go 1.18 +--- a/a.go -- +-package a - --func main() { -- hello() +-import ( +- lg "log" +- "fmt" //@rename("fmt", "fmty", fmtTofmty) +- f2 "fmt" //@rename("f2", "f2name", f2Tof2name),rename("fmt", "f2y", fmtTof2y) +-) +- +-func Random() int { +- y := 6 + 7 +- return y -} ---- hello.go -- --package main - --func hello() {} --` -- WithOptions( -- // TODO(rFindley) this doesn't work in multi-module workspace mode, because -- // it keeps around the last parsing modfile. Update this test to also -- // exercise the workspace module. -- Modes(Default), -- ).Run(t, mod, func(t *testing.T, env *Env) { -- env.OpenFile("go.mod") -- env.Await(env.DoneWithOpen()) -- env.RegexpReplace("go.mod", "module", "modul") -- // Confirm that we still have metadata with only on-disk edits. -- env.OpenFile("main.go") -- loc := env.GoToDefinition(env.RegexpSearch("main.go", "hello")) -- if filepath.Base(string(loc.URI)) != "hello.go" { -- t.Fatalf("expected definition in hello.go, got %s", loc.URI) -- } -- // Confirm that we no longer have metadata when the file is saved. -- env.SaveBufferWithoutActions("go.mod") -- _, err := env.Editor.Definition(env.Ctx, env.RegexpSearch("main.go", "hello")) -- if err == nil { -- t.Fatalf("expected error, got none") -- } -- }) +-func Random2(y int) int { //@rename("y", "z", yToz) +- return y -} - --func TestRemoveUnusedDependency(t *testing.T) { -- const proxy = ` ---- hasdep.com@v1.2.3/go.mod -- --module hasdep.com +-type Pos struct { +- x, y int +-} - --go 1.12 +-func (p *Pos) Sum() int { +- return p.x + p.y //@rename("x", "myX", xTomyX) +-} - --require example.com v1.2.3 ---- hasdep.com@v1.2.3/a/a.go -- --package a ---- example.com@v1.2.3/go.mod -- --module example.com +-func _() { +- var p Pos //@rename("p", "pos", pTopos) +- _ = p.Sum() //@rename("Sum", "GetSum", SumToGetSum) +-} - --go 1.12 ---- example.com@v1.2.3/blah/blah.go -- --package blah +-func sw() { +- var x interface{} - --const Name = "Blah" ---- random.com@v1.2.3/go.mod -- --module random.com +- switch y := x.(type) { //@rename("y", "y0", yToy0) +- case int: +- fmt.Printf("%d", y) //@rename("y", "y1", yToy1),rename("fmt", "format", fmtToformat) +- case string: +- lg.Printf("%s", y) //@rename("y", "y2", yToy2),rename("lg", "log", lgTolog) +- default: +- f2.Printf("%v", y) //@rename("y", "y3", yToy3),rename("f2", "fmt2", f2Tofmt2) +- } +-} +--- @SumToGetSum/a/a.go -- +-@@ -22 +22 @@ +--func (p *Pos) Sum() int { +-+func (p *Pos) GetSum() int { +-@@ -28 +28 @@ +-- _ = p.Sum() //@rename("Sum", "GetSum", SumToGetSum) +-+ _ = p.GetSum() //@rename("Sum", "GetSum", SumToGetSum) +--- @f2Tof2name/a/a.go -- +-@@ -6 +6 @@ +-- f2 "fmt" //@rename("f2", "f2name", f2Tof2name),rename("fmt", "f2y", fmtTof2y) +-+ f2name "fmt" //@rename("f2", "f2name", f2Tof2name),rename("fmt", "f2y", fmtTof2y) +-@@ -40 +40 @@ +-- f2.Printf("%v", y) //@rename("y", "y3", yToy3),rename("f2", "fmt2", f2Tofmt2) +-+ f2name.Printf("%v", y) //@rename("y", "y3", yToy3),rename("f2", "fmt2", f2Tofmt2) +--- @f2Tofmt2/a/a.go -- +-@@ -6 +6 @@ +-- f2 "fmt" //@rename("f2", "f2name", f2Tof2name),rename("fmt", "f2y", fmtTof2y) +-+ fmt2 "fmt" //@rename("f2", "f2name", f2Tof2name),rename("fmt", "f2y", fmtTof2y) +-@@ -40 +40 @@ +-- f2.Printf("%v", y) //@rename("y", "y3", yToy3),rename("f2", "fmt2", f2Tofmt2) +-+ fmt2.Printf("%v", y) //@rename("y", "y3", yToy3),rename("f2", "fmt2", f2Tofmt2) +--- @fmtTof2y/a/a.go -- +-@@ -6 +6 @@ +-- f2 "fmt" //@rename("f2", "f2name", f2Tof2name),rename("fmt", "f2y", fmtTof2y) +-+ f2y "fmt" //@rename("f2", "f2name", f2Tof2name),rename("fmt", "f2y", fmtTof2y) +-@@ -40 +40 @@ +-- f2.Printf("%v", y) //@rename("y", "y3", yToy3),rename("f2", "fmt2", f2Tofmt2) +-+ f2y.Printf("%v", y) //@rename("y", "y3", yToy3),rename("f2", "fmt2", f2Tofmt2) +--- @fmtTofmty/a/a.go -- +-@@ -5 +5 @@ +-- "fmt" //@rename("fmt", "fmty", fmtTofmty) +-+ fmty "fmt" //@rename("fmt", "fmty", fmtTofmty) +-@@ -36 +36 @@ +-- fmt.Printf("%d", y) //@rename("y", "y1", yToy1),rename("fmt", "format", fmtToformat) +-+ fmty.Printf("%d", y) //@rename("y", "y1", yToy1),rename("fmt", "format", fmtToformat) +--- @fmtToformat/a/a.go -- +-@@ -5 +5 @@ +-- "fmt" //@rename("fmt", "fmty", fmtTofmty) +-+ format "fmt" //@rename("fmt", "fmty", fmtTofmty) +-@@ -36 +36 @@ +-- fmt.Printf("%d", y) //@rename("y", "y1", yToy1),rename("fmt", "format", fmtToformat) +-+ format.Printf("%d", y) //@rename("y", "y1", yToy1),rename("fmt", "format", fmtToformat) +--- @lgTolog/a/a.go -- +-@@ -4 +4 @@ +-- lg "log" +-+ "log" +-@@ -38 +38 @@ +-- lg.Printf("%s", y) //@rename("y", "y2", yToy2),rename("lg", "log", lgTolog) +-+ log.Printf("%s", y) //@rename("y", "y2", yToy2),rename("lg", "log", lgTolog) +--- @pTopos/a/a.go -- +-@@ -27,2 +27,2 @@ +-- var p Pos //@rename("p", "pos", pTopos) +-- _ = p.Sum() //@rename("Sum", "GetSum", SumToGetSum) +-+ var pos Pos //@rename("p", "pos", pTopos) +-+ _ = pos.Sum() //@rename("Sum", "GetSum", SumToGetSum) +--- @xTomyX/a/a.go -- +-@@ -19 +19 @@ +-- x, y int +-+ myX, y int +-@@ -23 +23 @@ +-- return p.x + p.y //@rename("x", "myX", xTomyX) +-+ return p.myX + p.y //@rename("x", "myX", xTomyX) +--- @yToy0/a/a.go -- +-@@ -34 +34 @@ +-- switch y := x.(type) { //@rename("y", "y0", yToy0) +-+ switch y0 := x.(type) { //@rename("y", "y0", yToy0) +-@@ -36 +36 @@ +-- fmt.Printf("%d", y) //@rename("y", "y1", yToy1),rename("fmt", "format", fmtToformat) +-+ fmt.Printf("%d", y0) //@rename("y", "y1", yToy1),rename("fmt", "format", fmtToformat) +-@@ -38 +38 @@ +-- lg.Printf("%s", y) //@rename("y", "y2", yToy2),rename("lg", "log", lgTolog) +-+ lg.Printf("%s", y0) //@rename("y", "y2", yToy2),rename("lg", "log", lgTolog) +-@@ -40 +40 @@ +-- f2.Printf("%v", y) //@rename("y", "y3", yToy3),rename("f2", "fmt2", f2Tofmt2) +-+ f2.Printf("%v", y0) //@rename("y", "y3", yToy3),rename("f2", "fmt2", f2Tofmt2) +--- @yToy1/a/a.go -- +-@@ -34 +34 @@ +-- switch y := x.(type) { //@rename("y", "y0", yToy0) +-+ switch y1 := x.(type) { //@rename("y", "y0", yToy0) +-@@ -36 +36 @@ +-- fmt.Printf("%d", y) //@rename("y", "y1", yToy1),rename("fmt", "format", fmtToformat) +-+ fmt.Printf("%d", y1) //@rename("y", "y1", yToy1),rename("fmt", "format", fmtToformat) +-@@ -38 +38 @@ +-- lg.Printf("%s", y) //@rename("y", "y2", yToy2),rename("lg", "log", lgTolog) +-+ lg.Printf("%s", y1) //@rename("y", "y2", yToy2),rename("lg", "log", lgTolog) +-@@ -40 +40 @@ +-- f2.Printf("%v", y) //@rename("y", "y3", yToy3),rename("f2", "fmt2", f2Tofmt2) +-+ f2.Printf("%v", y1) //@rename("y", "y3", yToy3),rename("f2", "fmt2", f2Tofmt2) +--- @yToy2/a/a.go -- +-@@ -34 +34 @@ +-- switch y := x.(type) { //@rename("y", "y0", yToy0) +-+ switch y2 := x.(type) { //@rename("y", "y0", yToy0) +-@@ -36 +36 @@ +-- fmt.Printf("%d", y) //@rename("y", "y1", yToy1),rename("fmt", "format", fmtToformat) +-+ fmt.Printf("%d", y2) //@rename("y", "y1", yToy1),rename("fmt", "format", fmtToformat) +-@@ -38 +38 @@ +-- lg.Printf("%s", y) //@rename("y", "y2", yToy2),rename("lg", "log", lgTolog) +-+ lg.Printf("%s", y2) //@rename("y", "y2", yToy2),rename("lg", "log", lgTolog) +-@@ -40 +40 @@ +-- f2.Printf("%v", y) //@rename("y", "y3", yToy3),rename("f2", "fmt2", f2Tofmt2) +-+ f2.Printf("%v", y2) //@rename("y", "y3", yToy3),rename("f2", "fmt2", f2Tofmt2) +--- @yToy3/a/a.go -- +-@@ -34 +34 @@ +-- switch y := x.(type) { //@rename("y", "y0", yToy0) +-+ switch y3 := x.(type) { //@rename("y", "y0", yToy0) +-@@ -36 +36 @@ +-- fmt.Printf("%d", y) //@rename("y", "y1", yToy1),rename("fmt", "format", fmtToformat) +-+ fmt.Printf("%d", y3) //@rename("y", "y1", yToy1),rename("fmt", "format", fmtToformat) +-@@ -38 +38 @@ +-- lg.Printf("%s", y) //@rename("y", "y2", yToy2),rename("lg", "log", lgTolog) +-+ lg.Printf("%s", y3) //@rename("y", "y2", yToy2),rename("lg", "log", lgTolog) +-@@ -40 +40 @@ +-- f2.Printf("%v", y) //@rename("y", "y3", yToy3),rename("f2", "fmt2", f2Tofmt2) +-+ f2.Printf("%v", y3) //@rename("y", "y3", yToy3),rename("f2", "fmt2", f2Tofmt2) +--- @yToz/a/a.go -- +-@@ -14,2 +14,2 @@ +--func Random2(y int) int { //@rename("y", "z", yToz) +-- return y +-+func Random2(z int) int { //@rename("y", "z", yToz) +-+ return z +--- b/b.go -- +-package b - --go 1.12 ---- random.com@v1.2.3/blah/blah.go -- --package blah +-var c int //@renameerr("int", "uint", re"cannot be renamed") - --const Name = "Blah" --` -- t.Run("almost tidied", func(t *testing.T) { -- const mod = ` ---- go.mod -- --module mod.com +-func _() { +- a := 1 //@rename("a", "error", aToerror) +- a = 2 +- _ = a +-} - --go 1.12 +-var ( +- // Hello there. +- // Foo does the thing. +- Foo int //@rename("Foo", "Bob", FooToBob) +-) - --require hasdep.com v1.2.3 ---- go.sum -- --example.com v1.2.3 h1:ihBTGWGjTU3V4ZJ9OmHITkU9WQ4lGdQkMjgyLFk0FaY= --example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= --hasdep.com v1.2.3 h1:00y+N5oD+SpKoqV1zP2VOPawcW65Zb9NebANY3GSzGI= --hasdep.com v1.2.3/go.mod h1:ePVZOlez+KZEOejfLPGL2n4i8qiAjrkhQZ4wcImqAes= ---- main.go -- --package main +-/* +-Hello description +-*/ +-func Hello() {} //@rename("Hello", "Goodbye", HelloToGoodbye) - --func main() {} --` -- WithOptions( -- ProxyFiles(proxy), -- ).Run(t, mod, func(t *testing.T, env *Env) { -- env.OpenFile("go.mod") -- d := &protocol.PublishDiagnosticsParams{} -- env.AfterChange( -- Diagnostics(env.AtRegexp("go.mod", "require hasdep.com v1.2.3")), -- ReadDiagnostics("go.mod", d), -- ) -- const want = `module mod.com +--- c/c.go -- +-package c - --go 1.12 --` -- env.ApplyQuickFixes("go.mod", d.Diagnostics) -- if got := env.BufferText("go.mod"); got != want { -- t.Fatalf("unexpected content in go.mod:\n%s", compare.Text(want, got)) -- } -- }) -- }) +-import "golang.org/lsptests/rename/b" - -- t.Run("not tidied", func(t *testing.T) { -- const mod = ` ---- go.mod -- --module mod.com +-func _() { +- b.Hello() //@rename("Hello", "Goodbye", HelloToGoodbye) +-} - --go 1.12 +--- c/c2.go -- +-package c - --require hasdep.com v1.2.3 --require random.com v1.2.3 ---- go.sum -- --example.com v1.2.3 h1:ihBTGWGjTU3V4ZJ9OmHITkU9WQ4lGdQkMjgyLFk0FaY= --example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= --hasdep.com v1.2.3 h1:00y+N5oD+SpKoqV1zP2VOPawcW65Zb9NebANY3GSzGI= --hasdep.com v1.2.3/go.mod h1:ePVZOlez+KZEOejfLPGL2n4i8qiAjrkhQZ4wcImqAes= --random.com v1.2.3 h1:PzYTykzqqH6+qU0dIgh9iPFbfb4Mm8zNBjWWreRKtx0= --random.com v1.2.3/go.mod h1:8EGj+8a4Hw1clAp8vbaeHAsKE4sbm536FP7nKyXO+qQ= ---- main.go -- --package main +-//go:embed Static/* +-var Static embed.FS //@rename("Static", "static", StaticTostatic) +- +--- @FooToBob/b/b.go -- +-@@ -13,2 +13,2 @@ +-- // Foo does the thing. +-- Foo int //@rename("Foo", "Bob", FooToBob) +-+ // Bob does the thing. +-+ Bob int //@rename("Foo", "Bob", FooToBob) +--- @HelloToGoodbye/b/b.go -- +-@@ -18 +18 @@ +--Hello description +-+Goodbye description +-@@ -20 +20 @@ +--func Hello() {} //@rename("Hello", "Goodbye", HelloToGoodbye) +-+func Goodbye() {} //@rename("Hello", "Goodbye", HelloToGoodbye) +--- @aToerror/b/b.go -- +-@@ -6,3 +6,3 @@ +-- a := 1 //@rename("a", "error", aToerror) +-- a = 2 +-- _ = a +-+ error := 1 //@rename("a", "error", aToerror) +-+ error = 2 +-+ _ = error +--- @HelloToGoodbye/c/c.go -- +-@@ -6 +6 @@ +-- b.Hello() //@rename("Hello", "Goodbye", HelloToGoodbye) +-+ b.Goodbye() //@rename("Hello", "Goodbye", HelloToGoodbye) +--- @StaticTostatic/c/c2.go -- +-@@ -4 +4 @@ +--var Static embed.FS //@rename("Static", "static", StaticTostatic) +-+var static embed.FS //@rename("Static", "static", StaticTostatic) +diff -urN a/gopls/internal/test/marker/testdata/rename/shadow.txt b/gopls/internal/test/marker/testdata/rename/shadow.txt +--- a/gopls/internal/test/marker/testdata/rename/shadow.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/rename/shadow.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,36 +0,0 @@ - --func main() {} --` -- WithOptions( -- ProxyFiles(proxy), -- ).Run(t, mod, func(t *testing.T, env *Env) { -- d := &protocol.PublishDiagnosticsParams{} -- env.OpenFile("go.mod") -- pos := env.RegexpSearch("go.mod", "require hasdep.com v1.2.3").Range.Start -- env.AfterChange( -- Diagnostics(AtPosition("go.mod", pos.Line, pos.Character)), -- ReadDiagnostics("go.mod", d), -- ) -- const want = `module mod.com +--- shadow.go -- +-package shadow - --go 1.12 +-func _() { +- a := true +- b, c, _ := A(), B(), D() //@renameerr("A", "a", re"shadowed"),rename("B", "b", BTob),renameerr("b", "c", re"conflict"),rename("D", "d", DTod) +- d := false +- _, _, _, _ = a, b, c, d +-} - --require random.com v1.2.3 --` -- var diagnostics []protocol.Diagnostic -- for _, d := range d.Diagnostics { -- if d.Range.Start.Line != uint32(pos.Line) { -- continue -- } -- diagnostics = append(diagnostics, d) -- } -- env.ApplyQuickFixes("go.mod", diagnostics) -- if got := env.BufferText("go.mod"); got != want { -- t.Fatalf("unexpected content in go.mod:\n%s", compare.Text(want, got)) -- } -- }) -- }) +-func A() int { +- return 0 -} - --func TestSumUpdateQuickFix(t *testing.T) { -- const mod = ` ---- go.mod -- --module mod.com +-func B() int { +- return 0 +-} - --go 1.12 +-func D() int { +- return 0 +-} +--- @BTob/shadow.go -- +-@@ -5 +5 @@ +-- b, c, _ := A(), B(), D() //@renameerr("A", "a", re"shadowed"),rename("B", "b", BTob),renameerr("b", "c", re"conflict"),rename("D", "d", DTod) +-+ b, c, _ := A(), b(), D() //@renameerr("A", "a", re"shadowed"),rename("B", "b", BTob),renameerr("b", "c", re"conflict"),rename("D", "d", DTod) +-@@ -14 +14 @@ +--func B() int { +-+func b() int { +--- @DTod/shadow.go -- +-@@ -5 +5 @@ +-- b, c, _ := A(), B(), D() //@renameerr("A", "a", re"shadowed"),rename("B", "b", BTob),renameerr("b", "c", re"conflict"),rename("D", "d", DTod) +-+ b, c, _ := A(), B(), d() //@renameerr("A", "a", re"shadowed"),rename("B", "b", BTob),renameerr("b", "c", re"conflict"),rename("D", "d", DTod) +-@@ -18 +18 @@ +--func D() int { +-+func d() int { +diff -urN a/gopls/internal/test/marker/testdata/rename/testy.txt b/gopls/internal/test/marker/testdata/rename/testy.txt +--- a/gopls/internal/test/marker/testdata/rename/testy.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/rename/testy.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,41 +0,0 @@ - --require ( -- example.com v1.2.3 --) ---- go.sum -- ---- main.go -- --package main +--- flags -- +--ignore_extra_diags - --import ( -- "example.com/blah" --) +--- testy.go -- +-package testy - --func main() { -- blah.Hello() --} --` -- WithOptions( -- ProxyFiles(workspaceProxy), -- Modes(Default), -- ).Run(t, mod, func(t *testing.T, env *Env) { -- env.OpenFile("go.mod") -- params := &protocol.PublishDiagnosticsParams{} -- env.AfterChange( -- Diagnostics( -- env.AtRegexp("go.mod", `example.com`), -- WithMessage("go.sum is out of sync"), -- ), -- ReadDiagnostics("go.mod", params), -- ) -- env.ApplyQuickFixes("go.mod", params.Diagnostics) -- const want = `example.com v1.2.3 h1:Yryq11hF02fEf2JlOS2eph+ICE2/ceevGV3C9dl5V/c= --example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= --` -- if got := env.ReadWorkspaceFile("go.sum"); got != want { -- t.Fatalf("unexpected go.sum contents:\n%s", compare.Text(want, got)) -- } -- }) --} +-type tt int //@rename("tt", "testyType", ttTotestyType) - --func TestDownloadDeps(t *testing.T) { -- const proxy = ` ---- example.com@v1.2.3/go.mod -- --module example.com +-func a() { +- foo := 42 //@rename("foo", "bar", fooTobar) +-} +--- testy_test.go -- +-package testy - --go 1.12 +-import "testing" - --require random.org v1.2.3 ---- example.com@v1.2.3/blah/blah.go -- --package blah +-func TestSomething(t *testing.T) { +- var x int //@rename("x", "testyX", xTotestyX) +- a() //@rename("a", "b", aTob) +-} +--- @aTob/testy.go -- +-@@ -5 +5 @@ +--func a() { +-+func b() { +--- @aTob/testy_test.go -- +-@@ -7 +7 @@ +-- a() //@rename("a", "b", aTob) +-+ b() //@rename("a", "b", aTob) +--- @fooTobar/testy.go -- +-@@ -6 +6 @@ +-- foo := 42 //@rename("foo", "bar", fooTobar) +-+ bar := 42 //@rename("foo", "bar", fooTobar) +--- @ttTotestyType/testy.go -- +-@@ -3 +3 @@ +--type tt int //@rename("tt", "testyType", ttTotestyType) +-+type testyType int //@rename("tt", "testyType", ttTotestyType) +--- @xTotestyX/testy_test.go -- +-@@ -6 +6 @@ +-- var x int //@rename("x", "testyX", xTotestyX) +-+ var testyX int //@rename("x", "testyX", xTotestyX) +diff -urN a/gopls/internal/test/marker/testdata/rename/typeswitch.txt b/gopls/internal/test/marker/testdata/rename/typeswitch.txt +--- a/gopls/internal/test/marker/testdata/rename/typeswitch.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/rename/typeswitch.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,24 +0,0 @@ +-This test covers the special case of renaming a type switch var. - --import "random.org/bye" +--- p.go -- +-package p - --func SaySomething() { -- bye.Goodbye() +-func _(x interface{}) { +- switch y := x.(type) { //@rename("y", "z", yToZ) +- case string: +- print(y) //@rename("y", "z", yToZ) +- default: +- print(y) //@rename("y", "z", yToZ) +- } -} ---- random.org@v1.2.3/go.mod -- --module random.org - --go 1.12 ---- random.org@v1.2.3/bye/bye.go -- --package bye +--- @yToZ/p.go -- +-@@ -4 +4 @@ +-- switch y := x.(type) { //@rename("y", "z", yToZ) +-+ switch z := x.(type) { //@rename("y", "z", yToZ) +-@@ -6 +6 @@ +-- print(y) //@rename("y", "z", yToZ) +-+ print(z) //@rename("y", "z", yToZ) +-@@ -8 +8 @@ +-- print(y) //@rename("y", "z", yToZ) +-+ print(z) //@rename("y", "z", yToZ) +diff -urN a/gopls/internal/test/marker/testdata/rename/unexported.txt b/gopls/internal/test/marker/testdata/rename/unexported.txt +--- a/gopls/internal/test/marker/testdata/rename/unexported.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/rename/unexported.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,25 +0,0 @@ - --func Goodbye() { -- println("Bye") --} --` +-This test attempts to rename a.S.X to x, which would make it +-inaccessible from its external test package. The rename tool +-should report an error rather than wrecking the program. +-See issue #59403. - -- const mod = ` --- go.mod -- --module mod.com -- +-module example.com -go 1.12 ---- go.sum -- ---- main.go -- --package main - --import ( -- "example.com/blah" --) +--- a/a.go -- +-package a - --func main() { -- blah.SaySomething() --} --` -- WithOptions( -- ProxyFiles(proxy), -- Modes(Default), -- ).Run(t, mod, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- d := &protocol.PublishDiagnosticsParams{} -- env.AfterChange( -- Diagnostics( -- env.AtRegexp("main.go", `"example.com/blah"`), -- WithMessage(`could not import example.com/blah (no required module provides package "example.com/blah")`), -- ), -- ReadDiagnostics("main.go", d), -- ) -- env.ApplyQuickFixes("main.go", d.Diagnostics) -- env.AfterChange( -- NoDiagnostics(ForFile("main.go")), -- NoDiagnostics(ForFile("go.mod")), -- ) -- }) --} +-var S struct{ X int } //@renameerr("X", "x", oops) - --func TestInvalidGoVersion(t *testing.T) { -- const files = ` ---- go.mod -- --module mod.com +--- a/a_test.go -- +-package a_test - --go foo ---- main.go -- --package main --` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OnceMet( -- InitialWorkspaceLoad, -- Diagnostics(env.AtRegexp("go.mod", `go foo`), WithMessage("invalid go version")), -- ) -- env.WriteWorkspaceFile("go.mod", "module mod.com \n\ngo 1.12\n") -- env.AfterChange(NoDiagnostics(ForFile("go.mod"))) -- }) --} +-import "example.com/a" - --// This is a regression test for a bug in the line-oriented implementation --// of the "apply diffs" operation used by the fake editor. --func TestIssue57627(t *testing.T) { -- const files = ` ---- go.work -- --package main --` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("go.work") -- env.SetBufferContent("go.work", "go 1.18\nuse moda/a") -- env.SaveBuffer("go.work") // doesn't fail -- }) --} +-var Y = a.S.X - --func TestInconsistentMod(t *testing.T) { -- const proxy = ` ---- golang.org/x/mod@v0.7.0/go.mod -- --go 1.20 --module golang.org/x/mod ---- golang.org/x/mod@v0.7.0/a.go -- --package mod --func AutoQuote(string) string { return ""} ---- golang.org/x/mod@v0.9.0/go.mod -- --go 1.20 --module golang.org/x/mod ---- golang.org/x/mod@v0.9.0/a.go -- --package mod --func AutoQuote(string) string { return ""} --` -- const files = ` ---- go.work -- --go 1.20 --use ( -- ./a -- ./b --) +--- @oops -- +-a/a.go:3:15: renaming "X" to "x" would make it unexported +-a/a_test.go:5:13: breaking references from packages such as "example.com/a_test" +diff -urN a/gopls/internal/test/marker/testdata/selectionrange/selectionrange.txt b/gopls/internal/test/marker/testdata/selectionrange/selectionrange.txt +--- a/gopls/internal/test/marker/testdata/selectionrange/selectionrange.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/selectionrange/selectionrange.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,42 +0,0 @@ +-This test checks selection range functionality. - ---- a/go.mod -- --module a.mod.com --go 1.20 --require golang.org/x/mod v0.6.0 // yyy --replace golang.org/x/mod v0.6.0 => golang.org/x/mod v0.7.0 ---- a/main.go -- --package main --import "golang.org/x/mod" --import "fmt" --func main() {fmt.Println(mod.AutoQuote(""))} +--- foo.go -- +-package foo - ---- b/go.mod -- --module b.mod.com --go 1.20 --require golang.org/x/mod v0.9.0 // xxx ---- b/main.go -- --package aaa --import "golang.org/x/mod" --import "fmt" --func main() {fmt.Println(mod.AutoQuote(""))} --var A int +-import "time" - ---- b/c/go.mod -- --module c.b.mod.com --go 1.20 --require b.mod.com v0.4.2 --replace b.mod.com => ../ ---- b/c/main.go -- --package main --import "b.mod.com/aaa" --import "fmt" --func main() {fmt.Println(aaa.A)} --` -- testenv.NeedsGo1Point(t, 18) -- WithOptions( -- ProxyFiles(proxy), -- Modes(Default), -- ).Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("a/go.mod") -- ahints := env.InlayHints("a/go.mod") -- if len(ahints) != 1 { -- t.Errorf("expected exactly one hint, got %d: %#v", len(ahints), ahints) -- } -- env.OpenFile("b/c/go.mod") -- bhints := env.InlayHints("b/c/go.mod") -- if len(bhints) != 0 { -- t.Errorf("expected no hints, got %d: %#v", len(bhints), bhints) -- } -- }) +-func Bar(x, y int, t time.Time) int { +- zs := []int{1, 2, 3} //@selectionrange("1", a) +- +- for _, z := range zs { +- x = x + z + y + zs[1] //@selectionrange("1", b) +- } +- +- return x + y //@selectionrange("+", c) +-} +--- @a -- +-Ranges 0: +- 5:13-5:14 "1" +- 5:7-5:21 "[]int{1, 2, 3}" +- 5:1-5:21 "zs := []int{1, 2, 3}" +- 4:36-12:1 "{\\n\tzs := []int{...range(\"+\", c)\\n}" +- 4:0-12:1 "func Bar(x, y i...range(\"+\", c)\\n}" +- 0:0-12:1 "package foo\\n\\nim...range(\"+\", c)\\n}" +--- @b -- +-Ranges 0: +- 8:21-8:22 "1" +- 8:18-8:23 "zs[1]" +- 8:6-8:23 "x + z + y + zs[1]" +- 8:2-8:23 "x = x + z + y + zs[1]" +- 7:22-9:2 "{\\n\t\tx = x + z +...ange(\"1\", b)\\n\t}" +- 7:1-9:2 "for _, z := ran...ange(\"1\", b)\\n\t}" +- 4:36-12:1 "{\\n\tzs := []int{...range(\"+\", c)\\n}" +- 4:0-12:1 "func Bar(x, y i...range(\"+\", c)\\n}" +- 0:0-12:1 "package foo\\n\\nim...range(\"+\", c)\\n}" +--- @c -- +-Ranges 0: +- 11:8-11:13 "x + y" +- 11:1-11:13 "return x + y" +- 4:36-12:1 "{\\n\tzs := []int{...range(\"+\", c)\\n}" +- 4:0-12:1 "func Bar(x, y i...range(\"+\", c)\\n}" +- 0:0-12:1 "package foo\\n\\nim...range(\"+\", c)\\n}" +diff -urN a/gopls/internal/test/marker/testdata/signature/generic.txt b/gopls/internal/test/marker/testdata/signature/generic.txt +--- a/gopls/internal/test/marker/testdata/signature/generic.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/signature/generic.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,21 +0,0 @@ +-This test checks signature help on generic signatures. - +--- g.go -- +-package g +- +-type M[K comparable, V any] map[K]V +- +-// golang/go#61189: signatureHelp must handle pointer receivers. +-func (m *M[K, V]) Get(k K) V { +- return (*m)[k] -} -diff -urN a/gopls/internal/regtest/modfile/tempmodfile_test.go b/gopls/internal/regtest/modfile/tempmodfile_test.go ---- a/gopls/internal/regtest/modfile/tempmodfile_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/modfile/tempmodfile_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,41 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package modfile +-func Get[K comparable, V any](m M[K, V], k K) V { +- return m[k] +-} - --import ( -- "testing" +-func _() { +- var m M[int, string] +- _ = m.Get(0) //@signature("(", "Get(k int) string", 0) +- _ = Get(m, 0) //@signature("0", "Get(m M[int, string], k int) string", 1) +-} +diff -urN a/gopls/internal/test/marker/testdata/signature/issue63804.txt b/gopls/internal/test/marker/testdata/signature/issue63804.txt +--- a/gopls/internal/test/marker/testdata/signature/issue63804.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/signature/issue63804.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,13 +0,0 @@ +-Regresson test for #63804: conversion to built-in type caused panic. - -- . "golang.org/x/tools/gopls/internal/lsp/regtest" --) +-the server's Signature method never returns an actual error, +-so the best we can assert is that there is no result. - --// This test replaces an older, problematic test (golang/go#57784). But it has --// been a long time since the go command would mutate go.mod files. --// --// TODO(golang/go#61970): the tempModfile setting should be removed entirely. --func TestTempModfileUnchanged(t *testing.T) { -- // badMod has a go.mod file that is missing a go directive. -- const badMod = ` --- go.mod -- --module badmod.test/p ---- p.go -- --package p --` +-module example.com +-go 1.18 - -- WithOptions( -- Modes(Default), // no reason to test this with a remote gopls -- ProxyFiles(workspaceProxy), -- Settings{ -- "tempModfile": true, -- }, -- ).Run(t, badMod, func(t *testing.T, env *Env) { -- env.OpenFile("p.go") -- env.AfterChange() -- want := "module badmod.test/p\n" -- got := env.ReadWorkspaceFile("go.mod") -- if got != want { -- t.Errorf("go.mod content:\n%s\nwant:\n%s", got, want) -- } -- }) --} -diff -urN a/gopls/internal/regtest/template/template_test.go b/gopls/internal/regtest/template/template_test.go ---- a/gopls/internal/regtest/template/template_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/template/template_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,231 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +--- a/a.go -- +-package a - --package template +-var _ = int(123) //@signature("123", "", 0) +diff -urN a/gopls/internal/test/marker/testdata/signature/signature.txt b/gopls/internal/test/marker/testdata/signature/signature.txt +--- a/gopls/internal/test/marker/testdata/signature/signature.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/signature/signature.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,206 +0,0 @@ +-This test exercises basic tests for signature help. - --import ( -- "strings" -- "testing" +--- flags -- +--ignore_extra_diags - -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/hooks" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" +--- go.mod -- +-module golang.org/lsptests +- +-go 1.18 +- +--- signature/signature.go -- +-// Package signature has tests for signature help. +-package signature +- +-import ( +- "bytes" +- "encoding/json" +- "math/big" -) - --func TestMain(m *testing.M) { -- bug.PanicOnBugs = true -- Main(m, hooks.Options) +-func Foo(a string, b int) (c bool) { +- return -} - --func TestMultilineTokens(t *testing.T) { -- // 51731: panic: runtime error: slice bounds out of range [38:3] -- const files = ` ---- go.mod -- --module mod.com +-func Bar(float64, ...byte) { +-} - --go 1.17 ---- hi.tmpl -- --{{if (foÜx .X.Y)}}😀{{$A := -- "hi" -- }}{{.Z $A}}{{else}} --{{$A.X 12}} --{{foo (.X.Y) 23 ($A.Z)}} --{{end}} --` -- WithOptions( -- Settings{ -- "templateExtensions": []string{"tmpl"}, -- "semanticTokens": true, -- }, -- ).Run(t, files, func(t *testing.T, env *Env) { -- var p protocol.SemanticTokensParams -- p.TextDocument.URI = env.Sandbox.Workdir.URI("hi.tmpl") -- toks, err := env.Editor.Server.SemanticTokensFull(env.Ctx, &p) -- if err != nil { -- t.Errorf("semantic token failed: %v", err) -- } -- if toks == nil || len(toks.Data) == 0 { -- t.Errorf("got no semantic tokens") -- } -- }) +-type myStruct struct{} +- +-func (*myStruct) foo(e *json.Decoder) (*big.Int, error) { +- return nil, nil -} - --func TestTemplatesFromExtensions(t *testing.T) { -- const files = ` ---- go.mod -- --module mod.com +-type MyType struct{} - --go 1.12 ---- hello.tmpl -- --{{range .Planets}} --Hello {{}} <-- missing body --{{end}} --` -- WithOptions( -- Settings{ -- "templateExtensions": []string{"tmpl"}, -- "semanticTokens": true, -- }, -- ).Run(t, files, func(t *testing.T, env *Env) { -- // TODO: can we move this diagnostic onto {{}}? -- var diags protocol.PublishDiagnosticsParams -- env.OnceMet( -- InitialWorkspaceLoad, -- Diagnostics(env.AtRegexp("hello.tmpl", "()Hello {{}}")), -- ReadDiagnostics("hello.tmpl", &diags), -- ) -- d := diags.Diagnostics // issue 50786: check for Source -- if len(d) != 1 { -- t.Errorf("expected 1 diagnostic, got %d", len(d)) -- return -- } -- if d[0].Source != "template" { -- t.Errorf("expected Source 'template', got %q", d[0].Source) -- } -- // issue 50801 (even broken templates could return some semantic tokens) -- var p protocol.SemanticTokensParams -- p.TextDocument.URI = env.Sandbox.Workdir.URI("hello.tmpl") -- toks, err := env.Editor.Server.SemanticTokensFull(env.Ctx, &p) -- if err != nil { -- t.Errorf("semantic token failed: %v", err) -- } -- if toks == nil || len(toks.Data) == 0 { -- t.Errorf("got no semantic tokens") -- } +-type MyFunc func(foo int) string - -- env.WriteWorkspaceFile("hello.tmpl", "{{range .Planets}}\nHello {{.}}\n{{end}}") -- env.AfterChange(NoDiagnostics(ForFile("hello.tmpl"))) -- }) --} +-type Alias = int +-type OtherAlias = int +-type StringAlias = string - --func TestTemplatesObserveDirectoryFilters(t *testing.T) { -- const files = ` ---- go.mod -- --module mod.com +-func AliasSlice(a []*Alias) (b Alias) { return 0 } +-func AliasMap(a map[*Alias]StringAlias) (b, c map[*Alias]StringAlias) { return nil, nil } +-func OtherAliasMap(a, b map[Alias]OtherAlias) map[Alias]OtherAlias { return nil } - --go 1.12 ---- a/a.tmpl -- --A {{}} <-- missing body ---- b/b.tmpl -- --B {{}} <-- missing body --` +-func Qux() { +- Foo("foo", 123) //@signature("(", "Foo(a string, b int) (c bool)", 0) +- Foo("foo", 123) //@signature("123", "Foo(a string, b int) (c bool)", 1) +- Foo("foo", 123) //@signature(",", "Foo(a string, b int) (c bool)", 0) +- Foo("foo", 123) //@signature(" 1", "Foo(a string, b int) (c bool)", 1) +- Foo("foo", 123) //@signature(")", "Foo(a string, b int) (c bool)", 1) - -- WithOptions( -- Settings{ -- "directoryFilters": []string{"-b"}, -- "templateExtensions": []string{"tmpl"}, -- }, -- ).Run(t, files, func(t *testing.T, env *Env) { -- env.OnceMet( -- InitialWorkspaceLoad, -- Diagnostics(env.AtRegexp("a/a.tmpl", "()A")), -- NoDiagnostics(ForFile("b/b.tmpl")), -- ) -- }) --} +- Bar(13.37, 0x13) //@signature("13.37", "Bar(float64, ...byte)", 0) +- Bar(13.37, 0x37) //@signature("0x37", "Bar(float64, ...byte)", 1) +- Bar(13.37, 1, 2, 3, 4) //@signature("4", "Bar(float64, ...byte)", 1) - --func TestTemplatesFromLangID(t *testing.T) { -- const files = ` ---- go.mod -- --module mod.com +- fn := func(hi, there string) func(i int) rune { +- return func(int) rune { return 0 } +- } - --go 1.12 --` +- fn("hi", "there") //@signature("hi", "", 0) +- fn("hi", "there") //@signature(",", "fn(hi string, there string) func(i int) rune", 0) +- fn("hi", "there")(1) //@signature("1", "func(i int) rune", 0) - -- Run(t, files, func(t *testing.T, env *Env) { -- env.CreateBuffer("hello.tmpl", "") -- env.AfterChange( -- NoDiagnostics(ForFile("hello.tmpl")), // Don't get spurious errors for empty templates. -- ) -- env.SetBufferContent("hello.tmpl", "{{range .Planets}}\nHello {{}}\n{{end}}") -- env.Await(Diagnostics(env.AtRegexp("hello.tmpl", "()Hello {{}}"))) -- env.RegexpReplace("hello.tmpl", "{{}}", "{{.}}") -- env.Await(NoDiagnostics(ForFile("hello.tmpl"))) -- }) --} +- fnPtr := &fn +- (*fnPtr)("hi", "there") //@signature(",", "func(hi string, there string) func(i int) rune", 0) - --func TestClosingTemplatesMakesDiagnosticsDisappear(t *testing.T) { -- const files = ` ---- go.mod -- --module mod.com +- var fnIntf interface{} = Foo +- fnIntf.(func(string, int) bool)("hi", 123) //@signature("123", "func(string, int) bool", 1) - --go 1.12 ---- hello.tmpl -- --{{range .Planets}} --Hello {{}} <-- missing body --{{end}} --` +- (&bytes.Buffer{}).Next(2) //@signature("2", "Next(n int) []byte", 0) - -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("hello.tmpl") -- env.AfterChange( -- Diagnostics(env.AtRegexp("hello.tmpl", "()Hello {{}}")), -- ) -- // Since we don't have templateExtensions configured, closing hello.tmpl -- // should make its diagnostics disappear. -- env.CloseBuffer("hello.tmpl") -- env.AfterChange( -- NoDiagnostics(ForFile("hello.tmpl")), -- ) -- }) --} +- myFunc := MyFunc(func(n int) string { return "" }) +- myFunc(123) //@signature("123", "myFunc(foo int) string", 0) - --func TestMultipleSuffixes(t *testing.T) { -- const files = ` ---- go.mod -- --module mod.com +- var ms myStruct +- ms.foo(nil) //@signature("nil", "foo(e *json.Decoder) (*big.Int, error)", 0) - --go 1.12 ---- b.gotmpl -- --{{define "A"}}goo{{end}} ---- a.tmpl -- --{{template "A"}} --` +- _ = make([]int, 1, 2) //@signature("2", "make(t Type, size ...int) Type", 1) - -- WithOptions( -- Settings{ -- "templateExtensions": []string{"tmpl", "gotmpl"}, -- }, -- ).Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("a.tmpl") -- x := env.RegexpSearch("a.tmpl", `A`) -- loc := env.GoToDefinition(x) -- refs := env.References(loc) -- if len(refs) != 2 { -- t.Fatalf("got %v reference(s), want 2", len(refs)) -- } -- // make sure we got one from b.gotmpl -- want := env.Sandbox.Workdir.URI("b.gotmpl") -- if refs[0].URI != want && refs[1].URI != want { -- t.Errorf("failed to find reference to %s", shorten(want)) -- for i, r := range refs { -- t.Logf("%d: URI:%s %v", i, shorten(r.URI), r.Range) -- } -- } +- Foo(myFunc(123), 456) //@signature("myFunc", "Foo(a string, b int) (c bool)", 0) +- Foo(myFunc(123), 456) //@signature("123", "myFunc(foo int) string", 0) - -- content, nloc := env.Hover(loc) -- if loc != nloc { -- t.Errorf("loc? got %v, wanted %v", nloc, loc) -- } -- if content.Value != "template A defined" { -- t.Errorf("got %s, wanted 'template A defined", content.Value) -- } +- panic("oops!") //@signature(")", "panic(v any)", 0) +- println("hello", "world") //@signature(",", "println(args ...Type)", 0) +- +- Hello(func() { +- //@signature("//", "", 0) - }) +- +- AliasSlice() //@signature(")", "AliasSlice(a []*Alias) (b Alias)", 0) +- AliasMap() //@signature(")", "AliasMap(a map[*Alias]StringAlias) (b map[*Alias]StringAlias, c map[*Alias]StringAlias)", 0) +- OtherAliasMap() //@signature(")", "OtherAliasMap(a map[Alias]OtherAlias, b map[Alias]OtherAlias) map[Alias]OtherAlias", 0) -} - --// shorten long URIs --func shorten(fn protocol.DocumentURI) string { -- if len(fn) <= 20 { -- return string(fn) -- } -- pieces := strings.Split(string(fn), "/") -- if len(pieces) < 2 { -- return string(fn) -- } -- j := len(pieces) -- return pieces[j-2] + "/" + pieces[j-1] +-func Hello(func()) {} +- +--- signature/signature2.go -- +-package signature +- +-func _() { +- Foo(//@signature("//", "Foo(a string, b int) (c bool)", 0) -} - --// Hover needs tests -diff -urN a/gopls/internal/regtest/watch/setting_test.go b/gopls/internal/regtest/watch/setting_test.go ---- a/gopls/internal/regtest/watch/setting_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/watch/setting_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,85 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +--- signature/signature3.go -- +-package signature +- +-func _() { +- Foo("hello",//@signature("//", "Foo(a string, b int) (c bool)", 1) +-} - --package regtest +--- signature/signature_test.go -- +-package signature_test - -import ( -- "fmt" - "testing" - -- . "golang.org/x/tools/gopls/internal/lsp/regtest" +- sig "golang.org/lsptests/signature" -) - --func TestSubdirWatchPatterns(t *testing.T) { -- const files = ` ---- go.mod -- --module mod.test +-func TestSignature(t *testing.T) { +- sig.AliasSlice() //@signature(")", "AliasSlice(a []*sig.Alias) (b sig.Alias)", 0) +- sig.AliasMap() //@signature(")", "AliasMap(a map[*sig.Alias]sig.StringAlias) (b map[*sig.Alias]sig.StringAlias, c map[*sig.Alias]sig.StringAlias)", 0) +- sig.OtherAliasMap() //@signature(")", "OtherAliasMap(a map[sig.Alias]sig.OtherAlias, b map[sig.Alias]sig.OtherAlias) map[sig.Alias]sig.OtherAlias", 0) +-} - --go 1.18 ---- subdir/subdir.go -- --package subdir --` +--- snippets/snippets.go -- +-package snippets - -- tests := []struct { -- clientName string -- subdirWatchPatterns string -- wantWatched bool -- }{ -- {"other client", "on", true}, -- {"other client", "off", false}, -- {"other client", "auto", false}, -- {"Visual Studio Code", "auto", true}, -- } +-import ( +- "golang.org/lsptests/signature" +-) - -- for _, test := range tests { -- t.Run(fmt.Sprintf("%s_%s", test.clientName, test.subdirWatchPatterns), func(t *testing.T) { -- WithOptions( -- ClientName(test.clientName), -- Settings{ -- "subdirWatchPatterns": test.subdirWatchPatterns, -- }, -- ).Run(t, files, func(t *testing.T, env *Env) { -- var expectation Expectation -- if test.wantWatched { -- expectation = FileWatchMatching("subdir") -- } else { -- expectation = NoFileWatchMatching("subdir") -- } -- env.OnceMet( -- InitialWorkspaceLoad, -- expectation, -- ) -- }) -- }) -- } +-type CoolAlias = int //@item(CoolAlias, "CoolAlias", "int", "type") +- +-type structy struct { +- x signature.MyType -} - --// This test checks that we surface errors for invalid subdir watch patterns, --// as the triple of ("off"|"on"|"auto") may be confusing to users inclined to --// use (true|false) or some other truthy value. --func TestSubdirWatchPatterns_BadValues(t *testing.T) { -- tests := []struct { -- badValue interface{} -- wantMessage string -- }{ -- {true, "invalid type bool, expect string"}, -- {false, "invalid type bool, expect string"}, -- {"yes", `invalid option "yes"`}, -- } +-func X(_ map[signature.Alias]CoolAlias) (map[signature.Alias]CoolAlias) { +- return nil +-} - -- for _, test := range tests { -- t.Run(fmt.Sprint(test.badValue), func(t *testing.T) { -- WithOptions( -- Settings{ -- "subdirWatchPatterns": test.badValue, -- }, -- ).Run(t, "", func(t *testing.T, env *Env) { -- env.OnceMet( -- InitialWorkspaceLoad, -- ShownMessage(test.wantMessage), -- ) -- }) -- }) +-func _() { +- X() //@signature(")", "X(_ map[signature.Alias]CoolAlias) map[signature.Alias]CoolAlias", 0) +- _ = signature.MyType{} //@item(literalMyType, "signature.MyType{}", "", "var") +- s := structy{ +- x: //@snippet(" //", literalMyType, "signature.MyType{\\}") - } -} -diff -urN a/gopls/internal/regtest/watch/watch_test.go b/gopls/internal/regtest/watch/watch_test.go ---- a/gopls/internal/regtest/watch/watch_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/watch/watch_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,704 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package regtest +--- importedcomplit/importedcomplit.go -- +-package importedcomplit - -import ( -- "testing" +- // TODO(rfindley): re-enable after moving to new framework +- // "golang.org/lsptests/foo" - -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/hooks" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" +- // import completions (separate blocks to avoid comment alignment) +- "crypto/elli" //@complete("\" //", cryptoImport) +- +- "fm" //@complete("\" //", fmtImport) +- +- "go/pars" //@complete("\" //", parserImport) +- +- namedParser "go/pars" //@complete("\" //", parserImport) +- +- "golang.org/lspte" //@complete("\" //", lsptestsImport) +- +- "golang.org/lsptests/sign" //@complete("\" //", signatureImport) +- +- "golang.org/lsptests/sign" //@complete("ests", lsptestsImport) - -- "golang.org/x/tools/gopls/internal/lsp/fake" -- "golang.org/x/tools/gopls/internal/lsp/protocol" +- "golang.org/lsptests/signa" //@complete("na\" //", signatureImport) -) - --func TestMain(m *testing.M) { -- bug.PanicOnBugs = true -- Main(m, hooks.Options) +-func _() { +- var V int //@item(icVVar, "V", "int", "var") +- +- // TODO(rfindley): re-enable after moving to new framework +- // _ = foo.StructFoo{V} // complete("}", Value, icVVar) +-} +- +-func _() { +- var ( +- aa string //@item(icAAVar, "aa", "string", "var") +- ab int //@item(icABVar, "ab", "int", "var") +- ) +- +- // TODO(rfindley): re-enable after moving to new framework +- // _ = foo.StructFoo{a} // complete("}", abVar, aaVar) +- +- var s struct { +- AA string //@item(icFieldAA, "AA", "string", "field") +- AB int //@item(icFieldAB, "AB", "int", "field") +- } +- +- // TODO(rfindley): re-enable after moving to new framework +- //_ = foo.StructFoo{s.} // complete("}", icFieldAB, icFieldAA) +-} +- +-/* "fmt" */ //@item(fmtImport, "fmt", "\"fmt\"", "package") +-/* "go/parser" */ //@item(parserImport, "parser", "\"go/parser\"", "package") +-/* "golang.org/lsptests/signature" */ //@item(signatureImport, "signature", "\"golang.org/lsptests/signature\"", "package") +-/* "golang.org/lsptests/" */ //@item(lsptestsImport, "lsptests/", "\"golang.org/lsptests/\"", "package") +-/* "crypto/elliptic" */ //@item(cryptoImport, "elliptic", "\"crypto/elliptic\"", "package") +diff -urN a/gopls/internal/test/marker/testdata/stubmethods/basic_resolve.txt b/gopls/internal/test/marker/testdata/stubmethods/basic_resolve.txt +--- a/gopls/internal/test/marker/testdata/stubmethods/basic_resolve.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/stubmethods/basic_resolve.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,31 +0,0 @@ +-This test exercises basic 'stub methods' functionality, with resolve support. +-See basic.txt for the same test without resolve support. +- +--- capabilities.json -- +-{ +- "textDocument": { +- "codeAction": { +- "dataSupport": true, +- "resolveSupport": { +- "properties": ["edit"] +- } +- } +- } -} +--- go.mod -- +-module example.com +-go 1.12 +- +--- a/a.go -- +-package a +- +-type C int +- +-var _ error = C(0) //@suggestedfix(re"C.0.", re"missing method Error", stub) +--- @stub/a/a.go -- +-@@ -5 +5,5 @@ +-+// Error implements error. +-+func (c C) Error() string { +-+ panic("unimplemented") +-+} +-+ +diff -urN a/gopls/internal/test/marker/testdata/stubmethods/basic.txt b/gopls/internal/test/marker/testdata/stubmethods/basic.txt +--- a/gopls/internal/test/marker/testdata/stubmethods/basic.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/stubmethods/basic.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,20 +0,0 @@ +-This test exercises basic 'stub methods' functionality. +-See basic_resolve.txt for the same test with resolve support. - --func TestEditFile(t *testing.T) { -- const pkg = ` --- go.mod -- --module mod.com +-module example.com +-go 1.12 - --go 1.14 --- a/a.go -- -package a - --func _() { -- var x int --} --` -- // Edit the file when it's *not open* in the workspace, and check that -- // diagnostics are updated. -- t.Run("unopened", func(t *testing.T) { -- Run(t, pkg, func(t *testing.T, env *Env) { -- env.OnceMet( -- InitialWorkspaceLoad, -- Diagnostics(env.AtRegexp("a/a.go", "x")), -- ) -- env.WriteWorkspaceFile("a/a.go", `package a; func _() {};`) -- env.AfterChange( -- NoDiagnostics(ForFile("a/a.go")), -- ) -- }) -- }) +-type C int - -- // Edit the file when it *is open* in the workspace, and check that -- // diagnostics are *not* updated. -- t.Run("opened", func(t *testing.T) { -- Run(t, pkg, func(t *testing.T, env *Env) { -- env.OpenFile("a/a.go") -- // Insert a trivial edit so that we don't automatically update the buffer -- // (see CL 267577). -- env.EditBuffer("a/a.go", fake.NewEdit(0, 0, 0, 0, " ")) -- env.AfterChange() -- env.WriteWorkspaceFile("a/a.go", `package a; func _() {};`) -- env.AfterChange( -- Diagnostics(env.AtRegexp("a/a.go", "x")), -- ) -- }) -- }) --} +-var _ error = C(0) //@suggestedfix(re"C.0.", re"missing method Error", stub) +--- @stub/a/a.go -- +-@@ -5 +5,5 @@ +-+// Error implements error. +-+func (c C) Error() string { +-+ panic("unimplemented") +-+} +-+ +diff -urN a/gopls/internal/test/marker/testdata/stubmethods/issue61693.txt b/gopls/internal/test/marker/testdata/stubmethods/issue61693.txt +--- a/gopls/internal/test/marker/testdata/stubmethods/issue61693.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/stubmethods/issue61693.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,26 +0,0 @@ +-This test exercises stub methods functionality with variadic parameters. +- +-In golang/go#61693 stubmethods was panicking in this case. - --// Edit a dependency on disk and expect a new diagnostic. --func TestEditDependency(t *testing.T) { -- const pkg = ` --- go.mod -- -module mod.com - --go 1.14 ---- b/b.go -- --package b +-go 1.18 +--- main.go -- +-package main - --func B() int { return 0 } ---- a/a.go -- --package a +-type C int - --import ( -- "mod.com/b" --) +-func F(err ...error) {} - -func _() { -- _ = b.B() +- var x error +- F(x, C(0)) //@suggestedfix(re"C.0.", re"missing method Error", stub) -} --` -- Run(t, pkg, func(t *testing.T, env *Env) { -- env.OpenFile("a/a.go") -- env.AfterChange() -- env.WriteWorkspaceFile("b/b.go", `package b; func B() {};`) -- env.AfterChange( -- Diagnostics(env.AtRegexp("a/a.go", "b.B")), -- ) -- }) +--- @stub/main.go -- +-@@ -5 +5,5 @@ +-+// Error implements error. +-+func (c C) Error() string { +-+ panic("unimplemented") +-+} +-+ +diff -urN a/gopls/internal/test/marker/testdata/stubmethods/issue61830.txt b/gopls/internal/test/marker/testdata/stubmethods/issue61830.txt +--- a/gopls/internal/test/marker/testdata/stubmethods/issue61830.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/stubmethods/issue61830.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,24 +0,0 @@ +-This test verifies that method stubbing qualifies types relative to the current +-package. +- +--- p.go -- +-package p +- +-import "io" +- +-type B struct{} +- +-type I interface { +- M(io.Reader, B) -} - --// Edit both the current file and one of its dependencies on disk and --// expect diagnostic changes. --func TestEditFileAndDependency(t *testing.T) { -- const pkg = ` ---- go.mod -- --module mod.com +-type A struct{} - --go 1.14 ---- b/b.go -- --package b +-var _ I = &A{} //@suggestedfix(re"&A..", re"missing method M", stub) +--- @stub/p.go -- +-@@ -13 +13,5 @@ +-+// M implements I. +-+func (a *A) M(io.Reader, B) { +-+ panic("unimplemented") +-+} +-+ +diff -urN a/gopls/internal/test/marker/testdata/stubmethods/issue64078.txt b/gopls/internal/test/marker/testdata/stubmethods/issue64078.txt +--- a/gopls/internal/test/marker/testdata/stubmethods/issue64078.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/stubmethods/issue64078.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,36 +0,0 @@ +-This test verifies that the named receiver is generated. - --func B() int { return 0 } ---- a/a.go -- --package a +--- p.go -- +-package p - --import ( -- "mod.com/b" --) +-type A struct{} - --func _() { -- var x int -- _ = b.B() +-func (aa *A) M1() { +- panic("unimplemented") -} --` -- Run(t, pkg, func(t *testing.T, env *Env) { -- env.OnceMet( -- InitialWorkspaceLoad, -- Diagnostics(env.AtRegexp("a/a.go", "x")), -- ) -- env.WriteWorkspaceFiles(map[string]string{ -- "b/b.go": `package b; func B() {};`, -- "a/a.go": `package a - --import "mod.com/b" -- --func _() { -- b.B() --}`, -- }) -- env.AfterChange( -- NoDiagnostics(ForFile("a/a.go")), -- NoDiagnostics(ForFile("b/b.go")), -- ) -- }) +-type I interface { +- M1() +- M2(aa string) +- M3(bb string) +- M4() (aa string) -} - --// Delete a dependency and expect a new diagnostic. --func TestDeleteDependency(t *testing.T) { -- const pkg = ` ---- go.mod -- --module mod.com +-var _ I = &A{} //@suggestedfix(re"&A..", re"missing method M", stub) +--- @stub/p.go -- +-@@ -5 +5,15 @@ +-+// M2 implements I. +-+func (*A) M2(aa string) { +-+ panic("unimplemented") +-+} +-+ +-+// M3 implements I. +-+func (aa *A) M3(bb string) { +-+ panic("unimplemented") +-+} +-+ +-+// M4 implements I. +-+func (*A) M4() (aa string) { +-+ panic("unimplemented") +-+} +-+ +diff -urN a/gopls/internal/test/marker/testdata/stubmethods/issue64114.txt b/gopls/internal/test/marker/testdata/stubmethods/issue64114.txt +--- a/gopls/internal/test/marker/testdata/stubmethods/issue64114.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/stubmethods/issue64114.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,37 +0,0 @@ +-This test verifies that the embedded field has a method with the same name. - --go 1.14 ---- b/b.go -- --package b +--- issue64114.go -- +-package stub - --func B() int { return 0 } ---- a/a.go -- --package a +-// Regression test for issue #64114: code action "implement" is not listed. - --import ( -- "mod.com/b" --) +-var _ WriteTest = (*WriteStruct)(nil) //@suggestedfix("(", re"does not implement", issue64114) - --func _() { -- _ = b.B() --} --` -- Run(t, pkg, func(t *testing.T, env *Env) { -- env.OpenFile("a/a.go") -- env.AfterChange() -- env.RemoveWorkspaceFile("b/b.go") -- env.AfterChange( -- Diagnostics(env.AtRegexp("a/a.go", "\"mod.com/b\"")), -- ) -- }) +-type WriterTwoStruct struct{} +- +-// Write implements io.ReadWriter. +-func (t *WriterTwoStruct) RRRR(str string) error { +- panic("unimplemented") -} - --// Create a dependency on disk and expect the diagnostic to go away. --func TestCreateDependency(t *testing.T) { -- const missing = ` ---- go.mod -- --module mod.com +-type WriteTest interface { +- RRRR() +- WWWW() +-} - --go 1.14 ---- b/b.go -- --package b +-type WriteStruct struct { +- WriterTwoStruct +-} +--- @issue64114/issue64114.go -- +-@@ -22 +22,11 @@ +-+ +-+// RRRR implements WriteTest. +-+// Subtle: this method shadows the method (WriterTwoStruct).RRRR of WriteStruct.WriterTwoStruct. +-+func (w *WriteStruct) RRRR() { +-+ panic("unimplemented") +-+} +-+ +-+// WWWW implements WriteTest. +-+func (w *WriteStruct) WWWW() { +-+ panic("unimplemented") +-+} +diff -urN a/gopls/internal/test/marker/testdata/suggestedfix/embeddirective.txt b/gopls/internal/test/marker/testdata/suggestedfix/embeddirective.txt +--- a/gopls/internal/test/marker/testdata/suggestedfix/embeddirective.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/suggestedfix/embeddirective.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,22 +0,0 @@ +-This test checks the quick fix to add a missing "embed" import. - --func B() int { return 0 } ---- a/a.go -- --package a +--- embed.txt -- +-text +--- fix_import.go -- +-package embeddirective - -import ( -- "mod.com/c" +- "io" +- "os" -) - --func _() { -- c.C() --} --` -- Run(t, missing, func(t *testing.T, env *Env) { -- env.OnceMet( -- InitialWorkspaceLoad, -- Diagnostics(env.AtRegexp("a/a.go", "\"mod.com/c\"")), -- ) -- env.WriteWorkspaceFile("c/c.go", `package c; func C() {};`) -- env.AfterChange( -- NoDiagnostics(ForFile("a/a.go")), -- ) -- }) +-//go:embed embed.txt //@suggestedfix("//go:embed", re`must import "embed"`, fix_import) +-var t string +- +-func unused() { +- _ = os.Stdin +- _ = io.EOF -} +--- @fix_import/fix_import.go -- +-@@ -4 +4 @@ +-+ _ "embed" +diff -urN a/gopls/internal/test/marker/testdata/suggestedfix/issue65024.txt b/gopls/internal/test/marker/testdata/suggestedfix/issue65024.txt +--- a/gopls/internal/test/marker/testdata/suggestedfix/issue65024.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/suggestedfix/issue65024.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,78 +0,0 @@ +-Regression example.com for #65024, "incorrect package qualification when +-stubbing method in v2 module". - --// Create a new dependency and add it to the file on disk. --// This is similar to what might happen if you switch branches. --func TestCreateAndAddDependency(t *testing.T) { -- const original = ` ---- go.mod -- --module mod.com +-The second test (a-a) ensures that we don't use path-based heuristics +-to guess the PkgName of an import. - --go 1.14 ---- a/a.go -- +--- a/v2/go.mod -- +-module example.com/a/v2 +-go 1.18 +- +--- a/v2/a.go -- -package a - --func _() {} --` -- Run(t, original, func(t *testing.T, env *Env) { -- env.WriteWorkspaceFile("c/c.go", `package c; func C() {};`) -- env.WriteWorkspaceFile("a/a.go", `package a; import "mod.com/c"; func _() { c.C() }`) -- env.AfterChange( -- NoDiagnostics(ForFile("a/a.go")), -- ) -- }) --} +-type I interface { F() T } - --// Create a new file that defines a new symbol, in the same package. --func TestCreateFile(t *testing.T) { -- const pkg = ` ---- go.mod -- --module mod.com +-type T struct {} - --go 1.14 ---- a/a.go -- --package a +--- a/v2/b/b.go -- +-package b - --func _() { -- hello() --} --` -- Run(t, pkg, func(t *testing.T, env *Env) { -- env.OnceMet( -- InitialWorkspaceLoad, -- Diagnostics(env.AtRegexp("a/a.go", "hello")), -- ) -- env.WriteWorkspaceFile("a/a2.go", `package a; func hello() {};`) -- env.AfterChange( -- NoDiagnostics(ForFile("a/a.go")), -- ) -- }) --} +-import "example.com/a/v2" - --// Add a new method to an interface and implement it. --// Inspired by the structure of internal/lsp/source and internal/lsp/cache. --func TestCreateImplementation(t *testing.T) { -- const pkg = ` ---- go.mod -- --module mod.com +-type B struct{} - --go 1.14 ---- b/b.go -- --package b +-var _ a.I = &B{} //@ suggestedfix("&B{}", re"does not implement", out) - --type B interface{ -- Hello() string --} +-// This line makes the diff tidier. - --func SayHello(bee B) { -- println(bee.Hello()) --} ---- a/a.go -- +--- @out/a/v2/b/b.go -- +-@@ -7 +7,5 @@ +-+// F implements a.I. +-+func (b *B) F() a.T { +-+ panic("unimplemented") +-+} +-+ +-@@ -10 +15 @@ +-- +--- a-a/v2/go.mod -- +-// This module has a hyphenated name--how posh. +-// It won't do to use it as an identifier. +-// The correct name is the one in the package decl, +-// which in this case is not what the path heuristic would guess. +-module example.com/a-a/v2 +-go 1.18 +- +--- a-a/v2/a.go -- -package a +-type I interface { F() T } +-type T struct {} - --import "mod.com/b" +--- a-a/v2/b/b.go -- +-package b - --type X struct {} +-// Note: no existing import of a. - --func (_ X) Hello() string { -- return "" --} +-type B struct{} - --func _() { -- x := X{} -- b.SayHello(x) --} --` -- const newMethod = `package b --type B interface{ -- Hello() string -- Bye() string --} +-var _ I = &B{} //@ suggestedfix("&B{}", re"does not implement", out2) - --func SayHello(bee B) { -- println(bee.Hello()) --}` -- const implementation = `package a +-// This line makes the diff tidier. - --import "mod.com/b" +--- a-a/v2/b/import-a-I.go -- +-package b +-import "example.com/a-a/v2" +-type I = a.I - --type X struct {} +--- @out2/a-a/v2/b/b.go -- +-@@ -3 +3,2 @@ +-+import a "example.com/a-a/v2" +-+ +-@@ -7 +9,5 @@ +-+// F implements a.I. +-+func (b *B) F() a.T { +-+ panic("unimplemented") +-+} +-+ +-@@ -10 +17 @@ +-- +diff -urN a/gopls/internal/test/marker/testdata/suggestedfix/missingfunction.txt b/gopls/internal/test/marker/testdata/suggestedfix/missingfunction.txt +--- a/gopls/internal/test/marker/testdata/suggestedfix/missingfunction.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/suggestedfix/missingfunction.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,127 +0,0 @@ +-This test checks the quick fix for undefined functions. - --func (_ X) Hello() string { -- return "" +--- channels.go -- +-package missingfunction +- +-func channels(s string) { +- undefinedChannels(c()) //@suggestedfix("undefinedChannels", re"(undeclared|undefined)", channels) -} - --func (_ X) Bye() string { -- return "" +-func c() (<-chan string, chan string) { +- return make(<-chan string), make(chan string) -} +--- @channels/channels.go -- +-@@ -7 +7,4 @@ +-+func undefinedChannels(ch1 <-chan string, ch2 chan string) { +-+ panic("unimplemented") +-+} +-+ +--- consecutive.go -- +-package missingfunction - --func _() { -- x := X{} -- b.SayHello(x) --}` +-func consecutiveParams() { +- var s string +- undefinedConsecutiveParams(s, s) //@suggestedfix("undefinedConsecutiveParams", re"(undeclared|undefined)", consecutive) +-} +--- @consecutive/consecutive.go -- +-@@ -7 +7,4 @@ +-+ +-+func undefinedConsecutiveParams(s1, s2 string) { +-+ panic("unimplemented") +-+} +--- error.go -- +-package missingfunction - -- // Add the new method before the implementation. Expect diagnostics. -- t.Run("method before implementation", func(t *testing.T) { -- Run(t, pkg, func(t *testing.T, env *Env) { -- env.WriteWorkspaceFile("b/b.go", newMethod) -- env.AfterChange( -- Diagnostics(AtPosition("a/a.go", 12, 12)), -- ) -- env.WriteWorkspaceFile("a/a.go", implementation) -- env.AfterChange( -- NoDiagnostics(ForFile("a/a.go")), -- ) -- }) -- }) -- // Add the new implementation before the new method. Expect no diagnostics. -- t.Run("implementation before method", func(t *testing.T) { -- Run(t, pkg, func(t *testing.T, env *Env) { -- env.WriteWorkspaceFile("a/a.go", implementation) -- env.AfterChange( -- NoDiagnostics(ForFile("a/a.go")), -- ) -- env.WriteWorkspaceFile("b/b.go", newMethod) -- env.AfterChange( -- NoDiagnostics(ForFile("a/a.go")), -- ) -- }) -- }) -- // Add both simultaneously. Expect no diagnostics. -- t.Run("implementation and method simultaneously", func(t *testing.T) { -- Run(t, pkg, func(t *testing.T, env *Env) { -- env.WriteWorkspaceFiles(map[string]string{ -- "a/a.go": implementation, -- "b/b.go": newMethod, -- }) -- env.AfterChange( -- NoDiagnostics(ForFile("a/a.go")), -- NoDiagnostics(ForFile("b/b.go")), -- ) -- }) -- }) +-func errorParam() { +- var err error +- undefinedErrorParam(err) //@suggestedfix("undefinedErrorParam", re"(undeclared|undefined)", error) -} +--- @error/error.go -- +-@@ -7 +7,4 @@ +-+ +-+func undefinedErrorParam(err error) { +-+ panic("unimplemented") +-+} +--- literals.go -- +-package missingfunction - --// Tests golang/go#38498. Delete a file and then force a reload. --// Assert that we no longer try to load the file. --func TestDeleteFiles(t *testing.T) { -- const pkg = ` ---- go.mod -- --module mod.com +-type T struct{} - --go 1.14 ---- a/a.go -- --package a +-func literals() { +- undefinedLiterals("hey compiler", T{}, &T{}) //@suggestedfix("undefinedLiterals", re"(undeclared|undefined)", literals) +-} +--- @literals/literals.go -- +-@@ -8 +8,4 @@ +-+ +-+func undefinedLiterals(s string, t1 T, t2 *T) { +-+ panic("unimplemented") +-+} +--- operation.go -- +-package missingfunction - --func _() { -- var _ int +-import "time" +- +-func operation() { +- undefinedOperation(10 * time.Second) //@suggestedfix("undefinedOperation", re"(undeclared|undefined)", operation) -} ---- a/a_unneeded.go -- --package a --` -- t.Run("close then delete", func(t *testing.T) { -- WithOptions( -- Settings{"verboseOutput": true}, -- ).Run(t, pkg, func(t *testing.T, env *Env) { -- env.OpenFile("a/a.go") -- env.OpenFile("a/a_unneeded.go") -- env.Await( -- // Log messages are asynchronous to other events on the LSP stream, so we -- // can't use OnceMet or AfterChange here. -- LogMatching(protocol.Info, "a_unneeded.go", 1, false), -- ) +--- @operation/operation.go -- +-@@ -8 +8,4 @@ +-+ +-+func undefinedOperation(duration time.Duration) { +-+ panic("unimplemented") +-+} +--- selector.go -- +-package missingfunction - -- // Close and delete the open file, mimicking what an editor would do. -- env.CloseBuffer("a/a_unneeded.go") -- env.RemoveWorkspaceFile("a/a_unneeded.go") -- env.RegexpReplace("a/a.go", "var _ int", "fmt.Println(\"\")") -- env.AfterChange( -- Diagnostics(env.AtRegexp("a/a.go", "fmt")), -- ) -- env.SaveBuffer("a/a.go") -- env.Await( -- // There should only be one log message containing -- // a_unneeded.go, from the initial workspace load, which we -- // check for earlier. If there are more, there's a bug. -- LogMatching(protocol.Info, "a_unneeded.go", 1, false), -- NoDiagnostics(ForFile("a/a.go")), -- ) -- }) -- }) +-func selector() { +- m := map[int]bool{} +- undefinedSelector(m[1]) //@suggestedfix("undefinedSelector", re"(undeclared|undefined)", selector) +-} +--- @selector/selector.go -- +-@@ -7 +7,4 @@ +-+ +-+func undefinedSelector(b bool) { +-+ panic("unimplemented") +-+} +--- slice.go -- +-package missingfunction - -- t.Run("delete then close", func(t *testing.T) { -- WithOptions( -- Settings{"verboseOutput": true}, -- ).Run(t, pkg, func(t *testing.T, env *Env) { -- env.OpenFile("a/a.go") -- env.OpenFile("a/a_unneeded.go") -- env.Await( -- LogMatching(protocol.Info, "a_unneeded.go", 1, false), -- ) +-func slice() { +- undefinedSlice([]int{1, 2}) //@suggestedfix("undefinedSlice", re"(undeclared|undefined)", slice) +-} +--- @slice/slice.go -- +-@@ -6 +6,4 @@ +-+ +-+func undefinedSlice(i []int) { +-+ panic("unimplemented") +-+} +--- tuple.go -- +-package missingfunction - -- // Delete and then close the file. -- env.RemoveWorkspaceFile("a/a_unneeded.go") -- env.CloseBuffer("a/a_unneeded.go") -- env.RegexpReplace("a/a.go", "var _ int", "fmt.Println(\"\")") -- env.AfterChange( -- Diagnostics(env.AtRegexp("a/a.go", "fmt")), -- ) -- env.SaveBuffer("a/a.go") -- env.Await( -- // There should only be one log message containing -- // a_unneeded.go, from the initial workspace load, which we -- // check for earlier. If there are more, there's a bug. -- LogMatching(protocol.Info, "a_unneeded.go", 1, false), -- NoDiagnostics(ForFile("a/a.go")), -- ) -- }) -- }) +-func tuple() { +- undefinedTuple(b()) //@suggestedfix("undefinedTuple", re"(undeclared|undefined)", tuple) +-} +- +-func b() (string, error) { +- return "", nil +-} +--- @tuple/tuple.go -- +-@@ -7 +7,4 @@ +-+func undefinedTuple(s string, err error) { +-+ panic("unimplemented") +-+} +-+ +--- unique_params.go -- +-package missingfunction +- +-func uniqueArguments() { +- var s string +- var i int +- undefinedUniqueArguments(s, i, s) //@suggestedfix("undefinedUniqueArguments", re"(undeclared|undefined)", unique) -} +--- @unique/unique_params.go -- +-@@ -8 +8,4 @@ +-+ +-+func undefinedUniqueArguments(s1 string, i int, s2 string) { +-+ panic("unimplemented") +-+} +diff -urN a/gopls/internal/test/marker/testdata/suggestedfix/noresultvalues.txt b/gopls/internal/test/marker/testdata/suggestedfix/noresultvalues.txt +--- a/gopls/internal/test/marker/testdata/suggestedfix/noresultvalues.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/suggestedfix/noresultvalues.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,18 +0,0 @@ +-This test checks the quick fix for removing extra return values. - --// This change reproduces the behavior of switching branches, with multiple --// files being created and deleted. The key change here is the movement of a --// symbol from one file to another in a given package through a deletion and --// creation. To reproduce an issue with metadata invalidation in batched --// changes, the last change in the batch is an on-disk file change that doesn't --// require metadata invalidation. --func TestMoveSymbol(t *testing.T) { -- const pkg = ` ---- go.mod -- --module mod.com +-Note: gopls should really discard unnecessary return statements. - --go 1.14 ---- main.go -- --package main +--- noresultvalues.go -- +-package typeerrors - --import "mod.com/a" +-func x() { return nil } //@suggestedfix("nil", re"too many return", x) +- +-func y() { return nil, "hello" } //@suggestedfix("nil", re"too many return", y) +--- @x/noresultvalues.go -- +-@@ -3 +3 @@ +--func x() { return nil } //@suggestedfix("nil", re"too many return", x) +-+func x() { return } //@suggestedfix("nil", re"too many return", x) +--- @y/noresultvalues.go -- +-@@ -5 +5 @@ +--func y() { return nil, "hello" } //@suggestedfix("nil", re"too many return", y) +-+func y() { return } //@suggestedfix("nil", re"too many return", y) +diff -urN a/gopls/internal/test/marker/testdata/suggestedfix/self_assignment.txt b/gopls/internal/test/marker/testdata/suggestedfix/self_assignment.txt +--- a/gopls/internal/test/marker/testdata/suggestedfix/self_assignment.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/suggestedfix/self_assignment.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,19 +0,0 @@ +-Test of the suggested fix to remove unnecessary assignments. - --func main() { -- var x int -- x = a.Hello -- println(x) --} ---- a/a1.go -- --package a +--- a.go -- +-package suggestedfix - --var Hello int ---- a/a2.go -- --package a +-import ( +- "log" +-) - --func _() {} --` -- Run(t, pkg, func(t *testing.T, env *Env) { -- env.WriteWorkspaceFile("a/a3.go", "package a\n\nvar Hello int\n") -- env.RemoveWorkspaceFile("a/a1.go") -- env.WriteWorkspaceFile("a/a2.go", "package a; func _() {};") -- env.AfterChange( -- NoDiagnostics(ForFile("main.go")), -- ) -- }) +-func goodbye() { +- s := "hiiiiiii" +- s = s //@suggestedfix("s = s", re"self-assignment", fix) +- log.Print(s) -} - --// Reproduce golang/go#40456. --func TestChangeVersion(t *testing.T) { -- const proxy = ` ---- example.com@v1.2.3/go.mod -- --module example.com +--- @fix/a.go -- +-@@ -9 +9 @@ +-- s = s //@suggestedfix("s = s", re"self-assignment", fix) +-+ //@suggestedfix("s = s", re"self-assignment", fix) +diff -urN a/gopls/internal/test/marker/testdata/suggestedfix/stub.txt b/gopls/internal/test/marker/testdata/suggestedfix/stub.txt +--- a/gopls/internal/test/marker/testdata/suggestedfix/stub.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/suggestedfix/stub.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,365 +0,0 @@ +-This test checks the 'implement interface' quick fix. - --go 1.12 ---- example.com@v1.2.3/blah/blah.go -- --package blah +--- go.mod -- +-module golang.org/lsptests/stub - --const Name = "Blah" +-go 1.18 - --func X(x int) {} ---- example.com@v1.2.2/go.mod -- --module example.com +--- other/other.go -- +-package other - --go 1.12 ---- example.com@v1.2.2/blah/blah.go -- --package blah +-import ( +- "bytes" +- renamed_context "context" +-) - --const Name = "Blah" +-type Interface interface { +- Get(renamed_context.Context) *bytes.Buffer +-} - --func X() {} ---- random.org@v1.2.3/go.mod -- --module random.org +--- add_selector.go -- +-package stub - --go 1.12 ---- random.org@v1.2.3/blah/blah.go -- --package hello +-import "io" - --const Name = "Hello" --` -- const mod = ` ---- go.mod -- --module mod.com +-// This file tests that if an interface +-// method references a type from its own package +-// then our implementation must add the import/package selector +-// in the concrete method if the concrete type is outside of the interface +-// package +-var _ io.ReaderFrom = &readerFrom{} //@suggestedfix("&readerFrom", re"cannot use", readerFrom) - --go 1.12 +-type readerFrom struct{} +--- @readerFrom/add_selector.go -- +-@@ -13 +13,5 @@ +-+ +-+// ReadFrom implements io.ReaderFrom. +-+func (*readerFrom) ReadFrom(r io.Reader) (n int64, err error) { +-+ panic("unimplemented") +-+} +--- assign.go -- +-package stub - --require example.com v1.2.2 ---- go.sum -- --example.com v1.2.3 h1:OnPPkx+rW63kj9pgILsu12MORKhSlnFa3DVRJq1HZ7g= --example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= ---- main.go -- --package main +-import "io" - --import "example.com/blah" +-func _() { +- var br io.ByteWriter +- br = &byteWriter{} //@suggestedfix("&", re"does not implement", assign) +- _ = br +-} +- +-type byteWriter struct{} +--- @assign/assign.go -- +-@@ -12 +12,5 @@ +-+ +-+// WriteByte implements io.ByteWriter. +-+func (b *byteWriter) WriteByte(c byte) error { +-+ panic("unimplemented") +-+} +--- assign_multivars.go -- +-package stub +- +-import "io" +- +-func _() { +- var br io.ByteWriter +- var i int +- i, br = 1, &multiByteWriter{} //@suggestedfix("&", re"does not implement", assign_multivars) +- _, _ = i, br +-} +- +-type multiByteWriter struct{} +--- @assign_multivars/assign_multivars.go -- +-@@ -13 +13,5 @@ +-+ +-+// WriteByte implements io.ByteWriter. +-+func (m *multiByteWriter) WriteByte(c byte) error { +-+ panic("unimplemented") +-+} +--- call_expr.go -- +-package stub - -func main() { -- blah.X() +- check(&callExpr{}) //@suggestedfix("&", re"does not implement", call_expr) -} --` -- WithOptions(ProxyFiles(proxy)).Run(t, mod, func(t *testing.T, env *Env) { -- env.WriteWorkspaceFiles(map[string]string{ -- "go.mod": `module mod.com - --go 1.12 +-func check(err error) { +- if err != nil { +- panic(err) +- } +-} - --require example.com v1.2.3 --`, -- "main.go": `package main +-type callExpr struct{} +--- @call_expr/call_expr.go -- +-@@ -14 +14,5 @@ +-+ +-+// Error implements error. +-+func (c *callExpr) Error() string { +-+ panic("unimplemented") +-+} +--- embedded.go -- +-package stub - -import ( -- "example.com/blah" +- "io" +- "sort" -) - --func main() { -- blah.X(1) --} --`, -- }) -- env.AfterChange( -- env.DoneWithChangeWatchedFiles(), -- NoDiagnostics(ForFile("main.go")), -- ) -- }) +-var _ embeddedInterface = (*embeddedConcrete)(nil) //@suggestedfix("(", re"does not implement", embedded) +- +-type embeddedConcrete struct{} +- +-type embeddedInterface interface { +- sort.Interface +- io.Reader -} +--- @embedded/embedded.go -- +-@@ -12 +12,20 @@ +-+// Len implements embeddedInterface. +-+func (e *embeddedConcrete) Len() int { +-+ panic("unimplemented") +-+} +-+ +-+// Less implements embeddedInterface. +-+func (e *embeddedConcrete) Less(i int, j int) bool { +-+ panic("unimplemented") +-+} +-+ +-+// Read implements embeddedInterface. +-+func (e *embeddedConcrete) Read(p []byte) (n int, err error) { +-+ panic("unimplemented") +-+} +-+ +-+// Swap implements embeddedInterface. +-+func (e *embeddedConcrete) Swap(i int, j int) { +-+ panic("unimplemented") +-+} +-+ +--- err.go -- +-package stub - --// Reproduces golang/go#40340. --func TestSwitchFromGOPATHToModuleMode(t *testing.T) { -- const files = ` ---- foo/blah/blah.go -- --package blah +-func _() { +- var br error = &customErr{} //@suggestedfix("&", re"does not implement", err) +- _ = br +-} - --const Name = "" ---- main.go -- --package main +-type customErr struct{} +--- @err/err.go -- +-@@ -9 +9,5 @@ +-+ +-+// Error implements error. +-+func (c *customErr) Error() string { +-+ panic("unimplemented") +-+} +--- function_return.go -- +-package stub - --import "foo/blah" +-import ( +- "io" +-) - --func main() { -- _ = blah.Name +-func newCloser() io.Closer { +- return closer{} //@suggestedfix("c", re"does not implement", function_return) -} --` -- WithOptions( -- InGOPATH(), -- Modes(Default), // golang/go#57521: this test is temporarily failing in 'experimental' mode -- EnvVars{"GO111MODULE": "auto"}, -- ).Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- env.AfterChange( -- NoDiagnostics(ForFile("main.go")), -- ) -- if err := env.Sandbox.RunGoCommand(env.Ctx, "", "mod", []string{"init", "mod.com"}, nil, true); err != nil { -- t.Fatal(err) -- } - -- // TODO(golang/go#57558, golang/go#57512): file watching is asynchronous, -- // and we must wait for the view to be reconstructed before touching -- // main.go, so that the new view "knows" about main.go. This is a bug, but -- // awaiting the change here avoids it. -- env.AfterChange() +-type closer struct{} +--- @function_return/function_return.go -- +-@@ -12 +12,5 @@ +-+ +-+// Close implements io.Closer. +-+func (c closer) Close() error { +-+ panic("unimplemented") +-+} +--- generic_receiver.go -- +-package stub - -- env.RegexpReplace("main.go", `"foo/blah"`, `"mod.com/foo/blah"`) -- env.AfterChange( -- NoDiagnostics(ForFile("main.go")), -- ) -- }) --} +-import "io" - --// Reproduces golang/go#40487. --func TestSwitchFromModulesToGOPATH(t *testing.T) { -- const files = ` ---- foo/go.mod -- --module mod.com +-// This file tests that that the stub method generator accounts for concrete +-// types that have type parameters defined. +-var _ io.ReaderFrom = &genReader[string, int]{} //@suggestedfix("&genReader", re"does not implement", generic_receiver) - --go 1.14 ---- foo/blah/blah.go -- --package blah +-type genReader[T, Y any] struct { +- T T +- Y Y +-} +--- @generic_receiver/generic_receiver.go -- +-@@ -13 +13,5 @@ +-+ +-+// ReadFrom implements io.ReaderFrom. +-+func (g *genReader[T, Y]) ReadFrom(r io.Reader) (n int64, err error) { +-+ panic("unimplemented") +-+} +--- ignored_imports.go -- +-package stub - --const Name = "" ---- foo/main.go -- --package main +-import ( +- "compress/zlib" +- . "io" +- _ "io" +-) - --import "mod.com/blah" +-// This file tests that dot-imports and underscore imports +-// are properly ignored and that a new import is added to +-// reference method types - --func main() { -- _ = blah.Name --} --` -- WithOptions( -- InGOPATH(), -- EnvVars{"GO111MODULE": "auto"}, -- ).Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("foo/main.go") -- env.RemoveWorkspaceFile("foo/go.mod") -- env.AfterChange( -- Diagnostics(env.AtRegexp("foo/main.go", `"mod.com/blah"`)), -- ) -- env.RegexpReplace("foo/main.go", `"mod.com/blah"`, `"foo/blah"`) -- env.AfterChange( -- NoDiagnostics(ForFile("foo/main.go")), -- ) -- }) --} +-var ( +- _ Reader +- _ zlib.Resetter = (*ignoredResetter)(nil) //@suggestedfix("(", re"does not implement", ignored_imports) +-) - --func TestNewSymbolInTestVariant(t *testing.T) { -- const files = ` ---- go.mod -- --module mod.com +-type ignoredResetter struct{} +--- @ignored_imports/ignored_imports.go -- +-@@ -19 +19,5 @@ +-+ +-+// Reset implements zlib.Resetter. +-+func (i *ignoredResetter) Reset(r Reader, dict []byte) error { +-+ panic("unimplemented") +-+} +--- issue2606.go -- +-package stub - --go 1.12 ---- a/a.go -- --package a +-type I interface{ error } - --func bob() {} ---- a/a_test.go -- --package a +-type C int - --import "testing" +-var _ I = C(0) //@suggestedfix("C", re"does not implement", issue2606) +--- @issue2606/issue2606.go -- +-@@ -7 +7,5 @@ +-+// Error implements I. +-+func (c C) Error() string { +-+ panic("unimplemented") +-+} +-+ +--- multi_var.go -- +-package stub - --func TestBob(t *testing.T) { -- bob() --} --` -- Run(t, files, func(t *testing.T, env *Env) { -- // Add a new symbol to the package under test and use it in the test -- // variant. Expect no diagnostics. -- env.WriteWorkspaceFiles(map[string]string{ -- "a/a.go": `package a +-import "io" - --func bob() {} --func george() {} --`, -- "a/a_test.go": `package a +-// This test ensures that a variable declaration that +-// has multiple values on the same line can still be +-// analyzed correctly to target the interface implementation +-// diagnostic. +-var one, two, three io.Reader = nil, &multiVar{}, nil //@suggestedfix("&", re"does not implement", multi_var) - --import "testing" +-type multiVar struct{} +--- @multi_var/multi_var.go -- +-@@ -12 +12,5 @@ +-+ +-+// Read implements io.Reader. +-+func (m *multiVar) Read(p []byte) (n int, err error) { +-+ panic("unimplemented") +-+} +--- pointer.go -- +-package stub - --func TestAll(t *testing.T) { -- bob() -- george() +-import "io" +- +-func getReaderFrom() io.ReaderFrom { +- return &pointerImpl{} //@suggestedfix("&", re"does not implement", pointer) -} --`, -- }) -- env.AfterChange( -- NoDiagnostics(ForFile("a/a.go")), -- NoDiagnostics(ForFile("a/a_test.go")), -- ) -- // Now, add a new file to the test variant and use its symbol in the -- // original test file. Expect no diagnostics. -- env.WriteWorkspaceFiles(map[string]string{ -- "a/a_test.go": `package a - --import "testing" +-type pointerImpl struct{} +--- @pointer/pointer.go -- +-@@ -10 +10,5 @@ +-+ +-+// ReadFrom implements io.ReaderFrom. +-+func (p *pointerImpl) ReadFrom(r io.Reader) (n int64, err error) { +-+ panic("unimplemented") +-+} +--- renamed_import.go -- +-package stub - --func TestAll(t *testing.T) { -- bob() -- george() -- hi() --} --`, -- "a/a2_test.go": `package a +-import ( +- "compress/zlib" +- myio "io" +-) - --import "testing" +-var _ zlib.Resetter = &myIO{} //@suggestedfix("&", re"does not implement", renamed_import) +-var _ myio.Reader - --func hi() {} +-type myIO struct{} +--- @renamed_import/renamed_import.go -- +-@@ -12 +12,5 @@ +-+ +-+// Reset implements zlib.Resetter. +-+func (m *myIO) Reset(r myio.Reader, dict []byte) error { +-+ panic("unimplemented") +-+} +--- renamed_import_iface.go -- +-package stub - --func TestSomething(t *testing.T) {} --`, -- }) -- env.AfterChange( -- NoDiagnostics(ForFile("a/a_test.go")), -- NoDiagnostics(ForFile("a/a2_test.go")), -- ) -- }) --} -diff -urN a/gopls/internal/regtest/workspace/adhoc_test.go b/gopls/internal/regtest/workspace/adhoc_test.go ---- a/gopls/internal/regtest/workspace/adhoc_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/workspace/adhoc_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,42 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +-import ( +- "golang.org/lsptests/stub/other" +-) - --package workspace +-// This file tests that if an interface +-// method references an import from its own package +-// that the concrete type does not yet import, and that import happens +-// to be renamed, then we prefer the renaming of the interface. +-var _ other.Interface = &otherInterfaceImpl{} //@suggestedfix("&otherInterfaceImpl", re"does not implement", renamed_import_iface) +- +-type otherInterfaceImpl struct{} +--- @renamed_import_iface/renamed_import_iface.go -- +-@@ -4 +4,2 @@ +-+ "bytes" +-+ "context" +-@@ -14 +16,5 @@ +-+ +-+// Get implements other.Interface. +-+func (o *otherInterfaceImpl) Get(context.Context) *bytes.Buffer { +-+ panic("unimplemented") +-+} +--- stdlib.go -- +-package stub - -import ( -- "testing" +- "io" +-) - -- . "golang.org/x/tools/gopls/internal/lsp/regtest" -- "golang.org/x/tools/internal/testenv" +-var _ io.Writer = writer{} //@suggestedfix("w", re"does not implement", stdlib) +- +-type writer struct{} +--- @stdlib/stdlib.go -- +-@@ -10 +10,5 @@ +-+ +-+// Write implements io.Writer. +-+func (w writer) Write(p []byte) (n int, err error) { +-+ panic("unimplemented") +-+} +--- typedecl_group.go -- +-package stub +- +-// Regression test for Issue #56825: file corrupted by insertion of +-// methods after TypeSpec in a parenthesized TypeDecl. +- +-import "io" +- +-func newReadCloser() io.ReadCloser { +- return rdcloser{} //@suggestedfix("rd", re"does not implement", typedecl_group) +-} +- +-type ( +- A int +- rdcloser struct{} +- B int -) - --// Test for golang/go#57209: editing a file in an ad-hoc package should not --// trigger conflicting diagnostics. --func TestAdhoc_Edits(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) +-func _() { +- // Local types can't be stubbed as there's nowhere to put the methods. +- // The suggestedfix assertion can't express this yet. TODO(adonovan): support it. +- type local struct{} +- var _ io.ReadCloser = local{} //@diag("local", re"does not implement") +-} +--- @typedecl_group/typedecl_group.go -- +-@@ -18 +18,10 @@ +-+// Close implements io.ReadCloser. +-+func (r rdcloser) Close() error { +-+ panic("unimplemented") +-+} +-+ +-+// Read implements io.ReadCloser. +-+func (r rdcloser) Read(p []byte) (n int, err error) { +-+ panic("unimplemented") +-+} +-+ +diff -urN a/gopls/internal/test/marker/testdata/suggestedfix/undeclaredfunc.txt b/gopls/internal/test/marker/testdata/suggestedfix/undeclaredfunc.txt +--- a/gopls/internal/test/marker/testdata/suggestedfix/undeclaredfunc.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/suggestedfix/undeclaredfunc.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,19 +0,0 @@ +-This test checks the quick fix for "undeclared: f" that declares the +-missing function. See #47558. - -- const files = ` ---- a.go -- --package foo +-TODO(adonovan): infer the result variables from the context (int, in this case). - --const X = 1 +--- a.go -- +-package a - ---- b.go -- --package foo +-func _() int { return f(1, "") } //@suggestedfix(re"f.1", re"unde(fined|clared name): f", x) - --// import "errors" +--- @x/a.go -- +-@@ -3 +3 @@ +--func _() int { return f(1, "") } //@suggestedfix(re"f.1", re"unde(fined|clared name): f", x) +-+func _() int { return f(1, "") } +-@@ -5 +5,4 @@ +-+func f(i int, s string) { +-+ panic("unimplemented") +-+} //@suggestedfix(re"f.1", re"unde(fined|clared name): f", x) +-+ +diff -urN a/gopls/internal/test/marker/testdata/suggestedfix/undeclared.txt b/gopls/internal/test/marker/testdata/suggestedfix/undeclared.txt +--- a/gopls/internal/test/marker/testdata/suggestedfix/undeclared.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/suggestedfix/undeclared.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,42 +0,0 @@ +-Tests of suggested fixes for "undeclared name" diagnostics, +-which are of ("compiler", "error") type. - --const Y = X --` +--- go.mod -- +-module example.com +-go 1.12 - -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("b.go") +--- a.go -- +-package p - -- for i := 0; i < 10; i++ { -- env.RegexpReplace("b.go", `// import "errors"`, `import "errors"`) -- env.RegexpReplace("b.go", `import "errors"`, `// import "errors"`) -- env.AfterChange(NoDiagnostics()) -- } -- }) +-func a() { +- z, _ := 1+y, 11 //@suggestedfix("y", re"(undeclared name|undefined): y", a) +- _ = z -} -diff -urN a/gopls/internal/regtest/workspace/broken_test.go b/gopls/internal/regtest/workspace/broken_test.go ---- a/gopls/internal/regtest/workspace/broken_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/workspace/broken_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,263 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package workspace +--- @a/a.go -- +-@@ -4 +4 @@ +-+ y := +--- b.go -- +-package p - --import ( -- "strings" -- "testing" +-func b() { +- if 100 < 90 { +- } else if 100 > n+2 { //@suggestedfix("n", re"(undeclared name|undefined): n", b) +- } +-} - -- "golang.org/x/tools/gopls/internal/lsp" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" -- "golang.org/x/tools/internal/testenv" --) +--- @b/b.go -- +-@@ -4 +4 @@ +-+ n := +--- c.go -- +-package p - --// This file holds various tests for UX with respect to broken workspaces. --// --// TODO: consolidate other tests here. --// --// TODO: write more tests: --// - an explicit GOWORK value that doesn't exist --// - using modules and/or GOWORK inside of GOPATH? +-func c() { +- for i < 200 { //@suggestedfix("i", re"(undeclared name|undefined): i", c) +- } +- r() //@diag("r", re"(undeclared name|undefined): r") +-} - --// Test for golang/go#53933 --func TestBrokenWorkspace_DuplicateModules(t *testing.T) { -- // The go command error message was improved in Go 1.20 to mention multiple -- // modules. -- testenv.NeedsGo1Point(t, 20) +--- @c/c.go -- +-@@ -4 +4 @@ +-+ i := +diff -urN a/gopls/internal/test/marker/testdata/suggestedfix/unusedrequire_gowork.txt b/gopls/internal/test/marker/testdata/suggestedfix/unusedrequire_gowork.txt +--- a/gopls/internal/test/marker/testdata/suggestedfix/unusedrequire_gowork.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/suggestedfix/unusedrequire_gowork.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,48 +0,0 @@ +-This test checks the suggested fix to remove unused require statements from +-go.mod files, when a go.work file is used. - -- // This proxy module content is replaced by the workspace, but is still -- // required for module resolution to function in the Go command. -- const proxy = ` ---- example.com/foo@v0.0.1/go.mod -- --module example.com/foo +-Note that unlike unusedrequire.txt, we need not write go.sum files when +-a go.work file is used. - --go 1.12 --` +--- proxy/example.com@v1.0.0/x.go -- +-package pkg +-const X = 1 - -- const src = ` --- go.work -- --go 1.18 +-go 1.21 - -use ( -- ./package1 -- ./package1/vendor/example.com/foo -- ./package2 -- ./package2/vendor/example.com/foo +- ./a +- ./b -) +--- a/go.mod -- +-module mod.com/a - ---- package1/go.mod -- --module mod.test +-go 1.14 - --go 1.18 +-require example.com v1.0.0 //@suggestedfix("require", re"not used", a) - --require example.com/foo v0.0.1 ---- package1/main.go -- +--- @a/a/go.mod -- +-@@ -4,3 +4 @@ +-- +--require example.com v1.0.0 //@suggestedfix("require", re"not used", a) +-- +--- a/main.go -- -package main +-func main() {} - --import "example.com/foo" +--- b/go.mod -- +-module mod.com/b - --func main() { -- _ = foo.CompleteMe --} ---- package1/vendor/example.com/foo/go.mod -- --module example.com/foo +-go 1.14 - --go 1.18 ---- package1/vendor/example.com/foo/foo.go -- --package foo +-require example.com v1.0.0 //@suggestedfix("require", re"not used", b) - --const CompleteMe = 111 ---- package2/go.mod -- --module mod2.test +--- @b/b/go.mod -- +-@@ -4,3 +4 @@ +-- +--require example.com v1.0.0 //@suggestedfix("require", re"not used", b) +-- +--- b/main.go -- +-package main +-func main() {} +diff -urN a/gopls/internal/test/marker/testdata/suggestedfix/unusedrequire.txt b/gopls/internal/test/marker/testdata/suggestedfix/unusedrequire.txt +--- a/gopls/internal/test/marker/testdata/suggestedfix/unusedrequire.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/suggestedfix/unusedrequire.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,25 +0,0 @@ +-This test checks the suggested fix to remove unused require statements from +-go.mod files. - --go 1.18 +--- flags -- +--write_sumfile=a - --require example.com/foo v0.0.1 ---- package2/main.go -- --package main +--- proxy/example.com@v1.0.0/x.go -- +-package pkg +-const X = 1 - --import "example.com/foo" +--- a/go.mod -- +-module mod.com - --func main() { -- _ = foo.CompleteMe --} ---- package2/vendor/example.com/foo/go.mod -- --module example.com/foo +-go 1.14 - --go 1.18 ---- package2/vendor/example.com/foo/foo.go -- --package foo +-require example.com v1.0.0 //@suggestedfix("require", re"not used", a) - --const CompleteMe = 222 --` +--- @a/a/go.mod -- +-@@ -4,3 +4 @@ +-- +--require example.com v1.0.0 //@suggestedfix("require", re"not used", a) +-- +--- a/main.go -- +-package main +-func main() {} +diff -urN a/gopls/internal/test/marker/testdata/symbol/basic.txt b/gopls/internal/test/marker/testdata/symbol/basic.txt +--- a/gopls/internal/test/marker/testdata/symbol/basic.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/symbol/basic.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,114 +0,0 @@ +-Basic tests of textDocument/documentSymbols. - -- WithOptions( -- ProxyFiles(proxy), -- ).Run(t, src, func(t *testing.T, env *Env) { -- env.OpenFile("package1/main.go") -- env.AfterChange( -- OutstandingWork(lsp.WorkspaceLoadFailure, `module example.com/foo appears multiple times in workspace`), -- ) +--- symbol.go -- +-package main - -- // Remove the redundant vendored copy of example.com. -- env.WriteWorkspaceFile("go.work", `go 1.18 -- use ( -- ./package1 -- ./package2 -- ./package2/vendor/example.com/foo -- ) -- `) -- env.AfterChange(NoOutstandingWork(IgnoreTelemetryPromptWork)) +-//@symbol(want) - -- // Check that definitions in package1 go to the copy vendored in package2. -- location := string(env.GoToDefinition(env.RegexpSearch("package1/main.go", "CompleteMe")).URI) -- const wantLocation = "package2/vendor/example.com/foo/foo.go" -- if !strings.HasSuffix(location, wantLocation) { -- t.Errorf("got definition of CompleteMe at %q, want %q", location, wantLocation) -- } -- }) --} +-import "io" - --// Test for golang/go#43186: correcting the module path should fix errors --// without restarting gopls. --func TestBrokenWorkspace_WrongModulePath(t *testing.T) { -- const files = ` ---- go.mod -- --module mod.testx +-var _ = 1 - --go 1.18 ---- p/internal/foo/foo.go -- --package foo +-var x = 42 - --const C = 1 ---- p/internal/bar/bar.go -- --package bar +-var nested struct { +- nestedField struct { +- f int +- } +-} - --import "mod.test/p/internal/foo" +-const y = 43 - --const D = foo.C + 1 ---- p/internal/bar/bar_test.go -- --package bar_test +-type Number int - --import ( -- "mod.test/p/internal/foo" -- . "mod.test/p/internal/bar" --) +-type Alias = string - --const E = D + foo.C ---- p/internal/baz/baz_test.go -- --package baz_test +-type NumberAlias = Number - --import ( -- named "mod.test/p/internal/bar" +-type ( +- Boolean bool +- BoolAlias = bool -) - --const F = named.D - 3 --` +-type Foo struct { +- Quux +- W io.Writer +- Bar int +- baz string +- funcField func(int) int +-} - -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("p/internal/bar/bar.go") -- env.AfterChange( -- Diagnostics(env.AtRegexp("p/internal/bar/bar.go", "\"mod.test/p/internal/foo\"")), -- ) -- env.OpenFile("go.mod") -- env.RegexpReplace("go.mod", "mod.testx", "mod.test") -- env.SaveBuffer("go.mod") // saving triggers a reload -- env.AfterChange(NoDiagnostics()) -- }) +-type Quux struct { +- X, Y float64 -} - --func TestMultipleModules_Warning(t *testing.T) { -- msgForVersion := func(ver int) string { -- if ver >= 18 { -- return `gopls was not able to find modules in your workspace.` -- } else { -- return `gopls requires a module at the root of your workspace.` -- } -- } +-type EmptyStruct struct{} - -- const modules = ` ---- a/go.mod -- --module a.com +-func (f Foo) Baz() string { +- return f.baz +-} - --go 1.12 ---- a/a.go -- --package a ---- a/empty.go -- --// an empty file ---- b/go.mod -- --module b.com +-func _() {} - --go 1.12 ---- b/b.go -- --package b --` -- for _, go111module := range []string{"on", "auto"} { -- t.Run("GO111MODULE="+go111module, func(t *testing.T) { -- WithOptions( -- Modes(Default), -- EnvVars{"GO111MODULE": go111module}, -- ).Run(t, modules, func(t *testing.T, env *Env) { -- ver := env.GoVersion() -- msg := msgForVersion(ver) -- env.OpenFile("a/a.go") -- env.OpenFile("a/empty.go") -- env.OpenFile("b/go.mod") -- env.AfterChange( -- Diagnostics(env.AtRegexp("a/a.go", "package a")), -- Diagnostics(env.AtRegexp("b/go.mod", "module b.com")), -- OutstandingWork(lsp.WorkspaceLoadFailure, msg), -- ) +-func (q *Quux) Do() {} - -- // Changing the workspace folders to the valid modules should resolve -- // the workspace errors and diagnostics. -- // -- // TODO(rfindley): verbose work tracking doesn't follow changing the -- // workspace folder, therefore we can't invoke AfterChange here. -- env.ChangeWorkspaceFolders("a", "b") -- env.Await( -- NoDiagnostics(ForFile("a/a.go")), -- NoDiagnostics(ForFile("b/go.mod")), -- NoOutstandingWork(IgnoreTelemetryPromptWork), -- ) +-func main() { +-} - -- env.ChangeWorkspaceFolders(".") +-type Stringer interface { +- String() string +-} - -- // TODO(rfindley): when GO111MODULE=auto, we need to open or change a -- // file here in order to detect a critical error. This is because gopls -- // has forgotten about a/a.go, and therefore doesn't hit the heuristic -- // "all packages are command-line-arguments". -- // -- // This is broken, and could be fixed by adjusting the heuristic to -- // account for the scenario where there are *no* workspace packages, or -- // (better) trying to get workspace packages for each open file. See -- // also golang/go#54261. -- env.OpenFile("b/b.go") -- env.AfterChange( -- // TODO(rfindley): fix these missing diagnostics. -- // Diagnostics(env.AtRegexp("a/a.go", "package a")), -- // Diagnostics(env.AtRegexp("b/go.mod", "module b.com")), -- Diagnostics(env.AtRegexp("b/b.go", "package b")), -- OutstandingWork(lsp.WorkspaceLoadFailure, msg), -- ) -- }) -- }) -- } +-type ABer interface { +- B() +- A() string +-} - -- // Expect no warning if GO111MODULE=auto in a directory in GOPATH. -- t.Run("GOPATH_GO111MODULE_auto", func(t *testing.T) { -- WithOptions( -- Modes(Default), -- EnvVars{"GO111MODULE": "auto"}, -- InGOPATH(), -- ).Run(t, modules, func(t *testing.T, env *Env) { -- env.OpenFile("a/a.go") -- env.AfterChange( -- NoDiagnostics(ForFile("a/a.go")), -- NoOutstandingWork(IgnoreTelemetryPromptWork), -- ) -- }) -- }) +-type WithEmbeddeds interface { +- Do() +- ABer +- io.Writer -} -diff -urN a/gopls/internal/regtest/workspace/directoryfilters_test.go b/gopls/internal/regtest/workspace/directoryfilters_test.go ---- a/gopls/internal/regtest/workspace/directoryfilters_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/workspace/directoryfilters_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,259 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package workspace +-type EmptyInterface interface{} - --import ( -- "sort" -- "strings" -- "testing" +-func Dunk() int { return 0 } - -- . "golang.org/x/tools/gopls/internal/lsp/regtest" -- "golang.org/x/tools/internal/testenv" --) +-func dunk() {} - --// This file contains regression tests for the directoryFilters setting. --// --// TODO: --// - consolidate some of these tests into a single test --// - add more tests for changing directory filters +--- @want -- +-(*Quux).Do "func()" +-(Foo).Baz "func() string" +2 lines +-ABer "interface{...}" +3 lines +-ABer.A "func() string" +-ABer.B "func()" +-Alias "string" +-BoolAlias "bool" +-Boolean "bool" +-Dunk "func() int" +-EmptyInterface "interface{}" +-EmptyStruct "struct{}" +-Foo "struct{...}" +6 lines +-Foo.Bar "int" +-Foo.Quux "Quux" +-Foo.W "io.Writer" +-Foo.baz "string" +-Foo.funcField "func(int) int" +-Number "int" +-NumberAlias "Number" +-Quux "struct{...}" +2 lines +-Quux.X "float64" +-Quux.Y "float64" +-Stringer "interface{...}" +2 lines +-Stringer.String "func() string" +-WithEmbeddeds "interface{...}" +4 lines +-WithEmbeddeds.ABer "ABer" +-WithEmbeddeds.Do "func()" +-WithEmbeddeds.Writer "io.Writer" +-dunk "func()" +-main "func()" +1 lines +-nested "struct{...}" +4 lines +-nested.nestedField "struct{...}" +2 lines +-nested.nestedField.f "int" +-x "" +-y "" +diff -urN a/gopls/internal/test/marker/testdata/symbol/generic.txt b/gopls/internal/test/marker/testdata/symbol/generic.txt +--- a/gopls/internal/test/marker/testdata/symbol/generic.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/symbol/generic.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,23 +0,0 @@ +-Basic tests of textDocument/documentSymbols with generics. - --func TestDirectoryFilters(t *testing.T) { -- WithOptions( -- ProxyFiles(workspaceProxy), -- WorkspaceFolders("pkg"), -- Settings{ -- "directoryFilters": []string{"-inner"}, -- }, -- ).Run(t, workspaceModule, func(t *testing.T, env *Env) { -- syms := env.Symbol("Hi") -- sort.Slice(syms, func(i, j int) bool { return syms[i].ContainerName < syms[j].ContainerName }) -- for _, s := range syms { -- if strings.Contains(s.ContainerName, "inner") { -- t.Errorf("WorkspaceSymbol: found symbol %q with container %q, want \"inner\" excluded", s.Name, s.ContainerName) -- } -- } -- }) +--- symbol.go -- +-//@symbol(want) +- +-package main +- +-type T[P any] struct { +- F P -} - --func TestDirectoryFiltersLoads(t *testing.T) { -- // exclude, and its error, should be excluded from the workspace. -- const files = ` ---- go.mod -- --module example.com +-type Constraint interface { +- ~int | struct{ int } +- interface{ M() } +-} - --go 1.12 ---- exclude/exclude.go -- --package exclude +--- @want -- +-Constraint "interface{...}" +3 lines +-Constraint.interface{...} "" +-Constraint.interface{...}.M "func()" +-Constraint.~int | struct{int} "" +-T "struct{...}" +2 lines +-T.F "P" +diff -urN a/gopls/internal/test/marker/testdata/token/comment.txt b/gopls/internal/test/marker/testdata/token/comment.txt +--- a/gopls/internal/test/marker/testdata/token/comment.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/token/comment.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,55 +0,0 @@ +-This test checks the semantic tokens in comments (golang/go#64648). - --const _ = Nonexistant --` +-There will be doc links in the comments to reference other objects. Parse these +-links and output tokens according to the referenced object types, so that the +-editor can highlight them. This will help in checking the doc link errors and +-reading comments in the code. - -- WithOptions( -- Settings{"directoryFilters": []string{"-exclude"}}, -- ).Run(t, files, func(t *testing.T, env *Env) { -- env.OnceMet( -- InitialWorkspaceLoad, -- NoDiagnostics(ForFile("exclude/x.go")), -- ) -- }) +--- settings.json -- +-{ +- "semanticTokens": true -} - --func TestDirectoryFiltersTransitiveDep(t *testing.T) { -- // Even though exclude is excluded from the workspace, it should -- // still be importable as a non-workspace package. -- const files = ` ---- go.mod -- --module example.com +--- a.go -- +-package p - --go 1.12 ---- include/include.go -- --package include --import "example.com/exclude" +-import "strconv" - --const _ = exclude.X ---- exclude/exclude.go -- --package exclude +-const A = 1 +-var B = 2 - --const _ = Nonexistant // should be ignored, since this is a non-workspace package --const X = 1 --` +-type Foo int +- +- +-// [F] accept a [Foo], and print it. //@token("F", "function", ""),token("Foo", "type", "") +-func F(v Foo) { +- println(v) - -- WithOptions( -- Settings{"directoryFilters": []string{"-exclude"}}, -- ).Run(t, files, func(t *testing.T, env *Env) { -- env.OnceMet( -- InitialWorkspaceLoad, -- NoDiagnostics(ForFile("exclude/exclude.go")), // filtered out -- NoDiagnostics(ForFile("include/include.go")), // successfully builds -- ) -- }) -} - --func TestDirectoryFiltersWorkspaceModules(t *testing.T) { -- // Define a module include.com which should be in the workspace, plus a -- // module exclude.com which should be excluded and therefore come from -- // the proxy. -- const files = ` ---- include/go.mod -- --module include.com +-/* +- [F1] print [A] and [B] //@token("F1", "function", ""),token("A", "variable", ""),token("B", "variable", "") +-*/ +-func F1() { +- // print [A] and [B]. //@token("A", "variable", ""),token("B", "variable", "") +- println(A, B) +-} - --go 1.12 +-// [F2] use [strconv.Atoi] convert s, then print it //@token("F2", "function", ""),token("strconv", "namespace", ""),token("Atoi", "function", "") +-func F2(s string) { +- a, _ := strconv.Atoi("42") +- b, _ := strconv.Atoi("42") +- println(a, b) // this is a tail comment in F2 //hover(F2, "F2", F2) +-} +--- b.go -- +-package p - --require exclude.com v1.0.0 +-// [F3] accept [*Foo] //@token("F3", "function", ""),token("Foo", "type", "") +-func F3(v *Foo) { +- println(*v) +-} - ---- include/go.sum -- --exclude.com v1.0.0 h1:Q5QSfDXY5qyNCBeUiWovUGqcLCRZKoTs9XdBeVz+w1I= --exclude.com v1.0.0/go.mod h1:hFox2uDlNB2s2Jfd9tHlQVfgqUiLVTmh6ZKat4cvnj4= +-// [F4] equal [strconv.Atoi] //@token("F4", "function", ""),token("strconv", "namespace", ""),token("Atoi", "function", "") +-func F4(s string) (int, error) { +- return 0, nil +-} +diff -urN a/gopls/internal/test/marker/testdata/token/range.txt b/gopls/internal/test/marker/testdata/token/range.txt +--- a/gopls/internal/test/marker/testdata/token/range.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/token/range.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,29 +0,0 @@ +-This test checks the output of textDocument/semanticTokens/range. - ---- include/include.go -- --package include +-TODO: add more assertions. - --import "exclude.com" +--- settings.json -- +-{ +- "semanticTokens": true +-} - --var _ = exclude.X // satisfied only by the workspace version ---- exclude/go.mod -- --module exclude.com +--- a.go -- +-package p //@token("package", "keyword", "") - --go 1.12 ---- exclude/exclude.go -- --package exclude +-const C = 42 //@token("C", "variable", "definition readonly") - --const X = 1 --` -- const proxy = ` ---- exclude.com@v1.0.0/go.mod -- --module exclude.com +-func F() { //@token("F", "function", "definition") +- x := 2 + 3//@token("x", "variable", "definition"),token("2", "number", ""),token("+", "operator", "") +- _ = x //@token("x", "variable", "") +- _ = F //@token("F", "function", "") +-} - --go 1.12 ---- exclude.com@v1.0.0/exclude.go -- --package exclude --` -- WithOptions( -- Modes(Experimental), -- ProxyFiles(proxy), -- Settings{"directoryFilters": []string{"-exclude"}}, -- ).Run(t, files, func(t *testing.T, env *Env) { -- env.Await(Diagnostics(env.AtRegexp("include/include.go", `exclude.(X)`))) -- }) +-func _() { +- // A goto's label cannot be found by ascending the syntax tree. +- goto loop //@ token("goto", "keyword", ""), token("loop", "label", "") +- +-loop: //@token("loop", "label", "definition") +- for { +- continue loop //@ token("continue", "keyword", ""), token("loop", "label", "") +- } -} +diff -urN a/gopls/internal/test/marker/testdata/typedef/typedef.txt b/gopls/internal/test/marker/testdata/typedef/typedef.txt +--- a/gopls/internal/test/marker/testdata/typedef/typedef.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/typedef/typedef.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,68 +0,0 @@ +-This test exercises the textDocument/typeDefinition action. - --// Test for golang/go#46438: support for '**' in directory filters. --func TestDirectoryFilters_Wildcard(t *testing.T) { -- filters := []string{"-**/bye"} -- WithOptions( -- ProxyFiles(workspaceProxy), -- WorkspaceFolders("pkg"), -- Settings{ -- "directoryFilters": filters, -- }, -- ).Run(t, workspaceModule, func(t *testing.T, env *Env) { -- syms := env.Symbol("Bye") -- sort.Slice(syms, func(i, j int) bool { return syms[i].ContainerName < syms[j].ContainerName }) -- for _, s := range syms { -- if strings.Contains(s.ContainerName, "bye") { -- t.Errorf("WorkspaceSymbol: found symbol %q with container %q with filters %v", s.Name, s.ContainerName, filters) +--- typedef.go -- +-package typedef +- +-type Struct struct { //@loc(Struct, "Struct"), +- Field string +-} +- +-type Int int //@loc(Int, "Int") +- +-func _() { +- var ( +- value Struct +- point *Struct +- ) +- _ = value //@typedef("value", Struct) +- _ = point //@typedef("point", Struct) +- +- var ( +- array [3]Struct +- slice []Struct +- ch chan Struct +- complex [3]chan *[5][]Int +- ) +- _ = array //@typedef("array", Struct) +- _ = slice //@typedef("slice", Struct) +- _ = ch //@typedef("ch", Struct) +- _ = complex //@typedef("complex", Int) +- +- var s struct { +- x struct { +- xx struct { +- field1 []Struct +- field2 []Int - } - } -- }) +- } +- _ = s.x.xx.field1 //@typedef("field1", Struct) +- _ = s.x.xx.field2 //@typedef("field2", Int) -} - --// Test for golang/go#52993: wildcard directoryFilters should apply to --// goimports scanning as well. --func TestDirectoryFilters_ImportScanning(t *testing.T) { -- const files = ` ---- go.mod -- --module mod.test +-func F1() Int { return 0 } +-func F2() (Int, float64) { return 0, 0 } +-func F3() (Struct, int, bool, error) { return Struct{}, 0, false, nil } +-func F4() (**int, Int, bool, *error) { return nil, 0, false, nil } +-func F5() (int, float64, error, Struct) { return 0, 0, nil, Struct{} } +-func F6() (int, float64, ***Struct, error) { return 0, 0, nil, nil } - --go 1.12 ---- main.go -- --package main +-func _() { +- F1() //@typedef("F1", Int) +- F2() //@typedef("F2", Int) +- F3() //@typedef("F3", Struct) +- F4() //@typedef("F4", Int) +- F5() //@typedef("F5", Struct) +- F6() //@typedef("F6", Struct) - --func main() { -- bye.Goodbye() +- f := func() Int { return 0 } +- f() //@typedef("f", Int) -} ---- p/bye/bye.go -- --package bye - --func Goodbye() {} --` +-// https://github.com/golang/go/issues/38589#issuecomment-620350922 +-func _() { +- type myFunc func(int) Int //@loc(myFunc, "myFunc") - -- WithOptions( -- Settings{ -- "directoryFilters": []string{"-**/bye"}, -- }, -- // This test breaks in 'Experimental' mode, because with -- // experimentalWorkspaceModule set we the goimports scan behaves -- // differently. -- // -- // Since this feature is going away (golang/go#52897), don't investigate. -- Modes(Default), -- ).Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- beforeSave := env.BufferText("main.go") -- env.OrganizeImports("main.go") -- got := env.BufferText("main.go") -- if got != beforeSave { -- t.Errorf("after organizeImports code action, got modified buffer:\n%s", got) -- } -- }) +- var foo myFunc +- _ = foo() //@typedef("foo", myFunc), diag(")", re"not enough arguments") -} +diff -urN a/gopls/internal/test/marker/testdata/workspacesymbol/allscope.txt b/gopls/internal/test/marker/testdata/workspacesymbol/allscope.txt +--- a/gopls/internal/test/marker/testdata/workspacesymbol/allscope.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/workspacesymbol/allscope.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,30 +0,0 @@ +-This test verifies behavior when "symbolScope" is set to "all". - --// Test for golang/go#52993: non-wildcard directoryFilters should still be --// applied relative to the workspace folder, not the module root. --func TestDirectoryFilters_MultiRootImportScanning(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) // uses go.work -- -- const files = ` ---- go.work -- --go 1.18 +--- settings.json -- +-{ +- "symbolStyle": "full", +- "symbolMatcher": "casesensitive", +- "symbolScope": "all" +-} - --use ( -- a -- b --) ---- a/go.mod -- --module mod1.test +--- go.mod -- +-module mod.test/symbols - -go 1.18 ---- a/main.go -- --package main - --func main() { -- hi.Hi() --} ---- a/hi/hi.go -- --package hi +--- query.go -- +-package symbols - --func Hi() {} ---- b/go.mod -- --module mod2.test +-//@workspacesymbol("fmt.Println", println) - --go 1.18 ---- b/main.go -- --package main +--- fmt/fmt.go -- +-package fmt - --func main() { -- hi.Hi() --} ---- b/hi/hi.go -- --package hi +-import "fmt" - --func Hi() {} --` +-func Println(s string) { +- fmt.Println(s) +-} +--- @println -- +-fmt/fmt.go:5:6-13 mod.test/symbols/fmt.Println Function +-<unknown> fmt.Println Function +diff -urN a/gopls/internal/test/marker/testdata/workspacesymbol/caseinsensitive.txt b/gopls/internal/test/marker/testdata/workspacesymbol/caseinsensitive.txt +--- a/gopls/internal/test/marker/testdata/workspacesymbol/caseinsensitive.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/workspacesymbol/caseinsensitive.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,26 +0,0 @@ +-This file contains test for symbol matches using the caseinsensitive matcher. - -- WithOptions( -- Settings{ -- "directoryFilters": []string{"-hi"}, // this test fails with -**/hi -- }, -- ).Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("a/main.go") -- beforeSave := env.BufferText("a/main.go") -- env.OrganizeImports("a/main.go") -- got := env.BufferText("a/main.go") -- if got == beforeSave { -- t.Errorf("after organizeImports code action, got identical buffer:\n%s", got) -- } -- }) +--- settings.json -- +-{ +- "symbolMatcher": "caseinsensitive" -} -diff -urN a/gopls/internal/regtest/workspace/fromenv_test.go b/gopls/internal/regtest/workspace/fromenv_test.go ---- a/gopls/internal/regtest/workspace/fromenv_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/workspace/fromenv_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,79 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package workspace +--- go.mod -- +-module mod.test/caseinsensitive - --import ( -- "fmt" -- "path/filepath" -- "testing" +-go 1.18 - -- . "golang.org/x/tools/gopls/internal/lsp/regtest" -- "golang.org/x/tools/internal/testenv" --) +--- p.go -- +-package caseinsensitive - --// Test that setting go.work via environment variables or settings works. --func TestUseGoWorkOutsideTheWorkspace(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) +-//@workspacesymbol("", blank) +-//@workspacesymbol("randomgophervar", randomgophervar) - -- // As discussed in -- // https://github.com/golang/go/issues/59458#issuecomment-1513794691, we must -- // use \-separated paths in go.work use directives for this test to work -- // correctly on windows. -- var files = fmt.Sprintf(` ---- work/a/go.mod -- --module a.com +-var RandomGopherVariableA int +-var randomgopherVariableB int +-var RandomGopherOtherVariable int - --go 1.12 ---- work/a/a.go -- --package a ---- work/b/go.mod -- --module b.com +--- @blank -- +--- @randomgophervar -- +-p.go:6:5-26 RandomGopherVariableA Variable +-p.go:7:5-26 randomgopherVariableB Variable +diff -urN a/gopls/internal/test/marker/testdata/workspacesymbol/casesensitive.txt b/gopls/internal/test/marker/testdata/workspacesymbol/casesensitive.txt +--- a/gopls/internal/test/marker/testdata/workspacesymbol/casesensitive.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/workspacesymbol/casesensitive.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,116 +0,0 @@ +-This file contains tests for symbol matches using the casesensitive matcher. - --go 1.12 ---- work/b/b.go -- --package b +-For historical reasons, it also verifies general behavior of the symbol search. - --func _() { -- x := 1 // unused +--- settings.json -- +-{ +- "symbolMatcher": "casesensitive" -} ---- other/c/go.mod -- --module c.com - --go 1.18 ---- other/c/c.go -- --package c ---- config/go.work -- +--- go.mod -- +-module mod.test/casesensitive +- -go 1.18 - --use ( -- %s -- %s -- %s +--- main.go -- +-package main +- +-//@workspacesymbol("main.main", main) +-//@workspacesymbol("p.Message", Message) +-//@workspacesymbol("main.myvar", myvar) +-//@workspacesymbol("main.myType", myType) +-//@workspacesymbol("main.myType.Blahblah", blahblah) +-//@workspacesymbol("main.myStruct", myStruct) +-//@workspacesymbol("main.myStruct.myStructField", myStructField) +-//@workspacesymbol("main.myInterface", myInterface) +-//@workspacesymbol("main.myInterface.DoSomeCoolStuff", DoSomeCoolStuff) +-//@workspacesymbol("main.embed.myStruct", embeddedStruct) +-//@workspacesymbol("main.embed.nestedStruct.nestedStruct2.int", int) +-//@workspacesymbol("main.embed.nestedInterface.myInterface", nestedInterface) +-//@workspacesymbol("main.embed.nestedInterface.nestedMethod", nestedMethod) +-//@workspacesymbol("dunk", dunk) +-//@workspacesymbol("Dunk", Dunk) +- +-import ( +- "encoding/json" +- "fmt" -) --`, -- filepath.Join("$SANDBOX_WORKDIR", "work", "a"), -- filepath.Join("$SANDBOX_WORKDIR", "work", "b"), -- filepath.Join("$SANDBOX_WORKDIR", "other", "c"), -- ) - -- WithOptions( -- WorkspaceFolders("work"), // use a nested workspace dir, so that GOWORK is outside the workspace -- EnvVars{"GOWORK": filepath.Join("$SANDBOX_WORKDIR", "config", "go.work")}, -- ).Run(t, files, func(t *testing.T, env *Env) { -- // When we have an explicit GOWORK set, we should get a file watch request. -- env.OnceMet( -- InitialWorkspaceLoad, -- FileWatchMatching(`other`), -- FileWatchMatching(`config.go\.work`), -- ) -- env.Await(FileWatchMatching(`config.go\.work`)) -- // Even though work/b is not open, we should get its diagnostics as it is -- // included in the workspace. -- env.OpenFile("work/a/a.go") -- env.AfterChange( -- Diagnostics(env.AtRegexp("work/b/b.go", "x := 1"), WithMessage("not used")), -- ) -- }) +-func main() { // function +- fmt.Println("Hello") -} -diff -urN a/gopls/internal/regtest/workspace/metadata_test.go b/gopls/internal/regtest/workspace/metadata_test.go ---- a/gopls/internal/regtest/workspace/metadata_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/workspace/metadata_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,245 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package workspace +-var myvar int // variable - --import ( -- "strings" -- "testing" +-type myType string // basic type - -- . "golang.org/x/tools/gopls/internal/lsp/regtest" -- "golang.org/x/tools/internal/testenv" --) +-type myDecoder json.Decoder // to use the encoding/json import - --// TODO(rfindley): move workspace tests related to metadata bugs into this --// file. +-func (m *myType) Blahblah() {} // method - --func TestFixImportDecl(t *testing.T) { -- const src = ` ---- go.mod -- --module mod.test +-type myStruct struct { // struct type +- myStructField int // struct field +-} - --go 1.12 ---- p.go -- --package p +-type myInterface interface { // interface +- DoSomeCoolStuff() string // interface method +-} - --import ( -- _ "fmt" +-type embed struct { +- myStruct - --const C = 42 --` +- nestedStruct struct { +- nestedField int - -- Run(t, src, func(t *testing.T, env *Env) { -- env.OpenFile("p.go") -- env.RegexpReplace("p.go", "\"fmt\"", "\"fmt\"\n)") -- env.AfterChange( -- NoDiagnostics(ForFile("p.go")), -- ) -- }) +- nestedStruct2 struct { +- int +- } +- } +- +- nestedInterface interface { +- myInterface +- nestedMethod() +- } -} - --// Test that moving ignoring a file via build constraints causes diagnostics to --// be resolved. --func TestIgnoreFile(t *testing.T) { -- testenv.NeedsGo1Point(t, 17) // needs native overlays and support for go:build directives +-func Dunk() int { return 0 } - -- const src = ` ---- go.mod -- --module mod.test +-func dunk() {} - --go 1.12 ---- foo.go -- --package main +--- p/p.go -- +-package p - --func main() {} ---- bar.go -- --package main +-const Message = "Hello World." // constant +--- @DoSomeCoolStuff -- +-main.go:41:2-17 main.myInterface.DoSomeCoolStuff Method +--- @Dunk -- +-main.go:61:6-10 Dunk Function +--- @Message -- +-p/p.go:3:7-14 p.Message Constant +--- @blahblah -- +-main.go:34:18-26 main.myType.Blahblah Method +--- @dunk -- +-main.go:63:6-10 dunk Function +--- @int -- +-main.go:51:4-7 main.embed.nestedStruct.nestedStruct2.int Field +--- @main -- +-main.go:24:6-10 main.main Function +--- @myInterface -- +-main.go:40:6-17 main.myInterface Interface +-main.go:41:2-17 main.myInterface.DoSomeCoolStuff Method +--- @myStruct -- +-main.go:36:6-14 main.myStruct Struct +-main.go:37:2-15 main.myStruct.myStructField Field +--- @myStructField -- +-main.go:37:2-15 main.myStruct.myStructField Field +--- @myType -- +-main.go:30:6-12 main.myType Class +-main.go:34:18-26 main.myType.Blahblah Method +--- @myvar -- +-main.go:28:5-10 main.myvar Variable +--- @nestedInterface -- +-main.go:56:3-14 main.embed.nestedInterface.myInterface Interface +--- @nestedMethod -- +-main.go:57:3-15 main.embed.nestedInterface.nestedMethod Method +--- @embeddedStruct -- +-main.go:45:2-10 main.embed.myStruct Field +diff -urN a/gopls/internal/test/marker/testdata/workspacesymbol/issue44806.txt b/gopls/internal/test/marker/testdata/workspacesymbol/issue44806.txt +--- a/gopls/internal/test/marker/testdata/workspacesymbol/issue44806.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/workspacesymbol/issue44806.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,27 +0,0 @@ +-This test verifies the fix for the crash encountered in golang/go#44806. - --func main() {} -- ` +--- go.mod -- +-module mod.test/symbol - -- Run(t, src, func(t *testing.T, env *Env) { -- env.OpenFile("foo.go") -- env.OpenFile("bar.go") -- env.OnceMet( -- env.DoneWithOpen(), -- Diagnostics(env.AtRegexp("foo.go", "func (main)")), -- Diagnostics(env.AtRegexp("bar.go", "func (main)")), -- ) +-go 1.18 +--- symbol.go -- +-package symbol - -- // Ignore bar.go. This should resolve diagnostics. -- env.RegexpReplace("bar.go", "package main", "//go:build ignore\n\npackage main") +-//@workspacesymbol("m", m) - -- // To make this test pass with experimentalUseInvalidMetadata, we could make -- // an arbitrary edit that invalidates the snapshot, at which point the -- // orphaned diagnostics will be invalidated. -- // -- // But of course, this should not be necessary: we should invalidate stale -- // information when fresh metadata arrives. -- // env.RegexpReplace("foo.go", "package main", "package main // test") -- env.AfterChange( -- NoDiagnostics(ForFile("foo.go")), -- NoDiagnostics(ForFile("bar.go")), -- ) +-type T struct{} - -- // If instead of 'ignore' (which gopls treats as a standalone package) we -- // used a different build tag, we should get a warning about having no -- // packages for bar.go -- env.RegexpReplace("bar.go", "ignore", "excluded") -- env.AfterChange( -- Diagnostics(env.AtRegexp("bar.go", "package (main)"), WithMessage("not included in your workspace")), -- ) -- }) --} +-// We should accept all valid receiver syntax when scanning symbols. +-func (*(T)) m1() {} +-func (*T) m2() {} +-func (T) m3() {} +-func ((T)) m4() {} +-func ((*T)) m5() {} - --func TestReinitializeRepeatedly(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) // uses go.work +--- @m -- +-symbol.go:8:13-15 T.m1 Method +-symbol.go:9:11-13 T.m2 Method +-symbol.go:10:10-12 T.m3 Method +-symbol.go:11:12-14 T.m4 Method +-symbol.go:12:13-15 T.m5 Method +-symbol.go:5:6-7 symbol.T Struct +diff -urN a/gopls/internal/test/marker/testdata/workspacesymbol/workspacesymbol.txt b/gopls/internal/test/marker/testdata/workspacesymbol/workspacesymbol.txt +--- a/gopls/internal/test/marker/testdata/workspacesymbol/workspacesymbol.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/workspacesymbol/workspacesymbol.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,72 +0,0 @@ +-This test contains tests for basic functionality of the workspace/symbol +-request. +- +-TODO(rfindley): add a test for the legacy 'fuzzy' symbol matcher using setting ("symbolMatcher": "fuzzy"). This test uses the default matcher ("fastFuzzy"). +- +--- go.mod -- +-module mod.test/symbols - -- const multiModule = ` ---- go.work -- -go 1.18 - --use ( -- moda/a -- modb --) ---- moda/a/go.mod -- --module a.com +--- query.go -- +-package symbols - --require b.com v1.2.3 ---- moda/a/go.sum -- --b.com v1.2.3 h1:tXrlXP0rnjRpKNmkbLYoWBdq0ikb3C3bKK9//moAWBI= --b.com v1.2.3/go.mod h1:D+J7pfFBZK5vdIdZEFquR586vKKIkqG7Qjw9AxG5BQ8= ---- moda/a/a.go -- --package a +-//@workspacesymbol("rgop", rgop) +-//@workspacesymbol("randoma", randoma) +-//@workspacesymbol("randomb", randomb) - --import ( -- "b.com/b" --) +--- a/a.go -- +-package a - --func main() { -- var x int -- _ = b.Hello() -- // AAA --} ---- modb/go.mod -- --module b.com +-var RandomGopherVariableA = "a" - ---- modb/b/b.go -- --package b +-const RandomGopherConstantA = "a" - --func Hello() int { -- var x int --} --` -- WithOptions( -- ProxyFiles(workspaceModuleProxy), -- Settings{ -- // For this test, we want workspace diagnostics to start immediately -- // during change processing. -- "diagnosticsDelay": "0", -- }, -- ).Run(t, multiModule, func(t *testing.T, env *Env) { -- env.OpenFile("moda/a/a.go") -- env.AfterChange() +-const ( +- randomgopherinvariable = iota +-) - -- // This test verifies that we fully process workspace reinitialization -- // (which allows GOPROXY), even when the reinitialized snapshot is -- // invalidated by subsequent changes. -- // -- // First, update go.work to remove modb. This will cause reinitialization -- // to fetch b.com from the proxy. -- env.WriteWorkspaceFile("go.work", "go 1.18\nuse moda/a") -- // Next, wait for gopls to start processing the change. Because we've set -- // diagnosticsDelay to zero, this will start diagnosing the workspace (and -- // try to reinitialize on the snapshot context). -- env.Await(env.StartedChangeWatchedFiles()) -- // Finally, immediately make a file change to cancel the previous -- // operation. This is racy, but will usually cause initialization to be -- // canceled. -- env.RegexpReplace("moda/a/a.go", "AAA", "BBB") -- env.AfterChange() -- // Now, to satisfy a definition request, gopls will try to reload moda. But -- // without access to the proxy (because this is no longer a -- // reinitialization), this loading will fail. -- loc := env.GoToDefinition(env.RegexpSearch("moda/a/a.go", "Hello")) -- got := env.Sandbox.Workdir.URIToPath(loc.URI) -- if want := "b.com@v1.2.3/b/b.go"; !strings.HasSuffix(got, want) { -- t.Errorf("expected %s, got %v", want, got) -- } -- }) --} +--- a/a_test.go -- +-package a - --// Test for golang/go#59458. With lazy module loading, we may not need --// transitively required modules. --func TestNestedModuleLoading_Issue59458(t *testing.T) { -- testenv.NeedsGo1Point(t, 17) // needs lazy module loading +-var RandomGopherTestVariableA = "a" - -- // In this test, module b.com/nested requires b.com/other, which in turn -- // requires b.com, but b.com/nested does not reach b.com through the package -- // graph. Therefore, b.com/nested does not need b.com on 1.17 and later, -- // thanks to graph pruning. -- // -- // We verify that we can load b.com/nested successfully. Previously, we -- // couldn't, because loading the pattern b.com/nested/... matched the module -- // b.com, which exists in the module graph but does not have a go.sum entry. +--- a/a_x_test.go -- +-package a_test - -- const proxy = ` ---- b.com@v1.2.3/go.mod -- --module b.com +-var RandomGopherXTestVariableA = "a" - --go 1.18 ---- b.com@v1.2.3/b/b.go -- +--- b/b.go -- -package b - --func Hello() {} +-var RandomGopherVariableB = "b" - ---- b.com/other@v1.4.6/go.mod -- --module b.com/other +-type RandomGopherStructB struct { +- Bar int +-} +- +--- @rgop -- +-b/b.go:5:6-25 RandomGopherStructB Struct +-a/a.go:5:7-28 RandomGopherConstantA Constant +-a/a.go:3:5-26 RandomGopherVariableA Variable +-b/b.go:3:5-26 RandomGopherVariableB Variable +-a/a_test.go:3:5-30 RandomGopherTestVariableA Variable +-a/a_x_test.go:3:5-31 RandomGopherXTestVariableA Variable +-a/a.go:8:2-24 randomgopherinvariable Constant +-b/b.go:6:2-5 RandomGopherStructB.Bar Field +--- @randoma -- +-a/a.go:5:7-28 RandomGopherConstantA Constant +-a/a.go:3:5-26 RandomGopherVariableA Variable +-b/b.go:3:5-26 RandomGopherVariableB Variable +-a/a.go:8:2-24 randomgopherinvariable Constant +-a/a_test.go:3:5-30 RandomGopherTestVariableA Variable +-a/a_x_test.go:3:5-31 RandomGopherXTestVariableA Variable +-b/b.go:6:2-5 RandomGopherStructB.Bar Field +--- @randomb -- +-b/b.go:5:6-25 RandomGopherStructB Struct +-a/a.go:3:5-26 RandomGopherVariableA Variable +-b/b.go:3:5-26 RandomGopherVariableB Variable +-a/a.go:8:2-24 randomgopherinvariable Constant +-a/a_test.go:3:5-30 RandomGopherTestVariableA Variable +-a/a_x_test.go:3:5-31 RandomGopherXTestVariableA Variable +-b/b.go:6:2-5 RandomGopherStructB.Bar Field +diff -urN a/gopls/internal/test/marker/testdata/workspacesymbol/wsscope.txt b/gopls/internal/test/marker/testdata/workspacesymbol/wsscope.txt +--- a/gopls/internal/test/marker/testdata/workspacesymbol/wsscope.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/workspacesymbol/wsscope.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,29 +0,0 @@ +-This test verifies behavior when "symbolScope" is set to "workspace". +- +--- settings.json -- +-{ +- "symbolStyle": "full", +- "symbolMatcher": "casesensitive", +- "symbolScope": "workspace" +-} +- +--- go.mod -- +-module mod.test/symbols - -go 1.18 - --require b.com v1.2.3 ---- b.com/other@v1.4.6/go.sun -- --b.com v1.2.3 h1:AGjCxWRJLUuJiZ21IUTByr9buoa6+B6Qh5LFhVLKpn4= ---- b.com/other@v1.4.6/bar/bar.go -- --package bar +--- query.go -- +-package symbols - --import "b.com/b" +-//@workspacesymbol("fmt.Println", println) - --func _() { -- b.Hello() --} ---- b.com/other@v1.4.6/foo/foo.go -- --package foo +--- fmt/fmt.go -- +-package fmt - --const Foo = 0 --` +-import "fmt" - -- const files = ` ---- go.mod -- --module b.com/nested +-func Println(s string) { +- fmt.Println(s) +-} +--- @println -- +-fmt/fmt.go:5:6-13 mod.test/symbols/fmt.Println Function +diff -urN a/gopls/internal/test/marker/testdata/zeroconfig/adhoc.txt b/gopls/internal/test/marker/testdata/zeroconfig/adhoc.txt +--- a/gopls/internal/test/marker/testdata/zeroconfig/adhoc.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/zeroconfig/adhoc.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,49 +0,0 @@ +-This test checks that gopls works with multiple ad-hoc packages, which lack +-a go.mod file. - --go 1.18 +-We should be able to import standard library packages, get diagnostics, and +-reference symbols defined in the same directory. - --require b.com/other v1.4.6 ---- go.sum -- --b.com/other v1.4.6 h1:pHXSzGsk6DamYXp9uRdDB9A/ZQqAN9it+JudU0sBf94= --b.com/other v1.4.6/go.mod h1:T0TYuGdAHw4p/l0+1P/yhhYHfZRia7PaadNVDu58OWM= ---- nested.go -- --package nested +--- main.go -- +-package main - --import "b.com/other/foo" +-import "fmt" - --const C = foo.Foo --` -- WithOptions( -- ProxyFiles(proxy), -- ).Run(t, files, func(t *testing.T, env *Env) { -- env.OnceMet( -- InitialWorkspaceLoad, -- NoDiagnostics(), -- ) -- }) +-func main() { +- fmt.Println(mainMsg) //@def("mainMsg", mainMsg) +- fmt.Println(undef) //@diag("undef", re"undefined|undeclared") -} -diff -urN a/gopls/internal/regtest/workspace/misspelling_test.go b/gopls/internal/regtest/workspace/misspelling_test.go ---- a/gopls/internal/regtest/workspace/misspelling_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/workspace/misspelling_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,80 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +--- main2.go -- +-package main - --package workspace +-const mainMsg = "main" //@loc(mainMsg, "mainMsg") - --import ( -- "runtime" -- "testing" +--- a/a.go -- +-package a - -- . "golang.org/x/tools/gopls/internal/lsp/regtest" -- "golang.org/x/tools/gopls/internal/lsp/tests/compare" --) +-import "fmt" - --// Test for golang/go#57081. --func TestFormattingMisspelledURI(t *testing.T) { -- if runtime.GOOS != "windows" && runtime.GOOS != "darwin" { -- t.Skip("golang/go#57081 only reproduces on case-insensitive filesystems.") -- } -- const files = ` ---- go.mod -- --module mod.test +-func _() { +- fmt.Println(aMsg) //@def("aMsg", aMsg) +- fmt.Println(undef) //@diag("undef", re"undefined|undeclared") +-} - --go 1.19 ---- foo.go -- --package foo +--- a/a2.go -- +-package a - --const C = 2 // extra space is intentional --` +-const aMsg = "a" //@loc(aMsg, "aMsg") - -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("Foo.go") -- env.FormatBuffer("Foo.go") -- want := env.BufferText("Foo.go") +--- b/b.go -- +-package b - -- if want == "" { -- t.Fatalf("Foo.go is empty") -- } +-import "fmt" - -- // In golang/go#57081, we observed that if overlay cases don't match, gopls -- // will find (and format) the on-disk contents rather than the overlay, -- // resulting in invalid edits. -- // -- // Verify that this doesn't happen, by confirming that formatting is -- // idempotent. -- env.FormatBuffer("Foo.go") -- got := env.BufferText("Foo.go") -- if diff := compare.Text(want, got); diff != "" { -- t.Errorf("invalid content after second formatting:\n%s", diff) -- } -- }) +-func _() { +- fmt.Println(bMsg) //@def("bMsg", bMsg) +- fmt.Println(undef) //@diag("undef", re"undefined|undeclared") -} - --// Test that we can find packages for open files with different spelling on --// case-insensitive file systems. --func TestPackageForMisspelledURI(t *testing.T) { -- t.Skip("golang/go#57081: this test fails because the Go command does not load Foo.go correctly") -- if runtime.GOOS != "windows" && runtime.GOOS != "darwin" { -- t.Skip("golang/go#57081 only reproduces on case-insensitive filesystems.") -- } -- const files = ` +--- b/b2.go -- +-package b +- +-const bMsg = "b" //@loc(bMsg, "bMsg") +diff -urN a/gopls/internal/test/marker/testdata/zeroconfig/dynamicports.txt b/gopls/internal/test/marker/testdata/zeroconfig/dynamicports.txt +--- a/gopls/internal/test/marker/testdata/zeroconfig/dynamicports.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/zeroconfig/dynamicports.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,118 +0,0 @@ +-This test checks that the zero-config algorithm selects Views to cover first +-class ports. +- +-In this test, package a imports b, and b imports c. Package a contains files +-constrained by go:build directives, package b contains files constrained by the +-GOOS matching their file name, and package c is unconstrained. Various +-assertions check that diagnostics and navigation work as expected. +- --- go.mod -- --module mod.test +-module golang.org/lsptests - --go 1.19 ---- foo.go -- --package foo +--- a/a.go -- +-package a - --const C = D ---- bar.go -- --package foo +-import "golang.org/lsptests/b" - --const D = 2 --` +-var _ = b.F //@loc(F, "F") - -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("Foo.go") -- env.AfterChange(NoDiagnostics()) -- }) --} -diff -urN a/gopls/internal/regtest/workspace/quickfix_test.go b/gopls/internal/regtest/workspace/quickfix_test.go ---- a/gopls/internal/regtest/workspace/quickfix_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/workspace/quickfix_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,342 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. +--- a/linux64.go -- +-//go:build (linux && amd64) - --package workspace +-package a - --import ( -- "strings" -- "testing" +-import "golang.org/lsptests/b" - -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/gopls/internal/lsp/tests/compare" -- "golang.org/x/tools/internal/testenv" +-var _ int = 1<<32 -1 // OK on 64 bit platforms. Compare linux32.go below. - -- . "golang.org/x/tools/gopls/internal/lsp/regtest" +-var ( +- _ = b.LinuxOnly //@def("LinuxOnly", LinuxOnly) +- _ = b.DarwinOnly //@diag("DarwinOnly", re"(undefined|declared)") +- _ = b.WindowsOnly //@diag("WindowsOnly", re"(undefined|declared)") -) - --func TestQuickFix_UseModule(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) // needs go.work +--- a/linux32.go -- +-//go:build (linux && 386) - -- const files = ` ---- go.work -- --go 1.20 +-package a - --use ( -- ./a --) ---- a/go.mod -- --module mod.com/a +-import "golang.org/lsptests/b" - --go 1.18 +-var _ int = 1<<32 -1 //@diag("1<<32", re"overflows") - ---- a/main.go -- --package main +-var ( +- _ = b.LinuxOnly //@def("LinuxOnly", LinuxOnly) +- _ = b.DarwinOnly //@diag("DarwinOnly", re"(undefined|declared)") +- _ = b.WindowsOnly //@diag("WindowsOnly", re"(undefined|declared)") +-) - --import "mod.com/a/lib" +--- a/darwin64.go -- +-//go:build (darwin && amd64) - --func main() { -- _ = lib.C --} +-package a - ---- a/lib/lib.go -- --package lib +-import "golang.org/lsptests/b" - --const C = "b" ---- b/go.mod -- --module mod.com/b +-var ( +- _ = b.LinuxOnly //@diag("LinuxOnly", re"(undefined|declared)") +- _ = b.DarwinOnly //@def("DarwinOnly", DarwinOnly) +- _ = b.WindowsOnly //@diag("WindowsOnly", re"(undefined|declared)") +-) - --go 1.18 +--- a/windows64.go -- +-//go:build (windows && amd64) - ---- b/main.go -- --package main +-package a - --import "mod.com/b/lib" +-import "golang.org/lsptests/b" - --func main() { -- _ = lib.C --} +-var ( +- _ = b.LinuxOnly //@diag("LinuxOnly", re"(undefined|declared)") +- _ = b.DarwinOnly //@diag("DarwinOnly", re"(undefined|declared)") +- _ = b.WindowsOnly //@def("WindowsOnly", WindowsOnly) +-) - ---- b/lib/lib.go -- --package lib +--- b/b_other.go -- +-//go:build !linux && !darwin && !windows +-package b - --const C = "b" --` +-func F() {} - -- for _, title := range []string{ -- "Use this module", -- "Use all modules", -- } { -- t.Run(title, func(t *testing.T) { -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("b/main.go") -- var d protocol.PublishDiagnosticsParams -- env.AfterChange(ReadDiagnostics("b/main.go", &d)) -- fixes := env.GetQuickFixes("b/main.go", d.Diagnostics) -- var toApply []protocol.CodeAction -- for _, fix := range fixes { -- if strings.Contains(fix.Title, title) { -- toApply = append(toApply, fix) -- } -- } -- if len(toApply) != 1 { -- t.Fatalf("codeAction: got %d quick fixes matching %q, want 1; got: %v", len(toApply), title, toApply) -- } -- env.ApplyCodeAction(toApply[0]) -- env.AfterChange(NoDiagnostics()) -- want := `go 1.20 +--- b/b_linux.go -- +-package b - --use ( -- ./a -- ./b --) --` -- got := env.ReadWorkspaceFile("go.work") -- if diff := compare.Text(want, got); diff != "" { -- t.Errorf("unexpeced go.work content:\n%s", diff) -- } -- }) -- }) -- } +-import "golang.org/lsptests/c" +- +-func F() { //@refs("F", "F", F) +- x := c.Common //@diag("x", re"not used"),def("Common", Common) -} - --func TestQuickFix_AddGoWork(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) // needs go.work +-const LinuxOnly = "darwin" //@loc(LinuxOnly, "LinuxOnly") - -- const files = ` ---- a/go.mod -- --module mod.com/a +--- b/b_darwin.go -- +-package b - --go 1.18 +-import "golang.org/lsptests/c" - ---- a/main.go -- --package main +-func F() { //@refs("F", "F", F) +- x := c.Common //@diag("x", re"not used"),def("Common", Common) +-} - --import "mod.com/a/lib" +-const DarwinOnly = "darwin" //@loc(DarwinOnly, "DarwinOnly") - --func main() { -- _ = lib.C +--- b/b_windows.go -- +-package b +- +-import "golang.org/lsptests/c" +- +-func F() { //@refs("F", "F", F) +- x := c.Common //@diag("x", re"not used"),def("Common", Common) -} - ---- a/lib/lib.go -- --package lib +-const WindowsOnly = "windows" //@loc(WindowsOnly, "WindowsOnly") - --const C = "b" ---- b/go.mod -- --module mod.com/b +--- c/c.go -- +-package c - --go 1.18 +-const Common = 0 //@loc(Common, "Common") - ---- b/main.go -- +diff -urN a/gopls/internal/test/marker/testdata/zeroconfig/nested.txt b/gopls/internal/test/marker/testdata/zeroconfig/nested.txt +--- a/gopls/internal/test/marker/testdata/zeroconfig/nested.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/zeroconfig/nested.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,61 +0,0 @@ +-This test checks that gopls works with nested modules, including multiple +-nested modules. +- +--- main.go -- -package main - --import "mod.com/b/lib" +-import "fmt" - -func main() { -- _ = lib.C +- fmt.Println(mainMsg) //@def("mainMsg", mainMsg) +- fmt.Println(undef) //@diag("undef", re"undefined|undeclared") -} +--- main2.go -- +-package main - ---- b/lib/lib.go -- --package lib -- --const C = "b" --` +-const mainMsg = "main" //@loc(mainMsg, "mainMsg") - -- tests := []struct { -- name string -- file string -- title string -- want string // expected go.work content, excluding go directive line -- }{ -- { -- "use b", -- "b/main.go", -- "Add a go.work file using this module", -- ` --use ./b --`, -- }, -- { -- "use a", -- "a/main.go", -- "Add a go.work file using this module", -- ` --use ./a --`, -- }, -- { -- "use all", -- "a/main.go", -- "Add a go.work file using all modules", -- ` --use ( -- ./a -- ./b --) --`, -- }, -- } +--- mod1/go.mod -- +-module golang.org/lsptests/mod1 - -- for _, test := range tests { -- t.Run(test.name, func(t *testing.T) { -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile(test.file) -- var d protocol.PublishDiagnosticsParams -- env.AfterChange(ReadDiagnostics(test.file, &d)) -- fixes := env.GetQuickFixes(test.file, d.Diagnostics) -- var toApply []protocol.CodeAction -- for _, fix := range fixes { -- if strings.Contains(fix.Title, test.title) { -- toApply = append(toApply, fix) -- } -- } -- if len(toApply) != 1 { -- t.Fatalf("codeAction: got %d quick fixes matching %q, want 1; got: %v", len(toApply), test.title, toApply) -- } -- env.ApplyCodeAction(toApply[0]) -- env.AfterChange( -- NoDiagnostics(ForFile(test.file)), -- ) +-go 1.20 - -- got := env.ReadWorkspaceFile("go.work") -- // Ignore the `go` directive, which we assume is on the first line of -- // the go.work file. This allows the test to be independent of go version. -- got = strings.Join(strings.Split(got, "\n")[1:], "\n") -- if diff := compare.Text(test.want, got); diff != "" { -- t.Errorf("unexpected go.work content:\n%s", diff) -- } -- }) -- }) -- } --} +--- mod1/a/a.go -- +-package a - --func TestQuickFix_UnsavedGoWork(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) // needs go.work +-import ( +- "fmt" +- "golang.org/lsptests/mod1/b" +-) - -- const files = ` ---- go.work -- --go 1.21 +-func _() { +- fmt.Println(b.Msg) //@def("Msg", Msg) +- fmt.Println(undef) //@diag("undef", re"undefined|undeclared") +-} - --use ( -- ./a --) ---- a/go.mod -- --module mod.com/a +--- mod1/b/b.go -- +-package b - --go 1.18 +-const Msg = "1" //@loc(Msg, "Msg") - ---- a/main.go -- --package main +--- mod2/go.mod -- +-module golang.org/lsptests/mod2 - --func main() {} ---- b/go.mod -- --module mod.com/b +-require golang.org/lsptests/mod1 v0.0.1 - --go 1.18 +-replace golang.org/lsptests/mod1 => ../mod1 - ---- b/main.go -- --package main +-go 1.20 - --func main() {} --` +--- mod2/c/c.go -- +-package c - -- for _, title := range []string{ -- "Use this module", -- "Use all modules", -- } { -- t.Run(title, func(t *testing.T) { -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("go.work") -- env.OpenFile("b/main.go") -- env.RegexpReplace("go.work", "go 1.21", "go 1.21 // arbitrary comment") -- var d protocol.PublishDiagnosticsParams -- env.AfterChange(ReadDiagnostics("b/main.go", &d)) -- fixes := env.GetQuickFixes("b/main.go", d.Diagnostics) -- var toApply []protocol.CodeAction -- for _, fix := range fixes { -- if strings.Contains(fix.Title, title) { -- toApply = append(toApply, fix) -- } -- } -- if len(toApply) != 1 { -- t.Fatalf("codeAction: got %d quick fixes matching %q, want 1; got: %v", len(toApply), title, toApply) -- } -- fix := toApply[0] -- err := env.Editor.ApplyCodeAction(env.Ctx, fix) -- if err == nil { -- t.Fatalf("codeAction(%q) succeeded unexpectedly", fix.Title) -- } +-import ( +- "fmt" +- "golang.org/lsptests/mod1/b" +-) - -- if got := err.Error(); !strings.Contains(got, "must save") { -- t.Errorf("codeAction(%q) returned error %q, want containing \"must save\"", fix.Title, err) -- } -- }) -- }) -- } +-func _() { +- fmt.Println(b.Msg) //@def("Msg", Msg) +- fmt.Println(undef) //@diag("undef", re"undefined|undeclared") -} +diff -urN a/gopls/internal/test/marker/testdata/zeroconfig/nonworkspacemodule.txt b/gopls/internal/test/marker/testdata/zeroconfig/nonworkspacemodule.txt +--- a/gopls/internal/test/marker/testdata/zeroconfig/nonworkspacemodule.txt 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/test/marker/testdata/zeroconfig/nonworkspacemodule.txt 1970-01-01 00:00:00.000000000 +0000 +@@ -1,79 +0,0 @@ +-This test checks that gopls works with modules that aren't included in the +-workspace file. - --func TestQuickFix_GOWORKOff(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) // needs go.work -- -- const files = ` --- go.work -- --go 1.21 +-go 1.20 - -use ( - ./a +- ./b -) ---- a/go.mod -- --module mod.com/a -- --go 1.18 -- ---- a/main.go -- --package main - --func main() {} ---- b/go.mod -- --module mod.com/b +--- a/go.mod -- +-module golang.org/lsptests/a - -go 1.18 - ---- b/main.go -- --package main -- --func main() {} --` -- -- for _, title := range []string{ -- "Use this module", -- "Use all modules", -- } { -- t.Run(title, func(t *testing.T) { -- WithOptions( -- EnvVars{"GOWORK": "off"}, -- ).Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("go.work") -- env.OpenFile("b/main.go") -- var d protocol.PublishDiagnosticsParams -- env.AfterChange(ReadDiagnostics("b/main.go", &d)) -- fixes := env.GetQuickFixes("b/main.go", d.Diagnostics) -- var toApply []protocol.CodeAction -- for _, fix := range fixes { -- if strings.Contains(fix.Title, title) { -- toApply = append(toApply, fix) -- } -- } -- if len(toApply) != 1 { -- t.Fatalf("codeAction: got %d quick fixes matching %q, want 1; got: %v", len(toApply), title, toApply) -- } -- fix := toApply[0] -- err := env.Editor.ApplyCodeAction(env.Ctx, fix) -- if err == nil { -- t.Fatalf("codeAction(%q) succeeded unexpectedly", fix.Title) -- } -- -- if got := err.Error(); !strings.Contains(got, "GOWORK=off") { -- t.Errorf("codeAction(%q) returned error %q, want containing \"GOWORK=off\"", fix.Title, err) -- } -- }) -- }) -- } --} -diff -urN a/gopls/internal/regtest/workspace/standalone_test.go b/gopls/internal/regtest/workspace/standalone_test.go ---- a/gopls/internal/regtest/workspace/standalone_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/workspace/standalone_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,206 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package workspace +--- a/a.go -- +-package a - -import ( -- "sort" -- "testing" -- -- "github.com/google/go-cmp/cmp" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" +- "fmt" +- "golang.org/lsptests/a/lib" -) - --func TestStandaloneFiles(t *testing.T) { -- const files = ` ---- go.mod -- --module mod.test +-func _() { +- fmt.Println(lib.Msg) //@def("Msg", aMsg) +- fmt.Println(undef) //@diag("undef", re"undefined|undeclared") +-} - --go 1.16 ---- lib/lib.go -- +--- a/lib/lib.go -- -package lib - --const K = 0 +-const Msg = "hi" //@loc(aMsg, "Msg") - --type I interface { -- M() --} ---- lib/ignore.go -- --//go:build ignore --// +build ignore +--- b/go.mod -- +-module golang.org/lsptests/b - --package main +-go 1.18 +- +--- b/b.go -- +-package b - -import ( -- "mod.test/lib" +- "fmt" +- "golang.org/lsptests/b/lib" -) - --const K = 1 -- --type Mer struct{} --func (Mer) M() -- -func main() { -- println(lib.K + K) +- fmt.Println(lib.Msg) //@def("Msg", bMsg) +- fmt.Println(undef) //@diag("undef", re"undefined|undeclared") -} --` -- WithOptions( -- // On Go 1.17 and earlier, this test fails with -- // experimentalWorkspaceModule. Not investigated, as -- // experimentalWorkspaceModule will be removed. -- Modes(Default), -- ).Run(t, files, func(t *testing.T, env *Env) { -- // Initially, gopls should not know about the standalone file as it hasn't -- // been opened. Therefore, we should only find one symbol 'K'. -- // -- // (The choice of "K" is a little sleazy: it was originally "C" until -- // we started adding "unsafe" to the workspace unconditionally, which -- // caused a spurious match of "unsafe.Slice". But in practice every -- // workspace depends on unsafe.) -- syms := env.Symbol("K") -- if got, want := len(syms), 1; got != want { -- t.Errorf("got %d symbols, want %d (%+v)", got, want, syms) -- } - -- // Similarly, we should only find one reference to "K", and no -- // implementations of I. -- checkLocations := func(method string, gotLocations []protocol.Location, wantFiles ...string) { -- var gotFiles []string -- for _, l := range gotLocations { -- gotFiles = append(gotFiles, env.Sandbox.Workdir.URIToPath(l.URI)) -- } -- sort.Strings(gotFiles) -- sort.Strings(wantFiles) -- if diff := cmp.Diff(wantFiles, gotFiles); diff != "" { -- t.Errorf("%s(...): unexpected locations (-want +got):\n%s", method, diff) -- } -- } +--- b/lib/lib.go -- +-package lib - -- env.OpenFile("lib/lib.go") -- env.AfterChange(NoDiagnostics()) +-const Msg = "hi" //@loc(bMsg, "Msg") - -- // Replacing K with D should not cause any workspace diagnostics, since we -- // haven't yet opened the standalone file. -- env.RegexpReplace("lib/lib.go", "K", "D") -- env.AfterChange(NoDiagnostics()) -- env.RegexpReplace("lib/lib.go", "D", "K") -- env.AfterChange(NoDiagnostics()) +--- c/go.mod -- +-module golang.org/lsptests/c - -- refs := env.References(env.RegexpSearch("lib/lib.go", "K")) -- checkLocations("References", refs, "lib/lib.go") +-go 1.18 - -- impls := env.Implementations(env.RegexpSearch("lib/lib.go", "I")) -- checkLocations("Implementations", impls) // no implementations +--- c/c.go -- +-package c - -- // Opening the standalone file should not result in any diagnostics. -- env.OpenFile("lib/ignore.go") -- env.AfterChange(NoDiagnostics()) +-import ( +- "fmt" +- "golang.org/lsptests/c/lib" +-) - -- // Having opened the standalone file, we should find its symbols in the -- // workspace. -- syms = env.Symbol("K") -- if got, want := len(syms), 2; got != want { -- t.Fatalf("got %d symbols, want %d", got, want) -- } +-func main() { +- fmt.Println(lib.Msg) //@def("Msg", cMsg) +- fmt.Println(undef) //@diag("undef", re"undefined|undeclared") +-} - -- foundMainK := false -- var symNames []string -- for _, sym := range syms { -- symNames = append(symNames, sym.Name) -- if sym.Name == "main.K" { -- foundMainK = true -- } -- } -- if !foundMainK { -- t.Errorf("WorkspaceSymbol(\"K\") = %v, want containing main.K", symNames) -- } +--- c/lib/lib.go -- +-package lib - -- // We should resolve workspace definitions in the standalone file. -- fileLoc := env.GoToDefinition(env.RegexpSearch("lib/ignore.go", "lib.(K)")) -- file := env.Sandbox.Workdir.URIToPath(fileLoc.URI) -- if got, want := file, "lib/lib.go"; got != want { -- t.Errorf("GoToDefinition(lib.K) = %v, want %v", got, want) -- } +-const Msg = "hi" //@loc(cMsg, "Msg") +diff -urN a/gopls/internal/util/astutil/purge.go b/gopls/internal/util/astutil/purge.go +--- a/gopls/internal/util/astutil/purge.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/util/astutil/purge.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,74 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- // ...as well as intra-file definitions -- loc := env.GoToDefinition(env.RegexpSearch("lib/ignore.go", "\\+ (K)")) -- wantLoc := env.RegexpSearch("lib/ignore.go", "const (K)") -- if loc != wantLoc { -- t.Errorf("GoToDefinition(K) = %v, want %v", loc, wantLoc) -- } +-// Package astutil provides various AST utility functions for gopls. +-package astutil - -- // Renaming "lib.K" to "lib.D" should cause a diagnostic in the standalone -- // file. -- env.RegexpReplace("lib/lib.go", "K", "D") -- env.AfterChange(Diagnostics(env.AtRegexp("lib/ignore.go", "lib.(K)"))) +-import ( +- "bytes" +- "go/scanner" +- "go/token" - -- // Undoing the replacement should fix diagnostics -- env.RegexpReplace("lib/lib.go", "D", "K") -- env.AfterChange(NoDiagnostics()) +- "golang.org/x/tools/gopls/internal/util/safetoken" +-) - -- // Now that our workspace has no errors, we should be able to find -- // references and rename. -- refs = env.References(env.RegexpSearch("lib/lib.go", "K")) -- checkLocations("References", refs, "lib/lib.go", "lib/ignore.go") +-// PurgeFuncBodies returns a copy of src in which the contents of each +-// outermost {...} region except struct and interface types have been +-// deleted. This reduces the amount of work required to parse the +-// top-level declarations. +-// +-// PurgeFuncBodies does not preserve newlines or position information. +-// Also, if the input is invalid, parsing the output of +-// PurgeFuncBodies may result in a different tree due to its effects +-// on parser error recovery. +-func PurgeFuncBodies(src []byte) []byte { +- // Destroy the content of any {...}-bracketed regions that are +- // not immediately preceded by a "struct" or "interface" +- // token. That includes function bodies, composite literals, +- // switch/select bodies, and all blocks of statements. +- // This will lead to non-void functions that don't have return +- // statements, which of course is a type error, but that's ok. - -- impls = env.Implementations(env.RegexpSearch("lib/lib.go", "I")) -- checkLocations("Implementations", impls, "lib/ignore.go") +- var out bytes.Buffer +- file := token.NewFileSet().AddFile("", -1, len(src)) +- var sc scanner.Scanner +- sc.Init(file, src, nil, 0) +- var prev token.Token +- var cursor int // last consumed src offset +- var braces []token.Pos // stack of unclosed braces or -1 for struct/interface type +- for { +- pos, tok, _ := sc.Scan() +- if tok == token.EOF { +- break +- } +- switch tok { +- case token.COMMENT: +- // TODO(adonovan): opt: skip, to save an estimated 20% of time. - -- // Renaming should rename in the standalone package. -- env.Rename(env.RegexpSearch("lib/lib.go", "K"), "D") -- env.RegexpSearch("lib/ignore.go", "lib.D") -- }) +- case token.LBRACE: +- if prev == token.STRUCT || prev == token.INTERFACE { +- pos = -1 +- } +- braces = append(braces, pos) +- +- case token.RBRACE: +- if last := len(braces) - 1; last >= 0 { +- top := braces[last] +- braces = braces[:last] +- if top < 0 { +- // struct/interface type: leave alone +- } else if len(braces) == 0 { // toplevel only +- // Delete {...} body. +- start, _ := safetoken.Offset(file, top) +- end, _ := safetoken.Offset(file, pos) +- out.Write(src[cursor : start+len("{")]) +- cursor = end +- } +- } +- } +- prev = tok +- } +- out.Write(src[cursor:]) +- return out.Bytes() -} +diff -urN a/gopls/internal/util/astutil/purge_test.go b/gopls/internal/util/astutil/purge_test.go +--- a/gopls/internal/util/astutil/purge_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/util/astutil/purge_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,89 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func TestStandaloneFiles_Configuration(t *testing.T) { -- const files = ` ---- go.mod -- --module mod.test +-package astutil_test - --go 1.18 ---- lib.go -- --package lib // without this package, files are loaded as command-line-arguments ---- ignore.go -- --//go:build ignore --// +build ignore +-import ( +- "go/ast" +- "go/parser" +- "go/token" +- "os" +- "reflect" +- "testing" - --package main +- "golang.org/x/tools/go/packages" +- "golang.org/x/tools/gopls/internal/util/astutil" +- "golang.org/x/tools/internal/testenv" +-) - --// An arbitrary comment. +-// TestPurgeFuncBodies tests PurgeFuncBodies by comparing it against a +-// (less efficient) reference implementation that purges after parsing. +-func TestPurgeFuncBodies(t *testing.T) { +- testenv.NeedsGoBuild(t) // we need the source code for std - --func main() {} ---- standalone.go -- --//go:build standalone --// +build standalone +- // Load a few standard packages. +- config := packages.Config{Mode: packages.NeedCompiledGoFiles} +- pkgs, err := packages.Load(&config, "encoding/...") +- if err != nil { +- t.Fatal(err) +- } - --package main +- // preorder returns the nodes of tree f in preorder. +- preorder := func(f *ast.File) (nodes []ast.Node) { +- ast.Inspect(f, func(n ast.Node) bool { +- if n != nil { +- nodes = append(nodes, n) +- } +- return true +- }) +- return nodes +- } - --func main() {} --` +- packages.Visit(pkgs, nil, func(p *packages.Package) { +- for _, filename := range p.CompiledGoFiles { +- content, err := os.ReadFile(filename) +- if err != nil { +- t.Fatal(err) +- } - -- WithOptions( -- Settings{ -- "standaloneTags": []string{"standalone", "script"}, -- }, -- ).Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("ignore.go") -- env.OpenFile("standalone.go") +- fset := token.NewFileSet() - -- env.AfterChange( -- Diagnostics(env.AtRegexp("ignore.go", "package (main)")), -- NoDiagnostics(ForFile("standalone.go")), -- ) +- // Parse then purge (reference implementation). +- f1, _ := parser.ParseFile(fset, filename, content, 0) +- ast.Inspect(f1, func(n ast.Node) bool { +- switch n := n.(type) { +- case *ast.FuncDecl: +- if n.Body != nil { +- n.Body.List = nil +- } +- case *ast.FuncLit: +- n.Body.List = nil +- case *ast.CompositeLit: +- n.Elts = nil +- } +- return true +- }) - -- cfg := env.Editor.Config() -- cfg.Settings = map[string]interface{}{ -- "standaloneTags": []string{"ignore"}, +- // Purge before parse (logic under test). +- f2, _ := parser.ParseFile(fset, filename, astutil.PurgeFuncBodies(content), 0) +- +- // Compare sequence of node types. +- nodes1 := preorder(f1) +- nodes2 := preorder(f2) +- if len(nodes2) < len(nodes1) { +- t.Errorf("purged file has fewer nodes: %d vs %d", +- len(nodes2), len(nodes1)) +- nodes1 = nodes1[:len(nodes2)] // truncate +- } +- for i := range nodes1 { +- x, y := nodes1[i], nodes2[i] +- if reflect.TypeOf(x) != reflect.TypeOf(y) { +- t.Errorf("%s: got %T, want %T", +- fset.Position(x.Pos()), y, x) +- break +- } +- } - } -- env.ChangeConfiguration(cfg) -- env.AfterChange( -- NoDiagnostics(ForFile("ignore.go")), -- Diagnostics(env.AtRegexp("standalone.go", "package (main)")), -- ) - }) -} -diff -urN a/gopls/internal/regtest/workspace/workspace_test.go b/gopls/internal/regtest/workspace/workspace_test.go ---- a/gopls/internal/regtest/workspace/workspace_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/regtest/workspace/workspace_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,1258 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/util/astutil/util.go b/gopls/internal/util/astutil/util.go +--- a/gopls/internal/util/astutil/util.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/util/astutil/util.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,69 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package workspace +-package astutil - -import ( -- "context" -- "fmt" -- "path/filepath" -- "strings" -- "testing" -- -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/hooks" -- "golang.org/x/tools/gopls/internal/lsp" -- "golang.org/x/tools/gopls/internal/lsp/fake" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- "golang.org/x/tools/internal/gocommand" -- "golang.org/x/tools/internal/testenv" +- "go/ast" +- "go/token" - -- . "golang.org/x/tools/gopls/internal/lsp/regtest" +- "golang.org/x/tools/internal/typeparams" -) - --func TestMain(m *testing.M) { -- bug.PanicOnBugs = true -- Main(m, hooks.Options) --} +-// UnpackRecv unpacks a receiver type expression, reporting whether it is a +-// pointer recever, along with the type name identifier and any receiver type +-// parameter identifiers. +-// +-// Copied (with modifications) from go/types. +-func UnpackRecv(rtyp ast.Expr) (ptr bool, rname *ast.Ident, tparams []*ast.Ident) { +-L: // unpack receiver type +- // This accepts invalid receivers such as ***T and does not +- // work for other invalid receivers, but we don't care. The +- // validity of receiver expressions is checked elsewhere. +- for { +- switch t := rtyp.(type) { +- case *ast.ParenExpr: +- rtyp = t.X +- case *ast.StarExpr: +- ptr = true +- rtyp = t.X +- default: +- break L +- } +- } - --const workspaceProxy = ` ---- example.com@v1.2.3/go.mod -- --module example.com +- // unpack type parameters, if any +- switch rtyp.(type) { +- case *ast.IndexExpr, *ast.IndexListExpr: +- var indices []ast.Expr +- rtyp, _, indices, _ = typeparams.UnpackIndexExpr(rtyp) +- for _, arg := range indices { +- var par *ast.Ident +- switch arg := arg.(type) { +- case *ast.Ident: +- par = arg +- default: +- // ignore errors +- } +- if par == nil { +- par = &ast.Ident{NamePos: arg.Pos(), Name: "_"} +- } +- tparams = append(tparams, par) +- } +- } - --go 1.12 ---- example.com@v1.2.3/blah/blah.go -- --package blah +- // unpack receiver name +- if name, _ := rtyp.(*ast.Ident); name != nil { +- rname = name +- } - --import "fmt" +- return +-} - --func SaySomething() { -- fmt.Println("something") +-// NodeContains returns true if a node encloses a given position pos. +-// +-// Precondition: n must not be nil. +-func NodeContains(n ast.Node, pos token.Pos) bool { +- return n.Pos() <= pos && pos <= n.End() -} ---- random.org@v1.2.3/go.mod -- --module random.org +diff -urN a/gopls/internal/util/browser/browser.go b/gopls/internal/util/browser/browser.go +--- a/gopls/internal/util/browser/browser.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/util/browser/browser.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,67 +0,0 @@ +-// Copyright 2016 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --go 1.12 ---- random.org@v1.2.3/bye/bye.go -- --package bye +-// Package browser provides utilities for interacting with users' browsers. +-package browser - --func Goodbye() { -- println("Bye") +-import ( +- "os" +- "os/exec" +- "runtime" +- "time" +-) +- +-// Commands returns a list of possible commands to use to open a url. +-func Commands() [][]string { +- var cmds [][]string +- if exe := os.Getenv("BROWSER"); exe != "" { +- cmds = append(cmds, []string{exe}) +- } +- switch runtime.GOOS { +- case "darwin": +- cmds = append(cmds, []string{"/usr/bin/open"}) +- case "windows": +- cmds = append(cmds, []string{"cmd", "/c", "start"}) +- default: +- if os.Getenv("DISPLAY") != "" { +- // xdg-open is only for use in a desktop environment. +- cmds = append(cmds, []string{"xdg-open"}) +- } +- } +- cmds = append(cmds, +- []string{"chrome"}, +- []string{"google-chrome"}, +- []string{"chromium"}, +- []string{"firefox"}, +- ) +- return cmds -} --` - --// TODO: Add a replace directive. --const workspaceModule = ` ---- pkg/go.mod -- --module mod.com +-// Open tries to open url in a browser and reports whether it succeeded. +-func Open(url string) bool { +- for _, args := range Commands() { +- cmd := exec.Command(args[0], append(args[1:], url)...) +- if cmd.Start() == nil && appearsSuccessful(cmd, 3*time.Second) { +- return true +- } +- } +- return false +-} - --go 1.14 +-// appearsSuccessful reports whether the command appears to have run successfully. +-// If the command runs longer than the timeout, it's deemed successful. +-// If the command runs within the timeout, it's deemed successful if it exited cleanly. +-func appearsSuccessful(cmd *exec.Cmd, timeout time.Duration) bool { +- errc := make(chan error, 1) +- go func() { +- errc <- cmd.Wait() +- }() - --require ( -- example.com v1.2.3 -- random.org v1.2.3 --) ---- pkg/go.sum -- --example.com v1.2.3 h1:veRD4tUnatQRgsULqULZPjeoBGFr2qBhevSCZllD2Ds= --example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= --random.org v1.2.3 h1:+JE2Fkp7gS0zsHXGEQJ7hraom3pNTlkxC4b2qPfA+/Q= --random.org v1.2.3/go.mod h1:E9KM6+bBX2g5ykHZ9H27w16sWo3QwgonyjM44Dnej3I= ---- pkg/main.go -- --package main +- select { +- case <-time.After(timeout): +- return true +- case err := <-errc: +- return err == nil +- } +-} +diff -urN a/gopls/internal/util/browser/README.md b/gopls/internal/util/browser/README.md +--- a/gopls/internal/util/browser/README.md 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/util/browser/README.md 1970-01-01 00:00:00.000000000 +0000 +@@ -1 +0,0 @@ +-This package is a copy of cmd/internal/browser from the go distribution +\ No newline at end of file +diff -urN a/gopls/internal/util/bug/bug.go b/gopls/internal/util/bug/bug.go +--- a/gopls/internal/util/bug/bug.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/util/bug/bug.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,145 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --import ( -- "example.com/blah" -- "mod.com/inner" -- "random.org/bye" --) +-// Package bug provides utilities for reporting internal bugs, and being +-// notified when they occur. +-// +-// Philosophically, because gopls runs as a sidecar process that the user does +-// not directly control, sometimes it keeps going on broken invariants rather +-// than panicking. In those cases, bug reports provide a mechanism to alert +-// developers and capture relevant metadata. +-package bug - --func main() { -- blah.SaySomething() -- inner.Hi() -- bye.Goodbye() --} ---- pkg/main2.go -- --package main +-import ( +- "fmt" +- "runtime" +- "runtime/debug" +- "sort" +- "sync" +- "time" - --import "fmt" +- "golang.org/x/telemetry/counter" +-) - --func _() { -- fmt.Print("%s") --} ---- pkg/inner/inner.go -- --package inner +-// PanicOnBugs controls whether to panic when bugs are reported. +-// +-// It may be set to true during testing. +-// +-// TODO(adonovan): should we make the default true, and +-// suppress it only in the product (gopls/main.go)? +-var PanicOnBugs = false - --import "example.com/blah" +-var ( +- mu sync.Mutex +- exemplars map[string]Bug +- handlers []func(Bug) +-) - --func Hi() { -- blah.SaySomething() +-// A Bug represents an unexpected event or broken invariant. They are used for +-// capturing metadata that helps us understand the event. +-// +-// Bugs are JSON-serializable. +-type Bug struct { +- File string // file containing the call to bug.Report +- Line int // line containing the call to bug.Report +- Description string // description of the bug +- Key string // key identifying the bug (file:line if available) +- Stack string // call stack +- AtTime time.Time // time the bug was reported -} ---- goodbye/bye/bye.go -- --package bye -- --func Bye() {} ---- goodbye/go.mod -- --module random.org -- --go 1.12 --` - --// Confirm that find references returns all of the references in the module, --// regardless of what the workspace root is. --func TestReferences(t *testing.T) { -- for _, tt := range []struct { -- name, rootPath string -- }{ -- { -- name: "module root", -- rootPath: "pkg", -- }, -- { -- name: "subdirectory", -- rootPath: "pkg/inner", -- }, -- } { -- t.Run(tt.name, func(t *testing.T) { -- opts := []RunOption{ProxyFiles(workspaceProxy)} -- if tt.rootPath != "" { -- opts = append(opts, WorkspaceFolders(tt.rootPath)) -- } -- WithOptions(opts...).Run(t, workspaceModule, func(t *testing.T, env *Env) { -- f := "pkg/inner/inner.go" -- env.OpenFile(f) -- locations := env.References(env.RegexpSearch(f, `SaySomething`)) -- want := 3 -- if got := len(locations); got != want { -- t.Fatalf("expected %v locations, got %v", want, got) -- } -- }) -- }) -- } +-// Reportf reports a formatted bug message. +-func Reportf(format string, args ...interface{}) { +- report(fmt.Sprintf(format, args...)) -} - --// Make sure that analysis diagnostics are cleared for the whole package when --// the only opened file is closed. This test was inspired by the experience in --// VS Code, where clicking on a reference result triggers a --// textDocument/didOpen without a corresponding textDocument/didClose. --func TestClearAnalysisDiagnostics(t *testing.T) { -- WithOptions( -- ProxyFiles(workspaceProxy), -- WorkspaceFolders("pkg/inner"), -- ).Run(t, workspaceModule, func(t *testing.T, env *Env) { -- env.OpenFile("pkg/main.go") -- env.AfterChange( -- Diagnostics(env.AtRegexp("pkg/main2.go", "fmt.Print")), -- ) -- env.CloseBuffer("pkg/main.go") -- env.AfterChange( -- NoDiagnostics(ForFile("pkg/main2.go")), -- ) -- }) +-// Errorf calls fmt.Errorf for the given arguments, and reports the resulting +-// error message as a bug. +-func Errorf(format string, args ...interface{}) error { +- err := fmt.Errorf(format, args...) +- report(err.Error()) +- return err -} - --// TestReloadOnlyOnce checks that changes to the go.mod file do not result in --// redundant package loads (golang/go#54473). --// --// Note that this test may be fragile, as it depends on specific structure to --// log messages around reinitialization. Nevertheless, it is important for --// guarding against accidentally duplicate reloading. --func TestReloadOnlyOnce(t *testing.T) { -- WithOptions( -- ProxyFiles(workspaceProxy), -- WorkspaceFolders("pkg"), -- ).Run(t, workspaceModule, func(t *testing.T, env *Env) { -- dir := env.Sandbox.Workdir.URI("goodbye").SpanURI().Filename() -- goModWithReplace := fmt.Sprintf(`%s --replace random.org => %s --`, env.ReadWorkspaceFile("pkg/go.mod"), dir) -- env.WriteWorkspaceFile("pkg/go.mod", goModWithReplace) -- env.Await( -- LogMatching(protocol.Info, `packages\.Load #\d+\n`, 2, false), -- ) -- }) +-// Report records a new bug encountered on the server. +-// It uses reflection to report the position of the immediate caller. +-func Report(description string) { +- report(description) -} - --const workspaceModuleProxy = ` ---- example.com@v1.2.3/go.mod -- --module example.com +-// BugReportCount is a telemetry counter that tracks # of bug reports. +-var BugReportCount = counter.NewStack("gopls/bug", 16) - --go 1.12 ---- example.com@v1.2.3/blah/blah.go -- --package blah +-func report(description string) { +- _, file, line, ok := runtime.Caller(2) // all exported reporting functions call report directly - --import "fmt" +- key := "<missing callsite>" +- if ok { +- key = fmt.Sprintf("%s:%d", file, line) +- } - --func SaySomething() { -- fmt.Println("something") --} ---- b.com@v1.2.3/go.mod -- --module b.com +- if PanicOnBugs { +- panic(fmt.Sprintf("%s: %s", key, description)) +- } - --go 1.12 ---- b.com@v1.2.3/b/b.go -- --package b +- bug := Bug{ +- File: file, +- Line: line, +- Description: description, +- Key: key, +- Stack: string(debug.Stack()), +- AtTime: time.Now(), +- } - --func Hello() {} --` +- newBug := false +- mu.Lock() +- if _, ok := exemplars[key]; !ok { +- if exemplars == nil { +- exemplars = make(map[string]Bug) +- } +- exemplars[key] = bug // capture one exemplar per key +- newBug = true +- } +- hh := handlers +- handlers = nil +- mu.Unlock() - --func TestAutomaticWorkspaceModule_Interdependent(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) // uses go.work -- const multiModule = ` ---- moda/a/go.mod -- --module a.com +- if newBug { +- BugReportCount.Inc() +- } +- // Call the handlers outside the critical section since a +- // handler may itself fail and call bug.Report. Since handlers +- // are one-shot, the inner call should be trivial. +- for _, handle := range hh { +- handle(bug) +- } +-} - --require b.com v1.2.3 ---- moda/a/go.sum -- --b.com v1.2.3 h1:tXrlXP0rnjRpKNmkbLYoWBdq0ikb3C3bKK9//moAWBI= --b.com v1.2.3/go.mod h1:D+J7pfFBZK5vdIdZEFquR586vKKIkqG7Qjw9AxG5BQ8= ---- moda/a/a.go -- --package a +-// Handle adds a handler function that will be called with the next +-// bug to occur on the server. The handler only ever receives one bug. +-// It is called synchronously, and should return in a timely manner. +-func Handle(h func(Bug)) { +- mu.Lock() +- defer mu.Unlock() +- handlers = append(handlers, h) +-} - --import ( -- "b.com/b" --) +-// List returns a slice of bug exemplars -- the first bugs to occur at each +-// callsite. +-func List() []Bug { +- mu.Lock() +- defer mu.Unlock() - --func main() { -- var x int -- _ = b.Hello() --} ---- modb/go.mod -- --module b.com +- var bugs []Bug - ---- modb/b/b.go -- --package b +- for _, bug := range exemplars { +- bugs = append(bugs, bug) +- } - --func Hello() int { -- var x int --} --` -- WithOptions( -- ProxyFiles(workspaceModuleProxy), -- ).Run(t, multiModule, func(t *testing.T, env *Env) { -- env.RunGoCommand("work", "init") -- env.RunGoCommand("work", "use", "-r", ".") -- env.AfterChange( -- Diagnostics(env.AtRegexp("moda/a/a.go", "x")), -- Diagnostics(env.AtRegexp("modb/b/b.go", "x")), -- NoDiagnostics(env.AtRegexp("moda/a/a.go", `"b.com/b"`)), -- ) +- sort.Slice(bugs, func(i, j int) bool { +- return bugs[i].Key < bugs[j].Key - }) --} - --func TestModuleWithExclude(t *testing.T) { -- const proxy = ` ---- c.com@v1.2.3/go.mod -- --module c.com +- return bugs +-} +diff -urN a/gopls/internal/util/bug/bug_test.go b/gopls/internal/util/bug/bug_test.go +--- a/gopls/internal/util/bug/bug_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/util/bug/bug_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,91 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --go 1.12 +-package bug - --require b.com v1.2.3 ---- c.com@v1.2.3/blah/blah.go -- --package blah +-import ( +- "encoding/json" +- "fmt" +- "testing" +- "time" - --import "fmt" +- "github.com/google/go-cmp/cmp" +-) - --func SaySomething() { -- fmt.Println("something") +-func resetForTesting() { +- exemplars = nil +- handlers = nil -} ---- b.com@v1.2.3/go.mod -- --module b.com -- --go 1.12 ---- b.com@v1.2.4/b/b.go -- --package b - --func Hello() {} ---- b.com@v1.2.4/go.mod -- --module b.com +-func TestListBugs(t *testing.T) { +- defer resetForTesting() - --go 1.12 --` -- const multiModule = ` ---- go.mod -- --module a.com +- Report("bad") - --require c.com v1.2.3 +- wantBugs(t, "bad") - --exclude b.com v1.2.3 ---- go.sum -- --c.com v1.2.3 h1:n07Dz9fYmpNqvZMwZi5NEqFcSHbvLa9lacMX+/g25tw= --c.com v1.2.3/go.mod h1:/4TyYgU9Nu5tA4NymP5xyqE8R2VMzGD3TbJCwCOvHAg= ---- main.go -- --package a +- for i := 0; i < 3; i++ { +- Report(fmt.Sprintf("index:%d", i)) +- } - --func main() { -- var x int --} --` -- WithOptions( -- ProxyFiles(proxy), -- ).Run(t, multiModule, func(t *testing.T, env *Env) { -- env.OnceMet( -- InitialWorkspaceLoad, -- Diagnostics(env.AtRegexp("main.go", "x")), -- ) -- }) +- wantBugs(t, "bad", "index:0") -} - --// This change tests that the version of the module used changes after it has --// been deleted from the workspace. --// --// TODO(golang/go#55331): delete this placeholder along with experimental --// workspace module. --func TestDeleteModule_Interdependent(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) // uses go.work -- const multiModule = ` ---- go.work -- --go 1.18 -- --use ( -- moda/a -- modb --) ---- moda/a/go.mod -- --module a.com -- --require b.com v1.2.3 ---- moda/a/go.sum -- --b.com v1.2.3 h1:tXrlXP0rnjRpKNmkbLYoWBdq0ikb3C3bKK9//moAWBI= --b.com v1.2.3/go.mod h1:D+J7pfFBZK5vdIdZEFquR586vKKIkqG7Qjw9AxG5BQ8= ---- moda/a/a.go -- --package a +-func wantBugs(t *testing.T, want ...string) { +- t.Helper() - --import ( -- "b.com/b" --) +- bugs := List() +- if got, want := len(bugs), len(want); got != want { +- t.Errorf("List(): got %d bugs, want %d", got, want) +- return +- } - --func main() { -- var x int -- _ = b.Hello() +- for i, b := range bugs { +- if got, want := b.Description, want[i]; got != want { +- t.Errorf("bug.List()[%d] = %q, want %q", i, got, want) +- } +- } -} ---- modb/go.mod -- --module b.com - ---- modb/b/b.go -- --package b +-func TestBugHandler(t *testing.T) { +- defer resetForTesting() - --func Hello() int { -- var x int --} --` -- WithOptions( -- ProxyFiles(workspaceModuleProxy), -- ).Run(t, multiModule, func(t *testing.T, env *Env) { -- env.OpenFile("moda/a/a.go") -- env.Await(env.DoneWithOpen()) +- Report("unseen") - -- originalLoc := env.GoToDefinition(env.RegexpSearch("moda/a/a.go", "Hello")) -- original := env.Sandbox.Workdir.URIToPath(originalLoc.URI) -- if want := "modb/b/b.go"; !strings.HasSuffix(original, want) { -- t.Errorf("expected %s, got %v", want, original) -- } -- env.CloseBuffer(original) -- env.AfterChange() +- // Both handlers are called, in order of registration, only once. +- var got string +- Handle(func(b Bug) { got += "1:" + b.Description }) +- Handle(func(b Bug) { got += "2:" + b.Description }) - -- env.RemoveWorkspaceFile("modb/b/b.go") -- env.RemoveWorkspaceFile("modb/go.mod") -- env.WriteWorkspaceFile("go.work", "go 1.18\nuse moda/a") -- env.AfterChange() +- Report("seen") - -- gotLoc := env.GoToDefinition(env.RegexpSearch("moda/a/a.go", "Hello")) -- got := env.Sandbox.Workdir.URIToPath(gotLoc.URI) -- if want := "b.com@v1.2.3/b/b.go"; !strings.HasSuffix(got, want) { -- t.Errorf("expected %s, got %v", want, got) -- } -- }) +- Report("again") +- +- if want := "1:seen2:seen"; got != want { +- t.Errorf("got %q, want %q", got, want) +- } -} - --// Tests that the version of the module used changes after it has been added --// to the workspace. --func TestCreateModule_Interdependent(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) // uses go.work -- const multiModule = ` ---- go.work -- --go 1.18 +-func TestBugJSON(t *testing.T) { +- b1 := Bug{ +- File: "foo.go", +- Line: 1, +- Description: "a bug", +- Key: "foo.go:1", +- Stack: "<stack>", +- AtTime: time.Now(), +- } - --use ( -- moda/a --) ---- moda/a/go.mod -- --module a.com +- data, err := json.Marshal(b1) +- if err != nil { +- t.Fatal(err) +- } +- var b2 Bug +- if err := json.Unmarshal(data, &b2); err != nil { +- t.Fatal(err) +- } +- if diff := cmp.Diff(b1, b2); diff != "" { +- t.Errorf("bugs differ after JSON Marshal/Unmarshal (-b1 +b2):\n%s", diff) +- } +-} +diff -urN a/gopls/internal/util/constraints/constraint.go b/gopls/internal/util/constraints/constraint.go +--- a/gopls/internal/util/constraints/constraint.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/util/constraints/constraint.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,52 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --require b.com v1.2.3 ---- moda/a/go.sum -- --b.com v1.2.3 h1:tXrlXP0rnjRpKNmkbLYoWBdq0ikb3C3bKK9//moAWBI= --b.com v1.2.3/go.mod h1:D+J7pfFBZK5vdIdZEFquR586vKKIkqG7Qjw9AxG5BQ8= ---- moda/a/a.go -- --package a +-// Package constraints defines a set of useful constraints to be used +-// with type parameters. +-package constraints - --import ( -- "b.com/b" --) +-// Copied from x/exp/constraints. - --func main() { -- var x int -- _ = b.Hello() +-// Signed is a constraint that permits any signed integer type. +-// If future releases of Go add new predeclared signed integer types, +-// this constraint will be modified to include them. +-type Signed interface { +- ~int | ~int8 | ~int16 | ~int32 | ~int64 -} --` -- WithOptions( -- ProxyFiles(workspaceModuleProxy), -- ).Run(t, multiModule, func(t *testing.T, env *Env) { -- env.OpenFile("moda/a/a.go") -- loc := env.GoToDefinition(env.RegexpSearch("moda/a/a.go", "Hello")) -- original := env.Sandbox.Workdir.URIToPath(loc.URI) -- if want := "b.com@v1.2.3/b/b.go"; !strings.HasSuffix(original, want) { -- t.Errorf("expected %s, got %v", want, original) -- } -- env.CloseBuffer(original) -- env.WriteWorkspaceFiles(map[string]string{ -- "go.work": `go 1.18 - --use ( -- moda/a -- modb --) --`, -- "modb/go.mod": "module b.com", -- "modb/b/b.go": `package b +-// Unsigned is a constraint that permits any unsigned integer type. +-// If future releases of Go add new predeclared unsigned integer types, +-// this constraint will be modified to include them. +-type Unsigned interface { +- ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr +-} - --func Hello() int { -- var x int +-// Integer is a constraint that permits any integer type. +-// If future releases of Go add new predeclared integer types, +-// this constraint will be modified to include them. +-type Integer interface { +- Signed | Unsigned -} --`, -- }) -- env.AfterChange(Diagnostics(env.AtRegexp("modb/b/b.go", "x"))) -- gotLoc := env.GoToDefinition(env.RegexpSearch("moda/a/a.go", "Hello")) -- got := env.Sandbox.Workdir.URIToPath(gotLoc.URI) -- if want := "modb/b/b.go"; !strings.HasSuffix(got, want) { -- t.Errorf("expected %s, got %v", want, original) -- } -- }) +- +-// Float is a constraint that permits any floating-point type. +-// If future releases of Go add new predeclared floating-point types, +-// this constraint will be modified to include them. +-type Float interface { +- ~float32 | ~float64 -} - --// This test confirms that a gopls workspace can recover from initialization --// with one invalid module. --func TestOneBrokenModule(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) // uses go.work -- const multiModule = ` ---- go.work -- --go 1.18 +-// Complex is a constraint that permits any complex numeric type. +-// If future releases of Go add new predeclared complex numeric types, +-// this constraint will be modified to include them. +-type Complex interface { +- ~complex64 | ~complex128 +-} - --use ( -- moda/a -- modb +-// Ordered is a constraint that permits any ordered type: any type +-// that supports the operators < <= >= >. +-// If future releases of Go add new ordered types, +-// this constraint will be modified to include them. +-type Ordered interface { +- Integer | Float | ~string +-} +diff -urN a/gopls/internal/util/frob/frob.go b/gopls/internal/util/frob/frob.go +--- a/gopls/internal/util/frob/frob.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/util/frob/frob.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,408 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-// Package frob is a fast restricted object encoder/decoder in the +-// spirit of encoding/gob. +-// +-// As with gob, types that recursively contain functions, channels, +-// and unsafe.Pointers cannot be encoded, but frob has these +-// additional restrictions: +-// +-// - Interface values are not supported; this avoids the need for +-// the encoding to describe types. +-// +-// - Types that recursively contain private struct fields are not +-// permitted. +-// +-// - The encoding is unspecified and subject to change, so the encoder +-// and decoder must exactly agree on their implementation and on the +-// definitions of the target types. +-// +-// - Lengths (of arrays, slices, and maps) are currently assumed to +-// fit in 32 bits. +-// +-// - There is no error handling. All errors are reported by panicking. +-// +-// - Values are serialized as trees, not graphs, so shared subgraphs +-// are encoded repeatedly. +-// +-// - No attempt is made to detect cyclic data structures. +-package frob +- +-import ( +- "encoding/binary" +- "fmt" +- "math" +- "reflect" +- "sync" -) ---- moda/a/go.mod -- --module a.com - --require b.com v1.2.3 +-// A Codec[T] is an immutable encoder and decoder for values of type T. +-type Codec[T any] struct{ frob *frob } - ---- moda/a/a.go -- --package a +-// CodecFor[T] returns a codec for values of type T. +-// It panics if type T is unsuitable. +-func CodecFor[T any]() Codec[T] { +- frobsMu.Lock() +- defer frobsMu.Unlock() +- return Codec[T]{frobFor(reflect.TypeOf((*T)(nil)).Elem())} +-} - --import ( -- "b.com/b" +-func (codec Codec[T]) Encode(v T) []byte { return codec.frob.Encode(v) } +-func (codec Codec[T]) Decode(data []byte, ptr *T) { codec.frob.Decode(data, ptr) } +- +-var ( +- frobsMu sync.Mutex +- frobs = make(map[reflect.Type]*frob) -) - --func main() { -- var x int -- _ = b.Hello() +-// A frob is an encoder/decoder for a specific type. +-type frob struct { +- t reflect.Type +- kind reflect.Kind +- elems []*frob // elem (array/slice/ptr), key+value (map), fields (struct) -} ---- modb/go.mod -- --modul b.com // typo here - ---- modb/b/b.go -- --package b +-// frobFor returns the frob for a particular type. +-// Precondition: caller holds frobsMu. +-func frobFor(t reflect.Type) *frob { +- fr, ok := frobs[t] +- if !ok { +- fr = &frob{t: t, kind: t.Kind()} +- frobs[t] = fr - --func Hello() int { -- var x int --} --` -- WithOptions( -- ProxyFiles(workspaceModuleProxy), -- ).Run(t, multiModule, func(t *testing.T, env *Env) { -- env.OpenFile("modb/go.mod") -- env.AfterChange( -- Diagnostics(AtPosition("modb/go.mod", 0, 0)), -- ) -- env.RegexpReplace("modb/go.mod", "modul", "module") -- env.SaveBufferWithoutActions("modb/go.mod") -- env.AfterChange( -- Diagnostics(env.AtRegexp("modb/b/b.go", "x")), -- ) -- }) --} +- switch fr.kind { +- case reflect.Bool, +- reflect.Int, +- reflect.Int8, +- reflect.Int16, +- reflect.Int32, +- reflect.Int64, +- reflect.Uint, +- reflect.Uint8, +- reflect.Uint16, +- reflect.Uint32, +- reflect.Uint64, +- reflect.Uintptr, +- reflect.Float32, +- reflect.Float64, +- reflect.Complex64, +- reflect.Complex128, +- reflect.String: - --// TestBadGoWork exercises the panic from golang/vscode-go#2121. --func TestBadGoWork(t *testing.T) { -- const files = ` ---- go.work -- --use ./bar ---- bar/go.mod -- --module example.com/bar --` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("go.work") -- }) --} +- case reflect.Array, +- reflect.Slice, +- reflect.Pointer: +- fr.addElem(fr.t.Elem()) - --func TestUseGoWork(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) // uses go.work -- // This test validates certain functionality related to using a go.work -- // file to specify workspace modules. -- const multiModule = ` ---- moda/a/go.mod -- --module a.com +- case reflect.Map: +- fr.addElem(fr.t.Key()) +- fr.addElem(fr.t.Elem()) - --require b.com v1.2.3 ---- moda/a/go.sum -- --b.com v1.2.3 h1:tXrlXP0rnjRpKNmkbLYoWBdq0ikb3C3bKK9//moAWBI= --b.com v1.2.3/go.mod h1:D+J7pfFBZK5vdIdZEFquR586vKKIkqG7Qjw9AxG5BQ8= ---- moda/a/a.go -- --package a +- case reflect.Struct: +- for i := 0; i < fr.t.NumField(); i++ { +- field := fr.t.Field(i) +- if field.PkgPath != "" { +- panic(fmt.Sprintf("unexported field %v", field)) +- } +- fr.addElem(field.Type) +- } - --import ( -- "b.com/b" --) +- default: +- // chan, func, interface, unsafe.Pointer +- panic(fmt.Sprintf("type %v is not supported by frob", fr.t)) +- } +- } +- return fr +-} - --func main() { -- var x int -- _ = b.Hello() +-func (fr *frob) addElem(t reflect.Type) { +- fr.elems = append(fr.elems, frobFor(t)) -} ---- modb/go.mod -- --module b.com - --require example.com v1.2.3 ---- modb/go.sum -- --example.com v1.2.3 h1:Yryq11hF02fEf2JlOS2eph+ICE2/ceevGV3C9dl5V/c= --example.com v1.2.3/go.mod h1:Y2Rc5rVWjWur0h3pd9aEvK5Pof8YKDANh9gHA2Maujo= ---- modb/b/b.go -- --package b +-const magic = "frob" - --func Hello() int { -- var x int +-func (fr *frob) Encode(v any) []byte { +- rv := reflect.ValueOf(v) +- if rv.Type() != fr.t { +- panic(fmt.Sprintf("got %v, want %v", rv.Type(), fr.t)) +- } +- w := &writer{} +- w.bytes([]byte(magic)) +- fr.encode(w, rv) +- if uint64(len(w.data))>>32 != 0 { +- panic("too large") // includes all cases where len doesn't fit in 32 bits +- } +- return w.data -} ---- go.work -- --go 1.17 - --use ( -- ./moda/a --) --` -- WithOptions( -- ProxyFiles(workspaceModuleProxy), -- Settings{ -- "subdirWatchPatterns": "on", -- }, -- ).Run(t, multiModule, func(t *testing.T, env *Env) { -- // Initially, the go.work should cause only the a.com module to be loaded, -- // so we shouldn't get any file watches for modb. Further validate this by -- // jumping to a definition in b.com and ensuring that we go to the module -- // cache. -- env.OnceMet( -- InitialWorkspaceLoad, -- NoFileWatchMatching("modb"), -- ) -- env.OpenFile("moda/a/a.go") -- env.Await(env.DoneWithOpen()) +-// encode appends the encoding of value v, whose type must be fr.t. +-func (fr *frob) encode(out *writer, v reflect.Value) { +- switch fr.kind { +- case reflect.Bool: +- var b byte +- if v.Bool() { +- b = 1 +- } +- out.uint8(b) +- case reflect.Int: +- out.uint64(uint64(v.Int())) +- case reflect.Int8: +- out.uint8(uint8(v.Int())) +- case reflect.Int16: +- out.uint16(uint16(v.Int())) +- case reflect.Int32: +- out.uint32(uint32(v.Int())) +- case reflect.Int64: +- out.uint64(uint64(v.Int())) +- case reflect.Uint: +- out.uint64(v.Uint()) +- case reflect.Uint8: +- out.uint8(uint8(v.Uint())) +- case reflect.Uint16: +- out.uint16(uint16(v.Uint())) +- case reflect.Uint32: +- out.uint32(uint32(v.Uint())) +- case reflect.Uint64: +- out.uint64(v.Uint()) +- case reflect.Uintptr: +- out.uint64(v.Uint()) +- case reflect.Float32: +- out.uint32(math.Float32bits(float32(v.Float()))) +- case reflect.Float64: +- out.uint64(math.Float64bits(v.Float())) +- case reflect.Complex64: +- z := complex64(v.Complex()) +- out.uint32(math.Float32bits(real(z))) +- out.uint32(math.Float32bits(imag(z))) +- case reflect.Complex128: +- z := v.Complex() +- out.uint64(math.Float64bits(real(z))) +- out.uint64(math.Float64bits(imag(z))) - -- // To verify which modules are loaded, we'll jump to the definition of -- // b.Hello. -- checkHelloLocation := func(want string) error { -- loc := env.GoToDefinition(env.RegexpSearch("moda/a/a.go", "Hello")) -- file := env.Sandbox.Workdir.URIToPath(loc.URI) -- if !strings.HasSuffix(file, want) { -- return fmt.Errorf("expected %s, got %v", want, file) -- } -- return nil +- case reflect.Array: +- len := v.Type().Len() +- elem := fr.elems[0] +- for i := 0; i < len; i++ { +- elem.encode(out, v.Index(i)) - } - -- // Initially this should be in the module cache, as b.com is not replaced. -- if err := checkHelloLocation("b.com@v1.2.3/b/b.go"); err != nil { -- t.Fatal(err) +- case reflect.Slice: +- len := v.Len() +- out.uint32(uint32(len)) +- if len > 0 { +- elem := fr.elems[0] +- if elem.kind == reflect.Uint8 { +- // []byte fast path +- out.bytes(v.Bytes()) +- } else { +- for i := 0; i < len; i++ { +- elem.encode(out, v.Index(i)) +- } +- } - } - -- // Now, modify the go.work file on disk to activate the b.com module in -- // the workspace. -- env.WriteWorkspaceFile("go.work", ` --go 1.17 +- case reflect.Map: +- len := v.Len() +- out.uint32(uint32(len)) +- if len > 0 { +- kfrob, vfrob := fr.elems[0], fr.elems[1] +- for iter := v.MapRange(); iter.Next(); { +- kfrob.encode(out, iter.Key()) +- vfrob.encode(out, iter.Value()) +- } +- } - --use ( -- ./moda/a -- ./modb --) --`) +- case reflect.Pointer: +- if v.IsNil() { +- out.uint8(0) +- } else { +- out.uint8(1) +- fr.elems[0].encode(out, v.Elem()) +- } - -- // As of golang/go#54069, writing go.work to the workspace triggers a -- // workspace reload, and new file watches. -- env.AfterChange( -- Diagnostics(env.AtRegexp("modb/b/b.go", "x")), -- // TODO(golang/go#60340): we don't get a file watch yet, because -- // updateWatchedDirectories runs before snapshot.load. Instead, we get it -- // after the next change (the didOpen below). -- // FileWatchMatching("modb"), -- ) +- case reflect.String: +- len := v.Len() +- out.uint32(uint32(len)) +- if len > 0 { +- out.data = append(out.data, v.String()...) +- } - -- // Jumping to definition should now go to b.com in the workspace. -- if err := checkHelloLocation("modb/b/b.go"); err != nil { -- t.Fatal(err) +- case reflect.Struct: +- for i, elem := range fr.elems { +- elem.encode(out, v.Field(i)) - } - -- // Now, let's modify the go.work *overlay* (not on disk), and verify that -- // this change is only picked up once it is saved. -- env.OpenFile("go.work") -- env.AfterChange( -- // TODO(golang/go#60340): delete this expectation in favor of -- // the commented-out expectation above, once we fix the evaluation order -- // of file watches. We should not have to wait for a second change to get -- // the correct watches. -- FileWatchMatching("modb"), -- ) -- env.SetBufferContent("go.work", `go 1.17 +- default: +- panic(fr.t) +- } +-} - --use ( -- ./moda/a --)`) +-func (fr *frob) Decode(data []byte, ptr any) { +- rv := reflect.ValueOf(ptr).Elem() +- if rv.Type() != fr.t { +- panic(fmt.Sprintf("got %v, want %v", rv.Type(), fr.t)) +- } +- rd := &reader{data} +- if string(rd.bytes(4)) != magic { +- panic("not a frob-encoded message") +- } +- fr.decode(rd, rv) +- if len(rd.data) > 0 { +- panic("surplus bytes") +- } +-} - -- // Simply modifying the go.work file does not cause a reload, so we should -- // still jump within the workspace. -- // -- // TODO: should editing the go.work above cause modb diagnostics to be -- // suppressed? -- env.Await(env.DoneWithChange()) -- if err := checkHelloLocation("modb/b/b.go"); err != nil { -- t.Fatal(err) +-// decode reads from in, decodes a value, and sets addr to it. +-// addr must be a zero-initialized addressable variable of type fr.t. +-func (fr *frob) decode(in *reader, addr reflect.Value) { +- switch fr.kind { +- case reflect.Bool: +- addr.SetBool(in.uint8() != 0) +- case reflect.Int: +- addr.SetInt(int64(in.uint64())) +- case reflect.Int8: +- addr.SetInt(int64(in.uint8())) +- case reflect.Int16: +- addr.SetInt(int64(in.uint16())) +- case reflect.Int32: +- addr.SetInt(int64(in.uint32())) +- case reflect.Int64: +- addr.SetInt(int64(in.uint64())) +- case reflect.Uint: +- addr.SetUint(in.uint64()) +- case reflect.Uint8: +- addr.SetUint(uint64(in.uint8())) +- case reflect.Uint16: +- addr.SetUint(uint64(in.uint16())) +- case reflect.Uint32: +- addr.SetUint(uint64(in.uint32())) +- case reflect.Uint64: +- addr.SetUint(in.uint64()) +- case reflect.Uintptr: +- addr.SetUint(in.uint64()) +- case reflect.Float32: +- addr.SetFloat(float64(math.Float32frombits(in.uint32()))) +- case reflect.Float64: +- addr.SetFloat(math.Float64frombits(in.uint64())) +- case reflect.Complex64: +- addr.SetComplex(complex128(complex( +- math.Float32frombits(in.uint32()), +- math.Float32frombits(in.uint32()), +- ))) +- case reflect.Complex128: +- addr.SetComplex(complex( +- math.Float64frombits(in.uint64()), +- math.Float64frombits(in.uint64()), +- )) +- +- case reflect.Array: +- len := fr.t.Len() +- for i := 0; i < len; i++ { +- fr.elems[0].decode(in, addr.Index(i)) - } - -- // Saving should reload the workspace. -- env.SaveBufferWithoutActions("go.work") -- if err := checkHelloLocation("b.com@v1.2.3/b/b.go"); err != nil { -- t.Fatal(err) +- case reflect.Slice: +- len := int(in.uint32()) +- if len > 0 { +- elem := fr.elems[0] +- if elem.kind == reflect.Uint8 { +- // []byte fast path +- // (Not addr.SetBytes: we must make a copy.) +- addr.Set(reflect.AppendSlice(addr, reflect.ValueOf(in.bytes(len)))) +- } else { +- addr.Set(reflect.MakeSlice(fr.t, len, len)) +- for i := 0; i < len; i++ { +- elem.decode(in, addr.Index(i)) +- } +- } - } - -- // This fails if guarded with a OnceMet(DoneWithSave(), ...), because it is -- // delayed (and therefore not synchronous with the change). -- env.Await(NoDiagnostics(ForFile("modb/go.mod"))) +- case reflect.Map: +- len := int(in.uint32()) +- if len > 0 { +- m := reflect.MakeMapWithSize(fr.t, len) +- addr.Set(m) +- kfrob, vfrob := fr.elems[0], fr.elems[1] +- k := reflect.New(kfrob.t).Elem() +- v := reflect.New(vfrob.t).Elem() +- kzero := reflect.Zero(kfrob.t) +- vzero := reflect.Zero(vfrob.t) +- for i := 0; i < len; i++ { +- // TODO(adonovan): use SetZero from go1.20. +- // k.SetZero() +- // v.SetZero() +- k.Set(kzero) +- v.Set(vzero) +- kfrob.decode(in, k) +- vfrob.decode(in, v) +- m.SetMapIndex(k, v) +- } +- } - -- // Test Formatting. -- env.SetBufferContent("go.work", `go 1.18 -- use ( +- case reflect.Pointer: +- isNil := in.uint8() == 0 +- if !isNil { +- ptr := reflect.New(fr.elems[0].t) +- addr.Set(ptr) +- fr.elems[0].decode(in, ptr.Elem()) +- } - +- case reflect.String: +- len := int(in.uint32()) +- if len > 0 { +- addr.SetString(string(in.bytes(len))) +- } - +- case reflect.Struct: +- for i, elem := range fr.elems { +- elem.decode(in, addr.Field(i)) +- } - -- ./moda/a --) --`) // TODO(matloob): For some reason there's a "start position 7:0 is out of bounds" error when the ")" is on the last character/line in the file. Rob probably knows what's going on. -- env.SaveBuffer("go.work") -- env.Await(env.DoneWithSave()) -- gotWorkContents := env.ReadWorkspaceFile("go.work") -- wantWorkContents := `go 1.18 +- default: +- panic(fr.t) +- } +-} - --use ( -- ./moda/a --) --` -- if gotWorkContents != wantWorkContents { -- t.Fatalf("formatted contents of workspace: got %q; want %q", gotWorkContents, wantWorkContents) -- } -- }) +-var le = binary.LittleEndian +- +-type reader struct{ data []byte } +- +-func (r *reader) uint8() uint8 { +- v := r.data[0] +- r.data = r.data[1:] +- return v -} - --func TestUseGoWorkDiagnosticMissingModule(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) // uses go.work +-func (r *reader) uint16() uint16 { +- v := le.Uint16(r.data) +- r.data = r.data[2:] +- return v +-} - -- const files = ` ---- go.work -- --go 1.18 +-func (r *reader) uint32() uint32 { +- v := le.Uint32(r.data) +- r.data = r.data[4:] +- return v +-} - --use ./foo ---- bar/go.mod -- --module example.com/bar --` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("go.work") -- env.AfterChange( -- Diagnostics(env.AtRegexp("go.work", "use"), WithMessage("directory ./foo does not contain a module")), -- ) -- // The following tests is a regression test against an issue where we weren't -- // copying the workFile struct field on workspace when a new one was created in -- // (*workspace).invalidate. Set the buffer content to a working file so that -- // invalidate recognizes the workspace to be change and copies over the workspace -- // struct, and then set the content back to the old contents to make sure -- // the diagnostic still shows up. -- env.SetBufferContent("go.work", "go 1.18 \n\n use ./bar\n") -- env.AfterChange( -- NoDiagnostics(env.AtRegexp("go.work", "use")), -- ) -- env.SetBufferContent("go.work", "go 1.18 \n\n use ./foo\n") -- env.AfterChange( -- Diagnostics(env.AtRegexp("go.work", "use"), WithMessage("directory ./foo does not contain a module")), -- ) -- }) +-func (r *reader) uint64() uint64 { +- v := le.Uint64(r.data) +- r.data = r.data[8:] +- return v -} - --func TestUseGoWorkDiagnosticSyntaxError(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) -- const files = ` ---- go.work -- --go 1.18 -- --usa ./foo --replace --` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("go.work") -- env.AfterChange( -- Diagnostics(env.AtRegexp("go.work", "usa"), WithMessage("unknown directive: usa")), -- Diagnostics(env.AtRegexp("go.work", "replace"), WithMessage("usage: replace")), -- ) -- }) +-func (r *reader) bytes(n int) []byte { +- v := r.data[:n] +- r.data = r.data[n:] +- return v -} - --func TestUseGoWorkHover(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) +-type writer struct{ data []byte } - -- const files = ` ---- go.work -- --go 1.18 +-func (w *writer) uint8(v uint8) { w.data = append(w.data, v) } +-func (w *writer) uint16(v uint16) { w.data = le.AppendUint16(w.data, v) } +-func (w *writer) uint32(v uint32) { w.data = le.AppendUint32(w.data, v) } +-func (w *writer) uint64(v uint64) { w.data = le.AppendUint64(w.data, v) } +-func (w *writer) bytes(v []byte) { w.data = append(w.data, v...) } +diff -urN a/gopls/internal/util/frob/frob_test.go b/gopls/internal/util/frob/frob_test.go +--- a/gopls/internal/util/frob/frob_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/util/frob/frob_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,119 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --use ./foo --use ( -- ./bar -- ./bar/baz +-package frob_test +- +-import ( +- "math" +- "reflect" +- "testing" +- +- "golang.org/x/tools/gopls/internal/util/frob" -) ---- foo/go.mod -- --module example.com/foo ---- bar/go.mod -- --module example.com/bar ---- bar/baz/go.mod -- --module example.com/bar/baz --` -- Run(t, files, func(t *testing.T, env *Env) { -- env.OpenFile("go.work") - -- tcs := map[string]string{ -- `\./foo`: "example.com/foo", -- `(?m)\./bar$`: "example.com/bar", -- `\./bar/baz`: "example.com/bar/baz", -- } +-func TestBasics(t *testing.T) { +- type Basics struct { +- A []*string +- B [2]int +- C *Basics +- D map[string]int +- E []byte +- F []string +- } +- codec := frob.CodecFor[Basics]() - -- for hoverRE, want := range tcs { -- got, _ := env.Hover(env.RegexpSearch("go.work", hoverRE)) -- if got.Value != want { -- t.Errorf(`hover on %q: got %q, want %q`, hoverRE, got, want) -- } -- } -- }) +- s1, s2 := "hello", "world" +- x := Basics{ +- A: []*string{&s1, nil, &s2}, +- B: [...]int{1, 2}, +- C: &Basics{ +- B: [...]int{3, 4}, +- D: map[string]int{"one": 1}, +- }, +- E: []byte("hello"), +- F: []string{s1, s2}, +- } +- var y Basics +- codec.Decode(codec.Encode(x), &y) +- if !reflect.DeepEqual(x, y) { +- t.Fatalf("bad roundtrip: got %#v, want %#v", y, x) +- } -} - --func TestExpandToGoWork(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) -- const workspace = ` ---- moda/a/go.mod -- --module a.com +-func TestInts(t *testing.T) { +- type Ints struct { +- U uint +- U8 uint8 +- U16 uint16 +- U32 uint32 +- U64 uint64 +- UP uintptr +- I int +- I8 int8 +- I16 int16 +- I32 int32 +- I64 int64 +- F32 float32 +- F64 float64 +- C64 complex64 +- C128 complex128 +- } +- codec := frob.CodecFor[Ints]() - --require b.com v1.2.3 ---- moda/a/a.go -- --package a +- // maxima +- max1 := Ints{ +- U: math.MaxUint, +- U8: math.MaxUint8, +- U16: math.MaxUint16, +- U32: math.MaxUint32, +- U64: math.MaxUint64, +- UP: math.MaxUint, +- I: math.MaxInt, +- I8: math.MaxInt8, +- I16: math.MaxInt16, +- I32: math.MaxInt32, +- I64: math.MaxInt64, +- F32: math.MaxFloat32, +- F64: math.MaxFloat64, +- C64: complex(math.MaxFloat32, math.MaxFloat32), +- C128: complex(math.MaxFloat64, math.MaxFloat64), +- } +- var max2 Ints +- codec.Decode(codec.Encode(max1), &max2) +- if !reflect.DeepEqual(max1, max2) { +- t.Fatalf("max: bad roundtrip: got %#v, want %#v", max2, max1) +- } +- +- // minima +- min1 := Ints{ +- I: math.MinInt, +- I8: math.MinInt8, +- I16: math.MinInt16, +- I32: math.MinInt32, +- I64: math.MinInt64, +- F32: -math.MaxFloat32, +- F64: -math.MaxFloat32, +- C64: complex(-math.MaxFloat32, -math.MaxFloat32), +- C128: complex(-math.MaxFloat64, -math.MaxFloat64), +- } +- var min2 Ints +- codec.Decode(codec.Encode(min1), &min2) +- if !reflect.DeepEqual(min1, min2) { +- t.Fatalf("min: bad roundtrip: got %#v, want %#v", min2, min1) +- } +- +- // negatives (other than MinInt), to exercise conversions +- neg1 := Ints{ +- I: -1, +- I8: -1, +- I16: -1, +- I32: -1, +- I64: -1, +- } +- var neg2 Ints +- codec.Decode(codec.Encode(neg1), &neg2) +- if !reflect.DeepEqual(neg1, neg2) { +- t.Fatalf("neg: bad roundtrip: got %#v, want %#v", neg2, neg1) +- } +-} +diff -urN a/gopls/internal/util/goversion/goversion.go b/gopls/internal/util/goversion/goversion.go +--- a/gopls/internal/util/goversion/goversion.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/util/goversion/goversion.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,93 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-// Package goversions defines gopls's policy for which versions of Go it supports. +-package goversion - -import ( -- "b.com/b" +- "fmt" +- "strings" -) - --func main() { -- var x int -- _ = b.Hello() --} ---- modb/go.mod -- --module b.com +-// Support holds information about end-of-life Go version support. +-// +-// Exposed for testing. +-type Support struct { +- // GoVersion is the Go version to which these settings relate. +- GoVersion int - --require example.com v1.2.3 ---- modb/b/b.go -- --package b +- // DeprecatedVersion is the first version of gopls that no longer supports +- // this Go version. +- // +- // If unset, the version is already deprecated. +- DeprecatedVersion string - --func Hello() int { -- var x int +- // InstallGoplsVersion is the latest gopls version that supports this Go +- // version without warnings. +- InstallGoplsVersion string -} ---- go.work -- --go 1.17 - --use ( -- ./moda/a -- ./modb --) --` -- WithOptions( -- WorkspaceFolders("moda/a"), -- ).Run(t, workspace, func(t *testing.T, env *Env) { -- env.OpenFile("moda/a/a.go") -- env.Await(env.DoneWithOpen()) -- loc := env.GoToDefinition(env.RegexpSearch("moda/a/a.go", "Hello")) -- file := env.Sandbox.Workdir.URIToPath(loc.URI) -- want := "modb/b/b.go" -- if !strings.HasSuffix(file, want) { -- t.Errorf("expected %s, got %v", want, file) -- } -- }) +-// Supported maps Go versions to the gopls version in which support will +-// be deprecated, and the final gopls version supporting them without warnings. +-// Keep this in sync with gopls/README.md. +-// +-// Must be sorted in ascending order of Go version. +-// +-// Exposed (and mutable) for testing. +-var Supported = []Support{ +- {12, "", "v0.7.5"}, +- {15, "", "v0.9.5"}, +- {16, "", "v0.11.0"}, +- {17, "", "v0.11.0"}, +- {18, "v0.16.0", "v0.14.2"}, -} - --func TestNonWorkspaceFileCreation(t *testing.T) { -- const files = ` ---- work/go.mod -- --module mod.com -- --go 1.12 ---- work/x.go -- --package x --` -- -- const code = ` --package foo --import "fmt" --var _ = fmt.Printf --` -- WithOptions( -- WorkspaceFolders("work"), // so that outside/... is outside the workspace -- ).Run(t, files, func(t *testing.T, env *Env) { -- env.CreateBuffer("outside/foo.go", "") -- env.EditBuffer("outside/foo.go", fake.NewEdit(0, 0, 0, 0, code)) -- env.GoToDefinition(env.RegexpSearch("outside/foo.go", `Printf`)) -- }) +-// OldestSupported is the last X in Go 1.X that this version of gopls +-// supports without warnings. +-// +-// Exported for testing. +-func OldestSupported() int { +- return Supported[len(Supported)-1].GoVersion + 1 -} - --func TestGoWork_V2Module(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) // uses go.work -- // When using a go.work, we must have proxy content even if it is replaced. -- const proxy = ` ---- b.com/v2@v2.1.9/go.mod -- --module b.com/v2 +-// Message returns the message to display if the user has the given Go +-// version, if any. The goVersion variable is the X in Go 1.X. If +-// fromBuild is set, the Go version is the version used to build +-// gopls. Otherwise, it is the go command version. +-// +-// The second component of the result indicates whether the message is +-// an error, not a mere warning. +-// +-// If goVersion is invalid (< 0), it returns "", false. +-func Message(goVersion int, fromBuild bool) (string, bool) { +- if goVersion < 0 { +- return "", false +- } - --go 1.12 ---- b.com/v2@v2.1.9/b/b.go -- --package b +- for _, v := range Supported { +- if goVersion <= v.GoVersion { +- var msgBuilder strings.Builder - --func Ciao()() int { -- return 0 +- isError := true +- if fromBuild { +- fmt.Fprintf(&msgBuilder, "Gopls was built with Go version 1.%d", goVersion) +- } else { +- fmt.Fprintf(&msgBuilder, "Found Go version 1.%d", goVersion) +- } +- if v.DeprecatedVersion != "" { +- // not deprecated yet, just a warning +- fmt.Fprintf(&msgBuilder, ", which will be unsupported by gopls %s. ", v.DeprecatedVersion) +- isError = false // warning +- } else { +- fmt.Fprint(&msgBuilder, ", which is not supported by this version of gopls. ") +- } +- fmt.Fprintf(&msgBuilder, "Please upgrade to Go 1.%d or later and reinstall gopls. ", OldestSupported()) +- fmt.Fprintf(&msgBuilder, "If you can't upgrade and want this message to go away, please install gopls %s. ", v.InstallGoplsVersion) +- fmt.Fprint(&msgBuilder, "See https://go.dev/s/gopls-support-policy for more details.") +- +- return msgBuilder.String(), isError +- } +- } +- return "", false -} --` +diff -urN a/gopls/internal/util/goversion/goversion_test.go b/gopls/internal/util/goversion/goversion_test.go +--- a/gopls/internal/util/goversion/goversion_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/util/goversion/goversion_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,76 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - -- const multiModule = ` ---- go.work -- --go 1.18 +-package goversion_test - --use ( -- moda/a -- modb -- modb/v2 -- modc +-import ( +- "fmt" +- "strings" +- "testing" +- +- "golang.org/x/tools/gopls/internal/util/goversion" -) ---- moda/a/go.mod -- --module a.com - --require b.com/v2 v2.1.9 ---- moda/a/a.go -- --package a +-func TestMessage(t *testing.T) { +- // Note(rfindley): this test is a change detector, as it must be updated +- // whenever we deprecate a version. +- // +- // However, I chose to leave it as is since it gives us confidence in error +- // messages served for Go versions that we no longer support (and therefore +- // no longer run in CI). +- type test struct { +- goVersion int +- fromBuild bool +- wantContains []string // string fragments that we expect to see +- wantIsError bool // an error, not a mere warning +- } +- +- deprecated := func(goVersion int, lastVersion string) test { +- return test{ +- goVersion: goVersion, +- fromBuild: false, +- wantContains: []string{ +- fmt.Sprintf("Found Go version 1.%d", goVersion), +- "not supported", +- fmt.Sprintf("upgrade to Go 1.%d", goversion.OldestSupported()), +- fmt.Sprintf("install gopls %s", lastVersion), +- }, +- wantIsError: true, +- } +- } - --import ( -- "b.com/v2/b" --) +- tests := []struct { +- goVersion int +- fromBuild bool +- wantContains []string // string fragments that we expect to see +- wantIsError bool // an error, not a mere warning +- }{ +- {-1, false, nil, false}, +- deprecated(12, "v0.7.5"), +- deprecated(13, "v0.9.5"), +- deprecated(15, "v0.9.5"), +- deprecated(16, "v0.11.0"), +- deprecated(17, "v0.11.0"), +- {18, false, []string{"Found Go version 1.18", "unsupported by gopls v0.16.0", "upgrade to Go 1.19", "install gopls v0.14.2"}, false}, +- {18, true, []string{"Gopls was built with Go version 1.18", "unsupported by gopls v0.16.0", "upgrade to Go 1.19", "install gopls v0.14.2"}, false}, +- } - --func main() { -- var x int -- _ = b.Hi() --} ---- modb/go.mod -- --module b.com +- for _, test := range tests { +- gotMsg, gotIsError := goversion.Message(test.goVersion, test.fromBuild) - ---- modb/b/b.go -- --package b +- if len(test.wantContains) == 0 && gotMsg != "" { +- t.Errorf("versionMessage(%d) = %q, want \"\"", test.goVersion, gotMsg) +- } - --func Hello() int { -- var x int +- for _, want := range test.wantContains { +- if !strings.Contains(gotMsg, want) { +- t.Errorf("versionMessage(%d) = %q, want containing %q", test.goVersion, gotMsg, want) +- } +- } +- +- if gotIsError != test.wantIsError { +- t.Errorf("versionMessage(%d) isError = %v, want %v", test.goVersion, gotIsError, test.wantIsError) +- } +- } -} ---- modb/v2/go.mod -- --module b.com/v2 +diff -urN a/gopls/internal/util/immutable/immutable.go b/gopls/internal/util/immutable/immutable.go +--- a/gopls/internal/util/immutable/immutable.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/util/immutable/immutable.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,43 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - ---- modb/v2/b/b.go -- --package b +-// The immutable package defines immutable wrappers around common data +-// structures. These are used for additional type safety inside gopls. +-// +-// See the "persistent" package for copy-on-write data structures. +-package immutable - --func Hi() int { -- var x int +-// Map is an immutable wrapper around an ordinary Go map. +-type Map[K comparable, V any] struct { +- m map[K]V -} ---- modc/go.mod -- --module gopkg.in/yaml.v1 // test gopkg.in versions ---- modc/main.go -- --package main - --func main() { -- var x int +-// MapOf wraps the given Go map. +-// +-// The caller must not subsequently mutate the map. +-func MapOf[K comparable, V any](m map[K]V) Map[K, V] { +- return Map[K, V]{m} -} --` - -- WithOptions( -- ProxyFiles(proxy), -- ).Run(t, multiModule, func(t *testing.T, env *Env) { -- env.OnceMet( -- InitialWorkspaceLoad, -- // TODO(rfindley): assert on the full set of diagnostics here. We -- // should ensure that we don't have a diagnostic at b.Hi in a.go. -- Diagnostics(env.AtRegexp("moda/a/a.go", "x")), -- Diagnostics(env.AtRegexp("modb/b/b.go", "x")), -- Diagnostics(env.AtRegexp("modb/v2/b/b.go", "x")), -- Diagnostics(env.AtRegexp("modc/main.go", "x")), -- ) -- }) +-// Value returns the mapped value for k. +-// It is equivalent to the commaok form of an ordinary go map, and returns +-// (zero, false) if the key is not present. +-func (m Map[K, V]) Value(k K) (V, bool) { +- v, ok := m.m[k] +- return v, ok -} - --// Confirm that a fix for a tidy module will correct all modules in the --// workspace. --func TestMultiModule_OneBrokenModule(t *testing.T) { -- // In the earlier 'experimental workspace mode', gopls would aggregate go.sum -- // entries for the workspace module, allowing it to correctly associate -- // missing go.sum with diagnostics. With go.work files, this doesn't work: -- // the go.command will happily write go.work.sum. -- t.Skip("golang/go#57509: go.mod diagnostics do not work in go.work mode") -- testenv.NeedsGo1Point(t, 18) // uses go.work -- const files = ` ---- go.work -- --go 1.18 +-// Len returns the number of entries in the Map. +-func (m Map[K, V]) Len() int { +- return len(m.m) +-} - --use ( -- a -- b --) ---- go.work.sum -- ---- a/go.mod -- --module a.com +-// Range calls f for each mapped (key, value) pair. +-// There is no way to break out of the loop. +-// TODO: generalize when Go iterators (#61405) land. +-func (m Map[K, V]) Range(f func(k K, v V)) { +- for k, v := range m.m { +- f(k, v) +- } +-} +diff -urN a/gopls/internal/util/lru/lru_fuzz_test.go b/gopls/internal/util/lru/lru_fuzz_test.go +--- a/gopls/internal/util/lru/lru_fuzz_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/util/lru/lru_fuzz_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,38 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --go 1.12 ---- a/main.go -- --package main ---- b/go.mod -- --module b.com +-package lru_test - --go 1.12 +-import ( +- "testing" - --require ( -- example.com v1.2.3 +- "golang.org/x/tools/gopls/internal/util/lru" -) ---- b/go.sum -- ---- b/main.go -- --package b -- --import "example.com/blah" - --func main() { -- blah.Hello() --} --` -- WithOptions( -- ProxyFiles(workspaceProxy), -- ).Run(t, files, func(t *testing.T, env *Env) { -- params := &protocol.PublishDiagnosticsParams{} -- env.OpenFile("b/go.mod") -- env.AfterChange( -- Diagnostics( -- env.AtRegexp("go.mod", `example.com v1.2.3`), -- WithMessage("go.sum is out of sync"), -- ), -- ReadDiagnostics("b/go.mod", params), -- ) -- for _, d := range params.Diagnostics { -- if !strings.Contains(d.Message, "go.sum is out of sync") { -- continue -- } -- actions := env.GetQuickFixes("b/go.mod", []protocol.Diagnostic{d}) -- if len(actions) != 2 { -- t.Fatalf("expected 2 code actions, got %v", len(actions)) +-// Simple fuzzing test for consistency. +-func FuzzCache(f *testing.F) { +- type op struct { +- set bool +- key, value byte +- } +- f.Fuzz(func(t *testing.T, data []byte) { +- var ops []op +- for len(data) >= 3 { +- ops = append(ops, op{data[0]%2 == 0, data[1], data[2]}) +- data = data[3:] +- } +- cache := lru.New(100) +- var reference [256]byte +- for _, op := range ops { +- if op.set { +- reference[op.key] = op.value +- cache.Set(op.key, op.value, 1) +- } else { +- if v := cache.Get(op.key); v != nil && v != reference[op.key] { +- t.Fatalf("cache.Get(%d) = %d, want %d", op.key, v, reference[op.key]) +- } - } -- env.ApplyQuickFixes("b/go.mod", []protocol.Diagnostic{d}) - } -- env.AfterChange( -- NoDiagnostics(ForFile("b/go.mod")), -- ) - }) -} +diff -urN a/gopls/internal/util/lru/lru.go b/gopls/internal/util/lru/lru.go +--- a/gopls/internal/util/lru/lru.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/util/lru/lru.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,151 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// Sometimes users may have their module cache within the workspace. --// We shouldn't consider any module in the module cache to be in the workspace. --func TestGOMODCACHEInWorkspace(t *testing.T) { -- const mod = ` ---- a/go.mod -- --module a.com -- --go 1.12 ---- a/a.go -- --package a -- --func _() {} ---- a/c/c.go -- --package c ---- gopath/src/b/b.go -- --package b ---- gopath/pkg/mod/example.com/go.mod -- --module example.com +-// The lru package implements a fixed-size in-memory LRU cache. +-package lru - --go 1.12 ---- gopath/pkg/mod/example.com/main.go -- --package main --` -- WithOptions( -- EnvVars{"GOPATH": filepath.FromSlash("$SANDBOX_WORKDIR/gopath")}, -- Modes(Default), -- ).Run(t, mod, func(t *testing.T, env *Env) { -- env.Await( -- // Confirm that the build configuration is seen as valid, -- // even though there are technically multiple go.mod files in the -- // worskpace. -- LogMatching(protocol.Info, ".*valid build configuration = true.*", 1, false), -- ) -- }) --} +-import ( +- "container/heap" +- "fmt" +- "sync" +-) - --func TestAddAndRemoveGoWork(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) -- // Use a workspace with a module in the root directory to exercise the case -- // where a go.work is added to the existing root directory. This verifies -- // that we're detecting changes to the module source, not just the root -- // directory. -- const nomod = ` ---- go.mod -- --module a.com +-// A Cache is a fixed-size in-memory LRU cache. +-type Cache struct { +- capacity int - --go 1.16 ---- main.go -- --package main +- mu sync.Mutex +- used int // used capacity, in user-specified units +- m map[any]*entry // k/v lookup +- lru queue // min-atime priority queue of *entry +- clock int64 // clock time, incremented whenever the cache is updated +-} - --func main() {} ---- b/go.mod -- --module b.com +-type entry struct { +- key any +- value any +- size int // caller-specified size +- atime int64 // last access / set time +- index int // index of entry in the heap slice +-} - --go 1.16 ---- b/main.go -- --package main +-// New creates a new Cache with the given capacity, which must be positive. +-// +-// The cache capacity uses arbitrary units, which are specified during the Set +-// operation. +-func New(capacity int) *Cache { +- if capacity == 0 { +- panic("zero capacity") +- } - --func main() {} --` -- WithOptions( -- Modes(Default), -- ).Run(t, nomod, func(t *testing.T, env *Env) { -- env.OpenFile("main.go") -- env.OpenFile("b/main.go") -- // Since b/main.go is not in the workspace, it should have a warning on its -- // package declaration. -- env.AfterChange( -- NoDiagnostics(ForFile("main.go")), -- Diagnostics(env.AtRegexp("b/main.go", "package (main)")), -- ) -- env.WriteWorkspaceFile("go.work", `go 1.16 +- return &Cache{ +- capacity: capacity, +- m: make(map[any]*entry), +- } +-} - --use ( -- . -- b --) --`) -- env.AfterChange(NoDiagnostics()) -- // Removing the go.work file should put us back where we started. -- env.RemoveWorkspaceFile("go.work") +-// Get retrieves the value for the specified key, or nil if the key is not +-// found. +-// +-// If the key is found, its access time is updated. +-func (c *Cache) Get(key any) any { +- c.mu.Lock() +- defer c.mu.Unlock() - -- // TODO(golang/go#57558, golang/go#57508): file watching is asynchronous, -- // and we must wait for the view to be reconstructed before touching -- // b/main.go, so that the new view "knows" about b/main.go. This is simply -- // a bug, but awaiting the change here avoids it. -- env.Await(env.DoneWithChangeWatchedFiles()) +- c.clock++ // every access updates the clock - -- // TODO(rfindley): fix this bug: reopening b/main.go is necessary here -- // because we no longer "see" the file in any view. -- env.CloseBuffer("b/main.go") -- env.OpenFile("b/main.go") +- if e, ok := c.m[key]; ok { // cache hit +- e.atime = c.clock +- heap.Fix(&c.lru, e.index) +- return e.value +- } - -- env.AfterChange( -- NoDiagnostics(ForFile("main.go")), -- Diagnostics(env.AtRegexp("b/main.go", "package (main)")), -- ) -- }) +- return nil -} - --// Tests the fix for golang/go#52500. --func TestChangeTestVariant_Issue52500(t *testing.T) { -- const src = ` ---- go.mod -- --module mod.test +-// Set stores a value for the specified key, using its given size to update the +-// current cache size, evicting old entries as necessary to fit in the cache +-// capacity. +-// +-// Size must be a non-negative value. If size is larger than the cache +-// capacity, the value is not stored and the cache is not modified. +-func (c *Cache) Set(key, value any, size int) { +- if size < 0 { +- panic(fmt.Sprintf("size must be non-negative, got %d", size)) +- } +- if size > c.capacity { +- return // uncacheable +- } - --go 1.12 ---- main_test.go -- --package main_test +- c.mu.Lock() +- defer c.mu.Unlock() - --type Server struct{} +- c.clock++ - --const mainConst = otherConst ---- other_test.go -- --package main_test +- // Remove the existing cache entry for key, if it exists. +- e, ok := c.m[key] +- if ok { +- c.used -= e.size +- heap.Remove(&c.lru, e.index) +- delete(c.m, key) +- } - --const otherConst = 0 +- // Evict entries until the new value will fit. +- newUsed := c.used + size +- if newUsed < 0 { +- return // integer overflow; return silently +- } +- c.used = newUsed +- for c.used > c.capacity { +- // evict oldest entry +- e = heap.Pop(&c.lru).(*entry) +- c.used -= e.size +- delete(c.m, e.key) +- } - --func (Server) Foo() {} --` +- // Store the new value. +- // Opt: e is evicted, so it can be reused to reduce allocation. +- if e == nil { +- e = new(entry) +- } +- e.key = key +- e.value = value +- e.size = size +- e.atime = c.clock +- c.m[e.key] = e +- heap.Push(&c.lru, e) - -- Run(t, src, func(t *testing.T, env *Env) { -- env.OpenFile("other_test.go") -- env.RegexpReplace("other_test.go", "main_test", "main") +- if len(c.m) != len(c.lru) { +- panic("map and LRU are inconsistent") +- } +-} - -- // For this test to function, it is necessary to wait on both of the -- // expectations below: the bug is that when switching the package name in -- // other_test.go from main->main_test, metadata for main_test is not marked -- // as invalid. So we need to wait for the metadata of main_test.go to be -- // updated before moving other_test.go back to the main_test package. -- env.Await( -- Diagnostics(env.AtRegexp("other_test.go", "Server")), -- Diagnostics(env.AtRegexp("main_test.go", "otherConst")), -- ) -- env.RegexpReplace("other_test.go", "main", "main_test") -- env.AfterChange( -- NoDiagnostics(ForFile("other_test.go")), -- NoDiagnostics(ForFile("main_test.go")), -- ) +-// -- priority queue boilerplate -- - -- // This will cause a test failure if other_test.go is not in any package. -- _ = env.GoToDefinition(env.RegexpSearch("other_test.go", "Server")) -- }) --} +-// queue is a min-atime priority queue of cache entries. +-type queue []*entry - --// Test for golang/go#48929. --func TestClearNonWorkspaceDiagnostics(t *testing.T) { -- testenv.NeedsGo1Point(t, 18) // uses go.work +-func (q queue) Len() int { return len(q) } - -- const ws = ` ---- go.work -- --go 1.18 +-func (q queue) Less(i, j int) bool { return q[i].atime < q[j].atime } - --use ( -- ./b --) ---- a/go.mod -- --module a +-func (q queue) Swap(i, j int) { +- q[i], q[j] = q[j], q[i] +- q[i].index = i +- q[j].index = j +-} - --go 1.17 ---- a/main.go -- --package main +-func (q *queue) Push(x any) { +- e := x.(*entry) +- e.index = len(*q) +- *q = append(*q, e) +-} - --func main() { -- var V string +-func (q *queue) Pop() any { +- last := len(*q) - 1 +- e := (*q)[last] +- (*q)[last] = nil // aid GC +- *q = (*q)[:last] +- return e -} ---- b/go.mod -- --module b +diff -urN a/gopls/internal/util/lru/lru_test.go b/gopls/internal/util/lru/lru_test.go +--- a/gopls/internal/util/lru/lru_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/util/lru/lru_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,154 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --go 1.17 ---- b/main.go -- --package b +-package lru_test - -import ( -- _ "fmt" +- "bytes" +- cryptorand "crypto/rand" +- "fmt" +- "log" +- mathrand "math/rand" +- "strings" +- "testing" +- +- "golang.org/x/sync/errgroup" +- "golang.org/x/tools/gopls/internal/util/lru" -) --` -- Run(t, ws, func(t *testing.T, env *Env) { -- env.OpenFile("b/main.go") -- env.AfterChange( -- NoDiagnostics(ForFile("a/main.go")), -- ) -- env.OpenFile("a/main.go") -- env.AfterChange( -- Diagnostics(env.AtRegexp("a/main.go", "V"), WithMessage("not used")), -- ) -- env.CloseBuffer("a/main.go") - -- // Make an arbitrary edit because gopls explicitly diagnoses a/main.go -- // whenever it is "changed". -- // -- // TODO(rfindley): it should not be necessary to make another edit here. -- // Gopls should be smart enough to avoid diagnosing a. -- env.RegexpReplace("b/main.go", "package b", "package b // a package") -- env.AfterChange( -- NoDiagnostics(ForFile("a/main.go")), -- ) -- }) --} +-func TestCache(t *testing.T) { +- type get struct { +- key string +- want any +- } +- type set struct { +- key, value string +- } - --// Test that we don't get a version warning when the Go version in PATH is --// supported. --func TestOldGoNotification_SupportedVersion(t *testing.T) { -- v := goVersion(t) -- if v < lsp.OldestSupportedGoVersion() { -- t.Skipf("go version 1.%d is unsupported", v) +- tests := []struct { +- label string +- steps []any +- }{ +- {"empty cache", []any{ +- get{"a", nil}, +- get{"b", nil}, +- }}, +- {"zero-length string", []any{ +- set{"a", ""}, +- get{"a", ""}, +- }}, +- {"under capacity", []any{ +- set{"a", "123"}, +- set{"b", "456"}, +- get{"a", "123"}, +- get{"b", "456"}, +- }}, +- {"over capacity", []any{ +- set{"a", "123"}, +- set{"b", "456"}, +- set{"c", "78901"}, +- get{"a", nil}, +- get{"b", "456"}, +- get{"c", "78901"}, +- }}, +- {"access ordering", []any{ +- set{"a", "123"}, +- set{"b", "456"}, +- get{"a", "123"}, +- set{"c", "78901"}, +- get{"a", "123"}, +- get{"b", nil}, +- get{"c", "78901"}, +- }}, - } - -- Run(t, "", func(t *testing.T, env *Env) { -- env.OnceMet( -- InitialWorkspaceLoad, -- NoShownMessage("upgrade"), -- ) -- }) +- for _, test := range tests { +- t.Run(test.label, func(t *testing.T) { +- c := lru.New(10) +- for i, step := range test.steps { +- switch step := step.(type) { +- case get: +- if got := c.Get(step.key); got != step.want { +- t.Errorf("#%d: c.Get(%q) = %q, want %q", i, step.key, got, step.want) +- } +- case set: +- c.Set(step.key, step.value, len(step.value)) +- } +- } +- }) +- } -} - --// Test that we do get a version warning when the Go version in PATH is --// unsupported, though this test may never execute if we stop running CI at --// legacy Go versions (see also TestOldGoNotification_Fake) --func TestOldGoNotification_UnsupportedVersion(t *testing.T) { -- v := goVersion(t) -- if v >= lsp.OldestSupportedGoVersion() { -- t.Skipf("go version 1.%d is supported", v) +-// TestConcurrency exercises concurrent access to the same entry. +-// +-// It is a copy of TestConcurrency from the filecache package. +-func TestConcurrency(t *testing.T) { +- key := uniqueKey() +- const N = 100 // concurrency level +- +- // Construct N distinct values, each larger +- // than a typical 4KB OS file buffer page. +- var values [N][8192]byte +- for i := range values { +- if _, err := mathrand.Read(values[i][:]); err != nil { +- t.Fatalf("rand: %v", err) +- } - } - -- Run(t, "", func(t *testing.T, env *Env) { -- env.Await( -- // Note: cannot use OnceMet(InitialWorkspaceLoad, ...) here, as the -- // upgrade message may race with the IWL. -- ShownMessage("Please upgrade"), -- ) -- }) --} +- cache := lru.New(100 * 1e6) // 100MB cache - --func TestOldGoNotification_Fake(t *testing.T) { -- // Get the Go version from path, and make sure it's unsupported. -- // -- // In the future we'll stop running CI on legacy Go versions. By mutating the -- // oldest supported Go version here, we can at least ensure that the -- // ShowMessage pop-up works. -- ctx := context.Background() -- goversion, err := gocommand.GoVersion(ctx, gocommand.Invocation{}, &gocommand.Runner{}) -- if err != nil { -- t.Fatal(err) +- // get calls Get and verifies that the cache entry +- // matches one of the values passed to Set. +- get := func(mustBeFound bool) error { +- got := cache.Get(key) +- if got == nil { +- if !mustBeFound { +- return nil +- } +- return fmt.Errorf("Get did not return a value") +- } +- gotBytes := got.([]byte) +- for _, want := range values { +- if bytes.Equal(want[:], gotBytes) { +- return nil // a match +- } +- } +- return fmt.Errorf("Get returned a value that was never Set") +- } +- +- // Perform N concurrent calls to Set and Get. +- // All sets must succeed. +- // All gets must return nothing, or one of the Set values; +- // there is no third possibility. +- var group errgroup.Group +- for i := range values { +- i := i +- v := values[i][:] +- group.Go(func() error { +- cache.Set(key, v, len(v)) +- return nil +- }) +- group.Go(func() error { return get(false) }) - } -- defer func(t []lsp.GoVersionSupport) { -- lsp.GoVersionTable = t -- }(lsp.GoVersionTable) -- lsp.GoVersionTable = []lsp.GoVersionSupport{ -- {GoVersion: goversion, InstallGoplsVersion: "v1.0.0"}, +- if err := group.Wait(); err != nil { +- if strings.Contains(err.Error(), "operation not supported") || +- strings.Contains(err.Error(), "not implemented") { +- t.Skipf("skipping: %v", err) +- } +- t.Fatal(err) - } - -- Run(t, "", func(t *testing.T, env *Env) { -- env.Await( -- // Note: cannot use OnceMet(InitialWorkspaceLoad, ...) here, as the -- // upgrade message may race with the IWL. -- ShownMessage("Please upgrade"), -- ) -- }) +- // A final Get must report one of the values that was Set. +- if err := get(true); err != nil { +- t.Fatalf("final Get failed: %v", err) +- } -} - --// goVersion returns the version of the Go command in PATH. --func goVersion(t *testing.T) int { -- t.Helper() -- ctx := context.Background() -- goversion, err := gocommand.GoVersion(ctx, gocommand.Invocation{}, &gocommand.Runner{}) -- if err != nil { -- t.Fatal(err) +-// uniqueKey returns a key that has never been used before. +-func uniqueKey() (key [32]byte) { +- if _, err := cryptorand.Read(key[:]); err != nil { +- log.Fatalf("rand: %v", err) - } -- return goversion +- return -} -diff -urN a/gopls/internal/span/parse.go b/gopls/internal/span/parse.go ---- a/gopls/internal/span/parse.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/span/parse.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,114 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/util/maps/maps.go b/gopls/internal/util/maps/maps.go +--- a/gopls/internal/util/maps/maps.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/util/maps/maps.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,38 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package span -- --import ( -- "path/filepath" -- "strconv" -- "strings" -- "unicode/utf8" --) +-package maps - --// Parse returns the location represented by the input. --// Only file paths are accepted, not URIs. --// The returned span will be normalized, and thus if printed may produce a --// different string. --func Parse(input string) Span { -- return ParseInDir(input, ".") +-// Group returns a new non-nil map containing the elements of s grouped by the +-// keys returned from the key func. +-func Group[K comparable, V any](s []V, key func(V) K) map[K][]V { +- m := make(map[K][]V) +- for _, v := range s { +- k := key(v) +- m[k] = append(m[k], v) +- } +- return m -} - --// ParseInDir is like Parse, but interprets paths relative to wd. --func ParseInDir(input, wd string) Span { -- uri := func(path string) URI { -- if !filepath.IsAbs(path) { -- path = filepath.Join(wd, path) -- } -- return URIFromPath(path) -- } -- // :0:0#0-0:0#0 -- valid := input -- var hold, offset int -- hadCol := false -- suf := rstripSuffix(input) -- if suf.sep == "#" { -- offset = suf.num -- suf = rstripSuffix(suf.remains) -- } -- if suf.sep == ":" { -- valid = suf.remains -- hold = suf.num -- hadCol = true -- suf = rstripSuffix(suf.remains) -- } -- switch { -- case suf.sep == ":": -- return New(uri(suf.remains), NewPoint(suf.num, hold, offset), Point{}) -- case suf.sep == "-": -- // we have a span, fall out of the case to continue -- default: -- // separator not valid, rewind to either the : or the start -- return New(uri(valid), NewPoint(hold, 0, offset), Point{}) -- } -- // only the span form can get here -- // at this point we still don't know what the numbers we have mean -- // if have not yet seen a : then we might have either a line or a column depending -- // on whether start has a column or not -- // we build an end point and will fix it later if needed -- end := NewPoint(suf.num, hold, offset) -- hold, offset = 0, 0 -- suf = rstripSuffix(suf.remains) -- if suf.sep == "#" { -- offset = suf.num -- suf = rstripSuffix(suf.remains) -- } -- if suf.sep != ":" { -- // turns out we don't have a span after all, rewind -- return New(uri(valid), end, Point{}) +-// Keys returns the keys of the map M. +-func Keys[M ~map[K]V, K comparable, V any](m M) []K { +- r := make([]K, 0, len(m)) +- for k := range m { +- r = append(r, k) - } -- valid = suf.remains -- hold = suf.num -- suf = rstripSuffix(suf.remains) -- if suf.sep != ":" { -- // line#offset only -- return New(uri(valid), NewPoint(hold, 0, offset), end) +- return r +-} +- +-// SameKeys reports whether x and y have equal sets of keys. +-func SameKeys[K comparable, V1, V2 any](x map[K]V1, y map[K]V2) bool { +- if len(x) != len(y) { +- return false - } -- // we have a column, so if end only had one number, it is also the column -- if !hadCol { -- end = NewPoint(suf.num, end.v.Line, end.v.Offset) +- for k := range x { +- if _, ok := y[k]; !ok { +- return false +- } - } -- return New(uri(suf.remains), NewPoint(suf.num, hold, offset), end) +- return true -} +diff -urN a/gopls/internal/util/pathutil/util.go b/gopls/internal/util/pathutil/util.go +--- a/gopls/internal/util/pathutil/util.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/util/pathutil/util.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,49 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --type suffix struct { -- remains string -- sep string -- num int --} +-package pathutil - --func rstripSuffix(input string) suffix { -- if len(input) == 0 { -- return suffix{"", "", -1} -- } -- remains := input +-import ( +- "path/filepath" +- "strings" +-) - -- // Remove optional trailing decimal number. -- num := -1 -- last := strings.LastIndexFunc(remains, func(r rune) bool { return r < '0' || r > '9' }) -- if last >= 0 && last < len(remains)-1 { -- number, err := strconv.ParseInt(remains[last+1:], 10, 64) -- if err == nil { -- num = int(number) -- remains = remains[:last+1] +-// InDir checks whether path is in the file tree rooted at dir. +-// It checks only the lexical form of the file names. +-// It does not consider symbolic links. +-// +-// Copied from go/src/cmd/go/internal/search/search.go. +-func InDir(dir, path string) bool { +- pv := strings.ToUpper(filepath.VolumeName(path)) +- dv := strings.ToUpper(filepath.VolumeName(dir)) +- path = path[len(pv):] +- dir = dir[len(dv):] +- switch { +- default: +- return false +- case pv != dv: +- return false +- case len(path) == len(dir): +- if path == dir { +- return true +- } +- return false +- case dir == "": +- return path != "" +- case len(path) > len(dir): +- if dir[len(dir)-1] == filepath.Separator { +- if path[:len(dir)] == dir { +- return path[len(dir):] != "" +- } +- return false - } +- if path[len(dir)] == filepath.Separator && path[:len(dir)] == dir { +- if len(path) == len(dir)+1 { +- return true +- } +- return path[len(dir)+1:] != "" +- } +- return false - } -- // now see if we have a trailing separator -- r, w := utf8.DecodeLastRuneInString(remains) -- // TODO(adonovan): this condition is clearly wrong. Should the third byte be '-'? -- if r != ':' && r != '#' && r == '#' { -- return suffix{input, "", -1} -- } -- remains = remains[:len(remains)-w] -- return suffix{remains, string(r), num} -} -diff -urN a/gopls/internal/span/span.go b/gopls/internal/span/span.go ---- a/gopls/internal/span/span.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/span/span.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,249 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/util/persistent/map.go b/gopls/internal/util/persistent/map.go +--- a/gopls/internal/util/persistent/map.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/util/persistent/map.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,322 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --// Package span contains support for representing with positions and ranges in --// text files. --package span +-// The persistent package defines various persistent data structures; +-// that is, data structures that can be efficiently copied and modified +-// in sublinear time. +-package persistent - -import ( -- "encoding/json" - "fmt" -- "go/token" -- "path" -- "sort" +- "math/rand" - "strings" +- "sync/atomic" - -- "golang.org/x/tools/gopls/internal/lsp/safetoken" +- "golang.org/x/tools/gopls/internal/util/constraints" -) - --// A Span represents a range of text within a source file. The start --// and end points of a valid span may be hold either its byte offset, --// or its (line, column) pair, or both. Columns are measured in bytes. +-// Implementation details: +-// * Each value is reference counted by nodes which hold it. +-// * Each node is reference counted by its parent nodes. +-// * Each map is considered a top-level parent node from reference counting perspective. +-// * Each change does always effectively produce a new top level node. -// --// Spans are appropriate in user interfaces (e.g. command-line tools) --// and tests where a position is notated without access to the content --// of the file. +-// Functions which operate directly with nodes do have a notation in form of +-// `foo(arg1:+n1, arg2:+n2) (ret1:+n3)`. +-// Each argument is followed by a delta change to its reference counter. +-// In case if no change is expected, the delta will be `-0`. +- +-// Map is an associative mapping from keys to values. -// --// Use protocol.Mapper to convert between Span and other --// representations, such as go/token (also UTF-8) or the LSP protocol --// (UTF-16). The latter requires access to file contents. +-// Maps can be Cloned in constant time. +-// Get, Set, and Delete operations are done on average in logarithmic time. +-// Maps can be merged (via SetAll) in O(m log(n/m)) time for maps of size n and m, where m < n. -// --// See overview comments at ../lsp/protocol/mapper.go. --type Span struct { -- v span +-// Values are reference counted, and a client-supplied release function +-// is called when a value is no longer referenced by a map or any clone. +-// +-// Internally the implementation is based on a randomized persistent treap: +-// https://en.wikipedia.org/wiki/Treap. +-// +-// The zero value is ready to use. +-type Map[K constraints.Ordered, V any] struct { +- // Map is a generic wrapper around a non-generic implementation to avoid a +- // significant increase in the size of the executable. +- root *mapNode -} - --// Point represents a single point within a file. --// In general this should only be used as part of a Span, as on its own it --// does not carry enough information. --type Point struct { -- v point +-func (*Map[K, V]) less(l, r any) bool { +- return l.(K) < r.(K) -} - --// The private span/point types have public fields to support JSON --// encoding, but the public Span/Point types hide these fields by --// defining methods that shadow them. (This is used by a few of the --// command-line tool subcommands, which emit spans and have a -json --// flag.) -- --type span struct { -- URI URI `json:"uri"` -- Start point `json:"start"` -- End point `json:"end"` +-func (m *Map[K, V]) String() string { +- var buf strings.Builder +- buf.WriteByte('{') +- var sep string +- m.Range(func(k K, v V) { +- fmt.Fprintf(&buf, "%s%v: %v", sep, k, v) +- sep = ", " +- }) +- buf.WriteByte('}') +- return buf.String() -} - --type point struct { -- Line int `json:"line"` // 1-based line number -- Column int `json:"column"` // 1-based, UTF-8 codes (bytes) -- Offset int `json:"offset"` // 0-based byte offset +-type mapNode struct { +- key any +- value *refValue +- weight uint64 +- refCount int32 +- left, right *mapNode -} - --// Invalid is a span that reports false from IsValid --var Invalid = Span{v: span{Start: invalidPoint.v, End: invalidPoint.v}} -- --var invalidPoint = Point{v: point{Line: 0, Column: 0, Offset: -1}} -- --func New(uri URI, start, end Point) Span { -- s := Span{v: span{URI: uri, Start: start.v, End: end.v}} -- s.v.clean() -- return s +-type refValue struct { +- refCount int32 +- value any +- release func(key, value any) -} - --func NewPoint(line, col, offset int) Point { -- p := Point{v: point{Line: line, Column: col, Offset: offset}} -- p.v.clean() -- return p +-func newNodeWithRef[K constraints.Ordered, V any](key K, value V, release func(key, value any)) *mapNode { +- return &mapNode{ +- key: key, +- value: &refValue{ +- value: value, +- release: release, +- refCount: 1, +- }, +- refCount: 1, +- weight: rand.Uint64(), +- } -} - --// SortSpans sorts spans into a stable but unspecified order. --func SortSpans(spans []Span) { -- sort.SliceStable(spans, func(i, j int) bool { -- return compare(spans[i], spans[j]) < 0 -- }) +-func (node *mapNode) shallowCloneWithRef() *mapNode { +- atomic.AddInt32(&node.value.refCount, 1) +- return &mapNode{ +- key: node.key, +- value: node.value, +- weight: node.weight, +- refCount: 1, +- } -} - --// compare implements a three-valued ordered comparison of Spans. --func compare(a, b Span) int { -- // This is a textual comparison. It does not perform path -- // cleaning, case folding, resolution of symbolic links, -- // testing for existence, or any I/O. -- if cmp := strings.Compare(string(a.URI()), string(b.URI())); cmp != 0 { -- return cmp -- } -- if cmp := comparePoint(a.v.Start, b.v.Start); cmp != 0 { -- return cmp +-func (node *mapNode) incref() *mapNode { +- if node != nil { +- atomic.AddInt32(&node.refCount, 1) - } -- return comparePoint(a.v.End, b.v.End) +- return node -} - --func comparePoint(a, b point) int { -- if !a.hasPosition() { -- if a.Offset < b.Offset { -- return -1 -- } -- if a.Offset > b.Offset { -- return 1 -- } -- return 0 -- } -- if a.Line < b.Line { -- return -1 -- } -- if a.Line > b.Line { -- return 1 -- } -- if a.Column < b.Column { -- return -1 +-func (node *mapNode) decref() { +- if node == nil { +- return - } -- if a.Column > b.Column { -- return 1 +- if atomic.AddInt32(&node.refCount, -1) == 0 { +- if atomic.AddInt32(&node.value.refCount, -1) == 0 { +- if node.value.release != nil { +- node.value.release(node.key, node.value.value) +- } +- node.value.value = nil +- node.value.release = nil +- } +- node.left.decref() +- node.right.decref() - } -- return 0 -} - --func (s Span) HasPosition() bool { return s.v.Start.hasPosition() } --func (s Span) HasOffset() bool { return s.v.Start.hasOffset() } --func (s Span) IsValid() bool { return s.v.Start.isValid() } --func (s Span) IsPoint() bool { return s.v.Start == s.v.End } --func (s Span) URI() URI { return s.v.URI } --func (s Span) Start() Point { return Point{s.v.Start} } --func (s Span) End() Point { return Point{s.v.End} } --func (s *Span) MarshalJSON() ([]byte, error) { return json.Marshal(&s.v) } --func (s *Span) UnmarshalJSON(b []byte) error { return json.Unmarshal(b, &s.v) } -- --func (p Point) HasPosition() bool { return p.v.hasPosition() } --func (p Point) HasOffset() bool { return p.v.hasOffset() } --func (p Point) IsValid() bool { return p.v.isValid() } --func (p *Point) MarshalJSON() ([]byte, error) { return json.Marshal(&p.v) } --func (p *Point) UnmarshalJSON(b []byte) error { return json.Unmarshal(b, &p.v) } --func (p Point) Line() int { -- if !p.v.hasPosition() { -- panic(fmt.Errorf("position not set in %v", p.v)) +-// Clone returns a copy of the given map. It is a responsibility of the caller +-// to Destroy it at later time. +-func (pm *Map[K, V]) Clone() *Map[K, V] { +- return &Map[K, V]{ +- root: pm.root.incref(), - } -- return p.v.Line -} --func (p Point) Column() int { -- if !p.v.hasPosition() { -- panic(fmt.Errorf("position not set in %v", p.v)) -- } -- return p.v.Column +- +-// Destroy destroys the map. +-// +-// After Destroy, the Map should not be used again. +-func (pm *Map[K, V]) Destroy() { +- // The implementation of these two functions is the same, +- // but their intent is different. +- pm.Clear() -} --func (p Point) Offset() int { -- if !p.v.hasOffset() { -- panic(fmt.Errorf("offset not set in %v", p.v)) -- } -- return p.v.Offset +- +-// Clear removes all entries from the map. +-func (pm *Map[K, V]) Clear() { +- pm.root.decref() +- pm.root = nil -} - --func (p point) hasPosition() bool { return p.Line > 0 } --func (p point) hasOffset() bool { return p.Offset >= 0 } --func (p point) isValid() bool { return p.hasPosition() || p.hasOffset() } --func (p point) isZero() bool { -- return (p.Line == 1 && p.Column == 1) || (!p.hasPosition() && p.Offset == 0) +-// Keys returns all keys present in the map. +-func (pm *Map[K, V]) Keys() []K { +- var keys []K +- pm.root.forEach(func(k, _ any) { +- keys = append(keys, k.(K)) +- }) +- return keys -} - --func (s *span) clean() { -- //this presumes the points are already clean -- if !s.End.isValid() || (s.End == point{}) { -- s.End = s.Start -- } +-// Range calls f sequentially in ascending key order for all entries in the map. +-func (pm *Map[K, V]) Range(f func(key K, value V)) { +- pm.root.forEach(func(k, v any) { +- f(k.(K), v.(V)) +- }) -} - --func (p *point) clean() { -- if p.Line < 0 { -- p.Line = 0 +-func (node *mapNode) forEach(f func(key, value any)) { +- if node == nil { +- return - } -- if p.Column <= 0 { -- if p.Line > 0 { -- p.Column = 1 +- node.left.forEach(f) +- f(node.key, node.value.value) +- node.right.forEach(f) +-} +- +-// Get returns the map value associated with the specified key. +-// The ok result indicates whether an entry was found in the map. +-func (pm *Map[K, V]) Get(key K) (V, bool) { +- node := pm.root +- for node != nil { +- if key < node.key.(K) { +- node = node.left +- } else if node.key.(K) < key { +- node = node.right - } else { -- p.Column = 0 +- return node.value.value.(V), true - } - } -- if p.Offset == 0 && (p.Line > 1 || p.Column > 1) { -- p.Offset = -1 -- } +- var zero V +- return zero, false -} - --// Format implements fmt.Formatter to print the Location in a standard form. --// The format produced is one that can be read back in using Parse. --func (s Span) Format(f fmt.State, c rune) { -- fullForm := f.Flag('+') -- preferOffset := f.Flag('#') -- // we should always have a uri, simplify if it is file format -- //TODO: make sure the end of the uri is unambiguous -- uri := string(s.v.URI) -- if c == 'f' { -- uri = path.Base(uri) -- } else if !fullForm { -- uri = s.v.URI.Filename() -- } -- fmt.Fprint(f, uri) -- if !s.IsValid() || (!fullForm && s.v.Start.isZero() && s.v.End.isZero()) { -- return -- } -- // see which bits of start to write -- printOffset := s.HasOffset() && (fullForm || preferOffset || !s.HasPosition()) -- printLine := s.HasPosition() && (fullForm || !printOffset) -- printColumn := printLine && (fullForm || (s.v.Start.Column > 1 || s.v.End.Column > 1)) -- fmt.Fprint(f, ":") -- if printLine { -- fmt.Fprintf(f, "%d", s.v.Start.Line) -- } -- if printColumn { -- fmt.Fprintf(f, ":%d", s.v.Start.Column) +-// SetAll updates the map with key/value pairs from the other map, overwriting existing keys. +-// It is equivalent to calling Set for each entry in the other map but is more efficient. +-func (pm *Map[K, V]) SetAll(other *Map[K, V]) { +- root := pm.root +- pm.root = union(root, other.root, pm.less, true) +- root.decref() +-} +- +-// Set updates the value associated with the specified key. +-// If release is non-nil, it will be called with entry's key and value once the +-// key is no longer contained in the map or any clone. +-func (pm *Map[K, V]) Set(key K, value V, release func(key, value any)) { +- first := pm.root +- second := newNodeWithRef(key, value, release) +- pm.root = union(first, second, pm.less, true) +- first.decref() +- second.decref() +-} +- +-// union returns a new tree which is a union of first and second one. +-// If overwrite is set to true, second one would override a value for any duplicate keys. +-// +-// union(first:-0, second:-0) (result:+1) +-// Union borrows both subtrees without affecting their refcount and returns a +-// new reference that the caller is expected to call decref. +-func union(first, second *mapNode, less func(any, any) bool, overwrite bool) *mapNode { +- if first == nil { +- return second.incref() - } -- if printOffset { -- fmt.Fprintf(f, "#%d", s.v.Start.Offset) +- if second == nil { +- return first.incref() - } -- // start is written, do we need end? -- if s.IsPoint() { -- return +- +- if first.weight < second.weight { +- second, first, overwrite = first, second, !overwrite - } -- // we don't print the line if it did not change -- printLine = fullForm || (printLine && s.v.End.Line > s.v.Start.Line) -- fmt.Fprint(f, "-") -- if printLine { -- fmt.Fprintf(f, "%d", s.v.End.Line) +- +- left, mid, right := split(second, first.key, less, false) +- var result *mapNode +- if overwrite && mid != nil { +- result = mid.shallowCloneWithRef() +- } else { +- result = first.shallowCloneWithRef() +- } +- result.weight = first.weight +- result.left = union(first.left, left, less, overwrite) +- result.right = union(first.right, right, less, overwrite) +- left.decref() +- mid.decref() +- right.decref() +- return result +-} +- +-// split the tree midway by the key into three different ones. +-// Return three new trees: left with all nodes with smaller than key, mid with +-// the node matching the key, right with all nodes larger than key. +-// If there are no nodes in one of trees, return nil instead of it. +-// If requireMid is set (such as during deletion), then all return arguments +-// are nil if mid is not found. +-// +-// split(n:-0) (left:+1, mid:+1, right:+1) +-// Split borrows n without affecting its refcount, and returns three +-// new references that the caller is expected to call decref. +-func split(n *mapNode, key any, less func(any, any) bool, requireMid bool) (left, mid, right *mapNode) { +- if n == nil { +- return nil, nil, nil - } -- if printColumn { -- if printLine { -- fmt.Fprint(f, ":") +- +- if less(n.key, key) { +- left, mid, right := split(n.right, key, less, requireMid) +- if requireMid && mid == nil { +- return nil, nil, nil - } -- fmt.Fprintf(f, "%d", s.v.End.Column) +- newN := n.shallowCloneWithRef() +- newN.left = n.left.incref() +- newN.right = left +- return newN, mid, right +- } else if less(key, n.key) { +- left, mid, right := split(n.left, key, less, requireMid) +- if requireMid && mid == nil { +- return nil, nil, nil +- } +- newN := n.shallowCloneWithRef() +- newN.left = right +- newN.right = n.right.incref() +- return left, mid, newN - } -- if printOffset { -- fmt.Fprintf(f, "#%d", s.v.End.Offset) +- mid = n.shallowCloneWithRef() +- return n.left.incref(), mid, n.right.incref() +-} +- +-// Delete deletes the value for a key. +-// +-// The result reports whether the key was present in the map. +-func (pm *Map[K, V]) Delete(key K) bool { +- root := pm.root +- left, mid, right := split(root, key, pm.less, true) +- if mid == nil { +- return false - } +- pm.root = merge(left, right) +- left.decref() +- mid.decref() +- right.decref() +- root.decref() +- return true -} - --// SetRange implements packagestest.rangeSetter, allowing --// gopls' test suites to use Spans instead of Range in parameters. --func (span *Span) SetRange(file *token.File, start, end token.Pos) { -- point := func(pos token.Pos) Point { -- posn := safetoken.Position(file, pos) -- return NewPoint(posn.Line, posn.Column, posn.Offset) +-// merge two trees while preserving the weight invariant. +-// All nodes in left must have smaller keys than any node in right. +-// +-// merge(left:-0, right:-0) (result:+1) +-// Merge borrows its arguments without affecting their refcount +-// and returns a new reference that the caller is expected to call decref. +-func merge(left, right *mapNode) *mapNode { +- switch { +- case left == nil: +- return right.incref() +- case right == nil: +- return left.incref() +- case left.weight > right.weight: +- root := left.shallowCloneWithRef() +- root.left = left.left.incref() +- root.right = merge(left.right, right) +- return root +- default: +- root := right.shallowCloneWithRef() +- root.left = merge(left, right.left) +- root.right = right.right.incref() +- return root - } -- *span = New(URIFromPath(file.Name()), point(start), point(end)) -} -diff -urN a/gopls/internal/span/span_test.go b/gopls/internal/span/span_test.go ---- a/gopls/internal/span/span_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/span/span_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,57 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/util/persistent/map_test.go b/gopls/internal/util/persistent/map_test.go +--- a/gopls/internal/util/persistent/map_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/util/persistent/map_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,352 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --package span_test +-package persistent - -import ( - "fmt" -- "path/filepath" -- "strings" +- "math/rand" +- "reflect" +- "sync/atomic" - "testing" -- -- "golang.org/x/tools/gopls/internal/span" -) - --func TestFormat(t *testing.T) { -- formats := []string{"%v", "%#v", "%+v"} +-type mapEntry struct { +- key int +- value int +-} - -- // Element 0 is the input, and the elements 0-2 are the expected -- // output in [%v %#v %+v] formats. Thus the first must be in -- // canonical form (invariant under span.Parse + fmt.Sprint). -- // The '#' form displays offsets; the '+' form outputs a URI. -- // If len=4, element 0 is a noncanonical input and 1-3 are expected outputs. -- for _, test := range [][]string{ -- {"C:/file_a", "C:/file_a", "file:///C:/file_a:#0"}, -- {"C:/file_b:1:2", "C:/file_b:1:2", "file:///C:/file_b:1:2"}, -- {"C:/file_c:1000", "C:/file_c:1000", "file:///C:/file_c:1000:1"}, -- {"C:/file_d:14:9", "C:/file_d:14:9", "file:///C:/file_d:14:9"}, -- {"C:/file_e:1:2-7", "C:/file_e:1:2-7", "file:///C:/file_e:1:2-1:7"}, -- {"C:/file_f:500-502", "C:/file_f:500-502", "file:///C:/file_f:500:1-502:1"}, -- {"C:/file_g:3:7-8", "C:/file_g:3:7-8", "file:///C:/file_g:3:7-3:8"}, -- {"C:/file_h:3:7-4:8", "C:/file_h:3:7-4:8", "file:///C:/file_h:3:7-4:8"}, -- {"C:/file_i:#100", "C:/file_i:#100", "file:///C:/file_i:#100"}, -- {"C:/file_j:#26-#28", "C:/file_j:#26-#28", "file:///C:/file_j:#26-0#28"}, // 0#28? -- {"C:/file_h:3:7#26-4:8#37", // not canonical -- "C:/file_h:3:7-4:8", "C:/file_h:#26-#37", "file:///C:/file_h:3:7#26-4:8#37"}} { -- input := test[0] -- spn := span.Parse(input) -- wants := test[0:3] -- if len(test) == 4 { -- wants = test[1:4] -- } -- for i, format := range formats { -- want := toPath(wants[i]) -- if got := fmt.Sprintf(format, spn); got != want { -- t.Errorf("Sprintf(%q, %q) = %q, want %q", format, input, got, want) -- } -- } -- } +-type validatedMap struct { +- impl *Map[int, int] +- expected map[int]int // current key-value mapping. +- deleted map[mapEntry]int // maps deleted entries to their clock time of last deletion +- seen map[mapEntry]int // maps seen entries to their clock time of last insertion +- clock int -} - --func toPath(value string) string { -- if strings.HasPrefix(value, "file://") { -- return value +-func TestSimpleMap(t *testing.T) { +- deletedEntries := make(map[mapEntry]int) +- seenEntries := make(map[mapEntry]int) +- +- m1 := &validatedMap{ +- impl: new(Map[int, int]), +- expected: make(map[int]int), +- deleted: deletedEntries, +- seen: seenEntries, - } -- return filepath.FromSlash(value) --} -diff -urN a/gopls/internal/span/uri.go b/gopls/internal/span/uri.go ---- a/gopls/internal/span/uri.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/span/uri.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,177 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package span +- m3 := m1.clone() +- validateRef(t, m1, m3) +- m3.set(t, 8, 8) +- validateRef(t, m1, m3) +- m3.destroy() - --import ( -- "fmt" -- "net/url" -- "os" -- "path/filepath" -- "runtime" -- "strings" -- "unicode" --) +- assertSameMap(t, entrySet(deletedEntries), map[mapEntry]struct{}{ +- {key: 8, value: 8}: {}, +- }) - --const fileScheme = "file" +- validateRef(t, m1) +- m1.set(t, 1, 1) +- validateRef(t, m1) +- m1.set(t, 2, 2) +- validateRef(t, m1) +- m1.set(t, 3, 3) +- validateRef(t, m1) +- m1.remove(t, 2) +- validateRef(t, m1) +- m1.set(t, 6, 6) +- validateRef(t, m1) - --// URI represents the full URI for a file. --type URI string +- assertSameMap(t, entrySet(deletedEntries), map[mapEntry]struct{}{ +- {key: 2, value: 2}: {}, +- {key: 8, value: 8}: {}, +- }) - --func (uri URI) IsFile() bool { -- return strings.HasPrefix(string(uri), "file://") --} +- m2 := m1.clone() +- validateRef(t, m1, m2) +- m1.set(t, 6, 60) +- validateRef(t, m1, m2) +- m1.remove(t, 1) +- validateRef(t, m1, m2) - --// Filename returns the file path for the given URI. --// It is an error to call this on a URI that is not a valid filename. --func (uri URI) Filename() string { -- filename, err := filename(uri) -- if err != nil { -- panic(err) +- gotAllocs := int(testing.AllocsPerRun(10, func() { +- m1.impl.Delete(100) +- m1.impl.Delete(1) +- })) +- wantAllocs := 0 +- if gotAllocs != wantAllocs { +- t.Errorf("wanted %d allocs, got %d", wantAllocs, gotAllocs) - } -- return filepath.FromSlash(filename) --} - --func filename(uri URI) (string, error) { -- if uri == "" { -- return "", nil +- for i := 10; i < 14; i++ { +- m1.set(t, i, i) +- validateRef(t, m1, m2) - } - -- // This conservative check for the common case -- // of a simple non-empty absolute POSIX filename -- // avoids the allocation of a net.URL. -- if strings.HasPrefix(string(uri), "file:///") { -- rest := string(uri)[len("file://"):] // leave one slash -- for i := 0; i < len(rest); i++ { -- b := rest[i] -- // Reject these cases: -- if b < ' ' || b == 0x7f || // control character -- b == '%' || b == '+' || // URI escape -- b == ':' || // Windows drive letter -- b == '@' || b == '&' || b == '?' { // authority or query -- goto slow -- } -- } -- return rest, nil -- } --slow: +- m1.set(t, 10, 100) +- validateRef(t, m1, m2) - -- u, err := url.ParseRequestURI(string(uri)) -- if err != nil { -- return "", err -- } -- if u.Scheme != fileScheme { -- return "", fmt.Errorf("only file URIs are supported, got %q from %q", u.Scheme, uri) -- } -- // If the URI is a Windows URI, we trim the leading "/" and uppercase -- // the drive letter, which will never be case sensitive. -- if isWindowsDriveURIPath(u.Path) { -- u.Path = strings.ToUpper(string(u.Path[1])) + u.Path[2:] -- } +- m1.remove(t, 12) +- validateRef(t, m1, m2) - -- return u.Path, nil --} +- m2.set(t, 4, 4) +- validateRef(t, m1, m2) +- m2.set(t, 5, 5) +- validateRef(t, m1, m2) - --// TODO(adonovan): document this function, and any invariants of --// span.URI that it is supposed to establish. --func URIFromURI(s string) URI { -- if !strings.HasPrefix(s, "file://") { -- return URI(s) -- } +- m1.destroy() - -- if !strings.HasPrefix(s, "file:///") { -- // VS Code sends URLs with only two slashes, which are invalid. golang/go#39789. -- s = "file:///" + s[len("file://"):] -- } -- // Even though the input is a URI, it may not be in canonical form. VS Code -- // in particular over-escapes :, @, etc. Unescape and re-encode to canonicalize. -- path, err := url.PathUnescape(s[len("file://"):]) -- if err != nil { -- panic(err) -- } +- assertSameMap(t, entrySet(deletedEntries), map[mapEntry]struct{}{ +- {key: 2, value: 2}: {}, +- {key: 6, value: 60}: {}, +- {key: 8, value: 8}: {}, +- {key: 10, value: 10}: {}, +- {key: 10, value: 100}: {}, +- {key: 11, value: 11}: {}, +- {key: 12, value: 12}: {}, +- {key: 13, value: 13}: {}, +- }) - -- // File URIs from Windows may have lowercase drive letters. -- // Since drive letters are guaranteed to be case insensitive, -- // we change them to uppercase to remain consistent. -- // For example, file:///c:/x/y/z becomes file:///C:/x/y/z. -- if isWindowsDriveURIPath(path) { -- path = path[:1] + strings.ToUpper(string(path[1])) + path[2:] -- } -- u := url.URL{Scheme: fileScheme, Path: path} -- return URI(u.String()) --} +- m2.set(t, 7, 7) +- validateRef(t, m2) - --// SameExistingFile reports whether two spans denote the --// same existing file by querying the file system. --func SameExistingFile(a, b URI) bool { -- fa, err := filename(a) -- if err != nil { -- return false -- } -- fb, err := filename(b) -- if err != nil { -- return false -- } -- infoa, err := os.Stat(filepath.FromSlash(fa)) -- if err != nil { -- return false -- } -- infob, err := os.Stat(filepath.FromSlash(fb)) -- if err != nil { -- return false -- } -- return os.SameFile(infoa, infob) +- m2.destroy() +- +- assertSameMap(t, entrySet(seenEntries), entrySet(deletedEntries)) -} - --// URIFromPath returns a span URI for the supplied file path. --// --// For empty paths, URIFromPath returns the empty URI "". --// For non-empty paths, URIFromPath returns a uri with the file:// scheme. --func URIFromPath(path string) URI { -- if path == "" { -- return "" -- } -- // Handle standard library paths that contain the literal "$GOROOT". -- // TODO(rstambler): The go/packages API should allow one to determine a user's $GOROOT. -- const prefix = "$GOROOT" -- if len(path) >= len(prefix) && strings.EqualFold(prefix, path[:len(prefix)]) { -- suffix := path[len(prefix):] -- path = runtime.GOROOT() + suffix +-func TestRandomMap(t *testing.T) { +- deletedEntries := make(map[mapEntry]int) +- seenEntries := make(map[mapEntry]int) +- +- m := &validatedMap{ +- impl: new(Map[int, int]), +- expected: make(map[int]int), +- deleted: deletedEntries, +- seen: seenEntries, - } -- if !isWindowsDrivePath(path) { -- if abs, err := filepath.Abs(path); err == nil { -- path = abs +- +- keys := make([]int, 0, 1000) +- for i := 0; i < 1000; i++ { +- key := rand.Intn(10000) +- m.set(t, key, key) +- keys = append(keys, key) +- +- if i%10 == 1 { +- index := rand.Intn(len(keys)) +- last := len(keys) - 1 +- key = keys[index] +- keys[index], keys[last] = keys[last], keys[index] +- keys = keys[:last] +- +- m.remove(t, key) - } - } -- // Check the file path again, in case it became absolute. -- if isWindowsDrivePath(path) { -- path = "/" + strings.ToUpper(string(path[0])) + path[1:] -- } -- path = filepath.ToSlash(path) -- u := url.URL{ -- Scheme: fileScheme, -- Path: path, -- } -- return URI(u.String()) +- +- m.destroy() +- assertSameMap(t, entrySet(seenEntries), entrySet(deletedEntries)) -} - --// isWindowsDrivePath returns true if the file path is of the form used by --// Windows. We check if the path begins with a drive letter, followed by a ":". --// For example: C:/x/y/z. --func isWindowsDrivePath(path string) bool { -- if len(path) < 3 { -- return false +-func entrySet(m map[mapEntry]int) map[mapEntry]struct{} { +- set := make(map[mapEntry]struct{}) +- for k := range m { +- set[k] = struct{}{} - } -- return unicode.IsLetter(rune(path[0])) && path[1] == ':' +- return set -} - --// isWindowsDriveURIPath returns true if the file URI is of the format used by --// Windows URIs. The url.Parse package does not specially handle Windows paths --// (see golang/go#6027), so we check if the URI path has a drive prefix (e.g. "/C:"). --func isWindowsDriveURIPath(uri string) bool { -- if len(uri) < 4 { -- return false +-func TestUpdate(t *testing.T) { +- deletedEntries := make(map[mapEntry]int) +- seenEntries := make(map[mapEntry]int) +- +- m1 := &validatedMap{ +- impl: new(Map[int, int]), +- expected: make(map[int]int), +- deleted: deletedEntries, +- seen: seenEntries, - } -- return uri[0] == '/' && unicode.IsLetter(rune(uri[1])) && uri[2] == ':' +- m2 := m1.clone() +- +- m1.set(t, 1, 1) +- m1.set(t, 2, 2) +- m2.set(t, 2, 20) +- m2.set(t, 3, 3) +- m1.setAll(t, m2) +- +- m1.destroy() +- m2.destroy() +- assertSameMap(t, entrySet(seenEntries), entrySet(deletedEntries)) -} -diff -urN a/gopls/internal/span/uri_test.go b/gopls/internal/span/uri_test.go ---- a/gopls/internal/span/uri_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/span/uri_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,117 +0,0 @@ --// Copyright 2019 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --//go:build !windows --// +build !windows +-func validateRef(t *testing.T, maps ...*validatedMap) { +- t.Helper() - --package span_test +- actualCountByEntry := make(map[mapEntry]int32) +- nodesByEntry := make(map[mapEntry]map[*mapNode]struct{}) +- expectedCountByEntry := make(map[mapEntry]int32) +- for i, m := range maps { +- dfsRef(m.impl.root, actualCountByEntry, nodesByEntry) +- dumpMap(t, fmt.Sprintf("%d:", i), m.impl.root) +- } +- for entry, nodes := range nodesByEntry { +- expectedCountByEntry[entry] = int32(len(nodes)) +- } +- assertSameMap(t, expectedCountByEntry, actualCountByEntry) +-} - --import ( -- "testing" +-func dfsRef(node *mapNode, countByEntry map[mapEntry]int32, nodesByEntry map[mapEntry]map[*mapNode]struct{}) { +- if node == nil { +- return +- } - -- "golang.org/x/tools/gopls/internal/span" --) +- entry := mapEntry{key: node.key.(int), value: node.value.value.(int)} +- countByEntry[entry] = atomic.LoadInt32(&node.value.refCount) - --// TestURI tests the conversion between URIs and filenames. The test cases --// include Windows-style URIs and filepaths, but we avoid having OS-specific --// tests by using only forward slashes, assuming that the standard library --// functions filepath.ToSlash and filepath.FromSlash do not need testing. --func TestURIFromPath(t *testing.T) { -- for _, test := range []struct { -- path, wantFile string -- wantURI span.URI -- }{ -- { -- path: ``, -- wantFile: ``, -- wantURI: span.URI(""), -- }, -- { -- path: `C:/Windows/System32`, -- wantFile: `C:/Windows/System32`, -- wantURI: span.URI("file:///C:/Windows/System32"), -- }, -- { -- path: `C:/Go/src/bob.go`, -- wantFile: `C:/Go/src/bob.go`, -- wantURI: span.URI("file:///C:/Go/src/bob.go"), -- }, -- { -- path: `c:/Go/src/bob.go`, -- wantFile: `C:/Go/src/bob.go`, -- wantURI: span.URI("file:///C:/Go/src/bob.go"), -- }, -- { -- path: `/path/to/dir`, -- wantFile: `/path/to/dir`, -- wantURI: span.URI("file:///path/to/dir"), -- }, -- { -- path: `/a/b/c/src/bob.go`, -- wantFile: `/a/b/c/src/bob.go`, -- wantURI: span.URI("file:///a/b/c/src/bob.go"), -- }, -- { -- path: `c:/Go/src/bob george/george/george.go`, -- wantFile: `C:/Go/src/bob george/george/george.go`, -- wantURI: span.URI("file:///C:/Go/src/bob%20george/george/george.go"), -- }, -- } { -- got := span.URIFromPath(test.path) -- if got != test.wantURI { -- t.Errorf("URIFromPath(%q): got %q, expected %q", test.path, got, test.wantURI) -- } -- gotFilename := got.Filename() -- if gotFilename != test.wantFile { -- t.Errorf("Filename(%q): got %q, expected %q", got, gotFilename, test.wantFile) -- } +- nodes, ok := nodesByEntry[entry] +- if !ok { +- nodes = make(map[*mapNode]struct{}) +- nodesByEntry[entry] = nodes - } +- nodes[node] = struct{}{} +- +- dfsRef(node.left, countByEntry, nodesByEntry) +- dfsRef(node.right, countByEntry, nodesByEntry) -} - --func TestURIFromURI(t *testing.T) { -- for _, test := range []struct { -- inputURI, wantFile string -- wantURI span.URI -- }{ -- { -- inputURI: `file:///c:/Go/src/bob%20george/george/george.go`, -- wantFile: `C:/Go/src/bob george/george/george.go`, -- wantURI: span.URI("file:///C:/Go/src/bob%20george/george/george.go"), -- }, -- { -- inputURI: `file:///C%3A/Go/src/bob%20george/george/george.go`, -- wantFile: `C:/Go/src/bob george/george/george.go`, -- wantURI: span.URI("file:///C:/Go/src/bob%20george/george/george.go"), -- }, -- { -- inputURI: `file:///path/to/%25p%25ercent%25/per%25cent.go`, -- wantFile: `/path/to/%p%ercent%/per%cent.go`, -- wantURI: span.URI(`file:///path/to/%25p%25ercent%25/per%25cent.go`), -- }, -- { -- inputURI: `file:///C%3A/`, -- wantFile: `C:/`, -- wantURI: span.URI(`file:///C:/`), -- }, -- { -- inputURI: `file:///`, -- wantFile: `/`, -- wantURI: span.URI(`file:///`), -- }, -- { -- inputURI: `file://wsl%24/Ubuntu/home/wdcui/repo/VMEnclaves/cvm-runtime`, -- wantFile: `/wsl$/Ubuntu/home/wdcui/repo/VMEnclaves/cvm-runtime`, -- wantURI: span.URI(`file:///wsl$/Ubuntu/home/wdcui/repo/VMEnclaves/cvm-runtime`), -- }, -- } { -- got := span.URIFromURI(test.inputURI) -- if got != test.wantURI { -- t.Errorf("NewURI(%q): got %q, expected %q", test.inputURI, got, test.wantURI) -- } -- gotFilename := got.Filename() -- if gotFilename != test.wantFile { -- t.Errorf("Filename(%q): got %q, expected %q", got, gotFilename, test.wantFile) -- } +-func dumpMap(t *testing.T, prefix string, n *mapNode) { +- if n == nil { +- t.Logf("%s nil", prefix) +- return - } +- t.Logf("%s {key: %v, value: %v (ref: %v), ref: %v, weight: %v}", prefix, n.key, n.value.value, n.value.refCount, n.refCount, n.weight) +- dumpMap(t, prefix+"l", n.left) +- dumpMap(t, prefix+"r", n.right) -} -diff -urN a/gopls/internal/span/uri_windows_test.go b/gopls/internal/span/uri_windows_test.go ---- a/gopls/internal/span/uri_windows_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/span/uri_windows_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,112 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --//go:build windows --// +build windows - --package span_test +-func (vm *validatedMap) validate(t *testing.T) { +- t.Helper() - --import ( -- "testing" +- validateNode(t, vm.impl.root) - -- "golang.org/x/tools/gopls/internal/span" --) +- // Note: this validation may not make sense if maps were constructed using +- // SetAll operations. If this proves to be problematic, remove the clock, +- // deleted, and seen fields. +- for key, value := range vm.expected { +- entry := mapEntry{key: key, value: value} +- if deleteAt := vm.deleted[entry]; deleteAt > vm.seen[entry] { +- t.Fatalf("entry is deleted prematurely, key: %d, value: %d", key, value) +- } +- } - --// TestURI tests the conversion between URIs and filenames. The test cases --// include Windows-style URIs and filepaths, but we avoid having OS-specific --// tests by using only forward slashes, assuming that the standard library --// functions filepath.ToSlash and filepath.FromSlash do not need testing. --func TestURIFromPath(t *testing.T) { -- for _, test := range []struct { -- path, wantFile string -- wantURI span.URI -- }{ -- { -- path: ``, -- wantFile: ``, -- wantURI: span.URI(""), -- }, -- { -- path: `C:\Windows\System32`, -- wantFile: `C:\Windows\System32`, -- wantURI: span.URI("file:///C:/Windows/System32"), -- }, -- { -- path: `C:\Go\src\bob.go`, -- wantFile: `C:\Go\src\bob.go`, -- wantURI: span.URI("file:///C:/Go/src/bob.go"), -- }, -- { -- path: `c:\Go\src\bob.go`, -- wantFile: `C:\Go\src\bob.go`, -- wantURI: span.URI("file:///C:/Go/src/bob.go"), -- }, -- { -- path: `\path\to\dir`, -- wantFile: `C:\path\to\dir`, -- wantURI: span.URI("file:///C:/path/to/dir"), -- }, -- { -- path: `\a\b\c\src\bob.go`, -- wantFile: `C:\a\b\c\src\bob.go`, -- wantURI: span.URI("file:///C:/a/b/c/src/bob.go"), -- }, -- { -- path: `c:\Go\src\bob george\george\george.go`, -- wantFile: `C:\Go\src\bob george\george\george.go`, -- wantURI: span.URI("file:///C:/Go/src/bob%20george/george/george.go"), -- }, -- } { -- got := span.URIFromPath(test.path) -- if got != test.wantURI { -- t.Errorf("URIFromPath(%q): got %q, expected %q", test.path, got, test.wantURI) +- actualMap := make(map[int]int, len(vm.expected)) +- vm.impl.Range(func(key, value int) { +- if other, ok := actualMap[key]; ok { +- t.Fatalf("key is present twice, key: %d, first value: %d, second value: %d", key, value, other) +- } +- actualMap[key] = value +- }) +- +- assertSameMap(t, actualMap, vm.expected) +-} +- +-func validateNode(t *testing.T, node *mapNode) { +- if node == nil { +- return +- } +- +- if node.left != nil { +- if node.key.(int) < node.left.key.(int) { +- t.Fatalf("left child has larger key: %v vs %v", node.left.key, node.key) - } -- gotFilename := got.Filename() -- if gotFilename != test.wantFile { -- t.Errorf("Filename(%q): got %q, expected %q", got, gotFilename, test.wantFile) +- if node.left.weight > node.weight { +- t.Fatalf("left child has larger weight: %v vs %v", node.left.weight, node.weight) - } - } --} - --func TestURIFromURI(t *testing.T) { -- for _, test := range []struct { -- inputURI, wantFile string -- wantURI span.URI -- }{ -- { -- inputURI: `file:///c:/Go/src/bob%20george/george/george.go`, -- wantFile: `C:\Go\src\bob george\george\george.go`, -- wantURI: span.URI("file:///C:/Go/src/bob%20george/george/george.go"), -- }, -- { -- inputURI: `file:///C%3A/Go/src/bob%20george/george/george.go`, -- wantFile: `C:\Go\src\bob george\george\george.go`, -- wantURI: span.URI("file:///C:/Go/src/bob%20george/george/george.go"), -- }, -- { -- inputURI: `file:///c:/path/to/%25p%25ercent%25/per%25cent.go`, -- wantFile: `C:\path\to\%p%ercent%\per%cent.go`, -- wantURI: span.URI(`file:///C:/path/to/%25p%25ercent%25/per%25cent.go`), -- }, -- { -- inputURI: `file:///C%3A/`, -- wantFile: `C:\`, -- wantURI: span.URI(`file:///C:/`), -- }, -- { -- inputURI: `file:///`, -- wantFile: `\`, -- wantURI: span.URI(`file:///`), -- }, -- } { -- got := span.URIFromURI(test.inputURI) -- if got != test.wantURI { -- t.Errorf("NewURI(%q): got %q, expected %q", test.inputURI, got, test.wantURI) +- if node.right != nil { +- if node.right.key.(int) < node.key.(int) { +- t.Fatalf("right child has smaller key: %v vs %v", node.right.key, node.key) - } -- gotFilename := got.Filename() -- if gotFilename != test.wantFile { -- t.Errorf("Filename(%q): got %q, expected %q", got, gotFilename, test.wantFile) +- if node.right.weight > node.weight { +- t.Fatalf("right child has larger weight: %v vs %v", node.right.weight, node.weight) - } - } +- +- validateNode(t, node.left) +- validateNode(t, node.right) -} -diff -urN a/gopls/internal/telemetry/latency.go b/gopls/internal/telemetry/latency.go ---- a/gopls/internal/telemetry/latency.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/telemetry/latency.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,102 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. - --package telemetry +-func (vm *validatedMap) setAll(t *testing.T, other *validatedMap) { +- vm.impl.SetAll(other.impl) - --import ( -- "context" -- "errors" -- "fmt" -- "sort" -- "sync" -- "time" +- // Note: this is buggy because we are not updating vm.clock, vm.deleted, or +- // vm.seen. +- for key, value := range other.expected { +- vm.expected[key] = value +- } +- vm.validate(t) +-} - -- "golang.org/x/telemetry/counter" --) +-func (vm *validatedMap) set(t *testing.T, key, value int) { +- entry := mapEntry{key: key, value: value} - --// latencyKey is used for looking up latency counters. --type latencyKey struct { -- operation, bucket string -- isError bool +- vm.clock++ +- vm.seen[entry] = vm.clock +- +- vm.impl.Set(key, value, func(deletedKey, deletedValue any) { +- if deletedKey != key || deletedValue != value { +- t.Fatalf("unexpected passed in deleted entry: %v/%v, expected: %v/%v", deletedKey, deletedValue, key, value) +- } +- // Not safe if closure shared between two validatedMaps. +- vm.deleted[entry] = vm.clock +- }) +- vm.expected[key] = value +- vm.validate(t) +- +- gotValue, ok := vm.impl.Get(key) +- if !ok || gotValue != value { +- t.Fatalf("unexpected get result after insertion, key: %v, expected: %v, got: %v (%v)", key, value, gotValue, ok) +- } -} - --var ( -- latencyBuckets = []struct { -- end time.Duration -- name string -- }{ -- {10 * time.Millisecond, "<10ms"}, -- {50 * time.Millisecond, "<50ms"}, -- {100 * time.Millisecond, "<100ms"}, -- {200 * time.Millisecond, "<200ms"}, -- {500 * time.Millisecond, "<500ms"}, -- {1 * time.Second, "<1s"}, -- {5 * time.Second, "<5s"}, -- {24 * time.Hour, "<24h"}, +-func (vm *validatedMap) remove(t *testing.T, key int) { +- vm.clock++ +- deleted := vm.impl.Delete(key) +- if _, ok := vm.expected[key]; ok != deleted { +- t.Fatalf("Delete(%d) = %t, want %t", key, deleted, ok) - } +- delete(vm.expected, key) +- vm.validate(t) - -- latencyCounterMu sync.Mutex -- latencyCounters = make(map[latencyKey]*counter.Counter) // lazily populated --) +- gotValue, ok := vm.impl.Get(key) +- if ok { +- t.Fatalf("unexpected get result after removal, key: %v, got: %v", key, gotValue) +- } +-} - --// ForEachLatencyCounter runs the provided function for each current latency --// counter measuring the given operation. --// --// Exported for testing. --func ForEachLatencyCounter(operation string, isError bool, f func(*counter.Counter)) { -- latencyCounterMu.Lock() -- defer latencyCounterMu.Unlock() +-func (vm *validatedMap) clone() *validatedMap { +- expected := make(map[int]int, len(vm.expected)) +- for key, value := range vm.expected { +- expected[key] = value +- } - -- for k, v := range latencyCounters { -- if k.operation == operation && k.isError == isError { -- f(v) -- } +- return &validatedMap{ +- impl: vm.impl.Clone(), +- expected: expected, +- deleted: vm.deleted, +- seen: vm.seen, - } -} - --// getLatencyCounter returns the counter used to record latency of the given --// operation in the given bucket. --func getLatencyCounter(operation, bucket string, isError bool) *counter.Counter { -- latencyCounterMu.Lock() -- defer latencyCounterMu.Unlock() +-func (vm *validatedMap) destroy() { +- vm.impl.Destroy() +-} - -- key := latencyKey{operation, bucket, isError} -- c, ok := latencyCounters[key] -- if !ok { -- var name string -- if isError { -- name = fmt.Sprintf("gopls/%s/error-latency:%s", operation, bucket) -- } else { -- name = fmt.Sprintf("gopls/%s/latency:%s", operation, bucket) -- } -- c = counter.New(name) -- latencyCounters[key] = c +-func assertSameMap(t *testing.T, map1, map2 any) { +- t.Helper() +- +- if !reflect.DeepEqual(map1, map2) { +- t.Fatalf("different maps:\n%v\nvs\n%v", map1, map2) - } -- return c -} +diff -urN a/gopls/internal/util/persistent/set.go b/gopls/internal/util/persistent/set.go +--- a/gopls/internal/util/persistent/set.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/util/persistent/set.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,78 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --// StartLatencyTimer starts a timer for the gopls operation with the given --// name, and returns a func to stop the timer and record the latency sample. +-package persistent +- +-import "golang.org/x/tools/gopls/internal/util/constraints" +- +-// Set is a collection of elements of type K. -// --// If the context provided to the the resulting func is done, no observation is --// recorded. --func StartLatencyTimer(operation string) func(context.Context, error) { -- start := time.Now() -- return func(ctx context.Context, err error) { -- if errors.Is(ctx.Err(), context.Canceled) { -- // Ignore timing where the operation is cancelled, it may be influenced -- // by client behavior. -- return -- } -- latency := time.Since(start) -- bucketIdx := sort.Search(len(latencyBuckets), func(i int) bool { -- bucket := latencyBuckets[i] -- return latency < bucket.end +-// It uses immutable data structures internally, so that sets can be cloned in +-// constant time. +-// +-// The zero value is a valid empty set. +-type Set[K constraints.Ordered] struct { +- impl *Map[K, struct{}] +-} +- +-// Clone creates a copy of the receiver. +-func (s *Set[K]) Clone() *Set[K] { +- clone := new(Set[K]) +- if s.impl != nil { +- clone.impl = s.impl.Clone() +- } +- return clone +-} +- +-// Destroy destroys the set. +-// +-// After Destroy, the Set should not be used again. +-func (s *Set[K]) Destroy() { +- if s.impl != nil { +- s.impl.Destroy() +- } +-} +- +-// Contains reports whether s contains the given key. +-func (s *Set[K]) Contains(key K) bool { +- if s.impl == nil { +- return false +- } +- _, ok := s.impl.Get(key) +- return ok +-} +- +-// Range calls f sequentially in ascending key order for all entries in the set. +-func (s *Set[K]) Range(f func(key K)) { +- if s.impl != nil { +- s.impl.Range(func(key K, _ struct{}) { +- f(key) - }) -- if bucketIdx < len(latencyBuckets) { // ignore latency longer than a day :) -- bucketName := latencyBuckets[bucketIdx].name -- getLatencyCounter(operation, bucketName, err != nil).Inc() +- } +-} +- +-// AddAll adds all elements from other to the receiver set. +-func (s *Set[K]) AddAll(other *Set[K]) { +- if other.impl != nil { +- if s.impl == nil { +- s.impl = new(Map[K, struct{}]) - } +- s.impl.SetAll(other.impl) - } -} -diff -urN a/gopls/internal/telemetry/telemetry.go b/gopls/internal/telemetry/telemetry.go ---- a/gopls/internal/telemetry/telemetry.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/telemetry/telemetry.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,93 +0,0 @@ +- +-// Add adds an element to the set. +-func (s *Set[K]) Add(key K) { +- if s.impl == nil { +- s.impl = new(Map[K, struct{}]) +- } +- s.impl.Set(key, struct{}{}, nil) +-} +- +-// Remove removes an element from the set. +-func (s *Set[K]) Remove(key K) { +- if s.impl != nil { +- s.impl.Delete(key) +- } +-} +diff -urN a/gopls/internal/util/persistent/set_test.go b/gopls/internal/util/persistent/set_test.go +--- a/gopls/internal/util/persistent/set_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/util/persistent/set_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,132 +0,0 @@ -// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --//go:build go1.19 --// +build go1.19 -- --package telemetry +-package persistent_test - -import ( - "fmt" +- "strings" +- "testing" - -- "golang.org/x/telemetry" -- "golang.org/x/telemetry/counter" -- "golang.org/x/telemetry/upload" -- "golang.org/x/tools/gopls/internal/lsp/protocol" +- "golang.org/x/tools/gopls/internal/util/constraints" +- "golang.org/x/tools/gopls/internal/util/persistent" -) - --// Mode calls x/telemetry.Mode. --func Mode() string { -- return telemetry.Mode() --} +-func TestSet(t *testing.T) { +- const ( +- add = iota +- remove +- ) +- type op struct { +- op int +- v int +- } - --// SetMode calls x/telemetry.SetMode. --func SetMode(mode string) error { -- return telemetry.SetMode(mode) --} +- tests := []struct { +- label string +- ops []op +- want []int +- }{ +- {"empty", nil, nil}, +- {"singleton", []op{{add, 1}}, []int{1}}, +- {"add and remove", []op{ +- {add, 1}, +- {remove, 1}, +- }, nil}, +- {"interleaved and remove", []op{ +- {add, 1}, +- {add, 2}, +- {remove, 1}, +- {add, 3}, +- }, []int{2, 3}}, +- } - --// Start starts telemetry instrumentation. --func Start() { -- counter.Open() -- // upload only once at startup, hoping that users restart gopls often. -- go upload.Run(nil) +- for _, test := range tests { +- t.Run(test.label, func(t *testing.T) { +- var s persistent.Set[int] +- for _, op := range test.ops { +- switch op.op { +- case add: +- s.Add(op.v) +- case remove: +- s.Remove(op.v) +- } +- } +- +- if d := diff(&s, test.want); d != "" { +- t.Errorf("unexpected diff:\n%s", d) +- } +- }) +- } -} - --// RecordClientInfo records gopls client info. --func RecordClientInfo(params *protocol.ParamInitialize) { -- client := "gopls/client:other" -- if params != nil && params.ClientInfo != nil { -- switch params.ClientInfo.Name { -- case "Visual Studio Code": -- client = "gopls/client:vscode" -- case "Visual Studio Code - Insiders": -- client = "gopls/client:vscode-insiders" -- case "VSCodium": -- client = "gopls/client:vscodium" -- case "code-server": -- // https://github.com/coder/code-server/blob/3cb92edc76ecc2cfa5809205897d93d4379b16a6/ci/build/build-vscode.sh#L19 -- client = "gopls/client:code-server" -- case "Eglot": -- // https://lists.gnu.org/archive/html/bug-gnu-emacs/2023-03/msg00954.html -- client = "gopls/client:eglot" -- case "govim": -- // https://github.com/govim/govim/pull/1189 -- client = "gopls/client:govim" -- case "Neovim": -- // https://github.com/neovim/neovim/blob/42333ea98dfcd2994ee128a3467dfe68205154cd/runtime/lua/vim/lsp.lua#L1361 -- client = "gopls/client:neovim" -- case "coc.nvim": -- // https://github.com/neoclide/coc.nvim/blob/3dc6153a85ed0f185abec1deb972a66af3fbbfb4/src/language-client/client.ts#L994 -- client = "gopls/client:coc.nvim" -- case "Sublime Text LSP": -- // https://github.com/sublimelsp/LSP/blob/e608f878e7e9dd34aabe4ff0462540fadcd88fcc/plugin/core/sessions.py#L493 -- client = "gopls/client:sublimetext" -- default: -- // at least accumulate the client name locally -- counter.New(fmt.Sprintf("gopls/client-other:%s", params.ClientInfo.Name)).Inc() -- // but also record client:other -- } +-func TestSet_Clone(t *testing.T) { +- s1 := new(persistent.Set[int]) +- s1.Add(1) +- s1.Add(2) +- s2 := s1.Clone() +- s1.Add(3) +- s2.Add(4) +- if d := diff(s1, []int{1, 2, 3}); d != "" { +- t.Errorf("s1: unexpected diff:\n%s", d) +- } +- if d := diff(s2, []int{1, 2, 4}); d != "" { +- t.Errorf("s2: unexpected diff:\n%s", d) - } -- counter.Inc(client) -} - --// RecordViewGoVersion records the Go minor version number (1.x) used for a view. --func RecordViewGoVersion(x int) { -- if x < 0 { -- return +-func TestSet_AddAll(t *testing.T) { +- s1 := new(persistent.Set[int]) +- s1.Add(1) +- s1.Add(2) +- s2 := new(persistent.Set[int]) +- s2.Add(2) +- s2.Add(3) +- s2.Add(4) +- s3 := new(persistent.Set[int]) +- +- s := new(persistent.Set[int]) +- s.AddAll(s1) +- s.AddAll(s2) +- s.AddAll(s3) +- +- if d := diff(s1, []int{1, 2}); d != "" { +- t.Errorf("s1: unexpected diff:\n%s", d) +- } +- if d := diff(s2, []int{2, 3, 4}); d != "" { +- t.Errorf("s2: unexpected diff:\n%s", d) +- } +- if d := diff(s3, nil); d != "" { +- t.Errorf("s3: unexpected diff:\n%s", d) +- } +- if d := diff(s, []int{1, 2, 3, 4}); d != "" { +- t.Errorf("s: unexpected diff:\n%s", d) - } -- name := fmt.Sprintf("gopls/goversion:1.%d", x) -- counter.Inc(name) -} - --// AddForwardedCounters adds the given counters on behalf of clients. --// Names and values must have the same length. --func AddForwardedCounters(names []string, values []int64) { -- for i, n := range names { -- v := values[i] -- if n == "" || v < 0 { -- continue // Should we report an error? Who is the audience? +-func diff[K constraints.Ordered](got *persistent.Set[K], want []K) string { +- wantSet := make(map[K]struct{}) +- for _, w := range want { +- wantSet[w] = struct{}{} +- } +- var diff []string +- got.Range(func(key K) { +- if _, ok := wantSet[key]; !ok { +- diff = append(diff, fmt.Sprintf("+%v", key)) +- } +- }) +- for key := range wantSet { +- if !got.Contains(key) { +- diff = append(diff, fmt.Sprintf("-%v", key)) - } -- counter.Add("fwd/"+n, v) - } +- if len(diff) > 0 { +- d := new(strings.Builder) +- for _, l := range diff { +- fmt.Fprintln(d, l) +- } +- return d.String() +- } +- return "" -} -diff -urN a/gopls/internal/telemetry/telemetry_go118.go b/gopls/internal/telemetry/telemetry_go118.go ---- a/gopls/internal/telemetry/telemetry_go118.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/telemetry/telemetry_go118.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,30 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. +diff -urN a/gopls/internal/util/README.md b/gopls/internal/util/README.md +--- a/gopls/internal/util/README.md 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/util/README.md 1970-01-01 00:00:00.000000000 +0000 +@@ -1,7 +0,0 @@ +-# util +- +-This directory is not a Go package. +- +-Its subdirectories are for utility packages, defined as implementation +-helpers (not core machinery) that are used in different ways across +-the gopls codebase. +\ No newline at end of file +diff -urN a/gopls/internal/util/safetoken/safetoken.go b/gopls/internal/util/safetoken/safetoken.go +--- a/gopls/internal/util/safetoken/safetoken.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/util/safetoken/safetoken.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,127 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --//go:build !go1.19 --// +build !go1.19 +-// Package safetoken provides wrappers around methods in go/token, +-// that return errors rather than panicking. +-// +-// It also provides a central place for workarounds in the underlying +-// packages. The use of this package's functions instead of methods of +-// token.File (such as Offset, Position, and PositionFor) is mandatory +-// throughout the gopls codebase and enforced by a static check. +-package safetoken - --package telemetry +-import ( +- "fmt" +- "go/token" +-) - --import "golang.org/x/tools/gopls/internal/lsp/protocol" +-// Offset returns f.Offset(pos), but first checks that the file +-// contains the pos. +-// +-// The definition of "contains" here differs from that of token.File +-// in order to work around a bug in the parser (issue #57490): during +-// error recovery, the parser may create syntax nodes whose computed +-// End position is 1 byte beyond EOF, which would cause +-// token.File.Offset to panic. The workaround is that this function +-// accepts a Pos that is exactly 1 byte beyond EOF and maps it to the +-// EOF offset. +-func Offset(f *token.File, pos token.Pos) (int, error) { +- if !inRange(f, pos) { +- // Accept a Pos that is 1 byte beyond EOF, +- // and map it to the EOF offset. +- // (Workaround for #57490.) +- if int(pos) == f.Base()+f.Size()+1 { +- return f.Size(), nil +- } - --func Mode() string { -- return "local" +- return -1, fmt.Errorf("pos %d is not in range [%d:%d] of file %s", +- pos, f.Base(), f.Base()+f.Size(), f.Name()) +- } +- return int(pos) - f.Base(), nil -} - --func SetMode(mode string) error { -- return nil +-// Offsets returns Offset(start) and Offset(end). +-func Offsets(f *token.File, start, end token.Pos) (int, int, error) { +- startOffset, err := Offset(f, start) +- if err != nil { +- return 0, 0, fmt.Errorf("start: %v", err) +- } +- endOffset, err := Offset(f, end) +- if err != nil { +- return 0, 0, fmt.Errorf("end: %v", err) +- } +- return startOffset, endOffset, nil -} - --func Start() { +-// Pos returns f.Pos(offset), but first checks that the offset is +-// non-negative and not larger than the size of the file. +-func Pos(f *token.File, offset int) (token.Pos, error) { +- if !(0 <= offset && offset <= f.Size()) { +- return token.NoPos, fmt.Errorf("offset %d is not in range for file %s of size %d", offset, f.Name(), f.Size()) +- } +- return token.Pos(f.Base() + offset), nil +-} +- +-// inRange reports whether file f contains position pos, +-// according to the invariants of token.File. +-// +-// This function is not public because of the ambiguity it would +-// create w.r.t. the definition of "contains". Use Offset instead. +-func inRange(f *token.File, pos token.Pos) bool { +- return token.Pos(f.Base()) <= pos && pos <= token.Pos(f.Base()+f.Size()) -} - --func RecordClientInfo(params *protocol.ParamInitialize) { +-// Position returns the Position for the pos value in the given file. +-// +-// p must be NoPos, a valid Pos in the range of f, or exactly 1 byte +-// beyond the end of f. (See [Offset] for explanation.) +-// Any other value causes a panic. +-// +-// Line directives (//line comments) are ignored. +-func Position(f *token.File, pos token.Pos) token.Position { +- // Work around issue #57490. +- if int(pos) == f.Base()+f.Size()+1 { +- pos-- +- } +- +- // TODO(adonovan): centralize the workaround for +- // golang/go#41029 (newline at EOF) here too. +- +- return f.PositionFor(pos, false) -} - --func RecordViewGoVersion(x int) { +-// Line returns the line number for the given offset in the given file. +-func Line(f *token.File, pos token.Pos) int { +- return Position(f, pos).Line -} - --func AddForwardedCounters(names []string, values []int64) { +-// StartPosition converts a start Pos in the FileSet into a Position. +-// +-// Call this function only if start represents the start of a token or +-// parse tree, such as the result of Node.Pos(). If start is the end of +-// an interval, such as Node.End(), call EndPosition instead, as it +-// may need the correction described at [Position]. +-func StartPosition(fset *token.FileSet, start token.Pos) (_ token.Position) { +- if f := fset.File(start); f != nil { +- return Position(f, start) +- } +- return -} -diff -urN a/gopls/internal/telemetry/telemetry_test.go b/gopls/internal/telemetry/telemetry_test.go ---- a/gopls/internal/telemetry/telemetry_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/telemetry/telemetry_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,215 +0,0 @@ --// Copyright 2023 The Go Authors. All rights reserved. +- +-// EndPosition converts an end Pos in the FileSet into a Position. +-// +-// Call this function only if pos represents the end of +-// a non-empty interval, such as the result of Node.End(). +-func EndPosition(fset *token.FileSet, end token.Pos) (_ token.Position) { +- if f := fset.File(end); f != nil && int(end) > f.Base() { +- return Position(f, end) +- } +- +- // Work around issue #57490. +- if f := fset.File(end - 1); f != nil { +- return Position(f, end) +- } +- +- return +-} +diff -urN a/gopls/internal/util/safetoken/safetoken_test.go b/gopls/internal/util/safetoken/safetoken_test.go +--- a/gopls/internal/util/safetoken/safetoken_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/util/safetoken/safetoken_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,131 +0,0 @@ +-// Copyright 2021 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --//go:build go1.21 && !openbsd && !js && !wasip1 && !solaris && !android && !386 --// +build go1.21,!openbsd,!js,!wasip1,!solaris,!android,!386 -- --package telemetry_test +-package safetoken_test - -import ( -- "context" -- "errors" +- "fmt" +- "go/parser" +- "go/token" +- "go/types" - "os" -- "strconv" -- "strings" - "testing" -- "time" - -- "golang.org/x/telemetry/counter" -- "golang.org/x/telemetry/counter/countertest" // requires go1.21+ -- "golang.org/x/tools/gopls/internal/bug" -- "golang.org/x/tools/gopls/internal/hooks" -- "golang.org/x/tools/gopls/internal/lsp/command" -- "golang.org/x/tools/gopls/internal/lsp/protocol" -- . "golang.org/x/tools/gopls/internal/lsp/regtest" -- "golang.org/x/tools/gopls/internal/telemetry" +- "golang.org/x/tools/go/packages" +- "golang.org/x/tools/gopls/internal/util/safetoken" +- "golang.org/x/tools/internal/testenv" -) - --func TestMain(m *testing.M) { -- tmp, err := os.MkdirTemp("", "gopls-telemetry-test") -- if err != nil { -- panic(err) +-func TestWorkaroundIssue57490(t *testing.T) { +- // During error recovery the parser synthesizes various close +- // tokens at EOF, causing the End position of incomplete +- // syntax nodes, computed as Rbrace+len("}"), to be beyond EOF. +- src := `package p; func f() { var x struct` +- fset := token.NewFileSet() +- file, _ := parser.ParseFile(fset, "a.go", src, 0) +- tf := fset.File(file.Pos()) +- +- // Add another file to the FileSet. +- file2, _ := parser.ParseFile(fset, "b.go", "package q", 0) +- +- // This is the ambiguity of #57490... +- if file.End() != file2.Pos() { +- t.Errorf("file.End() %d != %d file2.Pos()", file.End(), file2.Pos()) +- } +- // ...which causes these statements to panic. +- if false { +- tf.Offset(file.End()) // panic: invalid Pos value 36 (should be in [1, 35]) +- tf.Position(file.End()) // panic: invalid Pos value 36 (should be in [1, 35]) - } -- countertest.Open(tmp) -- defer os.RemoveAll(tmp) -- Main(m, hooks.Options) --} - --func TestTelemetry(t *testing.T) { -- var ( -- goversion = "" -- editor = "vscode" // We set ClientName("Visual Studio Code") below. -- ) +- // The offset of the EOF position is the file size. +- offset, err := safetoken.Offset(tf, file.End()-1) +- if err != nil || offset != tf.Size() { +- t.Errorf("Offset(EOF) = (%d, %v), want token.File.Size %d", offset, err, tf.Size()) +- } - -- // Run gopls once to determine the Go version. -- WithOptions( -- Modes(Default), -- ).Run(t, "", func(_ *testing.T, env *Env) { -- goversion = strconv.Itoa(env.GoVersion()) -- }) +- // The offset of the file.End() position, 1 byte beyond EOF, +- // is also the size of the file. +- offset, err = safetoken.Offset(tf, file.End()) +- if err != nil || offset != tf.Size() { +- t.Errorf("Offset(ast.File.End()) = (%d, %v), want token.File.Size %d", offset, err, tf.Size()) +- } - -- // counters that should be incremented once per session -- sessionCounters := []*counter.Counter{ -- counter.New("gopls/client:" + editor), -- counter.New("gopls/goversion:1." + goversion), -- counter.New("fwd/vscode/linter:a"), +- if got, want := safetoken.Position(tf, file.End()).String(), "a.go:1:35"; got != want { +- t.Errorf("Position(ast.File.End()) = %s, want %s", got, want) - } -- initialCounts := make([]uint64, len(sessionCounters)) -- for i, c := range sessionCounters { -- count, err := countertest.ReadCounter(c) -- if err != nil { -- t.Fatalf("ReadCounter(%s): %v", c.Name(), err) -- } -- initialCounts[i] = count +- +- if got, want := safetoken.EndPosition(fset, file.End()).String(), "a.go:1:35"; got != want { +- t.Errorf("EndPosition(ast.File.End()) = %s, want %s", got, want) - } - -- // Verify that a properly configured session gets notified of a bug on the -- // server. -- WithOptions( -- Modes(Default), // must be in-process to receive the bug report below -- Settings{"showBugReports": true}, -- ClientName("Visual Studio Code"), -- ).Run(t, "", func(_ *testing.T, env *Env) { -- goversion = strconv.Itoa(env.GoVersion()) -- addForwardedCounters(env, []string{"vscode/linter:a"}, []int64{1}) -- const desc = "got a bug" -- bug.Report(desc) // want a stack counter with the trace starting from here. -- env.Await(ShownMessage(desc)) -- }) +- // Note that calling StartPosition on an end may yield the wrong file: +- if got, want := safetoken.StartPosition(fset, file.End()).String(), "b.go:1:1"; got != want { +- t.Errorf("StartPosition(ast.File.End()) = %s, want %s", got, want) +- } +-} - -- // gopls/editor:client -- // gopls/goversion:1.x -- // fwd/vscode/linter:a -- for i, c := range sessionCounters { -- want := initialCounts[i] + 1 -- got, err := countertest.ReadCounter(c) -- if err != nil || got != want { -- t.Errorf("ReadCounter(%q) = (%v, %v), want (%v, nil)", c.Name(), got, err, want) -- t.Logf("Current timestamp = %v", time.Now().UTC()) -- } +-// To reduce the risk of panic, or bugs for which this package +-// provides a workaround, this test statically reports references to +-// forbidden methods of token.File or FileSet throughout gopls and +-// suggests alternatives. +-func TestGoplsSourceDoesNotCallTokenFileMethods(t *testing.T) { +- testenv.NeedsGoPackages(t) +- testenv.NeedsLocalXTools(t) +- +- cfg := &packages.Config{ +- Mode: packages.NeedName | packages.NeedModule | packages.NeedCompiledGoFiles | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedSyntax | packages.NeedImports | packages.NeedDeps, - } +- cfg.Env = os.Environ() +- cfg.Env = append(cfg.Env, +- "GOPACKAGESDRIVER=off", +- "GOWORK=off", // necessary for -mod=mod below +- "GOFLAGS=-mod=mod", +- ) - -- // gopls/bug -- bugcount := bug.BugReportCount -- counts, err := countertest.ReadStackCounter(bugcount) +- pkgs, err := packages.Load(cfg, "go/token", "golang.org/x/tools/gopls/...") - if err != nil { -- t.Fatalf("ReadStackCounter(bugreportcount) failed - %v", err) +- t.Fatal(err) - } -- if len(counts) != 1 || !hasEntry(counts, t.Name(), 1) { -- t.Errorf("read stackcounter(%q) = (%#v, %v), want one entry", "gopls/bug", counts, err) -- t.Logf("Current timestamp = %v", time.Now().UTC()) +- var tokenPkg *packages.Package +- for _, pkg := range pkgs { +- if pkg.PkgPath == "go/token" { +- tokenPkg = pkg +- break +- } +- } +- if tokenPkg == nil { +- t.Fatal("missing package go/token") - } --} - --func addForwardedCounters(env *Env, names []string, values []int64) { -- args, err := command.MarshalArgs(command.AddTelemetryCountersArgs{ -- Names: names, Values: values, -- }) -- if err != nil { -- env.T.Fatal(err) +- File := tokenPkg.Types.Scope().Lookup("File") +- FileSet := tokenPkg.Types.Scope().Lookup("FileSet") +- +- alternative := make(map[types.Object]string) +- setAlternative := func(recv types.Object, old, new string) { +- oldMethod, _, _ := types.LookupFieldOrMethod(recv.Type(), true, recv.Pkg(), old) +- alternative[oldMethod] = new - } -- var res error -- env.ExecuteCommand(&protocol.ExecuteCommandParams{ -- Command: command.AddTelemetryCounters.ID(), -- Arguments: args, -- }, res) -- if res != nil { -- env.T.Errorf("%v failed - %v", command.AddTelemetryCounters.ID(), res) +- setAlternative(File, "Line", "safetoken.Line") +- setAlternative(File, "Offset", "safetoken.Offset") +- setAlternative(File, "Position", "safetoken.Position") +- setAlternative(File, "PositionFor", "safetoken.Position") +- setAlternative(FileSet, "Position", "safetoken.StartPosition or EndPosition") +- setAlternative(FileSet, "PositionFor", "safetoken.StartPosition or EndPosition") +- +- for _, pkg := range pkgs { +- switch pkg.PkgPath { +- case "go/token", "golang.org/x/tools/gopls/internal/util/safetoken": +- continue // allow calls within these packages +- } +- +- for ident, obj := range pkg.TypesInfo.Uses { +- if alt, ok := alternative[obj]; ok { +- posn := safetoken.StartPosition(pkg.Fset, ident.Pos()) +- fmt.Fprintf(os.Stderr, "%s: forbidden use of %v; use %s instead.\n", posn, obj, alt) +- t.Fail() +- } +- } - } -} +diff -urN a/gopls/internal/util/slices/slices.go b/gopls/internal/util/slices/slices.go +--- a/gopls/internal/util/slices/slices.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/util/slices/slices.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,116 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func hasEntry(counts map[string]uint64, pattern string, want uint64) bool { -- for k, v := range counts { -- if strings.Contains(k, pattern) && v == want { +-package slices +- +-// Clone returns a copy of the slice. +-// The elements are copied using assignment, so this is a shallow clone. +-// TODO(rfindley): use go1.21 slices.Clone. +-func Clone[S ~[]E, E any](s S) S { +- // The s[:0:0] preserves nil in case it matters. +- return append(s[:0:0], s...) +-} +- +-// Contains reports whether x is present in slice. +-// TODO(adonovan): use go1.21 slices.Contains. +-func Contains[S ~[]E, E comparable](slice S, x E) bool { +- for _, elem := range slice { +- if elem == x { - return true - } - } - return false -} - --func TestLatencyCounter(t *testing.T) { -- const operation = "TestLatencyCounter" // a unique operation name +-// IndexFunc returns the first index i satisfying f(s[i]), +-// or -1 if none do. +-// TODO(adonovan): use go1.21 slices.IndexFunc. +-func IndexFunc[S ~[]E, E any](s S, f func(E) bool) int { +- for i := range s { +- if f(s[i]) { +- return i +- } +- } +- return -1 +-} - -- stop := telemetry.StartLatencyTimer(operation) -- stop(context.Background(), nil) +-// ContainsFunc reports whether at least one +-// element e of s satisfies f(e). +-// TODO(adonovan): use go1.21 slices.ContainsFunc. +-func ContainsFunc[S ~[]E, E any](s S, f func(E) bool) bool { +- return IndexFunc(s, f) >= 0 +-} - -- for isError, want := range map[bool]uint64{false: 1, true: 0} { -- if got := totalLatencySamples(t, operation, isError); got != want { -- t.Errorf("totalLatencySamples(operation=%v, isError=%v) = %d, want %d", operation, isError, got, want) +-// Concat returns a new slice concatenating the passed in slices. +-// TODO(rfindley): use go1.22 slices.Concat. +-func Concat[S ~[]E, E any](slices ...S) S { +- size := 0 +- for _, s := range slices { +- size += len(s) +- if size < 0 { +- panic("len out of range") - } - } +- newslice := Grow[S](nil, size) +- for _, s := range slices { +- newslice = append(newslice, s...) +- } +- return newslice -} - --func TestLatencyCounter_Error(t *testing.T) { -- const operation = "TestLatencyCounter_Error" // a unique operation name +-// Grow increases the slice's capacity, if necessary, to guarantee space for +-// another n elements. After Grow(n), at least n elements can be appended +-// to the slice without another allocation. If n is negative or too large to +-// allocate the memory, Grow panics. +-// TODO(rfindley): use go1.21 slices.Grow. +-func Grow[S ~[]E, E any](s S, n int) S { +- if n < 0 { +- panic("cannot be negative") +- } +- if n -= cap(s) - len(s); n > 0 { +- s = append(s[:cap(s)], make([]E, n)...)[:len(s)] +- } +- return s +-} - -- stop := telemetry.StartLatencyTimer(operation) -- stop(context.Background(), errors.New("bad")) +-// DeleteFunc removes any elements from s for which del returns true, +-// returning the modified slice. +-// DeleteFunc zeroes the elements between the new length and the original length. +-// TODO(adonovan): use go1.21 slices.DeleteFunc. +-func DeleteFunc[S ~[]E, E any](s S, del func(E) bool) S { +- i := IndexFunc(s, del) +- if i == -1 { +- return s +- } +- // Don't start copying elements until we find one to delete. +- for j := i + 1; j < len(s); j++ { +- if v := s[j]; !del(v) { +- s[i] = v +- i++ +- } +- } +- clear(s[i:]) // zero/nil out the obsolete elements, for GC +- return s[:i] +-} - -- for isError, want := range map[bool]uint64{false: 0, true: 1} { -- if got := totalLatencySamples(t, operation, isError); got != want { -- t.Errorf("totalLatencySamples(operation=%v, isError=%v) = %d, want %d", operation, isError, got, want) +-func clear[T any](slice []T) { +- for i := range slice { +- slice[i] = *new(T) +- } +-} +- +-// Remove removes all values equal to elem from slice. +-// +-// The closest equivalent in the standard slices package is: +-// +-// DeleteFunc(func(x T) bool { return x == elem }) +-func Remove[T comparable](slice []T, elem T) []T { +- out := slice[:0] +- for _, v := range slice { +- if v != elem { +- out = append(out, v) - } - } +- return out -} +diff -urN a/gopls/internal/util/typesutil/typesutil.go b/gopls/internal/util/typesutil/typesutil.go +--- a/gopls/internal/util/typesutil/typesutil.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/util/typesutil/typesutil.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,49 +0,0 @@ +-// Copyright 2023 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func TestLatencyCounter_Cancellation(t *testing.T) { -- const operation = "TestLatencyCounter_Cancellation" +-package typesutil - -- stop := telemetry.StartLatencyTimer(operation) -- ctx, cancel := context.WithCancel(context.Background()) -- cancel() -- stop(ctx, nil) +-import ( +- "go/ast" +- "go/types" +-) - -- for isError, want := range map[bool]uint64{false: 0, true: 0} { -- if got := totalLatencySamples(t, operation, isError); got != want { -- t.Errorf("totalLatencySamples(operation=%v, isError=%v) = %d, want %d", operation, isError, got, want) -- } +-// ImportedPkgName returns the PkgName object declared by an ImportSpec. +-// TODO(adonovan): use go1.22's Info.PkgNameOf. +-func ImportedPkgName(info *types.Info, imp *ast.ImportSpec) (*types.PkgName, bool) { +- var obj types.Object +- if imp.Name != nil { +- obj = info.Defs[imp.Name] +- } else { +- obj = info.Implicits[imp] - } +- pkgname, ok := obj.(*types.PkgName) +- return pkgname, ok -} - --func totalLatencySamples(t *testing.T, operation string, isError bool) uint64 { -- var total uint64 -- telemetry.ForEachLatencyCounter(operation, isError, func(c *counter.Counter) { -- count, err := countertest.ReadCounter(c) -- if err != nil { -- t.Errorf("ReadCounter(%s) failed: %v", c.Name(), err) -- } else { -- total += count +-// FileQualifier returns a [types.Qualifier] function that qualifies +-// imported symbols appropriately based on the import environment of a +-// given file. +-func FileQualifier(f *ast.File, pkg *types.Package, info *types.Info) types.Qualifier { +- // Construct mapping of import paths to their defined or implicit names. +- imports := make(map[*types.Package]string) +- for _, imp := range f.Imports { +- if pkgname, ok := ImportedPkgName(info, imp); ok { +- imports[pkgname.Imported()] = pkgname.Name() - } -- }) -- return total +- } +- // Define qualifier to replace full package paths with names of the imports. +- return func(p *types.Package) string { +- if p == pkg { +- return "" +- } +- if name, ok := imports[p]; ok { +- if name == "." { +- return "" +- } +- return name +- } +- return p.Name() +- } -} +diff -urN a/gopls/internal/version/version.go b/gopls/internal/version/version.go +--- a/gopls/internal/version/version.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/version/version.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,29 +0,0 @@ +-// Copyright 2024 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. - --func TestLatencyInstrumentation(t *testing.T) { -- const files = ` ---- go.mod -- --module mod.test/a --go 1.18 ---- a.go -- --package a +-// Package version manages the gopls version. +-// +-// The VersionOverride variable may be used to set the gopls version at link +-// time. +-package version - --func _() { -- x := 0 -- _ = x --} --` +-import "runtime/debug" - -- // Verify that a properly configured session gets notified of a bug on the -- // server. -- WithOptions( -- Modes(Default), // must be in-process to receive the bug report below -- ).Run(t, files, func(_ *testing.T, env *Env) { -- env.OpenFile("a.go") -- before := totalLatencySamples(t, "completion", false) -- loc := env.RegexpSearch("a.go", "x") -- for i := 0; i < 10; i++ { -- env.Completion(loc) -- } -- after := totalLatencySamples(t, "completion", false) -- if after-before < 10 { -- t.Errorf("after 10 completions, completion counter went from %d to %d", before, after) +-var VersionOverride = "" +- +-// Version returns the gopls version. +-// +-// By default, this is read from runtime/debug.ReadBuildInfo, but may be +-// overridden by the [VersionOverride] variable. +-func Version() string { +- if VersionOverride != "" { +- return VersionOverride +- } +- if info, ok := debug.ReadBuildInfo(); ok { +- if info.Main.Version != "" { +- return info.Main.Version - } -- }) +- } +- return "(unknown)" -} diff -urN a/gopls/internal/vulncheck/copier.go b/gopls/internal/vulncheck/copier.go --- a/gopls/internal/vulncheck/copier.go 2000-01-01 00:00:00.000000000 -0000 @@ -140794,14 +150146,11 @@ diff -urN a/gopls/internal/vulncheck/osv/osv.go b/gopls/internal/vulncheck/osv/o diff -urN a/gopls/internal/vulncheck/scan/command.go b/gopls/internal/vulncheck/scan/command.go --- a/gopls/internal/vulncheck/scan/command.go 2000-01-01 00:00:00.000000000 -0000 +++ b/gopls/internal/vulncheck/scan/command.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,476 +0,0 @@ +@@ -1,165 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --//go:build go1.18 --// +build go1.18 -- -package scan - -import ( @@ -140812,26 +150161,16 @@ diff -urN a/gopls/internal/vulncheck/scan/command.go b/gopls/internal/vulncheck/ - "os" - "os/exec" - "sort" -- "strings" -- "sync" - "time" - -- "golang.org/x/mod/semver" - "golang.org/x/sync/errgroup" -- "golang.org/x/tools/go/packages" -- "golang.org/x/tools/gopls/internal/lsp/source" +- "golang.org/x/tools/gopls/internal/cache" - "golang.org/x/tools/gopls/internal/vulncheck" - "golang.org/x/tools/gopls/internal/vulncheck/govulncheck" - "golang.org/x/tools/gopls/internal/vulncheck/osv" -- isem "golang.org/x/tools/gopls/internal/vulncheck/semver" - "golang.org/x/vuln/scan" -) - --// GoVersionForVulnTest is an internal environment variable used in gopls --// testing to examine govulncheck behavior with a go version different --// than what `go version` returns in the system. --const GoVersionForVulnTest = "_GOPLS_TEST_VULNCHECK_GOVERSION" -- -// Main implements gopls vulncheck. -func Main(ctx context.Context, args ...string) error { - // wrapping govulncheck. @@ -140845,489 +150184,147 @@ diff -urN a/gopls/internal/vulncheck/scan/command.go b/gopls/internal/vulncheck/ -// RunGovulncheck implements the codelens "Run Govulncheck" -// that runs 'gopls vulncheck' and converts the output to gopls's internal data -// used for diagnostics and hover message construction. --func RunGovulncheck(ctx context.Context, pattern string, snapshot source.Snapshot, dir string, log io.Writer) (*vulncheck.Result, error) { +-// +-// TODO(rfindley): this should accept a *View (which exposes) Options, rather +-// than a snapshot. +-func RunGovulncheck(ctx context.Context, pattern string, snapshot *cache.Snapshot, dir string, log io.Writer) (*vulncheck.Result, error) { - vulncheckargs := []string{ - "vulncheck", "--", - "-json", - "-mode", "source", -- "-scan", "symbol", -- } -- if dir != "" { -- vulncheckargs = append(vulncheckargs, "-C", dir) -- } -- if db := getEnv(snapshot, "GOVULNDB"); db != "" { -- vulncheckargs = append(vulncheckargs, "-db", db) -- } -- vulncheckargs = append(vulncheckargs, pattern) -- // TODO: support -tags. need to compute tags args from opts.BuildFlags. -- // TODO: support -test. -- -- ir, iw := io.Pipe() -- handler := &govulncheckHandler{logger: log, osvs: map[string]*osv.Entry{}} -- -- stderr := new(bytes.Buffer) -- var g errgroup.Group -- // We run the govulncheck's analysis in a separate process as it can -- // consume a lot of CPUs and memory, and terminates: a separate process -- // is a perfect garbage collector and affords us ways to limit its resource usage. -- g.Go(func() error { -- defer iw.Close() -- -- cmd := exec.CommandContext(ctx, os.Args[0], vulncheckargs...) -- cmd.Env = getEnvSlices(snapshot) -- if goversion := getEnv(snapshot, GoVersionForVulnTest); goversion != "" { -- // Let govulncheck API use a different Go version using the (undocumented) hook -- // in https://go.googlesource.com/vuln/+/v1.0.1/internal/scan/run.go#76 -- cmd.Env = append(cmd.Env, "GOVERSION="+goversion) -- } -- cmd.Stderr = stderr // stream vulncheck's STDERR as progress reports -- cmd.Stdout = iw // let the other goroutine parses the result. -- -- if err := cmd.Start(); err != nil { -- return fmt.Errorf("failed to start govulncheck: %v", err) -- } -- if err := cmd.Wait(); err != nil { -- return fmt.Errorf("failed to run govulncheck: %v", err) -- } -- return nil -- }) -- g.Go(func() error { -- return govulncheck.HandleJSON(ir, handler) -- }) -- if err := g.Wait(); err != nil { -- if stderr.Len() > 0 { -- log.Write(stderr.Bytes()) -- } -- return nil, fmt.Errorf("failed to read govulncheck output: %v", err) -- } -- -- findings := handler.findings // sort so the findings in the result is deterministic. -- sort.Slice(findings, func(i, j int) bool { -- x, y := findings[i], findings[j] -- if x.OSV != y.OSV { -- return x.OSV < y.OSV -- } -- return x.Trace[0].Package < y.Trace[0].Package -- }) -- result := &vulncheck.Result{ -- Mode: vulncheck.ModeGovulncheck, -- AsOf: time.Now(), -- Entries: handler.osvs, -- Findings: findings, -- } -- return result, nil --} -- --type govulncheckHandler struct { -- logger io.Writer // forward progress reports to logger. -- err error -- -- osvs map[string]*osv.Entry -- findings []*govulncheck.Finding --} -- --// Config implements vulncheck.Handler. --func (h *govulncheckHandler) Config(config *govulncheck.Config) error { -- if config.GoVersion != "" { -- fmt.Fprintf(h.logger, "Go: %v\n", config.GoVersion) -- } -- if config.ScannerName != "" { -- scannerName := fmt.Sprintf("Scanner: %v", config.ScannerName) -- if config.ScannerVersion != "" { -- scannerName += "@" + config.ScannerVersion -- } -- fmt.Fprintln(h.logger, scannerName) -- } -- if config.DB != "" { -- dbInfo := fmt.Sprintf("DB: %v", config.DB) -- if config.DBLastModified != nil { -- dbInfo += fmt.Sprintf(" (DB updated: %v)", config.DBLastModified.String()) -- } -- fmt.Fprintln(h.logger, dbInfo) -- } -- return nil --} -- --// Finding implements vulncheck.Handler. --func (h *govulncheckHandler) Finding(finding *govulncheck.Finding) error { -- h.findings = append(h.findings, finding) -- return nil --} -- --// OSV implements vulncheck.Handler. --func (h *govulncheckHandler) OSV(entry *osv.Entry) error { -- h.osvs[entry.ID] = entry -- return nil --} -- --// Progress implements vulncheck.Handler. --func (h *govulncheckHandler) Progress(progress *govulncheck.Progress) error { -- if progress.Message != "" { -- fmt.Fprintf(h.logger, "%v\n", progress.Message) -- } -- return nil --} -- --func getEnv(snapshot source.Snapshot, key string) string { -- val, ok := snapshot.Options().Env[key] -- if ok { -- return val -- } -- return os.Getenv(key) --} -- --func getEnvSlices(snapshot source.Snapshot) []string { -- return append(os.Environ(), snapshot.Options().EnvSlice()...) --} -- --// semverToGoTag returns the Go standard library repository tag corresponding --// to semver, a version string without the initial "v". --// Go tags differ from standard semantic versions in a few ways, --// such as beginning with "go" instead of "v". --func semverToGoTag(v string) string { -- if strings.HasPrefix(v, "v0.0.0") { -- return "master" -- } -- // Special case: v1.0.0 => go1. -- if v == "v1.0.0" { -- return "go1" -- } -- if !semver.IsValid(v) { -- return fmt.Sprintf("<!%s:invalid semver>", v) -- } -- goVersion := semver.Canonical(v) -- prerelease := semver.Prerelease(goVersion) -- versionWithoutPrerelease := strings.TrimSuffix(goVersion, prerelease) -- patch := strings.TrimPrefix(versionWithoutPrerelease, semver.MajorMinor(goVersion)+".") -- if patch == "0" { -- versionWithoutPrerelease = strings.TrimSuffix(versionWithoutPrerelease, ".0") -- } -- goVersion = fmt.Sprintf("go%s", strings.TrimPrefix(versionWithoutPrerelease, "v")) -- if prerelease != "" { -- // Go prereleases look like "beta1" instead of "beta.1". -- // "beta1" is bad for sorting (since beta10 comes before beta9), so -- // require the dot form. -- i := finalDigitsIndex(prerelease) -- if i >= 1 { -- if prerelease[i-1] != '.' { -- return fmt.Sprintf("<!%s:final digits in a prerelease must follow a period>", v) -- } -- // Remove the dot. -- prerelease = prerelease[:i-1] + prerelease[i:] -- } -- goVersion += strings.TrimPrefix(prerelease, "-") -- } -- return goVersion --} -- --// finalDigitsIndex returns the index of the first digit in the sequence of digits ending s. --// If s doesn't end in digits, it returns -1. --func finalDigitsIndex(s string) int { -- // Assume ASCII (since the semver package does anyway). -- var i int -- for i = len(s) - 1; i >= 0; i-- { -- if s[i] < '0' || s[i] > '9' { -- break -- } -- } -- if i == len(s)-1 { -- return -1 -- } -- return i + 1 --} -- --// VulnerablePackages queries the vulndb and reports which vulnerabilities --// apply to this snapshot. The result contains a set of packages, --// grouped by vuln ID and by module. This implements the "import-based" --// vulnerability report on go.mod files. --func VulnerablePackages(ctx context.Context, snapshot source.Snapshot) (*vulncheck.Result, error) { -- // TODO(hyangah): can we let 'govulncheck' take a package list -- // used in the workspace and implement this function? -- -- // We want to report the intersection of vulnerable packages in the vulndb -- // and packages transitively imported by this module ('go list -deps all'). -- // We use snapshot.AllMetadata to retrieve the list of packages -- // as an approximation. -- // -- // TODO(hyangah): snapshot.AllMetadata is a superset of -- // `go list all` - e.g. when the workspace has multiple main modules -- // (multiple go.mod files), that can include packages that are not -- // used by this module. Vulncheck behavior with go.work is not well -- // defined. Figure out the meaning, and if we decide to present -- // the result as if each module is analyzed independently, make -- // gopls track a separate build list for each module and use that -- // information instead of snapshot.AllMetadata. -- metadata, err := snapshot.AllMetadata(ctx) -- if err != nil { -- return nil, err -- } -- -- // TODO(hyangah): handle vulnerabilities in the standard library. -- -- // Group packages by modules since vuln db is keyed by module. -- metadataByModule := map[source.PackagePath][]*source.Metadata{} -- for _, md := range metadata { -- modulePath := source.PackagePath(osv.GoStdModulePath) -- if mi := md.Module; mi != nil { -- modulePath = source.PackagePath(mi.Path) -- } -- metadataByModule[modulePath] = append(metadataByModule[modulePath], md) -- } -- -- var ( -- mu sync.Mutex -- // Keys are osv.Entry.ID -- osvs = map[string]*osv.Entry{} -- findings []*govulncheck.Finding -- ) -- -- goVersion := snapshot.Options().Env[GoVersionForVulnTest] -- if goVersion == "" { -- goVersion = snapshot.View().GoVersionString() -- } -- -- stdlibModule := &packages.Module{ -- Path: osv.GoStdModulePath, -- Version: goVersion, -- } -- -- // GOVULNDB may point the test db URI. -- db := getEnv(snapshot, "GOVULNDB") -- -- var group errgroup.Group -- group.SetLimit(10) // limit govulncheck api runs -- for _, mds := range metadataByModule { -- mds := mds -- group.Go(func() error { -- effectiveModule := stdlibModule -- if m := mds[0].Module; m != nil { -- effectiveModule = m -- } -- for effectiveModule.Replace != nil { -- effectiveModule = effectiveModule.Replace -- } -- ver := effectiveModule.Version -- if ver == "" || !isem.Valid(ver) { -- // skip invalid version strings. the underlying scan api is strict. -- return nil -- } -- -- // TODO(hyangah): batch these requests and add in-memory cache for efficiency. -- vulns, err := osvsByModule(ctx, db, effectiveModule.Path+"@"+ver) -- if err != nil { -- return err -- } -- if len(vulns) == 0 { // No known vulnerability. -- return nil -- } -- -- // set of packages in this module known to gopls. -- // This will be lazily initialized when we need it. -- var knownPkgs map[source.PackagePath]bool -- -- // Report vulnerabilities that affect packages of this module. -- for _, entry := range vulns { -- var vulnerablePkgs []*govulncheck.Finding -- fixed := fixedVersion(effectiveModule.Path, entry.Affected) -- -- for _, a := range entry.Affected { -- if a.Module.Ecosystem != osv.GoEcosystem || a.Module.Path != effectiveModule.Path { -- continue -- } -- for _, imp := range a.EcosystemSpecific.Packages { -- if knownPkgs == nil { -- knownPkgs = toPackagePathSet(mds) -- } -- if knownPkgs[source.PackagePath(imp.Path)] { -- vulnerablePkgs = append(vulnerablePkgs, &govulncheck.Finding{ -- OSV: entry.ID, -- FixedVersion: fixed, -- Trace: []*govulncheck.Frame{ -- { -- Module: effectiveModule.Path, -- Version: effectiveModule.Version, -- Package: imp.Path, -- }, -- }, -- }) -- } -- } -- } -- if len(vulnerablePkgs) == 0 { -- continue -- } -- mu.Lock() -- osvs[entry.ID] = entry -- findings = append(findings, vulnerablePkgs...) -- mu.Unlock() -- } -- return nil -- }) -- } -- if err := group.Wait(); err != nil { -- return nil, err -- } -- -- // Sort so the results are deterministic. -- sort.Slice(findings, func(i, j int) bool { -- x, y := findings[i], findings[j] -- if x.OSV != y.OSV { -- return x.OSV < y.OSV -- } -- return x.Trace[0].Package < y.Trace[0].Package -- }) -- ret := &vulncheck.Result{ -- Entries: osvs, -- Findings: findings, -- Mode: vulncheck.ModeImports, -- } -- return ret, nil --} -- --// toPackagePathSet transforms the metadata to a set of package paths. --func toPackagePathSet(mds []*source.Metadata) map[source.PackagePath]bool { -- pkgPaths := make(map[source.PackagePath]bool, len(mds)) -- for _, md := range mds { -- pkgPaths[md.PkgPath] = true -- } -- return pkgPaths --} -- --func fixedVersion(modulePath string, affected []osv.Affected) string { -- fixed := LatestFixed(modulePath, affected) -- if fixed != "" { -- fixed = versionString(modulePath, fixed) -- } -- return fixed --} -- --// versionString prepends a version string prefix (`v` or `go` --// depending on the modulePath) to the given semver-style version string. --func versionString(modulePath, version string) string { -- if version == "" { -- return "" -- } -- v := "v" + version -- // These are internal Go module paths used by the vuln DB -- // when listing vulns in standard library and the go command. -- if modulePath == "stdlib" || modulePath == "toolchain" { -- return semverToGoTag(v) -- } -- return v --} -- --// osvsByModule runs a govulncheck database query. --func osvsByModule(ctx context.Context, db, moduleVersion string) ([]*osv.Entry, error) { -- var args []string -- args = append(args, "-mode=query", "-json") -- if db != "" { -- args = append(args, "-db="+db) +- "-scan", "symbol", - } -- args = append(args, moduleVersion) +- if dir != "" { +- vulncheckargs = append(vulncheckargs, "-C", dir) +- } +- if db := cache.GetEnv(snapshot, "GOVULNDB"); db != "" { +- vulncheckargs = append(vulncheckargs, "-db", db) +- } +- vulncheckargs = append(vulncheckargs, pattern) +- // TODO: support -tags. need to compute tags args from opts.BuildFlags. +- // TODO: support -test. - - ir, iw := io.Pipe() -- handler := &osvReader{} +- handler := &govulncheckHandler{logger: log, osvs: map[string]*osv.Entry{}} - +- stderr := new(bytes.Buffer) - var g errgroup.Group +- // We run the govulncheck's analysis in a separate process as it can +- // consume a lot of CPUs and memory, and terminates: a separate process +- // is a perfect garbage collector and affords us ways to limit its resource usage. - g.Go(func() error { -- defer iw.Close() // scan API doesn't close cmd.Stderr/cmd.Stdout. -- cmd := scan.Command(ctx, args...) -- cmd.Stdout = iw -- // TODO(hakim): Do we need to set cmd.Env = getEnvSlices(), -- // or is the process environment good enough? +- defer iw.Close() +- +- cmd := exec.CommandContext(ctx, os.Args[0], vulncheckargs...) +- cmd.Env = getEnvSlices(snapshot) +- if goversion := cache.GetEnv(snapshot, cache.GoVersionForVulnTest); goversion != "" { +- // Let govulncheck API use a different Go version using the (undocumented) hook +- // in https://go.googlesource.com/vuln/+/v1.0.1/internal/scan/run.go#76 +- cmd.Env = append(cmd.Env, "GOVERSION="+goversion) +- } +- cmd.Stderr = stderr // stream vulncheck's STDERR as progress reports +- cmd.Stdout = iw // let the other goroutine parses the result. +- - if err := cmd.Start(); err != nil { -- return err +- return fmt.Errorf("failed to start govulncheck: %v", err) - } -- return cmd.Wait() +- if err := cmd.Wait(); err != nil { +- return fmt.Errorf("failed to run govulncheck: %v", err) +- } +- return nil - }) - g.Go(func() error { - return govulncheck.HandleJSON(ir, handler) - }) -- - if err := g.Wait(); err != nil { -- return nil, err +- if stderr.Len() > 0 { +- log.Write(stderr.Bytes()) +- } +- return nil, fmt.Errorf("failed to read govulncheck output: %v", err) - } -- return handler.entry, nil +- +- findings := handler.findings // sort so the findings in the result is deterministic. +- sort.Slice(findings, func(i, j int) bool { +- x, y := findings[i], findings[j] +- if x.OSV != y.OSV { +- return x.OSV < y.OSV +- } +- return x.Trace[0].Package < y.Trace[0].Package +- }) +- result := &vulncheck.Result{ +- Mode: vulncheck.ModeGovulncheck, +- AsOf: time.Now(), +- Entries: handler.osvs, +- Findings: findings, +- } +- return result, nil -} - --// osvReader implements govulncheck.Handler. --type osvReader struct { -- entry []*osv.Entry +-type govulncheckHandler struct { +- logger io.Writer // forward progress reports to logger. +- +- osvs map[string]*osv.Entry +- findings []*govulncheck.Finding -} - --func (h *osvReader) OSV(entry *osv.Entry) error { -- h.entry = append(h.entry, entry) +-// Config implements vulncheck.Handler. +-func (h *govulncheckHandler) Config(config *govulncheck.Config) error { +- if config.GoVersion != "" { +- fmt.Fprintf(h.logger, "Go: %v\n", config.GoVersion) +- } +- if config.ScannerName != "" { +- scannerName := fmt.Sprintf("Scanner: %v", config.ScannerName) +- if config.ScannerVersion != "" { +- scannerName += "@" + config.ScannerVersion +- } +- fmt.Fprintln(h.logger, scannerName) +- } +- if config.DB != "" { +- dbInfo := fmt.Sprintf("DB: %v", config.DB) +- if config.DBLastModified != nil { +- dbInfo += fmt.Sprintf(" (DB updated: %v)", config.DBLastModified.String()) +- } +- fmt.Fprintln(h.logger, dbInfo) +- } - return nil -} - --func (h *osvReader) Config(config *govulncheck.Config) error { +-// Finding implements vulncheck.Handler. +-func (h *govulncheckHandler) Finding(finding *govulncheck.Finding) error { +- h.findings = append(h.findings, finding) - return nil -} - --func (h *osvReader) Finding(finding *govulncheck.Finding) error { +-// OSV implements vulncheck.Handler. +-func (h *govulncheckHandler) OSV(entry *osv.Entry) error { +- h.osvs[entry.ID] = entry - return nil -} - --func (h *osvReader) Progress(progress *govulncheck.Progress) error { +-// Progress implements vulncheck.Handler. +-func (h *govulncheckHandler) Progress(progress *govulncheck.Progress) error { +- if progress.Message != "" { +- fmt.Fprintf(h.logger, "%v\n", progress.Message) +- } - return nil -} -diff -urN a/gopls/internal/vulncheck/scan/util.go b/gopls/internal/vulncheck/scan/util.go ---- a/gopls/internal/vulncheck/scan/util.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/vulncheck/scan/util.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,36 +0,0 @@ --// Copyright 2022 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --//go:build go1.18 --// +build go1.18 -- --package scan - --import ( -- "golang.org/x/mod/semver" -- "golang.org/x/tools/gopls/internal/vulncheck/osv" -- isem "golang.org/x/tools/gopls/internal/vulncheck/semver" --) -- --// LatestFixed returns the latest fixed version in the list of affected ranges, --// or the empty string if there are no fixed versions. --func LatestFixed(modulePath string, as []osv.Affected) string { -- v := "" -- for _, a := range as { -- if a.Module.Path != modulePath { -- continue -- } -- for _, r := range a.Ranges { -- if r.Type == osv.RangeTypeSemver { -- for _, e := range r.Events { -- if e.Fixed != "" && (v == "" || -- semver.Compare(isem.CanonicalizeSemverPrefix(e.Fixed), isem.CanonicalizeSemverPrefix(v)) > 0) { -- v = e.Fixed -- } -- } -- } -- } -- } -- return v +-func getEnvSlices(snapshot *cache.Snapshot) []string { +- return append(os.Environ(), snapshot.Options().EnvSlice()...) -} diff -urN a/gopls/internal/vulncheck/semver/semver.go b/gopls/internal/vulncheck/semver/semver.go --- a/gopls/internal/vulncheck/semver/semver.go 2000-01-01 00:00:00.000000000 -0000 +++ b/gopls/internal/vulncheck/semver/semver.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,59 +0,0 @@ +@@ -1,45 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --//go:build go1.18 --// +build go1.18 -- -// Package semver provides shared utilities for manipulating -// Go semantic versions. -package semver - -import ( -- "regexp" - "strings" - - "golang.org/x/mod/semver" @@ -141364,27 +150361,14 @@ diff -urN a/gopls/internal/vulncheck/semver/semver.go b/gopls/internal/vulncheck -func Valid(v string) bool { - return semver.IsValid(CanonicalizeSemverPrefix(v)) -} -- --var ( -- // Regexp for matching go tags. The groups are: -- // 1 the major.minor version -- // 2 the patch version, or empty if none -- // 3 the entire prerelease, if present -- // 4 the prerelease type ("beta" or "rc") -- // 5 the prerelease number -- tagRegexp = regexp.MustCompile(`^go(\d+\.\d+)(\.\d+|)((beta|rc|-pre)(\d+))?$`) --) diff -urN a/gopls/internal/vulncheck/semver/semver_test.go b/gopls/internal/vulncheck/semver/semver_test.go --- a/gopls/internal/vulncheck/semver/semver_test.go 2000-01-01 00:00:00.000000000 -0000 +++ b/gopls/internal/vulncheck/semver/semver_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,28 +0,0 @@ +@@ -1,25 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --//go:build go1.18 --// +build go1.18 -- -package semver - -import ( @@ -141460,14 +150444,11 @@ diff -urN a/gopls/internal/vulncheck/types.go b/gopls/internal/vulncheck/types.g diff -urN a/gopls/internal/vulncheck/vulntest/db.go b/gopls/internal/vulncheck/vulntest/db.go --- a/gopls/internal/vulncheck/vulntest/db.go 2000-01-01 00:00:00.000000000 -0000 +++ b/gopls/internal/vulncheck/vulntest/db.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,236 +0,0 @@ +@@ -1,233 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --//go:build go1.18 --// +build go1.18 -- -// Package vulntest provides helpers for vulncheck functionality testing. -package vulntest - @@ -141482,7 +150463,7 @@ diff -urN a/gopls/internal/vulncheck/vulntest/db.go b/gopls/internal/vulncheck/v - "strings" - "time" - -- "golang.org/x/tools/gopls/internal/span" +- "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/vulncheck/osv" - "golang.org/x/tools/txtar" -) @@ -141525,7 +150506,7 @@ diff -urN a/gopls/internal/vulncheck/vulntest/db.go b/gopls/internal/vulncheck/v -// URI returns the file URI that can be used for VULNDB environment -// variable. -func (db *DB) URI() string { -- u := span.URIFromPath(filepath.Join(db.disk, "ID")) +- u := protocol.URIFromPath(filepath.Join(db.disk, "ID")) - return string(u) -} - @@ -141700,14 +150681,11 @@ diff -urN a/gopls/internal/vulncheck/vulntest/db.go b/gopls/internal/vulncheck/v diff -urN a/gopls/internal/vulncheck/vulntest/db_test.go b/gopls/internal/vulncheck/vulntest/db_test.go --- a/gopls/internal/vulncheck/vulntest/db_test.go 2000-01-01 00:00:00.000000000 -0000 +++ b/gopls/internal/vulncheck/vulntest/db_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,79 +0,0 @@ +@@ -1,76 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --//go:build go1.18 --// +build go1.18 -- -package vulntest - -import ( @@ -141720,7 +150698,7 @@ diff -urN a/gopls/internal/vulncheck/vulntest/db_test.go b/gopls/internal/vulnch - "time" - - "github.com/google/go-cmp/cmp" -- "golang.org/x/tools/gopls/internal/span" +- "golang.org/x/tools/gopls/internal/protocol" - "golang.org/x/tools/gopls/internal/vulncheck/osv" -) - @@ -141740,7 +150718,7 @@ diff -urN a/gopls/internal/vulncheck/vulntest/db_test.go b/gopls/internal/vulnch - t.Fatal(err) - } - defer db.Clean() -- dbpath := span.URIFromURI(db.URI()).Filename() +- dbpath := protocol.DocumentURI(db.URI()).Path() - - // The generated JSON file will be in DB/GO-2022-0001.json. - got := readOSVEntry(t, filepath.Join(dbpath, "GO-2020-0001.json")) @@ -141783,14 +150761,11 @@ diff -urN a/gopls/internal/vulncheck/vulntest/db_test.go b/gopls/internal/vulnch diff -urN a/gopls/internal/vulncheck/vulntest/report.go b/gopls/internal/vulncheck/vulntest/report.go --- a/gopls/internal/vulncheck/vulntest/report.go 2000-01-01 00:00:00.000000000 -0000 +++ b/gopls/internal/vulncheck/vulntest/report.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,179 +0,0 @@ +@@ -1,176 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --//go:build go1.18 --// +build go1.18 -- -package vulntest - -import ( @@ -141966,14 +150941,11 @@ diff -urN a/gopls/internal/vulncheck/vulntest/report.go b/gopls/internal/vulnche diff -urN a/gopls/internal/vulncheck/vulntest/report_test.go b/gopls/internal/vulncheck/vulntest/report_test.go --- a/gopls/internal/vulncheck/vulntest/report_test.go 2000-01-01 00:00:00.000000000 -0000 +++ b/gopls/internal/vulncheck/vulntest/report_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,51 +0,0 @@ +@@ -1,48 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --//go:build go1.18 --// +build go1.18 -- -package vulntest - -import ( @@ -142018,145 +150990,531 @@ diff -urN a/gopls/internal/vulncheck/vulntest/report_test.go b/gopls/internal/vu - t.Errorf("mismatch (-want, +got):\n%s", diff) - } -} -diff -urN a/gopls/internal/vulncheck/vulntest/stdlib.go b/gopls/internal/vulncheck/vulntest/stdlib.go ---- a/gopls/internal/vulncheck/vulntest/stdlib.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/vulncheck/vulntest/stdlib.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,26 +0,0 @@ +diff -urN a/gopls/internal/vulncheck/vulntest/stdlib.go b/gopls/internal/vulncheck/vulntest/stdlib.go +--- a/gopls/internal/vulncheck/vulntest/stdlib.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/vulncheck/vulntest/stdlib.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,23 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package vulntest +- +-import ( +- "strings" +- +- "golang.org/x/mod/module" +-) +- +-// maybeStdlib reports whether the given import path could be part of the Go +-// standard library, by reporting whether the first component lacks a '.'. +-func maybeStdlib(path string) bool { +- if err := module.CheckImportPath(path); err != nil { +- return false +- } +- if i := strings.IndexByte(path, '/'); i != -1 { +- path = path[:i] +- } +- return !strings.Contains(path, ".") +-} +diff -urN a/gopls/internal/vulncheck/vulntest/stdlib_test.go b/gopls/internal/vulncheck/vulntest/stdlib_test.go +--- a/gopls/internal/vulncheck/vulntest/stdlib_test.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/vulncheck/vulntest/stdlib_test.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,24 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package vulntest +- +-import "testing" +- +-func TestMaybeStdlib(t *testing.T) { +- for _, test := range []struct { +- in string +- want bool +- }{ +- {"", false}, +- {"math/crypto", true}, +- {"github.com/pkg/errors", false}, +- {"Path is unknown", false}, +- } { +- got := maybeStdlib(test.in) +- if got != test.want { +- t.Errorf("%q: got %t, want %t", test.in, got, test.want) +- } +- } +-} +diff -urN a/gopls/internal/vulncheck/vulntest/testdata/GO-2020-0001.json b/gopls/internal/vulncheck/vulntest/testdata/GO-2020-0001.json +--- a/gopls/internal/vulncheck/vulntest/testdata/GO-2020-0001.json 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/vulncheck/vulntest/testdata/GO-2020-0001.json 1970-01-01 00:00:00.000000000 +0000 +@@ -1,50 +0,0 @@ +-{ +- "id": "GO-2020-0001", +- "modified": "0001-01-01T00:00:00Z", +- "published": "0001-01-01T00:00:00Z", +- "details": "The default Formatter for the Logger middleware (LoggerConfig.Formatter),\nwhich is included in the Default engine, allows attackers to inject arbitrary\nlog entries by manipulating the request path.\n", +- "affected": [ +- { +- "package": { +- "name": "github.com/gin-gonic/gin", +- "ecosystem": "Go" +- }, +- "ranges": [ +- { +- "type": "SEMVER", +- "events": [ +- { +- "introduced": "0" +- }, +- { +- "fixed": "1.6.0" +- } +- ] +- } +- ], +- "ecosystem_specific": { +- "imports": [ +- { +- "path": "github.com/gin-gonic/gin", +- "symbols": [ +- "defaultLogFormatter" +- ] +- } +- ] +- } +- } +- ], +- "references": [ +- { +- "type": "FIX", +- "url": "https://github.com/gin-gonic/gin/pull/1234" +- }, +- { +- "type": "FIX", +- "url": "https://github.com/gin-gonic/gin/commit/abcdefg" +- } +- ], +- "database_specific": { +- "url": "https://pkg.go.dev/vuln/GO-2020-0001" +- } +-} +\ No newline at end of file +diff -urN a/gopls/internal/vulncheck/vulntest/testdata/report.yaml b/gopls/internal/vulncheck/vulntest/testdata/report.yaml +--- a/gopls/internal/vulncheck/vulntest/testdata/report.yaml 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/vulncheck/vulntest/testdata/report.yaml 1970-01-01 00:00:00.000000000 +0000 +@@ -1,15 +0,0 @@ +-modules: +- - module: github.com/gin-gonic/gin +- versions: +- - fixed: 1.6.0 +- packages: +- - package: github.com/gin-gonic/gin +- symbols: +- - defaultLogFormatter +-description: | +- The default Formatter for the Logger middleware (LoggerConfig.Formatter), +- which is included in the Default engine, allows attackers to inject arbitrary +- log entries by manipulating the request path. +-references: +- - fix: https://github.com/gin-gonic/gin/pull/1234 +- - fix: https://github.com/gin-gonic/gin/commit/abcdefg +diff -urN a/gopls/internal/work/completion.go b/gopls/internal/work/completion.go +--- a/gopls/internal/work/completion.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/work/completion.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,161 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package work +- +-import ( +- "context" +- "errors" +- "fmt" +- "io/fs" +- "os" +- "path/filepath" +- "sort" +- "strings" +- +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/event" +-) +- +-func Completion(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) (*protocol.CompletionList, error) { +- ctx, done := event.Start(ctx, "work.Completion") +- defer done() +- +- // Get the position of the cursor. +- pw, err := snapshot.ParseWork(ctx, fh) +- if err != nil { +- return nil, fmt.Errorf("getting go.work file handle: %w", err) +- } +- cursor, err := pw.Mapper.PositionOffset(position) +- if err != nil { +- return nil, fmt.Errorf("computing cursor offset: %w", err) +- } +- +- // Find the use statement the user is in. +- use, pathStart, _ := usePath(pw, cursor) +- if use == nil { +- return &protocol.CompletionList{}, nil +- } +- completingFrom := use.Path[:cursor-pathStart] +- +- // We're going to find the completions of the user input +- // (completingFrom) by doing a walk on the innermost directory +- // of the given path, and comparing the found paths to make sure +- // that they match the component of the path after the +- // innermost directory. +- // +- // We'll maintain two paths when doing this: pathPrefixSlash +- // is essentially the path the user typed in, and pathPrefixAbs +- // is the path made absolute from the go.work directory. +- +- pathPrefixSlash := completingFrom +- pathPrefixAbs := filepath.FromSlash(pathPrefixSlash) +- if !filepath.IsAbs(pathPrefixAbs) { +- pathPrefixAbs = filepath.Join(filepath.Dir(pw.URI.Path()), pathPrefixAbs) +- } +- +- // pathPrefixDir is the directory that will be walked to find matches. +- // If pathPrefixSlash is not explicitly a directory boundary (is either equivalent to "." or +- // ends in a separator) we need to examine its parent directory to find sibling files that +- // match. +- depthBound := 5 +- pathPrefixDir, pathPrefixBase := pathPrefixAbs, "" +- pathPrefixSlashDir := pathPrefixSlash +- if filepath.Clean(pathPrefixSlash) != "." && !strings.HasSuffix(pathPrefixSlash, "/") { +- depthBound++ +- pathPrefixDir, pathPrefixBase = filepath.Split(pathPrefixAbs) +- pathPrefixSlashDir = dirNonClean(pathPrefixSlash) +- } +- +- var completions []string +- // Stop traversing deeper once we've hit 10k files to try to stay generally under 100ms. +- const numSeenBound = 10000 +- var numSeen int +- stopWalking := errors.New("hit numSeenBound") +- err = filepath.WalkDir(pathPrefixDir, func(wpath string, entry fs.DirEntry, err error) error { +- if err != nil { +- // golang/go#64225: an error reading a dir is expected, as the user may +- // be typing out a use directive for a directory that doesn't exist. +- return nil +- } +- if numSeen > numSeenBound { +- // Stop traversing if we hit bound. +- return stopWalking +- } +- numSeen++ +- +- // rel is the path relative to pathPrefixDir. +- // Make sure that it has pathPrefixBase as a prefix +- // otherwise it won't match the beginning of the +- // base component of the path the user typed in. +- rel := strings.TrimPrefix(wpath[len(pathPrefixDir):], string(filepath.Separator)) +- if entry.IsDir() && wpath != pathPrefixDir && !strings.HasPrefix(rel, pathPrefixBase) { +- return filepath.SkipDir +- } +- +- // Check for a match (a module directory). +- if filepath.Base(rel) == "go.mod" { +- relDir := strings.TrimSuffix(dirNonClean(rel), string(os.PathSeparator)) +- completionPath := join(pathPrefixSlashDir, filepath.ToSlash(relDir)) +- +- if !strings.HasPrefix(completionPath, completingFrom) { +- return nil +- } +- if strings.HasSuffix(completionPath, "/") { +- // Don't suggest paths that end in "/". This happens +- // when the input is a path that ends in "/" and +- // the completion is empty. +- return nil +- } +- completion := completionPath[len(completingFrom):] +- if completingFrom == "" && !strings.HasPrefix(completion, "./") { +- // Bias towards "./" prefixes. +- completion = join(".", completion) +- } +- +- completions = append(completions, completion) +- } +- +- if depth := strings.Count(rel, string(filepath.Separator)); depth >= depthBound { +- return filepath.SkipDir +- } +- return nil +- }) +- if err != nil && !errors.Is(err, stopWalking) { +- return nil, fmt.Errorf("walking to find completions: %w", err) +- } +- +- sort.Strings(completions) +- +- items := []protocol.CompletionItem{} // must be a slice +- for _, c := range completions { +- items = append(items, protocol.CompletionItem{ +- Label: c, +- InsertText: c, +- }) +- } +- return &protocol.CompletionList{Items: items}, nil +-} +- +-// dirNonClean is filepath.Dir, without the Clean at the end. +-func dirNonClean(path string) string { +- vol := filepath.VolumeName(path) +- i := len(path) - 1 +- for i >= len(vol) && !os.IsPathSeparator(path[i]) { +- i-- +- } +- return path[len(vol) : i+1] +-} +- +-func join(a, b string) string { +- if a == "" { +- return b +- } +- if b == "" { +- return a +- } +- return strings.TrimSuffix(a, "/") + "/" + b +-} +diff -urN a/gopls/internal/work/diagnostics.go b/gopls/internal/work/diagnostics.go +--- a/gopls/internal/work/diagnostics.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/work/diagnostics.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,92 +0,0 @@ +-// Copyright 2022 The Go Authors. All rights reserved. +-// Use of this source code is governed by a BSD-style +-// license that can be found in the LICENSE file. +- +-package work +- +-import ( +- "context" +- "fmt" +- "os" +- "path/filepath" +- +- "golang.org/x/mod/modfile" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/event" +-) +- +-func Diagnostics(ctx context.Context, snapshot *cache.Snapshot) (map[protocol.DocumentURI][]*cache.Diagnostic, error) { +- ctx, done := event.Start(ctx, "work.Diagnostics", snapshot.Labels()...) +- defer done() +- +- reports := map[protocol.DocumentURI][]*cache.Diagnostic{} +- uri := snapshot.View().GoWork() +- if uri == "" { +- return nil, nil +- } +- fh, err := snapshot.ReadFile(ctx, uri) +- if err != nil { +- return nil, err +- } +- reports[fh.URI()] = []*cache.Diagnostic{} +- diagnostics, err := diagnoseOne(ctx, snapshot, fh) +- if err != nil { +- return nil, err +- } +- for _, d := range diagnostics { +- fh, err := snapshot.ReadFile(ctx, d.URI) +- if err != nil { +- return nil, err +- } +- reports[fh.URI()] = append(reports[fh.URI()], d) +- } +- +- return reports, nil +-} +- +-func diagnoseOne(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]*cache.Diagnostic, error) { +- pw, err := snapshot.ParseWork(ctx, fh) +- if err != nil { +- if pw == nil || len(pw.ParseErrors) == 0 { +- return nil, err +- } +- return pw.ParseErrors, nil +- } +- +- // Add diagnostic if a directory does not contain a module. +- var diagnostics []*cache.Diagnostic +- for _, use := range pw.File.Use { +- rng, err := pw.Mapper.OffsetRange(use.Syntax.Start.Byte, use.Syntax.End.Byte) +- if err != nil { +- return nil, err +- } +- +- modfh, err := snapshot.ReadFile(ctx, modFileURI(pw, use)) +- if err != nil { +- return nil, err +- } +- if _, err := modfh.Content(); err != nil && os.IsNotExist(err) { +- diagnostics = append(diagnostics, &cache.Diagnostic{ +- URI: fh.URI(), +- Range: rng, +- Severity: protocol.SeverityError, +- Source: cache.WorkFileError, +- Message: fmt.Sprintf("directory %v does not contain a module", use.Path), +- }) +- } +- } +- return diagnostics, nil +-} +- +-func modFileURI(pw *cache.ParsedWorkFile, use *modfile.Use) protocol.DocumentURI { +- workdir := filepath.Dir(pw.URI.Path()) +- +- modroot := filepath.FromSlash(use.Path) +- if !filepath.IsAbs(modroot) { +- modroot = filepath.Join(workdir, modroot) +- } +- +- return protocol.URIFromPath(filepath.Join(modroot, "go.mod")) +-} +diff -urN a/gopls/internal/work/format.go b/gopls/internal/work/format.go +--- a/gopls/internal/work/format.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/work/format.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,30 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --//go:build go1.18 --// +build go1.18 -- --package vulntest +-package work - -import ( -- "strings" +- "context" - -- "golang.org/x/mod/module" +- "golang.org/x/mod/modfile" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/diff" +- "golang.org/x/tools/internal/event" -) - --// maybeStdlib reports whether the given import path could be part of the Go --// standard library, by reporting whether the first component lacks a '.'. --func maybeStdlib(path string) bool { -- if err := module.CheckImportPath(path); err != nil { -- return false -- } -- if i := strings.IndexByte(path, '/'); i != -1 { -- path = path[:i] +-func Format(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle) ([]protocol.TextEdit, error) { +- ctx, done := event.Start(ctx, "work.Format") +- defer done() +- +- pw, err := snapshot.ParseWork(ctx, fh) +- if err != nil { +- return nil, err - } -- return !strings.Contains(path, ".") +- formatted := modfile.Format(pw.File.Syntax) +- // Calculate the edits to be made due to the change. +- diffs := diff.Bytes(pw.Mapper.Content, formatted) +- return protocol.EditsFromDiffEdits(pw.Mapper, diffs) -} -diff -urN a/gopls/internal/vulncheck/vulntest/stdlib_test.go b/gopls/internal/vulncheck/vulntest/stdlib_test.go ---- a/gopls/internal/vulncheck/vulntest/stdlib_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/vulncheck/vulntest/stdlib_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,27 +0,0 @@ +diff -urN a/gopls/internal/work/hover.go b/gopls/internal/work/hover.go +--- a/gopls/internal/work/hover.go 2000-01-01 00:00:00.000000000 -0000 ++++ b/gopls/internal/work/hover.go 1970-01-01 00:00:00.000000000 +0000 +@@ -1,93 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - --//go:build go1.18 --// +build go1.18 +-package work - --package vulntest +-import ( +- "bytes" +- "context" +- "fmt" - --import "testing" +- "golang.org/x/mod/modfile" +- "golang.org/x/tools/gopls/internal/cache" +- "golang.org/x/tools/gopls/internal/file" +- "golang.org/x/tools/gopls/internal/protocol" +- "golang.org/x/tools/internal/event" +-) - --func TestMaybeStdlib(t *testing.T) { -- for _, test := range []struct { -- in string -- want bool -- }{ -- {"", false}, -- {"math/crypto", true}, -- {"github.com/pkg/errors", false}, -- {"Path is unknown", false}, -- } { -- got := maybeStdlib(test.in) -- if got != test.want { -- t.Errorf("%q: got %t, want %t", test.in, got, test.want) -- } +-func Hover(ctx context.Context, snapshot *cache.Snapshot, fh file.Handle, position protocol.Position) (*protocol.Hover, error) { +- // We only provide hover information for the view's go.work file. +- if fh.URI() != snapshot.View().GoWork() { +- return nil, nil +- } +- +- ctx, done := event.Start(ctx, "work.Hover") +- defer done() +- +- // Get the position of the cursor. +- pw, err := snapshot.ParseWork(ctx, fh) +- if err != nil { +- return nil, fmt.Errorf("getting go.work file handle: %w", err) +- } +- offset, err := pw.Mapper.PositionOffset(position) +- if err != nil { +- return nil, fmt.Errorf("computing cursor offset: %w", err) +- } +- +- // Confirm that the cursor is inside a use statement, and then find +- // the position of the use statement's directory path. +- use, pathStart, pathEnd := usePath(pw, offset) +- +- // The cursor position is not on a use statement. +- if use == nil { +- return nil, nil +- } +- +- // Get the mod file denoted by the use. +- modfh, err := snapshot.ReadFile(ctx, modFileURI(pw, use)) +- if err != nil { +- return nil, fmt.Errorf("getting modfile handle: %w", err) +- } +- pm, err := snapshot.ParseMod(ctx, modfh) +- if err != nil { +- return nil, fmt.Errorf("getting modfile handle: %w", err) +- } +- if pm.File.Module == nil { +- return nil, fmt.Errorf("modfile has no module declaration") +- } +- mod := pm.File.Module.Mod +- +- // Get the range to highlight for the hover. +- rng, err := pw.Mapper.OffsetRange(pathStart, pathEnd) +- if err != nil { +- return nil, err - } +- options := snapshot.Options() +- return &protocol.Hover{ +- Contents: protocol.MarkupContent{ +- Kind: options.PreferredContentFormat, +- Value: mod.Path, +- }, +- Range: rng, +- }, nil -} -diff -urN a/gopls/internal/vulncheck/vulntest/testdata/GO-2020-0001.json b/gopls/internal/vulncheck/vulntest/testdata/GO-2020-0001.json ---- a/gopls/internal/vulncheck/vulntest/testdata/GO-2020-0001.json 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/vulncheck/vulntest/testdata/GO-2020-0001.json 1970-01-01 00:00:00.000000000 +0000 -@@ -1,50 +0,0 @@ --{ -- "id": "GO-2020-0001", -- "modified": "0001-01-01T00:00:00Z", -- "published": "0001-01-01T00:00:00Z", -- "details": "The default Formatter for the Logger middleware (LoggerConfig.Formatter),\nwhich is included in the Default engine, allows attackers to inject arbitrary\nlog entries by manipulating the request path.\n", -- "affected": [ -- { -- "package": { -- "name": "github.com/gin-gonic/gin", -- "ecosystem": "Go" -- }, -- "ranges": [ -- { -- "type": "SEMVER", -- "events": [ -- { -- "introduced": "0" -- }, -- { -- "fixed": "1.6.0" -- } -- ] -- } -- ], -- "ecosystem_specific": { -- "imports": [ -- { -- "path": "github.com/gin-gonic/gin", -- "symbols": [ -- "defaultLogFormatter" -- ] -- } -- ] -- } +- +-func usePath(pw *cache.ParsedWorkFile, offset int) (use *modfile.Use, pathStart, pathEnd int) { +- for _, u := range pw.File.Use { +- path := []byte(u.Path) +- s, e := u.Syntax.Start.Byte, u.Syntax.End.Byte +- i := bytes.Index(pw.Mapper.Content[s:e], path) +- if i == -1 { +- // This should not happen. +- continue - } -- ], -- "references": [ -- { -- "type": "FIX", -- "url": "https://github.com/gin-gonic/gin/pull/1234" -- }, -- { -- "type": "FIX", -- "url": "https://github.com/gin-gonic/gin/commit/abcdefg" +- // Shift the start position to the location of the +- // module directory within the use statement. +- pathStart, pathEnd = s+i, s+i+len(path) +- if pathStart <= offset && offset <= pathEnd { +- return u, pathStart, pathEnd - } -- ], -- "database_specific": { -- "url": "https://pkg.go.dev/vuln/GO-2020-0001" - } +- return nil, 0, 0 -} -\ No newline at end of file -diff -urN a/gopls/internal/vulncheck/vulntest/testdata/report.yaml b/gopls/internal/vulncheck/vulntest/testdata/report.yaml ---- a/gopls/internal/vulncheck/vulntest/testdata/report.yaml 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/internal/vulncheck/vulntest/testdata/report.yaml 1970-01-01 00:00:00.000000000 +0000 -@@ -1,15 +0,0 @@ --modules: -- - module: github.com/gin-gonic/gin -- versions: -- - fixed: 1.6.0 -- packages: -- - package: github.com/gin-gonic/gin -- symbols: -- - defaultLogFormatter --description: | -- The default Formatter for the Logger middleware (LoggerConfig.Formatter), -- which is included in the Default engine, allows attackers to inject arbitrary -- log entries by manipulating the request path. --references: -- - fix: https://github.com/gin-gonic/gin/pull/1234 -- - fix: https://github.com/gin-gonic/gin/commit/abcdefg diff -urN a/gopls/main.go b/gopls/main.go --- a/gopls/main.go 2000-01-01 00:00:00.000000000 -0000 +++ b/gopls/main.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,30 +0,0 @@ +@@ -1,35 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. @@ -142176,21 +151534,26 @@ diff -urN a/gopls/main.go b/gopls/main.go - "context" - "os" - +- "golang.org/x/telemetry" +- "golang.org/x/tools/gopls/internal/cmd" - "golang.org/x/tools/gopls/internal/hooks" -- "golang.org/x/tools/gopls/internal/lsp/cmd" -- "golang.org/x/tools/gopls/internal/telemetry" +- versionpkg "golang.org/x/tools/gopls/internal/version" - "golang.org/x/tools/internal/tool" -) - +-var version = "" // if set by the linker, overrides the gopls version +- -func main() { -- telemetry.Start() +- versionpkg.VersionOverride = version +- +- telemetry.Start(telemetry.Config{ReportCrashes: true}) - ctx := context.Background() -- tool.Main(ctx, cmd.New("gopls", "", nil, hooks.Options), os.Args[1:]) +- tool.Main(ctx, cmd.New(hooks.Options), os.Args[1:]) -} diff -urN a/gopls/README.md b/gopls/README.md --- a/gopls/README.md 2000-01-01 00:00:00.000000000 -0000 +++ b/gopls/README.md 1970-01-01 00:00:00.000000000 +0000 -@@ -1,132 +0,0 @@ +@@ -1,133 +0,0 @@ -# `gopls`, the Go language server - -[![PkgGoDev](https://pkg.go.dev/badge/golang.org/x/tools/gopls)](https://pkg.go.dev/golang.org/x/tools/gopls) @@ -142287,6 +151650,7 @@ diff -urN a/gopls/README.md b/gopls/README.md -| Go 1.12 | [gopls@v0.7.5](https://github.com/golang/tools/releases/tag/gopls%2Fv0.7.5) | -| Go 1.15 | [gopls@v0.9.5](https://github.com/golang/tools/releases/tag/gopls%2Fv0.9.5) | -| Go 1.17 | [gopls@v0.11.0](https://github.com/golang/tools/releases/tag/gopls%2Fv0.11.0) | +-| Go 1.18 | [gopls@v0.14.2](https://github.com/golang/tools/releases/tag/gopls%2Fv0.14.2) | - -Our extended support is enforced via [continuous integration with older Go -versions](doc/contributing.md#ci). This legacy Go CI may not block releases: @@ -142326,7 +151690,7 @@ diff -urN a/gopls/README.md b/gopls/README.md diff -urN a/gopls/release/release.go b/gopls/release/release.go --- a/gopls/release/release.go 2000-01-01 00:00:00.000000000 -0000 +++ b/gopls/release/release.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,155 +0,0 @@ +@@ -1,106 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. @@ -142343,18 +151707,14 @@ diff -urN a/gopls/release/release.go b/gopls/release/release.go -import ( - "flag" - "fmt" -- "go/types" - "log" - "os" +- "os/exec" - "path/filepath" -- "strconv" - "strings" - -- exec "golang.org/x/sys/execabs" -- - "golang.org/x/mod/modfile" - "golang.org/x/mod/semver" -- "golang.org/x/tools/go/packages" -) - -var versionFlag = flag.String("version", "", "version to tag") @@ -142382,10 +151742,6 @@ diff -urN a/gopls/release/release.go b/gopls/release/release.go - if filepath.Base(wd) != "gopls" { - log.Fatalf("must run from the gopls module") - } -- // Confirm that they have updated the hardcoded version. -- if err := validateHardcodedVersion(*versionFlag); err != nil { -- log.Fatal(err) -- } - // Confirm that the versions in the go.mod file are correct. - if err := validateGoModFile(wd); err != nil { - log.Fatal(err) @@ -142394,47 +151750,6 @@ diff -urN a/gopls/release/release.go b/gopls/release/release.go - os.Exit(0) -} - --// validateHardcodedVersion reports whether the version hardcoded in the gopls --// binary is equivalent to the version being published. It reports an error if --// not. --func validateHardcodedVersion(version string) error { -- const debugPkg = "golang.org/x/tools/gopls/internal/lsp/debug" -- pkgs, err := packages.Load(&packages.Config{ -- Mode: packages.NeedName | packages.NeedFiles | -- packages.NeedCompiledGoFiles | packages.NeedImports | -- packages.NeedTypes | packages.NeedTypesSizes, -- }, debugPkg) -- if err != nil { -- return err -- } -- if len(pkgs) != 1 { -- return fmt.Errorf("expected 1 package, got %v", len(pkgs)) -- } -- pkg := pkgs[0] -- if len(pkg.Errors) > 0 { -- return fmt.Errorf("failed to load %q: first error: %w", debugPkg, pkg.Errors[0]) -- } -- obj := pkg.Types.Scope().Lookup("Version") -- c, ok := obj.(*types.Const) -- if !ok { -- return fmt.Errorf("no constant named Version") -- } -- hardcodedVersion, err := strconv.Unquote(c.Val().ExactString()) -- if err != nil { -- return err -- } -- if semver.Prerelease(hardcodedVersion) != "" { -- return fmt.Errorf("unexpected pre-release for hardcoded version: %s", hardcodedVersion) -- } -- // Don't worry about pre-release tags and expect that there is no build -- // suffix. -- version = strings.TrimSuffix(version, semver.Prerelease(version)) -- if hardcodedVersion != version { -- return fmt.Errorf("expected version to be %s, got %s", *versionFlag, hardcodedVersion) -- } -- return nil --} -- -func validateGoModFile(goplsDir string) error { - filename := filepath.Join(goplsDir, "go.mod") - data, err := os.ReadFile(filename) @@ -142482,304 +151797,3 @@ diff -urN a/gopls/release/release.go b/gopls/release/release.go - } - return nil -} -diff -urN a/gopls/test/debug/debug_test.go b/gopls/test/debug/debug_test.go ---- a/gopls/test/debug/debug_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/test/debug/debug_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,153 +0,0 @@ --// Copyright 2020 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package debug_test -- --// Provide 'static type checking' of the templates. This guards against changes in various --// gopls datastructures causing template execution to fail. The checking is done by --// the github.com/jba/templatecheck package. Before that is run, the test checks that --// its list of templates and their arguments corresponds to the arguments in --// calls to render(). The test assumes that all uses of templates are done through render(). -- --import ( -- "go/ast" -- "html/template" -- "os" -- "runtime" -- "sort" -- "strings" -- "testing" -- -- "github.com/jba/templatecheck" -- "golang.org/x/tools/go/packages" -- "golang.org/x/tools/gopls/internal/lsp/cache" -- "golang.org/x/tools/gopls/internal/lsp/debug" -- "golang.org/x/tools/internal/testenv" --) -- --var templates = map[string]struct { -- tmpl *template.Template -- data interface{} // a value of the needed type --}{ -- "MainTmpl": {debug.MainTmpl, &debug.Instance{}}, -- "DebugTmpl": {debug.DebugTmpl, nil}, -- "RPCTmpl": {debug.RPCTmpl, &debug.Rpcs{}}, -- "TraceTmpl": {debug.TraceTmpl, debug.TraceResults{}}, -- "CacheTmpl": {debug.CacheTmpl, &cache.Cache{}}, -- "SessionTmpl": {debug.SessionTmpl, &cache.Session{}}, -- "ViewTmpl": {debug.ViewTmpl, &cache.View{}}, -- "ClientTmpl": {debug.ClientTmpl, &debug.Client{}}, -- "ServerTmpl": {debug.ServerTmpl, &debug.Server{}}, -- "FileTmpl": {debug.FileTmpl, &cache.Overlay{}}, -- "InfoTmpl": {debug.InfoTmpl, "something"}, -- "MemoryTmpl": {debug.MemoryTmpl, runtime.MemStats{}}, -- "AnalysisTmpl": {debug.AnalysisTmpl, new(debug.State).Analysis()}, --} -- --func TestTemplates(t *testing.T) { -- testenv.NeedsGoPackages(t) -- testenv.NeedsLocalXTools(t) -- -- cfg := &packages.Config{ -- Mode: packages.NeedTypes | packages.NeedSyntax | packages.NeedTypesInfo, -- } -- cfg.Env = os.Environ() -- cfg.Env = append(cfg.Env, -- "GOPACKAGESDRIVER=off", -- "GOWORK=off", // necessary for -mod=mod below -- "GOFLAGS=-mod=mod", -- ) -- -- pkgs, err := packages.Load(cfg, "golang.org/x/tools/gopls/internal/lsp/debug") -- if err != nil { -- t.Fatal(err) -- } -- if len(pkgs) != 1 { -- t.Fatalf("expected a single package, but got %d", len(pkgs)) -- } -- p := pkgs[0] -- if len(p.Errors) != 0 { -- t.Fatalf("compiler error, e.g. %v", p.Errors[0]) -- } -- // find the calls to render in serve.go -- tree := treeOf(p, "serve.go") -- if tree == nil { -- t.Fatalf("found no syntax tree for %s", "serve.go") -- } -- renders := callsOf(p, tree, "render") -- if len(renders) == 0 { -- t.Fatalf("found no calls to render") -- } -- var found = make(map[string]bool) -- for _, r := range renders { -- if len(r.Args) != 2 { -- // template, func -- t.Fatalf("got %d args, expected 2", len(r.Args)) -- } -- t0, ok := p.TypesInfo.Types[r.Args[0]] -- if !ok || !t0.IsValue() || t0.Type.String() != "*html/template.Template" { -- t.Fatalf("no type info for template") -- } -- if id, ok := r.Args[0].(*ast.Ident); !ok { -- t.Errorf("expected *ast.Ident, got %T", r.Args[0]) -- } else { -- found[id.Name] = true -- } -- } -- // make sure found and templates have the same templates -- for k := range found { -- if _, ok := templates[k]; !ok { -- t.Errorf("code has template %s, but test does not", k) -- } -- } -- for k := range templates { -- if _, ok := found[k]; !ok { -- t.Errorf("test has template %s, code does not", k) -- } -- } -- // now check all the known templates, in alphabetic order, for determinacy -- keys := []string{} -- for k := range templates { -- keys = append(keys, k) -- } -- sort.Strings(keys) -- for _, k := range keys { -- v := templates[k] -- // the FuncMap is an annoyance; should not be necessary -- if err := templatecheck.CheckHTML(v.tmpl, v.data); err != nil { -- t.Errorf("%s: %v", k, err) -- continue -- } -- t.Logf("%s ok", k) -- } --} -- --func callsOf(p *packages.Package, tree *ast.File, name string) []*ast.CallExpr { -- var ans []*ast.CallExpr -- f := func(n ast.Node) bool { -- x, ok := n.(*ast.CallExpr) -- if !ok { -- return true -- } -- if y, ok := x.Fun.(*ast.Ident); ok { -- if y.Name == name { -- ans = append(ans, x) -- } -- } -- return true -- } -- ast.Inspect(tree, f) -- return ans --} -- --func treeOf(p *packages.Package, fname string) *ast.File { -- for _, tree := range p.Syntax { -- loc := tree.Package -- pos := p.Fset.PositionFor(loc, false) -- if strings.HasSuffix(pos.Filename, fname) { -- return tree -- } -- } -- return nil --} -diff -urN a/gopls/test/json_test.go b/gopls/test/json_test.go ---- a/gopls/test/json_test.go 2000-01-01 00:00:00.000000000 -0000 -+++ b/gopls/test/json_test.go 1970-01-01 00:00:00.000000000 +0000 -@@ -1,140 +0,0 @@ --// Copyright 2021 The Go Authors. All rights reserved. --// Use of this source code is governed by a BSD-style --// license that can be found in the LICENSE file. -- --package gopls_test -- --import ( -- "encoding/json" -- "fmt" -- "regexp" -- "strings" -- "testing" -- -- "github.com/google/go-cmp/cmp" -- "golang.org/x/tools/gopls/internal/lsp/protocol" --) -- --// verify that type errors in Initialize lsp messages don't cause --// any other unmarshalling errors. The code looks at single values and the --// first component of array values. Each occurrence is replaced by something --// of a different type, the resulting string unmarshalled, and compared to --// the unmarshalling of the unchanged strings. The test passes if there is no --// more than a single difference reported. That is, if changing a single value --// in the message changes no more than a single value in the unmarshalled struct, --// it is safe to ignore *json.UnmarshalTypeError. -- --// strings are changed to numbers or bools (true) --// bools are changed to numbers or strings --// numbers are changed to strings or bools -- --// a recent Initialize message taken from a log (at some point --// some field incompatibly changed from bool to int32) --const input = `{"processId":46408,"clientInfo":{"name":"Visual Studio Code - Insiders","version":"1.76.0-insider"},"locale":"en-us","rootPath":"/Users/pjw/hakim","rootUri":"file:///Users/pjw/hakim","capabilities":{"workspace":{"applyEdit":true,"workspaceEdit":{"documentChanges":true,"resourceOperations":["create","rename","delete"],"failureHandling":"textOnlyTransactional","normalizesLineEndings":true,"changeAnnotationSupport":{"groupsOnLabel":true}},"configuration":true,"didChangeWatchedFiles":{"dynamicRegistration":true,"relativePatternSupport":true},"symbol":{"dynamicRegistration":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]},"tagSupport":{"valueSet":[1]},"resolveSupport":{"properties":["location.range"]}},"codeLens":{"refreshSupport":true},"executeCommand":{"dynamicRegistration":true},"didChangeConfiguration":{"dynamicRegistration":true},"workspaceFolders":true,"semanticTokens":{"refreshSupport":true},"fileOperations":{"dynamicRegistration":true,"didCreate":true,"didRename":true,"didDelete":true,"willCreate":true,"willRename":true,"willDelete":true},"inlineValue":{"refreshSupport":true},"inlayHint":{"refreshSupport":true},"diagnostics":{"refreshSupport":true}},"textDocument":{"publishDiagnostics":{"relatedInformation":true,"versionSupport":false,"tagSupport":{"valueSet":[1,2]},"codeDescriptionSupport":true,"dataSupport":true},"synchronization":{"dynamicRegistration":true,"willSave":true,"willSaveWaitUntil":true,"didSave":true},"completion":{"dynamicRegistration":true,"contextSupport":true,"completionItem":{"snippetSupport":true,"commitCharactersSupport":true,"documentationFormat":["markdown","plaintext"],"deprecatedSupport":true,"preselectSupport":true,"tagSupport":{"valueSet":[1]},"insertReplaceSupport":true,"resolveSupport":{"properties":["documentation","detail","additionalTextEdits"]},"insertTextModeSupport":{"valueSet":[1,2]},"labelDetailsSupport":true},"insertTextMode":2,"completionItemKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25]},"completionList":{"itemDefaults":["commitCharacters","editRange","insertTextFormat","insertTextMode"]}},"hover":{"dynamicRegistration":true,"contentFormat":["markdown","plaintext"]},"signatureHelp":{"dynamicRegistration":true,"signatureInformation":{"documentationFormat":["markdown","plaintext"],"parameterInformation":{"labelOffsetSupport":true},"activeParameterSupport":true},"contextSupport":true},"definition":{"dynamicRegistration":true,"linkSupport":true},"references":{"dynamicRegistration":true},"documentHighlight":{"dynamicRegistration":true},"documentSymbol":{"dynamicRegistration":true,"symbolKind":{"valueSet":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]},"hierarchicalDocumentSymbolSupport":true,"tagSupport":{"valueSet":[1]},"labelSupport":true},"codeAction":{"dynamicRegistration":true,"isPreferredSupport":true,"disabledSupport":true,"dataSupport":true,"resolveSupport":{"properties":["edit"]},"codeActionLiteralSupport":{"codeActionKind":{"valueSet":["","quickfix","refactor","refactor.extract","refactor.inline","refactor.rewrite","source","source.organizeImports"]}},"honorsChangeAnnotations":false},"codeLens":{"dynamicRegistration":true},"formatting":{"dynamicRegistration":true},"rangeFormatting":{"dynamicRegistration":true},"onTypeFormatting":{"dynamicRegistration":true},"rename":{"dynamicRegistration":true,"prepareSupport":true,"prepareSupportDefaultBehavior":1,"honorsChangeAnnotations":true},"documentLink":{"dynamicRegistration":true,"tooltipSupport":true},"typeDefinition":{"dynamicRegistration":true,"linkSupport":true},"implementation":{"dynamicRegistration":true,"linkSupport":true},"colorProvider":{"dynamicRegistration":true},"foldingRange":{"dynamicRegistration":true,"rangeLimit":5000,"lineFoldingOnly":true,"foldingRangeKind":{"valueSet":["comment","imports","region"]},"foldingRange":{"collapsedText":false}},"declaration":{"dynamicRegistration":true,"linkSupport":true},"selectionRange":{"dynamicRegistration":true},"callHierarchy":{"dynamicRegistration":true},"semanticTokens":{"dynamicRegistration":true,"tokenTypes":["namespace","type","class","enum","interface","struct","typeParameter","parameter","variable","property","enumMember","event","function","method","macro","keyword","modifier","comment","string","number","regexp","operator","decorator"],"tokenModifiers":["declaration","definition","readonly","static","deprecated","abstract","async","modification","documentation","defaultLibrary"],"formats":["relative"],"requests":{"range":true,"full":{"delta":true}},"multilineTokenSupport":false,"overlappingTokenSupport":false,"serverCancelSupport":true,"augmentsSyntaxTokens":true},"linkedEditingRange":{"dynamicRegistration":true},"typeHierarchy":{"dynamicRegistration":true},"inlineValue":{"dynamicRegistration":true},"inlayHint":{"dynamicRegistration":true,"resolveSupport":{"properties":["tooltip","textEdits","label.tooltip","label.location","label.command"]}},"diagnostic":{"dynamicRegistration":true,"relatedDocumentSupport":false}},"window":{"showMessage":{"messageActionItem":{"additionalPropertiesSupport":true}},"showDocument":{"support":true},"workDoneProgress":true},"general":{"staleRequestSupport":{"cancel":true,"retryOnContentModified":["textDocument/semanticTokens/full","textDocument/semanticTokens/range","textDocument/semanticTokens/full/delta"]},"regularExpressions":{"engine":"ECMAScript","version":"ES2020"},"markdown":{"parser":"marked","version":"1.1.0"},"positionEncodings":["utf-16"]},"notebookDocument":{"synchronization":{"dynamicRegistration":true,"executionSummarySupport":true}}},"initializationOptions":{"usePlaceholders":true,"completionDocumentation":true,"verboseOutput":false,"build.directoryFilters":["-foof","-internal/lsp/protocol/typescript"],"codelenses":{"reference":true,"gc_details":true},"analyses":{"fillstruct":true,"staticcheck":true,"unusedparams":false,"composites":false},"semanticTokens":true,"noSemanticString":true,"noSemanticNumber":true,"templateExtensions":["tmpl","gotmpl"],"ui.completion.matcher":"Fuzzy","ui.inlayhint.hints":{"assignVariableTypes":false,"compositeLiteralFields":false,"compositeLiteralTypes":false,"constantValues":false,"functionTypeParameters":false,"parameterNames":false,"rangeVariableTypes":false},"ui.vulncheck":"Off","allExperiments":true},"trace":"off","workspaceFolders":[{"uri":"file:///Users/pjw/hakim","name":"hakim"}]}` -- --type DiffReporter struct { -- path cmp.Path -- diffs []string --} -- --func (r *DiffReporter) PushStep(ps cmp.PathStep) { -- r.path = append(r.path, ps) --} -- --func (r *DiffReporter) Report(rs cmp.Result) { -- if !rs.Equal() { -- vx, vy := r.path.Last().Values() -- r.diffs = append(r.diffs, fmt.Sprintf("%#v:\n\t-: %+v\n\t+: %+v\n", r.path, vx, vy)) -- } --} -- --func (r *DiffReporter) PopStep() { -- r.path = r.path[:len(r.path)-1] --} -- --func (r *DiffReporter) String() string { -- return strings.Join(r.diffs, "\n") --} -- --func TestStringChanges(t *testing.T) { -- // string as value -- stringLeaf := regexp.MustCompile(`:("[^"]*")`) -- leafs := stringLeaf.FindAllStringSubmatchIndex(input, -1) -- allDeltas(t, leafs, "23", "true") -- // string as first element of array -- stringArray := regexp.MustCompile(`[[]("[^"]*")`) -- arrays := stringArray.FindAllStringSubmatchIndex(input, -1) -- allDeltas(t, arrays, "23", "true") --} -- --func TestBoolChanges(t *testing.T) { -- boolLeaf := regexp.MustCompile(`:(true|false)(,|})`) -- leafs := boolLeaf.FindAllStringSubmatchIndex(input, -1) -- allDeltas(t, leafs, "23", `"xx"`) -- boolArray := regexp.MustCompile(`:[[](true|false)(,|])`) -- arrays := boolArray.FindAllStringSubmatchIndex(input, -1) -- allDeltas(t, arrays, "23", `"xx"`) --} -- --func TestNumberChanges(t *testing.T) { -- numLeaf := regexp.MustCompile(`:(\d+)(,|})`) -- leafs := numLeaf.FindAllStringSubmatchIndex(input, -1) -- allDeltas(t, leafs, "true", `"xx"`) -- numArray := regexp.MustCompile(`:[[](\d+)(,|])`) -- arrays := numArray.FindAllStringSubmatchIndex(input, -1) -- allDeltas(t, arrays, "true", `"xx"`) --} -- --// v is a set of matches. check that substituting any repl never --// creates more than 1 unmarshaling error --func allDeltas(t *testing.T, v [][]int, repls ...string) { -- t.Helper() -- for _, repl := range repls { -- for i, x := range v { -- err := tryChange(x[2], x[3], repl) -- if err != nil { -- t.Errorf("%d:%q %v", i, input[x[2]:x[3]], err) -- } -- } -- } --} -- --func tryChange(start, end int, repl string) error { -- var p, q protocol.ParamInitialize -- mod := input[:start] + repl + input[end:] -- excerpt := func() (string, string) { -- a := start - 5 -- if a < 0 { -- a = 0 -- } -- b := end + 5 -- if b > len(input) { -- // trusting repl to be no longer than what it replaces -- b = len(input) -- } -- ma := input[a:b] -- mb := mod[a:b] -- return ma, mb -- } -- -- if err := json.Unmarshal([]byte(input), &p); err != nil { -- return fmt.Errorf("%s %v", repl, err) -- } -- switch err := json.Unmarshal([]byte(mod), &q).(type) { -- case nil: //ok -- case *json.UnmarshalTypeError: -- break -- case *protocol.UnmarshalError: -- return nil // cmp.Diff produces several diffs for custom unmrshalers -- default: -- return fmt.Errorf("%T unexpected unmarshal error", err) -- } -- -- var r DiffReporter -- cmp.Diff(p, q, cmp.Reporter(&r)) -- if len(r.diffs) > 1 { // 0 is possible, e.g., for interface{} -- ma, mb := excerpt() -- return fmt.Errorf("got %d diffs for %q\n%s\n%s", len(r.diffs), repl, ma, mb) -- } -- return nil --} diff --git a/third_party/org_golang_x_tools-gazelle.patch b/third_party/org_golang_x_tools-gazelle.patch index d99ae0d222..593c224450 100644 --- a/third_party/org_golang_x_tools-gazelle.patch +++ b/third_party/org_golang_x_tools-gazelle.patch @@ -71,7 +71,7 @@ diff -urN b/blog/BUILD.bazel c/blog/BUILD.bazel diff -urN b/cmd/auth/authtest/BUILD.bazel c/cmd/auth/authtest/BUILD.bazel --- b/cmd/auth/authtest/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/cmd/auth/authtest/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,15 @@ +@@ -0,0 +1,14 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( @@ -79,7 +79,6 @@ diff -urN b/cmd/auth/authtest/BUILD.bazel c/cmd/auth/authtest/BUILD.bazel + srcs = ["authtest.go"], + importpath = "golang.org/x/tools/cmd/auth/authtest", + visibility = ["//visibility:private"], -+ deps = ["@org_golang_x_sys//execabs:go_default_library"], +) + +go_binary( @@ -108,7 +107,7 @@ diff -urN b/cmd/auth/cookieauth/BUILD.bazel c/cmd/auth/cookieauth/BUILD.bazel diff -urN b/cmd/auth/gitauth/BUILD.bazel c/cmd/auth/gitauth/BUILD.bazel --- b/cmd/auth/gitauth/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/cmd/auth/gitauth/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,15 @@ +@@ -0,0 +1,14 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( @@ -116,7 +115,6 @@ diff -urN b/cmd/auth/gitauth/BUILD.bazel c/cmd/auth/gitauth/BUILD.bazel + srcs = ["gitauth.go"], + importpath = "golang.org/x/tools/cmd/auth/gitauth", + visibility = ["//visibility:private"], -+ deps = ["@org_golang_x_sys//execabs:go_default_library"], +) + +go_binary( @@ -178,7 +176,7 @@ diff -urN b/cmd/benchcmp/BUILD.bazel c/cmd/benchcmp/BUILD.bazel diff -urN b/cmd/bisect/BUILD.bazel c/cmd/bisect/BUILD.bazel --- b/cmd/bisect/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/cmd/bisect/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,33 @@ +@@ -0,0 +1,32 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test") + +go_library( @@ -207,7 +205,6 @@ diff -urN b/cmd/bisect/BUILD.bazel c/cmd/bisect/BUILD.bazel + embed = [":bisect_lib"], + deps = [ + "//internal/bisect", -+ "//internal/compat", + "//internal/diffp", + "//txtar", + ], @@ -382,7 +379,7 @@ diff -urN b/cmd/callgraph/testdata/src/pkg/BUILD.bazel c/cmd/callgraph/testdata/ diff -urN b/cmd/compilebench/BUILD.bazel c/cmd/compilebench/BUILD.bazel --- b/cmd/compilebench/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/cmd/compilebench/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,15 @@ +@@ -0,0 +1,14 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( @@ -390,7 +387,6 @@ diff -urN b/cmd/compilebench/BUILD.bazel c/cmd/compilebench/BUILD.bazel + srcs = ["main.go"], + importpath = "golang.org/x/tools/cmd/compilebench", + visibility = ["//visibility:private"], -+ deps = ["@org_golang_x_sys//execabs:go_default_library"], +) + +go_binary( @@ -398,6 +394,47 @@ diff -urN b/cmd/compilebench/BUILD.bazel c/cmd/compilebench/BUILD.bazel + embed = [":compilebench_lib"], + visibility = ["//visibility:public"], +) +diff -urN b/cmd/deadcode/BUILD.bazel c/cmd/deadcode/BUILD.bazel +--- b/cmd/deadcode/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 ++++ c/cmd/deadcode/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 +@@ -0,0 +1,37 @@ ++load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test") ++ ++go_library( ++ name = "deadcode_lib", ++ srcs = [ ++ "deadcode.go", ++ "doc.go", ++ ], ++ embedsrcs = ["doc.go"], ++ importpath = "golang.org/x/tools/cmd/deadcode", ++ visibility = ["//visibility:private"], ++ deps = [ ++ "//go/callgraph", ++ "//go/callgraph/rta", ++ "//go/packages", ++ "//go/ssa", ++ "//go/ssa/ssautil", ++ "//internal/typesinternal", ++ "@org_golang_x_telemetry//:go_default_library", ++ ], ++) ++ ++go_binary( ++ name = "deadcode", ++ embed = [":deadcode_lib"], ++ visibility = ["//visibility:public"], ++) ++ ++go_test( ++ name = "deadcode_test", ++ srcs = ["deadcode_test.go"], ++ data = glob(["testdata/**"]), ++ deps = [ ++ "//internal/testenv", ++ "//txtar", ++ ], ++) diff -urN b/cmd/digraph/BUILD.bazel c/cmd/digraph/BUILD.bazel --- b/cmd/digraph/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/cmd/digraph/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 @@ -429,7 +466,7 @@ diff -urN b/cmd/digraph/BUILD.bazel c/cmd/digraph/BUILD.bazel diff -urN b/cmd/eg/BUILD.bazel c/cmd/eg/BUILD.bazel --- b/cmd/eg/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/cmd/eg/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,19 @@ +@@ -0,0 +1,18 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( @@ -440,7 +477,6 @@ diff -urN b/cmd/eg/BUILD.bazel c/cmd/eg/BUILD.bazel + deps = [ + "//go/packages", + "//refactor/eg", -+ "@org_golang_x_sys//execabs:go_default_library", + ], +) + @@ -477,7 +513,7 @@ diff -urN b/cmd/file2fuzz/BUILD.bazel c/cmd/file2fuzz/BUILD.bazel diff -urN b/cmd/fiximports/BUILD.bazel c/cmd/fiximports/BUILD.bazel --- b/cmd/fiximports/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/cmd/fiximports/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,63 @@ +@@ -0,0 +1,62 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test") + +go_library( @@ -485,7 +521,6 @@ diff -urN b/cmd/fiximports/BUILD.bazel c/cmd/fiximports/BUILD.bazel + srcs = ["main.go"], + importpath = "golang.org/x/tools/cmd/fiximports", + visibility = ["//visibility:private"], -+ deps = ["@org_golang_x_sys//execabs:go_default_library"], +) + +go_binary( @@ -685,106 +720,10 @@ diff -urN b/cmd/fiximports/testdata/src/titanic.biz/foo/BUILD.bazel c/cmd/fiximp + actual = ":foo", + visibility = ["//visibility:public"], +) -diff -urN b/cmd/getgo/BUILD.bazel c/cmd/getgo/BUILD.bazel ---- b/cmd/getgo/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ c/cmd/getgo/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,74 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test") -+ -+go_library( -+ name = "getgo_lib", -+ srcs = [ -+ "download.go", -+ "main.go", -+ "path.go", -+ "steps.go", -+ "system.go", -+ "system_unix.go", -+ "system_windows.go", -+ ], -+ importpath = "golang.org/x/tools/cmd/getgo", -+ visibility = ["//visibility:private"], -+ deps = select({ -+ "@io_bazel_rules_go//go/platform:aix": [ -+ "@org_golang_x_sys//execabs:go_default_library", -+ ], -+ "@io_bazel_rules_go//go/platform:android": [ -+ "@org_golang_x_sys//execabs:go_default_library", -+ ], -+ "@io_bazel_rules_go//go/platform:darwin": [ -+ "@org_golang_x_sys//execabs:go_default_library", -+ ], -+ "@io_bazel_rules_go//go/platform:dragonfly": [ -+ "@org_golang_x_sys//execabs:go_default_library", -+ ], -+ "@io_bazel_rules_go//go/platform:freebsd": [ -+ "@org_golang_x_sys//execabs:go_default_library", -+ ], -+ "@io_bazel_rules_go//go/platform:illumos": [ -+ "@org_golang_x_sys//execabs:go_default_library", -+ ], -+ "@io_bazel_rules_go//go/platform:ios": [ -+ "@org_golang_x_sys//execabs:go_default_library", -+ ], -+ "@io_bazel_rules_go//go/platform:js": [ -+ "@org_golang_x_sys//execabs:go_default_library", -+ ], -+ "@io_bazel_rules_go//go/platform:linux": [ -+ "@org_golang_x_sys//execabs:go_default_library", -+ ], -+ "@io_bazel_rules_go//go/platform:netbsd": [ -+ "@org_golang_x_sys//execabs:go_default_library", -+ ], -+ "@io_bazel_rules_go//go/platform:openbsd": [ -+ "@org_golang_x_sys//execabs:go_default_library", -+ ], -+ "@io_bazel_rules_go//go/platform:solaris": [ -+ "@org_golang_x_sys//execabs:go_default_library", -+ ], -+ "@io_bazel_rules_go//go/platform:windows": [ -+ "@org_golang_x_sys//execabs:go_default_library", -+ ], -+ "//conditions:default": [], -+ }), -+) -+ -+go_binary( -+ name = "getgo", -+ embed = [":getgo_lib"], -+ visibility = ["//visibility:public"], -+) -+ -+go_test( -+ name = "getgo_test", -+ srcs = [ -+ "download_test.go", -+ "main_test.go", -+ "path_test.go", -+ ], -+ embed = [":getgo_lib"], -+) -diff -urN b/cmd/getgo/server/BUILD.bazel c/cmd/getgo/server/BUILD.bazel ---- b/cmd/getgo/server/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ c/cmd/getgo/server/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,14 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") -+ -+go_library( -+ name = "server_lib", -+ srcs = ["main.go"], -+ importpath = "golang.org/x/tools/cmd/getgo/server", -+ visibility = ["//visibility:private"], -+) -+ -+go_binary( -+ name = "server", -+ embed = [":server_lib"], -+ visibility = ["//visibility:public"], -+) diff -urN b/cmd/go-contrib-init/BUILD.bazel c/cmd/go-contrib-init/BUILD.bazel --- b/cmd/go-contrib-init/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/cmd/go-contrib-init/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,21 @@ +@@ -0,0 +1,20 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test") + +go_library( @@ -792,7 +731,6 @@ diff -urN b/cmd/go-contrib-init/BUILD.bazel c/cmd/go-contrib-init/BUILD.bazel + srcs = ["contrib.go"], + importpath = "golang.org/x/tools/cmd/go-contrib-init", + visibility = ["//visibility:private"], -+ deps = ["@org_golang_x_sys//execabs:go_default_library"], +) + +go_binary( @@ -809,7 +747,7 @@ diff -urN b/cmd/go-contrib-init/BUILD.bazel c/cmd/go-contrib-init/BUILD.bazel diff -urN b/cmd/godex/BUILD.bazel c/cmd/godex/BUILD.bazel --- b/cmd/godex/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/cmd/godex/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,24 @@ +@@ -0,0 +1,25 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( @@ -827,6 +765,7 @@ diff -urN b/cmd/godex/BUILD.bazel c/cmd/godex/BUILD.bazel + ], + importpath = "golang.org/x/tools/cmd/godex", + visibility = ["//visibility:private"], ++ deps = ["//internal/aliases"], +) + +go_binary( @@ -837,7 +776,7 @@ diff -urN b/cmd/godex/BUILD.bazel c/cmd/godex/BUILD.bazel diff -urN b/cmd/godoc/BUILD.bazel c/cmd/godoc/BUILD.bazel --- b/cmd/godoc/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/cmd/godoc/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,41 @@ +@@ -0,0 +1,40 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test") + +go_library( @@ -860,7 +799,6 @@ diff -urN b/cmd/godoc/BUILD.bazel c/cmd/godoc/BUILD.bazel + "//godoc/vfs/zipfs", + "//internal/gocommand", + "//playground", -+ "@org_golang_x_sys//execabs:go_default_library", + ], +) + @@ -882,7 +820,7 @@ diff -urN b/cmd/godoc/BUILD.bazel c/cmd/godoc/BUILD.bazel diff -urN b/cmd/goimports/BUILD.bazel c/cmd/goimports/BUILD.bazel --- b/cmd/goimports/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/cmd/goimports/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,23 @@ +@@ -0,0 +1,22 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( @@ -897,7 +835,6 @@ diff -urN b/cmd/goimports/BUILD.bazel c/cmd/goimports/BUILD.bazel + deps = [ + "//internal/gocommand", + "//internal/imports", -+ "@org_golang_x_sys//execabs:go_default_library", + ], +) + @@ -987,454 +924,69 @@ diff -urN b/cmd/gorename/BUILD.bazel c/cmd/gorename/BUILD.bazel +) + +go_test( -+ name = "gorename_test", -+ srcs = ["gorename_test.go"], -+ deps = ["//internal/testenv"], -+) -diff -urN b/cmd/gotype/BUILD.bazel c/cmd/gotype/BUILD.bazel ---- b/cmd/gotype/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ c/cmd/gotype/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,18 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") -+ -+go_library( -+ name = "gotype_lib", -+ srcs = [ -+ "gotype.go", -+ "sizesFor18.go", -+ "sizesFor19.go", -+ ], -+ importpath = "golang.org/x/tools/cmd/gotype", -+ visibility = ["//visibility:private"], -+) -+ -+go_binary( -+ name = "gotype", -+ embed = [":gotype_lib"], -+ visibility = ["//visibility:public"], -+) -diff -urN b/cmd/goyacc/BUILD.bazel c/cmd/goyacc/BUILD.bazel ---- b/cmd/goyacc/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ c/cmd/goyacc/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,17 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") -+ -+go_library( -+ name = "goyacc_lib", -+ srcs = [ -+ "doc.go", -+ "yacc.go", -+ ], -+ importpath = "golang.org/x/tools/cmd/goyacc", -+ visibility = ["//visibility:private"], -+) -+ -+go_binary( -+ name = "goyacc", -+ embed = [":goyacc_lib"], -+ visibility = ["//visibility:public"], -+) -diff -urN b/cmd/goyacc/testdata/expr/BUILD.bazel c/cmd/goyacc/testdata/expr/BUILD.bazel ---- b/cmd/goyacc/testdata/expr/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ c/cmd/goyacc/testdata/expr/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,14 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_library") -+ -+go_library( -+ name = "expr_lib", -+ srcs = ["main.go"], -+ importpath = "golang.org/x/tools/cmd/goyacc/testdata/expr", -+ visibility = ["//visibility:public"], -+) -+ -+alias( -+ name = "go_default_library", -+ actual = ":expr_lib", -+ visibility = ["//visibility:public"], -+) -diff -urN b/cmd/guru/BUILD.bazel c/cmd/guru/BUILD.bazel ---- b/cmd/guru/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ c/cmd/guru/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,45 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test") -+ -+go_library( -+ name = "guru_lib", -+ srcs = [ -+ "definition.go", -+ "describe.go", -+ "freevars.go", -+ "guru.go", -+ "implements.go", -+ "isAlias18.go", -+ "isAlias19.go", -+ "main.go", -+ "pos.go", -+ "referrers.go", -+ "what.go", -+ ], -+ importpath = "golang.org/x/tools/cmd/guru", -+ visibility = ["//visibility:private"], -+ deps = [ -+ "//cmd/guru/serial", -+ "//go/ast/astutil", -+ "//go/buildutil", -+ "//go/loader", -+ "//go/types/typeutil", -+ "//imports", -+ "//refactor/importgraph", -+ ], -+) -+ -+go_binary( -+ name = "guru", -+ embed = [":guru_lib"], -+ visibility = ["//visibility:public"], -+) -+ -+go_test( -+ name = "guru_test", -+ srcs = [ -+ "guru_test.go", -+ "unit_test.go", -+ ], -+ embed = [":guru_lib"], -+ deps = ["//internal/testenv"], -+) -diff -urN b/cmd/guru/serial/BUILD.bazel c/cmd/guru/serial/BUILD.bazel ---- b/cmd/guru/serial/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ c/cmd/guru/serial/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,14 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_library") -+ -+go_library( -+ name = "serial", -+ srcs = ["serial.go"], -+ importpath = "golang.org/x/tools/cmd/guru/serial", -+ visibility = ["//visibility:public"], -+) -+ -+alias( -+ name = "go_default_library", -+ actual = ":serial", -+ visibility = ["//visibility:public"], -+) -diff -urN b/cmd/guru/testdata/src/alias/BUILD.bazel c/cmd/guru/testdata/src/alias/BUILD.bazel ---- b/cmd/guru/testdata/src/alias/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ c/cmd/guru/testdata/src/alias/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,14 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_library") -+ -+go_library( -+ name = "alias", -+ srcs = ["alias.go"], -+ importpath = "golang.org/x/tools/cmd/guru/testdata/src/alias", -+ visibility = ["//visibility:public"], -+) -+ -+alias( -+ name = "go_default_library", -+ actual = ":alias", -+ visibility = ["//visibility:public"], -+) -diff -urN b/cmd/guru/testdata/src/definition-json/BUILD.bazel c/cmd/guru/testdata/src/definition-json/BUILD.bazel ---- b/cmd/guru/testdata/src/definition-json/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ c/cmd/guru/testdata/src/definition-json/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,17 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_library") -+ -+go_library( -+ name = "definition-json", -+ srcs = [ -+ "main.go", -+ "type.go", -+ ], -+ importpath = "golang.org/x/tools/cmd/guru/testdata/src/definition-json", -+ visibility = ["//visibility:public"], -+) -+ -+alias( -+ name = "go_default_library", -+ actual = ":definition-json", -+ visibility = ["//visibility:public"], -+) -diff -urN b/cmd/guru/testdata/src/describe/BUILD.bazel c/cmd/guru/testdata/src/describe/BUILD.bazel ---- b/cmd/guru/testdata/src/describe/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ c/cmd/guru/testdata/src/describe/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,14 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_library") -+ -+go_library( -+ name = "describe", -+ srcs = ["main.go"], -+ importpath = "golang.org/x/tools/cmd/guru/testdata/src/describe", -+ visibility = ["//visibility:public"], -+) -+ -+alias( -+ name = "go_default_library", -+ actual = ":describe", -+ visibility = ["//visibility:public"], -+) -diff -urN b/cmd/guru/testdata/src/describe-json/BUILD.bazel c/cmd/guru/testdata/src/describe-json/BUILD.bazel ---- b/cmd/guru/testdata/src/describe-json/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ c/cmd/guru/testdata/src/describe-json/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,14 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_library") -+ -+go_library( -+ name = "describe-json", -+ srcs = ["main.go"], -+ importpath = "golang.org/x/tools/cmd/guru/testdata/src/describe-json", -+ visibility = ["//visibility:public"], -+) -+ -+alias( -+ name = "go_default_library", -+ actual = ":describe-json", -+ visibility = ["//visibility:public"], -+) -diff -urN b/cmd/guru/testdata/src/freevars/BUILD.bazel c/cmd/guru/testdata/src/freevars/BUILD.bazel ---- b/cmd/guru/testdata/src/freevars/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ c/cmd/guru/testdata/src/freevars/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,14 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") -+ -+go_library( -+ name = "freevars_lib", -+ srcs = ["main.go"], -+ importpath = "golang.org/x/tools/cmd/guru/testdata/src/freevars", -+ visibility = ["//visibility:private"], -+) -+ -+go_binary( -+ name = "freevars", -+ embed = [":freevars_lib"], -+ visibility = ["//visibility:public"], -+) -diff -urN b/cmd/guru/testdata/src/implements/BUILD.bazel c/cmd/guru/testdata/src/implements/BUILD.bazel ---- b/cmd/guru/testdata/src/implements/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ c/cmd/guru/testdata/src/implements/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,14 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") -+ -+go_library( -+ name = "implements_lib", -+ srcs = ["main.go"], -+ importpath = "golang.org/x/tools/cmd/guru/testdata/src/implements", -+ visibility = ["//visibility:private"], -+) -+ -+go_binary( -+ name = "implements", -+ embed = [":implements_lib"], -+ visibility = ["//visibility:public"], -+) -diff -urN b/cmd/guru/testdata/src/implements-json/BUILD.bazel c/cmd/guru/testdata/src/implements-json/BUILD.bazel ---- b/cmd/guru/testdata/src/implements-json/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ c/cmd/guru/testdata/src/implements-json/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,14 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") -+ -+go_library( -+ name = "implements-json_lib", -+ srcs = ["main.go"], -+ importpath = "golang.org/x/tools/cmd/guru/testdata/src/implements-json", -+ visibility = ["//visibility:private"], -+) -+ -+go_binary( -+ name = "implements-json", -+ embed = [":implements-json_lib"], -+ visibility = ["//visibility:public"], -+) -diff -urN b/cmd/guru/testdata/src/implements-methods/BUILD.bazel c/cmd/guru/testdata/src/implements-methods/BUILD.bazel ---- b/cmd/guru/testdata/src/implements-methods/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ c/cmd/guru/testdata/src/implements-methods/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,14 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") -+ -+go_library( -+ name = "implements-methods_lib", -+ srcs = ["main.go"], -+ importpath = "golang.org/x/tools/cmd/guru/testdata/src/implements-methods", -+ visibility = ["//visibility:private"], -+) -+ -+go_binary( -+ name = "implements-methods", -+ embed = [":implements-methods_lib"], -+ visibility = ["//visibility:public"], -+) -diff -urN b/cmd/guru/testdata/src/implements-methods-json/BUILD.bazel c/cmd/guru/testdata/src/implements-methods-json/BUILD.bazel ---- b/cmd/guru/testdata/src/implements-methods-json/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ c/cmd/guru/testdata/src/implements-methods-json/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,14 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") -+ -+go_library( -+ name = "implements-methods-json_lib", -+ srcs = ["main.go"], -+ importpath = "golang.org/x/tools/cmd/guru/testdata/src/implements-methods-json", -+ visibility = ["//visibility:private"], -+) -+ -+go_binary( -+ name = "implements-methods-json", -+ embed = [":implements-methods-json_lib"], -+ visibility = ["//visibility:public"], -+) -diff -urN b/cmd/guru/testdata/src/imports/BUILD.bazel c/cmd/guru/testdata/src/imports/BUILD.bazel ---- b/cmd/guru/testdata/src/imports/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ c/cmd/guru/testdata/src/imports/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,14 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") -+ -+go_library( -+ name = "imports_lib", -+ srcs = ["main.go"], -+ importpath = "golang.org/x/tools/cmd/guru/testdata/src/imports", -+ visibility = ["//visibility:private"], -+) -+ -+go_binary( -+ name = "imports", -+ embed = [":imports_lib"], -+ visibility = ["//visibility:public"], -+) -diff -urN b/cmd/guru/testdata/src/lib/BUILD.bazel c/cmd/guru/testdata/src/lib/BUILD.bazel ---- b/cmd/guru/testdata/src/lib/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ c/cmd/guru/testdata/src/lib/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,14 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_library") -+ -+go_library( -+ name = "lib", -+ srcs = ["lib.go"], -+ importpath = "golang.org/x/tools/cmd/guru/testdata/src/lib", -+ visibility = ["//visibility:public"], -+) -+ -+alias( -+ name = "go_default_library", -+ actual = ":lib", -+ visibility = ["//visibility:public"], -+) -diff -urN b/cmd/guru/testdata/src/lib/sublib/BUILD.bazel c/cmd/guru/testdata/src/lib/sublib/BUILD.bazel ---- b/cmd/guru/testdata/src/lib/sublib/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ c/cmd/guru/testdata/src/lib/sublib/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,14 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_library") -+ -+go_library( -+ name = "sublib", -+ srcs = ["sublib.go"], -+ importpath = "golang.org/x/tools/cmd/guru/testdata/src/lib/sublib", -+ visibility = ["//visibility:public"], -+) -+ -+alias( -+ name = "go_default_library", -+ actual = ":sublib", -+ visibility = ["//visibility:public"], -+) -diff -urN b/cmd/guru/testdata/src/main/BUILD.bazel c/cmd/guru/testdata/src/main/BUILD.bazel ---- b/cmd/guru/testdata/src/main/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ c/cmd/guru/testdata/src/main/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,14 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") -+ -+go_library( -+ name = "main_lib", -+ srcs = ["multi.go"], -+ importpath = "golang.org/x/tools/cmd/guru/testdata/src/main", -+ visibility = ["//visibility:private"], -+) -+ -+go_binary( -+ name = "main", -+ embed = [":main_lib"], -+ visibility = ["//visibility:public"], -+) -diff -urN b/cmd/guru/testdata/src/referrers/BUILD.bazel c/cmd/guru/testdata/src/referrers/BUILD.bazel ---- b/cmd/guru/testdata/src/referrers/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ c/cmd/guru/testdata/src/referrers/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,23 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test") -+ -+go_library( -+ name = "referrers_lib", -+ srcs = ["main.go"], -+ importpath = "golang.org/x/tools/cmd/guru/testdata/src/referrers", -+ visibility = ["//visibility:private"], -+) -+ -+go_binary( -+ name = "referrers", -+ embed = [":referrers_lib"], -+ visibility = ["//visibility:public"], -+) -+ -+go_test( -+ name = "referrers_test", -+ srcs = [ -+ "ext_test.go", -+ "int_test.go", -+ ], -+ embed = [":referrers_lib"], ++ name = "gorename_test", ++ srcs = ["gorename_test.go"], ++ deps = ["//internal/testenv"], +) -diff -urN b/cmd/guru/testdata/src/referrers-json/BUILD.bazel c/cmd/guru/testdata/src/referrers-json/BUILD.bazel ---- b/cmd/guru/testdata/src/referrers-json/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ c/cmd/guru/testdata/src/referrers-json/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,14 @@ +diff -urN b/cmd/gotype/BUILD.bazel c/cmd/gotype/BUILD.bazel +--- b/cmd/gotype/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 ++++ c/cmd/gotype/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 +@@ -0,0 +1,18 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( -+ name = "referrers-json_lib", -+ srcs = ["main.go"], -+ importpath = "golang.org/x/tools/cmd/guru/testdata/src/referrers-json", ++ name = "gotype_lib", ++ srcs = [ ++ "gotype.go", ++ "sizesFor18.go", ++ "sizesFor19.go", ++ ], ++ importpath = "golang.org/x/tools/cmd/gotype", + visibility = ["//visibility:private"], +) + +go_binary( -+ name = "referrers-json", -+ embed = [":referrers-json_lib"], ++ name = "gotype", ++ embed = [":gotype_lib"], + visibility = ["//visibility:public"], +) -diff -urN b/cmd/guru/testdata/src/what/BUILD.bazel c/cmd/guru/testdata/src/what/BUILD.bazel ---- b/cmd/guru/testdata/src/what/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ c/cmd/guru/testdata/src/what/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,14 @@ +diff -urN b/cmd/goyacc/BUILD.bazel c/cmd/goyacc/BUILD.bazel +--- b/cmd/goyacc/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 ++++ c/cmd/goyacc/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 +@@ -0,0 +1,17 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( -+ name = "what_lib", -+ srcs = ["main.go"], -+ importpath = "golang.org/x/tools/cmd/guru/testdata/src/what", ++ name = "goyacc_lib", ++ srcs = [ ++ "doc.go", ++ "yacc.go", ++ ], ++ importpath = "golang.org/x/tools/cmd/goyacc", + visibility = ["//visibility:private"], +) + +go_binary( -+ name = "what", -+ embed = [":what_lib"], ++ name = "goyacc", ++ embed = [":goyacc_lib"], + visibility = ["//visibility:public"], +) -diff -urN b/cmd/guru/testdata/src/what-json/BUILD.bazel c/cmd/guru/testdata/src/what-json/BUILD.bazel ---- b/cmd/guru/testdata/src/what-json/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ c/cmd/guru/testdata/src/what-json/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 +diff -urN b/cmd/goyacc/testdata/expr/BUILD.bazel c/cmd/goyacc/testdata/expr/BUILD.bazel +--- b/cmd/goyacc/testdata/expr/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 ++++ c/cmd/goyacc/testdata/expr/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 @@ -0,0 +1,14 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") ++load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( -+ name = "what-json_lib", ++ name = "expr_lib", + srcs = ["main.go"], -+ importpath = "golang.org/x/tools/cmd/guru/testdata/src/what-json", -+ visibility = ["//visibility:private"], ++ importpath = "golang.org/x/tools/cmd/goyacc/testdata/expr", ++ visibility = ["//visibility:public"], +) + -+go_binary( -+ name = "what-json", -+ embed = [":what-json_lib"], ++alias( ++ name = "go_default_library", ++ actual = ":expr_lib", + visibility = ["//visibility:public"], +) diff -urN b/cmd/html2article/BUILD.bazel c/cmd/html2article/BUILD.bazel @@ -1737,7 +1289,7 @@ diff -urN b/cmd/ssadump/BUILD.bazel c/cmd/ssadump/BUILD.bazel diff -urN b/cmd/stress/BUILD.bazel c/cmd/stress/BUILD.bazel --- b/cmd/stress/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/cmd/stress/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,53 @@ +@@ -0,0 +1,14 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( @@ -1745,45 +1297,6 @@ diff -urN b/cmd/stress/BUILD.bazel c/cmd/stress/BUILD.bazel + srcs = ["stress.go"], + importpath = "golang.org/x/tools/cmd/stress", + visibility = ["//visibility:private"], -+ deps = select({ -+ "@io_bazel_rules_go//go/platform:aix": [ -+ "@org_golang_x_sys//execabs:go_default_library", -+ ], -+ "@io_bazel_rules_go//go/platform:android": [ -+ "@org_golang_x_sys//execabs:go_default_library", -+ ], -+ "@io_bazel_rules_go//go/platform:darwin": [ -+ "@org_golang_x_sys//execabs:go_default_library", -+ ], -+ "@io_bazel_rules_go//go/platform:dragonfly": [ -+ "@org_golang_x_sys//execabs:go_default_library", -+ ], -+ "@io_bazel_rules_go//go/platform:freebsd": [ -+ "@org_golang_x_sys//execabs:go_default_library", -+ ], -+ "@io_bazel_rules_go//go/platform:illumos": [ -+ "@org_golang_x_sys//execabs:go_default_library", -+ ], -+ "@io_bazel_rules_go//go/platform:ios": [ -+ "@org_golang_x_sys//execabs:go_default_library", -+ ], -+ "@io_bazel_rules_go//go/platform:linux": [ -+ "@org_golang_x_sys//execabs:go_default_library", -+ ], -+ "@io_bazel_rules_go//go/platform:netbsd": [ -+ "@org_golang_x_sys//execabs:go_default_library", -+ ], -+ "@io_bazel_rules_go//go/platform:openbsd": [ -+ "@org_golang_x_sys//execabs:go_default_library", -+ ], -+ "@io_bazel_rules_go//go/platform:solaris": [ -+ "@org_golang_x_sys//execabs:go_default_library", -+ ], -+ "@io_bazel_rules_go//go/platform:windows": [ -+ "@org_golang_x_sys//execabs:go_default_library", -+ ], -+ "//conditions:default": [], -+ }), +) + +go_binary( @@ -1794,7 +1307,7 @@ diff -urN b/cmd/stress/BUILD.bazel c/cmd/stress/BUILD.bazel diff -urN b/cmd/stringer/BUILD.bazel c/cmd/stringer/BUILD.bazel --- b/cmd/stringer/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/cmd/stringer/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,69 @@ +@@ -0,0 +1,26 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test") + +go_library( @@ -1819,55 +1332,12 @@ diff -urN b/cmd/stringer/BUILD.bazel c/cmd/stringer/BUILD.bazel + "util_test.go", + ], + embed = [":stringer_lib"], -+ deps = [ -+ "//internal/testenv", -+ ] + select({ -+ "@io_bazel_rules_go//go/platform:aix": [ -+ "//internal/typeparams", -+ ], -+ "@io_bazel_rules_go//go/platform:darwin": [ -+ "//internal/typeparams", -+ ], -+ "@io_bazel_rules_go//go/platform:dragonfly": [ -+ "//internal/typeparams", -+ ], -+ "@io_bazel_rules_go//go/platform:freebsd": [ -+ "//internal/typeparams", -+ ], -+ "@io_bazel_rules_go//go/platform:illumos": [ -+ "//internal/typeparams", -+ ], -+ "@io_bazel_rules_go//go/platform:ios": [ -+ "//internal/typeparams", -+ ], -+ "@io_bazel_rules_go//go/platform:js": [ -+ "//internal/typeparams", -+ ], -+ "@io_bazel_rules_go//go/platform:linux": [ -+ "//internal/typeparams", -+ ], -+ "@io_bazel_rules_go//go/platform:netbsd": [ -+ "//internal/typeparams", -+ ], -+ "@io_bazel_rules_go//go/platform:openbsd": [ -+ "//internal/typeparams", -+ ], -+ "@io_bazel_rules_go//go/platform:plan9": [ -+ "//internal/typeparams", -+ ], -+ "@io_bazel_rules_go//go/platform:solaris": [ -+ "//internal/typeparams", -+ ], -+ "@io_bazel_rules_go//go/platform:windows": [ -+ "//internal/typeparams", -+ ], -+ "//conditions:default": [], -+ }), ++ deps = ["//internal/testenv"], +) diff -urN b/cmd/stringer/testdata/BUILD.bazel c/cmd/stringer/testdata/BUILD.bazel --- b/cmd/stringer/testdata/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/cmd/stringer/testdata/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,27 @@ +@@ -0,0 +1,29 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( @@ -1875,11 +1345,13 @@ diff -urN b/cmd/stringer/testdata/BUILD.bazel c/cmd/stringer/testdata/BUILD.baze + srcs = [ + "cgo.go", + "conv.go", ++ "conv2.go", + "day.go", + "gap.go", + "num.go", + "number.go", + "prime.go", ++ "prime2.go", + "tag_main.go", + "unum.go", + "unum2.go", @@ -1895,31 +1367,10 @@ diff -urN b/cmd/stringer/testdata/BUILD.bazel c/cmd/stringer/testdata/BUILD.baze + embed = [":testdata_lib"], + visibility = ["//visibility:public"], +) -diff -urN b/cmd/stringer/testdata/typeparams/BUILD.bazel c/cmd/stringer/testdata/typeparams/BUILD.bazel ---- b/cmd/stringer/testdata/typeparams/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ c/cmd/stringer/testdata/typeparams/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,17 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") -+ -+go_library( -+ name = "typeparams_lib", -+ srcs = [ -+ "conv2.go", -+ "prime2.go", -+ ], -+ importpath = "golang.org/x/tools/cmd/stringer/testdata/typeparams", -+ visibility = ["//visibility:private"], -+) -+ -+go_binary( -+ name = "typeparams", -+ embed = [":typeparams_lib"], -+ visibility = ["//visibility:public"], -+) diff -urN b/cmd/toolstash/BUILD.bazel c/cmd/toolstash/BUILD.bazel --- b/cmd/toolstash/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/cmd/toolstash/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,18 @@ +@@ -0,0 +1,17 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( @@ -1930,7 +1381,6 @@ diff -urN b/cmd/toolstash/BUILD.bazel c/cmd/toolstash/BUILD.bazel + ], + importpath = "golang.org/x/tools/cmd/toolstash", + visibility = ["//visibility:private"], -+ deps = ["@org_golang_x_sys//execabs:go_default_library"], +) + +go_binary( @@ -2016,7 +1466,7 @@ diff -urN b/cover/BUILD.bazel c/cover/BUILD.bazel diff -urN b/go/analysis/analysistest/BUILD.bazel c/go/analysis/analysistest/BUILD.bazel --- b/go/analysis/analysistest/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/analysis/analysistest/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,32 @@ +@@ -0,0 +1,33 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -2045,6 +1495,7 @@ diff -urN b/go/analysis/analysistest/BUILD.bazel c/go/analysis/analysistest/BUIL + srcs = ["analysistest_test.go"], + deps = [ + ":analysistest", ++ "//go/analysis", + "//go/analysis/passes/findcall", + "//internal/testenv", + ], @@ -2116,7 +1567,7 @@ diff -urN b/go/analysis/internal/analysisflags/BUILD.bazel c/go/analysis/interna diff -urN b/go/analysis/internal/checker/BUILD.bazel c/go/analysis/internal/checker/BUILD.bazel --- b/go/analysis/internal/checker/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/analysis/internal/checker/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,38 @@ +@@ -0,0 +1,39 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -2150,6 +1601,7 @@ diff -urN b/go/analysis/internal/checker/BUILD.bazel c/go/analysis/internal/chec + ":checker", + "//go/analysis", + "//go/analysis/analysistest", ++ "//go/analysis/multichecker", + "//go/analysis/passes/inspect", + "//go/ast/inspector", + "//internal/testenv", @@ -2284,16 +1736,12 @@ diff -urN b/go/analysis/passes/appends/testdata/src/b/BUILD.bazel c/go/analysis/ diff -urN b/go/analysis/passes/asmdecl/BUILD.bazel c/go/analysis/passes/asmdecl/BUILD.bazel --- b/go/analysis/passes/asmdecl/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/analysis/passes/asmdecl/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,31 @@ +@@ -0,0 +1,27 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "asmdecl", -+ srcs = [ -+ "arches_go118.go", -+ "arches_go119.go", -+ "asmdecl.go", -+ ], ++ srcs = ["asmdecl.go"], + importpath = "golang.org/x/tools/go/analysis/passes/asmdecl", + visibility = ["//visibility:public"], + deps = [ @@ -2349,7 +1797,7 @@ diff -urN b/go/analysis/passes/asmdecl/testdata/src/a/BUILD.bazel c/go/analysis/ diff -urN b/go/analysis/passes/assign/BUILD.bazel c/go/analysis/passes/assign/BUILD.bazel --- b/go/analysis/passes/assign/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/analysis/passes/assign/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,35 @@ +@@ -0,0 +1,34 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -2382,7 +1830,6 @@ diff -urN b/go/analysis/passes/assign/BUILD.bazel c/go/analysis/passes/assign/BU + deps = [ + ":assign", + "//go/analysis/analysistest", -+ "//internal/typeparams", + ], +) diff -urN b/go/analysis/passes/assign/testdata/src/a/BUILD.bazel c/go/analysis/passes/assign/testdata/src/a/BUILD.bazel @@ -2424,7 +1871,7 @@ diff -urN b/go/analysis/passes/assign/testdata/src/typeparams/BUILD.bazel c/go/a diff -urN b/go/analysis/passes/atomic/BUILD.bazel c/go/analysis/passes/atomic/BUILD.bazel --- b/go/analysis/passes/atomic/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/analysis/passes/atomic/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,35 @@ +@@ -0,0 +1,34 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -2457,7 +1904,6 @@ diff -urN b/go/analysis/passes/atomic/BUILD.bazel c/go/analysis/passes/atomic/BU + deps = [ + ":atomic", + "//go/analysis/analysistest", -+ "//internal/typeparams", + ], +) diff -urN b/go/analysis/passes/atomic/testdata/src/a/BUILD.bazel c/go/analysis/passes/atomic/testdata/src/a/BUILD.bazel @@ -2575,7 +2021,7 @@ diff -urN b/go/analysis/passes/atomicalign/testdata/src/b/BUILD.bazel c/go/analy diff -urN b/go/analysis/passes/bools/BUILD.bazel c/go/analysis/passes/bools/BUILD.bazel --- b/go/analysis/passes/bools/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/analysis/passes/bools/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,31 @@ +@@ -0,0 +1,30 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -2604,7 +2050,6 @@ diff -urN b/go/analysis/passes/bools/BUILD.bazel c/go/analysis/passes/bools/BUIL + deps = [ + ":bools", + "//go/analysis/analysistest", -+ "//internal/typeparams", + ], +) diff -urN b/go/analysis/passes/bools/testdata/src/a/BUILD.bazel c/go/analysis/passes/bools/testdata/src/a/BUILD.bazel @@ -2646,7 +2091,7 @@ diff -urN b/go/analysis/passes/bools/testdata/src/typeparams/BUILD.bazel c/go/an diff -urN b/go/analysis/passes/buildssa/BUILD.bazel c/go/analysis/passes/buildssa/BUILD.bazel --- b/go/analysis/passes/buildssa/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/analysis/passes/buildssa/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,28 @@ +@@ -0,0 +1,27 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -2672,7 +2117,6 @@ diff -urN b/go/analysis/passes/buildssa/BUILD.bazel c/go/analysis/passes/buildss + deps = [ + ":buildssa", + "//go/analysis/analysistest", -+ "//internal/typeparams", + ], +) diff -urN b/go/analysis/passes/buildssa/testdata/src/a/BUILD.bazel c/go/analysis/passes/buildssa/testdata/src/a/BUILD.bazel @@ -2789,7 +2233,7 @@ diff -urN b/go/analysis/passes/buildtag/testdata/src/a/BUILD.bazel c/go/analysis diff -urN b/go/analysis/passes/cgocall/BUILD.bazel c/go/analysis/passes/cgocall/BUILD.bazel --- b/go/analysis/passes/cgocall/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/analysis/passes/cgocall/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,33 @@ +@@ -0,0 +1,32 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -2820,7 +2264,6 @@ diff -urN b/go/analysis/passes/cgocall/BUILD.bazel c/go/analysis/passes/cgocall/ + deps = [ + ":cgocall", + "//go/analysis/analysistest", -+ "//internal/typeparams", + ], +) diff -urN b/go/analysis/passes/cgocall/testdata/src/a/BUILD.bazel c/go/analysis/passes/cgocall/testdata/src/a/BUILD.bazel @@ -2918,6 +2361,7 @@ diff -urN b/go/analysis/passes/composite/BUILD.bazel c/go/analysis/passes/compos + "//go/analysis", + "//go/analysis/passes/inspect", + "//go/ast/inspector", ++ "//internal/aliases", + "//internal/typeparams", + ], +) @@ -2934,7 +2378,6 @@ diff -urN b/go/analysis/passes/composite/BUILD.bazel c/go/analysis/passes/compos + deps = [ + ":composite", + "//go/analysis/analysistest", -+ "//internal/typeparams", + ], +) diff -urN b/go/analysis/passes/composite/testdata/src/a/BUILD.bazel c/go/analysis/passes/composite/testdata/src/a/BUILD.bazel @@ -3014,6 +2457,7 @@ diff -urN b/go/analysis/passes/copylock/BUILD.bazel c/go/analysis/passes/copyloc + "//go/analysis/passes/internal/analysisutil", + "//go/ast/astutil", + "//go/ast/inspector", ++ "//internal/aliases", + "//internal/typeparams", + ], +) @@ -3030,7 +2474,6 @@ diff -urN b/go/analysis/passes/copylock/BUILD.bazel c/go/analysis/passes/copyloc + deps = [ + ":copylock", + "//go/analysis/analysistest", -+ "//internal/typeparams", + ], +) diff -urN b/go/analysis/passes/copylock/testdata/src/a/BUILD.bazel c/go/analysis/passes/copylock/testdata/src/a/BUILD.bazel @@ -3077,7 +2520,7 @@ diff -urN b/go/analysis/passes/copylock/testdata/src/typeparams/BUILD.bazel c/go diff -urN b/go/analysis/passes/ctrlflow/BUILD.bazel c/go/analysis/passes/ctrlflow/BUILD.bazel --- b/go/analysis/passes/ctrlflow/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/analysis/passes/ctrlflow/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,31 @@ +@@ -0,0 +1,30 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -3106,7 +2549,6 @@ diff -urN b/go/analysis/passes/ctrlflow/BUILD.bazel c/go/analysis/passes/ctrlflo + deps = [ + ":ctrlflow", + "//go/analysis/analysistest", -+ "//internal/typeparams", + ], +) diff -urN b/go/analysis/passes/ctrlflow/testdata/src/a/BUILD.bazel c/go/analysis/passes/ctrlflow/testdata/src/a/BUILD.bazel @@ -3180,6 +2622,7 @@ diff -urN b/go/analysis/passes/deepequalerrors/BUILD.bazel c/go/analysis/passes/ + "//go/analysis/passes/internal/analysisutil", + "//go/ast/inspector", + "//go/types/typeutil", ++ "//internal/aliases", + ], +) + @@ -3195,7 +2638,6 @@ diff -urN b/go/analysis/passes/deepequalerrors/BUILD.bazel c/go/analysis/passes/ + deps = [ + ":deepequalerrors", + "//go/analysis/analysistest", -+ "//internal/typeparams", + ], +) diff -urN b/go/analysis/passes/deepequalerrors/testdata/src/a/BUILD.bazel c/go/analysis/passes/deepequalerrors/testdata/src/a/BUILD.bazel @@ -3373,7 +2815,7 @@ diff -urN b/go/analysis/passes/directive/testdata/src/a/BUILD.bazel c/go/analysi diff -urN b/go/analysis/passes/errorsas/BUILD.bazel c/go/analysis/passes/errorsas/BUILD.bazel --- b/go/analysis/passes/errorsas/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/analysis/passes/errorsas/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,31 @@ +@@ -0,0 +1,30 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -3402,7 +2844,6 @@ diff -urN b/go/analysis/passes/errorsas/BUILD.bazel c/go/analysis/passes/errorsa + deps = [ + ":errorsas", + "//go/analysis/analysistest", -+ "//internal/typeparams", + ], +) diff -urN b/go/analysis/passes/errorsas/testdata/src/a/BUILD.bazel c/go/analysis/passes/errorsas/testdata/src/a/BUILD.bazel @@ -3643,7 +3084,7 @@ diff -urN b/go/analysis/passes/framepointer/testdata/src/a/BUILD.bazel c/go/anal diff -urN b/go/analysis/passes/httpmux/BUILD.bazel c/go/analysis/passes/httpmux/BUILD.bazel --- b/go/analysis/passes/httpmux/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/analysis/passes/httpmux/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,29 @@ +@@ -0,0 +1,30 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -3657,6 +3098,7 @@ diff -urN b/go/analysis/passes/httpmux/BUILD.bazel c/go/analysis/passes/httpmux/ + "//go/analysis/passes/internal/analysisutil", + "//go/ast/inspector", + "//go/types/typeutil", ++ "//internal/typesinternal", + "@org_golang_x_mod//semver:go_default_library", + ], +) @@ -3716,7 +3158,7 @@ diff -urN b/go/analysis/passes/httpmux/testdata/src/a/BUILD.bazel c/go/analysis/ diff -urN b/go/analysis/passes/httpresponse/BUILD.bazel c/go/analysis/passes/httpresponse/BUILD.bazel --- b/go/analysis/passes/httpresponse/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/analysis/passes/httpresponse/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,30 @@ +@@ -0,0 +1,31 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -3729,6 +3171,8 @@ diff -urN b/go/analysis/passes/httpresponse/BUILD.bazel c/go/analysis/passes/htt + "//go/analysis/passes/inspect", + "//go/analysis/passes/internal/analysisutil", + "//go/ast/inspector", ++ "//internal/aliases", ++ "//internal/typesinternal", + ], +) + @@ -3744,7 +3188,6 @@ diff -urN b/go/analysis/passes/httpresponse/BUILD.bazel c/go/analysis/passes/htt + deps = [ + ":httpresponse", + "//go/analysis/analysistest", -+ "//internal/typeparams", + ], +) diff -urN b/go/analysis/passes/httpresponse/testdata/src/a/BUILD.bazel c/go/analysis/passes/httpresponse/testdata/src/a/BUILD.bazel @@ -3786,7 +3229,7 @@ diff -urN b/go/analysis/passes/httpresponse/testdata/src/typeparams/BUILD.bazel diff -urN b/go/analysis/passes/ifaceassert/BUILD.bazel c/go/analysis/passes/ifaceassert/BUILD.bazel --- b/go/analysis/passes/ifaceassert/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/analysis/passes/ifaceassert/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,36 @@ +@@ -0,0 +1,34 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -3794,7 +3237,6 @@ diff -urN b/go/analysis/passes/ifaceassert/BUILD.bazel c/go/analysis/passes/ifac + srcs = [ + "doc.go", + "ifaceassert.go", -+ "parameterized.go", + ], + embedsrcs = ["doc.go"], + importpath = "golang.org/x/tools/go/analysis/passes/ifaceassert", @@ -3820,7 +3262,6 @@ diff -urN b/go/analysis/passes/ifaceassert/BUILD.bazel c/go/analysis/passes/ifac + deps = [ + ":ifaceassert", + "//go/analysis/analysistest", -+ "//internal/typeparams", + ], +) diff -urN b/go/analysis/passes/ifaceassert/cmd/ifaceassert/BUILD.bazel c/go/analysis/passes/ifaceassert/cmd/ifaceassert/BUILD.bazel @@ -3906,17 +3347,18 @@ diff -urN b/go/analysis/passes/inspect/BUILD.bazel c/go/analysis/passes/inspect/ diff -urN b/go/analysis/passes/internal/analysisutil/BUILD.bazel c/go/analysis/passes/internal/analysisutil/BUILD.bazel --- b/go/analysis/passes/internal/analysisutil/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/analysis/passes/internal/analysisutil/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,29 @@ +@@ -0,0 +1,24 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "analysisutil", -+ srcs = [ -+ "extractdoc.go", -+ "util.go", -+ ], ++ srcs = ["util.go"], + importpath = "golang.org/x/tools/go/analysis/passes/internal/analysisutil", + visibility = ["//go/analysis/passes:__subpackages__"], ++ deps = [ ++ "//internal/aliases", ++ "//internal/analysisinternal", ++ ], +) + +alias( @@ -3927,19 +3369,13 @@ diff -urN b/go/analysis/passes/internal/analysisutil/BUILD.bazel c/go/analysis/p + +go_test( + name = "analysisutil_test", -+ srcs = [ -+ "extractdoc_test.go", -+ "util_test.go", -+ ], -+ deps = [ -+ ":analysisutil", -+ "//internal/typeparams", -+ ], ++ srcs = ["util_test.go"], ++ deps = [":analysisutil"], +) diff -urN b/go/analysis/passes/loopclosure/BUILD.bazel c/go/analysis/passes/loopclosure/BUILD.bazel --- b/go/analysis/passes/loopclosure/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/analysis/passes/loopclosure/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,35 @@ +@@ -0,0 +1,39 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -3957,6 +3393,8 @@ diff -urN b/go/analysis/passes/loopclosure/BUILD.bazel c/go/analysis/passes/loop + "//go/analysis/passes/internal/analysisutil", + "//go/ast/inspector", + "//go/types/typeutil", ++ "//internal/typesinternal", ++ "//internal/versions", + ], +) + @@ -3971,8 +3409,10 @@ diff -urN b/go/analysis/passes/loopclosure/BUILD.bazel c/go/analysis/passes/loop + srcs = ["loopclosure_test.go"], + deps = [ + ":loopclosure", ++ "//go/analysis", + "//go/analysis/analysistest", -+ "//internal/typeparams", ++ "//internal/testenv", ++ "//txtar", + ], +) diff -urN b/go/analysis/passes/loopclosure/testdata/src/a/BUILD.bazel c/go/analysis/passes/loopclosure/testdata/src/a/BUILD.bazel @@ -4055,7 +3495,7 @@ diff -urN b/go/analysis/passes/loopclosure/testdata/src/typeparams/BUILD.bazel c diff -urN b/go/analysis/passes/lostcancel/BUILD.bazel c/go/analysis/passes/lostcancel/BUILD.bazel --- b/go/analysis/passes/lostcancel/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/analysis/passes/lostcancel/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,36 @@ +@@ -0,0 +1,35 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -4089,7 +3529,6 @@ diff -urN b/go/analysis/passes/lostcancel/BUILD.bazel c/go/analysis/passes/lostc + deps = [ + ":lostcancel", + "//go/analysis/analysistest", -+ "//internal/typeparams", + ], +) diff -urN b/go/analysis/passes/lostcancel/cmd/lostcancel/BUILD.bazel c/go/analysis/passes/lostcancel/cmd/lostcancel/BUILD.bazel @@ -4171,7 +3610,7 @@ diff -urN b/go/analysis/passes/lostcancel/testdata/src/typeparams/BUILD.bazel c/ diff -urN b/go/analysis/passes/nilfunc/BUILD.bazel c/go/analysis/passes/nilfunc/BUILD.bazel --- b/go/analysis/passes/nilfunc/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/analysis/passes/nilfunc/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,35 @@ +@@ -0,0 +1,34 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -4204,7 +3643,6 @@ diff -urN b/go/analysis/passes/nilfunc/BUILD.bazel c/go/analysis/passes/nilfunc/ + deps = [ + ":nilfunc", + "//go/analysis/analysistest", -+ "//internal/typeparams", + ], +) diff -urN b/go/analysis/passes/nilfunc/testdata/src/a/BUILD.bazel c/go/analysis/passes/nilfunc/testdata/src/a/BUILD.bazel @@ -4246,7 +3684,7 @@ diff -urN b/go/analysis/passes/nilfunc/testdata/src/typeparams/BUILD.bazel c/go/ diff -urN b/go/analysis/passes/nilness/BUILD.bazel c/go/analysis/passes/nilness/BUILD.bazel --- b/go/analysis/passes/nilness/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/analysis/passes/nilness/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,38 @@ +@@ -0,0 +1,34 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -4275,14 +3713,10 @@ diff -urN b/go/analysis/passes/nilness/BUILD.bazel c/go/analysis/passes/nilness/ + +go_test( + name = "nilness_test", -+ srcs = [ -+ "nilness_go117_test.go", -+ "nilness_test.go", -+ ], ++ srcs = ["nilness_test.go"], + deps = [ + ":nilness", + "//go/analysis/analysistest", -+ "//internal/typeparams", + ], +) diff -urN b/go/analysis/passes/nilness/cmd/nilness/BUILD.bazel c/go/analysis/passes/nilness/cmd/nilness/BUILD.bazel @@ -4464,7 +3898,7 @@ diff -urN b/go/analysis/passes/pkgfact/testdata/src/c/BUILD.bazel c/go/analysis/ diff -urN b/go/analysis/passes/printf/BUILD.bazel c/go/analysis/passes/printf/BUILD.bazel --- b/go/analysis/passes/printf/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/analysis/passes/printf/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,38 @@ +@@ -0,0 +1,37 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -4483,6 +3917,7 @@ diff -urN b/go/analysis/passes/printf/BUILD.bazel c/go/analysis/passes/printf/BU + "//go/analysis/passes/internal/analysisutil", + "//go/ast/inspector", + "//go/types/typeutil", ++ "//internal/aliases", + "//internal/typeparams", + ], +) @@ -4499,8 +3934,6 @@ diff -urN b/go/analysis/passes/printf/BUILD.bazel c/go/analysis/passes/printf/BU + deps = [ + ":printf", + "//go/analysis/analysistest", -+ "//internal/testenv", -+ "//internal/typeparams", + ], +) diff -urN b/go/analysis/passes/printf/testdata/src/a/BUILD.bazel c/go/analysis/passes/printf/testdata/src/a/BUILD.bazel @@ -4730,6 +4163,7 @@ diff -urN b/go/analysis/passes/shift/BUILD.bazel c/go/analysis/passes/shift/BUIL + "//go/analysis/passes/inspect", + "//go/analysis/passes/internal/analysisutil", + "//go/ast/inspector", ++ "//internal/aliases", + "//internal/typeparams", + ], +) @@ -4746,7 +4180,6 @@ diff -urN b/go/analysis/passes/shift/BUILD.bazel c/go/analysis/passes/shift/BUIL + deps = [ + ":shift", + "//go/analysis/analysistest", -+ "//internal/typeparams", + ], +) diff -urN b/go/analysis/passes/shift/testdata/src/a/BUILD.bazel c/go/analysis/passes/shift/testdata/src/a/BUILD.bazel @@ -4843,7 +4276,7 @@ diff -urN b/go/analysis/passes/sigchanyzer/testdata/src/a/BUILD.bazel c/go/analy diff -urN b/go/analysis/passes/slog/BUILD.bazel c/go/analysis/passes/slog/BUILD.bazel --- b/go/analysis/passes/slog/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/analysis/passes/slog/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,35 @@ +@@ -0,0 +1,36 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -4861,6 +4294,7 @@ diff -urN b/go/analysis/passes/slog/BUILD.bazel c/go/analysis/passes/slog/BUILD. + "//go/analysis/passes/internal/analysisutil", + "//go/ast/inspector", + "//go/types/typeutil", ++ "//internal/typesinternal", + ], +) + @@ -4970,7 +4404,7 @@ diff -urN b/go/analysis/passes/sortslice/testdata/src/a/BUILD.bazel c/go/analysi diff -urN b/go/analysis/passes/stdmethods/BUILD.bazel c/go/analysis/passes/stdmethods/BUILD.bazel --- b/go/analysis/passes/stdmethods/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/analysis/passes/stdmethods/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,34 @@ +@@ -0,0 +1,33 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -5002,7 +4436,6 @@ diff -urN b/go/analysis/passes/stdmethods/BUILD.bazel c/go/analysis/passes/stdme + deps = [ + ":stdmethods", + "//go/analysis/analysistest", -+ "//internal/typeparams", + ], +) diff -urN b/go/analysis/passes/stdmethods/testdata/src/a/BUILD.bazel c/go/analysis/passes/stdmethods/testdata/src/a/BUILD.bazel @@ -5044,6 +4477,44 @@ diff -urN b/go/analysis/passes/stdmethods/testdata/src/typeparams/BUILD.bazel c/ + actual = ":typeparams", + visibility = ["//visibility:public"], +) +diff -urN b/go/analysis/passes/stdversion/BUILD.bazel c/go/analysis/passes/stdversion/BUILD.bazel +--- b/go/analysis/passes/stdversion/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 ++++ c/go/analysis/passes/stdversion/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 +@@ -0,0 +1,34 @@ ++load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") ++ ++go_library( ++ name = "stdversion", ++ srcs = ["stdversion.go"], ++ importpath = "golang.org/x/tools/go/analysis/passes/stdversion", ++ visibility = ["//visibility:public"], ++ deps = [ ++ "//go/analysis", ++ "//go/analysis/passes/inspect", ++ "//go/ast/inspector", ++ "//internal/typesinternal", ++ "//internal/versions", ++ ], ++) ++ ++alias( ++ name = "go_default_library", ++ actual = ":stdversion", ++ visibility = ["//visibility:public"], ++) ++ ++go_test( ++ name = "stdversion_test", ++ srcs = ["stdversion_test.go"], ++ data = glob(["testdata/**"]), ++ deps = [ ++ ":stdversion", ++ "//go/analysis", ++ "//go/analysis/analysistest", ++ "//internal/testenv", ++ "//txtar", ++ ], ++) diff -urN b/go/analysis/passes/stringintconv/BUILD.bazel c/go/analysis/passes/stringintconv/BUILD.bazel --- b/go/analysis/passes/stringintconv/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/analysis/passes/stringintconv/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 @@ -5064,6 +4535,7 @@ diff -urN b/go/analysis/passes/stringintconv/BUILD.bazel c/go/analysis/passes/st + "//go/analysis/passes/inspect", + "//go/analysis/passes/internal/analysisutil", + "//go/ast/inspector", ++ "//internal/aliases", + "//internal/typeparams", + ], +) @@ -5080,7 +4552,6 @@ diff -urN b/go/analysis/passes/stringintconv/BUILD.bazel c/go/analysis/passes/st + deps = [ + ":stringintconv", + "//go/analysis/analysistest", -+ "//internal/typeparams", + ], +) diff -urN b/go/analysis/passes/stringintconv/cmd/stringintconv/BUILD.bazel c/go/analysis/passes/stringintconv/cmd/stringintconv/BUILD.bazel @@ -5212,7 +4683,7 @@ diff -urN b/go/analysis/passes/structtag/testdata/src/a/BUILD.bazel c/go/analysi diff -urN b/go/analysis/passes/testinggoroutine/BUILD.bazel c/go/analysis/passes/testinggoroutine/BUILD.bazel --- b/go/analysis/passes/testinggoroutine/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/analysis/passes/testinggoroutine/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,35 @@ +@@ -0,0 +1,38 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -5220,6 +4691,7 @@ diff -urN b/go/analysis/passes/testinggoroutine/BUILD.bazel c/go/analysis/passes + srcs = [ + "doc.go", + "testinggoroutine.go", ++ "util.go", + ], + embedsrcs = ["doc.go"], + importpath = "golang.org/x/tools/go/analysis/passes/testinggoroutine", @@ -5228,7 +4700,10 @@ diff -urN b/go/analysis/passes/testinggoroutine/BUILD.bazel c/go/analysis/passes + "//go/analysis", + "//go/analysis/passes/inspect", + "//go/analysis/passes/internal/analysisutil", ++ "//go/ast/astutil", + "//go/ast/inspector", ++ "//go/types/typeutil", ++ "//internal/aliases", + "//internal/typeparams", + ], +) @@ -5245,7 +4720,6 @@ diff -urN b/go/analysis/passes/testinggoroutine/BUILD.bazel c/go/analysis/passes + deps = [ + ":testinggoroutine", + "//go/analysis/analysistest", -+ "//internal/typeparams", + ], +) diff -urN b/go/analysis/passes/testinggoroutine/testdata/src/a/BUILD.bazel c/go/analysis/passes/testinggoroutine/testdata/src/a/BUILD.bazel @@ -5290,7 +4764,7 @@ diff -urN b/go/analysis/passes/testinggoroutine/testdata/src/typeparams/BUILD.ba diff -urN b/go/analysis/passes/tests/BUILD.bazel c/go/analysis/passes/tests/BUILD.bazel --- b/go/analysis/passes/tests/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/analysis/passes/tests/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,33 @@ +@@ -0,0 +1,31 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -5305,7 +4779,6 @@ diff -urN b/go/analysis/passes/tests/BUILD.bazel c/go/analysis/passes/tests/BUIL + deps = [ + "//go/analysis", + "//go/analysis/passes/internal/analysisutil", -+ "//internal/typeparams", + ], +) + @@ -5321,7 +4794,6 @@ diff -urN b/go/analysis/passes/tests/BUILD.bazel c/go/analysis/passes/tests/BUIL + deps = [ + ":tests", + "//go/analysis/analysistest", -+ "//internal/typeparams", + ], +) diff -urN b/go/analysis/passes/tests/testdata/src/a/BUILD.bazel c/go/analysis/passes/tests/testdata/src/a/BUILD.bazel @@ -5505,7 +4977,7 @@ diff -urN b/go/analysis/passes/timeformat/testdata/src/b/BUILD.bazel c/go/analys diff -urN b/go/analysis/passes/unmarshal/BUILD.bazel c/go/analysis/passes/unmarshal/BUILD.bazel --- b/go/analysis/passes/unmarshal/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/analysis/passes/unmarshal/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,36 @@ +@@ -0,0 +1,35 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -5523,7 +4995,7 @@ diff -urN b/go/analysis/passes/unmarshal/BUILD.bazel c/go/analysis/passes/unmars + "//go/analysis/passes/internal/analysisutil", + "//go/ast/inspector", + "//go/types/typeutil", -+ "//internal/typeparams", ++ "//internal/typesinternal", + ], +) + @@ -5539,7 +5011,6 @@ diff -urN b/go/analysis/passes/unmarshal/BUILD.bazel c/go/analysis/passes/unmars + deps = [ + ":unmarshal", + "//go/analysis/analysistest", -+ "//internal/typeparams", + ], +) diff -urN b/go/analysis/passes/unmarshal/cmd/unmarshal/BUILD.bazel c/go/analysis/passes/unmarshal/cmd/unmarshal/BUILD.bazel @@ -5676,6 +5147,7 @@ diff -urN b/go/analysis/passes/unsafeptr/BUILD.bazel c/go/analysis/passes/unsafe + "//go/analysis/passes/internal/analysisutil", + "//go/ast/astutil", + "//go/ast/inspector", ++ "//internal/aliases", + ], +) + @@ -5691,7 +5163,6 @@ diff -urN b/go/analysis/passes/unsafeptr/BUILD.bazel c/go/analysis/passes/unsafe + deps = [ + ":unsafeptr", + "//go/analysis/analysistest", -+ "//internal/typeparams", + ], +) diff -urN b/go/analysis/passes/unsafeptr/testdata/src/a/BUILD.bazel c/go/analysis/passes/unsafeptr/testdata/src/a/BUILD.bazel @@ -5736,7 +5207,7 @@ diff -urN b/go/analysis/passes/unsafeptr/testdata/src/typeparams/BUILD.bazel c/g diff -urN b/go/analysis/passes/unusedresult/BUILD.bazel c/go/analysis/passes/unusedresult/BUILD.bazel --- b/go/analysis/passes/unusedresult/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/analysis/passes/unusedresult/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,36 @@ +@@ -0,0 +1,35 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -5770,7 +5241,6 @@ diff -urN b/go/analysis/passes/unusedresult/BUILD.bazel c/go/analysis/passes/unu + deps = [ + ":unusedresult", + "//go/analysis/analysistest", -+ "//internal/typeparams", + ], +) diff -urN b/go/analysis/passes/unusedresult/cmd/unusedresult/BUILD.bazel c/go/analysis/passes/unusedresult/cmd/unusedresult/BUILD.bazel @@ -5852,7 +5322,7 @@ diff -urN b/go/analysis/passes/unusedresult/testdata/src/typeparams/userdefs/BUI diff -urN b/go/analysis/passes/unusedwrite/BUILD.bazel c/go/analysis/passes/unusedwrite/BUILD.bazel --- b/go/analysis/passes/unusedwrite/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/analysis/passes/unusedwrite/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,33 @@ +@@ -0,0 +1,35 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -5869,6 +5339,8 @@ diff -urN b/go/analysis/passes/unusedwrite/BUILD.bazel c/go/analysis/passes/unus + "//go/analysis/passes/buildssa", + "//go/analysis/passes/internal/analysisutil", + "//go/ssa", ++ "//internal/aliases", ++ "//internal/typeparams", + ], +) + @@ -5907,7 +5379,7 @@ diff -urN b/go/analysis/passes/unusedwrite/testdata/src/a/BUILD.bazel c/go/analy diff -urN b/go/analysis/passes/usesgenerics/BUILD.bazel c/go/analysis/passes/usesgenerics/BUILD.bazel --- b/go/analysis/passes/usesgenerics/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/analysis/passes/usesgenerics/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,35 @@ +@@ -0,0 +1,34 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -5940,7 +5412,6 @@ diff -urN b/go/analysis/passes/usesgenerics/BUILD.bazel c/go/analysis/passes/use + deps = [ + ":usesgenerics", + "//go/analysis/analysistest", -+ "//internal/typeparams", + ], +) diff -urN b/go/analysis/passes/usesgenerics/testdata/src/a/BUILD.bazel c/go/analysis/passes/usesgenerics/testdata/src/a/BUILD.bazel @@ -6042,7 +5513,7 @@ diff -urN b/go/analysis/singlechecker/BUILD.bazel c/go/analysis/singlechecker/BU diff -urN b/go/analysis/unitchecker/BUILD.bazel c/go/analysis/unitchecker/BUILD.bazel --- b/go/analysis/unitchecker/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/analysis/unitchecker/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,69 @@ +@@ -0,0 +1,70 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -6054,7 +5525,7 @@ diff -urN b/go/analysis/unitchecker/BUILD.bazel c/go/analysis/unitchecker/BUILD. + "//go/analysis", + "//go/analysis/internal/analysisflags", + "//internal/facts", -+ "//internal/typeparams", ++ "//internal/versions", + ], +) + @@ -6097,6 +5568,7 @@ diff -urN b/go/analysis/unitchecker/BUILD.bazel c/go/analysis/unitchecker/BUILD. + "//go/analysis/passes/shift", + "//go/analysis/passes/sigchanyzer", + "//go/analysis/passes/stdmethods", ++ "//go/analysis/passes/stdversion", + "//go/analysis/passes/stringintconv", + "//go/analysis/passes/structtag", + "//go/analysis/passes/testinggoroutine", @@ -6115,7 +5587,7 @@ diff -urN b/go/analysis/unitchecker/BUILD.bazel c/go/analysis/unitchecker/BUILD. diff -urN b/go/ast/astutil/BUILD.bazel c/go/ast/astutil/BUILD.bazel --- b/go/ast/astutil/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/ast/astutil/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,31 @@ +@@ -0,0 +1,29 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -6128,7 +5600,6 @@ diff -urN b/go/ast/astutil/BUILD.bazel c/go/ast/astutil/BUILD.bazel + ], + importpath = "golang.org/x/tools/go/ast/astutil", + visibility = ["//visibility:public"], -+ deps = ["//internal/typeparams"], +) + +alias( @@ -6145,12 +5616,11 @@ diff -urN b/go/ast/astutil/BUILD.bazel c/go/ast/astutil/BUILD.bazel + "rewrite_test.go", + ], + embed = [":astutil"], -+ deps = ["//internal/typeparams"], +) diff -urN b/go/ast/inspector/BUILD.bazel c/go/ast/inspector/BUILD.bazel --- b/go/ast/inspector/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/ast/inspector/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,27 @@ +@@ -0,0 +1,23 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -6161,7 +5631,6 @@ diff -urN b/go/ast/inspector/BUILD.bazel c/go/ast/inspector/BUILD.bazel + ], + importpath = "golang.org/x/tools/go/ast/inspector", + visibility = ["//visibility:public"], -+ deps = ["//internal/typeparams"], +) + +alias( @@ -6173,15 +5642,12 @@ diff -urN b/go/ast/inspector/BUILD.bazel c/go/ast/inspector/BUILD.bazel +go_test( + name = "inspector_test", + srcs = ["inspector_test.go"], -+ deps = [ -+ ":inspector", -+ "//internal/typeparams", -+ ], ++ deps = [":inspector"], +) diff -urN b/go/buildutil/BUILD.bazel c/go/buildutil/BUILD.bazel --- b/go/buildutil/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/buildutil/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,35 @@ +@@ -0,0 +1,36 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -6215,6 +5681,7 @@ diff -urN b/go/buildutil/BUILD.bazel c/go/buildutil/BUILD.bazel + deps = [ + ":buildutil", + "//go/packages/packagestest", ++ "//internal/testenv", + ], +) diff -urN b/go/callgraph/BUILD.bazel c/go/callgraph/BUILD.bazel @@ -6257,7 +5724,7 @@ diff -urN b/go/callgraph/BUILD.bazel c/go/callgraph/BUILD.bazel diff -urN b/go/callgraph/cha/BUILD.bazel c/go/callgraph/cha/BUILD.bazel --- b/go/callgraph/cha/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/callgraph/cha/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,132 @@ +@@ -0,0 +1,119 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -6289,7 +5756,6 @@ diff -urN b/go/callgraph/cha/BUILD.bazel c/go/callgraph/cha/BUILD.bazel + "//go/loader", + "//go/ssa", + "//go/ssa/ssautil", -+ "//internal/typeparams", + ], + "@io_bazel_rules_go//go/platform:darwin": [ + ":cha", @@ -6297,7 +5763,6 @@ diff -urN b/go/callgraph/cha/BUILD.bazel c/go/callgraph/cha/BUILD.bazel + "//go/loader", + "//go/ssa", + "//go/ssa/ssautil", -+ "//internal/typeparams", + ], + "@io_bazel_rules_go//go/platform:dragonfly": [ + ":cha", @@ -6305,7 +5770,6 @@ diff -urN b/go/callgraph/cha/BUILD.bazel c/go/callgraph/cha/BUILD.bazel + "//go/loader", + "//go/ssa", + "//go/ssa/ssautil", -+ "//internal/typeparams", + ], + "@io_bazel_rules_go//go/platform:freebsd": [ + ":cha", @@ -6313,7 +5777,6 @@ diff -urN b/go/callgraph/cha/BUILD.bazel c/go/callgraph/cha/BUILD.bazel + "//go/loader", + "//go/ssa", + "//go/ssa/ssautil", -+ "//internal/typeparams", + ], + "@io_bazel_rules_go//go/platform:illumos": [ + ":cha", @@ -6321,7 +5784,6 @@ diff -urN b/go/callgraph/cha/BUILD.bazel c/go/callgraph/cha/BUILD.bazel + "//go/loader", + "//go/ssa", + "//go/ssa/ssautil", -+ "//internal/typeparams", + ], + "@io_bazel_rules_go//go/platform:ios": [ + ":cha", @@ -6329,7 +5791,6 @@ diff -urN b/go/callgraph/cha/BUILD.bazel c/go/callgraph/cha/BUILD.bazel + "//go/loader", + "//go/ssa", + "//go/ssa/ssautil", -+ "//internal/typeparams", + ], + "@io_bazel_rules_go//go/platform:js": [ + ":cha", @@ -6337,7 +5798,6 @@ diff -urN b/go/callgraph/cha/BUILD.bazel c/go/callgraph/cha/BUILD.bazel + "//go/loader", + "//go/ssa", + "//go/ssa/ssautil", -+ "//internal/typeparams", + ], + "@io_bazel_rules_go//go/platform:linux": [ + ":cha", @@ -6345,7 +5805,6 @@ diff -urN b/go/callgraph/cha/BUILD.bazel c/go/callgraph/cha/BUILD.bazel + "//go/loader", + "//go/ssa", + "//go/ssa/ssautil", -+ "//internal/typeparams", + ], + "@io_bazel_rules_go//go/platform:netbsd": [ + ":cha", @@ -6353,7 +5812,6 @@ diff -urN b/go/callgraph/cha/BUILD.bazel c/go/callgraph/cha/BUILD.bazel + "//go/loader", + "//go/ssa", + "//go/ssa/ssautil", -+ "//internal/typeparams", + ], + "@io_bazel_rules_go//go/platform:openbsd": [ + ":cha", @@ -6361,7 +5819,6 @@ diff -urN b/go/callgraph/cha/BUILD.bazel c/go/callgraph/cha/BUILD.bazel + "//go/loader", + "//go/ssa", + "//go/ssa/ssautil", -+ "//internal/typeparams", + ], + "@io_bazel_rules_go//go/platform:plan9": [ + ":cha", @@ -6369,7 +5826,6 @@ diff -urN b/go/callgraph/cha/BUILD.bazel c/go/callgraph/cha/BUILD.bazel + "//go/loader", + "//go/ssa", + "//go/ssa/ssautil", -+ "//internal/typeparams", + ], + "@io_bazel_rules_go//go/platform:solaris": [ + ":cha", @@ -6377,7 +5833,6 @@ diff -urN b/go/callgraph/cha/BUILD.bazel c/go/callgraph/cha/BUILD.bazel + "//go/loader", + "//go/ssa", + "//go/ssa/ssautil", -+ "//internal/typeparams", + ], + "@io_bazel_rules_go//go/platform:windows": [ + ":cha", @@ -6385,7 +5840,6 @@ diff -urN b/go/callgraph/cha/BUILD.bazel c/go/callgraph/cha/BUILD.bazel + "//go/loader", + "//go/ssa", + "//go/ssa/ssautil", -+ "//internal/typeparams", + ], + "//conditions:default": [], + }), @@ -6423,7 +5877,7 @@ diff -urN b/go/callgraph/rta/BUILD.bazel c/go/callgraph/rta/BUILD.bazel + "//go/callgraph", + "//go/ssa", + "//go/types/typeutil", -+ "//internal/compat", ++ "//internal/aliases", + ], +) + @@ -6444,7 +5898,7 @@ diff -urN b/go/callgraph/rta/BUILD.bazel c/go/callgraph/rta/BUILD.bazel + "//go/loader", + "//go/ssa", + "//go/ssa/ssautil", -+ "//internal/typeparams", ++ "//internal/aliases", + ], + "@io_bazel_rules_go//go/platform:darwin": [ + ":rta", @@ -6452,7 +5906,7 @@ diff -urN b/go/callgraph/rta/BUILD.bazel c/go/callgraph/rta/BUILD.bazel + "//go/loader", + "//go/ssa", + "//go/ssa/ssautil", -+ "//internal/typeparams", ++ "//internal/aliases", + ], + "@io_bazel_rules_go//go/platform:dragonfly": [ + ":rta", @@ -6460,7 +5914,7 @@ diff -urN b/go/callgraph/rta/BUILD.bazel c/go/callgraph/rta/BUILD.bazel + "//go/loader", + "//go/ssa", + "//go/ssa/ssautil", -+ "//internal/typeparams", ++ "//internal/aliases", + ], + "@io_bazel_rules_go//go/platform:freebsd": [ + ":rta", @@ -6468,7 +5922,7 @@ diff -urN b/go/callgraph/rta/BUILD.bazel c/go/callgraph/rta/BUILD.bazel + "//go/loader", + "//go/ssa", + "//go/ssa/ssautil", -+ "//internal/typeparams", ++ "//internal/aliases", + ], + "@io_bazel_rules_go//go/platform:illumos": [ + ":rta", @@ -6476,7 +5930,7 @@ diff -urN b/go/callgraph/rta/BUILD.bazel c/go/callgraph/rta/BUILD.bazel + "//go/loader", + "//go/ssa", + "//go/ssa/ssautil", -+ "//internal/typeparams", ++ "//internal/aliases", + ], + "@io_bazel_rules_go//go/platform:ios": [ + ":rta", @@ -6484,7 +5938,7 @@ diff -urN b/go/callgraph/rta/BUILD.bazel c/go/callgraph/rta/BUILD.bazel + "//go/loader", + "//go/ssa", + "//go/ssa/ssautil", -+ "//internal/typeparams", ++ "//internal/aliases", + ], + "@io_bazel_rules_go//go/platform:js": [ + ":rta", @@ -6492,7 +5946,7 @@ diff -urN b/go/callgraph/rta/BUILD.bazel c/go/callgraph/rta/BUILD.bazel + "//go/loader", + "//go/ssa", + "//go/ssa/ssautil", -+ "//internal/typeparams", ++ "//internal/aliases", + ], + "@io_bazel_rules_go//go/platform:linux": [ + ":rta", @@ -6500,7 +5954,7 @@ diff -urN b/go/callgraph/rta/BUILD.bazel c/go/callgraph/rta/BUILD.bazel + "//go/loader", + "//go/ssa", + "//go/ssa/ssautil", -+ "//internal/typeparams", ++ "//internal/aliases", + ], + "@io_bazel_rules_go//go/platform:netbsd": [ + ":rta", @@ -6508,7 +5962,7 @@ diff -urN b/go/callgraph/rta/BUILD.bazel c/go/callgraph/rta/BUILD.bazel + "//go/loader", + "//go/ssa", + "//go/ssa/ssautil", -+ "//internal/typeparams", ++ "//internal/aliases", + ], + "@io_bazel_rules_go//go/platform:openbsd": [ + ":rta", @@ -6516,7 +5970,7 @@ diff -urN b/go/callgraph/rta/BUILD.bazel c/go/callgraph/rta/BUILD.bazel + "//go/loader", + "//go/ssa", + "//go/ssa/ssautil", -+ "//internal/typeparams", ++ "//internal/aliases", + ], + "@io_bazel_rules_go//go/platform:plan9": [ + ":rta", @@ -6524,7 +5978,7 @@ diff -urN b/go/callgraph/rta/BUILD.bazel c/go/callgraph/rta/BUILD.bazel + "//go/loader", + "//go/ssa", + "//go/ssa/ssautil", -+ "//internal/typeparams", ++ "//internal/aliases", + ], + "@io_bazel_rules_go//go/platform:solaris": [ + ":rta", @@ -6532,7 +5986,7 @@ diff -urN b/go/callgraph/rta/BUILD.bazel c/go/callgraph/rta/BUILD.bazel + "//go/loader", + "//go/ssa", + "//go/ssa/ssautil", -+ "//internal/typeparams", ++ "//internal/aliases", + ], + "@io_bazel_rules_go//go/platform:windows": [ + ":rta", @@ -6540,7 +5994,7 @@ diff -urN b/go/callgraph/rta/BUILD.bazel c/go/callgraph/rta/BUILD.bazel + "//go/loader", + "//go/ssa", + "//go/ssa/ssautil", -+ "//internal/typeparams", ++ "//internal/aliases", + ], + "//conditions:default": [], + }), @@ -6548,7 +6002,7 @@ diff -urN b/go/callgraph/rta/BUILD.bazel c/go/callgraph/rta/BUILD.bazel diff -urN b/go/callgraph/static/BUILD.bazel c/go/callgraph/static/BUILD.bazel --- b/go/callgraph/static/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/callgraph/static/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,32 @@ +@@ -0,0 +1,31 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -6578,7 +6032,6 @@ diff -urN b/go/callgraph/static/BUILD.bazel c/go/callgraph/static/BUILD.bazel + "//go/loader", + "//go/ssa", + "//go/ssa/ssautil", -+ "//internal/typeparams", + ], +) diff -urN b/go/callgraph/vta/BUILD.bazel c/go/callgraph/vta/BUILD.bazel @@ -6602,6 +6055,7 @@ diff -urN b/go/callgraph/vta/BUILD.bazel c/go/callgraph/vta/BUILD.bazel + "//go/callgraph/vta/internal/trie", + "//go/ssa", + "//go/types/typeutil", ++ "//internal/aliases", + "//internal/typeparams", + ], +) @@ -6618,7 +6072,6 @@ diff -urN b/go/callgraph/vta/BUILD.bazel c/go/callgraph/vta/BUILD.bazel + "graph_test.go", + "helpers_test.go", + "propagation_test.go", -+ "vta_go117_test.go", + "vta_test.go", + ], + embed = [":vta"], @@ -6632,7 +6085,7 @@ diff -urN b/go/callgraph/vta/BUILD.bazel c/go/callgraph/vta/BUILD.bazel + "//go/ssa", + "//go/ssa/ssautil", + "//go/types/typeutil", -+ "//internal/typeparams", ++ "//internal/aliases", + ], +) diff -urN b/go/callgraph/vta/internal/trie/BUILD.bazel c/go/callgraph/vta/internal/trie/BUILD.bazel @@ -6707,7 +6160,7 @@ diff -urN b/go/callgraph/vta/testdata/src/t/BUILD.bazel c/go/callgraph/vta/testd diff -urN b/go/cfg/BUILD.bazel c/go/cfg/BUILD.bazel --- b/go/cfg/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/cfg/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,23 @@ +@@ -0,0 +1,27 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -6729,7 +6182,11 @@ diff -urN b/go/cfg/BUILD.bazel c/go/cfg/BUILD.bazel +go_test( + name = "cfg_test", + srcs = ["cfg_test.go"], -+ embed = [":cfg"], ++ deps = [ ++ ":cfg", ++ "//go/packages", ++ "//internal/testenv", ++ ], +) diff -urN b/go/expect/BUILD.bazel c/go/expect/BUILD.bazel --- b/go/expect/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 @@ -6869,7 +6326,7 @@ diff -urN b/go/gcexportdata/BUILD.bazel c/go/gcexportdata/BUILD.bazel diff -urN b/go/internal/cgo/BUILD.bazel c/go/internal/cgo/BUILD.bazel --- b/go/internal/cgo/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/internal/cgo/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,18 @@ +@@ -0,0 +1,17 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( @@ -6880,7 +6337,6 @@ diff -urN b/go/internal/cgo/BUILD.bazel c/go/internal/cgo/BUILD.bazel + ], + importpath = "golang.org/x/tools/go/internal/cgo", + visibility = ["//go:__subpackages__"], -+ deps = ["@org_golang_x_sys//execabs:go_default_library"], +) + +alias( @@ -6891,7 +6347,7 @@ diff -urN b/go/internal/cgo/BUILD.bazel c/go/internal/cgo/BUILD.bazel diff -urN b/go/internal/gccgoimporter/BUILD.bazel c/go/internal/gccgoimporter/BUILD.bazel --- b/go/internal/gccgoimporter/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/internal/gccgoimporter/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,36 @@ +@@ -0,0 +1,39 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -6907,7 +6363,10 @@ diff -urN b/go/internal/gccgoimporter/BUILD.bazel c/go/internal/gccgoimporter/BU + ], + importpath = "golang.org/x/tools/go/internal/gccgoimporter", + visibility = ["//go:__subpackages__"], -+ deps = ["@org_golang_x_sys//execabs:go_default_library"], ++ deps = [ ++ "//internal/aliases", ++ "//internal/typesinternal", ++ ], +) + +alias( @@ -6966,7 +6425,7 @@ diff -urN b/go/loader/BUILD.bazel c/go/loader/BUILD.bazel + "//go/ast/astutil", + "//go/buildutil", + "//go/internal/cgo", -+ "//internal/typeparams", ++ "//internal/versions", + ], +) + @@ -7056,9 +6515,9 @@ diff -urN b/go/packages/BUILD.bazel c/go/packages/BUILD.bazel + "//go/internal/packagesdriver", + "//internal/gocommand", + "//internal/packagesinternal", -+ "//internal/typeparams", + "//internal/typesinternal", -+ "@org_golang_x_sys//execabs:go_default_library", ++ "//internal/versions", ++ "@org_golang_x_sync//errgroup:go_default_library", + ], +) + @@ -7128,7 +6587,7 @@ diff -urN b/go/packages/internal/nodecount/BUILD.bazel c/go/packages/internal/no diff -urN b/go/packages/packagestest/BUILD.bazel c/go/packages/packagestest/BUILD.bazel --- b/go/packages/packagestest/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/packages/packagestest/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,42 @@ +@@ -0,0 +1,41 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -7146,7 +6605,6 @@ diff -urN b/go/packages/packagestest/BUILD.bazel c/go/packages/packagestest/BUIL + "//go/expect", + "//go/packages", + "//internal/gocommand", -+ "//internal/packagesinternal", + "//internal/proxydir", + "//internal/testenv", + ], @@ -7404,7 +6862,7 @@ diff -urN b/go/packages/packagestest/testdata/groups/two/primarymod/expect/BUILD diff -urN b/go/ssa/BUILD.bazel c/go/ssa/BUILD.bazel --- b/go/ssa/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/ssa/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,76 @@ +@@ -0,0 +1,78 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -7425,14 +6883,12 @@ diff -urN b/go/ssa/BUILD.bazel c/go/ssa/BUILD.bazel + "lvalue.go", + "methods.go", + "mode.go", -+ "parameterized.go", + "print.go", + "sanity.go", + "source.go", + "ssa.go", + "subst.go", + "util.go", -+ "versions_go122.go", + "wrappers.go", + ], + importpath = "golang.org/x/tools/go/ssa", @@ -7440,7 +6896,10 @@ diff -urN b/go/ssa/BUILD.bazel c/go/ssa/BUILD.bazel + deps = [ + "//go/ast/astutil", + "//go/types/typeutil", ++ "//internal/aliases", + "//internal/typeparams", ++ "//internal/typesinternal", ++ "//internal/versions", + ], +) + @@ -7459,10 +6918,10 @@ diff -urN b/go/ssa/BUILD.bazel c/go/ssa/BUILD.bazel + "builder_test.go", + "const_test.go", + "coretype_test.go", ++ "dom_test.go", + "example_test.go", + "instantiate_test.go", + "methods_test.go", -+ "parameterized_test.go", + "source_test.go", + "stdlib_test.go", + "subst_test.go", @@ -7476,6 +6935,7 @@ diff -urN b/go/ssa/BUILD.bazel c/go/ssa/BUILD.bazel + "//go/loader", + "//go/packages", + "//go/ssa/ssautil", ++ "//internal/aliases", + "//internal/testenv", + "//internal/typeparams", + "//txtar", @@ -7484,7 +6944,7 @@ diff -urN b/go/ssa/BUILD.bazel c/go/ssa/BUILD.bazel diff -urN b/go/ssa/interp/BUILD.bazel c/go/ssa/interp/BUILD.bazel --- b/go/ssa/interp/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/ssa/interp/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,43 @@ +@@ -0,0 +1,44 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -7502,6 +6962,8 @@ diff -urN b/go/ssa/interp/BUILD.bazel c/go/ssa/interp/BUILD.bazel + deps = [ + "//go/ssa", + "//go/types/typeutil", ++ "//internal/aliases", ++ "//internal/typeparams", + ], +) + @@ -7525,7 +6987,6 @@ diff -urN b/go/ssa/interp/BUILD.bazel c/go/ssa/interp/BUILD.bazel + "//go/ssa", + "//go/ssa/ssautil", + "//internal/testenv", -+ "//internal/typeparams", + ], +) diff -urN b/go/ssa/interp/testdata/fixedbugs/BUILD.bazel c/go/ssa/interp/testdata/fixedbugs/BUILD.bazel @@ -7861,7 +7322,7 @@ diff -urN b/go/ssa/ssautil/BUILD.bazel c/go/ssa/ssautil/BUILD.bazel + "//go/loader", + "//go/packages", + "//go/ssa", -+ "//internal/typeparams", ++ "//internal/versions", + ], +) + @@ -8290,7 +7751,7 @@ diff -urN b/go/ssa/testdata/src/unsafe/BUILD.bazel c/go/ssa/testdata/src/unsafe/ diff -urN b/go/types/internal/play/BUILD.bazel c/go/types/internal/play/BUILD.bazel --- b/go/types/internal/play/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/types/internal/play/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,20 @@ +@@ -0,0 +1,21 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( @@ -8302,6 +7763,7 @@ diff -urN b/go/types/internal/play/BUILD.bazel c/go/types/internal/play/BUILD.ba + "//go/ast/astutil", + "//go/packages", + "//go/types/typeutil", ++ "//internal/aliases", + "//internal/typeparams", + ], +) @@ -8314,7 +7776,7 @@ diff -urN b/go/types/internal/play/BUILD.bazel c/go/types/internal/play/BUILD.ba diff -urN b/go/types/objectpath/BUILD.bazel c/go/types/objectpath/BUILD.bazel --- b/go/types/objectpath/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/types/objectpath/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,29 @@ +@@ -0,0 +1,32 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -8322,7 +7784,10 @@ diff -urN b/go/types/objectpath/BUILD.bazel c/go/types/objectpath/BUILD.bazel + srcs = ["objectpath.go"], + importpath = "golang.org/x/tools/go/types/objectpath", + visibility = ["//visibility:public"], -+ deps = ["//internal/typeparams"], ++ deps = [ ++ "//internal/aliases", ++ "//internal/typesinternal", ++ ], +) + +alias( @@ -8347,7 +7812,7 @@ diff -urN b/go/types/objectpath/BUILD.bazel c/go/types/objectpath/BUILD.bazel diff -urN b/go/types/typeutil/BUILD.bazel c/go/types/typeutil/BUILD.bazel --- b/go/types/typeutil/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/go/types/typeutil/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,39 @@ +@@ -0,0 +1,40 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -8363,6 +7828,7 @@ diff -urN b/go/types/typeutil/BUILD.bazel c/go/types/typeutil/BUILD.bazel + visibility = ["//visibility:public"], + deps = [ + "//go/ast/astutil", ++ "//internal/aliases", + "//internal/typeparams", + ], +) @@ -8384,7 +7850,7 @@ diff -urN b/go/types/typeutil/BUILD.bazel c/go/types/typeutil/BUILD.bazel + ], + deps = [ + ":typeutil", -+ "//internal/typeparams", ++ "//internal/versions", + ], +) diff -urN b/godoc/analysis/BUILD.bazel c/godoc/analysis/BUILD.bazel @@ -8411,7 +7877,7 @@ diff -urN b/godoc/analysis/BUILD.bazel c/godoc/analysis/BUILD.bazel diff -urN b/godoc/BUILD.bazel c/godoc/BUILD.bazel --- b/godoc/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/godoc/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,66 @@ +@@ -0,0 +1,62 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -8435,8 +7901,6 @@ diff -urN b/godoc/BUILD.bazel c/godoc/BUILD.bazel + "spot.go", + "tab.go", + "template.go", -+ "tohtml_go119.go", -+ "tohtml_other.go", + "versions.go", + ], + importpath = "golang.org/x/tools/godoc", @@ -8446,7 +7910,6 @@ diff -urN b/godoc/BUILD.bazel c/godoc/BUILD.bazel + "//godoc/util", + "//godoc/vfs", + "//godoc/vfs/httpfs", -+ "//internal/typeparams", + "@com_github_yuin_goldmark//:go_default_library", + "@com_github_yuin_goldmark//parser:go_default_library", + "@com_github_yuin_goldmark//renderer/html:go_default_library", @@ -8475,7 +7938,6 @@ diff -urN b/godoc/BUILD.bazel c/godoc/BUILD.bazel + "//godoc/vfs", + "//godoc/vfs/gatefs", + "//godoc/vfs/mapfs", -+ "//internal/typeparams", + ], +) diff -urN b/godoc/redirect/BUILD.bazel c/godoc/redirect/BUILD.bazel @@ -8708,17 +8170,52 @@ diff -urN b/imports/BUILD.bazel c/imports/BUILD.bazel + actual = ":imports", + visibility = ["//visibility:public"], +) +diff -urN b/internal/aliases/BUILD.bazel c/internal/aliases/BUILD.bazel +--- b/internal/aliases/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 ++++ c/internal/aliases/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 +@@ -0,0 +1,27 @@ ++load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") ++ ++go_library( ++ name = "aliases", ++ srcs = [ ++ "aliases.go", ++ "aliases_go121.go", ++ "aliases_go122.go", ++ ], ++ importpath = "golang.org/x/tools/internal/aliases", ++ visibility = ["//:__subpackages__"], ++) ++ ++alias( ++ name = "go_default_library", ++ actual = ":aliases", ++ visibility = ["//:__subpackages__"], ++) ++ ++go_test( ++ name = "aliases_test", ++ srcs = ["aliases_test.go"], ++ deps = [ ++ ":aliases", ++ "//internal/testenv", ++ ], ++) diff -urN b/internal/analysisinternal/BUILD.bazel c/internal/analysisinternal/BUILD.bazel --- b/internal/analysisinternal/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/internal/analysisinternal/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,14 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_library") +@@ -0,0 +1,24 @@ ++load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "analysisinternal", -+ srcs = ["analysis.go"], ++ srcs = [ ++ "analysis.go", ++ "extractdoc.go", ++ ], + importpath = "golang.org/x/tools/internal/analysisinternal", + visibility = ["//:__subpackages__"], ++ deps = ["//internal/aliases"], +) + +alias( @@ -8726,10 +8223,16 @@ diff -urN b/internal/analysisinternal/BUILD.bazel c/internal/analysisinternal/BU + actual = ":analysisinternal", + visibility = ["//:__subpackages__"], +) ++ ++go_test( ++ name = "analysisinternal_test", ++ srcs = ["extractdoc_test.go"], ++ deps = [":analysisinternal"], ++) diff -urN b/internal/apidiff/BUILD.bazel c/internal/apidiff/BUILD.bazel --- b/internal/apidiff/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/internal/apidiff/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,30 @@ +@@ -0,0 +1,34 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -8743,6 +8246,10 @@ diff -urN b/internal/apidiff/BUILD.bazel c/internal/apidiff/BUILD.bazel + ], + importpath = "golang.org/x/tools/internal/apidiff", + visibility = ["//:__subpackages__"], ++ deps = [ ++ "//internal/aliases", ++ "//internal/typesinternal", ++ ], +) + +alias( @@ -8838,85 +8345,6 @@ diff -urN b/internal/bisect/BUILD.bazel c/internal/bisect/BUILD.bazel + srcs = ["bisect_test.go"], + embed = [":bisect"], +) -diff -urN b/internal/cmd/deadcode/BUILD.bazel c/internal/cmd/deadcode/BUILD.bazel ---- b/internal/cmd/deadcode/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ c/internal/cmd/deadcode/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,35 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library", "go_test") -+ -+go_library( -+ name = "deadcode_lib", -+ srcs = [ -+ "deadcode.go", -+ "doc.go", -+ ], -+ embedsrcs = ["doc.go"], -+ importpath = "golang.org/x/tools/internal/cmd/deadcode", -+ visibility = ["//visibility:private"], -+ deps = [ -+ "//go/callgraph", -+ "//go/callgraph/rta", -+ "//go/packages", -+ "//go/ssa", -+ "//go/ssa/ssautil", -+ ], -+) -+ -+go_binary( -+ name = "deadcode", -+ embed = [":deadcode_lib"], -+ visibility = ["//:__subpackages__"], -+) -+ -+go_test( -+ name = "deadcode_test", -+ srcs = ["deadcode_test.go"], -+ data = glob(["testdata/**"]), -+ deps = [ -+ "//internal/testenv", -+ "//txtar", -+ ], -+) -diff -urN b/internal/compat/BUILD.bazel c/internal/compat/BUILD.bazel ---- b/internal/compat/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ c/internal/compat/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,18 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_library") -+ -+go_library( -+ name = "compat", -+ srcs = [ -+ "appendf.go", -+ "appendf_118.go", -+ "doc.go", -+ ], -+ importpath = "golang.org/x/tools/internal/compat", -+ visibility = ["//:__subpackages__"], -+) -+ -+alias( -+ name = "go_default_library", -+ actual = ":compat", -+ visibility = ["//:__subpackages__"], -+) -diff -urN b/internal/constraints/BUILD.bazel c/internal/constraints/BUILD.bazel ---- b/internal/constraints/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ c/internal/constraints/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,14 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_library") -+ -+go_library( -+ name = "constraints", -+ srcs = ["constraint.go"], -+ importpath = "golang.org/x/tools/internal/constraints", -+ visibility = ["//:__subpackages__"], -+) -+ -+alias( -+ name = "go_default_library", -+ actual = ":constraints", -+ visibility = ["//:__subpackages__"], -+) diff -urN b/internal/diff/BUILD.bazel c/internal/diff/BUILD.bazel --- b/internal/diff/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/internal/diff/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 @@ -9354,14 +8782,15 @@ diff -urN b/internal/event/export/prometheus/BUILD.bazel c/internal/event/export diff -urN b/internal/event/keys/BUILD.bazel c/internal/event/keys/BUILD.bazel --- b/internal/event/keys/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/internal/event/keys/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,18 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_library") +@@ -0,0 +1,25 @@ ++load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "keys", + srcs = [ + "keys.go", + "standard.go", ++ "util.go", + ], + importpath = "golang.org/x/tools/internal/event/keys", + visibility = ["//:__subpackages__"], @@ -9373,6 +8802,12 @@ diff -urN b/internal/event/keys/BUILD.bazel c/internal/event/keys/BUILD.bazel + actual = ":keys", + visibility = ["//:__subpackages__"], +) ++ ++go_test( ++ name = "keys_test", ++ srcs = ["util_test.go"], ++ embed = [":keys"], ++) diff -urN b/internal/event/label/BUILD.bazel c/internal/event/label/BUILD.bazel --- b/internal/event/label/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/internal/event/label/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 @@ -9436,7 +8871,7 @@ diff -urN b/internal/facts/BUILD.bazel c/internal/facts/BUILD.bazel + deps = [ + "//go/analysis", + "//go/types/objectpath", -+ "//internal/typeparams", ++ "//internal/aliases", + ], +) + @@ -9453,8 +8888,8 @@ diff -urN b/internal/facts/BUILD.bazel c/internal/facts/BUILD.bazel + ":facts", + "//go/analysis/analysistest", + "//go/packages", ++ "//internal/aliases", + "//internal/testenv", -+ "//internal/typeparams", + ], +) diff -urN b/internal/fakenet/BUILD.bazel c/internal/fakenet/BUILD.bazel @@ -9514,7 +8949,7 @@ diff -urN b/internal/fuzzy/BUILD.bazel c/internal/fuzzy/BUILD.bazel diff -urN b/internal/gcimporter/BUILD.bazel c/internal/gcimporter/BUILD.bazel --- b/internal/gcimporter/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/internal/gcimporter/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,60 @@ +@@ -0,0 +1,59 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -9527,19 +8962,18 @@ diff -urN b/internal/gcimporter/BUILD.bazel c/internal/gcimporter/BUILD.bazel + "iimport.go", + "newInterface10.go", + "newInterface11.go", -+ "support_go117.go", + "support_go118.go", + "unified_no.go", -+ "ureader_no.go", + "ureader_yes.go", + ], + importpath = "golang.org/x/tools/internal/gcimporter", + visibility = ["//:__subpackages__"], + deps = [ + "//go/types/objectpath", ++ "//internal/aliases", + "//internal/pkgbits", + "//internal/tokeninternal", -+ "//internal/typeparams", ++ "//internal/typesinternal", + ], +) + @@ -9568,9 +9002,9 @@ diff -urN b/internal/gcimporter/BUILD.bazel c/internal/gcimporter/BUILD.bazel + "//go/gcexportdata", + "//go/loader", + "//go/packages", ++ "//internal/aliases", + "//internal/goroot", + "//internal/testenv", -+ "//internal/typeparams", + "//internal/typeparams/genericfeatures", + "@org_golang_x_sync//errgroup:go_default_library", + ], @@ -9708,7 +9142,7 @@ diff -urN b/internal/gcimporter/testdata/versions/BUILD.bazel c/internal/gcimpor diff -urN b/internal/gocommand/BUILD.bazel c/internal/gocommand/BUILD.bazel --- b/internal/gocommand/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/internal/gocommand/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,36 @@ +@@ -0,0 +1,35 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -9726,7 +9160,6 @@ diff -urN b/internal/gocommand/BUILD.bazel c/internal/gocommand/BUILD.bazel + "//internal/event/label", + "//internal/event/tag", + "@org_golang_x_mod//semver:go_default_library", -+ "@org_golang_x_sys//execabs:go_default_library", + ], +) + @@ -9790,7 +9223,7 @@ diff -urN b/internal/goroot/BUILD.bazel c/internal/goroot/BUILD.bazel diff -urN b/internal/imports/BUILD.bazel c/internal/imports/BUILD.bazel --- b/internal/imports/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/internal/imports/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,49 @@ +@@ -0,0 +1,50 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -9801,7 +9234,6 @@ diff -urN b/internal/imports/BUILD.bazel c/internal/imports/BUILD.bazel + "mod.go", + "mod_cache.go", + "sortimports.go", -+ "zstdlib.go", + ], + importpath = "golang.org/x/tools/internal/imports", + visibility = ["//:__subpackages__"], @@ -9810,6 +9242,7 @@ diff -urN b/internal/imports/BUILD.bazel c/internal/imports/BUILD.bazel + "//internal/event", + "//internal/gocommand", + "//internal/gopathwalk", ++ "//internal/stdlib", + "@org_golang_x_mod//module:go_default_library", + ], +) @@ -9835,6 +9268,7 @@ diff -urN b/internal/imports/BUILD.bazel c/internal/imports/BUILD.bazel + "//internal/gocommand", + "//internal/gopathwalk", + "//internal/proxydir", ++ "//internal/stdlib", + "//internal/testenv", + "//txtar", + "@org_golang_x_mod//module:go_default_library", @@ -9989,7 +9423,7 @@ diff -urN b/internal/memoize/BUILD.bazel c/internal/memoize/BUILD.bazel diff -urN b/internal/packagesinternal/BUILD.bazel c/internal/packagesinternal/BUILD.bazel --- b/internal/packagesinternal/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/internal/packagesinternal/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,15 @@ +@@ -0,0 +1,14 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( @@ -9997,7 +9431,6 @@ diff -urN b/internal/packagesinternal/BUILD.bazel c/internal/packagesinternal/BU + srcs = ["packages.go"], + importpath = "golang.org/x/tools/internal/packagesinternal", + visibility = ["//:__subpackages__"], -+ deps = ["//internal/gocommand"], +) + +alias( @@ -10005,38 +9438,6 @@ diff -urN b/internal/packagesinternal/BUILD.bazel c/internal/packagesinternal/BU + actual = ":packagesinternal", + visibility = ["//:__subpackages__"], +) -diff -urN b/internal/persistent/BUILD.bazel c/internal/persistent/BUILD.bazel ---- b/internal/persistent/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 -+++ c/internal/persistent/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,28 @@ -+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") -+ -+go_library( -+ name = "persistent", -+ srcs = [ -+ "map.go", -+ "set.go", -+ ], -+ importpath = "golang.org/x/tools/internal/persistent", -+ visibility = ["//:__subpackages__"], -+ deps = ["//internal/constraints"], -+) -+ -+alias( -+ name = "go_default_library", -+ actual = ":persistent", -+ visibility = ["//:__subpackages__"], -+) -+ -+go_test( -+ name = "persistent_test", -+ srcs = [ -+ "map_test.go", -+ "set_test.go", -+ ], -+ embed = [":persistent"], -+ deps = ["//internal/constraints"], -+) diff -urN b/internal/pkgbits/BUILD.bazel c/internal/pkgbits/BUILD.bazel --- b/internal/pkgbits/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/internal/pkgbits/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 @@ -10191,7 +9592,7 @@ diff -urN b/internal/refactor/inline/analyzer/testdata/src/b/BUILD.bazel c/inter diff -urN b/internal/refactor/inline/BUILD.bazel c/internal/refactor/inline/BUILD.bazel --- b/internal/refactor/inline/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/internal/refactor/inline/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,51 @@ +@@ -0,0 +1,52 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -10211,6 +9612,7 @@ diff -urN b/internal/refactor/inline/BUILD.bazel c/internal/refactor/inline/BUIL + "//go/ast/astutil", + "//go/types/typeutil", + "//imports", ++ "//internal/aliases", + "//internal/astutil", + "//internal/typeparams", + ], @@ -10342,10 +9744,31 @@ diff -urN b/internal/stack/stacktest/BUILD.bazel c/internal/stack/stacktest/BUIL + actual = ":stacktest", + visibility = ["//:__subpackages__"], +) +diff -urN b/internal/stdlib/BUILD.bazel c/internal/stdlib/BUILD.bazel +--- b/internal/stdlib/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 ++++ c/internal/stdlib/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 +@@ -0,0 +1,17 @@ ++load("@io_bazel_rules_go//go:def.bzl", "go_library") ++ ++go_library( ++ name = "stdlib", ++ srcs = [ ++ "manifest.go", ++ "stdlib.go", ++ ], ++ importpath = "golang.org/x/tools/internal/stdlib", ++ visibility = ["//:__subpackages__"], ++) ++ ++alias( ++ name = "go_default_library", ++ actual = ":stdlib", ++ visibility = ["//:__subpackages__"], ++) diff -urN b/internal/testenv/BUILD.bazel c/internal/testenv/BUILD.bazel --- b/internal/testenv/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/internal/testenv/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,24 @@ +@@ -0,0 +1,23 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( @@ -10361,7 +9784,6 @@ diff -urN b/internal/testenv/BUILD.bazel c/internal/testenv/BUILD.bazel + deps = [ + "//internal/goroot", + "@org_golang_x_mod//modfile:go_default_library", -+ "@org_golang_x_sys//execabs:go_default_library", + ], +) + @@ -10415,7 +9837,7 @@ diff -urN b/internal/tool/BUILD.bazel c/internal/tool/BUILD.bazel diff -urN b/internal/typeparams/BUILD.bazel c/internal/typeparams/BUILD.bazel --- b/internal/typeparams/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/internal/typeparams/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,39 @@ +@@ -0,0 +1,33 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -10423,16 +9845,14 @@ diff -urN b/internal/typeparams/BUILD.bazel c/internal/typeparams/BUILD.bazel + srcs = [ + "common.go", + "coretype.go", -+ "enabled_go117.go", -+ "enabled_go118.go", ++ "free.go", + "normalize.go", + "termlist.go", -+ "typeparams_go117.go", -+ "typeparams_go118.go", + "typeterm.go", + ], + importpath = "golang.org/x/tools/internal/typeparams", + visibility = ["//:__subpackages__"], ++ deps = ["//internal/aliases"], +) + +alias( @@ -10446,14 +9866,10 @@ diff -urN b/internal/typeparams/BUILD.bazel c/internal/typeparams/BUILD.bazel + srcs = [ + "common_test.go", + "coretype_test.go", ++ "free_test.go", + "normalize_test.go", -+ "typeparams_test.go", -+ ], -+ deps = [ -+ ":typeparams", -+ "//internal/apidiff", -+ "//internal/testenv", + ], ++ embed = [":typeparams"], +) diff -urN b/internal/typeparams/genericfeatures/BUILD.bazel c/internal/typeparams/genericfeatures/BUILD.bazel --- b/internal/typeparams/genericfeatures/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 @@ -10468,7 +9884,7 @@ diff -urN b/internal/typeparams/genericfeatures/BUILD.bazel c/internal/typeparam + visibility = ["//:__subpackages__"], + deps = [ + "//go/ast/inspector", -+ "//internal/typeparams", ++ "//internal/aliases", + ], +) + @@ -10480,7 +9896,7 @@ diff -urN b/internal/typeparams/genericfeatures/BUILD.bazel c/internal/typeparam diff -urN b/internal/typesinternal/BUILD.bazel c/internal/typesinternal/BUILD.bazel --- b/internal/typesinternal/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/internal/typesinternal/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,24 @@ +@@ -0,0 +1,30 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -10488,11 +9904,17 @@ diff -urN b/internal/typesinternal/BUILD.bazel c/internal/typesinternal/BUILD.ba + srcs = [ + "errorcode.go", + "errorcode_string.go", ++ "recv.go", ++ "toonew.go", + "types.go", -+ "types_118.go", + ], + importpath = "golang.org/x/tools/internal/typesinternal", + visibility = ["//:__subpackages__"], ++ deps = [ ++ "//internal/aliases", ++ "//internal/stdlib", ++ "//internal/versions", ++ ], +) + +alias( @@ -10505,6 +9927,47 @@ diff -urN b/internal/typesinternal/BUILD.bazel c/internal/typesinternal/BUILD.ba + name = "typesinternal_test", + srcs = ["errorcode_test.go"], +) +diff -urN b/internal/versions/BUILD.bazel c/internal/versions/BUILD.bazel +--- b/internal/versions/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 ++++ c/internal/versions/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 +@@ -0,0 +1,37 @@ ++load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") ++ ++go_library( ++ name = "versions", ++ srcs = [ ++ "features.go", ++ "gover.go", ++ "toolchain.go", ++ "toolchain_go119.go", ++ "toolchain_go120.go", ++ "toolchain_go121.go", ++ "types.go", ++ "types_go121.go", ++ "types_go122.go", ++ "versions.go", ++ ], ++ importpath = "golang.org/x/tools/internal/versions", ++ visibility = ["//:__subpackages__"], ++) ++ ++alias( ++ name = "go_default_library", ++ actual = ":versions", ++ visibility = ["//:__subpackages__"], ++) ++ ++go_test( ++ name = "versions_test", ++ srcs = [ ++ "types_test.go", ++ "versions_test.go", ++ ], ++ deps = [ ++ ":versions", ++ "//internal/testenv", ++ ], ++) diff -urN b/internal/xcontext/BUILD.bazel c/internal/xcontext/BUILD.bazel --- b/internal/xcontext/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/internal/xcontext/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 @@ -10544,7 +10007,7 @@ diff -urN b/playground/BUILD.bazel c/playground/BUILD.bazel diff -urN b/playground/socket/BUILD.bazel c/playground/socket/BUILD.bazel --- b/playground/socket/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/playground/socket/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,25 @@ +@@ -0,0 +1,24 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -10555,7 +10018,6 @@ diff -urN b/playground/socket/BUILD.bazel c/playground/socket/BUILD.bazel + deps = [ + "//txtar", + "@org_golang_x_net//websocket:go_default_library", -+ "@org_golang_x_sys//execabs:go_default_library", + ], +) + @@ -10797,7 +10259,7 @@ diff -urN b/refactor/importgraph/BUILD.bazel c/refactor/importgraph/BUILD.bazel diff -urN b/refactor/rename/BUILD.bazel c/refactor/rename/BUILD.bazel --- b/refactor/rename/BUILD.bazel 1970-01-01 00:00:00.000000000 +0000 +++ c/refactor/rename/BUILD.bazel 2000-01-01 00:00:00.000000000 -0000 -@@ -0,0 +1,42 @@ +@@ -0,0 +1,43 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( @@ -10816,9 +10278,10 @@ diff -urN b/refactor/rename/BUILD.bazel c/refactor/rename/BUILD.bazel + "//go/buildutil", + "//go/loader", + "//go/types/typeutil", ++ "//internal/typeparams", ++ "//internal/typesinternal", + "//refactor/importgraph", + "//refactor/satisfy", -+ "@org_golang_x_sys//execabs:go_default_library", + ], +) + @@ -10869,7 +10332,7 @@ diff -urN b/refactor/satisfy/BUILD.bazel c/refactor/satisfy/BUILD.bazel + srcs = ["find_test.go"], + deps = [ + ":satisfy", -+ "//internal/typeparams", ++ "//internal/versions", + ], +) diff -urN b/txtar/BUILD.bazel c/txtar/BUILD.bazel